mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Add boolean operations (#1759)
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user