Skip to content

Commit 9e19a46

Browse files
authored
Build vector pool storage in bulk and apply move-only Path node edits in place (#4736)
* Build a list from its elements and write an attribute for every item at once * Index connectivity roles once and build pools, paths, and IDs in bulk * Apply point and handle moves in place without reweaving the paths * Walk contours with curve types read once and tally junction spokes in a single pass
1 parent 514b894 commit 9e19a46

9 files changed

Lines changed: 792 additions & 191 deletions

File tree

‎node-graph/libraries/core-types/src/list.rs‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,23 @@ impl Attributes {
909909
attribute.0[index] = value;
910910
}
911911

912+
/// Replaces the attribute for the given key with one value per item, keeping its place unless its type changes.
913+
fn set_attribute_values<T: Clone + Send + Sync + Default + Debug + PartialEq + CacheHash + 'static>(&mut self, key: String, values: Vec<T>) {
914+
// An attribute changing type moves to the end, as it does when set one value at a time
915+
let existing = self.attributes.iter().position(|(k, _)| k == &key);
916+
let same_type = existing.is_some_and(|position| (*self.attributes[position].1).as_any().downcast_ref::<Attribute<T>>().is_some());
917+
918+
let attribute: Box<dyn AnyAttribute> = Box::new(Attribute(values));
919+
match existing {
920+
Some(position) if same_type => self.attributes[position].1 = attribute,
921+
Some(position) => {
922+
self.attributes.remove(position);
923+
self.attributes.push((key, attribute));
924+
}
925+
None => self.attributes.push((key, attribute)),
926+
}
927+
}
928+
912929
/// Returns a debug-formatted string for the value at the given index in the attribute for the given key.
913930
fn display_value(&self, key: &str, index: usize, overrides: fn(&dyn std::any::Any) -> Option<String>) -> Option<String> {
914931
self.attributes.iter().find_map(|(k, attribute)| {
@@ -1029,6 +1046,15 @@ impl<T> List<T> {
10291046
}
10301047
}
10311048

1049+
/// Creates a list holding the given elements in order, with no attributes.
1050+
pub fn new_from_elements(elements: Vec<T>) -> Self {
1051+
let len = elements.len();
1052+
Self {
1053+
element: elements,
1054+
attributes: Attributes::with_len(len),
1055+
}
1056+
}
1057+
10321058
/// Creates a list containing a single item from the given [`Item`], preserving its attributes.
10331059
pub fn new_from_item(item: Item<T>) -> Self {
10341060
let mut attributes = Attributes::new();
@@ -1140,6 +1166,18 @@ impl<T> List<T> {
11401166
self.attributes.set_value(key, index, value);
11411167
}
11421168

1169+
/// 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).
1170+
/// Logs an error and leaves the list unchanged if the number of values differs from the number of items.
1171+
pub fn set_attribute_values<U: Clone + Send + Sync + Default + Debug + PartialEq + CacheHash + 'static>(&mut self, key: impl Into<String>, values: Vec<U>) {
1172+
let key = key.into();
1173+
if values.len() != self.element.len() {
1174+
log::error!("Attribute '{key}' was given {} values for a list of {} items", values.len(), self.element.len());
1175+
return;
1176+
}
1177+
1178+
self.attributes.set_attribute_values(key, values);
1179+
}
1180+
11431181
/// 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).
11441182
/// Falls back to default if the value's type doesn't match an existing attribute.
11451183
pub fn set_attribute_value_dyn(&mut self, key: impl Into<String>, index: usize, value: AttributeValueDyn) {
@@ -1577,4 +1615,29 @@ mod tests {
15771615
other.push(Item::new_from_element(()).with_attribute(ATTR_START, 5_u64));
15781616
assert_eq!(other.attribute_cloned_or_default::<u64>(ATTR_START, 0), 0);
15791617
}
1618+
1619+
// Whole-attribute writes replace an existing attribute in place and refuse a value count that doesn't match
1620+
#[test]
1621+
fn set_attribute_values_writes_every_item_at_once() {
1622+
let mut list = List::new_from_elements(vec![10_u32, 20, 30]);
1623+
list.set_attribute(ATTR_START, 0, 7_u64);
1624+
list.set_attribute_values(ATTR_END, vec![1_u64, 2, 3]);
1625+
list.set_attribute_values(ATTR_START, vec![4_u64, 5, 6]);
1626+
1627+
assert_eq!(list.len(), 3);
1628+
assert_eq!(list.attribute_keys().collect::<Vec<_>>(), [ATTR_START, ATTR_END]);
1629+
assert_eq!(list.iter_attribute_values::<u64>(ATTR_START).unwrap().copied().collect::<Vec<_>>(), [4, 5, 6]);
1630+
1631+
list.set_attribute_values(ATTR_END, vec![9_u64]);
1632+
assert_eq!(list.iter_attribute_values::<u64>(ATTR_END).unwrap().copied().collect::<Vec<_>>(), [1, 2, 3]);
1633+
1634+
// Changing an attribute's type moves it last, whichever way it is written
1635+
let mut one_at_a_time = list.clone();
1636+
for index in 0..3 {
1637+
one_at_a_time.set_attribute(ATTR_START, index, index as f64);
1638+
}
1639+
list.set_attribute_values(ATTR_START, vec![0., 1., 2.]);
1640+
assert_eq!(list.attribute_keys().collect::<Vec<_>>(), [ATTR_END, ATTR_START]);
1641+
assert_eq!(list, one_at_a_time);
1642+
}
15801643
}

‎node-graph/libraries/vector-types/src/vector/addressing.rs‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,11 @@ pub fn spans_at(pool_index: u32, paths: &List<Path>, connectivity: &Connectivity
8686
let mut found = Vec::new();
8787

8888
for reference in connectivity.references(pool_index) {
89-
let path_index = reference.path as usize;
90-
let Some(path) = paths.element(path_index) else { continue };
91-
92-
let curve_type = curve_type_at(paths, path_index);
93-
if !path.is_anchor_reference(reference.index as usize, curve_type) {
89+
let Some(path) = paths.element(reference.path as usize) else { continue };
90+
if !reference.is_anchor() {
9491
continue;
9592
}
93+
let curve_type = reference.curve_type;
9694

9795
// An anchor arrives on the span before it and departs the one starting at it
9896
let anchor = reference.index as usize / curve_type.stride();

‎node-graph/libraries/vector-types/src/vector/connectivity.rs‎

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
1-
use super::path::{Path, curve_type_at};
1+
use super::path::{CurveType, Path, curve_type_at};
22
use core_types::list::List;
33

44
/// One path's reference to a pool point, naming where in the weave that point is used.
55
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
66
pub struct Reference {
77
pub path: u32,
88
pub index: u32,
9+
/// The curve type of the path holding this reference. Its stride decides whether the reference names an anchor.
10+
pub curve_type: CurveType,
11+
/// 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.
12+
pub spokes: u8,
13+
}
14+
15+
impl Reference {
16+
/// Whether this reference sits at an anchor slot of its path rather than a handle slot.
17+
pub fn is_anchor(&self) -> bool {
18+
(self.index as usize).is_multiple_of(self.curve_type.stride())
19+
}
920
}
1021

1122
/// Reverse index from each pool point to the references naming it.
@@ -39,11 +50,21 @@ impl Connectivity {
3950
let mut cursors = starts[..point_count].to_vec();
4051
let mut references = vec![Reference::default(); starts[point_count] as usize];
4152
for (path_index, path) in paths.iter_element_values().enumerate() {
53+
// Each path's curve type is read once here, so queries never look it up again
54+
let curve_type = curve_type_at(paths, path_index);
55+
let last = path.0.len().saturating_sub(1);
56+
4257
for (reference_index, point) in path.references().enumerate() {
4358
let Some(cursor) = cursors.get_mut(point as usize) else { continue };
59+
60+
let anchor = reference_index.is_multiple_of(curve_type.stride());
61+
let spokes = if anchor { u8::from(reference_index > 0) + u8::from(reference_index < last) } else { 0 };
62+
4463
references[*cursor as usize] = Reference {
4564
path: path_index as u32,
4665
index: reference_index as u32,
66+
curve_type,
67+
spokes,
4768
};
4869
*cursor += 1;
4970
}
@@ -66,27 +87,21 @@ impl Connectivity {
6687
!self.references(point).is_empty()
6788
}
6889

90+
/// Whether this point is a handle, named by paths only at handle slots.
91+
pub fn is_handle(&self, point: u32) -> bool {
92+
let references = self.references(point);
93+
!references.is_empty() && !references.iter().any(Reference::is_anchor)
94+
}
95+
6996
/// The segment ends meeting at this point: 1 at a free end, 2 along a chain, 3 or more at a junction.
7097
/// Handle references contribute none, since only anchors have segments radiating from them.
71-
pub fn spokes(&self, point: u32, paths: &List<Path>) -> usize {
72-
self.references(point)
73-
.iter()
74-
.filter_map(|reference| {
75-
let path = paths.element(reference.path as usize)?;
76-
if !path.is_anchor_reference(reference.index as usize, curve_type_at(paths, reference.path as usize)) {
77-
return None;
78-
}
79-
80-
let last = path.0.len().saturating_sub(1) as u32;
81-
Some(usize::from(reference.index > 0) + usize::from(reference.index < last))
82-
})
83-
.sum()
98+
pub fn spokes(&self, point: u32) -> usize {
99+
self.references(point).iter().map(|reference| usize::from(reference.spokes)).sum()
84100
}
85101
}
86102

87103
#[cfg(test)]
88104
mod tests {
89-
use super::super::path::CurveType;
90105
use super::*;
91106
use core_types::list::{ATTR_CURVE_TYPE, Item};
92107

@@ -101,21 +116,28 @@ mod tests {
101116
paths
102117
}
103118

119+
fn places(connectivity: &Connectivity, point: u32) -> Vec<(u32, u32)> {
120+
connectivity.references(point).iter().map(|reference| (reference.path, reference.index)).collect()
121+
}
122+
104123
// A closed circle crossed by a chord welded at two of its anchors, plus two points no path names
105124
#[test]
106125
fn spokes_at_welds_and_free_points() {
107126
let paths = paths(&[(CurveType::Cubic, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0]), (CurveType::Linear, &[0, 12, 6])]);
108127
let connectivity = Connectivity::build(15, &paths);
109128

110129
// The seam contributes two spokes and the chord a third
111-
assert_eq!(connectivity.spokes(0, &paths), 3);
112-
assert_eq!(connectivity.spokes(6, &paths), 3);
130+
assert_eq!(connectivity.spokes(0), 3);
131+
assert_eq!(connectivity.spokes(6), 3);
113132

114-
assert_eq!(connectivity.spokes(3, &paths), 2);
115-
assert_eq!(connectivity.spokes(12, &paths), 2);
133+
assert_eq!(connectivity.spokes(3), 2);
134+
assert_eq!(connectivity.spokes(12), 2);
116135

117136
// Handles carry no segments, and unreferenced points are free
118-
assert_eq!(connectivity.spokes(1, &paths), 0);
137+
assert_eq!(connectivity.spokes(1), 0);
138+
assert!(connectivity.is_handle(1));
139+
assert!(!connectivity.is_handle(0));
140+
assert!(!connectivity.is_handle(13));
119141
assert!(!connectivity.is_referenced(13));
120142
assert!(connectivity.is_referenced(12));
121143
}
@@ -125,9 +147,9 @@ mod tests {
125147
let paths = paths(&[(CurveType::Linear, &[0, 1, 2]), (CurveType::Linear, &[2, 3])]);
126148
let connectivity = Connectivity::build(4, &paths);
127149

128-
assert_eq!(connectivity.references(0), [Reference { path: 0, index: 0 }]);
129-
assert_eq!(connectivity.references(2), [Reference { path: 0, index: 2 }, Reference { path: 1, index: 0 }]);
130-
assert_eq!(connectivity.spokes(2, &paths), 2);
150+
assert_eq!(places(&connectivity, 0), [(0, 0)]);
151+
assert_eq!(places(&connectivity, 2), [(0, 2), (1, 0)]);
152+
assert_eq!(connectivity.spokes(2), 2);
131153
}
132154

133155
// References falling anywhere outside the pool are ignored, including one exactly at its end
@@ -136,10 +158,10 @@ mod tests {
136158
let paths = paths(&[(CurveType::Linear, &[0, 3, 1, 4])]);
137159
let connectivity = Connectivity::build(3, &paths);
138160

139-
assert_eq!(connectivity.references(0), [Reference { path: 0, index: 0 }]);
140-
assert_eq!(connectivity.references(1), [Reference { path: 0, index: 2 }]);
141-
assert_eq!(connectivity.references(3), []);
142-
assert_eq!(connectivity.references(4), []);
161+
assert_eq!(places(&connectivity, 0), [(0, 0)]);
162+
assert_eq!(places(&connectivity, 1), [(0, 2)]);
163+
assert!(places(&connectivity, 3).is_empty());
164+
assert!(places(&connectivity, 4).is_empty());
143165
}
144166

145167
// An open end has one spoke, which is what makes it a cap site
@@ -148,8 +170,8 @@ mod tests {
148170
let paths = paths(&[(CurveType::Linear, &[0, 1, 2])]);
149171
let connectivity = Connectivity::build(3, &paths);
150172

151-
assert_eq!(connectivity.spokes(0, &paths), 1);
152-
assert_eq!(connectivity.spokes(2, &paths), 1);
153-
assert_eq!(connectivity.spokes(1, &paths), 2);
173+
assert_eq!(connectivity.spokes(0), 1);
174+
assert_eq!(connectivity.spokes(2), 1);
175+
assert_eq!(connectivity.spokes(1), 2);
154176
}
155177
}

‎node-graph/libraries/vector-types/src/vector/contour.rs‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::misc::dvec2_to_point;
2-
use super::path::{Path, append_segment, curve_type_at};
2+
use super::path::{CurveType, Path, append_segment, curve_type_at};
33
use core_types::list::List;
44
use glam::DVec2;
55
use kurbo::BezPath;
@@ -100,26 +100,35 @@ impl SpanEnds {
100100
/// then the rest from the lowest point with spans left. A walk follows the earliest woven span waiting at each
101101
/// point, backwards when that span ends there, and closes on arriving back where it began.
102102
pub fn subpaths(point_count: usize, paths: &List<Path>) -> Vec<Subpath> {
103+
subpaths_with(point_count, paths, &curve_types(paths))
104+
}
105+
106+
/// The curve type of every path, read once so the walkers below never look one up per span.
107+
fn curve_types(paths: &List<Path>) -> Vec<CurveType> {
108+
(0..paths.len()).map(|index| curve_type_at(paths, index)).collect()
109+
}
110+
111+
fn subpaths_with(point_count: usize, paths: &List<Path>, curve_types: &[CurveType]) -> Vec<Subpath> {
103112
let mut edges = Vec::new();
104113
for (path_index, path) in paths.iter_element_values().enumerate() {
105-
let curve_type = curve_type_at(paths, path_index);
114+
let curve_type = curve_types.get(path_index).copied().unwrap_or_default();
106115
let stride = curve_type.stride();
116+
let references = path.0.iter_element_values().as_slice();
107117

108118
for span in 0..path.segment_count(curve_type) {
109-
let references = (span * stride..=(span + 1) * stride).map(|index| path.reference(index));
110-
let Some(references) = references.collect::<Option<Vec<u32>>>() else { continue };
119+
let Some(references) = references.get(span * stride..=(span + 1) * stride) else { continue };
111120

112121
// A span naming a point outside the pool cannot be drawn, so it takes no part in the walk
113-
if references.iter().any(|&reference| reference as usize >= point_count) {
122+
if references.iter().any(|&reference| !(0..point_count as i64).contains(&reference)) {
114123
continue;
115124
}
116125
let (Some(&start), Some(&end)) = (references.first(), references.last()) else { continue };
117126

118127
edges.push(Edge {
119128
path: path_index as u32,
120129
span: span as u32,
121-
start,
122-
end,
130+
start: start as u32,
131+
end: end as u32,
123132
});
124133
}
125134
}
@@ -174,12 +183,16 @@ pub fn subpaths(point_count: usize, paths: &List<Path>) -> Vec<Subpath> {
174183

175184
/// Emits one subpath as a bezpath, or `None` when it has no drawable geometry.
176185
pub fn subpath_to_bezpath(pool: &List<DVec2>, paths: &List<Path>, subpath: &Subpath) -> Option<BezPath> {
186+
subpath_to_bezpath_with(pool, paths, &curve_types(paths), subpath)
187+
}
188+
189+
fn subpath_to_bezpath_with(pool: &List<DVec2>, paths: &List<Path>, curve_types: &[CurveType], subpath: &Subpath) -> Option<BezPath> {
177190
let mut bezpath = BezPath::new();
178191
bezpath.move_to(dvec2_to_point(*pool.element(subpath.entry as usize)?));
179192

180193
for step in &subpath.steps {
181194
let path = paths.element(step.path as usize)?;
182-
let segment = path.segment(pool, curve_type_at(paths, step.path as usize), step.span as usize)?;
195+
let segment = path.segment(pool, *curve_types.get(step.path as usize)?, step.span as usize)?;
183196

184197
append_segment(&mut bezpath, if step.reversed { segment.reverse() } else { segment });
185198
}
@@ -193,7 +206,11 @@ pub fn subpath_to_bezpath(pool: &List<DVec2>, paths: &List<Path>, subpath: &Subp
193206

194207
/// Assembles the paths into contours for rendering, one bezpath per subpath in the order they are walked.
195208
pub fn contours(pool: &List<DVec2>, paths: &List<Path>) -> Vec<BezPath> {
196-
subpaths(pool.len(), paths).iter().filter_map(|subpath| subpath_to_bezpath(pool, paths, subpath)).collect()
209+
let curve_types = curve_types(paths);
210+
subpaths_with(pool.len(), paths, &curve_types)
211+
.iter()
212+
.filter_map(|subpath| subpath_to_bezpath_with(pool, paths, &curve_types, subpath))
213+
.collect()
197214
}
198215

199216
#[cfg(test)]

0 commit comments

Comments
 (0)