mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 04:08:12 +08:00
Add a node insertion button and layer renaming from the Properties panel (#2072)
* Add node button * Improve css a bit * Add layer renaming to the Properties panel and move New Layer to that, plus add unpinning to properties sections * Add tooltip * Re-add layer itself in listing * Final code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
3c839ffd2b
commit
5aa6716910
@@ -253,11 +253,6 @@ pub enum FrontendMessage {
|
|||||||
#[serde(rename = "openDocuments")]
|
#[serde(rename = "openDocuments")]
|
||||||
open_documents: Vec<FrontendDocumentDetails>,
|
open_documents: Vec<FrontendDocumentDetails>,
|
||||||
},
|
},
|
||||||
UpdatePropertyPanelOptionsLayout {
|
|
||||||
#[serde(rename = "layoutTarget")]
|
|
||||||
layout_target: LayoutTarget,
|
|
||||||
diff: Vec<WidgetDiff>,
|
|
||||||
},
|
|
||||||
UpdatePropertyPanelSectionsLayout {
|
UpdatePropertyPanelSectionsLayout {
|
||||||
#[serde(rename = "layoutTarget")]
|
#[serde(rename = "layoutTarget")]
|
||||||
layout_target: LayoutTarget,
|
layout_target: LayoutTarget,
|
||||||
|
|||||||
@@ -214,6 +214,17 @@ impl LayoutMessageHandler {
|
|||||||
|
|
||||||
responses.add(callback_message);
|
responses.add(callback_message);
|
||||||
}
|
}
|
||||||
|
Widget::NodeCatalog(node_type_input) => match action {
|
||||||
|
WidgetValueAction::Commit => {
|
||||||
|
let callback_message = (node_type_input.on_commit.callback)(&());
|
||||||
|
responses.add(callback_message);
|
||||||
|
}
|
||||||
|
WidgetValueAction::Update => {
|
||||||
|
let value = value.as_str().expect("NodeCatalog update was not of type String").to_string();
|
||||||
|
let callback_message = (node_type_input.on_update.callback)(&value);
|
||||||
|
responses.add(callback_message);
|
||||||
|
}
|
||||||
|
},
|
||||||
Widget::NumberInput(number_input) => match action {
|
Widget::NumberInput(number_input) => match action {
|
||||||
WidgetValueAction::Commit => {
|
WidgetValueAction::Commit => {
|
||||||
let callback_message = (number_input.on_commit.callback)(&());
|
let callback_message = (number_input.on_commit.callback)(&());
|
||||||
@@ -394,7 +405,6 @@ impl LayoutMessageHandler {
|
|||||||
LayoutTarget::LayersPanelOptions => FrontendMessage::UpdateLayersPanelOptionsLayout { layout_target, diff },
|
LayoutTarget::LayersPanelOptions => FrontendMessage::UpdateLayersPanelOptionsLayout { layout_target, diff },
|
||||||
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
|
LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"),
|
||||||
LayoutTarget::NodeGraphBar => FrontendMessage::UpdateNodeGraphBarLayout { layout_target, diff },
|
LayoutTarget::NodeGraphBar => FrontendMessage::UpdateNodeGraphBarLayout { layout_target, diff },
|
||||||
LayoutTarget::PropertiesOptions => FrontendMessage::UpdatePropertyPanelOptionsLayout { layout_target, diff },
|
|
||||||
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff },
|
LayoutTarget::PropertiesSections => FrontendMessage::UpdatePropertyPanelSectionsLayout { layout_target, diff },
|
||||||
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff },
|
LayoutTarget::ToolOptions => FrontendMessage::UpdateToolOptionsLayout { layout_target, diff },
|
||||||
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff },
|
LayoutTarget::ToolShelf => FrontendMessage::UpdateToolShelfLayout { layout_target, diff },
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ pub enum LayoutTarget {
|
|||||||
MenuBar,
|
MenuBar,
|
||||||
/// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons.
|
/// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons.
|
||||||
NodeGraphBar,
|
NodeGraphBar,
|
||||||
/// The bar at the top of the Properties panel containing the layer name and icon.
|
|
||||||
PropertiesOptions,
|
|
||||||
/// The body of the Properties panel containing many collapsable sections.
|
/// The body of the Properties panel containing many collapsable sections.
|
||||||
PropertiesSections,
|
PropertiesSections,
|
||||||
/// The bar directly above the canvas, left-aligned and to the right of the document mode dropdown.
|
/// The bar directly above the canvas, left-aligned and to the right of the document mode dropdown.
|
||||||
@@ -303,7 +301,7 @@ pub enum LayoutGroup {
|
|||||||
},
|
},
|
||||||
// TODO: Move this from being a child of `enum LayoutGroup` to being a child of `enum Layout`
|
// TODO: Move this from being a child of `enum LayoutGroup` to being a child of `enum Layout`
|
||||||
#[serde(rename = "section")]
|
#[serde(rename = "section")]
|
||||||
Section { name: String, visible: bool, id: u64, layout: SubLayout },
|
Section { name: String, visible: bool, pinned: bool, id: u64, layout: SubLayout },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LayoutGroup {
|
impl Default for LayoutGroup {
|
||||||
@@ -344,7 +342,7 @@ impl LayoutGroup {
|
|||||||
Widget::TextInput(x) => &mut x.tooltip,
|
Widget::TextInput(x) => &mut x.tooltip,
|
||||||
Widget::TextLabel(x) => &mut x.tooltip,
|
Widget::TextLabel(x) => &mut x.tooltip,
|
||||||
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip,
|
Widget::BreadcrumbTrailButtons(x) => &mut x.tooltip,
|
||||||
Widget::InvisibleStandinInput(_) | Widget::PivotInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) => continue,
|
Widget::InvisibleStandinInput(_) | Widget::PivotInput(_) | Widget::RadioInput(_) | Widget::Separator(_) | Widget::WorkingColorsInput(_) | Widget::NodeCatalog(_) => continue,
|
||||||
};
|
};
|
||||||
if val.is_empty() {
|
if val.is_empty() {
|
||||||
val.clone_from(&tooltip);
|
val.clone_from(&tooltip);
|
||||||
@@ -385,22 +383,25 @@ impl LayoutGroup {
|
|||||||
Self::Section {
|
Self::Section {
|
||||||
name: current_name,
|
name: current_name,
|
||||||
visible: current_visible,
|
visible: current_visible,
|
||||||
|
pinned: current_pinned,
|
||||||
id: current_id,
|
id: current_id,
|
||||||
layout: current_layout,
|
layout: current_layout,
|
||||||
},
|
},
|
||||||
Self::Section {
|
Self::Section {
|
||||||
name: new_name,
|
name: new_name,
|
||||||
visible: new_visible,
|
visible: new_visible,
|
||||||
|
pinned: new_pinned,
|
||||||
id: new_id,
|
id: new_id,
|
||||||
layout: new_layout,
|
layout: new_layout,
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
// Resend the entire panel if the lengths, names, visibility, or node IDs are different
|
// Resend the entire panel if the lengths, names, visibility, or node IDs are different
|
||||||
// TODO: Diff insersion and deletion of items
|
// TODO: Diff insersion and deletion of items
|
||||||
if current_layout.len() != new_layout.len() || *current_name != new_name || *current_visible != new_visible || *current_id != new_id {
|
if current_layout.len() != new_layout.len() || *current_name != new_name || *current_visible != new_visible || *current_pinned != new_pinned || *current_id != new_id {
|
||||||
// Update self to reflect new changes
|
// Update self to reflect new changes
|
||||||
current_name.clone_from(&new_name);
|
current_name.clone_from(&new_name);
|
||||||
*current_visible = new_visible;
|
*current_visible = new_visible;
|
||||||
|
*current_pinned = new_pinned;
|
||||||
*current_id = new_id;
|
*current_id = new_id;
|
||||||
current_layout.clone_from(&new_layout);
|
current_layout.clone_from(&new_layout);
|
||||||
|
|
||||||
@@ -408,6 +409,7 @@ impl LayoutGroup {
|
|||||||
let new_value = DiffUpdate::LayoutGroup(Self::Section {
|
let new_value = DiffUpdate::LayoutGroup(Self::Section {
|
||||||
name: new_name,
|
name: new_name,
|
||||||
visible: new_visible,
|
visible: new_visible,
|
||||||
|
pinned: new_pinned,
|
||||||
id: new_id,
|
id: new_id,
|
||||||
layout: new_layout,
|
layout: new_layout,
|
||||||
});
|
});
|
||||||
@@ -504,6 +506,7 @@ pub enum Widget {
|
|||||||
IconLabel(IconLabel),
|
IconLabel(IconLabel),
|
||||||
ImageLabel(ImageLabel),
|
ImageLabel(ImageLabel),
|
||||||
InvisibleStandinInput(InvisibleStandinInput),
|
InvisibleStandinInput(InvisibleStandinInput),
|
||||||
|
NodeCatalog(NodeCatalog),
|
||||||
NumberInput(NumberInput),
|
NumberInput(NumberInput),
|
||||||
ParameterExposeButton(ParameterExposeButton),
|
ParameterExposeButton(ParameterExposeButton),
|
||||||
PivotInput(PivotInput),
|
PivotInput(PivotInput),
|
||||||
@@ -580,6 +583,7 @@ impl DiffUpdate {
|
|||||||
| Widget::ImageLabel(_)
|
| Widget::ImageLabel(_)
|
||||||
| Widget::CurveInput(_)
|
| Widget::CurveInput(_)
|
||||||
| Widget::InvisibleStandinInput(_)
|
| Widget::InvisibleStandinInput(_)
|
||||||
|
| Widget::NodeCatalog(_)
|
||||||
| Widget::PivotInput(_)
|
| Widget::PivotInput(_)
|
||||||
| Widget::RadioInput(_)
|
| Widget::RadioInput(_)
|
||||||
| Widget::Separator(_)
|
| Widget::Separator(_)
|
||||||
|
|||||||
@@ -274,6 +274,21 @@ pub enum NumberInputMode {
|
|||||||
Range,
|
Range,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, WidgetBuilder, specta::Type)]
|
||||||
|
#[derivative(Debug, PartialEq, Default)]
|
||||||
|
pub struct NodeCatalog {
|
||||||
|
pub disabled: bool,
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
#[serde(skip)]
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
pub on_update: WidgetCallback<String>,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
|
pub on_commit: WidgetCallback<()>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
|
#[derive(Clone, Default, Derivative, serde::Serialize, serde::Deserialize, WidgetBuilder, specta::Type)]
|
||||||
#[derivative(Debug, PartialEq)]
|
#[derivative(Debug, PartialEq)]
|
||||||
pub struct RadioInput {
|
pub struct RadioInput {
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ pub enum DocumentMessage {
|
|||||||
},
|
},
|
||||||
CreateEmptyFolder,
|
CreateEmptyFolder,
|
||||||
DebugPrintDocument,
|
DebugPrintDocument,
|
||||||
|
DeleteNode {
|
||||||
|
node_id: NodeId,
|
||||||
|
},
|
||||||
DeleteSelectedLayers,
|
DeleteSelectedLayers,
|
||||||
DeselectAllLayers,
|
DeselectAllLayers,
|
||||||
DocumentHistoryBackward,
|
DocumentHistoryBackward,
|
||||||
@@ -128,6 +131,10 @@ pub enum DocumentMessage {
|
|||||||
SetBlendModeForSelectedLayers {
|
SetBlendModeForSelectedLayers {
|
||||||
blend_mode: BlendMode,
|
blend_mode: BlendMode,
|
||||||
},
|
},
|
||||||
|
SetNodePinned {
|
||||||
|
node_id: NodeId,
|
||||||
|
pinned: bool,
|
||||||
|
},
|
||||||
SetOpacityForSelectedLayers {
|
SetOpacityForSelectedLayers {
|
||||||
opacity: f64,
|
opacity: f64,
|
||||||
},
|
},
|
||||||
@@ -142,6 +149,10 @@ pub enum DocumentMessage {
|
|||||||
closure: Option<for<'a> fn(&'a mut SnappingState) -> &'a mut bool>,
|
closure: Option<for<'a> fn(&'a mut SnappingState) -> &'a mut bool>,
|
||||||
snapping_state: bool,
|
snapping_state: bool,
|
||||||
},
|
},
|
||||||
|
SetToNodeOrLayer {
|
||||||
|
node_id: NodeId,
|
||||||
|
is_layer: bool,
|
||||||
|
},
|
||||||
SetViewMode {
|
SetViewMode {
|
||||||
view_mode: ViewMode,
|
view_mode: ViewMode,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||||||
DocumentMessage::PropertiesPanel(message) => {
|
DocumentMessage::PropertiesPanel(message) => {
|
||||||
let properties_panel_message_handler_data = PropertiesPanelMessageHandlerData {
|
let properties_panel_message_handler_data = PropertiesPanelMessageHandlerData {
|
||||||
network_interface: &self.network_interface,
|
network_interface: &self.network_interface,
|
||||||
selection_path: &self.selection_network_path,
|
selection_network_path: &self.selection_network_path,
|
||||||
document_name: self.name.as_str(),
|
document_name: self.name.as_str(),
|
||||||
executor,
|
executor,
|
||||||
};
|
};
|
||||||
@@ -359,6 +359,17 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||||||
DocumentMessage::DebugPrintDocument => {
|
DocumentMessage::DebugPrintDocument => {
|
||||||
info!("{:#?}", self.network_interface);
|
info!("{:#?}", self.network_interface);
|
||||||
}
|
}
|
||||||
|
DocumentMessage::DeleteNode { node_id } => {
|
||||||
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
|
|
||||||
|
responses.add(NodeGraphMessage::DeleteNodes {
|
||||||
|
node_ids: vec![node_id],
|
||||||
|
delete_children: true,
|
||||||
|
});
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
responses.add(NodeGraphMessage::SelectedNodesUpdated);
|
||||||
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
|
}
|
||||||
DocumentMessage::DeleteSelectedLayers => {
|
DocumentMessage::DeleteSelectedLayers => {
|
||||||
responses.add(NodeGraphMessage::DeleteSelectedNodes { delete_children: true });
|
responses.add(NodeGraphMessage::DeleteSelectedNodes { delete_children: true });
|
||||||
}
|
}
|
||||||
@@ -1008,6 +1019,13 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||||||
responses.add(GraphOperationMessage::BlendModeSet { layer, blend_mode });
|
responses.add(GraphOperationMessage::BlendModeSet { layer, blend_mode });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DocumentMessage::SetNodePinned { node_id, pinned } => {
|
||||||
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
|
responses.add(NodeGraphMessage::SetPinned { node_id, pinned });
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
responses.add(NodeGraphMessage::SelectedNodesUpdated);
|
||||||
|
responses.add(NodeGraphMessage::SendGraph);
|
||||||
|
}
|
||||||
DocumentMessage::SetOpacityForSelectedLayers { opacity } => {
|
DocumentMessage::SetOpacityForSelectedLayers { opacity } => {
|
||||||
let opacity = opacity.clamp(0., 1.);
|
let opacity = opacity.clamp(0., 1.);
|
||||||
for layer in self.network_interface.selected_nodes(&[]).unwrap().selected_layers_except_artboards(&self.network_interface) {
|
for layer in self.network_interface.selected_nodes(&[]).unwrap().selected_layers_except_artboards(&self.network_interface) {
|
||||||
@@ -1027,6 +1045,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||||||
*closure(&mut self.snapping_state) = snapping_state;
|
*closure(&mut self.snapping_state) = snapping_state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DocumentMessage::SetToNodeOrLayer { node_id, is_layer } => {
|
||||||
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
|
responses.add(NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer });
|
||||||
|
}
|
||||||
DocumentMessage::SetViewMode { view_mode } => {
|
DocumentMessage::SetViewMode { view_mode } => {
|
||||||
self.view_mode = view_mode;
|
self.view_mode = view_mode;
|
||||||
responses.add_front(NodeGraphMessage::RunDocumentGraph);
|
responses.add_front(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
|||||||
@@ -268,17 +268,20 @@ impl<'a> ModifyInputsContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a new node if the node does not exist and update its inputs
|
// Create a new node if the node does not exist and update its inputs
|
||||||
existing_node_id.or_else(|| {
|
existing_node_id.or_else(|| self.create_node(reference))
|
||||||
let output_layer = self.get_output_layer()?;
|
}
|
||||||
let Some(node_definition) = resolve_document_node_type(reference) else {
|
|
||||||
log::error!("Node type {} does not exist in ModifyInputsContext::existing_node_id", reference);
|
/// Create a new node inside the layer
|
||||||
return None;
|
pub fn create_node(&mut self, reference: &str) -> Option<NodeId> {
|
||||||
};
|
let output_layer = self.get_output_layer()?;
|
||||||
let node_id = NodeId::new();
|
let Some(node_definition) = resolve_document_node_type(reference) else {
|
||||||
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
|
log::error!("Node type {} does not exist in ModifyInputsContext::existing_node_id", reference);
|
||||||
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[]);
|
return None;
|
||||||
Some(node_id)
|
};
|
||||||
})
|
let node_id = NodeId::new();
|
||||||
|
self.network_interface.insert_node(node_id, node_definition.default_node_template(), &[]);
|
||||||
|
self.network_interface.move_node_to_chain_start(&node_id, output_layer, &[]);
|
||||||
|
Some(node_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill_set(&mut self, fill: Fill) {
|
pub fn fill_set(&mut self, fill: Fill) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ pub struct NodePropertiesContext<'a> {
|
|||||||
pub executor: &'a mut NodeGraphExecutor,
|
pub executor: &'a mut NodeGraphExecutor,
|
||||||
pub network_interface: &'a NodeNetworkInterface,
|
pub network_interface: &'a NodeNetworkInterface,
|
||||||
pub selection_network_path: &'a [NodeId],
|
pub selection_network_path: &'a [NodeId],
|
||||||
|
pub document_name: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Acts as a description for a [DocumentNode] before it gets instantiated as one.
|
/// Acts as a description for a [DocumentNode] before it gets instantiated as one.
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ pub enum NodeGraphMessage {
|
|||||||
Init,
|
Init,
|
||||||
SelectedNodesUpdated,
|
SelectedNodesUpdated,
|
||||||
Copy,
|
Copy,
|
||||||
|
CreateNodeInLayerNoTransaction {
|
||||||
|
node_type: String,
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
},
|
||||||
|
CreateNodeInLayerWithTransaction {
|
||||||
|
node_type: String,
|
||||||
|
layer: LayerNodeIdentifier,
|
||||||
|
},
|
||||||
CreateNodeFromContextMenu {
|
CreateNodeFromContextMenu {
|
||||||
node_id: Option<NodeId>,
|
node_id: Option<NodeId>,
|
||||||
node_type: String,
|
node_type: String,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use super::utility_types::{BoxSelection, ContextMenuInformation, DragStart, Fron
|
|||||||
use super::{document_node_definitions, node_properties};
|
use super::{document_node_definitions, node_properties};
|
||||||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
|
use crate::messages::portfolio::document::graph_operation::utility_types::ModifyInputsContext;
|
||||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext;
|
use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext;
|
||||||
use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType};
|
use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType};
|
||||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
@@ -13,7 +14,7 @@ use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
|
|||||||
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
|
use graph_craft::document::{DocumentNodeImplementation, NodeId, NodeInput};
|
||||||
use graph_craft::proto::GraphErrors;
|
use graph_craft::proto::GraphErrors;
|
||||||
use graphene_core::*;
|
use graphene_core::*;
|
||||||
use renderer::{ClickTarget, Quad};
|
use renderer::Quad;
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2, IVec2};
|
use glam::{DAffine2, DVec2, IVec2};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
@@ -136,8 +137,20 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||||||
|
|
||||||
responses.add(FrontendMessage::TriggerTextCopy { copy_text });
|
responses.add(FrontendMessage::TriggerTextCopy { copy_text });
|
||||||
}
|
}
|
||||||
|
NodeGraphMessage::CreateNodeInLayerNoTransaction { node_type, layer } => {
|
||||||
|
let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
modify_inputs.create_node(&node_type);
|
||||||
|
}
|
||||||
|
NodeGraphMessage::CreateNodeInLayerWithTransaction { node_type, layer } => {
|
||||||
|
responses.add(DocumentMessage::AddTransaction);
|
||||||
|
responses.add(NodeGraphMessage::CreateNodeInLayerNoTransaction { node_type, layer });
|
||||||
|
responses.add(PropertiesPanelMessage::Refresh);
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
}
|
||||||
NodeGraphMessage::CreateNodeFromContextMenu { node_id, node_type, x, y } => {
|
NodeGraphMessage::CreateNodeFromContextMenu { node_id, node_type, x, y } => {
|
||||||
let node_id = node_id.unwrap_or_else(|| NodeId::new());
|
let node_id = node_id.unwrap_or_else(NodeId::new);
|
||||||
|
|
||||||
let Some(document_node_type) = document_node_definitions::resolve_document_node_type(&node_type) else {
|
let Some(document_node_type) = document_node_definitions::resolve_document_node_type(&node_type) else {
|
||||||
responses.add(DialogMessage::DisplayDialogError {
|
responses.add(DialogMessage::DisplayDialogError {
|
||||||
@@ -429,31 +442,6 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||||||
.map(|selected_nodes| selected_nodes.selected_nodes().cloned().collect())
|
.map(|selected_nodes| selected_nodes.selected_nodes().cloned().collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// If the user is clicking on the create nodes list or context menu, break here
|
|
||||||
if let Some(context_menu) = &self.context_menu {
|
|
||||||
let context_menu_viewport = network_metadata
|
|
||||||
.persistent_metadata
|
|
||||||
.navigation_metadata
|
|
||||||
.node_graph_to_viewport
|
|
||||||
.transform_point2(DVec2::new(context_menu.context_menu_coordinates.0 as f64, context_menu.context_menu_coordinates.1 as f64));
|
|
||||||
let (width, height) = if matches!(context_menu.context_menu_data, ContextMenuData::ToggleLayer { .. }) {
|
|
||||||
// Height and width for toggle layer menu
|
|
||||||
(173., 34.)
|
|
||||||
} else {
|
|
||||||
// Height and width for create node menu
|
|
||||||
(180., 200.)
|
|
||||||
};
|
|
||||||
let context_menu_subpath = bezier_rs::Subpath::new_rounded_rect(
|
|
||||||
DVec2::new(context_menu_viewport.x, context_menu_viewport.y),
|
|
||||||
DVec2::new(context_menu_viewport.x + width, context_menu_viewport.y + height),
|
|
||||||
[5.; 4],
|
|
||||||
);
|
|
||||||
let context_menu_click_target = ClickTarget::new(context_menu_subpath, 0.);
|
|
||||||
if context_menu_click_target.intersect_point(click, DAffine2::IDENTITY) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since the user is clicking elsewhere in the graph, ensure the add nodes list is closed
|
// Since the user is clicking elsewhere in the graph, ensure the add nodes list is closed
|
||||||
if self.context_menu.is_some() {
|
if self.context_menu.is_some() {
|
||||||
self.context_menu = None;
|
self.context_menu = None;
|
||||||
@@ -1585,14 +1573,26 @@ impl NodeGraphMessageHandler {
|
|||||||
0 => {
|
0 => {
|
||||||
let selected_nodes = nodes
|
let selected_nodes = nodes
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|node_id| network.nodes.get(node_id).map(|node| node_properties::generate_node_properties(node, *node_id, context)))
|
.filter_map(|node_id| network.nodes.get(node_id).map(|node| node_properties::generate_node_properties(node, *node_id, false, context)))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if !selected_nodes.is_empty() {
|
if !selected_nodes.is_empty() {
|
||||||
return selected_nodes;
|
return selected_nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut properties = vec![LayoutGroup::Row {
|
||||||
|
widgets: vec![
|
||||||
|
Separator::new(SeparatorType::Related).widget_holder(),
|
||||||
|
IconLabel::new("File").tooltip("Name of the current document").widget_holder(),
|
||||||
|
Separator::new(SeparatorType::Related).widget_holder(),
|
||||||
|
TextInput::new(context.document_name)
|
||||||
|
.on_update(|text_input| DocumentMessage::RenameDocument { new_name: text_input.value.clone() }.into())
|
||||||
|
.widget_holder(),
|
||||||
|
Separator::new(SeparatorType::Related).widget_holder(),
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
// And if no nodes are selected, show properties for all pinned nodes
|
// And if no nodes are selected, show properties for all pinned nodes
|
||||||
network
|
let pinned_node_properties = network
|
||||||
.nodes
|
.nodes
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(node_id, node)| {
|
.filter_map(|(node_id, node)| {
|
||||||
@@ -1604,28 +1604,68 @@ impl NodeGraphMessageHandler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if pinned {
|
if pinned {
|
||||||
Some(node_properties::generate_node_properties(node, *node_id, context))
|
Some(node_properties::generate_node_properties(node, *node_id, true, context))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
properties.extend(pinned_node_properties);
|
||||||
|
properties
|
||||||
}
|
}
|
||||||
// If one layer is selected, filter out all selected nodes that are not upstream of it. If there are no nodes left, show properties for the layer. Otherwise, show nothing.
|
// If one layer is selected, filter out all selected nodes that are not upstream of it. If there are no nodes left, show properties for the layer. Otherwise, show nothing.
|
||||||
1 => {
|
1 => {
|
||||||
|
let layer = layers[0];
|
||||||
let nodes_not_upstream_of_layer = nodes.into_iter().filter(|&selected_node_id| {
|
let nodes_not_upstream_of_layer = nodes.into_iter().filter(|&selected_node_id| {
|
||||||
!context
|
!context
|
||||||
.network_interface
|
.network_interface
|
||||||
.is_node_upstream_of_another_by_horizontal_flow(layers[0], context.selection_network_path, selected_node_id)
|
.is_node_upstream_of_another_by_horizontal_flow(layer, context.selection_network_path, selected_node_id)
|
||||||
});
|
});
|
||||||
if nodes_not_upstream_of_layer.count() > 0 {
|
if nodes_not_upstream_of_layer.count() > 0 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut layer_properties = vec![LayoutGroup::Row {
|
||||||
|
widgets: vec![
|
||||||
|
Separator::new(SeparatorType::Related).widget_holder(),
|
||||||
|
IconLabel::new("Layer").tooltip("Name of the selected layer").widget_holder(),
|
||||||
|
Separator::new(SeparatorType::Related).widget_holder(),
|
||||||
|
TextInput::new(context.document_name)
|
||||||
|
.on_update(move |text_input| {
|
||||||
|
NodeGraphMessage::SetDisplayName {
|
||||||
|
node_id: layer,
|
||||||
|
alias: text_input.value.clone(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.widget_holder(),
|
||||||
|
Separator::new(SeparatorType::Related).widget_holder(),
|
||||||
|
{
|
||||||
|
let node_chooser = NodeCatalog::new()
|
||||||
|
.on_update(move |node_type| {
|
||||||
|
NodeGraphMessage::CreateNodeInLayerWithTransaction {
|
||||||
|
node_type: node_type.clone(),
|
||||||
|
layer: LayerNodeIdentifier::new_unchecked(layer),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.widget_holder();
|
||||||
|
let popover_layout = vec![LayoutGroup::Row { widgets: vec![node_chooser] }];
|
||||||
|
PopoverButton::new()
|
||||||
|
.icon(Some("Node".to_string()))
|
||||||
|
.tooltip("Add an operation to the end of this layer's chain of nodes")
|
||||||
|
.popover_layout(popover_layout)
|
||||||
|
.widget_holder()
|
||||||
|
},
|
||||||
|
Separator::new(SeparatorType::Related).widget_holder(),
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
// Iterate through all the upstream nodes, but stop when we reach another layer (since that's a point where we switch from horizontal to vertical flow)
|
// Iterate through all the upstream nodes, but stop when we reach another layer (since that's a point where we switch from horizontal to vertical flow)
|
||||||
context
|
let node_properties = context
|
||||||
.network_interface
|
.network_interface
|
||||||
.upstream_flow_back_from_nodes(vec![layers[0]], context.selection_network_path, network_interface::FlowType::HorizontalFlow)
|
.upstream_flow_back_from_nodes(vec![layer], context.selection_network_path, network_interface::FlowType::HorizontalFlow)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.take_while(|(i, node_id)| {
|
.take_while(|(i, node_id)| {
|
||||||
if *i == 0 {
|
if *i == 0 {
|
||||||
@@ -1635,8 +1675,11 @@ impl NodeGraphMessageHandler {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter_map(|(_, node_id)| network.nodes.get(&node_id).map(|node| (node, node_id)))
|
.filter_map(|(_, node_id)| network.nodes.get(&node_id).map(|node| (node, node_id)))
|
||||||
.map(|(node, node_id)| node_properties::generate_node_properties(node, node_id, context))
|
.map(|(node, node_id)| node_properties::generate_node_properties(node, node_id, false, context))
|
||||||
.collect()
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
layer_properties.extend(node_properties);
|
||||||
|
layer_properties
|
||||||
}
|
}
|
||||||
// If multiple layers and/or nodes are selected, show nothing
|
// If multiple layers and/or nodes are selected, show nothing
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
|
|||||||
@@ -2253,7 +2253,7 @@ pub(crate) fn index_properties(document_node: &DocumentNode, node_id: NodeId, _c
|
|||||||
vec![LayoutGroup::Row { widgets: index }]
|
vec![LayoutGroup::Row { widgets: index }]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn generate_node_properties(document_node: &DocumentNode, node_id: NodeId, context: &mut NodePropertiesContext) -> LayoutGroup {
|
pub(crate) fn generate_node_properties(document_node: &DocumentNode, node_id: NodeId, pinned: bool, context: &mut NodePropertiesContext) -> LayoutGroup {
|
||||||
let reference = context.network_interface.reference(&node_id, context.selection_network_path).clone();
|
let reference = context.network_interface.reference(&node_id, context.selection_network_path).clone();
|
||||||
let layout = if let Some(ref reference) = reference {
|
let layout = if let Some(ref reference) = reference {
|
||||||
match super::document_node_definitions::resolve_document_node_type(reference) {
|
match super::document_node_definitions::resolve_document_node_type(reference) {
|
||||||
@@ -2267,6 +2267,7 @@ pub(crate) fn generate_node_properties(document_node: &DocumentNode, node_id: No
|
|||||||
LayoutGroup::Section {
|
LayoutGroup::Section {
|
||||||
name: reference.unwrap_or_default(),
|
name: reference.unwrap_or_default(),
|
||||||
visible: document_node.visible,
|
visible: document_node.visible,
|
||||||
|
pinned,
|
||||||
id: node_id.0,
|
id: node_id.0,
|
||||||
layout,
|
layout,
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-22
@@ -11,17 +11,13 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
|
|||||||
fn process_message(&mut self, message: PropertiesPanelMessage, responses: &mut VecDeque<Message>, (persistent_data, data): (&PersistentData, PropertiesPanelMessageHandlerData)) {
|
fn process_message(&mut self, message: PropertiesPanelMessage, responses: &mut VecDeque<Message>, (persistent_data, data): (&PersistentData, PropertiesPanelMessageHandlerData)) {
|
||||||
let PropertiesPanelMessageHandlerData {
|
let PropertiesPanelMessageHandlerData {
|
||||||
network_interface,
|
network_interface,
|
||||||
selection_path,
|
selection_network_path,
|
||||||
document_name,
|
document_name,
|
||||||
executor,
|
executor,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
match message {
|
match message {
|
||||||
PropertiesPanelMessage::Clear => {
|
PropertiesPanelMessage::Clear => {
|
||||||
responses.add(LayoutMessage::SendLayout {
|
|
||||||
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
|
|
||||||
layout_target: LayoutTarget::PropertiesOptions,
|
|
||||||
});
|
|
||||||
responses.add(LayoutMessage::SendLayout {
|
responses.add(LayoutMessage::SendLayout {
|
||||||
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
|
layout: Layout::WidgetLayout(WidgetLayout::new(vec![])),
|
||||||
layout_target: LayoutTarget::PropertiesSections,
|
layout_target: LayoutTarget::PropertiesSections,
|
||||||
@@ -31,27 +27,13 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
|
|||||||
let mut context = NodePropertiesContext {
|
let mut context = NodePropertiesContext {
|
||||||
persistent_data,
|
persistent_data,
|
||||||
responses,
|
responses,
|
||||||
executor,
|
|
||||||
network_interface,
|
network_interface,
|
||||||
selection_network_path: selection_path,
|
selection_network_path,
|
||||||
|
document_name,
|
||||||
|
executor,
|
||||||
};
|
};
|
||||||
|
|
||||||
let properties_sections = NodeGraphMessageHandler::collate_properties(&mut context);
|
let properties_sections = NodeGraphMessageHandler::collate_properties(&mut context);
|
||||||
|
|
||||||
let options_bar = vec![LayoutGroup::Row {
|
|
||||||
widgets: vec![
|
|
||||||
IconLabel::new("File").tooltip("Document name").widget_holder(),
|
|
||||||
Separator::new(SeparatorType::Related).widget_holder(),
|
|
||||||
TextInput::new(document_name)
|
|
||||||
.on_update(|text_input| DocumentMessage::RenameDocument { new_name: text_input.value.clone() }.into())
|
|
||||||
.widget_holder(),
|
|
||||||
],
|
|
||||||
}];
|
|
||||||
|
|
||||||
context.responses.add(LayoutMessage::SendLayout {
|
|
||||||
layout: Layout::WidgetLayout(WidgetLayout::new(options_bar)),
|
|
||||||
layout_target: LayoutTarget::PropertiesOptions,
|
|
||||||
});
|
|
||||||
context.responses.add(LayoutMessage::SendLayout {
|
context.responses.add(LayoutMessage::SendLayout {
|
||||||
layout: Layout::WidgetLayout(WidgetLayout::new(properties_sections)),
|
layout: Layout::WidgetLayout(WidgetLayout::new(properties_sections)),
|
||||||
layout_target: LayoutTarget::PropertiesSections,
|
layout_target: LayoutTarget::PropertiesSections,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crate::node_graph_executor::NodeGraphExecutor;
|
|||||||
|
|
||||||
pub struct PropertiesPanelMessageHandlerData<'a> {
|
pub struct PropertiesPanelMessageHandlerData<'a> {
|
||||||
pub network_interface: &'a NodeNetworkInterface,
|
pub network_interface: &'a NodeNetworkInterface,
|
||||||
pub selection_path: &'a [NodeId],
|
pub selection_network_path: &'a [NodeId],
|
||||||
pub document_name: &'a str,
|
pub document_name: &'a str,
|
||||||
pub executor: &'a mut NodeGraphExecutor,
|
pub executor: &'a mut NodeGraphExecutor,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12,2h-2v1h2c1.1,0,2,.9,2,2v6c0,1.1-.9,2-2,2H4c-1.1,0-2-.9-2-2v-3h-1v3c0,1.65,1.35,3,3,3h8c1.65,0,3-1.35,3-3v-6c0-1.65-1.35-3-3-3Z" />
|
||||||
|
<polygon points="10.25 9.25 12.5 8.5 10.25 7.75 9.5 5.5 8.75 7.75 6.5 8.5 8.75 9.25 9.5 11.5 10.25 9.25" />
|
||||||
|
<polygon points="5.3 5.3 8.5 4.5 5.3 3.7 4.5 .5 3.7 3.7 .5 4.5 3.7 5.3 4.5 8.5 5.3 5.3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 414 B |
@@ -0,0 +1,161 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, getContext, onMount } from "svelte";
|
||||||
|
|
||||||
|
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
|
||||||
|
import type { FrontendNodeType } from "@graphite/wasm-communication/messages";
|
||||||
|
|
||||||
|
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
||||||
|
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
||||||
|
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{ selectNodeType: string }>();
|
||||||
|
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
||||||
|
|
||||||
|
export let disabled = false;
|
||||||
|
|
||||||
|
let nodeSearchInput: TextInput | undefined = undefined;
|
||||||
|
let searchTerm = "";
|
||||||
|
|
||||||
|
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
|
||||||
|
|
||||||
|
type NodeCategoryDetails = {
|
||||||
|
nodes: FrontendNodeType[];
|
||||||
|
open: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, NodeCategoryDetails][] {
|
||||||
|
const categories = new Map<string, NodeCategoryDetails>();
|
||||||
|
|
||||||
|
nodeTypes.forEach((node) => {
|
||||||
|
let nameIncludesSearchTerm = node.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
|
||||||
|
// Quick and dirty hack to alias "Layer" to "Merge" in the search
|
||||||
|
if (node.name === "Merge") {
|
||||||
|
nameIncludesSearchTerm = nameIncludesSearchTerm || "Layer".toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchTerm.length > 0 && !nameIncludesSearchTerm && !node.category.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = categories.get(node.category);
|
||||||
|
let open = nameIncludesSearchTerm;
|
||||||
|
if (searchTerm.length === 0) {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
category.open = open;
|
||||||
|
category.nodes.push(node);
|
||||||
|
} else
|
||||||
|
categories.set(node.category, {
|
||||||
|
open,
|
||||||
|
nodes: [node],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const START_CATEGORIES_ORDER = ["UNCATEGORIZED", "General", "Value", "Math", "Style"];
|
||||||
|
const END_CATEGORIES_ORDER = ["Debug"];
|
||||||
|
return Array.from(categories)
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aIndex = START_CATEGORIES_ORDER.findIndex((x) => a[0].startsWith(x));
|
||||||
|
const bIndex = START_CATEGORIES_ORDER.findIndex((x) => b[0].startsWith(x));
|
||||||
|
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
||||||
|
if (aIndex !== -1) return -1;
|
||||||
|
if (bIndex !== -1) return 1;
|
||||||
|
return 0;
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aIndex = END_CATEGORIES_ORDER.findIndex((x) => a[0].startsWith(x));
|
||||||
|
const bIndex = END_CATEGORIES_ORDER.findIndex((x) => b[0].startsWith(x));
|
||||||
|
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
||||||
|
if (aIndex !== -1) return 1;
|
||||||
|
if (bIndex !== -1) return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setTimeout(() => nodeSearchInput?.focus(), 0);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="node-catalog">
|
||||||
|
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
||||||
|
<div class="list-results" on:wheel|passive|stopPropagation>
|
||||||
|
{#each nodeCategories as nodeCategory}
|
||||||
|
<details open={nodeCategory[1].open}>
|
||||||
|
<summary>
|
||||||
|
<TextLabel>{nodeCategory[0]}</TextLabel>
|
||||||
|
</summary>
|
||||||
|
{#each nodeCategory[1].nodes as nodeType}
|
||||||
|
<TextButton {disabled} label={nodeType.name} action={() => dispatch("selectNodeType", nodeType.name)} />
|
||||||
|
{/each}
|
||||||
|
</details>
|
||||||
|
{:else}
|
||||||
|
<TextLabel>No search results</TextLabel>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss" global>
|
||||||
|
.node-catalog {
|
||||||
|
max-height: 40vh;
|
||||||
|
min-width: 250px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
.text-input {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-results {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
// Together with the `margin-right: 4px;` on `details` below, this keeps a gap between the listings and the scrollbar
|
||||||
|
margin-right: -4px;
|
||||||
|
|
||||||
|
details {
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
// Together with the `margin-right: -4px;` on `.list-results` above, this keeps a gap between the listings and the scrollbar
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
|
&[open] summary .text-label::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
.text-label {
|
||||||
|
padding-left: 16px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
margin: auto;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: var(--icon-expand-collapse-arrow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-button {
|
||||||
|
width: 100%;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,23 +2,16 @@
|
|||||||
import { getContext, onMount } from "svelte";
|
import { getContext, onMount } from "svelte";
|
||||||
|
|
||||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||||
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelOptionsLayout, UpdatePropertyPanelSectionsLayout } from "@graphite/wasm-communication/messages";
|
import { defaultWidgetLayout, patchWidgetLayout, UpdatePropertyPanelSectionsLayout } from "@graphite/wasm-communication/messages";
|
||||||
|
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
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 WidgetLayout from "@graphite/components/widgets/WidgetLayout.svelte";
|
||||||
|
|
||||||
const editor = getContext<Editor>("editor");
|
const editor = getContext<Editor>("editor");
|
||||||
|
|
||||||
let propertiesOptionsLayout = defaultWidgetLayout();
|
|
||||||
let propertiesSectionsLayout = defaultWidgetLayout();
|
let propertiesSectionsLayout = defaultWidgetLayout();
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelOptionsLayout, (updatePropertyPanelOptionsLayout) => {
|
|
||||||
patchWidgetLayout(propertiesOptionsLayout, updatePropertyPanelOptionsLayout);
|
|
||||||
propertiesOptionsLayout = propertiesOptionsLayout;
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
|
editor.subscriptions.subscribeJsMessage(UpdatePropertyPanelSectionsLayout, (updatePropertyPanelSectionsLayout) => {
|
||||||
patchWidgetLayout(propertiesSectionsLayout, updatePropertyPanelSectionsLayout);
|
patchWidgetLayout(propertiesSectionsLayout, updatePropertyPanelSectionsLayout);
|
||||||
propertiesSectionsLayout = propertiesSectionsLayout;
|
propertiesSectionsLayout = propertiesSectionsLayout;
|
||||||
@@ -27,9 +20,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LayoutCol class="properties">
|
<LayoutCol class="properties">
|
||||||
<LayoutRow class="options-bar">
|
|
||||||
<WidgetLayout layout={propertiesOptionsLayout} />
|
|
||||||
</LayoutRow>
|
|
||||||
<LayoutCol class="sections" scrollableY={true}>
|
<LayoutCol class="sections" scrollableY={true}>
|
||||||
<WidgetLayout layout={propertiesSectionsLayout} />
|
<WidgetLayout layout={propertiesSectionsLayout} />
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
|
|||||||
@@ -7,14 +7,13 @@
|
|||||||
import type { IconName } from "@graphite/utility-functions/icons";
|
import type { IconName } from "@graphite/utility-functions/icons";
|
||||||
import type { Editor } from "@graphite/wasm-communication/editor";
|
import type { Editor } from "@graphite/wasm-communication/editor";
|
||||||
import type { Node } from "@graphite/wasm-communication/messages";
|
import type { Node } from "@graphite/wasm-communication/messages";
|
||||||
import type { FrontendNodeWire, FrontendNodeType, FrontendNode, FrontendGraphInput, FrontendGraphOutput, FrontendGraphDataType, WirePath } from "@graphite/wasm-communication/messages";
|
import type { FrontendNodeWire, FrontendNode, FrontendGraphInput, FrontendGraphOutput, FrontendGraphDataType, WirePath } from "@graphite/wasm-communication/messages";
|
||||||
|
|
||||||
|
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
|
||||||
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
|
||||||
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
|
||||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||||
import TextButton from "@graphite/components/widgets/buttons/TextButton.svelte";
|
|
||||||
import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte";
|
import RadioInput from "@graphite/components/widgets/inputs/RadioInput.svelte";
|
||||||
import TextInput from "@graphite/components/widgets/inputs/TextInput.svelte";
|
|
||||||
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
|
||||||
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
import TextLabel from "@graphite/components/widgets/labels/TextLabel.svelte";
|
||||||
const GRID_COLLAPSE_SPACING = 10;
|
const GRID_COLLAPSE_SPACING = 10;
|
||||||
@@ -25,14 +24,12 @@
|
|||||||
|
|
||||||
let graph: HTMLDivElement | undefined;
|
let graph: HTMLDivElement | undefined;
|
||||||
let nodesContainer: HTMLDivElement | undefined;
|
let nodesContainer: HTMLDivElement | undefined;
|
||||||
let nodeSearchInput: TextInput | undefined;
|
|
||||||
|
|
||||||
// TODO: Using this not-complete code, or another better approach, make it so the dragged in-progress connector correctly handles showing/hiding the SVG shape of the connector caps
|
// TODO: Using this not-complete code, or another better approach, make it so the dragged in-progress connector correctly handles showing/hiding the SVG shape of the connector caps
|
||||||
// let wireInProgressFromLayerTop: bigint | undefined = undefined;
|
// let wireInProgressFromLayerTop: bigint | undefined = undefined;
|
||||||
// let wireInProgressFromLayerBottom: bigint | undefined = undefined;
|
// let wireInProgressFromLayerBottom: bigint | undefined = undefined;
|
||||||
|
|
||||||
let nodeWirePaths: WirePath[] = [];
|
let nodeWirePaths: WirePath[] = [];
|
||||||
let searchTerm = "";
|
|
||||||
|
|
||||||
// TODO: Convert these arrays-of-arrays to a Map?
|
// TODO: Convert these arrays-of-arrays to a Map?
|
||||||
let inputs: SVGSVGElement[][] = [];
|
let inputs: SVGSVGElement[][] = [];
|
||||||
@@ -43,13 +40,6 @@
|
|||||||
|
|
||||||
$: gridSpacing = calculateGridSpacing($nodeGraph.transform.scale);
|
$: gridSpacing = calculateGridSpacing($nodeGraph.transform.scale);
|
||||||
$: dotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2;
|
$: dotRadius = 1 + Math.floor($nodeGraph.transform.scale - 0.5 + 0.001) / 2;
|
||||||
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
|
|
||||||
|
|
||||||
$: (() => {
|
|
||||||
if ($nodeGraph.contextMenuInformation?.contextMenuData === "CreateNode") {
|
|
||||||
setTimeout(() => nodeSearchInput?.focus(), 0);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
$: wirePaths = createWirePaths($nodeGraph.wirePathInProgress, nodeWirePaths);
|
$: wirePaths = createWirePaths($nodeGraph.wirePathInProgress, nodeWirePaths);
|
||||||
|
|
||||||
@@ -64,63 +54,6 @@
|
|||||||
return sparse;
|
return sparse;
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeCategoryDetails = {
|
|
||||||
nodes: FrontendNodeType[];
|
|
||||||
open: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, NodeCategoryDetails][] {
|
|
||||||
const categories = new Map<string, NodeCategoryDetails>();
|
|
||||||
|
|
||||||
nodeTypes.forEach((node) => {
|
|
||||||
let nameIncludesSearchTerm = node.name.toLowerCase().includes(searchTerm.toLowerCase());
|
|
||||||
// Quick and dirty hack to alias "Layer" to "Merge" in the search
|
|
||||||
if (node.name === "Merge") {
|
|
||||||
nameIncludesSearchTerm = nameIncludesSearchTerm || "Layer".toLowerCase().includes(searchTerm.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchTerm.length > 0 && !nameIncludesSearchTerm && !node.category.toLowerCase().includes(searchTerm.toLowerCase())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const category = categories.get(node.category);
|
|
||||||
let open = nameIncludesSearchTerm;
|
|
||||||
if (searchTerm.length === 0) {
|
|
||||||
open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (category) {
|
|
||||||
category.open = open;
|
|
||||||
category.nodes.push(node);
|
|
||||||
} else
|
|
||||||
categories.set(node.category, {
|
|
||||||
open,
|
|
||||||
nodes: [node],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const START_CATEGORIES_ORDER = ["UNCATEGORIZED", "General", "Value", "Math", "Style"];
|
|
||||||
const END_CATEGORIES_ORDER = ["Debug"];
|
|
||||||
return Array.from(categories)
|
|
||||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
||||||
.sort((a, b) => {
|
|
||||||
const aIndex = START_CATEGORIES_ORDER.findIndex((x) => a[0].startsWith(x));
|
|
||||||
const bIndex = START_CATEGORIES_ORDER.findIndex((x) => b[0].startsWith(x));
|
|
||||||
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
|
||||||
if (aIndex !== -1) return -1;
|
|
||||||
if (bIndex !== -1) return 1;
|
|
||||||
return 0;
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
const aIndex = END_CATEGORIES_ORDER.findIndex((x) => a[0].startsWith(x));
|
|
||||||
const bIndex = END_CATEGORIES_ORDER.findIndex((x) => b[0].startsWith(x));
|
|
||||||
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
|
|
||||||
if (aIndex !== -1) return 1;
|
|
||||||
if (bIndex !== -1) return -1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWirePaths(wirePathInProgress: WirePath | undefined, nodeWirePaths: WirePath[]): WirePath[] {
|
function createWirePaths(wirePathInProgress: WirePath | undefined, nodeWirePaths: WirePath[]): WirePath[] {
|
||||||
const maybeWirePathInProgress = wirePathInProgress ? [wirePathInProgress] : [];
|
const maybeWirePathInProgress = wirePathInProgress ? [wirePathInProgress] : [];
|
||||||
return [...maybeWirePathInProgress, ...nodeWirePaths];
|
return [...maybeWirePathInProgress, ...nodeWirePaths];
|
||||||
@@ -391,7 +324,6 @@
|
|||||||
{#if $nodeGraph.contextMenuInformation}
|
{#if $nodeGraph.contextMenuInformation}
|
||||||
<LayoutCol
|
<LayoutCol
|
||||||
class="context-menu"
|
class="context-menu"
|
||||||
classes={{ "create-node-menu": $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode" }}
|
|
||||||
data-context-menu
|
data-context-menu
|
||||||
styles={{
|
styles={{
|
||||||
left: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.x * $nodeGraph.transform.scale + $nodeGraph.transform.x}px`,
|
left: `${$nodeGraph.contextMenuInformation.contextMenuCoordinates.x * $nodeGraph.transform.scale + $nodeGraph.transform.x}px`,
|
||||||
@@ -399,21 +331,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"}
|
{#if $nodeGraph.contextMenuInformation.contextMenuData === "CreateNode"}
|
||||||
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
<NodeCatalog on:selectNodeType={(e) => createNode(e.detail)} />
|
||||||
<div class="list-results" on:wheel|passive|stopPropagation>
|
|
||||||
{#each nodeCategories as nodeCategory}
|
|
||||||
<details open={nodeCategory[1].open}>
|
|
||||||
<summary>
|
|
||||||
<TextLabel>{nodeCategory[0]}</TextLabel>
|
|
||||||
</summary>
|
|
||||||
{#each nodeCategory[1].nodes as nodeType}
|
|
||||||
<TextButton label={nodeType.name} action={() => createNode(nodeType.name)} />
|
|
||||||
{/each}
|
|
||||||
</details>
|
|
||||||
{:else}
|
|
||||||
<TextLabel>No search results</TextLabel>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
{@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData}
|
{@const contextMenuData = $nodeGraph.contextMenuInformation.contextMenuData}
|
||||||
<LayoutRow class="toggle-layer-or-node">
|
<LayoutRow class="toggle-layer-or-node">
|
||||||
@@ -871,64 +789,6 @@
|
|||||||
background-color: var(--color-3-darkgray);
|
background-color: var(--color-3-darkgray);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&.create-node-menu {
|
|
||||||
height: 200px; // For some reason, when attemping to make this taller, the bottom few categories don't open when clicked, but instead immediately close the menu
|
|
||||||
width: 180px; // Also when making this wider, clicking the scrollbar on the right edge of the menu causes the menu to close immediately
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-input {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-results {
|
|
||||||
overflow-y: auto;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
// Together with the `margin-right: 4px;` on `details` below, this keeps a gap between the listings and the scrollbar
|
|
||||||
margin-right: -4px;
|
|
||||||
|
|
||||||
details {
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
// Together with the `margin-right: -4px;` on `.list-results` above, this keeps a gap between the listings and the scrollbar
|
|
||||||
margin-right: 4px;
|
|
||||||
|
|
||||||
&[open] summary .text-label::before {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
summary {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
|
|
||||||
.text-label {
|
|
||||||
padding-left: 16px;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
margin: auto;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background: var(--icon-expand-collapse-arrow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-button {
|
|
||||||
width: 100%;
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-layer-or-node .text-label {
|
.toggle-layer-or-node .text-label {
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
|||||||
@@ -27,8 +27,21 @@
|
|||||||
<button class="header" class:expanded on:click|stopPropagation={() => (expanded = !expanded)} tabindex="0">
|
<button class="header" class:expanded on:click|stopPropagation={() => (expanded = !expanded)} tabindex="0">
|
||||||
<div class="expand-arrow" />
|
<div class="expand-arrow" />
|
||||||
<TextLabel bold={true}>{widgetData.name}</TextLabel>
|
<TextLabel bold={true}>{widgetData.name}</TextLabel>
|
||||||
|
{#if widgetData.pinned}
|
||||||
|
<IconButton
|
||||||
|
icon={"CheckboxChecked"}
|
||||||
|
tooltip={"Unpin this node so it's no longer shown here without a selection"}
|
||||||
|
size={24}
|
||||||
|
action={(e) => {
|
||||||
|
editor.handle.unpinNode(widgetData.id);
|
||||||
|
e?.stopPropagation();
|
||||||
|
}}
|
||||||
|
class={"show-only-on-hover"}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={"Trash"}
|
icon={"Trash"}
|
||||||
|
tooltip={"Delete this node from the layer chain"}
|
||||||
size={24}
|
size={24}
|
||||||
action={(e) => {
|
action={(e) => {
|
||||||
editor.handle.deleteNode(widgetData.id);
|
editor.handle.deleteNode(widgetData.id);
|
||||||
@@ -39,6 +52,7 @@
|
|||||||
<IconButton
|
<IconButton
|
||||||
icon={widgetData.visible ? "EyeVisible" : "EyeHidden"}
|
icon={widgetData.visible ? "EyeVisible" : "EyeHidden"}
|
||||||
hoverIcon={widgetData.visible ? "EyeHide" : "EyeShow"}
|
hoverIcon={widgetData.visible ? "EyeHide" : "EyeShow"}
|
||||||
|
tooltip={widgetData.visible ? "Hide this node" : "Show this node"}
|
||||||
size={24}
|
size={24}
|
||||||
action={(e) => {
|
action={(e) => {
|
||||||
editor.handle.toggleNodeVisibilityLayerPanel(widgetData.id);
|
editor.handle.toggleNodeVisibilityLayerPanel(widgetData.id);
|
||||||
@@ -68,10 +82,7 @@
|
|||||||
.widget-section {
|
.widget-section {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
|
margin-top: 4px;
|
||||||
+ .widget-section {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import type { Widget, WidgetSpanColumn, WidgetSpanRow } from "@graphite/wasm-communication/messages";
|
import type { Widget, WidgetSpanColumn, WidgetSpanRow } from "@graphite/wasm-communication/messages";
|
||||||
import { narrowWidgetProps, isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/wasm-communication/messages";
|
import { narrowWidgetProps, isWidgetSpanColumn, isWidgetSpanRow } from "@graphite/wasm-communication/messages";
|
||||||
|
|
||||||
|
import NodeCatalog from "@graphite/components/floating-menus/NodeCatalog.svelte";
|
||||||
import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte";
|
import BreadcrumbTrailButtons from "@graphite/components/widgets/buttons/BreadcrumbTrailButtons.svelte";
|
||||||
import ColorButton from "@graphite/components/widgets/buttons/ColorButton.svelte";
|
import ColorButton from "@graphite/components/widgets/buttons/ColorButton.svelte";
|
||||||
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
import IconButton from "@graphite/components/widgets/buttons/IconButton.svelte";
|
||||||
@@ -127,6 +128,10 @@
|
|||||||
{#if imageLabel}
|
{#if imageLabel}
|
||||||
<ImageLabel {...exclude(imageLabel)} />
|
<ImageLabel {...exclude(imageLabel)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
{@const nodeCatalog = narrowWidgetProps(component.props, "NodeCatalog")}
|
||||||
|
{#if nodeCatalog}
|
||||||
|
<NodeCatalog {...exclude(nodeCatalog)} on:selectNodeType={(e) => widgetValueCommitAndUpdate(index, e.detail)} />
|
||||||
|
{/if}
|
||||||
{@const numberInput = narrowWidgetProps(component.props, "NumberInput")}
|
{@const numberInput = narrowWidgetProps(component.props, "NumberInput")}
|
||||||
{#if numberInput}
|
{#if numberInput}
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||||||
const { target } = e;
|
const { target } = e;
|
||||||
const isTargetingCanvas = target instanceof Element && (target.closest("[data-viewport]") || target.closest("[data-node-graph]"));
|
const isTargetingCanvas = target instanceof Element && (target.closest("[data-viewport]") || target.closest("[data-node-graph]"));
|
||||||
const inDialog = target instanceof Element && target.closest("[data-dialog] [data-floating-menu-content]");
|
const inDialog = target instanceof Element && target.closest("[data-dialog] [data-floating-menu-content]");
|
||||||
|
const inContextMenu = target instanceof Element && target.closest("[data-context-menu]");
|
||||||
const inTextInput = target === textToolInteractiveInputElement;
|
const inTextInput = target === textToolInteractiveInputElement;
|
||||||
|
|
||||||
if (get(dialog).visible && !inDialog) {
|
if (get(dialog).visible && !inDialog) {
|
||||||
@@ -170,7 +171,7 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inTextInput) {
|
if (!inTextInput && !inContextMenu) {
|
||||||
if (textToolInteractiveInputElement) editor.handle.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText));
|
if (textToolInteractiveInputElement) editor.handle.onChangeText(textInputCleanup(textToolInteractiveInputElement.innerText));
|
||||||
else viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
|
else viewportPointerInteractionOngoing = isTargetingCanvas instanceof Element;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ import NodeOutput from "@graphite-frontend/assets/icon-16px-solid/node-output.sv
|
|||||||
import NodeShape from "@graphite-frontend/assets/icon-16px-solid/node-shape.svg";
|
import NodeShape from "@graphite-frontend/assets/icon-16px-solid/node-shape.svg";
|
||||||
import NodeText from "@graphite-frontend/assets/icon-16px-solid/node-text.svg";
|
import NodeText from "@graphite-frontend/assets/icon-16px-solid/node-text.svg";
|
||||||
import NodeTransform from "@graphite-frontend/assets/icon-16px-solid/node-transform.svg";
|
import NodeTransform from "@graphite-frontend/assets/icon-16px-solid/node-transform.svg";
|
||||||
|
import Node from "@graphite-frontend/assets/icon-16px-solid/node.svg";
|
||||||
import PadlockLocked from "@graphite-frontend/assets/icon-16px-solid/padlock-locked.svg";
|
import PadlockLocked from "@graphite-frontend/assets/icon-16px-solid/padlock-locked.svg";
|
||||||
import PadlockUnlocked from "@graphite-frontend/assets/icon-16px-solid/padlock-unlocked.svg";
|
import PadlockUnlocked from "@graphite-frontend/assets/icon-16px-solid/padlock-unlocked.svg";
|
||||||
import Paste from "@graphite-frontend/assets/icon-16px-solid/paste.svg";
|
import Paste from "@graphite-frontend/assets/icon-16px-solid/paste.svg";
|
||||||
@@ -205,6 +206,7 @@ const SOLID_16PX = {
|
|||||||
Layer: { svg: Layer, size: 16 },
|
Layer: { svg: Layer, size: 16 },
|
||||||
License: { svg: License, size: 16 },
|
License: { svg: License, size: 16 },
|
||||||
NewLayer: { svg: NewLayer, size: 16 },
|
NewLayer: { svg: NewLayer, size: 16 },
|
||||||
|
Node: { svg: Node, size: 16 },
|
||||||
NodeBlur: { svg: NodeBlur, size: 16 },
|
NodeBlur: { svg: NodeBlur, size: 16 },
|
||||||
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
|
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
|
||||||
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },
|
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },
|
||||||
@@ -226,9 +228,9 @@ const SOLID_16PX = {
|
|||||||
Reload: { svg: Reload, size: 16 },
|
Reload: { svg: Reload, size: 16 },
|
||||||
Rescale: { svg: Rescale, size: 16 },
|
Rescale: { svg: Rescale, size: 16 },
|
||||||
Reset: { svg: Reset, size: 16 },
|
Reset: { svg: Reset, size: 16 },
|
||||||
|
Reverse: { svg: Reverse, size: 16 },
|
||||||
ReverseRadialGradientToLeft: { svg: ReverseRadialGradientToLeft, size: 16 },
|
ReverseRadialGradientToLeft: { svg: ReverseRadialGradientToLeft, size: 16 },
|
||||||
ReverseRadialGradientToRight: { svg: ReverseRadialGradientToRight, size: 16 },
|
ReverseRadialGradientToRight: { svg: ReverseRadialGradientToRight, size: 16 },
|
||||||
Reverse: { svg: Reverse, size: 16 },
|
|
||||||
Settings: { svg: Settings, size: 16 },
|
Settings: { svg: Settings, size: 16 },
|
||||||
Stack: { svg: Stack, size: 16 },
|
Stack: { svg: Stack, size: 16 },
|
||||||
Trash: { svg: Trash, size: 16 },
|
Trash: { svg: Trash, size: 16 },
|
||||||
|
|||||||
@@ -1116,6 +1116,10 @@ export class NumberInput extends WidgetProps {
|
|||||||
minWidth!: number;
|
minWidth!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NodeCatalog extends WidgetProps {
|
||||||
|
disabled!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class PopoverButton extends WidgetProps {
|
export class PopoverButton extends WidgetProps {
|
||||||
style!: PopoverButtonStyle | undefined;
|
style!: PopoverButtonStyle | undefined;
|
||||||
|
|
||||||
@@ -1293,6 +1297,7 @@ const widgetSubTypes = [
|
|||||||
{ value: IconButton, name: "IconButton" },
|
{ value: IconButton, name: "IconButton" },
|
||||||
{ value: IconLabel, name: "IconLabel" },
|
{ value: IconLabel, name: "IconLabel" },
|
||||||
{ value: ImageLabel, name: "ImageLabel" },
|
{ value: ImageLabel, name: "ImageLabel" },
|
||||||
|
{ value: NodeCatalog, name: "NodeCatalog" },
|
||||||
{ value: NumberInput, name: "NumberInput" },
|
{ value: NumberInput, name: "NumberInput" },
|
||||||
{ value: ParameterExposeButton, name: "ParameterExposeButton" },
|
{ value: ParameterExposeButton, name: "ParameterExposeButton" },
|
||||||
{ value: PivotInput, name: "PivotInput" },
|
{ value: PivotInput, name: "PivotInput" },
|
||||||
@@ -1425,7 +1430,7 @@ export function isWidgetSpanRow(layoutRow: LayoutGroup): layoutRow is WidgetSpan
|
|||||||
return Boolean((layoutRow as WidgetSpanRow)?.rowWidgets);
|
return Boolean((layoutRow as WidgetSpanRow)?.rowWidgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WidgetSection = { name: string; visible: boolean; id: bigint; layout: LayoutGroup[] };
|
export type WidgetSection = { name: string; visible: boolean; pinned: boolean; id: bigint; layout: LayoutGroup[] };
|
||||||
export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSection {
|
export function isWidgetSection(layoutRow: LayoutGroup): layoutRow is WidgetSection {
|
||||||
return Boolean((layoutRow as WidgetSection)?.layout);
|
return Boolean((layoutRow as WidgetSection)?.layout);
|
||||||
}
|
}
|
||||||
@@ -1468,6 +1473,7 @@ function createLayoutGroup(layoutGroup: any): LayoutGroup {
|
|||||||
const result: WidgetSection = {
|
const result: WidgetSection = {
|
||||||
name: layoutGroup.section.name,
|
name: layoutGroup.section.name,
|
||||||
visible: layoutGroup.section.visible,
|
visible: layoutGroup.section.visible,
|
||||||
|
pinned: layoutGroup.section.pinned,
|
||||||
id: layoutGroup.section.id,
|
id: layoutGroup.section.id,
|
||||||
layout: layoutGroup.section.layout.map(createLayoutGroup),
|
layout: layoutGroup.section.layout.map(createLayoutGroup),
|
||||||
};
|
};
|
||||||
@@ -1502,8 +1508,6 @@ export class UpdateMenuBarLayout extends JsMessage {
|
|||||||
|
|
||||||
export class UpdateNodeGraphBarLayout extends WidgetDiffUpdate {}
|
export class UpdateNodeGraphBarLayout extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdatePropertyPanelOptionsLayout extends WidgetDiffUpdate {}
|
|
||||||
|
|
||||||
export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {}
|
export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdateToolOptionsLayout extends WidgetDiffUpdate {}
|
export class UpdateToolOptionsLayout extends WidgetDiffUpdate {}
|
||||||
@@ -1594,7 +1598,6 @@ export const messageMakers: Record<string, MessageMaker> = {
|
|||||||
UpdateNodeThumbnail,
|
UpdateNodeThumbnail,
|
||||||
UpdateNodeTypes,
|
UpdateNodeTypes,
|
||||||
UpdateOpenDocumentsList,
|
UpdateOpenDocumentsList,
|
||||||
UpdatePropertyPanelOptionsLayout,
|
|
||||||
UpdatePropertyPanelSectionsLayout,
|
UpdatePropertyPanelSectionsLayout,
|
||||||
UpdateToolOptionsLayout,
|
UpdateToolOptionsLayout,
|
||||||
UpdateToolShelfLayout,
|
UpdateToolShelfLayout,
|
||||||
|
|||||||
@@ -652,20 +652,16 @@ impl EditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Unpin a node given its node ID
|
||||||
|
#[wasm_bindgen(js_name = unpinNode)]
|
||||||
|
pub fn unpin_node(&self, id: u64) {
|
||||||
|
self.dispatch(DocumentMessage::SetNodePinned { node_id: NodeId(id), pinned: false });
|
||||||
|
}
|
||||||
|
|
||||||
/// Delete a layer or node given its node ID
|
/// Delete a layer or node given its node ID
|
||||||
#[wasm_bindgen(js_name = deleteNode)]
|
#[wasm_bindgen(js_name = deleteNode)]
|
||||||
pub fn delete_node(&self, id: u64) {
|
pub fn delete_node(&self, id: u64) {
|
||||||
let message = DocumentMessage::StartTransaction;
|
self.dispatch(DocumentMessage::DeleteNode { node_id: NodeId(id) });
|
||||||
self.dispatch(message);
|
|
||||||
|
|
||||||
let id = NodeId(id);
|
|
||||||
self.dispatch(NodeGraphMessage::DeleteNodes {
|
|
||||||
node_ids: vec![id],
|
|
||||||
delete_children: true,
|
|
||||||
});
|
|
||||||
self.dispatch(NodeGraphMessage::RunDocumentGraph);
|
|
||||||
self.dispatch(NodeGraphMessage::SelectedNodesUpdated);
|
|
||||||
self.dispatch(NodeGraphMessage::SendGraph);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle lock state of a layer from the layer list
|
/// Toggle lock state of a layer from the layer list
|
||||||
@@ -693,11 +689,7 @@ impl EditorHandle {
|
|||||||
/// Toggle display type for a layer
|
/// Toggle display type for a layer
|
||||||
#[wasm_bindgen(js_name = setToNodeOrLayer)]
|
#[wasm_bindgen(js_name = setToNodeOrLayer)]
|
||||||
pub fn set_to_node_or_layer(&self, id: u64, is_layer: bool) {
|
pub fn set_to_node_or_layer(&self, id: u64, is_layer: bool) {
|
||||||
let node_id = NodeId(id);
|
self.dispatch(DocumentMessage::SetToNodeOrLayer { node_id: NodeId(id), is_layer });
|
||||||
let message = DocumentMessage::StartTransaction;
|
|
||||||
self.dispatch(message);
|
|
||||||
let message = NodeGraphMessage::SetToNodeOrLayer { node_id, is_layer };
|
|
||||||
self.dispatch(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
|
#[wasm_bindgen(js_name = injectImaginatePollServerStatus)]
|
||||||
|
|||||||
@@ -1016,7 +1016,7 @@ impl NodeNetwork {
|
|||||||
|
|
||||||
/// Remove all nodes that contain [`DocumentNodeImplementation::Network`] by moving the nested nodes into the parent network.
|
/// Remove all nodes that contain [`DocumentNodeImplementation::Network`] by moving the nested nodes into the parent network.
|
||||||
pub fn flatten(&mut self, node_id: NodeId) {
|
pub fn flatten(&mut self, node_id: NodeId) {
|
||||||
self.flatten_with_fns(node_id, merge_ids, || NodeId::new())
|
self.flatten_with_fns(node_id, merge_ids, NodeId::new)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove all nodes that contain [`DocumentNodeImplementation::Network`] by moving the nested nodes into the parent network.
|
/// Remove all nodes that contain [`DocumentNodeImplementation::Network`] by moving the nested nodes into the parent network.
|
||||||
|
|||||||
Reference in New Issue
Block a user