Add locking layer feature

This commit is contained in:
haikalvidya
2024-03-26 15:12:55 +07:00
parent 0a9bd41be1
commit 0cb9398e30
12 changed files with 100 additions and 1 deletions

View File

@@ -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,

View File

@@ -478,6 +478,35 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> 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<Message>) {
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 });
}

View File

@@ -85,6 +85,7 @@ pub struct FrontendNode {
pub exposed_outputs: Vec<FrontendGraphOutput>,
pub position: (i32, i32),
pub disabled: bool,
pub locked: bool,
pub previewed: bool,
pub errors: Option<String>,
}

View File

@@ -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<NodeId>,
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<Item = LayerNodeIdentifier> + '_ {
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<Item = LayerNodeIdentifier> + '_ {
metadata.all_layers().filter(|layer| self.0.contains(&layer.to_node()))
}

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M8,3C3,3,0,8,0,8s3,5,8,5s8-5,8-5S13,3,8,3z M8,12c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S10.2,12,8,12z" />
<circle cx="9" cy="7" r="1.2" />
</svg>

After

Width:  |  Height:  |  Size: 216 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M8,4c3.5,0,5.9,2.8,6.8,4c-0.9,1.2-3.3,4-6.8,4S2.1,9.2,1.2,8C2.1,6.8,4.5,4,8,4 M8,3C3,3,0,8,0,8s3,5,8,5s8-5,8-5S13,3,8,3L8,3z" />
</svg>

After

Width:  |  Height:  |  Size: 207 B

View File

@@ -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"}
/>
<IconButton
class={"visibility"}
action={(e) => (toggleLayerLock(listing.entry.id), e?.stopPropagation())}
size={24}
icon={listing.entry.locked ? "Lock" : "Unlock"}
tooltip={listing.entry.locked ? "Locked" : "Unlocked"}
/>
</LayoutRow>
{/each}
</LayoutCol>

View File

@@ -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 },

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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(),

View File

@@ -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<NodeId>,
pub locked: Vec<NodeId>,
/// 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<Vec<NodeOutput>>,
}
@@ -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!(