From 743b8fcf8b3d22ad2ff336e51750febe07ed4168 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Tue, 13 May 2025 13:07:36 +0530 Subject: [PATCH 01/39] refactor: opacity + blend_mode -> blend_style --- .../document/graph_operation/utility_types.rs | 24 ++++++------ .../graph_modification_utils.rs | 10 ++--- node-graph/gcore/src/graphic_element.rs | 8 +++- node-graph/gcore/src/raster.rs | 38 +++++++++++++++++-- node-graph/gcore/src/raster/adjustments.rs | 35 +++++++++++++++++ 5 files changed, 92 insertions(+), 23 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 27d879a43d..7ea2b1aeae 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -58,13 +58,13 @@ impl<'a> ModifyInputsContext<'a> { /// Non layer nodes directly upstream of a layer are treated as part of that layer. See insert_index == 2 in the diagram /// -----> Post node /// | if insert_index == 0, return (Post node, Some(Layer1)) - /// -> Layer1 + /// -> Layer1 /// ↑ if insert_index == 1, return (Layer1, Some(Layer2)) - /// -> Layer2 + /// -> Layer2 /// ↑ /// -> NonLayerNode /// ↑ if insert_index == 2, return (NonLayerNode, Some(Layer3)) - /// -> Layer3 + /// -> Layer3 /// if insert_index == 3, return (Layer3, None) pub fn get_post_node_with_index(network_interface: &NodeNetworkInterface, parent: LayerNodeIdentifier, insert_index: usize) -> InputConnector { let mut post_node_input_connector = if parent == LayerNodeIdentifier::ROOT_PARENT { @@ -333,20 +333,18 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Fill(fill), false), false); } - pub fn opacity_set(&mut self, opacity: f64) { - let Some(opacity_node_id) = self.existing_node_id("Opacity", true) else { return }; - let input_connector = InputConnector::node(opacity_node_id, 1); - self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false); - } - pub fn blend_mode_set(&mut self, blend_mode: BlendMode) { - let Some(blend_mode_node_id) = self.existing_node_id("Blend Mode", true) else { - return; - }; - let input_connector = InputConnector::node(blend_mode_node_id, 1); + let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return }; + let input_connector = InputConnector::node(blend_node_id, 1); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false); } + pub fn opacity_set(&mut self, opacity: f64) { + let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return }; + let input_connector = InputConnector::node(blend_node_id, 2); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false); + } + pub fn stroke_set(&mut self, stroke: Stroke) { let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return }; diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index ea9ab3bb60..801edc7102 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -278,16 +278,16 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, network_interface: &NodeNetwor Some(color.to_linear_srgb()) } -/// Get the current blend mode of a layer from the closest Blend Mode node +/// Get the current blend mode of a layer from the closest Blending node pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { - let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blend Mode")?; + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?; let TaggedValue::BlendMode(blend_mode) = inputs.get(1)?.as_value()? else { return None; }; Some(*blend_mode) } -/// Get the current opacity of a layer from the closest Opacity node. +/// Get the current opacity of a layer from the closest Blending node. /// This may differ from the actual opacity contained within the data type reaching this layer, because that actual opacity may be: /// - Multiplied with additional opacity nodes earlier in the chain /// - Set by an Opacity node with an exposed input value driven by another node @@ -296,8 +296,8 @@ pub fn get_blend_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetwor /// /// With those limitations in mind, the intention of this function is to show just the value already present in an upstream Opacity node so that value can be directly edited. pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { - let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Opacity")?; - let TaggedValue::F64(opacity) = inputs.get(1)?.as_value()? else { + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?; + let TaggedValue::F64(opacity) = inputs.get(2)?.as_value()? else { return None; }; Some(*opacity) diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index 0590717f55..c45c16d5f1 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -15,8 +15,10 @@ pub mod renderer; #[derive(Copy, Clone, Debug, PartialEq, DynAny, specta::Type)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AlphaBlending { - pub opacity: f32, pub blend_mode: BlendMode, + pub opacity: f32, + pub fill: f32, + pub clip: bool, } impl Default for AlphaBlending { fn default() -> Self { @@ -26,14 +28,18 @@ impl Default for AlphaBlending { impl core::hash::Hash for AlphaBlending { fn hash(&self, state: &mut H) { self.opacity.to_bits().hash(state); + self.fill.to_bits().hash(state); self.blend_mode.hash(state); + self.clip.hash(state); } } impl AlphaBlending { pub const fn new() -> Self { Self { opacity: 1., + fill: 1., blend_mode: BlendMode::Normal, + clip: false, } } } diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index d3611978bf..fcd7ff30ef 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -320,8 +320,34 @@ impl SetBlendMode for ImageFrameTable { } } +trait SetClip { + fn set_clip(&mut self, clip: bool); +} + +impl SetClip for VectorDataTable { + fn set_clip(&mut self, clip: bool) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.clip = clip; + } + } +} +impl SetClip for GraphicGroupTable { + fn set_clip(&mut self, clip: bool) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.clip = clip; + } + } +} +impl SetClip for ImageFrameTable { + fn set_clip(&mut self, clip: bool) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.clip = clip; + } + } +} + #[node_macro::node(category("Style"))] -fn blend_mode( +fn blending( _: impl Ctx, #[implementations( GraphicGroupTable, @@ -330,14 +356,18 @@ fn blend_mode( )] mut value: T, blend_mode: BlendMode, + #[default(100.)] opacity: Percentage, + #[default(100.)] fill: Percentage, ) -> T { // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance) rather than applying to each row in its own table, which produces the undesired result value.set_blend_mode(blend_mode); + value.multiply_alpha(opacity / 100.); + value.multiply_fill(fill / 100.); value } #[node_macro::node(category("Style"))] -fn opacity( +fn clipping( _: impl Ctx, #[implementations( GraphicGroupTable, @@ -345,9 +375,9 @@ fn opacity( ImageFrameTable, )] mut value: T, - #[default(100.)] factor: Percentage, + clip: bool, ) -> T { // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance) rather than applying to each row in its own table, which produces the undesired result - value.multiply_alpha(factor / 100.); + value.set_clip(clip); value } diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 24be994091..11dcf0769a 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -1229,6 +1229,41 @@ where } } + +pub(super) trait MultiplyFill { + fn multiply_fill(&mut self, factor: f64); +} + +impl MultiplyFill for Color { + fn multiply_fill(&mut self, factor: f64) { + *self = Color::from_rgbaf32_unchecked(self.r(), self.g(), self.b(), (self.a() * factor as f32).clamp(0., 1.)) + } +} +impl MultiplyFill for VectorDataTable { + fn multiply_fill(&mut self, factor: f64) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.fill *= factor as f32; + } + } +} +impl MultiplyFill for GraphicGroupTable { + fn multiply_fill(&mut self, factor: f64) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.fill *= factor as f32; + } + } +} +impl MultiplyFill for ImageFrameTable

+where + GraphicElement: From>, +{ + fn multiply_fill(&mut self, factor: f64) { + for instance in self.instance_mut_iter() { + instance.alpha_blending.fill *= factor as f32; + } + } +} + // Aims for interoperable compatibility with: // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=nvrt%27%20%3D%20Invert-,%27post%27%20%3D%20Posterize,-%27thrs%27%20%3D%20Threshold // From af72397118bf057492d8ff25e799e1ca6b81d345 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Tue, 13 May 2025 18:08:03 +0530 Subject: [PATCH 02/39] Add code for clipping --- .../gcore/src/graphic_element/renderer.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 6b1f4f436f..85bd61006e 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -299,6 +299,7 @@ pub trait GraphicElementRendered { impl GraphicElementRendered for GraphicGroupTable { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + let mut uuid_state = None; for instance in self.instance_ref_iter() { render.parent_tag( "g", @@ -315,6 +316,22 @@ impl GraphicElementRendered for GraphicGroupTable { if instance.alpha_blending.blend_mode != BlendMode::default() { attributes.push("style", instance.alpha_blending.blend_mode.render()); } + + if instance.alpha_blending.clip { + let uuid = uuid_state.unwrap_or(generate_uuid()); + uuid_state = Some(uuid); + let id = format!("clip-{}", uuid); + let selector = format!("url(#{id})"); + + attributes.push("clip-path", selector); + } else if let Some(uuid) = uuid_state.take() { + let id = format!("clip-{}", uuid); + + let mut svg = SvgRender::new(); + instance.instance.render_svg(&mut svg, render_params); + + write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg.to_svg_string()).unwrap(); + } }, |render| { instance.instance.render_svg(render, render_params); From df33d70d32dc22a4d1296dfbfafe2c6b1a16956f Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Fri, 16 May 2025 05:45:39 +0530 Subject: [PATCH 03/39] Add alt-click masking --- .../portfolio/document/document_message.rs | 3 ++ .../document/document_message_handler.rs | 5 +++ .../graph_operation_message.rs | 3 ++ .../graph_operation_message_handler.rs | 7 ++++ .../document/graph_operation/utility_types.rs | 7 ++++ .../graph_modification_utils.rs | 8 +++++ frontend/src/components/panels/Layers.svelte | 6 ++++ frontend/wasm/src/editor_api.rs | 7 ++++ .../gcore/src/graphic_element/renderer.rs | 34 +++++++++++++------ node-graph/gcore/src/raster.rs | 2 +- 10 files changed, 70 insertions(+), 12 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index f6d2f1470a..bf2b60fdd5 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -121,6 +121,9 @@ pub enum DocumentMessage { SelectedLayersReorder { relative_index_offset: isize, }, + ClipLayer{ + id: NodeId, + }, SelectLayer { id: NodeId, ctrl: bool, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index d324c4931d..329e646665 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1052,6 +1052,11 @@ impl MessageHandler> for DocumentMessag DocumentMessage::SelectedLayersReorder { relative_index_offset } => { self.selected_layers_reorder(relative_index_offset, responses); } + DocumentMessage::ClipLayer { id } => { + let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]); + + responses.add(GraphOperationMessage::ClipModeToggle { layer }); + } DocumentMessage::SelectLayer { id, ctrl, shift } => { let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]); diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index a6e3fb38e9..bcfe6efb0b 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -29,6 +29,9 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, blend_mode: BlendMode, }, + ClipModeToggle { + layer: LayerNodeIdentifier, + }, StrokeSet { layer: LayerNodeIdentifier, stroke: Stroke, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 9f8b8586f1..d030ce4d7f 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -5,6 +5,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector}; use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode; use glam::{DAffine2, DVec2, IVec2}; use graph_craft::document::{NodeId, NodeInput}; use graphene_core::Color; @@ -51,6 +52,12 @@ impl MessageHandler> for Gr modify_inputs.blend_mode_set(blend_mode); } } + GraphOperationMessage::ClipModeToggle { layer } => { + let clip_mode = get_clip_mode(layer, network_interface); + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { + modify_inputs.clip_mode_toggle(clip_mode); + } + } GraphOperationMessage::StrokeSet { layer, stroke } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { modify_inputs.stroke_set(stroke); diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 7ea2b1aeae..8c01b09305 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -339,6 +339,13 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false); } + pub fn clip_mode_toggle(&mut self, clip_mode: Option) { + let clip = !clip_mode.map_or(false, |x| x); + let Some(clip_node_id) = self.existing_node_id("Clipping", true) else { return }; + let input_connector = InputConnector::node(clip_node_id, 1); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false); + } + pub fn opacity_set(&mut self, opacity: f64) { let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return }; let input_connector = InputConnector::node(blend_node_id, 2); diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 801edc7102..c17311f961 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -303,6 +303,14 @@ pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn Some(*opacity) } +pub fn get_clip_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Clipping")?; + let TaggedValue::Bool(clip) = inputs.get(1)?.as_value()? else { + return None; + }; + Some(*clip) +} + pub fn get_fill_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Fill") } diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 24ae3d7452..2ace6e7584 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -190,9 +190,15 @@ // Select the layer only if the accel and/or shift keys are pressed if (!oppositeAccel && !alt) selectLayer(listing, accel, shift); + if (alt) clipLayer(listing); + e.stopPropagation(); } + function clipLayer(listing: LayerListingInfo) { + editor.handle.clipLayer(listing.entry.id); + } + function selectLayer(listing: LayerListingInfo, accel: boolean, shift: boolean) { // Don't select while we are entering text to rename the layer if (listing.editingName) return; diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 489e2518dc..7284cc3fce 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -504,6 +504,13 @@ impl EditorHandle { self.dispatch(message); } + #[wasm_bindgen(js_name = clipLayer)] + pub fn clip_layer(&self, id: u64) { + let id = NodeId(id); + let message = DocumentMessage::ClipLayer { id }; + self.dispatch(message); + } + /// Modify the layer selection based on the layer which is clicked while holding down the Ctrl and/or Shift modifier keys used for range selection behavior #[wasm_bindgen(js_name = selectLayer)] pub fn select_layer(&self, id: u64, ctrl: bool, shift: bool) { diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 85bd61006e..39325d0c70 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -299,8 +299,9 @@ pub trait GraphicElementRendered { impl GraphicElementRendered for GraphicGroupTable { fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { + let mut iter = self.instance_ref_iter().peekable(); let mut uuid_state = None; - for instance in self.instance_ref_iter() { + while let Some(instance) = iter.next() { render.parent_tag( "g", |attributes| { @@ -317,20 +318,31 @@ impl GraphicElementRendered for GraphicGroupTable { attributes.push("style", instance.alpha_blending.blend_mode.render()); } - if instance.alpha_blending.clip { - let uuid = uuid_state.unwrap_or(generate_uuid()); - uuid_state = Some(uuid); - let id = format!("clip-{}", uuid); - let selector = format!("url(#{id})"); - - attributes.push("clip-path", selector); - } else if let Some(uuid) = uuid_state.take() { - let id = format!("clip-{}", uuid); + let next_clips = iter.peek().map_or(false, |next_instance| { + next_instance + .instance + .as_vector_data() + .is_some_and(|data| data.instance_ref_iter().all(|instance| instance.alpha_blending.clip)) + }); + if next_clips && uuid_state.is_none() { + let uuid = generate_uuid(); + let id = format!("mask-{}", uuid); + uuid_state = Some(uuid); let mut svg = SvgRender::new(); instance.instance.render_svg(&mut svg, render_params); - write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg.to_svg_string()).unwrap(); + write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg_defs).unwrap(); + write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg.to_svg_string()).unwrap(); + } else if let Some(uuid) = uuid_state { + if !next_clips { + uuid_state = None; + } + + let id = format!("mask-{}", uuid); + let selector = format!("url(#{id})"); + + attributes.push("mask", selector); } }, |render| { diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index fcd7ff30ef..36685039fd 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -347,7 +347,7 @@ impl SetClip for ImageFrameTable { } #[node_macro::node(category("Style"))] -fn blending( +fn blending( _: impl Ctx, #[implementations( GraphicGroupTable, From a9b53bb69d17cbdf40cd6ded0c1189fed7b81156 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Sat, 17 May 2025 03:18:25 +0530 Subject: [PATCH 04/39] Clip to all colors. Fill option --- .../portfolio/document/document_message.rs | 2 +- .../document/graph_operation/utility_types.rs | 4 +-- .../graph_modification_utils.rs | 4 +-- editor/src/node_graph_executor.rs | 2 +- editor/src/node_graph_executor/runtime.rs | 2 +- .../gcore/src/graphic_element/renderer.rs | 31 +++++++++++++------ node-graph/gcore/src/raster.rs | 18 ++--------- node-graph/gcore/src/raster/adjustments.rs | 1 - node-graph/gcore/src/vector/style.rs | 21 +++++++------ node-graph/gstd/src/wasm_application_io.rs | 2 +- 10 files changed, 43 insertions(+), 44 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index bf2b60fdd5..00b3f2c924 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -121,7 +121,7 @@ pub enum DocumentMessage { SelectedLayersReorder { relative_index_offset: isize, }, - ClipLayer{ + ClipLayer { id: NodeId, }, SelectLayer { diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 8c01b09305..4e732f2f32 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -341,8 +341,8 @@ impl<'a> ModifyInputsContext<'a> { pub fn clip_mode_toggle(&mut self, clip_mode: Option) { let clip = !clip_mode.map_or(false, |x| x); - let Some(clip_node_id) = self.existing_node_id("Clipping", true) else { return }; - let input_connector = InputConnector::node(clip_node_id, 1); + let Some(clip_node_id) = self.existing_node_id("Blending", true) else { return }; + let input_connector = InputConnector::node(clip_node_id, 4); self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::Bool(clip), false), false); } diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index c17311f961..df9ad13fe9 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -304,8 +304,8 @@ pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn } pub fn get_clip_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { - let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Clipping")?; - let TaggedValue::Bool(clip) = inputs.get(1)?.as_value()? else { + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?; + let TaggedValue::Bool(clip) = inputs.get(4)?.as_value()? else { return None; }; Some(*clip) diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 4e514e2a33..e48836b4b3 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -338,7 +338,7 @@ impl NodeGraphExecutor { fn debug_render(render_object: impl GraphicElementRendered, transform: DAffine2, responses: &mut VecDeque) { // Setup rendering let mut render = SvgRender::new(); - let render_params = RenderParams::new(ViewMode::Normal, None, false, false, false); + let render_params = RenderParams::new(ViewMode::Normal, None, false, false, false, false); // Render SVG render_object.render_svg(&mut render, &render_params); diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 703e57a397..bec4544e30 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -318,7 +318,7 @@ impl NodeRuntime { let bounds = graphic_element.bounding_box(DAffine2::IDENTITY, true); // Render the thumbnail from a `GraphicElement` into an SVG string - let render_params = RenderParams::new(ViewMode::Normal, bounds, true, false, false); + let render_params = RenderParams::new(ViewMode::Normal, bounds, true, false, false, false); let mut render = SvgRender::new(); graphic_element.render_svg(&mut render, &render_params); diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 39325d0c70..a856f6b35d 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -226,16 +226,19 @@ pub struct RenderParams { pub hide_artboards: bool, /// Are we exporting? Causes the text above an artboard to be hidden. pub for_export: bool, + /// Are we generating a mask in this render pass? Used to see if fill should be multiplied with alpha. + pub for_mask: bool, } impl RenderParams { - pub fn new(view_mode: ViewMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool, hide_artboards: bool, for_export: bool) -> Self { + pub fn new(view_mode: ViewMode, culling_bounds: Option<[DVec2; 2]>, thumbnail: bool, hide_artboards: bool, for_export: bool, for_mask: bool) -> Self { Self { view_mode, culling_bounds, thumbnail, hide_artboards, for_export, + for_mask, } } } @@ -310,8 +313,10 @@ impl GraphicElementRendered for GraphicGroupTable { attributes.push("transform", matrix); } - if instance.alpha_blending.opacity < 1. { - attributes.push("opacity", instance.alpha_blending.opacity.to_string()); + let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; + let opacity = instance.alpha_blending.opacity * factor; + if opacity < 1. { + attributes.push("opacity", opacity.to_string()); } if instance.alpha_blending.blend_mode != BlendMode::default() { @@ -330,7 +335,8 @@ impl GraphicElementRendered for GraphicGroupTable { let id = format!("mask-{}", uuid); uuid_state = Some(uuid); let mut svg = SvgRender::new(); - instance.instance.render_svg(&mut svg, render_params); + let render_params = RenderParams { for_mask: true, ..*render_params }; + instance.instance.render_svg(&mut svg, &render_params); write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg_defs).unwrap(); write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg.to_svg_string()).unwrap(); @@ -481,11 +487,13 @@ impl GraphicElementRendered for VectorDataTable { let fill_and_stroke = instance .instance .style - .render(render_params.view_mode, defs, element_transform, applied_stroke_transform, layer_bounds, transformed_bounds); + .render(defs, element_transform, applied_stroke_transform, layer_bounds, transformed_bounds, render_params); attributes.push_val(fill_and_stroke); - if instance.alpha_blending.opacity < 1. { - attributes.push("opacity", instance.alpha_blending.opacity.to_string()); + let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; + let opacity = instance.alpha_blending.opacity * factor; + if opacity < 1. { + attributes.push("opacity", opacity.to_string()); } if instance.alpha_blending.blend_mode != BlendMode::default() { @@ -872,7 +880,7 @@ impl GraphicElementRendered for ArtboardGroupTable { } impl GraphicElementRendered for ImageFrameTable { - fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) { + fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) { for instance in self.instance_ref_iter() { let transform = *instance.transform * render.transform; @@ -898,8 +906,10 @@ impl GraphicElementRendered for ImageFrameTable { if !matrix.is_empty() { attributes.push("transform", matrix); } - if instance.alpha_blending.opacity < 1. { - attributes.push("opacity", instance.alpha_blending.opacity.to_string()); + let factor = if render_params.for_mask { 1. } else { instance.alpha_blending.fill }; + let opacity = instance.alpha_blending.opacity * factor; + if opacity < 1. { + attributes.push("opacity", opacity.to_string()); } if instance.alpha_blending.blend_mode != BlendMode::default() { attributes.push("style", instance.alpha_blending.blend_mode.render()); @@ -1165,6 +1175,7 @@ impl GraphicElementRendered for Vec { attributes.push("x", (index * 120).to_string()); attributes.push("y", "40"); attributes.push("fill", format!("#{}", color.to_rgb_hex_srgb_from_gamma())); + debug!("{}", color.to_rgb_hex_srgb_from_gamma()); if color.a() < 1. { attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string()); } diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index 36685039fd..defa2802d8 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -347,7 +347,7 @@ impl SetClip for ImageFrameTable { } #[node_macro::node(category("Style"))] -fn blending( +fn blending( _: impl Ctx, #[implementations( GraphicGroupTable, @@ -358,26 +358,12 @@ fn blending( blend_mode: BlendMode, #[default(100.)] opacity: Percentage, #[default(100.)] fill: Percentage, + #[default(false)] clip: bool, ) -> T { // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance) rather than applying to each row in its own table, which produces the undesired result value.set_blend_mode(blend_mode); value.multiply_alpha(opacity / 100.); value.multiply_fill(fill / 100.); - value -} - -#[node_macro::node(category("Style"))] -fn clipping( - _: impl Ctx, - #[implementations( - GraphicGroupTable, - VectorDataTable, - ImageFrameTable, - )] - mut value: T, - clip: bool, -) -> T { - // TODO: Find a way to make this apply once to the table's parent (i.e. its row in its parent table or Instance) rather than applying to each row in its own table, which produces the undesired result value.set_clip(clip); value } diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 11dcf0769a..fe76e9259b 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -1229,7 +1229,6 @@ where } } - pub(super) trait MultiplyFill { fn multiply_fill(&mut self, factor: f64); } diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index ffb307749e..9048e06c2d 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -2,7 +2,7 @@ use crate::Color; use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT}; -use crate::renderer::format_transform_matrix; +use crate::renderer::{RenderParams, format_transform_matrix}; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use std::fmt::Write; @@ -200,7 +200,7 @@ impl Gradient { } /// Adds the gradient def through mutating the first argument, returning the gradient ID. - fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> u64 { + fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], render_params: &RenderParams) -> u64 { // TODO: Figure out how to use `self.transform` as part of the gradient transform, since that field (`Gradient::transform`) is currently never read from, it's only written to. let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); @@ -212,7 +212,8 @@ impl Gradient { if *position != 0. { let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.); } - let _ = write!(stop, r##" stop-color="#{}""##, color.to_rgb_hex_srgb_from_gamma()); + let hex = if !render_params.for_mask { color.to_rgb_hex_srgb_from_gamma() } else { "ffffff".to_string() }; + let _ = write!(stop, r##" stop-color="#{}""##, hex); if color.a() < 1. { let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.); } @@ -357,18 +358,19 @@ impl Fill { } /// Renders the fill, adding necessary defs through mutating the first argument. - pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String { + pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], render_params: &RenderParams) -> String { match self { Self::None => r#" fill="none""#.to_string(), Self::Solid(color) => { - let mut result = format!(r##" fill="#{}""##, color.to_rgb_hex_srgb_from_gamma()); + let hex = if !render_params.for_mask { color.to_rgb_hex_srgb_from_gamma() } else { "ffffff".to_string() }; + let mut result = format!(r##" fill="#{}""##, hex); if color.a() < 1. { let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.); } result } Self::Gradient(gradient) => { - let gradient_id = gradient.render_defs(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds); + let gradient_id = gradient.render_defs(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params); format!(r##" fill="url('#{gradient_id}')""##) } } @@ -892,10 +894,11 @@ impl PathStyle { } /// Renders the shape's fill and stroke attributes as a string with them concatenated together. - pub fn render(&self, view_mode: ViewMode, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String { + pub fn render(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], render_params: &RenderParams) -> String { + let view_mode = render_params.view_mode; match view_mode { ViewMode::Outline => { - let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds); + let fill_attribute = Fill::None.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params); let mut outline_stroke = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT); // Outline strokes should be non-scaling by default outline_stroke.non_scaling = true; @@ -903,7 +906,7 @@ impl PathStyle { format!("{fill_attribute}{stroke_attribute}") } _ => { - let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds); + let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params); let stroke_attribute = self.stroke.as_ref().map(|stroke| stroke.render()).unwrap_or_default(); format!("{fill_attribute}{stroke_attribute}") } diff --git a/node-graph/gstd/src/wasm_application_io.rs b/node-graph/gstd/src/wasm_application_io.rs index 0427aeb74c..5f71e1bd7e 100644 --- a/node-graph/gstd/src/wasm_application_io.rs +++ b/node-graph/gstd/src/wasm_application_io.rs @@ -249,7 +249,7 @@ async fn render<'a: 'n, T: 'n + GraphicElementRendered + WasmNotSend>( ctx.footprint(); let RenderConfig { hide_artboards, for_export, .. } = render_config; - let render_params = RenderParams::new(render_config.view_mode, None, false, hide_artboards, for_export); + let render_params = RenderParams::new(render_config.view_mode, None, false, hide_artboards, for_export, false); let data = data.eval(ctx.clone()).await; let editor_api = editor_api.eval(None).await; From 15d00f51e64283e6a3d19de6ea13e8081bf3485a Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Sat, 17 May 2025 04:13:50 +0530 Subject: [PATCH 05/39] Fix undo not working. Fix strokes not being white --- .../portfolio/document/document_message_handler.rs | 1 + node-graph/gcore/src/vector/style.rs | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 329e646665..ecf08e77d3 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1055,6 +1055,7 @@ impl MessageHandler> for DocumentMessag DocumentMessage::ClipLayer { id } => { let layer = LayerNodeIdentifier::new(id, &self.network_interface, &[]); + responses.add(DocumentMessage::AddTransaction); responses.add(GraphOperationMessage::ClipModeToggle { layer }); } DocumentMessage::SelectLayer { id, ctrl, shift } => { diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index 9048e06c2d..e92f89b553 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -628,7 +628,7 @@ impl Stroke { } /// Provide the SVG attributes for the stroke. - pub fn render(&self) -> String { + pub fn render(&self, render_params: &RenderParams) -> String { // Don't render a stroke at all if it would be invisible let Some(color) = self.color else { return String::new() }; if self.weight <= 0. || color.a() == 0. { @@ -644,7 +644,8 @@ impl Stroke { let line_join_miter_limit = (self.line_join_miter_limit != 4.).then_some(self.line_join_miter_limit); // Render the needed stroke attributes - let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma()); + let hex = if !render_params.for_mask { color.to_rgb_hex_srgb_from_gamma() } else { "ffffff".to_string() }; + let mut attributes = format!(r##" stroke="#{}""##, hex); if color.a() < 1. { let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.); } @@ -902,12 +903,12 @@ impl PathStyle { let mut outline_stroke = Stroke::new(Some(LAYER_OUTLINE_STROKE_COLOR), LAYER_OUTLINE_STROKE_WEIGHT); // Outline strokes should be non-scaling by default outline_stroke.non_scaling = true; - let stroke_attribute = outline_stroke.render(); + let stroke_attribute = outline_stroke.render(render_params); format!("{fill_attribute}{stroke_attribute}") } _ => { let fill_attribute = self.fill.render(svg_defs, element_transform, stroke_transform, bounds, transformed_bounds, render_params); - let stroke_attribute = self.stroke.as_ref().map(|stroke| stroke.render()).unwrap_or_default(); + let stroke_attribute = self.stroke.as_ref().map(|stroke| stroke.render(render_params)).unwrap_or_default(); format!("{fill_attribute}{stroke_attribute}") } } From e90fd26c5022d5c132eaf829c43ce8cc11f5571a Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Sat, 17 May 2025 04:34:20 +0530 Subject: [PATCH 06/39] Allow clipped to be grouped or raster --- node-graph/gcore/src/graphic_element/renderer.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index a856f6b35d..a8400c2ce9 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -324,10 +324,13 @@ impl GraphicElementRendered for GraphicGroupTable { } let next_clips = iter.peek().map_or(false, |next_instance| { - next_instance - .instance - .as_vector_data() - .is_some_and(|data| data.instance_ref_iter().all(|instance| instance.alpha_blending.clip)) + let instance = next_instance.instance; + instance.as_vector_data().is_some_and(|data| data.instance_ref_iter().all(|instance| instance.alpha_blending.clip)) + || instance.as_group().is_some_and(|data| data.instance_ref_iter().all(|instance| instance.alpha_blending.clip)) + || instance.as_raster().is_some_and(|data| match data { + RasterFrame::ImageFrame(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip), + RasterFrame::TextureFrame(data) => data.instance_ref_iter().all(|instance| instance.alpha_blending.clip), + }) }); if next_clips && uuid_state.is_none() { From f7e946e7d6a8c014f12dbbe33d028162ccec9ca2 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Sat, 17 May 2025 05:19:50 +0530 Subject: [PATCH 07/39] Switch to alpha mode in mask-type --- node-graph/gcore/src/graphic_element/renderer.rs | 2 +- node-graph/gcore/src/vector/style.rs | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index a8400c2ce9..1686f62ba6 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -342,7 +342,7 @@ impl GraphicElementRendered for GraphicGroupTable { instance.instance.render_svg(&mut svg, &render_params); write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg_defs).unwrap(); - write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg.to_svg_string()).unwrap(); + write!(&mut attributes.0.svg_defs, r##"{}"##, svg.svg.to_svg_string()).unwrap(); } else if let Some(uuid) = uuid_state { if !next_clips { uuid_state = None; diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index e92f89b553..4fb677e930 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -200,7 +200,7 @@ impl Gradient { } /// Adds the gradient def through mutating the first argument, returning the gradient ID. - fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], render_params: &RenderParams) -> u64 { + fn render_defs(&self, svg_defs: &mut String, element_transform: DAffine2, stroke_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2], _render_params: &RenderParams) -> u64 { // TODO: Figure out how to use `self.transform` as part of the gradient transform, since that field (`Gradient::transform`) is currently never read from, it's only written to. let bound_transform = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); @@ -212,8 +212,7 @@ impl Gradient { if *position != 0. { let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.); } - let hex = if !render_params.for_mask { color.to_rgb_hex_srgb_from_gamma() } else { "ffffff".to_string() }; - let _ = write!(stop, r##" stop-color="#{}""##, hex); + let _ = write!(stop, r##" stop-color="#{}""##, color.to_rgb_hex_srgb_from_gamma()); if color.a() < 1. { let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.); } @@ -362,8 +361,7 @@ impl Fill { match self { Self::None => r#" fill="none""#.to_string(), Self::Solid(color) => { - let hex = if !render_params.for_mask { color.to_rgb_hex_srgb_from_gamma() } else { "ffffff".to_string() }; - let mut result = format!(r##" fill="#{}""##, hex); + let mut result = format!(r##" fill="#{}""##, color.to_rgb_hex_srgb_from_gamma()); if color.a() < 1. { let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.); } @@ -628,7 +626,7 @@ impl Stroke { } /// Provide the SVG attributes for the stroke. - pub fn render(&self, render_params: &RenderParams) -> String { + pub fn render(&self, _render_params: &RenderParams) -> String { // Don't render a stroke at all if it would be invisible let Some(color) = self.color else { return String::new() }; if self.weight <= 0. || color.a() == 0. { @@ -644,8 +642,7 @@ impl Stroke { let line_join_miter_limit = (self.line_join_miter_limit != 4.).then_some(self.line_join_miter_limit); // Render the needed stroke attributes - let hex = if !render_params.for_mask { color.to_rgb_hex_srgb_from_gamma() } else { "ffffff".to_string() }; - let mut attributes = format!(r##" stroke="#{}""##, hex); + let mut attributes = format!(r##" stroke="#{}""##, color.to_rgb_hex_srgb_from_gamma()); if color.a() < 1. { let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.); } From 79cc9aa5428804a302ca30bd6adc7f3eb8be799e Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Mon, 19 May 2025 20:01:12 +0530 Subject: [PATCH 08/39] add plumbing to know if clipped in frontend and add fill slider --- .../portfolio/document/document_message.rs | 3 +++ .../document/document_message_handler.rs | 26 +++++++++++++++++++ .../graph_operation_message.rs | 4 +++ .../graph_operation_message_handler.rs | 5 ++++ .../document/graph_operation/utility_types.rs | 6 +++++ .../node_graph/node_graph_message_handler.rs | 2 ++ .../portfolio/document/utility_types/nodes.rs | 1 + .../graph_modification_utils.rs | 8 ++++++ frontend/src/components/panels/Layers.svelte | 5 ++++ frontend/src/messages.ts | 2 ++ 10 files changed, 62 insertions(+) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 00b3f2c924..06e9900dcd 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -145,6 +145,9 @@ pub enum DocumentMessage { SetOpacityForSelectedLayers { opacity: f64, }, + SetFillForSelectedLayers { + fill: f64, + }, SetOverlaysVisibility { visible: bool, overlays_type: Option, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index ecf08e77d3..3e8dc155c2 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -1152,6 +1152,12 @@ impl MessageHandler> for DocumentMessag responses.add(GraphOperationMessage::OpacitySet { layer, opacity }); } } + DocumentMessage::SetFillForSelectedLayers { fill } => { + let fill = fill.clamp(0., 1.); + for layer in self.network_interface.selected_nodes().selected_layers_except_artboards(&self.network_interface) { + responses.add(GraphOperationMessage::BlendingFillSet { layer, fill }); + } + } DocumentMessage::SetOverlaysVisibility { visible, overlays_type } => { let visibility_settings = &mut self.overlays_visibility_settings; let overlays_type = match overlays_type { @@ -2517,6 +2523,26 @@ impl DocumentMessageHandler { }) .on_commit(|_| DocumentMessage::AddTransaction.into()) .widget_holder(), + Separator::new(SeparatorType::Related).widget_holder(), + NumberInput::new(opacity) + .label("Fill") + .unit("%") + .display_decimal_places(2) + .disabled(disabled) + .min(0.) + .max(100.) + .range_min(Some(0.)) + .range_max(Some(100.)) + .mode_range() + .on_update(|number_input: &NumberInput| { + if let Some(value) = number_input.value { + DocumentMessage::SetFillForSelectedLayers { fill: value / 100. }.into() + } else { + Message::NoOp + } + }) + .on_commit(|_| DocumentMessage::AddTransaction.into()) + .widget_holder(), // Separator::new(SeparatorType::Unrelated).widget_holder(), // diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index bcfe6efb0b..803e028a67 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -21,6 +21,10 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, fill: Fill, }, + BlendingFillSet { + layer: LayerNodeIdentifier, + fill: f64, + }, OpacitySet { layer: LayerNodeIdentifier, opacity: f64, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index d030ce4d7f..a1c58f21af 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -42,6 +42,11 @@ impl MessageHandler> for Gr modify_inputs.fill_set(fill); } } + GraphOperationMessage::BlendingFillSet { layer, fill } => { + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { + modify_inputs.blending_fill_set(fill); + } + } GraphOperationMessage::OpacitySet { layer, opacity } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { modify_inputs.opacity_set(opacity); diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 4e732f2f32..e804c8ae1a 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -352,6 +352,12 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(opacity * 100.), false), false); } + pub fn blending_fill_set(&mut self, fill: f64) { + let Some(blend_node_id) = self.existing_node_id("Blending", true) else { return }; + let input_connector = InputConnector::node(blend_node_id, 3); + self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::F64(fill * 100.), false), false); + } + pub fn stroke_set(&mut self, stroke: Stroke) { let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return }; 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 d64a7535ad..9367d0ea9f 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 @@ -15,6 +15,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{ use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, LayerPanelEntry}; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::auto_panning::AutoPanning; +use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode; use crate::messages::tool::tool_messages::tool_prelude::{Key, MouseMotion}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use glam::{DAffine2, DVec2, IVec2}; @@ -2437,6 +2438,7 @@ impl NodeGraphMessageHandler { selected: selected_layers.contains(&node_id), ancestor_of_selected: ancestors_of_selected.contains(&node_id), descendant_of_selected: descendants_of_selected.contains(&node_id), + clipped: get_clip_mode(layer, network_interface).unwrap_or(false), }; responses.add(FrontendMessage::UpdateDocumentLayerDetails { data }); } diff --git a/editor/src/messages/portfolio/document/utility_types/nodes.rs b/editor/src/messages/portfolio/document/utility_types/nodes.rs index 2cac92257c..4381cb3061 100644 --- a/editor/src/messages/portfolio/document/utility_types/nodes.rs +++ b/editor/src/messages/portfolio/document/utility_types/nodes.rs @@ -55,6 +55,7 @@ pub struct LayerPanelEntry { pub ancestor_of_selected: bool, #[serde(rename = "descendantOfSelected")] pub descendant_of_selected: bool, + pub clipped: bool, } #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, PartialEq, Eq, specta::Type)] diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index df9ad13fe9..183010f4b0 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -303,6 +303,14 @@ pub fn get_opacity(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn Some(*opacity) } +pub fn get_fill(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?; + let TaggedValue::F64(fill) = inputs.get(3)?.as_value()? else { + return None; + }; + Some(*fill) +} + pub fn get_clip_mode(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { let inputs = NodeGraphLayer::new(layer, network_interface).find_node_inputs("Blending")?; let TaggedValue::Bool(clip) = inputs.get(4)?.as_value()? else { diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 2ace6e7584..f4f5f7f503 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -427,6 +427,7 @@ "descendant-of-selected": listing.entry.descendantOfSelected, "selected-but-not-in-selected-network": selected && !listing.entry.inSelectedNetwork, "insert-folder": (draggingData?.highlightFolder || false) && draggingData?.insertParentId === listing.entry.id, + "clipped-layer": listing.entry.clipped, }} styles={{ "--layer-indent-levels": `${listing.entry.depth - 1}` }} data-layer @@ -572,6 +573,10 @@ outline-offset: -3px; } + &.clipped-layer { + margin-left: 12px; + } + .expand-arrow { padding: 0; margin: 0; diff --git a/frontend/src/messages.ts b/frontend/src/messages.ts index 2272069cc9..8fbe5d80fc 100644 --- a/frontend/src/messages.ts +++ b/frontend/src/messages.ts @@ -907,6 +907,8 @@ export class LayerPanelEntry { ancestorOfSelected!: boolean; descendantOfSelected!: boolean; + + clipped!: boolean; } export class DisplayDialogDismiss extends JsMessage {} From a680c7fa2cebf9ac15704829e6185bcffd5b48c5 Mon Sep 17 00:00:00 2001 From: mtvare6 Date: Wed, 21 May 2025 05:02:37 +0530 Subject: [PATCH 09/39] Attempt at document upgrade code --- .../portfolio/portfolio_message_handler.rs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 1b6ab69355..04d7333a74 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -675,6 +675,67 @@ impl MessageHandler> for PortfolioMes } } + // Combine the Blend Mode node and Opacity node into one + if reference == "Blend Mode" && inputs_count == 2 { + let new_node_definition_name = "Blending"; + if let Some(blending_node_definition) = resolve_document_node_type(new_node_definition_name) { + let new_node_template = blending_node_definition.default_node_template(); + let new_document_node_struct = new_node_template.document_node; + + document + .network_interface + .replace_implementation(node_id, network_path, new_document_node_struct.implementation.clone()); + let old_inputs = document.network_interface.replace_inputs(node_id, new_document_node_struct.inputs.clone(), network_path); + document + .network_interface + .replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata.clone()); + document.network_interface.set_reference(node_id, network_path, Some(new_node_definition_name.to_string())); + + document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); + document.network_interface.set_input(&InputConnector::node(*node_id, 1), old_inputs[1].clone(), network_path); + document + .network_interface + .set_input(&InputConnector::node(*node_id, 2), NodeInput::value(TaggedValue::F64(100.0), false), network_path); + document + .network_interface + .set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::F64(100.0), false), network_path); + document + .network_interface + .set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::Bool(false), false), network_path); + } + } + + if reference == "Opacity" && inputs_count == 2 { + let new_node_definition_name = "Blending"; + if let Some(blending_node_definition) = resolve_document_node_type(new_node_definition_name) { + let new_node_template = blending_node_definition.default_node_template(); + let new_document_node_struct = new_node_template.document_node; + + document + .network_interface + .replace_implementation(node_id, network_path, new_document_node_struct.implementation.clone()); + let old_inputs = document.network_interface.replace_inputs(node_id, new_document_node_struct.inputs.clone(), network_path); + document + .network_interface + .replace_implementation_metadata(node_id, network_path, new_node_template.persistent_node_metadata.clone()); + document.network_interface.set_reference(node_id, network_path, Some(new_node_definition_name.to_string())); + + document.network_interface.set_input(&InputConnector::node(*node_id, 0), old_inputs[0].clone(), network_path); + document.network_interface.set_input( + &InputConnector::node(*node_id, 1), + NodeInput::value(TaggedValue::BlendMode(graphene_core::raster::BlendMode::Normal), false), + network_path, + ); + document.network_interface.set_input(&InputConnector::node(*node_id, 2), old_inputs[1].clone(), network_path); + document + .network_interface + .set_input(&InputConnector::node(*node_id, 3), NodeInput::value(TaggedValue::F64(100.0), false), network_path); + document + .network_interface + .set_input(&InputConnector::node(*node_id, 4), NodeInput::value(TaggedValue::Bool(false), false), network_path); + } + } + // Rename the old "Splines from Points" node to "Spline" and upgrade it to the new "Spline" node if reference == "Splines from Points" { document.network_interface.set_reference(node_id, network_path, Some("Spline".to_string())); From 209a69d97c118ccd20cdc881d7a3b218af6df4e4 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 20 May 2025 22:49:34 -0700 Subject: [PATCH 10/39] Fix fill slider --- .../document/document_message_handler.rs | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 161c3c6041..7cc4a582df 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -20,7 +20,7 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Flo use crate::messages::portfolio::document::utility_types::nodes::RawBuffer; use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; -use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_opacity}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_blend_mode, get_fill, get_opacity}; use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys; use crate::messages::tool::tool_messages::tool_prelude::Key; use crate::messages::tool::utility_types::ToolType; @@ -2467,38 +2467,47 @@ impl DocumentMessageHandler { let selected_layers_except_artboards = selected_nodes.selected_layers_except_artboards(&self.network_interface); // Look up the current opacity and blend mode of the selected layers (if any), and split the iterator into the first tuple and the rest. - let mut opacity_and_blend_mode = selected_layers_except_artboards.map(|layer| { + let mut blending_options = selected_layers_except_artboards.map(|layer| { ( get_opacity(layer, &self.network_interface).unwrap_or(100.), + get_fill(layer, &self.network_interface).unwrap_or(100.), get_blend_mode(layer, &self.network_interface).unwrap_or_default(), ) }); - let first_opacity_and_blend_mode = opacity_and_blend_mode.next(); - let result_opacity_and_blend_mode = opacity_and_blend_mode; + let first_blending_options = blending_options.next(); + let result_blending_options = blending_options; // If there are no selected layers, disable the opacity and blend mode widgets. - let disabled = first_opacity_and_blend_mode.is_none(); + let disabled = first_blending_options.is_none(); // Amongst the selected layers, check if the opacities and blend modes are identical across all layers. // The result is setting `option` and `blend_mode` to Some value if all their values are identical, or None if they are not. // If identical, we display the value in the widget. If not, we display a dash indicating dissimilarity. - let (opacity, blend_mode) = first_opacity_and_blend_mode - .map(|(first_opacity, first_blend_mode)| { + let (opacity, fill, blend_mode) = first_blending_options + .map(|(first_opacity, first_fill, first_blend_mode)| { let mut opacity_identical = true; + let mut fill_identical = true; let mut blend_mode_identical = true; - for (opacity, blend_mode) in result_opacity_and_blend_mode { + for (opacity, fill, blend_mode) in result_blending_options { if (opacity - first_opacity).abs() > (f64::EPSILON * 100.) { opacity_identical = false; } + if (fill - first_fill).abs() > (f64::EPSILON * 100.) { + fill_identical = false; + } if blend_mode != first_blend_mode { blend_mode_identical = false; } } - (opacity_identical.then_some(first_opacity), blend_mode_identical.then_some(first_blend_mode)) + ( + opacity_identical.then_some(first_opacity), + fill_identical.then_some(first_fill), + blend_mode_identical.then_some(first_blend_mode), + ) }) - .unwrap_or((None, None)); + .unwrap_or((None, None, None)); let blend_mode_menu_entries = BlendMode::list_svg_subset() .iter() @@ -2558,7 +2567,7 @@ impl DocumentMessageHandler { .tooltip("Opacity") .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - NumberInput::new(opacity) + NumberInput::new(fill) .label("Fill") .unit("%") .display_decimal_places(0) From 9f2e4369146d8635c2ea21360933ded9f5fa3005 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Tue, 20 May 2025 22:50:14 -0700 Subject: [PATCH 11/39] Add clipped styling and Alt-click layer border --- frontend/src/components/panels/Layers.svelte | 99 +++++++++++++++++--- 1 file changed, 84 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/panels/Layers.svelte b/frontend/src/components/panels/Layers.svelte index 4922dbeaaa..cbeef4cb82 100644 --- a/frontend/src/components/panels/Layers.svelte +++ b/frontend/src/components/panels/Layers.svelte @@ -1,5 +1,5 @@