From 51319579dc736eafcdb20c6b1ce0513faafbdedc Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Tue, 18 Feb 2025 14:11:21 +0000 Subject: [PATCH] WIP TODO squash this commit --- Cargo.lock | 24 ++- node-graph/gcore/Cargo.toml | 9 +- .../gcore/src/vector/merge_by_distance.rs | 94 +++++++++++ node-graph/gcore/src/vector/mod.rs | 2 + node-graph/gcore/src/vector/vector_data.rs | 24 +++ .../src/vector/vector_data/attributes.rs | 65 +++++++- .../gcore/src/vector/vector_data/indexed.rs | 146 ++++++++++++++++++ node-graph/gcore/src/vector/vector_nodes.rs | 27 +++- 8 files changed, 374 insertions(+), 17 deletions(-) create mode 100644 node-graph/gcore/src/vector/merge_by_distance.rs create mode 100644 node-graph/gcore/src/vector/vector_data/indexed.rs diff --git a/Cargo.lock b/Cargo.lock index e04ac9aeae..317b113f3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1730,6 +1730,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.0.35" @@ -2459,8 +2465,10 @@ dependencies = [ "node-macro", "num-derive", "num-traits", + "petgraph 0.7.1", "rand 0.9.0", "rand_chacha 0.9.0", + "rustc-hash 2.1.0", "rustybuzz 0.20.1", "serde", "serde_json", @@ -3881,7 +3889,7 @@ dependencies = [ "hexf-parse", "indexmap 2.7.1", "log", - "petgraph", + "petgraph 0.6.5", "rustc-hash 1.1.0", "spirv", "termcolor", @@ -4623,7 +4631,17 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ - "fixedbitset", + "fixedbitset 0.4.2", + "indexmap 2.7.1", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset 0.5.7", "indexmap 2.7.1", ] @@ -7158,7 +7176,7 @@ dependencies = [ "memchr", "nom", "once_cell", - "petgraph", + "petgraph 0.6.5", ] [[package]] diff --git a/node-graph/gcore/Cargo.toml b/node-graph/gcore/Cargo.toml index 80fc2462e0..d929a67636 100644 --- a/node-graph/gcore/Cargo.toml +++ b/node-graph/gcore/Cargo.toml @@ -28,10 +28,7 @@ std = [ "image", "reflections", ] -reflections = [ - "alloc", - "ctor", -] +reflections = ["alloc", "ctor"] serde = [ "dep:serde", "glam/serde", @@ -67,7 +64,7 @@ rand_chacha = { workspace = true, optional = true } bezier-rs = { workspace = true, optional = true } kurbo = { workspace = true, optional = true } base64 = { workspace = true, optional = true } -vello = { workspace = true, optional = true } +vello = { workspace = true, optional = true } wgpu = { workspace = true, optional = true } specta = { workspace = true, optional = true } rustybuzz = { workspace = true, optional = true } @@ -80,6 +77,8 @@ image = { workspace = true, optional = true, default-features = false, features "png", ] } math-parser = { path = "../../libraries/math-parser" } +rustc-hash = { workspace = true } +petgraph = "0.7.1" [dev-dependencies] # Workspace dependencies diff --git a/node-graph/gcore/src/vector/merge_by_distance.rs b/node-graph/gcore/src/vector/merge_by_distance.rs new file mode 100644 index 0000000000..15f17904a3 --- /dev/null +++ b/node-graph/gcore/src/vector/merge_by_distance.rs @@ -0,0 +1,94 @@ +//! Collapse all nodes with all edges < distance + +use core::mem; + +use glam::DVec2; +use petgraph::prelude::UnGraphMap; +use rustc_hash::FxHashSet; + +use super::PointId; +use super::VectorData; + +use super::IndexedVectorData; + +impl VectorData { + /// Collapse all nodes with all edges < distance + pub(crate) fn merge_by_distance(&mut self, distance: f64) { + // treat self as an undirected graph with point = node, and segment = edge + let mut data = IndexedVectorData::from_data(&*self); + + // Graph that will contain only short edges. References data graph + let mut short_edges = UnGraphMap::new(); + for seg_id in data.segments() { + let length = data.segment_chord_length(seg_id); + if length < distance { + let [start, end] = data.segment_ends(seg_id); + let start = data.point_graph.node_weight(start).unwrap().id; + let end = data.point_graph.node_weight(end).unwrap().id; + + short_edges.add_node(start); + short_edges.add_node(end); + short_edges.add_edge(start, end, seg_id); + } + } + + // Now group connected segments - all will be collapsed to a single point. + // Note: there are a few algos for this - perhaps test empirically to find fastest + let collapse: Vec> = petgraph::algo::tarjan_scc(&short_edges).into_iter().map(|connected| connected.into_iter().collect()).collect(); + let average_position = collapse + .iter() + .map(|collapse_set| { + let sum: DVec2 = collapse_set.iter().map(|&id| data.point_position(id)).sum(); + sum / collapse_set.len() as f64 + }) + .collect::>(); + + // steal the point->offset mapping before we drop the indexes + let point_to_offset = mem::replace(&mut data.point_to_offset, Default::default()); + drop(data); + + // we collect all points up and delete them at the end, so that our indices aren't invalidated + let mut points_to_delete = FxHashSet::default(); + let mut segments_to_delete = FxHashSet::default(); + for (mut collapse_set, average_pos) in collapse.into_iter().zip(average_position.into_iter()) { + // remove any segments where both endpoints are in the collapse set + segments_to_delete.extend(self.segment_domain.iter().filter_map(|(id, start_offset, end_offset, _)| { + let start = self.point_domain.ids()[start_offset]; + let end = self.point_domain.ids()[end_offset]; + if collapse_set.contains(&start) && collapse_set.contains(&end) { + Some(id) + } else { + None + } + })); + + // Delete all points but the first (arbitrary). Set that point's position to the + // average of the points, update segments to use replace all points with collapsed + // point. + + // Unwrap: set created from connected algo will not be empty + let first_id = collapse_set.iter().copied().next().unwrap(); + // `first_id` the point we will collapse to. + collapse_set.remove(&first_id); + let first_offset = point_to_offset[&first_id]; + self.point_domain.positions[first_offset] = average_pos; + + // look for segments with ends in collapse_set and replace them with the point we are collapsing to + for (_, start_offset, end_offset, ..) in self.segment_domain.iter_mut() { + let start_id = self.point_domain.ids()[*start_offset]; + let end_id = self.point_domain.ids()[*end_offset]; + if collapse_set.contains(&start_id) { + *start_offset = first_offset; + } else if collapse_set.contains(&end_id) { + *end_offset = first_offset; + } + } + + points_to_delete.extend(collapse_set) + } + self.segment_domain.retain(|id| !segments_to_delete.contains(id), usize::MAX); + self.point_domain.retain(&mut self.segment_domain, |id| !points_to_delete.contains(id)); + + // TODO: don't forget about faces + } +} diff --git a/node-graph/gcore/src/vector/mod.rs b/node-graph/gcore/src/vector/mod.rs index e729eede53..46ec7c3b6d 100644 --- a/node-graph/gcore/src/vector/mod.rs +++ b/node-graph/gcore/src/vector/mod.rs @@ -12,3 +12,5 @@ mod vector_nodes; pub use vector_nodes::*; pub use bezier_rs; + +mod merge_by_distance; diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index 8502b7178b..53816f1634 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -1,6 +1,9 @@ mod attributes; +mod indexed; mod modification; + pub use attributes::*; +pub use indexed::IndexedVectorData; pub use modification::*; use super::style::{PathStyle, Stroke}; @@ -12,6 +15,7 @@ use dyn_any::DynAny; use core::borrow::Borrow; use glam::{DAffine2, DVec2}; +use std::borrow::Cow; // TODO: Eventually remove this migration document upgrade code pub fn migrate_vector_data<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { @@ -35,6 +39,8 @@ pub type VectorDataTable = Instances; /// [VectorData] is passed between nodes. /// It contains a list of subpaths (that may be open or closed), a transform, and some style information. +/// +/// Segments are connected if they share end points. #[derive(Clone, Debug, PartialEq, DynAny)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct VectorData { @@ -65,6 +71,24 @@ impl core::hash::Hash for VectorData { } } +impl<'a> From<&'a VectorData> for Cow<'a, VectorData> { + fn from(value: &'a VectorData) -> Self { + Self::Borrowed(value) + } +} + +impl<'a> From<&'a mut VectorData> for Cow<'a, VectorData> { + fn from(value: &'a mut VectorData) -> Self { + Self::Borrowed(value) + } +} + +impl From for Cow<'static, VectorData> { + fn from(value: VectorData) -> Self { + Self::Owned(value) + } +} + impl VectorData { /// An empty subpath with no data, an identity transform, and a black fill. pub const fn empty() -> Self { diff --git a/node-graph/gcore/src/vector/vector_data/attributes.rs b/node-graph/gcore/src/vector/vector_data/attributes.rs index cb9b7b1db2..c3cdab40de 100644 --- a/node-graph/gcore/src/vector/vector_data/attributes.rs +++ b/node-graph/gcore/src/vector/vector_data/attributes.rs @@ -1,8 +1,10 @@ use crate::vector::vector_data::{HandleId, VectorData, VectorDataTable}; use crate::vector::ConcatElement; +use bezier_rs::BezierHandles; use dyn_any::DynAny; +use core::iter::zip; use glam::{DAffine2, DVec2}; use std::collections::HashMap; use std::hash::{Hash, Hasher}; @@ -82,7 +84,7 @@ impl core::hash::BuildHasher for NoHashBuilder { /// Stores data which is per-point. Each point is merely a position and can be used in a point cloud or to for a bézier path. In future this will be extendable at runtime with custom attributes. pub struct PointDomain { id: Vec, - positions: Vec, + pub(crate) positions: Vec, } impl core::hash::Hash for PointDomain { @@ -117,7 +119,8 @@ impl PointDomain { id_map.push(new_index); new_index += 1; } else { - id_map.push(usize::MAX); // A placeholder for invalid ids. This is checked after the segment domain is modified. + // A placeholder for invalid ids. This is checked after the segment domain is modified. + id_map.push(usize::MAX); } } @@ -149,6 +152,15 @@ impl PointDomain { self.positions[index] = position; } + pub fn set_position_by_id(&mut self, id: PointId, position: DVec2) { + let Some(idx) = self.resolve_id(id) else { + // If id not found do nothing. + debug_assert!(false, "tried to find Point ID that was not present"); + return; + }; + self.set_position(idx, position) + } + pub fn ids(&self) -> &[PointId] { &self.id } @@ -184,6 +196,11 @@ impl PointDomain { *pos = transform.transform_point2(*pos); } } + + /// Iterate over point IDs and positions + pub fn iter(&self) -> impl Iterator + '_ { + self.ids().iter().copied().zip(self.positions().iter().copied()) + } } #[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)] @@ -359,6 +376,7 @@ impl SegmentDomain { }) } + /// Get index from ID, `O(n)` fn id_to_index(&self, id: SegmentId) -> Option { debug_assert_eq!(self.ids.len(), self.handles.len()); debug_assert_eq!(self.ids.len(), self.start_point.len()); @@ -413,11 +431,35 @@ impl SegmentDomain { pub(crate) fn connected_count(&self, point: usize) -> usize { self.all_connected(point).count() } + + /// Iterate over segments in the domain. + /// + /// tuple is: (id, start point, end point, handles) + pub(crate) fn iter(&self) -> impl Iterator + '_ { + let ids = self.ids.iter().copied(); + let start_point = self.start_point.iter().copied(); + let end_point = self.end_point.iter().copied(); + let handles = self.handles.iter().copied(); + zip(ids, zip(start_point, zip(end_point, handles))).map(|(id, (start_point, (end_point, handles)))| (id, start_point, end_point, handles)) + } + + /// Iterate over segments in the domain. + /// + /// tuple is: (id, start point, end point, handles) + pub(crate) fn iter_mut(&mut self) -> impl Iterator + '_ { + let ids = self.ids.iter_mut(); + let start_point = self.start_point.iter_mut(); + let end_point = self.end_point.iter_mut(); + let handles = self.handles.iter_mut(); + zip(ids, zip(start_point, zip(end_point, handles))).map(|(id, (start_point, (end_point, handles)))| (id, start_point, end_point, handles)) + } } #[derive(Clone, Debug, Default, PartialEq, Hash, DynAny)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -/// Stores data which is per-region. A region is an enclosed area composed of a range of segments from the [`SegmentDomain`] that can be given a fill. In future this will be extendable at runtime with custom attributes. +/// Stores data which is per-region. A region is an enclosed area composed of a range +/// of segments from the [`SegmentDomain`] that can be given a fill. In future this will +/// be extendable at runtime with custom attributes. pub struct RegionDomain { ids: Vec, segment_range: Vec>, @@ -457,10 +499,6 @@ impl RegionDomain { self.fill.push(fill); } - fn _resolve_id(&self, id: RegionId) -> Option { - self.ids.iter().position(|&check_id| check_id == id) - } - pub fn next_id(&self) -> RegionId { self.ids.iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(RegionId::ZERO) } @@ -504,6 +542,12 @@ impl RegionDomain { } } +pub struct Region { + pub id: RegionId, + pub segment_range: core::ops::RangeInclusive, + pub fill: FillId, +} + impl VectorData { /// Construct a [`bezier_rs::Bezier`] curve spanning from the resolved position of the start and end points with the specified handles. fn segment_to_bezier_with_index(&self, start: usize, end: usize, handles: bezier_rs::BezierHandles) -> bezier_rs::Bezier { @@ -653,6 +697,13 @@ impl VectorData { self.segment_domain.map_ids(&id_map); self.region_domain.map_ids(&id_map); } + + pub fn regions(&self) -> impl Iterator + '_ { + let ids = self.region_domain.ids.iter().copied(); + let segment_range = self.region_domain.segment_range.iter().cloned(); + let fill = self.region_domain.fill.iter().copied(); + zip(ids, zip(segment_range, fill)).map(|(id, (segment_range, fill))| Region { id, segment_range, fill }) + } } #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] diff --git a/node-graph/gcore/src/vector/vector_data/indexed.rs b/node-graph/gcore/src/vector/vector_data/indexed.rs new file mode 100644 index 0000000000..2a963fd3a2 --- /dev/null +++ b/node-graph/gcore/src/vector/vector_data/indexed.rs @@ -0,0 +1,146 @@ +use std::borrow::Cow; + +use bezier_rs::BezierHandles; +use glam::DVec2; +use petgraph::graph::{EdgeIndex, NodeIndex, UnGraph}; +use rustc_hash::FxHashMap; + +use super::{PointId, SegmentId, VectorData}; + +pub struct IndexedVectorData<'a> { + /// This structure owns the data to allow convenient modification + storage + /// Can work on owned data or a reference + pub(crate) data: Cow<'a, VectorData>, + /// Points and segments form a graph. Store it here in a form amenable to graph algos + pub(crate) point_graph: UnGraph, + pub(crate) point_to_node: FxHashMap, + pub(crate) segment_to_edge: FxHashMap, + /// Get offset from point id + pub(crate) point_to_offset: FxHashMap, + /// Get offset from segment id + pub(crate) segment_to_offset: FxHashMap, + // TODO: faces +} + +pub struct Point { + pub offset: usize, + pub id: PointId, + pub position: DVec2, +} + +pub struct Segment { + pub id: SegmentId, + pub handles: BezierHandles, +} + +impl<'a> IndexedVectorData<'a> { + /// Build indexes (`O(n)` operation). + pub fn from_data(data: impl Into>) -> Self { + let data = data.into(); + let point_to_offset = data.point_domain.ids().iter().copied().enumerate().map(flip).collect::>(); + let segment_to_offset = data.segment_domain.ids().iter().copied().enumerate().map(flip).collect::>(); + let mut point_to_node = FxHashMap::default(); + let mut segment_to_edge = FxHashMap::default(); + + let mut graph = UnGraph::new_undirected(); + + for (offset, (point_id, position)) in data.point_domain.iter().enumerate() { + let idx = graph.add_node(Point { offset, id: point_id, position }); + point_to_node.insert(point_id, idx); + } + for (segment_id, start_offset, end_offset, handles) in data.segment_domain.iter() { + let start_id = data.point_domain.ids()[start_offset]; + let end_id = data.point_domain.ids()[end_offset]; + let segment = Segment { id: segment_id, handles }; + let edge = graph.add_edge(point_to_node[&start_id], point_to_node[&end_id], segment); + + segment_to_edge.insert(segment_id, edge); + } + + Self { + data, + point_graph: graph, + point_to_node, + segment_to_edge, + point_to_offset, + segment_to_offset, + } + } + + /// The underlying `VectorData`. + /// + /// `VectorData` is read-only to ensure the indexes remain consistent. + pub fn data(&self) -> &VectorData { + &self.data + } + + /// Discard indexes and recover `VectorData`. + /// + /// # Panics + /// + /// This function will panic if underlying data is borrowed, not owned. + pub fn into_data(self) -> VectorData { + let Cow::Owned(data) = self.data else { + panic!("expected owned data"); + }; + data + } + + /// Fetch the length of given segment's chord. + /// + /// `O(1)` + /// + /// # Panics + /// + /// Will panic if not segment with the given ID is found. + pub fn segment_chord_length(&self, id: SegmentId) -> f64 { + let edge_idx = self.segment_to_edge[&id]; + let (start, end) = self.point_graph.edge_endpoints(edge_idx).unwrap(); + let start_position = self.point_graph.node_weight(start).unwrap().position; + let end_position = self.point_graph.node_weight(end).unwrap().position; + (start_position - end_position).length() + } + + /// Iterate over all segments (by ID) + /// + /// O(n) (if all iterator is consumed) + pub fn segments(&self) -> impl Iterator + '_ { + self.data.segment_domain.ids().iter().copied() + } + + /// Get the ends of a segment + /// + /// The IDs will be ordered [smallest, largest] so they can be used to find other segments with + /// the same endpoints, regardless of direction. + /// + /// O(1) + /// + /// # Panics + /// + /// This function will panic if the ID is not present. + pub fn segment_ends(&self, id: SegmentId) -> [NodeIndex; 2] { + let (start, end) = self.point_graph.edge_endpoints(self.segment_to_edge[&id]).unwrap(); + if start < end { + [start, end] + } else { + [end, start] + } + } + + /// Get the physical location of a point + /// + /// O(1) + /// + /// # Panics + /// + /// Will panic if `id` isn't in the data. + pub fn point_position(&self, id: PointId) -> DVec2 { + let offset = self.point_to_offset[&id]; + self.data.point_domain.positions()[offset] + } +} + +/// flip fields in 2-tuple +fn flip((t, u): (T, U)) -> (U, T) { + (u, t) +} diff --git a/node-graph/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index be1a9f4a16..1589e275b4 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -1099,9 +1099,9 @@ fn bevel_algorithm(mut vector_data: VectorData, distance: f64) -> VectorData { bezier.split(bezier_rs::TValue::Parametric(parametric))[1] } - /// Produces a list that correspons with the point id. The value is how many segments are connected. + /// Produces a list that corresponds with the point id. The value is how many segments are connected. fn segments_connected_count(vector_data: &VectorData) -> Vec { - // Count the number of segments connectign to each point. + // Count the number of segments connecting to each point. let mut segments_connected_count = vec![0; vector_data.point_domain.ids().len()]; for &point_index in vector_data.segment_domain.start_point().iter().chain(vector_data.segment_domain.end_point()) { segments_connected_count[point_index] += 1; @@ -1220,6 +1220,29 @@ async fn bevel( VectorDataTable::new(result) } +#[node_macro::node(category("Vector"), path(graphene_core::vector))] +async fn merge_by_distance( + #[implementations( + (), + Footprint, + )] + footprint: F, + #[implementations( + () -> VectorDataTable, + Footprint -> VectorDataTable, + )] + source: impl Node, + #[default(10.)] distance: Length, +) -> VectorDataTable { + let source = source.eval(footprint).await; + let source = source.one_item(); + + let mut result = source.clone(); + result.merge_by_distance(distance); + + VectorDataTable::new(result) +} + #[node_macro::node(category("Vector"), path(graphene_core::vector))] async fn area(_: (), vector_data: impl Node) -> f64 { let vector_data = vector_data.eval(Footprint::default()).await;