mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-08-03 21:08:18 +00:00
Add boolean operations (#1759)
This commit is contained in:
parent
c80de41d28
commit
d40fb6caad
19 changed files with 409 additions and 55 deletions
|
@ -9,3 +9,44 @@ pub enum CentroidType {
|
|||
/// The center of mass for the arc length of a curved shape's perimeter, as if made out of an infinitely thin wire.
|
||||
Length,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)]
|
||||
pub enum BooleanOperation {
|
||||
#[default]
|
||||
Union,
|
||||
SubtractFront,
|
||||
SubtractBack,
|
||||
Intersect,
|
||||
Difference,
|
||||
Divide,
|
||||
}
|
||||
|
||||
impl BooleanOperation {
|
||||
pub fn list() -> [BooleanOperation; 6] {
|
||||
[
|
||||
BooleanOperation::Union,
|
||||
BooleanOperation::SubtractFront,
|
||||
BooleanOperation::SubtractBack,
|
||||
BooleanOperation::Intersect,
|
||||
BooleanOperation::Difference,
|
||||
BooleanOperation::Divide,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn icons() -> [&'static str; 6] {
|
||||
["BooleanUnion", "BooleanSubtractFront", "BooleanSubtractBack", "BooleanIntersect", "BooleanDifference", "BooleanDivide"]
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for BooleanOperation {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
BooleanOperation::Union => write!(f, "Union"),
|
||||
BooleanOperation::SubtractFront => write!(f, "Subtract Front"),
|
||||
BooleanOperation::SubtractBack => write!(f, "Subtract Back"),
|
||||
BooleanOperation::Intersect => write!(f, "Intersect"),
|
||||
BooleanOperation::Difference => write!(f, "Difference"),
|
||||
BooleanOperation::Divide => write!(f, "Divide"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,13 @@ pub mod value;
|
|||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize, specta::Type)]
|
||||
pub struct NodeId(pub u64);
|
||||
|
||||
// TODO: Find and replace all `NodeId(generate_uuid())` with `NodeId::new()`.
|
||||
impl NodeId {
|
||||
pub fn new() -> Self {
|
||||
Self(generate_uuid())
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for NodeId {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
|
|
|
@ -78,6 +78,7 @@ pub enum TaggedValue {
|
|||
RenderOutput(RenderOutput),
|
||||
Palette(Vec<Color>),
|
||||
CentroidType(graphene_core::vector::misc::CentroidType),
|
||||
BooleanOperation(graphene_core::vector::misc::BooleanOperation),
|
||||
}
|
||||
|
||||
#[allow(clippy::derived_hash_with_manual_eq)]
|
||||
|
@ -158,6 +159,7 @@ impl Hash for TaggedValue {
|
|||
Self::RenderOutput(x) => x.hash(state),
|
||||
Self::Palette(x) => x.hash(state),
|
||||
Self::CentroidType(x) => x.hash(state),
|
||||
Self::BooleanOperation(x) => x.hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,6 +227,7 @@ impl<'a> TaggedValue {
|
|||
TaggedValue::RenderOutput(x) => Box::new(x),
|
||||
TaggedValue::Palette(x) => Box::new(x),
|
||||
TaggedValue::CentroidType(x) => Box::new(x),
|
||||
TaggedValue::BooleanOperation(x) => Box::new(x),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,6 +306,7 @@ impl<'a> TaggedValue {
|
|||
TaggedValue::RenderOutput(_) => concrete!(RenderOutput),
|
||||
TaggedValue::Palette(_) => concrete!(Vec<Color>),
|
||||
TaggedValue::CentroidType(_) => concrete!(graphene_core::vector::misc::CentroidType),
|
||||
TaggedValue::BooleanOperation(_) => concrete!(graphene_core::vector::misc::BooleanOperation),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -371,6 +375,7 @@ impl<'a> TaggedValue {
|
|||
x if x == TypeId::of::<graphene_core::transform::Footprint>() => Ok(TaggedValue::Footprint(*downcast(input).unwrap())),
|
||||
x if x == TypeId::of::<Vec<Color>>() => Ok(TaggedValue::Palette(*downcast(input).unwrap())),
|
||||
x if x == TypeId::of::<graphene_core::vector::misc::CentroidType>() => Ok(TaggedValue::CentroidType(*downcast(input).unwrap())),
|
||||
x if x == TypeId::of::<graphene_core::vector::misc::BooleanOperation>() => Ok(TaggedValue::BooleanOperation(*downcast(input).unwrap())),
|
||||
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,6 +73,7 @@ tokio = { workspace = true, optional = true, features = ["fs", "io-std"] }
|
|||
image-compare = { version = "0.3.0", optional = true }
|
||||
vello = { workspace = true, optional = true }
|
||||
resvg = { workspace = true, optional = true }
|
||||
usvg = { workspace = true }
|
||||
serde = { workspace = true, optional = true, features = ["derive"] }
|
||||
web-sys = { workspace = true, optional = true, features = [
|
||||
"Window",
|
||||
|
|
|
@ -7,6 +7,8 @@ extern crate log;
|
|||
|
||||
pub mod raster;
|
||||
|
||||
pub mod vector;
|
||||
|
||||
pub mod http;
|
||||
|
||||
pub mod any;
|
||||
|
|
131
node-graph/gstd/src/vector.rs
Normal file
131
node-graph/gstd/src/vector.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
use crate::Node;
|
||||
|
||||
use bezier_rs::{ManipulatorGroup, Subpath};
|
||||
use graphene_core::transform::Footprint;
|
||||
use graphene_core::uuid::ManipulatorGroupId;
|
||||
use graphene_core::vector::misc::BooleanOperation;
|
||||
pub use graphene_core::vector::*;
|
||||
|
||||
use futures::Future;
|
||||
use glam::{DAffine2, DVec2};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub struct BooleanOperationNode<LowerVectorData, BooleanOp> {
|
||||
lower_vector_data: LowerVectorData,
|
||||
boolean_operation: BooleanOp,
|
||||
}
|
||||
|
||||
#[node_macro::node_fn(BooleanOperationNode)]
|
||||
async fn boolean_operation_node<Fut: Future<Output = VectorData>>(
|
||||
upper_vector_data: VectorData,
|
||||
lower_vector_data: impl Node<Footprint, Output = Fut>,
|
||||
boolean_operation: BooleanOperation,
|
||||
) -> VectorData {
|
||||
let lower_vector_data = self.lower_vector_data.eval(Footprint::default()).await;
|
||||
let transform_of_lower_into_space_of_upper = upper_vector_data.transform.inverse() * lower_vector_data.transform;
|
||||
|
||||
let upper_path_string = to_svg_string(&upper_vector_data, DAffine2::IDENTITY);
|
||||
let lower_path_string = to_svg_string(&lower_vector_data, transform_of_lower_into_space_of_upper);
|
||||
|
||||
let mut use_lower_style = false;
|
||||
|
||||
#[allow(unused_unsafe)]
|
||||
let result = unsafe {
|
||||
match boolean_operation {
|
||||
BooleanOperation::Union => boolean_union(upper_path_string, lower_path_string),
|
||||
BooleanOperation::SubtractFront => {
|
||||
use_lower_style = true;
|
||||
boolean_subtract(lower_path_string, upper_path_string)
|
||||
}
|
||||
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),
|
||||
}
|
||||
};
|
||||
|
||||
let mut result = from_svg_string(&result);
|
||||
result.transform = upper_vector_data.transform;
|
||||
result.style = if use_lower_style { lower_vector_data.style } else { upper_vector_data.style };
|
||||
result.alpha_blending = if use_lower_style { lower_vector_data.alpha_blending } else { upper_vector_data.alpha_blending };
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn to_svg_string(vector: &VectorData, transform: DAffine2) -> String {
|
||||
let mut path = String::new();
|
||||
for (_, subpath) in vector.region_bezier_paths() {
|
||||
let _ = subpath.subpath_to_svg(&mut path, transform);
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
fn from_svg_string(svg_string: &str) -> VectorData {
|
||||
let svg = format!(r#"<svg xmlns="http://www.w3.org/2000/svg"><path d="{}"></path></svg>"#, svg_string);
|
||||
let Some(tree) = usvg::Tree::from_str(&svg, &Default::default()).ok() else {
|
||||
return VectorData::empty();
|
||||
};
|
||||
let Some(usvg::Node::Path(path)) = tree.root.children.first() else {
|
||||
return VectorData::empty();
|
||||
};
|
||||
|
||||
VectorData::from_subpaths(convert_usvg_path(path))
|
||||
}
|
||||
|
||||
pub 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
|
||||
}
|
||||
|
||||
#[wasm_bindgen(module = "/../../frontend/src/utility-functions/computational-geometry.ts")]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_name = booleanUnion)]
|
||||
fn boolean_union(path1: String, path2: String) -> String;
|
||||
#[wasm_bindgen(js_name = booleanSubtract)]
|
||||
fn boolean_subtract(path1: String, path2: String) -> String;
|
||||
#[wasm_bindgen(js_name = booleanIntersect)]
|
||||
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;
|
||||
}
|
|
@ -713,6 +713,7 @@ 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]),
|
||||
vec![(
|
||||
ProtoNodeIdentifier::new("graphene_core::transform::CullNode<_>"),
|
||||
|args| {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue