This commit is contained in:
Mohd Mohsin 2025-07-07 10:21:48 +02:00 committed by GitHub
commit d6bcdee482
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 294 additions and 63 deletions

View file

@ -1,7 +1,8 @@
use super::poisson_disk::poisson_disk_sample;
use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
use crate::vector::misc::{PointSpacingType, dvec2_to_point};
use glam::DVec2;
use kurbo::{BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, Rect, Shape};
use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape};
/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1].
/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
@ -314,3 +315,16 @@ pub fn poisson_disk_points(bezpath_index: usize, bezpaths: &[(BezPath, Rect)], s
poisson_disk_sample(offset, width, height, separation_disk_diameter, point_in_shape_checker, line_intersect_shape_checker, rng)
}
/// Returns true if the Bezier curve is equivalent to a line.
///
/// **NOTE**: This is different from simply checking if the segment is [`PathSeg::Line`] or [`PathSeg::Quad`] or [`PathSeg::Cubic`]. Bezier curve can also be a line if the control points are colinear to the start and end points. Therefore if the handles exceed the start and end point, it will still be considered as a line.
pub fn is_linear(segment: &PathSeg) -> bool {
let is_colinear = |a: Point, b: Point, c: Point| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE };
match *segment {
PathSeg::Line(_) => true,
PathSeg::Quad(QuadBez { p0, p1, p2 }) => is_colinear(p0, p1, p2),
PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3),
}
}

View file

@ -1,6 +1,7 @@
use bezier_rs::BezierHandles;
use dyn_any::DynAny;
use glam::DVec2;
use kurbo::Point;
use kurbo::{CubicBez, Line, PathSeg, Point, QuadBez};
/// Represents different ways of calculating the centroid.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
@ -96,3 +97,33 @@ pub fn point_to_dvec2(point: Point) -> DVec2 {
pub fn dvec2_to_point(value: DVec2) -> Point {
Point { x: value.x, y: value.y }
}
pub fn segment_to_handles(segment: &PathSeg) -> BezierHandles {
match *segment {
PathSeg::Line(_) => BezierHandles::Linear,
PathSeg::Quad(QuadBez { p0: _, p1, p2: _ }) => BezierHandles::Quadratic { handle: point_to_dvec2(p1) },
PathSeg::Cubic(CubicBez { p0: _, p1, p2, p3: _ }) => BezierHandles::Cubic {
handle_start: point_to_dvec2(p1),
handle_end: point_to_dvec2(p2),
},
}
}
pub fn handles_to_segment(start: DVec2, handles: BezierHandles, end: DVec2) -> PathSeg {
match handles {
bezier_rs::BezierHandles::Linear => PathSeg::Line(Line::new(dvec2_to_point(start), dvec2_to_point(end))),
bezier_rs::BezierHandles::Quadratic { handle } => {
let p0 = dvec2_to_point(start);
let p1 = dvec2_to_point(handle);
let p2 = dvec2_to_point(end);
PathSeg::Quad(QuadBez::new(p0, p1, p2))
}
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
let p0 = dvec2_to_point(start);
let p1 = dvec2_to_point(handle_start);
let p2 = dvec2_to_point(handle_end);
let p3 = dvec2_to_point(end);
PathSeg::Cubic(CubicBez::new(p0, p1, p2, p3))
}
}
}

View file

