mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
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:
@@ -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>,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,6 +3,7 @@ mod portfolio_message_handler;
|
||||
|
||||
pub mod document;
|
||||
pub mod menu_bar;
|
||||
pub mod spreadsheet;
|
||||
pub mod utility_types;
|
||||
|
||||
#[doc(inline)]
|
||||
|
||||
@@ -15,6 +15,8 @@ pub enum PortfolioMessage {
|
||||
MenuBar(MenuBarMessage),
|
||||
#[child]
|
||||
Document(DocumentMessage),
|
||||
#[child]
|
||||
Spreadsheet(SpreadsheetMessage),
|
||||
|
||||
// Messages
|
||||
DocumentPassMessage {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
7
editor/src/messages/portfolio/spreadsheet/mod.rs
Normal file
7
editor/src/messages/portfolio/spreadsheet/mod.rs
Normal 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::*;
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 }]
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
50
frontend/src/components/panels/Spreadsheet.svelte
Normal file
50
frontend/src/components/panels/Spreadsheet.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||
import WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import type { PortfolioState } from "/src/state-providers/portfolio";
|
||||
|
||||
const portfolio = getContext<PortfolioState>("portfolio");
|
||||
</script>
|
||||
|
||||
<LayoutCol class="spreadsheet">
|
||||
<LayoutRow class="control-bar">
|
||||
<TextLabel>Spreadsheet Data for Node ID {$portfolio.spreadsheetNode}:</TextLabel>
|
||||
</LayoutRow>
|
||||
<LayoutCol class="body" scrollableY={true}>
|
||||
<WidgetLayout layout={$portfolio.spreadsheetWidgets} />
|
||||
</LayoutCol>
|
||||
</LayoutCol>
|
||||
|
||||
<style lang="scss" global>
|
||||
.spreadsheet {
|
||||
flex-grow: 1;
|
||||
padding: 4px;
|
||||
|
||||
.control-bar {
|
||||
height: 32px;
|
||||
--widget-height: 24px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.text-label {
|
||||
margin: calc((24px - var(--widget-height)) / 2 + 4px) 0;
|
||||
min-height: var(--widget-height);
|
||||
line-height: var(--widget-height);
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
margin: 0 -4px;
|
||||
width: calc(100% + 2 * 4px);
|
||||
margin-top: 8px;
|
||||
|
||||
span {
|
||||
white-space: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { isWidgetSpanColumn, isWidgetSpanRow, isWidgetSection, type WidgetLayout } from "@graphite/messages";
|
||||
import { isWidgetSpanColumn, isWidgetSpanRow, isWidgetSection, type WidgetLayout, isWidgetTable } from "@graphite/messages";
|
||||
|
||||
import WidgetSection from "@graphite/components/widgets/WidgetSection.svelte";
|
||||
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
|
||||
import WidgetTable from "@graphite/components/widgets/WidgetTable.svelte";
|
||||
|
||||
export let layout: WidgetLayout;
|
||||
let className = "";
|
||||
@@ -15,6 +16,8 @@
|
||||
<WidgetSpan widgetData={layoutGroup} layoutTarget={layout.layoutTarget} class={className} {classes} />
|
||||
{:else if isWidgetSection(layoutGroup)}
|
||||
<WidgetSection widgetData={layoutGroup} layoutTarget={layout.layoutTarget} class={className} {classes} />
|
||||
{:else if isWidgetTable(layoutGroup)}
|
||||
<WidgetTable widgetData={layoutGroup} layoutTarget={layout.layoutTarget} />
|
||||
{:else}
|
||||
<span style="color: #d6536e">Error: The widget layout that belongs here has an invalid layout group type</span>
|
||||
{/if}
|
||||
|
||||
42
frontend/src/components/widgets/WidgetTable.svelte
Normal file
42
frontend/src/components/widgets/WidgetTable.svelte
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { type WidgetTable as WidgetTableFromJsMessages } from "@graphite/messages";
|
||||
|
||||
import WidgetSpan from "@graphite/components/widgets/WidgetSpan.svelte";
|
||||
|
||||
export let widgetData: WidgetTableFromJsMessages;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export let layoutTarget: any; // TODO: Give this a real type
|
||||
</script>
|
||||
|
||||
<table>
|
||||
{#each widgetData.tableWidgets as row}
|
||||
<tr>
|
||||
{#each row as cell}
|
||||
<td>
|
||||
<WidgetSpan widgetData={{ rowWidgets: [cell] }} {layoutTarget} />
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
|
||||
<style lang="scss">
|
||||
table {
|
||||
background: var(--color-3-darkgray);
|
||||
border: none;
|
||||
border-spacing: 4px;
|
||||
border-radius: 2px;
|
||||
|
||||
td {
|
||||
background: var(--color-2-mildblack);
|
||||
vertical-align: top;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
tr:first-child td {
|
||||
background-image: var(--inheritance-dots-background-4-dimgray);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
</script>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</LayoutRow>
|
||||
{#if $portfolio.spreadsheetOpen}
|
||||
<LayoutRow class="workspace-grid-resize-gutter" data-gutter-vertical on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutRow class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["spreadsheet"] }} data-subdivision-name="spreadsheet">
|
||||
<Panel panelType="Spreadsheet" tabLabels={[{ name: "Spreadsheet" }]} tabActiveIndex={0} />
|
||||
</LayoutRow>
|
||||
{/if}
|
||||
</LayoutCol>
|
||||
<LayoutCol class="workspace-grid-resize-gutter" data-gutter-horizontal on:pointerdown={(e) => resizePanel(e)} />
|
||||
<LayoutCol class="workspace-grid-subdivision" styles={{ "flex-grow": panelSizes["details"] }} data-subdivision-name="details">
|
||||
|
||||
@@ -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<string, MessageMaker> = {
|
||||
UpdateEyedropperSamplingState,
|
||||
UpdateGraphFadeArtwork,
|
||||
UpdateGraphViewOverlay,
|
||||
UpdateSpreadsheetState,
|
||||
UpdateImportReorderIndex,
|
||||
UpdateImportsExports,
|
||||
UpdateInputHints,
|
||||
@@ -1664,6 +1686,7 @@ export const messageMakers: Record<string, MessageMaker> = {
|
||||
UpdateNodeThumbnail,
|
||||
UpdateOpenDocumentsList,
|
||||
UpdatePropertyPanelSectionsLayout,
|
||||
UpdateSpreadsheetLayout,
|
||||
UpdateToolOptionsLayout,
|
||||
UpdateToolShelfLayout,
|
||||
UpdateWirePathInProgress,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<T> {
|
||||
id: Vec<InstanceId>,
|
||||
#[serde(alias = "instances")]
|
||||
instance: Vec<T>,
|
||||
#[serde(default = "one_daffine2_default")]
|
||||
@@ -25,7 +24,6 @@ pub struct Instances<T> {
|
||||
impl<T> Instances<T> {
|
||||
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<T> Instances<T> {
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
id: Vec::new(),
|
||||
instance: Vec::new(),
|
||||
transform: Vec::new(),
|
||||
alpha_blending: Vec::new(),
|
||||
@@ -44,14 +41,12 @@ impl<T> Instances<T> {
|
||||
}
|
||||
|
||||
pub fn push(&mut self, instance: T) -> InstanceMut<T> {
|
||||
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<T> Instances<T> {
|
||||
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<T> Instances<T> {
|
||||
|
||||
pub fn one_instance(&self) -> Instance<T> {
|
||||
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<T> Instances<T> {
|
||||
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<T> Instances<T> {
|
||||
}
|
||||
|
||||
pub fn instances(&self) -> impl DoubleEndedIterator<Item = Instance<T>> {
|
||||
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<T> Instances<T> {
|
||||
}
|
||||
|
||||
pub fn instances_mut(&mut self) -> impl DoubleEndedIterator<Item = InstanceMut<T>> {
|
||||
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<Instance<T>> {
|
||||
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<InstanceMut<T>> {
|
||||
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<T: Default + Hash + 'static> Default for Instances<T> {
|
||||
@@ -150,7 +171,6 @@ impl<T: Default + Hash + 'static> Default for Instances<T> {
|
||||
|
||||
impl<T: Hash> core::hash::Hash for Instances<T> {
|
||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
||||
self.id.hash(state);
|
||||
for instance in &self.instance {
|
||||
instance.hash(state);
|
||||
}
|
||||
@@ -159,7 +179,7 @@ impl<T: Hash> core::hash::Hash for Instances<T> {
|
||||
|
||||
impl<T: PartialEq> PartialEq for Instances<T> {
|
||||
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<Option<NodeId>> {
|
||||
|
||||
#[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,
|
||||
|
||||
@@ -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<Item = (SegmentId, usize, usize, BezierHandles)> + '_ {
|
||||
pub fn iter(&self) -> impl Iterator<Item = (SegmentId, usize, usize, BezierHandles)> + '_ {
|
||||
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<Item = (RegionId, core::ops::RangeInclusive<SegmentId>, 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 {
|
||||
|
||||
Reference in New Issue
Block a user