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:
0HyperCube
2023-04-12 18:50:42 +01:00
committed by Keavon Chambers
parent 69f6ed3a11
commit f271964bf8
20 changed files with 130 additions and 58 deletions

View File

@@ -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;

View File

@@ -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)]

View File

@@ -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),

View File

@@ -1628,6 +1628,7 @@ impl DocumentMessageHandler {
path: path.clone(),
image_data: data.image_data.clone(),
mime: node_graph_frame.mime.clone(),
transform: None,
});
}
}

View File

@@ -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()))
}

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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),

View File

@@ -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(())