Add a stack-based Boolean Operation layer node (#1813)

* Multiple boolean operation node

* Change boolean operation ordering

* Complete layer boolean operation node

* Automatically insert new boolean operation node

* Remove divide operation

* Fix subtract operations

* Remove stack data from boolean operation properties

* Fix images and custom vectors

* Code cleanup

* Use slice instead of iter to avoid infinite type recursion

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
adamgerhant
2024-07-10 02:12:55 -07:00
committed by GitHub
parent f4e3e5ab2a
commit 9d749c49fb
14 changed files with 319 additions and 153 deletions

View File

@@ -42,6 +42,9 @@ pub enum DocumentMessage {
ClearArtboards,
ClearLayersPanel,
CommitTransaction,
InsertBooleanOperation {
operation: graphene_core::vector::misc::BooleanOperation,
},
CreateEmptyFolder,
DebugPrintDocument,
DeleteLayer {

View File

@@ -295,6 +295,46 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
});
}
DocumentMessage::CommitTransaction => (),
DocumentMessage::InsertBooleanOperation { operation } => {
let boolean_operation_node_id = NodeId(generate_uuid());
let parent = self
.metadata()
.deepest_common_ancestor(self.selected_nodes.selected_layers(self.metadata()), true)
.unwrap_or(LayerNodeIdentifier::ROOT_PARENT);
let insert_index = parent
.children(self.metadata())
.enumerate()
.find_map(|(index, item)| self.selected_nodes.selected_layers(self.metadata()).any(|x| x == item).then_some(index as usize))
.unwrap_or(0);
// Store a history step before doing anything
responses.add(DocumentMessage::StartTransaction);
// Create the new Boolean Operation node
responses.add(GraphOperationMessage::CreateBooleanOperationNode {
node_id: boolean_operation_node_id,
operation,
});
responses.add(GraphOperationMessage::InsertNodeAtStackIndex {
node_id: boolean_operation_node_id,
parent,
insert_index,
});
responses.add(GraphOperationMessage::MoveSelectedSiblingsToChild {
new_parent: LayerNodeIdentifier::new_unchecked(boolean_operation_node_id),
});
// Select the new node
responses.add(NodeGraphMessage::SelectedNodesSet {
nodes: vec![boolean_operation_node_id],
});
// Re-render
responses.add(NodeGraphMessage::RunDocumentGraph);
}
DocumentMessage::CreateEmptyFolder => {
let id = NodeId(generate_uuid());

View File

@@ -49,9 +49,6 @@ pub enum GraphOperationMessage {
parent: LayerNodeIdentifier,
insert_index: usize,
},
InsertBooleanOperation {
operation: BooleanOperation,
},
InsertNodeBetween {
// Post node
post_node_id: NodeId,

View File

@@ -258,84 +258,6 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
shift_self: true,
});
}
GraphOperationMessage::InsertBooleanOperation { operation } => {
let mut selected_layers = selected_nodes.selected_layers(document_metadata);
let upper_layer = selected_layers.next();
let lower_layer = selected_layers.next();
let Some(upper_layer) = upper_layer else { return };
let Some(upper_layer_node) = document_network.nodes.get(&upper_layer.to_node()) else { return };
let lower_layer_node = lower_layer.and_then(|lower_layer| document_network.nodes.get(&lower_layer.to_node()));
let Some(NodeInput::Node {
node_id: upper_node_id,
output_index: upper_output_index,
..
}) = upper_layer_node.inputs.get(1).cloned()
else {
return;
};
let (lower_node_id, lower_output_index) = match lower_layer_node.and_then(|lower_layer_node| lower_layer_node.inputs.get(1).cloned()) {
Some(NodeInput::Node {
node_id: lower_node_id,
output_index: lower_output_index,
..
}) => (Some(lower_node_id), Some(lower_output_index)),
_ => (None, None),
};
let boolean_operation_node_id = NodeId::new();
// Store a history step before doing anything
responses.add(DocumentMessage::StartTransaction);
// Create the new Boolean Operation node
responses.add(GraphOperationMessage::CreateBooleanOperationNode {
node_id: boolean_operation_node_id,
operation,
});
// Insert it in the upper layer's chain, right before it enters the upper layer
responses.add(GraphOperationMessage::InsertNodeBetween {
post_node_id: upper_layer.to_node(),
post_node_input_index: 1,
insert_node_id: boolean_operation_node_id,
insert_node_output_index: 0,
insert_node_input_index: 0,
pre_node_id: upper_node_id,
pre_node_output_index: upper_output_index,
});
// Connect the lower chain to the Boolean Operation node's lower input
if let (Some(lower_layer), Some(lower_node_id), Some(lower_output_index)) = (lower_layer, lower_node_id, lower_output_index) {
responses.add(GraphOperationMessage::SetNodeInput {
node_id: boolean_operation_node_id,
input_index: 1,
input: NodeInput::node(lower_node_id, lower_output_index),
});
// Delete the lower layer (but its chain is kept since it's still used by the Boolean Operation node)
responses.add(GraphOperationMessage::DeleteLayer { layer: lower_layer, reconnect: true });
}
// Put the Boolean Operation where the output layer is located, since this is the correct shift relative to its left input chain
responses.add(GraphOperationMessage::SetNodePosition {
node_id: boolean_operation_node_id,
position: upper_layer_node.metadata.position,
});
// After the previous step, the Boolean Operation node is overlapping the upper layer, so we need to shift and its entire chain to the left by its width plus some padding
responses.add(GraphOperationMessage::ShiftUpstream {
node_id: boolean_operation_node_id,
shift: (-8, 0).into(),
shift_self: true,
});
// Re-render
responses.add(NodeGraphMessage::RunDocumentGraph);
}
GraphOperationMessage::InsertNodeBetween {
post_node_id,
post_node_input_index,

View File

@@ -2574,15 +2574,89 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
DocumentNodeDefinition {
name: "Boolean Operation",
name: "Binary Boolean Operation",
category: "Vector",
implementation: DocumentNodeImplementation::proto("graphene_std::vector::BooleanOperationNode<_, _>"),
implementation: DocumentNodeImplementation::proto("graphene_std::vector::BinaryBooleanOperationNode<_, _>"),
inputs: vec![
DocumentInputType::value("Upper Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
DocumentInputType::value("Lower Vector Data", TaggedValue::VectorData(graphene_core::vector::VectorData::empty()), true),
DocumentInputType::value("Operation", TaggedValue::BooleanOperation(vector::misc::BooleanOperation::Union), false),
],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::VectorData)],
properties: node_properties::binary_boolean_operation_properties,
..Default::default()
},
DocumentNodeDefinition {
name: "Boolean Operation",
category: "Vector",
is_layer: true,
implementation: DocumentNodeImplementation::Network(NodeNetwork {
exports: vec![NodeInput::node(NodeId(4), 0)],
nodes: [
// Secondary (left) input type coercion
(
NodeId(0),
DocumentNode {
name: "Boolean Operation".to_string(),
inputs: vec![NodeInput::network(generic!(T), 1), NodeInput::network(concrete!(vector::misc::BooleanOperation), 2)],
implementation: DocumentNodeImplementation::proto("graphene_std::vector::BooleanOperationNode<_>"),
metadata: DocumentNodeMetadata { position: glam::IVec2::new(-16, -1) },
..Default::default()
},
),
// Primary (bottom) input type coercion
(
NodeId(1),
DocumentNode {
name: "To Graphic Group".to_string(),
inputs: vec![NodeInput::network(generic!(T), 0)],
implementation: DocumentNodeImplementation::proto("graphene_core::ToGraphicGroupNode"),
metadata: DocumentNodeMetadata { position: glam::IVec2::new(-16, -3) }, // To Graphic Group
..Default::default()
},
),
(
NodeId(2),
DocumentNode {
name: "To Graphic Element".to_string(),
inputs: vec![NodeInput::node(NodeId(0), 0)],
implementation: DocumentNodeImplementation::proto("graphene_core::ToGraphicElementNode"),
metadata: DocumentNodeMetadata { position: glam::IVec2::new(-10, 3) }, // To Graphic Element
..Default::default()
},
),
// The monitor node is used to display a thumbnail in the UI
(
NodeId(3),
DocumentNode {
inputs: vec![NodeInput::node(NodeId(2), 0)],
metadata: DocumentNodeMetadata { position: glam::IVec2::new(-7, -1) }, // Monitor
..monitor_node()
},
),
(
NodeId(4),
DocumentNode {
name: "ConstructLayer".to_string(),
manual_composition: Some(concrete!(Footprint)),
inputs: vec![NodeInput::node(NodeId(1), 0), NodeInput::node(NodeId(3), 0)],
implementation: DocumentNodeImplementation::proto("graphene_core::ConstructLayerNode<_, _>"),
metadata: DocumentNodeMetadata { position: glam::IVec2::new(1, -3) }, // ConstructLayer
..Default::default()
},
),
]
.into(),
imports_metadata: (NodeId(generate_uuid()), (-26, -4).into()),
exports_metadata: (NodeId(generate_uuid()), (8, -4).into()),
..Default::default()
}),
inputs: vec![
DocumentInputType::value("Graphical Data", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
DocumentInputType::value("Vector Data", TaggedValue::GraphicGroup(GraphicGroup::EMPTY), true),
DocumentInputType::value("Operation", TaggedValue::BooleanOperation(vector::misc::BooleanOperation::Union), false),
],
outputs: vec![DocumentOutputType::new("Vector", FrontendGraphDataType::Graphic)],
properties: node_properties::boolean_operation_properties,
..Default::default()
},

View File

@@ -2344,11 +2344,18 @@ pub fn circular_repeat_properties(document_node: &DocumentNode, node_id: NodeId,
]
}
pub fn boolean_operation_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<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);
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: other_vector_data }, opeartion]
vec![LayoutGroup::Row { widgets: lower_vector_data }, operation]
}
pub fn boolean_operation_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<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);
vec![LayoutGroup::Row { widgets: vector_data }, operation]
}
pub fn copy_to_points_properties(document_node: &DocumentNode, node_id: NodeId, _context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {

View File

@@ -148,21 +148,13 @@ impl SelectTool {
}
fn boolean_widgets(&self, selected_count: usize) -> impl Iterator<Item = WidgetHolder> {
let enabled = move |operation| {
if operation == BooleanOperation::Union {
(1..=2).contains(&selected_count)
} else {
selected_count == 2
}
};
let operations = BooleanOperation::list();
let icons = BooleanOperation::icons();
operations.into_iter().zip(icons).map(move |(operation, icon)| {
IconButton::new(icon, 24)
.tooltip(operation.to_string())
.disabled(!enabled(operation))
.on_update(move |_| GraphOperationMessage::InsertBooleanOperation { operation }.into())
.disabled(selected_count == 0)
.on_update(move |_| DocumentMessage::InsertBooleanOperation { operation }.into())
.widget_holder()
})
}