mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 15:28:04 +08:00
Brush tool live preview (#1116)
* Disable vector preview for brush tool * Fix brush preview * Fix warping * Left and right square brackets to change size * Add linear interpolation * Modfiy existing selected brush layer * Resolve warnings --------- Co-authored-by: Dennis Kobert <dennis@kobert.dev> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
parent
69f6ed3a11
commit
f271964bf8
@@ -62,6 +62,9 @@ pub const CREATE_CURVE_THRESHOLD: f64 = 5.;
|
||||
// Line tool
|
||||
pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
|
||||
// Brush tool
|
||||
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
|
||||
|
||||
// Scrollbars
|
||||
pub const SCROLLBAR_SPACING: f64 = 0.1;
|
||||
pub const ASYMPTOTIC_EFFECT: f64 = 0.5;
|
||||
|
||||
@@ -11,12 +11,13 @@ pub struct FrontendDocumentDetails {
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
||||
pub struct FrontendImageData {
|
||||
pub path: Vec<LayerId>,
|
||||
pub mime: String,
|
||||
#[serde(skip)]
|
||||
pub image_data: std::sync::Arc<Vec<u8>>,
|
||||
pub transform: Option<[f64; 6]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize, specta::Type)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::consts::{BIG_NUDGE_AMOUNT, NUDGE_AMOUNT};
|
||||
use crate::consts::{BIG_NUDGE_AMOUNT, BRUSH_SIZE_CHANGE_KEYBOARD, NUDGE_AMOUNT};
|
||||
use crate::messages::input_mapper::key_mapping::MappingVariant;
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeyStates};
|
||||
use crate::messages::input_mapper::utility_types::macros::*;
|
||||
@@ -6,6 +6,7 @@ use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
||||
use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping};
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpdate;
|
||||
|
||||
use glam::DVec2;
|
||||
|
||||
@@ -209,6 +210,8 @@ pub fn default_mapping() -> Mapping {
|
||||
entry!(PointerMove; action_dispatch=BrushToolMessage::PointerMove),
|
||||
entry!(KeyDown(Lmb); action_dispatch=BrushToolMessage::DragStart),
|
||||
entry!(KeyUp(Lmb); action_dispatch=BrushToolMessage::DragStop),
|
||||
entry!(KeyDown(BracketLeft); action_dispatch=BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ChangeDiameter(-BRUSH_SIZE_CHANGE_KEYBOARD))),
|
||||
entry!(KeyDown(BracketRight); action_dispatch=BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::ChangeDiameter(BRUSH_SIZE_CHANGE_KEYBOARD))),
|
||||
//
|
||||
// ToolMessage
|
||||
entry!(KeyDown(KeyV); action_dispatch=ToolMessage::ActivateToolSelect),
|
||||
|
||||
@@ -1628,6 +1628,7 @@ impl DocumentMessageHandler {
|
||||
path: path.clone(),
|
||||
image_data: data.image_data.clone(),
|
||||
mime: node_graph_frame.mime.clone(),
|
||||
transform: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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::misc::LayoutTarget;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
|
||||
use crate::messages::prelude::*;
|
||||
@@ -9,11 +10,9 @@ use crate::messages::tool::utility_types::{DocumentToolData, EventToMessageMap,
|
||||
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;
|
||||
@@ -60,6 +59,7 @@ pub enum BrushToolMessage {
|
||||
#[remain::sorted]
|
||||
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)]
|
||||
pub enum BrushToolMessageOptionsUpdate {
|
||||
ChangeDiameter(f64),
|
||||
Diameter(f64),
|
||||
Flow(f64),
|
||||
Hardness(f64),
|
||||
@@ -119,6 +119,18 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for BrushTo
|
||||
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::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::Hardness(hardness) => self.options.hardness = hardness,
|
||||
BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow,
|
||||
@@ -137,11 +149,13 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for BrushTo
|
||||
DragStart,
|
||||
DragStop,
|
||||
Abort,
|
||||
UpdateOptions,
|
||||
),
|
||||
Drawing => actions!(BrushToolMessageDiscriminant;
|
||||
DragStop,
|
||||
PointerMove,
|
||||
Abort,
|
||||
UpdateOptions,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -166,6 +180,19 @@ struct BrushToolData {
|
||||
path: Option<Vec<LayerId>>,
|
||||
}
|
||||
|
||||
impl BrushToolData {
|
||||
fn update_points(&self, responses: &mut VecDeque<Message>) {
|
||||
if let Some(layer_path) = self.path.clone() {
|
||||
responses.add(NodeGraphMessage::SetQualifiedInputValue {
|
||||
layer_path,
|
||||
node_path: vec![0],
|
||||
input_index: 1,
|
||||
value: TaggedValue::VecDVec2(self.points.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Fsm for BrushToolFsmState {
|
||||
type ToolData = BrushToolData;
|
||||
type ToolOptions = BrushOptions;
|
||||
@@ -189,8 +216,15 @@ impl Fsm for BrushToolFsmState {
|
||||
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 existing_points = load_existing_points(document);
|
||||
let new_layer = existing_points.is_none();
|
||||
if let Some((layer_path, points)) = existing_points {
|
||||
tool_data.path = Some(layer_path);
|
||||
tool_data.points = points;
|
||||
} else {
|
||||
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);
|
||||
|
||||
@@ -200,7 +234,11 @@ impl Fsm for BrushToolFsmState {
|
||||
tool_data.hardness = tool_options.hardness;
|
||||
tool_data.flow = tool_options.flow;
|
||||
|
||||
add_polyline(tool_data, global_tool_data, responses);
|
||||
if new_layer {
|
||||
add_brush_render(tool_data, global_tool_data, responses);
|
||||
} else {
|
||||
tool_data.update_points(responses);
|
||||
}
|
||||
|
||||
Drawing
|
||||
}
|
||||
@@ -208,17 +246,22 @@ impl Fsm for BrushToolFsmState {
|
||||
let pos = transform.inverse().transform_point2(input.mouse.position);
|
||||
|
||||
if tool_data.points.last() != Some(&pos) {
|
||||
// Linear interpolation for when the mouse has moved a lot between frames
|
||||
if let Some(&last_point) = tool_data.points.last() {
|
||||
let distance = (last_point - pos).length();
|
||||
let extra_points = (distance / (tool_data.diameter / 2.)).floor() as usize;
|
||||
tool_data.points.extend((0..extra_points).map(|i| last_point.lerp(pos, (i as f64 + 1.) / (extra_points as f64 + 1.))));
|
||||
}
|
||||
|
||||
tool_data.points.push(pos);
|
||||
}
|
||||
|
||||
add_polyline(tool_data, global_tool_data, responses);
|
||||
tool_data.update_points(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());
|
||||
@@ -250,21 +293,6 @@ impl Fsm for BrushToolFsmState {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -288,3 +316,22 @@ fn add_brush_render(data: &BrushToolData, tool_data: &DocumentToolData, response
|
||||
network.push_output_node();
|
||||
graph_modification_utils::new_custom_layer(network, layer_path.clone(), responses);
|
||||
}
|
||||
|
||||
fn load_existing_points(document: &DocumentMessageHandler) -> Option<(Vec<LayerId>, Vec<DVec2>)> {
|
||||
if document.selected_layers().count() != 1 {
|
||||
return None;
|
||||
}
|
||||
let layer_path = document.selected_layers().next()?.to_vec();
|
||||
let network = document.document_legacy.layer(&layer_path).ok().and_then(|layer| layer.as_node_graph().ok())?;
|
||||
let brush_node = network.nodes.get(&0)?;
|
||||
if brush_node.implementation != DocumentNodeImplementation::Unresolved("graphene_std::brush::BrushNode".into()) {
|
||||
return None;
|
||||
}
|
||||
let points_input = brush_node.inputs.get(1)?;
|
||||
let NodeInput::Value {
|
||||
tagged_value: TaggedValue::VecDVec2(points),
|
||||
..
|
||||
} = points_input else { return None };
|
||||
|
||||
Some((layer_path, points.clone()))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use document_legacy::LayerId;
|
||||
use document_legacy::Operation;
|
||||
use graphene_core::vector::style::Stroke;
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -217,9 +217,9 @@ fn remove_preview(data: &FreehandToolData) -> Message {
|
||||
}
|
||||
|
||||
fn add_polyline(data: &FreehandToolData, 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);
|
||||
let position = subpath.bounding_box().unwrap_or_default().into_iter().sum::<DVec2>() / 2.;
|
||||
|
||||
let layer_path = data.path.clone().unwrap();
|
||||
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
|
||||
|
||||
responses.add(GraphOperationMessage::StrokeSet {
|
||||
|
||||
@@ -971,12 +971,6 @@ fn rerender_selected_layers(tool_data: &mut SelectToolData, responses: &mut VecD
|
||||
}
|
||||
}
|
||||
|
||||
fn rerender_duplicated_layers(tool_data: &mut SelectToolData, responses: &mut VecDeque<Message>) {
|
||||
for layer_path in tool_data.not_duplicated_layers.iter().flatten() {
|
||||
responses.add(DocumentMessage::NodeGraphFrameGenerate { layer_path: layer_path.clone() });
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Majorly clean up these next five functions
|
||||
|
||||
fn drag_shallowest_manipulation(
|
||||
|
||||
@@ -262,10 +262,10 @@ fn add_spline(tool_data: &SplineToolData, global_tool_data: &DocumentToolData, s
|
||||
}
|
||||
|
||||
let subpath = bezier_rs::Subpath::new_cubic_spline(points);
|
||||
let position = subpath.bounding_box().unwrap_or_default().into_iter().sum::<DVec2>() / 2.;
|
||||
|
||||
let layer_path = tool_data.path.clone().unwrap();
|
||||
graph_modification_utils::new_vector_layer(vec![subpath], layer_path.clone(), responses);
|
||||
|
||||
responses.add(GraphOperationMessage::StrokeSet {
|
||||
layer: layer_path.clone(),
|
||||
stroke: Stroke::new(global_tool_data.primary_color, tool_data.weight),
|
||||
|
||||
@@ -269,9 +269,17 @@ impl NodeGraphExecutor {
|
||||
// Attempt to downcast to an image frame
|
||||
let ImageFrame { image, transform } = dyn_any::downcast(boxed_node_graph_output).map(|image_frame| *image_frame)?;
|
||||
|
||||
// Don't update the frame's transform if the new transform is DAffine2::ZERO.
|
||||
let transform = (!transform.abs_diff_eq(DAffine2::ZERO, f64::EPSILON)).then_some(transform.to_cols_array());
|
||||
|
||||
// If no image was generated, clear the frame
|
||||
if image.width == 0 || image.height == 0 {
|
||||
responses.push_back(DocumentMessage::FrameClear.into());
|
||||
|
||||
// Update the transform based on the graph output
|
||||
if let Some(transform) = transform {
|
||||
responses.push_back(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into());
|
||||
}
|
||||
} else {
|
||||
// Update the image data
|
||||
let (image_data, _size) = Self::encode_img(image, None, image::ImageOutputFormat::Bmp)?;
|
||||
@@ -289,17 +297,10 @@ impl NodeGraphExecutor {
|
||||
path: layer_path.clone(),
|
||||
image_data,
|
||||
mime,
|
||||
transform,
|
||||
}];
|
||||
responses.push_back(FrontendMessage::UpdateImageData { document_id, image_data }.into());
|
||||
}
|
||||
|
||||
// Don't update the frame's transform if the new transform is DAffine2::ZERO.
|
||||
if !transform.abs_diff_eq(DAffine2::ZERO, f64::EPSILON) {
|
||||
// Update the transform based on the graph output
|
||||
let transform = transform.to_cols_array();
|
||||
responses.push_back(Operation::SetLayerTransform { path: layer_path.clone(), transform }.into());
|
||||
responses.push_back(Operation::SetLayerVisibility { path: layer_path, visible: true }.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user