Skip to content
8 changes: 7 additions & 1 deletion node-graph/graph-craft/src/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,7 @@ mod test {
use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode};
use std::sync::atomic::AtomicU64;

/// Generates IDs from a counter all tests share, so the IDs a test gets depend on what else runs in parallel.
fn gen_node_id() -> NodeId {
static NODE_ID: AtomicU64 = AtomicU64::new(4);
NodeId(NODE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst))
Expand Down Expand Up @@ -1450,7 +1451,12 @@ mod test {
..Default::default()
};
network.populate_dependants();
network.flatten_with_fns(NodeId(1), |self_id, inner_id| NodeId(self_id.0 * 10 + inner_id.0), gen_node_id);

// A counter of its own, since the expected network names the nodes by the IDs it generates
let next_id = AtomicU64::new(4);
let gen_id = || NodeId(next_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst));
network.flatten_with_fns(NodeId(1), |self_id, inner_id| NodeId(self_id.0 * 10 + inner_id.0), gen_id);

let expected = flatten_add_expected();
println!("{expected:#?}");
println!("{network:#?}");
Expand Down
14 changes: 14 additions & 0 deletions node-graph/libraries/core-types/src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ pub const ATTR_DIAMETER: &str = "diameter";
pub const ATTR_HARDNESS: &str = "hardness";
/// Brush stroke item's per-pass paint coverage from `0.` to `1.` (`f64`).
pub const ATTR_FLOW: &str = "flow";
/// `List<Path>` of connected strokes woven through the point pool, on the item holding a `Vector`.
/// Absent means the pool is a point cloud.
pub const ATTR_PATHS: &str = "paths";
/// Path's `CurveType` naming the stride its point references weave in, on the `List<Path>` item
/// holding the path (implicit default `Linear`).
pub const ATTR_CURVE_TYPE: &str = "curve_type";
/// Point reference's `bool` tool state marking the tangent pair at this anchor occurrence as smooth,
/// on the `List<u32>` inside a `Path` (implicit default `false`). Editing tools keep a smooth pair
/// colinear. Rendering derives continuity from actual tangent geometry and does not read this.
pub const ATTR_SMOOTH: &str = "smooth";
/// Item's stable ID for external addressing (modifications, tools, animation), opt-in because indices are
/// the internal addressing: a `PointId` on a `Vector`'s point pool, or a `SegmentId` on the `List<u32>`
/// inside a `Path` at the anchor reference the segment departs from.
pub const ATTR_ID: &str = "id";
/// Text item's font size in document-space units (`f64`, implicit default `24.`).
pub const ATTR_FONT_SIZE: &str = "font_size";
/// Text item's font, as a `Resource` of the loaded font file.
Expand Down
229 changes: 229 additions & 0 deletions node-graph/libraries/vector-types/src/vector/addressing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
use super::connectivity::Connectivity;
use super::misc::HandleId;
use super::path::{CurveType, Path, curve_type_at};
use super::vector_attributes::{PointId, SegmentId};
use core_types::list::{ATTR_ID, List};
use glam::DVec2;

/// A segment's place in the weave: the span of one path between two consecutive anchors.
/// Segments are never stored, so this is how one is named.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Span {
pub path: u32,
pub index: u32,
}

impl Span {
/// The reference index where this span's starting anchor sits.
pub fn start_reference(&self, curve_type: CurveType) -> usize {
self.index as usize * curve_type.stride()
}

/// The reference index where this span's ending anchor sits.
pub fn end_reference(&self, curve_type: CurveType) -> usize {
self.start_reference(curve_type) + curve_type.stride()
}
}

/// Every span of every path, in path order.
pub fn spans(paths: &List<Path>) -> impl Iterator<Item = Span> + '_ {
paths.iter_element_values().enumerate().flat_map(|(path_index, path)| {
let curve_type = curve_type_at(paths, path_index);
(0..path.segment_count(curve_type) as u32).map(move |index| Span { path: path_index as u32, index })
})
}

/// The stable ID naming a span, stored on the anchor reference the span departs from. A reference cell left unnamed
/// holds `SegmentId::ZERO`, which sequential IDs never take.
pub fn segment_id_of(paths: &List<Path>, span: Span) -> Option<SegmentId> {
let path = paths.element(span.path as usize)?;
let reference = span.start_reference(curve_type_at(paths, span.path as usize));

path.0.attribute::<SegmentId>(ATTR_ID, reference).copied()
Comment thread
Keavon marked this conversation as resolved.
}

/// The span a segment ID names, found by scanning every span in O(references).
pub fn span_of_segment_id(paths: &List<Path>, segment: SegmentId) -> Option<Span> {
spans(paths).find(|&span| segment_id_of(paths, span) == Some(segment))
}

/// The pool points a span runs between.
pub fn span_endpoints(paths: &List<Path>, span: Span) -> Option<(u32, u32)> {
let path = paths.element(span.path as usize)?;
let curve_type = curve_type_at(paths, span.path as usize);
let start = path.0.element(span.start_reference(curve_type))?;
let end = path.0.element(span.end_reference(curve_type))?;

Some((*start, *end))
}

/// The pool point a handle of the given span occupies, absent on a linear span that has no handles.
pub fn handle_point(paths: &List<Path>, span: Span, primary: bool) -> Option<u32> {
let path = paths.element(span.path as usize)?;
let curve_type = curve_type_at(paths, span.path as usize);
if curve_type == CurveType::Linear {
return None;
}

// A quadratic span's single handle answers to either side
let offset = if primary || curve_type == CurveType::Quadratic { 1 } else { 2 };

path.0.element(span.start_reference(curve_type) + offset).copied()
}

