Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions editor/src/messages/input_mapper/input_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ pub fn input_mappings(zoom_with_scroll: bool) -> Mapping {
//
// DocumentMessage
entry!(KeyDown(Space); modifiers=[Control], action_dispatch=DocumentMessage::GraphViewOverlayToggle),
entry!(KeyDown(KeyQ); action_dispatch=DocumentMessage::ToggleDocumentMode),
entry!(KeyDownNoRepeat(Escape); action_dispatch=DocumentMessage::Escape),
entry!(KeyDown(Delete); action_dispatch=DocumentMessage::DeleteSelectedLayers),
entry!(KeyDown(Backspace); action_dispatch=DocumentMessage::DeleteSelectedLayers),
Expand Down
13 changes: 12 additions & 1 deletion editor/src/messages/portfolio/document/document_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::portfolio::document::data_panel::DataPanelMessage;
use crate::messages::portfolio::document::overlays::utility_types::{OverlayContext, OverlaysType};
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GridSnapping};
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, GridSnapping};
use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::prelude::*;
use glam::{DAffine2, IVec2};
Expand Down Expand Up @@ -189,6 +189,17 @@ pub enum DocumentMessage {
SetRenderMode {
render_mode: RenderMode,
},
ToggleDocumentMode,
SetDocumentMode {
document_mode: DocumentMode,
},
EnterMaskMode,
ExitMaskMode {
discard: bool,
},
DrawMarchingAntsOverlay {
context: OverlayContext,
},
AddTransaction,
StartTransaction,
EndTransaction,
Expand Down
103 changes: 102 additions & 1 deletion editor/src/messages/portfolio/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay
use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType, OverlaysVisibilitySettings, Pivot};
use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ};
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, DocumentMode, FlipAxis, PTZ};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate};
use crate::messages::portfolio::utility_types::{PanelType, PersistentData};
use crate::messages::prelude::*;
Expand Down Expand Up @@ -116,6 +116,16 @@ pub struct DocumentMessageHandler {
/// The name of the document, which is displayed in the tab and title bar of the editor.
#[serde(skip)]
pub name: String,
/// The current editor-only mode for the active document.
#[serde(skip)]
pub document_mode: DocumentMode,
/// The NodeId of the mask group layer when in MaskMode, or None otherwise.
#[serde(skip)]
pub mask_group_id: Option<NodeId>,
/// The layers that are targets for the current mask operation.
/// When entering MaskMode, this stores the selected layers so the mask can be applied to them on exit.
#[serde(skip)]
pub mask_target_layers: Vec<LayerNodeIdentifier>,
/// The path of the to the document file.
#[serde(skip)]
pub(crate) path: Option<PathBuf>,
Expand Down Expand Up @@ -174,6 +184,9 @@ impl Default for DocumentMessageHandler {
// Fields omitted from the saved document format
// =============================================
name: DEFAULT_DOCUMENT_NAME.to_string(),
document_mode: DocumentMode::default(),
mask_group_id: None,
mask_target_layers: Vec::new(),
path: None,
breadcrumb_network_path: Vec::new(),
selection_network_path: Vec::new(),
Expand Down Expand Up @@ -1115,6 +1128,85 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
self.render_mode = render_mode;
responses.add_front(NodeGraphMessage::RunDocumentGraph);
}
DocumentMessage::ToggleDocumentMode => {
self.document_mode = match self.document_mode {
DocumentMode::MaskMode => DocumentMode::DesignMode,
_ => DocumentMode::MaskMode,
};
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
DocumentMessage::SetDocumentMode { document_mode } => {
if self.document_mode != document_mode {
self.document_mode = document_mode;
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
}
DocumentMessage::EnterMaskMode => {
if self.document_mode == DocumentMode::MaskMode {
return;
}

self.document_mode = DocumentMode::MaskMode;

// Store the currently selected layers as mask targets
self.mask_target_layers = self.network_interface.selected_nodes().selected_layers(self.network_interface.document_metadata()).collect();

// Create a new group layer for the mask
let mask_group_id = NodeId::new();
self.mask_group_id = Some(mask_group_id);

responses.add(DocumentMessage::AddTransaction);

// Create the mask group layer as an empty custom layer container
graph_modification_utils::new_custom(mask_group_id, Vec::new(), self.new_layer_parent(true), responses);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Mask group creation can self-parent because parent resolution happens after entering MaskMode and storing the same mask group ID used for the new layer.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/document_message_handler.rs, line 1161:

<comment>Mask group creation can self-parent because parent resolution happens after entering MaskMode and storing the same mask group ID used for the new layer.</comment>

<file context>
@@ -1133,16 +1142,69 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
+				responses.add(DocumentMessage::AddTransaction);
+
+				// Create the mask group layer as an empty custom layer container
+				graph_modification_utils::new_custom(mask_group_id, Vec::new(), self.new_layer_parent(true), responses);
+
+				// Set the mask group opacity to 50% so the user can see artwork beneath
</file context>


// Set the mask group opacity to 50% so the user can see artwork beneath
responses.add(GraphOperationMessage::OpacitySet {
layer: LayerNodeIdentifier::new_unchecked(mask_group_id),
opacity: 0.5,
});

responses.add(DocumentMessage::EndTransaction);
responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
DocumentMessage::ExitMaskMode { discard } => {
if self.document_mode != DocumentMode::MaskMode {
return;
}

self.document_mode = DocumentMode::DesignMode;

if discard {
// Delete the mask group without applying it
if let Some(mask_group_id) = self.mask_group_id {
responses.add(DocumentMessage::AddTransaction);
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: vec![mask_group_id],
delete_children: true,
});
responses.add(DocumentMessage::EndTransaction);
}
} else {
// Rasterize the mask group and apply it to target layers
if let Some(mask_group_id) = self.mask_group_id {
responses.add(GraphOperationMessage::ApplyMaskStencil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Applying mask on ExitMaskMode mutates the graph without transaction boundaries, unlike the discard branch, causing inconsistent undo/history behavior.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/document_message_handler.rs, line 1192:

<comment>Applying mask on `ExitMaskMode` mutates the graph without transaction boundaries, unlike the discard branch, causing inconsistent undo/history behavior.</comment>

<file context>
@@ -1133,16 +1142,69 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
+				} else {
+					// Rasterize the mask group and apply it to target layers
+					if let Some(mask_group_id) = self.mask_group_id {
+						responses.add(GraphOperationMessage::ApplyMaskStencil {
+							layers: self.mask_target_layers.clone(),
+							mask_image: graphene_std::raster::Image::new(1, 1, graphene_std::Color::WHITE),
</file context>

layers: self.mask_target_layers.clone(),
mask_image: graphene_std::raster::Image::new(1, 1, graphene_std::Color::WHITE),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Mask apply path discards authored mask content by using a placeholder 1x1 white image and then deleting the mask group.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At editor/src/messages/portfolio/document/document_message_handler.rs, line 1194:

<comment>Mask apply path discards authored mask content by using a placeholder 1x1 white image and then deleting the mask group.</comment>

<file context>
@@ -1133,16 +1142,69 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
+					if let Some(mask_group_id) = self.mask_group_id {
+						responses.add(GraphOperationMessage::ApplyMaskStencil {
+							layers: self.mask_target_layers.clone(),
+							mask_image: graphene_std::raster::Image::new(1, 1, graphene_std::Color::WHITE),
+						});
+						responses.add(NodeGraphMessage::DeleteNodes {
</file context>

});
responses.add(NodeGraphMessage::DeleteNodes {
node_ids: vec![mask_group_id],
delete_children: true,
});
}
}

// Clear mask state
self.mask_group_id = None;
self.mask_target_layers.clear();

responses.add(PortfolioMessage::UpdateDocumentWidgets);
}
DocumentMessage::DrawMarchingAntsOverlay { context: _ } => {}
DocumentMessage::AddTransaction => {
// Reverse order since they are added to the front
responses.add_front(DocumentMessage::CommitTransaction);
Expand Down Expand Up @@ -1470,6 +1562,8 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
SaveDocument,
SelectAllLayers,
SetSnapping,
ToggleDocumentMode,
SetDocumentMode,
ToggleGridVisibility,
ToggleOverlaysVisibility,
ToggleSnapping,
Expand Down Expand Up @@ -2084,6 +2178,13 @@ impl DocumentMessageHandler {

/// Finds the parent folder which, based on the current selections, should be the container of any newly added layers.
pub fn new_layer_parent(&self, include_self: bool) -> LayerNodeIdentifier {
// If we're in MaskMode and have a mask group, all new layers should go into the mask group
if self.document_mode == DocumentMode::MaskMode
&& let Some(mask_group_id) = self.mask_group_id
{
return LayerNodeIdentifier::new_unchecked(mask_group_id);
}

let Some(selected_nodes) = self.network_interface.selected_nodes_in_nested_network(&self.selection_network_path) else {
warn!("No selected nodes found in new_layer_parent. Defaulting to ROOT_PARENT.");
return LayerNodeIdentifier::ROOT_PARENT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use graphene_std::text::{Font, TypesettingConfig};
use graphene_std::vector::PointId;
use graphene_std::vector::VectorModificationType;
use graphene_std::vector::style::{Fill, Stroke};
use graphene_std::raster::Image;
use graphene_std::Color;

#[impl_message(Message, DocumentMessage, GraphOperation)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -126,4 +128,8 @@ pub enum GraphOperationMessage {
/// When true, centers the SVG at the transform origin (clipboard paste / drag-drop). When false, keeps natural SVG coordinates (file-open flow).
center: bool,
},
ApplyMaskStencil {
layers: Vec<LayerNodeIdentifier>,
mask_image: Image<Color>,
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,17 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
// (skipped automatically when identity, so file-open with content at origin creates no Transform node).
modify_inputs.transform_set(placement_transform, TransformIn::Local, false);
}
GraphOperationMessage::ApplyMaskStencil { layers, mask_image } => {
let _ = mask_image;

// For each target layer, toggle clip mode so the existing clip infrastructure is engaged.
for layer in layers {
responses.add(GraphOperationMessage::ClipModeToggle { layer });
}
responses.add(NodeGraphMessage::RunDocumentGraph);
responses.add(NodeGraphMessage::SelectedNodesUpdated);
responses.add(NodeGraphMessage::SendGraph);
}
}
}

Expand Down
35 changes: 19 additions & 16 deletions editor/src/messages/portfolio/document/utility_types/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,26 @@ pub enum AlignAggregate {
Center,
}

// #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
// pub enum DocumentMode {
// #[default]
// DesignMode,
// SelectMode,
// GuideMode,
// }
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
pub enum DocumentMode {
#[default]
DesignMode,
SelectMode,
GuideMode,
MaskMode,
}

// impl fmt::Display for DocumentMode {
// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// match self {
// DocumentMode::DesignMode => write!(f, "Design Mode"),
// DocumentMode::SelectMode => write!(f, "Select Mode"),
// DocumentMode::GuideMode => write!(f, "Guide Mode"),
// }
// }
// }
impl fmt::Display for DocumentMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DocumentMode::DesignMode => write!(f, "Design Mode"),
DocumentMode::SelectMode => write!(f, "Select Mode"),
DocumentMode::GuideMode => write!(f, "Guide Mode"),
DocumentMode::MaskMode => write!(f, "Mask Mode"),
}
}
}

// impl DocumentMode {
// pub fn icon_name(&self) -> String {
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.