Attribute-based vector format refactor (#1624)

* Initial vector format structure

* Click targets

* Code review pass

* Remove subpaths from vector data

* Morph node & vector node tests

* Insignificant change

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0HyperCube 2024-03-09 18:27:30 +00:00 committed by GitHub
parent c8ea9e05a6
commit 218e9675fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 991 additions and 436 deletions

1
Cargo.lock generated
View File

@ -2366,6 +2366,7 @@ dependencies = [
"serde", "serde",
"specta", "specta",
"spirv-std", "spirv-std",
"tokio",
"usvg 0.39.0", "usvg 0.39.0",
"wasm-bindgen", "wasm-bindgen",
"web-sys", "web-sys",

View File

@ -4,7 +4,6 @@ use crate::messages::prelude::Message;
use bezier_rs::Subpath; use bezier_rs::Subpath;
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId;
use core::f64::consts::PI; use core::f64::consts::PI;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
@ -114,7 +113,7 @@ impl OverlayContext {
self.render_context.stroke(); self.render_context.stroke();
} }
pub fn outline<'a>(&mut self, subpaths: impl Iterator<Item = &'a Subpath<ManipulatorGroupId>>, transform: DAffine2) { pub fn outline<'a, Id: bezier_rs::Identifier>(&mut self, subpaths: impl Iterator<Item = &'a Subpath<Id>>, transform: DAffine2) {
self.render_context.begin_path(); self.render_context.begin_path();
for subpath in subpaths { for subpath in subpaths {
let mut curves = subpath.iter().peekable(); let mut curves = subpath.iter().peekable();

View File

@ -4,9 +4,9 @@ use graph_craft::document::{DocumentNode, NodeId, NodeNetwork};
use graphene_core::renderer::ClickTarget; use graphene_core::renderer::ClickTarget;
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::transform::Footprint; use graphene_core::transform::Footprint;
use graphene_core::uuid::ManipulatorGroupId;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_std::vector::PointId;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::num::NonZeroU64; use std::num::NonZeroU64;
@ -287,7 +287,7 @@ impl DocumentMetadata {
.reduce(Quad::combine_bounds) .reduce(Quad::combine_bounds)
} }
pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<ManipulatorGroupId>> { pub fn layer_outline(&self, layer: LayerNodeIdentifier) -> impl Iterator<Item = &bezier_rs::Subpath<PointId>> {
static EMPTY: Vec<ClickTarget> = Vec::new(); static EMPTY: Vec<ClickTarget> = Vec::new();
let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY); let click_targets = self.click_targets.get(&layer).unwrap_or(&EMPTY);
click_targets.iter().map(|click_target| &click_target.subpath) click_targets.iter().map(|click_target| &click_target.subpath)

View File

@ -125,7 +125,7 @@ impl ClosestSegment {
} }
fn t_min_max(bezier: &Bezier, layer_scale: DVec2) -> (f64, f64) { fn t_min_max(bezier: &Bezier, layer_scale: DVec2) -> (f64, f64) {
let length = bezier.apply_transformation(|point| point * layer_scale).length(Some(100)); let length = bezier.apply_transformation(|point| point * layer_scale).length(None);
let too_close_t = (INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE / length).min(0.5); let too_close_t = (INSERT_POINT_ON_SEGMENT_TOO_CLOSE_DISTANCE / length).min(0.5);
let t_min_euclidean = too_close_t; let t_min_euclidean = too_close_t;
@ -148,7 +148,7 @@ impl ClosestSegment {
// Linear approximation of parametric t-value ranges: // Linear approximation of parametric t-value ranges:
let t_min = self.t_min / self.scale; let t_min = self.t_min / self.scale;
let t_max = 1. - ((1. - self.t_max) / self.scale); let t_max = 1. - ((1. - self.t_max) / self.scale);
let t = self.bezier.project(layer_m_pos, None).max(t_min).min(t_max); let t = self.bezier.project(layer_m_pos).max(t_min).min(t_max);
self.t = t; self.t = t;
let bezier_point = self.bezier.evaluate(TValue::Parametric(t)); let bezier_point = self.bezier.evaluate(TValue::Parametric(t));
@ -1099,8 +1099,6 @@ impl ShapeState {
let scale = document_metadata.document_to_viewport.decompose_scale().x; let scale = document_metadata.document_to_viewport.decompose_scale().x;
let tolerance = tolerance + 0.5 * scale; // make more talerance at large scale let tolerance = tolerance + 0.5 * scale; // make more talerance at large scale
let lut_size = ((5. + scale) as usize).min(20); // need more precision at large scale
let projection_options = bezier_rs::ProjectionOptions { lut_size, ..Default::default() };
let mut closest = None; let mut closest = None;
let mut closest_distance_squared: f64 = tolerance * tolerance; let mut closest_distance_squared: f64 = tolerance * tolerance;
@ -1109,7 +1107,7 @@ impl ShapeState {
for (subpath_index, subpath) in subpaths.iter().enumerate() { for (subpath_index, subpath) in subpaths.iter().enumerate() {
for (manipulator_index, bezier) in subpath.iter().enumerate() { for (manipulator_index, bezier) in subpath.iter().enumerate() {
let t = bezier.project(layer_pos, Some(projection_options)); let t = bezier.project(layer_pos);
let layerspace = bezier.evaluate(TValue::Parametric(t)); let layerspace = bezier.evaluate(TValue::Parametric(t));
let screenspace = transform.transform_point2(layerspace); let screenspace = transform.transform_point2(layerspace);

View File

@ -172,8 +172,8 @@ impl<'a> SnapData<'a> {
fn ignore_bounds(&self, layer: LayerNodeIdentifier) -> bool { fn ignore_bounds(&self, layer: LayerNodeIdentifier) -> bool {
self.manipulators.iter().any(|&(ignore, _)| ignore == layer) self.manipulators.iter().any(|&(ignore, _)| ignore == layer)
} }
fn ignore_manipulator(&self, layer: LayerNodeIdentifier, manipulator: ManipulatorGroupId) -> bool { fn ignore_manipulator(&self, layer: LayerNodeIdentifier, manipulator: impl Into<ManipulatorGroupId>) -> bool {
self.manipulators.contains(&(layer, manipulator)) self.manipulators.contains(&(layer, manipulator.into()))
} }
} }
impl SnapManager { impl SnapManager {
@ -327,7 +327,7 @@ impl SnapManager {
if let Some(ind) = &self.indicator { if let Some(ind) = &self.indicator {
for curve in &ind.curves { for curve in &ind.curves {
let Some(curve) = curve else { continue }; let Some(curve) = curve else { continue };
overlay_context.outline([Subpath::from_bezier(curve)].iter(), to_viewport); overlay_context.outline::<ManipulatorGroupId>([Subpath::from_bezier(curve)].iter(), to_viewport);
} }
if let Some(quad) = ind.target_bounds { if let Some(quad) = ind.target_bounds {
overlay_context.quad(to_viewport * quad); overlay_context.quad(to_viewport * quad);

View File

@ -9,6 +9,7 @@ use bezier_rs::{Bezier, Identifier, Subpath, TValue};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graphene_core::renderer::Quad; use graphene_core::renderer::Quad;
use graphene_core::uuid::ManipulatorGroupId; use graphene_core::uuid::ManipulatorGroupId;
use graphene_std::vector::PointId;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct LayerSnapper { pub struct LayerSnapper {
@ -62,7 +63,7 @@ impl LayerSnapper {
for subpath in document.metadata.layer_outline(layer) { for subpath in document.metadata.layer_outline(layer) {
for (start_index, curve) in subpath.iter().enumerate() { for (start_index, curve) in subpath.iter().enumerate() {
let document_curve = curve.apply_transformation(|p| transform.transform_point2(p)); let document_curve = curve.apply_transformation(|p| transform.transform_point2(p));
let start = subpath.manipulator_groups()[start_index].id; let start = subpath.manipulator_groups()[start_index].id.into();
if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) { if snap_data.ignore_manipulator(layer, start) || snap_data.ignore_manipulator(layer, subpath.manipulator_groups()[(start_index + 1) % subpath.len()].id) {
continue; continue;
} }
@ -94,7 +95,7 @@ impl LayerSnapper {
if path.document_curve.start.distance_squared(path.document_curve.end) < tolerance * tolerance * 2. { if path.document_curve.start.distance_squared(path.document_curve.end) < tolerance * tolerance * 2. {
continue; continue;
} }
let time = path.document_curve.project(point.document_point, None); let time = path.document_curve.project(point.document_point);
let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time)); let snapped_point_document = path.document_curve.evaluate(bezier_rs::TValue::Parametric(time));
let distance = snapped_point_document.distance(point.document_point); let distance = snapped_point_document.distance(point.document_point);
@ -372,7 +373,7 @@ pub fn get_bbox_points(quad: Quad, points: &mut Vec<SnapCandidatePoint>, values:
fn handle_not_under(to_document: DAffine2) -> impl Fn(&DVec2) -> bool { fn handle_not_under(to_document: DAffine2) -> impl Fn(&DVec2) -> bool {
move |&offset: &DVec2| to_document.transform_vector2(offset).length_squared() >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE move |&offset: &DVec2| to_document.transform_vector2(offset).length_squared() >= HIDE_HANDLE_DISTANCE * HIDE_HANDLE_DISTANCE
} }
fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<ManipulatorGroupId>, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>, to_document: DAffine2) { fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<PointId>, snap_data: &SnapData, points: &mut Vec<SnapCandidatePoint>, to_document: DAffine2) {
let document = snap_data.document; let document = snap_data.document;
// Midpoints of linear segments // Midpoints of linear segments
if document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::LineMidpoint)) { if document.snapping_state.target_enabled(SnapTarget::Geometry(GeometrySnapTarget::LineMidpoint)) {
@ -418,7 +419,7 @@ fn subpath_anchor_snap_points(layer: LayerNodeIdentifier, subpath: &Subpath<Mani
} }
} }
pub fn group_smooth(group: &bezier_rs::ManipulatorGroup<ManipulatorGroupId>, to_document: DAffine2, subpath: &Subpath<ManipulatorGroupId>, index: usize) -> bool { pub fn group_smooth<Id: bezier_rs::Identifier>(group: &bezier_rs::ManipulatorGroup<Id>, to_document: DAffine2, subpath: &Subpath<Id>, index: usize) -> bool {
let anchor = group.anchor; let anchor = group.anchor;
let handle_in = group.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document)); let handle_in = group.in_handle.map(|handle| anchor - handle).filter(handle_not_under(to_document));
let handle_out = group.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document)); let handle_out = group.out_handle.map(|handle| handle - anchor).filter(handle_not_under(to_document));

View File

@ -1,4 +1,4 @@
use crate::utils::{f64_compare, TValue, TValueType}; use crate::utils::{TValue, TValueType};
use super::*; use super::*;
@ -21,44 +21,57 @@ impl Bezier {
return 1.; return 1.;
} }
let mut low = 0.; match self.handles {
let mut mid = 0.5; BezierHandles::Linear => euclidean_t,
let mut high = 1.; BezierHandles::Quadratic { handle } => {
// Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, level: u8, desired_len: f64) -> (f64, f64) {
let lower = a0.distance(a2);
let upper = a0.distance(a1) + a1.distance(a2);
if level >= 8 {
let approx_len = (lower + upper) / 2.;
return (approx_len, desired_len / approx_len);
}
// The euclidean t-value input generally correlates with the parametric t-value result. let b1 = 0.5 * (a0 + a1);
// So we can assume a low t-value has a short length from the start of the curve, and a high t-value has a short length from the end of the curve. let c1 = 0.5 * (a1 + a2);
// We'll use a strategy where we measure from either end of the curve depending on which side is closer than thus more likely to be proximate to the sought parametric t-value. let b2 = 0.5 * (b1 + c1);
// This allows us to use fewer segments to approximate the curve, which usually won't go much beyond half the curve. let (first_len, t) = recurse(a0, b1, b2, level + 1, desired_len);
let result_likely_closer_to_start = euclidean_t < 0.5; if first_len > desired_len {
// If the curve is near either end, we need even fewer segments to approximate the curve with reasonable accuracy. return (first_len, t * 0.5);
// A point that's likely near the center is the worst case where we need to use up to half the predefined number of max subdivisions. }
let subdivisions_proportional_to_likely_length = ((euclidean_t - 0.5).abs() * DEFAULT_LENGTH_SUBDIVISIONS as f64).round().max(1.) as usize; let (second_len, t) = recurse(b2, c1, a2, level + 1, desired_len - first_len);
(first_len + second_len, t * 0.5 + 0.5)
}
recurse(self.start, handle, self.end, 0, total_length * euclidean_t).1
}
BezierHandles::Cubic { handle_start, handle_end } => {
// Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, a3: DVec2, level: u8, desired_len: f64) -> (f64, f64) {
let lower = a0.distance(a3);
let upper = a0.distance(a1) + a1.distance(a2) + a2.distance(a3);
if level >= 8 {
let approx_len = (lower + upper) / 2.;
return (approx_len, desired_len / approx_len);
}
// Binary search for the parametric t-value that corresponds to the euclidean distance ratio by trimming the curve between the start and the tested parametric t-value during each iteration of the search. let b1 = 0.5 * (a0 + a1);
while low < high { let t0 = 0.5 * (a1 + a2);
mid = (low + high) / 2.; let c1 = 0.5 * (a2 + a3);
let b2 = 0.5 * (b1 + t0);
// We can search from the curve start to the sought point, or from the sought point to the curve end, depending on which side is likely closer to the result. let c2 = 0.5 * (t0 + c1);
let current_length = if result_likely_closer_to_start { let b3 = 0.5 * (b2 + c2);
let trimmed = self.trim(TValue::Parametric(0.), TValue::Parametric(mid)); let (first_len, t) = recurse(a0, b1, b2, b3, level + 1, desired_len);
trimmed.length(Some(subdivisions_proportional_to_likely_length)) if first_len > desired_len {
} else { return (first_len, t * 0.5);
let trimmed = self.trim(TValue::Parametric(mid), TValue::Parametric(1.)); }
let trimmed_length = trimmed.length(Some(subdivisions_proportional_to_likely_length)); let (second_len, t) = recurse(b3, c2, c1, a3, level + 1, desired_len - first_len);
total_length - trimmed_length (first_len + second_len, t * 0.5 + 0.5)
}; }
let current_euclidean_t = current_length / total_length; recurse(self.start, handle_start, handle_end, self.end, 0, total_length * euclidean_t).1
if f64_compare(current_euclidean_t, euclidean_t, error) {
break;
} else if current_euclidean_t < euclidean_t {
low = mid;
} else {
high = mid;
} }
} }
.clamp(0., 1.)
mid
} }
/// Convert a [TValue] to a parametric `t`-value. /// Convert a [TValue] to a parametric `t`-value.
@ -109,133 +122,86 @@ impl Bezier {
/// Return a selection of equidistant points on the bezier curve. /// Return a selection of equidistant points on the bezier curve.
/// If no value is provided for `steps`, then the function will default `steps` to be 10. /// If no value is provided for `steps`, then the function will default `steps` to be 10.
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#bezier/lookup-table/solo" title="Lookup-Table Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#bezier/lookup-table/solo" title="Lookup-Table Demo"></iframe>
pub fn compute_lookup_table(&self, steps: Option<usize>, tvalue_type: Option<TValueType>) -> Vec<DVec2> { pub fn compute_lookup_table(&self, steps: Option<usize>, tvalue_type: Option<TValueType>) -> impl Iterator<Item = DVec2> + '_ {
let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE); let steps = steps.unwrap_or(DEFAULT_LUT_STEP_SIZE);
let tvalue_type = tvalue_type.unwrap_or(TValueType::Parametric); let tvalue_type = tvalue_type.unwrap_or(TValueType::Parametric);
(0..=steps) (0..=steps).map(move |t| {
.map(|t| { let tvalue = match tvalue_type {
let tvalue = match tvalue_type { TValueType::Parametric => TValue::Parametric(t as f64 / steps as f64),
TValueType::Parametric => TValue::Parametric(t as f64 / steps as f64), TValueType::Euclidean => TValue::Euclidean(t as f64 / steps as f64),
TValueType::Euclidean => TValue::Euclidean(t as f64 / steps as f64), };
}; self.evaluate(tvalue)
self.evaluate(tvalue) })
})
.collect()
} }
/// Return an approximation of the length of the bezier curve. /// Return an approximation of the length of the bezier curve.
/// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is 1000. /// - `tolerance` - Tolerance used to approximate the curve.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/length/solo" title="Length Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/length/solo" title="Length Demo"></iframe>
pub fn length(&self, num_subdivisions: Option<usize>) -> f64 { pub fn length(&self, tolerance: Option<f64>) -> f64 {
match self.handles { match self.handles {
BezierHandles::Linear => (self.start - self.end).length(), BezierHandles::Linear => (self.start - self.end).length(),
_ => { BezierHandles::Quadratic { handle } => {
// Code example from <https://gamedev.stackexchange.com/questions/5373/moving-ships-between-two-planets-along-a-bezier-missing-some-equations-for-acce/5427#5427>. // Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, tolerance: f64, level: u8) -> f64 {
let lower = a0.distance(a2);
let upper = a0.distance(a1) + a1.distance(a2);
if upper - lower <= 2. * tolerance || level >= 8 {
return (lower + upper) / 2.;
}
// We will use an approximate approach where we split the curve into many subdivisions let b1 = 0.5 * (a0 + a1);
// and calculate the euclidean distance between the two endpoints of the subdivision let c1 = 0.5 * (a1 + a2);
let lookup_table = self.compute_lookup_table(Some(num_subdivisions.unwrap_or(DEFAULT_LENGTH_SUBDIVISIONS)), Some(TValueType::Parametric)); let b2 = 0.5 * (b1 + c1);
let approx_curve_length: f64 = lookup_table.windows(2).map(|points| (points[1] - points[0]).length()).sum(); recurse(a0, b1, b2, 0.5 * tolerance, level + 1) + recurse(b2, c1, a2, 0.5 * tolerance, level + 1)
}
recurse(self.start, handle, self.end, tolerance.unwrap_or_default(), 0)
}
BezierHandles::Cubic { handle_start, handle_end } => {
// Use Casteljau subdivision, noting that the length is more than the straight line distance from start to end but less than the straight line distance through the handles
fn recurse(a0: DVec2, a1: DVec2, a2: DVec2, a3: DVec2, tolerance: f64, level: u8) -> f64 {
let lower = a0.distance(a3);
let upper = a0.distance(a1) + a1.distance(a2) + a2.distance(a3);
if upper - lower <= 2. * tolerance || level >= 8 {
return (lower + upper) / 2.;
}
approx_curve_length let b1 = 0.5 * (a0 + a1);
let t0 = 0.5 * (a1 + a2);
let c1 = 0.5 * (a2 + a3);
let b2 = 0.5 * (b1 + t0);
let c2 = 0.5 * (t0 + c1);
let b3 = 0.5 * (b2 + c2);
recurse(a0, b1, b2, b3, 0.5 * tolerance, level + 1) + recurse(b3, c2, c1, a3, 0.5 * tolerance, level + 1)
}
recurse(self.start, handle_start, handle_end, self.end, tolerance.unwrap_or_default(), 0)
} }
} }
} }
/// Returns the parametric `t`-value that corresponds to the closest point on the curve to the provided point. /// Returns the parametric `t`-value that corresponds to the closest point on the curve to the provided point.
/// Uses a searching algorithm akin to binary search that can be customized using the optional [ProjectionOptions] struct.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/project/solo" title="Project Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#bezier/project/solo" title="Project Demo"></iframe>
pub fn project(&self, point: DVec2, options: Option<ProjectionOptions>) -> f64 { pub fn project(&self, point: DVec2) -> f64 {
let options = options.unwrap_or_default(); let sbasis = crate::symmetrical_basis::to_symmetrical_basis_pair(*self);
let ProjectionOptions { let derivative = sbasis.derivative();
lut_size, let dd = (sbasis - point).dot(&derivative);
convergence_epsilon, let roots = dd.roots();
convergence_limit,
iteration_limit,
} = options;
// TODO: Consider optimizations from precomputing useful values, or using the GPU let mut closest = 0.;
// First find the closest point from the results of a lookup table let mut min_dist_squared = self.evaluate(TValue::Parametric(0.)).distance_squared(point);
let lut = self.compute_lookup_table(Some(lut_size), Some(TValueType::Parametric));
let (minimum_position, minimum_distance) = utils::get_closest_point_in_lut(&lut, point);
// Get the t values to the left and right of the closest result in the lookup table for time in roots {
let lut_size_f64 = lut_size as f64; let distance = self.evaluate(TValue::Parametric(time)).distance_squared(point);
let minimum_position_f64 = minimum_position as f64; if distance < min_dist_squared {
let mut left_t = (minimum_position_f64 - 1.).max(0.) / lut_size_f64; closest = time;
let mut right_t = (minimum_position_f64 + 1.).min(lut_size_f64) / lut_size_f64; min_dist_squared = distance;
// Perform a finer search by finding closest t from 5 points between [left_t, right_t] inclusive
// Choose new left_t and right_t for a smaller range around the closest t and repeat the process
let mut final_t = left_t;
let mut distance;
// Increment minimum_distance to ensure that the distance < minimum_distance comparison will be true for at least one iteration
let mut new_minimum_distance = minimum_distance + 1.;
// Maintain the previous distance to identify convergence
let mut previous_distance;
// Counter to limit the number of iterations
let mut iteration_count = 0;
// Counter to identify how many iterations have had a similar result. Used for convergence test
let mut convergence_count = 0;
// Store calculated distances to minimize unnecessary recomputations
let mut distances: [f64; NUM_DISTANCES] = [
point.distance(lut[(minimum_position as i64 - 1).max(0) as usize]),
0.,
0.,
0.,
point.distance(lut[lut_size.min(minimum_position + 1)]),
];
while left_t <= right_t && convergence_count < convergence_limit && iteration_count < iteration_limit {
previous_distance = new_minimum_distance;
let step = (right_t - left_t) / (NUM_DISTANCES as f64 - 1.);
let mut iterator_t = left_t;
let mut target_index = 0;
// Iterate through first 4 points and will handle the right most point later
for (step_index, table_distance) in distances.iter_mut().enumerate().take(4) {
// Use previously computed distance for the left most point, and compute new values for the others
if step_index == 0 {
distance = *table_distance;
} else {
distance = point.distance(self.evaluate(TValue::Parametric(iterator_t)));
*table_distance = distance;
}
if distance < new_minimum_distance {
new_minimum_distance = distance;
target_index = step_index;
final_t = iterator_t
}
iterator_t += step;
}
// Check right most edge separately since step may not perfectly add up to it (floating point errors)
if distances[NUM_DISTANCES - 1] < new_minimum_distance {
new_minimum_distance = distances[NUM_DISTANCES - 1];
final_t = right_t;
}
// Update left_t and right_t to be the t values (final_t +/- step), while handling the edges (i.e. if final_t is 0, left_t will be 0 instead of -step)
// Ensure that the t values never exceed the [0, 1] range
left_t = (final_t - step).max(0.);
right_t = (final_t + step).min(1.);
// Re-use the corresponding computed distances (target_index is the index corresponding to final_t)
// Since target_index is a u_size, can't subtract one if it is zero
distances[0] = distances[if target_index == 0 { 0 } else { target_index - 1 }];
distances[NUM_DISTANCES - 1] = distances[(target_index + 1).min(NUM_DISTANCES - 1)];
iteration_count += 1;
// update count for consecutive iterations of similar minimum distances
if previous_distance - new_minimum_distance < convergence_epsilon {
convergence_count += 1;
} else {
convergence_count = 0;
} }
} }
final_t if self.evaluate(TValue::Parametric(1.)).distance_squared(point) < min_dist_squared {
closest = 1.;
}
closest
} }
} }
@ -259,11 +225,11 @@ mod tests {
#[test] #[test]
fn test_compute_lookup_table() { fn test_compute_lookup_table() {
let bezier1 = Bezier::from_quadratic_coordinates(10., 10., 30., 30., 50., 10.); let bezier1 = Bezier::from_quadratic_coordinates(10., 10., 30., 30., 50., 10.);
let lookup_table1 = bezier1.compute_lookup_table(Some(2), Some(TValueType::Parametric)); let lookup_table1 = bezier1.compute_lookup_table(Some(2), Some(TValueType::Parametric)).collect::<Vec<_>>();
assert_eq!(lookup_table1, vec![bezier1.start(), bezier1.evaluate(TValue::Parametric(0.5)), bezier1.end()]); assert_eq!(lookup_table1, vec![bezier1.start(), bezier1.evaluate(TValue::Parametric(0.5)), bezier1.end()]);
let bezier2 = Bezier::from_cubic_coordinates(10., 10., 30., 30., 70., 70., 90., 10.); let bezier2 = Bezier::from_cubic_coordinates(10., 10., 30., 30., 70., 70., 90., 10.);
let lookup_table2 = bezier2.compute_lookup_table(Some(4), Some(TValueType::Parametric)); let lookup_table2 = bezier2.compute_lookup_table(Some(4), Some(TValueType::Parametric)).collect::<Vec<_>>();
assert_eq!( assert_eq!(
lookup_table2, lookup_table2,
vec![ vec![
@ -296,10 +262,10 @@ mod tests {
#[test] #[test]
fn test_project() { fn test_project() {
let bezier1 = Bezier::from_cubic_coordinates(4., 4., 23., 45., 10., 30., 56., 90.); let bezier1 = Bezier::from_cubic_coordinates(4., 4., 23., 45., 10., 30., 56., 90.);
assert_eq!(bezier1.project(DVec2::ZERO, None), 0.); assert_eq!(bezier1.project(DVec2::ZERO), 0.);
assert_eq!(bezier1.project(DVec2::new(100., 100.), None), 1.); assert_eq!(bezier1.project(DVec2::new(100., 100.)), 1.);
let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.); let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.);
assert_eq!(bezier2.project(DVec2::new(100., 0.), None), 0.); assert_eq!(bezier2.project(DVec2::new(100., 0.)), 0.);
} }
} }

View File

@ -57,20 +57,12 @@ impl Bezier {
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment. /// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
pub fn handle_start(&self) -> Option<DVec2> { pub fn handle_start(&self) -> Option<DVec2> {
match self.handles { self.handles.start()
BezierHandles::Linear => None,
BezierHandles::Quadratic { handle } => Some(handle),
BezierHandles::Cubic { handle_start, .. } => Some(handle_start),
}
} }
/// Get the coordinates of the second handle point. This will return `None` for a quadratic segment. /// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
pub fn handle_end(&self) -> Option<DVec2> { pub fn handle_end(&self) -> Option<DVec2> {
match self.handles { self.handles.end()
BezierHandles::Linear { .. } => None,
BezierHandles::Quadratic { .. } => None,
BezierHandles::Cubic { handle_end, .. } => Some(handle_end),
}
} }
/// Get an iterator over the coordinates of all points in a vector. /// Get an iterator over the coordinates of all points in a vector.

View File

@ -14,7 +14,7 @@ use glam::DVec2;
use std::fmt::{Debug, Formatter, Result}; use std::fmt::{Debug, Formatter, Result};
/// Representation of the handle point(s) in a bezier segment. /// Representation of the handle point(s) in a bezier segment.
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BezierHandles { pub enum BezierHandles {
Linear, Linear,
@ -31,10 +31,55 @@ pub enum BezierHandles {
handle_end: DVec2, handle_end: DVec2,
}, },
} }
impl std::hash::Hash for BezierHandles {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
BezierHandles::Linear => {}
BezierHandles::Quadratic { handle } => handle.to_array().map(|v| v.to_bits()).hash(state),
BezierHandles::Cubic { handle_start, handle_end } => [handle_start, handle_end].map(|handle| handle.to_array().map(|v| v.to_bits())).hash(state),
}
}
}
impl BezierHandles { impl BezierHandles {
pub fn is_cubic(&self) -> bool { pub fn is_cubic(&self) -> bool {
matches!(self, Self::Cubic { .. }) matches!(self, Self::Cubic { .. })
} }
/// Get the coordinates of the bezier segment's first handle point. This represents the only handle in a quadratic segment.
pub fn start(&self) -> Option<DVec2> {
match *self {
BezierHandles::Cubic { handle_start, .. } | BezierHandles::Quadratic { handle: handle_start } => Some(handle_start),
_ => None,
}
}
/// Get the coordinates of the second handle point. This will return `None` for a quadratic segment.
pub fn end(&self) -> Option<DVec2> {
match *self {
BezierHandles::Cubic { handle_end, .. } => Some(handle_end),
_ => None,
}
}
/// Returns a Bezier curve that results from applying the transformation function to each handle point in the Bezier.
#[must_use]
pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Self {
match *self {
BezierHandles::Linear => Self::Linear,
BezierHandles::Quadratic { handle } => {
let handle = transformation_function(handle);
Self::Quadratic { handle }
}
BezierHandles::Cubic { handle_start, handle_end } => {
let handle_start = transformation_function(handle_start);
let handle_end = transformation_function(handle_end);
Self::Cubic { handle_start, handle_end }
}
}
}
} }
#[cfg(feature = "dyn-any")] #[cfg(feature = "dyn-any")]

View File

@ -1,30 +1,6 @@
use glam::DVec2; use glam::DVec2;
use std::fmt::{Debug, Formatter, Result}; use std::fmt::{Debug, Formatter, Result};
/// Struct to represent optional parameters that can be passed to the `project` function.
#[derive(Copy, Clone)]
pub struct ProjectionOptions {
/// Size of the lookup table for the initial passthrough. The default value is `20`.
pub lut_size: usize,
/// Difference used between floating point numbers to be considered as equal. The default value is `0.0001`
pub convergence_epsilon: f64,
/// Controls the number of iterations needed to consider that minimum distance to have converged. The default value is `3`.
pub convergence_limit: usize,
/// Controls the maximum total number of iterations to be used. The default value is `10`.
pub iteration_limit: usize,
}
impl Default for ProjectionOptions {
fn default() -> Self {
Self {
lut_size: 20,
convergence_epsilon: 1e-4,
convergence_limit: 3,
iteration_limit: 10,
}
}
}
/// Struct used to represent the different strategies for generating arc approximations. /// Struct used to represent the different strategies for generating arc approximations.
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub enum ArcStrategy { pub enum ArcStrategy {

View File

@ -105,19 +105,10 @@ impl Bezier {
/// Returns a Bezier curve that results from applying the transformation function to each point in the Bezier. /// Returns a Bezier curve that results from applying the transformation function to each point in the Bezier.
pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Bezier { pub fn apply_transformation(&self, transformation_function: impl Fn(DVec2) -> DVec2) -> Bezier {
let transformed_start = transformation_function(self.start); Self {
let transformed_end = transformation_function(self.end); start: transformation_function(self.start),
match self.handles { end: transformation_function(self.end),
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end), handles: self.handles.apply_transformation(transformation_function),
BezierHandles::Quadratic { handle } => {
let transformed_handle = transformation_function(handle);
Bezier::from_quadratic_dvec2(transformed_start, transformed_handle, transformed_end)
}
BezierHandles::Cubic { handle_start, handle_end } => {
let transformed_handle_start = transformation_function(handle_start);
let transformed_handle_end = transformation_function(handle_end);
Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end)
}
} }
} }
@ -315,12 +306,12 @@ impl Bezier {
BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end), BezierHandles::Linear => Bezier::from_linear_dvec2(transformed_start, transformed_end),
BezierHandles::Quadratic { handle: _ } => unreachable!(), BezierHandles::Quadratic { handle: _ } => unreachable!(),
BezierHandles::Cubic { handle_start, handle_end } => { BezierHandles::Cubic { handle_start, handle_end } => {
let handle_start_closest_t = intermediate.project(handle_start, None); let handle_start_closest_t = intermediate.project(handle_start);
let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance; let handle_start_scale_distance = (1. - handle_start_closest_t) * start_distance + handle_start_closest_t * end_distance;
let transformed_handle_start = let transformed_handle_start =
utils::scale_point_from_direction_vector(handle_start, intermediate.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance); utils::scale_point_from_direction_vector(handle_start, intermediate.normal(TValue::Parametric(handle_start_closest_t)), false, handle_start_scale_distance);
let handle_end_closest_t = intermediate.project(handle_start, None); let handle_end_closest_t = intermediate.project(handle_start);
let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance; let handle_end_scale_distance = (1. - handle_end_closest_t) * start_distance + handle_end_closest_t * end_distance;
let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, intermediate.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_distance); let transformed_handle_end = utils::scale_point_from_direction_vector(handle_end, intermediate.normal(TValue::Parametric(handle_end_closest_t)), false, handle_end_scale_distance);
Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end) Bezier::from_cubic_dvec2(transformed_start, transformed_handle_start, transformed_handle_end, transformed_end)
@ -810,7 +801,7 @@ mod tests {
.iter() .iter()
.map(|t| { .map(|t| {
let offset_point = offset_segment.evaluate(TValue::Parametric(*t)); let offset_point = offset_segment.evaluate(TValue::Parametric(*t));
let closest_point_t = bezier.project(offset_point, None); let closest_point_t = bezier.project(offset_point);
let closest_point = bezier.evaluate(TValue::Parametric(closest_point_t)); let closest_point = bezier.evaluate(TValue::Parametric(closest_point_t));
let actual_distance = offset_point.distance(closest_point); let actual_distance = offset_point.distance(closest_point);

View File

@ -4,8 +4,6 @@
pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3; pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
/// A stricter constant used to determine if `f64`s are equivalent. /// A stricter constant used to determine if `f64`s are equivalent.
pub const STRICT_MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-6; pub const STRICT_MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-6;
/// Number of distances used in search algorithm for `project`.
pub const NUM_DISTANCES: usize = 5;
/// Maximum allowed angle that the normal of the `start` or `end` point can make with the normal of the corresponding handle for a curve to be considered scalable/simple. /// Maximum allowed angle that the normal of the `start` or `end` point can make with the normal of the corresponding handle for a curve to be considered scalable/simple.
pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.; pub const SCALABLE_CURVE_MAX_ENDPOINT_NORMAL_ANGLE: f64 = std::f64::consts::PI / 3.;
/// Minimum allowable separation between adjacent `t` values when calculating curve intersections /// Minimum allowable separation between adjacent `t` values when calculating curve intersections
@ -19,8 +17,6 @@ pub const DEFAULT_EUCLIDEAN_ERROR_BOUND: f64 = 0.001;
pub const DEFAULT_T_VALUE: f64 = 0.5; pub const DEFAULT_T_VALUE: f64 = 0.5;
/// Default LUT step size in `compute_lookup_table` function. /// Default LUT step size in `compute_lookup_table` function.
pub const DEFAULT_LUT_STEP_SIZE: usize = 10; pub const DEFAULT_LUT_STEP_SIZE: usize = 10;
/// Default number of subdivisions used in `length` calculation.
pub const DEFAULT_LENGTH_SUBDIVISIONS: usize = 1000;
/// Default step size for `reduce` function. /// Default step size for `reduce` function.
pub const DEFAULT_REDUCE_STEP_SIZE: f64 = 0.01; pub const DEFAULT_REDUCE_STEP_SIZE: f64 = 0.01;

View File

@ -8,6 +8,7 @@ use std::fmt::Write;
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> { impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Create a new `Subpath` using a list of [ManipulatorGroup]s. /// Create a new `Subpath` using a list of [ManipulatorGroup]s.
/// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed. /// A `Subpath` with less than 2 [ManipulatorGroup]s may not be closed.
#[track_caller]
pub fn new(manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>>, closed: bool) -> Self { pub fn new(manipulator_groups: Vec<ManipulatorGroup<ManipulatorGroupId>>, closed: bool) -> Self {
assert!(!closed || manipulator_groups.len() > 1, "A closed Subpath must contain more than 1 ManipulatorGroup."); assert!(!closed || manipulator_groups.len() > 1, "A closed Subpath must contain more than 1 ManipulatorGroup.");
Self { manipulator_groups, closed } Self { manipulator_groups, closed }
@ -276,61 +277,71 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
// Number of points = number of points to find handles for // Number of points = number of points to find handles for
let len_points = points.len(); let len_points = points.len();
// matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html) let out_handles = solve_spline_first_handle(&points);
// because the 'a' coefficients are all 1 they need not be stored
// this algorithm does a variation of the above algorithm.
// Instead of using the traditional cubic: a + bt + ct^2 + dt^3, we use the bezier cubic.
let mut b = vec![DVec2::new(4., 4.); len_points];
b[0] = DVec2::new(2., 2.);
b[len_points - 1] = DVec2::new(2., 2.);
let mut c = vec![DVec2::new(1., 1.); len_points];
// 'd' is the the second point in a cubic bezier, which is what we solve for
let mut d = vec![DVec2::ZERO; len_points];
d[0] = DVec2::new(2. * points[1].x + points[0].x, 2. * points[1].y + points[0].y);
d[len_points - 1] = DVec2::new(3. * points[len_points - 1].x, 3. * points[len_points - 1].y);
for idx in 1..(len_points - 1) {
d[idx] = DVec2::new(4. * points[idx].x + 2. * points[idx + 1].x, 4. * points[idx].y + 2. * points[idx + 1].y);
}
// Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
// do row operations to eliminate `a` coefficients
c[0] /= -b[0];
d[0] /= -b[0];
#[allow(clippy::assign_op_pattern)]
for i in 1..len_points {
b[i] += c[i - 1];
// for some reason the below line makes the borrow checker mad
//d[i] += d[i-1]
d[i] = d[i] + d[i - 1];
c[i] /= -b[i];
d[i] /= -b[i];
}
// at this point b[i] == -a[i + 1], a[i] == 0,
// do row operations to eliminate 'c' coefficients and solve
d[len_points - 1] *= -1.;
#[allow(clippy::assign_op_pattern)]
for i in (0..len_points - 1).rev() {
d[i] = d[i] - (c[i] * d[i + 1]);
d[i] *= -1.; //d[i] /= b[i]
}
let mut subpath = Subpath::new(Vec::new(), false); let mut subpath = Subpath::new(Vec::new(), false);
// given the second point in the n'th cubic bezier, the third point is given by 2 * points[n+1] - b[n+1]. // given the second point in the n'th cubic bezier, the third point is given by 2 * points[n+1] - b[n+1].
// to find 'handle1_pos' for the n'th point we need the n-1 cubic bezier // to find 'handle1_pos' for the n'th point we need the n-1 cubic bezier
subpath.manipulator_groups.push(ManipulatorGroup::new(points[0], None, Some(d[0]))); subpath.manipulator_groups.push(ManipulatorGroup::new(points[0], None, Some(out_handles[0])));
for i in 1..len_points - 1 { for i in 1..len_points - 1 {
subpath.manipulator_groups.push(ManipulatorGroup::new(points[i], Some(2. * points[i] - d[i]), Some(d[i]))); subpath
.manipulator_groups
.push(ManipulatorGroup::new(points[i], Some(2. * points[i] - out_handles[i]), Some(out_handles[i])));
} }
subpath subpath
.manipulator_groups .manipulator_groups
.push(ManipulatorGroup::new(points[len_points - 1], Some(2. * points[len_points - 1] - d[len_points - 1]), None)); .push(ManipulatorGroup::new(points[len_points - 1], Some(2. * points[len_points - 1] - out_handles[len_points - 1]), None));
subpath subpath
} }
} }
pub fn solve_spline_first_handle(points: &[DVec2]) -> Vec<DVec2> {
let len_points = points.len();
// matrix coefficients a, b and c (see https://mathworld.wolfram.com/CubicSpline.html)
// because the 'a' coefficients are all 1 they need not be stored
// this algorithm does a variation of the above algorithm.
// Instead of using the traditional cubic: a + bt + ct^2 + dt^3, we use the bezier cubic.
let mut b = vec![DVec2::new(4., 4.); len_points];
b[0] = DVec2::new(2., 2.);
b[len_points - 1] = DVec2::new(2., 2.);
let mut c = vec![DVec2::new(1., 1.); len_points];
// 'd' is the the second point in a cubic bezier, which is what we solve for
let mut d = vec![DVec2::ZERO; len_points];
d[0] = DVec2::new(2. * points[1].x + points[0].x, 2. * points[1].y + points[0].y);
d[len_points - 1] = DVec2::new(3. * points[len_points - 1].x, 3. * points[len_points - 1].y);
for idx in 1..(len_points - 1) {
d[idx] = DVec2::new(4. * points[idx].x + 2. * points[idx + 1].x, 4. * points[idx].y + 2. * points[idx + 1].y);
}
// Solve with Thomas algorithm (see https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm)
// do row operations to eliminate `a` coefficients
c[0] /= -b[0];
d[0] /= -b[0];
#[allow(clippy::assign_op_pattern)]
for i in 1..len_points {
b[i] += c[i - 1];
// for some reason the below line makes the borrow checker mad
//d[i] += d[i-1]
d[i] = d[i] + d[i - 1];
c[i] /= -b[i];
d[i] /= -b[i];
}
// at this point b[i] == -a[i + 1], a[i] == 0,
// do row operations to eliminate 'c' coefficients and solve
d[len_points - 1] *= -1.;
#[allow(clippy::assign_op_pattern)]
for i in (0..len_points - 1).rev() {
d[i] = d[i] - (c[i] * d[i + 1]);
d[i] *= -1.; //d[i] /= b[i]
}
d
}

View File

@ -1,7 +1,6 @@
use super::*; use super::*;
use crate::consts::{DEFAULT_EUCLIDEAN_ERROR_BOUND, DEFAULT_LUT_STEP_SIZE}; use crate::consts::{DEFAULT_EUCLIDEAN_ERROR_BOUND, DEFAULT_LUT_STEP_SIZE};
use crate::utils::{SubpathTValue, TValue, TValueType}; use crate::utils::{SubpathTValue, TValue, TValueType};
use crate::ProjectionOptions;
use glam::DVec2; use glam::DVec2;
/// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`. /// Functionality relating to looking up properties of the `Subpath` or points along the `Subpath`.
@ -25,10 +24,10 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
} }
/// Return the sum of the approximation of the length of each `Bezier` curve along the `Subpath`. /// Return the sum of the approximation of the length of each `Bezier` curve along the `Subpath`.
/// - `num_subdivisions` - Number of subdivisions used to approximate the curve. The default value is `1000`. /// - `tolerance` - Tolerance used to approximate the curve.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/length/solo" title="Length Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/length/solo" title="Length Demo"></iframe>
pub fn length(&self, num_subdivisions: Option<usize>) -> f64 { pub fn length(&self, tolerance: Option<f64>) -> f64 {
self.iter().map(|bezier| bezier.length(num_subdivisions)).sum() self.iter().map(|bezier| bezier.length(tolerance)).sum()
} }
/// Converts from a subpath (composed of multiple segments) to a point along a certain segment represented. /// Converts from a subpath (composed of multiple segments) to a point along a certain segment represented.
@ -98,9 +97,8 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
} }
/// Returns the segment index and `t` value that corresponds to the closest point on the curve to the provided point. /// Returns the segment index and `t` value that corresponds to the closest point on the curve to the provided point.
/// Uses a searching algorithm akin to binary search that can be customized using the [ProjectionOptions] structure.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/project/solo" title="Project Demo"></iframe> /// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/project/solo" title="Project Demo"></iframe>
pub fn project(&self, point: DVec2, options: Option<ProjectionOptions>) -> Option<(usize, f64)> { pub fn project(&self, point: DVec2) -> Option<(usize, f64)> {
if self.is_empty() { if self.is_empty() {
return None; return None;
} }
@ -109,7 +107,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
let (index, (_, project_t)) = self let (index, (_, project_t)) = self
.iter() .iter()
.map(|bezier| { .map(|bezier| {
let project_t = bezier.project(point, options); let project_t = bezier.project(point);
(bezier.evaluate(TValue::Parametric(project_t)).distance(point), project_t) (bezier.evaluate(TValue::Parametric(project_t)).distance(point), project_t)
}) })
.enumerate() .enumerate()

View File

@ -4,6 +4,7 @@ mod manipulators;
mod solvers; mod solvers;
mod structs; mod structs;
mod transform; mod transform;
pub use core::*;
pub use structs::*; pub use structs::*;
use crate::Bezier; use crate::Bezier;

View File

@ -296,8 +296,8 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
let start_tangent = second_bezier.non_normalized_tangent(0.); let start_tangent = second_bezier.non_normalized_tangent(0.);
// Compute an average unit vector, weighing the segments by a rough estimation of their relative size. // Compute an average unit vector, weighing the segments by a rough estimation of their relative size.
let segment1_len = first_bezier.length(Some(5)); let segment1_len = first_bezier.length(None);
let segment2_len = second_bezier.length(Some(5)); let segment2_len = second_bezier.length(None);
let average_unit_tangent = (end_tangent.normalize() * segment1_len + start_tangent.normalize() * segment2_len) / (segment1_len + segment2_len); let average_unit_tangent = (end_tangent.normalize() * segment1_len + start_tangent.normalize() * segment2_len) / (segment1_len + segment2_len);
// Adjust start and end handles to fit the average tangent // Adjust start and end handles to fit the average tangent

View File

@ -90,11 +90,6 @@ pub fn compute_abc_for_cubic_through_points(start_point: DVec2, point_on_curve:
compute_abc_through_points(start_point, point_on_curve, end_point, t_cubed, cubed_one_minus_t) compute_abc_through_points(start_point, point_on_curve, end_point, t_cubed, cubed_one_minus_t)
} }
/// Return the index and the value of the closest point in the LUT compared to the provided point.
pub fn get_closest_point_in_lut(lut: &[DVec2], point: DVec2) -> (usize, f64) {
lut.iter().enumerate().map(|(i, p)| (i, point.distance_squared(*p))).min_by(|x, y| (x.1).total_cmp(&(y.1))).unwrap()
}
/// Find the roots of the linear equation `ax + b`. /// Find the roots of the linear equation `ax + b`.
pub fn solve_linear(a: f64, b: f64) -> [Option<f64>; 3] { pub fn solve_linear(a: f64, b: f64) -> [Option<f64>; 3] {
// There exist roots when `a` is not 0 // There exist roots when `a` is not 0

View File

@ -259,10 +259,13 @@ impl_type!(
); );
#[cfg(feature = "std")] #[cfg(feature = "std")]
use std::sync::*; use std::{
collections::{HashMap, HashSet},
sync::*,
};
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl_type!(Once, Mutex<T>, RwLock<T>); impl_type!(Once, Mutex<T>, RwLock<T>, HashSet<T>, HashMap<K, V>);
#[cfg(feature = "rc")] #[cfg(feature = "rc")]
use std::rc::Rc; use std::rc::Rc;

View File

@ -57,10 +57,11 @@ num-derive = { workspace = true }
num-traits = { workspace = true, default-features = false, features = ["i128"] } num-traits = { workspace = true, default-features = false, features = ["i128"] }
wasm-bindgen = { workspace = true, optional = true } wasm-bindgen = { workspace = true, optional = true }
js-sys = { workspace = true, optional = true } js-sys = { workspace = true, optional = true }
web-sys = { workspace = true, optional = true, features = [
"HtmlCanvasElement",
] }
usvg = { workspace = true } usvg = { workspace = true }
rand = { workspace = true, default-features = false, features = ["std_rng"] } rand = { workspace = true, default-features = false, features = ["std_rng"] }
[dependencies.web-sys] [dev-dependencies]
workspace = true tokio = { workspace = true, features = ["rt", "macros"] }
optional = true
features = ["HtmlCanvasElement"]

View File

@ -263,7 +263,7 @@ impl GraphicElement {
let mut builder = PathBuilder::new(); let mut builder = PathBuilder::new();
let transform = to_transform(vector_data.transform); let transform = to_transform(vector_data.transform);
for subpath in vector_data.subpaths.iter() { for subpath in vector_data.stroke_bezier_paths() {
let start = vector_data.transform.transform_point2(subpath[0].anchor); let start = vector_data.transform.transform_point2(subpath[0].anchor);
builder.move_to(start.x as f32, start.y as f32); builder.move_to(start.x as f32, start.y as f32);
for bezier in subpath.iter() { for bezier in subpath.iter() {

View File

@ -2,7 +2,8 @@ mod quad;
use crate::raster::{BlendMode, Image, ImageFrame}; use crate::raster::{BlendMode, Image, ImageFrame};
use crate::transform::Transform; use crate::transform::Transform;
use crate::uuid::{generate_uuid, ManipulatorGroupId}; use crate::uuid::generate_uuid;
use crate::vector::PointId;
use crate::{vector::VectorData, Artboard, Color, GraphicElement, GraphicGroup}; use crate::{vector::VectorData, Artboard, Color, GraphicElement, GraphicGroup};
pub use quad::Quad; pub use quad::Quad;
@ -14,7 +15,7 @@ use glam::{DAffine2, DVec2};
/// Represents a clickable target for the layer /// Represents a clickable target for the layer
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ClickTarget { pub struct ClickTarget {
pub subpath: bezier_rs::Subpath<ManipulatorGroupId>, pub subpath: bezier_rs::Subpath<PointId>,
pub stroke_width: f64, pub stroke_width: f64,
} }
@ -296,7 +297,10 @@ impl GraphicElementRendered for VectorData {
let transformed_bounds = self.bounding_box_with_transform(multiplied_transform).unwrap_or_default(); let transformed_bounds = self.bounding_box_with_transform(multiplied_transform).unwrap_or_default();
let mut path = String::new(); let mut path = String::new();
for subpath in &self.subpaths { for (_, subpath) in self.region_bezier_paths() {
let _ = subpath.subpath_to_svg(&mut path, multiplied_transform);
}
for subpath in self.stroke_bezier_paths() {
let _ = subpath.subpath_to_svg(&mut path, multiplied_transform); let _ = subpath.subpath_to_svg(&mut path, multiplied_transform);
} }
@ -326,11 +330,8 @@ impl GraphicElementRendered for VectorData {
fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) { fn add_click_targets(&self, click_targets: &mut Vec<ClickTarget>) {
let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight); let stroke_width = self.style.stroke().as_ref().map_or(0., crate::vector::style::Stroke::weight);
let update_closed = |mut subpath: bezier_rs::Subpath<ManipulatorGroupId>| { click_targets.extend(self.region_bezier_paths().map(|(_, subpath)| ClickTarget { stroke_width, subpath }));
subpath.set_closed(self.style.fill().is_some()); click_targets.extend(self.stroke_bezier_paths().map(|subpath| ClickTarget { stroke_width, subpath }));
subpath
};
click_targets.extend(self.subpaths.iter().cloned().map(update_closed).map(|subpath| ClickTarget { stroke_width, subpath }))
} }
fn to_usvg_node(&self) -> usvg::Node { fn to_usvg_node(&self) -> usvg::Node {
@ -340,7 +341,7 @@ impl GraphicElementRendered for VectorData {
let vector_data = self; let vector_data = self;
let transform = to_transform(vector_data.transform); let transform = to_transform(vector_data.transform);
for subpath in vector_data.subpaths.iter() { for subpath in vector_data.stroke_bezier_paths() {
let start = vector_data.transform.transform_point2(subpath[0].anchor); let start = vector_data.transform.transform_point2(subpath[0].anchor);
builder.move_to(start.x as f32, start.y as f32); builder.move_to(start.x as f32, start.y as f32);
for bezier in subpath.iter() { for bezier in subpath.iter() {

View File

@ -89,4 +89,14 @@ impl ManipulatorGroupId {
self.0 += 1; self.0 += 1;
Self(old) Self(old)
} }
pub(crate) fn inner(self) -> u64 {
self.0
}
}
impl From<crate::vector::PointId> for ManipulatorGroupId {
fn from(value: crate::vector::PointId) -> Self {
Self(value.inner())
}
} }

View File

@ -43,7 +43,7 @@ fn square_generator(_input: (), size_x: f64, size_y: f64) -> VectorData {
let corner1 = -size / 2.; let corner1 = -size / 2.;
let corner2 = size / 2.; let corner2 = size / 2.;
super::VectorData::from_subpaths(vec![Subpath::new_rect(corner1, corner2)]) super::VectorData::from_subpath(Subpath::new_rect(corner1, corner2))
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -83,7 +83,7 @@ pub struct LineGenerator<Pos1, Pos2> {
#[node_macro::node_fn(LineGenerator)] #[node_macro::node_fn(LineGenerator)]
fn line_generator(_input: (), pos_1: DVec2, pos_2: DVec2) -> VectorData { fn line_generator(_input: (), pos_1: DVec2, pos_2: DVec2) -> VectorData {
super::VectorData::from_subpaths(vec![Subpath::new_line(pos_1, pos_2)]) super::VectorData::from_subpath(Subpath::new_line(pos_1, pos_2))
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -93,7 +93,7 @@ pub struct SplineGenerator<Positions> {
#[node_macro::node_fn(SplineGenerator)] #[node_macro::node_fn(SplineGenerator)]
fn spline_generator(_input: (), positions: Vec<DVec2>) -> VectorData { fn spline_generator(_input: (), positions: Vec<DVec2>) -> VectorData {
super::VectorData::from_subpaths(vec![Subpath::new_cubic_spline(positions)]) super::VectorData::from_subpath(Subpath::new_cubic_spline(positions))
} }
// TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node // TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node

View File

@ -1,6 +1,9 @@
mod attributes;
use super::style::{PathStyle, Stroke}; use super::style::{PathStyle, Stroke};
use crate::Color; use crate::Color;
use crate::{uuid::ManipulatorGroupId, AlphaBlending}; use crate::{uuid::ManipulatorGroupId, AlphaBlending};
pub use attributes::*;
use bezier_rs::ManipulatorGroup; use bezier_rs::ManipulatorGroup;
use dyn_any::{DynAny, StaticType}; use dyn_any::{DynAny, StaticType};
@ -12,18 +15,23 @@ use glam::{DAffine2, DVec2};
#[derive(Clone, Debug, PartialEq, DynAny)] #[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VectorData { pub struct VectorData {
pub subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>,
pub transform: DAffine2, pub transform: DAffine2,
pub style: PathStyle, pub style: PathStyle,
pub alpha_blending: AlphaBlending, pub alpha_blending: AlphaBlending,
/// A list of all manipulator groups (referenced in `subpaths`) that have smooth handles (where their handles are colinear, or locked to 180° angles from one another) /// A list of all manipulator groups (referenced in `subpaths`) that have smooth handles (where their handles are colinear, or locked to 180° angles from one another)
/// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have subpath and mirror angle inputs"` to find it). /// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have subpath and mirror angle inputs"` to find it).
pub mirror_angle: Vec<ManipulatorGroupId>, pub mirror_angle: Vec<ManipulatorGroupId>,
pub point_domain: PointDomain,
pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain,
} }
impl core::hash::Hash for VectorData { impl core::hash::Hash for VectorData {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) { fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.subpaths.hash(state); self.point_domain.hash(state);
self.segment_domain.hash(state);
self.region_domain.hash(state);
self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state)); self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
self.style.hash(state); self.style.hash(state);
self.alpha_blending.hash(state); self.alpha_blending.hash(state);
@ -35,31 +43,63 @@ impl VectorData {
/// An empty subpath with no data, an identity transform, and a black fill. /// An empty subpath with no data, an identity transform, and a black fill.
pub const fn empty() -> Self { pub const fn empty() -> Self {
Self { Self {
subpaths: Vec::new(),
transform: DAffine2::IDENTITY, transform: DAffine2::IDENTITY,
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None), style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
alpha_blending: AlphaBlending::new(), alpha_blending: AlphaBlending::new(),
mirror_angle: Vec::new(), mirror_angle: Vec::new(),
point_domain: PointDomain::new(),
segment_domain: SegmentDomain::new(),
region_domain: RegionDomain::new(),
} }
} }
/// Iterator over the manipulator groups of the subpaths
pub fn manipulator_groups(&self) -> impl Iterator<Item = &ManipulatorGroup<ManipulatorGroupId>> + DoubleEndedIterator {
self.subpaths.iter().flat_map(|subpath| subpath.manipulator_groups())
}
pub fn manipulator_from_id(&self, id: ManipulatorGroupId) -> Option<&ManipulatorGroup<ManipulatorGroupId>> {
self.subpaths.iter().find_map(|subpath| subpath.manipulator_from_id(id))
}
/// Construct some new vector data from a single subpath with an identity transform and black fill. /// Construct some new vector data from a single subpath with an identity transform and black fill.
pub fn from_subpath(subpath: bezier_rs::Subpath<ManipulatorGroupId>) -> Self { pub fn from_subpath(subpath: bezier_rs::Subpath<ManipulatorGroupId>) -> Self {
Self::from_subpaths(vec![subpath]) Self::from_subpaths([subpath])
}
/// Push a subpath to the vector data
pub fn append_subpath<Id: bezier_rs::Identifier + Into<PointId> + Copy>(&mut self, subpath: bezier_rs::Subpath<Id>) {
for point in subpath.manipulator_groups() {
self.point_domain.push(point.id.into(), point.anchor);
}
let handles = |a: &ManipulatorGroup<_>, b: &ManipulatorGroup<_>| match (a.out_handle, b.in_handle) {
(None, None) => bezier_rs::BezierHandles::Linear,
(Some(handle), None) | (None, Some(handle)) => bezier_rs::BezierHandles::Quadratic { handle },
(Some(handle_start), Some(handle_end)) => bezier_rs::BezierHandles::Cubic { handle_start, handle_end },
};
let [mut first_seg, mut last_seg] = [None, None];
for pair in subpath.manipulator_groups().windows(2) {
let id = SegmentId::generate();
first_seg = Some(first_seg.unwrap_or(id));
last_seg = Some(id);
self.segment_domain.push(id, pair[0].id.into(), pair[1].id.into(), handles(&pair[0], &pair[1]), StrokeId::generate());
}
if subpath.closed() {
if let (Some(last), Some(first)) = (subpath.manipulator_groups().last(), subpath.manipulator_groups().first()) {
let id = SegmentId::generate();
first_seg = Some(first_seg.unwrap_or(id));
last_seg = Some(id);
self.segment_domain.push(id, last.id.into(), first.id.into(), handles(last, first), StrokeId::generate());
}
if let [Some(first_seg), Some(last_seg)] = [first_seg, last_seg] {
self.region_domain.push(RegionId::generate(), first_seg..=last_seg, FillId::generate());
}
}
} }
/// Construct some new vector data from subpaths with an identity transform and black fill. /// Construct some new vector data from subpaths with an identity transform and black fill.
pub fn from_subpaths(subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>) -> Self { pub fn from_subpaths(subpaths: impl IntoIterator<Item = bezier_rs::Subpath<ManipulatorGroupId>>) -> Self {
super::VectorData { subpaths, ..Self::empty() } let mut vector_data = Self::empty();
for subpath in subpaths.into_iter() {
vector_data.append_subpath(subpath);
}
vector_data
} }
/// Compute the bounding boxes of the subpaths without any transform /// Compute the bounding boxes of the subpaths without any transform
@ -69,9 +109,8 @@ impl VectorData {
/// Compute the bounding boxes of the subpaths with the specified transform /// Compute the bounding boxes of the subpaths with the specified transform
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> { pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.subpaths self.segment_bezier_iter()
.iter() .map(|(_, bezier, _, _)| bezier.apply_transformation(|point| transform.transform_point2(point)).bounding_box())
.filter_map(|subpath| subpath.bounding_box_with_transform(transform))
.reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])]) .reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])])
} }

View File

@ -0,0 +1,376 @@
use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
use std::collections::HashMap;
macro_rules! create_ids {
($($id:ident),*) => {
$(
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// A strongly typed ID
pub struct $id(u64);
impl $id {
/// Generate a new random id
pub fn generate() -> Self {
Self(crate::uuid::generate_uuid())
}
pub fn inner(self) -> u64 {
self.0
}
}
)*
};
}
create_ids! { PointId, SegmentId, RegionId, StrokeId, FillId }
#[derive(Clone, Debug, Default, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// Stores data which is per-point. Each point is merely a position and can be used in a point cloud or to for a bézier path. In future this will be extendable at runtime with custom attributes.
pub struct PointDomain {
id: Vec<PointId>,
positions: Vec<DVec2>,
}
impl core::hash::Hash for PointDomain {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.positions.iter().for_each(|pos| pos.to_array().map(|v| v.to_bits()).hash(state));
}
}
impl PointDomain {
pub const fn new() -> Self {
Self {
id: Vec::new(),
positions: Vec::new(),
}
}
pub fn clear(&mut self) {
self.id.clear();
self.positions.clear();
}
pub fn push(&mut self, id: PointId, position: DVec2) {
self.id.push(id);
self.positions.push(position);
}
pub fn positions(&self) -> &[DVec2] {
&self.positions
}
pub fn ids(&self) -> &[PointId] {
&self.id
}
pub fn pos_from_id(&self, id: PointId) -> Option<DVec2> {
let pos = self.resolve_id(id).map(|index| self.positions[index]);
if pos.is_none() {
warn!("Resolving pos of invalid id");
}
pos
}
fn resolve_id(&self, id: PointId) -> Option<usize> {
self.id.iter().position(|&check_id| check_id == id)
}
fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
self.id.extend(other.id.iter().map(|id| *id_map.point_map.get(id).unwrap_or(id)));
self.positions.extend(other.positions.iter().map(|&pos| transform.transform_point2(pos)));
}
fn transform(&mut self, transform: DAffine2) {
for pos in &mut self.positions {
*pos = transform.transform_point2(*pos);
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// Stores data which is per-segment. A segment is a bézier curve between two end points with a stroke. In future this will be extendable at runtime with custom attributes.
pub struct SegmentDomain {
ids: Vec<SegmentId>,
start_point: Vec<PointId>,
end_point: Vec<PointId>,
// TODO: Also store handle points as `PointId`s rather than Bezier-rs's internal `DVec2`s
handles: Vec<bezier_rs::BezierHandles>,
stroke: Vec<StrokeId>,
}
impl SegmentDomain {
pub const fn new() -> Self {
Self {
ids: Vec::new(),
start_point: Vec::new(),
end_point: Vec::new(),
handles: Vec::new(),
stroke: Vec::new(),
}
}
pub fn clear(&mut self) {
self.ids.clear();
self.start_point.clear();
self.end_point.clear();
self.handles.clear();
self.stroke.clear();
}
pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: bezier_rs::BezierHandles, stroke: StrokeId) {
self.ids.push(id);
self.start_point.push(start);
self.end_point.push(end);
self.handles.push(handles);
self.stroke.push(stroke);
}
fn resolve_id(&self, id: SegmentId) -> Option<usize> {
self.ids.iter().position(|&check_id| check_id == id)
}
fn resolve_range(&self, range: &core::ops::RangeInclusive<SegmentId>) -> Option<core::ops::RangeInclusive<usize>> {
match (self.resolve_id(*range.start()), self.resolve_id(*range.end())) {
(Some(start), Some(end)) => Some(start..=end),
_ => {
warn!("Resolving range with invalid id");
None
}
}
}
fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) {
self.ids.extend(other.ids.iter().map(|id| *id_map.segment_map.get(id).unwrap_or(id)));
self.start_point.extend(other.start_point.iter().map(|id| *id_map.point_map.get(id).unwrap_or(id)));
self.end_point.extend(other.end_point.iter().map(|id| *id_map.point_map.get(id).unwrap_or(id)));
self.handles.extend(other.handles.iter().map(|handles| handles.apply_transformation(|p| transform.transform_point2(p))));
self.stroke.extend(&other.stroke);
}
fn transform(&mut self, transform: DAffine2) {
for handles in &mut self.handles {
*handles = handles.apply_transformation(|p| transform.transform_point2(p));
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// Stores data which is per-region. A region is an enclosed area composed of a range of segments from the [`SegmentDomain`] that can be given a fill. In future this will be extendable at runtime with custom attributes.
pub struct RegionDomain {
ids: Vec<RegionId>,
segment_range: Vec<core::ops::RangeInclusive<SegmentId>>,
fill: Vec<FillId>,
}
impl RegionDomain {
pub const fn new() -> Self {
Self {
ids: Vec::new(),
segment_range: Vec::new(),
fill: Vec::new(),
}
}
pub fn clear(&mut self) {
self.ids.clear();
self.segment_range.clear();
self.fill.clear();
}
pub fn push(&mut self, id: RegionId, segment_range: core::ops::RangeInclusive<SegmentId>, fill: FillId) {
self.ids.push(id);
self.segment_range.push(segment_range);
self.fill.push(fill);
}
fn resolve_id(&self, id: RegionId) -> Option<usize> {
self.ids.iter().position(|&check_id| check_id == id)
}
fn concat(&mut self, other: &Self, _transform: DAffine2, id_map: &IdMap) {
self.ids.extend(other.ids.iter().map(|id| *id_map.region_map.get(id).unwrap_or(id)));
self.segment_range.extend(
other
.segment_range
.iter()
.map(|range| *id_map.segment_map.get(range.start()).unwrap_or(range.start())..=*id_map.segment_map.get(range.end()).unwrap_or(range.end())),
);
self.fill.extend(&other.fill);
}
}
impl super::VectorData {
/// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles. Returns [`None`] if either ID is invalid.
fn segment_to_bezier(&self, start: PointId, end: PointId, handles: bezier_rs::BezierHandles) -> Option<bezier_rs::Bezier> {
let start = self.point_domain.pos_from_id(start)?;
let end = self.point_domain.pos_from_id(end)?;
Some(bezier_rs::Bezier { start, end, handles })
}
/// Tries to convert a segment with the specified id to a [`bezier_rs::Bezier`], returning None if the id is invalid.
pub fn segment_from_id(&self, id: SegmentId) -> Option<bezier_rs::Bezier> {
let index = self.segment_domain.resolve_id(id)?;
self.segment_to_bezier(self.segment_domain.start_point[index], self.segment_domain.end_point[index], self.segment_domain.handles[index])
}
/// Iterator over all of the [`bezier_rs::Bezier`] following the order that they are stored in the segment domain, skipping invalid segments.
pub fn segment_bezier_iter(&self) -> impl Iterator<Item = (SegmentId, bezier_rs::Bezier, PointId, PointId)> + '_ {
let to_bezier = |(((&handles, &id), &start), &end)| self.segment_to_bezier(start, end, handles).map(|bezier| (id, bezier, start, end));
self.segment_domain
.handles
.iter()
.zip(&self.segment_domain.ids)
.zip(&self.segment_domain.start_point)
.zip(&self.segment_domain.end_point)
.filter_map(to_bezier)
}
/// Construct a [`bezier_rs::Bezier`] curve from an iterator of segments with (handles, start point, end point). Returns None if any ids are invalid or if the semgents are not continuous.
fn subpath_from_segments(&self, segments: impl Iterator<Item = (bezier_rs::BezierHandles, PointId, PointId)>) -> Option<bezier_rs::Subpath<PointId>> {
let mut first_point = None;
let mut groups = Vec::new();
let mut last: Option<(PointId, bezier_rs::BezierHandles)> = None;
let end_point = |last: Option<(PointId, bezier_rs::BezierHandles)>, next: Option<PointId>, groups: &mut Vec<_>| {
if let Some((disconnected_previous, previous_handle)) = last.filter(|(end, _)| !next.is_some_and(|next| next == *end)) {
groups.push(bezier_rs::ManipulatorGroup {
anchor: self.point_domain.pos_from_id(disconnected_previous)?,
in_handle: previous_handle.end(),
out_handle: None,
id: disconnected_previous,
});
}
Some(())
};
for (handle, start, end) in segments {
if last.is_some_and(|(previous_end, _)| previous_end != start) {
warn!("subpath_from_segments that were not continuous");
return None;
}
first_point = Some(first_point.unwrap_or(start));
end_point(last, Some(start), &mut groups)?;
groups.push(bezier_rs::ManipulatorGroup {
anchor: self.point_domain.pos_from_id(start)?,
in_handle: last.and_then(|(_, handle)| handle.end()),
out_handle: handle.start(),
id: start,
});
last = Some((end, handle));
}
end_point(last, None, &mut groups)?;
let closed = groups.len() > 1 && last.map(|(point, _)| point) == first_point;
Some(bezier_rs::Subpath::new(groups, closed))
}
/// Construct a [`bezier_rs::Bezier`] curve for each region, skipping invalid regions.
pub fn region_bezier_paths(&self) -> impl Iterator<Item = (RegionId, bezier_rs::Subpath<PointId>)> + '_ {
self.region_domain
.ids
.iter()
.zip(&self.region_domain.segment_range)
.filter_map(|(&id, segment_range)| self.segment_domain.resolve_range(segment_range).map(|range| (id, range)))
.filter_map(|(id, range)| {
let segments_iter = self.segment_domain.handles[range.clone()]
.iter()
.zip(&self.segment_domain.start_point[range.clone()])
.zip(&self.segment_domain.end_point[range])
.map(|((&handles, &start), &end)| (handles, start, end));
self.subpath_from_segments(segments_iter).map(|subpath| (id, subpath))
})
}
/// Construct a [`bezier_rs::Bezier`] curve for stroke.
pub fn stroke_bezier_paths(&self) -> StrokePathIter<'_> {
StrokePathIter { vector_data: self, segment_index: 0 }
}
/// Transforms this vector data
pub fn transform(&mut self, transform: DAffine2) {
self.point_domain.transform(transform);
self.segment_domain.transform(transform);
}
}
pub struct StrokePathIter<'a> {
vector_data: &'a super::VectorData,
segment_index: usize,
}
impl<'a> Iterator for StrokePathIter<'a> {
type Item = bezier_rs::Subpath<PointId>;
fn next(&mut self) -> Option<Self::Item> {
let segments = &self.vector_data.segment_domain;
if self.segment_index >= segments.end_point.len() {
return None;
}
let mut old_end = None;
let mut count = 0;
let segments_iter = segments.handles[self.segment_index..]
.iter()
.zip(&segments.start_point[self.segment_index..])
.zip(&segments.end_point[self.segment_index..])
.map(|((&handles, &start), &end)| (handles, start, end))
.take_while(|&(_, start, end)| {
let continuous = old_end.is_none() || old_end.is_some_and(|old_end| old_end == start);
old_end = Some(end);
count += 1;
continuous
});
let subpath = self.vector_data.subpath_from_segments(segments_iter);
self.segment_index += count;
subpath
}
}
impl bezier_rs::Identifier for PointId {
fn new() -> Self {
Self::generate()
}
}
impl From<crate::uuid::ManipulatorGroupId> for PointId {
fn from(value: crate::uuid::ManipulatorGroupId) -> Self {
Self(value.inner())
}
}
impl crate::vector::ConcatElement for super::VectorData {
fn concat(&mut self, other: &Self, transform: glam::DAffine2) {
let new_ids = other.point_domain.id.iter().filter(|id| self.point_domain.id.contains(id)).map(|&old| (old, PointId::generate()));
let point_map = new_ids.collect::<HashMap<_, _>>();
let new_ids = other
.segment_domain
.ids
.iter()
.filter(|id| self.segment_domain.ids.contains(id))
.map(|&old| (old, SegmentId::generate()));
let segment_map = new_ids.collect::<HashMap<_, _>>();
let new_ids = other.region_domain.ids.iter().filter(|id| self.region_domain.ids.contains(id)).map(|&old| (old, RegionId::generate()));
let region_map = new_ids.collect::<HashMap<_, _>>();
let id_map = IdMap { point_map, segment_map, region_map };
self.point_domain.concat(&other.point_domain, transform * other.transform, &id_map);
self.segment_domain.concat(&other.segment_domain, transform * other.transform, &id_map);
self.region_domain.concat(&other.region_domain, transform * other.transform, &id_map);
// TODO: properly deal with fills such as gradients
self.style = other.style.clone();
self.mirror_angle.extend(other.mirror_angle.iter().copied());
self.alpha_blending = other.alpha_blending;
}
}
struct IdMap {
point_map: HashMap<PointId, PointId>,
segment_map: HashMap<SegmentId, SegmentId>,
region_map: HashMap<RegionId, RegionId>,
}

View File

@ -1,5 +1,5 @@
use super::style::{Fill, FillType, Gradient, GradientType, Stroke}; use super::style::{Fill, FillType, Gradient, GradientType, Stroke};
use super::VectorData; use super::{PointId, SegmentId, StrokeId, VectorData};
use crate::renderer::GraphicElementRendered; use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, Transform, TransformMut}; use crate::transform::{Footprint, Transform, TransformMut};
use crate::{Color, GraphicGroup, Node}; use crate::{Color, GraphicGroup, Node};
@ -85,23 +85,17 @@ pub struct RepeatNode<Direction, Count> {
} }
#[node_macro::node_fn(RepeatNode)] #[node_macro::node_fn(RepeatNode)]
fn repeat_vector_data(mut vector_data: VectorData, direction: DVec2, count: u32) -> VectorData { fn repeat_vector_data(vector_data: VectorData, direction: DVec2, count: u32) -> VectorData {
// repeat the vector data // Repeat the vector data
let VectorData { subpaths, transform, .. } = &vector_data; let mut result = VectorData::empty();
let inverse = vector_data.transform.inverse();
let mut new_subpaths: Vec<Subpath<_>> = Vec::with_capacity(subpaths.len() * count as usize);
let inverse = transform.inverse();
let direction = inverse.transform_vector2(direction); let direction = inverse.transform_vector2(direction);
for i in 0..count { for i in 0..count {
let transform = DAffine2::from_translation(direction * i as f64); let transform = DAffine2::from_translation(direction * i as f64);
for mut subpath in subpaths.clone() { result.concat(&vector_data, transform);
subpath.apply_transform(transform);
new_subpaths.push(subpath);
}
} }
vector_data.subpaths = new_subpaths; result
vector_data
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -112,8 +106,8 @@ pub struct CircularRepeatNode<AngleOffset, Radius, Count> {
} }
#[node_macro::node_fn(CircularRepeatNode)] #[node_macro::node_fn(CircularRepeatNode)]
fn circular_repeat_vector_data(mut vector_data: VectorData, angle_offset: f64, radius: f64, count: u32) -> VectorData { fn circular_repeat_vector_data(vector_data: VectorData, angle_offset: f64, radius: f64, count: u32) -> VectorData {
let mut new_subpaths: Vec<Subpath<_>> = Vec::with_capacity(vector_data.subpaths.len() * count as usize); let mut result = VectorData::empty();
let Some(bounding_box) = vector_data.bounding_box() else { return vector_data }; let Some(bounding_box) = vector_data.bounding_box() else { return vector_data };
let center = (bounding_box[0] + bounding_box[1]) / 2.; let center = (bounding_box[0] + bounding_box[1]) / 2.;
@ -124,14 +118,10 @@ fn circular_repeat_vector_data(mut vector_data: VectorData, angle_offset: f64, r
let angle = (2. * std::f64::consts::PI / count as f64) * i as f64 + angle_offset.to_radians(); let angle = (2. * std::f64::consts::PI / count as f64) * i as f64 + angle_offset.to_radians();
let rotation = DAffine2::from_angle(angle); let rotation = DAffine2::from_angle(angle);
let transform = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform); let transform = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform);
for mut subpath in vector_data.subpaths.clone() { result.concat(&vector_data, transform);
subpath.apply_transform(transform);
new_subpaths.push(subpath);
}
} }
vector_data.subpaths = new_subpaths; result
vector_data
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -140,29 +130,16 @@ pub struct BoundingBoxNode;
#[node_macro::node_fn(BoundingBoxNode)] #[node_macro::node_fn(BoundingBoxNode)]
fn generate_bounding_box(vector_data: VectorData) -> VectorData { fn generate_bounding_box(vector_data: VectorData) -> VectorData {
let bounding_box = vector_data.bounding_box().unwrap(); let bounding_box = vector_data.bounding_box().unwrap();
VectorData::from_subpaths(vec![Subpath::new_rect( VectorData::from_subpath(Subpath::new_rect(
vector_data.transform.transform_point2(bounding_box[0]), vector_data.transform.transform_point2(bounding_box[0]),
vector_data.transform.transform_point2(bounding_box[1]), vector_data.transform.transform_point2(bounding_box[1]),
)]) ))
} }
pub trait ConcatElement { pub trait ConcatElement {
fn concat(&mut self, other: &Self, transform: DAffine2); fn concat(&mut self, other: &Self, transform: DAffine2);
} }
impl ConcatElement for VectorData {
fn concat(&mut self, other: &Self, transform: DAffine2) {
for mut subpath in other.subpaths.iter().cloned() {
subpath.apply_transform(transform * other.transform);
self.subpaths.push(subpath);
}
// TODO: properly deal with fills such as gradients
self.style = other.style.clone();
self.mirror_angle.extend(other.mirror_angle.iter().copied());
self.alpha_blending = other.alpha_blending;
}
}
impl ConcatElement for GraphicGroup { impl ConcatElement for GraphicGroup {
fn concat(&mut self, other: &Self, transform: DAffine2) { fn concat(&mut self, other: &Self, transform: DAffine2) {
// TODO: Decide if we want to keep this behavior whereby the layers are flattened // TODO: Decide if we want to keep this behavior whereby the layers are flattened
@ -198,7 +175,7 @@ async fn copy_to_points<I: GraphicElementRendered + Default + ConcatElement + Tr
let instance = self.instance.eval(footprint).await; let instance = self.instance.eval(footprint).await;
let random_scale_difference = random_scale_max - random_scale_min; let random_scale_difference = random_scale_max - random_scale_min;
let points_list = points.subpaths.iter().flat_map(|s| s.anchors()); let points_list = points.point_domain.positions();
let instance_bounding_box = instance.bounding_box(DAffine2::IDENTITY).unwrap_or_default(); let instance_bounding_box = instance.bounding_box(DAffine2::IDENTITY).unwrap_or_default();
let instance_center = -0.5 * (instance_bounding_box[0] + instance_bounding_box[1]); let instance_center = -0.5 * (instance_bounding_box[0] + instance_bounding_box[1]);
@ -210,7 +187,7 @@ async fn copy_to_points<I: GraphicElementRendered + Default + ConcatElement + Tr
let do_rotation = random_rotation.abs() > 1e-6; let do_rotation = random_rotation.abs() > 1e-6;
let mut result = I::default(); let mut result = I::default();
for point in points_list { for &point in points_list {
let center_transform = DAffine2::from_translation(instance_center); let center_transform = DAffine2::from_translation(instance_center);
let translation = points.transform.transform_point2(point); let translation = points.transform.transform_point2(point);
@ -253,7 +230,7 @@ pub struct SamplePoints<VectorData, Spacing, StartOffset, StopOffset, AdaptiveSp
} }
#[node_macro::node_fn(SamplePoints)] #[node_macro::node_fn(SamplePoints)]
async fn sample_points<FV: Future<Output = VectorData>, FL: Future<Output = Vec<Vec<f64>>>>( async fn sample_points<FV: Future<Output = VectorData>, FL: Future<Output = Vec<f64>>>(
footprint: Footprint, footprint: Footprint,
mut vector_data: impl Node<Footprint, Output = FV>, mut vector_data: impl Node<Footprint, Output = FV>,
spacing: f64, spacing: f64,
@ -262,18 +239,23 @@ async fn sample_points<FV: Future<Output = VectorData>, FL: Future<Output = Vec<
adaptive_spacing: bool, adaptive_spacing: bool,
lengths_of_segments_of_subpaths: impl Node<Footprint, Output = FL>, lengths_of_segments_of_subpaths: impl Node<Footprint, Output = FL>,
) -> VectorData { ) -> VectorData {
let mut vector_data = self.vector_data.eval(footprint).await; let vector_data = self.vector_data.eval(footprint).await;
let lengths_of_segments_of_subpaths = self.lengths_of_segments_of_subpaths.eval(footprint).await; let lengths_of_segments_of_subpaths = self.lengths_of_segments_of_subpaths.eval(footprint).await;
for (index, subpath) in &mut vector_data.subpaths.iter_mut().enumerate() { let mut bezier = vector_data.segment_bezier_iter().enumerate().peekable();
if subpath.is_empty() || !spacing.is_finite() || spacing <= 0. {
continue; let mut result = VectorData::empty();
result.transform = vector_data.transform;
while let Some((index, (segment, _, _, mut last_end))) = bezier.next() {
let mut lengths = vec![(segment, lengths_of_segments_of_subpaths.get(index).copied().unwrap_or_default())];
while let Some((index, (segment, _, _, end))) = bezier.peek().is_some_and(|(_, (_, _, start, _))| *start == last_end).then(|| bezier.next()).flatten() {
last_end = end;
lengths.push((segment, lengths_of_segments_of_subpaths.get(index).copied().unwrap_or_default()));
} }
subpath.apply_transform(vector_data.transform); let total_length: f64 = lengths.iter().map(|(_, len)| *len).sum();
let segment_lengths = &lengths_of_segments_of_subpaths[index];
let total_length: f64 = segment_lengths.iter().sum();
let mut used_length = total_length - start_offset - stop_offset; let mut used_length = total_length - start_offset - stop_offset;
if used_length <= 0. { if used_length <= 0. {
@ -282,35 +264,43 @@ async fn sample_points<FV: Future<Output = VectorData>, FL: Future<Output = Vec<
let count; let count;
if adaptive_spacing { if adaptive_spacing {
// With adaptive spacing, we widen or narrow the points as necessary to ensure the last point is always at the end of the path.
count = (used_length / spacing).round(); count = (used_length / spacing).round();
} else { } else {
// Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short before the end of the path.
count = (used_length / spacing + f64::EPSILON).floor(); count = (used_length / spacing + f64::EPSILON).floor();
used_length = used_length - used_length % spacing; used_length = used_length - used_length % spacing;
} }
if count >= 1. { if count < 1. {
let new_anchors = (0..=count as usize).map(|c| { continue;
let ratio = c as f64 / count;
// With adaptive spacing, we widen or narrow the points (that's the `round()` above) as necessary to ensure the last point is always at the end of the path.
// Without adaptive spacing, we just evenly space the points at the exact specified spacing, usually falling short (that's the `floor()` above) before the end of the path.
let t = (ratio * used_length + start_offset) / total_length;
let (segment_index, segment_t_euclidean) = subpath.global_euclidean_to_local_euclidean(t, segment_lengths.as_slice(), total_length);
let segment_t_parametric = subpath
.get_segment(segment_index)
.unwrap()
.euclidean_to_parametric_with_total_length(segment_t_euclidean, 0.001, segment_lengths[segment_index]);
subpath.get_segment(segment_index).unwrap().evaluate(TValue::Parametric(segment_t_parametric))
});
*subpath = Subpath::from_anchors(new_anchors, subpath.closed() && count as usize > 1);
} }
for c in 0..=count as usize {
let fraction = c as f64 / count;
let total_distance = fraction * used_length + start_offset;
subpath.apply_transform(vector_data.transform.inverse()); let (mut segment, mut length) = lengths[0];
let mut total_length_before = 0.;
for &(next_segment, next_length) in lengths.iter().skip(1) {
if total_length_before + length > total_distance {
break;
}
total_length_before += length;
segment = next_segment;
length = next_length;
}
let Some(segment) = vector_data.segment_from_id(segment) else { continue };
let segment = segment.apply_transformation(|point| vector_data.transform.transform_point2(point));
let parametric_t = segment.euclidean_to_parametric_with_total_length((total_distance - total_length_before) / length, 0.001, length);
let point = segment.evaluate(TValue::Parametric(parametric_t));
result.point_domain.push(PointId::generate(), vector_data.transform.inverse().transform_point2(point));
}
} }
vector_data
result
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -319,36 +309,32 @@ pub struct PoissonDiskPoints<SeparationDiskDiameter> {
} }
#[node_macro::node_fn(PoissonDiskPoints)] #[node_macro::node_fn(PoissonDiskPoints)]
fn poisson_disk_points(mut vector_data: VectorData, separation_disk_diameter: f64) -> VectorData { fn poisson_disk_points(vector_data: VectorData, separation_disk_diameter: f64) -> VectorData {
let mut rng = rand::rngs::StdRng::seed_from_u64(0); let mut rng = rand::rngs::StdRng::seed_from_u64(0);
for subpath in &mut vector_data.subpaths.iter_mut() { let mut result = VectorData::empty();
for (_, mut subpath) in vector_data.region_bezier_paths() {
if subpath.manipulator_groups().len() < 3 { if subpath.manipulator_groups().len() < 3 {
continue; continue;
} }
subpath.apply_transform(vector_data.transform); subpath.apply_transform(vector_data.transform);
let points = subpath.poisson_disk_points(separation_disk_diameter, || rng.gen::<f64>()).into_iter(); for point in subpath.poisson_disk_points(separation_disk_diameter, || rng.gen::<f64>()) {
*subpath = Subpath::from_anchors(points, false); result.point_domain.push(PointId::generate(), vector_data.transform.inverse().transform_point2(point));
}
subpath.apply_transform(vector_data.transform.inverse());
} }
vector_data result
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct LengthsOfSegmentsOfSubpaths; pub struct LengthsOfSegmentsOfSubpaths;
#[node_macro::node_fn(LengthsOfSegmentsOfSubpaths)] #[node_macro::node_fn(LengthsOfSegmentsOfSubpaths)]
fn lengths_of_segments_of_subpaths(mut vector_data: VectorData) -> Vec<Vec<f64>> { fn lengths_of_segments_of_subpaths(vector_data: VectorData) -> Vec<f64> {
vector_data vector_data
.subpaths .segment_bezier_iter()
.iter_mut() .map(|(_id, bezier, _, _)| bezier.apply_transformation(|point| vector_data.transform.transform_point2(point)).length(None))
.map(|subpath| {
subpath.apply_transform(vector_data.transform);
subpath.iter().map(|bezier| bezier.length(None)).collect()
})
.collect() .collect()
} }
@ -357,15 +343,20 @@ pub struct SplinesFromPointsNode;
#[node_macro::node_fn(SplinesFromPointsNode)] #[node_macro::node_fn(SplinesFromPointsNode)]
fn splines_from_points(mut vector_data: VectorData) -> VectorData { fn splines_from_points(mut vector_data: VectorData) -> VectorData {
for subpath in &mut vector_data.subpaths { let points = &vector_data.point_domain;
let mut spline = Subpath::new_cubic_spline(subpath.anchors());
// Preserve the manipulator group ids vector_data.segment_domain.clear();
for (spline_manipulator_group, original_manipulator_group) in spline.manipulator_groups_mut().iter_mut().zip(subpath.manipulator_groups()) {
spline_manipulator_group.id = original_manipulator_group.id;
}
*subpath = spline; let first_handles = bezier_rs::solve_spline_first_handle(points.positions());
for (start_index, end_index) in (0..(points.positions().len())).zip(1..(points.positions().len())) {
let handle_start = first_handles[start_index];
let handle_end = points.positions()[end_index] * 2. - first_handles[end_index];
let handles = bezier_rs::BezierHandles::Cubic { handle_start, handle_end };
vector_data
.segment_domain
.push(SegmentId::generate(), points.ids()[start_index], points.ids()[end_index], handles, StrokeId::generate())
} }
vector_data vector_data
@ -386,13 +377,17 @@ async fn morph<SourceFuture: Future<Output = VectorData>, TargetFuture: Future<O
start_index: u32, start_index: u32,
time: f64, time: f64,
) -> VectorData { ) -> VectorData {
let mut source = self.source.eval(footprint).await; let source = self.source.eval(footprint).await;
let mut target = self.target.eval(footprint).await; let target = self.target.eval(footprint).await;
let mut result = VectorData::empty();
// Lerp styles // Lerp styles
let style = source.style.lerp(&target.style, time); result.alpha_blending = if time < 0.5 { source.alpha_blending } else { target.alpha_blending };
result.style = source.style.lerp(&target.style, time);
for (source_path, target_path) in source.subpaths.iter_mut().zip(target.subpaths.iter_mut()) { let mut source_paths = source.stroke_bezier_paths();
let mut target_paths = target.stroke_bezier_paths();
for (mut source_path, mut target_path) in (&mut source_paths).zip(&mut target_paths) {
// Deal with mistmatched transforms // Deal with mistmatched transforms
source_path.apply_transform(source.transform); source_path.apply_transform(source.transform);
target_path.apply_transform(target.transform); target_path.apply_transform(target.transform);
@ -430,38 +425,198 @@ async fn morph<SourceFuture: Future<Output = VectorData>, TargetFuture: Future<O
target_path.insert(SubpathTValue::Parametric { segment_index, t: 0.5 }) target_path.insert(SubpathTValue::Parametric { segment_index, t: 0.5 })
} }
} }
}
// Mismatched subpath count
for source_path in source.subpaths.iter_mut().skip(target.subpaths.len()) {
source_path.apply_transform(source.transform);
target.subpaths.push(Subpath::from_anchors(
std::iter::repeat(source_path.manipulator_groups().first().map(|group| group.anchor).unwrap_or_default()).take(source_path.len()),
source_path.closed,
))
}
for target_path in target.subpaths.iter_mut().skip(source.subpaths.len()) {
target_path.apply_transform(target.transform);
source.subpaths.push(Subpath::from_anchors(
std::iter::repeat(target_path.manipulator_groups().first().map(|group| group.anchor).unwrap_or_default()).take(target_path.len()),
target_path.closed,
))
}
// Lerp points // Lerp points
for (subpath, target) in source.subpaths.iter_mut().zip(target.subpaths.iter()) { for (manipulator, target) in source_path.manipulator_groups_mut().iter_mut().zip(target_path.manipulator_groups()) {
for (manipulator, target) in subpath.manipulator_groups_mut().iter_mut().zip(target.manipulator_groups()) {
manipulator.in_handle = Some(manipulator.in_handle.unwrap_or(manipulator.anchor).lerp(target.in_handle.unwrap_or(target.anchor), time)); manipulator.in_handle = Some(manipulator.in_handle.unwrap_or(manipulator.anchor).lerp(target.in_handle.unwrap_or(target.anchor), time));
manipulator.out_handle = Some(manipulator.out_handle.unwrap_or(manipulator.anchor).lerp(target.out_handle.unwrap_or(target.anchor), time)); manipulator.out_handle = Some(manipulator.out_handle.unwrap_or(manipulator.anchor).lerp(target.out_handle.unwrap_or(target.anchor), time));
manipulator.anchor = manipulator.anchor.lerp(target.anchor, time); manipulator.anchor = manipulator.anchor.lerp(target.anchor, time);
} }
result.append_subpath(source_path);
}
// Mismatched subpath count
for mut source_path in source_paths {
source_path.apply_transform(source.transform);
let end = source_path.manipulator_groups().first().map(|group| group.anchor).unwrap_or_default();
for group in source_path.manipulator_groups_mut() {
group.anchor = group.anchor.lerp(end, time);
group.in_handle = group.in_handle.map(|handle| handle.lerp(end, time));
group.out_handle = group.in_handle.map(|handle| handle.lerp(end, time));
}
}
for mut target_path in target_paths {
target_path.apply_transform(target.transform);
let start = target_path.manipulator_groups().first().map(|group| group.anchor).unwrap_or_default();
for group in target_path.manipulator_groups_mut() {
group.anchor = start.lerp(group.anchor, time);
group.in_handle = group.in_handle.map(|handle| start.lerp(handle, time));
group.out_handle = group.in_handle.map(|handle| start.lerp(handle, time));
}
} }
// Create result result
let subpaths = std::mem::take(&mut source.subpaths); }
let mut current = if time < 0.5 { source } else { target };
current.style = style; #[cfg(test)]
current.subpaths = subpaths; mod test {
current.transform = DAffine2::IDENTITY; use bezier_rs::Bezier;
current use super::*;
use crate::transform::CullNode;
use crate::value::ClonedNode;
use std::pin::Pin;
#[derive(Clone)]
pub struct FutureWrapperNode<Node: Clone>(Node);
impl<'i, T: 'i, N: Node<'i, T> + Clone> Node<'i, T> for FutureWrapperNode<N>
where
N: Node<'i, T>,
{
type Output = Pin<Box<dyn core::future::Future<Output = N::Output> + 'i>>;
fn eval(&'i self, input: T) -> Self::Output {
Box::pin(async move { self.0.eval(input) })
}
}
#[test]
fn repeat() {
let direction = DVec2::X * 1.5;
let repeated = RepeatNode {
direction: ClonedNode::new(direction),
count: ClonedNode::new(3),
}
.eval(VectorData::from_subpath(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)));
assert_eq!(repeated.region_bezier_paths().count(), 3);
for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() {
assert_eq!(subpath.manipulator_groups()[0].anchor, direction * index as f64);
}
}
#[test]
fn circle_repeat() {
let repeated = CircularRepeatNode {
angle_offset: ClonedNode::new(45.),
radius: ClonedNode::new(4.),
count: ClonedNode::new(8),
}
.eval(VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)));
assert_eq!(repeated.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in repeated.region_bezier_paths().enumerate() {
let expected_angle = (index as f64 + 1.) * 45.;
let centre = (subpath.manipulator_groups()[0].anchor + subpath.manipulator_groups()[2].anchor) / 2.;
let actual_angle = DVec2::Y.angle_between(centre).to_degrees();
assert!((actual_angle - expected_angle).abs() % 360. < 1e-5);
}
}
#[test]
fn bounding_box() {
let bouding_box = BoundingBoxNode.eval(VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)));
assert_eq!(bouding_box.region_bezier_paths().count(), 1);
let subpath = bouding_box.region_bezier_paths().next().unwrap().1;
assert_eq!(&subpath.anchors()[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]);
}
#[tokio::test]
async fn copy_to_points() {
let points = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE * 10., DVec2::ONE * 10.));
let expected_points = points.point_domain.positions().to_vec();
let bouding_box = CopyToPoints {
points: CullNode::new(FutureWrapperNode(ClonedNode(points))),
instance: CullNode::new(FutureWrapperNode(ClonedNode(VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))))),
random_scale_min: FutureWrapperNode(ClonedNode(1.)),
random_scale_max: FutureWrapperNode(ClonedNode(1.)),
random_scale_bias: FutureWrapperNode(ClonedNode(0.)),
random_rotation: FutureWrapperNode(ClonedNode(0.)),
}
.eval(Footprint::default())
.await;
assert_eq!(bouding_box.region_bezier_paths().count(), expected_points.len());
for (index, (_, subpath)) in bouding_box.region_bezier_paths().enumerate() {
let offset = expected_points[index];
assert_eq!(
&subpath.anchors()[..4],
&[offset + DVec2::NEG_ONE, offset + DVec2::new(1., -1.), offset + DVec2::ONE, offset + DVec2::new(-1., 1.),]
);
}
}
#[tokio::test]
async fn sample_points() {
let path = VectorData::from_subpath(Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)));
let sample_points = SamplePoints {
vector_data: CullNode::new(FutureWrapperNode(ClonedNode(path))),
spacing: FutureWrapperNode(ClonedNode(30.)),
start_offset: FutureWrapperNode(ClonedNode(0.)),
stop_offset: FutureWrapperNode(ClonedNode(0.)),
adaptive_spacing: FutureWrapperNode(ClonedNode(false)),
lengths_of_segments_of_subpaths: CullNode::new(FutureWrapperNode(ClonedNode(vec![100.]))),
}
.eval(Footprint::default())
.await;
assert_eq!(sample_points.point_domain.positions().len(), 4);
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) {
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
}
}
#[tokio::test]
async fn adaptive_spacing() {
let path = VectorData::from_subpath(Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)));
let sample_points = SamplePoints {
vector_data: CullNode::new(FutureWrapperNode(ClonedNode(path))),
spacing: FutureWrapperNode(ClonedNode(18.)),
start_offset: FutureWrapperNode(ClonedNode(45.)),
stop_offset: FutureWrapperNode(ClonedNode(10.)),
adaptive_spacing: FutureWrapperNode(ClonedNode(true)),
lengths_of_segments_of_subpaths: CullNode::new(FutureWrapperNode(ClonedNode(vec![100.]))),
}
.eval(Footprint::default())
.await;
assert_eq!(sample_points.point_domain.positions().len(), 4);
for (pos, expected) in sample_points.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) {
assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}");
}
}
#[test]
fn poisson() {
let sample_points = PoissonDiskPoints {
separation_disk_diameter: ClonedNode(10. * std::f64::consts::SQRT_2),
}
.eval(VectorData::from_subpath(Subpath::new_ellipse(DVec2::NEG_ONE * 50., DVec2::ONE * 50.)));
assert!(
(20..=40).contains(&sample_points.point_domain.positions().len()),
"actual len {}",
sample_points.point_domain.positions().len()
);
for point in sample_points.point_domain.positions() {
assert!(point.length() < 50. + 1., "Expected point in circle {point}")
}
}
#[test]
fn lengths() {
let subpath = VectorData::from_subpath(Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.)));
let lengths = LengthsOfSegmentsOfSubpaths.eval(subpath);
assert_eq!(lengths, vec![100.]);
}
#[test]
fn spline() {
let subpath = VectorData::from_subpath(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.));
let spline = SplinesFromPointsNode.eval(subpath);
assert_eq!(spline.stroke_bezier_paths().count(), 1);
assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]);
}
#[tokio::test]
async fn morph() {
let source = VectorData::from_subpath(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.));
let target = VectorData::from_subpath(Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO));
let sample_points = MorphNode {
source: CullNode::new(FutureWrapperNode(ClonedNode(source))),
target: CullNode::new(FutureWrapperNode(ClonedNode(target))),
time: FutureWrapperNode(ClonedNode(0.5)),
start_index: FutureWrapperNode(ClonedNode(0)),
}
.eval(Footprint::default())
.await;
assert_eq!(
&sample_points.point_domain.positions()[..4],
vec![DVec2::new(-25., -50.), DVec2::new(50., -25.), DVec2::new(25., 50.), DVec2::new(-50., 25.)]
);
}
} }

View File

@ -60,7 +60,7 @@ pub struct VectorPointsNode;
#[node_fn(VectorPointsNode)] #[node_fn(VectorPointsNode)]
fn vector_points(vector: VectorData) -> Vec<DVec2> { fn vector_points(vector: VectorData) -> Vec<DVec2> {
vector.subpaths.iter().flat_map(|subpath| subpath.manipulator_groups().iter().map(|group| group.anchor)).collect() vector.point_domain.positions().to_vec()
} }
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]

View File

@ -749,7 +749,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
register_node!(graphene_std::raster::MandelbrotNode, input: Footprint, params: []), register_node!(graphene_std::raster::MandelbrotNode, input: Footprint, params: []),
async_node!(graphene_core::vector::CopyToPoints<_, _, _, _, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, Footprint => VectorData, () => f64, () => f64, () => f64, () => f64]), async_node!(graphene_core::vector::CopyToPoints<_, _, _, _, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, Footprint => VectorData, () => f64, () => f64, () => f64, () => f64]),
async_node!(graphene_core::vector::CopyToPoints<_, _, _, _, _, _>, input: Footprint, output: GraphicGroup, fn_params: [Footprint => VectorData, Footprint => GraphicGroup, () => f64, () => f64, () => f64, () => f64]), async_node!(graphene_core::vector::CopyToPoints<_, _, _, _, _, _>, input: Footprint, output: GraphicGroup, fn_params: [Footprint => VectorData, Footprint => GraphicGroup, () => f64, () => f64, () => f64, () => f64]),
async_node!(graphene_core::vector::SamplePoints<_, _, _, _, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, () => f64, () => f64, () => f64, () => bool, Footprint => Vec<Vec<f64>>]), async_node!(graphene_core::vector::SamplePoints<_, _, _, _, _, _>, input: Footprint, output: VectorData, fn_params: [Footprint => VectorData, () => f64, () => f64, () => f64, () => bool, Footprint => Vec<f64>]),
register_node!(graphene_core::vector::PoissonDiskPoints<_>, input: VectorData, params: [f64]), register_node!(graphene_core::vector::PoissonDiskPoints<_>, input: VectorData, params: [f64]),
register_node!(graphene_core::vector::LengthsOfSegmentsOfSubpaths, input: VectorData, params: []), register_node!(graphene_core::vector::LengthsOfSegmentsOfSubpaths, input: VectorData, params: []),
register_node!(graphene_core::vector::SplinesFromPointsNode, input: VectorData, params: []), register_node!(graphene_core::vector::SplinesFromPointsNode, input: VectorData, params: []),

