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 <keavon@keavon.com>
This commit is contained in:
0HyperCube 2023-12-12 09:27:23 +00:00 committed by GitHub
parent 6bce72dccd
commit 29222700f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 271 additions and 241 deletions

View File

@ -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<Item = LayerNodeIdentifier> + '_ {
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

View File

@ -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: {}">{}</g>"#,
self.blend_mode.to_svg_style_name(),
self.opacity,
self.thumbnail_cache.as_str()
);
let _ = write!(self.cache, r#")" style="opacity: {};{}">{}</g>"#, self.opacity, self.blend_mode.render(), self.thumbnail_cache.as_str());
self.cache_dirty = false;
}

View File

@ -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<DialogMessage, DialogData<'_>> 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,

View File

@ -206,7 +206,7 @@ pub enum FrontendMessage {
#[serde(rename = "hintData")]
hint_data: HintData,
},
UpdateLayerTreeOptionsLayout {
UpdateLayersPanelOptionsLayout {
#[serde(rename = "layoutTarget")]
layout_target: LayoutTarget,
diff: Vec<WidgetDiff>,

View File

@ -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 },

View File

@ -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.

View File

@ -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<DocumentMessage, DocumentInputs<'_>> 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<DocumentMessage, DocumentInputs<'_>> 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<DocumentMessage, DocumentInputs<'_>> 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::<Vec<_>>();
@ -697,8 +698,8 @@ impl MessageHandler<DocumentMessage, DocumentInputs<'_>> 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<DocumentMessage, DocumentInputs<'_>> 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<Message>, 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<Message>) {
// 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;
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;
}
}
}
match blend_mode {
None => blend_mode = Some(layer.blend_mode),
Some(blend_mode) => {
if blend_mode != layer.blend_mode {
blend_mode_is_mixed = true;
}
}
}
// 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;
if opacity_is_mixed {
opacity = None;
// 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;
}
if blend_mode_is_mixed {
blend_mode = None;
}
let blend_mode_menu_entries = BlendMode::list_modes_in_groups()
(opacity_identical.then(|| first_opacity), blend_mode_identical.then(|| first_blend_mode))
})
.unwrap_or((None, None));
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,
});
}

View File

@ -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],

View File

@ -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::<Vec<_>>();
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<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
responses.add(Operation::SetLayerFill { path: layer, fill });
}
}
GraphOperationMessage::OpacitySet { layer, opacity } => {
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<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
let mut modify_inputs = ModifyInputsContext::new(document, node_graph, responses);
let layer_nodes = modify_inputs.network.nodes.iter().filter(|(_, node)| node.is_layer()).map(|(id, _)| *id).collect::<Vec<_>>();
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);
}
}

View File

@ -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 {

View File

@ -680,7 +680,7 @@ impl PortfolioMessageHandler {
.map(|entry| FrontendMessage::UpdateDocumentLayerDetails { data: entry }.into())
.collect::<Vec<_>>(),
);
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);

View File

@ -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<Gradient> {
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<G
})
}
/// Get the current fill of a layer from the closest fill node
/// Get the current fill of a layer from the closest Fill node
pub fn get_fill_color(layer: LayerNodeIdentifier, document: &Document) -> Option<Color> {
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<NodeId> {
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<BlendMode> {
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<f32> {
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<NodeId> {
NodeGraphLayer::new(layer, document)?.node_id("Fill")
}
/// Gets properties from the text node
pub fn get_text_id(layer: LayerNodeIdentifier, document: &Document) -> Option<NodeId> {
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<NodeId> {
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<NodeInput>> {
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

View File

@ -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<Message>) -> 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() {

View File

@ -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<u32> {
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<Vec<_>> = EXPOSED_BLEND_MODES
let blend_mode_entries: Vec<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(),

View File

@ -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<Message>) {
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 });
}
}

View File

@ -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<LayerNodeIdent
return None;
}
if !is_text_layer(layer, &document.document_legacy) {
if !is_layer_fed_by_node_of_name(layer, &document.document_legacy, "Text") {
return None;
}

View File

@ -4,7 +4,7 @@
import { beginDraggingElement } from "@graphite/io-managers/drag";
import { platformIsMac } from "@graphite/utility-functions/platform";
import type { Editor } from "@graphite/wasm-communication/editor";
import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerTreeStructureJs, UpdateLayerTreeOptionsLayout } from "@graphite/wasm-communication/messages";
import { defaultWidgetLayout, patchWidgetLayout, UpdateDocumentLayerDetails, UpdateDocumentLayerTreeStructureJs, UpdateLayersPanelOptionsLayout } from "@graphite/wasm-communication/messages";
import type { LayerType, LayerPanelEntry } from "@graphite/wasm-communication/messages";
import LayoutCol from "@graphite/components/layout/LayoutCol.svelte";
@ -46,12 +46,12 @@
let dragInPanel = false;
// Layouts
let layerTreeOptionsLayout = defaultWidgetLayout();
let layersPanelOptionsLayout = defaultWidgetLayout();
onMount(() => {
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 @@
<LayoutCol class="layers" on:dragleave={() => (dragInPanel = false)}>
<LayoutRow class="options-bar" scrollableX={true}>
<WidgetLayout layout={layerTreeOptionsLayout} />
<WidgetLayout layout={layersPanelOptionsLayout} />
</LayoutRow>
<LayoutRow class="list-area" scrollableY={true}>
<LayoutCol class="list" bind:this={list} on:click={() => deselectAllLayers()} on:dragover={(e) => draggable && updateInsertLine(e)} on:dragend={() => draggable && drop()}>

View File

@ -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<string, MessageMaker> = {
UpdateGraphViewOverlayButtonLayout,
UpdateImageData,
UpdateInputHints,
UpdateLayerTreeOptionsLayout,
UpdateLayersPanelOptionsLayout,
UpdateMenuBarLayout,
UpdateMouseCursor,
UpdateNodeGraph,

View File

@ -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<usize> {
Self::list().iter().flat_map(|x| x.iter()).position(|&blend_mode| blend_mode == *self)
}
pub fn index_in_list_svg_subset(&self) -> Option<usize> {
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<LuminanceCalculation> {
luminance_calc: LuminanceCalculation,

View File

@ -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.