Bezier-rs: Add split for Subpath (#988)
* Add subpath split * Update comment and colors * Address comments * Improve visualization clarity * Code review --------- Co-authored-by: Rob Nadal <Robnadal44@gmail.com> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
511a8aa164
commit
a328e7d3ef
|
|
@ -3,6 +3,7 @@ mod lookup;
|
||||||
mod manipulators;
|
mod manipulators;
|
||||||
mod solvers;
|
mod solvers;
|
||||||
mod structs;
|
mod structs;
|
||||||
|
mod transform;
|
||||||
pub use structs::*;
|
pub use structs::*;
|
||||||
|
|
||||||
use crate::Bezier;
|
use crate::Bezier;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
use super::*;
|
||||||
|
use crate::ComputeType;
|
||||||
|
|
||||||
|
/// Functionality that transforms Subpaths, such as split, reduce, offset, etc.
|
||||||
|
impl Subpath {
|
||||||
|
/// Returns either one or two Subpaths that result from splitting the original Subpath at the point corresponding to `t`.
|
||||||
|
/// If the original Subpath was closed, a single open Subpath will be returned.
|
||||||
|
/// If the original Subpath was open, two open Subpaths will be returned.
|
||||||
|
pub fn split(&self, t: ComputeType) -> (Subpath, Option<Subpath>) {
|
||||||
|
match t {
|
||||||
|
ComputeType::Parametric(t) => {
|
||||||
|
assert!((0.0..=1.).contains(&t));
|
||||||
|
|
||||||
|
let number_of_curves = self.len_segments() as f64;
|
||||||
|
let scaled_t = t * number_of_curves;
|
||||||
|
|
||||||
|
let target_curve_index = scaled_t.floor() as i32;
|
||||||
|
let target_curve_t = scaled_t % 1.;
|
||||||
|
let num_manipulator_groups = self.manipulator_groups.len();
|
||||||
|
|
||||||
|
// The only case where `curve` would be `None` is if the provided argument was 1
|
||||||
|
let optional_curve = self.iter().nth(target_curve_index as usize);
|
||||||
|
let curve = optional_curve.unwrap_or_else(|| self.iter().last().unwrap());
|
||||||
|
|
||||||
|
let [first_bezier, second_bezier] = curve.split(if t == 1. { t } else { target_curve_t });
|
||||||
|
|
||||||
|
let mut clone = self.manipulator_groups.clone();
|
||||||
|
let (mut first_split, mut second_split) = if t > 0. {
|
||||||
|
let clone2 = clone.split_off(num_manipulator_groups.min((target_curve_index as usize) + 1));
|
||||||
|
(clone, clone2)
|
||||||
|
} else {
|
||||||
|
(vec![], clone)
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.closed && (t == 0. || t == 1.) {
|
||||||
|
// The entire vector of manipulator groups will be in the second_split because target_curve_index == 0.
|
||||||
|
// Add a new manipulator group with the same anchor as the first node to represent the end of the now opened subpath
|
||||||
|
let last_curve = self.iter().last().unwrap();
|
||||||
|
first_split.push(ManipulatorGroup {
|
||||||
|
anchor: first_bezier.end(),
|
||||||
|
in_handle: last_curve.handle_end(),
|
||||||
|
out_handle: None,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if !first_split.is_empty() {
|
||||||
|
let num_elements = first_split.len();
|
||||||
|
first_split[num_elements - 1].out_handle = first_bezier.handle_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !second_split.is_empty() {
|
||||||
|
second_split[0].in_handle = second_bezier.handle_end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push new manipulator groups to represent the location of the split at the end of the first group and at the start of the second
|
||||||
|
// If the split was at a manipulator group's anchor, add only one manipulator group
|
||||||
|
// Add it to the first list when the split location is on the first manipulator group, otherwise add to the second list
|
||||||
|
if target_curve_t != 0. || t == 0. {
|
||||||
|
first_split.push(ManipulatorGroup {
|
||||||
|
anchor: first_bezier.end(),
|
||||||
|
in_handle: first_bezier.handle_end(),
|
||||||
|
out_handle: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if t != 0. {
|
||||||
|
second_split.insert(
|
||||||
|
0,
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: second_bezier.start(),
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: second_bezier.handle_start(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.closed {
|
||||||
|
// "Rotate" the manipulator groups list so that the split point becomes the start and end of the open subpath
|
||||||
|
second_split.append(&mut first_split);
|
||||||
|
(Subpath::new(second_split, false), None)
|
||||||
|
} else {
|
||||||
|
(Subpath::new(first_split, false), Some(Subpath::new(second_split, false)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: change this implementation to Euclidean compute
|
||||||
|
ComputeType::Euclidean(_t) => todo!(),
|
||||||
|
ComputeType::EuclideanWithinError { t: _, epsilon: _ } => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use glam::DVec2;
|
||||||
|
|
||||||
|
fn set_up_open_subpath() -> Subpath {
|
||||||
|
let start = DVec2::new(20., 30.);
|
||||||
|
let middle1 = DVec2::new(80., 90.);
|
||||||
|
let middle2 = DVec2::new(100., 100.);
|
||||||
|
let end = DVec2::new(60., 45.);
|
||||||
|
|
||||||
|
let handle1 = DVec2::new(75., 85.);
|
||||||
|
let handle2 = DVec2::new(40., 30.);
|
||||||
|
let handle3 = DVec2::new(10., 10.);
|
||||||
|
|
||||||
|
Subpath::new(
|
||||||
|
vec![
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: start,
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: Some(handle1),
|
||||||
|
},
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: middle1,
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: Some(handle2),
|
||||||
|
},
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: middle2,
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: None,
|
||||||
|
},
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: end,
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: Some(handle3),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_up_closed_subpath() -> Subpath {
|
||||||
|
let mut subpath = set_up_open_subpath();
|
||||||
|
subpath.closed = true;
|
||||||
|
subpath
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_an_open_subpath() {
|
||||||
|
let subpath = set_up_open_subpath();
|
||||||
|
let location = subpath.evaluate(ComputeType::Parametric(0.2));
|
||||||
|
let split_pair = subpath.iter().next().unwrap().split((0.2 * 3.) % 1.);
|
||||||
|
let (first, second) = subpath.split(ComputeType::Parametric(0.2));
|
||||||
|
assert!(second.is_some());
|
||||||
|
let second = second.unwrap();
|
||||||
|
assert_eq!(first.manipulator_groups[1].anchor, location);
|
||||||
|
assert_eq!(second.manipulator_groups[0].anchor, location);
|
||||||
|
assert_eq!(split_pair[0], first.iter().last().unwrap());
|
||||||
|
assert_eq!(split_pair[1], second.iter().next().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_at_start_of_an_open_subpath() {
|
||||||
|
let subpath = set_up_open_subpath();
|
||||||
|
let location = subpath.evaluate(ComputeType::Parametric(0.));
|
||||||
|
let split_pair = subpath.iter().next().unwrap().split(0.);
|
||||||
|
let (first, second) = subpath.split(ComputeType::Parametric(0.));
|
||||||
|
assert!(second.is_some());
|
||||||
|
let second = second.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
first.manipulator_groups[0],
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: location,
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(first.manipulator_groups.len(), 1);
|
||||||
|
assert_eq!(second.manipulator_groups[0].anchor, location);
|
||||||
|
assert_eq!(split_pair[1], second.iter().next().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_at_end_of_an_open_subpath() {
|
||||||
|
let subpath = set_up_open_subpath();
|
||||||
|
let location = subpath.evaluate(ComputeType::Parametric(1.));
|
||||||
|
let split_pair = subpath.iter().last().unwrap().split(1.);
|
||||||
|
let (first, second) = subpath.split(ComputeType::Parametric(1.));
|
||||||
|
assert!(second.is_some());
|
||||||
|
let second = second.unwrap();
|
||||||
|
assert_eq!(first.manipulator_groups[3].anchor, location);
|
||||||
|
assert_eq!(split_pair[0], first.iter().last().unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
second.manipulator_groups[0],
|
||||||
|
ManipulatorGroup {
|
||||||
|
anchor: location,
|
||||||
|
in_handle: None,
|
||||||
|
out_handle: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(second.manipulator_groups.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_a_closed_subpath() {
|
||||||
|
let subpath = set_up_closed_subpath();
|
||||||
|
let location = subpath.evaluate(ComputeType::Parametric(0.2));
|
||||||
|
let split_pair = subpath.iter().next().unwrap().split((0.2 * 4.) % 1.);
|
||||||
|
let (first, second) = subpath.split(ComputeType::Parametric(0.2));
|
||||||
|
assert!(second.is_none());
|
||||||
|
assert_eq!(first.manipulator_groups[0].anchor, location);
|
||||||
|
assert_eq!(first.manipulator_groups[5].anchor, location);
|
||||||
|
assert_eq!(first.manipulator_groups.len(), 6);
|
||||||
|
assert_eq!(split_pair[0], first.iter().last().unwrap());
|
||||||
|
assert_eq!(split_pair[1], first.iter().next().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_at_start_of_a_closed_subpath() {
|
||||||
|
let subpath = set_up_closed_subpath();
|
||||||
|
let location = subpath.evaluate(ComputeType::Parametric(0.));
|
||||||
|
let (first, second) = subpath.split(ComputeType::Parametric(0.));
|
||||||
|
assert!(second.is_none());
|
||||||
|
assert_eq!(first.manipulator_groups[0].anchor, location);
|
||||||
|
assert_eq!(first.manipulator_groups[4].anchor, location);
|
||||||
|
assert_eq!(subpath.manipulator_groups[0..], first.manipulator_groups[..4]);
|
||||||
|
assert!(!first.closed);
|
||||||
|
assert_eq!(first.iter().last().unwrap(), subpath.iter().last().unwrap());
|
||||||
|
assert_eq!(first.iter().next().unwrap(), subpath.iter().next().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn split_at_end_of_a_closed_subpath() {
|
||||||
|
let subpath = set_up_closed_subpath();
|
||||||
|
let location = subpath.evaluate(ComputeType::Parametric(1.));
|
||||||
|
let (first, second) = subpath.split(ComputeType::Parametric(1.));
|
||||||
|
assert!(second.is_none());
|
||||||
|
assert_eq!(first.manipulator_groups[0].anchor, location);
|
||||||
|
assert_eq!(first.manipulator_groups[4].anchor, location);
|
||||||
|
assert_eq!(subpath.manipulator_groups[0..], first.manipulator_groups[..4]);
|
||||||
|
assert!(!first.closed);
|
||||||
|
assert_eq!(first.iter().last().unwrap(), subpath.iter().last().unwrap());
|
||||||
|
assert_eq!(first.iter().next().unwrap(), subpath.iter().next().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -56,6 +56,12 @@ const subpathFeatures = {
|
||||||
[175, 140],
|
[175, 140],
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
Split: {
|
||||||
|
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined, computeType: ComputeType): string => subpath.split(options.computeArgument, computeType),
|
||||||
|
sliderOptions: [{ ...tSliderOptions, variable: "computeArgument" }],
|
||||||
|
// TODO: Uncomment this after implementing the Euclidean version
|
||||||
|
// chooseComputeType: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SubpathFeatureName = keyof typeof subpathFeatures;
|
export type SubpathFeatureName = keyof typeof subpathFeatures;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use crate::svg_drawing::*;
|
||||||
use bezier_rs::{Bezier, ComputeType, ManipulatorGroup, ProjectionOptions, Subpath};
|
use bezier_rs::{Bezier, ComputeType, ManipulatorGroup, ProjectionOptions, Subpath};
|
||||||
|
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
use std::fmt::Write;
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
/// Wrapper of the `Subpath` struct to be used in JS.
|
/// Wrapper of the `Subpath` struct to be used in JS.
|
||||||
|
|
@ -207,4 +208,64 @@ impl WasmSubpath {
|
||||||
|
|
||||||
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
|
wrap_svg_tag(format!("{subpath_svg}{line_svg}{intersections_svg}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn split(&self, t: f64, compute_type: String) -> String {
|
||||||
|
let (main_subpath, optional_subpath) = match compute_type.as_str() {
|
||||||
|
"Euclidean" => self.0.split(ComputeType::Euclidean(t)),
|
||||||
|
"Parametric" => self.0.split(ComputeType::Parametric(t)),
|
||||||
|
_ => panic!("Unexpected ComputeType string: '{}'", compute_type),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut main_subpath_svg = String::new();
|
||||||
|
let mut other_subpath_svg = String::new();
|
||||||
|
if optional_subpath.is_some() {
|
||||||
|
main_subpath.to_svg(
|
||||||
|
&mut main_subpath_svg,
|
||||||
|
CURVE_ATTRIBUTES.to_string().replace(BLACK, ORANGE).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
|
||||||
|
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, ORANGE),
|
||||||
|
HANDLE_ATTRIBUTES.to_string().replace(GRAY, ORANGE),
|
||||||
|
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, ORANGE),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
main_subpath.iter().enumerate().for_each(|(index, bezier)| {
|
||||||
|
let hue1 = &format!("hsla({}, 100%, 50%, 0.5)", 40 * index);
|
||||||
|
let hue2 = &format!("hsla({}, 100%, 50%, 0.5)", 40 * (index + 1));
|
||||||
|
let gradient_id = &format!("gradient{}", index);
|
||||||
|
let start = bezier.start();
|
||||||
|
let end = bezier.end();
|
||||||
|
let _ = write!(
|
||||||
|
main_subpath_svg,
|
||||||
|
r#"<defs><linearGradient id="{}" x1="{}%" y1="{}%" x2="{}%" y2="{}%"><stop offset="0%" stop-color="{}"/><stop offset="100%" stop-color="{}"/></linearGradient></defs>"#,
|
||||||
|
gradient_id,
|
||||||
|
start.x / 2.,
|
||||||
|
start.y / 2.,
|
||||||
|
end.x / 2.,
|
||||||
|
end.y / 2.,
|
||||||
|
hue1,
|
||||||
|
hue2
|
||||||
|
);
|
||||||
|
|
||||||
|
let stroke = &format!("url(#{})", gradient_id);
|
||||||
|
bezier.curve_to_svg(
|
||||||
|
&mut main_subpath_svg,
|
||||||
|
CURVE_ATTRIBUTES.to_string().replace(BLACK, stroke).replace("stroke-width=\"2\"", "stroke-width=\"8\""),
|
||||||
|
);
|
||||||
|
bezier.anchors_to_svg(&mut main_subpath_svg, ANCHOR_ATTRIBUTES.to_string().replace(BLACK, hue1));
|
||||||
|
bezier.handles_to_svg(&mut main_subpath_svg, HANDLE_ATTRIBUTES.to_string().replace(GRAY, hue1));
|
||||||
|
bezier.handle_lines_to_svg(&mut main_subpath_svg, HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, hue1));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(subpath) = optional_subpath {
|
||||||
|
subpath.to_svg(
|
||||||
|
&mut other_subpath_svg,
|
||||||
|
CURVE_ATTRIBUTES.to_string().replace(BLACK, RED).replace("stroke-width=\"2\"", "stroke-width=\"8\"") + " opacity=\"0.5\"",
|
||||||
|
ANCHOR_ATTRIBUTES.to_string().replace(BLACK, RED),
|
||||||
|
HANDLE_ATTRIBUTES.to_string().replace(GRAY, RED),
|
||||||
|
HANDLE_LINE_ATTRIBUTES.to_string().replace(GRAY, RED),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap_svg_tag(format!("{}{}{}", self.to_default_svg(), main_subpath_svg, other_subpath_svg))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue