mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Add the 'Basic Brush' node and replace the legacy Brush tool implementation with it (#4469)
* Draw raster images with pad extension instead of repeat * Add the GPU basic brush renderer * Rework the brush tool around the GPU basic brush * Remove the CPU brush implementation
This commit is contained in:
@@ -135,7 +135,9 @@ pub const LINE_ROTATE_SNAP_ANGLE: f64 = 15.;
|
||||
|
||||
// BRUSH TOOL
|
||||
pub const BRUSH_SIZE_CHANGE_KEYBOARD: f64 = 5.;
|
||||
pub const DEFAULT_BRUSH_SIZE: f64 = 20.;
|
||||
pub const BRUSH_SIZE_DEFAULT: f64 = 40.;
|
||||
pub const BRUSH_HARDNESS_DEFAULT: f64 = 0.;
|
||||
pub const BRUSH_FLOW_DEFAULT: f64 = 100.;
|
||||
|
||||
// EYEDROPPER TOOL
|
||||
pub const EYEDROPPER_PREVIEW_AREA_RESOLUTION: u32 = 11;
|
||||
|
||||
@@ -275,11 +275,7 @@ impl PreferencesDialogMessageHandler {
|
||||
|
||||
let checkbox_id = CheckboxId::new();
|
||||
let brush_tool_description = "
|
||||
Enable the Brush tool to support basic raster-based layer painting.\n\
|
||||
\n\
|
||||
This legacy experimental tool has performance and quality limitations and is slated for replacement in future versions of Graphite that will have a renewed focus on raster graphics editing.\n\
|
||||
\n\
|
||||
Content created with the Brush tool may not be compatible with future versions of Graphite.\n\
|
||||
Enable the Brush tool to support basic layer painting.\n\
|
||||
\n\
|
||||
*Default: Off.*
|
||||
"
|
||||
|
||||
@@ -1066,7 +1066,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
|
||||
(true, Some(storage)) => storage
|
||||
.export_to_bytes(
|
||||
document_format::ExportFormat::Xz,
|
||||
document_format::ExportOptions::default(),
|
||||
document_format::ExportOptions {
|
||||
include_history: false,
|
||||
..Default::default()
|
||||
},
|
||||
export_load_handle.as_ref(),
|
||||
Some(&legacy_document),
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ use crate::messages::prelude::*;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use graph_craft::document::NodeId;
|
||||
use graphene_std::Color;
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::Image;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
@@ -112,9 +111,23 @@ pub enum GraphOperationMessage {
|
||||
layer: LayerNodeIdentifier,
|
||||
modification_type: VectorModificationType,
|
||||
},
|
||||
Brush {
|
||||
NewBrushGroupLayer {
|
||||
id: NodeId,
|
||||
strokes_node_id: NodeId,
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
color: Color,
|
||||
diameter: f64,
|
||||
hardness: f64,
|
||||
flow: f64,
|
||||
},
|
||||
NewBrushStrokesNode {
|
||||
layer: LayerNodeIdentifier,
|
||||
strokes: Vec<BrushStroke>,
|
||||
strokes_node_id: NodeId,
|
||||
color: Color,
|
||||
diameter: f64,
|
||||
hardness: f64,
|
||||
flow: f64,
|
||||
},
|
||||
SetUpstreamToChain {
|
||||
layer: LayerNodeIdentifier,
|
||||
|
||||
@@ -2,13 +2,14 @@ use super::transform_utils;
|
||||
use super::utility_types::{ModifyInputsContext, set_stroke_paint_order};
|
||||
use crate::consts::{LAYER_INDENT_OFFSET, STACK_VERTICAL_GAP};
|
||||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{BLEND_PATH_INPUT_INDEX, DefinitionIdentifier};
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{BLEND_PATH_INPUT_INDEX, DefinitionIdentifier, resolve_proto_node_type};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeNetworkInterface, OutputConnector};
|
||||
use crate::messages::portfolio::document::utility_types::nodes::CollapsedLayers;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils::get_clip_mode;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graph_craft::list;
|
||||
use graphene_std::renderer::convert_usvg_path::convert_usvg_path;
|
||||
@@ -172,10 +173,35 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageContext<'_>> for
|
||||
modify_inputs.vector_modify(modification_type);
|
||||
}
|
||||
}
|
||||
GraphOperationMessage::Brush { layer, strokes } => {
|
||||
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
|
||||
modify_inputs.brush_modify(strokes);
|
||||
}
|
||||
GraphOperationMessage::NewBrushGroupLayer {
|
||||
id,
|
||||
strokes_node_id,
|
||||
parent,
|
||||
insert_index,
|
||||
color,
|
||||
diameter,
|
||||
hardness,
|
||||
flow,
|
||||
} => {
|
||||
let layer = ModifyInputsContext::new(network_interface, responses).create_layer(id);
|
||||
insert_brush_strokes_chain(network_interface, layer, strokes_node_id, color, diameter, hardness, flow);
|
||||
|
||||
responses.add(NodeGraphMessage::MoveLayerToStack { layer, parent, insert_index });
|
||||
responses.add(GraphOperationMessage::SetUpstreamToChain { layer });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
GraphOperationMessage::NewBrushStrokesNode {
|
||||
layer,
|
||||
strokes_node_id,
|
||||
color,
|
||||
diameter,
|
||||
hardness,
|
||||
flow,
|
||||
} => {
|
||||
insert_brush_strokes_chain(network_interface, layer, strokes_node_id, color, diameter, hardness, flow);
|
||||
|
||||
responses.add(GraphOperationMessage::SetUpstreamToChain { layer });
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
GraphOperationMessage::SetUpstreamToChain { layer } => {
|
||||
let Some(OutputConnector::Node { node_id: first_chain_node, .. }) = network_interface.upstream_output_connector(&InputConnector::layer_secondary_input(layer.to_node()), &[]) else {
|
||||
@@ -847,6 +873,22 @@ fn import_usvg_node_inner(
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_brush_strokes_chain(network_interface: &mut NodeNetworkInterface, layer: LayerNodeIdentifier, strokes_node_id: NodeId, color: Color, diameter: f64, hardness: f64, flow: f64) {
|
||||
let Some(strokes_node_type) = resolve_proto_node_type(graphene_std::brush::brush_strokes::IDENTIFIER) else {
|
||||
log::error!("Brush strokes node does not exist");
|
||||
return;
|
||||
};
|
||||
let strokes_node = strokes_node_type.node_template_input_override([
|
||||
Some(NodeInput::value(TaggedValue::Strokes(Vec::new()), false)),
|
||||
Some(NodeInput::value(TaggedValue::Color(color), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(diameter), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(hardness), false)),
|
||||
Some(NodeInput::value(TaggedValue::F64(flow), false)),
|
||||
]);
|
||||
network_interface.insert_node(strokes_node_id, strokes_node, &[]);
|
||||
network_interface.set_input(&InputConnector::node_at_index(layer.to_node(), 1), NodeInput::node(strokes_node_id, 0), &[]);
|
||||
}
|
||||
|
||||
/// Helper to apply path data (vector geometry, fill, stroke, transform) to a layer.
|
||||
fn import_usvg_path(modify_inputs: &mut ModifyInputsContext, node: &usvg::Node, path: &usvg::Path, layer: LayerNodeIdentifier, gradient_info: &SvgGradientInfo) {
|
||||
let bezpath = convert_usvg_path(path);
|
||||
|
||||
@@ -13,7 +13,6 @@ use graph_craft::application_io::resource::ResourceId;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graph_craft::{ProtoNodeIdentifier, list};
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::raster_types::Image;
|
||||
use graphene_std::text::{Font, TypesettingConfig};
|
||||
@@ -977,17 +976,6 @@ impl<'a> ModifyInputsContext<'a> {
|
||||
self.responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
|
||||
pub fn brush_modify(&mut self, strokes: Vec<BrushStroke>) {
|
||||
let Some(brush_node_id) = self.existing_proto_node_id(graphene_std::brush::brush::brush::IDENTIFIER, true) else {
|
||||
return;
|
||||
};
|
||||
self.set_input_with_refresh(
|
||||
InputConnector::node(brush_node_id, graphene_std::brush::brush::brush::TraceInput),
|
||||
NodeInput::value(TaggedValue::BrushStrokes(strokes), false),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn resize_artboard(&mut self, location: DVec2, dimensions: DVec2) {
|
||||
let Some(artboard_node_id) = self.existing_network_node_id("Artboard", true) else {
|
||||
return;
|
||||
|
||||
@@ -17,7 +17,6 @@ use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
||||
use graph_craft::{Type, concrete};
|
||||
use graphene_std::animation::RealTimeMode;
|
||||
use graphene_std::brush::brush_stroke::BrushTrace;
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::extract_xy::XY;
|
||||
use graphene_std::raster::{
|
||||
@@ -286,7 +285,6 @@ pub(crate) fn property_from_type(
|
||||
Some(x) if id_is::<DAffine2>(x) => transform_widget(default_info, &mut extra_widgets),
|
||||
Some(x) if id_is::<Color>(x) => color_widget(default_info, ColorInput::default().allow_none(false)),
|
||||
Some(x) if id_is::<Gradient>(x) => color_widget(default_info, ColorInput::default().allow_none(false)),
|
||||
Some(x) if id_is::<BrushTrace>(x) => brush_strokes_widget(default_info).into(),
|
||||
// ============
|
||||
// STRUCT TYPES
|
||||
// ============
|
||||
@@ -460,33 +458,6 @@ pub fn vector_modification_widget(parameter_widgets_info: ParameterWidgetsInfo)
|
||||
widgets
|
||||
}
|
||||
|
||||
pub fn brush_strokes_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<WidgetInstance> {
|
||||
let ParameterWidgetsInfo { document_node, node_id: _, index, .. } = parameter_widgets_info;
|
||||
|
||||
let mut widgets = start_widgets(¶meter_widgets_info);
|
||||
|
||||
let Some(document_node) = document_node else { return widgets };
|
||||
let Some(input) = document_node.inputs.get(index) else { return widgets };
|
||||
|
||||
if let Some(TaggedValue::BrushStrokes(strokes)) = input.as_non_exposed_value() {
|
||||
let stroke_count = strokes.len();
|
||||
let sample_count: usize = strokes.iter().map(|s| s.trace.len()).sum();
|
||||
let label = if stroke_count == 0 {
|
||||
"Empty".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{stroke_count} {} / {sample_count} {}",
|
||||
if stroke_count == 1 { "Stroke" } else { "Strokes" },
|
||||
if sample_count == 1 { "Sample" } else { "Samples" }
|
||||
)
|
||||
};
|
||||
|
||||
widgets.extend_from_slice(&[Separator::new(SeparatorStyle::Unrelated).widget_instance(), TextLabel::new(label).widget_instance()]);
|
||||
}
|
||||
|
||||
widgets
|
||||
}
|
||||
|
||||
pub fn image_data_widget(parameter_widgets_info: ParameterWidgetsInfo) -> Vec<WidgetInstance> {
|
||||
let ParameterWidgetsInfo { document_node, node_id: _, index, .. } = parameter_widgets_info;
|
||||
|
||||
|
||||
@@ -76,25 +76,6 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
|
||||
aliases: &["graphene_core::raster::OpacityNode", "graphene_core::blending_nodes::OpacityNode"],
|
||||
},
|
||||
// ================================
|
||||
// brush
|
||||
// ================================
|
||||
NodeReplacement {
|
||||
node: graphene_std::brush::brush::blit::IDENTIFIER,
|
||||
aliases: &["graphene_brush::BlitNode", "graphene_std::brush::BlitNode", "graphene_brush::brush::BlitNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::brush::brush::brush::IDENTIFIER,
|
||||
aliases: &["graphene_brush::BrushNode", "graphene_std::brush::BrushNode", "graphene_brush::brush::BrushNode"],
|
||||
},
|
||||
NodeReplacement {
|
||||
node: graphene_std::brush::brush::brush_stamp_generator::IDENTIFIER,
|
||||
aliases: &[
|
||||
"graphene_brush::BrushStampGeneratorNode",
|
||||
"graphene_std::brush::BrushStampGeneratorNode",
|
||||
"graphene_brush::brush::BrushStampGeneratorNode",
|
||||
],
|
||||
},
|
||||
// ================================
|
||||
// gcore
|
||||
// ================================
|
||||
NodeReplacement {
|
||||
@@ -1189,30 +1170,6 @@ pub fn document_migration_upgrades(document: &mut DocumentMessageHandler, reset_
|
||||
}
|
||||
}
|
||||
|
||||
// The "Brush" wrapper network was replaced with the `brush` proto node directly. Convert old `Network("Brush")` instances to the proto node, forwarding all 3 inputs (Background, Trace, Cache) one-to-one.
|
||||
// This must run as a pre-pass before the recursive iteration below: replacing the outer Brush's network impl orphans its child paths, and the recursive iteration would log errors for those stale paths.
|
||||
let brush_layers: Vec<(NodeId, Vec<NodeId>)> = document
|
||||
.network_interface
|
||||
.document_network()
|
||||
.recursive_nodes()
|
||||
.filter_map(|(node_id, _, path)| (document.network_interface.reference(node_id, &path) == Some(DefinitionIdentifier::Network("Brush".into()))).then_some((*node_id, path)))
|
||||
.collect();
|
||||
for (node_id, network_path) in &brush_layers {
|
||||
// Pre-load `outward_wires` so the chain-break check inside `set_input` resolves the original upstream→node wire from cache
|
||||
// rather than triggering a fresh rebuild from the (already-mutated) post-`replace_inputs` state, which would orphan wires.
|
||||
let _ = document.network_interface.outward_wires(network_path);
|
||||
let new_reference = DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER);
|
||||
let Some(definition) = resolve_document_node_type(&new_reference) else { continue };
|
||||
let mut node_template = definition.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
let Some(old_inputs) = document.network_interface.replace_inputs(node_id, network_path, &mut node_template) else {
|
||||
continue;
|
||||
};
|
||||
for (index, input) in old_inputs.iter().take(3).enumerate() {
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, index), input.clone(), network_path);
|
||||
}
|
||||
}
|
||||
|
||||
// The "Transform" wrapper network was replaced with the `transform` proto node directly. Convert old `Network("Transform")` instances to the proto node, forwarding the 5 user-facing inputs (Value, Translation, Rotation, Scale, Skew) and dropping the legacy migration sentinels (Origin Offset, Scale Appearance) at indices 5 and 6 if present.
|
||||
// Pre-pass for the same reason as the Brush migration above: replacing the outer Transform's network impl orphans its child paths.
|
||||
let transform_layers: Vec<(NodeId, Vec<NodeId>, usize)> = document
|
||||
@@ -2229,36 +2186,6 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
}
|
||||
|
||||
// Old shape: [background, bounds, trace, cache]. Both "bounds" (input 1) and "cache" (input 3) are dropped, and "cache" is now stored as
|
||||
// internal node state via `#[data]` on the brush node, so it is not a node input at all in the new shape.
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && inputs_count == 4 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 1), old_inputs[2].clone(), network_path);
|
||||
}
|
||||
|
||||
// Old shape: [background, trace, cache]. The "cache" input is dropped because the brush node now stores its cache as internal `#[data]` state.
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && inputs_count == 3 {
|
||||
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
|
||||
let old_inputs = document.network_interface.replace_inputs(node_id, network_path, &mut node_template)?;
|
||||
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 0), old_inputs[0].clone(), network_path);
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 1), old_inputs[1].clone(), network_path);
|
||||
}
|
||||
|
||||
// A brush node saved before `Item<Raster<CPU>>` had a default stored its unconnected background as the invalid `()`,
|
||||
// which fails type resolution against the raster primary; adopt the definition's empty-raster default instead.
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && matches!(node.inputs.first().and_then(|input| input.as_value()), Some(TaggedValue::None)) {
|
||||
let default_background = resolve_document_node_type(&reference)?.node_template.inputs.first()?.clone();
|
||||
document.network_interface.set_input(&InputConnector::node_at_index(*node_id, 0), default_background, network_path);
|
||||
}
|
||||
|
||||
if reference == DefinitionIdentifier::ProtoNode(ProtoNodeIdentifier::new("graphene_core::vector::RemoveHandlesNode")) {
|
||||
let mut node_template = resolve_document_node_type(&DefinitionIdentifier::ProtoNode(graphene_std::vector::auto_tangents::IDENTIFIER))?.default_node_template();
|
||||
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
|
||||
|
||||
@@ -93,6 +93,7 @@ impl MessageHandler<PreferencesMessage, PreferencesMessageContext<'_>> for Prefe
|
||||
zoom_with_scroll: self.zoom_with_scroll,
|
||||
});
|
||||
responses.add(FrontendMessage::UpdateUIScale { scale: self.ui_scale });
|
||||
responses.add(ToolMessage::RefreshToolShelf);
|
||||
}
|
||||
PreferencesMessage::ResetToDefaults => {
|
||||
responses.add(PreferencesMessage::Load { preferences: Self::default() });
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
use super::tool_prelude::*;
|
||||
use crate::consts::DEFAULT_BRUSH_SIZE;
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{BRUSH_FLOW_DEFAULT, BRUSH_HARDNESS_DEFAULT, BRUSH_SIZE_DEFAULT};
|
||||
use crate::messages::portfolio::document::graph_operation::transform_utils::get_current_transform;
|
||||
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_proto_node_type};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::FlowType;
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, solid};
|
||||
use graph_craft::document::NodeId;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, OutputConnector};
|
||||
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, selection_changed_since_last_sync, solid};
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{NodeId, NodeInput};
|
||||
use graphene_std::Color;
|
||||
use graphene_std::brush::brush_stroke::{BrushInputSample, BrushStroke, BrushStyle};
|
||||
use graphene_std::brush::basic_brush::basic_brush as active_brush;
|
||||
use graphene_std::brush::{Channel, Stroke};
|
||||
use graphene_std::color::SRGBA8;
|
||||
use graphene_std::raster::BlendMode;
|
||||
use graphene_std::vector::style::FillChoice;
|
||||
|
||||
const BRUSH_MAX_SIZE: f64 = 5000.;
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum DrawMode {
|
||||
Draw = 0,
|
||||
Erase,
|
||||
Restore,
|
||||
}
|
||||
const SAMPLE_MERGE_DISTANCE: f64 = 1.;
|
||||
const SAMPLE_MERGE_PRESSURE: f64 = 0.01;
|
||||
|
||||
#[derive(Default, ExtractField)]
|
||||
pub struct BrushTool {
|
||||
@@ -34,32 +28,35 @@ pub struct BrushOptions {
|
||||
diameter: f64,
|
||||
hardness: f64,
|
||||
flow: f64,
|
||||
spacing: f64,
|
||||
color: ToolColorOptions,
|
||||
blend_mode: BlendMode,
|
||||
draw_mode: DrawMode,
|
||||
last_synced_selection: Vec<LayerNodeIdentifier>,
|
||||
}
|
||||
|
||||
impl Default for BrushOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
diameter: DEFAULT_BRUSH_SIZE,
|
||||
hardness: 0.,
|
||||
flow: 100.,
|
||||
spacing: 20.,
|
||||
diameter: BRUSH_SIZE_DEFAULT,
|
||||
hardness: BRUSH_HARDNESS_DEFAULT,
|
||||
flow: BRUSH_FLOW_DEFAULT,
|
||||
color: ToolColorOptions::default(),
|
||||
blend_mode: BlendMode::Normal,
|
||||
draw_mode: DrawMode::Draw,
|
||||
last_synced_selection: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BrushOptions {
|
||||
fn active_color(&self) -> Color {
|
||||
self.color.active_color().unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[impl_message(Message, ToolMessage, Brush)]
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum BrushToolMessage {
|
||||
// Standard messages
|
||||
Abort,
|
||||
SelectionChanged,
|
||||
WorkingColorChanged,
|
||||
|
||||
// Tool-specific messages
|
||||
@@ -72,14 +69,11 @@ pub enum BrushToolMessage {
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum BrushToolMessageOptionsUpdate {
|
||||
BlendMode(BlendMode),
|
||||
ChangeDiameter(f64),
|
||||
Color(Option<Color>),
|
||||
Diameter(f64),
|
||||
DrawMode(DrawMode),
|
||||
Flow(f64),
|
||||
Hardness(f64),
|
||||
Spacing(f64),
|
||||
Flow(f64),
|
||||
WorkingColorsChanged,
|
||||
}
|
||||
|
||||
@@ -104,7 +98,7 @@ impl ToolMetadata for BrushTool {
|
||||
|
||||
impl LayoutHolder for BrushTool {
|
||||
fn layout(&self) -> Layout {
|
||||
let mut widgets = vec![
|
||||
let widgets = vec![
|
||||
ColorInput::new(FillChoice::<SRGBA8>::from(self.options.color.fill_choice.as_ref().unwrap_or(&FillChoice::None)))
|
||||
.mixed(self.options.color.fill_choice.is_none())
|
||||
.narrow(true)
|
||||
@@ -119,9 +113,13 @@ impl LayoutHolder for BrushTool {
|
||||
NumberInput::new(Some(self.options.diameter))
|
||||
.label("Diameter")
|
||||
.min(1.)
|
||||
.max(BRUSH_MAX_SIZE) /* Anything bigger would cause the application to be unresponsive and eventually die */
|
||||
.unit(" px")
|
||||
.on_update(|number_input: &NumberInput| BrushToolMessage::UpdateOptions { options: BrushToolMessageOptionsUpdate::Diameter(number_input.value.unwrap()) }.into())
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::Diameter(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
NumberInput::new(Some(self.options.hardness))
|
||||
@@ -151,63 +149,8 @@ impl LayoutHolder for BrushTool {
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||
NumberInput::new(Some(self.options.spacing))
|
||||
.label("Spacing")
|
||||
.min(1.)
|
||||
.max(100.)
|
||||
.mode_range()
|
||||
.unit("%")
|
||||
.on_update(|number_input: &NumberInput| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::Spacing(number_input.value.unwrap()),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
.widget_instance(),
|
||||
];
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||
|
||||
let draw_mode_entries: Vec<_> = [DrawMode::Draw, DrawMode::Erase, DrawMode::Restore]
|
||||
.into_iter()
|
||||
.map(|draw_mode| {
|
||||
RadioEntryData::new(format!("{draw_mode:?}")).label(format!("{draw_mode:?}")).on_update(move |_| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::DrawMode(draw_mode),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
widgets.push(RadioInput::new(draw_mode_entries).selected_index(Some(self.options.draw_mode as u32)).widget_instance());
|
||||
|
||||
widgets.push(Separator::new(SeparatorStyle::Related).widget_instance());
|
||||
|
||||
let blend_mode_entries: Vec<Vec<_>> = BlendMode::list()
|
||||
.iter()
|
||||
.map(|section| {
|
||||
section
|
||||
.iter()
|
||||
.map(|blend_mode| {
|
||||
MenuListEntry::new(format!("{blend_mode:?}")).label(blend_mode.to_string()).on_commit(|_| {
|
||||
BrushToolMessage::UpdateOptions {
|
||||
options: BrushToolMessageOptionsUpdate::BlendMode(*blend_mode),
|
||||
}
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
widgets.push(
|
||||
DropdownInput::new(blend_mode_entries)
|
||||
.selected_index(self.options.blend_mode.index_in_list().map(|index| index as u32))
|
||||
.tooltip_description("The blend mode used with the background when performing a brush stroke. Only used in draw mode.")
|
||||
.disabled(self.options.draw_mode != DrawMode::Draw)
|
||||
.widget_instance(),
|
||||
);
|
||||
|
||||
Layout(vec![LayoutGroup::row(widgets)])
|
||||
}
|
||||
}
|
||||
@@ -215,12 +158,18 @@ impl LayoutHolder for BrushTool {
|
||||
#[message_handler_data]
|
||||
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for BrushTool {
|
||||
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
|
||||
if matches!(&message, ToolMessage::Brush(BrushToolMessage::SelectionChanged)) {
|
||||
if self.fsm_state == BrushToolFsmState::Ready && selection_changed_since_last_sync(&mut self.options.last_synced_selection, context.document) {
|
||||
self.sync_options_from_selection(context.document, responses);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let ToolMessage::Brush(BrushToolMessage::UpdateOptions { options }) = message else {
|
||||
self.fsm_state.process_event(message, &mut self.data, context, &self.options, responses, true);
|
||||
return;
|
||||
};
|
||||
match options {
|
||||
BrushToolMessageOptionsUpdate::BlendMode(blend_mode) => self.options.blend_mode = blend_mode,
|
||||
BrushToolMessageOptionsUpdate::ChangeDiameter(change) => {
|
||||
let needs_rounding = ((self.options.diameter + change.abs() / 2.) % change.abs() - change.abs() / 2.).abs() > 0.5;
|
||||
if needs_rounding && change > 0. {
|
||||
@@ -234,12 +183,9 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Brus
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
BrushToolMessageOptionsUpdate::Diameter(diameter) => self.options.diameter = diameter,
|
||||
BrushToolMessageOptionsUpdate::DrawMode(draw_mode) => self.options.draw_mode = draw_mode,
|
||||
BrushToolMessageOptionsUpdate::Hardness(hardness) => self.options.hardness = hardness,
|
||||
BrushToolMessageOptionsUpdate::Flow(flow) => self.options.flow = flow,
|
||||
BrushToolMessageOptionsUpdate::Spacing(spacing) => self.options.spacing = spacing,
|
||||
BrushToolMessageOptionsUpdate::Color(color) => {
|
||||
// User picked a color: push to the global primary working color (no tool-local customization).
|
||||
if let Some(color) = color {
|
||||
responses.add(ToolMessage::SelectWorkingColor { color, primary: true });
|
||||
}
|
||||
@@ -273,30 +219,115 @@ impl ToolTransition for BrushTool {
|
||||
fn event_to_message_map(&self) -> EventToMessageMap {
|
||||
EventToMessageMap {
|
||||
tool_abort: Some(BrushToolMessage::Abort.into()),
|
||||
selection_changed: Some(BrushToolMessage::SelectionChanged.into()),
|
||||
working_color_changed: Some(BrushToolMessage::WorkingColorChanged.into()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BrushTool {
|
||||
fn sync_options_from_selection(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||
let Some(strokes_node) = selected_strokes_node(document) else { return };
|
||||
let Some(node) = document.network_interface.document_network().nodes.get(&strokes_node) else {
|
||||
return;
|
||||
};
|
||||
let value = |index: usize| node.inputs.get(index).and_then(|input| input.as_value());
|
||||
if let Some(TaggedValue::F64(diameter)) = value(STROKES_DIAMETER_INPUT) {
|
||||
self.options.diameter = *diameter;
|
||||
}
|
||||
if let Some(TaggedValue::F64(hardness)) = value(STROKES_HARDNESS_INPUT) {
|
||||
self.options.hardness = *hardness;
|
||||
}
|
||||
if let Some(TaggedValue::F64(flow)) = value(STROKES_FLOW_INPUT) {
|
||||
self.options.flow = *flow;
|
||||
}
|
||||
if let Some(TaggedValue::Color(color)) = value(STROKES_COLOR_INPUT)
|
||||
&& *color != self.options.active_color()
|
||||
{
|
||||
responses.add(ToolMessage::SelectWorkingColor { color: *color, primary: true });
|
||||
} else {
|
||||
self.send_layout(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const STROKES_COLOR_INPUT: usize = 1;
|
||||
const STROKES_DIAMETER_INPUT: usize = 2;
|
||||
const STROKES_HARDNESS_INPUT: usize = 3;
|
||||
const STROKES_FLOW_INPUT: usize = 4;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct BrushToolData {
|
||||
strokes: Vec<BrushStroke>,
|
||||
stroke: Stroke,
|
||||
stroke_node_id: Option<NodeId>,
|
||||
strokes_before: Vec<Stroke>,
|
||||
layer: Option<LayerNodeIdentifier>,
|
||||
transform: DAffine2,
|
||||
last_sample: (DVec2, Option<f64>),
|
||||
}
|
||||
|
||||
enum BrushTarget {
|
||||
Existing { strokes_node_id: NodeId, strokes: Vec<Stroke> },
|
||||
NewGroup { parent: LayerNodeIdentifier, insert_index: usize },
|
||||
FillEmpty { layer: LayerNodeIdentifier },
|
||||
}
|
||||
|
||||
impl BrushToolData {
|
||||
fn load_existing_strokes(&mut self, document: &DocumentMessageHandler) -> Option<LayerNodeIdentifier> {
|
||||
self.transform = DAffine2::IDENTITY;
|
||||
fn resolve_target(&mut self, document: &DocumentMessageHandler, options: &BrushOptions) -> Option<(LayerNodeIdentifier, BrushTarget)> {
|
||||
self.layer = None;
|
||||
|
||||
if document.network_interface.selected_nodes().selected_layers(document.metadata()).count() != 1 {
|
||||
let selected_nodes = document.network_interface.selected_nodes();
|
||||
let mut selected_layers = selected_nodes.selected_layers(document.metadata());
|
||||
let selected_layer = selected_layers.next().filter(|_| selected_layers.next().is_none())?;
|
||||
|
||||
if self.load_brush_layer(document, selected_layer) {
|
||||
return Some((
|
||||
selected_layer,
|
||||
BrushTarget::NewGroup {
|
||||
parent: selected_layer,
|
||||
insert_index: 0,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let parent = selected_layer.parent(document.metadata()).filter(|&parent| parent != LayerNodeIdentifier::ROOT_PARENT)?;
|
||||
if !self.load_brush_layer(document, parent) {
|
||||
return None;
|
||||
}
|
||||
let layer = document.network_interface.selected_nodes().selected_layers(document.metadata()).next()?;
|
||||
|
||||
self.layer = Some(layer);
|
||||
for node_id in document.network_interface.upstream_flow_back_from_nodes(vec![layer.to_node()], &[], FlowType::HorizontalFlow) {
|
||||
let Some(output) = document.network_interface.upstream_output_connector(&InputConnector::node_at_index(selected_layer.to_node(), 1), &[]) else {
|
||||
return Some((parent, BrushTarget::FillEmpty { layer: selected_layer }));
|
||||
};
|
||||
|
||||
let new_group = || {
|
||||
let insert_index = parent.children(document.metadata()).position(|child| child == selected_layer).unwrap_or_default();
|
||||
BrushTarget::NewGroup { parent, insert_index }
|
||||
};
|
||||
let OutputConnector::Node { node_id: strokes_node_id, .. } = output else {
|
||||
return Some((parent, new_group()));
|
||||
};
|
||||
if document.network_interface.reference(&strokes_node_id, &[]) != Some(DefinitionIdentifier::ProtoNode(graphene_std::brush::brush_strokes::IDENTIFIER)) {
|
||||
return Some((parent, new_group()));
|
||||
}
|
||||
let strokes = document
|
||||
.network_interface
|
||||
.document_network()
|
||||
.nodes
|
||||
.get(&strokes_node_id)
|
||||
.and_then(|node| node.inputs.first())
|
||||
.and_then(|input| input.as_value())
|
||||
.and_then(|value| if let TaggedValue::Strokes(strokes) = value { Some(strokes.clone()) } else { None });
|
||||
match strokes {
|
||||
Some(strokes) if Self::style_matches(document, strokes_node_id, options) => Some((parent, BrushTarget::Existing { strokes_node_id, strokes })),
|
||||
_ => Some((parent, new_group())),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_brush_layer(&mut self, document: &DocumentMessageHandler, candidate: LayerNodeIdentifier) -> bool {
|
||||
self.transform = DAffine2::IDENTITY;
|
||||
|
||||
for node_id in document.network_interface.upstream_flow_back_from_nodes(vec![candidate.to_node()], &[], FlowType::HorizontalFlow) {
|
||||
let Some(node) = document.network_interface.document_network().nodes.get(&node_id) else {
|
||||
continue;
|
||||
};
|
||||
@@ -304,12 +335,9 @@ impl BrushToolData {
|
||||
continue;
|
||||
};
|
||||
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::brush::brush::brush::IDENTIFIER) && node_id != layer.to_node() {
|
||||
let points_input = node.inputs.get(1)?;
|
||||
let Some(TaggedValue::BrushStrokes(strokes)) = points_input.as_value() else { continue };
|
||||
self.strokes = strokes.clone();
|
||||
|
||||
return Some(layer);
|
||||
if reference == DefinitionIdentifier::ProtoNode(active_brush::IDENTIFIER) && node_id != candidate.to_node() {
|
||||
self.layer = Some(candidate);
|
||||
return true;
|
||||
}
|
||||
|
||||
if reference == DefinitionIdentifier::ProtoNode(graphene_std::transform_nodes::transform::IDENTIFIER) {
|
||||
@@ -318,13 +346,39 @@ impl BrushToolData {
|
||||
}
|
||||
|
||||
self.transform = DAffine2::IDENTITY;
|
||||
None
|
||||
false
|
||||
}
|
||||
|
||||
fn update_strokes(&self, responses: &mut VecDeque<Message>) {
|
||||
let Some(layer) = self.layer else { return };
|
||||
let strokes = self.strokes.clone();
|
||||
responses.add(GraphOperationMessage::Brush { layer, strokes });
|
||||
fn style_matches(document: &DocumentMessageHandler, strokes_node: NodeId, options: &BrushOptions) -> bool {
|
||||
let Some(node) = document.network_interface.document_network().nodes.get(&strokes_node) else {
|
||||
return false;
|
||||
};
|
||||
let value = |index: usize| node.inputs.get(index).and_then(|input| input.as_value());
|
||||
matches!(value(STROKES_COLOR_INPUT), Some(TaggedValue::Color(color)) if *color == options.active_color())
|
||||
&& matches!(value(STROKES_DIAMETER_INPUT), Some(TaggedValue::F64(diameter)) if *diameter == options.diameter)
|
||||
&& matches!(value(STROKES_HARDNESS_INPUT), Some(TaggedValue::F64(hardness)) if *hardness == options.hardness)
|
||||
&& matches!(value(STROKES_FLOW_INPUT), Some(TaggedValue::F64(flow)) if *flow == options.flow)
|
||||
}
|
||||
|
||||
fn push_sample(&mut self, position: DVec2, pressure: Option<f64>, elapsed_milliseconds: f64) {
|
||||
self.stroke.position.push(position);
|
||||
if let Channel::Samples(times) = &mut self.stroke.time {
|
||||
times.push(elapsed_milliseconds / 1000.);
|
||||
}
|
||||
if let Channel::Samples(pressures) = &mut self.stroke.pressure {
|
||||
pressures.push(pressure.unwrap_or(1.) as f32);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_stroke(&self, responses: &mut VecDeque<Message>) {
|
||||
let Some(stroke_node_id) = self.stroke_node_id else { return };
|
||||
let mut strokes = self.strokes_before.clone();
|
||||
strokes.push(self.stroke.clone());
|
||||
responses.add(NodeGraphMessage::SetInputValue {
|
||||
node_id: stroke_node_id,
|
||||
input_index: 0,
|
||||
value: TaggedValue::Strokes(strokes).into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,60 +400,96 @@ impl Fsm for BrushToolFsmState {
|
||||
match (self, event) {
|
||||
(BrushToolFsmState::Ready, BrushToolMessage::DragStart) => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
let loaded_layer = tool_data.load_existing_strokes(document);
|
||||
|
||||
if let Some(layer) = loaded_layer {
|
||||
let pos = document
|
||||
.network_interface
|
||||
.document_metadata()
|
||||
.downstream_transform_to_viewport(layer)
|
||||
.inverse()
|
||||
.transform_point2(input.mouse.position);
|
||||
let layer_position = tool_data.transform.inverse().transform_point2(pos);
|
||||
let layer_document_scale = document.metadata().downstream_transform_to_viewport(layer) * tool_data.transform;
|
||||
|
||||
// TODO: Also scale it based on the input image ('Background' input).
|
||||
// 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((layer_document_scale.matrix2 * glam::DVec2::X).length())
|
||||
.max((layer_document_scale.matrix2 * glam::DVec2::Y).length());
|
||||
|
||||
// Start a new stroke with a single sample
|
||||
let blend_mode = match tool_options.draw_mode {
|
||||
DrawMode::Draw => tool_options.blend_mode,
|
||||
DrawMode::Erase => BlendMode::Erase,
|
||||
DrawMode::Restore => BlendMode::Restore,
|
||||
};
|
||||
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,
|
||||
blend_mode,
|
||||
},
|
||||
});
|
||||
|
||||
tool_data.update_strokes(responses);
|
||||
BrushToolFsmState::Drawing
|
||||
}
|
||||
// Create the new layer, wait for the render output to return its transform, and then create the rest of the layer
|
||||
else {
|
||||
// A new brush layer needs a graph run before the stroke can start.
|
||||
let Some((brush_layer, target)) = tool_data.resolve_target(document, tool_options) else {
|
||||
new_brush_layer(document, responses);
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
responses.add(DeferMessage::AfterGraphRun {
|
||||
messages: vec![BrushToolMessage::DragStart.into()],
|
||||
});
|
||||
BrushToolFsmState::Ready
|
||||
return BrushToolFsmState::Ready;
|
||||
};
|
||||
|
||||
let pos = document
|
||||
.network_interface
|
||||
.document_metadata()
|
||||
.downstream_transform_to_viewport(brush_layer)
|
||||
.inverse()
|
||||
.transform_point2(input.mouse.position);
|
||||
let layer_position = tool_data.transform.inverse().transform_point2(pos);
|
||||
|
||||
let pressure = input.mouse.pressure;
|
||||
tool_data.stroke = Stroke {
|
||||
time: Channel::Samples(Vec::new()),
|
||||
seed: generate_uuid(),
|
||||
..Default::default()
|
||||
};
|
||||
if pressure.is_some() {
|
||||
tool_data.stroke.pressure = Channel::Samples(Vec::new());
|
||||
}
|
||||
tool_data.push_sample(layer_position, pressure, input.mouse.time.unwrap_or(0.));
|
||||
tool_data.last_sample = (input.mouse.position, pressure);
|
||||
|
||||
match target {
|
||||
BrushTarget::Existing { strokes_node_id, strokes } => {
|
||||
tool_data.stroke_node_id = Some(strokes_node_id);
|
||||
tool_data.strokes_before = strokes;
|
||||
}
|
||||
BrushTarget::NewGroup { parent, insert_index } => {
|
||||
let group_id = NodeId::new();
|
||||
let strokes_node_id = NodeId::new();
|
||||
tool_data.stroke_node_id = Some(strokes_node_id);
|
||||
tool_data.strokes_before = Vec::new();
|
||||
responses.add(GraphOperationMessage::NewBrushGroupLayer {
|
||||
id: group_id,
|
||||
strokes_node_id,
|
||||
parent,
|
||||
insert_index,
|
||||
color: tool_options.active_color(),
|
||||
diameter: tool_options.diameter,
|
||||
hardness: tool_options.hardness,
|
||||
flow: tool_options.flow,
|
||||
});
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![group_id] });
|
||||
}
|
||||
BrushTarget::FillEmpty { layer } => {
|
||||
let strokes_node_id = NodeId::new();
|
||||
tool_data.stroke_node_id = Some(strokes_node_id);
|
||||
tool_data.strokes_before = Vec::new();
|
||||
responses.add(GraphOperationMessage::NewBrushStrokesNode {
|
||||
layer,
|
||||
strokes_node_id,
|
||||
color: tool_options.active_color(),
|
||||
diameter: tool_options.diameter,
|
||||
hardness: tool_options.hardness,
|
||||
flow: tool_options.flow,
|
||||
});
|
||||
}
|
||||
}
|
||||
tool_data.update_stroke(responses);
|
||||
|
||||
BrushToolFsmState::Drawing
|
||||
}
|
||||
|
||||
(BrushToolFsmState::Drawing, BrushToolMessage::PointerMove) => {
|
||||
if let Some(layer) = tool_data.layer
|
||||
&& let Some(stroke) = tool_data.strokes.last_mut()
|
||||
{
|
||||
let pressure = input.mouse.pressure;
|
||||
|
||||
if pressure == Some(0.) {
|
||||
return BrushToolFsmState::Drawing;
|
||||
}
|
||||
|
||||
let (last_position, last_pressure) = tool_data.last_sample;
|
||||
let moved = input.mouse.position.distance(last_position) >= SAMPLE_MERGE_DISTANCE;
|
||||
let pressure_changed = match (pressure, last_pressure) {
|
||||
(Some(pressure), Some(last_pressure)) => (pressure - last_pressure).abs() >= SAMPLE_MERGE_PRESSURE,
|
||||
(pressure, last_pressure) => pressure.is_some() != last_pressure.is_some(),
|
||||
};
|
||||
if !moved && !pressure_changed {
|
||||
return BrushToolFsmState::Drawing;
|
||||
}
|
||||
|
||||
if let Some(layer) = tool_data.layer {
|
||||
let layer_position = document
|
||||
.network_interface
|
||||
.document_metadata()
|
||||
@@ -408,26 +498,31 @@ impl Fsm for BrushToolFsmState {
|
||||
.transform_point2(input.mouse.position);
|
||||
let layer_position = tool_data.transform.inverse().transform_point2(layer_position);
|
||||
|
||||
stroke.trace.push(BrushInputSample { position: layer_position })
|
||||
tool_data.push_sample(layer_position, pressure, input.mouse.time.unwrap_or(0.));
|
||||
tool_data.last_sample = (input.mouse.position, pressure);
|
||||
}
|
||||
tool_data.update_strokes(responses);
|
||||
tool_data.update_stroke(responses);
|
||||
|
||||
BrushToolFsmState::Drawing
|
||||
}
|
||||
|
||||
(BrushToolFsmState::Drawing, BrushToolMessage::DragStop) => {
|
||||
if !tool_data.strokes.is_empty() {
|
||||
if tool_data.stroke_node_id.is_some() {
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
} else {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
}
|
||||
tool_data.strokes.clear();
|
||||
tool_data.stroke_node_id = None;
|
||||
tool_data.stroke = Stroke::default();
|
||||
tool_data.strokes_before = Vec::new();
|
||||
|
||||
BrushToolFsmState::Ready
|
||||
}
|
||||
(BrushToolFsmState::Drawing, BrushToolMessage::Abort) => {
|
||||
responses.add(DocumentMessage::AbortTransaction);
|
||||
tool_data.strokes.clear();
|
||||
tool_data.stroke_node_id = None;
|
||||
tool_data.stroke = Stroke::default();
|
||||
tool_data.strokes_before = Vec::new();
|
||||
|
||||
BrushToolFsmState::Ready
|
||||
}
|
||||
@@ -458,12 +553,40 @@ impl Fsm for BrushToolFsmState {
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_strokes_node(document: &DocumentMessageHandler) -> Option<NodeId> {
|
||||
let selected_nodes = document.network_interface.selected_nodes();
|
||||
let mut selected_layers = selected_nodes.selected_layers(document.metadata());
|
||||
let selected_layer = selected_layers.next().filter(|_| selected_layers.next().is_none())?;
|
||||
|
||||
let group = if is_brush_layer(document, selected_layer) {
|
||||
selected_layer.children(document.metadata()).next()?
|
||||
} else {
|
||||
let parent = selected_layer.parent(document.metadata()).filter(|&parent| parent != LayerNodeIdentifier::ROOT_PARENT)?;
|
||||
if !is_brush_layer(document, parent) {
|
||||
return None;
|
||||
}
|
||||
selected_layer
|
||||
};
|
||||
|
||||
let OutputConnector::Node { node_id, .. } = document.network_interface.upstream_output_connector(&InputConnector::node_at_index(group.to_node(), 1), &[])? else {
|
||||
return None;
|
||||
};
|
||||
(document.network_interface.reference(&node_id, &[]) == Some(DefinitionIdentifier::ProtoNode(graphene_std::brush::brush_strokes::IDENTIFIER))).then_some(node_id)
|
||||
}
|
||||
|
||||
fn is_brush_layer(document: &DocumentMessageHandler, candidate: LayerNodeIdentifier) -> bool {
|
||||
document
|
||||
.network_interface
|
||||
.upstream_flow_back_from_nodes(vec![candidate.to_node()], &[], FlowType::HorizontalFlow)
|
||||
.any(|node_id| node_id != candidate.to_node() && document.network_interface.reference(&node_id, &[]) == Some(DefinitionIdentifier::ProtoNode(active_brush::IDENTIFIER)))
|
||||
}
|
||||
|
||||
fn new_brush_layer(document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> LayerNodeIdentifier {
|
||||
responses.add(DocumentMessage::DeselectAllLayers);
|
||||
|
||||
let brush_node = resolve_proto_node_type(graphene_std::brush::brush::brush::IDENTIFIER)
|
||||
let brush_node = resolve_proto_node_type(active_brush::IDENTIFIER)
|
||||
.expect("Brush node does not exist")
|
||||
.default_node_template();
|
||||
.node_template_input_override([None, Some(NodeInput::value(TaggedValue::BrushCache(Default::default()), false))]);
|
||||
|
||||
let id = NodeId::new();
|
||||
responses.add(GraphOperationMessage::NewCustomLayer {
|
||||
|
||||
Reference in New Issue
Block a user