Add boolean operations (#1759)
This commit is contained in:
parent
c80de41d28
commit
d40fb6caad
|
|
@ -2329,6 +2329,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tokio",
|
||||
"url",
|
||||
"usvg",
|
||||
"vello",
|
||||
"vulkan-executor",
|
||||
"wasm-bindgen",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()))),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ extern crate log;
|
|||
|
||||
pub mod raster;
|
||||
|
||||
pub mod vector;
|
||||
|
||||
pub mod http;
|
||||
|
||||
pub mod any;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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| {
|
||||
|
|
|
|||
Loading…
Reference in New Issue