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:
parent
c8ea9e05a6
commit
218e9675fd
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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.);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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")]
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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])])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
}
|
||||||
|
|
@ -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.)]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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: []),
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue