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 10:27:30 -08:00
committed by GitHub
co-authored by Keavon Chambers
parent c8ea9e05a6
commit 218e9675fd
30 changed files with 991 additions and 436 deletions
+56 -45
View File
@@ -8,6 +8,7 @@ use std::fmt::Write;
impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
/// Create a new `Subpath` using a list of [ManipulatorGroup]s.
/// 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 {
assert!(!closed || manipulator_groups.len() > 1, "A closed Subpath must contain more than 1 ManipulatorGroup.");
Self { manipulator_groups, closed }
@@ -276,61 +277,71 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
// Number of points = number of points to find handles for
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]
}
let out_handles = solve_spline_first_handle(&points);
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].
// 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 {
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
.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
}
}
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
}
+5 -7
View File
@@ -1,7 +1,6 @@
use super::*;
use crate::consts::{DEFAULT_EUCLIDEAN_ERROR_BOUND, DEFAULT_LUT_STEP_SIZE};
use crate::utils::{SubpathTValue, TValue, TValueType};
use crate::ProjectionOptions;
use glam::DVec2;
/// 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`.
/// - `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>
pub fn length(&self, num_subdivisions: Option<usize>) -> f64 {
self.iter().map(|bezier| bezier.length(num_subdivisions)).sum()
pub fn length(&self, tolerance: Option<f64>) -> f64 {
self.iter().map(|bezier| bezier.length(tolerance)).sum()
}
/// 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.
/// 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>
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() {
return None;
}
@@ -109,7 +107,7 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
let (index, (_, project_t)) = self
.iter()
.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)
})
.enumerate()
+1
View File
@@ -4,6 +4,7 @@ mod manipulators;
mod solvers;
mod structs;
mod transform;
pub use core::*;
pub use structs::*;
use crate::Bezier;
+2 -2
View File
@@ -296,8 +296,8 @@ impl<ManipulatorGroupId: crate::Identifier> Subpath<ManipulatorGroupId> {
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.
let segment1_len = first_bezier.length(Some(5));
let segment2_len = second_bezier.length(Some(5));
let segment1_len = first_bezier.length(None);
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);
// Adjust start and end handles to fit the average tangent