Add boolean operations (#1759)

This commit is contained in:
Keavon Chambers 2024-05-25 22:02:00 -07:00 committed by GitHub
parent c80de41d28
commit d40fb6caad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 409 additions and 55 deletions

1
Cargo.lock generated
View File

@ -2329,6 +2329,7 @@ dependencies = [
"serde_json",
"tokio",
"url",
"usvg",
"vello",
"vulkan-executor",
"wasm-bindgen",

View File

@ -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,

View File

@ -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,

View File

@ -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<GraphOperationMessage, GraphOperationMessageData<'_>> 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<GraphOperationMessage, GraphOperationMessageData<'_>> 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<usvg::Fill>, modify_inputs: &mut ModifyInputsCo
});
}
}
fn convert_usvg_path(path: &usvg::Path) -> Vec<Subpath<ManipulatorGroupId>> {
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
}

View File

@ -2507,6 +2507,19 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
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",

View File

@ -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<String>) -> Vec<LayoutGroup> {
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<WidgetHolder> {
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<LayoutGroup> {
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<LayoutGroup> {
let instance = vector_widget(document_node, node_id, 1, "Instance", true);

View File

@ -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<Item = WidgetHolder> {
["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());
}

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<rect x="4" y="4" width="8" height="8" />
<polygon points="13 4 13 12 13 13 12 13 4 13 4 16 16 16 16 4 13 4" />
<polygon points="3 3 4 3 12 3 12 0 0 0 0 12 3 12 3 4 3 3" />
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@ -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",

View File

@ -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": {

View File

@ -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;
}

View File

@ -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 },

View File

@ -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"),
}
}
}

View File

@ -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)

View File

@ -78,6 +78,7 @@ pub enum TaggedValue {
RenderOutput(RenderOutput),
Palette(Vec<Color>),
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<Color>),
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::<graphene_core::transform::Footprint>() => Ok(TaggedValue::Footprint(*downcast(input).unwrap())),
x if x == TypeId::of::<Vec<Color>>() => Ok(TaggedValue::Palette(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::misc::CentroidType>() => Ok(TaggedValue::CentroidType(*downcast(input).unwrap())),
x if x == TypeId::of::<graphene_core::vector::misc::BooleanOperation>() => Ok(TaggedValue::BooleanOperation(*downcast(input).unwrap())),
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
}
}

View File

@ -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",

View File

@ -7,6 +7,8 @@ extern crate log;
pub mod raster;
pub mod vector;
pub mod http;
pub mod any;

View File

@ -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<LowerVectorData, BooleanOp> {
lower_vector_data: LowerVectorData,
boolean_operation: BooleanOp,
}
#[node_macro::node_fn(BooleanOperationNode)]
async fn boolean_operation_node<Fut: Future<Output = VectorData>>(
upper_vector_data: VectorData,
lower_vector_data: impl Node<Footprint, Output = Fut>,
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 xmlns="http://www.w3.org/2000/svg"><path d="{}"></path></svg>"#, 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<Subpath<ManipulatorGroupId>> {
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;
}

View File

@ -713,6 +713,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
register_node!(graphene_core::vector::BoundingBoxNode, input: VectorData, params: []),
register_node!(graphene_core::vector::SolidifyStrokeNode, input: VectorData, params: []),
register_node!(graphene_core::vector::CircularRepeatNode<_, _, _>, 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| {