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
19 changed files with 409 additions and 55 deletions

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());
}