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