mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
Add a 'Max Concavity' angle to the Convex Hull node for concave hulls
Dents whose boundary turns backward by no more than the angle are flattened by the hull; deeper dents keep their original boundary. At the maximum of 360 degrees the node produces the fully convex hull as before, and at 0 degrees every dent survives. Bridges between separate subpaths always apply, so disjoint islands stay connected and interior geometry (such as holes) never survives at any setting. The partial hull reuses the convex pipeline, keeping collinear hull vertices so pockets (the boundary stretch each bridge spans, located by gap matching along the subpath) can attach to them. Each pocket is processed by a canonical deletion sweep that repeatedly removes the cheapest reflex sample vertex whose bypassing chord stays on the empty side of the boundary and within the angle budget, which handles nested dents inherently and can never cut into material bumps. Surviving chords are polished to exact tangencies with the same closed-form solves used for hull bridges, and kept stretches are emitted as exact cuts of the original segments.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@ use vector_types::gradient::{build_transform_with_y_preservation, initial_gradie
|
|||||||
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
|
use vector_types::subpath::{BezierHandles, ManipulatorGroup};
|
||||||
use vector_types::vector::PointDomain;
|
use vector_types::vector::PointDomain;
|
||||||
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath};
|
use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath};
|
||||||
use vector_types::vector::algorithms::convex_hull::convex_hull_of_geometry;
|
use vector_types::vector::algorithms::convex_hull::{SubpathRun, convex_hull_of_geometry, partial_convex_hull_of_geometry};
|
||||||
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt;
|
||||||
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
|
use vector_types::vector::algorithms::offset_subpath::offset_bezpath;
|
||||||
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
|
use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open};
|
||||||
@@ -581,7 +581,7 @@ pub fn merge_by_distance(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps all of the input geometry in its convex hull: the shape a taut rubber band would form when stretched around it.
|
/// Wraps all of the input geometry in its convex hull: the shape a taut rubber band would form when stretched around it. Dents in the geometry may optionally be kept based on how sharply their boundary turns backward.
|
||||||
///
|
///
|
||||||
/// Convex portions of curved segments are kept exactly as they are, and the boundary departs from a curve only where it must, continuing along a straight bridging line that leaves and rejoins the curves at perfect tangents. The anchor points, floating points, and subpaths (open or closed) of all the input shapes are wrapped together into one combined hull.
|
/// Convex portions of curved segments are kept exactly as they are, and the boundary departs from a curve only where it must, continuing along a straight bridging line that leaves and rejoins the curves at perfect tangents. The anchor points, floating points, and subpaths (open or closed) of all the input shapes are wrapped together into one combined hull.
|
||||||
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
#[node_macro::node(category("Vector: Modifier"), path(core_types::vector))]
|
||||||
@@ -590,12 +590,21 @@ async fn convex_hull<I: IntoGraphicList>(
|
|||||||
/// The `List` of vector paths to wrap in the convex hull. Nested `List`s are automatically flattened.
|
/// The `List` of vector paths to wrap in the convex hull. Nested `List`s are automatically flattened.
|
||||||
#[implementations(List<Graphic>, List<Vector>)]
|
#[implementations(List<Graphic>, List<Vector>)]
|
||||||
content: I,
|
content: I,
|
||||||
|
/// The maximum backward turning, in degrees, of a dent that still gets flattened by the hull.
|
||||||
|
///
|
||||||
|
/// At the maximum of 360°, the result is the fully convex hull. Lower values keep any dent whose boundary turns backward by more than this angle, so decreasing the value preserves progressively more concave detail, and at 0° every dent survives. Bridges between separate subpaths always apply, and interior geometry (such as holes) never survives.
|
||||||
|
#[range]
|
||||||
|
#[hard(0..360)]
|
||||||
|
#[default(360.)]
|
||||||
|
max_concavity: Angle,
|
||||||
) -> List<Vector> {
|
) -> List<Vector> {
|
||||||
let content = content.into_graphic_list();
|
let content = content.into_graphic_list();
|
||||||
let flattened: List<Vector> = content.clone().into_flattened_list();
|
let flattened: List<Vector> = content.clone().into_flattened_list();
|
||||||
|
|
||||||
// Gather the world-space segments and floating anchor points of every input item
|
// Gather the world-space segments and floating anchor points of every input item, remembering
|
||||||
|
// which contiguous run of segments forms each subpath
|
||||||
let mut segments = Vec::new();
|
let mut segments = Vec::new();
|
||||||
|
let mut runs = Vec::new();
|
||||||
let mut loose_points = Vec::new();
|
let mut loose_points = Vec::new();
|
||||||
for index in 0..flattened.len() {
|
for index in 0..flattened.len() {
|
||||||
let Some(element) = flattened.element(index) else { continue };
|
let Some(element) = flattened.element(index) else { continue };
|
||||||
@@ -603,7 +612,15 @@ async fn convex_hull<I: IntoGraphicList>(
|
|||||||
let affine = Affine::new(transform.to_cols_array());
|
let affine = Affine::new(transform.to_cols_array());
|
||||||
|
|
||||||
for bezpath in element.stroke_bezpath_iter() {
|
for bezpath in element.stroke_bezpath_iter() {
|
||||||
|
let start = segments.len();
|
||||||
segments.extend(bezpath.segments().map(|segment| affine * segment));
|
segments.extend(bezpath.segments().map(|segment| affine * segment));
|
||||||
|
if segments.len() > start {
|
||||||
|
let closed = matches!(bezpath.elements().last(), Some(kurbo::PathEl::ClosePath));
|
||||||
|
runs.push(SubpathRun {
|
||||||
|
segments: start..segments.len(),
|
||||||
|
closed,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anchor points not connected to any segment still participate in the hull
|
// Anchor points not connected to any segment still participate in the hull
|
||||||
@@ -615,7 +632,12 @@ async fn convex_hull<I: IntoGraphicList>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hull = convex_hull_of_geometry(&segments, &loose_points);
|
// The maximum angle means unlimited: the fully convex hull
|
||||||
|
let hull = if max_concavity >= 360. {
|
||||||
|
convex_hull_of_geometry(&segments, &loose_points)
|
||||||
|
} else {
|
||||||
|
partial_convex_hull_of_geometry(&segments, &runs, &loose_points, max_concavity.to_radians())
|
||||||
|
};
|
||||||
|
|
||||||
// Carry over the attributes and stroke of the last input item, matching the Boolean Operation node
|
// Carry over the attributes and stroke of the last input item, matching the Boolean Operation node
|
||||||
let Some(last_index) = flattened.len().checked_sub(1) else { return List::new() };
|
let Some(last_index) = flattened.len().checked_sub(1) else { return List::new() };
|
||||||
@@ -3425,7 +3447,7 @@ mod test {
|
|||||||
floating.point_domain.push(PointId::generate(), DVec2::new(50., 200.));
|
floating.point_domain.push(PointId::generate(), DVec2::new(50., 200.));
|
||||||
content.push(Item::new_from_element(floating));
|
content.push(Item::new_from_element(floating));
|
||||||
|
|
||||||
let hull = super::convex_hull(Footprint::default(), content).await;
|
let hull = super::convex_hull(Footprint::default(), content, 360.).await;
|
||||||
let element = hull.element(0).unwrap();
|
let element = hull.element(0).unwrap();
|
||||||
|
|
||||||
// The hull is the pentagon spanning both squares' outer corners and the floating point
|
// The hull is the pentagon spanning both squares' outer corners and the floating point
|
||||||
@@ -3440,6 +3462,27 @@ mod test {
|
|||||||
assert_eq!(transform, DAffine2::IDENTITY);
|
assert_eq!(transform, DAffine2::IDENTITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn convex_hull_max_concavity_keeps_dents() {
|
||||||
|
// A square with a rectangular notch in its top edge (180 degrees of backward turning)
|
||||||
|
let mut notched = BezPath::new();
|
||||||
|
notched.move_to(Point::new(0., 0.));
|
||||||
|
for point in [(100., 0.), (100., 100.), (60., 100.), (60., 60.), (40., 60.), (40., 100.), (0., 100.)] {
|
||||||
|
notched.line_to(Point::new(point.0, point.1));
|
||||||
|
}
|
||||||
|
notched.close_path();
|
||||||
|
let content = List::new_from_element(Vector::from_bezpath(notched));
|
||||||
|
|
||||||
|
// Below the notch's corner turning nothing flattens; at the maximum the hull is fully convex
|
||||||
|
let kept = super::convex_hull(Footprint::default(), content.clone(), 80.).await;
|
||||||
|
let kept_area: f64 = kept.element(0).unwrap().stroke_bezpath_iter().map(|bezpath| bezpath.area().abs()).sum();
|
||||||
|
assert!((kept_area - 9200.).abs() < 1., "the notch must survive at 80 degrees, got area {kept_area}");
|
||||||
|
|
||||||
|
let convex = super::convex_hull(Footprint::default(), content, 360.).await;
|
||||||
|
let convex_area: f64 = convex.element(0).unwrap().stroke_bezpath_iter().map(|bezpath| bezpath.area().abs()).sum();
|
||||||
|
assert!((convex_area - 10_000.).abs() < 1., "the hull must be fully convex at 360 degrees, got area {convex_area}");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn sample_polyline() {
|
async fn sample_polyline() {
|
||||||
let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]);
|
let path = BezPath::from_vec(vec![PathEl::MoveTo(Point::ZERO), PathEl::CurveTo(Point::ZERO, Point::new(100., 0.), Point::new(100., 0.))]);
|
||||||
|
|||||||
Reference in New Issue
Block a user