View File

@ -164,7 +164,7 @@ impl WasmBezier {
"Euclidean" => TValueType::Euclidean, "Euclidean" => TValueType::Euclidean,
_ => panic!("Unexpected TValue string: '{t_variant}'"), _ => panic!("Unexpected TValue string: '{t_variant}'"),
}; };
let table_values: Vec<DVec2> = self.0.compute_lookup_table(Some(steps), Some(tvalue_type)); let table_values: Vec<DVec2> = self.0.compute_lookup_table(Some(steps), Some(tvalue_type)).collect();
let circles: String = table_values let circles: String = table_values
.iter() .iter()
.map(|point| draw_circle(*point, 3., RED, 1.5, WHITE)) .map(|point| draw_circle(*point, 3., RED, 1.5, WHITE))
@ -293,7 +293,7 @@ impl WasmBezier {
} }
pub fn project(&self, x: f64, y: f64) -> String { pub fn project(&self, x: f64, y: f64) -> String {
let projected_t_value = self.0.project(DVec2::new(x, y), None); let projected_t_value = self.0.project(DVec2::new(x, y));
let projected_point = self.0.evaluate(TValue::Parametric(projected_t_value)); let projected_point = self.0.evaluate(TValue::Parametric(projected_t_value));
let bezier = self.get_bezier_path(); let bezier = self.get_bezier_path();

View File

@ -236,7 +236,7 @@ impl WasmSubpath {
} }
pub fn project(&self, x: f64, y: f64) -> String { pub fn project(&self, x: f64, y: f64) -> String {
let (segment_index, projected_t) = self.0.project(DVec2::new(x, y), None).unwrap(); let (segment_index, projected_t) = self.0.project(DVec2::new(x, y)).unwrap();
let projected_point = self.0.evaluate(SubpathTValue::Parametric { segment_index, t: projected_t }); let projected_point = self.0.evaluate(SubpathTValue::Parametric { segment_index, t: projected_t });
let subpath_svg = self.to_default_svg(); let subpath_svg = self.to_default_svg();