mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
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
30 changed files with 991 additions and 436 deletions
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])])
|
||||
}
|
||||
|
||||
|
|
|
|||
376
node-graph/gcore/src/vector/vector_data/attributes.rs
Normal file
376
node-graph/gcore/src/vector/vector_data/attributes.rs
Normal 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>,
|
||||
}
|
||||
|
|
@ -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.)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue