From 29222700f46bff4a69e760b7e84af2ea229e0572 Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:27:23 +0000 Subject: [PATCH] Fix the blend mode and opacity widgets of the Layers panel (#1506) * Fix blend mode and opacity * Cleanup and bug fixes --------- Co-authored-by: Keavon Chambers --- document-legacy/src/document_metadata.rs | 8 +- document-legacy/src/layers/layer_info.rs | 8 +- .../messages/dialog/dialog_message_handler.rs | 4 +- .../src/messages/frontend/frontend_message.rs | 2 +- .../messages/layout/layout_message_handler.rs | 2 +- .../layout/utility_types/layout_widget.rs | 2 +- .../document/document_message_handler.rs | 106 +++++------ .../node_graph/graph_operation_message.rs | 9 + .../graph_operation_message_handler.rs | 33 +++- .../node_properties.rs | 14 +- .../portfolio/portfolio_message_handler.rs | 2 +- .../graph_modification_utils.rs | 66 ++++--- .../tool/tool_messages/artboard_tool.rs | 7 +- .../messages/tool/tool_messages/brush_tool.rs | 35 +--- .../tool/tool_messages/select_tool.rs | 9 +- .../messages/tool/tool_messages/text_tool.rs | 6 +- frontend/src/components/panels/Layers.svelte | 12 +- frontend/src/wasm-communication/messages.ts | 4 +- node-graph/gcore/src/raster/adjustments.rs | 177 +++++++++--------- node-graph/graph-craft/src/document.rs | 6 + 20 files changed, 271 insertions(+), 241 deletions(-) diff --git a/document-legacy/src/document_metadata.rs b/document-legacy/src/document_metadata.rs index b2ab66a0..30e880f6 100644 --- a/document-legacy/src/document_metadata.rs +++ b/document-legacy/src/document_metadata.rs @@ -54,6 +54,10 @@ impl DocumentMetadata { self.all_layers().filter(|layer| self.selected_nodes.contains(&layer.to_node())) } + pub fn selected_layers_except_artboards(&self) -> impl Iterator + '_ { + self.selected_layers().filter(move |layer| !self.artboards.contains(layer)) + } + pub fn selected_layers_contains(&self, layer: LayerNodeIdentifier) -> bool { self.selected_layers().any(|selected| selected == layer) } @@ -259,7 +263,7 @@ impl DocumentMetadata { } pub fn is_artboard(layer: LayerNodeIdentifier, network: &NodeNetwork) -> bool { - network.upstream_flow_back_from_nodes(vec![layer.to_node()], true).any(|(node, _)| node.name == "Artboard") + network.upstream_flow_back_from_nodes(vec![layer.to_node()], true).any(|(node, _)| node.is_artboard()) } pub fn is_folder(layer: LayerNodeIdentifier, network: &NodeNetwork) -> bool { @@ -267,7 +271,7 @@ pub fn is_folder(layer: LayerNodeIdentifier, network: &NodeNetwork) -> bool { || network .upstream_flow_back_from_nodes(vec![layer.to_node()], true) .skip(1) - .any(|(node, _)| node.name == "Artboard" || node.is_layer()) + .any(|(node, _)| node.is_artboard() || node.is_layer()) } // click targets diff --git a/document-legacy/src/layers/layer_info.rs b/document-legacy/src/layers/layer_info.rs index 7546800c..c5bf2e55 100644 --- a/document-legacy/src/layers/layer_info.rs +++ b/document-legacy/src/layers/layer_info.rs @@ -342,13 +342,7 @@ impl Layer { self.transform.to_cols_array().iter().enumerate().for_each(|(i, f)| { let _ = self.cache.write_str(&(f.to_string() + if i == 5 { "" } else { "," })); }); - let _ = write!( - self.cache, - r#")" style="mix-blend-mode: {}; opacity: {}">{}"#, - self.blend_mode.to_svg_style_name(), - self.opacity, - self.thumbnail_cache.as_str() - ); + let _ = write!(self.cache, r#")" style="opacity: {};{}">{}"#, self.opacity, self.blend_mode.render(), self.thumbnail_cache.as_str()); self.cache_dirty = false; } diff --git a/editor/src/messages/dialog/dialog_message_handler.rs b/editor/src/messages/dialog/dialog_message_handler.rs index 5a130cad..4a243116 100644 --- a/editor/src/messages/dialog/dialog_message_handler.rs +++ b/editor/src/messages/dialog/dialog_message_handler.rs @@ -1,7 +1,7 @@ use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog}; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; -use crate::messages::tool::common_functionality::graph_modification_utils::is_artboard; +use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name; /// Stores the dialogs which require state. These are the ones that have their own message handlers, and are not the ones defined in `simple_dialogs`. #[derive(Debug, Default, Clone)] @@ -78,7 +78,7 @@ impl MessageHandler> for DialogMessageHandler { .document_legacy .metadata .all_layers() - .filter(|&layer| is_artboard(layer, &document.document_legacy)) + .filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.document_legacy, "Artboard")) .map(|layer| { ( layer, diff --git a/editor/src/messages/frontend/frontend_message.rs b/editor/src/messages/frontend/frontend_message.rs index f8665780..386f519c 100644 --- a/editor/src/messages/frontend/frontend_message.rs +++ b/editor/src/messages/frontend/frontend_message.rs @@ -206,7 +206,7 @@ pub enum FrontendMessage { #[serde(rename = "hintData")] hint_data: HintData, }, - UpdateLayerTreeOptionsLayout { + UpdateLayersPanelOptionsLayout { #[serde(rename = "layoutTarget")] layout_target: LayoutTarget, diff: Vec, diff --git a/editor/src/messages/layout/layout_message_handler.rs b/editor/src/messages/layout/layout_message_handler.rs index dd90dbbc..500870b2 100644 --- a/editor/src/messages/layout/layout_message_handler.rs +++ b/editor/src/messages/layout/layout_message_handler.rs @@ -289,7 +289,7 @@ impl LayoutMessageHandler { LayoutTarget::DocumentBar => FrontendMessage::UpdateDocumentBarLayout { layout_target, diff }, LayoutTarget::DocumentMode => FrontendMessage::UpdateDocumentModeLayout { layout_target, diff }, LayoutTarget::GraphViewOverlayButton => FrontendMessage::UpdateGraphViewOverlayButtonLayout { layout_target, diff }, - LayoutTarget::LayerTreeOptions => FrontendMessage::UpdateLayerTreeOptionsLayout { layout_target, diff }, + LayoutTarget::LayersPanelOptions => FrontendMessage::UpdateLayersPanelOptionsLayout { layout_target, diff }, LayoutTarget::MenuBar => unreachable!("Menu bar is not diffed"), LayoutTarget::NodeGraphBar => FrontendMessage::UpdateNodeGraphBarLayout { layout_target, diff }, LayoutTarget::PropertiesOptions => FrontendMessage::UpdatePropertyPanelOptionsLayout { layout_target, diff }, diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index 73fb9dfd..bd223c2d 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -27,7 +27,7 @@ pub enum LayoutTarget { /// The button below the tool shelf and directly above the working colors which lets the user toggle the node graph overlaid on the canvas. GraphViewOverlayButton, /// Options for opacity seen at the top of the Layers panel. - LayerTreeOptions, + LayersPanelOptions, /// The dropdown menu at the very top of the application: File, Edit, etc. MenuBar, /// Bar at the top of the node graph containing the location and the "Preview" and "Hide" buttons. diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index e3ded402..b47cbc8d 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -12,6 +12,7 @@ use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, use crate::messages::portfolio::document::utility_types::vectorize_layer_metadata; use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; +use crate::messages::tool::common_functionality::graph_modification_utils::{get_blend_mode, get_opacity}; use crate::messages::tool::utility_types::ToolType; use crate::node_graph_executor::NodeGraphExecutor; @@ -222,7 +223,7 @@ impl MessageHandler> for DocumentMessageHand responses.add(FolderChanged { affected_folder_path: vec![] }); responses.add(BroadcastEvent::SelectionChanged); - self.update_layer_tree_options_bar_widgets(responses, &render_data); + self.update_layers_panel_options_bar_widgets(responses); } AlignSelectedLayers { axis, aggregate } => { self.backup(responses); @@ -268,7 +269,7 @@ impl MessageHandler> for DocumentMessageHand // Clear the options bar responses.add(LayoutMessage::SendLayout { layout: Layout::WidgetLayout(Default::default()), - layout_target: LayoutTarget::LayerTreeOptions, + layout_target: LayoutTarget::LayersPanelOptions, }); } CommitTransaction => (), @@ -449,7 +450,7 @@ impl MessageHandler> for DocumentMessageHand responses.add(FrontendMessage::UpdateDocumentLayerDetails { data: layer_entry }); } responses.add(PropertiesPanelMessage::CheckSelectedWasUpdated { path: affected_layer_path }); - self.update_layer_tree_options_bar_widgets(responses, &render_data); + self.update_layers_panel_options_bar_widgets(responses); } MoveSelectedLayersTo { parent, insert_index } => { let selected_layers = self.metadata().selected_layers().collect::>(); @@ -697,8 +698,8 @@ impl MessageHandler> for DocumentMessageHand } SetBlendModeForSelectedLayers { blend_mode } => { self.backup(responses); - for path in self.selected_layers() { - responses.add(DocumentOperation::SetLayerBlendMode { path: path.to_vec(), blend_mode }); + for layer in self.metadata().selected_layers_except_artboards() { + responses.add(GraphOperationMessage::BlendModeSet { layer: layer.to_path(), blend_mode }); } } SetImageBlobUrl { @@ -737,10 +738,10 @@ impl MessageHandler> for DocumentMessageHand } SetOpacityForSelectedLayers { opacity } => { self.backup(responses); - let opacity = opacity.clamp(0., 1.); + let opacity = opacity.clamp(0., 1.) as f32; - for path in self.selected_layers().map(|path| path.to_vec()) { - responses.add(DocumentOperation::SetLayerOpacity { path, opacity }); + for layer in self.metadata().selected_layers_except_artboards() { + responses.add(GraphOperationMessage::OpacitySet { layer: layer.to_path(), opacity }); } } SetOverlaysVisibility { visible } => { @@ -1519,72 +1520,71 @@ impl DocumentMessageHandler { }); } - pub fn update_layer_tree_options_bar_widgets(&self, responses: &mut VecDeque, render_data: &RenderData) { - let mut opacity = None; - let mut opacity_is_mixed = false; + pub fn update_layers_panel_options_bar_widgets(&self, responses: &mut VecDeque) { + // Get an iterator over the selected layers (excluding artboards which don't have an opacity or blend mode). + let selected_layers_except_artboards = self.metadata().selected_layers_except_artboards(); - let mut blend_mode = None; - let mut blend_mode_is_mixed = false; + // 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| { + ( + get_opacity(layer, &self.document_legacy).unwrap_or(100.), + get_blend_mode(layer, &self.document_legacy).unwrap_or_default(), + ) + }); + let first_opacity_and_blend_mode = opacity_and_blend_mode.next(); + let result_opacity_and_blend_mode = opacity_and_blend_mode; - self.layer_metadata - .keys() - .filter_map(|path| self.layer_panel_entry_from_path(path, render_data)) - .filter(|layer_panel_entry| layer_panel_entry.layer_metadata.selected) - .flat_map(|layer_panel_entry| self.document_legacy.layer(layer_panel_entry.path.as_slice())) - .for_each(|layer| { - match opacity { - None => opacity = Some(layer.opacity), - Some(opacity) => { - if (opacity - layer.opacity).abs() > (1. / 1_000_000.) { - opacity_is_mixed = true; - } + // If there are no selected layers, disable the opacity and blend mode widgets. + let disabled = first_opacity_and_blend_mode.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 mut opacity_identical = true; + let mut blend_mode_identical = true; + + for (opacity, blend_mode) in result_opacity_and_blend_mode { + if (opacity - first_opacity).abs() > (f32::EPSILON * 100.) { + opacity_identical = false; + } + if blend_mode != first_blend_mode { + blend_mode_identical = false; } } - match blend_mode { - None => blend_mode = Some(layer.blend_mode), - Some(blend_mode) => { - if blend_mode != layer.blend_mode { - blend_mode_is_mixed = true; - } - } - } - }); + (opacity_identical.then(|| first_opacity), blend_mode_identical.then(|| first_blend_mode)) + }) + .unwrap_or((None, None)); - if opacity_is_mixed { - opacity = None; - } - if blend_mode_is_mixed { - blend_mode = None; - } - - let blend_mode_menu_entries = BlendMode::list_modes_in_groups() + let blend_mode_menu_entries = BlendMode::list_svg_subset() .iter() .map(|modes| { modes .iter() - .map(|mode| { - MenuListEntry::new(mode.to_string()) - .value(mode.to_string()) - .on_update(|_| DocumentMessage::SetBlendModeForSelectedLayers { blend_mode: *mode }.into()) + .map(|&blend_mode| { + MenuListEntry::new(blend_mode.to_string()) + .value(blend_mode.to_string()) + .on_update(move |_| DocumentMessage::SetBlendModeForSelectedLayers { blend_mode }.into()) }) .collect() }) .collect(); - let layer_tree_options = WidgetLayout::new(vec![LayoutGroup::Row { + let layers_panel_options_bar = WidgetLayout::new(vec![LayoutGroup::Row { widgets: vec![ DropdownInput::new(blend_mode_menu_entries) - .selected_index(blend_mode.map(|blend_mode| blend_mode as u32)) - .disabled(blend_mode.is_none() && !blend_mode_is_mixed) + .selected_index(blend_mode.map(|blend_mode| blend_mode.index_in_list_svg_subset()).flatten().map(|index| index as u32)) + .disabled(disabled) .draw_icon(false) .widget_holder(), Separator::new(SeparatorType::Related).widget_holder(), - NumberInput::new(opacity.map(|opacity| opacity * 100.)) + NumberInput::new(opacity.map(|opacity| opacity as f64)) .label("Opacity") .unit("%") .display_decimal_places(2) - .disabled(opacity.is_none() && !opacity_is_mixed) + .disabled(disabled) .min(0.) .max(100.) .range_min(Some(0.)) @@ -1613,8 +1613,8 @@ impl DocumentMessageHandler { }]); responses.add(LayoutMessage::SendLayout { - layout: Layout::WidgetLayout(layer_tree_options), - layout_target: LayoutTarget::LayerTreeOptions, + layout: Layout::WidgetLayout(layers_panel_options_bar), + layout_target: LayoutTarget::LayersPanelOptions, }); } diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs index d13bfd31..d45165c1 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs @@ -4,6 +4,7 @@ use bezier_rs::Subpath; use document_legacy::document_metadata::LayerNodeIdentifier; use graph_craft::document::DocumentNode; use graph_craft::document::NodeId; +use graphene_core::raster::BlendMode; use graphene_core::raster::ImageFrame; use graphene_core::text::Font; use graphene_core::uuid::ManipulatorGroupId; @@ -23,6 +24,14 @@ pub enum GraphOperationMessage { layer: LayerIdentifier, fill: Fill, }, + OpacitySet { + layer: LayerIdentifier, + opacity: f32, + }, + BlendModeSet { + layer: LayerIdentifier, + blend_mode: BlendMode, + }, UpdateBounds { layer: LayerIdentifier, old_bounds: [DVec2; 2], diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs index 5f650669..f08eb5c8 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs @@ -7,7 +7,7 @@ use document_legacy::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use document_legacy::{LayerId, Operation}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{generate_uuid, DocumentNode, NodeId, NodeInput, NodeNetwork, NodeOutput}; -use graphene_core::raster::ImageFrame; +use graphene_core::raster::{BlendMode, ImageFrame}; use graphene_core::text::Font; use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::brush_stroke::BrushStroke; @@ -284,7 +284,6 @@ impl<'a> ModifyInputsContext<'a> { let new_input = output_node.inputs.first().cloned().filter(|input| input.as_node().is_some()); let node_id = generate_uuid(); - output_node.metadata.position.x += 8; output_node.inputs[0] = NodeInput::node(node_id, 0); let Some(node_type) = resolve_document_node_type(name) else { @@ -294,6 +293,12 @@ impl<'a> ModifyInputsContext<'a> { let mut new_document_node = node_type.to_document_node_default_inputs([new_input], metadata); update_input(&mut new_document_node.inputs, node_id, self.document_metadata); self.network.nodes.insert(node_id, new_document_node); + + let upstream_nodes = self.network.upstream_flow_back_from_nodes(vec![node_id], true).map(|(_, id)| id).collect::>(); + for node_id in upstream_nodes { + let Some(node) = self.network.nodes.get_mut(&node_id) else { continue }; + node.metadata.position.x -= 8; + } } /// Changes the inputs of a specific node @@ -367,6 +372,18 @@ impl<'a> ModifyInputsContext<'a> { }); } + fn opacity_set(&mut self, opacity: f32) { + self.modify_inputs("Opacity", false, |inputs, _node_id, _metadata| { + inputs[1] = NodeInput::value(TaggedValue::F32(opacity * 100.), false); + }); + } + + fn blend_mode_set(&mut self, blend_mode: BlendMode) { + self.modify_inputs("Blend Mode", false, |inputs, _node_id, _metadata| { + inputs[1] = NodeInput::value(TaggedValue::BlendMode(blend_mode), false); + }); + } + fn stroke_set(&mut self, stroke: Stroke) { self.modify_inputs("Stroke", false, |inputs, _node_id, _metadata| { inputs[1] = NodeInput::value(TaggedValue::OptionalColor(stroke.color), false); @@ -564,6 +581,16 @@ impl MessageHandler { + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(&layer, document, node_graph, responses) { + modify_inputs.opacity_set(opacity); + } + } + GraphOperationMessage::BlendModeSet { layer, blend_mode } => { + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(&layer, document, node_graph, responses) { + modify_inputs.blend_mode_set(blend_mode); + } + } GraphOperationMessage::UpdateBounds { layer, old_bounds, new_bounds } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(&layer, document, node_graph, responses) { modify_inputs.update_bounds(old_bounds, new_bounds); @@ -715,7 +742,7 @@ impl MessageHandler>(); for layer in layer_nodes { - if modify_inputs.network.upstream_flow_back_from_nodes(vec![layer], true).any(|(node, _id)| node.name == "Artboard") { + if modify_inputs.network.upstream_flow_back_from_nodes(vec![layer], true).any(|(node, _id)| node.is_artboard()) { modify_inputs.delete_layer(layer); } } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index cde38c9b..47a0875e 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -368,33 +368,35 @@ fn noise_type(document_node: &DocumentNode, node_id: u64, index: usize, name: &s LayoutGroup::Row { widgets }.with_tooltip("Type of Noise") } -//TODO Use generalized Version of this as soon as it's available +// TODO: Use generalized version of this as soon as it's available fn blend_mode(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); if let &NodeInput::Value { - tagged_value: TaggedValue::BlendMode(mode), + tagged_value: TaggedValue::BlendMode(blend_mode), exposed: false, } = &document_node.inputs[index] { - let entries = BlendMode::list() + let entries = BlendMode::list_svg_subset() .iter() .map(|category| { category .iter() - .map(|mode| MenuListEntry::new(mode.to_string()).on_update(update_value(move |_| TaggedValue::BlendMode(*mode), node_id, index))) + .map(|blend_mode| MenuListEntry::new(blend_mode.to_string()).on_update(update_value(move |_| TaggedValue::BlendMode(*blend_mode), node_id, index))) .collect() }) .collect(); widgets.extend_from_slice(&[ Separator::new(SeparatorType::Unrelated).widget_holder(), - DropdownInput::new(entries).selected_index(Some(mode as u32)).widget_holder(), + DropdownInput::new(entries) + .selected_index(blend_mode.index_in_list_svg_subset().map(|index| index as u32)) + .widget_holder(), ]); } LayoutGroup::Row { widgets }.with_tooltip("Formula used for blending") } -// TODO: Generalize this for all dropdowns ( also see blend_mode and channel_extration ) +// TODO: Generalize this for all dropdowns (also see blend_mode and channel_extration) fn luminance_calculation(document_node: &DocumentNode, node_id: u64, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); if let &NodeInput::Value { diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index 35aafebc..2b7d0f4f 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -680,7 +680,7 @@ impl PortfolioMessageHandler { .map(|entry| FrontendMessage::UpdateDocumentLayerDetails { data: entry }.into()) .collect::>(), ); - new_document.update_layer_tree_options_bar_widgets(responses, &render_data); + new_document.update_layers_panel_options_bar_widgets(responses); self.documents.insert(document_id, new_document); 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 90293bef..81887576 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -4,7 +4,7 @@ use crate::messages::prelude::*; use bezier_rs::{ManipulatorGroup, Subpath}; use document_legacy::{document::Document, document_metadata::LayerNodeIdentifier, LayerId, Operation}; use graph_craft::document::{value::TaggedValue, DocumentNode, NodeId, NodeInput, NodeNetwork}; -use graphene_core::raster::ImageFrame; +use graphene_core::raster::{BlendMode, ImageFrame}; use graphene_core::text::Font; use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::style::{FillType, Gradient}; @@ -98,7 +98,7 @@ pub fn get_mirror_handles(layer: LayerNodeIdentifier, document: &Document) -> Op } } -/// Get the current gradient of a layer from the closest fill node +/// Get the current gradient of a layer from the closest Fill node pub fn get_gradient(layer: LayerNodeIdentifier, document: &Document) -> Option { let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Fill")?; let TaggedValue::FillType(FillType::Gradient) = inputs.get(1)?.as_value()? else { @@ -128,7 +128,7 @@ pub fn get_gradient(layer: LayerNodeIdentifier, document: &Document) -> Option Option { let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Fill")?; let TaggedValue::Color(color) = inputs.get(2)?.as_value()? else { @@ -137,14 +137,39 @@ pub fn get_fill_color(layer: LayerNodeIdentifier, document: &Document) -> Option Some(*color) } -pub fn get_text_id(layer: LayerNodeIdentifier, document: &Document) -> Option { - NodeGraphLayer::new(layer, document)?.node_id("Text") +/// Get the current blend mode of a layer from the closest Blend Mode node +pub fn get_blend_mode(layer: LayerNodeIdentifier, document: &Document) -> Option { + let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Blend Mode")?; + 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. +/// 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 parameter value driven by another node +/// - Already factored into the pixel alpha channel of an image +/// - The default value of 100% if no Opacity node is present, but this function returns None in that case +/// 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, document: &Document) -> Option { + let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Opacity")?; + let TaggedValue::F32(opacity) = inputs.get(1)?.as_value()? else { + return None; + }; + Some(*opacity) +} + pub fn get_fill_id(layer: LayerNodeIdentifier, document: &Document) -> Option { NodeGraphLayer::new(layer, document)?.node_id("Fill") } -/// Gets properties from the text node +pub fn get_text_id(layer: LayerNodeIdentifier, document: &Document) -> Option { + NodeGraphLayer::new(layer, document)?.node_id("Text") +} + +/// Gets properties from the Text node pub fn get_text(layer: LayerNodeIdentifier, document: &Document) -> Option<(&String, &Font, f64)> { let inputs = NodeGraphLayer::new(layer, document)?.find_node_inputs("Text")?; let NodeInput::Value { @@ -174,19 +199,9 @@ pub fn get_text(layer: LayerNodeIdentifier, document: &Document) -> Option<(&Str Some((text, font, font_size)) } -/// Is a specified layer an artboard? -pub fn is_artboard(layer: LayerNodeIdentifier, document: &Document) -> bool { - NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.uses_node("Artboard")) -} - -/// Is a specified layer a shape? -pub fn is_shape_layer(layer: LayerNodeIdentifier, document: &Document) -> bool { - NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.uses_node("Shape")) -} - -/// Is a specified layer text? -pub fn is_text_layer(layer: LayerNodeIdentifier, document: &Document) -> bool { - NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.uses_node("Text")) +/// Checks if a specified layer uses an upstream node matching the given name. +pub fn is_layer_fed_by_node_of_name(layer: LayerNodeIdentifier, document: &Document, node_name: &str) -> bool { + NodeGraphLayer::new(layer, document).is_some_and(|layer| layer.find_node_inputs(node_name).is_some()) } /// Convert subpaths to an iterator of manipulator groups @@ -243,19 +258,18 @@ impl<'a> NodeGraphLayer<'a> { self.node_graph.upstream_flow_back_from_nodes(vec![self.layer_node], true) } - /// Does a node exist in the layer's primary flow - pub fn uses_node(&self, node_name: &str) -> bool { - self.primary_layer_flow().any(|(node, _id)| node.name == node_name) - } - /// Node id of a node if it exists in the layer's primary flow pub fn node_id(&self, node_name: &str) -> Option { self.primary_layer_flow().find(|(node, _id)| node.name == node_name).map(|(_node, id)| id) } - /// Find all of the inputs of a specific node within the layer's primary flow + /// Find all of the inputs of a specific node within the layer's primary flow, up until the next layer is reached. pub fn find_node_inputs(&self, node_name: &str) -> Option<&'a Vec> { - self.primary_layer_flow().find(|(node, _id)| node.name == node_name).map(|(node, _id)| &node.inputs) + self.primary_layer_flow() + .skip(1) + .take_while(|(node, _)| !node.is_layer()) + .find(|(node, _)| node.name == node_name) + .map(|(node, _id)| &node.inputs) } /// Find a specific input of a node within the layer's primary flow diff --git a/editor/src/messages/tool/tool_messages/artboard_tool.rs b/editor/src/messages/tool/tool_messages/artboard_tool.rs index 8d440eb5..b5905a23 100644 --- a/editor/src/messages/tool/tool_messages/artboard_tool.rs +++ b/editor/src/messages/tool/tool_messages/artboard_tool.rs @@ -1,6 +1,6 @@ use super::tool_prelude::*; use crate::application::generate_uuid; -use crate::messages::tool::common_functionality::graph_modification_utils::is_artboard; +use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name; use crate::messages::tool::common_functionality::snapping::SnapManager; use crate::messages::tool::common_functionality::transformation_cage::*; @@ -150,7 +150,10 @@ impl ArtboardToolData { fn select_artboard(&mut self, document: &DocumentMessageHandler, render_data: &RenderData, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) -> bool { responses.add(DocumentMessage::StartTransaction); - let mut intersections = document.document_legacy.click_xray(input.mouse.position).filter(|&layer| is_artboard(layer, &document.document_legacy)); + let mut intersections = document + .document_legacy + .click_xray(input.mouse.position) + .filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.document_legacy, "Artboard")); responses.add(BroadcastEvent::DocumentIsDirty); if let Some(intersection) = intersections.next() { diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index f733b47a..58bc264b 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -11,39 +11,8 @@ use graphene_core::uuid::generate_uuid; use graphene_core::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle}; use graphene_core::Color; -const EXPOSED_BLEND_MODES: &[&[BlendMode]] = { - use BlendMode::*; - &[ - // Basic group - &[Normal], - // Darken group - &[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor], - // Lighten group - &[Lighten, Screen, ColorDodge, LinearDodge, LighterColor], - // Contrast group - &[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix], - // Inversion group - &[Difference, Exclusion, Subtract, Divide], - // Component group - &[Hue, Saturation, Color, Luminosity], - ] -}; - const BRUSH_MAX_SIZE: f64 = 5000.; -fn blend_mode_dropdown_idx(target_blend_mode: BlendMode) -> Option { - let mut i = 0; - for group in EXPOSED_BLEND_MODES { - for &blend_mode in group.iter() { - if blend_mode == target_blend_mode { - return Some(i); - } - i += 1; - } - } - None -} - #[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum DrawMode { Draw = 0, @@ -192,7 +161,7 @@ impl LayoutHolder for BrushTool { widgets.push(Separator::new(SeparatorType::Related).widget_holder()); - let blend_mode_entries: Vec> = EXPOSED_BLEND_MODES + let blend_mode_entries: Vec> = BlendMode::list() .iter() .map(|group| { group @@ -207,7 +176,7 @@ impl LayoutHolder for BrushTool { .collect(); widgets.push( DropdownInput::new(blend_mode_entries) - .selected_index(blend_mode_dropdown_idx(self.options.blend_mode)) + .selected_index(self.options.blend_mode.index_in_list().map(|index| index as u32)) .tooltip("The blend mode used with the background when performing a brush stroke. Only used in draw mode.") .disabled(self.options.draw_mode != DrawMode::Draw) .widget_holder(), diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 76c5ca72..2c598785 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -4,8 +4,7 @@ use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE}; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis}; use crate::messages::portfolio::document::utility_types::transformation::Selected; -use crate::messages::tool::common_functionality::graph_modification_utils::is_shape_layer; -use crate::messages::tool::common_functionality::graph_modification_utils::is_text_layer; +use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name; use crate::messages::tool::common_functionality::path_outline::*; use crate::messages::tool::common_functionality::pivot::Pivot; use crate::messages::tool::common_functionality::snapping::{self, SnapManager}; @@ -804,7 +803,7 @@ impl Fsm for SelectToolFsmState { if let Some(layer) = selected_layers.next() { // Check that only one layer is selected - if selected_layers.next().is_none() && is_text_layer(layer, &document.document_legacy) { + if selected_layers.next().is_none() && is_layer_fed_by_node_of_name(layer, &document.document_legacy, "Text") { responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }); responses.add(TextToolMessage::EditSelected); } @@ -952,10 +951,10 @@ fn edit_layer_shallowest_manipulation(document: &DocumentMessageHandler, layer: } fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, document: &Document, responses: &mut VecDeque) { - if is_text_layer(layer, document) { + if is_layer_fed_by_node_of_name(layer, document, "Text") { responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }); responses.add(TextToolMessage::EditSelected); - } else if is_shape_layer(layer, document) { + } else if is_layer_fed_by_node_of_name(layer, document, "Shape") { responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }); } } diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index bb789162..342aad89 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -3,7 +3,7 @@ use super::tool_prelude::*; use crate::application::generate_uuid; use crate::consts::COLOR_ACCENT; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; -use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_text_layer}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, is_layer_fed_by_node_of_name}; use document_legacy::document_metadata::LayerNodeIdentifier; use document_legacy::intersection::Quad; @@ -277,7 +277,7 @@ impl TextToolData { if let Some(clicked_text_layer_path) = document .document_legacy .click(mouse, document.network()) - .filter(|&layer| is_text_layer(layer, &document.document_legacy)) + .filter(|&layer| is_layer_fed_by_node_of_name(layer, &document.document_legacy, "Text")) { self.start_editing_layer(clicked_text_layer_path, state, document, render_data, responses); @@ -417,7 +417,7 @@ fn can_edit_selected(document: &DocumentMessageHandler) -> Option { - editor.subscriptions.subscribeJsMessage(UpdateLayerTreeOptionsLayout, (updateLayerTreeOptionsLayout) => { - patchWidgetLayout(layerTreeOptionsLayout, updateLayerTreeOptionsLayout); - layerTreeOptionsLayout = layerTreeOptionsLayout; + editor.subscriptions.subscribeJsMessage(UpdateLayersPanelOptionsLayout, (updateLayersPanelOptionsLayout) => { + patchWidgetLayout(layersPanelOptionsLayout, updateLayersPanelOptionsLayout); + layersPanelOptionsLayout = layersPanelOptionsLayout; }); editor.subscriptions.subscribeJsMessage(UpdateDocumentLayerTreeStructureJs, (updateDocumentLayerTreeStructure) => { @@ -301,7 +301,7 @@ (dragInPanel = false)}> - + deselectAllLayers()} on:dragover={(e) => draggable && updateInsertLine(e)} on:dragend={() => draggable && drop()}> diff --git a/frontend/src/wasm-communication/messages.ts b/frontend/src/wasm-communication/messages.ts index eb697823..b359f14d 100644 --- a/frontend/src/wasm-communication/messages.ts +++ b/frontend/src/wasm-communication/messages.ts @@ -1345,7 +1345,7 @@ export class UpdateDocumentModeLayout extends WidgetDiffUpdate {} export class UpdateGraphViewOverlayButtonLayout extends WidgetDiffUpdate {} -export class UpdateLayerTreeOptionsLayout extends WidgetDiffUpdate {} +export class UpdateLayersPanelOptionsLayout extends WidgetDiffUpdate {} // Extends JsMessage instead of WidgetDiffUpdate because the menu bar isn't diffed export class UpdateMenuBarLayout extends JsMessage { @@ -1441,7 +1441,7 @@ export const messageMakers: Record = { UpdateGraphViewOverlayButtonLayout, UpdateImageData, UpdateInputHints, - UpdateLayerTreeOptionsLayout, + UpdateLayersPanelOptionsLayout, UpdateMenuBarLayout, UpdateMouseCursor, UpdateNodeGraph, diff --git a/node-graph/gcore/src/raster/adjustments.rs b/node-graph/gcore/src/raster/adjustments.rs index 729a0aae..2e56fa80 100644 --- a/node-graph/gcore/src/raster/adjustments.rs +++ b/node-graph/gcore/src/raster/adjustments.rs @@ -52,42 +52,14 @@ impl core::fmt::Display for LuminanceCalculation { } } -impl BlendMode { - pub fn list() -> [&'static [BlendMode]; 6] { - [ - // Normal group - &[BlendMode::Normal], - // Darken group - &[BlendMode::Darken, BlendMode::Multiply, BlendMode::ColorBurn, BlendMode::LinearBurn, BlendMode::DarkerColor], - // Lighten group - &[BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge, BlendMode::LinearDodge, BlendMode::LighterColor], - // Contrast group - &[ - BlendMode::Overlay, - BlendMode::SoftLight, - BlendMode::HardLight, - BlendMode::VividLight, - BlendMode::LinearLight, - BlendMode::PinLight, - BlendMode::HardMix, - ], - // Inversion group - &[BlendMode::Difference, BlendMode::Exclusion, BlendMode::Subtract, BlendMode::Divide], - // Component group - &[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity], - ] - } -} - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "std", derive(specta::Type))] #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, DynAny, Hash)] #[repr(i32)] // TODO: Enable Int8 capability for SPIR-V so that we don't need this? pub enum BlendMode { - #[default] // Basic group + #[default] Normal, - // Not supported by SVG, but we should someday support: Dissolve // Darken group Darken, @@ -130,6 +102,95 @@ pub enum BlendMode { MultiplyAlpha, } +impl BlendMode { + /// All standard blend modes ordered by group. + pub fn list() -> [&'static [BlendMode]; 6] { + use BlendMode::*; + [ + // Normal group + &[Normal], + // Darken group + &[Darken, Multiply, ColorBurn, LinearBurn, DarkerColor], + // Lighten group + &[Lighten, Screen, ColorDodge, LinearDodge, LighterColor], + // Contrast group + &[Overlay, SoftLight, HardLight, VividLight, LinearLight, PinLight, HardMix], + // Inversion group + &[Difference, Exclusion, Subtract, Divide], + // Component group + &[Hue, Saturation, Color, Luminosity], + ] + } + + /// The subset of [`BlendMode::list()`] that is supported by SVG. + pub fn list_svg_subset() -> [&'static [BlendMode]; 6] { + use BlendMode::*; + [ + // Normal group + &[Normal], + // Darken group + &[Darken, Multiply, ColorBurn], + // Lighten group + &[Lighten, Screen, ColorDodge], + // Contrast group + &[Overlay, SoftLight, HardLight], + // Inversion group + &[Difference, Exclusion], + // Component group + &[Hue, Saturation, Color, Luminosity], + ] + } + + pub fn index_in_list(&self) -> Option { + Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self) + } + + pub fn index_in_list_svg_subset(&self) -> Option { + Self::list_svg_subset().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self) + } + + /// Convert the enum to the CSS string for the blend mode. + /// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values) + pub fn to_svg_style_name(&self) -> Option<&'static str> { + match self { + // Normal group + BlendMode::Normal => Some("normal"), + // Darken group + BlendMode::Darken => Some("darken"), + BlendMode::Multiply => Some("multiply"), + BlendMode::ColorBurn => Some("color-burn"), + // Lighten group + BlendMode::Lighten => Some("lighten"), + BlendMode::Screen => Some("screen"), + BlendMode::ColorDodge => Some("color-dodge"), + // Contrast group + BlendMode::Overlay => Some("overlay"), + BlendMode::SoftLight => Some("soft-light"), + BlendMode::HardLight => Some("hard-light"), + // Inversion group + BlendMode::Difference => Some("difference"), + BlendMode::Exclusion => Some("exclusion"), + // Component group + BlendMode::Hue => Some("hue"), + BlendMode::Saturation => Some("saturation"), + BlendMode::Color => Some("color"), + BlendMode::Luminosity => Some("luminosity"), + _ => None, + } + } + + /// Renders the blend mode CSS style declaration. + pub fn render(&self) -> String { + format!( + r#" mix-blend-mode: {};"#, + self.to_svg_style_name().unwrap_or_else(|| { + warn!("Unsupported blend mode {self:?}"); + "normal" + }) + ) + } +} + impl core::fmt::Display for BlendMode { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { @@ -173,64 +234,6 @@ impl core::fmt::Display for BlendMode { } } -impl BlendMode { - /// Convert the enum to the CSS string for the blend mode. - /// [Read more](https://developer.mozilla.org/en-US/docs/Web/CSS/blend-mode#values) - pub fn to_svg_style_name(&self) -> &'static str { - match self { - // Normal group - BlendMode::Normal => "normal", - // Darken group - BlendMode::Darken => "darken", - BlendMode::Multiply => "multiply", - BlendMode::ColorBurn => "color-burn", - // Lighten group - BlendMode::Lighten => "lighten", - BlendMode::Screen => "screen", - BlendMode::ColorDodge => "color-dodge", - // Contrast group - BlendMode::Overlay => "overlay", - BlendMode::SoftLight => "soft-light", - BlendMode::HardLight => "hard-light", - // Inversion group - BlendMode::Difference => "difference", - BlendMode::Exclusion => "exclusion", - // Component group - BlendMode::Hue => "hue", - BlendMode::Saturation => "saturation", - BlendMode::Color => "color", - BlendMode::Luminosity => "luminosity", - _ => { - warn!("Unsupported blend mode {self:?}"); - "normal" - } - } - } - - /// Renders the blend mode CSS style declaration. - pub fn render(&self) -> String { - format!(r#" mix-blend-mode: {};"#, self.to_svg_style_name()) - } - - /// List of all the blend modes in their conventional ordering and grouping. - pub fn list_modes_in_groups() -> [&'static [BlendMode]; 6] { - [ - // Normal group - &[BlendMode::Normal], - // Darken group - &[BlendMode::Darken, BlendMode::Multiply, BlendMode::ColorBurn], - // Lighten group - &[BlendMode::Lighten, BlendMode::Screen, BlendMode::ColorDodge], - // Contrast group - &[BlendMode::Overlay, BlendMode::SoftLight, BlendMode::HardLight], - // Inversion group - &[BlendMode::Difference, BlendMode::Exclusion], - // Component group - &[BlendMode::Hue, BlendMode::Saturation, BlendMode::Color, BlendMode::Luminosity], - ] - } -} - #[derive(Debug, Clone, Copy, Default)] pub struct LuminanceNode { luminance_calc: LuminanceCalculation, diff --git a/node-graph/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 2f10d5ad..1e6a7fee 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -276,6 +276,12 @@ impl DocumentNode { // TODO: Or, more fundamentally separate the concept of a layer from a node. self.name == "Layer" } + + pub fn is_artboard(&self) -> bool { + // TODO: Use something more robust than checking against a string. + // TODO: Or, more fundamentally separate the concept of a layer from a node. + self.name == "Artboard" + } } /// Represents the possible inputs to a node.