/// The stable ID of a pool point.
pub fn point_id_at(pool: &List<DVec2>, index: u32) -> Option<PointId> {
pool.attribute::<PointId>(ATTR_ID, index as usize).copied()
}

/// The pool index a point ID names.
pub fn index_of_point_id(pool: &List<DVec2>, point: PointId) -> Option<u32> {
pool.iter_attribute_values::<PointId>(ATTR_ID)?.position(|&id| id == point).map(|index| index as u32)
}

/// The spans meeting at a pool point, each paired with whether the point is where that span ends.
pub fn spans_at(pool_index: u32, paths: &List<Path>, connectivity: &Connectivity) -> Vec<(Span, bool)> {
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) {
continue;
}

// An anchor departs the span starting at it and arrives on the one before it
let anchor = reference.index as usize / curve_type.stride();
if anchor < path.segment_count(curve_type) {
found.push((
Span {
path: reference.path,
index: anchor as u32,
},
false,
));
}
if let Some(previous) = anchor.checked_sub(1) {
found.push((
Span {
path: reference.path,
index: previous as u32,
},
true,
));
}
}

found
}

/// The segments departing the given pool point.
pub fn start_connected(pool_index: u32, paths: &List<Path>, connectivity: &Connectivity) -> Vec<SegmentId> {
spans_at(pool_index, paths, connectivity)
.into_iter()
.filter(|&(_, arriving)| !arriving)
.filter_map(|(span, _)| segment_id_of(paths, span))
.collect()
}

/// The segments arriving at the given pool point.
pub fn end_connected(pool_index: u32, paths: &List<Path>, connectivity: &Connectivity) -> Vec<SegmentId> {
spans_at(pool_index, paths, connectivity)
.into_iter()
.filter(|&(_, arriving)| arriving)
.filter_map(|(span, _)| segment_id_of(paths, span))
.collect()
}

/// The handles of every segment meeting the given pool point, naming the side that faces it.
pub fn all_connected(pool_index: u32, paths: &List<Path>, connectivity: &Connectivity) -> Vec<HandleId> {
spans_at(pool_index, paths, connectivity)
.into_iter()
.filter_map(|(span, arriving)| {
let segment = segment_id_of(paths, span)?;
Some(if arriving { HandleId::end(segment) } else { HandleId::primary(segment) })
})
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
use core_types::list::{ATTR_CURVE_TYPE, Item};

fn pool(count: u32) -> List<DVec2> {
(0..count)
.map(|index| Item::new_from_element(DVec2::new(index as f64, 0.)).with_attribute(ATTR_ID, PointId::generate()))
.collect()
}

fn paths_of(specs: &[(CurveType, &[u32])]) -> List<Path> {
let mut paths = List::new();

for &(curve_type, references) in specs {
let mut path = Path(references.iter().map(|&reference| Item::new_from_element(reference)).collect());

// Name each span on the anchor it departs from
for span in 0..path.segment_count(curve_type) {
path.0.set_attribute(ATTR_ID, span * curve_type.stride(), SegmentId::generate());
}

paths.push(Item::new_from_element(path).with_attribute(ATTR_CURVE_TYPE, curve_type));
}

paths
}

#[test]
fn spans_name_every_segment() {
let paths = paths_of(&[(CurveType::Linear, &[0, 1, 2]), (CurveType::Cubic, &[2, 3, 4, 5])]);

let all: Vec<_> = spans(&paths).collect();
assert_eq!(all.len(), 3);
assert_eq!(all[2], Span { path: 1, index: 0 });

for span in all {
let id = segment_id_of(&paths, span).expect("every span was named");
assert_eq!(span_of_segment_id(&paths, id), Some(span));
}
}

#[test]
fn spans_resolve_their_endpoints_and_handles() {
let paths = paths_of(&[(CurveType::Cubic, &[0, 1, 2, 3])]);
let span = Span { path: 0, index: 0 };

assert_eq!(span_endpoints(&paths, span), Some((0, 3)));
assert_eq!(handle_point(&paths, span, true), Some(1));
assert_eq!(handle_point(&paths, span, false), Some(2));

let linear = paths_of(&[(CurveType::Linear, &[0, 1])]);
assert_eq!(handle_point(&linear, Span { path: 0, index: 0 }, true), None);
}

// A welded anchor reports the segments on both sides, each from the path that owns it
#[test]
fn connectivity_by_segment_id() {
let paths = paths_of(&[(CurveType::Linear, &[0, 1]), (CurveType::Linear, &[1, 2])]);
let connectivity = Connectivity::build(3, &paths);

assert_eq!(start_connected(0, &paths, &connectivity).len(), 1);
assert!(end_connected(0, &paths, &connectivity).is_empty());

assert_eq!(start_connected(1, &paths, &connectivity).len(), 1);
assert_eq!(end_connected(1, &paths, &connectivity).len(), 1);
assert_eq!(all_connected(1, &paths, &connectivity).len(), 2);
}

#[test]
fn point_ids_resolve_both_ways() {
let pool = pool(4);

for index in 0..4 {
let id = point_id_at(&pool, index).expect("every point was named");
assert_eq!(index_of_point_id(&pool, id), Some(index));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ pub(crate) const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;

/// Maximum distance at which two points are treated as one and the same point.
pub(crate) const MAX_COINCIDENT_POINT_DISTANCE: f64 = 1e-7;

/// Distance below which a path's closing point is treated as coincident with its start point.
/// Matches Kurbo's default path accuracy, so points within an offset operation's own precision are not split into separate anchors.
Comment thread
Keavon marked this conversation as resolved.
pub(crate) const CLOSE_POINT_TOLERANCE: f64 = 1e-6;
Loading
Loading