Polish user-created subgraph nodes: imports in the Properties panel; reorder/delete/rename imports/exports (#2105)

* Remove imports/exports

* WIP: Autogenerated properties

* WIP: Input based properties

* WIP: Hashmap based input overrides

* Migrate noise pattern node to input properties

* Reorder exports

* Continue migrating properties

* WIP: Improve reorder exports

* Automatically populate all input properties for sub networks

* Complete reorder import and export

* Add widget override to node macro

* Migrate assign colors to input based properties

* WIP: Full node property override

* Node based properties override for proto nodes

* Migrate all node properties to be input based

* Rename imports/exports

* improve UI

* Protonode input valid implementations

* Valid type list

* Small formatting fixes

* Polishing small issues

* Document upgrade

* fix tests

* Upgrade noise pattern node

* remove console log

* Fix upgrade script for Noise Pattern

* Improve the Properties panel representation for graphical data

* Re-export demo art

* Code review

* code review improvements

* Cleanup for node properties overrides

* Reexport demo art

* Fix clippy lints

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adam Gerhant 2025-01-20 21:13:14 -08:00 committed by GitHub
parent ad68b1e5c8
commit eec0ef761c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 3660 additions and 2006 deletions

350
Cargo.lock generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -146,6 +146,14 @@ pub enum FrontendMessage {
UpdateGraphViewOverlay {
open: bool,
},
UpdateImportReorderIndex {
#[serde(rename = "importIndex")]
index: Option<usize>,
},
UpdateExportReorderIndex {
#[serde(rename = "exportIndex")]
index: Option<usize>,
},
UpdateLayerWidths {
#[serde(rename = "layerWidths")]
layer_widths: HashMap<NodeId, u32>,

View File

@ -194,7 +194,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
}
DocumentMessage::PropertiesPanel(message) => {
let properties_panel_message_handler_data = PropertiesPanelMessageHandlerData {
network_interface: &self.network_interface,
network_interface: &mut self.network_interface,
selection_network_path: &self.selection_network_path,
document_name: self.name.as_str(),
executor,
@ -391,6 +391,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
self.selection_network_path.clone_from(&self.breadcrumb_network_path);
responses.add(NodeGraphMessage::SendGraph);
responses.add(DocumentMessage::ZoomCanvasToFitAll);
responses.add(NodeGraphMessage::SetGridAlignedEdges);
}
DocumentMessage::Escape => {
if self.node_graph_handler.drag_start.is_some() {
@ -1007,7 +1008,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
}
responses.add(PropertiesPanelMessage::Refresh);
responses.add(NodeGraphMessage::UpdateLayerPanel);
responses.add(NodeGraphMessage::UpdateInSelectedNetwork)
responses.add(NodeGraphMessage::UpdateInSelectedNetwork);
}
DocumentMessage::SetBlendModeForSelectedLayers { blend_mode } => {
for layer in self.network_interface.selected_nodes(&[]).unwrap().selected_layers_except_artboards(&self.network_interface) {
@ -1018,6 +1019,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
self.graph_fade_artwork_percentage = percentage;
responses.add(FrontendMessage::UpdateGraphFadeArtwork { percentage });
}
DocumentMessage::SetNodePinned { node_id, pinned } => {
responses.add(DocumentMessage::StartTransaction);
responses.add(NodeGraphMessage::SetPinned { node_id, pinned });
@ -1224,21 +1226,13 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
.navigation_handler
.calculate_offset_transform(ipp.viewport_bounds.center(), &network_metadata.persistent_metadata.navigation_metadata.node_graph_ptz);
self.network_interface.set_transform(transform, &self.breadcrumb_network_path);
let imports = self.network_interface.frontend_imports(&self.breadcrumb_network_path).unwrap_or_default();
let exports = self.network_interface.frontend_exports(&self.breadcrumb_network_path).unwrap_or_default();
let add_import = self.network_interface.frontend_import_modify(&self.breadcrumb_network_path);
let add_export = self.network_interface.frontend_export_modify(&self.breadcrumb_network_path);
responses.add(DocumentMessage::RenderRulers);
responses.add(DocumentMessage::RenderScrollbars);
responses.add(NodeGraphMessage::UpdateEdges);
responses.add(NodeGraphMessage::UpdateBoxSelection);
responses.add(FrontendMessage::UpdateImportsExports {
imports,
exports,
add_import,
add_export,
});
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(FrontendMessage::UpdateNodeGraphTransform {
transform: Transform {
scale: transform.matrix2.x_axis.x,
@ -1465,26 +1459,22 @@ impl DocumentMessageHandler {
}
pub fn deserialize_document(serialized_content: &str) -> Result<Self, EditorError> {
let document_message_handler = serde_json::from_str::<OldDocumentMessageHandler>(serialized_content)
.map_or_else(
|_| serde_json::from_str::<DocumentMessageHandler>(serialized_content),
|old_message_handler| {
let default_document_message_handler = DocumentMessageHandler {
network_interface: NodeNetworkInterface::from_old_network(old_message_handler.network),
collapsed: old_message_handler.collapsed,
commit_hash: old_message_handler.commit_hash,
document_ptz: old_message_handler.document_ptz,
document_mode: old_message_handler.document_mode,
view_mode: old_message_handler.view_mode,
overlays_visible: old_message_handler.overlays_visible,
rulers_visible: old_message_handler.rulers_visible,
graph_view_overlay_open: old_message_handler.graph_view_overlay_open,
snapping_state: old_message_handler.snapping_state,
..Default::default()
};
Ok(default_document_message_handler)
},
)
let document_message_handler = serde_json::from_str::<DocumentMessageHandler>(serialized_content)
.or_else(|_| {
serde_json::from_str::<OldDocumentMessageHandler>(serialized_content).map(|old_message_handler| DocumentMessageHandler {
network_interface: NodeNetworkInterface::from_old_network(old_message_handler.network),
collapsed: old_message_handler.collapsed,
commit_hash: old_message_handler.commit_hash,
document_ptz: old_message_handler.document_ptz,
document_mode: old_message_handler.document_mode,
view_mode: old_message_handler.view_mode,
overlays_visible: old_message_handler.overlays_visible,
rulers_visible: old_message_handler.rulers_visible,
graph_view_overlay_open: old_message_handler.graph_view_overlay_open,
snapping_state: old_message_handler.snapping_state,
..Default::default()
})
})
.map_err(|e| EditorError::DocumentDeserialization(e.to_string()))?;
Ok(document_message_handler)
}
@ -2089,7 +2079,7 @@ impl DocumentMessageHandler {
/// Create a network interface with a single export
fn default_document_network_interface() -> NodeNetworkInterface {
let mut network_interface = NodeNetworkInterface::default();
network_interface.add_export(TaggedValue::ArtboardGroup(graphene_core::ArtboardGroup::EMPTY), -1, "".to_string(), &[]);
network_interface.add_export(TaggedValue::ArtboardGroup(graphene_core::ArtboardGroup::EMPTY), -1, "", &[]);
network_interface
}

View File

@ -215,7 +215,7 @@ impl<'a> ModifyInputsContext<'a> {
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist").default_node_template();
let image = resolve_document_node_type("Image")
.expect("Image node does not exist")
.node_template_input_override([Some(NodeInput::value(TaggedValue::ImageFrame(image_frame), false))]);
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::ImageFrame(image_frame), false))]);
let image_id = NodeId::new();
self.network_interface.insert_node(image_id, image, &[]);
@ -256,7 +256,11 @@ impl<'a> ModifyInputsContext<'a> {
let mut existing_node_id = None;
for upstream_node in upstream.collect::<Vec<_>>() {
// Check if this is the node we have been searching for.
if self.network_interface.reference(&upstream_node, &[]).is_some_and(|node_reference| node_reference == reference) {
if self
.network_interface
.reference(&upstream_node, &[])
.is_some_and(|node_reference| *node_reference == Some(reference.to_string()))
{
existing_node_id = Some(upstream_node);
break;
}

View File

@ -1,7 +1,7 @@
use super::utility_types::Direction;
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate, OutputConnector};
use crate::messages::portfolio::document::utility_types::network_interface::{ImportOrExport, InputConnector, NodeTemplate, OutputConnector};
use crate::messages::prelude::*;
use glam::IVec2;
@ -58,8 +58,7 @@ pub enum NodeGraphMessage {
EnterNestedNetwork,
DuplicateSelectedNodes,
ExposeInput {
node_id: NodeId,
input_index: usize,
input_connector: InputConnector,
new_exposed: bool,
},
InsertNode {
@ -98,6 +97,20 @@ pub enum NodeGraphMessage {
shift: Key,
},
PrintSelectedNodeCoordinates,
RemoveImport {
import_index: usize,
},
RemoveExport {
export_index: usize,
},
ReorderImport {
start_index: usize,
end_index: usize,
},
ReorderExport {
start_index: usize,
end_index: usize,
},
RunDocumentGraph,
ForceRunDocumentGraph,
SelectedNodesAdd {
@ -153,6 +166,14 @@ pub enum NodeGraphMessage {
TogglePreviewImpl {
node_id: NodeId,
},
SetImportExportName {
name: String,
index: ImportOrExport,
},
SetImportExportNameImpl {
name: String,
index: ImportOrExport,
},
ToggleSelectedAsLayersOrNodes,
ToggleSelectedLocked,
ToggleLocked {
@ -180,6 +201,7 @@ pub enum NodeGraphMessage {
},
UpdateEdges,
UpdateBoxSelection,
UpdateImportsExports,
UpdateLayerPanel,
UpdateNewNodeGraph,
UpdateTypes {

View File

@ -71,6 +71,12 @@ pub struct NodeGraphMessageHandler {
auto_panning: AutoPanning,
/// The node to preview on mouse up if alt-clicked
preview_on_mouse_up: Option<NodeId>,
// The index of the import that is being moved
reordering_import: Option<usize>,
// The index of the export that is being moved
reordering_export: Option<usize>,
// The end index of the moved port
end_index: Option<usize>,
}
/// NodeGraphMessageHandler always modifies the network which the selected nodes are in. No GraphOperationMessages should be added here, since those messages will always affect the document network.
@ -99,8 +105,14 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_layer_id] });
}
NodeGraphMessage::AddImport => network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, String::new(), breadcrumb_network_path),
NodeGraphMessage::AddExport => network_interface.add_export(graph_craft::document::value::TaggedValue::None, -1, String::new(), breadcrumb_network_path),
NodeGraphMessage::AddImport => {
network_interface.add_import(graph_craft::document::value::TaggedValue::None, true, -1, "", breadcrumb_network_path);
responses.add(NodeGraphMessage::SendGraph);
}
NodeGraphMessage::AddExport => {
network_interface.add_export(graph_craft::document::value::TaggedValue::None, -1, "", breadcrumb_network_path);
responses.add(NodeGraphMessage::SendGraph);
}
NodeGraphMessage::Init => {
responses.add(BroadcastMessage::SubscribeEvent {
on: BroadcastEvent::SelectionChanged,
@ -298,35 +310,29 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
{
return;
};
let Some(network) = network_interface.network(selection_network_path) else {
log::error!("Could not get network in EnterNestedNetwork");
return;
};
let Some(node) = network.nodes.get(&node_id) else { return };
if let DocumentNodeImplementation::Network(_) = node.implementation {
if let Some(DocumentNodeImplementation::Network(_)) = network_interface.implementation(&node_id, selection_network_path) {
responses.add(DocumentMessage::EnterNestedNetwork { node_id });
}
}
NodeGraphMessage::ExposeInput { node_id, input_index, new_exposed } => {
let Some(network) = network_interface.network(selection_network_path) else {
NodeGraphMessage::ExposeInput { input_connector, new_exposed } => {
let InputConnector::Node { node_id, input_index } = input_connector else {
log::error!("Cannot expose/hide export");
return;
};
let Some(node) = network.nodes.get(&node_id) else {
let Some(node) = network_interface.document_node(&node_id, selection_network_path) else {
log::error!("Could not find node {node_id} in NodeGraphMessage::ExposeInput");
return;
};
let Some(mut input) = node.inputs.get(input_index).cloned() else {
log::error!("Could not find input {input_index} in NodeGraphMessage::ExposeInput");
return;
};
if let NodeInput::Value { exposed, .. } = &mut input {
*exposed = new_exposed;
} else {
// TODO: Should network and node inputs be able to be hidden?
log::error!("Could not hide/show input: {:?} since it is not NodeInput::Value", input);
} else if !new_exposed {
// If hiding an input that is not a value, then disconnect it. This will convert it to a value input.
responses.add(NodeGraphMessage::DisconnectInput { input_connector });
responses.add(NodeGraphMessage::ExposeInput { input_connector, new_exposed });
return;
}
@ -336,6 +342,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
input_connector: InputConnector::node(node_id, input_index),
input,
});
responses.add(PropertiesPanelMessage::Refresh);
responses.add(NodeGraphMessage::SendGraph);
}
@ -559,23 +566,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
let node_graph_point = network_metadata.persistent_metadata.navigation_metadata.node_graph_to_viewport.inverse().transform_point2(click);
let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else {
log::error!("Could not get modify import export in PointerDown");
return;
};
if modify_import_export.add_export.intersect_point_no_stroke(node_graph_point) {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::AddExport);
responses.add(NodeGraphMessage::SendGraph);
return;
} else if modify_import_export.add_import.intersect_point_no_stroke(node_graph_point) {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::AddImport);
responses.add(NodeGraphMessage::SendGraph);
return;
}
if network_interface
.layer_click_target_from_click(click, network_interface::LayerClickTargetTypes::Grip, selection_network_path)
.is_some()
@ -651,6 +641,37 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
return;
}
let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else {
log::error!("Could not get modify import export in PointerDown");
return;
};
if modify_import_export.add_import_export.clicked_input_port_from_point(node_graph_point).is_some() {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::AddExport);
return;
} else if modify_import_export.add_import_export.clicked_output_port_from_point(node_graph_point).is_some() {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::AddImport);
return;
} else if let Some(remove_import_index) = modify_import_export.remove_imports_exports.clicked_output_port_from_point(node_graph_point) {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::RemoveImport { import_index: remove_import_index });
return;
} else if let Some(remove_export_index) = modify_import_export.remove_imports_exports.clicked_input_port_from_point(node_graph_point) {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::RemoveExport { export_index: remove_export_index });
return;
} else if let Some(move_import_index) = modify_import_export.reorder_imports_exports.clicked_output_port_from_point(node_graph_point) {
responses.add(DocumentMessage::StartTransaction);
self.reordering_import = Some(move_import_index);
return;
} else if let Some(move_export_index) = modify_import_export.reorder_imports_exports.clicked_input_port_from_point(node_graph_point) {
responses.add(DocumentMessage::StartTransaction);
self.reordering_export = Some(move_export_index);
return;
}
self.selection_before_pointer_down = network_interface
.selected_nodes(selection_network_path)
.map(|selected_nodes| selected_nodes.selected_nodes().cloned().collect())
@ -881,6 +902,46 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(NodeGraphMessage::ShiftSelectedNodesByAmount { graph_delta, rubber_band: true });
} else if self.box_selection_start.is_some() {
responses.add(NodeGraphMessage::UpdateBoxSelection);
} else if self.reordering_import.is_some() {
let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else {
log::error!("Could not get modify import export in PointerUp");
return;
};
// Find the first import that is below the mouse position
self.end_index = Some(
modify_import_export
.reorder_imports_exports
.output_ports()
.find_map(|(index, click_target)| {
let Some(position) = click_target.bounding_box().map(|bbox| (bbox[0].y + bbox[1].y) / 2.) else {
log::error!("Could not get bounding box for import: {index}");
return None;
};
(position > point.y).then_some(*index)
})
.unwrap_or(modify_import_export.reorder_imports_exports.output_ports().count()),
);
responses.add(FrontendMessage::UpdateImportReorderIndex { index: self.end_index });
} else if self.reordering_export.is_some() {
let Some(modify_import_export) = network_interface.modify_import_export(selection_network_path) else {
log::error!("Could not get modify import export in PointerUp");
return;
};
// Find the first export that is below the mouse position
self.end_index = Some(
modify_import_export
.reorder_imports_exports
.input_ports()
.find_map(|(index, click_target)| {
let Some(position) = click_target.bounding_box().map(|bbox| (bbox[0].y + bbox[1].y) / 2.) else {
log::error!("Could not get bounding box for export: {index}");
return None;
};
(position > point.y).then_some(*index)
})
.unwrap_or(modify_import_export.reorder_imports_exports.input_ports().count()),
);
responses.add(FrontendMessage::UpdateExportReorderIndex { index: self.end_index });
}
}
NodeGraphMessage::PointerUp => {
@ -1129,15 +1190,34 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
}
self.select_if_not_dragged = None;
}
// End of reordering an import
else if let (Some(moving_import), Some(end_index)) = (self.reordering_import, self.end_index) {
responses.add(NodeGraphMessage::ReorderImport {
start_index: moving_import,
end_index,
});
responses.add(DocumentMessage::EndTransaction);
}
// End of reordering an export
else if let (Some(moving_export), Some(end_index)) = (self.reordering_export, self.end_index) {
responses.add(NodeGraphMessage::ReorderExport {
start_index: moving_export,
end_index,
});
responses.add(DocumentMessage::EndTransaction);
}
self.drag_start = None;
self.begin_dragging = false;
self.box_selection_start = None;
self.wire_in_progress_from_connector = None;
self.wire_in_progress_to_connector = None;
self.reordering_export = None;
self.reordering_import = None;
responses.add(DocumentMessage::EndTransaction);
responses.add(FrontendMessage::UpdateWirePathInProgress { wire_path: None });
responses.add(FrontendMessage::UpdateBox { box_selection: None })
responses.add(FrontendMessage::UpdateBox { box_selection: None });
responses.add(FrontendMessage::UpdateImportReorderIndex { index: None });
responses.add(FrontendMessage::UpdateExportReorderIndex { index: None });
}
NodeGraphMessage::PointerOutsideViewport { shift } => {
if self.drag_start.is_some() || self.box_selection_start.is_some() {
@ -1186,6 +1266,26 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
// }
// }
}
NodeGraphMessage::RemoveImport { import_index: usize } => {
network_interface.remove_import(usize, selection_network_path);
responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
NodeGraphMessage::RemoveExport { export_index: usize } => {
network_interface.remove_export(usize, selection_network_path);
responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
NodeGraphMessage::ReorderImport { start_index, end_index } => {
network_interface.reorder_import(start_index, end_index, selection_network_path);
responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
NodeGraphMessage::ReorderExport { start_index, end_index } => {
network_interface.reorder_export(start_index, end_index, selection_network_path);
responses.add(NodeGraphMessage::SendGraph);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
NodeGraphMessage::RunDocumentGraph => {
responses.add(PortfolioMessage::SubmitGraphRender { document_id, ignore_hash: false });
}
@ -1230,16 +1330,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
let wires = Self::collect_wires(network_interface, breadcrumb_network_path);
let nodes = self.collect_nodes(network_interface, breadcrumb_network_path);
let (layer_widths, chain_widths, has_left_input_wire) = network_interface.collect_layer_widths(breadcrumb_network_path);
let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default();
let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default();
let add_import = network_interface.frontend_import_modify(breadcrumb_network_path);
let add_export = network_interface.frontend_export_modify(breadcrumb_network_path);
responses.add(FrontendMessage::UpdateImportsExports {
imports,
exports,
add_import,
add_export,
});
responses.add(NodeGraphMessage::UpdateImportsExports);
responses.add(FrontendMessage::UpdateNodeGraph { nodes, wires });
responses.add(FrontendMessage::UpdateLayerWidths {
layer_widths,
@ -1253,16 +1344,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
if graph_view_overlay_open {
network_interface.set_grid_aligned_edges(DVec2::new(ipp.viewport_bounds.bottom_right.x - ipp.viewport_bounds.top_left.x, 0.), breadcrumb_network_path);
// Send the new edges to the frontend
let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default();
let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default();
let add_import = network_interface.frontend_import_modify(breadcrumb_network_path);
let add_export = network_interface.frontend_export_modify(breadcrumb_network_path);
responses.add(FrontendMessage::UpdateImportsExports {
imports,
exports,
add_import,
add_export,
});
responses.add(NodeGraphMessage::UpdateImportsExports);
}
}
NodeGraphMessage::SetInputValue { node_id, input_index, value } => {
@ -1272,7 +1354,10 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
input,
});
responses.add(PropertiesPanelMessage::Refresh);
if (!network_interface.reference(&node_id, selection_network_path).is_some_and(|reference| reference == "Imaginate") || input_index == 0)
if (!network_interface
.reference(&node_id, selection_network_path)
.is_some_and(|reference| *reference == Some("Imaginate".to_string()))
|| input_index == 0)
&& network_interface.connected_to_output(&node_id, selection_network_path)
{
responses.add(NodeGraphMessage::RunDocumentGraph);
@ -1377,6 +1462,13 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => {
network_interface.set_display_name(&node_id, alias, selection_network_path);
}
NodeGraphMessage::SetImportExportName { name, index } => {
responses.add(DocumentMessage::StartTransaction);
responses.add(NodeGraphMessage::SetImportExportNameImpl { name, index });
responses.add(DocumentMessage::EndTransaction);
responses.add(NodeGraphMessage::UpdateImportsExports);
}
NodeGraphMessage::SetImportExportNameImpl { name, index } => network_interface.set_import_export_name(name, index, breadcrumb_network_path),
NodeGraphMessage::TogglePreview { node_id } => {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::TogglePreviewImpl { node_id });
@ -1427,13 +1519,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
let node_ids = selected_nodes.selected_nodes().cloned().collect::<Vec<_>>();
// If any of the selected nodes are pinned, unpin them all. Otherwise, pin them all.
let pinned = !node_ids.iter().all(|node_id| {
if let Some(node) = network_interface.node_metadata(node_id, breadcrumb_network_path) {
node.persistent_metadata.pinned
} else {
false
}
});
let pinned = !node_ids.iter().all(|node_id| network_interface.is_pinned(node_id, breadcrumb_network_path));
responses.add(DocumentMessage::AddTransaction);
for node_id in &node_ids {
@ -1442,9 +1528,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids });
}
NodeGraphMessage::ToggleSelectedVisibility => {
let Some(network) = network_interface.network(selection_network_path) else {
return;
};
let Some(selected_nodes) = network_interface.selected_nodes(selection_network_path) else {
log::error!("Could not get selected nodes in NodeGraphMessage::ToggleSelectedLocked");
return;
@ -1452,7 +1535,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
let node_ids = selected_nodes.selected_nodes().cloned().collect::<Vec<_>>();
// If any of the selected nodes are hidden, show them all. Otherwise, hide them all.
let visible = !node_ids.iter().all(|node_id| network.nodes.get(node_id).is_some_and(|node| node.visible));
let visible = !node_ids.iter().all(|node_id| network_interface.is_visible(node_id, selection_network_path));
responses.add(DocumentMessage::AddTransaction);
for node_id in &node_ids {
@ -1461,16 +1544,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(NodeGraphMessage::SetLockedOrVisibilitySideEffects { node_ids });
}
NodeGraphMessage::ToggleVisibility { node_id } => {
let Some(network) = network_interface.network(selection_network_path) else {
return;
};
let Some(node) = network.nodes.get(&node_id) else {
log::error!("Cannot get node {node_id} in NodeGraphMessage::ToggleVisibility");
return;
};
let visible = !node.visible;
let visible = !network_interface.is_visible(&node_id, selection_network_path);
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::SetVisibility { node_id, visible });
@ -1552,6 +1626,32 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(FrontendMessage::UpdateBox { box_selection })
}
}
NodeGraphMessage::UpdateImportsExports => {
let imports = network_interface.frontend_imports(breadcrumb_network_path).unwrap_or_default();
let exports = network_interface.frontend_exports(breadcrumb_network_path).unwrap_or_default();
let add_import = network_interface
.frontend_import_export_modify(
|modify_import_export_click_target| modify_import_export_click_target.add_import_export.output_ports().collect::<Vec<_>>(),
breadcrumb_network_path,
)
.into_iter()
.next();
let add_export = network_interface
.frontend_import_export_modify(
|modify_import_export_click_target| modify_import_export_click_target.add_import_export.input_ports().collect::<Vec<_>>(),
breadcrumb_network_path,
)
.into_iter()
.next();
responses.add(FrontendMessage::UpdateImportsExports {
imports,
exports,
add_import,
add_export,
});
}
NodeGraphMessage::UpdateLayerPanel => {
Self::update_layer_panel(network_interface, selection_network_path, collapsed, responses);
}
@ -1671,14 +1771,7 @@ impl NodeGraphMessageHandler {
let has_selection = selected_nodes.has_selected_nodes();
let selection_includes_layers = network_interface.selected_nodes(&[]).unwrap().selected_layers(network_interface.document_metadata()).count() > 0;
let selection_all_locked = network_interface.selected_nodes(&[]).unwrap().selected_unlocked_layers(network_interface).count() == 0;
let selection_all_visible = selected_nodes.selected_nodes().all(|id| {
if let Some(node) = network.nodes.get(id) {
node.visible
} else {
error!("Could not get node {id} in update_selection_action_buttons");
true
}
});
let selection_all_visible = selected_nodes.selected_nodes().all(|node_id| network_interface.is_visible(node_id, breadcrumb_network_path));
let mut widgets = vec![
PopoverButton::new()
@ -1839,10 +1932,6 @@ impl NodeGraphMessageHandler {
/// Collate the properties panel sections for a node graph
pub fn collate_properties(context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
// If the selected nodes are in the document network, use the document network. Otherwise, use the nested network
let Some(network) = context.network_interface.network(context.selection_network_path) else {
warn!("No network in collate_properties");
return Vec::new();
};
let Some(selected_nodes) = context.network_interface.selected_nodes(context.selection_network_path) else {
warn!("No selected nodes in collate_properties");
return Vec::new();
@ -1868,25 +1957,13 @@ impl NodeGraphMessageHandler {
match layers.len() {
// If no layers are selected, show properties for all selected nodes
0 => {
let selected_nodes = nodes
.iter()
.filter_map(|node_id| {
network.nodes.get(node_id).map(|node| {
let pinned = if let Some(node) = context.network_interface.node_metadata(node_id, context.selection_network_path) {
node.persistent_metadata.pinned
} else {
error!("Could not get node {node_id} in collate_properties");
false
};
node_properties::generate_node_properties(node, *node_id, pinned, context)
})
})
.collect::<Vec<_>>();
let selected_nodes = nodes.iter().map(|node_id| node_properties::generate_node_properties(*node_id, context)).collect::<Vec<_>>();
if !selected_nodes.is_empty() {
return selected_nodes;
}
// TODO: Display properties for encapsulating node when no nodes are selected in a nested network
// This may require store a separate path for the properties panel
let mut properties = vec![LayoutGroup::Row {
widgets: vec![
Separator::new(SeparatorType::Related).widget_holder(),
@ -1900,20 +1977,20 @@ impl NodeGraphMessageHandler {
],
}];
let Some(network) = context.network_interface.network(context.selection_network_path) else {
warn!("No network in collate_properties");
return Vec::new();
};
// And if no nodes are selected, show properties for all pinned nodes
let pinned_node_properties = network
.nodes
.keys()
.cloned()
.collect::<Vec<_>>()
.iter()
.filter_map(|(node_id, node)| {
let pinned = if let Some(node) = context.network_interface.node_metadata(node_id, context.selection_network_path) {
node.persistent_metadata.pinned
} else {
error!("Could not get node {node_id} in collate_properties");
false
};
if pinned {
Some(node_properties::generate_node_properties(node, *node_id, pinned, context))
.filter_map(|node_id| {
if context.network_interface.is_pinned(node_id, context.selection_network_path) {
Some(node_properties::generate_node_properties(*node_id, context))
} else {
None
}
@ -1983,17 +2060,9 @@ impl NodeGraphMessageHandler {
!context.network_interface.is_layer(node_id, context.selection_network_path)
}
})
.filter_map(|(_, node_id)| network.nodes.get(&node_id).map(|node| (node, node_id)))
.map(|(node, node_id)| {
let pinned = if let Some(node) = context.network_interface.node_metadata(&node_id, context.selection_network_path) {
node.persistent_metadata.pinned
} else {
error!("Could not get node {node_id} in collate_properties");
false
};
node_properties::generate_node_properties(node, node_id, pinned, context)
})
.collect::<Vec<_>>()
.iter()
.map(|(_, node_id)| node_properties::generate_node_properties(*node_id, context))
.collect::<Vec<_>>();
layer_properties.extend(node_properties);
@ -2110,8 +2179,9 @@ impl NodeGraphMessageHandler {
let inputs = frontend_inputs_lookup.remove(&node_id).unwrap_or_default();
let mut inputs = inputs.into_iter().map(|input| {
input.map(|input| FrontendGraphInput {
data_type: FrontendGraphDataType::with_type(&input.ty),
data_type: FrontendGraphDataType::displayed_type(&input.ty, &input.type_source),
resolved_type: Some(format!("{:?} from {:?}", &input.ty, input.type_source)),
valid_types: input.valid_types.iter().map(|ty| ty.to_string()).collect(),
name: input.name.unwrap_or_else(|| input.ty.nested_type().to_string()),
connected_to: input.output_connector,
})
@ -2122,8 +2192,8 @@ impl NodeGraphMessageHandler {
let output_types = network_interface.output_types(&node_id, breadcrumb_network_path);
let primary_output_type = output_types.first().cloned().flatten();
let frontend_data_type = if let Some((output_type, _)) = &primary_output_type {
FrontendGraphDataType::with_type(output_type)
let frontend_data_type = if let Some((output_type, type_source)) = &primary_output_type {
FrontendGraphDataType::displayed_type(output_type, type_source)
} else {
FrontendGraphDataType::General
};
@ -2144,8 +2214,8 @@ impl NodeGraphMessageHandler {
if index == 0 && network_interface.has_primary_output(&node_id, breadcrumb_network_path) {
continue;
}
let frontend_data_type = if let Some((output_type, _)) = &exposed_output {
FrontendGraphDataType::with_type(output_type)
let frontend_data_type = if let Some((output_type, type_source)) = &exposed_output {
FrontendGraphDataType::displayed_type(output_type, type_source)
} else {
FrontendGraphDataType::General
};
@ -2203,7 +2273,7 @@ impl NodeGraphMessageHandler {
.node_metadata(&node_id, breadcrumb_network_path)
.is_some_and(|node_metadata| node_metadata.persistent_metadata.is_layer()),
can_be_layer: can_be_layer_lookup.contains(&node_id),
reference: network_interface.reference(&node_id, breadcrumb_network_path),
reference: network_interface.reference(&node_id, breadcrumb_network_path).cloned().unwrap_or_default(),
display_name: network_interface.frontend_display_name(&node_id, breadcrumb_network_path),
primary_input,
exposed_inputs,
@ -2273,7 +2343,7 @@ impl NodeGraphMessageHandler {
|| (
// Check if the last node in the chain has an exposed left input
network_interface.upstream_flow_back_from_nodes(vec![node_id], &[], network_interface::FlowType::HorizontalFlow).last().is_some_and(|node_id|
network_interface.network(&[]).unwrap().nodes.get(&node_id).map_or_else(||{log::error!("Could not get node {node_id} in update_layer_panel"); false}, |node| {
network_interface.document_node(&node_id, &[]).map_or_else(||{log::error!("Could not get node {node_id} in update_layer_panel"); false}, |node| {
if network_interface.is_layer(&node_id, &[]) {
node.inputs.iter().filter(|input| input.is_exposed_to_frontend(true)).nth(1).is_some_and(|input| input.as_value().is_some())
} else {
@ -2284,7 +2354,7 @@ impl NodeGraphMessageHandler {
let parents_visible = layer.ancestors(network_interface.document_metadata()).filter(|&ancestor| ancestor != layer).all(|layer| {
if layer != LayerNodeIdentifier::ROOT_PARENT {
network_interface.network(&[]).unwrap().nodes.get(&layer.to_node()).map(|node| node.visible).unwrap_or_default()
network_interface.document_node(&layer.to_node(), &[]).map(|node| node.visible).unwrap_or_default()
} else {
true
}
@ -2389,6 +2459,7 @@ struct InputLookup {
name: Option<String>,
ty: Type,
type_source: TypeSource,
valid_types: Vec<Type>,
output_connector: Option<OutputConnector>,
}
@ -2412,8 +2483,10 @@ fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface:
}
// Get the name from the metadata here (since it also requires a reference to the `network_interface`)
let name = network_interface.input_name(&node_id, index, breadcrumb_network_path);
let name = network_interface
.input_name(&node_id, index, breadcrumb_network_path)
.filter(|s| !s.is_empty())
.map(|name| name.to_string());
// Get the output connector that feeds into this input (done here as well for simplicity)
let connector = OutputConnector::from_input(input);
@ -2430,13 +2503,21 @@ fn frontend_inputs_lookup(breadcrumb_network_path: &[NodeId], network_interface:
for (index, value) in value.iter_mut().enumerate() {
// Skip not exposed inputs for efficiency
let Some(value) = value else { continue };
// Resolve the type (done in a separate loop because it requires a mutable reference to the `network_interface`)
let (ty, type_source) = network_interface.input_type(&InputConnector::node(node_id, index), breadcrumb_network_path);
value.ty = ty;
value.type_source = type_source;
}
}
for (&node_id, value) in frontend_inputs_lookup.iter_mut() {
for (index, value) in value.iter_mut().enumerate() {
// Skip not exposed inputs for efficiency
let Some(value) = value else { continue };
// Resolve the type (done in a separate loop because it requires a mutable reference to the `network_interface`)
value.valid_types = network_interface.valid_input_types(&InputConnector::node(node_id, index), breadcrumb_network_path);
}
}
frontend_inputs_lookup
}
@ -2462,6 +2543,9 @@ impl Default for NodeGraphMessageHandler {
deselect_on_pointer_up: None,
auto_panning: Default::default(),
preview_on_mouse_up: None,
reordering_export: None,
reordering_import: None,
end_index: None,
}
}
}

View File

@ -2,7 +2,7 @@ use graph_craft::document::value::TaggedValue;
use graph_craft::document::NodeId;
use graphene_core::Type;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector};
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, OutputConnector, TypeSource};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum FrontendGraphDataType {
@ -11,12 +11,12 @@ pub enum FrontendGraphDataType {
Raster,
VectorData,
Number,
Graphic,
Group,
Artboard,
}
impl FrontendGraphDataType {
pub fn with_type(input: &Type) -> Self {
fn with_type(input: &Type) -> Self {
match TaggedValue::from_type_or_none(input) {
TaggedValue::Image(_) | TaggedValue::ImageFrame(_) => Self::Raster,
TaggedValue::Subpaths(_) | TaggedValue::VectorData(_) => Self::VectorData,
@ -30,11 +30,18 @@ impl FrontendGraphDataType {
| TaggedValue::F64Array4(_)
| TaggedValue::VecF64(_)
| TaggedValue::VecDVec2(_) => Self::Number,
TaggedValue::GraphicGroup(_) | TaggedValue::GraphicElement(_) => Self::Graphic,
TaggedValue::GraphicGroup(_) | TaggedValue::GraphicElement(_) => Self::Group,
TaggedValue::ArtboardGroup(_) => Self::Artboard,
_ => Self::General,
}
}
pub fn displayed_type(input: &Type, type_source: &TypeSource) -> Self {
match type_source {
TypeSource::Error(_) | TypeSource::RandomProtonodeImplementation => Self::General,
_ => Self::with_type(input),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
@ -44,6 +51,8 @@ pub struct FrontendGraphInput {
pub name: String,
#[serde(rename = "resolvedType")]
pub resolved_type: Option<String>,
#[serde(rename = "validTypes")]
pub valid_types: Vec<String>,
#[serde(rename = "connectedTo")]
pub connected_to: Option<OutputConnector>,
}
@ -180,6 +189,8 @@ pub struct FrontendClickTargets {
pub all_nodes_bounding_box: String,
#[serde(rename = "importExportsBoundingBox")]
pub import_exports_bounding_box: String,
#[serde(rename = "modifyImportExport")]
pub modify_import_export: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]

View File

@ -4,7 +4,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::Node
use crate::node_graph_executor::NodeGraphExecutor;
pub struct PropertiesPanelMessageHandlerData<'a> {
pub network_interface: &'a NodeNetworkInterface,
pub network_interface: &'a mut NodeNetworkInterface,
pub selection_network_path: &'a [NodeId],
pub document_name: &'a str,
pub executor: &'a mut NodeGraphExecutor,

View File

@ -384,7 +384,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
document_serialized_content,
} => {
// It can be helpful to temporarily set `upgrade_from_before_editable_subgraphs` to true if it's desired to upgrade a piece of artwork to use fresh copies of all nodes
let upgrade_from_before_editable_subgraphs = document_serialized_content.contains("node_output_index");
let replace_implementations_from_definition = document_serialized_content.contains("node_output_index");
let upgrade_vector_manipulation_format = document_serialized_content.contains("ManipulatorGroupIds") && !document_name.contains("__DO_NOT_UPGRADE__");
let document_name = document_name.replace("__DO_NOT_UPGRADE__", "");
@ -409,7 +409,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
// TODO: Eventually remove this document upgrade code
// Upgrade all old nodes to support editable subgraphs introduced in #1750
if upgrade_from_before_editable_subgraphs {
if replace_implementations_from_definition {
// This can be used, if uncommented, to upgrade demo artwork with outdated document node internals from their definitions. Delete when it's no longer needed.
// Used for upgrading old internal networks for demo artwork nodes. Will reset all node internals for any opened file
for node_id in &document
@ -614,6 +614,31 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
.network_interface
.set_input(&InputConnector::node(NodeId(0), 1), NodeInput::value(TaggedValue::String(label), false), &[*node_id]);
}
if reference == "Image" && inputs_count == 1 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let new_image_node = node_definition.default_node_template();
document.network_interface.replace_implementation(node_id, &[], new_image_node.document_node.implementation);
// Insert a new empty input for the image
document.network_interface.add_import(TaggedValue::None, false, 0, "Empty", &[*node_id]);
document.network_interface.set_reference(node_id, &[], Some("Image".to_string()));
}
if reference == "Noise Pattern" && inputs_count == 15 {
let node_definition = crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type(reference).unwrap();
let new_noise_pattern_node = node_definition.default_node_template();
document.network_interface.replace_implementation(node_id, &[], new_noise_pattern_node.document_node.implementation);
let old_inputs = document.network_interface.replace_inputs(node_id, new_noise_pattern_node.document_node.inputs.clone(), &[]);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::None, false), &[]);
for (i, input) in old_inputs.iter().enumerate() {
document.network_interface.set_input(&InputConnector::node(*node_id, i + 1), input.clone(), &[]);
}
}
}
// TODO: Eventually remove this document upgrade code

View File

@ -186,7 +186,7 @@ impl<'a> NodeGraphLayer<'a> {
/// Node id of a node if it exists in the layer's primary flow
pub fn upstream_node_id_from_name(&self, node_name: &str) -> Option<NodeId> {
self.horizontal_layer_flow()
.find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| reference == node_name))
.find(|node_id| self.network_interface.reference(node_id, &[]).is_some_and(|reference| *reference == Some(node_name.to_string())))
}
/// Find all of the inputs of a specific node within the layer's primary flow, up until the next layer is reached.
@ -194,7 +194,7 @@ impl<'a> NodeGraphLayer<'a> {
self.horizontal_layer_flow()
.skip(1)// Skip self
.take_while(|node_id| !self.network_interface.is_layer(node_id,&[]))
.find(|node_id| self.network_interface.reference(node_id,&[]).is_some_and(|reference| reference == node_name))
.find(|node_id| self.network_interface.reference(node_id,&[]).is_some_and(|reference| *reference == Some(node_name.to_string())))
.and_then(|node_id| self.network_interface.network(&[]).unwrap().nodes.get(&node_id).map(|node| &node.inputs))
}

View File

@ -277,7 +277,7 @@ impl BrushToolData {
let Some(reference) = document.network_interface.reference(&node_id, &[]) else {
continue;
};
if reference == "Brush" && node_id != layer.to_node() {
if *reference == Some("Brush".to_string()) && node_id != layer.to_node() {
let points_input = node.inputs.get(2)?;
let Some(TaggedValue::BrushStrokes(strokes)) = points_input.as_value() else {
continue;
@ -285,7 +285,7 @@ impl BrushToolData {
self.strokes.clone_from(strokes);
return Some(layer);
} else if reference == "Transform" {
} else if *reference == Some("Transform".to_string()) {
let upstream = document.metadata().upstream_transform(node_id);
let pivot = DAffine2::from_translation(upstream.transform_point2(get_current_normalized_pivot(&node.inputs)));
self.transform = pivot * get_current_transform(&node.inputs) * pivot.inverse() * self.transform;

View File

@ -793,7 +793,10 @@ impl Fsm for PenToolFsmState {
(_, PenToolMessage::Redo) => {
tool_data.point_index = (tool_data.point_index + 1).min(tool_data.latest_points.len().saturating_sub(1));
tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, responses);
(tool_data.point_index == 0).then_some(PenToolFsmState::Ready).unwrap_or(PenToolFsmState::PlacingAnchor)
match tool_data.point_index {
0 => PenToolFsmState::Ready,
_ => PenToolFsmState::PlacingAnchor,
}
}
_ => self,
}

View File

@ -123,8 +123,8 @@
--color-data-vectordata-dim: #4b778c;
--color-data-number: #cbbab4;
--color-data-number-dim: #87736b;
--color-data-graphic: #6b84e8;
--color-data-graphic-dim: #4a557b;
--color-data-group: #6b84e8;
--color-data-group-dim: #4a557b;
--color-data-artboard: #70a898;
--color-data-artboard-dim: #3a6156;

View File

@ -47,6 +47,69 @@
$: wirePaths = createWirePaths($nodeGraph.wirePathInProgress, nodeWirePaths);
let inputElement: HTMLInputElement;
let hoveringImportIndex: number | undefined = undefined;
let hoveringExportIndex: number | undefined = undefined;
let editingNameImportIndex: number | undefined = undefined;
let editingNameExportIndex: number | undefined = undefined;
let editingNameText = "";
function exportsToEdgeTextInputWidth() {
let exportTextDivs = document.querySelectorAll(`[data-export-text-edge]`);
let exportTextDiv = Array.from(exportTextDivs).find((div) => {
return div.getAttribute("data-index") === String(editingNameExportIndex);
});
if (!graph || !exportTextDiv) return "50px";
let distance = graph.getBoundingClientRect().right - exportTextDiv.getBoundingClientRect().right;
return distance - 15 + "px";
}
function importsToEdgeTextInputWidth() {
let importTextDivs = document.querySelectorAll(`[data-import-text-edge]`);
let importTextDiv = Array.from(importTextDivs).find((div) => {
return div.getAttribute("data-index") === String(editingNameImportIndex);
});
if (!graph || !importTextDiv) return "50px";
let distance = importTextDiv.getBoundingClientRect().left - graph.getBoundingClientRect().left;
return distance - 15 + "px";
}
function setEditingImportNameIndex(index: number, currentName: string) {
focusInput(currentName);
editingNameImportIndex = index;
}
function setEditingExportNameIndex(index: number, currentName: string) {
focusInput(currentName);
editingNameExportIndex = index;
}
function focusInput(currentName: string) {
editingNameText = currentName;
setTimeout(() => {
if (inputElement) {
inputElement.focus();
}
}, 0);
}
function setEditingImportName(event: Event) {
if (editingNameImportIndex !== undefined) {
let text = (event.target as HTMLInputElement)?.value;
editor.handle.setImportName(editingNameImportIndex, text);
editingNameImportIndex = undefined;
}
}
function setEditingExportName(event: Event) {
if (editingNameExportIndex !== undefined) {
let text = (event.target as HTMLInputElement)?.value;
editor.handle.setExportName(editingNameExportIndex, text);
editingNameExportIndex = undefined;
}
}
function calculateGridSpacing(scale: number): number {
const dense = scale * GRID_SIZE;
let sparse = dense;
@ -267,6 +330,10 @@
return value.resolvedType ? `Resolved Data: ${value.resolvedType}` : `Unresolved Data: ${value.dataType}`;
}
function validTypesText(value: FrontendGraphInput): string {
return `Valid Types:\n${value.validTypes.join(",\n ")}`;
}
function outputConnectedToText(output: FrontendGraphOutput): string {
if (output.connectedTo.length === 0) {
return "Connected to nothing";
@ -396,6 +463,9 @@
{/each}
<path class="all-nodes-bounding-box" d={$nodeGraph.clickTargets.allNodesBoundingBox} />
<path class="all-nodes-bounding-box" d={$nodeGraph.clickTargets.importExportsBoundingBox} />
{#each $nodeGraph.clickTargets.modifyImportExport as pathString}
<path class="modify-import-export" d={pathString} />
{/each}
</svg>
</div>
{/if}
@ -439,15 +509,54 @@
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
{/if}
</svg>
<p class="import-text" style:--offset-left={position.x / 24} style:--offset-top={position.y / 24}>{outputMetadata.name}</p>
<div
class="edit-import-export import"
on:pointerenter={() => (hoveringImportIndex = index)}
on:pointerleave={() => (hoveringImportIndex = undefined)}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
>
{#if editingNameImportIndex == index}
<input
class="import-text-input"
type="text"
style:width={importsToEdgeTextInputWidth()}
bind:this={inputElement}
bind:value={editingNameText}
on:blur={setEditingImportName}
on:keydown={(e) => e.key === "Enter" && setEditingImportName(e)}
/>
{:else}
<p class="import-text" on:dblclick={() => setEditingImportNameIndex(index, outputMetadata.name)}>{outputMetadata.name}</p>
{/if}
{#if hoveringImportIndex === index || editingNameImportIndex === index}
<IconButton
size={16}
icon={"Remove"}
class="remove-button-import"
data-index={index}
data-import-text-edge
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
<div class="reorder-drag-grip" title="Reorder this import"></div>
{/if}
</div>
{/each}
{#if $nodeGraph.reorderImportIndex !== undefined}
{@const position = {
x: Number($nodeGraph.imports[0].position.x),
y: Number($nodeGraph.imports[0].position.y) + Number($nodeGraph.reorderImportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={(position.x - 48) / 24} style:--offset-top={(position.y - 4) / 24} />
{/if}
{#if $nodeGraph.addImport !== undefined}
<div class="plus" style:--offset-left={$nodeGraph.addImport.x / 24} style:--offset-top={$nodeGraph.addImport.y / 24}>
<IconButton
class={"visibility"}
data-visibility-button
size={24}
icon={"Add"}
icon="Add"
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
@ -474,13 +583,50 @@
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color-dim)" />
{/if}
</svg>
<p class="export-text" style:--offset-left={position.x / 24} style:--offset-top={position.y / 24}>{inputMetadata.name}</p>
<div
class="edit-import-export export"
on:pointerenter={() => (hoveringExportIndex = index)}
on:pointerleave={() => (hoveringExportIndex = undefined)}
style:--offset-left={position.x / 24}
style:--offset-top={position.y / 24}
>
{#if hoveringExportIndex === index || editingNameExportIndex === index}
<div class="reorder-drag-grip" title="Reorder this export"></div>
<IconButton
size={16}
icon={"Remove"}
class="remove-button-export"
data-index={index}
data-export-text-edge
action={() => {
/* Button is purely visual, clicking is handled in NodeGraphMessage::PointerDown */
}}
/>
{/if}
{#if editingNameExportIndex === index}
<input
type="text"
style:width={exportsToEdgeTextInputWidth()}
bind:this={inputElement}
bind:value={editingNameText}
on:blur={setEditingExportName}
on:keydown={(e) => e.key === "Enter" && setEditingExportName(e)}
/>
{:else}
<p class="export-text" on:dblclick={() => setEditingExportNameIndex(index, inputMetadata.name)}>{inputMetadata.name}</p>
{/if}
</div>
{/each}
{#if $nodeGraph.reorderExportIndex !== undefined}
{@const position = {
x: Number($nodeGraph.exports[0].position.x),
y: Number($nodeGraph.exports[0].position.y) + Number($nodeGraph.reorderExportIndex) * 24,
}}
<div class="reorder-bar" style:--offset-left={position.x / 24} style:--offset-top={(position.y - 4) / 24} />
{/if}
{#if $nodeGraph.addExport !== undefined}
<div class="plus" style:--offset-left={$nodeGraph.addExport.x / 24} style:--offset-top={$nodeGraph.addExport.y / 24}>
<IconButton
class={"visibility"}
data-visibility-button
size={24}
icon={"Add"}
action={() => {
@ -566,7 +712,7 @@
bind:this={inputs[nodeIndex + 1][0]}
>
{#if node.primaryInput}
<title>{`${dataTypeTooltip(node.primaryInput)}\n${inputConnectedToText(node.primaryInput)}`}</title>
<title>{`${dataTypeTooltip(node.primaryInput)}\n${validTypesText(node.primaryInput)}\n${inputConnectedToText(node.primaryInput)}`}</title>
{/if}
{#if node.primaryInput?.connectedTo !== undefined}
<path d="M0,0H8V8L5.479,6.319a2.666,2.666,0,0,0-2.959,0L0,8Z" fill="var(--data-color)" />
@ -591,7 +737,7 @@
style:--data-color-dim={`var(--color-data-${stackDataInput.dataType.toLowerCase()}-dim)`}
bind:this={inputs[nodeIndex + 1][1]}
>
<title>{`${dataTypeTooltip(stackDataInput)}\n${inputConnectedToText(stackDataInput)}`}</title>
<title>{`${dataTypeTooltip(stackDataInput)}\n${validTypesText(stackDataInput)}\n${inputConnectedToText(stackDataInput)}`}</title>
{#if stackDataInput.connectedTo !== undefined}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
@ -698,7 +844,7 @@
style:--data-color-dim={`var(--color-data-${node.primaryInput.dataType.toLowerCase()}-dim)`}
bind:this={inputs[nodeIndex + 1][0]}
>
<title>{`${dataTypeTooltip(node.primaryInput)}\n${inputConnectedToText(node.primaryInput)}`}</title>
<title>{`${dataTypeTooltip(node.primaryInput)}\n${validTypesText(node.primaryInput)}\n${inputConnectedToText(node.primaryInput)}`}</title>
{#if node.primaryInput.connectedTo !== undefined}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
@ -718,7 +864,7 @@
style:--data-color-dim={`var(--color-data-${secondary.dataType.toLowerCase()}-dim)`}
bind:this={inputs[nodeIndex + 1][index + (node.primaryInput ? 1 : 0)]}
>
<title>{`${dataTypeTooltip(secondary)}\n${inputConnectedToText(secondary)}`}</title>
<title>{`${dataTypeTooltip(secondary)}\n${validTypesText(secondary)}\n${inputConnectedToText(secondary)}`}</title>
{#if secondary.connectedTo !== undefined}
<path d="M0,6.306A1.474,1.474,0,0,0,2.356,7.724L7.028,5.248c1.3-.687,1.3-1.809,0-2.5L2.356.276A1.474,1.474,0,0,0,0,1.694Z" fill="var(--data-color)" />
{:else}
@ -880,6 +1026,10 @@
.all-nodes-bounding-box {
stroke: purple;
}
.modify-import-export {
stroke: orange;
}
}
}
@ -918,30 +1068,66 @@
left: calc(var(--offset-left) * 24px);
}
.reorder-bar {
position: absolute;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
width: 50px;
height: 2px;
background: white;
}
.plus {
margin-top: -4px;
margin-left: -4px;
position: absolute;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
}
.export-text {
.edit-import-export {
position: absolute;
margin-top: 0;
margin-left: 20px;
display: flex;
align-items: center;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
}
margin-top: -5px;
height: 24px;
.import-text {
position: absolute;
text-align: right;
top: calc(var(--offset-top) * 24px);
left: calc(var(--offset-left) * 24px);
margin-top: 0;
margin-left: calc(-100px - 2px);
width: 100px;
&.import {
right: calc(100% - var(--offset-left) * 24px);
}
&.export {
left: calc(var(--offset-left) * 24px + 17px);
}
.import-text {
text-align: right;
text-wrap: nowrap;
}
.export-text {
text-wrap: nowrap;
}
.import-text-input {
text-align: right;
}
.remove-button-import {
margin-left: 3px;
}
.remove-button-export {
margin-right: 3px;
}
.reorder-drag-grip {
width: 8px;
height: 24px;
background-position: 2px 8px;
border-radius: 2px;
margin: -6px 0;
background-image: var(--icon-drag-grip-hover);
}
}
}

View File

@ -73,6 +73,14 @@ export class UpdateInSelectedNetwork extends JsMessage {
readonly inSelectedNetwork!: boolean;
}
export class UpdateImportReorderIndex extends JsMessage {
readonly importIndex!: number | undefined;
}
export class UpdateExportReorderIndex extends JsMessage {
readonly exportIndex!: number | undefined;
}
const LayerWidths = Transform(({ obj }) => obj.layerWidths);
const ChainWidths = Transform(({ obj }) => obj.chainWidths);
const HasLeftInputWire = Transform(({ obj }) => obj.hasLeftInputWire);
@ -170,6 +178,7 @@ export type FrontendClickTargets = {
readonly iconClickTargets: string[];
readonly allNodesBoundingBox: string;
readonly importExportsBoundingBox: string;
readonly modifyImportExport: string[];
};
export type ContextMenuInformation = {
@ -178,7 +187,7 @@ export type ContextMenuInformation = {
contextMenuData: "CreateNode" | { nodeId: bigint; currentlyIsNode: boolean };
};
export type FrontendGraphDataType = "General" | "Raster" | "VectorData" | "Number" | "Graphic" | "Artboard";
export type FrontendGraphDataType = "General" | "Raster" | "VectorData" | "Number" | "Group" | "Artboard";
export class Node {
readonly index!: bigint;
@ -210,6 +219,8 @@ export class FrontendGraphInput {
readonly resolvedType!: string | undefined;
readonly validTypes!: string[];
@CreateOutputConnectorOptional
connectedTo!: Node | undefined;
}
@ -1592,6 +1603,8 @@ export const messageMakers: Record<string, MessageMaker> = {
UpdateImportsExports,
UpdateInputHints,
UpdateInSelectedNetwork,
UpdateExportReorderIndex,
UpdateImportReorderIndex,
UpdateLayersPanelControlBarLayout,
UpdateLayerWidths,
UpdateMenuBarLayout,

View File

@ -15,6 +15,8 @@ import {
UpdateClickTargets,
UpdateContextMenuInformation,
UpdateInSelectedNetwork,
UpdateImportReorderIndex,
UpdateExportReorderIndex,
UpdateImportsExports,
UpdateLayerWidths,
UpdateNodeGraph,
@ -47,6 +49,8 @@ export function createNodeGraphState(editor: Editor) {
selected: [] as bigint[],
transform: { scale: 1, x: 0, y: 0 },
inSelectedNetwork: true,
reorderImportIndex: undefined as number | undefined,
reorderExportIndex: undefined as number | undefined,
});
// Set up message subscriptions on creation
@ -76,6 +80,18 @@ export function createNodeGraphState(editor: Editor) {
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateImportReorderIndex, (updateImportReorderIndex) => {
update((state) => {
state.reorderImportIndex = updateImportReorderIndex.importIndex;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateExportReorderIndex, (updateExportReorderIndex) => {
update((state) => {
state.reorderExportIndex = updateExportReorderIndex.exportIndex;
return state;
});
});
editor.subscriptions.subscribeJsMessage(UpdateImportsExports, (updateImportsExports) => {
update((state) => {
state.imports = updateImportsExports.imports;

View File

@ -12,7 +12,7 @@ use editor::consts::FILE_SAVE_SUFFIX;
use editor::messages::input_mapper::utility_types::input_keyboard::ModifierKeys;
use editor::messages::input_mapper::utility_types::input_mouse::{EditorMouseState, ScrollDelta, ViewportBounds};
use editor::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use editor::messages::portfolio::document::utility_types::network_interface::NodeTemplate;
use editor::messages::portfolio::document::utility_types::network_interface::{ImportOrExport, NodeTemplate};
use editor::messages::portfolio::utility_types::Platform;
use editor::messages::prelude::*;
use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
@ -697,6 +697,26 @@ impl EditorHandle {
self.dispatch(DocumentMessage::SetToNodeOrLayer { node_id: NodeId(id), is_layer });
}
/// Set the name of an import or export
#[wasm_bindgen(js_name = setImportName)]
pub fn set_import_name(&self, index: usize, name: String) {
let message = NodeGraphMessage::SetImportExportName {
name,
index: ImportOrExport::Import(index),
};
self.dispatch(message);
}
/// Set the name of an export
#[wasm_bindgen(js_name = setExportName)]
pub fn set_export_name(&self, index: usize, name: String) {
let message = NodeGraphMessage::SetImportExportName {
name,
index: ImportOrExport::Export(index),
};
self.dispatch(message);
}
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
pub fn inject_imaginate_poll_server_status(&self) {
self.dispatch(PortfolioMessage::ImaginatePollServerStatus);
@ -754,7 +774,10 @@ impl EditorHandle {
if let Some(network) = document_node.implementation.get_network() {
let mut nodes_to_upgrade = Vec::new();
for (node_id, _) in network.nodes.iter().collect::<Vec<_>>() {
if document.network_interface.reference(node_id, &[]).is_some_and(|reference| reference == "To Artboard")
if document
.network_interface
.reference(node_id, &[])
.is_some_and(|reference| *reference == Some("To Artboard".to_string()))
&& document
.network_interface
.network(&[])
@ -770,7 +793,7 @@ impl EditorHandle {
document
.network_interface
.replace_implementation(&node_id, &[], DocumentNodeImplementation::proto("graphene_core::ToArtboardNode"));
document.network_interface.add_import(TaggedValue::IVec2(glam::IVec2::default()), false, 2, "".to_string(), &[node_id]);
document.network_interface.add_import(TaggedValue::IVec2(glam::IVec2::default()), false, 2, "", &[node_id]);
}
}
}

View File

@ -37,7 +37,7 @@ impl ValueProvider for MathNodeContext {
}
/// Calculates a mathematical expression with input values "A" and "B"
#[node_macro::node(category("Math"))]
#[node_macro::node(category("General"), properties("math_properties"))]
fn math<U: num_traits::float::Float>(
_: (),
/// The value of "A" when calculating the expression

View File

@ -1185,7 +1185,7 @@ impl DomainWarpType {
// Aims for interoperable compatibility with:
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=%27mixr%27%20%3D%20Channel%20Mixer
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Lab%20color%20only-,Channel%20Mixer,-Key%20is%20%27mixr
#[node_macro::node(category("Raster: Adjustment"))]
#[node_macro::node(category("Raster: Adjustment"), properties("channel_mixer_properties"))]
async fn channel_mixer<F: 'n + Send, T: Adjust<Color>>(
#[implementations(
(),
@ -1555,7 +1555,7 @@ async fn posterize<F: 'n + Send, T: Adjust<Color>>(
//
// Algorithm based on:
// https://geraldbakker.nl/psnumbers/exposure.html
#[node_macro::node(category("Raster: Adjustment"))]
#[node_macro::node(category("Raster: Adjustment"), properties("exposure_properties"))]
async fn exposure<F: 'n + Send, T: Adjust<Color>>(
#[implementations(
(),
@ -1683,7 +1683,13 @@ mod index_node {
use crate::raster::{Color, ImageFrame};
#[node_macro::node(category(""))]
pub fn index<T: Default + Clone>(_: (), #[implementations(Vec<ImageFrame<Color>>, Vec<Color>)] input: Vec<T>, index: u32) -> T {
pub fn index<T: Default + Clone>(
_: (),
#[implementations(Vec<ImageFrame<Color>>, Vec<Color>)]
#[widget(ParsedWidgetOverride::Hidden)]
input: Vec<T>,
index: u32,
) -> T {
if (index as usize) < input.len() {
input[index as usize].clone()
} else {

View File

@ -29,27 +29,39 @@ pub mod types {
pub type Resolution = glam::UVec2;
}
// Translation struct between macro and definition
#[derive(Clone)]
pub struct NodeMetadata {
pub display_name: &'static str,
pub category: Option<&'static str>,
pub fields: Vec<FieldMetadata>,
pub description: &'static str,
pub properties: Option<&'static str>,
}
// Translation struct between macro and definition
#[derive(Clone, Debug)]
pub struct FieldMetadata {
pub name: &'static str,
pub description: &'static str,
pub exposed: bool,
pub value_source: ValueSource,
pub widget_override: RegistryWidgetOverride,
pub value_source: RegistryValueSource,
pub number_min: Option<f64>,
pub number_max: Option<f64>,
pub number_mode_range: Option<(f64, f64)>,
}
#[derive(Clone, Debug)]
pub enum ValueSource {
pub enum RegistryWidgetOverride {
None,
Hidden,
String(&'static str),
Custom(&'static str),
}
#[derive(Clone, Debug)]
pub enum RegistryValueSource {
None,
Default(&'static str),
Scope(&'static str),

View File

@ -55,7 +55,7 @@ fn ellipse<F: 'n + Send>(#[implementations((), Footprint)] _footprint: F, _prima
ellipse
}
#[node_macro::node(category("Vector: Shape"))]
#[node_macro::node(category("Vector: Shape"), properties("rectangle_properties"))]
fn rectangle<F: 'n + Send, T: CornerRadius>(
#[implementations((), Footprint)] _footprint: F,
_primary: (),

View File

@ -50,14 +50,15 @@ async fn assign_colors<F: 'n + Send, T: VectorIterMut>(
Footprint -> GraphicGroup,
Footprint -> VectorData,
)]
#[widget(ParsedWidgetOverride::Hidden)]
vector_group: impl Node<F, Output = T>,
#[default(true)] fill: bool,
stroke: bool,
gradient: GradientStops,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_gradient")] gradient: GradientStops,
reverse: bool,
randomize: bool,
seed: SeedValue,
repeat_every: u32,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_randomize")] randomize: bool,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_seed")] seed: SeedValue,
#[widget(ParsedWidgetOverride::Custom = "assign_colors_repeat_every")] repeat_every: u32,
) -> T {
let mut input = vector_group.eval(footprint).await;
let length = input.vector_iter_mut().count();
@ -89,7 +90,7 @@ async fn assign_colors<F: 'n + Send, T: VectorIterMut>(
input
}
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))]
async fn fill<F: 'n + Send, FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
#[implementations(
(),
@ -161,7 +162,7 @@ async fn fill<F: 'n + Send, FillTy: Into<Fill> + 'n + Send, TargetTy: VectorIter
target
}
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector))]
#[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("stroke_properties"))]
async fn stroke<F: 'n + Send, ColorTy: Into<Option<Color>> + 'n + Send, TargetTy: VectorIterMut + 'n + Send>(
#[implementations(
(),
@ -430,7 +431,7 @@ async fn bounding_box<F: 'n + Send>(
result
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
#[node_macro::node(category("Vector"), path(graphene_core::vector), properties("offset_path_properties"))]
async fn offset_path<F: 'n + Send>(
#[implementations(
(),

View File

@ -762,7 +762,7 @@ pub struct NodeNetwork {
/// Each export is a reference to a node within this network, paired with its output index, that is the source of the network's exported data.
#[cfg_attr(feature = "serde", serde(alias = "outputs", deserialize_with = "deserialize_exports"))] // TODO: Eventually remove this alias document upgrade code
pub exports: Vec<NodeInput>,
/// TODO: Instead of storing import types in each NodeInput::Network connection, the types are stored here. This is similar to how types need to be defined for parameters when creating a function in Rust.
// TODO: Instead of storing import types in each NodeInput::Network connection, the types are stored here. This is similar to how types need to be defined for parameters when creating a function in Rust.
// pub import_types: Vec<Type>,
/// The list of all nodes in this network.
#[cfg_attr(

View File

@ -430,24 +430,11 @@ generate_imaginate_node! {
tiling: Tiling: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct ImageFrameNode<P, Transform> {
transform: Transform,
_p: PhantomData<P>,
}
#[node_macro::old_node_fn(ImageFrameNode<_P>)]
fn image_frame<_P: Pixel>(image: Image<_P>, transform: DAffine2) -> ImageFrame<_P> {
ImageFrame {
image,
transform,
alpha_blending: AlphaBlending::default(),
}
}
#[node_macro::node(category("Raster: Generator"))]
#[allow(clippy::too_many_arguments)]
fn noise_pattern(
footprint: Footprint,
_primary: (),
clip: bool,
seed: u32,
scale: f64,

View File

@ -55,12 +55,22 @@ async fn draw_image_frame(_: (), image: ImageFrame<graphene_core::raster::SRGBA8
#[node_macro::node(category("Network"))]
async fn load_resource<'a: 'n>(_: (), _primary: (), #[scope("editor-api")] editor: &'a WasmEditorApi, url: String) -> Arc<[u8]> {
editor.application_io.as_ref().unwrap().load_resource(url).unwrap().await.unwrap()
let Some(api) = editor.application_io.as_ref() else {
return Arc::from(include_bytes!("../../graph-craft/src/null.png").to_vec());
};
let Ok(data) = api.load_resource(url) else {
return Arc::from(include_bytes!("../../graph-craft/src/null.png").to_vec());
};
let Ok(data) = data.await else {
return Arc::from(include_bytes!("../../graph-craft/src/null.png").to_vec());
};
data
}
#[node_macro::node(category("Raster"))]
fn decode_image(_: (), data: Arc<[u8]>) -> ImageFrame<Color> {
let image = image::load_from_memory(data.as_ref()).expect("Failed to decode image");
let Some(image) = image::load_from_memory(data.as_ref()).ok() else { return ImageFrame::default() };
let image = image.to_rgba32f();
let image = ImageFrame {
image: Image {

View File

@ -361,7 +361,6 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Footprint, fn_params: [Footprint => Option<WgpuSurface>]),
async_node!(graphene_core::memo::ImpureMemoNode<_, _, _>, input: Footprint, fn_params: [Footprint => TextureFrame]),
register_node!(graphene_core::structural::ConsNode<_, _>, input: Image<Color>, params: [&str]),
register_node!(graphene_std::raster::ImageFrameNode<_, _>, input: Image<Color>, params: [DAffine2]),
];
let mut map: HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> = HashMap::new();
for (id, entry) in graphene_core::registry::NODE_REGISTRY.lock().unwrap().iter() {

View File

@ -84,21 +84,36 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
ParsedField::Regular { ty, .. } => ty.clone(),
ParsedField::Node { output_type, input_type, .. } => match parsed.is_async {
true => parse_quote!(&'n impl #graphene_core::Node<'n, #input_type, Output: core::future::Future<Output=#output_type> + #graphene_core::WasmNotSend>),
false => parse_quote!(&'n impl #graphene_core::Node<'n, #input_type, Output = #output_type>),
},
})
.collect();
let widget_override: Vec<_> = fields
.iter()
.map(|field| {
let parsed_widget_override = match field {
ParsedField::Regular { widget_override, .. } => widget_override,
ParsedField::Node { widget_override, .. } => widget_override,
};
match parsed_widget_override {
ParsedWidgetOverride::None => quote!(RegistryWidgetOverride::None),
ParsedWidgetOverride::Hidden => quote!(RegistryWidgetOverride::Hidden),
ParsedWidgetOverride::String(lit_str) => quote!(RegistryWidgetOverride::String(#lit_str)),
ParsedWidgetOverride::Custom(lit_str) => quote!(RegistryWidgetOverride::Custom(#lit_str)),
}
})
.collect();
let value_sources: Vec<_> = fields
.iter()
.map(|field| match field {
ParsedField::Regular { value_source, .. } => match value_source {
ValueSource::Default(data) => quote!(ValueSource::Default(stringify!(#data))),
ValueSource::Scope(data) => quote!(ValueSource::Scope(#data)),
_ => quote!(ValueSource::None),
ParsedValueSource::Default(data) => quote!(RegistryValueSource::Default(stringify!(#data))),
ParsedValueSource::Scope(data) => quote!(RegistryValueSource::Scope(#data)),
_ => quote!(RegistryValueSource::None),
},
_ => quote!(ValueSource::None),
_ => quote!(RegistryValueSource::None),
})
.collect();
@ -213,6 +228,8 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
let register_node_impl = generate_register_node_impl(parsed, &field_names, &struct_name, &identifier)?;
let import_name = format_ident!("_IMPORT_STUB_{}", mod_name.to_string().to_case(Case::UpperSnake));
let properties = &attributes.properties_string.as_ref().map(|value| quote!(Some(#value))).unwrap_or(quote!(None));
Ok(quote! {
/// Underlying implementation for [#struct_name]
#[inline]
@ -235,7 +252,7 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
use gcore::{Node, NodeIOTypes, concrete, fn_type, future, ProtoNodeIdentifier, WasmNotSync, NodeIO};
use gcore::value::ClonedNode;
use gcore::ops::TypeNode;
use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, DynFuture, TypeErasedBox, PanicNode, ValueSource};
use gcore::registry::{NodeMetadata, FieldMetadata, NODE_REGISTRY, NODE_METADATA, DynAnyNode, DowncastBothNode, DynFuture, TypeErasedBox, PanicNode, RegistryValueSource, RegistryWidgetOverride};
use gcore::ctor::ctor;
// Use the types specified in the implementation
@ -266,10 +283,12 @@ pub(crate) fn generate_node_code(parsed: &ParsedNodeFn) -> syn::Result<TokenStre
display_name: #display_name,
category: #category,
description: #description,
properties: #properties,
fields: vec![
#(
FieldMetadata {
name: #input_names,
widget_override: #widget_override,
description: #input_descriptions,
exposed: #exposed,
value_source: #value_sources,

View File

@ -39,26 +39,69 @@ pub(crate) struct NodeFnAttributes {
pub(crate) display_name: Option<LitStr>,
pub(crate) path: Option<Path>,
pub(crate) skip_impl: bool,
pub(crate) properties_string: Option<LitStr>,
// Add more attributes as needed
}
#[derive(Debug, Default)]
pub enum ValueSource {
pub enum ParsedValueSource {
#[default]
None,
Default(TokenStream2),
Scope(LitStr),
}
// #[widget(ParsedWidgetOverride::Hidden)]
// #[widget(ParsedWidgetOverride::String = "Some string")]
// #[widget(ParsedWidgetOverride::Custom = "Custom string")]
#[derive(Debug, Default)]
pub enum ParsedWidgetOverride {
#[default]
None,
Hidden,
String(LitStr),
Custom(LitStr),
}
impl Parse for ParsedWidgetOverride {
fn parse(input: ParseStream) -> syn::Result<Self> {
// Parse the full path (e.g., ParsedWidgetOverride::Hidden)
let path: Path = input.parse()?;
// Ensure the path starts with `ParsedWidgetOverride`
if path.segments.len() == 2 && path.segments[0].ident == "ParsedWidgetOverride" {
let variant = &path.segments[1].ident;
match variant.to_string().as_str() {
"Hidden" => Ok(ParsedWidgetOverride::Hidden),
"String" => {
input.parse::<syn::Token![=]>()?;
let lit: LitStr = input.parse()?;
Ok(ParsedWidgetOverride::String(lit))
}
"Custom" => {
input.parse::<syn::Token![=]>()?;
let lit: LitStr = input.parse()?;
Ok(ParsedWidgetOverride::Custom(lit))
}
_ => Err(syn::Error::new(variant.span(), "Unknown ParsedWidgetOverride variant")),
}
} else {
Err(syn::Error::new(input.span(), "Expected ParsedWidgetOverride::<variant>"))
}
}
}
#[derive(Debug)]
pub(crate) enum ParsedField {
Regular {
pat_ident: PatIdent,
name: Option<LitStr>,
description: String,
widget_override: ParsedWidgetOverride,
ty: Type,
exposed: bool,
value_source: ValueSource,
value_source: ParsedValueSource,
number_min: Option<LitFloat>,
number_max: Option<LitFloat>,
number_mode_range: Option<ExprTuple>,
@ -68,6 +111,7 @@ pub(crate) enum ParsedField {
pat_ident: PatIdent,
name: Option<LitStr>,
description: String,
widget_override: ParsedWidgetOverride,
input_type: Type,
output_type: Type,
implementations: Punctuated<Implementation, Comma>,
@ -126,6 +170,7 @@ impl Parse for NodeFnAttributes {
let mut display_name = None;
let mut path = None;
let mut skip_impl = false;
let mut properties_string = None;
let content = input;
// let content;
@ -165,6 +210,16 @@ impl Parse for NodeFnAttributes {
}
skip_impl = true;
}
Meta::List(meta) if meta.path.is_ident("properties") => {
if properties_string.is_some() {
return Err(Error::new_spanned(path, "Multiple 'properties_string' attributes are not allowed"));
}
let parsed_properties_string: LitStr = meta
.parse_args()
.map_err(|_| Error::new_spanned(meta, "Expected a string for 'properties', e.g., name(\"channel_mixer_properties\")"))?;
properties_string = Some(parsed_properties_string);
}
_ => {
return Err(Error::new_spanned(
meta,
@ -187,6 +242,7 @@ impl Parse for NodeFnAttributes {
display_name,
path,
skip_impl,
properties_string,
})
}
}
@ -343,13 +399,21 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
.map(|attr| attr.parse_args().map_err(|e| Error::new_spanned(attr, format!("Invalid `name` value for argument '{}': {}", ident, e))))
.transpose()?;
let widget_override = extract_attribute(attrs, "widget")
.map(|attr| {
attr.parse_args()
.map_err(|e| Error::new_spanned(attr, format!("Invalid `widget override` value for argument '{}': {}", ident, e)))
})
.transpose()?
.unwrap_or_default();
let exposed = extract_attribute(attrs, "expose").is_some();
let value_source = match (default_value, scope) {
(Some(_), Some(_)) => return Err(Error::new_spanned(&pat_ident, "Cannot have both `default` and `scope` attributes")),
(Some(default_value), _) => ValueSource::Default(default_value),
(_, Some(scope)) => ValueSource::Scope(scope),
_ => ValueSource::None,
(Some(default_value), _) => ParsedValueSource::Default(default_value),
(_, Some(scope)) => ParsedValueSource::Scope(scope),
_ => ParsedValueSource::None,
};
let number_min = extract_attribute(attrs, "min")
@ -405,7 +469,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
let (input_type, output_type) = node_input_type
.zip(node_output_type)
.ok_or_else(|| Error::new_spanned(&ty, "Invalid Node type. Expected `impl Node<Input, Output = OutputType>`"))?;
if !matches!(&value_source, ValueSource::None) {
if !matches!(&value_source, ParsedValueSource::None) {
return Err(Error::new_spanned(&ty, "No default values for `impl Node` allowed"));
}
let implementations = extract_attribute(attrs, "implementations")
@ -417,6 +481,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
pat_ident,
name,
description,
widget_override,
input_type,
output_type,
implementations,
@ -430,6 +495,7 @@ fn parse_field(pat_ident: PatIdent, ty: Type, attrs: &[Attribute]) -> syn::Resul
pat_ident,
name,
description,
widget_override,
exposed,
number_min,
number_max,
@ -550,11 +616,11 @@ mod tests {
assert_eq!(p_name, e_name);
assert_eq!(p_exp, e_exp);
match (p_default, e_default) {
(ValueSource::None, ValueSource::None) => {}
(ValueSource::Default(p), ValueSource::Default(e)) => {
(ParsedValueSource::None, ParsedValueSource::None) => {}
(ParsedValueSource::Default(p), ParsedValueSource::Default(e)) => {
assert_eq!(p.to_token_stream().to_string(), e.to_token_stream().to_string());
}
(ValueSource::Scope(p), ValueSource::Scope(e)) => {
(ParsedValueSource::Scope(p), ParsedValueSource::Scope(e)) => {
assert_eq!(p.value(), e.value());
}
_ => panic!("Mismatched default values"),
@ -602,6 +668,7 @@ mod tests {
display_name: None,
path: Some(parse_quote!(graphene_core::TestNode)),
skip_impl: true,
properties_string: None,
},
fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()),
@ -619,9 +686,10 @@ mod tests {
pat_ident: pat_ident("b"),
name: None,
description: String::new(),
widget_override: ParsedWidgetOverride::None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::None,
value_source: ParsedValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
@ -655,6 +723,7 @@ mod tests {
display_name: None,
path: None,
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("transform", Span::call_site()),
struct_name: Ident::new("Transform", Span::call_site()),
@ -673,6 +742,7 @@ mod tests {
pat_ident: pat_ident("transform_target"),
name: None,
description: String::new(),
widget_override: ParsedWidgetOverride::None,
input_type: parse_quote!(Footprint),
output_type: parse_quote!(T),
implementations: Punctuated::new(),
@ -681,9 +751,10 @@ mod tests {
pat_ident: pat_ident("translate"),
name: None,
description: String::new(),
widget_override: ParsedWidgetOverride::None,
ty: parse_quote!(DVec2),
exposed: false,
value_source: ValueSource::None,
value_source: ParsedValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
@ -715,6 +786,7 @@ mod tests {
display_name: None,
path: None,
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("circle", Span::call_site()),
struct_name: Ident::new("Circle", Span::call_site()),
@ -732,9 +804,10 @@ mod tests {
pat_ident: pat_ident("radius"),
name: None,
description: String::new(),
widget_override: ParsedWidgetOverride::None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::Default(quote!(50.)),
value_source: ParsedValueSource::Default(quote!(50.)),
number_min: None,
number_max: None,
number_mode_range: None,
@ -764,6 +837,7 @@ mod tests {
display_name: None,
path: None,
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("levels", Span::call_site()),
struct_name: Ident::new("Levels", Span::call_site()),
@ -781,9 +855,10 @@ mod tests {
pat_ident: pat_ident("shadows"),
name: None,
description: String::new(),
widget_override: ParsedWidgetOverride::None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::None,
value_source: ParsedValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
@ -825,6 +900,7 @@ mod tests {
display_name: None,
path: Some(parse_quote!(graphene_core::TestNode)),
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("add", Span::call_site()),
struct_name: Ident::new("Add", Span::call_site()),
@ -842,9 +918,10 @@ mod tests {
pat_ident: pat_ident("b"),
name: None,
description: String::from("b"),
widget_override: ParsedWidgetOverride::None,
ty: parse_quote!(f64),
exposed: false,
value_source: ValueSource::None,
value_source: ParsedValueSource::None,
number_min: Some(parse_quote!(-500.)),
number_max: Some(parse_quote!(500.)),
number_mode_range: Some(parse_quote!((0., 100.))),
@ -874,6 +951,7 @@ mod tests {
display_name: None,
path: None,
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("load_image", Span::call_site()),
struct_name: Ident::new("LoadImage", Span::call_site()),
@ -892,8 +970,9 @@ mod tests {
name: None,
ty: parse_quote!(String),
description: String::new(),
widget_override: ParsedWidgetOverride::None,
exposed: true,
value_source: ValueSource::None,
value_source: ParsedValueSource::None,
number_min: None,
number_max: None,
number_mode_range: None,
@ -923,6 +1002,7 @@ mod tests {
display_name: Some(parse_quote!("CustomNode2")),
path: None,
skip_impl: false,
properties_string: None,
},
fn_name: Ident::new("custom_node", Span::call_site()),
struct_name: Ident::new("CustomNode", Span::call_site()),