Implement the Brush tool (#1099)

* Implement Brush Node

* Add color Input

* Add VectorPointsNode

* Add Erase Node

* Adapt compilation infrastructure to allow non Image Frame inputs

* Remove debug output from TransformNode

* Fix transform calculation

* Fix Blending by making the brush texture use associated alpha

* Code improvements and UX polish

* Rename Opacity to Flow

* Add erase option to brush node + fix freehand tool

* Fix crash

* Revert erase implementation

* Fix flattening id calculation

* Fix some transformation issues

* Fix changing the pivot location

* Fix vector data modify bounds

* Minor fn name cleanup

* Fix some tests

* Fix tests

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: hypercube <0hypercube@gmail.com>
This commit is contained in:
Dennis Kobert
2023-04-11 10:35:21 +02:00
committed by Keavon Chambers
co-authored by Keavon Chambers hypercube
parent 758f757775
commit 589ff9a2d3
36 changed files with 1527 additions and 406 deletions
@@ -3,6 +3,7 @@ use crate::messages::prelude::*;
use bezier_rs::Subpath;
use document_legacy::{LayerId, Operation};
use graph_craft::document::NodeNetwork;
use graphene_core::uuid::ManipulatorGroupId;
use glam::DAffine2;
@@ -10,9 +11,12 @@ use std::collections::VecDeque;
/// Create a new vector layer from a vector of [`bezier_rs::Subpath`].
pub fn new_vector_layer(subpaths: Vec<Subpath<ManipulatorGroupId>>, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
let network = node_graph::new_vector_network(subpaths);
new_custom_layer(network, layer_path, responses);
}
pub fn new_custom_layer(network: NodeNetwork, layer_path: Vec<LayerId>, responses: &mut VecDeque<Message>) {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(
Operation::AddNodeGraphFrame {
path: layer_path.clone(),
+5 -3
View File
@@ -61,9 +61,9 @@ pub enum ToolMessage {
#[child]
Text(TextToolMessage),
// #[remain::unsorted]
// #[child]
// Brush(BrushToolMessage),
#[remain::unsorted]
#[child]
Brush(BrushToolMessage),
// #[remain::unsorted]
// #[child]
// Heal(HealToolMessage),
@@ -119,6 +119,8 @@ pub enum ToolMessage {
#[remain::unsorted]
ActivateToolShape,
#[remain::unsorted]
ActivateToolBrush,
#[remain::unsorted]
ActivateToolImaginate,
#[remain::unsorted]
@@ -69,6 +69,8 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
#[remain::unsorted]
ToolMessage::ActivateToolShape => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Shape }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolBrush => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Brush }.into()),
#[remain::unsorted]
ToolMessage::ActivateToolImaginate => responses.push_front(ToolMessage::ActivateTool { tool_type: ToolType::Imaginate }.into()),
@@ -276,6 +278,7 @@ impl MessageHandler<ToolMessage, (&DocumentMessageHandler, u64, &InputPreprocess
ActivateToolEllipse,
ActivateToolShape,
ActivateToolBrush,
ActivateToolImaginate,
SelectRandomPrimaryColor,
@@ -0,0 +1,290 @@
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, WidgetLayout};
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
use document_legacy::LayerId;
use document_legacy::Operation;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
use graph_craft::{concrete, Type, TypeDescriptor};
use graphene_core::vector::style::Stroke;
use graphene_core::Cow;
use glam::DVec2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct BrushTool {
fsm_state: BrushToolFsmState,
data: BrushToolData,
options: BrushOptions,
}
pub struct BrushOptions {
diameter: f64,
hardness: f64,
flow: f64,
}
impl Default for BrushOptions {
fn default() -> Self {
Self {
diameter: 40.,
hardness: 50.,
flow: 100.,
}
}
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Brush)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
pub enum BrushToolMessage {
// Standard messages
#[remain::unsorted]
Abort,
// Tool-specific messages
DragStart,
DragStop,
PointerMove,
UpdateOptions(BrushToolMessageOptionsUpdate),
}
#[remain::sorted]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
pub enum BrushToolMessageOptionsUpdate {
Diameter(f64),
Flow(f64),
Hardness(f64),
}
#[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 diameter = 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();
let hardness = 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();
let flow = 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();
let separator = Separator::new(SeparatorDirection::Horizontal, SeparatorType::Related).widget_holder();
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row {
widgets: vec![diameter, separator.clone(), hardness, separator, flow],
}]))
}
}
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::Diameter(diameter) => self.options.diameter = diameter,
BrushToolMessageOptionsUpdate::Hardness(hardness) => self.options.hardness = hardness,
BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow,
}
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,
),
Drawing => actions!(BrushToolMessageDiscriminant;
DragStop,
PointerMove,
Abort,
),
}
}
}
impl ToolTransition for BrushTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
document_dirty: None,
tool_abort: Some(BrushToolMessage::Abort.into()),
selection_changed: None,
}
}
}
#[derive(Clone, Debug, Default)]
struct BrushToolData {
points: Vec<DVec2>,
diameter: f64,
hardness: f64,
flow: f64,
path: Option<Vec<LayerId>>,
}
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 {
use BrushToolFsmState::*;
use BrushToolMessage::*;
let transform = document.document_legacy.root.transform;
if let ToolMessage::Brush(event) = event {
match (self, event) {
(Ready, DragStart) => {
responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
tool_data.path = Some(document.get_path_for_new_layer());
let pos = transform.inverse().transform_point2(input.mouse.position);
tool_data.points.push(pos);
tool_data.diameter = tool_options.diameter;
tool_data.hardness = tool_options.hardness;
tool_data.flow = tool_options.flow;
add_polyline(tool_data, global_tool_data, responses);
Drawing
}
(Drawing, PointerMove) => {
let pos = transform.inverse().transform_point2(input.mouse.position);
if tool_data.points.last() != Some(&pos) {
tool_data.points.push(pos);
}
add_polyline(tool_data, global_tool_data, responses);
Drawing
}
(Drawing, DragStop) | (Drawing, Abort) => {
if !tool_data.points.is_empty() {
responses.push_back(remove_preview(tool_data));
add_brush_render(tool_data, global_tool_data, responses);
responses.push_back(DocumentMessage::CommitTransaction.into());
} else {
responses.push_back(DocumentMessage::AbortTransaction.into());
}
tool_data.path = None;
tool_data.points.clear();
Ready
}
_ => 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 Polyline")])]),
BrushToolFsmState::Drawing => HintData(vec![]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}
fn remove_preview(data: &BrushToolData) -> Message {
Operation::DeleteLayer { path: data.path.clone().unwrap() }.into()
}
fn add_polyline(data: &BrushToolData, tool_data: &DocumentToolData, responses: &mut VecDeque<Message>) {
let layer_path = data.path.clone().unwrap();
let subpath = bezier_rs::Subpath::from_anchors(data.points.iter().copied(), false);
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
responses.add(GraphOperationMessage::StrokeSet {
layer: layer_path,
stroke: Stroke::new(tool_data.primary_color.apply_opacity(data.flow as f32 / 100.), data.diameter),
});
}
fn add_brush_render(data: &BrushToolData, tool_data: &DocumentToolData, responses: &mut VecDeque<Message>) {
let layer_path = data.path.clone().unwrap();
let brush_node = DocumentNode {
name: "Brush".to_string(),
inputs: vec![
NodeInput::ShortCircut(concrete!(())),
NodeInput::value(TaggedValue::VecDVec2(data.points.clone()), false),
// Diameter
NodeInput::value(TaggedValue::F64(data.diameter), false),
// Hardness
NodeInput::value(TaggedValue::F64(data.hardness), false),
// Flow
NodeInput::value(TaggedValue::F64(data.flow), false),
// Color
NodeInput::value(TaggedValue::Color(tool_data.primary_color), false),
],
implementation: DocumentNodeImplementation::Unresolved("graphene_std::brush::BrushNode".into()),
metadata: graph_craft::document::DocumentNodeMetadata { position: (8, 4).into() },
};
let mut network = NodeNetwork::value_network(brush_node);
network.push_output_node();
graph_modification_utils::new_custom_layer(network, layer_path.clone(), responses);
}
@@ -226,10 +226,4 @@ fn add_polyline(data: &FreehandToolData, tool_data: &DocumentToolData, responses
layer: layer_path.clone(),
stroke: Stroke::new(tool_data.primary_color, data.weight),
});
responses.add(GraphOperationMessage::TransformSet {
layer: layer_path,
transform: DAffine2::from_translation(position),
transform_in: TransformIn::Local,
skip_rerender: false,
});
}
@@ -1,4 +1,5 @@
pub mod artboard_tool;
pub mod brush_tool;
pub mod ellipse_tool;
pub mod eyedropper_tool;
pub mod fill_tool;
@@ -270,10 +270,4 @@ fn add_spline(tool_data: &SplineToolData, global_tool_data: &DocumentToolData, s
layer: layer_path.clone(),
stroke: Stroke::new(global_tool_data.primary_color, tool_data.weight),
});
responses.add(GraphOperationMessage::TransformSet {
layer: layer_path,
transform: glam::DAffine2::from_translation(position),
transform_in: TransformIn::Local,
skip_rerender: false,
})
}
+3 -8
View File
@@ -410,12 +410,7 @@ fn list_tools_in_groups() -> Vec<Vec<ToolAvailability>> {
// Raster tool group
ToolAvailability::Available(Box::<frame_tool::NodeGraphFrameTool>::default()),
ToolAvailability::Available(Box::<imaginate_tool::ImaginateTool>::default()),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Brush,
icon_name: "RasterBrushTool".into(),
tooltip: "Coming Soon: Brush Tool (B)".into(),
tooltip_shortcut: None,
}),
ToolAvailability::Available(Box::<brush_tool::BrushTool>::default()),
ToolAvailability::ComingSoon(ToolEntry {
tool_type: ToolType::Heal,
icon_name: "RasterHealTool".into(),
@@ -472,7 +467,7 @@ pub fn tool_message_to_tool_type(tool_message: &ToolMessage) -> ToolType {
ToolMessage::Text(_) => ToolType::Text,
// Raster tool group
// ToolMessage::Brush(_) => ToolType::Brush,
ToolMessage::Brush(_) => ToolType::Brush,
// ToolMessage::Heal(_) => ToolType::Heal,
// ToolMessage::Clone(_) => ToolType::Clone,
// ToolMessage::Patch(_) => ToolType::Patch,
@@ -509,7 +504,7 @@ pub fn tool_type_to_activate_tool_message(tool_type: ToolType) -> ToolMessageDis
ToolType::Text => ToolMessageDiscriminant::ActivateToolText,
// Raster tool group
// ToolType::Brush => ToolMessageDiscriminant::ActivateToolBrush,
ToolType::Brush => ToolMessageDiscriminant::ActivateToolBrush,
// ToolType::Heal => ToolMessageDiscriminant::ActivateToolHeal,
// ToolType::Clone => ToolMessageDiscriminant::ActivateToolClone,
// ToolType::Patch => ToolMessageDiscriminant::ActivateToolPatch,