Skip to content

Allow the Fill tool to flood fill raster images #2519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ use graphene_core::{Artboard, Color};
#[impl_message(Message, DocumentMessage, GraphOperation)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum GraphOperationMessage {
FillRaster {
layer: LayerNodeIdentifier,
fills: Vec<Fill>,
start_pos: Vec<DVec2>,
tolerance: f64,
},
FillSet {
layer: LayerNodeIdentifier,
fill: Fill,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
let network_interface = data.network_interface;

match message {
GraphOperationMessage::FillRaster { layer, fills, start_pos, tolerance } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.fill_raster(fills, start_pos, tolerance);
}
}
GraphOperationMessage::FillSet { layer, fill } => {
if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) {
modify_inputs.fill_set(fill);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,13 @@ impl<'a> ModifyInputsContext<'a> {
self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false);
}

pub fn fill_raster(&mut self, fills: Vec<Fill>, start_pos: Vec<DVec2>, tolerance: f64) {
let Some(raster_fill_node_id) = self.existing_node_id("Flood Fill", true) else { return };
self.set_input_with_refresh(InputConnector::node(raster_fill_node_id, 1), NodeInput::value(TaggedValue::VecFill(fills), false), false);
self.set_input_with_refresh(InputConnector::node(raster_fill_node_id, 2), NodeInput::value(TaggedValue::VecDVec2(start_pos), false), false);
self.set_input_with_refresh(InputConnector::node(raster_fill_node_id, 3), NodeInput::value(TaggedValue::F64(tolerance), false), false);
}

pub fn stroke_set(&mut self, stroke: Stroke) {
let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return };

Expand Down
115 changes: 92 additions & 23 deletions editor/src/messages/tool/tool_messages/fill_tool.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use super::tool_prelude::*;
use crate::messages::portfolio::document::graph_operation::transform_utils::get_current_transform;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use graphene_core::vector::style::Fill;
use graph_craft::document::value::TaggedValue;
use graphene_std::vector::style::Fill;

