Skip to content

Commit 46f48a5

Browse files
authored
Add new vector pool storage types, not yet used (#4732)
* Add the Path and CurveType primitives for the pool-based Vector format * Resolve path segments and emit contours against the Vector point pool * Read a vector's paths and their curve types off its holder item * Weave bezpath contours into pool points and stride-uniform paths * Index which paths meet at each pool point for topology queries * Assemble render contours by following welds across split paths * Address segments as path spans while keeping stable point and segment IDs * Give the 'flatten_add' test its own node ID counter so it can't flake
1 parent 243a83c commit 46f48a5

10 files changed

Lines changed: 1222 additions & 7 deletions

File tree

‎node-graph/graph-craft/src/document.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,7 @@ mod test {
13391339
use crate::proto::{ConstructionArgs, ProtoNetwork, ProtoNode};
13401340
use std::sync::atomic::AtomicU64;
13411341

1342+
/// Generates IDs from a counter all tests share, so the IDs a test gets depend on what else runs in parallel.
13421343
fn gen_node_id() -> NodeId {
13431344
static NODE_ID: AtomicU64 = AtomicU64::new(4);
13441345
NodeId(NODE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst))
@@ -1450,7 +1451,12 @@ mod test {
14501451
..Default::default()
14511452
};
14521453
network.populate_dependants();
1453-
network.flatten_with_fns(NodeId(1), |self_id, inner_id| NodeId(self_id.0 * 10 + inner_id.0), gen_node_id);
1454+
1455+
// A counter of its own, since the expected network names the nodes by the IDs it generates
1456+
let next_id = AtomicU64::new(4);
1457+
let gen_id = || NodeId(next_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst));
1458+
network.flatten_with_fns(NodeId(1), |self_id, inner_id| NodeId(self_id.0 * 10 + inner_id.0), gen_id);
1459+
14541460
let expected = flatten_add_expected();
14551461
println!("{expected:#?}");
14561462
println!("{network:#?}");

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ pub const ATTR_DIAMETER: &str = "diameter";
105105
pub const ATTR_HARDNESS: &str = "hardness";
106106
/// Brush stroke item's per-pass paint coverage from `0.` to `1.` (`f64`).
107107
pub const ATTR_FLOW: &str = "flow";
108+
/// `List<Path>` of connected strokes woven through the point pool, on the item holding a `Vector`.
109+
/// Absent means the pool is a point cloud.
110+
pub const ATTR_PATHS: &str = "paths";
111+
/// Path's `CurveType` naming the stride its point references weave in, on the `List<Path>` item
112+
/// holding the path (implicit default `Linear`).
113+
pub const ATTR_CURVE_TYPE: &str = "curve_type";
114+
/// Point reference's `bool` tool state marking the tangent pair at this anchor occurrence as smooth,
115+
/// on the `List<u32>` inside a `Path` (implicit default `false`). Editing tools keep a smooth pair
116+
/// colinear. Rendering derives continuity from actual tangent geometry and does not read this.
117+
pub const ATTR_SMOOTH: &str = "smooth";
118+
/// Item's stable ID for external addressing (modifications, tools, animation), opt-in because indices are
119+
/// the internal addressing: a `PointId` on a `Vector`'s point pool, or a `SegmentId` on the `List<u32>`
120+
/// inside a `Path` at the anchor reference the segment departs from.
121+
pub const ATTR_ID: &str = "id";
108122
/// Text item's font size in document-space units (`f64`, implicit default `24.`).
109123
pub const ATTR_FONT_SIZE: &str = "font_size";
110124
/// Text item's font, as a `Resource` of the loaded font file.
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
use super::connectivity::Connectivity;
2+
use super::misc::HandleId;
3+
use super::path::{CurveType, Path, curve_type_at};
4+
use super::vector_attributes::{PointId, SegmentId};
5+
use core_types::list::{ATTR_ID, List};
6+
use glam::DVec2;
7+
8+
/// A segment's place in the weave: the span of one path between two consecutive anchors.
9+
/// Segments are never stored, so this is how one is named.
10+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11+
pub struct Span {
12+
pub path: u32,
13+
pub index: u32,
14+
}
15+
16+
impl Span {
17+
/// The reference index where this span's starting anchor sits.
18+
pub fn start_reference(&self, curve_type: CurveType) -> usize {
19+
self.index as usize * curve_type.stride()
20+
}
21+
22+
/// The reference index where this span's ending anchor sits.
23+
pub fn end_reference(&self, curve_type: CurveType) -> usize {
24+
self.start_reference(curve_type) + curve_type.stride()
25+
}
26+
}
27+
28+
/// Every span of every path, in path order.
29+
pub fn spans(paths: &List<Path>) -> impl Iterator<Item = Span> + '_ {
30+
paths.iter_element_values().enumerate().flat_map(|(path_index, path)| {
31+
let curve_type = curve_type_at(paths, path_index);
32+
(0..path.segment_count(curve_type) as u32).map(move |index| Span { path: path_index as u32, index })
33+
})
34+
}
35+
36+
/// The stable ID naming a span, stored on the anchor reference the span departs from. A reference cell left unnamed
37+
/// holds `SegmentId::ZERO`, which sequential IDs never take.
38+
pub fn segment_id_of(paths: &List<Path>, span: Span) -> Option<SegmentId> {
39+
let path = paths.element(span.path as usize)?;
40+
let reference = span.start_reference(curve_type_at(paths, span.path as usize));
41+
42+
path.0.attribute::<SegmentId>(ATTR_ID, reference).copied()
43+
}
44+
45+
/// The span a segment ID names, found by scanning every span in O(references).
46+
pub fn span_of_segment_id(paths: &List<Path>, segment: SegmentId) -> Option<Span> {
47+
spans(paths).find(|&span| segment_id_of(paths, span) == Some(segment))
48+
}
49+
50+
/// The pool points a span runs between.
51+
pub fn span_endpoints(paths: &List<Path>, span: Span) -> Option<(u32, u32)> {
52+
let path = paths.element(span.path as usize)?;
53+
let curve_type = curve_type_at(paths, span.path as usize);
54+
let start = path.0.element(span.start_reference(curve_type))?;
55+
let end = path.0.element(span.end_reference(curve_type))?;
56+
57+
Some((*start, *end))
58+
}
59+
60+
/// The pool point a handle of the given span occupies, absent on a linear span that has no handles.
61+
pub fn handle_point(paths: &List<Path>, span: Span, primary: bool) -> Option<u32> {
62+
let path = paths.element(span.path as usize)?;
63+
let curve_type = curve_type_at(paths, span.path as usize);
64+
if curve_type == CurveType::Linear {
65+
return None;
66+
}
67+
68+
// A quadratic span's single handle answers to either side
69+
let offset = if primary || curve_type == CurveType::Quadratic { 1 } else { 2 };
70+
71+
path.0.element(span.start_reference(curve_type) + offset).copied()
72+
}
73+
74+
/// The stable ID of a pool point.
75+
pub fn point_id_at(pool: &List<DVec2>, index: u32) -> Option<PointId> {
76+
pool.attribute::<PointId>(ATTR_ID, index as usize).copied()
77+
}
78+
79+
/// The pool index a point ID names.
80+
pub fn index_of_point_id(pool: &List<DVec2>, point: PointId) -> Option<u32> {
81+
pool.iter_attribute_values::<PointId>(ATTR_ID)?.position(|&id| id == point).map(|index| index as u32)
82+
}
83+
84+
/// The spans meeting at a pool point, each paired with whether the point is where that span ends.
85+
pub fn spans_at(pool_index: u32, paths: &List<Path>, connectivity: &Connectivity) -> Vec<(Span, bool)> {
86+
let mut found = Vec::new();
87+
88+
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) {
94+
continue;
95+
}
96+
97+
// An anchor departs the span starting at it and arrives on the one before it
98+
let anchor = reference.index as usize / curve_type.stride();
99+
if anchor < path.segment_count(curve_type) {
100+
found.push((
101+
Span {
102+
path: reference.path,
103+
index: anchor as u32,
104+
},
105+
false,
106+
));
107+
}
108+
if let Some(previous) = anchor.checked_sub(1) {
109+
found.push((
110+
Span {
111+
path: reference.path,
112+
index: previous as u32,
113+
},
114+
true,
115+
));
116+
}
117+
}
118+
119+
found
120+
}
121+
122+
/// The segments departing the given pool point.
123+
pub fn start_connected(pool_index: u32, paths: &List<Path>, connectivity: &Connectivity) -> Vec<SegmentId> {
124+
spans_at(pool_index, paths, connectivity)
125+
.into_iter()
126+
.filter(|&(_, arriving)| !arriving)
127+
.filter_map(|(span, _)| segment_id_of(paths, span))
128+
.collect()
129+
}
130+
131+
/// The segments arriving at the given pool point.
132+
pub fn end_connected(pool_index: u32, paths: &List<Path>, connectivity: &Connectivity) -> Vec<SegmentId> {
133+
spans_at(pool_index, paths, connectivity)
134+
.into_iter()
135+
.filter(|&(_, arriving)| arriving)
136+
.filter_map(|(span, _)| segment_id_of(paths, span))
137+
.collect()
138+
}
139+
140+
/// The handles of every segment meeting the given pool point, naming the side that faces it.
141+
pub fn all_connected(pool_index: u32, paths: &List<Path>, connectivity: &Connectivity) -> Vec<HandleId> {
142+
spans_at(pool_index, paths, connectivity)
143+
.into_iter()
144+
.filter_map(|(span, arriving)| {
145+
let segment = segment_id_of(paths, span)?;
146+
Some(if arriving { HandleId::end(segment) } else { HandleId::primary(segment) })
147+
})
148+
.collect()
149+
}
150+
151+
#[cfg(test)]
152+
mod tests {
153+
use super::*;
154+
use core_types::list::{ATTR_CURVE_TYPE, Item};
155+
156+
fn pool(count: u32) -> List<DVec2> {
157+
(0..count)
158+
.map(|index| Item::new_from_element(DVec2::new(index as f64, 0.)).with_attribute(ATTR_ID, PointId::generate()))
159+
.collect()
160+
}
161+
162+
fn paths_of(specs: &[(CurveType, &[u32])]) -> List<Path> {
163+
let mut paths = List::new();
164+
165+
for &(curve_type, references) in specs {
166+
let mut path = Path(references.iter().map(|&reference| Item::new_from_element(reference)).collect());
167+
168+
// Name each span on the anchor it departs from
169+
for span in 0..path.segment_count(curve_type) {
170+
path.0.set_attribute(ATTR_ID, span * curve_type.stride(), SegmentId::generate());
171+
}
172+
173+
paths.push(Item::new_from_element(path).with_attribute(ATTR_CURVE_TYPE, curve_type));
174+
}
175+
176+
paths
177+
}
178+
179+
#[test]
180+
fn spans_name_every_segment() {
181+
let paths = paths_of(&[(CurveType::Linear, &[0, 1, 2]), (CurveType::Cubic, &[2, 3, 4, 5])]);
182+
183+
let all: Vec<_> = spans(&paths).collect();
184+
assert_eq!(all.len(), 3);
185+
assert_eq!(all[2], Span { path: 1, index: 0 });
186+
187+
for span in all {
188+
let id = segment_id_of(&paths, span).expect("every span was named");
189+
assert_eq!(span_of_segment_id(&paths, id), Some(span));
190+
}
191+
}
192+
193+
#[test]
194+
fn spans_resolve_their_endpoints_and_handles() {
195+
let paths = paths_of(&[(CurveType::Cubic, &[0, 1, 2, 3])]);
196+
let span = Span { path: 0, index: 0 };
197+
198+
assert_eq!(span_endpoints(&paths, span), Some((0, 3)));
199+
assert_eq!(handle_point(&paths, span, true), Some(1));
200+
assert_eq!(handle_point(&paths, span, false), Some(2));
201+
202+
let linear = paths_of(&[(CurveType::Linear, &[0, 1])]);
203+
assert_eq!(handle_point(&linear, Span { path: 0, index: 0 }, true), None);
204+
}
205+
206+
// A welded anchor reports the segments on both sides, each from the path that owns it
207+
#[test]
208+
fn connectivity_by_segment_id() {
209+
let paths = paths_of(&[(CurveType::Linear, &[0, 1]), (CurveType::Linear, &[1, 2])]);
210+
let connectivity = Connectivity::build(3, &paths);
211+
212+
assert_eq!(start_connected(0, &paths, &connectivity).len(), 1);
213+
assert!(end_connected(0, &paths, &connectivity).is_empty());
214+
215+
assert_eq!(start_connected(1, &paths, &connectivity).len(), 1);
216+
assert_eq!(end_connected(1, &paths, &connectivity).len(), 1);
217+
assert_eq!(all_connected(1, &paths, &connectivity).len(), 2);
218+
}
219+
220+
#[test]
221+
fn point_ids_resolve_both_ways() {
222+
let pool = pool(4);
223+
224+
for index in 0..4 {
225+
let id = point_id_at(&pool, index).expect("every point was named");
226+
assert_eq!(index_of_point_id(&pool, id), Some(index));
227+
}
228+
}
229+
}

‎node-graph/libraries/vector-types/src/vector/algorithms/consts.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ pub(crate) const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3;
66

77
/// Maximum distance at which two points are treated as one and the same point.
88
pub(crate) const MAX_COINCIDENT_POINT_DISTANCE: f64 = 1e-7;
9+
10+
/// Distance below which a path's closing point is treated as coincident with its start point.
11+
/// Matches Kurbo's default path accuracy, so points within an offset operation's own precision are not split into separate anchors.
12+
pub(crate) const CLOSE_POINT_TOLERANCE: f64 = 1e-6;

0 commit comments

Comments
 (0)