Separate the Merge node from the Boolean Operation node (#1933)

* Rework boolean operation node

* Set Boolean Operation name for layer

* Remove memoize

* Update both demo artworks that use booleans

* Delete dead code, rename input connectors

* Remove more dead code

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
adamgerhant 2024-08-15 17:36:07 -07:00 committed by GitHub
parent fa981a0897
commit 33739b9ad4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 60 additions and 273 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -24,7 +24,7 @@ use crate::messages::tool::utility_types::ToolType;
use crate::node_graph_executor::NodeGraphExecutor;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
use graph_craft::document::{NodeId, NodeNetwork, OldNodeNetwork};
use graphene_core::raster::BlendMode;
use graphene_core::raster::ImageFrame;
use graphene_core::vector::style::ViewMode;
@ -329,28 +329,19 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent);
let folder_id = NodeId(generate_uuid());
let new_group_node = super::node_graph::document_node_types::resolve_document_node_type("Boolean Operation")
.expect("Failed to create merge node")
.node_template_input_override([
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)),
]);
responses.add(NodeGraphMessage::InsertNode {
node_id: folder_id,
node_template: new_group_node,
});
let new_group_folder = LayerNodeIdentifier::new_unchecked(folder_id);
// Move the boolean operation to the correct position
responses.add(NodeGraphMessage::MoveLayerToStack {
layer: new_group_folder,
let boolean_operation_layer = LayerNodeIdentifier::new_unchecked(folder_id);
responses.add(GraphOperationMessage::NewBooleanOperationLayer {
id: folder_id,
operation,
parent,
insert_index,
});
responses.add(NodeGraphMessage::SetDisplayNameImpl {
node_id: folder_id,
alias: "Boolean Operation".to_string(),
});
// Move all shallowest selected layers as children
responses.add(DocumentMessage::MoveSelectedLayersToGroup { parent: new_group_folder });
responses.add(DocumentMessage::MoveSelectedLayersToGroup { parent: boolean_operation_layer });
}
DocumentMessage::CreateEmptyFolder => {
let id = NodeId(generate_uuid());
@ -651,10 +642,11 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
if self.graph_view_overlay_open {
responses.add(NodeGraphMessage::ShiftNodes {
node_ids: self.network_interface.selected_nodes(&[]).unwrap().selected_nodes().cloned().collect(),
displacement_x: delta_x.signum() as i32,
displacement_y: delta_y.signum() as i32,
displacement_x: if delta_x == 0.0 { 0 } else { delta_x.signum() as i32 },
displacement_y: if delta_y == 0.0 { 0 } else { delta_y.signum() as i32 },
move_upstream: ipp.keyboard.get(Key::Shift as usize),
});
return;
}

View file

@ -71,6 +71,12 @@ pub enum GraphOperationMessage {
parent: LayerNodeIdentifier,
insert_index: usize,
},
NewBooleanOperationLayer {
id: NodeId,
operation: graphene_std::vector::misc::BooleanOperation,
parent: LayerNodeIdentifier,
insert_index: usize,
},
NewCustomLayer {
id: NodeId,
nodes: Vec<(NodeId, NodeTemplate)>,

View file

@ -145,6 +145,13 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
GraphOperationMessage::NewBooleanOperationLayer { id, operation, parent, insert_index } => {
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
let layer = modify_inputs.create_layer(id);
modify_inputs.insert_boolean_data(operation, layer);
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
responses.add(NodeGraphMessage::RunDocumentGraph);
}
GraphOperationMessage::NewCustomLayer { id, nodes, parent, insert_index } => {
let mut modify_inputs = ModifyInputsContext::new(network_interface, responses);
let layer = modify_inputs.create_layer(id);

View file

@ -132,6 +132,18 @@ impl<'a> ModifyInputsContext<'a> {
self.network_interface.insert_node(new_id, artboard_node_template, &[]);
LayerNodeIdentifier::new(new_id, self.network_interface)
}
pub fn insert_boolean_data(&mut self, operation: graphene_std::vector::misc::BooleanOperation, layer: LayerNodeIdentifier) {
let boolean = resolve_document_node_type("Boolean Operation").expect("Boolean node does not exist").node_template_input_override([
Some(NodeInput::value(TaggedValue::GraphicGroup(graphene_std::GraphicGroup::EMPTY), true)),
Some(NodeInput::value(TaggedValue::BooleanOperation(operation), false)),
]);
let boolean_id = NodeId(generate_uuid());
self.network_interface.insert_node(boolean_id, boolean, &[]);
self.network_interface.move_node_to_chain_start(&boolean_id, layer, &[]);
}
pub fn insert_vector_data(&mut self, subpaths: Vec<Subpath<PointId>>, layer: LayerNodeIdentifier) {
let shape = resolve_document_node_type("Shape")
.expect("Shape node does not exist")

View file

@ -3719,201 +3719,20 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
category: "Vector",
properties: node_properties::circular_repeat_properties,
},
DocumentNodeDefinition {
identifier: "Binary Boolean Operation",
node_template: NodeTemplate {
document_node: DocumentNode {
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(1), 0)],
nodes: [
DocumentNode {
inputs: vec![
NodeInput::network(concrete!(graphene_core::vector::VectorData), 0),
NodeInput::network(concrete!(graphene_core::vector::VectorData), 1),
NodeInput::network(concrete!(vector::misc::BooleanOperation), 2),
],
implementation: DocumentNodeImplementation::proto("graphene_std::vector::BinaryBooleanOperationNode<_, _>"),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::ImpureMemoNode<_, _, _>")),
manual_composition: Some(concrete!(Footprint)),
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
inputs: vec![
NodeInput::value(TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
NodeInput::value(TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
NodeInput::value(TaggedValue::BooleanOperation(vector::misc::BooleanOperation::Union), false),
],
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "BinaryBooleanOperation".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-17, -3)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "MemoizeImpure".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-10, -3)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
},
..Default::default()
}),
input_names: vec!["Upper Vector Data".to_string(), "Lower Vector Data".to_string(), "Operation".to_string()],
output_names: vec!["Vector".to_string()],
..Default::default()
},
},
category: "Vector",
properties: node_properties::binary_boolean_operation_properties,
},
DocumentNodeDefinition {
identifier: "Boolean Operation",
node_template: NodeTemplate {
document_node: DocumentNode {
inputs: vec![
NodeInput::value(TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
NodeInput::value(TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
NodeInput::value(TaggedValue::BooleanOperation(vector::misc::BooleanOperation::Union), false),
],
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(5), 0)],
nodes: [
// Primary (bottom) input type coercion
DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 0)],
implementation: DocumentNodeImplementation::proto("graphene_core::ToGraphicGroupNode"),
..Default::default()
},
// Secondary (left) input type coercion
DocumentNode {
inputs: vec![NodeInput::network(generic!(T), 1), NodeInput::network(concrete!(vector::misc::BooleanOperation), 2)],
implementation: DocumentNodeImplementation::proto("graphene_std::vector::BooleanOperationNode<_>"),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(1), 0)],
implementation: DocumentNodeImplementation::proto("graphene_core::ToGraphicElementNode"),
..Default::default()
},
DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_core::memo::ImpureMemoNode<_, _, _>")),
manual_composition: Some(concrete!(Footprint)),
..Default::default()
},
// The monitor node is used to display a thumbnail in the UI
DocumentNode {
inputs: vec![NodeInput::node(NodeId(3), 0)],
implementation: DocumentNodeImplementation::proto("graphene_core::memo::MonitorNode<_, _, _>"),
manual_composition: Some(generic!(T)),
skip_deduplication: true,
..Default::default()
},
DocumentNode {
manual_composition: Some(concrete!(Footprint)),
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::node(NodeId(4), 0)],
implementation: DocumentNodeImplementation::proto("graphene_core::ConstructLayerNode<_, _>"),
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
}),
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("graphene_std::vector::BooleanOperationNode<_>")),
..Default::default()
},
persistent_node_metadata: DocumentNodePersistentMetadata {
network_metadata: Some(NodeNetworkMetadata {
persistent_metadata: NodeNetworkPersistentMetadata {
node_metadata: [
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "ToGraphicGroup".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-9, -3)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "BooleanOperation".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-16, -1)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "ToGraphicElement".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-9, -1)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "MemoizeImpure".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(-2, -1)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "Monitor".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(5, -1)),
..Default::default()
},
..Default::default()
},
DocumentNodeMetadata {
persistent_metadata: DocumentNodePersistentMetadata {
display_name: "ConstructLayer".to_string(),
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(12, -3)),
..Default::default()
},
..Default::default()
},
]
.into_iter()
.enumerate()
.map(|(id, node)| (NodeId(id as u64), node))
.collect(),
..Default::default()
},
..Default::default()
}),
input_names: vec!["Graphical Data".to_string(), "Vector Data".to_string(), "Operation".to_string()],
input_names: vec!["Group of Paths".to_string(), "Operation".to_string()],
output_names: vec!["Vector".to_string()],
node_type_metadata: NodeTypePersistentMetadata::layer(IVec2::new(0, 0)),
..Default::default()
},
},

