mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-22 20:28:13 +08:00
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:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
hypercube
parent
758f757775
commit
589ff9a2d3
@@ -205,6 +205,11 @@ pub fn default_mapping() -> Mapping {
|
||||
entry!(KeyDown(Lmb); action_dispatch=FillToolMessage::LeftPointerDown),
|
||||
entry!(KeyDown(Rmb); action_dispatch=FillToolMessage::RightPointerDown),
|
||||
//
|
||||
// BrushToolMessage
|
||||
entry!(PointerMove; action_dispatch=BrushToolMessage::PointerMove),
|
||||
entry!(KeyDown(Lmb); action_dispatch=BrushToolMessage::DragStart),
|
||||
entry!(KeyUp(Lmb); action_dispatch=BrushToolMessage::DragStop),
|
||||
//
|
||||
// ToolMessage
|
||||
entry!(KeyDown(KeyV); action_dispatch=ToolMessage::ActivateToolSelect),
|
||||
entry!(KeyDown(KeyZ); action_dispatch=ToolMessage::ActivateToolNavigate),
|
||||
@@ -219,6 +224,7 @@ pub fn default_mapping() -> Mapping {
|
||||
entry!(KeyDown(KeyM); action_dispatch=ToolMessage::ActivateToolRectangle),
|
||||
entry!(KeyDown(KeyE); action_dispatch=ToolMessage::ActivateToolEllipse),
|
||||
entry!(KeyDown(KeyY); action_dispatch=ToolMessage::ActivateToolShape),
|
||||
entry!(KeyDown(KeyB); action_dispatch=ToolMessage::ActivateToolBrush),
|
||||
entry!(KeyDown(KeyX); modifiers=[Shift, Accel], action_dispatch=ToolMessage::ResetColors),
|
||||
entry!(KeyDown(KeyX); modifiers=[Shift], action_dispatch=ToolMessage::SwapColors),
|
||||
entry!(KeyDown(KeyC); modifiers=[Alt], action_dispatch=ToolMessage::SelectRandomPrimaryColor),
|
||||
|
||||
@@ -32,7 +32,9 @@ use document_legacy::layers::style::{Fill, RenderData, ViewMode};
|
||||
use document_legacy::layers::text_layer::Font;
|
||||
use document_legacy::{DocumentError, DocumentResponse, LayerId, Operation as DocumentOperation};
|
||||
use graph_craft::document::NodeId;
|
||||
use graph_craft::{concrete, Type, TypeDescriptor};
|
||||
use graphene_core::raster::{Color, ImageFrame};
|
||||
use graphene_core::Cow;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -1050,39 +1052,39 @@ impl DocumentMessageHandler {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Skip processing under node graph frame input if not connected
|
||||
if !node_network.connected_to_output(node_network.inputs[0], false) {
|
||||
return Some(
|
||||
PortfolioMessage::ProcessNodeGraphFrame {
|
||||
// Find the primary input type of the node graph
|
||||
let primary_input_type = node_network.input_types().next().clone();
|
||||
let response = match primary_input_type {
|
||||
// Only calclate the frame if the primary input is an image
|
||||
Some(ty) if ty == concrete!(ImageFrame) => {
|
||||
// Calculate the size of the region to be exported
|
||||
let old_transforms = self.remove_document_transform();
|
||||
let transform = self.document_legacy.multiply_transforms(&layer_path).unwrap();
|
||||
let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
|
||||
|
||||
let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path));
|
||||
self.restore_document_transform(old_transforms);
|
||||
|
||||
FrontendMessage::TriggerNodeGraphFrameGenerate {
|
||||
document_id,
|
||||
layer_path,
|
||||
image_data: Default::default(),
|
||||
size: (0, 0),
|
||||
svg,
|
||||
size,
|
||||
imaginate_node,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate the size of the region to be exported
|
||||
|
||||
let old_transforms = self.remove_document_transform();
|
||||
let transform = self.document_legacy.multiply_transforms(&layer_path).unwrap();
|
||||
let size = DVec2::new(transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
|
||||
|
||||
let svg = self.render_document(size, transform.inverse(), persistent_data, DocumentRenderMode::OnlyBelowLayerInFolder(&layer_path));
|
||||
self.restore_document_transform(old_transforms);
|
||||
|
||||
Some(
|
||||
FrontendMessage::TriggerNodeGraphFrameGenerate {
|
||||
.into()
|
||||
}
|
||||
// Skip processing under node graph frame input if not connected
|
||||
_ => PortfolioMessage::ProcessNodeGraphFrame {
|
||||
document_id,
|
||||
layer_path,
|
||||
svg,
|
||||
size,
|
||||
image_data: Default::default(),
|
||||
size: (0, 0),
|
||||
imaginate_node,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
};
|
||||
Some(response)
|
||||
}
|
||||
|
||||
/// Remove the artwork and artboard pan/tilt/zoom to render it without the user's viewport navigation, and save it to be restored at the end
|
||||
|
||||
+9
-6
@@ -119,7 +119,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
});
|
||||
}
|
||||
|
||||
fn transform_change(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, skip_rerender: bool) {
|
||||
fn transform_change(&mut self, transform: DAffine2, transform_in: TransformIn, parent_transform: DAffine2, bounds: LayerBounds, skip_rerender: bool) {
|
||||
self.modify_inputs("Transform", skip_rerender, |inputs| {
|
||||
let layer_transform = transform_utils::get_current_transform(inputs);
|
||||
let to = match transform_in {
|
||||
@@ -127,7 +127,8 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
TransformIn::Scope { scope } => scope * parent_transform,
|
||||
TransformIn::Viewport => parent_transform,
|
||||
};
|
||||
let transform = to.inverse() * transform * to * layer_transform;
|
||||
let pivot = DAffine2::from_translation(bounds.layerspace_pivot(transform_utils::get_current_normalized_pivot(inputs)));
|
||||
let transform = pivot.inverse() * to.inverse() * transform * to * pivot * layer_transform;
|
||||
transform_utils::update_transform(inputs, transform);
|
||||
});
|
||||
}
|
||||
@@ -140,7 +141,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
TransformIn::Viewport => parent_transform,
|
||||
};
|
||||
let pivot = DAffine2::from_translation(bounds.layerspace_pivot(transform_utils::get_current_normalized_pivot(inputs)));
|
||||
let transform = to.inverse() * transform * pivot;
|
||||
let transform = pivot.inverse() * to.inverse() * transform * pivot;
|
||||
transform_utils::update_transform(inputs, transform);
|
||||
});
|
||||
}
|
||||
@@ -150,7 +151,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
let layer_transform = transform_utils::get_current_transform(inputs);
|
||||
let old_pivot_transform = DAffine2::from_translation(bounds.local_pivot(transform_utils::get_current_normalized_pivot(inputs)));
|
||||
let new_pivot_transform = DAffine2::from_translation(bounds.local_pivot(new_pivot));
|
||||
let transform = layer_transform * old_pivot_transform.inverse() * new_pivot_transform;
|
||||
let transform = new_pivot_transform.inverse() * old_pivot_transform * layer_transform * old_pivot_transform.inverse() * new_pivot_transform;
|
||||
transform_utils::update_transform(inputs, transform);
|
||||
inputs[5] = NodeInput::value(TaggedValue::DVec2(new_pivot), false);
|
||||
});
|
||||
@@ -184,6 +185,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
|
||||
[new_bounds_min, new_bounds_max] = transform_utils::nonzero_subpath_bounds(subpaths);
|
||||
});
|
||||
|
||||
self.modify_inputs("Transform", false, |inputs| {
|
||||
let layer_transform = transform_utils::get_current_transform(inputs);
|
||||
let normalized_pivot = transform_utils::get_current_normalized_pivot(inputs);
|
||||
@@ -193,7 +195,7 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
let new_pivot_transform = DAffine2::from_translation(new_layerspace_pivot);
|
||||
let old_pivot_transform = DAffine2::from_translation(old_layerspace_pivot);
|
||||
|
||||
let transform = layer_transform * old_pivot_transform.inverse() * new_pivot_transform;
|
||||
let transform = new_pivot_transform.inverse() * old_pivot_transform * layer_transform * old_pivot_transform.inverse() * new_pivot_transform;
|
||||
transform_utils::update_transform(inputs, transform);
|
||||
});
|
||||
}
|
||||
@@ -225,8 +227,9 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
|
||||
skip_rerender,
|
||||
} => {
|
||||
let parent_transform = document.multiply_transforms(&layer[..layer.len() - 1]).unwrap_or_default();
|
||||
let bounds = LayerBounds::new(document, &layer);
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new(&layer, document, node_graph, responses) {
|
||||
modify_inputs.transform_change(transform, transform_in, parent_transform, skip_rerender);
|
||||
modify_inputs.transform_change(transform, transform_in, parent_transform, bounds, skip_rerender);
|
||||
}
|
||||
|
||||
let transform = transform.to_cols_array();
|
||||
|
||||
+56
-9
@@ -8,6 +8,7 @@ use graph_craft::document::*;
|
||||
use graph_craft::imaginate_input::ImaginateSamplingMethod;
|
||||
use graph_craft::NodeIdentifier;
|
||||
use graphene_core::raster::{BlendMode, Color, Image, ImageFrame, LuminanceCalculation};
|
||||
use graphene_core::vector::VectorData;
|
||||
use graphene_core::*;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -448,6 +449,35 @@ fn static_nodes() -> Vec<DocumentNodeType> {
|
||||
}],
|
||||
properties: node_properties::blur_image_properties,
|
||||
},
|
||||
DocumentNodeType {
|
||||
name: "Brush",
|
||||
category: "Brush",
|
||||
identifier: NodeImplementation::proto("graphene_std::brush::BrushNode"),
|
||||
inputs: vec![
|
||||
DocumentInputType::value("None", TaggedValue::None, false),
|
||||
DocumentInputType::value("Trace", TaggedValue::VecDVec2((0..2).map(|x| DVec2::new(x as f64 * 10., 0.)).collect()), true),
|
||||
DocumentInputType::value("Diameter", TaggedValue::F64(40.), false),
|
||||
DocumentInputType::value("Hardness", TaggedValue::F64(50.), false),
|
||||
DocumentInputType::value("Flow", TaggedValue::F64(100.), false),
|
||||
DocumentInputType::value("Color", TaggedValue::Color(Color::BLACK), false),
|
||||
],
|
||||
outputs: vec![DocumentOutputType {
|
||||
name: "Image",
|
||||
data_type: FrontendGraphDataType::Raster,
|
||||
}],
|
||||
properties: node_properties::brush_node_properties,
|
||||
},
|
||||
DocumentNodeType {
|
||||
name: "Extract Vector Points",
|
||||
category: "Brush",
|
||||
identifier: NodeImplementation::proto("graphene_std::brush::VectorPointsNode"),
|
||||
inputs: vec![DocumentInputType::value("VectorData", TaggedValue::VectorData(VectorData::empty()), true)],
|
||||
outputs: vec![DocumentOutputType {
|
||||
name: "Vector Points",
|
||||
data_type: FrontendGraphDataType::General,
|
||||
}],
|
||||
properties: node_properties::no_properties,
|
||||
},
|
||||
DocumentNodeType {
|
||||
name: "Cache",
|
||||
category: "Structural",
|
||||
@@ -490,6 +520,14 @@ fn static_nodes() -> Vec<DocumentNodeType> {
|
||||
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
|
||||
properties: |_document_node, _node_id, _context| node_properties::string_properties("A bitmap image embedded in this node"),
|
||||
},
|
||||
DocumentNodeType {
|
||||
name: "Ref",
|
||||
category: "Structural",
|
||||
identifier: NodeImplementation::proto("graphene_std::memo::CacheNode"),
|
||||
inputs: vec![DocumentInputType::value("Image", TaggedValue::ImageFrame(ImageFrame::empty()), true)],
|
||||
outputs: vec![DocumentOutputType::new("Image", FrontendGraphDataType::Raster)],
|
||||
properties: node_properties::no_properties,
|
||||
},
|
||||
#[cfg(feature = "gpu")]
|
||||
DocumentNodeType {
|
||||
name: "GpuImage",
|
||||
@@ -841,8 +879,18 @@ impl DocumentNodeType {
|
||||
}
|
||||
|
||||
pub fn wrap_network_in_scope(network: NodeNetwork) -> NodeNetwork {
|
||||
// if the network has no inputs, it doesn't need to be wrapped in a scope
|
||||
if network.inputs.is_empty() {
|
||||
return network;
|
||||
}
|
||||
|
||||
assert_eq!(network.inputs.len(), 1, "Networks wrapped in scope must have exactly one input");
|
||||
let input_type = network.nodes[&network.inputs[0]].inputs.iter().find(|&i| matches!(i, NodeInput::Network(_))).unwrap().clone();
|
||||
let input = network.nodes[&network.inputs[0]].inputs.iter().find(|&i| matches!(i, NodeInput::Network(_))).cloned();
|
||||
|
||||
// if the network has no network inputs, it doesn't need to be wrapped in a scope either
|
||||
let Some(input_type) = input else {
|
||||
return network;
|
||||
};
|
||||
|
||||
let inner_network = DocumentNode {
|
||||
name: "Scope".to_string(),
|
||||
@@ -850,6 +898,7 @@ pub fn wrap_network_in_scope(network: NodeNetwork) -> NodeNetwork {
|
||||
inputs: vec![NodeInput::node(0, 1)],
|
||||
metadata: DocumentNodeMetadata::default(),
|
||||
};
|
||||
|
||||
// wrap the inner network in a scope
|
||||
let nodes = vec![
|
||||
resolve_document_node_type("Begin Scope")
|
||||
@@ -890,7 +939,6 @@ pub fn new_image_network(output_offset: i32, output_node_id: NodeId) -> NodeNetw
|
||||
}
|
||||
|
||||
pub fn new_vector_network(subpaths: Vec<bezier_rs::Subpath<uuid::ManipulatorGroupId>>) -> NodeNetwork {
|
||||
let input = resolve_document_node_type("Input Frame").expect("Input Frame node does not exist");
|
||||
let path_generator = resolve_document_node_type("Path Generator").expect("Path Generator node does not exist");
|
||||
let transform = resolve_document_node_type("Transform").expect("Transform node does not exist");
|
||||
let fill = resolve_document_node_type("Fill").expect("Fill node does not exist");
|
||||
@@ -905,15 +953,14 @@ pub fn new_vector_network(subpaths: Vec<bezier_rs::Subpath<uuid::ManipulatorGrou
|
||||
};
|
||||
|
||||
NodeNetwork {
|
||||
inputs: vec![0],
|
||||
outputs: vec![NodeOutput::new(5, 0)],
|
||||
inputs: vec![],
|
||||
outputs: vec![NodeOutput::new(4, 0)],
|
||||
nodes: [
|
||||
input.to_document_node_default_inputs([], next_pos()),
|
||||
path_generator.to_document_node_default_inputs([Some(NodeInput::value(TaggedValue::Subpaths(subpaths), false))], next_pos()),
|
||||
transform.to_document_node_default_inputs([Some(NodeInput::node(1, 0))], next_pos()),
|
||||
fill.to_document_node_default_inputs([Some(NodeInput::node(2, 0))], next_pos()),
|
||||
stroke.to_document_node_default_inputs([Some(NodeInput::node(3, 0))], next_pos()),
|
||||
output.to_document_node_default_inputs([Some(NodeInput::node(4, 0))], next_pos()),
|
||||
transform.to_document_node_default_inputs([Some(NodeInput::node(0, 0))], next_pos()),
|
||||
fill.to_document_node_default_inputs([Some(NodeInput::node(1, 0))], next_pos()),
|
||||
stroke.to_document_node_default_inputs([Some(NodeInput::node(2, 0))], next_pos()),
|
||||
output.to_document_node_default_inputs([Some(NodeInput::node(3, 0))], next_pos()),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
|
||||
+10
@@ -515,6 +515,16 @@ pub fn blur_image_properties(document_node: &DocumentNode, node_id: NodeId, _con
|
||||
vec![LayoutGroup::Row { widgets: radius }, LayoutGroup::Row { widgets: sigma }]
|
||||
}
|
||||
|
||||
pub fn brush_node_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let color = color_widget(document_node, node_id, 5, "Color", ColorInput::default(), true);
|
||||
|
||||
let size = number_widget(document_node, node_id, 2, "Diameter", NumberInput::default().min(0.).max(100.).unit(" px"), true);
|
||||
let hardness = number_widget(document_node, node_id, 3, "Hardness", NumberInput::default().min(0.).max(100.).unit("%"), true);
|
||||
let flow = number_widget(document_node, node_id, 4, "Flow", NumberInput::default().min(1.).max(100.).unit("%"), true);
|
||||
|
||||
vec![color, LayoutGroup::Row { widgets: size }, LayoutGroup::Row { widgets: hardness }, LayoutGroup::Row { widgets: flow }]
|
||||
}
|
||||
|
||||
pub fn adjust_threshold_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||
let thereshold_min = number_widget(document_node, node_id, 1, "Min Luminance", NumberInput::default().min(0.).max(100.).unit("%"), true);
|
||||
let thereshold_max = number_widget(document_node, node_id, 2, "Max Luminance", NumberInput::default().min(0.).max(100.).unit("%"), true);
|
||||
|
||||
@@ -32,6 +32,7 @@ pub use crate::messages::workspace::{WorkspaceMessage, WorkspaceMessageDiscrimin
|
||||
pub use crate::messages::broadcast::broadcast_event::{BroadcastEvent, BroadcastEventDiscriminant};
|
||||
pub use crate::messages::message::{Message, MessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::artboard_tool::{ArtboardToolMessage, ArtboardToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::brush_tool::{BrushToolMessage, BrushToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::ellipse_tool::{EllipseToolMessage, EllipseToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::eyedropper_tool::{EyedropperToolMessage, EyedropperToolMessageDiscriminant};
|
||||
pub use crate::messages::tool::tool_messages::fill_tool::{FillToolMessage, FillToolMessageDiscriminant};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,6 +9,7 @@ use document_legacy::{LayerId, Operation};
|
||||
use dyn_any::DynAny;
|
||||
use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork, NodeOutput};
|
||||
use graph_craft::executor::Compiler;
|
||||
use graph_craft::{concrete, Type, TypeDescriptor};
|
||||
use graphene_core::raster::{Image, ImageFrame};
|
||||
use graphene_core::vector::VectorData;
|
||||
use interpreted_executor::executor::DynamicExecutor;
|
||||
@@ -42,7 +43,11 @@ impl NodeGraphExecutor {
|
||||
use dyn_any::IntoDynAny;
|
||||
use graph_craft::executor::Executor;
|
||||
|
||||
self.executor.execute(image_frame.into_dyn()).map_err(|e| e.to_string())
|
||||
match self.executor.input_type() {
|
||||
Some(t) if t == concrete!(ImageFrame) => self.executor.execute(image_frame.into_dyn()).map_err(|e| e.to_string()),
|
||||
Some(t) if t == concrete!(()) => self.executor.execute(().into_dyn()).map_err(|e| e.to_string()),
|
||||
_ => Err("Invalid input type".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes an input for a node in the graph
|
||||
|
||||
Reference in New Issue
Block a user