From 0cb9398e30c372566c457726d5fe94e605266de5 Mon Sep 17 00:00:00 2001 From: haikalvidya Date: Tue, 26 Mar 2024 15:12:55 +0700 Subject: [PATCH] Add locking layer feature --- .../document/node_graph/node_graph_message.rs | 8 ++++ .../node_graph/node_graph_message_handler.rs | 43 ++++++++++++++++++- .../document/node_graph/utility_types.rs | 1 + .../portfolio/document/utility_types/nodes.rs | 9 ++++ frontend/assets/icon-16px-solid/lock.svg | 4 ++ frontend/assets/icon-16px-solid/unlock.svg | 3 ++ frontend/src/components/panels/Layers.svelte | 11 +++++ frontend/src/utility-functions/icons.ts | 4 ++ frontend/src/wasm-communication/messages.ts | 4 ++ frontend/wasm/src/editor_api.rs | 8 ++++ node-graph/compilation-client/src/main.rs | 1 + node-graph/graph-craft/src/document.rs | 5 +++ 12 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 frontend/assets/icon-16px-solid/lock.svg create mode 100644 frontend/assets/icon-16px-solid/unlock.svg diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index b22d1535e8..33f9473d97 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -97,6 +97,14 @@ pub enum NodeGraphMessage { node_id: NodeId, hidden: bool, }, + ToggleSelectedLocked, + ToggleLocked { + node_id: NodeId, + }, + SetLocked { + node_id: NodeId, + locked: bool, + }, SetName { node_id: NodeId, name: String, diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index a0a8333489..6fdf435fd1 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -478,6 +478,35 @@ impl<'a> MessageHandler> for NodeGrap } self.update_selection_action_buttons(document_network, selected_nodes, responses); } + NodeGraphMessage::ToggleSelectedLocked => { + if let Some(network) = document_network.nested_network(&self.network) { + responses.add(DocumentMessage::StartTransaction); + + let new_locked = !selected_nodes.selected_nodes().any(|id| network.locked.contains(id)); + for &node_id in selected_nodes.selected_nodes() { + responses.add(NodeGraphMessage::SetLocked { node_id, locked: new_locked }); + } + } + } + NodeGraphMessage::ToggleLocked { node_id } => { + if let Some(network) = document_network.nested_network(&self.network) { + let new_locked = !network.locked.contains(&node_id); + responses.add(NodeGraphMessage::SetLocked { node_id, locked: new_locked }); + } + } + NodeGraphMessage::SetLocked { node_id, locked } => { + if let Some(network) = document_network.nested_network_mut(&self.network) { + if !locked { + network.locked.retain(|&id| node_id != id); + } else if !network.imports.contains(&node_id) && !network.original_outputs().iter().any(|output| output.node_id == node_id) { + network.locked.push(node_id); + } + if network.connected_to_output(node_id) { + responses.add(NodeGraphMessage::RunDocumentGraph); + } + } + self.update_selection_action_buttons(document_network, selected_nodes, responses); + } NodeGraphMessage::SetName { node_id, name } => { responses.add(DocumentMessage::StartTransaction); responses.add(NodeGraphMessage::SetNameImpl { node_id, name }); @@ -554,7 +583,7 @@ impl NodeGraphMessageHandler { }); } - /// Updates the buttons for disable and preview + /// Updates the buttons for disable, locked and preview fn update_selection_action_buttons(&mut self, document_network: &NodeNetwork, selected_nodes: &SelectedNodes, responses: &mut VecDeque) { if let Some(network) = document_network.nested_network(&self.network) { let mut widgets = Vec::new(); @@ -566,6 +595,7 @@ impl NodeGraphMessageHandler { if selection.next().is_some() { // Check if any of the selected nodes are disabled let is_hidden = selected_nodes.selected_nodes().any(|id| network.disabled.contains(id)); + let is_locked = selected_nodes.selected_nodes().any(|id| network.locked.contains(id)); // Check if multiple nodes are selected let multiple_nodes = selection.next().is_some(); @@ -580,6 +610,15 @@ impl NodeGraphMessageHandler { .widget_holder(); widgets.push(hide_button); + let (lock_unlock_label, lock_unlock_icon) = if is_locked { ("Make Unlock", "Lock") } else { ("Make Lock", "Unlock") }; + let lock_button = TextButton::new(lock_unlock_label) + .icon(Some(lock_unlock_icon.to_string())) + .tooltip(if is_locked { "Unlock selected nodes/layers" } else { "Lock selected nodes/layers" }.to_string() + if multiple_nodes { "s" } else { "" }) + .tooltip_shortcut(action_keys!(NodeGraphMessageDiscriminant::ToggleLocked)) + .on_update(move |_| NodeGraphMessage::ToggleSelectedLocked.into()) + .widget_holder(); + widgets.push(lock_button); + widgets.push(Separator::new(SeparatorType::Related).widget_holder()); } @@ -746,6 +785,7 @@ impl NodeGraphMessageHandler { position: node.metadata.position.into(), previewed: network.outputs_contain(node_id), disabled: network.disabled.contains(&node_id), + locked: network.locked.contains(&node_id), errors: errors.map(|e| format!("{e:?}")), }); } @@ -775,6 +815,7 @@ impl NodeGraphMessageHandler { name: network.nodes.get(&node_id).map(|node| node.alias.clone()).unwrap_or_default(), tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() }, disabled: network.disabled.contains(&node_id), + locked: network.locked.contains(&node_id), }; responses.add(FrontendMessage::UpdateDocumentLayerDetails { data }); } diff --git a/editor/src/messages/portfolio/document/node_graph/utility_types.rs b/editor/src/messages/portfolio/document/node_graph/utility_types.rs index d528b377bf..8c2f70c54b 100644 --- a/editor/src/messages/portfolio/document/node_graph/utility_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/utility_types.rs @@ -85,6 +85,7 @@ pub struct FrontendNode { pub exposed_outputs: Vec, pub position: (i32, i32), pub disabled: bool, + pub locked: bool, pub previewed: bool, pub errors: Option, } diff --git a/editor/src/messages/portfolio/document/utility_types/nodes.rs b/editor/src/messages/portfolio/document/utility_types/nodes.rs index ffeb5cd5b4..5674d91ec3 100644 --- a/editor/src/messages/portfolio/document/utility_types/nodes.rs +++ b/editor/src/messages/portfolio/document/utility_types/nodes.rs @@ -47,6 +47,7 @@ pub struct LayerPanelEntry { pub layer_classification: LayerClassification, pub expanded: bool, pub disabled: bool, + pub locked: bool, #[serde(rename = "parentId")] pub parent_id: Option, pub depth: usize, @@ -64,6 +65,14 @@ impl SelectedNodes { self.selected_layers(metadata).filter(move |&layer| self.layer_visible(layer, network, metadata)) } + pub fn layer_ulocked(&self, layer: LayerNodeIdentifier, network: &NodeNetwork, metadata: &DocumentMetadata) -> bool { + !layer.ancestors(metadata).any(|layer| network.locked.contains(&layer.to_node())) + } + + pub fn selected_ulocked_layers<'a>(&'a self, network: &'a NodeNetwork, metadata: &'a DocumentMetadata) -> impl Iterator + '_ { + self.selected_layers(metadata).filter(move |&layer| self.layer_ulocked(layer, network, metadata)) + } + pub fn selected_layers<'a>(&'a self, metadata: &'a DocumentMetadata) -> impl Iterator + '_ { metadata.all_layers().filter(|layer| self.0.contains(&layer.to_node())) } diff --git a/frontend/assets/icon-16px-solid/lock.svg b/frontend/assets/icon-16px-solid/lock.svg new file mode 100644 index 0000000000..6fbb87df15 --- /dev/null +++ b/frontend/assets/icon-16px-solid/lock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/assets/icon-16px-solid/unlock.svg b/frontend/assets/icon-16px-solid/unlock.svg new file mode 100644 index 0000000000..960def4e9b --- /dev/null +++ b/frontend/assets/icon-16px-solid/unlock.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index df5bc5a729..dce7fc8452 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -133,6 +133,10 @@ editor.instance.toggleLayerVisibility(id); } + function toggleLayerLock(id: bigint) { + editor.instance.toggleLayerLock(id); + } + function handleExpandArrowClick(id: bigint) { editor.instance.toggleLayerExpansion(id); } @@ -420,6 +424,13 @@ icon={listing.entry.disabled ? "EyeHidden" : "EyeVisible"} tooltip={listing.entry.disabled ? "Disabled" : "Enabled"} /> + (toggleLayerLock(listing.entry.id), e?.stopPropagation())} + size={24} + icon={listing.entry.locked ? "Lock" : "Unlock"} + tooltip={listing.entry.locked ? "Locked" : "Unlocked"} + /> {/each} diff --git a/frontend/src/utility-functions/icons.ts b/frontend/src/utility-functions/icons.ts index 6abb9c6d6e..05b562da8f 100644 --- a/frontend/src/utility-functions/icons.ts +++ b/frontend/src/utility-functions/icons.ts @@ -119,6 +119,7 @@ import IconsGrid from "@graphite-frontend/assets/icon-16px-solid/icons-grid.svg" import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg"; import Layer from "@graphite-frontend/assets/icon-16px-solid/layer.svg"; import License from "@graphite-frontend/assets/icon-16px-solid/license.svg"; +import Lock from "@graphite-frontend/assets/icon-16px-solid/lock.svg"; import NodeBlur from "@graphite-frontend/assets/icon-16px-solid/node-blur.svg"; import NodeBrushwork from "@graphite-frontend/assets/icon-16px-solid/node-brushwork.svg"; import NodeColorCorrection from "@graphite-frontend/assets/icon-16px-solid/node-color-correction.svg"; @@ -140,6 +141,7 @@ import Rescale from "@graphite-frontend/assets/icon-16px-solid/rescale.svg"; import Reset from "@graphite-frontend/assets/icon-16px-solid/reset.svg"; import Settings from "@graphite-frontend/assets/icon-16px-solid/settings.svg"; import Trash from "@graphite-frontend/assets/icon-16px-solid/trash.svg"; +import Unlock from "@graphite-frontend/assets/icon-16px-solid/unlock.svg"; import ViewModeNormal from "@graphite-frontend/assets/icon-16px-solid/view-mode-normal.svg"; import ViewModeOutline from "@graphite-frontend/assets/icon-16px-solid/view-mode-outline.svg"; import ViewModePixels from "@graphite-frontend/assets/icon-16px-solid/view-mode-pixels.svg"; @@ -187,6 +189,7 @@ const SOLID_16PX = { Image: { svg: Image, size: 16 }, Layer: { svg: Layer, size: 16 }, License: { svg: License, size: 16 }, + Lock: { svg: Lock, size: 16 }, NodeBlur: { svg: NodeBlur, size: 16 }, NodeBrushwork: { svg: NodeBrushwork, size: 16 }, NodeColorCorrection: { svg: NodeColorCorrection, size: 16 }, @@ -208,6 +211,7 @@ const SOLID_16PX = { Reset: { svg: Reset, size: 16 }, Settings: { svg: Settings, size: 16 }, Trash: { svg: Trash, size: 16 }, + Unlock: { svg: Unlock, size: 16 }, ViewModeNormal: { svg: ViewModeNormal, size: 16 }, ViewModeOutline: { svg: ViewModeOutline, size: 16 }, ViewModePixels: { svg: ViewModePixels, size: 16 }, diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index 3df903e1f2..547cc96078 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -126,6 +126,8 @@ export class FrontendNode { readonly disabled!: boolean; + readonly locked!: boolean; + readonly errors!: string | undefined; } @@ -615,6 +617,8 @@ export class LayerPanelEntry { disabled!: boolean; + locked!: boolean; + parentId!: bigint | undefined; id!: bigint; diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 99631d0426..acadbb8e4c 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -762,6 +762,14 @@ impl JsEditorHandle { self.dispatch(message); } + /// Toggle lock state of a layer from the layer list + #[wasm_bindgen(js_name = toggleLayerLock)] + pub fn toggle_layer_lock(&self, id: u64) { + let id = NodeId(id); + let message = NodeGraphMessage::ToggleLocked { node_id: id }; + self.dispatch(message); + } + /// Toggle expansions state of a layer from the layer list #[wasm_bindgen(js_name = toggleLayerExpansion)] pub fn toggle_layer_expansion(&self, id: u64) { diff --git a/node-graph/compilation-client/src/main.rs b/node-graph/compilation-client/src/main.rs index 72f1593f07..8976fe3dd3 100644 --- a/node-graph/compilation-client/src/main.rs +++ b/node-graph/compilation-client/src/main.rs @@ -40,6 +40,7 @@ fn add_network() -> NodeNetwork { imports: vec![], exports: vec![NodeOutput::new(NodeId(0), 0)], disabled: vec![], + locked: vec![], previous_outputs: None, nodes: [DocumentNode { name: "Blend Image".into(), diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index dddfc08662..3b8244f249 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -526,6 +526,7 @@ pub struct NodeNetwork { /// Nodes that the user has disabled/hidden with the visibility eye icon. /// These nodes get replaced with Identity nodes during the graph flattening step. pub disabled: Vec, + pub locked: Vec, /// In the case when another node is previewed (chosen by the user as a temporary output), this stores what it previously was so it can be restored later. pub previous_outputs: Option>, } @@ -541,6 +542,7 @@ impl std::hash::Hash for NodeNetwork { node.hash(state); } self.disabled.hash(state); + self.locked.hash(state); self.previous_outputs.hash(state); } } @@ -568,6 +570,7 @@ impl NodeNetwork { exports: vec![NodeOutput::new(NodeId(0), 0)], nodes: [(NodeId(0), node)].into_iter().collect(), disabled: vec![], + locked: vec![], previous_outputs: None, } } @@ -816,6 +819,7 @@ impl NodeNetwork { self.imports.iter_mut().for_each(|id| *id = f(*id)); self.exports.iter_mut().for_each(|output| output.node_id = f(output.node_id)); self.disabled.iter_mut().for_each(|id| *id = f(*id)); + self.locked.iter_mut().for_each(|id| *id = f(*id)); self.previous_outputs .iter_mut() .for_each(|nodes| nodes.iter_mut().for_each(|output| output.node_id = f(output.node_id))); @@ -984,6 +988,7 @@ impl NodeNetwork { // Copy nodes from the inner network into the parent network self.nodes.extend(inner_network.nodes); self.disabled.extend(inner_network.disabled); + self.locked.extend(inner_network.locked); let mut network_offsets = HashMap::new(); assert_eq!(