458 lines
15 KiB
Rust
458 lines
15 KiB
Rust
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<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,
|
|
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<Color>),
|
|
ColorType(ToolColorType),
|
|
Diameter(f64),
|
|
DrawMode(DrawMode),
|
|
Flow(f64),
|
|
Hardness(f64),
|
|
Spacing(f64),
|
|
WorkingColors(Option<Color>, Option<Color>),
|
|
}
|
|
|
|
#[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<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<ToolMessage, &mut ToolActionHandlerData<'a>> for BrushTool {
|
|
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, 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<BrushStroke>,
|
|
layer_path: Vec<LayerId>,
|
|
transform: DAffine2,
|
|
}
|
|
|
|
impl BrushToolData {
|
|
fn load_existing_strokes(&mut self, document: &DocumentMessageHandler) -> Option<&Vec<LayerId>> {
|
|
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<Message>) {
|
|
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<Message>,
|
|
) -> 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<Message>) {
|
|
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<Message>) {
|
|
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
|
}
|
|
}
|
|
|
|
fn add_brush_render(_tool_options: &BrushOptions, data: &BrushToolData, responses: &mut VecDeque<Message>) {
|
|
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);
|
|
}
|