Files
Graphite/node-graph/nodes/path-bool/src/lib.rs
Jo e3ad59ad9c Add tooltip documentation to web request and brush related nodes (#3427)
* Add tooltip documentation to web request, text, and brush nodes

  - Add descriptive tooltips to web request nodes (GET/POST, string/image conversion, resource loading, image decoding)
  - Add tooltip to GPU rasterization node explaining footprint usage
  - Improve text node tooltip with clearer per_glyph_instances parameter explanation
  - Add tooltips to brush stamp generator and blit operations explaining their internal roles
  - Fix typos in tooltip and variable names (it's→its, perview→preview, instnace→instance)

* Fixed 2 issues with previous changes. Reversed order of documentation about per_glyph_instances. Can it be 3 lines?

* Improve post_request node documentation

Change doc comment to describe the node's purpose rather than focusing
only on the "Discard Result" parameter. Now clearly states this is an
HTTP POST operation with optional async behavior.

* Fix post_request documentation error

Corrected doc comment to say "HTTP POST" instead of "HTTP GET" since
this function performs POST requests with binary data.

* Review changes

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
2026-01-13 08:35:14 +00:00

444 lines
17 KiB
Rust

use core_types::table::{Table, TableRow, TableRowRef};
use core_types::{Color, Ctx};
use dyn_any::DynAny;
use glam::{DAffine2, DVec2};
use graphic_types::vector_types::subpath::{ManipulatorGroup, PathSegPoints, Subpath, pathseg_points};
use graphic_types::vector_types::vector::PointId;
use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use graphic_types::vector_types::vector::style::Fill;
use graphic_types::{Graphic, Vector};
pub use path_bool as path_bool_lib;
use path_bool::{FillRule, PathBooleanOperation};
use std::ops::Mul;
// Import specta so derive macros can find it
use core_types::specta;
// TODO: Fix boolean ops to work by removing .transform() and .one_instance_*() calls,
// TODO: since before we used a Vec of single-row tables and now we use a single table
// TODO: with multiple rows while still assuming a single row for the boolean operations.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum BooleanOperation {
#[default]
#[icon("BooleanUnion")]
Union,
#[icon("BooleanSubtractFront")]
SubtractFront,
#[icon("BooleanSubtractBack")]
SubtractBack,
#[icon("BooleanIntersect")]
Intersect,
#[icon("BooleanDifference")]
Difference,
}
/// Combines the geometric forms of one or more closed paths into a new vector path that results from cutting or joining the paths by the chosen method.
#[node_macro::node(category(""))]
async fn boolean_operation<I: graphic_types::IntoGraphicTable + 'n + Send + Clone>(
_: impl Ctx,
/// The table of vector paths to perform the boolean operation on. Nested tables are automatically flattened.
#[implementations(Table<Graphic>, Table<Vector>)]
content: I,
/// Which boolean operation to perform on the paths.
///
/// Union combines all paths while cutting out overlapping areas (even the interiors of a single path).
/// Subtraction cuts overlapping areas out from the last (Subtract Front) or first (Subtract Back) path.
/// Intersection cuts away all but the overlapping areas shared by every path.
/// Difference cuts away the overlapping areas shared by every path, leaving only the non-overlapping areas.
operation: BooleanOperation,
) -> Table<Vector> {
let content = content.into_graphic_table();
// The first index is the bottom of the stack
let mut result_vector_table = boolean_operation_on_vector_table(flatten_vector(&content).iter(), operation);
// Replace the transformation matrix with a mutation of the vector points themselves
if let Some(result_vector) = result_vector_table.iter_mut().next() {
let transform = *result_vector.transform;
*result_vector.transform = DAffine2::IDENTITY;
Vector::transform(result_vector.element, transform);
result_vector.element.style.set_stroke_transform(DAffine2::IDENTITY);
result_vector.element.upstream_data = Some(content.clone());
// Clean up the boolean operation result by merging duplicated points
result_vector.element.merge_by_distance_spatial(*result_vector.transform, 0.0001);
}
result_vector_table
}
fn boolean_operation_on_vector_table<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>> + Clone, boolean_operation: BooleanOperation) -> Table<Vector> {
match boolean_operation {
BooleanOperation::Union => union(vector),
BooleanOperation::SubtractFront => subtract(vector),
BooleanOperation::SubtractBack => subtract(vector.rev()),
BooleanOperation::Intersect => intersect(vector),
BooleanOperation::Difference => difference(vector),
}
}
fn union<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>>) -> Table<Vector> {
// Reverse the vector table rows so that the result style is the style of the first vector row
let mut vector_reversed = vector.rev();
let mut result_vector_table = Table::new_from_row(vector_reversed.next().map(|x| x.into_cloned()).unwrap_or_default());
let mut first_row = result_vector_table.iter_mut().next().expect("Expected the one row we just pushed");
// Loop over all vector table rows and union it with the result
let default = TableRow::default();
let mut second_vector = Some(vector_reversed.next().unwrap_or(default.as_ref()));
while let Some(lower_vector) = second_vector {
let transform_of_lower_into_space_of_upper = first_row.transform.inverse() * *lower_vector.transform;
let result = &mut first_row.element;
let upper_path_string = to_path(result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector.element, 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_path(&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 = vector_reversed.next();
}
result_vector_table
}
fn subtract<'a>(vector: impl Iterator<Item = TableRowRef<'a, Vector>>) -> Table<Vector> {
let mut vector = vector.into_iter();
let mut result_vector_table = Table::new_from_row(vector.next().map(|x| x.into_cloned()).unwrap_or_default());
let mut first_row = result_vector_table.iter_mut().next().expect("Expected the one row we just pushed");
let first_row_transform = if first_row.transform.matrix2.determinant() != 0. {
first_row.transform.inverse()
} else {
DAffine2::IDENTITY
};
let mut next_vector = vector.next();
while let Some(lower_vector) = next_vector {
let transform_of_lower_into_space_of_upper = first_row_transform * *lower_vector.transform;
let result = &mut first_row.element;
let upper_path_string = to_path(result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector.element, 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_path(&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 = vector.next();
}
result_vector_table
}
fn intersect<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>>) -> Table<Vector> {
let mut vector = vector.rev();
let mut result_vector_table = Table::new_from_row(vector.next().map(|x| x.into_cloned()).unwrap_or_default());
let mut first_row = result_vector_table.iter_mut().next().expect("Expected the one row we just pushed");
let default = TableRow::default();
let mut second_vector = Some(vector.next().unwrap_or(default.as_ref()));
// For each vector table row, set the result to the intersection of that path and the current result
while let Some(lower_vector) = second_vector {
let transform_of_lower_into_space_of_upper = first_row.transform.inverse() * *lower_vector.transform;
let result = &mut first_row.element;
let upper_path_string = to_path(result, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector.element, 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_path(&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 = vector.next();
}
result_vector_table
}
fn difference<'a>(vector: impl DoubleEndedIterator<Item = TableRowRef<'a, Vector>> + Clone) -> Table<Vector> {
let mut vector_iter = vector.clone().rev();
let mut any_intersection = TableRow::default();
let default = TableRow::default();
let mut second_vector = Some(vector_iter.next().unwrap_or(default.as_ref()));
// Find where all vector table row paths intersect at least once
while let Some(lower_vector) = second_vector {
let filtered_vector = vector.clone().filter(|v| *v != lower_vector).collect::<Vec<_>>().into_iter();
let unioned = boolean_operation_on_vector_table(filtered_vector, BooleanOperation::Union);
let first_row = unioned.iter().next().expect("Expected at least one row after the boolean union");
let transform_of_lower_into_space_of_upper = first_row.transform.inverse() * *lower_vector.transform;
let upper_path_string = to_path(first_row.element, DAffine2::IDENTITY);
let lower_path_string = to_path(lower_vector.element, 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 element = from_path(&boolean_intersection_string);
element.style = first_row.element.style.clone();
let boolean_intersection_result = TableRow {
element,
transform: *first_row.transform,
alpha_blending: *first_row.alpha_blending,
source_node_id: *first_row.source_node_id,
};
let transform_of_lower_into_space_of_upper = boolean_intersection_result.transform.inverse() * any_intersection.transform;
let upper_path_string = to_path(&boolean_intersection_result.element, DAffine2::IDENTITY);
let lower_path_string = to_path(&any_intersection.element, transform_of_lower_into_space_of_upper);
#[allow(unused_unsafe)]
let union_result = from_path(&unsafe { boolean_union(upper_path_string, lower_path_string) });
any_intersection.element = union_result;
any_intersection.transform = boolean_intersection_result.transform;
any_intersection.element.style = boolean_intersection_result.element.style.clone();
any_intersection.alpha_blending = boolean_intersection_result.alpha_blending;
second_vector = vector_iter.next();
}
// Subtract the area where they intersect at least once from the union of all vector paths
let union = boolean_operation_on_vector_table(vector, BooleanOperation::Union);
boolean_operation_on_vector_table(union.iter().chain(std::iter::once(any_intersection.as_ref())), BooleanOperation::SubtractFront)
}
fn flatten_vector(graphic_table: &Table<Graphic>) -> Table<Vector> {
graphic_table
.iter()
.flat_map(|element| {
match element.element.clone() {
Graphic::Vector(vector) => {
// Apply the parent graphic's transform to each element of the vector table
vector
.into_iter()
.map(|mut sub_vector| {
sub_vector.transform = *element.transform * sub_vector.transform;
sub_vector
})
.collect::<Vec<_>>()
}
Graphic::RasterCPU(image) => {
let make_row = |transform| {
// Convert the image frame into a rectangular subpath with the image's transform
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
// Create a vector table row from the rectangular subpath, with a default black fill
let mut element = Vector::from_subpath(subpath);
element.style.set_fill(Fill::Solid(Color::BLACK));
TableRow { element, ..Default::default() }
};
// Apply the parent graphic's transform to each raster element
image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::<Vec<_>>()
}
Graphic::RasterGPU(image) => {
let make_row = |transform| {
// Convert the image frame into a rectangular subpath with the image's transform
let mut subpath = Subpath::new_rectangle(DVec2::ZERO, DVec2::ONE);
subpath.apply_transform(transform);
// Create a vector table row from the rectangular subpath, with a default black fill
let mut element = Vector::from_subpath(subpath);
element.style.set_fill(Fill::Solid(Color::BLACK));
TableRow { element, ..Default::default() }
};
// Apply the parent graphic's transform to each raster element
image.iter().map(|row| make_row(*element.transform * *row.transform)).collect::<Vec<_>>()
}
Graphic::Graphic(mut graphic) => {
// Apply the parent graphic's transform to each element of inner table
for sub_element in graphic.iter_mut() {
*sub_element.transform = *element.transform * *sub_element.transform;
}
// Recursively flatten the inner table into the output vector table
let unioned = boolean_operation_on_vector_table(flatten_vector(&graphic).iter(), BooleanOperation::Union);
unioned.into_iter().collect::<Vec<_>>()
}
Graphic::Color(color) => color
.into_iter()
.map(|row| {
let mut element = Vector::default();
element.style.set_fill(Fill::Solid(row.element));
element.style.set_stroke_transform(DAffine2::IDENTITY);
TableRow {
element,
transform: row.transform,
alpha_blending: row.alpha_blending,
source_node_id: row.source_node_id,
}
})
.collect::<Vec<_>>(),
Graphic::Gradient(gradient) => gradient
.into_iter()
.map(|row| {
let mut element = Vector::default();
element.style.set_fill(Fill::Gradient(graphic_types::vector_types::gradient::Gradient {
stops: row.element,
..Default::default()
}));
element.style.set_stroke_transform(DAffine2::IDENTITY);
TableRow {
element,
transform: row.transform,
alpha_blending: row.alpha_blending,
source_node_id: row.source_node_id,
}
})
.collect::<Vec<_>>(),
}
})
.collect()
}
fn to_path(vector: &Vector, transform: DAffine2) -> Vec<path_bool::PathSegment> {
let mut path = Vec::new();
for subpath in vector.stroke_bezier_paths() {
to_path_segments(&mut path, &subpath, transform);
}
path
}
fn to_path_segments(path: &mut Vec<path_bool::PathSegment>, subpath: &Subpath<PointId>, transform: DAffine2) {
use path_bool::PathSegment;
let mut global_start = None;
let mut global_end = DVec2::ZERO;
for bezier in subpath.iter() {
const EPS: f64 = 1e-8;
let transform_point = |pos: DVec2| transform.transform_point2(pos).mul(EPS.recip()).round().mul(EPS);
let PathSegPoints { p0, p1, p2, p3 } = pathseg_points(bezier);
let p0 = transform_point(p0);
let p1 = p1.map(transform_point);
let p2 = p2.map(transform_point);
let p3 = transform_point(p3);
if global_start.is_none() {
global_start = Some(p0);
}
global_end = p3;
let segment = match (p1, p2) {
(None, None) => PathSegment::Line(p0, p3),
(None, Some(p2)) | (Some(p2), None) => PathSegment::Quadratic(p0, p2, p3),
(Some(p1), Some(p2)) => PathSegment::Cubic(p0, p1, p2, p3),
};
path.push(segment);
}
if let Some(start) = global_start {
path.push(PathSegment::Line(global_end, start));
}
}
fn from_path(path_data: &[Path]) -> Vector {
const EPSILON: f64 = 1e-5;
fn is_close(a: DVec2, b: DVec2) -> bool {
(a - b).length_squared() < EPSILON * EPSILON
}
let mut all_subpaths = Vec::new();
for path in path_data.iter().filter(|path| !path.is_empty()) {
let cubics: Vec<[DVec2; 4]> = path.iter().map(|segment| segment.to_cubic()).collect();
let mut manipulators_list = Vec::new();
let mut current_start = None;
for (index, cubic) in cubics.iter().enumerate() {
let [start, handle1, handle2, end] = *cubic;
if current_start.is_none() || !is_close(start, current_start.unwrap()) {
// Start a new subpath
if !manipulators_list.is_empty() {
all_subpaths.push(Subpath::new(std::mem::take(&mut manipulators_list), true));
}
// Use the correct in-handle (None) and out-handle for the start point
manipulators_list.push(ManipulatorGroup::new(start, None, Some(handle1)));
} else {
// Update the out-handle of the previous point
if let Some(last) = manipulators_list.last_mut() {
last.out_handle = Some(handle1);
}
}
// Add the end point with the correct in-handle and out-handle (None)
manipulators_list.push(ManipulatorGroup::new(end, Some(handle2), None));
current_start = Some(end);
// Check if this is the last segment
if index == cubics.len() - 1 {
all_subpaths.push(Subpath::new(manipulators_list, true));
manipulators_list = Vec::new(); // Reset manipulators for the next path
}
}
}
Vector::from_subpaths(all_subpaths, false)
}
type Path = Vec<path_bool::PathSegment>;
fn boolean_union(a: Path, b: Path) -> Vec<Path> {
path_bool(a, b, PathBooleanOperation::Union)
}
fn path_bool(a: Path, b: Path, op: PathBooleanOperation) -> Vec<Path> {
match path_bool::path_boolean(&a, FillRule::NonZero, &b, FillRule::NonZero, op) {
Ok(results) => results,
Err(e) => {
let a_path = path_bool::path_to_path_data(&a, 0.001);
let b_path = path_bool::path_to_path_data(&b, 0.001);
log::error!("Boolean error {e:?} encountered while processing {a_path}\n {op:?}\n {b_path}");
Vec::new()
}
}
}
fn boolean_subtract(a: Path, b: Path) -> Vec<Path> {
path_bool(a, b, PathBooleanOperation::Difference)
}
pub fn boolean_intersect(a: Path, b: Path) -> Vec<Path> {
path_bool(a, b, PathBooleanOperation::Intersection)
}