From bf81a31ff99ca0d71b896e4d7c98e381f875ed88 Mon Sep 17 00:00:00 2001 From: Haikal <76188139+haikalvidya@users.noreply.github.com> Date: Thu, 4 Apr 2024 05:50:23 +0700 Subject: [PATCH] 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 7c93259bc2bef492e203d6ac9c48852112e6c3a3. * Revert "Fix icon logic on panel locked layer" This reverts commit 33939f2e84431d64e6bc2bef07161eafcfba0c0e. * 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> --- .../messages/input_mapper/default_mapping.rs | 1 + .../document/document_message_handler.rs | 31 ++++++++---- .../document/node_graph/node_graph_message.rs | 8 +++ .../node_graph/node_graph_message_handler.rs | 50 +++++++++++++++++-- .../document/node_graph/utility_types.rs | 1 + .../document/utility_types/clipboards.rs | 1 + .../utility_types/document_metadata.rs | 11 ++++ .../portfolio/document/utility_types/nodes.rs | 13 +++++ .../portfolio/portfolio_message_handler.rs | 4 ++ .../tool/common_functionality/pivot.rs | 8 +-- .../transformation_cage.rs | 6 ++- .../tool/tool_messages/select_tool.rs | 12 ++--- .../transform_layer_message_handler.rs | 2 +- frontend/src/components/panels/Layers.svelte | 12 +++-- frontend/src/wasm-communication/messages.ts | 2 + frontend/wasm/src/editor_api.rs | 8 +++ node-graph/graph-craft/src/document.rs | 4 ++ 17 files changed, 142 insertions(+), 32 deletions(-) diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index 4920f8d7..eec0b96e 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -58,6 +58,7 @@ pub fn default_mapping() -> Mapping { entry!(KeyDown(KeyC); modifiers=[Accel], action_dispatch=NodeGraphMessage::Copy), entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=NodeGraphMessage::DuplicateSelectedNodes), entry!(KeyDown(KeyH); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedVisibility), + entry!(KeyDown(KeyL); modifiers=[Accel], action_dispatch=NodeGraphMessage::ToggleSelectedLocked), // // TransformLayerMessage entry!(KeyDown(Enter); action_dispatch=TransformLayerMessage::ApplyTransformOperation), diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index e708c31b..16cda6c7 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -190,7 +190,7 @@ impl MessageHandler> for DocumentMessag AlignAggregate::Max => combined_box[1], 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 { continue; }; @@ -313,10 +313,10 @@ impl MessageHandler> for DocumentMessag FlipAxis::X => 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 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 { layer, transform: DAffine2::from_scale(scale), @@ -463,7 +463,7 @@ impl MessageHandler> for DocumentMessag for layer in self .selected_nodes .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 { layer, @@ -498,7 +498,7 @@ impl MessageHandler> for DocumentMessag for layer in self .selected_nodes .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 original_transform = self.metadata().upstream_transform(layer.to_node()); @@ -622,11 +622,11 @@ impl MessageHandler> for DocumentMessag } DocumentMessage::SelectAllLayers => { let metadata = self.metadata(); - let all_layers_except_artboards_and_invisible = metadata + let all_layers_except_artboards_invisible_and_locked = metadata .all_layers() .filter(move |&layer| !metadata.is_artboard(layer)) - .filter(|&layer| self.selected_nodes.layer_visible(layer, metadata)); - let nodes = all_layers_except_artboards_and_invisible.map(|layer| layer.to_node()).collect(); + .filter(|&layer| self.selected_nodes.layer_visible(layer, metadata) && !self.selected_nodes.layer_locked(layer, metadata)); + let nodes = all_layers_except_artboards_invisible_and_locked.map(|layer| layer.to_node()).collect(); responses.add(NodeGraphMessage::SelectedNodesSet { nodes }); } DocumentMessage::SelectedLayersLower => { @@ -834,6 +834,7 @@ impl DocumentMessageHandler { .root() .descendants(&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_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)))) @@ -847,6 +848,7 @@ impl DocumentMessageHandler { .root() .descendants(&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(move |(layer, target)| target.iter().any(|target: &ClickTarget| target.intersect_point(point, self.metadata.transform_to_document(*layer)))) .map(|(layer, _)| layer) @@ -865,6 +867,13 @@ impl DocumentMessageHandler { .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 { &self.network } @@ -1361,7 +1370,7 @@ impl DocumentMessageHandler { 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_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 { widgets: vec![ @@ -1415,8 +1424,8 @@ impl DocumentMessageHandler { IconButton::new(if selection_all_locked { "PadlockLocked" } else { "PadlockUnlocked" }, 24) .hover_icon(Some((if selection_all_locked { "PadlockUnlocked" } else { "PadlockLocked" }).into())) .tooltip(if selection_all_locked { "Unlock Selected" } else { "Lock Selected" }) - .tooltip_shortcut(action_keys!(DialogMessageDiscriminant::RequestComingSoonDialog)) - .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1127) }.into()) + .tooltip_shortcut(action_keys!(NodeGraphMessageDiscriminant::ToggleSelectedLocked)) + .on_update(|_| NodeGraphMessage::ToggleSelectedLocked.into()) .disabled(!has_selection) .widget_holder(), IconButton::new(if selection_all_visible { "EyeVisible" } else { "EyeHidden" }, 24) 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 7769be00..0bcf4169 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, visible: 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 b0be96da..f8e2c7fb 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 @@ -487,6 +487,40 @@ impl<'a> MessageHandler> for NodeGrap 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); } NodeGraphMessage::SetName { node_id, name } => { @@ -551,9 +585,9 @@ impl<'a> MessageHandler> for NodeGrap impl NodeGraphMessageHandler { pub fn actions_with_node_graph_open(&self, graph_open: bool) -> ActionList { 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 { - actions!(NodeGraphMessageDiscriminant; ToggleSelectedVisibility) + actions!(NodeGraphMessageDiscriminant; ToggleSelectedVisibility, ToggleSelectedLocked) } else { 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) { if let Some(network) = document_network.nested_network(&self.network) { let mut widgets = Vec::new(); @@ -759,6 +793,7 @@ impl NodeGraphMessageHandler { position: node.metadata.position.into(), previewed: network.outputs_contain(node_id), visible: node.visible, + locked: node.locked, errors: errors.map(|e| format!("{e:?}")), }); } @@ -784,6 +819,11 @@ impl NodeGraphMessageHandler { .filter(|&ancestor| ancestor != layer) .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 { id: node_id, layer_classification, @@ -795,8 +835,8 @@ impl NodeGraphMessageHandler { tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() }, visible: node.visible, parents_visible, - unlocked: true, - parents_unlocked: true, + unlocked: !node.locked, + parents_unlocked, }; 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 6bf6ff53..0f71ce0d 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 visible: bool, + pub locked: bool, pub previewed: bool, pub errors: Option, } diff --git a/editor/src/messages/portfolio/document/utility_types/clipboards.rs b/editor/src/messages/portfolio/document/utility_types/clipboards.rs index a9db4211..31aa6148 100644 --- a/editor/src/messages/portfolio/document/utility_types/clipboards.rs +++ b/editor/src/messages/portfolio/document/utility_types/clipboards.rs @@ -20,6 +20,7 @@ pub struct CopyBufferEntry { pub nodes: HashMap, pub selected: bool, pub visible: bool, + pub locked: bool, pub collapsed: bool, pub alias: String, } diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index 94467529..1132bfae 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -23,6 +23,7 @@ pub struct DocumentMetadata { artboards: HashSet, folders: HashSet, hidden: HashSet, + locked: HashSet, click_targets: HashMap>, /// Transform from document space to viewport space. pub document_to_viewport: DAffine2, @@ -36,6 +37,7 @@ impl Default for DocumentMetadata { artboards: HashSet::new(), folders: HashSet::new(), hidden: HashSet::new(), + locked: HashSet::new(), click_targets: HashMap::new(), document_to_viewport: DAffine2::IDENTITY, } @@ -126,6 +128,10 @@ impl DocumentMetadata { !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 pub fn folders_sorted_by_most_nested(&self, layers: impl Iterator) -> Vec { let mut folders: Vec<_> = layers.filter(|layer| self.folders.contains(layer)).collect(); @@ -149,6 +155,7 @@ impl DocumentMetadata { self.artboards = HashSet::new(); self.folders = HashSet::new(); self.hidden = HashSet::new(); + self.locked = HashSet::new(); let id = graph.exports[0].node_id; let Some(output_node) = graph.nodes.get(&id) else { @@ -180,6 +187,10 @@ impl DocumentMetadata { if !current_node.visible { self.hidden.insert(current_node_id); } + + if current_node.locked { + self.locked.insert(current_node_id); + } } // Get the sibling below diff --git a/editor/src/messages/portfolio/document/utility_types/nodes.rs b/editor/src/messages/portfolio/document/utility_types/nodes.rs index c792d57f..5dae6cda 100644 --- a/editor/src/messages/portfolio/document/utility_types/nodes.rs +++ b/editor/src/messages/portfolio/document/utility_types/nodes.rs @@ -71,6 +71,19 @@ impl SelectedNodes { 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 + '_ { + 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 + '_ { + 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 + '_ { metadata.all_layers().filter(|layer| self.0.contains(&layer.to_node())) } diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index d6042afd..ed2753a8 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -203,6 +203,7 @@ impl MessageHandler> for PortfolioMes .collect(), selected: active_document.selected_nodes.selected_layers_contains(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, alias: previous_alias, }); @@ -391,6 +392,9 @@ impl MessageHandler> for PortfolioMes if !entry.visible { responses.add(NodeGraphMessage::SetVisibility { node_id: id, visible: false }); } + if entry.locked { + responses.add(NodeGraphMessage::SetLocked { node_id: id, locked: true }); + } } }; diff --git a/editor/src/messages/tool/common_functionality/pivot.rs b/editor/src/messages/tool/common_functionality/pivot.rs index c762ca50..c9e4a163 100644 --- a/editor/src/messages/tool/common_functionality/pivot.rs +++ b/editor/src/messages/tool/common_functionality/pivot.rs @@ -45,7 +45,7 @@ impl Pivot { /// Recomputes the pivot position and transform. 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 { // If no layers are selected then we revert things back to default 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 let xy_summation = document .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)) .reduce(|a, b| a + b) .unwrap_or_default(); let pivot = xy_summation / selected_layers_count as f64; 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.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. pub fn set_viewport_position(&self, position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { - 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 pivot = transform.inverse().transform_point2(position); // Only update the pivot when computed position is finite. Infinite can happen when scale is 0. diff --git a/editor/src/messages/tool/common_functionality/transformation_cage.rs b/editor/src/messages/tool/common_functionality/transformation_cage.rs index b47c38e3..81909ddd 100644 --- a/editor/src/messages/tool/common_functionality/transformation_cage.rs +++ b/editor/src/messages/tool/common_functionality/transformation_cage.rs @@ -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 angle = -mouse_position.angle_between(DVec2::X); 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 { position } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 5894dd04..b1718e4d 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -386,12 +386,12 @@ impl Fsm for SelectToolFsmState { (_, SelectToolMessage::Overlays(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_count = selected_layers_count; // 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)); } @@ -405,13 +405,13 @@ impl Fsm for SelectToolFsmState { // Update bounds let transform = document .selected_nodes - .selected_visible_layers(document.metadata()) + .selected_visible_and_unlocked_layers(document.metadata()) .next() .map(|layer| document.metadata().transform_to_viewport(layer)); let transform = transform.unwrap_or(DAffine2::IDENTITY); let bounds = document .selected_nodes - .selected_visible_layers(document.metadata()) + .selected_visible_and_unlocked_layers(document.metadata()) .filter_map(|layer| { document .metadata() @@ -472,7 +472,7 @@ impl Fsm for SelectToolFsmState { .map(|bounding_box| bounding_box.check_rotate(input.mouse.position)) .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); // 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 constraint = SnapConstraint::Line { 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) } else { diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index 67524c52..1773d01c 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -48,7 +48,7 @@ impl<'a> MessageHandler> for TransformL let selected_layers = document .selected_nodes .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::>(); let mut selected = Selected::new( diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 03f86205..90f3d7a6 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); } @@ -424,11 +428,11 @@ (toggleLayerVisibility(listing.entry.id), e?.stopPropagation())} + action={(e) => (toggleLayerLock(listing.entry.id), e?.stopPropagation())} size={24} - icon={listing.entry.parentsUnlocked ? "PadlockLocked" : "PadlockUnlocked"} - hoverIcon={listing.entry.parentsUnlocked ? "PadlockUnlocked" : "PadlockLocked"} - tooltip={listing.entry.parentsUnlocked ? "Unlock" : "Lock"} + icon={listing.entry.unlocked ? "PadlockUnlocked" : "PadlockLocked"} + hoverIcon={listing.entry.unlocked ? "PadlockLocked" : "PadlockUnlocked"} + tooltip={listing.entry.unlocked ? "Lock" : "Unlock"} /> {/if}