Add layer locking feature (#1702)

* Add locking layer feature

* Update locked state data to adjust the refactor

* Make the locked layer cannot be selected using pointer and select all key

* Make locked layer cannot be moved and disable bounding box

* Add locked status selected node on CopyBuffer

* Make locked layer cannot be selected when selected all layers, and disabled GRS and nudging operation on locked layer

* Add refresh document metadata before update button on visible and locked

* Updated from master

* Fix icon logic on panel locked layer

* Make the child locked when the parent is locked, and the child cannot be unlocked if the parent is locked

* Revert "Make the child locked when the parent is locked, and the child cannot be unlocked if the parent is locked"

This reverts commit 7c93259bc2.

* Revert "Fix icon logic on panel locked layer"

This reverts commit 33939f2e84.

* Delete Make Lock button in the node graph top bar

* Add ToggleSelectedLocked to action_with_node_graph_open

* Fix parent and child locking behavior icon on panel

* Fix boolean operator on icon button locking layer

* Make bolean logic more readable in icon button locking layer

* Fix locking layer can be moved or resizing when selected with unlocking layer, disabled pivot widget on locking layer, disable all action on pivot point, alignment, flipping, and boolean operation for locking layer

* Fix axis align drag crash

---------

Co-authored-by: 0hypercube <0hypercube@gmail.com>
This commit is contained in:
Haikal 2024-04-04 05:50:23 +07:00 committed by Keavon Chambers
parent 0f43a254af
commit bf81a31ff9
17 changed files with 142 additions and 32 deletions

View file

@ -58,6 +58,7 @@ pub fn default_mapping() -> Mapping {
entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=NodeGraphMessage::Copy), entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=NodeGraphMessage::Copy),
entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes), entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes),
entry!(KeyDown(KeyH); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedVisibility), entry!(KeyDown(KeyH); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedVisibility),
entry!(KeyDown(KeyL); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedLocked),
// //
// TransformLayerMessage // TransformLayerMessage
entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation), entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation),

View file

