use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::input_mapper::utility_types::input_keyboard::MouseMotion; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, WidgetCallback, WidgetLayout}; use crate::messages::layout::utility_types::misc::LayoutTarget; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::node_graph::transform_utils::get_current_transform; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; use document_legacy::layers::layer_layer::CachedOutputData; use document_legacy::LayerId; use graph_craft::document::value::TaggedValue; use graph_craft::document::{NodeInput, NodeNetwork}; use graphene_core::raster::{BlendMode, ImageFrame}; use graphene_core::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle}; use graphene_core::Color; use glam::DAffine2; use serde::{Deserialize, Serialize}; 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], ] }; 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, Erase, Restore, } #[derive(Default)] pub struct BrushTool { fsm_state: BrushToolFsmState, data: BrushToolData, options: BrushOptions, } pub struct BrushOptions { diameter: f64, hardness: f64, flow: f64, spacing: f64, color: ToolColorOptions, blend_mode: BlendMode, draw_mode: DrawMode, } impl Default for BrushOptions { fn default() -> Self { Self { diameter: 40., hardness: 0., flow: 100., spacing: 20., color: ToolColorOptions::default(), blend_mode: BlendMode::Normal, draw_mode: DrawMode::Draw, } } } #[remain::sorted] #[impl_message(Message, ToolMessage, Brush)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum BrushToolMessage { // Standard messages #[remain::unsorted] Abort, #[remain::unsorted] WorkingColorChanged, // Tool-specific messages DragStart, DragStop, PointerMove, UpdateOptions(BrushToolMessageOptionsUpdate), } #[remain::sorted] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum BrushToolMessageOptionsUpdate { BlendMode(BlendMode), ChangeDiameter(f64), Color(Option), ColorType(ToolColorType), Diameter(f64), DrawMode(DrawMode), Flow(f64), Hardness(f64), Spacing(f64), WorkingColors(Option, Option), } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum BrushToolFsmState { #[default] Ready, Drawing, } impl ToolMetadata for BrushTool { fn icon_name(&self) -> String { "RasterBrushTool".into() } fn tooltip(&self) -> String { "Brush Tool".into() } fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { ToolType::Brush } } impl PropertyHolder for BrushTool { fn properties(&self) -> Layout { let mut widgets = vec![ NumberInput::new(Some(self.options.diameter)) .label("Diameter") .min(1.) .unit(" px") .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Diameter(number_input.value.unwrap())).into()) .widget_holder(), WidgetHolder::related_separator(), NumberInput::new(Some(self.options.hardness)) .label("Hardness") .min(0.) .max(100.) .unit("%") .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Hardness(number_input.value.unwrap())).into()) .widget_holder(), WidgetHolder::related_separator(), NumberInput::new(Some(self.options.flow)) .label("Flow") .min(1.) .max(100.) .unit("%") .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Flow(number_input.value.unwrap())).into()) .widget_holder(), WidgetHolder::related_separator(), NumberInput::new(Some(self.options.spacing)) .label("Spacing") .min(1.) .max(100.) .unit("%") .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Spacing(number_input.value.unwrap())).into()) .widget_holder(), ]; widgets.push(WidgetHolder::section_separator()); let draw_mode_entries: Vec<_> = [DrawMode::Draw, DrawMode::Erase, DrawMode::Restore] .into_iter() .map(|draw_mode| RadioEntryData::new(format!("{draw_mode:?}")).on_update(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::DrawMode(draw_mode)).into())) .collect(); widgets.push(RadioInput::new(draw_mode_entries).selected_index(self.options.draw_mode as u32).widget_holder()); widgets.push(WidgetHolder::section_separator()); widgets.append(&mut self.options.color.create_widgets( "Color", false, |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ColorType(color_type.clone())).into()), |color: &ColorInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Color(color.value)).into(), )); widgets.push(WidgetHolder::related_separator()); let blend_mode_entries: Vec> = EXPOSED_BLEND_MODES .iter() .map(|group| { group .iter() .map(|blend_mode| { DropdownEntryData::new(format!("{blend_mode}")) .value(format!("{blend_mode:?}")) .on_update(|_| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::BlendMode(*blend_mode)).into()) }) .collect() }) .collect(); widgets.push( DropdownInput::new(blend_mode_entries) .selected_index(blend_mode_dropdown_idx(self.options.blend_mode)) .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(), ); Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } impl<'a> MessageHandler> for BrushTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { if let ToolMessage::Brush(BrushToolMessage::UpdateOptions(action)) = message { match action { BrushToolMessageOptionsUpdate::BlendMode(blend_mode) => self.options.blend_mode = blend_mode, BrushToolMessageOptionsUpdate::ChangeDiameter(change) => { let needs_rounding = ((self.options.diameter + change.abs() / 2.) % change.abs() - change.abs() / 2.).abs() > 0.5; if needs_rounding && change > 0. { self.options.diameter = (self.options.diameter / change.abs()).ceil() * change.abs(); } else if needs_rounding && change < 0. { self.options.diameter = (self.options.diameter / change.abs()).floor() * change.abs(); } else { self.options.diameter = (self.options.diameter / change.abs()).round() * change.abs() + change; } self.options.diameter = self.options.diameter.max(1.); self.register_properties(responses, LayoutTarget::ToolOptions); } BrushToolMessageOptionsUpdate::Diameter(diameter) => self.options.diameter = diameter, BrushToolMessageOptionsUpdate::DrawMode(draw_mode) => self.options.draw_mode = draw_mode, BrushToolMessageOptionsUpdate::Hardness(hardness) => self.options.hardness = hardness, BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow, BrushToolMessageOptionsUpdate::Spacing(spacing) => self.options.spacing = spacing, BrushToolMessageOptionsUpdate::Color(color) => { self.options.color.custom_color = color; self.options.color.color_type = ToolColorType::Custom; } BrushToolMessageOptionsUpdate::ColorType(color_type) => self.options.color.color_type = color_type, BrushToolMessageOptionsUpdate::WorkingColors(primary, secondary) => { self.options.color.primary_working_color = primary; self.options.color.secondary_working_color = secondary; } } self.register_properties(responses, LayoutTarget::ToolOptions); return; } self.fsm_state.process_event(message, &mut self.data, tool_data, &self.options, responses, true); } fn actions(&self) -> ActionList { use BrushToolFsmState::*; match self.fsm_state { Ready => actions!(BrushToolMessageDiscriminant; DragStart, DragStop, Abort, UpdateOptions, ), Drawing => actions!(BrushToolMessageDiscriminant; DragStop, PointerMove, Abort, UpdateOptions, ), } } } impl ToolTransition for BrushTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(BrushToolMessage::Abort.into()), working_color_changed: Some(BrushToolMessage::WorkingColorChanged.into()), ..Default::default() } } } #[derive(Clone, Debug, Default)] struct BrushToolData { strokes: Vec, layer_path: Vec, transform: DAffine2, } impl BrushToolData { fn load_existing_strokes(&mut self, document: &DocumentMessageHandler) -> Option<&Vec> { self.transform = DAffine2::IDENTITY; if document.selected_layers().count() != 1 { return None; } self.layer_path = document.selected_layers().next()?.to_vec(); let layer = document.document_legacy.layer(&self.layer_path).ok().and_then(|layer| layer.as_layer().ok())?; let network = &layer.network; for (node, _node_id) in network.primary_flow() { if node.name == "Brush" { let points_input = node.inputs.get(2)?; let NodeInput::Value { tagged_value: TaggedValue::BrushStrokes(strokes), .. } = points_input else { continue; }; self.strokes = strokes.clone(); return Some(&self.layer_path); } else if node.name == "Transform" { self.transform = get_current_transform(&node.inputs) * self.transform; } } self.transform = DAffine2::IDENTITY; matches!(layer.cached_output_data, CachedOutputData::BlobURL(_) | CachedOutputData::SurfaceId(_)).then_some(&self.layer_path) } fn update_strokes(&self, responses: &mut VecDeque) { let layer = self.layer_path.clone(); let strokes = self.strokes.clone(); responses.add(GraphOperationMessage::Brush { layer, strokes }); } } impl Fsm for BrushToolFsmState { type ToolData = BrushToolData; type ToolOptions = BrushOptions; fn transition( self, event: ToolMessage, tool_data: &mut Self::ToolData, ToolActionHandlerData { document, global_tool_data, input, .. }: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { let document_position = (document.document_legacy.root.transform).inverse().transform_point2(input.mouse.position); let layer_position = tool_data.transform.inverse().transform_point2(document_position); if let ToolMessage::Brush(event) = event { match (self, event) { (BrushToolFsmState::Ready, BrushToolMessage::DragStart) => { responses.add(DocumentMessage::StartTransaction); let layer_path = tool_data.load_existing_strokes(document); let new_layer = layer_path.is_none(); if new_layer { responses.add(DocumentMessage::DeselectAllLayers); tool_data.layer_path = document.get_path_for_new_layer(); } let layer_position = tool_data.transform.inverse().transform_point2(document_position); // TODO: Also scale it based on the input image ('Background' parameter). // TODO: Resizing the input image results in a different brush size from the chosen diameter. let layer_scale = 0.0001_f64 // Safety against division by zero .max((tool_data.transform.matrix2 * glam::DVec2::X).length()) .max((tool_data.transform.matrix2 * glam::DVec2::Y).length()); // Start a new stroke with a single sample let blend_mode = match tool_options.draw_mode { DrawMode::Draw => tool_options.blend_mode, DrawMode::Erase => BlendMode::Erase, DrawMode::Restore => BlendMode::Restore, }; tool_data.strokes.push(BrushStroke { trace: vec![BrushInputSample { position: layer_position }], style: BrushStyle { color: tool_options.color.active_color().unwrap_or_default(), diameter: tool_options.diameter / layer_scale, hardness: tool_options.hardness, flow: tool_options.flow, spacing: tool_options.spacing, blend_mode, }, }); if new_layer { add_brush_render(tool_options, tool_data, responses); } tool_data.update_strokes(responses); BrushToolFsmState::Drawing } (BrushToolFsmState::Drawing, BrushToolMessage::PointerMove) => { if let Some(stroke) = tool_data.strokes.last_mut() { stroke.trace.push(BrushInputSample { position: layer_position }) } tool_data.update_strokes(responses); BrushToolFsmState::Drawing } (BrushToolFsmState::Drawing, BrushToolMessage::DragStop) | (BrushToolFsmState::Drawing, BrushToolMessage::Abort) => { if !tool_data.strokes.is_empty() { responses.add(DocumentMessage::CommitTransaction); } else { responses.add(DocumentMessage::AbortTransaction); } tool_data.strokes.clear(); BrushToolFsmState::Ready } (_, BrushToolMessage::WorkingColorChanged) => { responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::WorkingColors( Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color), ))); self } _ => self, } } else { self } } fn update_hints(&self, responses: &mut VecDeque) { let hint_data = match self { BrushToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Stroke")])]), BrushToolFsmState::Drawing => HintData(vec![]), }; responses.add(FrontendMessage::UpdateInputHints { hint_data }); } fn update_cursor(&self, responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } fn add_brush_render(_tool_options: &BrushOptions, data: &BrushToolData, responses: &mut VecDeque) { let mut network = NodeNetwork::default(); let output_node = network.push_output_node(); if let Some(node) = network.nodes.get_mut(&output_node) { node.inputs.push(NodeInput::value(TaggedValue::ImageFrame(ImageFrame::empty()), true)) } graph_modification_utils::new_custom_layer(network, data.layer_path.clone(), responses); }