diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index 128f1ff515a..399f573bdca 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -909,6 +909,23 @@ impl Attributes { attribute.0[index] = value; } + /// Replaces the attribute for the given key with one value per item, keeping its place unless its type changes. + fn set_attribute_values(&mut self, key: String, values: Vec) { + // An attribute changing type moves to the end, as it does when set one value at a time + let existing = self.attributes.iter().position(|(k, _)| k == &key); + let same_type = existing.is_some_and(|position| (*self.attributes[position].1).as_any().downcast_ref::>().is_some()); + + let attribute: Box = Box::new(Attribute(values)); + match existing { + Some(position) if same_type => self.attributes[position].1 = attribute, + Some(position) => { + self.attributes.remove(position); + self.attributes.push((key, attribute)); + } + None => self.attributes.push((key, attribute)), + } + } + /// Returns a debug-formatted string for the value at the given index in the attribute for the given key. fn display_value(&self, key: &str, index: usize, overrides: fn(&dyn std::any::Any) -> Option) -> Option { self.attributes.iter().find_map(|(k, attribute)| { @@ -1029,6 +1046,15 @@ impl List { } } + /// Creates a list holding the given elements in order, with no attributes. + pub fn new_from_elements(elements: Vec) -> Self { + let len = elements.len(); + Self { + element: elements, + attributes: Attributes::with_len(len), + } + } + /// Creates a list containing a single item from the given [`Item`], preserving its attributes. pub fn new_from_item(item: Item) -> Self { let mut attributes = Attributes::new(); @@ -1140,6 +1166,18 @@ impl List { self.attributes.set_value(key, index, value); } + /// Sets the attribute's value for every item at once from one value per item, replacing any values the key held (an empty list still gains the key). + /// Logs an error and leaves the list unchanged if the number of values differs from the number of items. + pub fn set_attribute_values(&mut self, key: impl Into, values: Vec) { + let key = key.into(); + if values.len() != self.element.len() { + log::error!("Attribute '{key}' was given {} values for a list of {} items", values.len(), self.element.len()); + return; + } + + self.attributes.set_attribute_values(key, values); + } + /// Sets a single type-erased attribute value at the given index, creating the attribute from the value's underlying type if it doesn't exist (padded with defaults to match the list's length). /// Falls back to default if the value's type doesn't match an existing attribute. pub fn set_attribute_value_dyn(&mut self, key: impl Into, index: usize, value: AttributeValueDyn) { @@ -1577,4 +1615,29 @@ mod tests { other.push(Item::new_from_element(()).with_attribute(ATTR_START, 5_u64)); assert_eq!(other.attribute_cloned_or_default::(ATTR_START, 0), 0); } + + // Whole-attribute writes replace an existing attribute in place and refuse a value count that doesn't match + #[test] + fn set_attribute_values_writes_every_item_at_once() { + let mut list = List::new_from_elements(vec![10_u32, 20, 30]); + list.set_attribute(ATTR_START, 0, 7_u64); + list.set_attribute_values(ATTR_END, vec![1_u64, 2, 3]); + list.set_attribute_values(ATTR_START, vec![4_u64, 5, 6]); + + assert_eq!(list.len(), 3); + assert_eq!(list.attribute_keys().collect::>(), [ATTR_START, ATTR_END]); + assert_eq!(list.iter_attribute_values::(ATTR_START).unwrap().copied().collect::>(), [4, 5, 6]); + + list.set_attribute_values(ATTR_END, vec![9_u64]); + assert_eq!(list.iter_attribute_values::(ATTR_END).unwrap().copied().collect::>(), [1, 2, 3]); + + // Changing an attribute's type moves it last, whichever way it is written + let mut one_at_a_time = list.clone(); + for index in 0..3 { + one_at_a_time.set_attribute(ATTR_START, index, index as f64); + } + list.set_attribute_values(ATTR_START, vec![0., 1., 2.]); + assert_eq!(list.attribute_keys().collect::>(), [ATTR_END, ATTR_START]); + assert_eq!(list, one_at_a_time); + } } diff --git a/node-graph/libraries/vector-types/src/vector/addressing.rs b/node-graph/libraries/vector-types/src/vector/addressing.rs index 7481e3d4c61..4f25b09f786 100644 --- a/node-graph/libraries/vector-types/src/vector/addressing.rs +++ b/node-graph/libraries/vector-types/src/vector/addressing.rs @@ -86,13 +86,11 @@ pub fn spans_at(pool_index: u32, paths: &List, connectivity: &Connectivity let mut found = Vec::new(); for reference in connectivity.references(pool_index) { - let path_index = reference.path as usize; - let Some(path) = paths.element(path_index) else { continue }; - - let curve_type = curve_type_at(paths, path_index); - if !path.is_anchor_reference(reference.index as usize, curve_type) { + let Some(path) = paths.element(reference.path as usize) else { continue }; + if !reference.is_anchor() { continue; } + let curve_type = reference.curve_type; // An anchor arrives on the span before it and departs the one starting at it let anchor = reference.index as usize / curve_type.stride(); diff --git a/node-graph/libraries/vector-types/src/vector/connectivity.rs b/node-graph/libraries/vector-types/src/vector/connectivity.rs index 1848b70debb..9938aa9f76a 100644 --- a/node-graph/libraries/vector-types/src/vector/connectivity.rs +++ b/node-graph/libraries/vector-types/src/vector/connectivity.rs @@ -1,4 +1,4 @@ -use super::path::{Path, curve_type_at}; +use super::path::{CurveType, Path, curve_type_at}; use core_types::list::List; /// One path's reference to a pool point, naming where in the weave that point is used. @@ -6,6 +6,17 @@ use core_types::list::List; pub struct Reference { pub path: u32, pub index: u32, + /// The curve type of the path holding this reference. Its stride decides whether the reference names an anchor. + pub curve_type: CurveType, + /// The segment ends radiating from this reference: none for a handle or a path's only reference, one at a path end, two along a path. + pub spokes: u8, +} + +impl Reference { + /// Whether this reference sits at an anchor slot of its path rather than a handle slot. + pub fn is_anchor(&self) -> bool { + (self.index as usize).is_multiple_of(self.curve_type.stride()) + } } /// Reverse index from each pool point to the references naming it. @@ -39,11 +50,21 @@ impl Connectivity { let mut cursors = starts[..point_count].to_vec(); let mut references = vec![Reference::default(); starts[point_count] as usize]; for (path_index, path) in paths.iter_element_values().enumerate() { + // Each path's curve type is read once here, so queries never look it up again + let curve_type = curve_type_at(paths, path_index); + let last = path.0.len().saturating_sub(1); + for (reference_index, point) in path.references().enumerate() { let Some(cursor) = cursors.get_mut(point as usize) else { continue }; + + let anchor = reference_index.is_multiple_of(curve_type.stride()); + let spokes = if anchor { u8::from(reference_index > 0) + u8::from(reference_index < last) } else { 0 }; + references[*cursor as usize] = Reference { path: path_index as u32, index: reference_index as u32, + curve_type, + spokes, }; *cursor += 1; } @@ -66,27 +87,21 @@ impl Connectivity { !self.references(point).is_empty() } + /// Whether this point is a handle, named by paths only at handle slots. + pub fn is_handle(&self, point: u32) -> bool { + let references = self.references(point); + !references.is_empty() && !references.iter().any(Reference::is_anchor) + } + /// The segment ends meeting at this point: 1 at a free end, 2 along a chain, 3 or more at a junction. /// Handle references contribute none, since only anchors have segments radiating from them. - pub fn spokes(&self, point: u32, paths: &List) -> usize { - self.references(point) - .iter() - .filter_map(|reference| { - let path = paths.element(reference.path as usize)?; - if !path.is_anchor_reference(reference.index as usize, curve_type_at(paths, reference.path as usize)) { - return None; - } - - let last = path.0.len().saturating_sub(1) as u32; - Some(usize::from(reference.index > 0) + usize::from(reference.index < last)) - }) - .sum() + pub fn spokes(&self, point: u32) -> usize { + self.references(point).iter().map(|reference| usize::from(reference.spokes)).sum() } } #[cfg(test)] mod tests { - use super::super::path::CurveType; use super::*; use core_types::list::{ATTR_CURVE_TYPE, Item}; @@ -101,6 +116,10 @@ mod tests { paths } + fn places(connectivity: &Connectivity, point: u32) -> Vec<(u32, u32)> { + connectivity.references(point).iter().map(|reference| (reference.path, reference.index)).collect() + } + // A closed circle crossed by a chord welded at two of its anchors, plus two points no path names #[test] fn spokes_at_welds_and_free_points() { @@ -108,14 +127,17 @@ mod tests { let connectivity = Connectivity::build(15, &paths); // The seam contributes two spokes and the chord a third - assert_eq!(connectivity.spokes(0, &paths), 3); - assert_eq!(connectivity.spokes(6, &paths), 3); + assert_eq!(connectivity.spokes(0), 3); + assert_eq!(connectivity.spokes(6), 3); - assert_eq!(connectivity.spokes(3, &paths), 2); - assert_eq!(connectivity.spokes(12, &paths), 2); + assert_eq!(connectivity.spokes(3), 2); + assert_eq!(connectivity.spokes(12), 2); // Handles carry no segments, and unreferenced points are free - assert_eq!(connectivity.spokes(1, &paths), 0); + assert_eq!(connectivity.spokes(1), 0); + assert!(connectivity.is_handle(1)); + assert!(!connectivity.is_handle(0)); + assert!(!connectivity.is_handle(13)); assert!(!connectivity.is_referenced(13)); assert!(connectivity.is_referenced(12)); } @@ -125,9 +147,9 @@ mod tests { let paths = paths(&[(CurveType::Linear, &[0, 1, 2]), (CurveType::Linear, &[2, 3])]); let connectivity = Connectivity::build(4, &paths); - assert_eq!(connectivity.references(0), [Reference { path: 0, index: 0 }]); - assert_eq!(connectivity.references(2), [Reference { path: 0, index: 2 }, Reference { path: 1, index: 0 }]); - assert_eq!(connectivity.spokes(2, &paths), 2); + assert_eq!(places(&connectivity, 0), [(0, 0)]); + assert_eq!(places(&connectivity, 2), [(0, 2), (1, 0)]); + assert_eq!(connectivity.spokes(2), 2); } // References falling anywhere outside the pool are ignored, including one exactly at its end @@ -136,10 +158,10 @@ mod tests { let paths = paths(&[(CurveType::Linear, &[0, 3, 1, 4])]); let connectivity = Connectivity::build(3, &paths); - assert_eq!(connectivity.references(0), [Reference { path: 0, index: 0 }]); - assert_eq!(connectivity.references(1), [Reference { path: 0, index: 2 }]); - assert_eq!(connectivity.references(3), []); - assert_eq!(connectivity.references(4), []); + assert_eq!(places(&connectivity, 0), [(0, 0)]); + assert_eq!(places(&connectivity, 1), [(0, 2)]); + assert!(places(&connectivity, 3).is_empty()); + assert!(places(&connectivity, 4).is_empty()); } // An open end has one spoke, which is what makes it a cap site @@ -148,8 +170,8 @@ mod tests { let paths = paths(&[(CurveType::Linear, &[0, 1, 2])]); let connectivity = Connectivity::build(3, &paths); - assert_eq!(connectivity.spokes(0, &paths), 1); - assert_eq!(connectivity.spokes(2, &paths), 1); - assert_eq!(connectivity.spokes(1, &paths), 2); + assert_eq!(connectivity.spokes(0), 1); + assert_eq!(connectivity.spokes(2), 1); + assert_eq!(connectivity.spokes(1), 2); } } diff --git a/node-graph/libraries/vector-types/src/vector/contour.rs b/node-graph/libraries/vector-types/src/vector/contour.rs index c3be65e84b2..10c72301000 100644 --- a/node-graph/libraries/vector-types/src/vector/contour.rs +++ b/node-graph/libraries/vector-types/src/vector/contour.rs @@ -1,5 +1,5 @@ use super::misc::dvec2_to_point; -use super::path::{Path, append_segment, curve_type_at}; +use super::path::{CurveType, Path, append_segment, curve_type_at}; use core_types::list::List; use glam::DVec2; use kurbo::BezPath; @@ -100,17 +100,26 @@ impl SpanEnds { /// then the rest from the lowest point with spans left. A walk follows the earliest woven span waiting at each /// point, backwards when that span ends there, and closes on arriving back where it began. pub fn subpaths(point_count: usize, paths: &List) -> Vec { + subpaths_with(point_count, paths, &curve_types(paths)) +} + +/// The curve type of every path, read once so the walkers below never look one up per span. +fn curve_types(paths: &List) -> Vec { + (0..paths.len()).map(|index| curve_type_at(paths, index)).collect() +} + +fn subpaths_with(point_count: usize, paths: &List, curve_types: &[CurveType]) -> Vec { let mut edges = Vec::new(); for (path_index, path) in paths.iter_element_values().enumerate() { - let curve_type = curve_type_at(paths, path_index); + let curve_type = curve_types.get(path_index).copied().unwrap_or_default(); let stride = curve_type.stride(); + let references = path.0.iter_element_values().as_slice(); for span in 0..path.segment_count(curve_type) { - let references = (span * stride..=(span + 1) * stride).map(|index| path.reference(index)); - let Some(references) = references.collect::>>() else { continue }; + let Some(references) = references.get(span * stride..=(span + 1) * stride) else { continue }; // A span naming a point outside the pool cannot be drawn, so it takes no part in the walk - if references.iter().any(|&reference| reference as usize >= point_count) { + if references.iter().any(|&reference| !(0..point_count as i64).contains(&reference)) { continue; } let (Some(&start), Some(&end)) = (references.first(), references.last()) else { continue }; @@ -118,8 +127,8 @@ pub fn subpaths(point_count: usize, paths: &List) -> Vec { edges.push(Edge { path: path_index as u32, span: span as u32, - start, - end, + start: start as u32, + end: end as u32, }); } } @@ -174,12 +183,16 @@ pub fn subpaths(point_count: usize, paths: &List) -> Vec { /// Emits one subpath as a bezpath, or `None` when it has no drawable geometry. pub fn subpath_to_bezpath(pool: &List, paths: &List, subpath: &Subpath) -> Option { + subpath_to_bezpath_with(pool, paths, &curve_types(paths), subpath) +} + +fn subpath_to_bezpath_with(pool: &List, paths: &List, curve_types: &[CurveType], subpath: &Subpath) -> Option { let mut bezpath = BezPath::new(); bezpath.move_to(dvec2_to_point(*pool.element(subpath.entry as usize)?)); for step in &subpath.steps { let path = paths.element(step.path as usize)?; - let segment = path.segment(pool, curve_type_at(paths, step.path as usize), step.span as usize)?; + let segment = path.segment(pool, *curve_types.get(step.path as usize)?, step.span as usize)?; append_segment(&mut bezpath, if step.reversed { segment.reverse() } else { segment }); } @@ -193,7 +206,11 @@ pub fn subpath_to_bezpath(pool: &List, paths: &List, subpath: &Subp /// Assembles the paths into contours for rendering, one bezpath per subpath in the order they are walked. pub fn contours(pool: &List, paths: &List) -> Vec { - subpaths(pool.len(), paths).iter().filter_map(|subpath| subpath_to_bezpath(pool, paths, subpath)).collect() + let curve_types = curve_types(paths); + subpaths_with(pool.len(), paths, &curve_types) + .iter() + .filter_map(|subpath| subpath_to_bezpath_with(pool, paths, &curve_types, subpath)) + .collect() } #[cfg(test)] diff --git a/node-graph/libraries/vector-types/src/vector/flatten.rs b/node-graph/libraries/vector-types/src/vector/flatten.rs index 75cb09556be..f8734ffda2d 100644 --- a/node-graph/libraries/vector-types/src/vector/flatten.rs +++ b/node-graph/libraries/vector-types/src/vector/flatten.rs @@ -1,11 +1,10 @@ -use super::addressing; use super::misc::{BezierHandles, HandleId, HandleType, segment_to_handles}; use super::path::{CurveType, Path, curve_type_at, push_path}; use super::vector_attributes::{PointId, SegmentId}; use super::vector_types::Vector; -use core_types::list::{ATTR_ID, Item, List}; +use core_types::list::{ATTR_ID, List}; use glam::DVec2; -use std::collections::HashMap; +use rustc_hash::{FxHashMap, FxHashSet}; /// One path under construction: its curve type, the pool references it weaves, and the IDs of the spans between them. type Run = (CurveType, Vec, Vec); @@ -29,50 +28,59 @@ impl WorkingVector { pub fn flatten(vector: &Vector, paths: &List) -> Self { let mut working = Self::default(); let connectivity = vector.connectivity(paths); + let pool_ids = vector.0.iter_attribute_values::(ATTR_ID).map(|ids| ids.as_slice()).unwrap_or_default(); let mut pool_to_working = vec![None; vector.point_count()]; for (index, slot) in pool_to_working.iter_mut().enumerate() { - if !vector.is_handle_point(paths, &connectivity, index as u32) { + if !connectivity.is_handle(index as u32) { *slot = Some(working.point_ids.len()); - working.point_ids.push(vector.point_id_at(index as u32).unwrap_or_default()); + working.point_ids.push(pool_ids.get(index).copied().unwrap_or_default()); working.point_positions.push(vector.position(index).unwrap_or_default()); } } + let working_point = |pool_index: u32| pool_to_working.get(pool_index as usize).copied().flatten(); // One pass so a span that fails to resolve leaves every array at the same length - for span in addressing::spans(paths) { - let Some(path) = paths.element(span.path as usize) else { continue }; - let Some(segment) = path.segment(&vector.0, curve_type_at(paths, span.path as usize), span.index as usize) else { - continue; - }; - let Some((start, end)) = addressing::span_endpoints(paths, span) else { continue }; - let working_point = |pool_index: u32| pool_to_working.get(pool_index as usize).copied().flatten(); - let (Some(start), Some(end)) = (working_point(start), working_point(end)) else { continue }; - - working.segment_ids.push(addressing::segment_id_of(paths, span).unwrap_or_default()); - working.segment_handles.push(segment_to_handles(&segment)); - working.segment_start.push(start); - working.segment_end.push(end); + for (path_index, path) in paths.iter_element_values().enumerate() { + let curve_type = curve_type_at(paths, path_index); + let stride = curve_type.stride(); + let span_ids = path.0.iter_attribute_values::(ATTR_ID).map(|ids| ids.as_slice()).unwrap_or_default(); + + for span in 0..path.segment_count(curve_type) { + let Some(segment) = path.segment(&vector.0, curve_type, span) else { continue }; + let (Some(start), Some(end)) = (path.reference(span * stride), path.reference(span * stride + stride)) else { + continue; + }; + let (Some(start), Some(end)) = (working_point(start), working_point(end)) else { continue }; + + working.segment_ids.push(span_ids.get(span * stride).copied().unwrap_or_default()); + working.segment_handles.push(segment_to_handles(&segment)); + working.segment_start.push(start); + working.segment_end.push(end); + } } - working.colinear = vector.colinear_pairs(paths); + working.colinear = vector.colinear_pairs_with(paths, &connectivity); working } /// Weaves the working form back into a pool and paths, gathering consecutive segments that meet head to tail into one path. pub fn reweave(self, vector: &mut Vector, paths: &mut List) { - *vector = Vector::default(); *paths = List::new(); - for (index, (&id, &position)) in self.point_ids.iter().zip(&self.point_positions).enumerate() { - vector.0.push(Item::new_from_element(position)); - vector.0.set_attribute(ATTR_ID, index, id); - } - let anchor_count = vector.0.len(); + // The pool is built as plain arrays and handed to the list whole, anchors first and each segment's handles after + let anchor_count = self.point_ids.len().min(self.point_positions.len()); + let mut positions = self.point_positions; + positions.truncate(anchor_count); + let mut point_ids = self.point_ids; + point_ids.truncate(anchor_count); let mut runs: Vec = Vec::new(); - let mut segment_anchors = HashMap::new(); + + // Only segments a G1 pair names need their anchors recorded, to narrow the pairs to smooth flags below + let paired: FxHashSet = self.colinear.iter().flat_map(|pair| pair.map(|handle| handle.segment)).collect(); + let mut segment_anchors = FxHashMap::default(); let ends = self.segment_start.iter().zip(&self.segment_end); for ((&id, (&start, &end)), handles) in self.segment_ids.iter().zip(ends).zip(&self.segment_handles) { @@ -97,13 +105,21 @@ impl WorkingVector { let Some((_, references, ids)) = runs.last_mut() else { continue }; for position in handle_positions { - references.push(vector.0.len() as u32); - vector.0.push(Item::new_from_element(position).with_attribute(ATTR_ID, PointId::UNNAMED)); + references.push(positions.len() as u32); + positions.push(position); + point_ids.push(PointId::UNNAMED); } references.push(end as u32); ids.push(id); - segment_anchors.insert(id, (start as u32, end as u32)); + if paired.contains(&id) { + segment_anchors.insert(id, (start as u32, end as u32)); + } + } + + vector.0 = List::new_from_elements(positions); + if !point_ids.is_empty() { + vector.0.set_attribute_values(ATTR_ID, point_ids); } for (curve_type, references, ids) in runs { @@ -157,7 +173,7 @@ pub fn vector_from_legacy_parts( #[cfg(test)] mod tests { use super::*; - use core_types::list::{ATTR_CURVE_TYPE, ATTR_ID}; + use core_types::list::{ATTR_CURVE_TYPE, ATTR_ID, Item}; fn working(points: usize, segments: &[(usize, usize, BezierHandles)]) -> WorkingVector { WorkingVector { diff --git a/node-graph/libraries/vector-types/src/vector/path.rs b/node-graph/libraries/vector-types/src/vector/path.rs index 00c546fa97c..3e55529aec9 100644 --- a/node-graph/libraries/vector-types/src/vector/path.rs +++ b/node-graph/libraries/vector-types/src/vector/path.rs @@ -50,7 +50,7 @@ pub struct Path(pub List); impl Path { /// Builds a path from pool indices, with no per-reference attributes. pub fn from_references(references: impl IntoIterator) -> Self { - Self(references.into_iter().map(|reference| Item::new_from_element(i64::from(reference))).collect()) + Self(List::new_from_elements(references.into_iter().map(i64::from).collect())) } /// The pool index named by the reference at `index`, or `None` if out of bounds. @@ -69,11 +69,6 @@ impl Path { len > 1 && self.0.element(0) == self.0.element(len - 1) } - /// Whether the reference at the given index is an anchor (rather than a handle) under the given curve type. - pub fn is_anchor_reference(&self, index: usize, curve_type: CurveType) -> bool { - index.is_multiple_of(curve_type.stride()) - } - /// The number of segments this path draws, one fewer than its anchor count when open and equal to it when closed. pub fn segment_count(&self, curve_type: CurveType) -> usize { self.0.len().saturating_sub(1) / curve_type.stride() @@ -138,9 +133,14 @@ impl Path { pub fn push_path(paths: &mut List, curve_type: CurveType, references: impl IntoIterator, ids: impl IntoIterator) { let mut path = Path::from_references(references); - let span_count = path.segment_count(curve_type); - for (span, id) in ids.into_iter().enumerate().take(span_count) { - path.0.set_attribute(ATTR_ID, span * curve_type.stride(), id); + let stride = curve_type.stride(); + let mut ids = ids.into_iter().take(path.segment_count(curve_type)).peekable(); + if ids.peek().is_some() { + let mut span_ids = vec![SegmentId::default(); path.0.len()]; + for (span, id) in ids.enumerate() { + span_ids[span * stride] = id; + } + path.0.set_attribute_values(ATTR_ID, span_ids); } paths.push(Item::new_from_element(path).with_attribute(ATTR_CURVE_TYPE, curve_type)); @@ -180,6 +180,7 @@ pub fn curve_type_at(paths: &List, index: usize) -> CurveType { /// path keeps one stride. The runs of a split subpath share the anchor they meet at, welding them. struct PathWeaver { positions: Vec, + anchors: Vec, offset: u32, woven: Vec<(CurveType, Vec)>, references: Vec, @@ -191,6 +192,7 @@ impl PathWeaver { fn new(offset: u32) -> Self { Self { positions: Vec::new(), + anchors: Vec::new(), offset, woven: Vec::new(), references: Vec::new(), @@ -199,9 +201,10 @@ impl PathWeaver { } } - /// Adds a point to the pool being built and returns the reference naming it. - fn push_point(&mut self, point: Point) -> u32 { + /// Adds an anchor or handle point to the pool being built and returns the reference naming it. + fn push_point(&mut self, point: Point, anchor: bool) -> u32 { self.positions.push(point_to_dvec2(point)); + self.anchors.push(anchor); self.offset + self.positions.len() as u32 - 1 } @@ -248,7 +251,7 @@ impl PathWeaver { match element { PathEl::MoveTo(point) => { self.flush(); - let reference = self.push_point(point); + let reference = self.push_point(point, true); self.subpath_start = Some(reference); self.references.push(reference); } @@ -264,8 +267,9 @@ impl PathWeaver { return; } - for &point in points { - let reference = self.push_point(point); + // Every point but the last is a handle leading to the anchor that ends the segment + for (index, &point) in points.iter().enumerate() { + let reference = self.push_point(point, index + 1 == points.len()); self.references.push(reference); } } @@ -286,6 +290,7 @@ impl PathWeaver { self.references.push(start); if last as usize + 1 == self.offset as usize + self.positions.len() { self.positions.pop(); + self.anchors.pop(); } } else if self.begin_segment(CurveType::Linear) { self.references.push(start); @@ -294,29 +299,35 @@ impl PathWeaver { self.flush(); } - fn finish(mut self, pool: &mut List, paths: &mut List) { + /// Appends the woven points and paths, returning whether each appended point is an anchor rather than a handle. + fn finish(mut self, pool: &mut List, paths: &mut List) -> Vec { self.flush(); - for position in self.positions { - pool.push(Item::new_from_element(position)); - } + pool.extend(List::new_from_elements(self.positions)); for (curve_type, references) in self.woven { let path = Path::from_references(references); paths.push(Item::new_from_element(path).with_attribute(ATTR_CURVE_TYPE, curve_type)); } + + self.anchors } } /// Appends a bezpath's contours to a point pool and its paths. pub fn append_bezpath(pool: &mut List, paths: &mut List, bezpath: &BezPath) { + weave_bezpath(pool, paths, bezpath); +} + +/// Appends a bezpath's contours to a point pool and its paths, returning whether each appended point is an anchor. +pub(crate) fn weave_bezpath(pool: &mut List, paths: &mut List, bezpath: &BezPath) -> Vec { let mut weaver = PathWeaver::new(pool.len() as u32); for &element in bezpath.elements() { weaver.push_element(element); } - weaver.finish(pool, paths); + weaver.finish(pool, paths) } /// Builds a fresh point pool and its paths from a bezpath. @@ -365,19 +376,6 @@ mod tests { ); } - // Anchor occurrences fall on stride multiples, and the references between them are handles - #[test] - fn anchor_references_by_stride() { - let path = path(&[0, 1, 2, 3]); - - assert!(path.is_anchor_reference(0, CurveType::Cubic)); - assert!(!path.is_anchor_reference(1, CurveType::Cubic)); - assert!(!path.is_anchor_reference(2, CurveType::Cubic)); - assert!(path.is_anchor_reference(3, CurveType::Cubic)); - assert!(path.is_anchor_reference(2, CurveType::Quadratic)); - assert!(path.is_anchor_reference(2, CurveType::Linear)); - } - // A reference naming a point the pool does not have drops its segment instead of panicking #[test] fn dangling_reference_skips_its_segment() { diff --git a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index 7a799d0e7be..1450a6ece55 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -1,9 +1,9 @@ use crate::vector::addressing::{self, Span}; use crate::vector::contour::subpaths; use crate::vector::misc::{ManipulatorGroup, Tangent, point_to_dvec2, segment_to_handles}; -use crate::vector::path::{Path, append_segment, curve_type_at}; +use crate::vector::path::{CurveType, Path, append_segment, curve_type_at}; use crate::vector::vector_types::Vector; -use core_types::list::{Item, List}; +use core_types::list::{ATTR_ID, Item, List}; use dyn_any::DynAny; use fixedbitset::FixedBitSet; use glam::DVec2; @@ -589,8 +589,23 @@ impl SegmentGraph { impl Vector { /// Whether any point has more than two segment ends meeting at it, making the vector a mesh. pub fn is_branching(&self, paths: &List) -> bool { - let connectivity = self.connectivity(paths); - (0..self.point_count() as u32).any(|point| connectivity.spokes(point, paths) > 2) + // Tally the segment ends at each anchor in one pass over the references, stopping at the first junction + let mut spokes = vec![0_u8; self.point_count()]; + for (path_index, path) in paths.iter_element_values().enumerate() { + let stride = curve_type_at(paths, path_index).stride(); + let references = path.0.iter_element_values().as_slice(); + let last = references.len().saturating_sub(1); + + for (index, &point) in references.iter().enumerate().step_by(stride) { + let Some(count) = spokes.get_mut(point as usize) else { continue }; + *count = count.saturating_add(u8::from(index > 0) + u8::from(index < last)); + if *count > 2 { + return true; + } + } + } + + false } /// Determines if face-by-face fill rendering should be used. Branching vectors are meshes, whose @@ -735,17 +750,23 @@ impl Vector { pub fn stroke_manipulator_groups(&self, paths: &List) -> impl Iterator, bool)> { let mut groups = Vec::new(); + let curve_types: Vec = (0..paths.len()).map(|index| curve_type_at(paths, index)).collect(); + let ids = self.0.iter_attribute_values::(ATTR_ID).map(|ids| ids.as_slice()).unwrap_or_default(); + let id_at = |point: u32| ids.get(point as usize).copied().unwrap_or_default(); + for subpath in subpaths(self.point_count(), paths) { let mut manipulators_list: Vec = Vec::new(); let mut in_handle: Option = None; for step in &subpath.steps { - let span = Span { path: step.path, index: step.span }; let Some(path) = paths.element(step.path as usize) else { continue }; - let Some(segment) = path.segment(&self.0, curve_type_at(paths, step.path as usize), step.span as usize) else { + let Some(&curve_type) = curve_types.get(step.path as usize) else { continue }; + let Some(segment) = path.segment(&self.0, curve_type, step.span as usize) else { continue }; + + let first = step.span as usize * curve_type.stride(); + let (Some(start), Some(end)) = (path.reference(first), path.reference(first + curve_type.stride())) else { continue; }; - let Some((start, end)) = addressing::span_endpoints(paths, span) else { continue }; // A quadratic's one handle leaves its anchor whichever way the span is crossed let handles = segment_to_handles(&segment); @@ -756,7 +777,7 @@ impl Vector { anchor: point_to_dvec2(if step.reversed { segment.end() } else { segment.start() }), in_handle, out_handle: handles.start(), - id: self.point_id_at(anchor).unwrap_or_default(), + id: id_at(anchor), }); in_handle = handles.end(); @@ -771,7 +792,7 @@ impl Vector { anchor: position, in_handle, out_handle: None, - id: self.point_id_at(subpath.exit).unwrap_or_default(), + id: id_at(subpath.exit), }); } diff --git a/node-graph/libraries/vector-types/src/vector/vector_modification.rs b/node-graph/libraries/vector-types/src/vector/vector_modification.rs index 743831087f1..c9c8dc80cb6 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_modification.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_modification.rs @@ -1,11 +1,12 @@ use super::*; use crate::vector::flatten::WorkingVector; use crate::vector::misc::{BezierHandles, HandleId, HandleType}; -use crate::vector::path::Path; -use core_types::list::List; +use crate::vector::path::{CurveType, Path, curve_type_at}; +use core_types::list::{ATTR_ID, List}; use core_types::uuid::generate_uuid; use dyn_any::DynAny; use glam::DVec2; +use rustc_hash::{FxHashMap, FxHashSet}; use serde::de::{SeqAccess, Visitor}; use serde::ser::SerializeSeq; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -53,31 +54,37 @@ impl PointModification { working.point_ids.retain(|id| !self.remove.contains(id)); // Gathering the moves first keeps the segment pass below off the common path where nothing moves - let mut moved: HashMap = HashMap::new(); - for (index, id) in working.point_ids.iter().enumerate() { - let Some(&delta) = self.delta.get(id) else { continue }; - if !delta.is_finite() { - warn!("Invalid delta when applying a point modification"); - continue; - } + let mut moved = Vec::new(); + let mut any_moved = false; + if !self.delta.is_empty() { + moved.resize(working.point_ids.len(), None); + for (index, id) in working.point_ids.iter().enumerate() { + let Some(&delta) = self.delta.get(id) else { continue }; + if !delta.is_finite() { + warn!("Invalid delta when applying a point modification"); + continue; + } - working.point_positions[index] += delta; - moved.insert(index, delta); + working.point_positions[index] += delta; + moved[index] = Some(delta); + any_moved = true; + } } // Handles travel with their anchor, carried in one pass over the segment ends - if !moved.is_empty() { + if any_moved { + let moved_by = |index: usize| moved.get(index).copied().flatten(); for index in 0..working.segment_ids.len() { - if let Some(&delta) = moved.get(&working.segment_start[index]) { + if let Some(delta) = moved_by(working.segment_start[index]) { working.segment_handles[index].move_start(delta); } - if let Some(&delta) = moved.get(&working.segment_end[index]) { + if let Some(delta) = moved_by(working.segment_end[index]) { working.segment_handles[index].move_end(delta); } } } - let mut named: HashSet = if self.add.is_empty() { HashSet::new() } else { working.point_ids.iter().copied().collect() }; + let mut named: FxHashSet = if self.add.is_empty() { FxHashSet::default() } else { working.point_ids.iter().copied().collect() }; for &add_id in &self.add { let Some(&position) = self.delta.get(&add_id) else { continue }; if !position.is_finite() { @@ -95,6 +102,67 @@ impl PointModification { } } + /// Moves each point named by a delta directly in the pool, carrying the handles that follow it. `handles` marks the pool's handles, + /// none of which may be shared between segments, or a shared one would move once for each. + fn move_in_place(&self, vector: &mut Vector, paths: &List, handles: &[bool]) { + if self.delta.is_empty() { + return; + } + + let ids = vector.0.iter_attribute_values::(ATTR_ID).map(|ids| ids.as_slice()).unwrap_or_default(); + + // Deltas move only anchors and free points, as the working form leaves handles out of its points + let mut moved = vec![None; vector.point_count()]; + let mut any_moved = false; + for (index, _) in handles.iter().enumerate().filter(|&(_, &handle)| !handle) { + let Some(&delta) = self.delta.get(&ids.get(index).copied().unwrap_or_default()) else { continue }; + if !delta.is_finite() { + warn!("Invalid delta when applying a point modification"); + continue; + } + + moved[index] = Some(delta); + any_moved = true; + } + + if !any_moved { + return; + } + + for (position, delta) in vector.0.iter_element_values_mut().zip(&moved) { + if let Some(delta) = delta { + *position += *delta; + } + } + + // A segment's first handle follows its start anchor, and a cubic's second handle follows its end anchor + for (path_index, path) in paths.iter_element_values().enumerate() { + let curve_type = curve_type_at(paths, path_index); + if curve_type == CurveType::Linear { + continue; + } + + let stride = curve_type.stride(); + let moved_by = |reference: usize| path.reference(reference).and_then(|point| moved.get(point as usize).copied().flatten()); + + for span in 0..path.segment_count(curve_type) { + let start = span * stride; + let mut carry = |handle_reference: usize, delta: Option| { + if let (Some(delta), Some(handle)) = (delta, path.reference(handle_reference)) + && let Some(position) = vector.0.element_mut(handle as usize) + { + *position += delta; + } + }; + + carry(start + 1, moved_by(start)); + if curve_type == CurveType::Cubic { + carry(start + 2, moved_by(start + stride)); + } + } + } + } + /// Create a new modification that will convert an empty [`Vector`] into the target working form. fn create_from_working(working: &WorkingVector) -> Self { Self { @@ -152,7 +220,7 @@ impl SegmentModification { // The points no longer change here, so one index serves every endpoint lookup below. // Nothing looks an endpoint up in the common pure-handle case, so it skips the build. - let mut point_index: HashMap = HashMap::new(); + let mut point_index: FxHashMap = FxHashMap::default(); if !self.start_point.is_empty() || !self.end_point.is_empty() || !self.add.is_empty() { point_index.reserve(working.point_ids.len()); for (index, &id) in working.point_ids.iter().enumerate() { @@ -219,7 +287,11 @@ impl SegmentModification { }; } - let mut named: HashSet = if self.add.is_empty() { HashSet::new() } else { working.segment_ids.iter().copied().collect() }; + let mut named: FxHashSet = if self.add.is_empty() { + FxHashSet::default() + } else { + working.segment_ids.iter().copied().collect() + }; for &add_id in &self.add { let Some(&start) = self.start_point.get(&add_id) else { continue }; let Some(&end) = self.end_point.get(&add_id) else { continue }; @@ -266,6 +338,98 @@ impl SegmentModification { assert!(working.segment_end.iter().all(|&index| index < working.point_ids.len()), "index should be in range"); } + /// The curve type a segment ends up with once its handle edits apply, following the same rules as [`Self::apply`]. + fn edited_curve_type(current: CurveType, primary: Option>, end: Option>) -> CurveType { + match (primary.map(|handle| handle.is_some()), end.map(|handle| handle.is_some())) { + (Some(true), Some(true)) => CurveType::Cubic, + (Some(true), Some(false)) | (Some(false), Some(true)) => CurveType::Quadratic, + (Some(false), Some(false)) | (Some(false), None) => CurveType::Linear, + (None, Some(false)) if current == CurveType::Cubic => CurveType::Quadratic, + (None, Some(true)) if current == CurveType::Linear => CurveType::Quadratic, + (None, Some(true)) => CurveType::Cubic, + (Some(true), None) if current == CurveType::Linear => CurveType::Quadratic, + _ => current, + } + } + + /// Calls the visitor with each span a handle edit names, along with its curve type, references, starting reference, and edits. + fn for_each_edited_span(&self, paths: &List, mut visit: impl FnMut(CurveType, &Path, usize, Option>, Option>)) { + for (path_index, path) in paths.iter_element_values().enumerate() { + let curve_type = curve_type_at(paths, path_index); + let stride = curve_type.stride(); + let ids = path.0.iter_attribute_values::(ATTR_ID).map(|ids| ids.as_slice()).unwrap_or_default(); + + for span in 0..path.segment_count(curve_type) { + let id = ids.get(span * stride).copied().unwrap_or_default(); + let (primary, end) = (self.handle_primary.get(&id).copied(), self.handle_end.get(&id).copied()); + if primary.is_some() || end.is_some() { + visit(curve_type, path, span * stride, primary, end); + } + } + } + } + + /// Whether every handle edit leaves its segment's curve type as it is, so the edits can land in the pool without reweaving. + fn handle_edits_keep_curve_types(&self, paths: &List) -> bool { + if self.handle_primary.is_empty() && self.handle_end.is_empty() { + return true; + } + + let mut keeps = true; + self.for_each_edited_span(paths, |curve_type, _, _, primary, end| { + keeps &= Self::edited_curve_type(curve_type, primary, end) == curve_type; + }); + keeps + } + + /// Sets each edited handle directly in the pool, relative to its anchor's current position. Only valid once + /// [`Self::handle_edits_keep_curve_types`] has confirmed no curve type changes and [`Vector::point_roles`] that no handle is shared. + fn set_handles_in_place(&self, vector: &mut Vector, paths: &List) { + if self.handle_primary.is_empty() && self.handle_end.is_empty() { + return; + } + + let mut writes = Vec::new(); + self.for_each_edited_span(paths, |curve_type, path, start, primary, end| { + let position_at = |reference: usize| path.reference(reference).and_then(|point| vector.position(point as usize)); + let (Some(start_position), Some(end_position)) = (position_at(start), position_at(start + curve_type.stride())) else { + return; + }; + + let new_start = primary.map(|handle| handle.map(|handle| handle + start_position)); + let new_end = end.map(|handle| handle.map(|handle| handle + end_position)); + if !new_start.unwrap_or_default().is_none_or(|start| start.is_finite()) || !new_end.unwrap_or_default().is_none_or(|end| end.is_finite()) { + warn!("Invalid handles when applying a segment modification"); + return; + } + + let mut write = |reference: usize, position: DVec2| { + if let Some(point) = path.reference(reference) { + writes.push((point, position)); + } + }; + match (curve_type, new_start.flatten(), new_end.flatten()) { + // A quadratic's lone handle takes whichever side's position the edit sets + (CurveType::Quadratic, Some(handle), _) | (CurveType::Quadratic, None, Some(handle)) => write(start + 1, handle), + (CurveType::Cubic, handle_start, handle_end) => { + if let Some(handle_start) = handle_start { + write(start + 1, handle_start); + } + if let Some(handle_end) = handle_end { + write(start + 2, handle_end); + } + } + _ => {} + } + }); + + for (point, position) in writes { + if let Some(slot) = vector.0.element_mut(point as usize) { + *slot = position; + } + } + } + /// Create a new modification that will convert an empty [`Vector`] into the target working form. fn create_from_working(working: &WorkingVector) -> Self { let point_id = |(&segment, &index): (&SegmentId, &usize)| (segment, working.point_ids[index]); @@ -489,6 +653,38 @@ impl VectorModification { self.points == PointModification::default() && self.segments == SegmentModification::default() && self.add_g1_continuous.is_empty() && self.remove_g1_continuous.is_empty() } + /// Whether this modification only moves existing points and handles, adding, removing, and reconnecting nothing. + fn only_moves_points(&self) -> bool { + // Every field is named so that adding one forces a decision here + let Self { + points: PointModification { + add: added_points, + remove: removed_points, + delta: _, + }, + segments, + add_g1_continuous, + remove_g1_continuous, + } = self; + let SegmentModification { + add: added_segments, + remove: removed_segments, + start_point, + end_point, + handle_primary: _, + handle_end: _, + } = segments; + + added_points.is_empty() + && removed_points.is_empty() + && added_segments.is_empty() + && removed_segments.is_empty() + && start_point.is_empty() + && end_point.is_empty() + && add_g1_continuous.is_empty() + && remove_g1_continuous.is_empty() + } + /// Apply this modification to the specified [`Vector`] and its paths. pub fn apply(&self, vector: &mut Vector, paths: &mut List) { // An untouched vector keeps its incoming path structure instead of being flattened and rewoven @@ -496,18 +692,34 @@ impl VectorModification { return; } + // Moving points and handles leaves the weave intact, so the pool is edited where it lies in O(points + references), keeping + // upstream storage as it is. A handle shared between segments would move once for each there, so that weave is rewoven instead. + if self.only_moves_points() && self.segments.handle_edits_keep_curve_types(paths) { + let (handles, handles_unshared) = vector.point_roles(paths); + if handles_unshared { + self.points.move_in_place(vector, paths, &handles); + self.segments.set_handles_in_place(vector, paths); + return; + } + } + + self.apply_through_working_form(vector, paths); + } + + /// Applies this modification by flattening to the legacy working form, editing that, and weaving the result back. + fn apply_through_working_form(&self, vector: &mut Vector, paths: &mut List) { let mut working = WorkingVector::flatten(vector, paths); self.points.apply(&mut working); self.segments.apply(&mut working); - let surviving_segments: HashSet = working.segment_ids.iter().copied().collect(); + let surviving_segments: FxHashSet = working.segment_ids.iter().copied().collect(); let valid = |val: &[HandleId; 2]| surviving_segments.contains(&val[0].segment) && surviving_segments.contains(&val[1].segment); working .colinear .retain(|val| !self.remove_g1_continuous.contains(val) && !self.remove_g1_continuous.contains(&[val[1], val[0]]) && valid(val)); // Both orientations go in, since a pair and its reverse name the same tangent - let mut present: HashSet<[HandleId; 2]> = working.colinear.iter().flat_map(|&[first, second]| [[first, second], [second, first]]).collect(); + let mut present: FxHashSet<[HandleId; 2]> = working.colinear.iter().flat_map(|&[first, second]| [[first, second], [second, first]]).collect(); for handles in &self.add_g1_continuous { if !present.contains(handles) && valid(handles) { working.colinear.push(*handles); @@ -714,6 +926,249 @@ mod tests { vector.position(vector.index_of_point_id(id).unwrap() as usize).unwrap() } + /// What the tests compare of a modification's result: anchors by ID, segments by ID, the stroke walk, and G1 pairs. + fn describe(vector: &Vector, paths: &List) -> String { + let mut anchors: Vec = vector + .anchor_points(paths) + .map(|id| format!("{id:?}@{:?}", vector.index_of_point_id(id).and_then(|index| vector.position(index as usize)))) + .collect(); + anchors.sort(); + + let mut segments: Vec = vector.segment_iter(paths).map(|(id, segment, start, end)| format!("{id:?}:{start:?}>{end:?}:{segment:?}")).collect(); + segments.sort(); + + let strokes: Vec = vector.stroke_bezpath_iter(paths).map(|bezpath| bezpath.to_svg()).collect(); + + let mut pairs: Vec = vector + .colinear_pairs(paths) + .into_iter() + .map(|pair| { + let mut handles = pair.map(|handle| format!("{handle:?}")); + handles.sort(); + handles.join("~") + }) + .collect(); + pairs.sort(); + + format!("{anchors:?}\n{segments:?}\n{strokes:?}\n{pairs:?}") + } + + /// Every curve type, a weld where they change, a smooth anchor, and a second subpath. + fn mixed_geometry() -> (Vector, List) { + let mut bezpath = BezPath::new(); + bezpath.move_to((0., 0.)); + bezpath.curve_to((10., -10.), (20., -10.), (30., 0.)); + bezpath.line_to((30., 30.)); + bezpath.quad_to((15., 45.), (0., 30.)); + bezpath.close_path(); + bezpath.move_to((100., 100.)); + bezpath.curve_to((110., 90.), (120., 90.), (130., 100.)); + + let (mut vector, mut paths) = Vector::from_bezpath(&bezpath); + let smooth_anchor = anchor_id(&vector, &paths, 1); + vector.set_smooth(&mut paths, smooth_anchor, true); + + (vector, paths) + } + + // Moving points and handles where they lie gives the same result as flattening and reweaving + #[test] + fn moving_in_place_matches_the_working_form() { + let (vector, paths) = mixed_geometry(); + let anchors: Vec = vector.anchor_points(&paths).collect(); + let segments: Vec = vector.segment_iter(&paths).map(|(id, ..)| id).collect(); + + let mut modification = VectorModification::default(); + for (index, &anchor) in anchors.iter().enumerate() { + let delta = DVec2::new(index as f64, -(index as f64) * 2.); + modification.modify(&VectorModificationType::ApplyPointDelta { point: anchor, delta }); + } + let handle_edits = [ + (HandleId::primary(segments[0]), DVec2::new(3., 4.)), + (HandleId::primary(segments[2]), DVec2::new(-5., 6.)), + (HandleId::end(segments[4]), DVec2::new(7., -8.)), + ]; + for (handle, relative_position) in handle_edits { + modification.modify(&VectorModificationType::SetHandlePosition { handle, relative_position }); + } + + let (mut in_place, mut in_place_paths) = (vector.clone(), paths.clone()); + modification.apply(&mut in_place, &mut in_place_paths); + let (mut rewoven, mut rewoven_paths) = (vector.clone(), paths.clone()); + modification.apply_through_working_form(&mut rewoven, &mut rewoven_paths); + + assert_eq!(describe(&in_place, &in_place_paths), describe(&rewoven, &rewoven_paths)); + assert_ne!(describe(&in_place, &in_place_paths), describe(&vector, &paths)); + assert_eq!(in_place_paths, paths, "moving points leaves the weave untouched"); + } + + // An edit that turns a line into a curve changes the weave, so it still goes through the working form + #[test] + fn curve_type_changes_still_apply() { + let (mut vector, mut paths) = mixed_geometry(); + let line = vector.segment_iter(&paths).nth(1).map(|(id, ..)| id).unwrap(); + + let mut modification = VectorModification::default(); + let handles = [Some(DVec2::new(1., 1.)), Some(DVec2::new(-1., -1.))]; + modification.modify(&VectorModificationType::SetHandles { segment: line, handles }); + modification.apply(&mut vector, &mut paths); + + let curved = vector.segment_iter(&paths).find(|&(id, ..)| id == line).map(|(_, segment, ..)| segment); + assert!(matches!(curved, Some(PathSeg::Cubic(_)))); + } + + // Every way of editing a segment's two handles gives the same result in place as through the working form + #[test] + fn every_handle_edit_matches_the_working_form() { + let mut bezpath = BezPath::new(); + bezpath.move_to((0., 0.)); + bezpath.line_to((10., 0.)); + bezpath.move_to((0., 20.)); + bezpath.quad_to((5., 30.), (10., 20.)); + bezpath.move_to((0., 40.)); + bezpath.curve_to((3., 50.), (7., 50.), (10., 40.)); + let (vector, paths) = Vector::from_bezpath(&bezpath); + + let edits = [None, Some(None), Some(Some(DVec2::new(2., 3.)))]; + for (segment, ..) in vector.segment_iter(&paths) { + for (primary, end) in edits.into_iter().flat_map(|primary| edits.map(|end| (primary, end))) { + let mut modification = VectorModification::default(); + if let Some(primary) = primary { + modification.segments.handle_primary.insert(segment, primary); + } + if let Some(end) = end { + modification.segments.handle_end.insert(segment, end); + } + + let (mut in_place, mut in_place_paths) = (vector.clone(), paths.clone()); + modification.apply(&mut in_place, &mut in_place_paths); + let (mut rewoven, mut rewoven_paths) = (vector.clone(), paths.clone()); + modification.apply_through_working_form(&mut rewoven, &mut rewoven_paths); + + assert_eq!( + describe(&in_place, &in_place_paths), + describe(&rewoven, &rewoven_paths), + "{segment:?} edited by {primary:?} and {end:?}" + ); + } + } + } + + // A handle shared by two segments is unwelded by a move, as the working form does, instead of moving for both + #[test] + fn a_shared_handle_is_rewoven_instead_of_moved_twice() { + use crate::vector::path::push_path; + use core_types::list::Item; + + let ids = [PointId::generate(), PointId::generate(), PointId::generate(), PointId::generate()]; + let anchors = [DVec2::new(0., 0.), DVec2::new(10., 0.), DVec2::new(0., 10.), DVec2::new(10., 10.)]; + let mut vector = Vector::default(); + for (&position, id) in anchors.iter().zip(ids) { + vector.0.push(Item::new_from_element(position).with_attribute(ATTR_ID, id)); + } + for position in [DVec2::new(5., 5.), DVec2::new(8., -2.), DVec2::new(8., 12.)] { + vector.0.push(Item::new_from_element(position).with_attribute(ATTR_ID, PointId::UNNAMED)); + } + let mut paths = List::new(); + push_path(&mut paths, CurveType::Cubic, [0, 4, 5, 1], [SegmentId::generate()]); + push_path(&mut paths, CurveType::Cubic, [2, 4, 6, 3], [SegmentId::generate()]); + + let mut modification = VectorModification::default(); + modification.modify(&VectorModificationType::ApplyPointDelta { point: ids[0], delta: DVec2::X }); + modification.modify(&VectorModificationType::ApplyPointDelta { point: ids[2], delta: DVec2::Y }); + + let (mut applied, mut applied_paths) = (vector.clone(), paths.clone()); + modification.apply(&mut applied, &mut applied_paths); + let (mut rewoven, mut rewoven_paths) = (vector.clone(), paths.clone()); + modification.apply_through_working_form(&mut rewoven, &mut rewoven_paths); + + assert_eq!(describe(&applied, &applied_paths), describe(&rewoven, &rewoven_paths)); + } + + // A delta that isn't finite moves nothing, and neither do the handles beside it, while a finite one beside it still applies + #[test] + fn non_finite_deltas_are_ignored_in_place() { + let (vector, paths) = mixed_geometry(); + let [skipped, moved] = [anchor_id(&vector, &paths, 0), anchor_id(&vector, &paths, 1)]; + + // Stored directly, since recording an edit already discards a non-finite value + let mut modification = VectorModification::default(); + modification.points.delta.insert(skipped, DVec2::new(f64::NAN, 0.)); + modification.points.delta.insert(moved, DVec2::new(3., 4.)); + + let (mut in_place, mut in_place_paths) = (vector.clone(), paths.clone()); + modification.apply(&mut in_place, &mut in_place_paths); + let (mut rewoven, mut rewoven_paths) = (vector.clone(), paths.clone()); + modification.apply_through_working_form(&mut rewoven, &mut rewoven_paths); + + assert_eq!(describe(&in_place, &in_place_paths), describe(&rewoven, &rewoven_paths)); + assert_eq!(anchor_position(&in_place, &in_place_paths, 0), anchor_position(&vector, &paths, 0)); + assert_eq!(anchor_position(&in_place, &in_place_paths, 1), anchor_position(&vector, &paths, 1) + DVec2::new(3., 4.)); + } + + // A point that is a handle in one path and an anchor in another has two ways to move, so it goes through the working form + #[test] + fn a_point_both_handle_and_anchor_is_rewoven_instead_of_moved_in_place() { + use crate::vector::path::push_path; + use core_types::list::Item; + + let ids: [PointId; 4] = std::array::from_fn(|_| PointId::generate()); + let anchors = [DVec2::new(0., 0.), DVec2::new(10., 0.), DVec2::new(5., 5.), DVec2::new(5., 20.)]; + let mut vector = Vector::default(); + for (position, id) in anchors.into_iter().zip(ids) { + vector.0.push(Item::new_from_element(position).with_attribute(ATTR_ID, id)); + } + vector.0.push(Item::new_from_element(DVec2::new(8., -2.)).with_attribute(ATTR_ID, PointId::UNNAMED)); + + // Point 2 is the cubic's first handle and the line's start + let mut paths = List::new(); + push_path(&mut paths, CurveType::Cubic, [0, 2, 4, 1], [SegmentId::generate()]); + push_path(&mut paths, CurveType::Linear, [2, 3], [SegmentId::generate()]); + assert!(!vector.point_roles(&paths).1, "a point with both roles should rule out moving in place"); + + let mut modification = VectorModification::default(); + modification.modify(&VectorModificationType::ApplyPointDelta { point: ids[0], delta: DVec2::X }); + modification.modify(&VectorModificationType::ApplyPointDelta { point: ids[2], delta: DVec2::Y }); + + let (mut applied, mut applied_paths) = (vector.clone(), paths.clone()); + modification.apply(&mut applied, &mut applied_paths); + let (mut rewoven, mut rewoven_paths) = (vector.clone(), paths.clone()); + modification.apply_through_working_form(&mut rewoven, &mut rewoven_paths); + + assert_eq!(describe(&applied, &applied_paths), describe(&rewoven, &rewoven_paths)); + } + + // A free point and an anchor where three segments meet move in place just as the working form moves them + #[test] + fn free_points_and_junctions_move_in_place_like_the_working_form() { + use crate::vector::path::push_path; + + let (mut vector, mut paths) = mixed_geometry(); + for position in [(200., 200.), (-20., -20.)] { + let mut lone_point = BezPath::new(); + lone_point.move_to(position); + vector.append_bezpath(&mut paths, &lone_point); + } + + // The closed loop starts at pool point 0, so a line from there to the second lone point makes it a junction + let spur_end = vector.point_count() as u32 - 1; + push_path(&mut paths, CurveType::Linear, [0, spur_end], [SegmentId::generate()]); + + let mut modification = VectorModification::default(); + for (index, anchor) in vector.anchor_points(&paths).enumerate() { + let delta = DVec2::new(index as f64 + 1., 2.); + modification.modify(&VectorModificationType::ApplyPointDelta { point: anchor, delta }); + } + + let (mut in_place, mut in_place_paths) = (vector.clone(), paths.clone()); + modification.apply(&mut in_place, &mut in_place_paths); + let (mut rewoven, mut rewoven_paths) = (vector.clone(), paths.clone()); + modification.apply_through_working_form(&mut rewoven, &mut rewoven_paths); + + assert_eq!(describe(&in_place, &in_place_paths), describe(&rewoven, &rewoven_paths)); + assert_eq!(in_place_paths, paths, "moving points leaves the weave untouched"); + } + // Re-adding a segment or point under an ID the geometry already holds adds no second copy under that ID #[test] fn duplicate_adds_are_skipped() { diff --git a/node-graph/libraries/vector-types/src/vector/vector_types.rs b/node-graph/libraries/vector-types/src/vector/vector_types.rs index 4b10f762911..05508d8056a 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_types.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_types.rs @@ -2,7 +2,7 @@ use super::addressing; use super::connectivity::Connectivity; use super::contour::{contours, subpaths}; use super::misc::{HandleId, ManipulatorPointId, dvec2_to_point}; -use super::path::{CurveType, Path, append_bezpath, curve_type_at, no_paths, paths_from_item_attributes, paths_from_list_row_attributes, push_path}; +use super::path::{CurveType, Path, curve_type_at, no_paths, paths_from_item_attributes, paths_from_list_row_attributes, push_path, weave_bezpath}; use super::style::{Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use super::vector_attributes::{PointId, SegmentId}; use core_types::bounds::{BoundingBox, RenderBoundingBox}; @@ -105,38 +105,37 @@ fn next_segment_id(paths: &List) -> SegmentId { } impl Vector { - /// Names the anchors and spans appended since the given watermarks, continuing the ID sequences. - /// Handles take `PointId::UNNAMED`. - fn stamp_appended_ids(&mut self, paths: &mut List, first_new_point: usize, first_new_path: usize) { - let mut point_id = next_point_id(&self.0); - let mut segment_id = next_segment_id(paths); + /// Weaves a bezpath's contours onto this pool and the given paths, naming the new anchors and spans + /// by continuing the ID sequences. Handles take `PointId::UNNAMED`. + pub fn append_bezpath(&mut self, paths: &mut List, bezpath: &BezPath) { + let (first_new_point, first_new_path) = (self.0.len(), paths.len()); + let anchors = weave_bezpath(&mut self.0, paths, bezpath); // Pool order is traversal order, so anchors and free points take IDs in the order they were drawn - let connectivity = Connectivity::build(self.0.len(), paths); - for index in first_new_point..self.0.len() { - let id = if self.is_handle_point(paths, &connectivity, index as u32) { - PointId::UNNAMED - } else { - point_id.next_id() - }; - self.0.set_attribute(ATTR_ID, index, id); + if !anchors.is_empty() { + let mut point_id = next_point_id(&self.0); + let ids = self.0.iter_attribute_values_mut_or_default::(ATTR_ID).skip(first_new_point); + for (id, anchor) in ids.zip(anchors) { + *id = if anchor { point_id.next_id() } else { PointId::UNNAMED }; + } } + let mut segment_id = next_segment_id(paths); for path_index in first_new_path..paths.len() { let curve_type = curve_type_at(paths, path_index); let Some(path) = paths.element_mut(path_index) else { continue }; - for span in 0..path.segment_count(curve_type) { - path.0.set_attribute(ATTR_ID, span * curve_type.stride(), segment_id.next_id()); + let span_count = path.segment_count(curve_type); + if span_count == 0 { + continue; } - } - } - /// Weaves a bezpath's contours onto this pool and the given paths, naming the new anchors and spans. - pub fn append_bezpath(&mut self, paths: &mut List, bezpath: &BezPath) { - let (first_new_point, first_new_path) = (self.0.len(), paths.len()); - append_bezpath(&mut self.0, paths, bezpath); - self.stamp_appended_ids(paths, first_new_point, first_new_path); + let mut span_ids = vec![SegmentId::default(); path.0.len()]; + for span in 0..span_count { + span_ids[span * curve_type.stride()] = segment_id.next_id(); + } + path.0.set_attribute_values(ATTR_ID, span_ids); + } } /// Builds a fresh pool and paths from a single bezpath. @@ -365,23 +364,41 @@ impl Vector { } impl Vector { - /// Whether the pool point at the given index is a handle, referenced only between anchors. - pub(crate) fn is_handle_point(&self, paths: &List, connectivity: &Connectivity, index: u32) -> bool { - let references = connectivity.references(index); + /// Marks each pool point that paths name only at handle slots, in O(points + references). + pub(crate) fn handle_mask(&self, paths: &List) -> Vec { + self.point_roles(paths).0 + } + + /// The handle mask, along with whether every handle is one segment's alone: no point fills two handle slots, and none + /// is an anchor in one place and a handle in another. + pub(crate) fn point_roles(&self, paths: &List) -> (Vec, bool) { + // Each point is referenced by no path, only at a handle slot, or at an anchor slot somewhere + const UNREFERENCED: u8 = 0; + const HANDLE: u8 = 1; + const ANCHOR: u8 = 2; + + let mut roles = vec![UNREFERENCED; self.0.len()]; + let mut handles_unshared = true; + for (path_index, path) in paths.iter_element_values().enumerate() { + let stride = curve_type_at(paths, path_index).stride(); + for (reference_index, point) in path.references().enumerate() { + let Some(role) = roles.get_mut(point as usize) else { continue }; + if reference_index.is_multiple_of(stride) { + handles_unshared &= *role != HANDLE; + *role = ANCHOR; + } else { + handles_unshared &= *role == UNREFERENCED; + *role = (*role).max(HANDLE); + } + } + } - !references.is_empty() - && references - .iter() - .all(|reference| !matches!(paths.element(reference.path as usize), Some(path) if path.is_anchor_reference(reference.index as usize, curve_type_at(paths, reference.path as usize)))) + (roles.into_iter().map(|role| role == HANDLE).collect(), handles_unshared) } /// The pool indices of all anchor points and free points in pool order, skipping handles. pub fn anchor_indices(&self, paths: &List) -> Vec { - let connectivity = self.connectivity(paths); - (0..self.0.len() as u32) - .filter(|&index| !self.is_handle_point(paths, &connectivity, index)) - .map(|index| index as usize) - .collect() + self.handle_mask(paths).into_iter().enumerate().filter(|&(_, handle)| !handle).map(|(index, _)| index).collect() } /// Drops the paths and every point used only as a handle, leaving the anchors and free points as a bare cloud. @@ -401,11 +418,8 @@ impl Vector { /// Iterator over the IDs of all anchor points and free points, skipping handles. pub fn anchor_points(&self, paths: &List) -> impl Iterator { - let connectivity = self.connectivity(paths); - let anchors: Vec<_> = (0..self.0.len() as u32) - .filter(|&index| !self.is_handle_point(paths, &connectivity, index)) - .filter_map(|index| self.point_id_at(index)) - .collect(); + let ids = self.0.iter_attribute_values::(ATTR_ID).map(|ids| ids.as_slice()).unwrap_or_default(); + let anchors: Vec<_> = self.handle_mask(paths).into_iter().zip(ids).filter(|&(handle, _)| !handle).map(|(_, &id)| id).collect(); anchors.into_iter() } @@ -414,14 +428,14 @@ impl Vector { pub fn anchor_endpoints(&self, paths: &List) -> impl Iterator { let connectivity = self.connectivity(paths); let endpoints: Vec<_> = (0..self.0.len() as u32) - .filter(|&index| connectivity.spokes(index, paths) == 1) + .filter(|&index| connectivity.spokes(index) == 1) .filter_map(|index| self.point_id_at(index)) .collect(); endpoints.into_iter() } - // Each per-point query below builds its own connectivity index, O(points + references) with two allocations. + // Each per-point query below builds its own connectivity index, O(points + references) with three allocations. // Callers looping over many points should hoist `connectivity()` and use `addressing` directly. /// Enumerate all segments that start at the point. @@ -458,7 +472,7 @@ impl Vector { pub fn connected_count(&self, paths: &List, point: PointId) -> usize { let Some(index) = self.index_of_point_id(point) else { return 0 }; - self.connectivity(paths).spokes(index, paths) + self.connectivity(paths).spokes(index) } /// Whether any segment starts or ends at the point. @@ -497,12 +511,8 @@ impl Vector { impl Vector { /// Whether the junction at the given anchor keeps its tangent pair smooth, read from any of its occurrences. fn smooth_at_anchor_index(&self, paths: &List, connectivity: &Connectivity, index: u32) -> bool { - connectivity.references(index).iter().any(|reference| { + connectivity.references(index).iter().filter(|reference| reference.is_anchor()).any(|reference| { let Some(path) = paths.element(reference.path as usize) else { return false }; - if !path.is_anchor_reference(reference.index as usize, curve_type_at(paths, reference.path as usize)) { - return false; - } - path.0.attribute::(ATTR_SMOOTH, reference.index as usize).copied().unwrap_or_default() }) } @@ -510,7 +520,7 @@ impl Vector { /// Whether the anchor at the given pool index forms a smooth tangent pair, which needs exactly two /// segment ends meeting there. A junction has no unambiguous pair to keep smooth. fn pairs_smoothly(&self, paths: &List, connectivity: &Connectivity, index: u32) -> bool { - self.smooth_at_anchor_index(paths, connectivity, index) && connectivity.spokes(index, paths) == 2 + self.smooth_at_anchor_index(paths, connectivity, index) && connectivity.spokes(index) == 2 } /// Whether the anchor (or the handle's anchor) keeps a smooth tangent pair, which needs exactly two segment ends meeting there. @@ -535,15 +545,19 @@ impl Vector { /// Every G1 pair: the two facing handles at each smooth anchor where exactly two segment ends meet. pub fn colinear_pairs(&self, paths: &List) -> Vec<[HandleId; 2]> { - let connectivity = self.connectivity(paths); + self.colinear_pairs_with(paths, &self.connectivity(paths)) + } + + /// The G1 pairs, read through a connectivity the caller already built. + pub(crate) fn colinear_pairs_with(&self, paths: &List, connectivity: &Connectivity) -> Vec<[HandleId; 2]> { let mut pairs = Vec::new(); for index in 0..self.point_count() as u32 { - if connectivity.spokes(index, paths) != 2 || !self.smooth_at_anchor_index(paths, &connectivity, index) { + if connectivity.spokes(index) != 2 || !self.smooth_at_anchor_index(paths, connectivity, index) { continue; } - let handles: Vec = addressing::spans_at(index, paths, &connectivity) + let handles: Vec = addressing::spans_at(index, paths, connectivity) .into_iter() .filter_map(|(span, arriving)| { let segment = addressing::segment_id_of(paths, span)?; @@ -607,12 +621,9 @@ impl Vector { let connectivity = self.connectivity(paths); for index in indices { - for reference in connectivity.references(index) { - let curve_type = curve_type_at(paths, reference.path as usize); + for reference in connectivity.references(index).iter().filter(|reference| reference.is_anchor()) { let Some(path) = paths.element_mut(reference.path as usize) else { continue }; - if path.is_anchor_reference(reference.index as usize, curve_type) { - path.0.set_attribute(ATTR_SMOOTH, reference.index as usize, smooth); - } + path.0.set_attribute(ATTR_SMOOTH, reference.index as usize, smooth); } } }