mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-04 13:30:48 +00: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:
parent
f4e3e5ab2a
commit
9d749c49fb
14 changed files with 319 additions and 153 deletions
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -20,10 +20,6 @@ export function booleanDifference(path1: string, path2: string): string {
|
|||
return booleanOperation(path1, path2, "exclude");
|
||||
}
|
||||
|
||||
export function booleanDivide(path1: string, path2: string): string {
|
||||
return booleanOperation(path1, path2, "intersect") + booleanOperation(path1, path2, "exclude");
|
||||
}
|
||||
|
||||
function booleanOperation(path1: string, path2: string, operation: "unite" | "subtract" | "intersect" | "exclude"): string {
|
||||
const paperPath1 = new paper.CompoundPath(path1);
|
||||
const paperPath2 = new paper.CompoundPath(path2);
|
||||
|
|
|
@ -57,7 +57,7 @@ impl core::hash::Hash for GraphicGroup {
|
|||
}
|
||||
|
||||
/// The possible forms of graphical content held in a Vec by the `elements` field of [`GraphicElement`].
|
||||
/// Can be another recursively nested [`GraphicGroup`], [`VectorData`], an [`ImageFrame`], text (not yet implemented), or an [`Artboard`].
|
||||
/// Can be another recursively nested [`GraphicGroup`], a [`VectorData`] shape, an [`ImageFrame`], or an [`Artboard`].
|
||||
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum GraphicElement {
|
||||
|
@ -67,10 +67,6 @@ pub enum GraphicElement {
|
|||
VectorData(Box<VectorData>),
|
||||
/// A bitmap image with a finite position and extent, equivalent to the SVG <image> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image
|
||||
ImageFrame(ImageFrame<Color>),
|
||||
// TODO: Switch from `String` to a proper formatted typography type
|
||||
/// Text, equivalent to the SVG <text> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/text
|
||||
/// (Not yet implemented.)
|
||||
Text(String),
|
||||
/// The bounds for displaying a page of contained content
|
||||
Artboard(Artboard),
|
||||
}
|
||||
|
@ -366,28 +362,6 @@ impl GraphicElement {
|
|||
bounding_box: None,
|
||||
}))
|
||||
}
|
||||
GraphicElement::Text(text) => usvg::Node::Text(Box::new(usvg::Text {
|
||||
id: String::new(),
|
||||
abs_transform: usvg::Transform::identity(),
|
||||
rendering_mode: usvg::TextRendering::OptimizeSpeed,
|
||||
writing_mode: usvg::WritingMode::LeftToRight,
|
||||
chunks: vec![usvg::TextChunk {
|
||||
text: text.clone(),
|
||||
x: None,
|
||||
y: None,
|
||||
anchor: usvg::TextAnchor::Start,
|
||||
spans: vec![],
|
||||
text_flow: usvg::TextFlow::Linear,
|
||||
}],
|
||||
dx: Vec::new(),
|
||||
dy: Vec::new(),
|
||||
rotate: Vec::new(),
|
||||
bounding_box: None,
|
||||
abs_bounding_box: None,
|
||||
stroke_bounding_box: None,
|
||||
abs_stroke_bounding_box: None,
|
||||
flattened: None,
|
||||
})),
|
||||
GraphicElement::GraphicGroup(group) => {
|
||||
let mut group_element = usvg::Group::default();
|
||||
|
||||
|
|
|
@ -554,7 +554,6 @@ impl GraphicElementRendered for GraphicElement {
|
|||
match self {
|
||||
GraphicElement::VectorData(vector_data) => vector_data.render_svg(render, render_params),
|
||||
GraphicElement::ImageFrame(image_frame) => image_frame.render_svg(render, render_params),
|
||||
GraphicElement::Text(_) => todo!("Render a text GraphicElement"),
|
||||
GraphicElement::GraphicGroup(graphic_group) => graphic_group.render_svg(render, render_params),
|
||||
GraphicElement::Artboard(artboard) => artboard.render_svg(render, render_params),
|
||||
}
|
||||
|
@ -564,7 +563,6 @@ impl GraphicElementRendered for GraphicElement {
|
|||
match self {
|
||||
GraphicElement::VectorData(vector_data) => GraphicElementRendered::bounding_box(&**vector_data, transform),
|
||||
GraphicElement::ImageFrame(image_frame) => image_frame.bounding_box(transform),
|
||||
GraphicElement::Text(_) => todo!("Bounds of a text GraphicElement"),
|
||||
GraphicElement::GraphicGroup(graphic_group) => graphic_group.bounding_box(transform),
|
||||
GraphicElement::Artboard(artboard) => artboard.bounding_box(transform),
|
||||
}
|
||||
|
@ -574,7 +572,6 @@ impl GraphicElementRendered for GraphicElement {
|
|||
match self {
|
||||
GraphicElement::VectorData(vector_data) => vector_data.add_click_targets(click_targets),
|
||||
GraphicElement::ImageFrame(image_frame) => image_frame.add_click_targets(click_targets),
|
||||
GraphicElement::Text(_) => todo!("click target for text GraphicElement"),
|
||||
GraphicElement::GraphicGroup(graphic_group) => graphic_group.add_click_targets(click_targets),
|
||||
GraphicElement::Artboard(artboard) => artboard.add_click_targets(click_targets),
|
||||
}
|
||||
|
@ -584,7 +581,6 @@ impl GraphicElementRendered for GraphicElement {
|
|||
match self {
|
||||
GraphicElement::VectorData(vector_data) => vector_data.to_usvg_node(),
|
||||
GraphicElement::ImageFrame(image_frame) => image_frame.to_usvg_node(),
|
||||
GraphicElement::Text(text) => text.to_usvg_node(),
|
||||
GraphicElement::GraphicGroup(graphic_group) => graphic_group.to_usvg_node(),
|
||||
GraphicElement::Artboard(artboard) => artboard.to_usvg_node(),
|
||||
}
|
||||
|
@ -594,7 +590,6 @@ impl GraphicElementRendered for GraphicElement {
|
|||
match self {
|
||||
GraphicElement::VectorData(vector_data) => vector_data.contains_artboard(),
|
||||
GraphicElement::ImageFrame(image_frame) => image_frame.contains_artboard(),
|
||||
GraphicElement::Text(text) => text.contains_artboard(),
|
||||
GraphicElement::GraphicGroup(graphic_group) => graphic_group.contains_artboard(),
|
||||
GraphicElement::Artboard(artboard) => artboard.contains_artboard(),
|
||||
}
|
||||
|
|
|
@ -75,7 +75,6 @@ impl Transform for GraphicElement {
|
|||
match self {
|
||||
GraphicElement::VectorData(vector_shape) => vector_shape.transform(),
|
||||
GraphicElement::ImageFrame(image_frame) => image_frame.transform(),
|
||||
GraphicElement::Text(_) => todo!("Transform of text"),
|
||||
GraphicElement::GraphicGroup(graphic_group) => graphic_group.transform(),
|
||||
GraphicElement::Artboard(artboard) => artboard.transform(),
|
||||
}
|
||||
|
@ -84,7 +83,6 @@ impl Transform for GraphicElement {
|
|||
match self {
|
||||
GraphicElement::VectorData(vector_shape) => vector_shape.local_pivot(pivot),
|
||||
GraphicElement::ImageFrame(image_frame) => image_frame.local_pivot(pivot),
|
||||
GraphicElement::Text(_) => todo!("Transform of text"),
|
||||
GraphicElement::GraphicGroup(graphic_group) => graphic_group.local_pivot(pivot),
|
||||
GraphicElement::Artboard(artboard) => artboard.local_pivot(pivot),
|
||||
}
|
||||
|
@ -93,7 +91,6 @@ impl Transform for GraphicElement {
|
|||
match self {
|
||||
GraphicElement::VectorData(vector_shape) => vector_shape.decompose_scale(),
|
||||
GraphicElement::ImageFrame(image_frame) => image_frame.decompose_scale(),
|
||||
GraphicElement::Text(_) => todo!("Transform of text"),
|
||||
GraphicElement::GraphicGroup(graphic_group) => graphic_group.decompose_scale(),
|
||||
GraphicElement::Artboard(artboard) => artboard.decompose_scale(),
|
||||
}
|
||||
|
@ -104,7 +101,6 @@ impl TransformMut for GraphicElement {
|
|||
match self {
|
||||
GraphicElement::VectorData(vector_shape) => vector_shape.transform_mut(),
|
||||
GraphicElement::ImageFrame(image_frame) => image_frame.transform_mut(),
|
||||
GraphicElement::Text(_) => todo!("Transform of text"),
|
||||
GraphicElement::GraphicGroup(graphic_group) => graphic_group.transform_mut(),
|
||||
GraphicElement::Artboard(_) => todo!("Transform of artboard"),
|
||||
}
|
||||
|
|
|
@ -18,23 +18,21 @@ pub enum BooleanOperation {
|
|||
SubtractBack,
|
||||
Intersect,
|
||||
Difference,
|
||||
Divide,
|
||||
}
|
||||
|
||||
impl BooleanOperation {
|
||||
pub fn list() -> [BooleanOperation; 6] {
|
||||
pub fn list() -> [BooleanOperation; 5] {
|
||||
[
|
||||
BooleanOperation::Union,
|
||||
BooleanOperation::SubtractFront,
|
||||
BooleanOperation::SubtractBack,
|
||||
BooleanOperation::Intersect,
|
||||
BooleanOperation::Difference,
|
||||
BooleanOperation::Divide,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn icons() -> [&'static str; 6] {
|
||||
["BooleanUnion", "BooleanSubtractFront", "BooleanSubtractBack", "BooleanIntersect", "BooleanDifference", "BooleanDivide"]
|
||||
pub fn icons() -> [&'static str; 5] {
|
||||
["BooleanUnion", "BooleanSubtractFront", "BooleanSubtractBack", "BooleanIntersect", "BooleanDifference"]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,7 +44,6 @@ impl core::fmt::Display for BooleanOperation {
|
|||
BooleanOperation::SubtractBack => write!(f, "Subtract Back"),
|
||||
BooleanOperation::Intersect => write!(f, "Intersect"),
|
||||
BooleanOperation::Difference => write!(f, "Difference"),
|
||||
BooleanOperation::Divide => write!(f, "Divide"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
use crate::Node;
|
||||
|
||||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use graphene_core::transform::Footprint;
|
||||
use graphene_core::vector::misc::BooleanOperation;
|
||||
use graphene_core::raster::ImageFrame;
|
||||
pub use graphene_core::vector::*;
|
||||
use graphene_core::Color;
|
||||
use graphene_core::{transform::Footprint, GraphicGroup};
|
||||
use graphene_core::{vector::misc::BooleanOperation, GraphicElement};
|
||||
|
||||
use futures::Future;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub struct BooleanOperationNode<LowerVectorData, BooleanOp> {
|
||||
pub struct BinaryBooleanOperationNode<LowerVectorData, BooleanOp> {
|
||||
lower_vector_data: LowerVectorData,
|
||||
boolean_operation: BooleanOp,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(BooleanOperationNode)]
|
||||
async fn boolean_operation_node<Fut: Future<Output = VectorData>>(
|
||||
#[node_macro::node_fn(BinaryBooleanOperationNode)]
|
||||
async fn binary_boolean_operation_node<Fut: Future<Output = VectorData>>(
|
||||
upper_vector_data: VectorData,
|
||||
lower_vector_data: impl Node<Footprint, Output = Fut>,
|
||||
boolean_operation: BooleanOperation,
|
||||
|
@ -39,7 +41,6 @@ async fn boolean_operation_node<Fut: Future<Output = VectorData>>(
|
|||
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),
|
||||
BooleanOperation::Divide => boolean_divide(upper_path_string, lower_path_string),
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -51,9 +52,182 @@ async fn boolean_operation_node<Fut: Future<Output = VectorData>>(
|
|||
result
|
||||
}
|
||||
|
||||
pub struct BooleanOperationNode<BooleanOp> {
|
||||
boolean_operation: BooleanOp,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(BooleanOperationNode)]
|
||||
fn boolean_operation_node(graphic_group: GraphicGroup, boolean_operation: BooleanOperation) -> VectorData {
|
||||
fn vector_from_image<P: graphene_core::raster::Pixel>(image_frame: &ImageFrame<P>) -> VectorData {
|
||||
let corner1 = DVec2::ZERO;
|
||||
let corner2 = DVec2::new(1., 1.);
|
||||
let mut subpath = Subpath::new_rect(corner1, corner2);
|
||||
subpath.apply_transform(image_frame.transform);
|
||||
let mut vector_data = VectorData::from_subpath(subpath);
|
||||
vector_data
|
||||
.style
|
||||
.set_fill(graphene_core::vector::style::Fill::Solid(Color::from_rgb_str("777777").unwrap().to_gamma_srgb()));
|
||||
vector_data
|
||||
}
|
||||
|
||||
fn union_vector_data(graphic_element: &GraphicElement) -> VectorData {
|
||||
match graphic_element {
|
||||
GraphicElement::VectorData(vector_data) => *vector_data.clone(),
|
||||
// Union all vector data in the graphic group into a single vector
|
||||
GraphicElement::GraphicGroup(graphic_group) => {
|
||||
let vector_data = collect_vector_data(graphic_group);
|
||||
boolean_operation_on_vector_data(&vector_data, BooleanOperation::Union)
|
||||
}
|
||||
GraphicElement::ImageFrame(image) => vector_from_image(image),
|
||||
// Union all vector data in the artboard into a single vector
|
||||
GraphicElement::Artboard(artboard) => {
|
||||
let artboard_subpath = Subpath::new_rect(artboard.location.as_dvec2(), artboard.location.as_dvec2() + artboard.dimensions.as_dvec2());
|
||||
|
||||
let mut artboard_vector = VectorData::from_subpath(artboard_subpath);
|
||||
artboard_vector.style.set_fill(graphene_core::vector::style::Fill::Solid(artboard.background));
|
||||
|
||||
let mut vector_data = vec![artboard_vector];
|
||||
vector_data.extend(collect_vector_data(&artboard.graphic_group).into_iter());
|
||||
|
||||
boolean_operation_on_vector_data(&vector_data, BooleanOperation::Union)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_vector_data(graphic_group: &GraphicGroup) -> Vec<VectorData> {
|
||||
// Ensure all non vector data in the graphic group is converted to vector data
|
||||
graphic_group.iter().map(union_vector_data).collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn subtract<'a>(vector_data: impl Iterator<Item = &'a VectorData>) -> VectorData {
|
||||
let mut vector_data = vector_data.into_iter();
|
||||
let mut result = vector_data.next().cloned().unwrap_or_default();
|
||||
let mut next_vector_data = vector_data.next();
|
||||
|
||||
while let Some(lower_vector_data) = next_vector_data {
|
||||
let transform_of_lower_into_space_of_upper = result.transform.inverse() * lower_vector_data.transform;
|
||||
|
||||
let upper_path_string = to_svg_string(&result, DAffine2::IDENTITY);
|
||||
let lower_path_string = to_svg_string(&lower_vector_data, transform_of_lower_into_space_of_upper);
|
||||
|
||||
#[allow(unused_unsafe)]
|
||||
let boolean_operation_string = unsafe { boolean_subtract(upper_path_string, lower_path_string) };
|
||||
let boolean_operation_result = from_svg_string(&boolean_operation_string);
|
||||
|
||||
result.colinear_manipulators = boolean_operation_result.colinear_manipulators;
|
||||
result.point_domain = boolean_operation_result.point_domain;
|
||||
result.segment_domain = boolean_operation_result.segment_domain;
|
||||
result.region_domain = boolean_operation_result.region_domain;
|
||||
|
||||
next_vector_data = vector_data.next();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn boolean_operation_on_vector_data(vector_data: &[VectorData], boolean_operation: BooleanOperation) -> VectorData {
|
||||
match boolean_operation {
|
||||
BooleanOperation::Union => {
|
||||
// Reverse vector data so that the result style is the style of the first vector data
|
||||
let mut vector_data = vector_data.iter().rev();
|
||||
let mut result = vector_data.next().cloned().unwrap_or_default();
|
||||
let mut second_vector_data = Some(vector_data.next().unwrap_or(const { &VectorData::empty() }));
|
||||
|
||||
// Loop over all vector data and union it with the result
|
||||
while let Some(lower_vector_data) = second_vector_data {
|
||||
let transform_of_lower_into_space_of_upper = result.transform.inverse() * lower_vector_data.transform;
|
||||
|
||||
let upper_path_string = to_svg_string(&result, DAffine2::IDENTITY);
|
||||
let lower_path_string = to_svg_string(lower_vector_data, transform_of_lower_into_space_of_upper);
|
||||
|
||||
#[allow(unused_unsafe)]
|
||||
let boolean_operation_string = unsafe { boolean_union(upper_path_string, lower_path_string) };
|
||||
let boolean_operation_result = from_svg_string(&boolean_operation_string);
|
||||
|
||||
result.colinear_manipulators = boolean_operation_result.colinear_manipulators;
|
||||
result.point_domain = boolean_operation_result.point_domain;
|
||||
result.segment_domain = boolean_operation_result.segment_domain;
|
||||
result.region_domain = boolean_operation_result.region_domain;
|
||||
second_vector_data = vector_data.next();
|
||||
}
|
||||
result
|
||||
}
|
||||
BooleanOperation::SubtractFront => subtract(vector_data.iter()),
|
||||
BooleanOperation::SubtractBack => subtract(vector_data.iter().rev()),
|
||||
BooleanOperation::Intersect => {
|
||||
let mut vector_data = vector_data.iter().rev();
|
||||
let mut result = vector_data.next().cloned().unwrap_or_default();
|
||||
let mut second_vector_data = Some(vector_data.next().unwrap_or(const { &VectorData::empty() }));
|
||||
|
||||
// For each vector data, set the result to the intersection of that data and the result
|
||||
while let Some(lower_vector_data) = second_vector_data {
|
||||
let transform_of_lower_into_space_of_upper = result.transform.inverse() * lower_vector_data.transform;
|
||||
|
||||
let upper_path_string = to_svg_string(&result, DAffine2::IDENTITY);
|
||||
let lower_path_string = to_svg_string(lower_vector_data, transform_of_lower_into_space_of_upper);
|
||||
|
||||
#[allow(unused_unsafe)]
|
||||
let boolean_operation_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) };
|
||||
let boolean_operation_result = from_svg_string(&boolean_operation_string);
|
||||
|
||||
result.colinear_manipulators = boolean_operation_result.colinear_manipulators;
|
||||
result.point_domain = boolean_operation_result.point_domain;
|
||||
result.segment_domain = boolean_operation_result.segment_domain;
|
||||
result.region_domain = boolean_operation_result.region_domain;
|
||||
second_vector_data = vector_data.next();
|
||||
}
|
||||
result
|
||||
}
|
||||
BooleanOperation::Difference => {
|
||||
let mut vector_data_iter = vector_data.iter().rev();
|
||||
let mut any_intersection = VectorData::empty();
|
||||
let mut second_vector_data = Some(vector_data_iter.next().unwrap_or(const { &VectorData::empty() }));
|
||||
|
||||
// Find where all vector data intersect at least once
|
||||
while let Some(lower_vector_data) = second_vector_data {
|
||||
let all_other_vector_data = boolean_operation_on_vector_data(&vector_data.iter().filter(|v| v != &lower_vector_data).cloned().collect::<Vec<_>>(), BooleanOperation::Union);
|
||||
|
||||
let transform_of_lower_into_space_of_upper = all_other_vector_data.transform.inverse() * lower_vector_data.transform;
|
||||
|
||||
let upper_path_string = to_svg_string(&all_other_vector_data, DAffine2::IDENTITY);
|
||||
let lower_path_string = to_svg_string(lower_vector_data, transform_of_lower_into_space_of_upper);
|
||||
|
||||
#[allow(unused_unsafe)]
|
||||
let boolean_intersection_string = unsafe { boolean_intersect(upper_path_string, lower_path_string) };
|
||||
let mut boolean_intersection_result = from_svg_string(&boolean_intersection_string);
|
||||
|
||||
boolean_intersection_result.transform = all_other_vector_data.transform;
|
||||
boolean_intersection_result.style = all_other_vector_data.style.clone();
|
||||
boolean_intersection_result.alpha_blending = all_other_vector_data.alpha_blending;
|
||||
|
||||
let transform_of_lower_into_space_of_upper = boolean_intersection_result.transform.inverse() * any_intersection.transform;
|
||||
|
||||
let upper_path_string = to_svg_string(&boolean_intersection_result, DAffine2::IDENTITY);
|
||||
let lower_path_string = to_svg_string(&any_intersection, transform_of_lower_into_space_of_upper);
|
||||
|
||||
#[allow(unused_unsafe)]
|
||||
let union_result = from_svg_string(&unsafe { boolean_union(upper_path_string, lower_path_string) });
|
||||
any_intersection = union_result;
|
||||
|
||||
any_intersection.transform = boolean_intersection_result.transform;
|
||||
any_intersection.style = boolean_intersection_result.style.clone();
|
||||
any_intersection.alpha_blending = boolean_intersection_result.alpha_blending;
|
||||
|
||||
second_vector_data = vector_data_iter.next();
|
||||
}
|
||||
// Subtract the area where they intersect at least once from the union of all vector data
|
||||
let union = boolean_operation_on_vector_data(vector_data, BooleanOperation::Union);
|
||||
boolean_operation_on_vector_data(&[union, any_intersection], BooleanOperation::SubtractFront)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The first index is the bottom of the stack
|
||||
boolean_operation_on_vector_data(&collect_vector_data(&graphic_group), boolean_operation)
|
||||
}
|
||||
|
||||
fn to_svg_string(vector: &VectorData, transform: DAffine2) -> String {
|
||||
let mut path = String::new();
|
||||
for (_, subpath) in vector.region_bezier_paths() {
|
||||
for subpath in vector.stroke_bezier_paths() {
|
||||
let _ = subpath.subpath_to_svg(&mut path, transform);
|
||||
}
|
||||
path
|
||||
|
@ -125,6 +299,4 @@ extern "C" {
|
|||
fn boolean_intersect(path1: String, path2: String) -> String;
|
||||
#[wasm_bindgen(js_name = booleanDifference)]
|
||||
fn boolean_difference(path1: String, path2: String) -> String;
|
||||
#[wasm_bindgen(js_name = booleanDivide)]
|
||||
fn boolean_divide(path1: String, path2: String) -> String;
|
||||
}
|
||||
|
|
|
@ -719,7 +719,8 @@ 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::BooleanOperationNode<_, _>, input: VectorData, output: VectorData, fn_params: [Footprint => VectorData, () => graphene_core::vector::misc::BooleanOperation]),
|
||||
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<_>"),
|
||||
|args| {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue