Attribute-based vector format refactor (#1624)

* Initial vector format structure

* Click targets

* Code review pass

* Remove subpaths from vector data

* Morph node & vector node tests

* Insignificant change

---------

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

View file

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

View file

@ -263,7 +263,7 @@ impl GraphicElement {
let mut builder = PathBuilder::new();
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);
builder.move_to(start.x as f32, start.y as f32);
for bezier in subpath.iter() {

View file

@ -2,7 +2,8 @@ mod quad;
use crate::raster::{BlendMode, Image, ImageFrame};
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};
pub use quad::Quad;
@ -14,7 +15,7 @@ use glam::{DAffine2, DVec2};
/// Represents a clickable target for the layer
#[derive(Clone, Debug)]
pub struct ClickTarget {
pub subpath: bezier_rs::Subpath<ManipulatorGroupId>,
pub subpath: bezier_rs::Subpath<PointId>,
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 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);
}
@ -326,11 +330,8 @@ impl GraphicElementRendered for VectorData {
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 update_closed = |mut subpath: bezier_rs::Subpath<ManipulatorGroupId>| {
subpath.set_closed(self.style.fill().is_some());
subpath
};
click_targets.extend(self.subpaths.iter().cloned().map(update_closed).map(|subpath| ClickTarget { stroke_width, subpath }))
click_targets.extend(self.region_bezier_paths().map(|(_, subpath)| ClickTarget { stroke_width, subpath }));
click_targets.extend(self.stroke_bezier_paths().map(|subpath| ClickTarget { stroke_width, subpath }));
}
fn to_usvg_node(&self) -> usvg::Node {
@ -340,7 +341,7 @@ impl GraphicElementRendered for VectorData {
let vector_data = self;
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);
builder.move_to(start.x as f32, start.y as f32);
for bezier in subpath.iter() {

View file

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

View file

@ -43,7 +43,7 @@ fn square_generator(_input: (), size_x: f64, size_y: f64) -> VectorData {
let corner1 = -size / 2.;
let 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)]
@ -83,7 +83,7 @@ pub struct LineGenerator<Pos1, Pos2> {
#[node_macro::node_fn(LineGenerator)]
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)]
@ -93,7 +93,7 @@ pub struct SplineGenerator<Positions> {
#[node_macro::node_fn(SplineGenerator)]
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

View file

@ -1,6 +1,9 @@
mod attributes;
use super::style::{PathStyle, Stroke};
use crate::Color;
use crate::{uuid::ManipulatorGroupId, AlphaBlending};
pub use attributes::*;
use bezier_rs::ManipulatorGroup;
use dyn_any::{DynAny, StaticType};
@ -12,18 +15,23 @@ use glam::{DAffine2, DVec2};
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VectorData {
pub subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>,
pub transform: DAffine2,
pub style: PathStyle,
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)
/// 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 point_domain: PointDomain,
pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain,
}
impl core::hash::Hash for VectorData {
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.style.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.
pub const fn empty() -> Self {
Self {
subpaths: Vec::new(),
transform: DAffine2::IDENTITY,
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
alpha_blending: AlphaBlending::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.
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.
pub fn from_subpaths(subpaths: Vec<bezier_rs::Subpath<ManipulatorGroupId>>) -> Self {
super::VectorData { subpaths, ..Self::empty() }
pub fn from_subpaths(subpaths: impl IntoIterator<Item = bezier_rs::Subpath<ManipulatorGroupId>>) -> Self {
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
@ -69,9 +109,8 @@ impl VectorData {
/// Compute the bounding boxes of the subpaths with the specified transform
pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> {
self.subpaths
.iter()
.filter_map(|subpath| subpath.bounding_box_with_transform(transform))
self.segment_bezier_iter()
.map(|(_, bezier, _, _)| bezier.apply_transformation(|point| transform.transform_point2(point)).bounding_box())
.reduce(|b1, b2| [b1[0].min(b2[0]), b1[1].max(b2[1])])
}

View file

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

View file

@ -1,5 +1,5 @@
use super::style::{Fill, FillType, Gradient, GradientType, Stroke};
use super::VectorData;
use super::{PointId, SegmentId, StrokeId, VectorData};
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, Transform, TransformMut};
use crate::{Color, GraphicGroup, Node};
@ -85,23 +85,17 @@ pub struct RepeatNode<Direction, Count> {
}
#[node_macro::node_fn(RepeatNode)]
fn repeat_vector_data(mut vector_data: VectorData, direction: DVec2, count: u32) -> VectorData {
// repeat the vector data
let VectorData { subpaths, transform, .. } = &vector_data;
let mut new_subpaths: Vec<Subpath<_>> = Vec::with_capacity(subpaths.len() * count as usize);
let inverse = transform.inverse();
fn repeat_vector_data(vector_data: VectorData, direction: DVec2, count: u32) -> VectorData {
// Repeat the vector data
let mut result = VectorData::empty();
let inverse = vector_data.transform.inverse();
let direction = inverse.transform_vector2(direction);
for i in 0..count {
let transform = DAffine2::from_translation(direction * i as f64);
for mut subpath in subpaths.clone() {
subpath.apply_transform(transform);
new_subpaths.push(subpath);
}
result.concat(&vector_data, transform);
}
vector_data.subpaths = new_subpaths;
vector_data
result
}
#[derive(Debug, Clone, Copy)]
@ -112,8 +106,8 @@ pub struct CircularRepeatNode<AngleOffset, Radius, Count> {
}
#[node_macro::node_fn(CircularRepeatNode)]
fn circular_repeat_vector_data(mut 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);
fn circular_repeat_vector_data(vector_data: VectorData, angle_offset: f64, radius: f64, count: u32) -> VectorData {
let mut result = VectorData::empty();
let Some(bounding_box) = vector_data.bounding_box() else { return vector_data };
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 rotation = DAffine2::from_angle(angle);
let transform = DAffine2::from_translation(center) * rotation * DAffine2::from_translation(base_transform);
for mut subpath in vector_data.subpaths.clone() {
subpath.apply_transform(transform);
new_subpaths.push(subpath);
}
result.concat(&vector_data, transform);
}
vector_data.subpaths = new_subpaths;
vector_data
result
}
#[derive(Debug, Clone, Copy)]
@ -140,29 +130,16 @@ pub struct BoundingBoxNode;
#[node_macro::node_fn(BoundingBoxNode)]
fn generate_bounding_box(vector_data: VectorData) -> VectorData {
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[1]),
)])
))
}
pub trait ConcatElement {
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 {
fn concat(&mut self, other: &Self, transform: DAffine2) {
// 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 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_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 mut result = I::default();
for point in points_list {
for &point in points_list {
let center_transform = DAffine2::from_translation(instance_center);
let translation = points.transform.transform_point2(point);
@ -253,7 +230,7 @@ pub struct SamplePoints<VectorData, Spacing, StartOffset, StopOffset, AdaptiveSp
}
#[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,
mut vector_data: impl Node<Footprint, Output = FV>,
spacing: f64,
@ -262,18 +239,23 @@ async fn sample_points<FV: Future<Output = VectorData>, FL: Future<Output = Vec<
adaptive_spacing: bool,
lengths_of_segments_of_subpaths: impl Node<Footprint, Output = FL>,
) -> 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;
for (index, subpath) in &mut vector_data.subpaths.iter_mut().enumerate() {
if subpath.is_empty() || !spacing.is_finite() || spacing <= 0. {
continue;
let mut bezier = vector_data.segment_bezier_iter().enumerate().peekable();
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 segment_lengths = &lengths_of_segments_of_subpaths[index];
let total_length: f64 = segment_lengths.iter().sum();
let total_length: f64 = lengths.iter().map(|(_, len)| *len).sum();
let mut used_length = total_length - start_offset - stop_offset;
if used_length <= 0. {
@ -282,35 +264,43 @@ async fn sample_points<FV: Future<Output = VectorData>, FL: Future<Output = Vec<
let count;
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();
} 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();
used_length = used_length - used_length % spacing;
}
if count >= 1. {
let new_anchors = (0..=count as usize).map(|c| {
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);
if count < 1. {
continue;
}
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)]
@ -319,36 +309,32 @@ pub struct PoissonDiskPoints<SeparationDiskDiameter> {
}
#[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);
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 {
continue;
}
subpath.apply_transform(vector_data.transform);
let points = subpath.poisson_disk_points(separation_disk_diameter, || rng.gen::<f64>()).into_iter();
*subpath = Subpath::from_anchors(points, false);
subpath.apply_transform(vector_data.transform.inverse());
for point in subpath.poisson_disk_points(separation_disk_diameter, || rng.gen::<f64>()) {
result.point_domain.push(PointId::generate(), vector_data.transform.inverse().transform_point2(point));
}
}
vector_data
result
}
#[derive(Debug, Clone, Copy)]
pub struct 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
.subpaths
.iter_mut()
.map(|subpath| {
subpath.apply_transform(vector_data.transform);
subpath.iter().map(|bezier| bezier.length(None)).collect()
})
.segment_bezier_iter()
.map(|(_id, bezier, _, _)| bezier.apply_transformation(|point| vector_data.transform.transform_point2(point)).length(None))
.collect()
}
@ -357,15 +343,20 @@ pub struct SplinesFromPointsNode;
#[node_macro::node_fn(SplinesFromPointsNode)]
fn splines_from_points(mut vector_data: VectorData) -> VectorData {
for subpath in &mut vector_data.subpaths {
let mut spline = Subpath::new_cubic_spline(subpath.anchors());
let points = &vector_data.point_domain;
// Preserve the manipulator group ids
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;
}
vector_data.segment_domain.clear();
*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
@ -386,13 +377,17 @@ async fn morph<SourceFuture: Future<Output = VectorData>, TargetFuture: Future<O
start_index: u32,
time: f64,
) -> VectorData {
let mut source = self.source.eval(footprint).await;
let mut target = self.target.eval(footprint).await;
let source = self.source.eval(footprint).await;
let target = self.target.eval(footprint).await;
let mut result = VectorData::empty();
// 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
source_path.apply_transform(source.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 })
}
}
}
// 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
for (subpath, target) in source.subpaths.iter_mut().zip(target.subpaths.iter()) {
for (manipulator, target) in subpath.manipulator_groups_mut().iter_mut().zip(target.manipulator_groups()) {
// Lerp points
for (manipulator, target) in source_path.manipulator_groups_mut().iter_mut().zip(target_path.manipulator_groups()) {
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.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
let subpaths = std::mem::take(&mut source.subpaths);
let mut current = if time < 0.5 { source } else { target };
current.style = style;
current.subpaths = subpaths;
current.transform = DAffine2::IDENTITY;
current
result
}
#[cfg(test)]
mod test {
use bezier_rs::Bezier;
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.)]
);
}
}