From 9d749c49fb413115e9b94ec1a16d0f6aa1bc6326 Mon Sep 17 00:00:00 2001 From: adamgerhant <116332429+adamgerhant@users.noreply.github.com> Date: Wed, 10 Jul 2024 02:12:55 -0700 Subject: [PATCH] Add a stack-based Boolean Operation layer node (#1813) * Multiple boolean operation node * Change boolean operation ordering * Complete layer boolean operation node * Automatically insert new boolean operation node * Remove divide operation * Fix subtract operations * Remove stack data from boolean operation properties * Fix images and custom vectors * Code cleanup * Use slice instead of iter to avoid infinite type recursion --------- Co-authored-by: Keavon Chambers --- .../portfolio/document/document_message.rs | 3 + .../document/document_message_handler.rs | 40 ++++ .../graph_operation_message.rs | 3 - .../graph_operation_message_handler.rs | 78 ------- .../node_graph/document_node_types.rs | 78 ++++++- .../document/node_graph/node_properties.rs | 15 +- .../tool/tool_messages/select_tool.rs | 12 +- .../computational-geometry.ts | 4 - node-graph/gcore/src/graphic_element.rs | 28 +-- .../gcore/src/graphic_element/renderer.rs | 5 - node-graph/gcore/src/transform.rs | 4 - node-graph/gcore/src/vector/misc.rs | 9 +- node-graph/gstd/src/vector.rs | 190 +++++++++++++++++- .../interpreted-executor/src/node_registry.rs | 3 +- 14 files changed, 319 insertions(+), 153 deletions(-) diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 484f0fd6..18b9e9c1 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -42,6 +42,9 @@ pub enum DocumentMessage { ClearArtboards, ClearLayersPanel, CommitTransaction, + InsertBooleanOperation { + operation: graphene_core::vector::misc::BooleanOperation, + }, CreateEmptyFolder, DebugPrintDocument, DeleteLayer { diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 87b22cb3..ddd443fb 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -295,6 +295,46 @@ impl MessageHandler> for DocumentMessag }); } DocumentMessage::CommitTransaction => (), + DocumentMessage::InsertBooleanOperation { operation } => { + let boolean_operation_node_id = NodeId(generate_uuid()); + + let parent = self + .metadata() + .deepest_common_ancestor(self.selected_nodes.selected_layers(self.metadata()), true) + .unwrap_or(LayerNodeIdentifier::ROOT_PARENT); + + let insert_index = parent + .children(self.metadata()) + .enumerate() + .find_map(|(index, item)| self.selected_nodes.selected_layers(self.metadata()).any(|x| x == item).then_some(index as usize)) + .unwrap_or(0); + + // Store a history step before doing anything + responses.add(DocumentMessage::StartTransaction); + + // Create the new Boolean Operation node + responses.add(GraphOperationMessage::CreateBooleanOperationNode { + node_id: boolean_operation_node_id, + operation, + }); + + responses.add(GraphOperationMessage::InsertNodeAtStackIndex { + node_id: boolean_operation_node_id, + parent, + insert_index, + }); + + responses.add(GraphOperationMessage::MoveSelectedSiblingsToChild { + new_parent: LayerNodeIdentifier::new_unchecked(boolean_operation_node_id), + }); + + // Select the new node + responses.add(NodeGraphMessage::SelectedNodesSet { + nodes: vec![boolean_operation_node_id], + }); + // Re-render + responses.add(NodeGraphMessage::RunDocumentGraph); + } DocumentMessage::CreateEmptyFolder => { let id = NodeId(generate_uuid()); 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 68005a43..5681ec56 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 @@ -49,9 +49,6 @@ pub enum GraphOperationMessage { parent: LayerNodeIdentifier, insert_index: usize, }, - InsertBooleanOperation { - operation: BooleanOperation, - }, InsertNodeBetween { // Post node post_node_id: NodeId, 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 2f79539e..3d87101d 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 @@ -258,84 +258,6 @@ impl MessageHandler> for Gr shift_self: true, }); } - GraphOperationMessage::InsertBooleanOperation { operation } => { - let mut selected_layers = selected_nodes.selected_layers(document_metadata); - - let upper_layer = selected_layers.next(); - let lower_layer = selected_layers.next(); - - let Some(upper_layer) = upper_layer else { return }; - - let Some(upper_layer_node) = document_network.nodes.get(&upper_layer.to_node()) else { return }; - let lower_layer_node = lower_layer.and_then(|lower_layer| document_network.nodes.get(&lower_layer.to_node())); - - let Some(NodeInput::Node { - node_id: upper_node_id, - output_index: upper_output_index, - .. - }) = upper_layer_node.inputs.get(1).cloned() - else { - return; - }; - let (lower_node_id, lower_output_index) = match lower_layer_node.and_then(|lower_layer_node| lower_layer_node.inputs.get(1).cloned()) { - Some(NodeInput::Node { - node_id: lower_node_id, - output_index: lower_output_index, - .. - }) => (Some(lower_node_id), Some(lower_output_index)), - _ => (None, None), - }; - - let boolean_operation_node_id = NodeId::new(); - - // Store a history step before doing anything - responses.add(DocumentMessage::StartTransaction); - - // Create the new Boolean Operation node - responses.add(GraphOperationMessage::CreateBooleanOperationNode { - node_id: boolean_operation_node_id, - operation, - }); - - // Insert it in the upper layer's chain, right before it enters the upper layer - responses.add(GraphOperationMessage::InsertNodeBetween { - post_node_id: upper_layer.to_node(), - post_node_input_index: 1, - insert_node_id: boolean_operation_node_id, - insert_node_output_index: 0, - insert_node_input_index: 0, - pre_node_id: upper_node_id, - pre_node_output_index: upper_output_index, - }); - - // Connect the lower chain to the Boolean Operation node's lower input - if let (Some(lower_layer), Some(lower_node_id), Some(lower_output_index)) = (lower_layer, lower_node_id, lower_output_index) { - responses.add(GraphOperationMessage::SetNodeInput { - node_id: boolean_operation_node_id, - input_index: 1, - input: NodeInput::node(lower_node_id, lower_output_index), - }); - - // Delete the lower layer (but its chain is kept since it's still used by the Boolean Operation node) - responses.add(GraphOperationMessage::DeleteLayer { layer: lower_layer, reconnect: true }); - } - - // Put the Boolean Operation where the output layer is located, since this is the correct shift relative to its left input chain - responses.add(GraphOperationMessage::SetNodePosition { - node_id: boolean_operation_node_id, - position: upper_layer_node.metadata.position, - }); - - // After the previous step, the Boolean Operation node is overlapping the upper layer, so we need to shift and its entire chain to the left by its width plus some padding - responses.add(GraphOperationMessage::ShiftUpstream { - node_id: boolean_operation_node_id, - shift: (-8, 0).into(), - shift_self: true, - }); - - // Re-render - responses.add(NodeGraphMessage::RunDocumentGraph); - } GraphOperationMessage::InsertNodeBetween { post_node_id, post_node_input_index, diff --git a/editor/src/messages/portfolio/document/node_graph/document_node_types.rs b/editor/src/messages/portfolio/document/node_graph/document_node_types.rs index ab5d08de..91a01c99 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_types.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_types.rs @@ -2574,15 +2574,89 @@ fn static_nodes() -> Vec { ..Default::default() }, DocumentNodeDefinition { - name: "Boolean Operation", + name: "Binary Boolean Operation", category: "Vector", - implementation: DocumentNodeImplementation::proto("graphene_std::vector::BooleanOperationNode<_, _>"), + implementation: DocumentNodeImplementation::proto("graphene_std::vector::BinaryBooleanOperationNode<_, _>"), inputs: vec![ DocumentInputType::value("Upper Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true), DocumentInputType::value("Lower Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true), DocumentInputType::value("Operation", TaggedValue::BooleanOperation(vector::misc::BooleanOperation::Union), false), ], outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)], + properties: node_properties::binary_boolean_operation_properties, + ..Default::default() + }, + DocumentNodeDefinition { + name: "Boolean Operation", + category: "Vector", + is_layer: true, + implementation: DocumentNodeImplementation::Network(NodeNetwork { + exports: vec![NodeInput::node(NodeId(4), 0)], + nodes: [ + // Secondary (left) input type coercion + ( + NodeId(0), + DocumentNode { + name: "Boolean Operation".to_string(), + inputs: vec![NodeInput::network(generic!(T), 1), NodeInput::network(concrete!(vector::misc::BooleanOperation), 2)], + implementation: DocumentNodeImplementation::proto("graphene_std::vector::BooleanOperationNode<_>"), + metadata: DocumentNodeMetadata { position: glam::IVec2::new(-16, -1) }, + ..Default::default() + }, + ), + // Primary (bottom) input type coercion + ( + NodeId(1), + DocumentNode { + name: "To Graphic Group".to_string(), + inputs: vec![NodeInput::network(generic!(T), 0)], + implementation: DocumentNodeImplementation::proto("graphene_core::ToGraphicGroupNode"), + metadata: DocumentNodeMetadata { position: glam::IVec2::new(-16, -3) }, // To Graphic Group + ..Default::default() + }, + ), + ( + NodeId(2), + DocumentNode { + name: "To Graphic Element".to_string(), + inputs: vec![NodeInput::node(NodeId(0), 0)], + implementation: DocumentNodeImplementation::proto("graphene_core::ToGraphicElementNode"), + metadata: DocumentNodeMetadata { position: glam::IVec2::new(-10, 3) }, // To Graphic Element + ..Default::default() + }, + ), + // The monitor node is used to display a thumbnail in the UI + ( + NodeId(3), + DocumentNode { + inputs: vec![NodeInput::node(NodeId(2), 0)], + metadata: DocumentNodeMetadata { position: glam::IVec2::new(-7, -1) }, // Monitor + ..monitor_node() + }, + ), + ( + NodeId(4), + DocumentNode { + name: "ConstructLayer".to_string(), + manual_composition: Some(concrete!(Footprint)), + inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::node(NodeId(3), 0)], + implementation: DocumentNodeImplementation::proto("graphene_core::ConstructLayerNode<_, _>"), + metadata: DocumentNodeMetadata { position: glam::IVec2::new(1, -3) }, // ConstructLayer + ..Default::default() + }, + ), + ] + .into(), + imports_metadata: (NodeId(generate_uuid()), (-26, -4).into()), + exports_metadata: (NodeId(generate_uuid()), (8, -4).into()), + ..Default::default() + }), + inputs: vec![ + DocumentInputType::value("Graphical Data", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true), + DocumentInputType::value("Vector Data", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true), + DocumentInputType::value("Operation", TaggedValue::BooleanOperation(vector::misc::BooleanOperation::Union), false), + ], + outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Graphic)], properties: node_properties::boolean_operation_properties, ..Default::default() }, diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 793cc97e..74523bec 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -2344,11 +2344,18 @@ pub fn circular_repeat_properties(document_node: &DocumentNode, node_id: NodeId, ] } -pub fn boolean_operation_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { - let other_vector_data = vector_widget(document_node, node_id, 1, "Lower Vector Data", true); - let opeartion = boolean_operation_radio_buttons(document_node, node_id, 2, "Operation", true); +pub fn binary_boolean_operation_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let lower_vector_data = vector_widget(document_node, node_id, 1, "Lower Vector Data", true); + let operation = boolean_operation_radio_buttons(document_node, node_id, 2, "Operation", true); - vec![LayoutGroup::Row { widgets: other_vector_data }, opeartion] + vec![LayoutGroup::Row { widgets: lower_vector_data }, operation] +} + +pub fn boolean_operation_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { + let vector_data = vector_widget(document_node, node_id, 1, "Vector Data", true); + let operation = boolean_operation_radio_buttons(document_node, node_id, 2, "Operation", true); + + vec![LayoutGroup::Row { widgets: vector_data }, operation] } pub fn copy_to_points_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 6e7d41b7..7c943fa8 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -148,21 +148,13 @@ impl SelectTool { } fn boolean_widgets(&self, selected_count: usize) -> impl Iterator { - let enabled = move |operation| { - if operation == BooleanOperation::Union { - (1..=2).contains(&selected_count) - } else { - selected_count == 2 - } - }; - let operations = BooleanOperation::list(); let icons = BooleanOperation::icons(); operations.into_iter().zip(icons).map(move |(operation, icon)| { IconButton::new(icon, 24) .tooltip(operation.to_string()) - .disabled(!enabled(operation)) - .on_update(move |_| GraphOperationMessage::InsertBooleanOperation { operation }.into()) + .disabled(selected_count == 0) + .on_update(move |_| DocumentMessage::InsertBooleanOperation { operation }.into()) .widget_holder() }) } diff --git a/frontend/src/utility-functions/computational-geometry.ts b/frontend/src/utility-functions/computational-geometry.ts index 80bb8cec..28900201 100644 --- a/frontend/src/utility-functions/computational-geometry.ts +++ b/frontend/src/utility-functions/computational-geometry.ts @@ -20,10 +20,6 @@ export function booleanDifference(path1: string, path2: string): string { return booleanOperation(path1, path2, "exclude"); } -export function booleanDivide(path1: string, path2: string): string { - return booleanOperation(path1, path2, "intersect") + booleanOperation(path1, path2, "exclude"); -} - function booleanOperation(path1: string, path2: string, operation: "unite" | "subtract" | "intersect" | "exclude"): string { const paperPath1 = new paper.CompoundPath(path1); const paperPath2 = new paper.CompoundPath(path2); diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index dc24c0db..62aa203a 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -57,7 +57,7 @@ impl core::hash::Hash for GraphicGroup { } /// The possible forms of graphical content held in a Vec by the `elements` field of [`GraphicElement`]. -/// Can be another recursively nested [`GraphicGroup`], [`VectorData`], an [`ImageFrame`], text (not yet implemented), or an [`Artboard`]. +/// Can be another recursively nested [`GraphicGroup`], a [`VectorData`] shape, an [`ImageFrame`], or an [`Artboard`]. #[derive(Clone, Debug, Hash, PartialEq, DynAny)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum GraphicElement { @@ -67,10 +67,6 @@ pub enum GraphicElement { VectorData(Box), /// A bitmap image with a finite position and extent, equivalent to the SVG tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image ImageFrame(ImageFrame), - // TODO: Switch from `String` to a proper formatted typography type - /// Text, equivalent to the SVG tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text - /// (Not yet implemented.) - Text(String), /// The bounds for displaying a page of contained content Artboard(Artboard), } @@ -366,28 +362,6 @@ impl GraphicElement { bounding_box: None, })) } - GraphicElement::Text(text) => usvg::Node::Text(Box::new(usvg::Text { - id: String::new(), - abs_transform: usvg::Transform::identity(), - rendering_mode: usvg::TextRendering::OptimizeSpeed, - writing_mode: usvg::WritingMode::LeftToRight, - chunks: vec![usvg::TextChunk { - text: text.clone(), - x: None, - y: None, - anchor: usvg::TextAnchor::Start, - spans: vec![], - text_flow: usvg::TextFlow::Linear, - }], - dx: Vec::new(), - dy: Vec::new(), - rotate: Vec::new(), - bounding_box: None, - abs_bounding_box: None, - stroke_bounding_box: None, - abs_stroke_bounding_box: None, - flattened: None, - })), GraphicElement::GraphicGroup(group) => { let mut group_element = usvg::Group::default(); diff --git a/node-graph/gcore/src/graphic_element/renderer.rs b/node-graph/gcore/src/graphic_element/renderer.rs index 5bacefdc..7c3be602 100644 --- a/node-graph/gcore/src/graphic_element/renderer.rs +++ b/node-graph/gcore/src/graphic_element/renderer.rs @@ -554,7 +554,6 @@ impl GraphicElementRendered for GraphicElement { match self { GraphicElement::VectorData(vector_data) => vector_data.render_svg(render, render_params), GraphicElement::ImageFrame(image_frame) => image_frame.render_svg(render, render_params), - GraphicElement::Text(_) => todo!("Render a text GraphicElement"), GraphicElement::GraphicGroup(graphic_group) => graphic_group.render_svg(render, render_params), GraphicElement::Artboard(artboard) => artboard.render_svg(render, render_params), } @@ -564,7 +563,6 @@ impl GraphicElementRendered for GraphicElement { match self { GraphicElement::VectorData(vector_data) => GraphicElementRendered::bounding_box(&**vector_data, transform), GraphicElement::ImageFrame(image_frame) => image_frame.bounding_box(transform), - GraphicElement::Text(_) => todo!("Bounds of a text GraphicElement"), GraphicElement::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform), GraphicElement::Artboard(artboard) => artboard.bounding_box(transform), } @@ -574,7 +572,6 @@ impl GraphicElementRendered for GraphicElement { match self { GraphicElement::VectorData(vector_data) => vector_data.add_click_targets(click_targets), GraphicElement::ImageFrame(image_frame) => image_frame.add_click_targets(click_targets), - GraphicElement::Text(_) => todo!("click target for text GraphicElement"), GraphicElement::GraphicGroup(graphic_group) => graphic_group.add_click_targets(click_targets), GraphicElement::Artboard(artboard) => artboard.add_click_targets(click_targets), } @@ -584,7 +581,6 @@ impl GraphicElementRendered for GraphicElement { match self { GraphicElement::VectorData(vector_data) => vector_data.to_usvg_node(), GraphicElement::ImageFrame(image_frame) => image_frame.to_usvg_node(), - GraphicElement::Text(text) => text.to_usvg_node(), GraphicElement::GraphicGroup(graphic_group) => graphic_group.to_usvg_node(), GraphicElement::Artboard(artboard) => artboard.to_usvg_node(), } @@ -594,7 +590,6 @@ impl GraphicElementRendered for GraphicElement { match self { GraphicElement::VectorData(vector_data) => vector_data.contains_artboard(), GraphicElement::ImageFrame(image_frame) => image_frame.contains_artboard(), - GraphicElement::Text(text) => text.contains_artboard(), GraphicElement::GraphicGroup(graphic_group) => graphic_group.contains_artboard(), GraphicElement::Artboard(artboard) => artboard.contains_artboard(), } diff --git a/node-graph/gcore/src/transform.rs b/node-graph/gcore/src/transform.rs index 2d336ee9..32a06551 100644 --- a/node-graph/gcore/src/transform.rs +++ b/node-graph/gcore/src/transform.rs @@ -75,7 +75,6 @@ impl Transform for GraphicElement { match self { GraphicElement::VectorData(vector_shape) => vector_shape.transform(), GraphicElement::ImageFrame(image_frame) => image_frame.transform(), - GraphicElement::Text(_) => todo!("Transform of text"), GraphicElement::GraphicGroup(graphic_group) => graphic_group.transform(), GraphicElement::Artboard(artboard) => artboard.transform(), } @@ -84,7 +83,6 @@ impl Transform for GraphicElement { match self { GraphicElement::VectorData(vector_shape) => vector_shape.local_pivot(pivot), GraphicElement::ImageFrame(image_frame) => image_frame.local_pivot(pivot), - GraphicElement::Text(_) => todo!("Transform of text"), GraphicElement::GraphicGroup(graphic_group) => graphic_group.local_pivot(pivot), GraphicElement::Artboard(artboard) => artboard.local_pivot(pivot), } @@ -93,7 +91,6 @@ impl Transform for GraphicElement { match self { GraphicElement::VectorData(vector_shape) => vector_shape.decompose_scale(), GraphicElement::ImageFrame(image_frame) => image_frame.decompose_scale(), - GraphicElement::Text(_) => todo!("Transform of text"), GraphicElement::GraphicGroup(graphic_group) => graphic_group.decompose_scale(), GraphicElement::Artboard(artboard) => artboard.decompose_scale(), } @@ -104,7 +101,6 @@ impl TransformMut for GraphicElement { match self { GraphicElement::VectorData(vector_shape) => vector_shape.transform_mut(), GraphicElement::ImageFrame(image_frame) => image_frame.transform_mut(), - GraphicElement::Text(_) => todo!("Transform of text"), GraphicElement::GraphicGroup(graphic_group) => graphic_group.transform_mut(), GraphicElement::Artboard(_) => todo!("Transform of artboard"), } diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index c7ed14df..900d70f8 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -18,23 +18,21 @@ pub enum BooleanOperation { SubtractBack, Intersect, Difference, - Divide, } impl BooleanOperation { - pub fn list() -> [BooleanOperation; 6] { + pub fn list() -> [BooleanOperation; 5] { [ BooleanOperation::Union, BooleanOperation::SubtractFront, BooleanOperation::SubtractBack, BooleanOperation::Intersect, BooleanOperation::Difference, - BooleanOperation::Divide, ] } - pub fn icons() -> [&'static str; 6] { - ["BooleanUnion", "BooleanSubtractFront", "BooleanSubtractBack", "BooleanIntersect", "BooleanDifference", "BooleanDivide"] + pub fn icons() -> [&'static str; 5] { + ["BooleanUnion", "BooleanSubtractFront", "BooleanSubtractBack", "BooleanIntersect", "BooleanDifference"] } } @@ -46,7 +44,6 @@ impl core::fmt::Display for BooleanOperation { BooleanOperation::SubtractBack => write!(f, "Subtract Back"), BooleanOperation::Intersect => write!(f, "Intersect"), BooleanOperation::Difference => write!(f, "Difference"), - BooleanOperation::Divide => write!(f, "Divide"), } } } diff --git a/node-graph/gstd/src/vector.rs b/node-graph/gstd/src/vector.rs index 2f9bdc76..1fe5800f 100644 --- a/node-graph/gstd/src/vector.rs +++ b/node-graph/gstd/src/vector.rs @@ -1,21 +1,23 @@ use crate::Node; use bezier_rs::{ManipulatorGroup, Subpath}; -use graphene_core::transform::Footprint; -use graphene_core::vector::misc::BooleanOperation; +use graphene_core::raster::ImageFrame; pub use graphene_core::vector::*; +use graphene_core::Color; +use graphene_core::{transform::Footprint, GraphicGroup}; +use graphene_core::{vector::misc::BooleanOperation, GraphicElement}; use futures::Future; use glam::{DAffine2, DVec2}; use wasm_bindgen::prelude::*; -pub struct BooleanOperationNode { +pub struct BinaryBooleanOperationNode { lower_vector_data: LowerVectorData, boolean_operation: BooleanOp, } -#[node_macro::node_fn(BooleanOperationNode)] -async fn boolean_operation_node>( +#[node_macro::node_fn(BinaryBooleanOperationNode)] +async fn binary_boolean_operation_node>( upper_vector_data: VectorData, lower_vector_data: impl Node, boolean_operation: BooleanOperation, @@ -39,7 +41,6 @@ async fn boolean_operation_node>( BooleanOperation::SubtractBack => boolean_subtract(upper_path_string, lower_path_string), BooleanOperation::Intersect => boolean_intersect(upper_path_string, lower_path_string), BooleanOperation::Difference => boolean_difference(upper_path_string, lower_path_string), - BooleanOperation::Divide => boolean_divide(upper_path_string, lower_path_string), } }; @@ -51,9 +52,182 @@ async fn boolean_operation_node>( result } +pub struct BooleanOperationNode { + boolean_operation: BooleanOp, +} + +#[node_macro::node_fn(BooleanOperationNode)] +fn boolean_operation_node(graphic_group: GraphicGroup, boolean_operation: BooleanOperation) -> VectorData { + fn vector_from_image(image_frame: &ImageFrame

) -> VectorData { + let corner1 = DVec2::ZERO; + let corner2 = DVec2::new(1., 1.); + let mut subpath = Subpath::new_rect(corner1, corner2); + subpath.apply_transform(image_frame.transform); + let mut vector_data = VectorData::from_subpath(subpath); + vector_data + .style + .set_fill(graphene_core::vector::style::Fill::Solid(Color::from_rgb_str("777777").unwrap().to_gamma_srgb())); + vector_data + } + + fn union_vector_data(graphic_element: &GraphicElement) -> VectorData { + match graphic_element { + GraphicElement::VectorData(vector_data) => *vector_data.clone(), + // Union all vector data in the graphic group into a single vector + GraphicElement::GraphicGroup(graphic_group) => { + let vector_data = collect_vector_data(graphic_group); + boolean_operation_on_vector_data(&vector_data, BooleanOperation::Union) + } + GraphicElement::ImageFrame(image) => vector_from_image(image), + // Union all vector data in the artboard into a single vector + GraphicElement::Artboard(artboard) => { + let artboard_subpath = Subpath::new_rect(artboard.location.as_dvec2(), artboard.location.as_dvec2() + artboard.dimensions.as_dvec2()); + + let mut artboard_vector = VectorData::from_subpath(artboard_subpath); + artboard_vector.style.set_fill(graphene_core::vector::style::Fill::Solid(artboard.background)); + + let mut vector_data = vec![artboard_vector]; + vector_data.extend(collect_vector_data(&artboard.graphic_group).into_iter()); + + boolean_operation_on_vector_data(&vector_data, BooleanOperation::Union) + } + } + } + + fn collect_vector_data(graphic_group: &GraphicGroup) -> Vec { + // Ensure all non vector data in the graphic group is converted to vector data + graphic_group.iter().map(union_vector_data).collect::>() + } + + fn subtract<'a>(vector_data: impl Iterator) -> VectorData { + let mut vector_data = vector_data.into_iter(); + let mut result = vector_data.next().cloned().unwrap_or_default(); + let mut next_vector_data = vector_data.next(); + + while let Some(lower_vector_data) = next_vector_data { + let transform_of_lower_into_space_of_upper = result.transform.inverse() * lower_vector_data.transform; + + let upper_path_string = to_svg_string(&result, DAffine2::IDENTITY); + let lower_path_string = to_svg_string(&lower_vector_data, transform_of_lower_into_space_of_upper); + + #[allow(unused_unsafe)] + let boolean_operation_string = unsafe { boolean_subtract(upper_path_string, lower_path_string) }; + let boolean_operation_result = from_svg_string(&boolean_operation_string); + + result.colinear_manipulators = boolean_operation_result.colinear_manipulators; + result.point_domain = boolean_operation_result.point_domain; + result.segment_domain = boolean_operation_result.segment_domain; + result.region_domain = boolean_operation_result.region_domain; + + next_vector_data = vector_data.next(); + } + result + } + + fn boolean_operation_on_vector_data(vector_data: &[VectorData], boolean_operation: BooleanOperation) -> VectorData { + match boolean_operation { + BooleanOperation::Union => { + // Reverse vector data so that the result style is the style of the first vector data + let mut vector_data = vector_data.iter().rev(); + let mut result = vector_data.next().cloned().unwrap_or_default(); + let mut second_vector_data = Some(vector_data.next().unwrap_or(const { &VectorData::empty() })); + + // Loop over all vector data and union it with the result + while let Some(lower_vector_data) = second_vector_data { + let transform_of_lower_into_space_of_upper = result.transform.inverse() * lower_vector_data.transform; + + let upper_path_string = to_svg_string(&result, DAffine2::IDENTITY); + let lower_path_string = to_svg_string(lower_vector_data, transform_of_lower_into_space_of_upper); + + #[allow(unused_unsafe)] + let boolean_operation_string = unsafe { boolean_union(upper_path_string, lower_path_string) }; + let boolean_operation_result = from_svg_string(&boolean_operation_string); + + result.colinear_manipulators = boolean_operation_result.colinear_manipulators; + result.point_domain = boolean_operation_result.point_domain; + result.segment_domain = boolean_operation_result.segment_domain; + result.region_domain = boolean_operation_result.region_domain; + second_vector_data = vector_data.next(); + } + result + } + BooleanOperation::SubtractFront => subtract(vector_data.iter()), + BooleanOperation::SubtractBack => subtract(vector_data.iter().rev()), + BooleanOperation::Intersect => { + let mut vector_data = vector_data.iter().rev(); + let mut result = vector_data.next().cloned().unwrap_or_default(); + let mut second_vector_data = Some(vector_data.next().unwrap_or(const { &VectorData::empty() })); + + // For each vector data, set the result to the intersection of that data and the result + while let Some(lower_vector_data) = second_vector_data { + let transform_of_lower_into_space_of_upper = result.transform.inverse() * lower_vector_data.transform; + + let upper_path_string = to_svg_string(&result, DAffine2::IDENTITY); + let lower_path_string = to_svg_string(lower_vector_data, transform_of_lower_into_space_of_upper); + + #[allow(unused_unsafe)] + let boolean_operation_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) }; + let boolean_operation_result = from_svg_string(&boolean_operation_string); + + result.colinear_manipulators = boolean_operation_result.colinear_manipulators; + result.point_domain = boolean_operation_result.point_domain; + result.segment_domain = boolean_operation_result.segment_domain; + result.region_domain = boolean_operation_result.region_domain; + second_vector_data = vector_data.next(); + } + result + } + BooleanOperation::Difference => { + let mut vector_data_iter = vector_data.iter().rev(); + let mut any_intersection = VectorData::empty(); + let mut second_vector_data = Some(vector_data_iter.next().unwrap_or(const { &VectorData::empty() })); + + // Find where all vector data intersect at least once + while let Some(lower_vector_data) = second_vector_data { + let all_other_vector_data = boolean_operation_on_vector_data(&vector_data.iter().filter(|v| v != &lower_vector_data).cloned().collect::>(), BooleanOperation::Union); + + let transform_of_lower_into_space_of_upper = all_other_vector_data.transform.inverse() * lower_vector_data.transform; + + let upper_path_string = to_svg_string(&all_other_vector_data, DAffine2::IDENTITY); + let lower_path_string = to_svg_string(lower_vector_data, transform_of_lower_into_space_of_upper); + + #[allow(unused_unsafe)] + let boolean_intersection_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) }; + let mut boolean_intersection_result = from_svg_string(&boolean_intersection_string); + + boolean_intersection_result.transform = all_other_vector_data.transform; + boolean_intersection_result.style = all_other_vector_data.style.clone(); + boolean_intersection_result.alpha_blending = all_other_vector_data.alpha_blending; + + let transform_of_lower_into_space_of_upper = boolean_intersection_result.transform.inverse() * any_intersection.transform; + + let upper_path_string = to_svg_string(&boolean_intersection_result, DAffine2::IDENTITY); + let lower_path_string = to_svg_string(&any_intersection, transform_of_lower_into_space_of_upper); + + #[allow(unused_unsafe)] + let union_result = from_svg_string(&unsafe { boolean_union(upper_path_string, lower_path_string) }); + any_intersection = union_result; + + any_intersection.transform = boolean_intersection_result.transform; + any_intersection.style = boolean_intersection_result.style.clone(); + any_intersection.alpha_blending = boolean_intersection_result.alpha_blending; + + second_vector_data = vector_data_iter.next(); + } + // Subtract the area where they intersect at least once from the union of all vector data + let union = boolean_operation_on_vector_data(vector_data, BooleanOperation::Union); + boolean_operation_on_vector_data(&[union, any_intersection], BooleanOperation::SubtractFront) + } + } + } + + // The first index is the bottom of the stack + boolean_operation_on_vector_data(&collect_vector_data(&graphic_group), boolean_operation) +} + fn to_svg_string(vector: &VectorData, transform: DAffine2) -> String { let mut path = String::new(); - for (_, subpath) in vector.region_bezier_paths() { + for subpath in vector.stroke_bezier_paths() { let _ = subpath.subpath_to_svg(&mut path, transform); } path @@ -125,6 +299,4 @@ extern "C" { fn boolean_intersect(path1: String, path2: String) -> String; #[wasm_bindgen(js_name = booleanDifference)] fn boolean_difference(path1: String, path2: String) -> String; - #[wasm_bindgen(js_name = booleanDivide)] - fn boolean_divide(path1: String, path2: String) -> String; } diff --git a/node-graph/interpreted-executor/src/node_registry.rs b/node-graph/interpreted-executor/src/node_registry.rs index c3604b90..e0b8e30a 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -719,7 +719,8 @@ fn node_registry() -> HashMap, input: VectorData, params: [f64, f64, u32]), - async_node!(graphene_std::vector::BooleanOperationNode<_, _>, input: VectorData, output: VectorData, fn_params: [Footprint => VectorData, () => graphene_core::vector::misc::BooleanOperation]), + async_node!(graphene_std::vector::BinaryBooleanOperationNode<_, _>, input: VectorData, output: VectorData, fn_params: [Footprint => VectorData, () => graphene_core::vector::misc::BooleanOperation]), + register_node!(graphene_std::vector::BooleanOperationNode<_>, input: GraphicGroup, fn_params: [() => graphene_core::vector::misc::BooleanOperation]), vec![( ProtoNodeIdentifier::new("graphene_core::transform::CullNode<_>"), |args| {