@ -226,10 +226,10 @@ impl VectorData {
pub fn close_subpaths(&mut self) {
let segments_to_add: Vec<_> = self
.stroke_bezier_paths()
.filter(|subpath| !subpath.closed)
.filter_map(|subpath| {
let (first, last) = subpath.manipulator_groups().first().zip(subpath.manipulator_groups().last())?;
.build_stroke_path_iter()
.filter(|(_, closed)| !closed)
.filter_map(|(manipulator_groups, _)| {
let (first, last) = manipulator_groups.first().zip(manipulator_groups.last())?;
let (start, end) = self.point_domain.resolve_id(first.id).zip(self.point_domain.resolve_id(last.id))?;
Some((start, end))
})
@ -370,7 +370,7 @@ impl VectorData {
}
pub fn check_point_inside_shape(&self, vector_data_transform: DAffine2, point: DVec2) -> bool {
let bez_paths: Vec<_> = self
let number = self
.stroke_bezpath_iter()
.map(|mut bezpath| {
// TODO: apply transform to points instead of modifying the paths
@ -379,19 +379,9 @@ impl VectorData {
let bbox = bezpath.bounding_box();
(bezpath, bbox)
})
.collect();
// Check against all paths the point is contained in to compute the correct winding number
let mut number = 0;
for (shape, bbox) in bez_paths {
if bbox.x0 > point.x || bbox.y0 > point.y || bbox.x1 < point.x || bbox.y1 < point.y {
continue;
}
let winding = shape.winding(dvec2_to_point(point));
number += winding;
}
.filter(|(_, bbox)| bbox.contains(dvec2_to_point(point)))
.map(|(bezpath, _)| bezpath.winding(dvec2_to_point(point)))
.sum::<i32>();
// Non-zero fill rule
number != 0

View file

@ -440,6 +440,35 @@ impl SegmentDomain {
let handles = self.handles.iter_mut();
zip(ids, zip(start_point, zip(end_point, handles))).map(|(id, (start_point, (end_point, handles)))| (id, start_point, end_point, handles))
}
pub(crate) fn pair_handles_and_points_mut_by_index(
&mut self,
index1: usize,
index2: usize,
) -> (&mut bezier_rs::BezierHandles, &mut usize, &mut usize, &mut bezier_rs::BezierHandles, &mut usize, &mut usize) {
// Use split_at_mut to avoid multiple mutable borrows of the same slice
let (handles_first, handles_second) = self.handles.split_at_mut(index2.max(index1));
let (start_first, start_second) = self.start_point.split_at_mut(index2.max(index1));
let (end_first, end_second) = self.end_point.split_at_mut(index2.max(index1));
let (h1, h2) = if index1 < index2 {
(&mut handles_first[index1], &mut handles_second[0])
} else {
(&mut handles_second[0], &mut handles_first[index2])
};
let (sp1, sp2) = if index1 < index2 {
(&mut start_first[index1], &mut start_second[0])
} else {
(&mut start_second[0], &mut start_first[index2])
};
let (ep1, ep2) = if index1 < index2 {
(&mut end_first[index1], &mut end_second[0])
} else {
(&mut end_second[0], &mut end_first[index2])
};
(h1, sp1, ep1, h2, sp2, ep2)
}
}
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]

View file

@ -9,19 +9,23 @@ use crate::instances::{Instance, InstanceMut, Instances};
use crate::raster_types::{CPU, GPU, RasterDataTable};
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, Multiplier, Percentage, PixelLength, PixelSize, SeedValue};
use crate::transform::{Footprint, ReferencePoint, Transform};
use crate::vector::PointDomain;
use crate::vector::algorithms::bezpath_algorithms::{eval_pathseg_euclidean, is_linear};
use crate::vector::algorithms::merge_by_distance::MergeByDistanceExt;
use crate::vector::misc::{MergeByDistanceAlgorithm, PointSpacingType};
use crate::vector::misc::{handles_to_segment, segment_to_handles};
use crate::vector::style::{PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use crate::vector::{FillId, PointDomain, RegionId};
use crate::vector::{FillId, RegionId};
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
use bezier_rs::{Join, ManipulatorGroup, Subpath};
use bezier_rs::{BezierHandles, Join, ManipulatorGroup, Subpath};
use core::f64::consts::PI;
use core::hash::{Hash, Hasher};
use glam::{DAffine2, DVec2};
use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, ParamCurve, PathEl, PathSeg, Shape};
use rand::{Rng, SeedableRng};
use std::collections::hash_map::DefaultHasher;
use std::f64::consts::PI;
use std::f64::consts::TAU;
use std::hash::{Hash, Hasher};
/// Implemented for types that can be converted to an iterator of vector data.
/// Used for the fill and stroke node so they can be used on VectorData or GraphicGroup
@ -1737,10 +1741,9 @@ async fn morph(_: impl Ctx, source: VectorDataTable, #[expose] target: VectorDat
fn bevel_algorithm(mut vector_data: VectorData, vector_data_transform: DAffine2, distance: f64) -> VectorData {
// Splits a bézier curve based on a distance measurement
fn split_distance(bezier: bezier_rs::Bezier, distance: f64, length: f64) -> bezier_rs::Bezier {
const EUCLIDEAN_ERROR: f64 = 0.001;
let parametric = bezier.euclidean_to_parametric_with_total_length((distance / length).clamp(0., 1.), EUCLIDEAN_ERROR, length);
bezier.split(bezier_rs::TValue::Parametric(parametric))[1]
fn split_distance(bezier: PathSeg, distance: f64, length: f64) -> PathSeg {
let parametric = eval_pathseg_euclidean(bezier, (distance / length).clamp(0., 1.), DEFAULT_ACCURACY);
bezier.subsegment(parametric..1.)
}
/// Produces a list that corresponds with the point ID. The value is how many segments are connected.
@ -1779,65 +1782,229 @@ fn bevel_algorithm(mut vector_data: VectorData, vector_data_transform: DAffine2,
}
}
fn calculate_distance_to_spilt(bezier1: PathSeg, bezier2: PathSeg, bevel_length: f64) -> f64 {
if is_linear(&bezier1) && is_linear(&bezier2) {
let v1 = (bezier1.end() - bezier1.start()).normalize();
let v2 = (bezier1.end() - bezier2.end()).normalize();
let dot_product = v1.dot(v2);
let angle_rad = dot_product.acos();
bevel_length / ((angle_rad / 2.0).sin())
} else {
let length1 = bezier1.perimeter(DEFAULT_ACCURACY);
let length2 = bezier2.perimeter(DEFAULT_ACCURACY);
let max_split = length1.min(length2);
let mut split_distance = 0.0;
let mut best_diff = f64::MAX;
let mut current_best_distance = 0.0;
let clamp_and_round = |value: f64| ((value * 1000.0).round() / 1000.0).clamp(0.0, 1.0);
const INITIAL_SAMPLES: usize = 50;
for i in 0..=INITIAL_SAMPLES {
let distance_sample = max_split * (i as f64 / INITIAL_SAMPLES as f64);
let x_point_t = eval_pathseg_euclidean(bezier1, 1.0 - clamp_and_round(distance_sample / length1), DEFAULT_ACCURACY);
let y_point_t = eval_pathseg_euclidean(bezier2, clamp_and_round(distance_sample / length2), DEFAULT_ACCURACY);
let x_point = bezier1.eval(x_point_t);
let y_point = bezier2.eval(y_point_t);
let distance = x_point.distance(y_point);
let diff = (bevel_length - distance).abs();
if diff < best_diff {
best_diff = diff;
current_best_distance = distance_sample;
}
if bevel_length - distance < 0.0 {
split_distance = distance_sample;
if i > 0 {
let prev_sample = max_split * ((i - 1) as f64 / INITIAL_SAMPLES as f64);
const REFINE_STEPS: usize = 10;
for j in 1..=REFINE_STEPS {
let refined_sample = prev_sample + (distance_sample - prev_sample) * (j as f64 / REFINE_STEPS as f64);
let x_point_t = eval_pathseg_euclidean(bezier1, 1.0 - (refined_sample / length1).clamp(0.0, 1.0), DEFAULT_ACCURACY);
let y_point_t = eval_pathseg_euclidean(bezier2, (refined_sample / length2).clamp(0.0, 1.0), DEFAULT_ACCURACY);
let x_point = bezier1.eval(x_point_t);
let y_point = bezier2.eval(y_point_t);
let distance = x_point.distance(y_point);
if bevel_length - distance < 0.0 {
split_distance = refined_sample;
break;
}
}
}
break;
}
}
if split_distance == 0.0 && current_best_distance > 0.0 {
split_distance = current_best_distance;
}
split_distance
}
}
fn sort_segments(segment_domain: &SegmentDomain) -> Vec<usize> {
let start_points = segment_domain.start_point();
let end_points = segment_domain.end_point();
let mut sorted_segments = Vec::new();
sorted_segments.push(0);
for _ in 0..start_points.len() {
match sorted_segments.last() {
Some(last) => {
if let Some(index) = start_points.iter().position(|&p| p == end_points[*last as usize]) {
if index == 0 {
break;
} else {
sorted_segments.push(index);
}
}
}
None => break,
}
}
sorted_segments
}
fn update_existing_segments(vector_data: &mut VectorData, vector_data_transform: DAffine2, distance: f64, segments_connected: &mut [usize]) -> Vec<[usize; 2]> {
let mut next_id = vector_data.point_domain.next_id();
let mut new_segments = Vec::new();
for (handles, start_point_index, end_point_index) in vector_data.segment_domain.handles_and_points_mut() {
// Convert the original segment to a bezier
let mut bezier = bezier_rs::Bezier {
start: vector_data.point_domain.positions()[*start_point_index],
end: vector_data.point_domain.positions()[*end_point_index],
handles: *handles,
};
let sorted_segments = sort_segments(&vector_data.segment_domain);
let segment_domain = &mut vector_data.segment_domain;
let segment_domain_length = segment_domain.ids().len();
if bezier.is_linear() {
bezier.handles = bezier_rs::BezierHandles::Linear;
let mut first_original_length = 0.;
let mut first_length = 0.;
let mut prev_original_length = 0.;
let mut prev_length = 0.;
for i in 0..segment_domain_length {
let (index, next_index) = if i == segment_domain_length - 1 { (i, 0) } else { (i, i + 1) };
let (handles, start_point, end_point, next_handles, next_start_point, next_end_point) =
segment_domain.pair_handles_and_points_mut_by_index(sorted_segments[index], sorted_segments[next_index]);
let start = vector_data.point_domain.positions()[*start_point];
let end = vector_data.point_domain.positions()[*end_point];
let mut bezier = handles_to_segment(start, *handles, end);
bezier = Affine::new(vector_data_transform.to_cols_array()) * bezier;
let next_start = vector_data.point_domain.positions()[*next_start_point];
let next_end = vector_data.point_domain.positions()[*next_end_point];
let mut next_bezier = handles_to_segment(next_start, *next_handles, next_end);
next_bezier = Affine::new(vector_data_transform.to_cols_array()) * next_bezier;
let spilt_distance = calculate_distance_to_spilt(bezier, next_bezier, distance);
if is_linear(&bezier) {
let start = point_to_dvec2(bezier.start());
let end = point_to_dvec2(bezier.end());
bezier = handles_to_segment(start, BezierHandles::Linear, end);
}
bezier = bezier.apply_transformation(|p| vector_data_transform.transform_point2(p));
if is_linear(&next_bezier) {
let start = point_to_dvec2(next_bezier.start());
let end = point_to_dvec2(next_bezier.end());
next_bezier = handles_to_segment(start, BezierHandles::Linear, end);
}
let inverse_transform = (vector_data_transform.matrix2.determinant() != 0.).then(|| vector_data_transform.inverse()).unwrap_or_default();
let original_length = bezier.length(None);
let mut length = original_length;
// Only split if the length is big enough to make it worthwhile
let valid_length = length > 1e-10;
if segments_connected[*start_point_index] > 0 && valid_length {
// Apply the bevel to the start
let distance = distance.min(original_length / 2.);
bezier = split_distance(bezier, distance, length);
length = (length - distance).max(0.);
// Update the start position
let pos = inverse_transform.transform_point2(bezier.start);
create_or_modify_point(&mut vector_data.point_domain, segments_connected, pos, start_point_index, &mut next_id, &mut new_segments);
if index == 0 && next_index == 1 {
first_original_length = bezier.perimeter(DEFAULT_ACCURACY);
first_length = first_original_length;
}
let (original_length, length) = if index == 0 {
(bezier.perimeter(DEFAULT_ACCURACY), bezier.perimeter(DEFAULT_ACCURACY))
} else {
(prev_original_length, prev_length)
};
let (next_original_length, mut next_length) = if index == segment_domain_length - 1 && next_index == 0 {
(first_original_length, first_length)
} else {
(next_bezier.perimeter(DEFAULT_ACCURACY), next_bezier.perimeter(DEFAULT_ACCURACY))
};
// Only split if the length is big enough to make it worthwhile
let valid_length = length > 1e-10;
if segments_connected[*end_point_index] > 0 && valid_length {
if segments_connected[*end_point] > 0 && valid_length {
// Apply the bevel to the end
let distance = distance.min(original_length / 2.);
bezier = split_distance(bezier.reversed(), distance, length).reversed();
let distance = spilt_distance.min(original_length.min(next_original_length) / 2.);
bezier = split_distance(bezier.reverse(), distance, length).reverse();
if index == 0 && next_index == 1 {
first_length = (length - distance).max(0.);
}
// Update the end position
let pos = inverse_transform.transform_point2(bezier.end);
create_or_modify_point(&mut vector_data.point_domain, segments_connected, pos, end_point_index, &mut next_id, &mut new_segments);
let pos = inverse_transform.transform_point2(point_to_dvec2(bezier.end()));
create_or_modify_point(&mut vector_data.point_domain, segments_connected, pos, end_point, &mut next_id, &mut new_segments);
}
// Update the handles
*handles = bezier.handles.apply_transformation(|p| inverse_transform.transform_point2(p));
*handles = segment_to_handles(&bezier).apply_transformation(|p| inverse_transform.transform_point2(p));
// Only split if the length is big enough to make it worthwhile
let valid_length = next_length > 1e-10;
if segments_connected[*next_start_point] > 0 && valid_length {
// Apply the bevel to the start
let distance = spilt_distance.min(next_original_length.min(original_length) / 2.);
next_bezier = split_distance(next_bezier, distance, next_length);
next_length = (next_length - distance).max(0.);
// Update the start position
let pos = inverse_transform.transform_point2(point_to_dvec2(next_bezier.start()));
create_or_modify_point(&mut vector_data.point_domain, segments_connected, pos, next_start_point, &mut next_id, &mut new_segments);
// Update the handles
*next_handles = segment_to_handles(&next_bezier).apply_transformation(|p| inverse_transform.transform_point2(p));
}
prev_original_length = next_original_length;
prev_length = next_length;
}
new_segments
}
fn insert_new_segments(vector_data: &mut VectorData, new_segments: &[[usize; 2]]) {
let mut next_id = vector_data.segment_domain.next_id();
for &[start, end] in new_segments {
vector_data.segment_domain.push(next_id.next_id(), start, end, bezier_rs::BezierHandles::Linear, StrokeId::ZERO);
let handles = bezier_rs::BezierHandles::Linear;
vector_data.segment_domain.push(next_id.next_id(), start, end, handles, StrokeId::ZERO);
}
}
let mut segments_connected = segments_connected_count(&vector_data);
let new_segments = update_existing_segments(&mut vector_data, vector_data_transform, distance, &mut segments_connected);
insert_new_segments(&mut vector_data, &new_segments);
if distance > 1.0 {
let mut segments_connected = segments_connected_count(&vector_data);
let new_segments = update_existing_segments(&mut vector_data, vector_data_transform, distance, &mut segments_connected);
insert_new_segments(&mut vector_data, &new_segments);
}
vector_data
}
@ -1849,7 +2016,7 @@ fn bevel(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length)
for source_instance in source.instance_iter() {
result_table.push(Instance {
instance: bevel_algorithm(source_instance.instance, source_instance.transform, distance),
..Default::default()
..source_instance
});
}
@ -1905,7 +2072,7 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<
.instance_ref_iter()
.map(|vector_data_instance| {
let scale = vector_data_instance.transform.decompose_scale();
vector_data_instance.instance.stroke_bezier_paths().map(|subpath| subpath.area(Some(1e-3), Some(1e-3))).sum::<f64>() * scale.x * scale.y
vector_data_instance.instance.stroke_bezpath_iter().map(|subpath| subpath.area() * scale.x * scale.y).sum::<f64>()
})
.sum()
}