Instance tables refactor part 2: move the transform and alpha_blending fields up a level (#2249)

* Fix domain data structure field plural naming

* Rename method one_item to one_instance

Rename method one_item to one_instance

* Move the Instance<T> methods over to providing an Instance<T>/InstanceMut<T>

Move the Instance<T> methods over to providing an Instance<T>/InstanceMut<T>

* Add transform and alpha_blending fields to Instances<T>

* Finish the refactor (Brush tool is broken though)

* Add test for brush node

* Fix brush node

* Fix default empty images being 1x1 instead of 0x0 as they should be

* Fix tests

* Fix path transform

* Add correct upgrading to move the transform/blending up a level

---------

Co-authored-by: hypercube <0hypercube@gmail.com>
This commit is contained in:
Keavon Chambers
2025-03-02 01:26:36 -08:00
parent 4ff2bdb04f
commit f1160e1ca6
33 changed files with 1099 additions and 984 deletions

View File

@@ -1,4 +1,4 @@
use crate::vector::{HandleId, PointId, VectorData, VectorDataTable};
use crate::vector::{HandleId, VectorData, VectorDataTable};
use crate::Ctx;
use bezier_rs::Subpath;
@@ -105,15 +105,3 @@ fn star(
fn line(_: impl Ctx, _primary: (), #[default((0., -50.))] start: DVec2, #[default((0., 50.))] end: DVec2) -> VectorDataTable {
VectorDataTable::new(VectorData::from_subpath(Subpath::new_line(start, end)))
}
// TODO(TrueDoctor): I removed the Arc requirement we should think about when it makes sense to use it vs making a generic value node
#[node_macro::node(category(""))]
fn path(_: impl Ctx, path_data: Vec<Subpath<PointId>>, colinear_manipulators: Vec<PointId>) -> VectorDataTable {
let mut vector_data = VectorData::from_subpaths(path_data, false);
vector_data.colinear_manipulators = colinear_manipulators
.iter()
.filter_map(|&point| super::ManipulatorPointId::Anchor(point).get_handle_pair(&vector_data))
.collect();
VectorDataTable::new(vector_data)
}

View File

@@ -17,16 +17,50 @@ use glam::{DAffine2, DVec2};
pub fn migrate_vector_data<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<VectorDataTable, D::Error> {
use serde::Deserialize;
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OldVectorData {
pub transform: DAffine2,
pub alpha_blending: AlphaBlending,
pub style: PathStyle,
/// A list of all manipulator groups (referenced in `subpaths`) that have colinear handles (where they're locked at 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 both `subpath` and `colinear_manipulators` inputs"` to find it).
pub colinear_manipulators: Vec<[HandleId; 2]>,
pub point_domain: PointDomain,
pub segment_domain: SegmentDomain,
pub region_domain: RegionDomain,
// Used to store the upstream graphic group during destructive Boolean Operations (and other nodes with a similar effect) so that click targets can be preserved.
pub upstream_graphic_group: Option<GraphicGroupTable>,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
enum EitherFormat {
VectorData(VectorData),
OldVectorData(OldVectorData),
VectorDataTable(VectorDataTable),
}
Ok(match EitherFormat::deserialize(deserializer)? {
EitherFormat::VectorData(vector_data) => VectorDataTable::new(vector_data),
EitherFormat::OldVectorData(old) => {
let mut vector_data_table = VectorDataTable::new(VectorData {
style: old.style,
colinear_manipulators: old.colinear_manipulators,
point_domain: old.point_domain,
segment_domain: old.segment_domain,
region_domain: old.region_domain,
upstream_graphic_group: old.upstream_graphic_group,
});
*vector_data_table.one_instance_mut().transform = old.transform;
*vector_data_table.one_instance_mut().alpha_blending = old.alpha_blending;
vector_data_table
}
EitherFormat::VectorDataTable(vector_data_table) => vector_data_table,
})
}
@@ -38,9 +72,8 @@ pub type VectorDataTable = Instances<VectorData>;
#[derive(Clone, Debug, PartialEq, DynAny)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VectorData {
pub transform: DAffine2,
pub style: PathStyle,
pub alpha_blending: AlphaBlending,
/// A list of all manipulator groups (referenced in `subpaths`) that have colinear handles (where they're locked at 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 both `subpath` and `colinear_manipulators` inputs"` to find it).
pub colinear_manipulators: Vec<[HandleId; 2]>,
@@ -58,20 +91,17 @@ impl core::hash::Hash for VectorData {
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);
self.colinear_manipulators.hash(state);
}
}
impl VectorData {
/// An empty subpath with no data, an identity transform, and a black fill.
// TODO: Replace with just `Default`
pub const fn empty() -> Self {
Self {
transform: DAffine2::IDENTITY,
style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None),
alpha_blending: AlphaBlending::new(),
colinear_manipulators: Vec::new(),
point_domain: PointDomain::new(),
segment_domain: SegmentDomain::new(),
@@ -190,11 +220,6 @@ impl VectorData {
bounds_min + bounds_size * normalized_pivot
}
/// Compute the pivot in local space with the current transform applied
pub fn local_pivot(&self, normalized_pivot: DVec2) -> DVec2 {
self.transform.transform_point2(self.layerspace_pivot(normalized_pivot))
}
pub fn start_point(&self) -> impl Iterator<Item = PointId> + '_ {
self.segment_domain.start_point().iter().map(|&index| self.point_domain.ids()[index])
}

View File

@@ -1,3 +1,4 @@
use crate::transform::Transform;
use crate::vector::vector_data::{HandleId, VectorData, VectorDataTable};
use crate::vector::ConcatElement;
@@ -82,32 +83,30 @@ impl core::hash::BuildHasher for NoHashBuilder {
/// 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>,
#[serde(alias = "positions")]
position: 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));
self.position.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(),
}
Self { id: Vec::new(), position: Vec::new() }
}
pub fn clear(&mut self) {
self.id.clear();
self.positions.clear();
self.position.clear();
}
pub fn retain(&mut self, segment_domain: &mut SegmentDomain, f: impl Fn(&PointId) -> bool) {
let mut keep = self.id.iter().map(&f);
self.positions.retain(|_| keep.next().unwrap_or_default());
self.position.retain(|_| keep.next().unwrap_or_default());
// TODO(TrueDoctor): Consider using a prefix sum to avoid this Vec allocation (https://github.com/GraphiteEditor/Graphite/pull/1949#discussion_r1741711562)
let mut id_map = Vec::with_capacity(self.ids().len());
@@ -131,19 +130,19 @@ impl PointDomain {
pub fn push(&mut self, id: PointId, position: DVec2) {
debug_assert!(!self.id.contains(&id));
self.id.push(id);
self.positions.push(position);
self.position.push(position);
}
pub fn positions(&self) -> &[DVec2] {
&self.positions
&self.position
}
pub fn positions_mut(&mut self) -> impl Iterator<Item = (PointId, &mut DVec2)> {
self.id.iter().copied().zip(self.positions.iter_mut())
self.id.iter().copied().zip(self.position.iter_mut())
}
pub fn set_position(&mut self, index: usize, position: DVec2) {
self.positions[index] = position;
self.position[index] = position;
}
pub fn ids(&self) -> &[PointId] {
@@ -156,7 +155,7 @@ impl PointDomain {
#[track_caller]
pub fn position_from_id(&self, id: PointId) -> Option<DVec2> {
let pos = self.resolve_id(id).map(|index| self.positions[index]);
let pos = self.resolve_id(id).map(|index| self.position[index]);
if pos.is_none() {
warn!("Resolving pos of invalid id");
}
@@ -169,7 +168,7 @@ impl PointDomain {
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)));
self.position.extend(other.position.iter().map(|&pos| transform.transform_point2(pos)));
}
fn map_ids(&mut self, id_map: &IdMap) {
@@ -177,7 +176,7 @@ impl PointDomain {
}
fn transform(&mut self, transform: DAffine2) {
for pos in &mut self.positions {
for pos in &mut self.position {
*pos = transform.transform_point2(*pos);
}
}
@@ -187,7 +186,8 @@ impl PointDomain {
#[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>,
#[serde(alias = "ids")]
id: Vec<SegmentId>,
start_point: Vec<usize>,
end_point: Vec<usize>,
handles: Vec<bezier_rs::BezierHandles>,
@@ -197,7 +197,7 @@ pub struct SegmentDomain {
impl SegmentDomain {
pub const fn new() -> Self {
Self {
ids: Vec::new(),
id: Vec::new(),
start_point: Vec::new(),
end_point: Vec::new(),
handles: Vec::new(),
@@ -206,7 +206,7 @@ impl SegmentDomain {
}
pub fn clear(&mut self) {
self.ids.clear();
self.id.clear();
self.start_point.clear();
self.end_point.clear();
self.handles.clear();
@@ -215,7 +215,7 @@ impl SegmentDomain {
pub fn retain(&mut self, f: impl Fn(&SegmentId) -> bool, points_length: usize) {
let additional_delete_ids = self
.ids
.id
.iter()
.zip(&self.start_point)
.zip(&self.end_point)
@@ -236,17 +236,17 @@ impl SegmentDomain {
}
};
let mut keep = self.ids.iter().map(can_delete());
let mut keep = self.id.iter().map(can_delete());
self.start_point.retain(|_| keep.next().unwrap_or_default());
let mut keep = self.ids.iter().map(can_delete());
let mut keep = self.id.iter().map(can_delete());
self.end_point.retain(|_| keep.next().unwrap_or_default());
let mut keep = self.ids.iter().map(can_delete());
let mut keep = self.id.iter().map(can_delete());
self.handles.retain(|_| keep.next().unwrap_or_default());
let mut keep = self.ids.iter().map(can_delete());
let mut keep = self.id.iter().map(can_delete());
self.stroke.retain(|_| keep.next().unwrap_or_default());
let mut delete_iter = additional_delete_ids.iter().peekable();
self.ids.retain(move |id| {
self.id.retain(move |id| {
if delete_iter.peek() == Some(&id) {
delete_iter.next();
false
@@ -257,7 +257,7 @@ impl SegmentDomain {
}
pub fn ids(&self) -> &[SegmentId] {
&self.ids
&self.id
}
pub fn next_id(&self) -> SegmentId {
@@ -289,9 +289,9 @@ impl SegmentDomain {
}
pub(crate) fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: bezier_rs::BezierHandles, stroke: StrokeId) {
debug_assert!(!self.ids.contains(&id), "Tried to push an existing point to a point domain");
debug_assert!(!self.id.contains(&id), "Tried to push an existing point to a point domain");
self.ids.push(id);
self.id.push(id);
self.start_point.push(start);
self.end_point.push(end);
self.handles.push(handles);
@@ -299,15 +299,15 @@ impl SegmentDomain {
}
pub(crate) fn start_point_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut usize)> {
self.ids.iter().copied().zip(self.start_point.iter_mut())
self.id.iter().copied().zip(self.start_point.iter_mut())
}
pub(crate) fn end_point_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut usize)> {
self.ids.iter().copied().zip(self.end_point.iter_mut())
self.id.iter().copied().zip(self.end_point.iter_mut())
}
pub(crate) fn handles_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut bezier_rs::BezierHandles, usize, usize)> {
let nested = self.ids.iter().zip(&mut self.handles).zip(&self.start_point).zip(&self.end_point);
let nested = self.id.iter().zip(&mut self.handles).zip(&self.start_point).zip(&self.end_point);
nested.map(|(((&a, b), &c), &d)| (a, b, c, d))
}
@@ -317,7 +317,7 @@ impl SegmentDomain {
}
pub fn stroke_mut(&mut self) -> impl Iterator<Item = (SegmentId, &mut StrokeId)> {
self.ids.iter().copied().zip(self.stroke.iter_mut())
self.id.iter().copied().zip(self.stroke.iter_mut())
}
pub(crate) fn segment_start_from_id(&self, segment: SegmentId) -> Option<usize> {
@@ -348,15 +348,15 @@ impl SegmentDomain {
}
fn id_to_index(&self, id: SegmentId) -> Option<usize> {
debug_assert_eq!(self.ids.len(), self.handles.len());
debug_assert_eq!(self.ids.len(), self.start_point.len());
debug_assert_eq!(self.ids.len(), self.end_point.len());
self.ids.iter().position(|&check_id| check_id == id)
debug_assert_eq!(self.id.len(), self.handles.len());
debug_assert_eq!(self.id.len(), self.start_point.len());
debug_assert_eq!(self.id.len(), self.end_point.len());
self.id.iter().position(|&check_id| check_id == id)
}
fn resolve_range(&self, range: &core::ops::RangeInclusive<SegmentId>) -> Option<core::ops::RangeInclusive<usize>> {
match (self.id_to_index(*range.start()), self.id_to_index(*range.end())) {
(Some(start), Some(end)) if start.max(end) < self.handles.len().min(self.ids.len()).min(self.start_point.len()).min(self.end_point.len()) => Some(start..=end),
(Some(start), Some(end)) if start.max(end) < self.handles.len().min(self.id.len()).min(self.start_point.len()).min(self.end_point.len()) => Some(start..=end),
_ => {
warn!("Resolving range with invalid id");
None
@@ -365,7 +365,7 @@ impl SegmentDomain {
}
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.id.extend(other.id.iter().map(|id| *id_map.segment_map.get(id).unwrap_or(id)));
self.start_point.extend(other.start_point.iter().map(|&index| id_map.point_offset + index));
self.end_point.extend(other.end_point.iter().map(|&index| id_map.point_offset + index));
self.handles.extend(other.handles.iter().map(|handles| handles.apply_transformation(|p| transform.transform_point2(p))));
@@ -373,7 +373,7 @@ impl SegmentDomain {
}
fn map_ids(&mut self, id_map: &IdMap) {
self.ids.iter_mut().for_each(|id| *id = *id_map.segment_map.get(id).unwrap_or(id));
self.id.iter_mut().for_each(|id| *id = *id_map.segment_map.get(id).unwrap_or(id));
}
fn transform(&mut self, transform: DAffine2) {
@@ -384,12 +384,12 @@ impl SegmentDomain {
/// Enumerate all segments that start at the point.
pub(crate) fn start_connected(&self, point: usize) -> impl Iterator<Item = SegmentId> + '_ {
self.start_point.iter().zip(&self.ids).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg)
self.start_point.iter().zip(&self.id).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg)
}
/// Enumerate all segments that end at the point.
pub(crate) fn end_connected(&self, point: usize) -> impl Iterator<Item = SegmentId> + '_ {
self.end_point.iter().zip(&self.ids).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg)
self.end_point.iter().zip(&self.id).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg)
}
/// Enumerate all segments that start or end at a point, converting them to [`HandleId`s]. Note that the handles may not exist e.g. for a linear segment.
@@ -407,7 +407,8 @@ impl SegmentDomain {
#[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>,
#[serde(alias = "ids")]
id: Vec<RegionId>,
segment_range: Vec<core::ops::RangeInclusive<SegmentId>>,
fill: Vec<FillId>,
}
@@ -415,54 +416,54 @@ pub struct RegionDomain {
impl RegionDomain {
pub const fn new() -> Self {
Self {
ids: Vec::new(),
id: Vec::new(),
segment_range: Vec::new(),
fill: Vec::new(),
}
}
pub fn clear(&mut self) {
self.ids.clear();
self.id.clear();
self.segment_range.clear();
self.fill.clear();
}
pub fn retain(&mut self, f: impl Fn(&RegionId) -> bool) {
let mut keep = self.ids.iter().map(&f);
let mut keep = self.id.iter().map(&f);
self.segment_range.retain(|_| keep.next().unwrap_or_default());
let mut keep = self.ids.iter().map(&f);
let mut keep = self.id.iter().map(&f);
self.fill.retain(|_| keep.next().unwrap_or_default());
self.ids.retain(&f);
self.id.retain(&f);
}
pub fn push(&mut self, id: RegionId, segment_range: core::ops::RangeInclusive<SegmentId>, fill: FillId) {
if self.ids.contains(&id) {
if self.id.contains(&id) {
warn!("Duplicate region");
return;
}
self.ids.push(id);
self.id.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)
self.id.iter().position(|&check_id| check_id == id)
}
pub fn next_id(&self) -> RegionId {
self.ids.iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(RegionId::ZERO)
self.id.iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(RegionId::ZERO)
}
pub fn segment_range_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut core::ops::RangeInclusive<SegmentId>)> {
self.ids.iter().copied().zip(self.segment_range.iter_mut())
self.id.iter().copied().zip(self.segment_range.iter_mut())
}
pub fn fill_mut(&mut self) -> impl Iterator<Item = (RegionId, &mut FillId)> {
self.ids.iter().copied().zip(self.fill.iter_mut())
self.id.iter().copied().zip(self.fill.iter_mut())
}
pub fn ids(&self) -> &[RegionId] {
&self.ids
&self.id
}
pub fn segment_range(&self) -> &[core::ops::RangeInclusive<SegmentId>] {
@@ -474,7 +475,7 @@ impl RegionDomain {
}
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.id.extend(other.id.iter().map(|id| *id_map.region_map.get(id).unwrap_or(id)));
self.segment_range.extend(
other
.segment_range
@@ -485,7 +486,7 @@ impl RegionDomain {
}
fn map_ids(&mut self, id_map: &IdMap) {
self.ids.iter_mut().for_each(|id| *id = *id_map.region_map.get(id).unwrap_or(id));
self.id.iter_mut().for_each(|id| *id = *id_map.region_map.get(id).unwrap_or(id));
self.segment_range
.iter_mut()
.for_each(|range| *range = *id_map.segment_map.get(range.start()).unwrap_or(range.start())..=*id_map.segment_map.get(range.end()).unwrap_or(range.end()));
@@ -525,7 +526,7 @@ impl VectorData {
self.segment_domain
.handles
.iter()
.zip(&self.segment_domain.ids)
.zip(&self.segment_domain.id)
.zip(self.segment_domain.start_point())
.zip(self.segment_domain.end_point())
.map(to_bezier)
@@ -574,7 +575,7 @@ impl VectorData {
/// 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
.id
.iter()
.zip(&self.region_domain.segment_range)
.filter_map(|(&id, segment_range)| self.segment_domain.resolve_range(segment_range).map(|range| (id, range)))
@@ -774,16 +775,16 @@ impl ConcatElement for VectorData {
let point_map = new_ids.collect::<HashMap<_, _>>();
let new_ids = other
.segment_domain
.ids
.id
.iter()
.filter(|id| self.segment_domain.ids.contains(id))
.filter(|id| self.segment_domain.id.contains(id))
.map(|&old| (old, old.generate_from_hash(node_id)));
let segment_map = new_ids.collect::<HashMap<_, _>>();
let new_ids = other
.region_domain
.ids
.id
.iter()
.filter(|id| self.region_domain.ids.contains(id))
.filter(|id| self.region_domain.id.contains(id))
.map(|&old| (old, old.generate_from_hash(node_id)));
let region_map = new_ids.collect::<HashMap<_, _>>();
let id_map = IdMap {
@@ -792,20 +793,20 @@ impl ConcatElement for VectorData {
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);
self.point_domain.concat(&other.point_domain, transform, &id_map);
self.segment_domain.concat(&other.segment_domain, transform, &id_map);
self.region_domain.concat(&other.region_domain, transform, &id_map);
// TODO: properly deal with fills such as gradients
self.style = other.style.clone();
self.colinear_manipulators.extend(other.colinear_manipulators.iter().copied());
self.alpha_blending = other.alpha_blending;
}
}
impl ConcatElement for VectorDataTable {
fn concat(&mut self, other: &Self, transform: glam::DAffine2, node_id: u64) {
for (instance, other_instance) in self.instances_mut().zip(other.instances()) {
instance.concat(other_instance, transform, node_id);
*instance.alpha_blending = *other_instance.alpha_blending;
instance.instance.concat(other_instance.instance, transform * other_instance.transform(), node_id);
}
}
}

View File

@@ -1,4 +1,5 @@
use super::*;
use crate::transform::TransformMut;
use crate::uuid::generate_uuid;
use crate::Ctx;
@@ -425,11 +426,14 @@ impl core::hash::Hash for VectorModification {
/// A node that applies a procedural modification to some [`VectorData`].
#[node_macro::node(category(""))]
async fn path_modify(_ctx: impl Ctx, mut vector_data: VectorDataTable, modification: Box<VectorModification>) -> VectorDataTable {
let vector_data = vector_data.one_item_mut();
let vector_data_transform = *vector_data.one_instance().transform;
let vector_data = vector_data.one_instance_mut().instance;
modification.apply(vector_data);
VectorDataTable::new(vector_data.clone())
let mut result = VectorDataTable::new(vector_data.clone());
*result.transform_mut() = vector_data_transform;
result
}
#[test]

View File

@@ -1,12 +1,13 @@
use super::misc::CentroidType;
use super::style::{Fill, Gradient, GradientStops, Stroke};
use super::{PointId, SegmentDomain, SegmentId, StrokeId, VectorData, VectorDataTable};
use crate::instances::InstanceMut;
use crate::registry::types::{Angle, Fraction, IntegerCount, Length, SeedValue};
use crate::renderer::GraphicElementRendered;
use crate::transform::{Footprint, Transform, TransformMut};
use crate::vector::style::LineJoin;
use crate::vector::PointDomain;
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroup, GraphicGroupTable, OwnedContextImpl};
use crate::{CloneVarArgs, Color, Context, Ctx, ExtractAll, GraphicElement, GraphicGroupTable, OwnedContextImpl};
use bezier_rs::{Cap, Join, Subpath, SubpathTValue, TValue};
use glam::{DAffine2, DVec2};
@@ -20,15 +21,16 @@ trait VectorIterMut {
impl VectorIterMut for GraphicGroupTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)> {
let instance = self.one_item_mut();
let parent_transform = instance.transform;
let parent_transform = self.transform();
let instance = self.one_instance_mut().instance;
// Grab only the direct children
instance.iter_mut().filter_map(|(element, _)| element.as_vector_data_mut()).map(move |vector_data| {
let vector_data = vector_data.one_item_mut();
let transform = parent_transform * vector_data.transform;
(vector_data, transform)
let transform = parent_transform * vector_data.transform();
let vector_data_instance = vector_data.one_instance_mut().instance;
(vector_data_instance, transform)
})
}
}
@@ -36,8 +38,8 @@ impl VectorIterMut for GraphicGroupTable {
impl VectorIterMut for VectorDataTable {
fn vector_iter_mut(&mut self) -> impl Iterator<Item = (&mut VectorData, DAffine2)> {
self.instances_mut().map(|instance| {
let transform = instance.transform;
(instance, transform)
let transform = instance.transform();
(instance.instance, transform)
})
}
}
@@ -176,11 +178,10 @@ async fn repeat<I: 'n + GraphicElementRendered + Transform + TransformMut + Send
let instances = instances.max(1);
let total = (instances - 1) as f64;
let mut result = GraphicGroup::default();
let mut result_table = GraphicGroupTable::default();
let result = result_table.one_instance_mut().instance;
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else {
return GraphicGroupTable::new(result);
};
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table };
let center = (bounding_box[0] + bounding_box[1]) / 2.;
@@ -196,7 +197,7 @@ async fn repeat<I: 'n + GraphicElementRendered + Transform + TransformMut + Send
result.push((new_instance, None));
}
GraphicGroupTable::new(result)
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
@@ -211,11 +212,10 @@ async fn circular_repeat<I: 'n + GraphicElementRendered + Transform + TransformM
let first_vector_transform = instance.transform();
let instances = instances.max(1);
let mut result = GraphicGroup::default();
let mut result_table = GraphicGroupTable::default();
let result = result_table.one_instance_mut().instance;
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else {
return GraphicGroupTable::new(result);
};
let Some(bounding_box) = instance.bounding_box(DAffine2::IDENTITY) else { return result_table };
let center = (bounding_box[0] + bounding_box[1]) / 2.;
let base_transform = DVec2::new(0., radius) - center;
@@ -232,7 +232,7 @@ async fn circular_repeat<I: 'n + GraphicElementRendered + Transform + TransformM
result.push((new_instance, None));
}
GraphicGroupTable::new(result)
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
@@ -249,7 +249,8 @@ async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
random_rotation: Angle,
random_rotation_seed: SeedValue,
) -> GraphicGroupTable {
let points = points.one_item();
let points_transform = points.transform();
let points = points.one_instance().instance;
let instance_transform = instance.transform();
@@ -266,11 +267,13 @@ async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
let do_scale = random_scale_difference.abs() > 1e-6;
let do_rotation = random_rotation.abs() > 1e-6;
let mut result = GraphicGroup::default();
let mut result_table = GraphicGroupTable::default();
let result = result_table.one_instance_mut().instance;
for &point in points_list {
let center_transform = DAffine2::from_translation(instance_center);
let translation = points.transform.transform_point2(point);
let translation = points_transform.transform_point2(point);
let rotation = if do_rotation {
let degrees = (rotation_rng.random::<f64>() - 0.5) * random_rotation;
@@ -301,14 +304,15 @@ async fn copy_to_points<I: GraphicElementRendered + TransformMut + Send + 'n>(
result.push((new_instance, None));
}
GraphicGroupTable::new(result)
result_table
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
let vector_data = vector_data.one_item();
let vector_data_transform = vector_data.transform();
let vector_data = vector_data.one_instance().instance;
let bounding_box = vector_data.bounding_box_with_transform(vector_data.transform).unwrap();
let bounding_box = vector_data.bounding_box_with_transform(vector_data_transform).unwrap();
let mut result = VectorData::from_subpath(Subpath::new_rect(bounding_box[0], bounding_box[1]));
result.style = vector_data.style.clone();
result.style.set_stroke_transform(DAffine2::IDENTITY);
@@ -318,7 +322,8 @@ async fn bounding_box(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTa
#[node_macro::node(category("Vector"), path(graphene_core::vector), properties("offset_path_properties"))]
async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, line_join: LineJoin, #[default(4.)] miter_limit: f64) -> VectorDataTable {
let vector_data = vector_data.one_item();
let vector_data_transform = vector_data.transform();
let vector_data = vector_data.one_instance().instance;
let subpaths = vector_data.stroke_bezier_paths();
let mut result = VectorData::empty();
@@ -327,7 +332,7 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, l
// Perform operation on all subpaths in this shape.
for mut subpath in subpaths {
subpath.apply_transform(vector_data.transform);
subpath.apply_transform(vector_data_transform);
// Taking the existing stroke data and passing it to Bezier-rs to generate new paths.
let subpath_out = subpath.offset(
@@ -348,9 +353,9 @@ async fn offset_path(_: impl Ctx, vector_data: VectorDataTable, distance: f64, l
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDataTable {
let vector_data = vector_data.one_item();
let vector_data_transform = vector_data.transform();
let vector_data = vector_data.one_instance().instance;
let transform = &vector_data.transform;
let style = &vector_data.style;
let subpaths = vector_data.stroke_bezier_paths();
@@ -359,7 +364,7 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
// Perform operation on all subpaths in this shape.
for mut subpath in subpaths {
let stroke = style.stroke().unwrap();
subpath.apply_transform(*transform);
subpath.apply_transform(vector_data_transform);
// Taking the existing stroke data and passing it to Bezier-rs to generate new paths.
let subpath_out = subpath.outline(
@@ -398,34 +403,35 @@ async fn solidify_stroke(_: impl Ctx, vector_data: VectorDataTable) -> VectorDat
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn flatten_vector_elements(_: impl Ctx, graphic_group_input: GraphicGroupTable) -> VectorDataTable {
let graphic_group_input = graphic_group_input.one_item();
// A node based solution to support passing through vector data could be a network node with a cache node connected to
// a flatten vector elements connected to an if else node, another connection from the cache directly
// To the if else node, and another connection from the cache to a matches type node connected to the if else node.
fn concat_group(graphic_group: &GraphicGroup, current_transform: DAffine2, result: &mut VectorData) {
for (element, reference) in graphic_group.iter() {
fn concat_group(graphic_group_table: &GraphicGroupTable, current_transform: DAffine2, result: &mut InstanceMut<VectorData>) {
for (element, reference) in graphic_group_table.one_instance().instance.iter() {
match element {
GraphicElement::VectorData(vector_data) => {
for instance in vector_data.instances() {
result.concat(instance, current_transform, reference.map(|node_id| node_id.0).unwrap_or_default());
*result.alpha_blending = *instance.alpha_blending;
result
.instance
.concat(instance.instance, current_transform * instance.transform(), reference.map(|node_id| node_id.0).unwrap_or_default());
}
}
GraphicElement::GraphicGroup(graphic_group) => {
let graphic_group = graphic_group.one_item();
concat_group(graphic_group, current_transform * graphic_group.transform, result);
concat_group(graphic_group, current_transform * graphic_group.transform(), result);
}
_ => {}
}
}
}
let mut result = VectorData::empty();
concat_group(graphic_group_input, DAffine2::IDENTITY, &mut result);
let mut result_table = VectorDataTable::default();
let mut result_instance = result_table.one_instance_mut();
// TODO: This leads to incorrect stroke widths when flattening groups with different transforms.
result.style.set_stroke_transform(DAffine2::IDENTITY);
result_instance.instance.style.set_stroke_transform(DAffine2::IDENTITY);
concat_group(&graphic_group_input, DAffine2::IDENTITY, &mut result_instance);
VectorDataTable::new(result)
result_table
}
pub trait ConcatElement {
@@ -434,16 +440,18 @@ pub trait ConcatElement {
impl ConcatElement for GraphicGroupTable {
fn concat(&mut self, other: &Self, transform: DAffine2, _node_id: u64) {
let own = self.one_item_mut();
let other = other.one_item();
let other_transform = other.transform();
let self_group = self.one_instance_mut().instance;
let other_group = other.one_instance().instance;
// TODO: Decide if we want to keep this behavior whereby the layers are flattened
for (mut element, footprint_mapping) in other.iter().cloned() {
*element.transform_mut() = transform * element.transform() * other.transform();
own.push((element, footprint_mapping));
for (mut element, footprint_mapping) in other_group.iter().cloned() {
*element.transform_mut() = transform * element.transform() * other_transform;
self_group.push((element, footprint_mapping));
}
own.alpha_blending = other.alpha_blending;
*self.one_instance_mut().alpha_blending = *other.one_instance().alpha_blending;
}
}
@@ -451,14 +459,16 @@ impl ConcatElement for GraphicGroupTable {
async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64, start_offset: f64, stop_offset: f64, adaptive_spacing: bool, subpath_segment_lengths: Vec<f64>) -> VectorDataTable {
// Limit the smallest spacing to something sensible to avoid freezing the application.
let spacing = spacing.max(0.01);
let vector_data = vector_data.one_item();
let vector_data_transform = vector_data.transform();
let vector_data = vector_data.one_instance().instance;
// Create an iterator over the bezier segments with enumeration and peeking capability.
let mut bezier = vector_data.segment_bezier_iter().enumerate().peekable();
// Initialize the result VectorData with the same transformation as the input.
let mut result = VectorData::empty();
result.transform = vector_data.transform;
let mut result = VectorDataTable::default();
*result.transform_mut() = vector_data_transform;
// Iterate over each segment in the bezier iterator.
while let Some((index, (segment_id, _, start_point_index, mut last_end))) = bezier.next() {
@@ -537,7 +547,7 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
// Retrieve the segment and apply transformation.
let Some(segment) = vector_data.segment_from_id(current_segment_id) else { continue };
let segment = segment.apply_transformation(|point| vector_data.transform.transform_point2(point));
let segment = segment.apply_transformation(|point| vector_data_transform.transform_point2(point));
// Calculate the position on the segment.
let parametric_t = segment.euclidean_to_parametric_with_total_length((total_distance - total_length_before) / length, 0.001, length);
@@ -545,10 +555,10 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
// Generate a new PointId and add the point to result.point_domain.
let point_id = PointId::generate();
result.point_domain.push(point_id, vector_data.transform.inverse().transform_point2(point));
result.one_instance_mut().instance.point_domain.push(point_id, vector_data_transform.inverse().transform_point2(point));
// Store the index of the point.
let point_index = result.point_domain.ids().len() - 1;
let point_index = result.one_instance_mut().instance.point_domain.ids().len() - 1;
point_indices.push(point_index);
}
@@ -565,7 +575,7 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
let stroke_id = StrokeId::generate();
// Add the segment to result.segment_domain.
result.segment_domain.push(segment_id, start_index, end_index, handles, stroke_id);
result.one_instance_mut().instance.segment_domain.push(segment_id, start_index, end_index, handles, stroke_id);
}
}
@@ -582,17 +592,17 @@ async fn sample_points(_: impl Ctx, vector_data: VectorDataTable, spacing: f64,
let stroke_id = StrokeId::generate();
// Add the closing segment to result.segment_domain.
result.segment_domain.push(segment_id, last_index, first_index, handles, stroke_id);
result.one_instance_mut().instance.segment_domain.push(segment_id, last_index, first_index, handles, stroke_id);
}
}
}
// Transfer the style from the input vector data to the result.
result.style = vector_data.style.clone();
result.style.set_stroke_transform(vector_data.transform);
result.one_instance_mut().instance.style = vector_data.style.clone();
result.one_instance_mut().instance.style.set_stroke_transform(vector_data_transform);
// Return the resulting vector data with newly generated points and segments.
VectorDataTable::new(result)
result
}
#[node_macro::node(category(""), path(graphene_core::vector))]
@@ -604,7 +614,8 @@ async fn poisson_disk_points(
separation_disk_diameter: f64,
seed: SeedValue,
) -> VectorDataTable {
let vector_data = vector_data.one_item();
let vector_data_transform = vector_data.transform();
let vector_data = vector_data.one_instance().instance;
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
let mut result = VectorData::empty();
@@ -618,7 +629,7 @@ async fn poisson_disk_points(
continue;
}
subpath.apply_transform(vector_data.transform);
subpath.apply_transform(vector_data_transform);
let mut previous_point_index: Option<usize> = None;
@@ -648,17 +659,18 @@ async fn poisson_disk_points(
#[node_macro::node(category(""), path(graphene_core::vector))]
async fn subpath_segment_lengths(_: impl Ctx, vector_data: VectorDataTable) -> Vec<f64> {
let vector_data = vector_data.one_item();
let vector_data_transform = vector_data.transform();
let vector_data = vector_data.one_instance().instance;
vector_data
.segment_bezier_iter()
.map(|(_id, bezier, _, _)| bezier.apply_transformation(|point| vector_data.transform.transform_point2(point)).length(None))
.map(|(_id, bezier, _, _)| bezier.apply_transformation(|point| vector_data_transform.transform_point2(point)).length(None))
.collect()
}
#[node_macro::node(name("Spline"), category("Vector"), path(graphene_core::vector))]
async fn spline(_: impl Ctx, mut vector_data: VectorDataTable) -> VectorDataTable {
let vector_data = vector_data.one_item_mut();
let vector_data = vector_data.one_instance_mut().instance;
// Exit early if there are no points to generate splines from.
if vector_data.point_domain.positions().is_empty() {
@@ -700,7 +712,8 @@ async fn spline(_: impl Ctx, mut vector_data: VectorDataTable) -> VectorDataTabl
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn jitter_points(_: impl Ctx, vector_data: VectorDataTable, #[default(5.)] amount: f64, seed: SeedValue) -> VectorDataTable {
let mut vector_data = vector_data.one_item().clone();
let vector_data_transform = vector_data.transform();
let mut vector_data = vector_data.one_instance().instance.clone();
let mut rng = rand::rngs::StdRng::seed_from_u64(seed.into());
@@ -718,30 +731,29 @@ async fn jitter_points(_: impl Ctx, vector_data: VectorDataTable, #[default(5.)]
if !already_applied[*start] {
let start_position = vector_data.point_domain.positions()[*start];
let start_position = vector_data.transform.transform_point2(start_position);
let start_position = vector_data_transform.transform_point2(start_position);
vector_data.point_domain.set_position(*start, start_position + start_delta);
already_applied[*start] = true;
}
if !already_applied[*end] {
let end_position = vector_data.point_domain.positions()[*end];
let end_position = vector_data.transform.transform_point2(end_position);
let end_position = vector_data_transform.transform_point2(end_position);
vector_data.point_domain.set_position(*end, end_position + end_delta);
already_applied[*end] = true;
}
match handles {
bezier_rs::BezierHandles::Cubic { handle_start, handle_end } => {
*handle_start = vector_data.transform.transform_point2(*handle_start) + start_delta;
*handle_end = vector_data.transform.transform_point2(*handle_end) + end_delta;
*handle_start = vector_data_transform.transform_point2(*handle_start) + start_delta;
*handle_end = vector_data_transform.transform_point2(*handle_end) + end_delta;
}
bezier_rs::BezierHandles::Quadratic { handle } => {
*handle = vector_data.transform.transform_point2(*handle) + (start_delta + end_delta) / 2.;
*handle = vector_data_transform.transform_point2(*handle) + (start_delta + end_delta) / 2.;
}
bezier_rs::BezierHandles::Linear => {}
}
}
vector_data.transform = DAffine2::IDENTITY;
vector_data.style.set_stroke_transform(DAffine2::IDENTITY);
VectorDataTable::new(vector_data)
@@ -757,23 +769,29 @@ async fn morph(
time: Fraction,
#[min(0.)] start_index: IntegerCount,
) -> VectorDataTable {
let source = source.one_item();
let target = target.one_item();
let mut result = VectorData::empty();
let time = time.clamp(0., 1.);
let source_alpha_blending = source.one_instance().alpha_blending;
let target_alpha_blending = target.one_instance().alpha_blending;
let source_transform = source.transform();
let target_transform = target.transform();
let source = source.one_instance().instance;
let target = target.one_instance().instance;
let mut result = VectorDataTable::default();
// Lerp styles
result.alpha_blending = if time < 0.5 { source.alpha_blending } else { target.alpha_blending };
result.style = source.style.lerp(&target.style, time);
*result.one_instance_mut().alpha_blending = if time < 0.5 { *source_alpha_blending } else { *target_alpha_blending };
result.one_instance_mut().instance.style = source.style.lerp(&target.style, time);
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 mismatched transforms
source_path.apply_transform(source.transform);
target_path.apply_transform(target.transform);
source_path.apply_transform(source_transform);
target_path.apply_transform(target_transform);
// Deal with mismatched start index
for _ in 0..start_index {
@@ -816,11 +834,12 @@ async fn morph(
manipulator.anchor = manipulator.anchor.lerp(target.anchor, time);
}
result.append_subpath(source_path, true);
result.one_instance_mut().instance.append_subpath(source_path, true);
}
// Mismatched subpath count
for mut source_path in source_paths {
source_path.apply_transform(source.transform);
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);
@@ -829,7 +848,7 @@ async fn morph(
}
}
for mut target_path in target_paths {
target_path.apply_transform(target.transform);
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);
@@ -838,10 +857,10 @@ async fn morph(
}
}
VectorDataTable::new(result)
result
}
fn bevel_algorithm(mut vector_data: VectorData, distance: f64) -> VectorData {
fn bevel_algorithm(mut vector_data: VectorData, vector_data_transform: DAffine2, distance: f64) -> VectorData {
// Splits a bézier curve based on a distance measurement
fn split_distance(bezier: bezier_rs::Bezier, distance: f64, length: f64) -> bezier_rs::Bezier {
const EUCLIDEAN_ERROR: f64 = 0.001;
@@ -885,7 +904,7 @@ fn bevel_algorithm(mut vector_data: VectorData, distance: f64) -> VectorData {
}
}
fn update_existing_segments(vector_data: &mut VectorData, distance: f64, segments_connected: &mut [u8]) -> Vec<[usize; 2]> {
fn update_existing_segments(vector_data: &mut VectorData, vector_data_transform: DAffine2, distance: f64, segments_connected: &mut [u8]) -> Vec<[usize; 2]> {
let mut next_id = vector_data.point_domain.next_id();
let mut new_segments = Vec::new();
@@ -900,8 +919,8 @@ fn bevel_algorithm(mut vector_data: VectorData, distance: f64) -> VectorData {
if bezier.is_linear() {
bezier.handles = bezier_rs::BezierHandles::Linear;
}
bezier = bezier.apply_transformation(|p| vector_data.transform.transform_point2(p));
let inverse_transform = (vector_data.transform.matrix2.determinant() != 0.).then(|| vector_data.transform.inverse()).unwrap_or_default();
bezier = bezier.apply_transformation(|p| vector_data_transform.transform_point2(p));
let inverse_transform = (vector_data_transform.matrix2.determinant() != 0.).then(|| vector_data_transform.inverse()).unwrap_or_default();
let original_length = bezier.length(None);
let mut length = original_length;
@@ -942,7 +961,7 @@ fn bevel_algorithm(mut vector_data: VectorData, distance: f64) -> VectorData {
}
let mut segments_connected = segments_connected_count(&vector_data);
let new_segments = update_existing_segments(&mut vector_data, distance, &mut segments_connected);
let new_segments = update_existing_segments(&mut vector_data, vector_data_transform, distance, &mut segments_connected);
insert_new_segments(&mut vector_data, &new_segments);
vector_data
@@ -950,22 +969,28 @@ fn bevel_algorithm(mut vector_data: VectorData, distance: f64) -> VectorData {
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
fn bevel(_: impl Ctx, source: VectorDataTable, #[default(10.)] distance: Length) -> VectorDataTable {
let source = source.one_item();
let source_transform = source.transform();
let source = source.one_instance().instance;
VectorDataTable::new(bevel_algorithm(source.clone(), distance))
let mut result = VectorDataTable::new(bevel_algorithm(source.clone(), source_transform, distance));
*result.transform_mut() = source_transform;
result
}
#[node_macro::node(category("Vector"), path(graphene_core::vector))]
async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<Context<'static>, Output = VectorDataTable>) -> f64 {
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
let vector_data = vector_data.eval(new_ctx).await;
let vector_data = vector_data.one_item();
let vector_data_transform = vector_data.transform();
let vector_data = vector_data.one_instance().instance;
let mut area = 0.;
let scale = vector_data.transform.decompose_scale();
let scale = vector_data_transform.decompose_scale();
for subpath in vector_data.stroke_bezier_paths() {
area += subpath.area(Some(1e-3), Some(1e-3));
}
area * scale[0] * scale[1]
}
@@ -973,7 +998,9 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<
async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl Node<Context<'static>, Output = VectorDataTable>, centroid_type: CentroidType) -> DVec2 {
let new_ctx = OwnedContextImpl::from(ctx).with_footprint(Footprint::default()).into_context();
let vector_data = vector_data.eval(new_ctx).await;
let vector_data = vector_data.one_item();
let vector_data_transform = vector_data.transform();
let vector_data = vector_data.one_instance().instance;
if centroid_type == CentroidType::Area {
let mut area = 0.;
@@ -990,7 +1017,7 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl N
if area != 0. {
centroid /= area;
return vector_data.transform().transform_point2(centroid);
return vector_data_transform.transform_point2(centroid);
}
}
@@ -1005,13 +1032,13 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, vector_data: impl N
if length != 0. {
centroid /= length;
return vector_data.transform().transform_point2(centroid);
return vector_data_transform.transform_point2(centroid);
}
let positions = vector_data.point_domain.positions();
if !positions.is_empty() {
let centroid = positions.iter().sum::<DVec2>() / (positions.len() as f64);
return vector_data.transform().transform_point2(centroid);
return vector_data_transform.transform_point2(centroid);
}
DVec2::ZERO
@@ -1047,7 +1074,7 @@ mod test {
let instances = 3;
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
let vector_data = vector_data.one_item();
let vector_data = vector_data.one_instance().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 3);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
@@ -1059,7 +1086,7 @@ mod test {
let instances = 8;
let repeated = super::repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE)), direction, 0., instances).await;
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
let vector_data = vector_data.one_item();
let vector_data = vector_data.one_instance().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
assert!((subpath.manipulator_groups()[0].anchor - direction * index as f64 / (instances - 1) as f64).length() < 1e-5);
@@ -1069,7 +1096,7 @@ mod test {
async fn circle_repeat() {
let repeated = super::circular_repeat(Footprint::default(), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE)), 45., 4., 8).await;
let vector_data = super::flatten_vector_elements(Footprint::default(), repeated).await;
let vector_data = vector_data.one_item();
let vector_data = vector_data.one_instance().instance;
assert_eq!(vector_data.region_bezier_paths().count(), 8);
for (index, (_, subpath)) in vector_data.region_bezier_paths().enumerate() {
let expected_angle = (index as f64 + 1.) * 45.;
@@ -1081,20 +1108,21 @@ mod test {
#[tokio::test]
async fn bounding_box() {
let bounding_box = super::bounding_box((), vector_node(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE))).await;
let bounding_box = bounding_box.one_item();
let bounding_box = bounding_box.one_instance().instance;
assert_eq!(bounding_box.region_bezier_paths().count(), 1);
let subpath = bounding_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.),]);
// Test a VectorData with non-zero rotation
let mut square = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE));
square.transform *= DAffine2::from_angle(core::f64::consts::FRAC_PI_4);
let square = VectorData::from_subpath(Subpath::new_rect(DVec2::NEG_ONE, DVec2::ONE));
let mut square = VectorDataTable::new(square);
*square.one_instance_mut().transform_mut() *= DAffine2::from_angle(core::f64::consts::FRAC_PI_4);
let bounding_box = BoundingBoxNode {
vector_data: FutureWrapperNode(VectorDataTable::new(square)),
vector_data: FutureWrapperNode(square),
}
.eval(Footprint::default())
.await;
let bounding_box = bounding_box.one_item();
let bounding_box = bounding_box.one_instance().instance;
assert_eq!(bounding_box.region_bezier_paths().count(), 1);
let subpath = bounding_box.region_bezier_paths().next().unwrap().1;
let sqrt2 = core::f64::consts::SQRT_2;
@@ -1108,7 +1136,7 @@ mod test {
let expected_points = VectorData::from_subpath(points.clone()).point_domain.positions().to_vec();
let copy_to_points = super::copy_to_points(Footprint::default(), vector_node(points), vector_node(instance), 1., 1., 0., 0, 0., 0).await;
let flattened_copy_to_points = super::flatten_vector_elements(Footprint::default(), copy_to_points).await;
let flattened_copy_to_points = flattened_copy_to_points.one_item();
let flattened_copy_to_points = flattened_copy_to_points.one_instance().instance;
assert_eq!(flattened_copy_to_points.region_bezier_paths().count(), expected_points.len());
for (index, (_, subpath)) in flattened_copy_to_points.region_bezier_paths().enumerate() {
let offset = expected_points[index];
@@ -1122,7 +1150,7 @@ mod test {
async fn sample_points() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 30., 0., 0., false, vec![100.]).await;
let sample_points = sample_points.one_item();
let sample_points = sample_points.one_instance().instance;
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}");
@@ -1132,7 +1160,7 @@ mod test {
async fn adaptive_spacing() {
let path = Subpath::from_bezier(&Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::X * 100., DVec2::X * 100.));
let sample_points = super::sample_points(Footprint::default(), vector_node(path), 18., 45., 10., true, vec![100.]).await;
let sample_points = sample_points.one_item();
let sample_points = sample_points.one_instance().instance;
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}");
@@ -1147,7 +1175,7 @@ mod test {
0,
)
.await;
let sample_points = sample_points.one_item();
let sample_points = sample_points.one_instance().instance;
assert!(
(20..=40).contains(&sample_points.point_domain.positions().len()),
"actual len {}",
@@ -1166,7 +1194,7 @@ mod test {
#[tokio::test]
async fn spline() {
let spline = super::spline(Footprint::default(), vector_node(Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.))).await;
let spline = spline.one_item();
let spline = spline.one_instance().instance;
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.)]);
}
@@ -1175,7 +1203,7 @@ mod test {
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
let target = Subpath::new_ellipse(DVec2::NEG_ONE * 100., DVec2::ZERO);
let sample_points = super::morph(Footprint::default(), vector_node(source), vector_node(target), 0.5, 0).await;
let sample_points = sample_points.one_item();
let sample_points = sample_points.one_instance().instance;
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.)]
@@ -1186,14 +1214,19 @@ mod test {
fn contains_segment(vector: VectorData, target: bezier_rs::Bezier) {
let segments = vector.segment_bezier_iter().map(|x| x.1);
let count = segments.filter(|bezier| bezier.abs_diff_eq(&target, 0.01) || bezier.reversed().abs_diff_eq(&target, 0.01)).count();
assert_eq!(count, 1, "Incorrect number of {target:#?} in {:#?}", vector.segment_bezier_iter().collect::<Vec<_>>());
assert_eq!(
count,
1,
"Expected exactly one matching segment for {target:?}, but found {count}. The given segments are: {:#?}",
vector.segment_bezier_iter().collect::<Vec<_>>()
);
}
#[tokio::test]
async fn bevel_rect() {
let source = Subpath::new_rect(DVec2::ZERO, DVec2::ONE * 100.);
let beveled = super::bevel(Footprint::default(), vector_node(source), 5.);
let beveled = beveled.one_item();
let beveled = beveled.one_instance().instance;
assert_eq!(beveled.point_domain.positions().len(), 8);
assert_eq!(beveled.segment_domain.ids().len(), 8);
@@ -1216,7 +1249,7 @@ mod test {
let curve = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::new(10., 0.), DVec2::new(10., 100.), DVec2::X * 100.);
let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), curve], false);
let beveled = super::bevel((), vector_node(source), 5.);
let beveled = beveled.one_item();
let beveled = beveled.one_instance().instance;
assert_eq!(beveled.point_domain.positions().len(), 4);
assert_eq!(beveled.segment_domain.ids().len(), 3);
@@ -1232,32 +1265,34 @@ mod test {
#[tokio::test]
async fn bevel_with_transform() {
let curve = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::new(1., 0.), DVec2::new(1., 10.), DVec2::X * 10.);
let source = Subpath::<PointId>::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -10., DVec2::ZERO), curve], false);
let mut vector_data = VectorData::from_subpath(source);
let curve = Bezier::from_cubic_dvec2(DVec2::new(0., 0.), DVec2::new(1., 0.), DVec2::new(1., 10.), DVec2::new(10., 0.));
let source = Subpath::<PointId>::from_beziers(&[Bezier::from_linear_dvec2(DVec2::new(-10., 0.), DVec2::ZERO), curve], false);
let vector_data = VectorData::from_subpath(source);
let mut vector_data_table = VectorDataTable::new(vector_data.clone());
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.));
vector_data.transform = transform;
*vector_data_table.one_instance_mut().transform_mut() = transform;
let beveled = super::bevel((), VectorDataTable::new(vector_data), 5.);
let beveled = beveled.one_item();
let beveled = beveled.one_instance().instance;
assert_eq!(beveled.point_domain.positions().len(), 4);
assert_eq!(beveled.segment_domain.ids().len(), 3);
assert_eq!(beveled.transform, transform);
// Segments
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-0.5, 0.), DVec2::new(-10., 0.)));
let trimmed = curve.trim(bezier_rs::TValue::Euclidean(0.5 / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), DVec2::new(-10., 0.)));
let trimmed = curve.trim(bezier_rs::TValue::Euclidean(5. / curve.length(Some(0.00001))), bezier_rs::TValue::Parametric(1.));
contains_segment(beveled.clone(), trimmed);
// Join
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-0.5, 0.), trimmed.start));
contains_segment(beveled.clone(), bezier_rs::Bezier::from_linear_dvec2(DVec2::new(-5., 0.), trimmed.start));
}
#[tokio::test]
async fn bevel_too_high() {
let source = Subpath::from_anchors([DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)], false);
let beveled = super::bevel(Footprint::default(), vector_node(source), 999.);
let beveled = beveled.one_item();
let beveled = beveled.one_instance().instance;
assert_eq!(beveled.point_domain.positions().len(), 6);
assert_eq!(beveled.segment_domain.ids().len(), 5);
@@ -1278,7 +1313,7 @@ mod test {
let point = Bezier::from_cubic_dvec2(DVec2::ZERO, DVec2::ZERO, DVec2::ZERO, DVec2::ZERO);
let source = Subpath::from_beziers(&[Bezier::from_linear_dvec2(DVec2::X * -100., DVec2::ZERO), point, curve], false);
let beveled = super::bevel(Footprint::default(), vector_node(source), 5.);
let beveled = beveled.one_item();
let beveled = beveled.one_instance().instance;
assert_eq!(beveled.point_domain.positions().len(), 6);
assert_eq!(beveled.segment_domain.ids().len(), 5);