mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-20 03:18:06 +08:00
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:
@@ -42,6 +42,9 @@ pub enum DocumentMessage {
|
||||
ClearArtboards,
|
||||
ClearLayersPanel,
|
||||
CommitTransaction,
|
||||
InsertBooleanOperation {
|
||||
operation: graphene_core::vector::misc::BooleanOperation,
|
||||
},
|
||||
CreateEmptyFolder,
|
||||
DebugPrintDocument,
|
||||
DeleteLayer {
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -49,9 +49,6 @@ pub enum GraphOperationMessage {
|
||||
parent: LayerNodeIdentifier,
|
||||
insert_index: usize,
|
||||
},
|
||||
InsertBooleanOperation {
|
||||
operation: BooleanOperation,
|
||||
},
|
||||
InsertNodeBetween {
|
||||
// Post node
|
||||
post_node_id: NodeId,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
},
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user