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 c7d14c2a7b
commit ed6140b4a7
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(())

View File

@ -98,11 +98,13 @@ fn handle_message(message: String) -> String {
for image in image_data {
let path = image.path.clone();
let mime = image.mime.clone();
let transform = image.transform.clone();
images.insert(format!("{:?}_{}", &image.path, document_id), image);
stub_data.push(FrontendImageData {
path,
mime,
image_data: Arc::new(Vec::new()),
transform,
});
}
FrontendMessage::UpdateImageData { document_id, image_data: stub_data }

View File

@ -529,6 +529,7 @@
}
&::placeholder {
opacity: 1;
color: inherit;
font-style: italic;
}

View File

@ -113,7 +113,7 @@ export function createPortfolioState(editor: Editor) {
image.src = blobURL;
await image.decode();
editor.instance.setImageBlobURL(updateImageData.documentId, element.path, blobURL, image.naturalWidth, image.naturalHeight);
editor.instance.setImageBlobURL(updateImageData.documentId, element.path, blobURL, image.naturalWidth, image.naturalHeight, element.transform);
});
});
editor.subscriptions.subscribeJsMessage(TriggerNodeGraphFrameGenerate, async (triggerNodeGraphFrameGenerate) => {

View File

@ -11,7 +11,7 @@ export type Editor = Readonly<ReturnType<typeof createEditor>>;
let wasmImport: WasmRawInstance | undefined;
let editorInstance: WasmEditorInstance | undefined;
export async function updateImage(path: BigUint64Array, mime: string, imageData: Uint8Array, documentId: bigint): Promise<void> {
export async function updateImage(path: BigUint64Array, mime: string, imageData: Uint8Array, transform: Float64Array, documentId: bigint): Promise<void> {
const blob = new Blob([imageData], { type: mime });
const blobURL = URL.createObjectURL(blob);
@ -21,7 +21,7 @@ export async function updateImage(path: BigUint64Array, mime: string, imageData:
image.src = blobURL;
await image.decode();
editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight);
editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight,transform);
}
export async function fetchImage(path: BigUint64Array, mime: string, documentId: bigint, url: string): Promise<void> {
@ -35,7 +35,7 @@ export async function fetchImage(path: BigUint64Array, mime: string, documentId:
image.src = blobURL;
await image.decode();
editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight);
editorInstance?.setImageBlobURL(documentId, path, blobURL, image.naturalWidth, image.naturalHeight, undefined);
}
const tauri = "__TAURI_METADATA__" in window && import("@tauri-apps/api");

View File

@ -768,6 +768,8 @@ export class ImaginateImageData {
readonly mime!: string;
readonly imageData!: Uint8Array;
readonly transform!: Float64Array ;
}
export class DisplayDialogDismiss extends JsMessage {}

View File

@ -32,7 +32,7 @@ pub fn set_random_seed(seed: u64) {
/// This avoids creating a json with a list millions of numbers long.
#[wasm_bindgen(module = "@graphite/wasm-communication/editor")]
extern "C" {
fn updateImage(path: Vec<u64>, mime: String, imageData: &[u8], document_id: u64);
fn updateImage(path: Vec<u64>, mime: String, imageData: &[u8], transform: js_sys::Float64Array, document_id: u64);
fn fetchImage(path: Vec<u64>, mime: String, document_id: u64, identifier: String);
//fn dispatchTauri(message: String) -> String;
fn dispatchTauri(message: String);
@ -114,7 +114,16 @@ impl JsEditorHandle {
if let FrontendMessage::UpdateImageData { document_id, image_data } = message {
for image in image_data {
#[cfg(not(feature = "tauri"))]
updateImage(image.path, image.mime, &image.image_data, document_id);
{
let transform = if let Some(transform_val) = image.transform {
let transform = js_sys::Float64Array::new_with_length(6);
transform.copy_from(&transform_val);
transform
} else {
js_sys::Float64Array::default()
};
updateImage(image.path, image.mime, &image.image_data, transform, document_id);
}
#[cfg(feature = "tauri")]
fetchImage(image.path.clone(), image.mime, document_id, format!("http://localhost:3001/image/{:?}_{}", &image.path, document_id));
}
@ -495,15 +504,22 @@ impl JsEditorHandle {
/// Sends the blob URL generated by JS to the Image layer
#[wasm_bindgen(js_name = setImageBlobURL)]
pub fn set_image_blob_url(&self, document_id: u64, layer_path: Vec<LayerId>, blob_url: String, width: f64, height: f64) {
pub fn set_image_blob_url(&self, document_id: u64, layer_path: Vec<LayerId>, blob_url: String, width: f64, height: f64, transform: Option<js_sys::Float64Array>) {
let resolution = (width, height);
let message = PortfolioMessage::SetImageBlobUrl {
document_id,
layer_path,
layer_path: layer_path.clone(),
blob_url,
resolution,
};
self.dispatch(message);
if let Some(array) = transform.filter(|array| array.length() == 6) {
let mut transform: [f64; 6] = [0.; 6];
array.copy_to(&mut transform);
let message = document_legacy::Operation::SetLayerTransform { path: layer_path, transform };
self.dispatch(message);
}
}
/// Sends the blob URL generated by JS to the Imaginate layer in the respective document

View File

@ -1,5 +1,5 @@
use core::marker::PhantomData;
use dyn_any::{DynAny, StaticType, StaticTypeSized};
use dyn_any::{StaticType, StaticTypeSized};
use crate::Node;

View File

@ -274,7 +274,8 @@ fn blend_image_tuple<MapFn>(images: (ImageFrame, ImageFrame), map_fn: &'any_inpu
where
MapFn: for<'any_input> Node<'any_input, (Color, Color), Output = Color> + 'input + Clone,
{
let (mut background, foreground) = images;
let (background, foreground) = images;
let node = BlendImageNode::new(ClonedNode::new(background), ValueNode::new(map_fn.clone()));
node.eval(foreground)
}

View File

@ -7,7 +7,7 @@ use graph_craft::document::value::UpcastNode;
use graph_craft::document::NodeId;
use graph_craft::executor::Executor;
use graph_craft::proto::{ConstructionArgs, ProtoNetwork, ProtoNode, TypingContext};
use graph_craft::{Type, TypeDescriptor};
use graph_craft::Type;
use graphene_std::any::{Any, TypeErasedPinned, TypeErasedPinnedRef};
use crate::node_registry;

View File

@ -21,7 +21,7 @@ use graph_craft::proto::NodeConstructor;
use graphene_core::{concrete, fn_type, generic, value_fn};
use graphene_std::memo::{CacheNode, LetNode};
use graphene_std::raster::{BlendImageTupleNode, MapImageFrameNode};
use graphene_std::raster::BlendImageTupleNode;
use crate::executor::NodeContainer;

View File

@ -2,8 +2,8 @@ use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{format_ident, ToTokens};
use syn::{
parse_macro_input, punctuated::Punctuated, token::Comma, FnArg, GenericParam, Ident, ItemFn, Lifetime, Pat, PatIdent, PatType, PathArguments, PredicateType, ReturnType, Token, TraitBound, Type,
TypeParam, TypeParamBound, WhereClause, WherePredicate,
parse_macro_input, punctuated::Punctuated, token::Comma, FnArg, GenericParam, Ident, ItemFn, Lifetime, Pat, PatIdent, PathArguments, PredicateType, ReturnType, Token, TraitBound, Type, TypeParam,
TypeParamBound, WhereClause, WherePredicate,
};
#[proc_macro_attribute]
@ -51,7 +51,7 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream {
.filter(|gen| {
if let GenericParam::Type(ty) = gen {
!function.sig.inputs.iter().take(1).any(|param_ty| match param_ty {
FnArg::Typed(pat_ty) => pat_ty.ty.to_token_stream().to_string() == ty.ident.to_string(),
FnArg::Typed(pat_ty) => ty.ident == pat_ty.ty.to_token_stream().to_string(),
_ => false,
})
} else {