Skip to content

Commit cbb615e

Browse files
committed
add enter and exit maskMode
1 parent 5b1ca12 commit cbb615e

File tree

3 files changed

+93
-7
lines changed

3 files changed

+93
-7
lines changed

editor/src/messages/portfolio/document/document_message_handler.rs

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ pub struct DocumentMessageHandler {
119119
/// The current editor-only mode for the active document.
120120
#[serde(skip)]
121121
pub document_mode: DocumentMode,
122+
/// The NodeId of the mask group layer when in MaskMode, or None otherwise.
123+
#[serde(skip)]
124+
pub mask_group_id: Option<NodeId>,
125+
/// The layers that are targets for the current mask operation.
126+
/// When entering MaskMode, this stores the selected layers so the mask can be applied to them on exit.
127+
#[serde(skip)]
128+
pub mask_target_layers: Vec<LayerNodeIdentifier>,
122129
/// The path of the to the document file.
123130
#[serde(skip)]
124131
pub(crate) path: Option<PathBuf>,
@@ -178,6 +185,8 @@ impl Default for DocumentMessageHandler {
178185
// =============================================
179186
name: DEFAULT_DOCUMENT_NAME.to_string(),
180187
document_mode: DocumentMode::default(),
188+
mask_group_id: None,
189+
mask_target_layers: Vec::new(),
181190
path: None,
182191
breadcrumb_network_path: Vec::new(),
183192
selection_network_path: Vec::new(),
@@ -1133,16 +1142,69 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
11331142
}
11341143
}
11351144
DocumentMessage::EnterMaskMode => {
1136-
if self.document_mode != DocumentMode::MaskMode {
1137-
self.document_mode = DocumentMode::MaskMode;
1138-
responses.add(PortfolioMessage::UpdateDocumentWidgets);
1145+
if self.document_mode == DocumentMode::MaskMode {
1146+
return;
11391147
}
1148+
1149+
self.document_mode = DocumentMode::MaskMode;
1150+
1151+
// Store the currently selected layers as mask targets
1152+
self.mask_target_layers = self.network_interface.selected_nodes().selected_layers(self.network_interface.document_metadata()).collect();
1153+
1154+
// Create a new group layer for the mask
1155+
let mask_group_id = NodeId::new();
1156+
self.mask_group_id = Some(mask_group_id);
1157+
1158+
responses.add(DocumentMessage::AddTransaction);
1159+
1160+
// Create the mask group layer as an empty custom layer container
1161+
graph_modification_utils::new_custom(mask_group_id, Vec::new(), self.new_layer_parent(true), responses);
1162+
1163+
// Set the mask group opacity to 50% so the user can see artwork beneath
1164+
responses.add(GraphOperationMessage::OpacitySet {
1165+
layer: LayerNodeIdentifier::new_unchecked(mask_group_id),
1166+
opacity: 0.5,
1167+
});
1168+
1169+
responses.add(DocumentMessage::EndTransaction);
1170+
responses.add(PortfolioMessage::UpdateDocumentWidgets);
11401171
}
1141-
DocumentMessage::ExitMaskMode { discard: _ } => {
1142-
if self.document_mode != DocumentMode::DesignMode {
1143-
self.document_mode = DocumentMode::DesignMode;
1144-
responses.add(PortfolioMessage::UpdateDocumentWidgets);
1172+
DocumentMessage::ExitMaskMode { discard } => {
1173+
if self.document_mode != DocumentMode::MaskMode {
1174+
return;
11451175
}
1176+
1177+
self.document_mode = DocumentMode::DesignMode;
1178+
1179+
if discard {
1180+
// Delete the mask group without applying it
1181+
if let Some(mask_group_id) = self.mask_group_id {
1182+
responses.add(DocumentMessage::AddTransaction);
1183+
responses.add(NodeGraphMessage::DeleteNodes {
1184+
node_ids: vec![mask_group_id],
1185+
delete_children: true,
1186+
});
1187+
responses.add(DocumentMessage::EndTransaction);
1188+
}
1189+
} else {
1190+
// Rasterize the mask group and apply it to target layers
1191+
if let Some(mask_group_id) = self.mask_group_id {
1192+
responses.add(GraphOperationMessage::ApplyMaskStencil {
1193+
layers: self.mask_target_layers.clone(),
1194+
mask_image: graphene_std::raster::Image::new(1, 1, graphene_std::Color::WHITE),
1195+
});
1196+
responses.add(NodeGraphMessage::DeleteNodes {
1197+
node_ids: vec![mask_group_id],
1198+
delete_children: true,
1199+
});
1200+
}
1201+
}
1202+
1203+
// Clear mask state
1204+
self.mask_group_id = None;
1205+
self.mask_target_layers.clear();
1206+
1207+
responses.add(PortfolioMessage::UpdateDocumentWidgets);
11461208
}
11471209
DocumentMessage::DrawMarchingAntsOverlay { context: _ } => {}
11481210
DocumentMessage::AddTransaction => {
@@ -2116,6 +2178,13 @@ impl DocumentMessageHandler {
21162178

21172179
/// Finds the parent folder which, based on the current selections, should be the container of any newly added layers.
21182180
pub fn new_layer_parent(&self, include_self: bool) -> LayerNodeIdentifier {
2181+
// If we're in MaskMode and have a mask group, all new layers should go into the mask group
2182+
if self.document_mode == DocumentMode::MaskMode
2183+
&& let Some(mask_group_id) = self.mask_group_id
2184+
{
2185+
return LayerNodeIdentifier::new_unchecked(mask_group_id);
2186+
}
2187+
21192188
let Some(selected_nodes) = self.network_interface.selected_nodes_in_nested_network(&self.selection_network_path) else {
21202189
warn!("No selected nodes found in new_layer_parent. Defaulting to ROOT_PARENT.");
21212190
return LayerNodeIdentifier::ROOT_PARENT;

editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use graphene_std::text::{Font, TypesettingConfig};
1414
use graphene_std::vector::PointId;
1515
use graphene_std::vector::VectorModificationType;
1616
use graphene_std::vector::style::{Fill, Stroke};
17+
use graphene_std::raster::Image;
18+
use graphene_std::Color;
1719

1820
#[impl_message(Message, DocumentMessage, GraphOperation)]
1921
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -126,4 +128,8 @@ pub enum GraphOperationMessage {
126128
/// When true, centers the SVG at the transform origin (clipboard paste / drag-drop). When false, keeps natural SVG coordinates (file-open flow).
127129
center: bool,
128130
},
131+
ApplyMaskStencil {
132+
layers: Vec<LayerNodeIdentifier>,
133+
mask_image: Image<Color>,
134+
},
129135
}

editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,17 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
439439
// (skipped automatically when identity, so file-open with content at origin creates no Transform node).
440440
modify_inputs.transform_set(placement_transform, TransformIn::Local, false);
441441
}
442+
GraphOperationMessage::ApplyMaskStencil { layers, mask_image } => {
443+
let _ = mask_image;
444+
445+
// For each target layer, toggle clip mode so the existing clip infrastructure is engaged.
446+
for layer in layers {
447+
responses.add(GraphOperationMessage::ClipModeToggle { layer });
448+
}
449+
responses.add(NodeGraphMessage::RunDocumentGraph);
450+
responses.add(NodeGraphMessage::SelectedNodesUpdated);
451+
responses.add(NodeGraphMessage::SendGraph);
452+
}
442453
}
443454
}
444455

0 commit comments

Comments
 (0)