View file

@ -1043,7 +1043,7 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
responses.add(NodeGraphMessage::SendGraph);
}
NodeGraphMessage::SetDisplayNameImpl { node_id, alias } => {
network_interface.set_display_name(&node_id, selection_network_path, alias);
network_interface.set_display_name(&node_id, alias, selection_network_path);
}
NodeGraphMessage::TogglePreview { node_id } => {
responses.add(DocumentMessage::StartTransaction);

View file

@ -2398,18 +2398,18 @@ pub fn circular_repeat_properties(document_node: &DocumentNode, node_id: NodeId,
]
}
pub fn binary_boolean_operation_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
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: lower_vector_data }, operation]
}
pub fn boolean_operation_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
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);
let group_of_paths_index = 0;
let operation_index = 1;
vec![LayoutGroup::Row { widgets: vector_data }, operation]
let mut widgets = start_widgets(document_node, node_id, group_of_paths_index, "Group of Paths", FrontendGraphDataType::Graphic, true);
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
widgets.push(TextLabel::new("The output of a layer stack, which contains all elements to operate on").widget_holder());
let operation = boolean_operation_radio_buttons(document_node, node_id, operation_index, "Operation", true);
vec![LayoutGroup::Row { widgets }, operation]
}
pub fn copy_to_points_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {

View file

@ -3027,7 +3027,7 @@ impl NodeNetworkInterface {
// }
// }
pub fn set_display_name(&mut self, node_id: &NodeId, network_path: &[NodeId], display_name: String) {
pub fn set_display_name(&mut self, node_id: &NodeId, display_name: String, network_path: &[NodeId]) {
let Some(node_metadata) = self.node_metadata_mut(node_id, network_path) else {
log::error!("Could not get node {node_id} in set_visibility");
return;

View file

@ -16,11 +16,7 @@ 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");
}
function booleanOperation(path1: string, path2: string, operation: "unite" | "subtract" | "intersect" | "exclude"): string {
function booleanOperation(path1: string, path2: string, operation: "unite" | "subtract" | "intersect"): string {
const paperPath1 = new paper.CompoundPath(path1);
const paperPath2 = new paper.CompoundPath(path2);
const result = paperPath1[operation](paperPath2);

View file

@ -2,58 +2,20 @@ use crate::Node;
use bezier_rs::{ManipulatorGroup, Subpath};
use graphene_core::transform::Transform;
use graphene_core::vector::misc::BooleanOperation;
pub use graphene_core::vector::*;
use graphene_core::Color;
use graphene_core::{transform::Footprint, GraphicGroup};
use graphene_core::{vector::misc::BooleanOperation, GraphicElement};
use graphene_core::{Color, GraphicElement, GraphicGroup};
use glam::{DAffine2, DVec2};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
pub struct BinaryBooleanOperationNode<LowerVectorData, BooleanOp> {
lower_vector_data: LowerVectorData,
boolean_operation: BooleanOp,
}
#[node_macro::node_fn(BinaryBooleanOperationNode)]
async fn binary_boolean_operation_node(upper_vector_data: VectorData, lower_vector_data: impl Node<Footprint, Output = VectorData>, 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),
}
};
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
}
pub struct BooleanOperationNode<BooleanOp> {
boolean_operation: BooleanOp,
operation: BooleanOp,
}
#[node_macro::node_fn(BooleanOperationNode)]
fn boolean_operation_node(graphic_group: GraphicGroup, boolean_operation: BooleanOperation) -> VectorData {
fn boolean_operation_node(group_of_paths: GraphicGroup, operation: BooleanOperation) -> VectorData {
fn vector_from_image<T: Transform>(image_frame: T) -> VectorData {
let corner1 = DVec2::ZERO;
let corner2 = DVec2::new(1., 1.);
@ -212,7 +174,7 @@ fn boolean_operation_node(graphic_group: GraphicGroup, boolean_operation: Boolea
}
// The first index is the bottom of the stack
boolean_operation_on_vector_data(&collect_vector_data(&graphic_group), boolean_operation)
boolean_operation_on_vector_data(&collect_vector_data(&group_of_paths), operation)
}
fn to_svg_string(vector: &VectorData, transform: DAffine2) -> String {
@ -288,8 +250,6 @@ extern "C" {
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;
}
#[cfg(not(target_arch = "wasm32"))]
fn boolean_union(_path1: String, _path2: String) -> String {
@ -303,7 +263,3 @@ fn boolean_subtract(_path1: String, _path2: String) -> String {
fn boolean_intersect(_path1: String, _path2: String) -> String {
String::from("M0,0 L1,0 L1,1 L0,1 Z")
}
#[cfg(not(target_arch = "wasm32"))]
fn boolean_difference(_path1: String, _path2: String) -> String {
String::from("M0,0 L1,0 L1,1 L0,1 Z")
}

View file

@ -686,7 +686,6 @@ 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::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<_>"),