diff --git a/Cargo.lock b/Cargo.lock index 552e6661..010cfa9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2329,6 +2329,7 @@ dependencies = [ "serde_json", "tokio", "url", + "usvg", "vello", "vulkan-executor", "wasm-bindgen", diff --git a/editor/src/messages/dialog/dialog_message_handler.rs b/editor/src/messages/dialog/dialog_message_handler.rs index 69d658a1..4f3d09ab 100644 --- a/editor/src/messages/dialog/dialog_message_handler.rs +++ b/editor/src/messages/dialog/dialog_message_handler.rs @@ -1,7 +1,6 @@ use super::simple_dialogs::{self, AboutGraphiteDialog, ComingSoonDialog, DemoArtworkDialog, LicensesDialog}; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::prelude::*; -use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name; pub struct DialogMessageData<'a> { pub portfolio: &'a PortfolioMessageHandler, 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 725a6857..38845efe 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 @@ -13,6 +13,7 @@ use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::brush_stroke::BrushStroke; use graphene_core::vector::style::{Fill, Stroke}; use graphene_core::{Artboard, Color}; +use graphene_std::vector::misc::BooleanOperation; use glam::{DAffine2, DVec2, IVec2}; @@ -25,6 +26,10 @@ pub enum GraphOperationMessage { parent: LayerNodeIdentifier, insert_index: isize, }, + CreateBooleanOperationNode { + node_id: NodeId, + operation: BooleanOperation, + }, DisconnectInput { node_id: NodeId, input_index: usize, @@ -38,14 +43,20 @@ pub enum GraphOperationMessage { parent: NodeId, insert_index: usize, }, + InsertBooleanOperation { + operation: BooleanOperation, + }, InsertNodeBetween { + // Post node post_node_id: NodeId, post_node_input_index: usize, - insert_node_output_index: usize, + // Inserted node insert_node_id: NodeId, + insert_node_output_index: usize, insert_node_input_index: usize, - pre_node_output_index: usize, + // Pre node pre_node_id: NodeId, + pre_node_output_index: usize, }, MoveSelectedSiblingsToChild { new_parent: 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 da9dd1bb..434d4398 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 @@ -5,13 +5,13 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc use crate::messages::portfolio::document::utility_types::nodes::{CollapsedLayers, SelectedNodes}; use crate::messages::prelude::*; -use bezier_rs::{ManipulatorGroup, Subpath}; +use graph_craft::document::value::TaggedValue; use graph_craft::document::{generate_uuid, NodeId, NodeInput, NodeNetwork}; use graphene_core::renderer::Quad; use graphene_core::text::Font; -use graphene_core::uuid::ManipulatorGroupId; use graphene_core::vector::style::{Fill, Gradient, GradientType, LineCap, LineJoin, Stroke}; use graphene_core::Color; +use graphene_std::vector::convert_usvg_path; use glam::{DAffine2, DVec2, IVec2}; @@ -96,6 +96,19 @@ impl MessageHandler> for Gr responses.add(NodeGraphMessage::RunDocumentGraph); } + GraphOperationMessage::CreateBooleanOperationNode { node_id, operation } => { + let new_boolean_operation_node = resolve_document_node_type("Boolean Operation") + .expect("Failed to create a Boolean Operation node") + .to_document_node_default_inputs( + [ + Some(NodeInput::value(TaggedValue::VectorData(graphene_std::vector::VectorData::empty()), true)), + Some(NodeInput::value(TaggedValue::VectorData(graphene_std::vector::VectorData::empty()), true)), + Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)), + ], + Default::default(), + ); + document_network.nodes.insert(node_id, new_boolean_operation_node); + } GraphOperationMessage::DisconnectInput { node_id, input_index } => { let Some(node_to_disconnect) = document_network.nodes.get(&node_id) else { warn!("Node {} not found in DisconnectInput", node_id); @@ -174,6 +187,82 @@ impl MessageHandler> for Gr shift_self: true, }); } + GraphOperationMessage::InsertBooleanOperation { operation } => { + let mut selected_layers = selected_nodes.selected_layers(&document_metadata); + + let first_selected_layer = selected_layers.next(); + let second_selected_layer = selected_layers.next(); + let other_selected_layer = selected_layers.next(); + + let (Some(upper_layer), Some(lower_layer), None) = (first_selected_layer, second_selected_layer, other_selected_layer) else { + return; + }; + + let Some(upper_layer_node) = document_network.nodes.get(&upper_layer.to_node()) else { return }; + let Some(lower_layer_node) = document_network.nodes.get(&lower_layer.to_node()) else { return }; + + let Some(NodeInput::Node { + node_id: upper_node_id, + output_index: upper_output_index, + .. + }) = upper_layer_node.inputs.get(1).cloned() + else { + return; + }; + let Some(NodeInput::Node { + node_id: lower_node_id, + output_index: lower_output_index, + .. + }) = lower_layer_node.inputs.get(1).cloned() + else { + return; + }; + + 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 + responses.add(NodeGraphMessage::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(DocumentMessage::DeleteLayer { id: lower_layer.to_node() }); + + // Put the Boolean Operation where the output layer is located, since this is the correct shift relative to its left input chain + responses.add(NodeGraphMessage::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(NodeGraphMessage::ShiftUpstream { + node_id: boolean_operation_node_id, + shift: (-8, 0).into(), + shift_self: true, + }) + } GraphOperationMessage::InsertNodeBetween { post_node_id, post_node_input_index, @@ -639,47 +728,3 @@ fn apply_usvg_fill(fill: &Option, modify_inputs: &mut ModifyInputsCo }); } } - -fn convert_usvg_path(path: &usvg::Path) -> Vec> { - let mut subpaths = Vec::new(); - let mut groups = Vec::new(); - - let mut points = path.data.points().iter(); - let to_vec = |p: &usvg::tiny_skia_path::Point| DVec2::new(p.x as f64, p.y as f64); - - for verb in path.data.verbs() { - match verb { - usvg::tiny_skia_path::PathVerb::Move => { - subpaths.push(Subpath::new(std::mem::take(&mut groups), false)); - let Some(start) = points.next().map(to_vec) else { continue }; - groups.push(ManipulatorGroup::new(start, Some(start), Some(start))); - } - usvg::tiny_skia_path::PathVerb::Line => { - let Some(end) = points.next().map(to_vec) else { continue }; - groups.push(ManipulatorGroup::new(end, Some(end), Some(end))); - } - usvg::tiny_skia_path::PathVerb::Quad => { - let Some(handle) = points.next().map(to_vec) else { continue }; - let Some(end) = points.next().map(to_vec) else { continue }; - if let Some(last) = groups.last_mut() { - last.out_handle = Some(last.anchor + (2. / 3.) * (handle - last.anchor)); - } - groups.push(ManipulatorGroup::new(end, Some(end + (2. / 3.) * (handle - end)), Some(end))); - } - usvg::tiny_skia_path::PathVerb::Cubic => { - let Some(first_handle) = points.next().map(to_vec) else { continue }; - let Some(second_handle) = points.next().map(to_vec) else { continue }; - let Some(end) = points.next().map(to_vec) else { continue }; - if let Some(last) = groups.last_mut() { - last.out_handle = Some(first_handle); - } - groups.push(ManipulatorGroup::new(end, Some(second_handle), Some(end))); - } - usvg::tiny_skia_path::PathVerb::Close => { - subpaths.push(Subpath::new(std::mem::take(&mut groups), true)); - } - } - } - subpaths.push(Subpath::new(groups, false)); - subpaths -} 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 c67f4df8..5f53c5d1 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 @@ -2507,6 +2507,19 @@ fn static_nodes() -> Vec { properties: node_properties::circular_repeat_properties, ..Default::default() }, + DocumentNodeDefinition { + name: "Boolean Operation", + category: "Vector", + implementation: DocumentNodeImplementation::proto("graphene_std::vector::BooleanOperationNode<_, _>"), + 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::Subpath)], + properties: node_properties::boolean_operation_properties, + ..Default::default() + }, DocumentNodeDefinition { name: "Copy to Points", category: "Vector", 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 41693163..27ef4fa3 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -18,6 +18,7 @@ use graphene_core::vector::misc::CentroidType; use graphene_core::vector::style::{FillType, GradientType, LineCap, LineJoin}; use glam::{DVec2, IVec2, UVec2}; +use graphene_std::vector::misc::BooleanOperation; pub fn string_properties(text: impl Into) -> Vec { let widget = TextLabel::new(text).widget_holder(); @@ -321,7 +322,7 @@ fn font_inputs(document_node: &DocumentNode, node_id: NodeId, index: usize, name } fn vector_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> Vec { - let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Vector, blank_assist); + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::Subpath, blank_assist); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(TextLabel::new("Vector data must be supplied through the graph").widget_holder()); @@ -611,6 +612,36 @@ fn luminance_calculation(document_node: &DocumentNode, node_id: NodeId, index: u LayoutGroup::Row { widgets }.with_tooltip("Formula used to calculate the luminance of a pixel") } +fn boolean_operation_radio_buttons(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); + + if let &NodeInput::Value { + tagged_value: TaggedValue::BooleanOperation(calculation), + exposed: false, + } = &document_node.inputs[index] + { + let operations = BooleanOperation::list(); + let icons = BooleanOperation::icons(); + let mut entries = Vec::with_capacity(operations.len()); + + for (operation, icon) in operations.into_iter().zip(icons.into_iter()) { + entries.push( + RadioEntryData::new(format!("{operation:?}")) + .icon(icon) + .tooltip(operation.to_string()) + .on_update(update_value(move |_| TaggedValue::BooleanOperation(operation), node_id, index)) + .on_commit(commit_value), + ); + } + + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + RadioInput::new(entries).selected_index(Some(calculation as u32)).widget_holder(), + ]); + } + LayoutGroup::Row { widgets } +} + fn line_cap_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, FrontendGraphDataType::General, blank_assist); if let &NodeInput::Value { @@ -2330,6 +2361,13 @@ 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); + + vec![LayoutGroup::Row { widgets: other_vector_data }, opeartion] +} + pub fn copy_to_points_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec { let instance = vector_widget(document_node, node_id, 1, "Instance", true); diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index f2d36dc7..a89e80fd 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -17,6 +17,7 @@ use crate::messages::tool::common_functionality::transformation_cage::*; use graph_craft::document::{DocumentNode, NodeId, NodeNetwork}; use graphene_core::renderer::Quad; +use graphene_std::vector::misc::BooleanOperation; use std::fmt; @@ -147,10 +148,12 @@ impl SelectTool { } fn boolean_widgets(&self) -> impl Iterator { - ["Union", "Subtract Front", "Subtract Back", "Intersect", "Difference"].into_iter().map(|name| { - IconButton::new(format!("Boolean{}", name.replace(' ', "")), 24) - .tooltip(format!("Boolean {name} (coming soon)")) - .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into()) + let operations = BooleanOperation::list(); + let icons = BooleanOperation::icons(); + operations.into_iter().zip(icons.into_iter()).map(|(operation, icon)| { + IconButton::new(icon, 24) + .tooltip(operation.to_string()) + .on_update(move |_| GraphOperationMessage::InsertBooleanOperation { operation }.into()) .widget_holder() }) } @@ -191,7 +194,7 @@ impl LayoutHolder for SelectTool { widgets.extend(self.flip_widgets(disabled)); // Boolean - if self.tool_data.selected_layers_count >= 2 { + if self.tool_data.selected_layers_count == 2 { widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.extend(self.boolean_widgets()); } diff --git a/frontend/assets/icon-16px-solid/boolean-divide.svg b/frontend/assets/icon-16px-solid/boolean-divide.svg new file mode 100644 index 00000000..fca2f12b --- /dev/null +++ b/frontend/assets/icon-16px-solid/boolean-divide.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index add894f6..8f9cf5d9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "@tauri-apps/api": "^1.5.3", "class-transformer": "^0.5.1", "idb-keyval": "^6.2.1", + "paper": "^0.12.17", "reflect-metadata": "^0.2.1" }, "devDependencies": { @@ -3952,6 +3953,14 @@ "url": "https://github.com/sponsors/dword-design" } }, + "node_modules/paper": { + "version": "0.12.17", + "resolved": "https://registry.npmjs.org/paper/-/paper-0.12.17.tgz", + "integrity": "sha512-oCe+e1C2w8hKIcGoAqUjD0GGxGPv+itrRXlEFUmp3H8tY/NTnHOkYgpJFPGw6OJ8Q1Wa6+RgzlY7Dx/2WWHtkA==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8224,6 +8233,11 @@ "integrity": "sha512-gFL35q7kbE/zBaPA3UKhp2vSzcPYx2ecbYuwv1ucE9Il6IIgBDweBlH8D68UFGZic2MkllKa2KHCfC1IQBQUYA==", "dev": true }, + "paper": { + "version": "0.12.17", + "resolved": "https://registry.npmjs.org/paper/-/paper-0.12.17.tgz", + "integrity": "sha512-oCe+e1C2w8hKIcGoAqUjD0GGxGPv+itrRXlEFUmp3H8tY/NTnHOkYgpJFPGw6OJ8Q1Wa6+RgzlY7Dx/2WWHtkA==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 42d0983d..1369f6c9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "@tauri-apps/api": "^1.5.3", "class-transformer": "^0.5.1", "idb-keyval": "^6.2.1", + "paper": "^0.12.17", "reflect-metadata": "^0.2.1" }, "devDependencies": { diff --git a/frontend/src/utility-functions/computational-geometry.ts b/frontend/src/utility-functions/computational-geometry.ts new file mode 100644 index 00000000..922ada78 --- /dev/null +++ b/frontend/src/utility-functions/computational-geometry.ts @@ -0,0 +1,34 @@ +import paper from "paper/dist/paper-core"; + +// Required setup to be used headlessly +paper.setup(new paper.Size(1, 1)); +paper.view.autoUpdate = false; + +export function booleanUnion(path1: string, path2: string): string { + return booleanOperation(path1, path2, "unite"); +} + +export function booleanSubtract(path1: string, path2: string): string { + return booleanOperation(path1, path2, "subtract"); +} + +export function booleanIntersect(path1: string, path2: string): string { + return booleanOperation(path1, path2, "intersect"); +} + +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.Path(path1); + const paperPath2 = new paper.Path(path2); + const result = paperPath1[operation](paperPath2); + paperPath1.remove(); + paperPath2.remove(); + return result.pathData; +} diff --git a/frontend/src/utility-functions/icons.ts b/frontend/src/utility-functions/icons.ts index 37204edd..13834eed 100644 --- a/frontend/src/utility-functions/icons.ts +++ b/frontend/src/utility-functions/icons.ts @@ -97,6 +97,7 @@ import AlignTop from "@graphite-frontend/assets/icon-16px-solid/align-top.svg"; import AlignVerticalCenter from "@graphite-frontend/assets/icon-16px-solid/align-vertical-center.svg"; import Artboard from "@graphite-frontend/assets/icon-16px-solid/artboard.svg"; import BooleanDifference from "@graphite-frontend/assets/icon-16px-solid/boolean-difference.svg"; +import BooleanDivide from "@graphite-frontend/assets/icon-16px-solid/boolean-divide.svg"; import BooleanIntersect from "@graphite-frontend/assets/icon-16px-solid/boolean-intersect.svg"; import BooleanSubtractBack from "@graphite-frontend/assets/icon-16px-solid/boolean-subtract-back.svg"; import BooleanSubtractFront from "@graphite-frontend/assets/icon-16px-solid/boolean-subtract-front.svg"; @@ -171,6 +172,7 @@ const SOLID_16PX = { AlignVerticalCenter: { svg: AlignVerticalCenter, size: 16 }, Artboard: { svg: Artboard, size: 16 }, BooleanDifference: { svg: BooleanDifference, size: 16 }, + BooleanDivide: { svg: BooleanDivide, size: 16 }, BooleanIntersect: { svg: BooleanIntersect, size: 16 }, BooleanSubtractBack: { svg: BooleanSubtractBack, size: 16 }, BooleanSubtractFront: { svg: BooleanSubtractFront, size: 16 }, diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index da48ee10..c7ed14df 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -9,3 +9,44 @@ pub enum CentroidType { /// The center of mass for the arc length of a curved shape's perimeter, as if made out of an infinitely thin wire. Length, } + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] +pub enum BooleanOperation { + #[default] + Union, + SubtractFront, + SubtractBack, + Intersect, + Difference, + Divide, +} + +impl BooleanOperation { + pub fn list() -> [BooleanOperation; 6] { + [ + BooleanOperation::Union, + BooleanOperation::SubtractFront, + BooleanOperation::SubtractBack, + BooleanOperation::Intersect, + BooleanOperation::Difference, + BooleanOperation::Divide, + ] + } + + pub fn icons() -> [&'static str; 6] { + ["BooleanUnion", "BooleanSubtractFront", "BooleanSubtractBack", "BooleanIntersect", "BooleanDifference", "BooleanDivide"] + } +} + +impl core::fmt::Display for BooleanOperation { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + BooleanOperation::Union => write!(f, "Union"), + BooleanOperation::SubtractFront => write!(f, "Subtract Front"), + 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/graph-craft/src/document.rs b/node-graph/graph-craft/src/document.rs index 189ef99f..2aad66c6 100644 --- a/node-graph/graph-craft/src/document.rs +++ b/node-graph/graph-craft/src/document.rs @@ -16,6 +16,13 @@ pub mod value; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, specta::Type)] pub struct NodeId(pub u64); +// TODO: Find and replace all `NodeId(generate_uuid())` with `NodeId::new()`. +impl NodeId { + pub fn new() -> Self { + Self(generate_uuid()) + } +} + impl core::fmt::Display for NodeId { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!(f, "{}", self.0) diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 66111d61..29740c17 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -78,6 +78,7 @@ pub enum TaggedValue { RenderOutput(RenderOutput), Palette(Vec), CentroidType(graphene_core::vector::misc::CentroidType), + BooleanOperation(graphene_core::vector::misc::BooleanOperation), } #[allow(clippy::derived_hash_with_manual_eq)] @@ -158,6 +159,7 @@ impl Hash for TaggedValue { Self::RenderOutput(x) => x.hash(state), Self::Palette(x) => x.hash(state), Self::CentroidType(x) => x.hash(state), + Self::BooleanOperation(x) => x.hash(state), } } } @@ -225,6 +227,7 @@ impl<'a> TaggedValue { TaggedValue::RenderOutput(x) => Box::new(x), TaggedValue::Palette(x) => Box::new(x), TaggedValue::CentroidType(x) => Box::new(x), + TaggedValue::BooleanOperation(x) => Box::new(x), } } @@ -303,6 +306,7 @@ impl<'a> TaggedValue { TaggedValue::RenderOutput(_) => concrete!(RenderOutput), TaggedValue::Palette(_) => concrete!(Vec), TaggedValue::CentroidType(_) => concrete!(graphene_core::vector::misc::CentroidType), + TaggedValue::BooleanOperation(_) => concrete!(graphene_core::vector::misc::BooleanOperation), } } @@ -371,6 +375,7 @@ impl<'a> TaggedValue { x if x == TypeId::of::() => Ok(TaggedValue::Footprint(*downcast(input).unwrap())), x if x == TypeId::of::>() => Ok(TaggedValue::Palette(*downcast(input).unwrap())), x if x == TypeId::of::() => Ok(TaggedValue::CentroidType(*downcast(input).unwrap())), + x if x == TypeId::of::() => Ok(TaggedValue::BooleanOperation(*downcast(input).unwrap())), _ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))), } } diff --git a/node-graph/gstd/Cargo.toml b/node-graph/gstd/Cargo.toml index d64aa17f..f4c7ca1f 100644 --- a/node-graph/gstd/Cargo.toml +++ b/node-graph/gstd/Cargo.toml @@ -73,6 +73,7 @@ tokio = { workspace = true, optional = true, features = ["fs", "io-std"] } image-compare = { version = "0.3.0", optional = true } vello = { workspace = true, optional = true } resvg = { workspace = true, optional = true } +usvg = { workspace = true } serde = { workspace = true, optional = true, features = ["derive"] } web-sys = { workspace = true, optional = true, features = [ "Window", diff --git a/node-graph/gstd/src/lib.rs b/node-graph/gstd/src/lib.rs index b54f4af7..a705c5e5 100644 --- a/node-graph/gstd/src/lib.rs +++ b/node-graph/gstd/src/lib.rs @@ -7,6 +7,8 @@ extern crate log; pub mod raster; +pub mod vector; + pub mod http; pub mod any; diff --git a/node-graph/gstd/src/vector.rs b/node-graph/gstd/src/vector.rs new file mode 100644 index 00000000..8fc5e056 --- /dev/null +++ b/node-graph/gstd/src/vector.rs @@ -0,0 +1,131 @@ +use crate::Node; + +use bezier_rs::{ManipulatorGroup, Subpath}; +use graphene_core::transform::Footprint; +use graphene_core::uuid::ManipulatorGroupId; +use graphene_core::vector::misc::BooleanOperation; +pub use graphene_core::vector::*; + +use futures::Future; +use glam::{DAffine2, DVec2}; +use wasm_bindgen::prelude::*; + +pub struct BooleanOperationNode { + lower_vector_data: LowerVectorData, + boolean_operation: BooleanOp, +} + +#[node_macro::node_fn(BooleanOperationNode)] +async fn boolean_operation_node>( + upper_vector_data: VectorData, + lower_vector_data: impl Node, + boolean_operation: BooleanOperation, +) -> VectorData { + let lower_vector_data = self.lower_vector_data.eval(Footprint::default()).await; + let transform_of_lower_into_space_of_upper = upper_vector_data.transform.inverse() * lower_vector_data.transform; + + let upper_path_string = to_svg_string(&upper_vector_data, DAffine2::IDENTITY); + let lower_path_string = to_svg_string(&lower_vector_data, transform_of_lower_into_space_of_upper); + + let mut use_lower_style = false; + + #[allow(unused_unsafe)] + let result = unsafe { + match boolean_operation { + BooleanOperation::Union => boolean_union(upper_path_string, lower_path_string), + BooleanOperation::SubtractFront => { + use_lower_style = true; + boolean_subtract(lower_path_string, upper_path_string) + } + 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), + } + }; + + let mut result = from_svg_string(&result); + result.transform = upper_vector_data.transform; + result.style = if use_lower_style { lower_vector_data.style } else { upper_vector_data.style }; + result.alpha_blending = if use_lower_style { lower_vector_data.alpha_blending } else { upper_vector_data.alpha_blending }; + + result +} + +fn to_svg_string(vector: &VectorData, transform: DAffine2) -> String { + let mut path = String::new(); + for (_, subpath) in vector.region_bezier_paths() { + let _ = subpath.subpath_to_svg(&mut path, transform); + } + path +} + +fn from_svg_string(svg_string: &str) -> VectorData { + let svg = format!(r#""#, svg_string); + let Some(tree) = usvg::Tree::from_str(&svg, &Default::default()).ok() else { + return VectorData::empty(); + }; + let Some(usvg::Node::Path(path)) = tree.root.children.first() else { + return VectorData::empty(); + }; + + VectorData::from_subpaths(convert_usvg_path(path)) +} + +pub fn convert_usvg_path(path: &usvg::Path) -> Vec> { + let mut subpaths = Vec::new(); + let mut groups = Vec::new(); + + let mut points = path.data.points().iter(); + let to_vec = |p: &usvg::tiny_skia_path::Point| DVec2::new(p.x as f64, p.y as f64); + + for verb in path.data.verbs() { + match verb { + usvg::tiny_skia_path::PathVerb::Move => { + subpaths.push(Subpath::new(std::mem::take(&mut groups), false)); + let Some(start) = points.next().map(to_vec) else { continue }; + groups.push(ManipulatorGroup::new(start, Some(start), Some(start))); + } + usvg::tiny_skia_path::PathVerb::Line => { + let Some(end) = points.next().map(to_vec) else { continue }; + groups.push(ManipulatorGroup::new(end, Some(end), Some(end))); + } + usvg::tiny_skia_path::PathVerb::Quad => { + let Some(handle) = points.next().map(to_vec) else { continue }; + let Some(end) = points.next().map(to_vec) else { continue }; + if let Some(last) = groups.last_mut() { + last.out_handle = Some(last.anchor + (2. / 3.) * (handle - last.anchor)); + } + groups.push(ManipulatorGroup::new(end, Some(end + (2. / 3.) * (handle - end)), Some(end))); + } + usvg::tiny_skia_path::PathVerb::Cubic => { + let Some(first_handle) = points.next().map(to_vec) else { continue }; + let Some(second_handle) = points.next().map(to_vec) else { continue }; + let Some(end) = points.next().map(to_vec) else { continue }; + if let Some(last) = groups.last_mut() { + last.out_handle = Some(first_handle); + } + groups.push(ManipulatorGroup::new(end, Some(second_handle), Some(end))); + } + usvg::tiny_skia_path::PathVerb::Close => { + subpaths.push(Subpath::new(std::mem::take(&mut groups), true)); + } + } + } + subpaths.push(Subpath::new(groups, false)); + subpaths +} + +#[wasm_bindgen(module = "/../../frontend/src/utility-functions/computational-geometry.ts")] +extern "C" { + #[wasm_bindgen(js_name = booleanUnion)] + fn boolean_union(path1: String, path2: String) -> String; + #[wasm_bindgen(js_name = booleanSubtract)] + fn boolean_subtract(path1: String, path2: String) -> String; + #[wasm_bindgen(js_name = booleanIntersect)] + 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 424e7198..c8454804 100644 --- a/node-graph/interpreted-executor/src/node_registry.rs +++ b/node-graph/interpreted-executor/src/node_registry.rs @@ -713,6 +713,7 @@ 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]), vec![( ProtoNodeIdentifier::new("graphene_core::transform::CullNode<_>"), |args| {