From 7148b199ec0f27d940d5eedd3197bb7b1e15e2ba Mon Sep 17 00:00:00 2001 From: Orson Peters Date: Sat, 27 May 2023 22:55:49 +0200 Subject: [PATCH] Make Brush tool use per-stroke options and improve its performance (#1242) * Laid groundwork for per-stroke brush parameters. * Added new spacing parameter. * Added back interpolation, using spacing parameter. * Move bounding box code into core. * Initial working prototype of per-stroke styles. * Removed now useless brush node properties. * Made default spacing 50% for performance comparison. * Quick and dirty prototype for BlitNode copied from blend. * Fixed error after rebase. * Optimized the blitting loop. * Pretty big optimization for into_flat_u8. * Insert brush node for images * Fix starting position transform * UX polish * Code review nits --------- Co-authored-by: 0hypercube <0hypercube@gmail.com> Co-authored-by: Keavon Chambers --- .../node_graph/graph_operation_message.rs | 5 + .../graph_operation_message_handler.rs | 20 +- .../document_node_types.rs | 8 +- .../node_properties.rs | 38 +-- .../messages/tool/tool_messages/brush_tool.rs | 222 ++++++++---------- editor/src/node_graph_executor.rs | 1 - node-graph/gcore/src/raster.rs | 1 + node-graph/gcore/src/raster/bbox.rs | 86 +++++++ node-graph/gcore/src/raster/image.rs | 54 +++-- node-graph/gcore/src/vector/brush_stroke.rs | 109 +++++++++ node-graph/gcore/src/vector/mod.rs | 1 + node-graph/graph-craft/src/document/value.rs | 15 +- node-graph/gstd/src/brush.rs | 80 ++++++- node-graph/gstd/src/raster.rs | 92 +------- .../interpreted-executor/src/node_registry.rs | 96 ++++---- 15 files changed, 491 insertions(+), 337 deletions(-) create mode 100644 node-graph/gcore/src/raster/bbox.rs create mode 100644 node-graph/gcore/src/vector/brush_stroke.rs diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs index 56b5227a..fd03b130 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message.rs @@ -1,6 +1,7 @@ use crate::messages::prelude::*; use graphene_core::uuid::ManipulatorGroupId; +use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::vector::ManipulatorPointId; @@ -46,6 +47,10 @@ pub enum GraphOperationMessage { layer: LayerIdentifier, modification: VectorDataModification, }, + Brush { + layer: LayerIdentifier, + strokes: Vec, + }, } #[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] diff --git a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs index 28da7318..0613ddcf 100644 --- a/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/graph_operation_message_handler.rs @@ -5,12 +5,13 @@ use document_legacy::document::Document; use document_legacy::{LayerId, Operation}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork}; +use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::style::{Fill, FillType, Stroke}; use transform_utils::LayerBounds; use glam::{DAffine2, DVec2}; -mod transform_utils; +pub mod transform_utils; #[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)] pub struct GraphOperationMessageHandler; @@ -220,6 +221,16 @@ impl<'a> ModifyInputsContext<'a> { self.update_bounds([old_bounds_min, old_bounds_max], [new_bounds_min, new_bounds_max]); } + + fn brush_modify(&mut self, strokes: Vec) { + self.modify_inputs("Brush", false, |inputs| { + if matches!(inputs[0], NodeInput::Node { .. }) { + inputs[1] = core::mem::replace(&mut inputs[0], NodeInput::value(TaggedValue::None, false)); + } + inputs[0] = NodeInput::value(TaggedValue::None, false); + inputs[3] = NodeInput::value(TaggedValue::BrushStrokes(strokes), false); + }); + } } impl MessageHandler for GraphOperationMessageHandler { @@ -244,7 +255,6 @@ impl MessageHandler { if let Some(mut modify_inputs) = ModifyInputsContext::new(&layer, document, node_graph, responses) { modify_inputs.vector_modify(modification); } } + GraphOperationMessage::Brush { layer, strokes } => { + if let Some(mut modify_inputs) = ModifyInputsContext::new(&layer, document, node_graph, responses) { + modify_inputs.brush_modify(strokes); + } + } } } diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs index f8d5f320..e16cbc13 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/document_node_types.rs @@ -639,17 +639,13 @@ fn static_nodes() -> Vec { DocumentInputType::value("None", TaggedValue::None, false), DocumentInputType::value("Background", TaggedValue::ImageFrame(ImageFrame::empty()), true), DocumentInputType::value("Bounds", TaggedValue::ImageFrame(ImageFrame::empty()), true), - 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), + DocumentInputType::value("Trace", TaggedValue::BrushStrokes(Vec::new()), false), ], outputs: vec![DocumentOutputType { name: "Image", data_type: FrontendGraphDataType::Raster, }], - properties: node_properties::brush_node_properties, + properties: node_properties::no_properties, }, DocumentNodeType { name: "Extract Vector Points", diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 5221ffc8..fcc219b6 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -1,21 +1,17 @@ +use super::document_node_types::NodePropertiesContext; +use super::FrontendGraphDataType; use crate::messages::layout::utility_types::widget_prelude::*; - use crate::messages::prelude::*; -use document_legacy::layers::layer_info::LayerDataTypeDiscriminant; -use document_legacy::Operation; -use glam::{DVec2, IVec2}; use graph_craft::concrete; use graph_craft::document::value::TaggedValue; use graph_craft::document::{DocumentNode, NodeId, NodeInput}; use graphene_core::raster::{BlendMode, Color, ImageFrame, LuminanceCalculation, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice}; use graphene_core::text::Font; use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin}; - use graphene_core::{Cow, Type, TypeDescriptor}; -use super::document_node_types::NodePropertiesContext; -use super::FrontendGraphDataType; +use glam::{DVec2, IVec2}; pub fn string_properties(text: impl Into) -> Vec { let widget = WidgetHolder::text_widget(text); @@ -129,7 +125,7 @@ fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name widgets } -fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, x: &str, y: &str, mut assist: impl FnMut(&mut Vec)) -> LayoutGroup { +fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, x: &str, y: &str, unit: &str, mut assist: impl FnMut(&mut Vec)) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Vector, false); assist(&mut widgets); @@ -143,13 +139,13 @@ fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name WidgetHolder::unrelated_separator(), NumberInput::new(Some(vec2.x)) .label(x) - .unit(" px") + .unit(unit) .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), vec2.y)), node_id, index)) .widget_holder(), WidgetHolder::related_separator(), NumberInput::new(Some(vec2.y)) .label(y) - .unit(" px") + .unit(unit) .on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, input.value.unwrap())), node_id, index)) .widget_holder(), ]); @@ -165,14 +161,14 @@ fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name NumberInput::new(Some(vec2.x as f64)) .int() .label(x) - .unit(" px") + .unit(unit) .on_update(update_value(update_x, node_id, index)) .widget_holder(), WidgetHolder::related_separator(), NumberInput::new(Some(vec2.y as f64)) .int() .label(y) - .unit(" px") + .unit(unit) .on_update(update_value(update_y, node_id, index)) .widget_holder(), ]); @@ -707,16 +703,6 @@ 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 { - let color = color_widget(document_node, node_id, 7, "Color", ColorInput::default().allow_none(false), true); - - let size = number_widget(document_node, node_id, 4, "Diameter", NumberInput::default().min(1.).max(100.).unit(" px"), true); - let hardness = number_widget(document_node, node_id, 5, "Hardness", NumberInput::default().min(0.).max(100.).unit("%"), true); - let flow = number_widget(document_node, node_id, 6, "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 { 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); @@ -945,7 +931,7 @@ pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _cont add_blank_assist(widgets); } }; - let translation = vec2_widget(document_node, node_id, 1, "Translation", "X", "Y", translation_assist); + let translation = vec2_widget(document_node, node_id, 1, "Translation", "X", "Y", " px", translation_assist); let rotation = { let index = 2; @@ -972,7 +958,7 @@ pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _cont LayoutGroup::Row { widgets } }; - let scale = vec2_widget(document_node, node_id, 3, "Scale", "X", "Y", add_blank_assist); + let scale = vec2_widget(document_node, node_id, 3, "Scale", "W", "H", "x", add_blank_assist); vec![translation, rotation, scale] } @@ -1658,8 +1644,8 @@ pub fn layer_properties(document_node: &DocumentNode, node_id: NodeId, _context: ] } pub fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let location = vec2_widget(document_node, node_id, 1, "Location", "X", "Y", add_blank_assist); - let dimensions = vec2_widget(document_node, node_id, 2, "Dimensions", "W", "H", add_blank_assist); + let location = vec2_widget(document_node, node_id, 1, "Location", "X", "Y", " px", add_blank_assist); + let dimensions = vec2_widget(document_node, node_id, 2, "Dimensions", "W", "H", " px", add_blank_assist); let background = color_widget(document_node, node_id, 3, "Background", ColorInput::default().allow_none(false), true); vec![location, dimensions, background] } diff --git a/editor/src/messages/tool/tool_messages/brush_tool.rs b/editor/src/messages/tool/tool_messages/brush_tool.rs index 04667a42..7012afba 100644 --- a/editor/src/messages/tool/tool_messages/brush_tool.rs +++ b/editor/src/messages/tool/tool_messages/brush_tool.rs @@ -4,19 +4,22 @@ use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, 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::portfolio::document::node_graph::transform_utils::get_current_transform; use crate::messages::prelude::*; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType}; use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo}; +use document_legacy::layers::layer_layer::CachedOutputData; use document_legacy::LayerId; use graph_craft::document::value::TaggedValue; -use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork}; +use graph_craft::document::{NodeId, NodeInput, NodeNetwork}; use graphene_core::raster::ImageFrame; +use graphene_core::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle}; use graphene_core::Color; -use glam::DVec2; +use glam::DAffine2; use serde::{Deserialize, Serialize}; #[derive(Default)] @@ -30,6 +33,7 @@ pub struct BrushOptions { diameter: f64, hardness: f64, flow: f64, + spacing: f64, color: ToolColorOptions, } @@ -39,6 +43,7 @@ impl Default for BrushOptions { diameter: 40., hardness: 50., flow: 100., + spacing: 50., color: ToolColorOptions::default(), } } @@ -70,6 +75,7 @@ pub enum BrushToolMessageOptionsUpdate { Diameter(f64), Flow(f64), Hardness(f64), + Spacing(f64), WorkingColors(Option, Option), } @@ -117,6 +123,14 @@ impl PropertyHolder for BrushTool { .unit("%") .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Flow(number_input.value.unwrap())).into()) .widget_holder(), + WidgetHolder::related_separator(), + NumberInput::new(Some(self.options.spacing)) + .label("Spacing") + .min(1.) + .max(100.) + .unit("%") + .on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Spacing(number_input.value.unwrap())).into()) + .widget_holder(), ]; widgets.push(WidgetHolder::section_separator()); @@ -152,6 +166,7 @@ impl<'a> MessageHandler> for BrushTo BrushToolMessageOptionsUpdate::Diameter(diameter) => self.options.diameter = diameter, BrushToolMessageOptionsUpdate::Hardness(hardness) => self.options.hardness = hardness, BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow, + BrushToolMessageOptionsUpdate::Spacing(spacing) => self.options.spacing = spacing, BrushToolMessageOptionsUpdate::Color(color) => { self.options.color.custom_color = color; self.options.color.color_type = ToolColorType::Custom; @@ -206,39 +221,45 @@ impl ToolTransition for BrushTool { #[derive(Clone, Debug, Default)] struct BrushToolData { - points: Vec>, - path: Option>, + strokes: Vec, + layer_path: Vec, + node_path: Vec, + transform: DAffine2, } impl BrushToolData { - fn update_points(&self, responses: &mut VecDeque) { - if let Some(layer_path) = self.path.clone() { - let points = self.points.iter().flatten().cloned().collect(); - responses.add(NodeGraphMessage::SetQualifiedInputValue { - layer_path, - node_path: vec![0], - input_index: 3, - value: TaggedValue::VecDVec2(points), - }); + fn load_existing_strokes(&mut self, document: &DocumentMessageHandler) -> Option<&Vec> { + self.transform = DAffine2::IDENTITY; + if document.selected_layers().count() != 1 { + return None; } + self.layer_path = document.selected_layers().next()?.to_vec(); + let layer = document.document_legacy.layer(&self.layer_path).ok().and_then(|layer| layer.as_layer().ok())?; + let network = &layer.network; + for (node, _node_id) in network.primary_flow() { + if node.name == "Brush" { + let points_input = node.inputs.get(3)?; + let NodeInput::Value { tagged_value: TaggedValue::BrushStrokes(strokes), .. } = points_input else { + continue; + }; + self.strokes = strokes.clone(); + + return Some(&self.layer_path); + } else if node.name == "Transform" { + self.transform = get_current_transform(&node.inputs) * self.transform; + } + } + + self.transform = DAffine2::IDENTITY; + + matches!(layer.cached_output_data, CachedOutputData::BlobURL(_)).then_some(&self.layer_path) } - // fn update_image(&self, node_graph: &NodeGraphExecutor, responses: &mut VecDeque) { - // let Some(image) = node_graph.introspect_node(&[1]) else { return; }; - // let image: &ImageFrame = image.downcast_ref().unwrap(); - // self.set_image(image.clone(), responses) - // } - // - // fn set_image(&self, image_frame: ImageFrame, responses: &mut VecDeque) { - // if let Some(layer_path) = self.path.clone() { - // responses.add(NodeGraphMessage::SetQualifiedInputValue { - // layer_path, - // node_path: vec![0], - // input_index: 1, - // value: TaggedValue::ImageFrame(image_frame), - // }); - // } - // } + fn update_strokes(&self, brush_options: &BrushOptions, responses: &mut VecDeque) { + let layer = self.layer_path.clone(); + let strokes = self.strokes.clone(); + responses.add(GraphOperationMessage::Brush { layer, strokes }); + } } impl Fsm for BrushToolFsmState { @@ -255,78 +276,68 @@ impl Fsm for BrushToolFsmState { tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> Self { - use BrushToolFsmState::*; - use BrushToolMessage::*; - - let transform = document.document_legacy.root.transform; + let document_position = (document.document_legacy.root.transform).inverse().transform_point2(input.mouse.position); + let layer_position = tool_data.transform.inverse().transform_point2(document_position); if let ToolMessage::Brush(event) = event { match (self, event) { - (Ready, DragStart) => { + (BrushToolFsmState::Ready, BrushToolMessage::DragStart) => { responses.add(DocumentMessage::StartTransaction); - 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.set_image(image, responses); - if tool_data.points.is_empty() { - tool_data.points.push(points); - } - } else { + let layer_path = tool_data.load_existing_strokes(document); + let new_layer = layer_path.is_none(); + if new_layer { responses.add(DocumentMessage::DeselectAllLayers); - tool_data.path = Some(document.get_path_for_new_layer()); + tool_data.layer_path = document.get_path_for_new_layer(); } + let layer_position = tool_data.transform.inverse().transform_point2(document_position); + // TODO: Also scale it based on the input image ('Background' parameter). + // TODO: Resizing the input image results in a different brush size from the chosen diameter. + let layer_scale = 0.0001_f64 // Safety against division by zero + .max((tool_data.transform.matrix2 * glam::DVec2::X).length()) + .max((tool_data.transform.matrix2 * glam::DVec2::Y).length()); - let pos = transform.inverse().transform_point2(input.mouse.position); - - tool_data.points.push(vec![pos]); + // Start a new stroke with a single sample + tool_data.strokes.push(BrushStroke { + trace: vec![BrushInputSample { position: layer_position }], + style: BrushStyle { + color: tool_options.color.active_color().unwrap_or_default(), + diameter: tool_options.diameter / layer_scale, + hardness: tool_options.hardness, + flow: tool_options.flow, + spacing: tool_options.spacing, + }, + }); if new_layer { add_brush_render(tool_options, tool_data, responses); - } else { - //tool_data.update_image(node_graph, responses); - tool_data.update_points(responses); } + tool_data.update_strokes(tool_options, responses); - Drawing + BrushToolFsmState::Drawing } - (Drawing, PointerMove) => { - let pos = transform.inverse().transform_point2(input.mouse.position); - if tool_data.points.last().and_then(|x| x.last()) != Some(&pos) { - // Linear interpolation for when the mouse has moved a lot between frames - if let Some(&last_point) = tool_data.points.last().and_then(|x| x.last()) { - let distance = (last_point - pos).length(); - let extra_points = (distance / (tool_options.diameter / 2.)).floor() as usize; - tool_data - .points - .last_mut() - .unwrap() - .extend((0..extra_points).map(|i| last_point.lerp(pos, (i as f64 + 1.) / (extra_points as f64 + 1.)))); - } - - if let Some(x) = tool_data.points.last_mut() { - x.push(pos) - } + (BrushToolFsmState::Drawing, BrushToolMessage::PointerMove) => { + if let Some(stroke) = tool_data.strokes.last_mut() { + stroke.trace.push(BrushInputSample { position: layer_position }) } + tool_data.update_strokes(tool_options, responses); - tool_data.update_points(responses); - - Drawing + BrushToolFsmState::Drawing } - (Drawing, DragStop) | (Drawing, Abort) => { - if !tool_data.points.is_empty() { + + (BrushToolFsmState::Drawing, BrushToolMessage::DragStop) | (BrushToolFsmState::Drawing, BrushToolMessage::Abort) => { + if !tool_data.strokes.is_empty() { responses.add(DocumentMessage::CommitTransaction); } else { responses.add(DocumentMessage::AbortTransaction); } - tool_data.points.clear(); - tool_data.path = None; + tool_data.strokes.clear(); - Ready + BrushToolFsmState::Ready } - (_, WorkingColorChanged) => { + + (_, BrushToolMessage::WorkingColorChanged) => { responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::WorkingColors( Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color), @@ -342,7 +353,7 @@ impl Fsm for BrushToolFsmState { fn update_hints(&self, responses: &mut VecDeque) { let hint_data = match self { - BrushToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polyline")])]), + BrushToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Stroke")])]), BrushToolFsmState::Drawing => HintData(vec![]), }; @@ -355,55 +366,10 @@ impl Fsm for BrushToolFsmState { } fn add_brush_render(tool_options: &BrushOptions, data: &BrushToolData, responses: &mut VecDeque) { - let layer_path = data.path.clone().unwrap(); - - let brush_node = DocumentNode { - name: "Brush".to_string(), - inputs: vec![ - NodeInput::value(TaggedValue::None, false), - NodeInput::value(TaggedValue::ImageFrame(ImageFrame::empty()), true), - NodeInput::value(TaggedValue::ImageFrame(ImageFrame::empty()), true), - NodeInput::value(TaggedValue::VecDVec2(data.points.last().cloned().unwrap_or_default()), false), - // Diameter - NodeInput::value(TaggedValue::F64(tool_options.diameter), false), - // Hardness - NodeInput::value(TaggedValue::F64(tool_options.hardness), false), - // Flow - NodeInput::value(TaggedValue::F64(tool_options.flow), false), - // Color - NodeInput::value(TaggedValue::Color(tool_options.color.active_color().unwrap()), false), - ], - implementation: DocumentNodeImplementation::Unresolved("graphene_std::brush::BrushNode".into()), - metadata: graph_craft::document::DocumentNodeMetadata { position: (8, 4).into() }, - ..Default::default() - }; - // let monitor_node = DocumentNode { - // name: "Monitor".to_string(), - // implementation: DocumentNodeImplementation::Unresolved("graphene_std::memo::MonitorNode<_>".into()), - // ..Default::default() - // }; - let mut network = NodeNetwork::value_network(brush_node); - //network.push_node(monitor_node, true); - network.push_output_node(); - graph_modification_utils::new_custom_layer(network, layer_path, responses); -} - -fn load_existing_points(document: &DocumentMessageHandler) -> Option<(Vec, Vec)> { - if document.selected_layers().count() != 1 { - return None; + let mut network = NodeNetwork::default(); + let output_node = network.push_output_node(); + if let Some(node) = network.nodes.get_mut(&output_node) { + node.inputs.push(NodeInput::value(TaggedValue::ImageFrame(ImageFrame::empty()), true)) } - let layer_path = document.selected_layers().next()?.to_vec(); - let network = document.document_legacy.layer(&layer_path).ok().and_then(|layer| layer.as_layer_network().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(3)?; - let NodeInput::Value { - tagged_value: TaggedValue::VecDVec2(points), - .. - } = points_input else { - return None }; - - Some((layer_path, points.clone())) + graph_modification_utils::new_custom_layer(network, data.layer_path.clone(), responses); } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index d3d47d92..0ee52477 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -10,7 +10,6 @@ use document_legacy::{LayerId, Operation}; use graph_craft::document::value::TaggedValue; use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork}; use graph_craft::executor::Compiler; -use graph_craft::imaginate_input::*; use graph_craft::{concrete, Type, TypeDescriptor}; use graphene_core::raster::{Image, ImageFrame}; use graphene_core::renderer::{SvgSegment, SvgSegmentList}; diff --git a/node-graph/gcore/src/raster.rs b/node-graph/gcore/src/raster.rs index 842814ae..69837775 100644 --- a/node-graph/gcore/src/raster.rs +++ b/node-graph/gcore/src/raster.rs @@ -8,6 +8,7 @@ use glam::DVec2; pub use self::color::{Color, Luma}; pub mod adjustments; +pub mod bbox; #[cfg(not(target_arch = "spirv"))] pub mod brightness_contrast; pub mod color; diff --git a/node-graph/gcore/src/raster/bbox.rs b/node-graph/gcore/src/raster/bbox.rs new file mode 100644 index 00000000..182cb7f2 --- /dev/null +++ b/node-graph/gcore/src/raster/bbox.rs @@ -0,0 +1,86 @@ +use dyn_any::{DynAny, StaticType}; +use glam::{DAffine2, DVec2}; + +#[derive(Debug, Clone, DynAny)] +pub struct AxisAlignedBbox { + pub start: DVec2, + pub end: DVec2, +} + +impl AxisAlignedBbox { + pub const ZERO: Self = Self { start: DVec2::ZERO, end: DVec2::ZERO }; + + pub fn size(&self) -> DVec2 { + self.end - self.start + } + + pub fn to_transform(&self) -> DAffine2 { + DAffine2::from_translation(self.start) * DAffine2::from_scale(self.size()) + } + + pub fn contains(&self, point: DVec2) -> bool { + point.x >= self.start.x && point.x <= self.end.x && point.y >= self.start.y && point.y <= self.end.y + } + + pub fn intersects(&self, other: &AxisAlignedBbox) -> bool { + other.start.x <= self.end.x && other.end.x >= self.start.x && other.start.y <= self.end.y && other.end.y >= self.start.y + } + + pub fn union(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox { + AxisAlignedBbox { + start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)), + end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)), + } + } + pub fn union_non_empty(&self, other: &AxisAlignedBbox) -> Option { + match (self.size() == DVec2::ZERO, other.size() == DVec2::ZERO) { + (true, true) => None, + (true, _) => Some(other.clone()), + (_, true) => Some(self.clone()), + _ => Some(AxisAlignedBbox { + start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)), + end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)), + }), + } + } +} + +#[derive(Debug, Clone)] +pub struct Bbox { + pub top_left: DVec2, + pub top_right: DVec2, + pub bottom_left: DVec2, + pub bottom_right: DVec2, +} + +impl Bbox { + pub fn unit() -> Self { + Self { + top_left: DVec2::new(0., 1.), + top_right: DVec2::new(1., 1.), + bottom_left: DVec2::new(0., 0.), + bottom_right: DVec2::new(1., 0.), + } + } + + pub fn affine_transform(self, transform: DAffine2) -> Self { + Self { + top_left: transform.transform_point2(self.top_left), + top_right: transform.transform_point2(self.top_right), + bottom_left: transform.transform_point2(self.bottom_left), + bottom_right: transform.transform_point2(self.bottom_right), + } + } + + pub fn to_axis_aligned_bbox(&self) -> AxisAlignedBbox { + let start_x = self.top_left.x.min(self.top_right.x).min(self.bottom_left.x).min(self.bottom_right.x); + let start_y = self.top_left.y.min(self.top_right.y).min(self.bottom_left.y).min(self.bottom_right.y); + let end_x = self.top_left.x.max(self.top_right.x).max(self.bottom_left.x).max(self.bottom_right.x); + let end_y = self.top_left.y.max(self.top_right.y).max(self.bottom_left.y).max(self.bottom_right.y); + + AxisAlignedBbox { + start: DVec2::new(start_x, start_y), + end: DVec2::new(end_x, end_y), + } + } +} diff --git a/node-graph/gcore/src/raster/image.rs b/node-graph/gcore/src/raster/image.rs index 2793a8fc..6365a8cb 100644 --- a/node-graph/gcore/src/raster/image.rs +++ b/node-graph/gcore/src/raster/image.rs @@ -143,25 +143,47 @@ where let Image { width, height, data } = self; assert!(data.len() == width as usize * height as usize); - let mut result = Vec::with_capacity(data.len() * 4); + // Cache the last sRGB value we computed, speeds up fills. + let mut last_r = 0.; + let mut last_r_srgb = 0u8; + let mut last_g = 0.; + let mut last_g_srgb = 0u8; + let mut last_b = 0.; + let mut last_b_srgb = 0u8; + + let mut result = vec![0; data.len() * 4]; + let mut i = 0; for color in data { let a = color.a().to_f32(); - if a < 0.5 / 255.0 { - // This would map to fully transparent anyway, avoid expensive encoding. - result.push(0); - result.push(0); - result.push(0); - result.push(0); - } else { - let undo_premultiply = 1.0 / a; - let r = float_to_srgb_u8(color.r().to_f32() * undo_premultiply); - let g = float_to_srgb_u8(color.g().to_f32() * undo_premultiply); - let b = float_to_srgb_u8(color.b().to_f32() * undo_premultiply); - result.push(r); - result.push(g); - result.push(b); - result.push((a * 255.0 + 0.5) as u8); + // Smaller alpha values than this would map to fully transparent + // anyway, avoid expensive encoding. + if a >= 0.5 / 255. { + let undo_premultiply = 1. / a; + let r = color.r().to_f32() * undo_premultiply; + let g = color.g().to_f32() * undo_premultiply; + let b = color.b().to_f32() * undo_premultiply; + + // Compute new sRGB value if necessary. + if r != last_r { + last_r = r; + last_r_srgb = float_to_srgb_u8(r); + } + if g != last_g { + last_g = g; + last_g_srgb = float_to_srgb_u8(g); + } + if b != last_b { + last_b = b; + last_b_srgb = float_to_srgb_u8(b); + } + + result[i] = last_r_srgb; + result[i + 1] = last_g_srgb; + result[i + 2] = last_b_srgb; + result[i + 3] = (a * 255. + 0.5) as u8; } + + i += 4; } (result, width, height) diff --git a/node-graph/gcore/src/vector/brush_stroke.rs b/node-graph/gcore/src/vector/brush_stroke.rs new file mode 100644 index 00000000..4098873f --- /dev/null +++ b/node-graph/gcore/src/vector/brush_stroke.rs @@ -0,0 +1,109 @@ +use crate::raster::bbox::AxisAlignedBbox; +use crate::Color; + +use dyn_any::{DynAny, StaticType}; +use glam::DVec2; +use std::hash::{Hash, Hasher}; + +/// The style of a brush. +#[derive(Clone, Debug, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BrushStyle { + pub color: Color, + pub diameter: f64, + pub hardness: f64, + pub flow: f64, + pub spacing: f64, // Spacing as a fraction of the diameter. +} + +impl Default for BrushStyle { + fn default() -> Self { + Self { + color: Color::BLACK, + diameter: 40., + hardness: 50., + flow: 100., + spacing: 50., // Percentage of diameter. + } + } +} + +impl Hash for BrushStyle { + fn hash(&self, state: &mut H) { + self.color.hash(state); + self.diameter.to_bits().hash(state); + self.hardness.to_bits().hash(state); + self.flow.to_bits().hash(state); + } +} + +/// A single sample of brush parameters across the brush stroke. +#[derive(Clone, Debug, PartialEq, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BrushInputSample { + pub position: DVec2, + // Future work: pressure, stylus angle, etc. +} + +impl Hash for BrushInputSample { + fn hash(&self, state: &mut H) { + self.position.x.to_bits().hash(state); + self.position.y.to_bits().hash(state); + } +} + +/// The parameters for a single stroke brush. +#[derive(Clone, Debug, PartialEq, Hash, Default, DynAny)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BrushStroke { + pub style: BrushStyle, + pub trace: Vec, +} + +impl BrushStroke { + pub fn bounding_box(&self) -> AxisAlignedBbox { + let radius = self.style.diameter / 2.; + self.trace + .iter() + .map(|sample| AxisAlignedBbox { + start: sample.position + DVec2::new(-radius, -radius), + end: sample.position + DVec2::new(radius, radius), + }) + .reduce(|a, b| a.union(&b)) + .unwrap_or(AxisAlignedBbox::ZERO) + } + + pub fn compute_blit_points(&self) -> Vec { + // We always travel in a straight line towards the next user input, + // placing a blit point every time we travelled our spacing distance. + let spacing_dist = self.style.spacing / 100. * self.style.diameter; + + let Some(first_sample) = self.trace.first() else { return Vec::new(); }; + + let mut cur_pos = first_sample.position; + let mut result = vec![cur_pos]; + let mut dist_until_next_blit = spacing_dist; + for sample in &self.trace[1..] { + // Travel to the next sample. + let delta = sample.position - cur_pos; + let mut dist_left = delta.length(); + let unit_step = delta / dist_left; + + while dist_left >= dist_until_next_blit { + // Take a step to the next blit point. + cur_pos += dist_until_next_blit * unit_step; + dist_left -= dist_until_next_blit; + + // Blit. + result.push(cur_pos); + dist_until_next_blit = spacing_dist; + } + + // Take the partial step to land at the sample. + dist_until_next_blit -= dist_left; + cur_pos = sample.position; + } + + result + } +} diff --git a/node-graph/gcore/src/vector/mod.rs b/node-graph/gcore/src/vector/mod.rs index da93c9f5..ccf7ff6f 100644 --- a/node-graph/gcore/src/vector/mod.rs +++ b/node-graph/gcore/src/vector/mod.rs @@ -1,3 +1,4 @@ +pub mod brush_stroke; pub mod consts; pub mod generator_nodes; pub mod manipulator_group; diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 5c5fee9d..b9808886 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -54,7 +54,7 @@ pub enum TaggedValue { OptionalColor(Option), ManipulatorGroupIds(Vec), Font(graphene_core::text::Font), - VecDVec2(Vec), + BrushStrokes(Vec), Segments(Vec>), EditorApi(graphene_core::EditorApi<'static>), DocumentNode(DocumentNode), @@ -115,12 +115,7 @@ impl Hash for TaggedValue { Self::OptionalColor(color) => color.hash(state), Self::ManipulatorGroupIds(mirror) => mirror.hash(state), Self::Font(font) => font.hash(state), - Self::VecDVec2(vec_dvec2) => { - vec_dvec2.len().hash(state); - for dvec2 in vec_dvec2 { - dvec2.to_array().iter().for_each(|x| x.to_bits().hash(state)); - } - } + Self::BrushStrokes(brush_strokes) => brush_strokes.hash(state), Self::Segments(segments) => { for segment in segments { segment.hash(state) @@ -176,7 +171,7 @@ impl<'a> TaggedValue { TaggedValue::OptionalColor(x) => Box::new(x), TaggedValue::ManipulatorGroupIds(x) => Box::new(x), TaggedValue::Font(x) => Box::new(x), - TaggedValue::VecDVec2(x) => Box::new(x), + TaggedValue::BrushStrokes(x) => Box::new(x), TaggedValue::Segments(x) => Box::new(x), TaggedValue::EditorApi(x) => Box::new(x), TaggedValue::DocumentNode(x) => Box::new(x), @@ -239,7 +234,7 @@ impl<'a> TaggedValue { TaggedValue::OptionalColor(_) => concrete!(Option), TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec), TaggedValue::Font(_) => concrete!(graphene_core::text::Font), - TaggedValue::VecDVec2(_) => concrete!(Vec), + TaggedValue::BrushStrokes(_) => concrete!(Vec), TaggedValue::Segments(_) => concrete!(graphene_core::raster::IndexNode>>), TaggedValue::EditorApi(_) => concrete!(graphene_core::EditorApi), TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode), @@ -291,7 +286,7 @@ impl<'a> TaggedValue { x if x == TypeId::of::>() => Some(TaggedValue::OptionalColor(*downcast(input).unwrap())), x if x == TypeId::of::>() => Some(TaggedValue::ManipulatorGroupIds(*downcast(input).unwrap())), x if x == TypeId::of::() => Some(TaggedValue::Font(*downcast(input).unwrap())), - x if x == TypeId::of::>() => Some(TaggedValue::VecDVec2(*downcast(input).unwrap())), + x if x == TypeId::of::>() => Some(TaggedValue::BrushStrokes(*downcast(input).unwrap())), x if x == TypeId::of::>>>() => Some(TaggedValue::Segments(*downcast(input).unwrap())), x if x == TypeId::of::() => Some(TaggedValue::EditorApi(*downcast(input).unwrap())), x if x == TypeId::of::() => Some(TaggedValue::DocumentNode(*downcast(input).unwrap())), diff --git a/node-graph/gstd/src/brush.rs b/node-graph/gstd/src/brush.rs index 7b28d18f..10cf8873 100644 --- a/node-graph/gstd/src/brush.rs +++ b/node-graph/gstd/src/brush.rs @@ -1,7 +1,7 @@ use std::marker::PhantomData; use glam::{DAffine2, DVec2}; -use graphene_core::raster::{Alpha, Color, Pixel, Sample}; +use graphene_core::raster::{Alpha, Color, ImageFrame, Pixel, Sample}; use graphene_core::transform::{Transform, TransformMut}; use graphene_core::vector::VectorData; use graphene_core::Node; @@ -21,6 +21,22 @@ where iter.fold(initial, |a, x| lambda.eval((a, x))) } +#[derive(Clone, Debug, PartialEq)] +pub struct ChainApplyNode { + pub value: Value, +} + +#[node_fn(ChainApplyNode)] +async fn chain_apply(iter: I, mut value: T) -> T +where + I::Item: for<'a> Node<'a, T, Output = T>, +{ + for lambda in iter { + value = lambda.eval(value); + } + value +} + #[derive(Clone, Debug, PartialEq)] pub struct IntoIterNode { _t: PhantomData, @@ -100,7 +116,7 @@ pub struct EraseNode { #[node_fn(EraseNode)] fn erase(input: (Color, Color), flow: f64) -> Color { let (input, brush) = input; - let alpha = input.a() * (1.0 - flow as f32 * brush.a()); + let alpha = input.a() * (1. - flow as f32 * brush.a()); Color::from_unassociated_alpha(input.r(), input.g(), input.b(), alpha) } @@ -134,6 +150,54 @@ fn translate_node(offset: DVec2, mut translatable: Data) -> translatable } +#[derive(Debug, Clone, Copy)] +pub struct BlitNode { + texture: Texture, + positions: Positions, + blend_mode: BlendFn, + _p: PhantomData

, +} + +#[node_fn(BlitNode<_P>)] +fn blit_node<_P: Alpha + Pixel + std::fmt::Debug, BlendFn>(mut target: ImageFrame<_P>, texture: ImageFrame<_P>, positions: Vec, blend_mode: BlendFn) -> ImageFrame<_P> +where + BlendFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>, +{ + for position in positions { + let target_size = DVec2::new(target.image.width as f64, target.image.height as f64); + let texture_size = DVec2::new(texture.image.width as f64, texture.image.height as f64); + let document_to_target = target.transform.inverse(); + let start = document_to_target.transform_point2(position) * target_size - texture_size / 2.; + let stop = start + texture_size; + + // Half-open integer ranges [start, stop). + let clamp_start = start.clamp(DVec2::ZERO, target_size).as_uvec2(); + let clamp_stop = stop.clamp(DVec2::ZERO, target_size).as_uvec2(); + + let blit_area_offset = (clamp_start.as_dvec2() - start).as_uvec2().min(texture_size.as_uvec2()); + let blit_area_dimensions = (clamp_stop - clamp_start).min(texture_size.as_uvec2() - blit_area_offset); + + // Tight blitting loop. Eagerly assert bounds to hopefully eliminate bounds check inside loop. + let texture_index = |x: u32, y: u32| -> usize { (y as usize * texture.image.width as usize) + (x as usize) }; + let target_index = |x: u32, y: u32| -> usize { (y as usize * target.image.width as usize) + (x as usize) }; + + let max_y = (blit_area_offset.y + blit_area_dimensions.y).saturating_sub(1); + let max_x = (blit_area_offset.x + blit_area_dimensions.x).saturating_sub(1); + assert!(texture_index(max_x, max_y) < texture.image.data.len()); + assert!(target_index(max_x, max_y) < target.image.data.len()); + + for y in blit_area_offset.y..blit_area_offset.y + blit_area_dimensions.y { + for x in blit_area_offset.x..blit_area_offset.x + blit_area_dimensions.x { + let src_pixel = texture.image.data[texture_index(x, y)]; + let dst_pixel = &mut target.image.data[target_index(x + clamp_start.x, y + clamp_start.y)]; + *dst_pixel = blend_mode.eval((src_pixel, *dst_pixel)); + } + } + } + + target +} + #[cfg(test)] mod test { use super::*; @@ -152,10 +216,10 @@ mod test { fn test_translate_node() { let image = Image::new(10, 10, Color::TRANSPARENT); let mut image = ImageFrame { image, transform: DAffine2::IDENTITY }; - image.translate(DVec2::new(1.0, 2.0)); + image.translate(DVec2::new(1., 2.)); let translate_node = TranslateNode::new(ClonedNode::new(image)); - let image = translate_node.eval(DVec2::new(1.0, 2.0)); - assert_eq!(image.transform(), DAffine2::from_translation(DVec2::new(2.0, 4.0))); + let image = translate_node.eval(DVec2::new(1., 2.)); + assert_eq!(image.transform(), DAffine2::from_translation(DVec2::new(2., 4.))); } #[test] @@ -177,9 +241,9 @@ mod test { #[test] fn test_brush() { - let brush_texture_node = BrushStampGeneratorNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(1.0), ClonedNode::new(1.0)); + let brush_texture_node = BrushStampGeneratorNode::new(ClonedNode::new(Color::BLACK), ClonedNode::new(1.), ClonedNode::new(1.)); let image = brush_texture_node.eval(20.); - let trace = vec![DVec2::new(0.0, 0.0), DVec2::new(10.0, 0.0)]; + let trace = vec![DVec2::new(0., 0.), DVec2::new(10., 0.)]; let trace = ClonedNode::new(trace.into_iter()); let translate_node = TranslateNode::new(ClonedNode::new(image)); let frames = MapNode::new(ValueNode::new(translate_node)); @@ -189,7 +253,7 @@ mod test { let background_bounds = background_bounds.eval(frames.clone().into_iter()); let background_bounds = ClonedNode::new(background_bounds.unwrap().to_transform()); let background_image = background_bounds.then(EmptyImageNode::new(ClonedNode::new(Color::TRANSPARENT))); - let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(BlendMode::Normal), ClonedNode::new(1.0)); + let blend_node = graphene_core::raster::BlendNode::new(ClonedNode::new(BlendMode::Normal), ClonedNode::new(1.)); let final_image = ReduceNode::new(background_image, ValueNode::new(BlendImageTupleNode::new(ValueNode::new(blend_node)))); let final_image = final_image.eval(frames.into_iter()); assert_eq!(final_image.image.height, 20); diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 45be24ed..ac0a3997 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -3,6 +3,7 @@ use glam::{DAffine2, DVec2}; use graphene_core::raster::{Alpha, BlendMode, BlendNode, Image, ImageFrame, Linear, LinearChannel, Luminance, Pixel, RGBMut, Raster, RasterMut, RedGreenBlue, Sample}; use graphene_core::transform::Transform; +use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox}; use graphene_core::value::CopiedNode; use graphene_core::{Color, Node}; @@ -95,85 +96,6 @@ where image } -#[derive(Debug, Clone, DynAny)] -pub struct AxisAlignedBbox { - start: DVec2, - end: DVec2, -} - -impl AxisAlignedBbox { - pub fn size(&self) -> DVec2 { - self.end - self.start - } - - pub fn to_transform(&self) -> DAffine2 { - DAffine2::from_translation(self.start) * DAffine2::from_scale(self.size()) - } - - pub fn contains(&self, point: DVec2) -> bool { - point.x >= self.start.x && point.x <= self.end.x && point.y >= self.start.y && point.y <= self.end.y - } - - pub fn intersects(&self, other: &AxisAlignedBbox) -> bool { - other.start.x <= self.end.x && other.end.x >= self.start.x && other.start.y <= self.end.y && other.end.y >= self.start.y - } - - pub fn union(&self, other: &AxisAlignedBbox) -> AxisAlignedBbox { - AxisAlignedBbox { - start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)), - end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)), - } - } - pub fn union_non_empty(&self, other: &AxisAlignedBbox) -> Option { - match (self.size() == DVec2::ZERO, other.size() == DVec2::ZERO) { - (true, true) => None, - (true, _) => Some(other.clone()), - (_, true) => Some(self.clone()), - _ => Some(AxisAlignedBbox { - start: DVec2::new(self.start.x.min(other.start.x), self.start.y.min(other.start.y)), - end: DVec2::new(self.end.x.max(other.end.x), self.end.y.max(other.end.y)), - }), - } - } -} - -#[derive(Debug, Clone)] -struct Bbox { - top_left: DVec2, - top_right: DVec2, - bottom_left: DVec2, - bottom_right: DVec2, -} - -impl Bbox { - fn axis_aligned_bbox(&self) -> AxisAlignedBbox { - let start_x = self.top_left.x.min(self.top_right.x).min(self.bottom_left.x).min(self.bottom_right.x); - let start_y = self.top_left.y.min(self.top_right.y).min(self.bottom_left.y).min(self.bottom_right.y); - let end_x = self.top_left.x.max(self.top_right.x).max(self.bottom_left.x).max(self.bottom_right.x); - let end_y = self.top_left.y.max(self.top_right.y).max(self.bottom_left.y).max(self.bottom_right.y); - - AxisAlignedBbox { - start: DVec2::new(start_x, start_y), - end: DVec2::new(end_x, end_y), - } - } -} - -fn compute_transformed_bounding_box(transform: DAffine2) -> Bbox { - let top_left = DVec2::new(0., 1.); - let top_right = DVec2::new(1., 1.); - let bottom_left = DVec2::new(0., 0.); - let bottom_right = DVec2::new(1., 0.); - let transform = |p| transform.transform_point2(p); - - Bbox { - top_left: transform(top_left), - top_right: transform(top_right), - bottom_left: transform(bottom_left), - bottom_right: transform(bottom_right), - } -} - #[derive(Debug, Clone, Copy)] pub struct InsertChannelNode { insertion: Insertion, @@ -325,8 +247,8 @@ fn blend_new_image<_P: Alpha + Pixel + Debug, MapFn, Frame: Sample + where MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>, { - let foreground_aabb = compute_transformed_bounding_box(foreground.transform()).axis_aligned_bbox(); - let background_aabb = compute_transformed_bounding_box(background.transform()).axis_aligned_bbox(); + let foreground_aabb = Bbox::unit().affine_transform(foreground.transform()).to_axis_aligned_bbox(); + let background_aabb = Bbox::unit().affine_transform(background.transform()).to_axis_aligned_bbox(); let Some(aabb) = foreground_aabb.union_non_empty(&background_aabb) else {return ImageFrame::empty()}; @@ -363,7 +285,7 @@ where let bg_to_fg = background.transform() * DAffine2::from_scale(1. / background_size); // Footprint of the foreground image (0,0) (1, 1) in the background image space - let bg_aabb = compute_transformed_bounding_box(background.transform().inverse() * foreground.transform()).axis_aligned_bbox(); + let bg_aabb = Bbox::unit().affine_transform(background.transform().inverse() * foreground.transform()).to_axis_aligned_bbox(); // Clamp the foreground image to the background image let start = (bg_aabb.start * background_size).max(DVec2::ZERO).as_uvec2(); @@ -393,8 +315,8 @@ pub struct ExtendImageNode { #[node_macro::node_fn(ExtendImageNode)] fn extend_image_node(foreground: ImageFrame, background: ImageFrame) -> ImageFrame { - let foreground_aabb = compute_transformed_bounding_box(foreground.transform()).axis_aligned_bbox(); - let background_aabb = compute_transformed_bounding_box(background.transform()).axis_aligned_bbox(); + let foreground_aabb = Bbox::unit().affine_transform(foreground.transform()).to_axis_aligned_bbox(); + let background_aabb = Bbox::unit().affine_transform(background.transform()).to_axis_aligned_bbox(); if foreground_aabb.contains(background_aabb.start) && foreground_aabb.contains(background_aabb.end) { return foreground; @@ -412,7 +334,7 @@ pub struct MergeBoundingBoxNode { fn merge_bounding_box_node<_Data: Transform>(input: (Option, _Data)) -> Option { let (initial_aabb, data) = input; - let snd_aabb = compute_transformed_bounding_box(data.transform()).axis_aligned_bbox(); + let snd_aabb = Bbox::unit().affine_transform(data.transform()).to_axis_aligned_bbox(); if let Some(fst_aabb) = initial_aabb { fst_aabb.union_non_empty(&snd_aabb) diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index 51404a5b..3f251bdb 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -1,32 +1,27 @@ -use glam::{DAffine2, DVec2}; - use graph_craft::document::DocumentNode; +use graph_craft::proto::{NodeConstructor, TypeErasedPinned}; use graphene_core::ops::IdNode; -use graphene_core::vector::VectorData; -use once_cell::sync::Lazy; -use std::collections::HashMap; - +use graphene_core::quantization::QuantizationChannels; +use graphene_core::raster::bbox::AxisAlignedBbox; use graphene_core::raster::color::Color; use graphene_core::structural::Then; use graphene_core::value::{ClonedNode, CopiedNode, ValueNode}; -use graphene_core::{fn_type, raster::*}; -use graphene_core::{Node, NodeIO, NodeIOTypes}; -use graphene_std::brush::*; -use graphene_std::raster::*; - -use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyInRefNode, DynAnyNode, FutureWrapperNode, IntoTypeErasedNode, TypeErasedPinnedRef}; - -use graphene_core::{Cow, NodeIdentifier, Type, TypeDescriptor}; - -use graph_craft::proto::{NodeConstructor, TypeErasedPinned}; - +use graphene_core::vector::brush_stroke::BrushStroke; +use graphene_core::vector::VectorData; use graphene_core::{concrete, generic, value_fn}; +use graphene_core::{fn_type, raster::*}; +use graphene_core::{Cow, NodeIdentifier, Type, TypeDescriptor}; +use graphene_core::{Node, NodeIO, NodeIOTypes}; +use graphene_std::any::{ComposeTypeErased, DowncastBothNode, DynAnyInRefNode, DynAnyNode, FutureWrapperNode, IntoTypeErasedNode, TypeErasedPinnedRef}; +use graphene_std::brush::*; use graphene_std::memo::{CacheNode, LetNode}; use graphene_std::raster::BlendImageTupleNode; +use graphene_std::raster::*; use dyn_any::StaticType; - -use graphene_core::quantization::QuantizationChannels; +use glam::{DAffine2, DVec2}; +use once_cell::sync::Lazy; +use std::collections::HashMap; macro_rules! construct_node { ($args: ident, $path:ty, [$($type:tt),*]) => { async move { @@ -242,7 +237,7 @@ fn node_registry() -> HashMap, input: &Vec, params: []), + register_node!(graphene_std::brush::IntoIterNode<_>, input: &Vec, params: []), vec![( NodeIdentifier::new("graphene_std::brush::BrushNode"), |args| { @@ -253,55 +248,48 @@ fn node_registry() -> HashMap> = DowncastBothNode::new(args[0]); let bounds: DowncastBothNode<(), ImageFrame> = DowncastBothNode::new(args[1]); - let trace: DowncastBothNode<(), Vec> = DowncastBothNode::new(args[2]); - let diameter: DowncastBothNode<(), f64> = DowncastBothNode::new(args[3]); - let hardness: DowncastBothNode<(), f64> = DowncastBothNode::new(args[4]); - let flow: DowncastBothNode<(), f64> = DowncastBothNode::new(args[5]); - let color: DowncastBothNode<(), Color> = DowncastBothNode::new(args[6]); + let strokes: DowncastBothNode<(), Vec> = DowncastBothNode::new(args[2]); - let stamp = BrushStampGeneratorNode::new(CopiedNode::new(color.eval(()).await), CopiedNode::new(hardness.eval(()).await), CopiedNode::new(flow.eval(()).await)); - let stamp = stamp.eval(diameter.eval(()).await); - - let frames = TranslateNode::new(CopiedNode::new(stamp)); - let frames = MapNode::new(ValueNode::new(frames)); - let frames = frames.eval(trace.eval(()).await.into_iter()).collect::>(); - - let background_bounds = ReduceNode::new(ClonedNode::new(None), ValueNode::new(MergeBoundingBoxNode::new())); - let background_bounds = background_bounds.eval(frames.clone().into_iter()); - let background_bounds = MergeBoundingBoxNode::new().eval((background_bounds, image.eval(()).await)); - let mut background_bounds = CopiedNode::new(background_bounds.unwrap().to_transform()); + let strokes = strokes.eval(()).await; + let bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO); + let mut background_bounds = CopiedNode::new(bbox.to_transform()); let bounds_transform = bounds.eval(()).await.transform; if bounds_transform != DAffine2::ZERO { background_bounds = CopiedNode::new(bounds_transform); } - let background_image = background_bounds.then(EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT))); - let blend_node = graphene_core::raster::BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.)); + let blank_image = background_bounds.then(EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT))); + let background = image.and_then(ExtendImageNode::new(blank_image)); - let background = ExtendImageNode::new(background_image); - let background_image = image.and_then(background); + let mut blits = Vec::new(); + for stroke in strokes { + let stamp = BrushStampGeneratorNode::new(CopiedNode::new(stroke.style.color), CopiedNode::new(stroke.style.hardness), CopiedNode::new(stroke.style.flow)); + let stamp = stamp.eval(stroke.style.diameter); - let final_image = ReduceNode::new(ClonedNode::new(background_image.eval(()).await), ValueNode::new(BlendImageTupleNode::new(ValueNode::new(blend_node)))); - let final_image = ClonedNode::new(frames.into_iter()).then(final_image); + let transform = DAffine2::from_scale_angle_translation(DVec2::splat(stroke.style.diameter), 0., -DVec2::splat(stroke.style.diameter / 2.0)); + let blank_texture = EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(transform); - let final_image = FutureWrapperNode::new(final_image); - let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(final_image)); - any.into_type_erased() + let blend_params = graphene_core::raster::BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.)); + let blend_executor = BlendImageTupleNode::new(ValueNode::new(blend_params)); + let texture = blend_executor.eval((blank_texture, stamp)); + + let translations: Vec<_> = stroke.compute_blit_points().into_iter().collect(); + let blit_node = BlitNode::new(ClonedNode::new(texture), ClonedNode::new(translations), ClonedNode::new(blend_params)); + blits.push(blit_node); + } + + let all_blits = ChainApplyNode::new(background); + let node = ClonedNode::new(blits.into_iter()).then(all_blits); + + let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(node)); + Box::pin(any) as TypeErasedPinned }) }, NodeIOTypes::new( concrete!(()), concrete!(ImageFrame), - vec![ - value_fn!(ImageFrame), - value_fn!(ImageFrame), - value_fn!(Vec), - value_fn!(f64), - value_fn!(f64), - value_fn!(f64), - value_fn!(Color), - ], + vec![value_fn!(ImageFrame), value_fn!(ImageFrame), value_fn!(Vec)], ), )], vec![(