Skip to content
Closed
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 {
fills: Vec<Fill>,
start_pos: Vec<DVec2>,
tolerance: 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?
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
1 change: 1 addition & 0 deletions node-graph/gcore/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ wasm-bindgen = { workspace = true, optional = true }
js-sys = { workspace = true, optional = true }
web-sys = { workspace = true, optional = true, features = [
"HtmlCanvasElement",
"console",
] }
image = { workspace = true, optional = true, default-features = false, features = [
"png",
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
59 changes: 59 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,65 @@ 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 {
let lab1 = self.to_lab();
let lab2 = other.to_lab();
let distance = self.lab_distance(other).min(100.);
let alpha_diff = (self.a() - other.a()).abs();
distance <= threshold && alpha_diff < f32::EPSILON
}
}

#[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