@ -190,7 +190,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
AlignAggregate::Max => combined_box[1], AlignAggregate::Max => combined_box[1],
AlignAggregate::Center => (combined_box[0] + combined_box[1]) / 2., AlignAggregate::Center => (combined_box[0] + combined_box[1]) / 2.,
}; };
for layer in self.selected_nodes.selected_layers(self.metadata()) { for layer in self.selected_nodes.selected_unlocked_layers(self.metadata()) {
let Some(bbox) = self.metadata().bounding_box_viewport(layer) else { let Some(bbox) = self.metadata().bounding_box_viewport(layer) else {
continue; continue;
}; };
@ -313,10 +313,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
FlipAxis::X => DVec2::new(-1., 1.), FlipAxis::X => DVec2::new(-1., 1.),
FlipAxis::Y => DVec2::new(1., -1.), FlipAxis::Y => DVec2::new(1., -1.),
}; };
if let Some([min, max]) = self.selected_visible_layers_bounding_box_viewport() { if let Some([min, max]) = self.selected_visible_and_unlock_layers_bounding_box_viewport() {
let center = (max + min) / 2.; let center = (max + min) / 2.;
let bbox_trans = DAffine2::from_translation(-center); let bbox_trans = DAffine2::from_translation(-center);
for layer in self.selected_nodes.selected_layers(self.metadata()) { for layer in self.selected_nodes.selected_unlocked_layers(self.metadata()) {
responses.add(GraphOperationMessage::TransformChange { responses.add(GraphOperationMessage::TransformChange {
layer, layer,
transform: DAffine2::from_scale(scale), transform: DAffine2::from_scale(scale),
@ -463,7 +463,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
for layer in self for layer in self
.selected_nodes .selected_nodes
.selected_layers(self.metadata()) .selected_layers(self.metadata())
.filter(|&layer| self.selected_nodes.layer_visible(layer, self.metadata())) .filter(|&layer| self.selected_nodes.layer_visible(layer, self.metadata()) && !self.selected_nodes.layer_locked(layer, self.metadata()))
{ {
responses.add(GraphOperationMessage::TransformChange { responses.add(GraphOperationMessage::TransformChange {
layer, layer,
@ -498,7 +498,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
for layer in self for layer in self
.selected_nodes .selected_nodes
.selected_layers(self.metadata()) .selected_layers(self.metadata())
.filter(|&layer| self.selected_nodes.layer_visible(layer, self.metadata())) .filter(|&layer| self.selected_nodes.layer_visible(layer, self.metadata()) && !self.selected_nodes.layer_locked(layer, self.metadata()))
{ {
let to = self.metadata().document_to_viewport.inverse() * self.metadata().downstream_transform_to_viewport(layer); let to = self.metadata().document_to_viewport.inverse() * self.metadata().downstream_transform_to_viewport(layer);
let original_transform = self.metadata().upstream_transform(layer.to_node()); let original_transform = self.metadata().upstream_transform(layer.to_node());
@ -622,11 +622,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
} }
DocumentMessage::SelectAllLayers => { DocumentMessage::SelectAllLayers => {
let metadata = self.metadata(); let metadata = self.metadata();
let all_layers_except_artboards_and_invisible = metadata let all_layers_except_artboards_invisible_and_locked = metadata
.all_layers() .all_layers()
.filter(move |&layer| !metadata.is_artboard(layer)) .filter(move |&layer| !metadata.is_artboard(layer))
.filter(|&layer| self.selected_nodes.layer_visible(layer, metadata)); .filter(|&layer| self.selected_nodes.layer_visible(layer, metadata) && !self.selected_nodes.layer_locked(layer, metadata));
let nodes = all_layers_except_artboards_and_invisible.map(|layer| layer.to_node()).collect(); let nodes = all_layers_except_artboards_invisible_and_locked.map(|layer| layer.to_node()).collect();
responses.add(NodeGraphMessage::SelectedNodesSet { nodes }); responses.add(NodeGraphMessage::SelectedNodesSet { nodes });
} }
DocumentMessage::SelectedLayersLower => { DocumentMessage::SelectedLayersLower => {
@ -834,6 +834,7 @@ impl DocumentMessageHandler {
.root() .root()
.descendants(&self.metadata) .descendants(&self.metadata)
.filter(|&layer| self.selected_nodes.layer_visible(layer, self.metadata())) .filter(|&layer| self.selected_nodes.layer_visible(layer, self.metadata()))
.filter(|&layer| !self.selected_nodes.layer_locked(layer, self.metadata()))
.filter(|&layer| !is_artboard(layer, network)) .filter(|&layer| !is_artboard(layer, network))
.filter_map(|layer| self.metadata.click_target(layer).map(|targets| (layer, targets))) .filter_map(|layer| self.metadata.click_target(layer).map(|targets| (layer, targets)))
.filter(move |(layer, target)| target.iter().any(move |target| target.intersect_rectangle(document_quad, self.metadata.transform_to_document(*layer)))) .filter(move |(layer, target)| target.iter().any(move |target| target.intersect_rectangle(document_quad, self.metadata.transform_to_document(*layer))))
@ -847,6 +848,7 @@ impl DocumentMessageHandler {
.root() .root()
.descendants(&self.metadata) .descendants(&self.metadata)
.filter(|&layer| self.selected_nodes.layer_visible(layer, self.metadata())) .filter(|&layer| self.selected_nodes.layer_visible(layer, self.metadata()))
.filter(|&layer| !self.selected_nodes.layer_locked(layer, self.metadata()))
.filter_map(|layer| self.metadata.click_target(layer).map(|targets| (layer, targets))) .filter_map(|layer| self.metadata.click_target(layer).map(|targets| (layer, targets)))
.filter(move |(layer, target)| target.iter().any(|target: &ClickTarget| target.intersect_point(point, self.metadata.transform_to_document(*layer)))) .filter(move |(layer, target)| target.iter().any(|target: &ClickTarget| target.intersect_point(point, self.metadata.transform_to_document(*layer))))
.map(|(layer, _)| layer) .map(|(layer, _)| layer)
@ -865,6 +867,13 @@ impl DocumentMessageHandler {
.reduce(graphene_core::renderer::Quad::combine_bounds) .reduce(graphene_core::renderer::Quad::combine_bounds)
} }
pub fn selected_visible_and_unlock_layers_bounding_box_viewport(&self) -> Option<[DVec2; 2]> {
self.selected_nodes
.selected_visible_and_unlocked_layers(self.metadata())
.filter_map(|layer| self.metadata.bounding_box_viewport(layer))
.reduce(graphene_core::renderer::Quad::combine_bounds)
}
pub fn network(&self) -> &NodeNetwork { pub fn network(&self) -> &NodeNetwork {
&self.network &self.network
} }
@ -1361,7 +1370,7 @@ impl DocumentMessageHandler {
let has_selection = self.selected_nodes.selected_layers(self.metadata()).next().is_some(); let has_selection = self.selected_nodes.selected_layers(self.metadata()).next().is_some();
let selection_all_visible = self.selected_nodes.selected_layers(self.metadata()).all(|layer| self.metadata().node_is_visible(layer.to_node())); let selection_all_visible = self.selected_nodes.selected_layers(self.metadata()).all(|layer| self.metadata().node_is_visible(layer.to_node()));
let selection_all_locked = false; // TODO: Implement let selection_all_locked = self.selected_nodes.selected_layers(self.metadata()).all(|layer| self.metadata().node_is_locked(layer.to_node()));
let layers_panel_options_bar = WidgetLayout::new(vec![LayoutGroup::Row { let layers_panel_options_bar = WidgetLayout::new(vec![LayoutGroup::Row {
widgets: vec![ widgets: vec![
@ -1415,8 +1424,8 @@ impl DocumentMessageHandler {
IconButton::new(if selection_all_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24) IconButton::new(if selection_all_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24)
.hover_icon(Some((if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" }).into())) .hover_icon(Some((if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" }).into()))
.tooltip(if selection_all_locked { "Unlock Selected" } else { "Lock Selected" }) .tooltip(if selection_all_locked { "Unlock Selected" } else { "Lock Selected" })
.tooltip_shortcut(action_keys!(DialogMessageDiscriminant::RequestComingSoonDialog)) .tooltip_shortcut(action_keys!(NodeGraphMessageDiscriminant::ToggleSelectedLocked))
.on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1127) }.into()) .on_update(|_| NodeGraphMessage::ToggleSelectedLocked.into())
.disabled(!has_selection) .disabled(!has_selection)
.widget_holder(), .widget_holder(),
IconButton::new(if selection_all_visible { "EyeVisible" } else { "EyeHidden" }, 24) IconButton::new(if selection_all_visible { "EyeVisible" } else { "EyeHidden" }, 24)

View file

@ -97,6 +97,14 @@ pub enum NodeGraphMessage {
node_id: NodeId, node_id: NodeId,
visible: bool, visible: bool,
}, },
ToggleSelectedLocked,
ToggleLocked {
node_id: NodeId,
},
SetLocked {
node_id: NodeId,
locked: bool,
},
SetName { SetName {
node_id: NodeId, node_id: NodeId,
name: String, name: String,

View file

@ -487,6 +487,40 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(NodeGraphMessage::RunDocumentGraph); responses.add(NodeGraphMessage::RunDocumentGraph);
} }
})(); })();
document_metadata.load_structure(document_network, selected_nodes);
self.update_selection_action_buttons(document_network, document_metadata, selected_nodes, responses);
}
NodeGraphMessage::ToggleSelectedLocked => {
responses.add(DocumentMessage::StartTransaction);
let is_locked = !selected_nodes.selected_nodes().any(|&id| document_metadata.node_is_locked(id));
for &node_id in selected_nodes.selected_nodes() {
responses.add(NodeGraphMessage::SetLocked { node_id, locked: is_locked });
}
}
NodeGraphMessage::ToggleLocked { node_id } => {
responses.add(DocumentMessage::StartTransaction);
let is_locked = !document_metadata.node_is_locked(node_id);
responses.add(NodeGraphMessage::SetLocked { node_id, locked: is_locked });
}
NodeGraphMessage::SetLocked { node_id, locked } => {
if let Some(network) = document_network.nested_network_mut(&self.network) {
let is_locked = if !locked {
false
} else if !network.imports.contains(&node_id) && !network.original_outputs().iter().any(|output| output.node_id == node_id) {
true
} else {
return;
};
let Some(node) = network.nodes.get_mut(&node_id) else { return };
node.locked = is_locked;
if network.connected_to_output(node_id) {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
document_metadata.load_structure(document_network, selected_nodes);
self.update_selection_action_buttons(document_network, document_metadata, selected_nodes, responses); self.update_selection_action_buttons(document_network, document_metadata, selected_nodes, responses);
} }
NodeGraphMessage::SetName { node_id, name } => { NodeGraphMessage::SetName { node_id, name } => {
@ -551,9 +585,9 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
impl NodeGraphMessageHandler { impl NodeGraphMessageHandler {
pub fn actions_with_node_graph_open(&self, graph_open: bool) -> ActionList { pub fn actions_with_node_graph_open(&self, graph_open: bool) -> ActionList {
if self.has_selection && graph_open { if self.has_selection && graph_open {
actions!(NodeGraphMessageDiscriminant; ToggleSelectedVisibility, DuplicateSelectedNodes, DeleteSelectedNodes, Cut, Copy) actions!(NodeGraphMessageDiscriminant; ToggleSelectedVisibility, ToggleSelectedLocked, DuplicateSelectedNodes, DeleteSelectedNodes, Cut, Copy)
} else if self.has_selection { } else if self.has_selection {
actions!(NodeGraphMessageDiscriminant; ToggleSelectedVisibility) actions!(NodeGraphMessageDiscriminant; ToggleSelectedVisibility, ToggleSelectedLocked)
} else { } else {
actions!(NodeGraphMessageDiscriminant;) actions!(NodeGraphMessageDiscriminant;)
} }
@ -567,7 +601,7 @@ impl NodeGraphMessageHandler {
}); });
} }
/// Updates the buttons for visibility and preview /// Updates the buttons for visibility, locked, and preview
fn update_selection_action_buttons(&mut self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, selected_nodes: &SelectedNodes, responses: &mut VecDeque<Message>) { fn update_selection_action_buttons(&mut self, document_network: &NodeNetwork, document_metadata: &DocumentMetadata, selected_nodes: &SelectedNodes, responses: &mut VecDeque<Message>) {
if let Some(network) = document_network.nested_network(&self.network) { if let Some(network) = document_network.nested_network(&self.network) {
let mut widgets = Vec::new(); let mut widgets = Vec::new();
@ -759,6 +793,7 @@ impl NodeGraphMessageHandler {
position: node.metadata.position.into(), position: node.metadata.position.into(),
previewed: network.outputs_contain(node_id), previewed: network.outputs_contain(node_id),
visible: node.visible, visible: node.visible,
locked: node.locked,
errors: errors.map(|e| format!("{e:?}")), errors: errors.map(|e| format!("{e:?}")),
}); });
} }
@ -784,6 +819,11 @@ impl NodeGraphMessageHandler {
.filter(|&ancestor| ancestor != layer) .filter(|&ancestor| ancestor != layer)
.all(|layer| network.nodes.get(&layer.to_node()).map(|node| node.visible).unwrap_or_default()); .all(|layer| network.nodes.get(&layer.to_node()).map(|node| node.visible).unwrap_or_default());
let parents_unlocked = layer
.ancestors(metadata)
.filter(|&ancestor| ancestor != layer)
.all(|layer| network.nodes.get(&layer.to_node()).map(|node| !node.locked).unwrap_or_default());
let data = LayerPanelEntry { let data = LayerPanelEntry {
id: node_id, id: node_id,
layer_classification, layer_classification,
@ -795,8 +835,8 @@ impl NodeGraphMessageHandler {
tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() }, tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() },
visible: node.visible, visible: node.visible,
parents_visible, parents_visible,
unlocked: true, unlocked: !node.locked,
parents_unlocked: true, parents_unlocked,
}; };
responses.add(FrontendMessage::UpdateDocumentLayerDetails { data }); responses.add(FrontendMessage::UpdateDocumentLayerDetails { data });
} }

View file

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

View file

@ -20,6 +20,7 @@ pub struct CopyBufferEntry {
pub nodes: HashMap<NodeId, DocumentNode>, pub nodes: HashMap<NodeId, DocumentNode>,
pub selected: bool, pub selected: bool,
pub visible: bool, pub visible: bool,
pub locked: bool,
pub collapsed: bool, pub collapsed: bool,
pub alias: String, pub alias: String,
} }

View file

@ -23,6 +23,7 @@ pub struct DocumentMetadata {
artboards: HashSet<LayerNodeIdentifier>, artboards: HashSet<LayerNodeIdentifier>,
folders: HashSet<LayerNodeIdentifier>, folders: HashSet<LayerNodeIdentifier>,
hidden: HashSet<NodeId>, hidden: HashSet<NodeId>,
locked: HashSet<NodeId>,
click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>, click_targets: HashMap<LayerNodeIdentifier, Vec<ClickTarget>>,
/// Transform from document space to viewport space. /// Transform from document space to viewport space.
pub document_to_viewport: DAffine2, pub document_to_viewport: DAffine2,
@ -36,6 +37,7 @@ impl Default for DocumentMetadata {
artboards: HashSet::new(), artboards: HashSet::new(),
folders: HashSet::new(), folders: HashSet::new(),
hidden: HashSet::new(), hidden: HashSet::new(),
locked: HashSet::new(),
click_targets: HashMap::new(), click_targets: HashMap::new(),
document_to_viewport: DAffine2::IDENTITY, document_to_viewport: DAffine2::IDENTITY,
} }
@ -126,6 +128,10 @@ impl DocumentMetadata {
!self.hidden.contains(&layer) !self.hidden.contains(&layer)
} }
pub fn node_is_locked(&self, layer: NodeId) -> bool {
self.locked.contains(&layer)
}
/// Folders sorted from most nested to least nested /// Folders sorted from most nested to least nested
pub fn folders_sorted_by_most_nested(&self, layers: impl Iterator<Item = LayerNodeIdentifier>) -> Vec<LayerNodeIdentifier> { pub fn folders_sorted_by_most_nested(&self, layers: impl Iterator<Item = LayerNodeIdentifier>) -> Vec<LayerNodeIdentifier> {
let mut folders: Vec<_> = layers.filter(|layer| self.folders.contains(layer)).collect(); let mut folders: Vec<_> = layers.filter(|layer| self.folders.contains(layer)).collect();
@ -149,6 +155,7 @@ impl DocumentMetadata {
self.artboards = HashSet::new(); self.artboards = HashSet::new();
self.folders = HashSet::new(); self.folders = HashSet::new();
self.hidden = HashSet::new(); self.hidden = HashSet::new();
self.locked = HashSet::new();
let id = graph.exports[0].node_id; let id = graph.exports[0].node_id;
let Some(output_node) = graph.nodes.get(&id) else { let Some(output_node) = graph.nodes.get(&id) else {
@ -180,6 +187,10 @@ impl DocumentMetadata {
if !current_node.visible { if !current_node.visible {
self.hidden.insert(current_node_id); self.hidden.insert(current_node_id);
} }
if current_node.locked {
self.locked.insert(current_node_id);
}
} }
// Get the sibling below // Get the sibling below

View file

@ -71,6 +71,19 @@ impl SelectedNodes {
self.selected_layers(metadata).filter(move |&layer| self.layer_visible(layer, metadata)) self.selected_layers(metadata).filter(move |&layer| self.layer_visible(layer, metadata))
} }
pub fn layer_locked(&self, layer: LayerNodeIdentifier, metadata: &DocumentMetadata) -> bool {
layer.ancestors(metadata).any(|layer| metadata.node_is_locked(layer.to_node()))
}
pub fn selected_unlocked_layers<'a>(&'a self, metadata: &'a DocumentMetadata) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
self.selected_layers(metadata).filter(move |&layer| !self.layer_locked(layer, metadata))
}
pub fn selected_visible_and_unlocked_layers<'a>(&'a self, metadata: &'a DocumentMetadata) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
self.selected_layers(metadata)
.filter(move |&layer| self.layer_visible(layer, metadata) && !self.layer_locked(layer, metadata))
}
pub fn selected_layers<'a>(&'a self, metadata: &'a DocumentMetadata) -> impl Iterator<Item = LayerNodeIdentifier> + '_ { 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())) metadata.all_layers().filter(|layer| self.0.contains(&layer.to_node()))
} }

View file

@ -203,6 +203,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
.collect(), .collect(),
selected: active_document.selected_nodes.selected_layers_contains(layer, active_document.metadata()), selected: active_document.selected_nodes.selected_layers_contains(layer, active_document.metadata()),
visible: active_document.selected_nodes.layer_visible(layer, active_document.metadata()), visible: active_document.selected_nodes.layer_visible(layer, active_document.metadata()),
locked: active_document.selected_nodes.layer_locked(layer, active_document.metadata()),
collapsed: false, collapsed: false,
alias: previous_alias, alias: previous_alias,
}); });
@ -391,6 +392,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageData<'_>> for PortfolioMes
if !entry.visible { if !entry.visible {
responses.add(NodeGraphMessage::SetVisibility { node_id: id, visible: false }); responses.add(NodeGraphMessage::SetVisibility { node_id: id, visible: false });
} }
if entry.locked {
responses.add(NodeGraphMessage::SetLocked { node_id: id, locked: true });
}
} }
}; };

View file

@ -45,7 +45,7 @@ impl Pivot {
/// Recomputes the pivot position and transform. /// Recomputes the pivot position and transform.
fn recalculate_pivot(&mut self, document: &DocumentMessageHandler) { fn recalculate_pivot(&mut self, document: &DocumentMessageHandler) {
let mut layers = document.selected_nodes.selected_visible_layers(document.metadata()); let mut layers = document.selected_nodes.selected_visible_and_unlocked_layers(document.metadata());
let Some(first) = layers.next() else { let Some(first) = layers.next() else {
// If no layers are selected then we revert things back to default // If no layers are selected then we revert things back to default
self.normalized_pivot = DVec2::splat(0.5); self.normalized_pivot = DVec2::splat(0.5);
@ -66,14 +66,14 @@ impl Pivot {
// If more than one layer is selected we use the AABB with the mean of the pivots // If more than one layer is selected we use the AABB with the mean of the pivots
let xy_summation = document let xy_summation = document
.selected_nodes .selected_nodes
.selected_visible_layers(document.metadata()) .selected_visible_and_unlocked_layers(document.metadata())
.map(|layer| graph_modification_utils::get_viewport_pivot(layer, &document.network, &document.metadata)) .map(|layer| graph_modification_utils::get_viewport_pivot(layer, &document.network, &document.metadata))
.reduce(|a, b| a + b) .reduce(|a, b| a + b)
.unwrap_or_default(); .unwrap_or_default();
let pivot = xy_summation / selected_layers_count as f64; let pivot = xy_summation / selected_layers_count as f64;
self.pivot = Some(pivot); self.pivot = Some(pivot);
let [min, max] = document.selected_visible_layers_bounding_box_viewport().unwrap_or([DVec2::ZERO, DVec2::ONE]); let [min, max] = document.selected_visible_and_unlock_layers_bounding_box_viewport().unwrap_or([DVec2::ZERO, DVec2::ONE]);
self.normalized_pivot = (pivot - min) / (max - min); self.normalized_pivot = (pivot - min) / (max - min);
self.transform_from_normalized = DAffine2::from_translation(min) * DAffine2::from_scale(max - min); self.transform_from_normalized = DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
@ -101,7 +101,7 @@ impl Pivot {
/// Sets the viewport position of the pivot for all selected layers. /// Sets the viewport position of the pivot for all selected layers.
pub fn set_viewport_position(&self, position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn set_viewport_position(&self, position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
for layer in document.selected_nodes.selected_visible_layers(document.metadata()) { for layer in document.selected_nodes.selected_visible_and_unlocked_layers(document.metadata()) {
let transform = Self::get_layer_pivot_transform(layer, document); let transform = Self::get_layer_pivot_transform(layer, document);
let pivot = transform.inverse().transform_point2(position); let pivot = transform.inverse().transform_point2(position);
// Only update the pivot when computed position is finite. Infinite can happen when scale is 0. // Only update the pivot when computed position is finite. Infinite can happen when scale is 0.

View file

@ -206,7 +206,11 @@ pub fn axis_align_drag(axis_align: bool, position: DVec2, start: DVec2) -> DVec2
let snap_resolution = SELECTION_DRAG_ANGLE.to_radians(); let snap_resolution = SELECTION_DRAG_ANGLE.to_radians();
let angle = -mouse_position.angle_between(DVec2::X); let angle = -mouse_position.angle_between(DVec2::X);
let snapped_angle = (angle / snap_resolution).round() * snap_resolution; let snapped_angle = (angle / snap_resolution).round() * snap_resolution;
DVec2::new(snapped_angle.cos(), snapped_angle.sin()) * mouse_position.length() + start if snapped_angle.is_finite() {
start + DVec2::new(snapped_angle.cos(), snapped_angle.sin()) * mouse_position.length()
} else {
start
}
} else { } else {
position position
} }

View file

@ -386,12 +386,12 @@ impl Fsm for SelectToolFsmState {
(_, SelectToolMessage::Overlays(mut overlay_context)) => { (_, SelectToolMessage::Overlays(mut overlay_context)) => {
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
let selected_layers_count = document.selected_nodes.selected_layers(document.metadata()).count(); let selected_layers_count = document.selected_nodes.selected_unlocked_layers(document.metadata()).count();
tool_data.selected_layers_changed = selected_layers_count != tool_data.selected_layers_count; tool_data.selected_layers_changed = selected_layers_count != tool_data.selected_layers_count;
tool_data.selected_layers_count = selected_layers_count; tool_data.selected_layers_count = selected_layers_count;
// Outline selected layers // Outline selected layers
for layer in document.selected_nodes.selected_visible_layers(document.metadata()) { for layer in document.selected_nodes.selected_visible_and_unlocked_layers(document.metadata()) {
overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer));
} }
@ -405,13 +405,13 @@ impl Fsm for SelectToolFsmState {
// Update bounds // Update bounds
let transform = document let transform = document
.selected_nodes .selected_nodes
.selected_visible_layers(document.metadata()) .selected_visible_and_unlocked_layers(document.metadata())
.next() .next()
.map(|layer| document.metadata().transform_to_viewport(layer)); .map(|layer| document.metadata().transform_to_viewport(layer));
let transform = transform.unwrap_or(DAffine2::IDENTITY); let transform = transform.unwrap_or(DAffine2::IDENTITY);
let bounds = document let bounds = document
.selected_nodes .selected_nodes
.selected_visible_layers(document.metadata()) .selected_visible_and_unlocked_layers(document.metadata())
.filter_map(|layer| { .filter_map(|layer| {
document document
.metadata() .metadata()
@ -472,7 +472,7 @@ impl Fsm for SelectToolFsmState {
.map(|bounding_box| bounding_box.check_rotate(input.mouse.position)) .map(|bounding_box| bounding_box.check_rotate(input.mouse.position))
.unwrap_or_default(); .unwrap_or_default();
let mut selected: Vec<_> = document.selected_nodes.selected_visible_layers(document.metadata()).collect(); let mut selected: Vec<_> = document.selected_nodes.selected_visible_and_unlocked_layers(document.metadata()).collect();
let intersection = document.click(input.mouse.position, &document.network); let intersection = document.click(input.mouse.position, &document.network);
// If the user is dragging the bounding box bounds, go into ResizingBounds mode. // If the user is dragging the bounding box bounds, go into ResizingBounds mode.
@ -626,7 +626,7 @@ impl Fsm for SelectToolFsmState {
let snapped = if axis_align { let snapped = if axis_align {
let constraint = SnapConstraint::Line { let constraint = SnapConstraint::Line {
origin: point.document_point, origin: point.document_point,
direction: total_mouse_delta_document.normalize(), direction: total_mouse_delta_document.try_normalize().unwrap_or(DVec2::X),
}; };
tool_data.snap_manager.constrained_snap(&snap_data, point, constraint, None) tool_data.snap_manager.constrained_snap(&snap_data, point, constraint, None)
} else { } else {

View file

@ -48,7 +48,7 @@ impl<'a> MessageHandler<TransformLayerMessage, TransformData<'a>> for TransformL
let selected_layers = document let selected_layers = document
.selected_nodes .selected_nodes
.selected_layers(document.metadata()) .selected_layers(document.metadata())
.filter(|&layer| document.metadata().node_is_visible(layer.to_node())) .filter(|&layer| document.metadata().node_is_visible(layer.to_node()) && !document.metadata().node_is_locked(layer.to_node()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut selected = Selected::new( let mut selected = Selected::new(

View file

@ -133,6 +133,10 @@
editor.instance.toggleLayerVisibility(id); editor.instance.toggleLayerVisibility(id);
} }
function toggleLayerLock(id: bigint) {
editor.instance.toggleLayerLock(id);
}
function handleExpandArrowClick(id: bigint) { function handleExpandArrowClick(id: bigint) {
editor.instance.toggleLayerExpansion(id); editor.instance.toggleLayerExpansion(id);
} }
@ -424,11 +428,11 @@
<IconButton <IconButton
class={"status-toggle"} class={"status-toggle"}
classes={{ inactive: !listing.entry.parentsUnlocked }} classes={{ inactive: !listing.entry.parentsUnlocked }}
action={(e) => (toggleLayerVisibility(listing.entry.id), e?.stopPropagation())} action={(e) => (toggleLayerLock(listing.entry.id), e?.stopPropagation())}
size={24} size={24}
icon={listing.entry.parentsUnlocked ? "PadlockLocked" : "PadlockUnlocked"} icon={listing.entry.unlocked ? "PadlockUnlocked" : "PadlockLocked"}
hoverIcon={listing.entry.parentsUnlocked ? "PadlockUnlocked" : "PadlockLocked"} hoverIcon={listing.entry.unlocked ? "PadlockLocked" : "PadlockUnlocked"}
tooltip={listing.entry.parentsUnlocked ? "Unlock" : "Lock"} tooltip={listing.entry.unlocked ? "Lock" : "Unlock"}
/> />
{/if} {/if}
<IconButton <IconButton

View file

@ -126,6 +126,8 @@ export class FrontendNode {
readonly visible!: boolean; readonly visible!: boolean;
readonly unlocked!: boolean;
readonly errors!: string | undefined; readonly errors!: string | undefined;
} }

View file

@ -762,6 +762,14 @@ impl JsEditorHandle {
self.dispatch(message); 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 /// Toggle expansions state of a layer from the layer list
#[wasm_bindgen(js_name = toggleLayerExpansion)] #[wasm_bindgen(js_name = toggleLayerExpansion)]
pub fn toggle_layer_expansion(&self, id: u64) { pub fn toggle_layer_expansion(&self, id: u64) {

View file

@ -162,6 +162,9 @@ pub struct DocumentNode {
/// Represents the eye icon for hiding/showing the node in the graph UI. When hidden, a node gets replaced with an identity node during the graph flattening step. /// Represents the eye icon for hiding/showing the node in the graph UI. When hidden, a node gets replaced with an identity node during the graph flattening step.
#[serde(default = "return_true")] #[serde(default = "return_true")]
pub visible: bool, pub visible: bool,
/// Represents the lock icon for locking/unlocking the node in the graph UI. When locked, a node cannot be moved in the graph UI.
#[serde(default)]
pub locked: bool,
/// Metadata about the node including its position in the graph UI. /// Metadata about the node including its position in the graph UI.
pub metadata: DocumentNodeMetadata, pub metadata: DocumentNodeMetadata,
/// When two different proto nodes hash to the same value (e.g. two value nodes each containing `2_u32` or two multiply nodes that have the same node IDs as input), the duplicates are removed. /// When two different proto nodes hash to the same value (e.g. two value nodes each containing `2_u32` or two multiply nodes that have the same node IDs as input), the duplicates are removed.
@ -210,6 +213,7 @@ impl Default for DocumentNode {
has_primary_output: true, has_primary_output: true,
implementation: Default::default(), implementation: Default::default(),
visible: true, visible: true,
locked: Default::default(),
metadata: Default::default(), metadata: Default::default(),
skip_deduplication: Default::default(), skip_deduplication: Default::default(),
world_state_hash: Default::default(), world_state_hash: Default::default(),