Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions node-graph/libraries/core-types/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: Clone + Send + Sync + Default + Debug + PartialEq + CacheHash + 'static>(&mut self, key: String, values: Vec<T>) {
// 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::<Attribute<T>>().is_some());

let attribute: Box<dyn AnyAttribute> = 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<String>) -> Option<String> {
self.attributes.iter().find_map(|(k, attribute)| {
Expand Down Expand Up @@ -1029,6 +1046,15 @@ impl<T> List<T> {
}
}

/// Creates a list holding the given elements in order, with no attributes.
pub fn new_from_elements(elements: Vec<T>) -> 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<T>) -> Self {
let mut attributes = Attributes::new();
Expand Down Expand Up @@ -1140,6 +1166,18 @@ impl<T> List<T> {
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<U: Clone + Send + Sync + Default + Debug + PartialEq + CacheHash + 'static>(&mut self, key: impl Into<String>, values: Vec<U>) {
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<String>, index: usize, value: AttributeValueDyn) {
Expand Down Expand Up @@ -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::<u64>(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::<Vec<_>>(), [ATTR_START, ATTR_END]);
assert_eq!(list.iter_attribute_values::<u64>(ATTR_START).unwrap().copied().collect::<Vec<_>>(), [4, 5, 6]);

list.set_attribute_values(ATTR_END, vec![9_u64]);
assert_eq!(list.iter_attribute_values::<u64>(ATTR_END).unwrap().copied().collect::<Vec<_>>(), [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::<Vec<_>>(), [ATTR_END, ATTR_START]);
assert_eq!(list, one_at_a_time);
}
}
8 changes: 3 additions & 5 deletions node-graph/libraries/vector-types/src/vector/addressing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,11 @@ pub fn spans_at(pool_index: u32, paths: &List<Path>, 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();
Expand Down
82 changes: 52 additions & 30 deletions node-graph/libraries/vector-types/src/vector/connectivity.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
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.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
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.
Expand Down Expand Up @@ -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;
}
Expand All @@ -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<Path>) -> 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};

Expand All @@ -101,21 +116,28 @@ 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() {
let paths = paths(&[(CurveType::Cubic, &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0]), (CurveType::Linear, &[0, 12, 6])]);
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));
}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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);
}
}
35 changes: 26 additions & 9 deletions node-graph/libraries/vector-types/src/vector/contour.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -100,26 +100,35 @@ 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<Path>) -> Vec<Subpath> {
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<Path>) -> Vec<CurveType> {
(0..paths.len()).map(|index| curve_type_at(paths, index)).collect()
}

fn subpaths_with(point_count: usize, paths: &List<Path>, curve_types: &[CurveType]) -> Vec<Subpath> {
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::<Option<Vec<u32>>>() 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 };

edges.push(Edge {
path: path_index as u32,
span: span as u32,
start,
end,
start: start as u32,
end: end as u32,
});
}
}
Expand Down Expand Up @@ -174,12 +183,16 @@ pub fn subpaths(point_count: usize, paths: &List<Path>) -> Vec<Subpath> {

/// Emits one subpath as a bezpath, or `None` when it has no drawable geometry.
pub fn subpath_to_bezpath(pool: &List<DVec2>, paths: &List<Path>, subpath: &Subpath) -> Option<BezPath> {
subpath_to_bezpath_with(pool, paths, &curve_types(paths), subpath)
}

fn subpath_to_bezpath_with(pool: &List<DVec2>, paths: &List<Path>, curve_types: &[CurveType], subpath: &Subpath) -> Option<BezPath> {
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 });
}
Expand All @@ -193,7 +206,11 @@ pub fn subpath_to_bezpath(pool: &List<DVec2>, paths: &List<Path>, subpath: &Subp

/// Assembles the paths into contours for rendering, one bezpath per subpath in the order they are walked.
pub fn contours(pool: &List<DVec2>, paths: &List<Path>) -> Vec<BezPath> {
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)]
Expand Down
Loading
Loading