#[derive(Default)]
pub struct FillTool {
fsm_state: FillToolFsmState,
Expand Down Expand Up @@ -38,7 +42,8 @@ impl LayoutHolder for FillTool {

impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for FillTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
self.fsm_state.process_event(message, &mut (), tool_data, &(), responses, true);
let raster_fill_tool_data = &mut FillToolData::default();
self.fsm_state.process_event(message, raster_fill_tool_data, tool_data, &(), responses, true);
}
fn actions(&self) -> ActionList {
match self.fsm_state {
Expand Down Expand Up @@ -71,37 +76,111 @@ enum FillToolFsmState {
Filling,
}

#[derive(Clone, Debug, Default)]
struct FillToolData {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to set the tolerance in the tool's control bar. It should range from 0 (only the identically colored pixels) to 1 (all pixels regardless of color). We should also have a checkbox for contiguous.

fills: Vec<Fill>,
start_pos: Vec<DVec2>,
tolerance: f64,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tolerance: f64,
tolerance: Vec<f64>,

}

impl FillToolData {
fn load_existing_fills(&mut self, document: &mut DocumentMessageHandler, layer_identifier: LayerNodeIdentifier) -> Option<LayerNodeIdentifier> {
let node_graph_layer = NodeGraphLayer::new(layer_identifier, &document.network_interface);
let existing_fills = node_graph_layer.find_node_inputs("Flood Fill");

if let Some(existing_fills) = existing_fills {
let fills = if let Some(TaggedValue::VecFill(fills)) = existing_fills[1].as_value().cloned() {
fills
} else {
Vec::new()
};
let start_pos = if let Some(TaggedValue::VecDVec2(start_pos)) = existing_fills[2].as_value().cloned() {
start_pos
} else {
Vec::new()
};
let tolerance = if let Some(TaggedValue::F64(tolerance)) = existing_fills[3].as_value().cloned() {
tolerance
} else {
1.
};

*self = Self { fills, start_pos, tolerance };
}

// TODO: Why do we overwrite the tolerance that we just set a couple lines above?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.tolerance = 1.;

None
}
}

impl Fsm for FillToolFsmState {
type ToolData = ();
type ToolData = FillToolData;
type ToolOptions = ();

fn transition(self, event: ToolMessage, _tool_data: &mut Self::ToolData, handler_data: &mut ToolActionHandlerData, _tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, handler_data: &mut ToolActionHandlerData, _tool_options: &Self::ToolOptions, responses: &mut VecDeque<Message>) -> Self {
let ToolActionHandlerData {
document, global_tool_data, input, ..
} = handler_data;

let ToolMessage::Fill(event) = event else { return self };
match (self, event) {
(FillToolFsmState::Ready, color_event) => {
let Some(layer_identifier) = document.click(input) else {
return self;
};
// If the layer is a raster layer, don't fill it, wait till the flood fill tool is implemented
if NodeGraphLayer::is_raster_layer(layer_identifier, &mut document.network_interface) {
return self;
}
let Some(layer_identifier) = document.click(input) else { return self };
let fill = match color_event {
FillToolMessage::FillPrimaryColor => Fill::Solid(global_tool_data.primary_color.to_gamma_srgb()),
FillToolMessage::FillSecondaryColor => Fill::Solid(global_tool_data.secondary_color.to_gamma_srgb()),
_ => return self,
};

responses.add(DocumentMessage::AddTransaction);
responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill });

// If the layer is a raster layer, we perform a flood fill
if NodeGraphLayer::is_raster_layer(layer_identifier, &mut document.network_interface) {
// Try to load existing fills for this layer
tool_data.load_existing_fills(document, layer_identifier);

// Get position in layer space
let layer_pos = document
.network_interface
.document_metadata()
.downstream_transform_to_viewport(layer_identifier)
.inverse()
.transform_point2(input.mouse.position);

let node_graph_layer = NodeGraphLayer::new(layer_identifier, &document.network_interface);
if let Some(transform_inputs) = node_graph_layer.find_node_inputs("Transform") {
let image_transform = get_current_transform(transform_inputs);
let image_local_pos = image_transform.inverse().transform_point2(layer_pos);

// Store the fill in our tool data with its position
tool_data.fills.push(fill.clone());
tool_data.start_pos.push(image_local_pos);
}

// Send the fill operation message
responses.add(GraphOperationMessage::FillRaster {
layer: layer_identifier,
fills: tool_data.fills.clone(),
start_pos: tool_data.start_pos.clone(),
tolerance: tool_data.tolerance,
});
}
// Otherwise the layer is assumed to be a vector layer, so we apply a vector fill
else {
responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill });
}

FillToolFsmState::Filling
}
(FillToolFsmState::Filling, FillToolMessage::PointerUp) => FillToolFsmState::Ready,
(FillToolFsmState::Filling, FillToolMessage::PointerUp) => {
// Clear the `fills` and `start_pos` data when we're done
tool_data.fills.clear();
tool_data.start_pos.clear();

FillToolFsmState::Ready
}
(FillToolFsmState::Filling, FillToolMessage::Abort) => {
responses.add(DocumentMessage::AbortTransaction);

Expand Down Expand Up @@ -136,7 +215,6 @@ mod test_fill {

async fn get_fills(editor: &mut EditorTestUtils) -> Vec<Fill> {
let instrumented = editor.eval_graph().await;

instrumented.grab_all_input::<fill::FillInput<Fill>>(&editor.runtime).collect()
}

Expand All @@ -149,15 +227,6 @@ mod test_fill {
assert!(get_fills(&mut editor,).await.is_empty());
}

#[tokio::test]
async fn ignore_raster() {
let mut editor = EditorTestUtils::create();
editor.new_document().await;
editor.create_raster_image(Image::new(100, 100, Color::WHITE), Some((0., 0.))).await;
editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await;
assert!(get_fills(&mut editor,).await.is_empty());
}

#[tokio::test]
async fn primary() {
let mut editor = EditorTestUtils::create();
Expand Down
2 changes: 1 addition & 1 deletion node-graph/gcore/src/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy
#[default(2.)]
#[implementations(f64, f64, &f64, &f64, f32, f32, &f32, &f32, u32, u32, &u32, &u32, DVec2, f64, DVec2)]
modulus: T,
always_positive: bool,
#[default(true)] always_positive: bool,
) -> <U as Rem<T>>::Output {
if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus }
}
Expand Down
55 changes: 55 additions & 0 deletions node-graph/gcore/src/raster/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1049,6 +1049,61 @@ impl Color {
..*self
}
}

/// Convert RGB to XYZ color space
fn to_xyz(self) -> [f64; 3] {
let r = self.red as f64;
let g = self.green as f64;
let b = self.blue as f64;

// sRGB to XYZ conversion matrix
let x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375;
let y = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
let z = r * 0.0193339 + g * 0.1191920 + b * 0.9503041;

[x, y, z]
}

/// Convert XYZ to LAB color space
fn xyz_to_lab(xyz: [f64; 3]) -> [f64; 3] {
// D65 illuminant reference values
let xn = 0.950489;
let yn = 1.;
let zn = 1.088840;

let x = xyz[0];
let y = xyz[1];
let z = xyz[2];

let fx = if x / xn > 0.008856 { (x / xn).powf(1. / 3.) } else { (903.3 * x / xn + 16.) / 116. };
let fy = if y / yn > 0.008856 { (y / yn).powf(1. / 3.) } else { (903.3 * y / yn + 16.) / 116. };
let fz = if z / zn > 0.008856 { (z / zn).powf(1. / 3.) } else { (903.3 * z / zn + 16.) / 116. };

let l = 116. * fy - 16.;
let a = 500. * (fx - fy);
let b = 200. * (fy - fz);

[l, a, b]
}

/// Convert RGB to LAB color space
pub fn to_lab(&self) -> [f64; 3] {
Self::xyz_to_lab(self.to_xyz())
}

/// Calculate the distance between two colors in LAB space
pub fn lab_distance_squared(&self, other: &Color) -> f64 {
let lab1 = self.to_lab();
let lab2 = other.to_lab();

// Euclidean distance in LAB space
(lab1[0] - lab2[0]).powi(2) + (lab1[1] - lab2[1]).powi(2) + (lab1[2] - lab2[2]).powi(2)
}

/// Check if two colors are similar within a threshold in LAB space
pub fn is_similar_lab(&self, other: &Color, threshold: f64) -> bool {
(self.alpha - other.alpha).abs() <= 0.01 && self.lab_distance_squared(other) <= threshold.powi(2)
}
}

#[test]
Expand Down
5 changes: 4 additions & 1 deletion node-graph/gcore/src/raster/discrete_srgb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ mod tests {
#[test]
fn test_float_to_srgb_u8() {
for u in 0..=u8::MAX {
assert!(srgb_u8_to_float(u) == srgb_u8_to_float_ref(u));
let float_val = srgb_u8_to_float(u);
let ref_val = srgb_u8_to_float_ref(u);
// Allow for a small epsilon difference due to floating-point precision
assert!((float_val - ref_val).abs() < 1e-5);
}
}

Expand Down
7 changes: 7 additions & 0 deletions node-graph/graph-craft/src/document/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ tagged_value! {
// ImaginateMaskStartingFill(ImaginateMaskStartingFill),
// ImaginateController(ImaginateController),
Fill(graphene_core::vector::style::Fill),
VecFill(Vec<graphene_core::vector::style::Fill>),
Stroke(graphene_core::vector::style::Stroke),
F64Array4([f64; 4]),
// TODO: Eventually remove this alias document upgrade code
Expand Down Expand Up @@ -426,4 +427,10 @@ mod fake_hash {
self.1.hash(state)
}
}
impl<T: FakeHash, U: FakeHash> FakeHash for (T, U) {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.hash(state);
self.1.hash(state);
}
}
}
Loading
Loading