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 <keavon@keavon.com>
This commit is contained in:
parent
0586d52f3a
commit
7148b199ec
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
|
|
||||||
use graphene_core::uuid::ManipulatorGroupId;
|
use graphene_core::uuid::ManipulatorGroupId;
|
||||||
|
use graphene_core::vector::brush_stroke::BrushStroke;
|
||||||
use graphene_core::vector::style::{Fill, Stroke};
|
use graphene_core::vector::style::{Fill, Stroke};
|
||||||
use graphene_core::vector::ManipulatorPointId;
|
use graphene_core::vector::ManipulatorPointId;
|
||||||
|
|
||||||
|
|
@ -46,6 +47,10 @@ pub enum GraphOperationMessage {
|
||||||
layer: LayerIdentifier,
|
layer: LayerIdentifier,
|
||||||
modification: VectorDataModification,
|
modification: VectorDataModification,
|
||||||
},
|
},
|
||||||
|
Brush {
|
||||||
|
layer: LayerIdentifier,
|
||||||
|
strokes: Vec<BrushStroke>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
#[derive(PartialEq, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@ use document_legacy::document::Document;
|
||||||
use document_legacy::{LayerId, Operation};
|
use document_legacy::{LayerId, Operation};
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork};
|
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 graphene_core::vector::style::{Fill, FillType, Stroke};
|
||||||
use transform_utils::LayerBounds;
|
use transform_utils::LayerBounds;
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
|
|
||||||
mod transform_utils;
|
pub mod transform_utils;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct GraphOperationMessageHandler;
|
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]);
|
self.update_bounds([old_bounds_min, old_bounds_max], [new_bounds_min, new_bounds_max]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn brush_modify(&mut self, strokes: Vec<BrushStroke>) {
|
||||||
|
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<GraphOperationMessage, (&mut Document, &mut NodeGraphMessageHandler)> for GraphOperationMessageHandler {
|
impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessageHandler)> for GraphOperationMessageHandler {
|
||||||
|
|
@ -244,7 +255,6 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
|
||||||
responses.add(Operation::SetLayerStroke { path: layer, stroke });
|
responses.add(Operation::SetLayerStroke { path: layer, stroke });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GraphOperationMessage::TransformChange {
|
GraphOperationMessage::TransformChange {
|
||||||
layer,
|
layer,
|
||||||
transform,
|
transform,
|
||||||
|
|
@ -298,12 +308,16 @@ impl MessageHandler<GraphOperationMessage, (&mut Document, &mut NodeGraphMessage
|
||||||
let pivot = pivot.into();
|
let pivot = pivot.into();
|
||||||
responses.add(Operation::SetPivot { layer_path: layer, pivot });
|
responses.add(Operation::SetPivot { layer_path: layer, pivot });
|
||||||
}
|
}
|
||||||
|
|
||||||
GraphOperationMessage::Vector { layer, modification } => {
|
GraphOperationMessage::Vector { layer, modification } => {
|
||||||
if let Some(mut modify_inputs) = ModifyInputsContext::new(&layer, document, node_graph, responses) {
|
if let Some(mut modify_inputs) = ModifyInputsContext::new(&layer, document, node_graph, responses) {
|
||||||
modify_inputs.vector_modify(modification);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -639,17 +639,13 @@ fn static_nodes() -> Vec<DocumentNodeType> {
|
||||||
DocumentInputType::value("None", TaggedValue::None, false),
|
DocumentInputType::value("None", TaggedValue::None, false),
|
||||||
DocumentInputType::value("Background", TaggedValue::ImageFrame(ImageFrame::empty()), true),
|
DocumentInputType::value("Background", TaggedValue::ImageFrame(ImageFrame::empty()), true),
|
||||||
DocumentInputType::value("Bounds", 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("Trace", TaggedValue::BrushStrokes(Vec::new()), false),
|
||||||
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 {
|
outputs: vec![DocumentOutputType {
|
||||||
name: "Image",
|
name: "Image",
|
||||||
data_type: FrontendGraphDataType::Raster,
|
data_type: FrontendGraphDataType::Raster,
|
||||||
}],
|
}],
|
||||||
properties: node_properties::brush_node_properties,
|
properties: node_properties::no_properties,
|
||||||
},
|
},
|
||||||
DocumentNodeType {
|
DocumentNodeType {
|
||||||
name: "Extract Vector Points",
|
name: "Extract Vector Points",
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,17 @@
|
||||||
|
use super::document_node_types::NodePropertiesContext;
|
||||||
|
use super::FrontendGraphDataType;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
|
|
||||||
use crate::messages::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::concrete;
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
|
use graph_craft::document::{DocumentNode, NodeId, NodeInput};
|
||||||
use graphene_core::raster::{BlendMode, Color, ImageFrame, LuminanceCalculation, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice};
|
use graphene_core::raster::{BlendMode, Color, ImageFrame, LuminanceCalculation, RedGreenBlue, RelativeAbsolute, SelectiveColorChoice};
|
||||||
use graphene_core::text::Font;
|
use graphene_core::text::Font;
|
||||||
use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin};
|
use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin};
|
||||||
|
|
||||||
use graphene_core::{Cow, Type, TypeDescriptor};
|
use graphene_core::{Cow, Type, TypeDescriptor};
|
||||||
|
|
||||||
use super::document_node_types::NodePropertiesContext;
|
use glam::{DVec2, IVec2};
|
||||||
use super::FrontendGraphDataType;
|
|
||||||
|
|
||||||
pub fn string_properties(text: impl Into<String>) -> Vec<LayoutGroup> {
|
pub fn string_properties(text: impl Into<String>) -> Vec<LayoutGroup> {
|
||||||
let widget = WidgetHolder::text_widget(text);
|
let widget = WidgetHolder::text_widget(text);
|
||||||
|
|
@ -129,7 +125,7 @@ fn bool_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name
|
||||||
widgets
|
widgets
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, x: &str, y: &str, mut assist: impl FnMut(&mut Vec<WidgetHolder>)) -> 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<WidgetHolder>)) -> LayoutGroup {
|
||||||
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Vector, false);
|
let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Vector, false);
|
||||||
|
|
||||||
assist(&mut widgets);
|
assist(&mut widgets);
|
||||||
|
|
@ -143,13 +139,13 @@ fn vec2_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name
|
||||||
WidgetHolder::unrelated_separator(),
|
WidgetHolder::unrelated_separator(),
|
||||||
NumberInput::new(Some(vec2.x))
|
NumberInput::new(Some(vec2.x))
|
||||||
.label(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))
|
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(input.value.unwrap(), vec2.y)), node_id, index))
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
WidgetHolder::related_separator(),
|
WidgetHolder::related_separator(),
|
||||||
NumberInput::new(Some(vec2.y))
|
NumberInput::new(Some(vec2.y))
|
||||||
.label(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))
|
.on_update(update_value(move |input: &NumberInput| TaggedValue::DVec2(DVec2::new(vec2.x, input.value.unwrap())), node_id, index))
|
||||||
.widget_holder(),
|
.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))
|
NumberInput::new(Some(vec2.x as f64))
|
||||||
.int()
|
.int()
|
||||||
.label(x)
|
.label(x)
|
||||||
.unit(" px")
|
.unit(unit)
|
||||||
.on_update(update_value(update_x, node_id, index))
|
.on_update(update_value(update_x, node_id, index))
|
||||||
.widget_holder(),
|
.widget_holder(),
|
||||||
WidgetHolder::related_separator(),
|
WidgetHolder::related_separator(),
|
||||||
NumberInput::new(Some(vec2.y as f64))
|
NumberInput::new(Some(vec2.y as f64))
|
||||||
.int()
|
.int()
|
||||||
.label(y)
|
.label(y)
|
||||||
.unit(" px")
|
.unit(unit)
|
||||||
.on_update(update_value(update_y, node_id, index))
|
.on_update(update_value(update_y, node_id, index))
|
||||||
.widget_holder(),
|
.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 }]
|
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, 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<LayoutGroup> {
|
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_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);
|
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);
|
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 rotation = {
|
||||||
let index = 2;
|
let index = 2;
|
||||||
|
|
@ -972,7 +958,7 @@ pub fn transform_properties(document_node: &DocumentNode, node_id: NodeId, _cont
|
||||||
LayoutGroup::Row { widgets }
|
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]
|
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<LayoutGroup> {
|
pub fn artboard_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
let location = vec2_widget(document_node, node_id, 1, "Location", "X", "Y", 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", 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);
|
let background = color_widget(document_node, node_id, 3, "Background", ColorInput::default().allow_none(false), true);
|
||||||
vec![location, dimensions, background]
|
vec![location, dimensions, background]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::misc::LayoutTarget;
|
||||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||||
use crate::messages::layout::utility_types::widgets::input_widgets::NumberInput;
|
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::prelude::*;
|
||||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
|
||||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
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::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||||
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
use crate::messages::tool::utility_types::{HintData, HintGroup, HintInfo};
|
||||||
|
|
||||||
|
use document_legacy::layers::layer_layer::CachedOutputData;
|
||||||
use document_legacy::LayerId;
|
use document_legacy::LayerId;
|
||||||
use graph_craft::document::value::TaggedValue;
|
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::raster::ImageFrame;
|
||||||
|
use graphene_core::vector::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
|
||||||
use graphene_core::Color;
|
use graphene_core::Color;
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DAffine2;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -30,6 +33,7 @@ pub struct BrushOptions {
|
||||||
diameter: f64,
|
diameter: f64,
|
||||||
hardness: f64,
|
hardness: f64,
|
||||||
flow: f64,
|
flow: f64,
|
||||||
|
spacing: f64,
|
||||||
color: ToolColorOptions,
|
color: ToolColorOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +43,7 @@ impl Default for BrushOptions {
|
||||||
diameter: 40.,
|
diameter: 40.,
|
||||||
hardness: 50.,
|
hardness: 50.,
|
||||||
flow: 100.,
|
flow: 100.,
|
||||||
|
spacing: 50.,
|
||||||
color: ToolColorOptions::default(),
|
color: ToolColorOptions::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +75,7 @@ pub enum BrushToolMessageOptionsUpdate {
|
||||||
Diameter(f64),
|
Diameter(f64),
|
||||||
Flow(f64),
|
Flow(f64),
|
||||||
Hardness(f64),
|
Hardness(f64),
|
||||||
|
Spacing(f64),
|
||||||
WorkingColors(Option<Color>, Option<Color>),
|
WorkingColors(Option<Color>, Option<Color>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,6 +123,14 @@ impl PropertyHolder for BrushTool {
|
||||||
.unit("%")
|
.unit("%")
|
||||||
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Flow(number_input.value.unwrap())).into())
|
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::Flow(number_input.value.unwrap())).into())
|
||||||
.widget_holder(),
|
.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());
|
widgets.push(WidgetHolder::section_separator());
|
||||||
|
|
@ -152,6 +166,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for BrushTo
|
||||||
BrushToolMessageOptionsUpdate::Diameter(diameter) => self.options.diameter = diameter,
|
BrushToolMessageOptionsUpdate::Diameter(diameter) => self.options.diameter = diameter,
|
||||||
BrushToolMessageOptionsUpdate::Hardness(hardness) => self.options.hardness = hardness,
|
BrushToolMessageOptionsUpdate::Hardness(hardness) => self.options.hardness = hardness,
|
||||||
BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow,
|
BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow,
|
||||||
|
BrushToolMessageOptionsUpdate::Spacing(spacing) => self.options.spacing = spacing,
|
||||||
BrushToolMessageOptionsUpdate::Color(color) => {
|
BrushToolMessageOptionsUpdate::Color(color) => {
|
||||||
self.options.color.custom_color = color;
|
self.options.color.custom_color = color;
|
||||||
self.options.color.color_type = ToolColorType::Custom;
|
self.options.color.color_type = ToolColorType::Custom;
|
||||||
|
|
@ -206,39 +221,45 @@ impl ToolTransition for BrushTool {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
struct BrushToolData {
|
struct BrushToolData {
|
||||||
points: Vec<Vec<DVec2>>,
|
strokes: Vec<BrushStroke>,
|
||||||
path: Option<Vec<LayerId>>,
|
layer_path: Vec<LayerId>,
|
||||||
|
node_path: Vec<NodeId>,
|
||||||
|
transform: DAffine2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrushToolData {
|
impl BrushToolData {
|
||||||
fn update_points(&self, responses: &mut VecDeque<Message>) {
|
fn load_existing_strokes(&mut self, document: &DocumentMessageHandler) -> Option<&Vec<LayerId>> {
|
||||||
if let Some(layer_path) = self.path.clone() {
|
self.transform = DAffine2::IDENTITY;
|
||||||
let points = self.points.iter().flatten().cloned().collect();
|
if document.selected_layers().count() != 1 {
|
||||||
responses.add(NodeGraphMessage::SetQualifiedInputValue {
|
return None;
|
||||||
layer_path,
|
|
||||||
node_path: vec![0],
|
|
||||||
input_index: 3,
|
|
||||||
value: TaggedValue::VecDVec2(points),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
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<Message>) {
|
fn update_strokes(&self, brush_options: &BrushOptions, responses: &mut VecDeque<Message>) {
|
||||||
// let Some(image) = node_graph.introspect_node(&[1]) else { return; };
|
let layer = self.layer_path.clone();
|
||||||
// let image: &ImageFrame<Color> = image.downcast_ref().unwrap();
|
let strokes = self.strokes.clone();
|
||||||
// self.set_image(image.clone(), responses)
|
responses.add(GraphOperationMessage::Brush { layer, strokes });
|
||||||
// }
|
}
|
||||||
//
|
|
||||||
// fn set_image(&self, image_frame: ImageFrame<Color>, 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::ImageFrame(image_frame),
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fsm for BrushToolFsmState {
|
impl Fsm for BrushToolFsmState {
|
||||||
|
|
@ -255,78 +276,68 @@ impl Fsm for BrushToolFsmState {
|
||||||
tool_options: &Self::ToolOptions,
|
tool_options: &Self::ToolOptions,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
use BrushToolFsmState::*;
|
let document_position = (document.document_legacy.root.transform).inverse().transform_point2(input.mouse.position);
|
||||||
use BrushToolMessage::*;
|
let layer_position = tool_data.transform.inverse().transform_point2(document_position);
|
||||||
|
|
||||||
let transform = document.document_legacy.root.transform;
|
|
||||||
|
|
||||||
if let ToolMessage::Brush(event) = event {
|
if let ToolMessage::Brush(event) = event {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(Ready, DragStart) => {
|
(BrushToolFsmState::Ready, BrushToolMessage::DragStart) => {
|
||||||
responses.add(DocumentMessage::StartTransaction);
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
let existing_points = load_existing_points(document);
|
let layer_path = tool_data.load_existing_strokes(document);
|
||||||
let new_layer = existing_points.is_none();
|
let new_layer = layer_path.is_none();
|
||||||
if let Some((layer_path, points)) = existing_points {
|
if new_layer {
|
||||||
tool_data.path = Some(layer_path);
|
|
||||||
//tool_data.set_image(image, responses);
|
|
||||||
if tool_data.points.is_empty() {
|
|
||||||
tool_data.points.push(points);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
responses.add(DocumentMessage::DeselectAllLayers);
|
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);
|
// Start a new stroke with a single sample
|
||||||
|
tool_data.strokes.push(BrushStroke {
|
||||||
tool_data.points.push(vec![pos]);
|
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 {
|
if new_layer {
|
||||||
add_brush_render(tool_options, tool_data, responses);
|
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) {
|
(BrushToolFsmState::Drawing, BrushToolMessage::PointerMove) => {
|
||||||
// Linear interpolation for when the mouse has moved a lot between frames
|
if let Some(stroke) = tool_data.strokes.last_mut() {
|
||||||
if let Some(&last_point) = tool_data.points.last().and_then(|x| x.last()) {
|
stroke.trace.push(BrushInputSample { position: layer_position })
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
tool_data.update_strokes(tool_options, responses);
|
||||||
|
|
||||||
tool_data.update_points(responses);
|
BrushToolFsmState::Drawing
|
||||||
|
|
||||||
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);
|
responses.add(DocumentMessage::CommitTransaction);
|
||||||
} else {
|
} else {
|
||||||
responses.add(DocumentMessage::AbortTransaction);
|
responses.add(DocumentMessage::AbortTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
tool_data.points.clear();
|
tool_data.strokes.clear();
|
||||||
tool_data.path = None;
|
|
||||||
|
|
||||||
Ready
|
BrushToolFsmState::Ready
|
||||||
}
|
}
|
||||||
(_, WorkingColorChanged) => {
|
|
||||||
|
(_, BrushToolMessage::WorkingColorChanged) => {
|
||||||
responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::WorkingColors(
|
responses.add(BrushToolMessage::UpdateOptions(BrushToolMessageOptionsUpdate::WorkingColors(
|
||||||
Some(global_tool_data.primary_color),
|
Some(global_tool_data.primary_color),
|
||||||
Some(global_tool_data.secondary_color),
|
Some(global_tool_data.secondary_color),
|
||||||
|
|
@ -342,7 +353,7 @@ impl Fsm for BrushToolFsmState {
|
||||||
|
|
||||||
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||||
let hint_data = match self {
|
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![]),
|
BrushToolFsmState::Drawing => HintData(vec![]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -355,55 +366,10 @@ impl Fsm for BrushToolFsmState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_brush_render(tool_options: &BrushOptions, data: &BrushToolData, responses: &mut VecDeque<Message>) {
|
fn add_brush_render(tool_options: &BrushOptions, data: &BrushToolData, responses: &mut VecDeque<Message>) {
|
||||||
let layer_path = data.path.clone().unwrap();
|
let mut network = NodeNetwork::default();
|
||||||
|
let output_node = network.push_output_node();
|
||||||
let brush_node = DocumentNode {
|
if let Some(node) = network.nodes.get_mut(&output_node) {
|
||||||
name: "Brush".to_string(),
|
node.inputs.push(NodeInput::value(TaggedValue::ImageFrame(ImageFrame::empty()), true))
|
||||||
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<LayerId>, Vec<DVec2>)> {
|
|
||||||
if document.selected_layers().count() != 1 {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
let layer_path = document.selected_layers().next()?.to_vec();
|
graph_modification_utils::new_custom_layer(network, data.layer_path.clone(), responses);
|
||||||
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()))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ use document_legacy::{LayerId, Operation};
|
||||||
use graph_craft::document::value::TaggedValue;
|
use graph_craft::document::value::TaggedValue;
|
||||||
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
|
use graph_craft::document::{generate_uuid, DocumentNodeImplementation, NodeId, NodeNetwork};
|
||||||
use graph_craft::executor::Compiler;
|
use graph_craft::executor::Compiler;
|
||||||
use graph_craft::imaginate_input::*;
|
|
||||||
use graph_craft::{concrete, Type, TypeDescriptor};
|
use graph_craft::{concrete, Type, TypeDescriptor};
|
||||||
use graphene_core::raster::{Image, ImageFrame};
|
use graphene_core::raster::{Image, ImageFrame};
|
||||||
use graphene_core::renderer::{SvgSegment, SvgSegmentList};
|
use graphene_core::renderer::{SvgSegment, SvgSegmentList};
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use glam::DVec2;
|
||||||
pub use self::color::{Color, Luma};
|
pub use self::color::{Color, Luma};
|
||||||
|
|
||||||
pub mod adjustments;
|
pub mod adjustments;
|
||||||
|
pub mod bbox;
|
||||||
#[cfg(not(target_arch = "spirv"))]
|
#[cfg(not(target_arch = "spirv"))]
|
||||||
pub mod brightness_contrast;
|
pub mod brightness_contrast;
|
||||||
pub mod color;
|
pub mod color;
|
||||||
|
|
|
||||||
|
|
@ -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<AxisAlignedBbox> {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -143,25 +143,47 @@ where
|
||||||
let Image { width, height, data } = self;
|
let Image { width, height, data } = self;
|
||||||
assert!(data.len() == width as usize * height as usize);
|
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 {
|
for color in data {
|
||||||
let a = color.a().to_f32();
|
let a = color.a().to_f32();
|
||||||
if a < 0.5 / 255.0 {
|
// Smaller alpha values than this would map to fully transparent
|
||||||
// This would map to fully transparent anyway, avoid expensive encoding.
|
// anyway, avoid expensive encoding.
|
||||||
result.push(0);
|
if a >= 0.5 / 255. {
|
||||||
result.push(0);
|
let undo_premultiply = 1. / a;
|
||||||
result.push(0);
|
let r = color.r().to_f32() * undo_premultiply;
|
||||||
result.push(0);
|
let g = color.g().to_f32() * undo_premultiply;
|
||||||
} else {
|
let b = color.b().to_f32() * undo_premultiply;
|
||||||
let undo_premultiply = 1.0 / a;
|
|
||||||
let r = float_to_srgb_u8(color.r().to_f32() * undo_premultiply);
|
// Compute new sRGB value if necessary.
|
||||||
let g = float_to_srgb_u8(color.g().to_f32() * undo_premultiply);
|
if r != last_r {
|
||||||
let b = float_to_srgb_u8(color.b().to_f32() * undo_premultiply);
|
last_r = r;
|
||||||
result.push(r);
|
last_r_srgb = float_to_srgb_u8(r);
|
||||||
result.push(g);
|
}
|
||||||
result.push(b);
|
if g != last_g {
|
||||||
result.push((a * 255.0 + 0.5) as u8);
|
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)
|
(result, width, height)
|
||||||
|
|
|
||||||
|
|
@ -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<H: Hasher>(&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<H: Hasher>(&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<BrushInputSample>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<DVec2> {
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod brush_stroke;
|
||||||
pub mod consts;
|
pub mod consts;
|
||||||
pub mod generator_nodes;
|
pub mod generator_nodes;
|
||||||
pub mod manipulator_group;
|
pub mod manipulator_group;
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ pub enum TaggedValue {
|
||||||
OptionalColor(Option<graphene_core::raster::color::Color>),
|
OptionalColor(Option<graphene_core::raster::color::Color>),
|
||||||
ManipulatorGroupIds(Vec<graphene_core::uuid::ManipulatorGroupId>),
|
ManipulatorGroupIds(Vec<graphene_core::uuid::ManipulatorGroupId>),
|
||||||
Font(graphene_core::text::Font),
|
Font(graphene_core::text::Font),
|
||||||
VecDVec2(Vec<DVec2>),
|
BrushStrokes(Vec<graphene_core::vector::brush_stroke::BrushStroke>),
|
||||||
Segments(Vec<graphene_core::raster::ImageFrame<Color>>),
|
Segments(Vec<graphene_core::raster::ImageFrame<Color>>),
|
||||||
EditorApi(graphene_core::EditorApi<'static>),
|
EditorApi(graphene_core::EditorApi<'static>),
|
||||||
DocumentNode(DocumentNode),
|
DocumentNode(DocumentNode),
|
||||||
|
|
@ -115,12 +115,7 @@ impl Hash for TaggedValue {
|
||||||
Self::OptionalColor(color) => color.hash(state),
|
Self::OptionalColor(color) => color.hash(state),
|
||||||
Self::ManipulatorGroupIds(mirror) => mirror.hash(state),
|
Self::ManipulatorGroupIds(mirror) => mirror.hash(state),
|
||||||
Self::Font(font) => font.hash(state),
|
Self::Font(font) => font.hash(state),
|
||||||
Self::VecDVec2(vec_dvec2) => {
|
Self::BrushStrokes(brush_strokes) => brush_strokes.hash(state),
|
||||||
vec_dvec2.len().hash(state);
|
|
||||||
for dvec2 in vec_dvec2 {
|
|
||||||
dvec2.to_array().iter().for_each(|x| x.to_bits().hash(state));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self::Segments(segments) => {
|
Self::Segments(segments) => {
|
||||||
for segment in segments {
|
for segment in segments {
|
||||||
segment.hash(state)
|
segment.hash(state)
|
||||||
|
|
@ -176,7 +171,7 @@ impl<'a> TaggedValue {
|
||||||
TaggedValue::OptionalColor(x) => Box::new(x),
|
TaggedValue::OptionalColor(x) => Box::new(x),
|
||||||
TaggedValue::ManipulatorGroupIds(x) => Box::new(x),
|
TaggedValue::ManipulatorGroupIds(x) => Box::new(x),
|
||||||
TaggedValue::Font(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::Segments(x) => Box::new(x),
|
||||||
TaggedValue::EditorApi(x) => Box::new(x),
|
TaggedValue::EditorApi(x) => Box::new(x),
|
||||||
TaggedValue::DocumentNode(x) => Box::new(x),
|
TaggedValue::DocumentNode(x) => Box::new(x),
|
||||||
|
|
@ -239,7 +234,7 @@ impl<'a> TaggedValue {
|
||||||
TaggedValue::OptionalColor(_) => concrete!(Option<graphene_core::Color>),
|
TaggedValue::OptionalColor(_) => concrete!(Option<graphene_core::Color>),
|
||||||
TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec<graphene_core::uuid::ManipulatorGroupId>),
|
TaggedValue::ManipulatorGroupIds(_) => concrete!(Vec<graphene_core::uuid::ManipulatorGroupId>),
|
||||||
TaggedValue::Font(_) => concrete!(graphene_core::text::Font),
|
TaggedValue::Font(_) => concrete!(graphene_core::text::Font),
|
||||||
TaggedValue::VecDVec2(_) => concrete!(Vec<DVec2>),
|
TaggedValue::BrushStrokes(_) => concrete!(Vec<graphene_core::vector::brush_stroke::BrushStroke>),
|
||||||
TaggedValue::Segments(_) => concrete!(graphene_core::raster::IndexNode<Vec<graphene_core::raster::ImageFrame<Color>>>),
|
TaggedValue::Segments(_) => concrete!(graphene_core::raster::IndexNode<Vec<graphene_core::raster::ImageFrame<Color>>>),
|
||||||
TaggedValue::EditorApi(_) => concrete!(graphene_core::EditorApi),
|
TaggedValue::EditorApi(_) => concrete!(graphene_core::EditorApi),
|
||||||
TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode),
|
TaggedValue::DocumentNode(_) => concrete!(crate::document::DocumentNode),
|
||||||
|
|
@ -291,7 +286,7 @@ impl<'a> TaggedValue {
|
||||||
x if x == TypeId::of::<Option<graphene_core::Color>>() => Some(TaggedValue::OptionalColor(*downcast(input).unwrap())),
|
x if x == TypeId::of::<Option<graphene_core::Color>>() => Some(TaggedValue::OptionalColor(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<Vec<graphene_core::uuid::ManipulatorGroupId>>() => Some(TaggedValue::ManipulatorGroupIds(*downcast(input).unwrap())),
|
x if x == TypeId::of::<Vec<graphene_core::uuid::ManipulatorGroupId>>() => Some(TaggedValue::ManipulatorGroupIds(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<graphene_core::text::Font>() => Some(TaggedValue::Font(*downcast(input).unwrap())),
|
x if x == TypeId::of::<graphene_core::text::Font>() => Some(TaggedValue::Font(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<Vec<DVec2>>() => Some(TaggedValue::VecDVec2(*downcast(input).unwrap())),
|
x if x == TypeId::of::<Vec<graphene_core::vector::brush_stroke::BrushStroke>>() => Some(TaggedValue::BrushStrokes(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<graphene_core::raster::IndexNode<Vec<graphene_core::raster::ImageFrame<Color>>>>() => Some(TaggedValue::Segments(*downcast(input).unwrap())),
|
x if x == TypeId::of::<graphene_core::raster::IndexNode<Vec<graphene_core::raster::ImageFrame<Color>>>>() => Some(TaggedValue::Segments(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<graphene_core::EditorApi>() => Some(TaggedValue::EditorApi(*downcast(input).unwrap())),
|
x if x == TypeId::of::<graphene_core::EditorApi>() => Some(TaggedValue::EditorApi(*downcast(input).unwrap())),
|
||||||
x if x == TypeId::of::<crate::document::DocumentNode>() => Some(TaggedValue::DocumentNode(*downcast(input).unwrap())),
|
x if x == TypeId::of::<crate::document::DocumentNode>() => Some(TaggedValue::DocumentNode(*downcast(input).unwrap())),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
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::transform::{Transform, TransformMut};
|
||||||
use graphene_core::vector::VectorData;
|
use graphene_core::vector::VectorData;
|
||||||
use graphene_core::Node;
|
use graphene_core::Node;
|
||||||
|
|
@ -21,6 +21,22 @@ where
|
||||||
iter.fold(initial, |a, x| lambda.eval((a, x)))
|
iter.fold(initial, |a, x| lambda.eval((a, x)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct ChainApplyNode<Value> {
|
||||||
|
pub value: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[node_fn(ChainApplyNode)]
|
||||||
|
async fn chain_apply<I: Iterator, T>(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)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct IntoIterNode<T> {
|
pub struct IntoIterNode<T> {
|
||||||
_t: PhantomData<T>,
|
_t: PhantomData<T>,
|
||||||
|
|
@ -100,7 +116,7 @@ pub struct EraseNode<Flow> {
|
||||||
#[node_fn(EraseNode)]
|
#[node_fn(EraseNode)]
|
||||||
fn erase(input: (Color, Color), flow: f64) -> Color {
|
fn erase(input: (Color, Color), flow: f64) -> Color {
|
||||||
let (input, brush) = input;
|
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)
|
Color::from_unassociated_alpha(input.r(), input.g(), input.b(), alpha)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,6 +150,54 @@ fn translate_node<Data: TransformMut>(offset: DVec2, mut translatable: Data) ->
|
||||||
translatable
|
translatable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct BlitNode<P, Texture, Positions, BlendFn> {
|
||||||
|
texture: Texture,
|
||||||
|
positions: Positions,
|
||||||
|
blend_mode: BlendFn,
|
||||||
|
_p: PhantomData<P>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[node_fn(BlitNode<_P>)]
|
||||||
|
fn blit_node<_P: Alpha + Pixel + std::fmt::Debug, BlendFn>(mut target: ImageFrame<_P>, texture: ImageFrame<_P>, positions: Vec<DVec2>, 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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -152,10 +216,10 @@ mod test {
|
||||||
fn test_translate_node() {
|
fn test_translate_node() {
|
||||||
let image = Image::new(10, 10, Color::TRANSPARENT);
|
let image = Image::new(10, 10, Color::TRANSPARENT);
|
||||||
let mut image = ImageFrame { image, transform: DAffine2::IDENTITY };
|
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 translate_node = TranslateNode::new(ClonedNode::new(image));
|
||||||
let image = translate_node.eval(DVec2::new(1.0, 2.0));
|
let image = translate_node.eval(DVec2::new(1., 2.));
|
||||||
assert_eq!(image.transform(), DAffine2::from_translation(DVec2::new(2.0, 4.0)));
|
assert_eq!(image.transform(), DAffine2::from_translation(DVec2::new(2., 4.)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -177,9 +241,9 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_brush() {
|
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 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 trace = ClonedNode::new(trace.into_iter());
|
||||||
let translate_node = TranslateNode::new(ClonedNode::new(image));
|
let translate_node = TranslateNode::new(ClonedNode::new(image));
|
||||||
let frames = MapNode::new(ValueNode::new(translate_node));
|
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 = background_bounds.eval(frames.clone().into_iter());
|
||||||
let background_bounds = ClonedNode::new(background_bounds.unwrap().to_transform());
|
let background_bounds = ClonedNode::new(background_bounds.unwrap().to_transform());
|
||||||
let background_image = background_bounds.then(EmptyImageNode::new(ClonedNode::new(Color::TRANSPARENT)));
|
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 = ReduceNode::new(background_image, ValueNode::new(BlendImageTupleNode::new(ValueNode::new(blend_node))));
|
||||||
let final_image = final_image.eval(frames.into_iter());
|
let final_image = final_image.eval(frames.into_iter());
|
||||||
assert_eq!(final_image.image.height, 20);
|
assert_eq!(final_image.image.height, 20);
|
||||||
|
|
|
||||||
|
|
@ -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::raster::{Alpha, BlendMode, BlendNode, Image, ImageFrame, Linear, LinearChannel, Luminance, Pixel, RGBMut, Raster, RasterMut, RedGreenBlue, Sample};
|
||||||
use graphene_core::transform::Transform;
|
use graphene_core::transform::Transform;
|
||||||
|
|
||||||
|
use graphene_core::raster::bbox::{AxisAlignedBbox, Bbox};
|
||||||
use graphene_core::value::CopiedNode;
|
use graphene_core::value::CopiedNode;
|
||||||
use graphene_core::{Color, Node};
|
use graphene_core::{Color, Node};
|
||||||
|
|
||||||
|
|
@ -95,85 +96,6 @@ where
|
||||||
image
|
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<AxisAlignedBbox> {
|
|
||||||
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)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct InsertChannelNode<P, S, Insertion, TargetChannel> {
|
pub struct InsertChannelNode<P, S, Insertion, TargetChannel> {
|
||||||
insertion: Insertion,
|
insertion: Insertion,
|
||||||
|
|
@ -325,8 +247,8 @@ fn blend_new_image<_P: Alpha + Pixel + Debug, MapFn, Frame: Sample<Pixel = _P> +
|
||||||
where
|
where
|
||||||
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>,
|
MapFn: for<'any_input> Node<'any_input, (_P, _P), Output = _P>,
|
||||||
{
|
{
|
||||||
let foreground_aabb = compute_transformed_bounding_box(foreground.transform()).axis_aligned_bbox();
|
let foreground_aabb = Bbox::unit().affine_transform(foreground.transform()).to_axis_aligned_bbox();
|
||||||
let background_aabb = compute_transformed_bounding_box(background.transform()).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()};
|
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);
|
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
|
// 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
|
// Clamp the foreground image to the background image
|
||||||
let start = (bg_aabb.start * background_size).max(DVec2::ZERO).as_uvec2();
|
let start = (bg_aabb.start * background_size).max(DVec2::ZERO).as_uvec2();
|
||||||
|
|
@ -393,8 +315,8 @@ pub struct ExtendImageNode<Background> {
|
||||||
|
|
||||||
#[node_macro::node_fn(ExtendImageNode)]
|
#[node_macro::node_fn(ExtendImageNode)]
|
||||||
fn extend_image_node(foreground: ImageFrame<Color>, background: ImageFrame<Color>) -> ImageFrame<Color> {
|
fn extend_image_node(foreground: ImageFrame<Color>, background: ImageFrame<Color>) -> ImageFrame<Color> {
|
||||||
let foreground_aabb = compute_transformed_bounding_box(foreground.transform()).axis_aligned_bbox();
|
let foreground_aabb = Bbox::unit().affine_transform(foreground.transform()).to_axis_aligned_bbox();
|
||||||
let background_aabb = compute_transformed_bounding_box(background.transform()).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) {
|
if foreground_aabb.contains(background_aabb.start) && foreground_aabb.contains(background_aabb.end) {
|
||||||
return foreground;
|
return foreground;
|
||||||
|
|
@ -412,7 +334,7 @@ pub struct MergeBoundingBoxNode<Data> {
|
||||||
fn merge_bounding_box_node<_Data: Transform>(input: (Option<AxisAlignedBbox>, _Data)) -> Option<AxisAlignedBbox> {
|
fn merge_bounding_box_node<_Data: Transform>(input: (Option<AxisAlignedBbox>, _Data)) -> Option<AxisAlignedBbox> {
|
||||||
let (initial_aabb, data) = input;
|
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 {
|
if let Some(fst_aabb) = initial_aabb {
|
||||||
fst_aabb.union_non_empty(&snd_aabb)
|
fst_aabb.union_non_empty(&snd_aabb)
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,27 @@
|
||||||
use glam::{DAffine2, DVec2};
|
|
||||||
|
|
||||||
use graph_craft::document::DocumentNode;
|
use graph_craft::document::DocumentNode;
|
||||||
|
use graph_craft::proto::{NodeConstructor, TypeErasedPinned};
|
||||||
use graphene_core::ops::IdNode;
|
use graphene_core::ops::IdNode;
|
||||||
use graphene_core::vector::VectorData;
|
use graphene_core::quantization::QuantizationChannels;
|
||||||
use once_cell::sync::Lazy;
|
use graphene_core::raster::bbox::AxisAlignedBbox;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use graphene_core::raster::color::Color;
|
use graphene_core::raster::color::Color;
|
||||||
use graphene_core::structural::Then;
|
use graphene_core::structural::Then;
|
||||||
use graphene_core::value::{ClonedNode, CopiedNode, ValueNode};
|
use graphene_core::value::{ClonedNode, CopiedNode, ValueNode};
|
||||||
use graphene_core::{fn_type, raster::*};
|
use graphene_core::vector::brush_stroke::BrushStroke;
|
||||||
use graphene_core::{Node, NodeIO, NodeIOTypes};
|
use graphene_core::vector::VectorData;
|
||||||
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::{concrete, generic, value_fn};
|
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::memo::{CacheNode, LetNode};
|
||||||
use graphene_std::raster::BlendImageTupleNode;
|
use graphene_std::raster::BlendImageTupleNode;
|
||||||
|
use graphene_std::raster::*;
|
||||||
|
|
||||||
use dyn_any::StaticType;
|
use dyn_any::StaticType;
|
||||||
|
use glam::{DAffine2, DVec2};
|
||||||
use graphene_core::quantization::QuantizationChannels;
|
use once_cell::sync::Lazy;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
macro_rules! construct_node {
|
macro_rules! construct_node {
|
||||||
($args: ident, $path:ty, [$($type:tt),*]) => { async move {
|
($args: ident, $path:ty, [$($type:tt),*]) => { async move {
|
||||||
|
|
@ -242,7 +237,7 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
||||||
vec![Type::Fn(Box::new(generic!(T)), Box::new(generic!(V))), Type::Fn(Box::new(generic!(V)), Box::new(generic!(U)))],
|
vec![Type::Fn(Box::new(generic!(T)), Box::new(generic!(V))), Type::Fn(Box::new(generic!(V)), Box::new(generic!(U)))],
|
||||||
),
|
),
|
||||||
)],
|
)],
|
||||||
register_node!(graphene_std::brush::IntoIterNode<_>, input: &Vec<DVec2>, params: []),
|
register_node!(graphene_std::brush::IntoIterNode<_>, input: &Vec<BrushStroke>, params: []),
|
||||||
vec![(
|
vec![(
|
||||||
NodeIdentifier::new("graphene_std::brush::BrushNode"),
|
NodeIdentifier::new("graphene_std::brush::BrushNode"),
|
||||||
|args| {
|
|args| {
|
||||||
|
|
@ -253,55 +248,48 @@ fn node_registry() -> HashMap<NodeIdentifier, HashMap<NodeIOTypes, NodeConstruct
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let image: DowncastBothNode<(), ImageFrame<Color>> = DowncastBothNode::new(args[0]);
|
let image: DowncastBothNode<(), ImageFrame<Color>> = DowncastBothNode::new(args[0]);
|
||||||
let bounds: DowncastBothNode<(), ImageFrame<Color>> = DowncastBothNode::new(args[1]);
|
let bounds: DowncastBothNode<(), ImageFrame<Color>> = DowncastBothNode::new(args[1]);
|
||||||
let trace: DowncastBothNode<(), Vec<DVec2>> = DowncastBothNode::new(args[2]);
|
let strokes: DowncastBothNode<(), Vec<BrushStroke>> = 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 stamp = BrushStampGeneratorNode::new(CopiedNode::new(color.eval(()).await), CopiedNode::new(hardness.eval(()).await), CopiedNode::new(flow.eval(()).await));
|
let strokes = strokes.eval(()).await;
|
||||||
let stamp = stamp.eval(diameter.eval(()).await);
|
let bbox = strokes.iter().map(|s| s.bounding_box()).reduce(|a, b| a.union(&b)).unwrap_or(AxisAlignedBbox::ZERO);
|
||||||
|
|
||||||
let frames = TranslateNode::new(CopiedNode::new(stamp));
|
|
||||||
let frames = MapNode::new(ValueNode::new(frames));
|
|
||||||
let frames = frames.eval(trace.eval(()).await.into_iter()).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
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 mut background_bounds = CopiedNode::new(bbox.to_transform());
|
||||||
let bounds_transform = bounds.eval(()).await.transform;
|
let bounds_transform = bounds.eval(()).await.transform;
|
||||||
if bounds_transform != DAffine2::ZERO {
|
if bounds_transform != DAffine2::ZERO {
|
||||||
background_bounds = CopiedNode::new(bounds_transform);
|
background_bounds = CopiedNode::new(bounds_transform);
|
||||||
}
|
}
|
||||||
|
|
||||||
let background_image = background_bounds.then(EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)));
|
let blank_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 background = image.and_then(ExtendImageNode::new(blank_image));
|
||||||
|
|
||||||
let background = ExtendImageNode::new(background_image);
|
let mut blits = Vec::new();
|
||||||
let background_image = image.and_then(background);
|
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 transform = DAffine2::from_scale_angle_translation(DVec2::splat(stroke.style.diameter), 0., -DVec2::splat(stroke.style.diameter / 2.0));
|
||||||
let final_image = ClonedNode::new(frames.into_iter()).then(final_image);
|
let blank_texture = EmptyImageNode::new(CopiedNode::new(Color::TRANSPARENT)).eval(transform);
|
||||||
|
|
||||||
let final_image = FutureWrapperNode::new(final_image);
|
let blend_params = graphene_core::raster::BlendNode::new(CopiedNode::new(BlendMode::Normal), CopiedNode::new(100.));
|
||||||
let any: DynAnyNode<(), _, _> = graphene_std::any::DynAnyNode::new(ValueNode::new(final_image));
|
let blend_executor = BlendImageTupleNode::new(ValueNode::new(blend_params));
|
||||||
any.into_type_erased()
|
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(
|
NodeIOTypes::new(
|
||||||
concrete!(()),
|
concrete!(()),
|
||||||
concrete!(ImageFrame<Color>),
|
concrete!(ImageFrame<Color>),
|
||||||
vec![
|
vec![value_fn!(ImageFrame<Color>), value_fn!(ImageFrame<Color>), value_fn!(Vec<BrushStroke>)],
|
||||||
value_fn!(ImageFrame<Color>),
|
|
||||||
value_fn!(ImageFrame<Color>),
|
|
||||||
value_fn!(Vec<DVec2>),
|
|
||||||
value_fn!(f64),
|
|
||||||
value_fn!(f64),
|
|
||||||
value_fn!(f64),
|
|
||||||
value_fn!(Color),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
)],
|
)],
|
||||||
vec![(
|
vec![(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue