From cc297a5b9b58493baf3a0e0f00717ce59643757f Mon Sep 17 00:00:00 2001 From: Yiyin Gu Date: Sat, 22 Mar 2025 23:02:29 -0400 Subject: [PATCH 1/8] Add raster fill node to graph --- .../graph_operation_message.rs | 5 ++ .../graph_operation_message_handler.rs | 5 ++ .../document/graph_operation/utility_types.rs | 8 +++ .../messages/tool/tool_messages/fill_tool.rs | 15 +++-- node-graph/gstd/src/raster.rs | 62 +++++++++++++++++++ 5 files changed, 90 insertions(+), 5 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index a6e3fb38e9..6f64552810 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -21,6 +21,11 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, fill: Fill, }, + RasterFillSet { + layer: LayerNodeIdentifier, + fill: Fill, + position: DVec2, + }, OpacitySet { layer: LayerNodeIdentifier, opacity: f64, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index b65968fcc3..06bb54d1a0 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -41,6 +41,11 @@ impl MessageHandler> for Gr modify_inputs.fill_set(fill); } } + GraphOperationMessage::RasterFillSet { layer, fill, position } => { + if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { + modify_inputs.raster_fill_set(fill, position); + } + } GraphOperationMessage::OpacitySet { layer, opacity } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { modify_inputs.opacity_set(opacity); diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index aec230d880..9d5d42cd4b 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -347,6 +347,14 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false); } + pub fn raster_fill_set(&mut self, fill: Fill, position: DVec2) { + let Some(raster_fill_node_id) = self.existing_node_id("Raster Fill", true) else { return }; + let input_connector_1 = InputConnector::node(raster_fill_node_id, 1); + self.set_input_with_refresh(input_connector_1, NodeInput::value(TaggedValue::Fill(fill), false), false); + let input_connector_2 = InputConnector::node(raster_fill_node_id, 2); + self.set_input_with_refresh(input_connector_2, NodeInput::value(TaggedValue::DVec2(position), false), false); + } + pub fn stroke_set(&mut self, stroke: Stroke) { let Some(stroke_node_id) = self.existing_node_id("Stroke", true) else { return }; diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index fe61621373..cf9c1c6185 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -86,10 +86,6 @@ impl Fsm for FillToolFsmState { 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 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()), @@ -97,7 +93,16 @@ impl Fsm for FillToolFsmState { }; responses.add(DocumentMessage::AddTransaction); - responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill }); + // If the layer is a raster layer, use the raster fill command instead + if NodeGraphLayer::is_raster_layer(layer_identifier, &mut document.network_interface) { + responses.add(GraphOperationMessage::RasterFillSet { + layer: layer_identifier, + fill, + position: input.mouse.position, + }); + } else { + responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill }); + } FillToolFsmState::Filling } diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 8384c2e923..3f4417a898 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -7,6 +7,7 @@ use graphene_core::raster::{ Alpha, AlphaMut, Bitmap, BitmapMut, CellularDistanceFunction, CellularReturnType, DomainWarpType, FractalType, Linear, LinearChannel, Luminance, NoiseType, Pixel, RGBMut, RedGreenBlue, Sample, }; use graphene_core::transform::{Transform, TransformMut}; +use graphene_core::vector::style::{Fill, Gradient}; use graphene_core::{AlphaBlending, Color, Ctx, ExtractFootprint, GraphicElement, Node}; use rand::prelude::*; use rand_chacha::ChaCha8Rng; @@ -684,6 +685,67 @@ fn mandelbrot_impl(c: Vec2, max_iter: usize) -> usize { max_iter } +#[node_macro::node(category("Raster"))] +fn raster_fill + 'n + Send>( + _: impl Ctx, + #[implementations(ImageFrameTable)] + /// The vector elements, or group of vector elements, to apply the fill to. + mut image: ImageFrameTable, + #[implementations( + Fill, + Option, + Color, + Gradient, + )] + #[default(Color::BLACK)] + /// The fill to paint the path with. + fill: F, + /// The position of the fill in the image. + position: DVec2, +) -> ImageFrameTable { + let width = image.width(); + let height = image.height(); + + if width == 0 || height == 0 { + return image; + } + + // Transform the global position to local image space + let image_transform = image.transform(); + let image_size = DVec2::new(width as f64, height as f64); + // Transform from global space to local pixel space + let bg_to_local = DAffine2::from_scale(image_size) * image_transform.inverse(); + let local_pos = bg_to_local.transform_point2(position); + + // Convert to pixel coordinates + let pixel_x = local_pos.x.floor() as i32; + let pixel_y = local_pos.y.floor() as i32; + + let fill = fill.into(); + let color = match fill { + Fill::Solid(color) => color, + Fill::Gradient(_) => Color::RED, // Debug color for gradient + Fill::None => Color::TRANSPARENT, + }; + + // Fill a 10x10 square around the clicked position + for dy in -5..5 { + for dx in -5..5 { + let x = pixel_x + dx; + let y = pixel_y + dy; + + // Check bounds + if x >= 0 && y >= 0 && x < width as i32 && y < height as i32 { + if let Some(pixel) = image.get_pixel_mut(x as u32, y as u32) { + *pixel = color; + } + } + } + } + + image +} + fn map_color(iter: usize, max_iter: usize) -> Color { let v = iter as f32 / max_iter as f32; Color::from_rgbaf32_unchecked(v, v, v, 1.) From 1d8ce81ff42380697a5f916ead7df81b137db990 Mon Sep 17 00:00:00 2001 From: Yiyin Gu Date: Sun, 30 Mar 2025 05:28:55 -0400 Subject: [PATCH 2/8] it works v1.0 --- .../graph_operation_message.rs | 4 +- .../graph_operation_message_handler.rs | 4 +- .../document/graph_operation/utility_types.rs | 8 +- .../messages/tool/tool_messages/fill_tool.rs | 82 ++++++++++++++-- node-graph/graph-craft/src/document/value.rs | 7 ++ node-graph/gstd/src/raster.rs | 98 ++++++++++++------- 6 files changed, 148 insertions(+), 55 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 6f64552810..1c2672802b 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -23,8 +23,8 @@ pub enum GraphOperationMessage { }, RasterFillSet { layer: LayerNodeIdentifier, - fill: Fill, - position: DVec2, + fills: Vec, + start_pos: Vec, }, OpacitySet { layer: LayerNodeIdentifier, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 06bb54d1a0..eab3755eb9 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -41,9 +41,9 @@ impl MessageHandler> for Gr modify_inputs.fill_set(fill); } } - GraphOperationMessage::RasterFillSet { layer, fill, position } => { + GraphOperationMessage::RasterFillSet { layer, fills, start_pos } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { - modify_inputs.raster_fill_set(fill, position); + modify_inputs.raster_fill_set(fills, start_pos); } } GraphOperationMessage::OpacitySet { layer, opacity } => { diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 9d5d42cd4b..6ae9004f4d 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -347,12 +347,10 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false); } - pub fn raster_fill_set(&mut self, fill: Fill, position: DVec2) { + pub fn raster_fill_set(&mut self, fills: Vec, start_pos: Vec) { let Some(raster_fill_node_id) = self.existing_node_id("Raster Fill", true) else { return }; - let input_connector_1 = InputConnector::node(raster_fill_node_id, 1); - self.set_input_with_refresh(input_connector_1, NodeInput::value(TaggedValue::Fill(fill), false), false); - let input_connector_2 = InputConnector::node(raster_fill_node_id, 2); - self.set_input_with_refresh(input_connector_2, NodeInput::value(TaggedValue::DVec2(position), false), false); + self.set_input_with_refresh(InputConnector::node(raster_fill_node_id, 1), NodeInput::value(TaggedValue::FillCache(fills), false), false); + self.set_input_with_refresh(InputConnector::node(raster_fill_node_id, 2), NodeInput::value(TaggedValue::VecDVec2(start_pos), false), false); } pub fn stroke_set(&mut self, stroke: Stroke) { diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index cf9c1c6185..c49fd613fd 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -1,6 +1,11 @@ +use graph_craft::document::value::TaggedValue; +use graphene_std::vector::style::Fill; + 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; + #[derive(Default)] pub struct FillTool { fsm_state: FillToolFsmState, @@ -12,7 +17,7 @@ pub enum FillToolMessage { // Standard messages Abort, - // Tool-specific messages + // Tool-specific messagesty-dlp PointerUp, FillPrimaryColor, FillSecondaryColor, @@ -38,7 +43,8 @@ impl LayoutHolder for FillTool { impl<'a> MessageHandler> for FillTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { - self.fsm_state.process_event(message, &mut (), tool_data, &(), responses, true); + let tool_datas = &mut RasterFillToolData::default(); + self.fsm_state.process_event(message, tool_datas, tool_data, &(), responses, true); } fn actions(&self) -> ActionList { match self.fsm_state { @@ -71,11 +77,42 @@ enum FillToolFsmState { Filling, } +#[derive(Clone, Debug, Default)] +struct RasterFillToolData { + fills: Vec, + start_pos: Vec, + layer: Option, +} + +impl RasterFillToolData { + fn load_existing_fills(&mut self, document: &mut DocumentMessageHandler, layer_identifier: LayerNodeIdentifier) -> Option { + let node_graph_layer = NodeGraphLayer::new(layer_identifier, &mut document.network_interface); + let existing_fills = node_graph_layer.find_node_inputs("Raster Fill"); + info!("existing_fills: {:?}", existing_fills); + if let Some(existing_fills) = existing_fills { + let fills = if let Some(TaggedValue::FillCache(fills)) = existing_fills[1].as_value() { + fills.clone() + } else { + vec![] + }; + let start_pos = if let Some(TaggedValue::VecDVec2(start_pos)) = existing_fills[2].as_value() { + start_pos.clone() + } else { + vec![] + }; + self.fills = fills; + self.start_pos = start_pos; + self.layer = Some(layer_identifier); + } + None + } +} + impl Fsm for FillToolFsmState { - type ToolData = (); + type ToolData = RasterFillToolData; type ToolOptions = (); - fn transition(self, event: ToolMessage, _tool_data: &mut Self::ToolData, handler_data: &mut ToolActionHandlerData, _tool_options: &Self::ToolOptions, responses: &mut VecDeque) -> Self { + fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, handler_data: &mut ToolActionHandlerData, _tool_options: &Self::ToolOptions, responses: &mut VecDeque) -> Self { let ToolActionHandlerData { document, global_tool_data, input, .. } = handler_data; @@ -93,20 +130,47 @@ impl Fsm for FillToolFsmState { }; responses.add(DocumentMessage::AddTransaction); - // If the layer is a raster layer, use the raster fill command instead + // If the layer is a raster layer, use the raster fill functionality 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, &mut 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::RasterFillSet { layer: layer_identifier, - fill, - position: input.mouse.position, + fills: tool_data.fills.clone(), + start_pos: tool_data.start_pos.clone(), }); } else { + // For vector layers, use the existing functionality responses.add(GraphOperationMessage::FillSet { layer: layer_identifier, fill }); } FillToolFsmState::Filling } - (FillToolFsmState::Filling, FillToolMessage::PointerUp) => FillToolFsmState::Ready, + (FillToolFsmState::Filling, FillToolMessage::PointerUp) => { + // Clear the fills data when we're done + tool_data.fills.clear(); + tool_data.start_pos.clear(); + FillToolFsmState::Ready + } (FillToolFsmState::Filling, FillToolMessage::Abort) => { responses.add(DocumentMessage::AbortTransaction); diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 42fcdf5e70..a93913e2fd 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -206,6 +206,7 @@ tagged_value! { Font(graphene_core::text::Font), BrushStrokes(Vec), BrushCache(BrushCache), + FillCache(Vec), DocumentNode(DocumentNode), Curve(graphene_core::raster::curve::Curve), Footprint(graphene_core::transform::Footprint), @@ -427,4 +428,10 @@ mod fake_hash { self.1.hash(state) } } + impl FakeHash for (T, U) { + fn hash(&self, state: &mut H) { + self.0.hash(state); + self.1.hash(state); + } + } } diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 3f4417a898..147c7d8d69 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -686,58 +686,82 @@ fn mandelbrot_impl(c: Vec2, max_iter: usize) -> usize { } #[node_macro::node(category("Raster"))] -fn raster_fill + 'n + Send>( +fn raster_fill + 'n + Send + Clone>( _: impl Ctx, #[implementations(ImageFrameTable)] - /// The vector elements, or group of vector elements, to apply the fill to. + /// The raster elements to apply the fill to. mut image: ImageFrameTable, #[implementations( - Fill, - Option, - Color, - Gradient, + Vec, + Vec>, + Vec, + Vec )] - #[default(Color::BLACK)] - /// The fill to paint the path with. - fill: F, - /// The position of the fill in the image. - position: DVec2, + #[default(Vec)] + /// The fills to paint the path with. + fills: Vec, + /// The positions of the fill in image-local coordinates. + #[hidden] + positions: Vec, ) -> ImageFrameTable { let width = image.width(); let height = image.height(); - - if width == 0 || height == 0 { + if width == 0 || height == 0 || fills.is_empty() || positions.is_empty() { return image; } - // Transform the global position to local image space - let image_transform = image.transform(); - let image_size = DVec2::new(width as f64, height as f64); - // Transform from global space to local pixel space - let bg_to_local = DAffine2::from_scale(image_size) * image_transform.inverse(); - let local_pos = bg_to_local.transform_point2(position); - - // Convert to pixel coordinates - let pixel_x = local_pos.x.floor() as i32; - let pixel_y = local_pos.y.floor() as i32; - - let fill = fill.into(); - let color = match fill { - Fill::Solid(color) => color, - Fill::Gradient(_) => Color::RED, // Debug color for gradient - Fill::None => Color::TRANSPARENT, - }; + // Process the minimum number of fill and position pairs + let fill_count = fills.len().min(positions.len()); + + for i in 0..fill_count { + // Get the fill and position for this iteration + let fill = fills[i].clone().into(); + let position = positions[i]; + + // Scale position to pixel coordinates + let image_size = DVec2::new(width as f64, height as f64); + let local_pos = position * image_size; + + // Convert to pixel coordinates + let pixel_x = local_pos.x.floor() as i32; + let pixel_y = local_pos.y.floor() as i32; + + let color = match fill { + Fill::Solid(color) => color, + Fill::Gradient(_) => Color::RED, // TODO: Implement raster gradient fill + Fill::None => Color::TRANSPARENT, + }; + + // Get the target color at the clicked position + let target_color = match image.get_pixel(pixel_x as u32, pixel_y as u32) { + Some(pixel) => pixel.clone(), + None => continue, + }; + + // If the target color is the same as the fill color, no need to fill + if target_color == color { + continue; + } - // Fill a 10x10 square around the clicked position - for dy in -5..5 { - for dx in -5..5 { - let x = pixel_x + dx; - let y = pixel_y + dy; + // Flood fill algorithm using a stack + let mut stack = Vec::new(); + stack.push((pixel_x, pixel_y)); + while let Some((x, y)) = stack.pop() { // Check bounds - if x >= 0 && y >= 0 && x < width as i32 && y < height as i32 { - if let Some(pixel) = image.get_pixel_mut(x as u32, y as u32) { + if x < 0 || y < 0 || x >= width as i32 || y >= height as i32 { + continue; + } + + // Get current pixel + if let Some(pixel) = image.get_pixel_mut(x as u32, y as u32) { + // If pixel matches target color, fill it and add neighbors to stack + if *pixel == target_color { *pixel = color; + stack.push((x + 1, y)); // Right + stack.push((x - 1, y)); // Left + stack.push((x, y + 1)); // Down + stack.push((x, y - 1)); // Up } } } From 1ca145d07a43d4a9cb5d496b15bd8c7b91a87785 Mon Sep 17 00:00:00 2001 From: Yiyin Gu Date: Sat, 5 Apr 2025 19:38:24 -0400 Subject: [PATCH 3/8] fix color bug + add threshold adjustment based on LAB --- .../graph_operation_message.rs | 1 + .../graph_operation_message_handler.rs | 9 ++- .../document/graph_operation/utility_types.rs | 3 +- .../messages/tool/tool_messages/fill_tool.rs | 10 +++- node-graph/gcore/src/raster/color.rs | 57 +++++++++++++++++++ node-graph/gstd/src/raster.rs | 9 ++- 6 files changed, 82 insertions(+), 7 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 1c2672802b..7dde146a7a 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -25,6 +25,7 @@ pub enum GraphOperationMessage { layer: LayerNodeIdentifier, fills: Vec, start_pos: Vec, + similarity_threshold: f64, }, OpacitySet { layer: LayerNodeIdentifier, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index eab3755eb9..d92dfc5ebe 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -41,9 +41,14 @@ impl MessageHandler> for Gr modify_inputs.fill_set(fill); } } - GraphOperationMessage::RasterFillSet { layer, fills, start_pos } => { + GraphOperationMessage::RasterFillSet { + layer, + fills, + start_pos, + similarity_threshold, + } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { - modify_inputs.raster_fill_set(fills, start_pos); + modify_inputs.raster_fill_set(fills, start_pos, similarity_threshold); } } GraphOperationMessage::OpacitySet { layer, opacity } => { diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 6ae9004f4d..406ab48ee0 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -347,10 +347,11 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false); } - pub fn raster_fill_set(&mut self, fills: Vec, start_pos: Vec) { + pub fn raster_fill_set(&mut self, fills: Vec, start_pos: Vec, similarity_threshold: f64) { let Some(raster_fill_node_id) = self.existing_node_id("Raster Fill", true) else { return }; self.set_input_with_refresh(InputConnector::node(raster_fill_node_id, 1), NodeInput::value(TaggedValue::FillCache(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(similarity_threshold), false), false); } pub fn stroke_set(&mut self, stroke: Stroke) { diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index c49fd613fd..ead2da01bd 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -82,13 +82,13 @@ struct RasterFillToolData { fills: Vec, start_pos: Vec, layer: Option, + similarity_threshold: f64, } impl RasterFillToolData { fn load_existing_fills(&mut self, document: &mut DocumentMessageHandler, layer_identifier: LayerNodeIdentifier) -> Option { let node_graph_layer = NodeGraphLayer::new(layer_identifier, &mut document.network_interface); let existing_fills = node_graph_layer.find_node_inputs("Raster Fill"); - info!("existing_fills: {:?}", existing_fills); if let Some(existing_fills) = existing_fills { let fills = if let Some(TaggedValue::FillCache(fills)) = existing_fills[1].as_value() { fills.clone() @@ -100,10 +100,17 @@ impl RasterFillToolData { } else { vec![] }; + let similarity_threshold = if let Some(TaggedValue::F64(similarity_threshold)) = existing_fills[3].as_value() { + *similarity_threshold + } else { + 1. + }; self.fills = fills; self.start_pos = start_pos; self.layer = Some(layer_identifier); + self.similarity_threshold = similarity_threshold; } + self.similarity_threshold = 1.; None } } @@ -157,6 +164,7 @@ impl Fsm for FillToolFsmState { layer: layer_identifier, fills: tool_data.fills.clone(), start_pos: tool_data.start_pos.clone(), + similarity_threshold: tool_data.similarity_threshold, }); } else { // For vector layers, use the existing functionality diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 3be18d1952..456f52f6ab 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -1049,6 +1049,63 @@ 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.0; + 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.0 / 3.0) } else { (903.3 * x / xn + 16.0) / 116.0 }; + + let fy = if y / yn > 0.008856 { (y / yn).powf(1.0 / 3.0) } else { (903.3 * y / yn + 16.0) / 116.0 }; + + let fz = if z / zn > 0.008856 { (z / zn).powf(1.0 / 3.0) } else { (903.3 * z / zn + 16.0) / 116.0 }; + + let l = 116.0 * fy - 16.0; + let a = 500.0 * (fx - fy); + let b = 200.0 * (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(&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)).sqrt() + } + + /// Check if two colors are similar within a threshold in LAB space + pub fn is_similar_lab(&self, other: &Color, threshold: f64) -> bool { + self.lab_distance(other) <= threshold && (self.alpha - other.alpha).abs() <= 0.01 + } } #[test] diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 147c7d8d69..5722a4a1f3 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -701,8 +701,11 @@ fn raster_fill + 'n + Send + Clone>( /// The fills to paint the path with. fills: Vec, /// The positions of the fill in image-local coordinates. - #[hidden] positions: Vec, + /// The threshold for color similarity in LAB space. + #[default(1.)] + #[range((0., 10.))] + similarity_threshold: f64, ) -> ImageFrameTable { let width = image.width(); let height = image.height(); @@ -727,7 +730,7 @@ fn raster_fill + 'n + Send + Clone>( let pixel_y = local_pos.y.floor() as i32; let color = match fill { - Fill::Solid(color) => color, + Fill::Solid(color) => color.to_linear_srgb(), Fill::Gradient(_) => Color::RED, // TODO: Implement raster gradient fill Fill::None => Color::TRANSPARENT, }; @@ -756,7 +759,7 @@ fn raster_fill + 'n + Send + Clone>( // Get current pixel if let Some(pixel) = image.get_pixel_mut(x as u32, y as u32) { // If pixel matches target color, fill it and add neighbors to stack - if *pixel == target_color { + if pixel.is_similar_lab(&target_color, similarity_threshold) { *pixel = color; stack.push((x + 1, y)); // Right stack.push((x - 1, y)); // Left From 59dacf2a1c12300a3a1dd4ceacf06977a4f917e3 Mon Sep 17 00:00:00 2001 From: Yiyin Gu Date: Thu, 10 Apr 2025 14:58:59 -0400 Subject: [PATCH 4/8] Edit test cases for new raster fill --- .../messages/tool/tool_messages/fill_tool.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 0f9c501c8d..17bc05f489 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -227,12 +227,27 @@ mod test_fill { } #[tokio::test] - async fn ignore_raster() { + async fn primary_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.select_primary_color(Color::GREEN).await; editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await; - assert!(get_fills(&mut editor,).await.is_empty()); + let fills = get_fills(&mut editor).await; + assert_eq!(fills.len(), 1); + assert_eq!(fills[0].as_solid().unwrap().to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb()); + } + + #[tokio::test] + async fn secondary_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.select_secondary_color(Color::YELLOW).await; + editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await; + let fills = get_fills(&mut editor).await; + assert_eq!(fills.len(), 1); + assert_eq!(fills[0].as_solid().unwrap().to_rgba8_srgb(), Color::YELLOW.to_rgba8_srgb()); } #[tokio::test] From 3a69b792b6469236118ba7e2ac5adb82db1b6af4 Mon Sep 17 00:00:00 2001 From: Yiyin Gu Date: Thu, 10 Apr 2025 21:20:49 -0400 Subject: [PATCH 5/8] Delete ignore raster test case --- .../messages/tool/tool_messages/fill_tool.rs | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 17bc05f489..904f0cf988 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -213,7 +213,6 @@ mod test_fill { async fn get_fills(editor: &mut EditorTestUtils) -> Vec { let instrumented = editor.eval_graph().await; - instrumented.grab_all_input::>(&editor.runtime).collect() } @@ -226,30 +225,6 @@ mod test_fill { assert!(get_fills(&mut editor,).await.is_empty()); } - #[tokio::test] - async fn primary_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.select_primary_color(Color::GREEN).await; - editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::empty()).await; - let fills = get_fills(&mut editor).await; - assert_eq!(fills.len(), 1); - assert_eq!(fills[0].as_solid().unwrap().to_rgba8_srgb(), Color::GREEN.to_rgba8_srgb()); - } - - #[tokio::test] - async fn secondary_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.select_secondary_color(Color::YELLOW).await; - editor.click_tool(ToolType::Fill, MouseKeys::LEFT, DVec2::new(2., 2.), ModifierKeys::SHIFT).await; - let fills = get_fills(&mut editor).await; - assert_eq!(fills.len(), 1); - assert_eq!(fills[0].as_solid().unwrap().to_rgba8_srgb(), Color::YELLOW.to_rgba8_srgb()); - } - #[tokio::test] async fn primary() { let mut editor = EditorTestUtils::create(); From a1d1e5180b81750ae40c4da123e3bcd7c110a360 Mon Sep 17 00:00:00 2001 From: Yiyin Gu Date: Fri, 11 Apr 2025 00:41:14 -0400 Subject: [PATCH 6/8] Update f32 assert to avoid == --- .vscode/launch.json | 2 ++ node-graph/gcore/src/raster/discrete_srgb.rs | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index df57674a13..0c21ac535c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -32,6 +32,8 @@ "cargo": { "args": [ "test", + "--features", + "gpu", "--no-run", "--bin=graphite", "--package=graphite", diff --git a/node-graph/gcore/src/raster/discrete_srgb.rs b/node-graph/gcore/src/raster/discrete_srgb.rs index 13a06e30ab..2bd82602b0 100644 --- a/node-graph/gcore/src/raster/discrete_srgb.rs +++ b/node-graph/gcore/src/raster/discrete_srgb.rs @@ -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,); } } From 614d83be48bbfbf178517461ad936ced4e5708ea Mon Sep 17 00:00:00 2001 From: Yiyin Gu Date: Fri, 11 Apr 2025 00:58:42 -0400 Subject: [PATCH 7/8] reverse unwanted changes --- .vscode/launch.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 0c21ac535c..df57674a13 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -32,8 +32,6 @@ "cargo": { "args": [ "test", - "--features", - "gpu", "--no-run", "--bin=graphite", "--package=graphite", From 7915469f1ae4418a86d5947f0d77b58dd113ea3d Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 13 Apr 2025 01:59:18 -0700 Subject: [PATCH 8/8] Code review --- .../graph_operation_message.rs | 12 +-- .../graph_operation_message_handler.rs | 13 +--- .../document/graph_operation/utility_types.rs | 8 +- .../messages/tool/tool_messages/fill_tool.rs | 74 ++++++++++--------- node-graph/gcore/src/ops.rs | 2 +- node-graph/gcore/src/raster/color.rs | 24 +++--- node-graph/gcore/src/raster/discrete_srgb.rs | 2 +- node-graph/graph-craft/src/document/value.rs | 2 +- node-graph/gstd/src/raster.rs | 20 +++-- 9 files changed, 75 insertions(+), 82 deletions(-) diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs index 7dde146a7a..dbb1a0fe41 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message.rs @@ -17,15 +17,15 @@ use graphene_core::{Artboard, Color}; #[impl_message(Message, DocumentMessage, GraphOperation)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum GraphOperationMessage { - FillSet { - layer: LayerNodeIdentifier, - fill: Fill, - }, - RasterFillSet { + FillRaster { layer: LayerNodeIdentifier, fills: Vec, start_pos: Vec, - similarity_threshold: f64, + tolerance: f64, + }, + FillSet { + layer: LayerNodeIdentifier, + fill: Fill, }, OpacitySet { layer: LayerNodeIdentifier, diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 5a8d2e4da5..7addf8a573 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -36,19 +36,14 @@ impl MessageHandler> for Gr let network_interface = data.network_interface; match message { - GraphOperationMessage::FillSet { layer, fill } => { + GraphOperationMessage::FillRaster { layer, fills, start_pos, tolerance } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { - modify_inputs.fill_set(fill); + modify_inputs.fill_raster(fills, start_pos, tolerance); } } - GraphOperationMessage::RasterFillSet { - layer, - fills, - start_pos, - similarity_threshold, - } => { + GraphOperationMessage::FillSet { layer, fill } => { if let Some(mut modify_inputs) = ModifyInputsContext::new_with_layer(layer, network_interface, responses) { - modify_inputs.raster_fill_set(fills, start_pos, similarity_threshold); + modify_inputs.fill_set(fill); } } GraphOperationMessage::OpacitySet { layer, opacity } => { diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 9f9357a920..1e5f07d5a3 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -347,11 +347,11 @@ impl<'a> ModifyInputsContext<'a> { self.set_input_with_refresh(input_connector, NodeInput::value(TaggedValue::BlendMode(blend_mode), false), false); } - pub fn raster_fill_set(&mut self, fills: Vec, start_pos: Vec, similarity_threshold: f64) { - let Some(raster_fill_node_id) = self.existing_node_id("Raster Fill", true) else { return }; - self.set_input_with_refresh(InputConnector::node(raster_fill_node_id, 1), NodeInput::value(TaggedValue::FillCache(fills), false), false); + pub fn fill_raster(&mut self, fills: Vec, start_pos: Vec, 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(similarity_threshold), 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) { diff --git a/editor/src/messages/tool/tool_messages/fill_tool.rs b/editor/src/messages/tool/tool_messages/fill_tool.rs index 904f0cf988..32490ec14f 100644 --- a/editor/src/messages/tool/tool_messages/fill_tool.rs +++ b/editor/src/messages/tool/tool_messages/fill_tool.rs @@ -1,10 +1,9 @@ -use graph_craft::document::value::TaggedValue; -use graphene_std::vector::style::Fill; - 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 graph_craft::document::value::TaggedValue; +use graphene_std::vector::style::Fill; #[derive(Default)] pub struct FillTool { @@ -17,7 +16,7 @@ pub enum FillToolMessage { // Standard messages Abort, - // Tool-specific messagesty-dlp + // Tool-specific messages PointerUp, FillPrimaryColor, FillSecondaryColor, @@ -43,8 +42,8 @@ impl LayoutHolder for FillTool { impl<'a> MessageHandler> for FillTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { - let tool_datas = &mut RasterFillToolData::default(); - self.fsm_state.process_event(message, tool_datas, 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 { @@ -78,45 +77,46 @@ enum FillToolFsmState { } #[derive(Clone, Debug, Default)] -struct RasterFillToolData { +struct FillToolData { fills: Vec, start_pos: Vec, - layer: Option, - similarity_threshold: f64, + tolerance: f64, } -impl RasterFillToolData { +impl FillToolData { fn load_existing_fills(&mut self, document: &mut DocumentMessageHandler, layer_identifier: LayerNodeIdentifier) -> Option { - let node_graph_layer = NodeGraphLayer::new(layer_identifier, &mut document.network_interface); - let existing_fills = node_graph_layer.find_node_inputs("Raster Fill"); + 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::FillCache(fills)) = existing_fills[1].as_value() { - fills.clone() + let fills = if let Some(TaggedValue::VecFill(fills)) = existing_fills[1].as_value().cloned() { + fills } else { - vec![] + Vec::new() }; - let start_pos = if let Some(TaggedValue::VecDVec2(start_pos)) = existing_fills[2].as_value() { - start_pos.clone() + let start_pos = if let Some(TaggedValue::VecDVec2(start_pos)) = existing_fills[2].as_value().cloned() { + start_pos } else { - vec![] + Vec::new() }; - let similarity_threshold = if let Some(TaggedValue::F64(similarity_threshold)) = existing_fills[3].as_value() { - *similarity_threshold + let tolerance = if let Some(TaggedValue::F64(tolerance)) = existing_fills[3].as_value().cloned() { + tolerance } else { 1. }; - self.fills = fills; - self.start_pos = start_pos; - self.layer = Some(layer_identifier); - self.similarity_threshold = similarity_threshold; + + *self = Self { fills, start_pos, tolerance }; } - self.similarity_threshold = 1.; + + // 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 = RasterFillToolData; + 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) -> Self { @@ -127,9 +127,7 @@ impl Fsm for FillToolFsmState { 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; - }; + 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()), @@ -137,7 +135,8 @@ impl Fsm for FillToolFsmState { }; responses.add(DocumentMessage::AddTransaction); - // If the layer is a raster layer, use the raster fill functionality + + // 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); @@ -150,33 +149,36 @@ impl Fsm for FillToolFsmState { .inverse() .transform_point2(input.mouse.position); - let node_graph_layer = NodeGraphLayer::new(layer_identifier, &mut document.network_interface); + 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::RasterFillSet { + responses.add(GraphOperationMessage::FillRaster { layer: layer_identifier, fills: tool_data.fills.clone(), start_pos: tool_data.start_pos.clone(), - similarity_threshold: tool_data.similarity_threshold, + tolerance: tool_data.tolerance, }); - } else { - // For vector layers, use the existing functionality + } + // 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) => { - // Clear the fills data when we're done + // 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) => { diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index 768950418a..936cd546a1 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -132,7 +132,7 @@ fn modulo>>, 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, ) -> >::Output { if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus } } diff --git a/node-graph/gcore/src/raster/color.rs b/node-graph/gcore/src/raster/color.rs index 456f52f6ab..a4d51918d6 100644 --- a/node-graph/gcore/src/raster/color.rs +++ b/node-graph/gcore/src/raster/color.rs @@ -1051,7 +1051,7 @@ impl Color { } /// Convert RGB to XYZ color space - fn to_xyz(&self) -> [f64; 3] { + fn to_xyz(self) -> [f64; 3] { let r = self.red as f64; let g = self.green as f64; let b = self.blue as f64; @@ -1068,22 +1068,20 @@ impl Color { fn xyz_to_lab(xyz: [f64; 3]) -> [f64; 3] { // D65 illuminant reference values let xn = 0.950489; - let yn = 1.0; + 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.0 / 3.0) } else { (903.3 * x / xn + 16.0) / 116.0 }; + 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 fy = if y / yn > 0.008856 { (y / yn).powf(1.0 / 3.0) } else { (903.3 * y / yn + 16.0) / 116.0 }; - - let fz = if z / zn > 0.008856 { (z / zn).powf(1.0 / 3.0) } else { (903.3 * z / zn + 16.0) / 116.0 }; - - let l = 116.0 * fy - 16.0; - let a = 500.0 * (fx - fy); - let b = 200.0 * (fy - fz); + let l = 116. * fy - 16.; + let a = 500. * (fx - fy); + let b = 200. * (fy - fz); [l, a, b] } @@ -1094,17 +1092,17 @@ impl Color { } /// Calculate the distance between two colors in LAB space - pub fn lab_distance(&self, other: &Color) -> f64 { + 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)).sqrt() + (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.lab_distance(other) <= threshold && (self.alpha - other.alpha).abs() <= 0.01 + (self.alpha - other.alpha).abs() <= 0.01 && self.lab_distance_squared(other) <= threshold.powi(2) } } diff --git a/node-graph/gcore/src/raster/discrete_srgb.rs b/node-graph/gcore/src/raster/discrete_srgb.rs index 2bd82602b0..3dcd1a85b2 100644 --- a/node-graph/gcore/src/raster/discrete_srgb.rs +++ b/node-graph/gcore/src/raster/discrete_srgb.rs @@ -165,7 +165,7 @@ mod tests { 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,); + assert!((float_val - ref_val).abs() < 1e-5); } } diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 26bed17455..79055485d1 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -172,6 +172,7 @@ tagged_value! { // ImaginateMaskStartingFill(ImaginateMaskStartingFill), // ImaginateController(ImaginateController), Fill(graphene_core::vector::style::Fill), + VecFill(Vec), Stroke(graphene_core::vector::style::Stroke), F64Array4([f64; 4]), // TODO: Eventually remove this alias document upgrade code @@ -205,7 +206,6 @@ tagged_value! { Font(graphene_core::text::Font), BrushStrokes(Vec), BrushCache(BrushCache), - FillCache(Vec), DocumentNode(DocumentNode), Curve(graphene_core::raster::curve::Curve), Footprint(graphene_core::transform::Footprint), diff --git a/node-graph/gstd/src/raster.rs b/node-graph/gstd/src/raster.rs index 5722a4a1f3..4191d02189 100644 --- a/node-graph/gstd/src/raster.rs +++ b/node-graph/gstd/src/raster.rs @@ -686,10 +686,10 @@ fn mandelbrot_impl(c: Vec2, max_iter: usize) -> usize { } #[node_macro::node(category("Raster"))] -fn raster_fill + 'n + Send + Clone>( +fn flood_fill + 'n + Send + Clone>( _: impl Ctx, #[implementations(ImageFrameTable)] - /// The raster elements to apply the fill to. + /// The raster content to apply the flood fill to. mut image: ImageFrameTable, #[implementations( Vec, @@ -697,15 +697,16 @@ fn raster_fill + 'n + Send + Clone>( Vec, Vec )] - #[default(Vec)] /// The fills to paint the path with. + #[default(Vec)] fills: Vec, - /// The positions of the fill in image-local coordinates. + /// The starting positions of the flood fill points in the layer's local coordinates. positions: Vec, + // TODO: What is the range? I'd expect 0-255, but we should also rescale that to 0-1. But this crashes at larger values approaching 100. And tolerance should be per-position not global to the node. /// The threshold for color similarity in LAB space. #[default(1.)] - #[range((0., 10.))] - similarity_threshold: f64, + #[range((0., 50.))] + tolerance: f64, ) -> ImageFrameTable { let width = image.width(); let height = image.height(); @@ -736,10 +737,7 @@ fn raster_fill + 'n + Send + Clone>( }; // Get the target color at the clicked position - let target_color = match image.get_pixel(pixel_x as u32, pixel_y as u32) { - Some(pixel) => pixel.clone(), - None => continue, - }; + let Some(target_color) = image.get_pixel(pixel_x as u32, pixel_y as u32) else { continue }; // If the target color is the same as the fill color, no need to fill if target_color == color { @@ -759,7 +757,7 @@ fn raster_fill + 'n + Send + Clone>( // Get current pixel if let Some(pixel) = image.get_pixel_mut(x as u32, y as u32) { // If pixel matches target color, fill it and add neighbors to stack - if pixel.is_similar_lab(&target_color, similarity_threshold) { + if pixel.is_similar_lab(&target_color, tolerance) { *pixel = color; stack.push((x + 1, y)); // Right stack.push((x - 1, y)); // Left