From 3cb8835618157f3c4e4ada86e47f9767a7b36393 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 21 Aug 2026 21:48:31 -0700 Subject: [PATCH 1/8] Swap Vector storage to the point pool and rebuild the vector-types crate on it --- node-graph/libraries/core-types/src/bounds.rs | 28 + node-graph/libraries/core-types/src/list.rs | 16 +- .../core-types/src/render_complexity.rs | 21 +- .../vector-types/src/vector/addressing.rs | 51 +- .../vector/algorithms/merge_by_distance.rs | 170 +- .../vector-types/src/vector/connectivity.rs | 6 +- .../vector-types/src/vector/contour.rs | 11 +- .../vector-types/src/vector/flatten.rs | 324 ++++ .../libraries/vector-types/src/vector/misc.rs | 55 +- .../libraries/vector-types/src/vector/mod.rs | 1 + .../libraries/vector-types/src/vector/path.rs | 83 +- .../src/vector/vector_attributes.rs | 1606 +++++++---------- .../src/vector/vector_modification.rs | 428 ++--- .../vector-types/src/vector/vector_types.rs | 1009 +++++++---- 14 files changed, 2142 insertions(+), 1667 deletions(-) create mode 100644 node-graph/libraries/vector-types/src/vector/flatten.rs diff --git a/node-graph/libraries/core-types/src/bounds.rs b/node-graph/libraries/core-types/src/bounds.rs index 17af647c10b..f4fcd282a07 100644 --- a/node-graph/libraries/core-types/src/bounds.rs +++ b/node-graph/libraries/core-types/src/bounds.rs @@ -1,4 +1,5 @@ use crate::Color; +use crate::list::{Item, List}; use glam::{DAffine2, DVec2}; #[derive(Clone, Copy, Default, Debug, PartialEq)] @@ -20,6 +21,33 @@ pub trait BoundingBox { /// Types with no meaningful contribution (e.g., `Color`) return `Infinite` from both; the runtime substitutes a /// small fallback rectangle at the end if no finite bounds remain after combining. fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox; + + /// The bounds of an item's element, for the thumbnail when `thumbnail` is set. A type with geometry that + /// partly rides its item's attributes overrides this to read them, and the rest defer to the element alone. + fn bounding_box_in_item(item: &Item, transform: DAffine2, include_stroke: bool, thumbnail: bool) -> RenderBoundingBox + where + Self: Sized, + { + let element = item.element(); + if thumbnail { + element.thumbnail_bounding_box(transform, include_stroke) + } else { + element.bounding_box(transform, include_stroke) + } + } + + /// As [`Self::bounding_box_in_item`], for the element at one index of a list. + fn bounding_box_in_list(list: &List, index: usize, transform: DAffine2, include_stroke: bool, thumbnail: bool) -> RenderBoundingBox + where + Self: Sized, + { + let Some(element) = list.element(index) else { return RenderBoundingBox::None }; + if thumbnail { + element.thumbnail_bounding_box(transform, include_stroke) + } else { + element.bounding_box(transform, include_stroke) + } + } } macro_rules! none_impl { diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index d9f75d319aa..c2d346f3b6a 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -112,11 +112,11 @@ pub const ATTR_PATHS: &str = "paths"; /// 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` inside a `Path` (implicit default `false`). Editing tools keep a smooth pair +/// on the `List` 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` +/// the internal addressing: a `PointId` on a `Vector`'s point pool, or a `SegmentId` on the `List` /// 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.`). @@ -1214,8 +1214,8 @@ impl BoundingBox for List { fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { let mut combined_bounds = None; - for (element, item_transform) in self.iter_element_values().zip(self.iter_attribute_values_or_default::(ATTR_TRANSFORM)) { - match element.bounding_box(transform * item_transform, include_stroke) { + for (index, item_transform) in self.iter_attribute_values_or_default::(ATTR_TRANSFORM).enumerate() { + match T::bounding_box_in_list(self, index, transform * item_transform, include_stroke, false) { RenderBoundingBox::None => continue, RenderBoundingBox::Infinite => return RenderBoundingBox::Infinite, RenderBoundingBox::Rectangle(bounds) => match combined_bounds { @@ -1236,8 +1236,8 @@ impl BoundingBox for List { let mut combined_bounds = None; let mut any_infinite = false; - for (element, item_transform) in self.iter_element_values().zip(self.iter_attribute_values_or_default::(ATTR_TRANSFORM)) { - match element.thumbnail_bounding_box(transform * item_transform, include_stroke) { + for (index, item_transform) in self.iter_attribute_values_or_default::(ATTR_TRANSFORM).enumerate() { + match T::bounding_box_in_list(self, index, transform * item_transform, include_stroke, true) { RenderBoundingBox::None => continue, RenderBoundingBox::Infinite => any_infinite = true, RenderBoundingBox::Rectangle(bounds) => match combined_bounds { @@ -1357,12 +1357,12 @@ impl BoundingBox for Item { /// Computes the element's bounding box, composing the item's transform attribute with the given transform. fn bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM); - self.element().bounding_box(transform * item_transform, include_stroke) + T::bounding_box_in_item(self, transform * item_transform, include_stroke, false) } fn thumbnail_bounding_box(&self, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { let item_transform: DAffine2 = self.attribute_cloned_or_default(ATTR_TRANSFORM); - self.element().thumbnail_bounding_box(transform * item_transform, include_stroke) + T::bounding_box_in_item(self, transform * item_transform, include_stroke, true) } } diff --git a/node-graph/libraries/core-types/src/render_complexity.rs b/node-graph/libraries/core-types/src/render_complexity.rs index b9bbeeb2016..5d891b57611 100644 --- a/node-graph/libraries/core-types/src/render_complexity.rs +++ b/node-graph/libraries/core-types/src/render_complexity.rs @@ -6,17 +6,34 @@ pub trait RenderComplexity { fn render_complexity(&self) -> usize { 0 } + + /// The complexity of an item's element. A type with content that partly rides its item's attributes overrides + /// this to read them, and the rest defer to the element alone. + fn render_complexity_in_item(item: &Item) -> usize + where + Self: Sized, + { + item.element().render_complexity() + } + + /// As [`Self::render_complexity_in_item`], for the element at one index of a list. + fn render_complexity_in_list(list: &List, index: usize) -> usize + where + Self: Sized, + { + list.element(index).map_or(0, Self::render_complexity) + } } impl RenderComplexity for Item { fn render_complexity(&self) -> usize { - self.element().render_complexity() + T::render_complexity_in_item(self) } } impl RenderComplexity for List { fn render_complexity(&self) -> usize { - self.iter_element_values().map(|element| element.render_complexity()).fold(0, usize::saturating_add) + (0..self.len()).map(|index| T::render_complexity_in_list(self, index)).fold(0, usize::saturating_add) } } diff --git a/node-graph/libraries/vector-types/src/vector/addressing.rs b/node-graph/libraries/vector-types/src/vector/addressing.rs index a78719c3688..7481e3d4c61 100644 --- a/node-graph/libraries/vector-types/src/vector/addressing.rs +++ b/node-graph/libraries/vector-types/src/vector/addressing.rs @@ -51,10 +51,10 @@ pub fn span_of_segment_id(paths: &List, segment: SegmentId) -> Option, 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))?; + let start = path.reference(span.start_reference(curve_type))?; + let end = path.reference(span.end_reference(curve_type))?; - Some((*start, *end)) + Some((start, end)) } /// The pool point a handle of the given span occupies, absent on a linear span that has no handles. @@ -68,7 +68,7 @@ pub fn handle_point(paths: &List, span: Span, primary: bool) -> Option, point: PointId) -> Option { pool.iter_attribute_values::(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. +/// The spans meeting at a pool point in stored span order, each paired with whether the point is where that span ends. pub fn spans_at(pool_index: u32, paths: &List, connectivity: &Connectivity) -> Vec<(Span, bool)> { let mut found = Vec::new(); @@ -94,24 +94,24 @@ pub fn spans_at(pool_index: u32, paths: &List, connectivity: &Connectivity continue; } - // An anchor departs the span starting at it and arrives on the one before it + // An anchor arrives on the span before it and departs the one starting at it let anchor = reference.index as usize / curve_type.stride(); - if anchor < path.segment_count(curve_type) { + if let Some(previous) = anchor.checked_sub(1) { found.push(( Span { path: reference.path, - index: anchor as u32, + index: previous as u32, }, - false, + true, )); } - if let Some(previous) = anchor.checked_sub(1) { + if anchor < path.segment_count(curve_type) { found.push(( Span { path: reference.path, - index: previous as u32, + index: anchor as u32, }, - true, + false, )); } } @@ -137,10 +137,13 @@ pub fn end_connected(pool_index: u32, paths: &List, connectivity: &Connect .collect() } -/// The handles of every segment meeting the given pool point, naming the side that faces it. +/// The handles of every segment meeting the given pool point, naming the side that faces it, with departing segments first. pub fn all_connected(pool_index: u32, paths: &List, connectivity: &Connectivity) -> Vec { - spans_at(pool_index, paths, connectivity) + let (arriving, departing): (Vec<_>, Vec<_>) = spans_at(pool_index, paths, connectivity).into_iter().partition(|&(_, arriving)| arriving); + + departing .into_iter() + .chain(arriving) .filter_map(|(span, arriving)| { let segment = segment_id_of(paths, span)?; Some(if arriving { HandleId::end(segment) } else { HandleId::primary(segment) }) @@ -163,7 +166,7 @@ mod tests { 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()); + let mut path = Path::from_references(references.iter().copied()); // Name each span on the anchor it departs from for span in 0..path.segment_count(curve_type) { @@ -217,6 +220,24 @@ mod tests { assert_eq!(all_connected(1, &paths, &connectivity).len(), 2); } + // The tools take the first connected handle, so it must not depend on whether the anchor welds two paths or sits along one + #[test] + fn connected_handles_lead_with_departing_segments() { + let paths = paths_of(&[(CurveType::Linear, &[0, 1]), (CurveType::Cubic, &[1, 2, 3, 4, 5, 6, 7])]); + let connectivity = Connectivity::build(8, &paths); + let segment = |path, index| segment_id_of(&paths, Span { path, index }).expect("every span was named"); + + let welded = all_connected(1, &paths, &connectivity); + assert_eq!(welded, [HandleId::primary(segment(1, 0)), HandleId::end(segment(0, 0))]); + + let along_path = all_connected(4, &paths, &connectivity); + assert_eq!(along_path, [HandleId::primary(segment(1, 1)), HandleId::end(segment(1, 0))]); + + // The spans themselves stay in stored order + let spans = spans_at(4, &paths, &connectivity); + assert_eq!(spans, [(Span { path: 1, index: 0 }, true), (Span { path: 1, index: 1 }, false)]); + } + #[test] fn point_ids_resolve_both_ways() { let pool = pool(4); diff --git a/node-graph/libraries/vector-types/src/vector/algorithms/merge_by_distance.rs b/node-graph/libraries/vector-types/src/vector/algorithms/merge_by_distance.rs index e4cf8f25a5f..3bed3916cf1 100644 --- a/node-graph/libraries/vector-types/src/vector/algorithms/merge_by_distance.rs +++ b/node-graph/libraries/vector-types/src/vector/algorithms/merge_by_distance.rs @@ -1,4 +1,7 @@ -use crate::vector::{PointDomain, PointId, SegmentDomain, SegmentId, Vector}; +use crate::vector::flatten::WorkingVector; +use crate::vector::path::Path; +use crate::vector::{PointId, SegmentId, Vector}; +use core_types::list::List; use glam::{DAffine2, DVec2}; use petgraph::graph::{EdgeIndex, NodeIndex, UnGraph}; use petgraph::prelude::UnGraphMap; @@ -7,20 +10,22 @@ use rustc_hash::FxHashSet; pub trait MergeByDistanceExt { /// Collapse all points with edges shorter than the specified distance - fn merge_by_distance_topological(&mut self, distance: f64); - fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64); + fn merge_by_distance_topological(&mut self, paths: &mut List, distance: f64); + fn merge_by_distance_spatial(&mut self, paths: &mut List, transform: DAffine2, distance: f64); } impl MergeByDistanceExt for Vector { - fn merge_by_distance_topological(&mut self, distance: f64) { - // Treat self as an undirected graph - let indices = VectorIndex::build_from(self); + fn merge_by_distance_topological(&mut self, paths: &mut List, distance: f64) { + let mut working = WorkingVector::flatten(self, paths); + + // Treat the working form as an undirected graph + let indices = VectorIndex::build_from(&working); // TODO: We lose information on the winding order by using an undirected graph. Switch to a directed graph and fix the algorithm to handle that. // Graph containing only short edges, referencing the data graph let mut short_edges = UnGraphMap::new(); - for segment_id in self.segment_ids().iter().copied() { + for segment_id in working.segment_ids.iter().copied() { let length = indices.segment_chord_length(segment_id); if length < distance { let [start, end] = indices.segment_ends(segment_id); @@ -39,7 +44,7 @@ impl MergeByDistanceExt for Vector { let average_position = collapse .iter() .map(|collapse_set| { - let sum: DVec2 = collapse_set.iter().map(|&id| indices.point_position(id, self)).sum(); + let sum: DVec2 = collapse_set.iter().map(|&id| indices.point_position(id, &working)).sum(); sum / collapse_set.len() as f64 }) .collect::>(); @@ -49,11 +54,13 @@ impl MergeByDistanceExt for Vector { let mut segments_to_delete = FxHashSet::default(); for (mut collapse_set, average_pos) in collapse.into_iter().zip(average_position) { // 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 } - })); + for segment_index in 0..working.segment_ids.len() { + let start = working.point_ids[working.segment_start[segment_index]]; + let end = working.point_ids[working.segment_end[segment_index]]; + if collapse_set.contains(&start) && collapse_set.contains(&end) { + segments_to_delete.insert(working.segment_ids[segment_index]); + } + } // Delete all points but the first, set its position to the average, and update segments let first_id = collapse_set.iter().copied().next().unwrap(); @@ -61,52 +68,77 @@ impl MergeByDistanceExt for Vector { let first_offset = indices.point_to_offset[&first_id]; // Look for segments with endpoints in `collapse_set` and replace them with the point we are collapsing to - for (_, start_offset, end_offset, handles) in self.segment_domain.iter_mut() { - let start_id = self.point_domain.ids()[*start_offset]; - let end_id = self.point_domain.ids()[*end_offset]; + for segment_index in 0..working.segment_ids.len() { + let start_id = working.point_ids[working.segment_start[segment_index]]; + let end_id = working.point_ids[working.segment_end[segment_index]]; // Update Bezier handles for moved points if start_id == first_id { - let point_position = self.point_domain.position[*start_offset]; - handles.move_start(average_pos - point_position); + let point_position = working.point_positions[working.segment_start[segment_index]]; + working.segment_handles[segment_index].move_start(average_pos - point_position); } if end_id == first_id { - let point_position = self.point_domain.position[*end_offset]; - handles.move_end(average_pos - point_position); + let point_position = working.point_positions[working.segment_end[segment_index]]; + working.segment_handles[segment_index].move_end(average_pos - point_position); } // Replace removed points with the collapsed point if collapse_set.contains(&start_id) { - let point_position = self.point_domain.position[*start_offset]; - *start_offset = first_offset; - handles.move_start(average_pos - point_position); + let point_position = working.point_positions[working.segment_start[segment_index]]; + working.segment_start[segment_index] = first_offset; + working.segment_handles[segment_index].move_start(average_pos - point_position); } if collapse_set.contains(&end_id) { - let point_position = self.point_domain.position[*end_offset]; - *end_offset = first_offset; - handles.move_end(average_pos - point_position); + let point_position = working.point_positions[working.segment_end[segment_index]]; + working.segment_end[segment_index] = first_offset; + working.segment_handles[segment_index].move_end(average_pos - point_position); } } // Update the position of the collapsed point - self.point_domain.position[first_offset] = average_pos; + working.point_positions[first_offset] = average_pos; 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)); + // Drop the collapsed segments, then the merged-away points, remapping segment endpoints + let keep_segment: Vec = working.segment_ids.iter().map(|id| !segments_to_delete.contains(id)).collect(); + retain_by(&mut working.segment_ids, &keep_segment); + retain_by(&mut working.segment_start, &keep_segment); + retain_by(&mut working.segment_end, &keep_segment); + retain_by(&mut working.segment_handles, &keep_segment); + + let mut point_map = Vec::with_capacity(working.point_ids.len()); + let mut new_index = 0; + let keep_point: Vec = working + .point_ids + .iter() + .map(|id| { + let keep = !points_to_delete.contains(id); + point_map.push(if keep { new_index } else { usize::MAX }); + new_index += usize::from(keep); + keep + }) + .collect(); + retain_by(&mut working.point_ids, &keep_point); + retain_by(&mut working.point_positions, &keep_point); + for index in working.segment_start.iter_mut().chain(working.segment_end.iter_mut()) { + *index = point_map.get(*index).copied().unwrap_or(usize::MAX); + } + + working.reweave(self, paths); } - fn merge_by_distance_spatial(&mut self, transform: DAffine2, distance: f64) { - let point_count = self.point_domain.positions().len(); + fn merge_by_distance_spatial(&mut self, paths: &mut List, transform: DAffine2, distance: f64) { + let working = WorkingVector::flatten(self, paths); + let point_count = working.point_positions.len(); // Find min x and y for grid cell normalization let mut min_x = f64::MAX; let mut min_y = f64::MAX; // Calculate mins without collecting all positions - for &pos in self.point_domain.positions() { + for &pos in &working.point_positions { let transformed_pos = transform.transform_point2(pos); min_x = min_x.min(transformed_pos.x); min_y = min_y.min(transformed_pos.y); @@ -118,7 +150,7 @@ impl MergeByDistanceExt for Vector { // Add points to grid cells without collecting all positions first for i in 0..point_count { - let pos = transform.transform_point2(self.point_domain.positions()[i]); + let pos = transform.transform_point2(working.point_positions[i]); let grid_x = ((pos.x - min_x) / distance).floor() as i32; let grid_y = ((pos.y - min_y) / distance).floor() as i32; @@ -137,7 +169,7 @@ impl MergeByDistanceExt for Vector { continue; } - let pos_i = transform.transform_point2(self.point_domain.positions()[i]); + let pos_i = transform.transform_point2(working.point_positions[i]); let grid_x = ((pos_i.x - min_x) / distance).floor() as i32; let grid_y = ((pos_i.y - min_y) / distance).floor() as i32; @@ -151,7 +183,7 @@ impl MergeByDistanceExt for Vector { if let Some(indices) = grid.get(&neighbor_cell) { for &j in indices { if j > i && point_index_map[j].is_none() { - let pos_j = transform.transform_point2(self.point_domain.positions()[j]); + let pos_j = transform.transform_point2(working.point_positions[j]); if pos_i.distance(pos_j) <= distance { group.push(j); } @@ -164,7 +196,7 @@ impl MergeByDistanceExt for Vector { // Create merged point - calculate positions as needed let merged_position = group .iter() - .map(|&idx| transform.transform_point2(self.point_domain.positions()[idx])) + .map(|&idx| transform.transform_point2(working.point_positions[idx])) .fold(DVec2::ZERO, |sum, pos| sum + pos) / group.len() as f64; @@ -172,7 +204,7 @@ impl MergeByDistanceExt for Vector { let merged_index = merged_positions.len(); merged_positions.push(merged_position); - merged_indices.push(self.point_domain.ids()[group[0]]); + merged_indices.push(working.point_ids[group[0]]); // Update mapping for all points in the group for &idx in &group { @@ -180,43 +212,43 @@ impl MergeByDistanceExt for Vector { } } - // Create new point domain with merged points - let mut new_point_domain = PointDomain::new(); - for (idx, pos) in merged_indices.into_iter().zip(merged_positions) { - new_point_domain.push(idx, pos); - } - - // Update segment domain - let mut new_segment_domain = SegmentDomain::new(); - for segment_idx in 0..self.segment_domain.ids().len() { - let id = self.segment_domain.ids()[segment_idx]; - let start = self.segment_domain.start_point()[segment_idx]; - let end = self.segment_domain.end_point()[segment_idx]; - let handles = self.segment_domain.handles()[segment_idx]; + // Build the merged working form, skipping segments with endpoints that collapsed together + let mut merged = WorkingVector { + point_ids: merged_indices, + point_positions: merged_positions, + colinear: working.colinear, + ..Default::default() + }; - // Get new indices for start and end points - let new_start = point_index_map[start].unwrap(); - let new_end = point_index_map[end].unwrap(); + for segment_index in 0..working.segment_ids.len() { + let new_start = point_index_map[working.segment_start[segment_index]].unwrap(); + let new_end = point_index_map[working.segment_end[segment_index]].unwrap(); - // Skip segments where start and end points were merged if new_start != new_end { - new_segment_domain.push(id, new_start, new_end, handles); + merged.segment_ids.push(working.segment_ids[segment_index]); + merged.segment_start.push(new_start); + merged.segment_end.push(new_end); + merged.segment_handles.push(working.segment_handles[segment_index]); } } - // Create new vector geometry - self.point_domain = new_point_domain; - self.segment_domain = new_segment_domain; + merged.reweave(self, paths); } } -/// All the fixed fields of a point from the point domain. +/// Keeps only the elements flagged true, in order. +fn retain_by(values: &mut Vec, keep: &[bool]) { + let mut flags = keep.iter(); + values.retain(|_| *flags.next().unwrap_or(&false)); +} + +/// All the fixed fields of a point from the working form. pub(crate) struct Point { pub id: PointId, pub position: DVec2, } -/// Useful indexes to speed up various operations on [`Vector`]. +/// Useful indexes to speed up various operations on the flattened working form. /// /// Important: It is the user's responsibility to ensure the indexes remain valid after mutations to the data. pub(crate) struct VectorIndex { @@ -231,23 +263,23 @@ pub(crate) struct VectorIndex { } impl VectorIndex { - /// Construct a [`VectorIndex`] by building indexes from the given [`Vector`]. Takes `O(n)` time. - fn build_from(data: &Vector) -> Self { - let point_to_offset = data.point_domain.ids().iter().copied().enumerate().map(|(a, b)| (b, a)).collect::>(); + /// Construct a [`VectorIndex`] by building indexes from the given working form. Takes `O(n)` time. + fn build_from(working: &WorkingVector) -> Self { + let point_to_offset = working.point_ids.iter().copied().enumerate().map(|(a, b)| (b, a)).collect::>(); let mut point_to_node = FxHashMap::default(); let mut segment_to_edge = FxHashMap::default(); let mut graph = UnGraph::new_undirected(); - for (point_id, position) in data.point_domain.iter() { + for (&point_id, &position) in working.point_ids.iter().zip(&working.point_positions) { let idx = graph.add_node(Point { id: point_id, position }); point_to_node.insert(point_id, idx); } - for (segment_id, start_offset, end_offset, ..) in data.segment_domain.iter() { - let start_id = data.point_domain.ids()[start_offset]; - let end_id = data.point_domain.ids()[end_offset]; + for (segment_index, &segment_id) in working.segment_ids.iter().enumerate() { + let start_id = working.point_ids[working.segment_start[segment_index]]; + let end_id = working.point_ids[working.segment_end[segment_index]]; let edge = graph.add_edge(point_to_node[&start_id], point_to_node[&end_id], ()); segment_to_edge.insert(segment_id, edge); @@ -290,8 +322,8 @@ impl VectorIndex { /// # Panics /// /// Will panic if `id` isn't in the data. - fn point_position(&self, id: PointId, data: &Vector) -> DVec2 { + fn point_position(&self, id: PointId, working: &WorkingVector) -> DVec2 { let offset = self.point_to_offset[&id]; - data.point_domain.positions()[offset] + working.point_positions[offset] } } diff --git a/node-graph/libraries/vector-types/src/vector/connectivity.rs b/node-graph/libraries/vector-types/src/vector/connectivity.rs index bcd42d3c90c..1848b70debb 100644 --- a/node-graph/libraries/vector-types/src/vector/connectivity.rs +++ b/node-graph/libraries/vector-types/src/vector/connectivity.rs @@ -24,7 +24,7 @@ impl Connectivity { pub fn build(point_count: usize, paths: &List) -> Self { let mut starts = vec![0_u32; point_count + 1]; for path in paths.iter_element_values() { - for &point in path.0.iter_element_values() { + for point in path.references() { if let Some(count) = (point as usize).checked_add(1).and_then(|slot| starts.get_mut(slot)) { *count += 1; } @@ -39,7 +39,7 @@ 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() { - for (reference_index, &point) in path.0.iter_element_values().enumerate() { + for (reference_index, point) in path.references().enumerate() { let Some(cursor) = cursors.get_mut(point as usize) else { continue }; references[*cursor as usize] = Reference { path: path_index as u32, @@ -94,7 +94,7 @@ mod tests { let mut paths = List::new(); for &(curve_type, references) in specs { - let path = Path(references.iter().map(|&reference| Item::new_from_element(reference)).collect()); + let path = Path::from_references(references.iter().copied()); paths.push(Item::new_from_element(path).with_attribute(ATTR_CURVE_TYPE, curve_type)); } diff --git a/node-graph/libraries/vector-types/src/vector/contour.rs b/node-graph/libraries/vector-types/src/vector/contour.rs index cbcf5648f34..c3be65e84b2 100644 --- a/node-graph/libraries/vector-types/src/vector/contour.rs +++ b/node-graph/libraries/vector-types/src/vector/contour.rs @@ -106,7 +106,7 @@ pub fn subpaths(point_count: usize, paths: &List) -> Vec { let stride = curve_type.stride(); for span in 0..path.segment_count(curve_type) { - let references = (span * stride..=(span + 1) * stride).map(|index| path.0.element(index).copied()); + let references = (span * stride..=(span + 1) * stride).map(|index| path.reference(index)); let Some(references) = references.collect::>>() else { continue }; // A span naming a point outside the pool cannot be drawn, so it takes no part in the walk @@ -213,10 +213,7 @@ mod tests { /// Linear paths over a pool of the given size, one path per list of references. fn linear(point_count: usize, paths: &[&[u32]]) -> (List, List) { let pool = (0..point_count).map(|index| Item::new_from_element(DVec2::new(index as f64, (index * index) as f64))).collect(); - let paths = paths - .iter() - .map(|references| Item::new_from_element(Path(references.iter().copied().map(Item::new_from_element).collect()))) - .collect(); + let paths = paths.iter().map(|references| Item::new_from_element(Path::from_references(references.iter().copied()))).collect(); (pool, paths) } @@ -229,8 +226,8 @@ mod tests { let mut anchors = vec![subpath.entry]; for step in &subpath.steps { let path = paths.element(step.path as usize).expect("the step names a path"); - let (start, end) = (path.0.element(step.span as usize), path.0.element(step.span as usize + 1)); - anchors.push(*if step.reversed { start } else { end }.expect("the span has both ends")); + let (start, end) = (path.reference(step.span as usize), path.reference(step.span as usize + 1)); + anchors.push(if step.reversed { start } else { end }.expect("the span has both ends")); } if subpath.closed { anchors.pop(); diff --git a/node-graph/libraries/vector-types/src/vector/flatten.rs b/node-graph/libraries/vector-types/src/vector/flatten.rs new file mode 100644 index 00000000000..fb125bcfbf3 --- /dev/null +++ b/node-graph/libraries/vector-types/src/vector/flatten.rs @@ -0,0 +1,324 @@ +use super::addressing; +use super::misc::{BezierHandles, HandleId, HandleType, segment_to_handles}; +use super::path::{CurveType, Path, curve_type_at, push_path}; +use super::vector_attributes::{PointId, SegmentId}; +use super::vector_types::Vector; +use core_types::list::{ATTR_ID, Item, List}; +use glam::DVec2; +use std::collections::HashMap; + +/// One path under construction: its curve type, the pool references it weaves, and the IDs of the spans between them. +type Run = (CurveType, Vec, Vec); + +/// The legacy flat form that modifications and merge passes are expressed against: anchors with IDs, +/// segments with IDs and absolute handles, and G1 pairs. Callers flatten storage into this, edit it, +/// and weave it back. +#[derive(Default)] +pub struct WorkingVector { + pub point_ids: Vec, + pub point_positions: Vec, + pub segment_ids: Vec, + pub segment_start: Vec, + pub segment_end: Vec, + pub segment_handles: Vec, + pub colinear: Vec<[HandleId; 2]>, +} + +impl WorkingVector { + /// Reads the pool and paths into the working form, one working point per anchor or free point. + pub fn flatten(vector: &Vector, paths: &List) -> Self { + let mut working = Self::default(); + let connectivity = vector.connectivity(paths); + + let mut pool_to_working = vec![None; vector.point_count()]; + for (index, slot) in pool_to_working.iter_mut().enumerate() { + if !vector.is_handle_point(paths, &connectivity, index as u32) { + *slot = Some(working.point_ids.len()); + working.point_ids.push(vector.point_id_at(index as u32).unwrap_or_default()); + working.point_positions.push(vector.position(index).unwrap_or_default()); + } + } + + // One pass so a span that fails to resolve leaves every array at the same length + for span in addressing::spans(paths) { + let Some(path) = paths.element(span.path as usize) else { continue }; + let Some(segment) = path.segment(&vector.0, curve_type_at(paths, span.path as usize), span.index as usize) else { + continue; + }; + let Some((start, end)) = addressing::span_endpoints(paths, span) else { continue }; + let working_point = |pool_index: u32| pool_to_working.get(pool_index as usize).copied().flatten(); + let (Some(start), Some(end)) = (working_point(start), working_point(end)) else { continue }; + + working.segment_ids.push(addressing::segment_id_of(paths, span).unwrap_or_default()); + working.segment_handles.push(segment_to_handles(&segment)); + working.segment_start.push(start); + working.segment_end.push(end); + } + + working.colinear = vector.colinear_pairs(paths); + + working + } + + /// Weaves the working form back into a pool and paths, gathering consecutive segments that meet head to tail into one path. + pub fn reweave(self, vector: &mut Vector, paths: &mut List) { + *vector = Vector::default(); + *paths = List::new(); + + for (index, (&id, &position)) in self.point_ids.iter().zip(&self.point_positions).enumerate() { + vector.0.push(Item::new_from_element(position)); + vector.0.set_attribute(ATTR_ID, index, id); + } + let anchor_count = vector.0.len(); + + let mut runs: Vec = Vec::new(); + let mut segment_anchors = HashMap::new(); + + let ends = self.segment_start.iter().zip(&self.segment_end); + for ((&id, (&start, &end)), handles) in self.segment_ids.iter().zip(ends).zip(&self.segment_handles) { + if start >= anchor_count || end >= anchor_count { + continue; + } + + let (curve_type, handle_positions) = match *handles { + BezierHandles::Linear => (CurveType::Linear, Vec::new()), + BezierHandles::Quadratic { handle } => (CurveType::Quadratic, vec![handle]), + BezierHandles::Cubic { handle_start, handle_end } => (CurveType::Cubic, vec![handle_start, handle_end]), + }; + + // Runs stay in segment order, since the order spans are woven in decides how subpaths are walked. + // A run carries on through a junction and gives way only once it has closed. + let continues_run = runs + .last() + .is_some_and(|(run_curve_type, references, _)| *run_curve_type == curve_type && references.last() == Some(&(start as u32)) && references.first() != references.last()); + if !continues_run { + runs.push((curve_type, vec![start as u32], Vec::new())); + } + + let Some((_, references, ids)) = runs.last_mut() else { continue }; + for position in handle_positions { + references.push(vector.0.len() as u32); + vector.0.push(Item::new_from_element(position).with_attribute(ATTR_ID, PointId::UNNAMED)); + } + references.push(end as u32); + ids.push(id); + + segment_anchors.insert(id, (start as u32, end as u32)); + } + + for (curve_type, references, ids) in runs { + push_path(paths, curve_type, references, ids); + } + + // G1 pairs narrow to the smooth flag on the anchor both handles face, and other pairings are dropped + let anchor = |handle: HandleId| { + let &(start, end) = segment_anchors.get(&handle.segment)?; + Some(if handle.ty == HandleType::Primary { start } else { end }) + }; + let smooth_anchors: Vec = self + .colinear + .into_iter() + .filter_map(|[first, second]| anchor(first).zip(anchor(second))) + .filter_map(|(first, second)| (first == second).then_some(first)) + .collect(); + + vector.set_smooth_at_indices(paths, smooth_anchors, true); + } + + pub fn resolve_id(&self, id: PointId) -> Option { + self.point_ids.iter().position(|&check_id| check_id == id) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core_types::list::{ATTR_CURVE_TYPE, ATTR_ID}; + + fn working(points: usize, segments: &[(usize, usize, BezierHandles)]) -> WorkingVector { + WorkingVector { + point_ids: (0..points).map(|_| PointId::generate()).collect(), + point_positions: (0..points).map(|index| DVec2::new(index as f64, 0.)).collect(), + segment_ids: segments.iter().map(|_| SegmentId::generate()).collect(), + segment_start: segments.iter().map(|&(start, ..)| start).collect(), + segment_end: segments.iter().map(|&(_, end, _)| end).collect(), + segment_handles: segments.iter().map(|(.., handles)| *handles).collect(), + colinear: Vec::new(), + } + } + + fn woven(working: WorkingVector) -> (Vector, List) { + let (mut vector, mut paths) = (Vector::default(), List::new()); + working.reweave(&mut vector, &mut paths); + + (vector, paths) + } + + fn references(paths: &List, index: usize) -> Vec { + paths.element(index).expect("the path was woven").references().collect() + } + + // Four sides that meet head to tail become one closed path, its last reference naming its first + #[test] + fn reweave_gathers_a_rectangle_into_one_path() { + let sides = [ + (0, 1, BezierHandles::Linear), + (1, 2, BezierHandles::Linear), + (2, 3, BezierHandles::Linear), + (3, 0, BezierHandles::Linear), + ]; + let (_, paths) = woven(working(4, &sides)); + + assert_eq!(paths.len(), 1); + assert_eq!(references(&paths, 0), vec![0, 1, 2, 3, 0]); + + let rectangle = paths.element(0).expect("the run became a path"); + assert!(rectangle.is_closed()); + assert_eq!(rectangle.segment_count(CurveType::Linear), 4); + } + + // A run carries on through a junction, and the third segment leaving that anchor starts its own path + #[test] + fn reweave_carries_a_run_through_a_junction() { + let spokes = [(0, 1, BezierHandles::Linear), (1, 2, BezierHandles::Linear), (1, 3, BezierHandles::Linear)]; + let (_, paths) = woven(working(4, &spokes)); + + assert_eq!(paths.len(), 2); + assert_eq!(references(&paths, 0), vec![0, 1, 2]); + assert_eq!(references(&paths, 1), vec![1, 3]); + } + + // Two loops meeting at one anchor stay apart, since a run gives way once it has closed + #[test] + fn reweave_keeps_loops_meeting_at_an_anchor_apart() { + let lobes = [ + (0, 1, BezierHandles::Linear), + (1, 0, BezierHandles::Linear), + (0, 2, BezierHandles::Linear), + (2, 0, BezierHandles::Linear), + ]; + let (_, paths) = woven(working(3, &lobes)); + + assert_eq!(paths.len(), 2); + assert_eq!(references(&paths, 0), vec![0, 1, 0]); + assert_eq!(references(&paths, 1), vec![0, 2, 0]); + } + + // One path weaves one stride, so a curve following a line starts its own + #[test] + fn reweave_breaks_a_run_at_a_verb_change() { + let handles = BezierHandles::Cubic { + handle_start: DVec2::new(1., 1.), + handle_end: DVec2::new(2., 1.), + }; + let (_, paths) = woven(working(3, &[(0, 1, BezierHandles::Linear), (1, 2, handles)])); + + assert_eq!(paths.len(), 2); + assert_eq!(curve_type_at(&paths, 0), CurveType::Linear); + assert_eq!(curve_type_at(&paths, 1), CurveType::Cubic); + } + + // Consecutive curves weave into one path, each span's handles filling the references between its anchors + #[test] + fn reweave_gathers_cubic_handles_into_one_path() { + let handles = |offset: f64| BezierHandles::Cubic { + handle_start: DVec2::new(offset, 1.), + handle_end: DVec2::new(offset + 1., 1.), + }; + let (vector, paths) = woven(working(3, &[(0, 1, handles(0.)), (1, 2, handles(2.))])); + + assert_eq!(paths.len(), 1); + assert_eq!(references(&paths, 0), vec![0, 3, 4, 1, 5, 6, 2]); + assert_eq!(vector.point_count(), 7); + assert_eq!(paths.element(0).expect("the run became a path").segment_count(CurveType::Cubic), 2); + } + + // Beveling appends its corner segments after the sides, which keep that order and still walk as one closed contour + #[test] + fn reweave_keeps_segments_appended_out_of_order_in_order() { + let sides = [(0, 1), (2, 3), (4, 5), (6, 7)]; + let corners = [(1, 2), (3, 4), (5, 6), (7, 0)]; + let segments: Vec<_> = sides.into_iter().chain(corners).map(|(start, end)| (start, end, BezierHandles::Linear)).collect(); + + let (vector, paths) = woven(working(8, &segments)); + + assert_eq!(paths.len(), 8); + assert_eq!(references(&paths, 3), vec![6, 7]); + assert_eq!(references(&paths, 4), vec![1, 2]); + + let contours: Vec<_> = vector.stroke_bezpath_iter(&paths).collect(); + assert_eq!(contours.len(), 1); + assert_eq!(contours[0].elements().len(), 10); + } + + // Separate shapes stay where they were, so nothing downstream sees geometry shuffle + #[test] + fn reweave_keeps_untouched_shapes_in_order() { + let triangle = [(0, 1), (1, 2), (2, 0)]; + let open_pair = [(3, 4), (4, 5)]; + let segments: Vec<_> = triangle.into_iter().chain(open_pair).map(|(start, end)| (start, end, BezierHandles::Linear)).collect(); + + let (_, paths) = woven(working(6, &segments)); + + assert_eq!(paths.len(), 2); + assert_eq!(references(&paths, 0), vec![0, 1, 2, 0]); + assert_eq!(references(&paths, 1), vec![3, 4, 5]); + } + + // A span naming a point the pool does not have drops out of every array at once, keeping them aligned + #[test] + fn flatten_drops_an_unresolvable_span_from_every_array() { + let mut vector = Vector::default(); + for index in 0..2 { + vector.0.push(Item::new_from_element(DVec2::new(index as f64, 0.)).with_attribute(ATTR_ID, PointId::generate())); + } + + let mut path = Path::from_references([99, 0, 1]); + let drawn = SegmentId::generate(); + path.0.set_attribute(ATTR_ID, 0, SegmentId::generate()); + path.0.set_attribute(ATTR_ID, 1, drawn); + + let mut paths = List::new(); + paths.push(Item::new_from_element(path).with_attribute(ATTR_CURVE_TYPE, CurveType::Linear)); + + let working = WorkingVector::flatten(&vector, &paths); + assert_eq!(working.segment_ids, vec![drawn]); + assert_eq!(working.segment_start, vec![0]); + assert_eq!(working.segment_end, vec![1]); + } + + // A segment naming a point the pool lacks is dropped, leaving the run around it woven as one path + #[test] + fn unresolvable_segment_leaves_the_run_intact() { + let mut working = working(3, &[(0, 1, BezierHandles::Linear), (1, 2, BezierHandles::Linear)]); + working.segment_ids.push(SegmentId::generate()); + working.segment_start.push(1); + working.segment_end.push(99); + working.segment_handles.push(BezierHandles::Linear); + + let (_, paths) = woven(working); + assert_eq!(paths.len(), 1); + assert_eq!(references(&paths, 0), [0, 1, 2]); + } + + // Gathering runs stays invisible to the bridge: the same segments come back in the same order + #[test] + fn reweave_round_trips_through_flatten() { + let sides = [ + (0, 1, BezierHandles::Linear), + (1, 2, BezierHandles::Linear), + (2, 3, BezierHandles::Linear), + (3, 0, BezierHandles::Linear), + ]; + let original = working(4, &sides); + let (point_ids, segment_ids) = (original.point_ids.clone(), original.segment_ids.clone()); + + let (vector, paths) = woven(original); + let round_tripped = WorkingVector::flatten(&vector, &paths); + + assert_eq!(round_tripped.point_ids, point_ids); + assert_eq!(round_tripped.segment_ids, segment_ids); + assert_eq!(round_tripped.segment_start, vec![0, 1, 2, 3]); + assert_eq!(round_tripped.segment_end, vec![1, 2, 3, 0]); + } +} diff --git a/node-graph/libraries/vector-types/src/vector/misc.rs b/node-graph/libraries/vector-types/src/vector/misc.rs index 7cbdc30746d..43e84b7f38b 100644 --- a/node-graph/libraries/vector-types/src/vector/misc.rs +++ b/node-graph/libraries/vector-types/src/vector/misc.rs @@ -1,5 +1,6 @@ use super::PointId; use super::algorithms::consts::MAX_COINCIDENT_POINT_DISTANCE; +use super::path::Path; use crate::vector::{SegmentId, Vector}; use core_types::list::{Item, List}; use dyn_any::DynAny; @@ -413,37 +414,39 @@ pub enum ManipulatorPointId { impl ManipulatorPointId { /// Attempt to retrieve the manipulator position in layer space (no transformation applied). #[must_use] - #[track_caller] - pub fn get_position(&self, vector: &Vector) -> Option { + pub fn get_position(&self, vector: &Vector, paths: &List) -> Option { match self { - ManipulatorPointId::Anchor(id) => vector.point_domain.position_from_id(*id), - ManipulatorPointId::PrimaryHandle(id) => vector.segment_from_id(*id).and_then(|segment| segment_to_handles(&segment).start()), - ManipulatorPointId::EndHandle(id) => vector.segment_from_id(*id).and_then(|segment| segment_to_handles(&segment).end()), + ManipulatorPointId::Anchor(id) => vector.index_of_point_id(*id).and_then(|index| vector.position(index as usize)), + ManipulatorPointId::PrimaryHandle(id) => vector.segment_from_id(paths, *id).and_then(|segment| segment_to_handles(&segment).start()), + ManipulatorPointId::EndHandle(id) => vector.segment_from_id(paths, *id).and_then(|segment| segment_to_handles(&segment).end()), } } - pub fn get_anchor_position(&self, vector: &Vector) -> Option { + pub fn get_anchor_position(&self, vector: &Vector, paths: &List) -> Option { match self { - ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_) => self.get_anchor(vector).and_then(|id| vector.point_domain.position_from_id(id)), - _ => self.get_position(vector), + ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_) => self + .get_anchor(vector, paths) + .and_then(|id| vector.index_of_point_id(id)) + .and_then(|index| vector.position(index as usize)), + _ => self.get_position(vector, paths), } } /// Attempt to get a pair of handles. For an anchor this is the first two handles connected. For a handle it is self and the first opposing handle. #[must_use] - pub fn get_handle_pair(self, vector: &Vector) -> Option<[HandleId; 2]> { + pub fn get_handle_pair(self, vector: &Vector, paths: &List) -> Option<[HandleId; 2]> { match self { - ManipulatorPointId::Anchor(point) => vector.all_connected(point).take(2).collect::>().try_into().ok(), + ManipulatorPointId::Anchor(point) => vector.all_connected(paths, point).take(2).collect::>().try_into().ok(), ManipulatorPointId::PrimaryHandle(segment) => { - let point = vector.segment_domain.segment_start_from_id(segment)?; + let point = vector.segment_start_from_id(paths, segment)?; let current = HandleId::primary(segment); - let other = vector.segment_domain.all_connected(point).find(|&value| value != current); + let other = vector.all_connected(paths, point).find(|&value| value != current); other.map(|other| [current, other]) } ManipulatorPointId::EndHandle(segment) => { - let point = vector.segment_domain.segment_end_from_id(segment)?; + let point = vector.segment_end_from_id(paths, segment)?; let current = HandleId::end(segment); - let other = vector.segment_domain.all_connected(point).find(|&value| value != current); + let other = vector.all_connected(paths, point).find(|&value| value != current); other.map(|other| [current, other]) } } @@ -452,22 +455,22 @@ impl ManipulatorPointId { /// Finds all the connected handles of a point. /// For an anchor it is all the connected handles. /// For a handle it is all the handles connected to its corresponding anchor other than the current handle. - pub fn get_all_connected_handles(self, vector: &Vector) -> Option> { + pub fn get_all_connected_handles(self, vector: &Vector, paths: &List) -> Option> { match self { ManipulatorPointId::Anchor(point) => { - let connected = vector.all_connected(point).collect::>(); + let connected = vector.all_connected(paths, point).collect::>(); Some(connected) } ManipulatorPointId::PrimaryHandle(segment) => { - let point = vector.segment_domain.segment_start_from_id(segment)?; + let point = vector.segment_start_from_id(paths, segment)?; let current = HandleId::primary(segment); - let connected = vector.segment_domain.all_connected(point).filter(|&value| value != current).collect::>(); + let connected = vector.all_connected(paths, point).filter(|&value| value != current).collect::>(); Some(connected) } ManipulatorPointId::EndHandle(segment) => { - let point = vector.segment_domain.segment_end_from_id(segment)?; + let point = vector.segment_end_from_id(paths, segment)?; let current = HandleId::end(segment); - let connected = vector.segment_domain.all_connected(point).filter(|&value| value != current).collect::>(); + let connected = vector.all_connected(paths, point).filter(|&value| value != current).collect::>(); Some(connected) } } @@ -475,11 +478,11 @@ impl ManipulatorPointId { /// Attempt to find the closest anchor. If self is already an anchor then it is just self. If it is a start or end handle, then the start or end point is chosen. #[must_use] - pub fn get_anchor(self, vector: &Vector) -> Option { + pub fn get_anchor(self, vector: &Vector, paths: &List) -> Option { match self { ManipulatorPointId::Anchor(point) => Some(point), - ManipulatorPointId::PrimaryHandle(segment) => vector.segment_start_from_id(segment), - ManipulatorPointId::EndHandle(segment) => vector.segment_end_from_id(segment), + ManipulatorPointId::PrimaryHandle(segment) => vector.segment_start_from_id(paths, segment), + ManipulatorPointId::EndHandle(segment) => vector.segment_end_from_id(paths, segment), } } @@ -561,12 +564,12 @@ impl HandleId { } /// Calculate the magnitude of the handle from the anchor. - pub fn length(self, vector: &Vector) -> f64 { - let Some(anchor_position) = self.to_manipulator_point().get_anchor_position(vector) else { + pub fn length(self, vector: &Vector, paths: &List) -> f64 { + let Some(anchor_position) = self.to_manipulator_point().get_anchor_position(vector, paths) else { // TODO: This was previously an unwrap which was encountered, so this is a temporary way to avoid a crash return 0.; }; - let handle_position = self.to_manipulator_point().get_position(vector); + let handle_position = self.to_manipulator_point().get_position(vector, paths); handle_position.map(|pos| (pos - anchor_position).length()).unwrap_or(f64::MAX) } diff --git a/node-graph/libraries/vector-types/src/vector/mod.rs b/node-graph/libraries/vector-types/src/vector/mod.rs index e290c269b06..435175c1bc1 100644 --- a/node-graph/libraries/vector-types/src/vector/mod.rs +++ b/node-graph/libraries/vector-types/src/vector/mod.rs @@ -3,6 +3,7 @@ pub mod algorithms; pub mod click_target; mod connectivity; mod contour; +mod flatten; pub mod misc; mod path; pub mod reference_point; diff --git a/node-graph/libraries/vector-types/src/vector/path.rs b/node-graph/libraries/vector-types/src/vector/path.rs index 4227f610ae6..00c546fa97c 100644 --- a/node-graph/libraries/vector-types/src/vector/path.rs +++ b/node-graph/libraries/vector-types/src/vector/path.rs @@ -1,6 +1,7 @@ use super::algorithms::consts::CLOSE_POINT_TOLERANCE; use super::misc::{dvec2_to_point, point_to_dvec2}; -use core_types::list::{ATTR_CURVE_TYPE, ATTR_PATHS, Item, List}; +use super::vector_attributes::SegmentId; +use core_types::list::{ATTR_CURVE_TYPE, ATTR_ID, ATTR_PATHS, Item, List}; use glam::DVec2; use graphene_hash::CacheHash; use kurbo::{BezPath, CubicBez, Line, PathEl, PathSeg, Point, QuadBez}; @@ -41,10 +42,27 @@ impl std::fmt::Display for CurveType { /// One connected stroke of geometry: an ordered list of references (indices into the owning `Vector`'s point pool) /// woven in its curve type's stride order. Welding is two paths, or two ends of one path, referencing the same anchor. /// Per-reference data such as `ATTR_SMOOTH` rides this list's attributes. +/// +/// References are stored as `i64`, the integer type the data model standardizes on, and read back as `u32` pool indices. #[derive(Clone, Debug, Default, PartialEq, CacheHash)] -pub struct Path(pub List); +pub struct Path(pub List); impl Path { + /// Builds a path from pool indices, with no per-reference attributes. + pub fn from_references(references: impl IntoIterator) -> Self { + Self(references.into_iter().map(|reference| Item::new_from_element(i64::from(reference))).collect()) + } + + /// The pool index named by the reference at `index`, or `None` if out of bounds. + pub fn reference(&self, index: usize) -> Option { + self.0.element(index).map(|&reference| reference as u32) + } + + /// The pool indices named by each reference, in weave order. + pub fn references(&self) -> impl ExactSizeIterator + '_ { + self.0.iter_element_values().map(|&reference| reference as u32) + } + /// Whether this path is closed, indicated by its first and last references naming the same pool point. pub fn is_closed(&self) -> bool { let len = self.0.len(); @@ -63,7 +81,7 @@ impl Path { /// The pool position named by the reference at `index`, or `None` if either lookup falls out of bounds. pub fn position(&self, pool: &List, index: usize) -> Option { - pool.element(*self.0.element(index)? as usize).copied() + pool.element(self.reference(index)? as usize).copied() } /// The segment at `index`, resolved against the point pool. @@ -77,6 +95,55 @@ impl Path { CurveType::Cubic => PathSeg::Cubic(CubicBez::new(point(0)?, point(1)?, point(2)?, point(3)?)), }) } + + /// A copy of the spans in `start_span..end_span`, keeping both boundary anchors so the pieces stay welded. + pub fn clone_span_range(&self, curve_type: CurveType, start_span: usize, end_span: usize) -> Path { + let stride = curve_type.stride(); + let first_reference = start_span * stride; + let last_reference = (end_span * stride).min(self.0.len().saturating_sub(1)); + + let mut piece = Path((first_reference..=last_reference).filter_map(|index| self.0.clone_item(index)).collect()); + + // The final anchor's cell may hold the ID of a span outside the piece, which its own piece keeps + let last_index = piece.0.len().saturating_sub(1); + if piece.0.attribute::(ATTR_ID, last_index).is_some() { + piece.0.set_attribute(ATTR_ID, last_index, SegmentId::default()); + } + + piece + } + + /// Reverses the path in place, so the weave walks its references in the opposite direction. + /// Per-reference attributes travel with their references, and each span's ID moves to the anchor it now departs from. + pub fn reverse(&mut self, curve_type: CurveType) { + let stride = curve_type.stride(); + let count = self.segment_count(curve_type); + let ids: Vec> = (0..count).map(|span| self.0.attribute::(ATTR_ID, span * stride).copied()).collect(); + + self.0 = (0..self.0.len()).rev().filter_map(|index| self.0.clone_item(index)).collect(); + + // Span k of the reversed path walks old span (count - 1 - k) backwards, so it takes over that span's ID + for span in 0..count { + if let Some(id) = ids[count - 1 - span] { + self.0.set_attribute(ATTR_ID, span * stride, id); + } + } + if count > 0 && self.0.attribute::(ATTR_ID, count * stride).is_some() { + self.0.set_attribute(ATTR_ID, count * stride, SegmentId::default()); + } + } +} + +/// Appends one path weaving the given references in the curve type's stride, naming its spans with the given IDs in order. +pub fn push_path(paths: &mut List, curve_type: CurveType, references: impl IntoIterator, ids: impl IntoIterator) { + let mut path = Path::from_references(references); + + let span_count = path.segment_count(curve_type); + for (span, id) in ids.into_iter().enumerate().take(span_count) { + path.0.set_attribute(ATTR_ID, span * curve_type.stride(), id); + } + + paths.push(Item::new_from_element(path).with_attribute(ATTR_CURVE_TYPE, curve_type)); } /// Appends a segment to a bezpath already positioned at that segment's start point. @@ -98,6 +165,12 @@ pub fn paths_from_list_row_attributes(list: &List, index: usize) -> Option list.attribute(ATTR_PATHS, index) } +/// The empty weave, standing in for a pool with a holder that carries no paths. +pub fn no_paths() -> &'static List { + static EMPTY: std::sync::LazyLock> = std::sync::LazyLock::new(List::new); + &EMPTY +} + /// The curve type the path at `index` weaves in. pub fn curve_type_at(paths: &List, index: usize) -> CurveType { paths.attribute_cloned_or_default(ATTR_CURVE_TYPE, index) @@ -229,7 +302,7 @@ impl PathWeaver { } for (curve_type, references) in self.woven { - let path = Path(references.into_iter().map(Item::new_from_element).collect()); + let path = Path::from_references(references); paths.push(Item::new_from_element(path).with_attribute(ATTR_CURVE_TYPE, curve_type)); } } @@ -264,7 +337,7 @@ mod tests { } fn path(references: &[u32]) -> Path { - Path(references.iter().map(|&reference| Item::new_from_element(reference)).collect()) + Path::from_references(references.iter().copied()) } #[test] diff --git a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs index 16fb9a092cd..7a799d0e7be 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_attributes.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_attributes.rs @@ -1,12 +1,15 @@ -use crate::vector::misc::{BezierHandles, HandleId, ManipulatorGroup, Tangent, dvec2_to_point}; +use crate::vector::addressing::{self, Span}; +use crate::vector::contour::subpaths; +use crate::vector::misc::{ManipulatorGroup, Tangent, point_to_dvec2, segment_to_handles}; +use crate::vector::path::{Path, append_segment, curve_type_at}; use crate::vector::vector_types::Vector; +use core_types::list::{Item, List}; use dyn_any::DynAny; use fixedbitset::FixedBitSet; -use glam::{DAffine2, DVec2}; -use kurbo::{CubicBez, Line, ParamCurve, PathSeg, QuadBez}; -use std::collections::{HashMap, HashSet}; +use glam::DVec2; +use kurbo::{ParamCurve, PathSeg}; +use std::collections::HashSet; use std::hash::{Hash, Hasher}; -use std::iter::zip; /// A simple macro for creating strongly typed ids (to avoid confusion when passing around ids). macro_rules! create_ids { @@ -38,7 +41,7 @@ macro_rules! create_ids { self.0 } - /// Adds one to the current value and returns the old value. Note that the ids are not going to be unique unless you use the largest id. + /// Adds one to the current value and returns it. pub fn next_id(&mut self) -> Self { self.0 += 1; *self @@ -50,6 +53,11 @@ macro_rules! create_ids { create_ids! { PointId, SegmentId } +impl PointId { + /// The filler naming nothing, carried by pool points that are handles rather than anchors. + pub const UNNAMED: PointId = PointId(u64::MAX); +} + /// A no-op hasher that allows writing u64s (the id type). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct NoHash(Option); @@ -63,7 +71,7 @@ impl Hasher for NoHash { } fn write_u64(&mut self, i: u64) { debug_assert!(self.0.is_none()); - self.0 = Some(i) + self.0 = Some(i); } } @@ -78,475 +86,51 @@ impl std::hash::BuildHasher for NoHashBuilder { } } -#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, DynAny)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -/// 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, - #[cfg_attr(feature = "serde", serde(alias = "positions"))] - pub(crate) position: Vec, +/// A flat view of every resolvable span as an edge of the segment graph, so graph algorithms can +/// address segments by one index. Rebuilt on demand rather than stored. +pub(crate) struct SegmentGraph { + pub spans: Vec, + pub ids: Vec, + pub start_point: Vec, + pub end_point: Vec, + pub segments: Vec, + pub point_count: usize, } -impl PointDomain { - pub const fn new() -> Self { - Self { id: Vec::new(), position: Vec::new() } - } - - #[inline(always)] - pub fn reserve(&mut self, additional: usize) { - self.id.reserve(additional); - self.position.reserve(additional); - } - - pub(crate) fn retain(&mut self, segment_domain: &mut SegmentDomain, f: impl Fn(&PointId) -> bool) { - let mut keep = self.id.iter().map(&f); - self.position.retain(|_| keep.next().unwrap_or_default()); - - // TODO(TrueDoctor): Consider using a prefix sum to avoid this Vec allocation (https://github.com/GraphiteEditor/Graphite/pull/1949#discussion_r1741711562) - let mut id_map = Vec::with_capacity(self.ids().len()); - let mut new_index = 0; - for id in self.ids() { - if f(id) { - id_map.push(new_index); - new_index += 1; - } else { - // A placeholder for invalid IDs. This is checked after the segment domain is modified. - id_map.push(usize::MAX); - } - } - - let update_index = |index: &mut usize| *index = id_map[*index]; - segment_domain.start_point.iter_mut().for_each(update_index); - segment_domain.end_point.iter_mut().for_each(update_index); - - self.id.retain(f); - } - - pub fn push(&mut self, id: PointId, position: DVec2) { - #[cfg(debug_assertions)] - if self.id.contains(&id) { - warn!("Tried to push a duplicate point to a point domain"); - return; - } - - self.push_unchecked(id, position); - } - - #[inline(always)] - pub fn push_unchecked(&mut self, id: PointId, position: DVec2) { - self.id.push(id); - self.position.push(position); - } - - pub fn positions(&self) -> &[DVec2] { - &self.position - } - - pub fn positions_mut(&mut self) -> impl Iterator { - self.id.iter().copied().zip(self.position.iter_mut()) - } - - pub fn set_position(&mut self, index: usize, position: DVec2) { - self.position[index] = position; - } - - pub fn ids(&self) -> &[PointId] { - &self.id - } - - pub fn next_id(&self) -> PointId { - self.ids().iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(PointId::ZERO) - } - - #[track_caller] - pub fn position_from_id(&self, id: PointId) -> Option { - let pos = self.resolve_id(id).map(|index| self.position[index]); - if pos.is_none() { - warn!("Resolving pos of invalid id"); - } - pos - } - - pub fn resolve_id(&self, id: PointId) -> Option { - self.id.iter().position(|&check_id| check_id == id) - } - - pub(crate) fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) { - self.id.extend(other.id.iter().map(|id| *id_map.point_map.get(id).unwrap_or(id))); - self.position.extend(other.position.iter().map(|&pos| transform.transform_point2(pos))); - } - - pub(crate) fn map_ids(&mut self, id_map: &IdMap) { - self.id.iter_mut().for_each(|id| *id = *id_map.point_map.get(id).unwrap_or(id)); - } - - pub(crate) fn transform(&mut self, transform: DAffine2) { - for pos in &mut self.position { - *pos = transform.transform_point2(*pos); - } - } - - pub fn len(&self) -> usize { - self.id.len() - } - - pub fn is_empty(&self) -> bool { - self.id.is_empty() - } - - /// 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, graphene_hash::CacheHash, DynAny)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -/// Stores data which is per-segment. A segment is a bézier curve between two end points. In future this will be extendable at runtime with custom attributes. -pub struct SegmentDomain { - #[cfg_attr(feature = "serde", serde(alias = "ids"))] - id: Vec, - start_point: Vec, - end_point: Vec, - handles: Vec, -} - -impl SegmentDomain { - pub const fn new() -> Self { - Self { - id: Vec::new(), +impl SegmentGraph { + pub fn new(vector: &Vector, paths: &List) -> Self { + let mut graph = Self { + spans: Vec::new(), + ids: Vec::new(), start_point: Vec::new(), end_point: Vec::new(), - handles: Vec::new(), - } - } - - #[inline(always)] - pub fn reserve(&mut self, additional: usize) { - self.id.reserve(additional); - self.start_point.reserve(additional); - self.end_point.reserve(additional); - self.handles.reserve(additional); - } - - pub(crate) fn retain(&mut self, f: impl Fn(&SegmentId) -> bool, points_length: usize) { - let additional_delete_ids = self - .id - .iter() - .zip(&self.start_point) - .zip(&self.end_point) - .filter(|((_, start), end)| **start >= points_length || **end >= points_length) - .map(|x| *x.0.0) - .collect::>(); - - let can_delete = || { - let f = &f; - let mut delete_iter = additional_delete_ids.iter().peekable(); - move |id| { - if delete_iter.peek() == Some(&id) { - delete_iter.next(); - false - } else { - f(id) - } - } - }; - - let mut keep = self.id.iter().map(can_delete()); - self.start_point.retain(|_| keep.next().unwrap_or_default()); - let mut keep = self.id.iter().map(can_delete()); - self.end_point.retain(|_| keep.next().unwrap_or_default()); - let mut keep = self.id.iter().map(can_delete()); - self.handles.retain(|_| keep.next().unwrap_or_default()); - - let mut delete_iter = additional_delete_ids.iter().peekable(); - self.id.retain(move |id| { - if delete_iter.peek() == Some(&id) { - delete_iter.next(); - false - } else { - f(id) - } - }); - } - - pub fn ids(&self) -> &[SegmentId] { - &self.id - } - - pub fn next_id(&self) -> SegmentId { - self.ids().iter().copied().max_by(|a, b| a.0.cmp(&b.0)).map(|mut id| id.next_id()).unwrap_or(SegmentId::ZERO) - } - - pub fn start_point(&self) -> &[usize] { - &self.start_point - } - - pub fn end_point(&self) -> &[usize] { - &self.end_point - } - - pub fn set_start_point(&mut self, segment_index: usize, new: usize) { - self.start_point[segment_index] = new; - } - - pub fn set_end_point(&mut self, segment_index: usize, new: usize) { - self.end_point[segment_index] = new; - } - - pub fn set_handles(&mut self, segment_index: usize, new: BezierHandles) { - self.handles[segment_index] = new; - } - - pub fn handles(&self) -> &[BezierHandles] { - &self.handles - } - - pub fn push(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles) { - #[cfg(debug_assertions)] - if self.id.contains(&id) { - warn!("Tried to push a duplicate segment to a segment domain"); - return; - } - - self.push_unchecked(id, start, end, handles); - } - - #[inline(always)] - pub fn push_unchecked(&mut self, id: SegmentId, start: usize, end: usize, handles: BezierHandles) { - self.id.push(id); - self.start_point.push(start); - self.end_point.push(end); - self.handles.push(handles); - } - - pub(crate) fn start_point_mut(&mut self) -> impl Iterator { - self.id.iter().copied().zip(self.start_point.iter_mut()) - } - - pub(crate) fn end_point_mut(&mut self) -> impl Iterator { - self.id.iter().copied().zip(self.end_point.iter_mut()) - } - - pub(crate) fn handles_mut(&mut self) -> impl Iterator { - let nested = self.id.iter().zip(&mut self.handles).zip(&self.start_point).zip(&self.end_point); - nested.map(|(((&a, b), &c), &d)| (a, b, c, d)) - } - - pub fn handles_and_points_mut(&mut self) -> impl Iterator { - let nested = self.handles.iter_mut().zip(&mut self.start_point).zip(&mut self.end_point); - nested.map(|((a, b), c)| (a, b, c)) - } - - pub(crate) fn segment_start_from_id(&self, segment: SegmentId) -> Option { - self.id_to_index(segment).and_then(|index| self.start_point.get(index)).copied() - } - - pub(crate) fn segment_end_from_id(&self, segment: SegmentId) -> Option { - self.id_to_index(segment).and_then(|index| self.end_point.get(index)).copied() - } - - /// Returns an array for the start and end points of a segment. - pub(crate) fn points_from_id(&self, segment: SegmentId) -> Option<[usize; 2]> { - self.segment_start_from_id(segment).and_then(|start| self.segment_end_from_id(segment).map(|end| [start, end])) - } - - /// Attempts to find another point in the segment that is not the one passed in. - pub(crate) fn other_point(&self, segment: SegmentId, current: usize) -> Option { - self.points_from_id(segment).and_then(|points| points.into_iter().find(|&point| point != current)) - } - - /// Gets all points connected to the current one but not including the current one. - pub(crate) fn connected_points(&self, current: usize) -> impl Iterator + '_ { - self.start_point.iter().zip(&self.end_point).filter_map(move |(&a, &b)| match (a == current, b == current) { - (true, false) => Some(b), - (false, true) => Some(a), - _ => None, - }) - } - - /// Get index from ID by linear search. Takes `O(n)` time. - fn id_to_index(&self, id: SegmentId) -> Option { - debug_assert_eq!(self.id.len(), self.handles.len()); - debug_assert_eq!(self.id.len(), self.start_point.len()); - debug_assert_eq!(self.id.len(), self.end_point.len()); - self.id.iter().position(|&check_id| check_id == id) - } - - pub(crate) fn concat(&mut self, other: &Self, transform: DAffine2, id_map: &IdMap) { - self.id.extend(other.id.iter().map(|id| *id_map.segment_map.get(id).unwrap_or(id))); - self.start_point.extend(other.start_point.iter().map(|&index| id_map.point_offset + index)); - self.end_point.extend(other.end_point.iter().map(|&index| id_map.point_offset + index)); - self.handles.extend(other.handles.iter().map(|handles| handles.apply_transformation(|p| transform.transform_point2(p)))); - } - - pub(crate) fn map_ids(&mut self, id_map: &IdMap) { - self.id.iter_mut().for_each(|id| *id = *id_map.segment_map.get(id).unwrap_or(id)); - } - - pub(crate) fn transform(&mut self, transform: DAffine2) { - for handles in &mut self.handles { - *handles = handles.apply_transformation(|p| transform.transform_point2(p)); - } - } - - /// Enumerate all segments that start at the point. - pub(crate) fn start_connected(&self, point: usize) -> impl Iterator + '_ { - self.start_point.iter().zip(&self.id).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg) - } - - /// Enumerate all segments that end at the point. - pub(crate) fn end_connected(&self, point: usize) -> impl Iterator + '_ { - self.end_point.iter().zip(&self.id).filter(move |&(&found_point, _)| found_point == point).map(|(_, &seg)| seg) - } - - /// Enumerate all segments that start or end at a point, converting them to [`HandleId`s]. Note that the handles may not exist e.g. for a linear segment. - pub(crate) fn all_connected(&self, point: usize) -> impl Iterator + '_ { - self.start_connected(point).map(HandleId::primary).chain(self.end_connected(point).map(HandleId::end)) - } - - /// Enumerate the number of segments connected to a point. If a segment starts and ends at a point then it is counted twice. - pub(crate) fn connected_count(&self, point: usize) -> usize { - self.all_connected(point).count() - } - - /// Enumerate the number of segments connected to a point. If a segment starts and ends at a point then it is counted twice. - pub(crate) fn any_connected(&self, point: usize) -> bool { - self.all_connected(point).next().is_some() - } - - /// Computes the direction-of-travel tangent at one endpoint of a segment. - /// Uses the "first distinct control point" pattern: iterates through the Bezier control points - /// from the anchor outward, returning the direction to the first one that differs in position. - /// This handles zero-length handles by finding the tangent direction in the limit. - /// Returns `DVec2::ZERO` if all control points coincide (fully degenerate segment). - fn segment_tangent_at_endpoint(&self, segment_index: usize, positions: &[DVec2], at_start: bool) -> DVec2 { - let anchor_start = positions[self.start_point[segment_index]]; - let anchor_end = positions[self.end_point[segment_index]]; - - // Build ordered control points for this segment - let (points, count) = match self.handles[segment_index] { - BezierHandles::Linear => ([anchor_start, anchor_end, DVec2::ZERO, DVec2::ZERO], 2), - BezierHandles::Quadratic { handle } => ([anchor_start, handle, anchor_end, DVec2::ZERO], 3), - BezierHandles::Cubic { handle_start, handle_end } => ([anchor_start, handle_start, handle_end, anchor_end], 4), + segments: Vec::new(), + point_count: vector.point_count(), }; - let not_near = |a: DVec2, b: DVec2| a.distance_squared(b) > f64::EPSILON * 1e3; - - if at_start { - let anchor = points[0]; - points[1..count].iter().find(|&&p| not_near(p, anchor)).map_or(DVec2::ZERO, |&point| point - anchor) - } else { - let anchor = points[count - 1]; - points[..count - 1].iter().rev().find(|&&p| not_near(p, anchor)).map_or(DVec2::ZERO, |&point| anchor - point) - } - } - - /// Computes the average tangent direction at a point based on its 1 or 2 connected segments. - /// Returns `None` for points with 0 or 3+ connections (ambiguous or undefined tangent), - /// or if the tangent is degenerate (all control points coincide). - pub fn point_tangent(&self, point_index: usize, positions: &[DVec2]) -> Option { - // Collect connected segments with their relationship to this point (at_start flag) - let mut connections: [(usize, bool); 2] = [(0, false); 2]; - let mut connection_count = 0; - - for (segment_index, (&start, &end)) in self.start_point.iter().zip(&self.end_point).enumerate() { - // Self-loop segments count as two connections (outgoing and incoming) - let is_start = start == point_index; - let is_end = end == point_index; - - if !is_start && !is_end { + for span in addressing::spans(paths) { + let Some(path) = paths.element(span.path as usize) else { continue }; + let Some((start, end)) = addressing::span_endpoints(paths, span) else { continue }; + let Some(segment) = path.segment(&vector.0, curve_type_at(paths, span.path as usize), span.index as usize) else { continue; - } - - if is_start { - if connection_count >= 2 { - return None; - } - connections[connection_count] = (segment_index, true); - connection_count += 1; - } - if is_end { - if connection_count >= 2 { - return None; - } - connections[connection_count] = (segment_index, false); - connection_count += 1; - } - } - - if connection_count == 0 { - return None; - } - - // Compute the direction-of-travel tangent for the first connection - let (segment_index, at_start) = connections[0]; - let tangent1 = self.segment_tangent_at_endpoint(segment_index, positions, at_start).try_normalize(); - - if connection_count == 1 { - return tangent1; - } - - // Compute the direction-of-travel tangent for the second connection - let (segment_index, at_start) = connections[1]; - let tangent2 = self.segment_tangent_at_endpoint(segment_index, positions, at_start).try_normalize(); - - // Average the two normalized tangents - let average = tangent1? + tangent2?; + }; - // If the tangents are nearly opposite (straight-through), use t1 directly - if average.length_squared() < (f64::EPSILON * 1e3).powi(2) { - return tangent1; + graph.spans.push(span); + graph.ids.push(addressing::segment_id_of(paths, span).unwrap_or_default()); + graph.start_point.push(start as usize); + graph.end_point.push(end as usize); + graph.segments.push(segment); } - average.try_normalize() + graph } - /// Iterates over segments in the domain. - /// - /// Tuple is: (id, start point, end point, handles) - pub fn iter(&self) -> impl Iterator + '_ { - let ids = self.id.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)) - } - - /// Iterates over segments in the domain, mutably. - /// - /// Tuple is: (id, start point, end point, handles) - pub(crate) fn iter_mut(&mut self) -> impl Iterator + '_ { - let ids = self.id.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)) - } - - pub fn pair_handles_and_points_mut_by_index(&mut self, index1: usize, index2: usize) -> (&mut BezierHandles, &mut usize, &mut usize, &mut BezierHandles, &mut usize, &mut usize) { - // Use split_at_mut to avoid multiple mutable borrows of the same slice - let (handles_first, handles_second) = self.handles.split_at_mut(index2.max(index1)); - let (start_first, start_second) = self.start_point.split_at_mut(index2.max(index1)); - let (end_first, end_second) = self.end_point.split_at_mut(index2.max(index1)); - - let (h1, h2) = if index1 < index2 { - (&mut handles_first[index1], &mut handles_second[0]) - } else { - (&mut handles_second[0], &mut handles_first[index2]) - }; - let (sp1, sp2) = if index1 < index2 { - (&mut start_first[index1], &mut start_second[0]) - } else { - (&mut start_second[0], &mut start_first[index2]) - }; - let (ep1, ep2) = if index1 < index2 { - (&mut end_first[index1], &mut end_second[0]) - } else { - (&mut end_second[0], &mut end_first[index2]) - }; + pub fn len(&self) -> usize { + self.spans.len() + } - (h1, sp1, ep1, h2, sp2, ep2) + fn segment(&self, index: usize, reversed: bool) -> PathSeg { + if reversed { self.segments[index].reverse() } else { self.segments[index] } } } @@ -715,221 +299,227 @@ impl Faces { } } -impl Vector { - /// Construct a [`kurbo::PathSeg`] by resolving the points from their ids. - fn path_segment_from_index(&self, start: usize, end: usize, handles: BezierHandles) -> PathSeg { - let start = dvec2_to_point(self.point_domain.positions()[start]); - let end = dvec2_to_point(self.point_domain.positions()[end]); - - match handles { - BezierHandles::Linear => PathSeg::Line(Line::new(start, end)), - BezierHandles::Quadratic { handle } => PathSeg::Quad(QuadBez::new(start, dvec2_to_point(handle), end)), - BezierHandles::Cubic { handle_start, handle_end } => PathSeg::Cubic(CubicBez::new(start, dvec2_to_point(handle_start), dvec2_to_point(handle_end), end)), - } - } - - /// Tries to convert a segment with the specified id to a [`PathSeg`], returning None if the id is invalid. - pub fn segment_from_id(&self, id: SegmentId) -> Option { - self.segment_points_from_id(id).map(|(_, _, segment)| segment) - } - - /// Tries to convert a segment with the specified id to the start and end points and a [`PathSeg`], returning None if the id is invalid. - pub fn segment_points_from_id(&self, id: SegmentId) -> Option<(PointId, PointId, PathSeg)> { - Some(self.segment_points_from_index(self.segment_domain.id_to_index(id)?)) - } - - /// Converts a segment with the specified index to the start and end points and a [`PathSeg`]. - pub fn segment_points_from_index(&self, index: usize) -> (PointId, PointId, PathSeg) { - let start = self.segment_domain.start_point[index]; - let end = self.segment_domain.end_point[index]; - let start_id = self.point_domain.ids()[start]; - let end_id = self.point_domain.ids()[end]; - (start_id, end_id, self.path_segment_from_index(start, end, self.segment_domain.handles[index])) - } +impl SegmentGraph { + /// Computes the direction-of-travel tangent at one endpoint of a segment. + /// Uses the "first distinct control point" pattern: iterates through the Bezier control points + /// from the anchor outward, returning the direction to the first one that differs in position. + /// This handles zero-length handles by finding the tangent direction in the limit. + /// Returns `DVec2::ZERO` if all control points coincide (fully degenerate segment). + fn segment_tangent_at_endpoint(&self, segment_index: usize, at_start: bool) -> DVec2 { + let (points, count) = match self.segments[segment_index] { + PathSeg::Line(line) => ([point_to_dvec2(line.p0), point_to_dvec2(line.p1), DVec2::ZERO, DVec2::ZERO], 2), + PathSeg::Quad(quad) => ([point_to_dvec2(quad.p0), point_to_dvec2(quad.p1), point_to_dvec2(quad.p2), DVec2::ZERO], 3), + PathSeg::Cubic(cubic) => ([point_to_dvec2(cubic.p0), point_to_dvec2(cubic.p1), point_to_dvec2(cubic.p2), point_to_dvec2(cubic.p3)], 4), + }; - /// Iterator over all of the [`PathSeg`]s following the order that they are stored in the segment domain, skipping invalid segments. - pub fn segment_iter(&self) -> impl Iterator { - let to_segment = |(((&handles, &id), &start), &end)| (id, self.path_segment_from_index(start, end, handles), self.point_domain.ids()[start], self.point_domain.ids()[end]); + let not_near = |a: DVec2, b: DVec2| a.distance_squared(b) > f64::EPSILON * 1e3; - self.segment_domain - .handles - .iter() - .zip(&self.segment_domain.id) - .zip(self.segment_domain.start_point()) - .zip(self.segment_domain.end_point()) - .map(to_segment) + if at_start { + let anchor = points[0]; + points[1..count].iter().find(|&&p| not_near(p, anchor)).map_or(DVec2::ZERO, |&point| point - anchor) + } else { + let anchor = points[count - 1]; + points[..count - 1].iter().rev().find(|&&p| not_near(p, anchor)).map_or(DVec2::ZERO, |&point| anchor - point) + } } - pub fn auto_join_paths(&self) -> Vec { - let segments = self.segment_domain.iter().map(|(id, start, end, _)| HalfEdge::new(id, start, end, false)); + /// Computes the average tangent direction at a point based on its 1 or 2 connected segments. + /// Returns `None` for points with 0 or 3+ connections (ambiguous or undefined tangent), + /// or if the tangent is degenerate (all control points coincide). + pub fn point_tangent(&self, point_index: usize) -> Option { + // Collect connected segments with their relationship to this point (at_start flag) + let mut connections: [(usize, bool); 2] = [(0, false); 2]; + let mut connection_count = 0; - let mut paths: Vec = Vec::new(); - let mut current_path: Option<&mut FoundSubpath> = None; - let mut previous: Option<(usize, usize)> = None; + for (segment_index, (&start, &end)) in self.start_point.iter().zip(&self.end_point).enumerate() { + // Self-loop segments count as two connections (outgoing and incoming) + let is_start = start == point_index; + let is_end = end == point_index; - // First pass. Generates subpaths from continuous segments. - for seg_ref in segments { - let (start, end) = (seg_ref.start, seg_ref.end); + if !is_start && !is_end { + continue; + } - if previous.is_some_and(|(_, prev_end)| start == prev_end) { - if let Some(path) = current_path.as_mut() { - path.push(seg_ref); + if is_start { + if connection_count >= 2 { + return None; } - } else { - paths.push(FoundSubpath::from_segment(seg_ref)); - current_path = paths.last_mut(); + connections[connection_count] = (segment_index, true); + connection_count += 1; + } + if is_end { + if connection_count >= 2 { + return None; + } + connections[connection_count] = (segment_index, false); + connection_count += 1; } - - previous = Some((start, end)); } - // Second pass. Try to join paths together. - let mut joined_paths = Vec::new(); - - loop { - let mut prev_index: Option = None; - let original_len = paths.len(); - - for current in paths.into_iter() { - // If there's no previous subpath, start a new one - if prev_index.is_none() { - joined_paths.push(current); - prev_index = Some(joined_paths.len() - 1); - continue; - } + if connection_count == 0 { + return None; + } - let prev = &mut joined_paths[prev_index.unwrap()]; + // Compute the direction-of-travel tangent for the first connection + let (segment_index, at_start) = connections[0]; + let tangent1 = self.segment_tangent_at_endpoint(segment_index, at_start).try_normalize(); - // Compare segment connections - let (prev_first, prev_last) = prev.endpoints().unwrap(); - let (cur_first, cur_last) = current.endpoints().unwrap(); + if connection_count == 1 { + return tangent1; + } - // Join paths if the endpoints connect - if prev_last.end == cur_first.start { - prev.edges.extend(current.edges.into_iter().map(|s| s.normalize_direction())); - } else if prev_first.start == cur_last.end { - prev.edges.splice(0..0, current.edges.into_iter().rev().map(|s| s.normalize_direction())); - } else if prev_last.end == cur_last.end { - prev.edges.extend(current.edges.into_iter().rev().map(|s| s.reversed().normalize_direction())); - } else if prev_first.start == cur_first.start { - prev.edges.splice(0..0, current.edges.into_iter().map(|s| s.reversed().normalize_direction())); - } else { - // If not connected, start a new subpath - joined_paths.push(current); - prev_index = Some(joined_paths.len() - 1); - } - } + // Compute the direction-of-travel tangent for the second connection + let (segment_index, at_start) = connections[1]; + let tangent2 = self.segment_tangent_at_endpoint(segment_index, at_start).try_normalize(); - // If no paths were joined in this pass, we're done - if joined_paths.len() == original_len { - return joined_paths; - } + // Average the two normalized tangents + let average = tangent1? + tangent2?; - // Repeat pass with newly joined paths - paths = joined_paths; - joined_paths = Vec::new(); + // If the tangents are nearly opposite (straight-through), use t1 directly + if average.length_squared() < (f64::EPSILON * 1e3).powi(2) { + return tangent1; } + + average.try_normalize() } - /// Construct a [`kurbo::BezPath`] from an iterator of segments with (handles, start point, end point) independently of discontinuities. - pub fn bezpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator) -> Option { - let mut first_point = None; - let mut manipulators_list = Vec::new(); - let mut last: Option<(usize, BezierHandles)> = None; + /// Walks every face orbit of the segment graph and traces each one's path and signed area. + fn face_orbits(&self) -> FaceOrbits { + let mut adjacency: Vec> = vec![Vec::new(); self.point_count]; + for (segment_index, (&start, &end)) in self.start_point.iter().zip(&self.end_point).enumerate() { + adjacency[start].push(FaceSide { segment_index, reversed: false }); + adjacency[end].push(FaceSide { segment_index, reversed: true }); + } - for (handle, start, end) in segments { - first_point = Some(first_point.unwrap_or(start)); + for neighbors in &mut adjacency { + neighbors.sort_by(|a, b| { + let angle = [a, b].map(|side| { + let tangent = self.segment(side.segment_index, side.reversed).tangent_at_start(); + tangent.y.atan2(tangent.x) + }); + angle[0].partial_cmp(&angle[1]).unwrap_or(std::cmp::Ordering::Equal) + }) + } - manipulators_list.push(ManipulatorGroup { - anchor: self.point_domain.positions()[start], - in_handle: last.and_then(|(_, handle)| handle.end()), - out_handle: handle.start(), - id: self.point_domain.ids()[start], - }); + let mut faces: Faces = Faces::new(); + let mut seen = FaceSideSet::new(self.len()); - last = Some((end, handle)); + for segment_index in 0..self.len() { + for &reversed in &[false, true] { + let side = FaceSide { segment_index, reversed }; + if seen.contains(side) { + continue; + } + if self.construct_face(&adjacency, side, &mut faces, &mut seen).is_none() { + // Undo `seen` markings for sides added during this failed face construction, + // so they remain available for future face constructions starting from different sides. + if let Some(&last_start) = faces.face_start.last() { + for &failed_side in &faces.sides[last_start..] { + seen.remove(failed_side); + } + } + faces.backtrack(); + } + } } - let closed = manipulators_list.len() > 1 && last.map(|(point, _)| point) == first_point; + let mut paths = Vec::with_capacity(faces.face_start.len()); + let mut areas = Vec::with_capacity(faces.face_start.len()); + let mut side_to_orbit = vec![usize::MAX; self.len() * 2]; + for orbit_index in 0..faces.face_start.len() { + let start = faces.face_start[orbit_index]; + let end = faces.face_start.get(orbit_index + 1).copied().unwrap_or(faces.sides.len()); + let sides = &faces.sides[start..end]; - if let Some((end, last_handle)) = last { - if closed { - manipulators_list[0].in_handle = last_handle.end(); - } else { - manipulators_list.push(ManipulatorGroup { - anchor: self.point_domain.positions()[end], - in_handle: last_handle.end(), - out_handle: None, - id: self.point_domain.ids()[end], - }); + for side in sides { + side_to_orbit[FaceSideSet::index_of(side)] = orbit_index; } + let path = self.face_path(sides); + areas.push(kurbo::Shape::area(&path)); + paths.push(path); } - Some(crate::vector::misc::bezpath_from_manipulator_groups(&manipulators_list, closed)) + FaceOrbits { faces, paths, areas, side_to_orbit } } - pub fn build_stroke_path_iter(&self) -> StrokePathIter<'_> { - let mut points = vec![StrokePathIterPointMetadata::default(); self.point_domain.ids().len()]; - for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() { - points[start].set(StrokePathIterPointSegmentMetadata::new(segment_index, false)); - points[end].set(StrokePathIterPointSegmentMetadata::new(segment_index, true)); - } + /// Computes each point's degree in the two-core: the graph left after iteratively stripping dead-end segments. + /// Returns the degrees and which segments were pruned as spurs. + fn two_core(&self) -> (Vec, Vec, Vec>) { + let segment_count = self.len(); + let point_count = self.point_count; - StrokePathIter { - vector: self, - points, - skip: 0, - done_one: false, + let mut degree = vec![0_usize; point_count]; + let mut adjacency: Vec> = vec![Vec::new(); point_count]; + for (segment_index, (&start, &end)) in self.start_point.iter().zip(&self.end_point).enumerate() { + degree[start] += 1; + degree[end] += 1; + adjacency[start].push(segment_index); + adjacency[end].push(segment_index); } - } - /// Construct and return an iterator of `(Vec, bool)` for each stroke. - /// The boolean in the tuple indicates if the path is closed. - pub fn stroke_manipulator_groups(&self) -> impl Iterator, bool)> { - self.build_stroke_path_iter() - } + let mut pruned = vec![false; segment_count]; + let mut leaf_points: Vec = (0..point_count).filter(|&point| degree[point] == 1).collect(); + while let Some(point) = leaf_points.pop() { + if degree[point] != 1 { + continue; + } + let Some(&segment_index) = adjacency[point].iter().find(|&&segment_index| !pruned[segment_index]) else { + continue; + }; - /// Construct a [`kurbo::BezPath`] curve for stroke. - pub fn stroke_bezpath_iter(&self) -> impl Iterator { - self.build_stroke_path_iter() - .map(|(manipulators_list, closed)| crate::vector::misc::bezpath_from_manipulator_groups(&manipulators_list, closed)) - } + pruned[segment_index] = true; + for endpoint in [self.start_point[segment_index], self.end_point[segment_index]] { + degree[endpoint] -= 1; + if degree[endpoint] == 1 { + leaf_points.push(endpoint); + } + } + } - pub fn transform(&mut self, transform: DAffine2) { - self.point_domain.transform(transform); - self.segment_domain.transform(transform); + (degree, pruned, adjacency) } - pub fn vector_new_ids_from_hash(&mut self, node_id: u64) { - let point_map = self.point_domain.ids().iter().map(|&old| (old, old.generate_from_hash(node_id))).collect::>(); - let segment_map = self.segment_domain.ids().iter().map(|&old| (old, old.generate_from_hash(node_id))).collect::>(); + /// Traces one face orbit's sides into a path, reversing each segment walked against its drawn direction. + fn face_path(&self, sides: &[FaceSide]) -> kurbo::BezPath { + let mut path = kurbo::BezPath::new(); + let Some(first_side) = sides.first() else { return path }; - let id_map = IdMap { - point_offset: self.point_domain.ids().len(), - point_map, - segment_map, - }; + path.move_to(self.segment(first_side.segment_index, first_side.reversed).start()); + for side in sides { + append_segment(&mut path, self.segment(side.segment_index, side.reversed)); + } - self.point_domain.map_ids(&id_map); - self.segment_domain.map_ids(&id_map); + path } - pub fn is_branching(&self) -> bool { - // Tally segment endpoints per point in one `O(points + segments)` pass, short-circuiting once any point exceeds two - let mut connected_count = vec![0_u8; self.point_domain.len()]; - for &point_index in self.segment_domain.start_point().iter().chain(self.segment_domain.end_point()) { - connected_count[point_index] += 1; - if connected_count[point_index] > 2 { - return true; + fn construct_face(&self, adjacency: &[Vec], first: FaceSide, faces: &mut Faces, seen: &mut FaceSideSet) -> Option<()> { + faces.start_new_face(); + let max_iterations = self.len() * 2; + let mut side = first; + for _iteration in 1..max_iterations { + if seen.contains(side) { + return None; + } + seen.insert(side); + faces.add_side(side); + let next_vertex = if side.reversed { + self.start_point[side.segment_index] + } else { + self.end_point[side.segment_index] + }; + let neighbors = &adjacency[next_vertex]; + let side_index = neighbors.iter().position(|s| { + FaceSide { + segment_index: s.segment_index, + reversed: !s.reversed, + } == side + })?; + side = neighbors[(side_index + 1) % neighbors.len()]; + if side == first { + return Some(()); } } - false - } - - /// Determines if face-by-face fill rendering should be used. Branching vectors are meshes, whose - /// bounded faces are found and filled individually rather than filling the stroke path directly. - pub fn use_face_fill(&self) -> bool { - self.is_branching() + None } /// Returns the fillable faces of the segment graph: bounded regions, skipping the unbounded @@ -967,7 +557,7 @@ impl Vector { // contour was drawn as one unit. Walls shared among several faces are mesh and stay filled. let is_standalone_loop = non_spur_sides .iter() - .all(|side| core_degree[self.segment_domain.start_point[side.segment_index]] == 2 && core_degree[self.segment_domain.end_point[side.segment_index]] == 2); + .all(|side| core_degree[self.start_point[side.segment_index]] == 2 && core_degree[self.end_point[side.segment_index]] == 2); let mut mirror_orbits = non_spur_sides.iter().map(|side| orbits.side_to_orbit[FaceSideSet::index_of(&side.mirrored())]); let first_mirror_orbit = mirror_orbits.next().unwrap_or(usize::MAX); let surrounded_by_one_face = first_mirror_orbit != usize::MAX && orbits.areas[first_mirror_orbit] <= 0. && mirror_orbits.all(|orbit| orbit == first_mirror_orbit); @@ -990,180 +580,218 @@ impl Vector { face_path.extend(reversed_negative.clone()); } } - } - - kept_faces.into_iter().map(|(path, _)| path).collect() - } + } + + kept_faces.into_iter().map(|(path, _)| path).collect() + } +} + +impl Vector { + /// Whether any point has more than two segment ends meeting at it, making the vector a mesh. + pub fn is_branching(&self, paths: &List) -> bool { + let connectivity = self.connectivity(paths); + (0..self.point_count() as u32).any(|point| connectivity.spokes(point, paths) > 2) + } + + /// Determines if face-by-face fill rendering should be used. Branching vectors are meshes, whose + /// bounded faces are found and filled individually rather than filling the stroke path directly. + pub fn use_face_fill(&self, paths: &List) -> bool { + self.is_branching(paths) + } + + /// Returns the fillable faces of the segment graph, one closed path per bounded face. + pub fn construct_faces(&self, paths: &List) -> Vec { + SegmentGraph::new(self, paths).construct_faces() + } + + /// Computes the average tangent direction at a point based on its 1 or 2 connected segments. + pub fn point_tangent(&self, paths: &List, point_index: usize) -> Option { + SegmentGraph::new(self, paths).point_tangent(point_index) + } + + /// Every segment's stable ID, in span order. + pub fn segment_ids(&self, paths: &List) -> Vec { + addressing::spans(paths).map(|span| addressing::segment_id_of(paths, span).unwrap_or_default()).collect() + } + + pub fn auto_join_paths(&self, paths: &List) -> Vec { + let graph = SegmentGraph::new(self, paths); + let segments = (0..graph.len()).map(|index| HalfEdge::new(graph.ids[index], graph.start_point[index], graph.end_point[index], false)); + + let mut found: Vec = Vec::new(); + let mut current_path: Option<&mut FoundSubpath> = None; + let mut previous: Option<(usize, usize)> = None; + + // First pass. Generates subpaths from continuous segments. + for seg_ref in segments { + let (start, end) = (seg_ref.start, seg_ref.end); + + if previous.is_some_and(|(_, prev_end)| start == prev_end) { + if let Some(path) = current_path.as_mut() { + path.push(seg_ref); + } + } else { + found.push(FoundSubpath::from_segment(seg_ref)); + current_path = found.last_mut(); + } - /// Walks every face orbit of the segment graph and traces each one's path and signed area. - fn face_orbits(&self) -> FaceOrbits { - let mut adjacency: Vec> = vec![Vec::new(); self.point_domain.len()]; - for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() { - adjacency[start].push(FaceSide { segment_index, reversed: false }); - adjacency[end].push(FaceSide { segment_index, reversed: true }); + previous = Some((start, end)); } - for neighbors in &mut adjacency { - neighbors.sort_by(|a, b| { - let angle = [a, b].map(|side| { - let curve = self.path_segment_from_index( - self.segment_domain.start_point[side.segment_index], - self.segment_domain.end_point[side.segment_index], - self.segment_domain.handles[side.segment_index], - ); - let curve = if side.reversed { curve.reverse() } else { curve }; - let tangent = curve.tangent_at_start(); - tangent.y.atan2(tangent.x) - }); - angle[0].partial_cmp(&angle[1]).unwrap_or(std::cmp::Ordering::Equal) - }) - } + // Second pass. Try to join paths together. + let mut paths_to_join = found; + let mut joined_paths = Vec::new(); - let mut faces: Faces = Faces::new(); - let mut seen = FaceSideSet::new(self.segment_domain.id.len()); + loop { + let mut prev_index: Option = None; + let original_len = paths_to_join.len(); - for segment_index in 0..self.segment_domain.id.len() { - for &reversed in &[false, true] { - let side = FaceSide { segment_index, reversed }; - if seen.contains(side) { + for current in paths_to_join.into_iter() { + // If there's no previous subpath, start a new one + if prev_index.is_none() { + joined_paths.push(current); + prev_index = Some(joined_paths.len() - 1); continue; } - if self.construct_face(&adjacency, side, &mut faces, &mut seen).is_none() { - // Undo `seen` markings for sides added during this failed face construction, - // so they remain available for future face constructions starting from different sides. - if let Some(&last_start) = faces.face_start.last() { - for &failed_side in &faces.sides[last_start..] { - seen.remove(failed_side); - } - } - faces.backtrack(); + + let prev = &mut joined_paths[prev_index.unwrap()]; + + // Compare segment connections + let (prev_first, prev_last) = prev.endpoints().unwrap(); + let (cur_first, cur_last) = current.endpoints().unwrap(); + + // Join paths if the endpoints connect + if prev_last.end == cur_first.start { + prev.edges.extend(current.edges.into_iter().map(|s| s.normalize_direction())); + } else if prev_first.start == cur_last.end { + prev.edges.splice(0..0, current.edges.into_iter().rev().map(|s| s.normalize_direction())); + } else if prev_last.end == cur_last.end { + prev.edges.extend(current.edges.into_iter().rev().map(|s| s.reversed().normalize_direction())); + } else if prev_first.start == cur_first.start { + prev.edges.splice(0..0, current.edges.into_iter().map(|s| s.reversed().normalize_direction())); + } else { + // If not connected, start a new subpath + joined_paths.push(current); + prev_index = Some(joined_paths.len() - 1); } } - } - - let mut paths = Vec::with_capacity(faces.face_start.len()); - let mut areas = Vec::with_capacity(faces.face_start.len()); - let mut side_to_orbit = vec![usize::MAX; self.segment_domain.id.len() * 2]; - for orbit_index in 0..faces.face_start.len() { - let start = faces.face_start[orbit_index]; - let end = faces.face_start.get(orbit_index + 1).copied().unwrap_or(faces.sides.len()); - let sides = &faces.sides[start..end]; - for side in sides { - side_to_orbit[FaceSideSet::index_of(side)] = orbit_index; + // If no paths were joined in this pass, we're done + if joined_paths.len() == original_len { + return joined_paths; } - let path = self.face_path(sides); - areas.push(kurbo::Shape::area(&path)); - paths.push(path); - } - FaceOrbits { faces, paths, areas, side_to_orbit } + // Repeat pass with newly joined paths + paths_to_join = joined_paths; + joined_paths = Vec::new(); + } } - /// Computes each point's degree in the two-core: the graph left after iteratively stripping dead-end segments. - /// Returns the degrees and which segments were pruned as spurs. - fn two_core(&self) -> (Vec, Vec, Vec>) { - let segment_count = self.segment_domain.id.len(); - let point_count = self.point_domain.len(); + /// Construct a [`kurbo::BezPath`] from an iterator of segments with (segment, start point, end point) independently of discontinuities. + pub fn bezpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator) -> Option { + let handle = |segment: PathSeg, from_start: bool| { + let handles = segment_to_handles(&segment); + if from_start { handles.start() } else { handles.end() } + }; - let mut degree = vec![0_usize; point_count]; - let mut adjacency: Vec> = vec![Vec::new(); point_count]; - for (segment_index, (&start, &end)) in self.segment_domain.start_point.iter().zip(&self.segment_domain.end_point).enumerate() { - degree[start] += 1; - degree[end] += 1; - adjacency[start].push(segment_index); - adjacency[end].push(segment_index); + let mut first_point = None; + let mut manipulators_list = Vec::new(); + let mut last: Option<(usize, PathSeg)> = None; + + for (segment, start, end) in segments { + first_point = Some(first_point.unwrap_or(start)); + + manipulators_list.push(ManipulatorGroup { + anchor: self.position(start)?, + in_handle: last.and_then(|(_, segment)| handle(segment, false)), + out_handle: handle(segment, true), + id: self.point_id_at(start as u32).unwrap_or_default(), + }); + + last = Some((end, segment)); } - let mut pruned = vec![false; segment_count]; - let mut leaf_points: Vec = (0..point_count).filter(|&point| degree[point] == 1).collect(); - while let Some(point) = leaf_points.pop() { - if degree[point] != 1 { - continue; - } - let Some(&segment_index) = adjacency[point].iter().find(|&&segment_index| !pruned[segment_index]) else { - continue; - }; + let closed = manipulators_list.len() > 1 && last.map(|(point, _)| point) == first_point; - pruned[segment_index] = true; - for endpoint in [self.segment_domain.start_point[segment_index], self.segment_domain.end_point[segment_index]] { - degree[endpoint] -= 1; - if degree[endpoint] == 1 { - leaf_points.push(endpoint); - } + if let Some((end, last_segment)) = last { + if closed { + manipulators_list[0].in_handle = handle(last_segment, false); + } else { + manipulators_list.push(ManipulatorGroup { + anchor: self.position(end)?, + in_handle: handle(last_segment, false), + out_handle: None, + id: self.point_id_at(end as u32).unwrap_or_default(), + }); } } - (degree, pruned, adjacency) + Some(crate::vector::misc::bezpath_from_manipulator_groups(&manipulators_list, closed)) } - /// Traces one face orbit's sides into a path, reversing each segment walked against its drawn direction. - fn face_path(&self, sides: &[FaceSide]) -> kurbo::BezPath { - let mut path = kurbo::BezPath::new(); - let Some(first_side) = sides.first() else { return path }; + /// Construct and return an iterator of `(Vec, bool)` for each stroke. + /// The boolean in the tuple indicates if the path is closed. + /// The groups pair index-for-index with the contours emission, both walking the same subpaths. + pub fn stroke_manipulator_groups(&self, paths: &List) -> impl Iterator, bool)> { + let mut groups = Vec::new(); + + for subpath in subpaths(self.point_count(), paths) { + let mut manipulators_list: Vec = Vec::new(); + let mut in_handle: Option = None; + + for step in &subpath.steps { + let span = Span { path: step.path, index: step.span }; + let Some(path) = paths.element(step.path as usize) else { continue }; + let Some(segment) = path.segment(&self.0, curve_type_at(paths, step.path as usize), step.span as usize) else { + continue; + }; + let Some((start, end)) = addressing::span_endpoints(paths, span) else { continue }; - let get_point = |point: usize| dvec2_to_point(self.point_domain.positions()[point]); - let start_point_index = if first_side.reversed { - self.segment_domain.end_point[first_side.segment_index] - } else { - self.segment_domain.start_point[first_side.segment_index] - }; - path.move_to(get_point(start_point_index)); + // A quadratic's one handle leaves its anchor whichever way the span is crossed + let handles = segment_to_handles(&segment); + let handles = if step.reversed { handles.reversed() } else { handles }; + let anchor = if step.reversed { end } else { start }; - for side in sides { - let (handle, end_index) = match side.reversed { - false => (self.segment_domain.handles[side.segment_index], self.segment_domain.end_point[side.segment_index]), - true => (self.segment_domain.handles[side.segment_index].reversed(), self.segment_domain.start_point[side.segment_index]), - }; - let path_element = match handle { - BezierHandles::Linear => kurbo::PathEl::LineTo(get_point(end_index)), - BezierHandles::Quadratic { handle } => kurbo::PathEl::QuadTo(dvec2_to_point(handle), get_point(end_index)), - BezierHandles::Cubic { handle_start, handle_end } => kurbo::PathEl::CurveTo(dvec2_to_point(handle_start), dvec2_to_point(handle_end), get_point(end_index)), - }; - path.push(path_element); - } + manipulators_list.push(ManipulatorGroup { + anchor: point_to_dvec2(if step.reversed { segment.end() } else { segment.start() }), + in_handle, + out_handle: handles.start(), + id: self.point_id_at(anchor).unwrap_or_default(), + }); - path - } + in_handle = handles.end(); + } - fn construct_face(&self, adjacency: &[Vec], first: FaceSide, faces: &mut Faces, seen: &mut FaceSideSet) -> Option<()> { - faces.start_new_face(); - let max_iterations = self.segment_domain.id.len() * 2; - let mut side = first; - for _iteration in 1..max_iterations { - if seen.contains(side) { - return None; + if subpath.closed { + if let Some(first) = manipulators_list.first_mut() { + first.in_handle = in_handle; + } + } else if let Some(position) = self.position(subpath.exit as usize) { + manipulators_list.push(ManipulatorGroup { + anchor: position, + in_handle, + out_handle: None, + id: self.point_id_at(subpath.exit).unwrap_or_default(), + }); } - seen.insert(side); - faces.add_side(side); - let next_vertex = if side.reversed { - self.segment_domain.start_point[side.segment_index] - } else { - self.segment_domain.end_point[side.segment_index] - }; - let neighbors = &adjacency[next_vertex]; - let side_index = neighbors.iter().position(|s| { - FaceSide { - segment_index: s.segment_index, - reversed: !s.reversed, - } == side - })?; - side = neighbors[(side_index + 1) % neighbors.len()]; - if side == first { - return Some(()); + + if !manipulators_list.is_empty() { + groups.push((manipulators_list, subpath.closed)); } } - None + + groups.into_iter() } /// Normalizes the winding direction of every simple closed loop so nesting depth alone decides fill: /// loops at even depth wind positive and loops nested inside them wind negative, regardless of the direction they happened /// to be drawn in. Loops passing through branch points, and open runs including dead-end spurs, are left untouched. - pub fn normalize_winding_directions(&mut self) { - let segment_count = self.segment_domain.id.len(); + pub fn normalize_winding_directions(&mut self, paths: &mut List) { + let graph = SegmentGraph::new(self, paths); + let segment_count = graph.len(); // Strip dead-end spurs first so a spur hanging off a loop doesn't disguise it - let (degree, pruned, adjacency) = self.two_core(); + let (degree, pruned, adjacency) = graph.two_core(); // Walk out each simple loop: a chain of surviving segments passing only through degree-2 points. // Chains that reach a branch point are mesh or welded structure and keep their drawn directions. @@ -1173,8 +801,8 @@ impl Vector { if visited[first_segment] { continue; } - let start = self.segment_domain.start_point[first_segment]; - let end = self.segment_domain.end_point[first_segment]; + let start = graph.start_point[first_segment]; + let end = graph.end_point[first_segment]; if degree[start] != 2 || degree[end] != 2 { visited[first_segment] = true; continue; @@ -1196,12 +824,8 @@ impl Vector { break; }; - let walked_reversed = self.segment_domain.end_point[next_segment] == current_point; - current_point = if walked_reversed { - self.segment_domain.start_point[next_segment] - } else { - self.segment_domain.end_point[next_segment] - }; + let walked_reversed = graph.end_point[next_segment] == current_point; + current_point = if walked_reversed { graph.start_point[next_segment] } else { graph.end_point[next_segment] }; loop_sides.push((next_segment, walked_reversed)); } @@ -1219,24 +843,9 @@ impl Vector { .map(|loop_sides| { let mut path = kurbo::BezPath::new(); let (first_segment, first_reversed) = loop_sides[0]; - let start_point = if first_reversed { - self.segment_domain.end_point[first_segment] - } else { - self.segment_domain.start_point[first_segment] - }; - path.move_to(dvec2_to_point(self.point_domain.positions()[start_point])); + path.move_to(graph.segment(first_segment, first_reversed).start()); for &(segment_index, walked_reversed) in loop_sides { - let (handle, end_index) = match walked_reversed { - false => (self.segment_domain.handles[segment_index], self.segment_domain.end_point[segment_index]), - true => (self.segment_domain.handles[segment_index].reversed(), self.segment_domain.start_point[segment_index]), - }; - let end = dvec2_to_point(self.point_domain.positions()[end_index]); - let path_element = match handle { - BezierHandles::Linear => kurbo::PathEl::LineTo(end), - BezierHandles::Quadratic { handle } => kurbo::PathEl::QuadTo(dvec2_to_point(handle), end), - BezierHandles::Cubic { handle_start, handle_end } => kurbo::PathEl::CurveTo(dvec2_to_point(handle_start), dvec2_to_point(handle_end), end), - }; - path.push(path_element); + append_segment(&mut path, graph.segment(segment_index, walked_reversed)); } path.close_path(); path @@ -1246,7 +855,7 @@ impl Vector { // Nesting depth counts the bounded face regions covering each loop, so containment still registers when the // surrounding contour is branching mesh structure rather than a simple loop. The bounding box check keeps // the containment tests from being `O(loops × faces × segments)` on many-loop paths like converted text. - let orbits = self.face_orbits(); + let orbits = graph.face_orbits(); let bounded_orbits: Vec<(&kurbo::BezPath, kurbo::Rect, HashSet)> = (0..orbits.face_count()) .filter(|&orbit_index| orbits.areas[orbit_index] <= 0.) .map(|orbit_index| { @@ -1256,7 +865,8 @@ impl Vector { }) .collect(); - // Reverse the segments of each loop whose drawn winding disagrees with its nesting parity + // Collect the loops drawn with a winding that disagrees with their nesting parity + let mut reversals: Vec<&Vec<(usize, bool)>> = Vec::new(); for (loop_index, loop_sides) in loops.iter().enumerate() { // A segment midpoint is a safe winding sample, as in `construct_faces` let Some(first_segment) = loop_paths[loop_index].segments().next() else { continue }; @@ -1270,159 +880,158 @@ impl Vector { let area = kurbo::Shape::area(&loop_paths[loop_index]); let wants_positive = depth % 2 == 0; if area != 0. && (area > 0.) != wants_positive { - for &(segment_index, _) in loop_sides { - let segment_domain = &mut self.segment_domain; - std::mem::swap(&mut segment_domain.start_point[segment_index], &mut segment_domain.end_point[segment_index]); - segment_domain.handles[segment_index] = segment_domain.handles[segment_index].reversed(); - } + reversals.push(loop_sides); } } - } -} -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] -struct StrokePathIterPointSegmentMetadata { - segment_index: usize, - start_from_end: bool, -} - -impl StrokePathIterPointSegmentMetadata { - #[must_use] - const fn new(segment_index: usize, start_from_end: bool) -> Self { - Self { segment_index, start_from_end } - } - #[must_use] - const fn flipped(&self) -> Self { - Self { - segment_index: self.segment_index, - start_from_end: !self.start_from_end, + let reversed: HashSet<(u32, u32)> = reversals + .into_iter() + .flatten() + .map(|&(segment_index, _)| (graph.spans[segment_index].path, graph.spans[segment_index].index)) + .collect(); + if !reversed.is_empty() { + *paths = reverse_spans_in_place(paths, &reversed); } } } -#[derive(Clone, Default)] -struct StrokePathIterPointMetadata(tinyvec::TinyVec<[StrokePathIterPointSegmentMetadata; 2]>); - -impl StrokePathIterPointMetadata { - fn set(&mut self, value: StrokePathIterPointSegmentMetadata) { - self.0.insert(0, value); - } - #[must_use] - fn connected(&self) -> usize { - self.0.len() - } - #[must_use] - fn take_first(&mut self) -> Option { - self.0.pop() - } - fn take_eq(&mut self, target: StrokePathIterPointSegmentMetadata) -> bool { - let has_taken = self.0.contains(&target); - self.0.retain(|value| *value != target); - has_taken - } -} - -#[derive(Clone)] -pub struct StrokePathIter<'a> { - vector: &'a Vector, - points: Vec, - skip: usize, - done_one: bool, -} - -impl Iterator for StrokePathIter<'_> { - type Item = (Vec, bool); +/// Reweaves the paths with the given `(path, span)` pairs reversed. Each reversed span keeps its place in span +/// order, which decides how subpaths are walked, so it becomes a path of its own among the pieces left around it. +fn reverse_spans_in_place(paths: &List, reversed: &HashSet<(u32, u32)>) -> List { + let mut rewoven = List::new(); - fn next(&mut self) -> Option { - let mut current_start = None; - // First iterate over the single connected points - if !self.done_one { - current_start = self.points.iter().enumerate().skip(self.skip).find(|(_, val)| val.connected() == 1); - self.done_one = current_start.is_none(); - self.skip = current_start.map_or(0, |(index, _)| index + 1); - } + for path_index in 0..paths.len() { + let Some(template) = paths.clone_item(path_index) else { continue }; + let curve_type = curve_type_at(paths, path_index); + let span_count = template.element().segment_count(curve_type); + let is_reversed = |span: usize| reversed.contains(&(path_index as u32, span as u32)); - // If we've already done the single connected, then go through looking at multi connected - if current_start.is_none() { - current_start = self.points.iter().enumerate().skip(self.skip).find(|(_, val)| val.connected() > 0); - self.skip = current_start.map_or(self.points.len(), |(index, _)| index); + if !(0..span_count).any(is_reversed) { + rewoven.push(template); + continue; } - // If there is no starting point, exit - let current_start = current_start?.0; - - // There will always be at least one segment connected to this one - let mut point_index = current_start; - let mut manipulators_list = Vec::new(); - let mut in_handle = None; - let mut closed = false; - loop { - let Some(val) = self.points[point_index].take_first() else { - // Dead end - manipulators_list.push(ManipulatorGroup { - anchor: self.vector.point_domain.positions()[point_index], - in_handle, - out_handle: None, - id: self.vector.point_domain.ids()[point_index], - }); - - break; - }; + let path = template.element(); + let mut push_piece = |piece: Path| rewoven.push(Item::from_parts(piece, template.attributes().clone())); - let mut handles = self.vector.segment_domain.handles()[val.segment_index]; - if val.start_from_end { - handles = handles.reversed(); + let mut kept_from = 0; + for span in (0..span_count).filter(|&span| is_reversed(span)) { + if kept_from < span { + push_piece(path.clone_span_range(curve_type, kept_from, span)); } - let next_point_index = if val.start_from_end { - self.vector.segment_domain.start_point()[val.segment_index] - } else { - self.vector.segment_domain.end_point()[val.segment_index] - }; - manipulators_list.push(ManipulatorGroup { - anchor: self.vector.point_domain.positions()[point_index], - in_handle, - out_handle: handles.start(), - id: self.vector.point_domain.ids()[point_index], - }); - in_handle = handles.end(); + let mut flipped = path.clone_span_range(curve_type, span, span + 1); + flipped.reverse(curve_type); + push_piece(flipped); - point_index = next_point_index; - self.points[next_point_index].take_eq(val.flipped()); - if next_point_index == current_start { - closed = true; - manipulators_list[0].in_handle = in_handle; - break; - } + kept_from = span + 1; + } + if kept_from < span_count { + push_piece(path.clone_span_range(curve_type, kept_from, span_count)); } - - Some((manipulators_list, closed)) } -} -/// Represents the conversion of IDs used when concatenating vector paths with conflicting IDs. -pub(crate) struct IdMap { - pub point_offset: usize, - pub point_map: HashMap, - pub segment_map: HashMap, + rewoven } #[cfg(test)] mod tests { use super::*; + use crate::vector::path::CurveType; + use core_types::list::{ATTR_CURVE_TYPE, ATTR_ID, Item}; use kurbo::Shape; - fn build_vector(points: &[DVec2], segments: &[(usize, usize)]) -> Vector { + fn build_vector(points: &[DVec2], segments: &[(usize, usize)]) -> (Vector, List) { let mut vector = Vector::default(); let mut point_id = PointId::ZERO; - for &position in points { - vector.point_domain.push(point_id.next_id(), position); + for (index, &position) in points.iter().enumerate() { + vector.0.push(Item::new_from_element(position)); + vector.0.set_attribute(ATTR_ID, index, point_id.next_id()); } + + let mut paths = List::new(); let mut segment_id = SegmentId::ZERO; for &(start, end) in segments { - vector.segment_domain.push(segment_id.next_id(), start, end, BezierHandles::Linear); + let mut path = Path::from_references([start as u32, end as u32]); + path.0.set_attribute(ATTR_ID, 0, segment_id.next_id()); + paths.push(Item::new_from_element(path).with_attribute(ATTR_CURVE_TYPE, CurveType::Linear)); + } + + (vector, paths) + } + + // A morph pairs manipulators by position, so a loop's walk is fixed: from its lowest anchor, along the earliest span there + #[test] + fn a_closed_loop_walks_from_its_lowest_anchor_along_its_earliest_span() { + let (vector, mut paths) = build_vector(&[DVec2::new(0., 10.), DVec2::new(0., 0.), DVec2::new(10., 0.)], &[]); + + // The loop weaves 1 to 2 to 0 and back, so the earliest span at anchor 0 arrives there and is crossed backwards + let mut path = Path::from_references([1, 2, 0, 1]); + let mut segment_id = SegmentId::ZERO; + for span in 0..3 { + path.0.set_attribute(ATTR_ID, span, segment_id.next_id()); + } + paths.push(Item::new_from_element(path).with_attribute(ATTR_CURVE_TYPE, CurveType::Linear)); + + let (manipulators, closed) = vector.stroke_manipulator_groups(&paths).next().expect("the loop is one subpath"); + assert!(closed); + let anchors: Vec = manipulators.iter().map(|manipulator| manipulator.anchor).collect(); + assert_eq!(anchors, vec![DVec2::new(0., 10.), DVec2::new(10., 0.), DVec2::new(0., 0.)]); + } + + /// Manipulator groups and contours are two views of the same walk, so they must agree on order and + /// geometry. Click targets, the snapper's point recovery, and outline stitching all rely on it. + #[test] + fn manipulator_groups_and_contours_agree() { + use crate::vector::misc::bezpath_from_manipulator_groups; + use crate::vector::path::pool_and_paths_from_bezpath; + + // A closed contour woven before an open one, so path order alone would emit them the other way around + let mut source = kurbo::BezPath::new(); + source.move_to((0., 0.)); + source.line_to((10., 0.)); + source.line_to((10., 10.)); + source.close_path(); + source.move_to((20., 20.)); + source.line_to((30., 20.)); + source.curve_to((35., 25.), (35., 30.), (30., 35.)); + + let (pool, paths) = pool_and_paths_from_bezpath(&source); + let vector = Vector(pool); + + let groups: Vec<_> = vector.stroke_manipulator_groups(&paths).collect(); + let contours = vector.stroke_bezpath_iter(&paths).collect::>(); + assert_eq!(groups.len(), 2); + assert_eq!(contours.len(), 2); + + // Open runs lead, and each group re-emits its own contour + assert!(!groups[0].1, "the open run should be emitted first"); + assert!(groups[1].1, "the closed loop should follow"); + for (index, (manipulators, closed)) in groups.iter().enumerate() { + let rebuilt = bezpath_from_manipulator_groups(manipulators, *closed); + assert_eq!(rebuilt.elements(), contours[index].elements(), "group {index} should describe contour {index}"); } - vector + } + + /// Two paths welded head to head, so the subpath must walk the second one backward and still + /// emit anchors and geometry in travel order. + #[test] + fn subpath_walks_a_welded_path_backward() { + use crate::vector::misc::bezpath_from_manipulator_groups; + + let points = [DVec2::new(0., 0.), DVec2::new(10., 0.), DVec2::new(20., 0.)]; + let (vector, paths) = build_vector(&points, &[(0, 1), (2, 1)]); + + let contours = vector.stroke_bezpath_iter(&paths).collect::>(); + assert_eq!(contours.len(), 1, "the shared endpoint welds both paths into one contour"); + + let groups: Vec<_> = vector.stroke_manipulator_groups(&paths).collect(); + assert_eq!(groups.len(), 1); + let (manipulators, closed) = &groups[0]; + assert!(!closed); + assert_eq!(manipulators.iter().map(|group| group.anchor).collect::>(), points.to_vec()); + + assert_eq!(bezpath_from_manipulator_groups(manipulators, *closed).elements(), contours[0].elements()); } /// Square wound positively with a reverse-wound triangle welded to its corner: @@ -1438,10 +1047,10 @@ mod tests { DVec2::new(4., 2.), ]; let segments = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 4), (4, 5), (5, 0)]; - let vector = build_vector(&points, &segments); - assert!(vector.is_branching()); + let (vector, paths) = build_vector(&points, &segments); + assert!(vector.is_branching(&paths)); - let faces = vector.construct_faces(); + let faces = vector.construct_faces(&paths); assert_eq!(faces.len(), 1); assert_ne!(faces[0].winding(kurbo::Point::new(8., 8.)), 0, "the region around the triangle should be filled"); assert_eq!(faces[0].winding(kurbo::Point::new(1.5, 1.5)), 0, "the reverse-wound triangle interior should stay empty"); @@ -1459,9 +1068,9 @@ mod tests { DVec2::new(4., 2.), ]; let segments = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 5), (5, 4), (4, 0)]; - let vector = build_vector(&points, &segments); + let (vector, paths) = build_vector(&points, &segments); - let faces = vector.construct_faces(); + let faces = vector.construct_faces(&paths); assert_eq!(faces.len(), 2); assert!( faces.iter().any(|face| face.winding(kurbo::Point::new(1.5, 1.5)) != 0), @@ -1485,10 +1094,10 @@ mod tests { DVec2::new(0., 5.), ]; let segments = [(0, 4), (1, 4), (1, 5), (2, 5), (2, 6), (3, 6), (3, 7), (0, 7), (4, 5), (5, 6), (6, 7), (4, 7)]; - let vector = build_vector(&points, &segments); - assert!(vector.use_face_fill()); + let (vector, paths) = build_vector(&points, &segments); + assert!(vector.use_face_fill(&paths)); - let faces = vector.construct_faces(); + let faces = vector.construct_faces(&paths); assert_eq!(faces.len(), 5); assert!(faces.iter().any(|face| face.winding(kurbo::Point::new(5., 5.)) != 0), "the center diamond cell should be filled"); assert!(faces.iter().any(|face| face.winding(kurbo::Point::new(1., 1.)) != 0), "the corner cells should be filled"); @@ -1510,10 +1119,10 @@ mod tests { DVec2::new(1., 5.), ]; let segments = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 8)]; - let vector = build_vector(&points, &segments); - assert!(vector.is_branching()); + let (vector, paths) = build_vector(&points, &segments); + assert!(vector.is_branching(&paths)); - let faces = vector.construct_faces(); + let faces = vector.construct_faces(&paths); assert_eq!(faces.len(), 1); assert_ne!(faces[0].winding(kurbo::Point::new(1., 1.)), 0, "the ring around the hole should be filled"); assert_eq!(faces[0].winding(kurbo::Point::new(5., 5.)), 0, "the reverse-wound hole should stay empty"); @@ -1534,10 +1143,10 @@ mod tests { DVec2::new(1., 5.), ]; let segments = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 4), (0, 7)]; - let vector = build_vector(&points, &segments); - assert!(vector.is_branching()); + let (vector, paths) = build_vector(&points, &segments); + assert!(vector.is_branching(&paths)); - let faces = vector.construct_faces(); + let faces = vector.construct_faces(&paths); assert_eq!(faces.len(), 1); assert_ne!(faces[0].winding(kurbo::Point::new(1., 1.)), 0, "the region around the hole should be filled"); assert_eq!(faces[0].winding(kurbo::Point::new(5., 7.5)), 0, "the snapped hole should stay empty"); @@ -1556,10 +1165,10 @@ mod tests { DVec2::new(20., 10.), ]; let segments = [(0, 1), (1, 2), (2, 3), (3, 0), (3, 4), (4, 5), (5, 2)]; - let vector = build_vector(&points, &segments); - assert!(vector.is_branching()); + let (vector, paths) = build_vector(&points, &segments); + assert!(vector.is_branching(&paths)); - let faces = vector.construct_faces(); + let faces = vector.construct_faces(&paths); assert_eq!(faces.len(), 2); assert!(faces.iter().any(|face| face.winding(kurbo::Point::new(5., 5.)) != 0), "the reverse-drawn left cell should be filled"); assert!(faces.iter().any(|face| face.winding(kurbo::Point::new(15., 5.)) != 0), "the right cell should be filled"); @@ -1571,11 +1180,11 @@ mod tests { fn fully_enclosed_reverse_drawn_mesh_cell_still_fills() { let points = [DVec2::new(4., 3.), DVec2::new(6., 3.), DVec2::new(5., 5.), DVec2::new(0., 0.), DVec2::new(10., 0.), DVec2::new(5., 10.)]; let segments = [(0, 2), (2, 1), (1, 0), (3, 4), (4, 5), (5, 3), (0, 3), (1, 4), (2, 5)]; - let vector = build_vector(&points, &segments); - assert!(vector.is_branching()); - assert!(drawn_signed_area(&vector, 0..3) < 0.); + let (vector, paths) = build_vector(&points, &segments); + assert!(vector.is_branching(&paths)); + assert!(drawn_signed_area(&vector, &paths, 0..3) < 0.); - let faces = vector.construct_faces(); + let faces = vector.construct_faces(&paths); assert_eq!(faces.len(), 4); assert!(faces.iter().any(|face| face.winding(kurbo::Point::new(5., 4.)) != 0), "the enclosed hub cell should be filled"); } @@ -1595,11 +1204,11 @@ mod tests { DVec2::new(7., 3.), ]; let segments = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4)]; - let vector = build_vector(&points, &segments); - assert!(vector.is_branching()); - assert!(drawn_signed_area(&vector, 4..8) < 0.); + let (vector, paths) = build_vector(&points, &segments); + assert!(vector.is_branching(&paths)); + assert!(drawn_signed_area(&vector, &paths, 4..8) < 0.); - let faces = vector.construct_faces(); + let faces = vector.construct_faces(&paths); assert_eq!(faces.len(), 1); assert_ne!(faces[0].winding(kurbo::Point::new(8., 5.)), 0, "the ring should be filled"); assert_eq!(faces[0].winding(kurbo::Point::new(5., 5.)), 0, "the bridged hole should stay empty"); @@ -1620,13 +1229,13 @@ mod tests { DVec2::new(3., 1.), ]; let segments = [(0, 1), (1, 2), (2, 3), (3, 0), (1, 3), (4, 5), (5, 6), (6, 7), (7, 4)]; - let mut vector = build_vector(&points, &segments); - assert!(drawn_signed_area(&vector, 5..9) < 0.); + let (mut vector, mut paths) = build_vector(&points, &segments); + assert!(drawn_signed_area(&vector, &paths, 5..9) < 0.); - vector.normalize_winding_directions(); - assert!(drawn_signed_area(&vector, 5..9) < 0., "normalization must not flip a hole nested under mesh structure"); + vector.normalize_winding_directions(&mut paths); + assert!(drawn_signed_area(&vector, &paths, 5..9) < 0., "normalization must not flip a hole nested under mesh structure"); - let faces = vector.construct_faces(); + let faces = vector.construct_faces(&paths); assert_eq!(faces.len(), 2); assert!(faces.iter().all(|face| face.winding(kurbo::Point::new(2., 2.)) == 0), "the hole should stay empty"); assert!(faces.iter().any(|face| face.winding(kurbo::Point::new(0.5, 0.5)) != 0), "the triangle around the hole should be filled"); @@ -1639,19 +1248,20 @@ mod tests { fn spur_does_not_rescue_a_negative_space_loop() { let points = [DVec2::new(0., 0.), DVec2::new(0., 10.), DVec2::new(10., 10.), DVec2::new(10., 0.), DVec2::new(5., 5.)]; let segments = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 4)]; - let vector = build_vector(&points, &segments); + let (vector, paths) = build_vector(&points, &segments); - let faces = vector.construct_faces(); + let faces = vector.construct_faces(&paths); assert!(faces.is_empty(), "a reverse-wound loop with an interior spur should produce no filled faces"); } /// Shoelace sum over the drawn direction of a range of line segments, so tests can read the /// stored winding directly rather than through a path walker that may pick its own direction. - fn drawn_signed_area(vector: &Vector, segment_range: std::ops::Range) -> f64 { + fn drawn_signed_area(vector: &Vector, paths: &List, segment_range: std::ops::Range) -> f64 { + let graph = SegmentGraph::new(vector, paths); segment_range .map(|segment_index| { - let start = vector.point_domain.positions()[vector.segment_domain.start_point()[segment_index]]; - let end = vector.point_domain.positions()[vector.segment_domain.end_point()[segment_index]]; + let start = vector.position(graph.start_point[segment_index]).unwrap(); + let end = vector.position(graph.end_point[segment_index]).unwrap(); start.perp_dot(end) }) .sum::() @@ -1661,11 +1271,11 @@ mod tests { #[test] fn normalization_flips_a_reverse_wound_loop() { let points = [DVec2::new(0., 0.), DVec2::new(0., 10.), DVec2::new(10., 10.), DVec2::new(10., 0.)]; - let mut vector = build_vector(&points, &[(0, 1), (1, 2), (2, 3), (3, 0)]); - assert!(drawn_signed_area(&vector, 0..4) < 0.); - vector.normalize_winding_directions(); + let (mut vector, mut paths) = build_vector(&points, &[(0, 1), (1, 2), (2, 3), (3, 0)]); + assert!(drawn_signed_area(&vector, &paths, 0..4) < 0.); + vector.normalize_winding_directions(&mut paths); - assert!(drawn_signed_area(&vector, 0..4) > 0., "a lone loop should wind positively after normalization"); + assert!(drawn_signed_area(&vector, &paths, 0..4) > 0., "a lone loop should wind positively after normalization"); } #[test] @@ -1681,11 +1291,11 @@ mod tests { DVec2::new(3., 7.), ]; let segments = [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4)]; - let mut vector = build_vector(&points, &segments); - vector.normalize_winding_directions(); + let (mut vector, mut paths) = build_vector(&points, &segments); + vector.normalize_winding_directions(&mut paths); - assert!(drawn_signed_area(&vector, 0..4) > 0., "the outer loop should wind positively"); - assert!(drawn_signed_area(&vector, 4..8) < 0., "the nested loop should wind negatively"); + assert!(drawn_signed_area(&vector, &paths, 0..4) > 0., "the outer loop should wind positively"); + assert!(drawn_signed_area(&vector, &paths, 4..8) < 0., "the nested loop should wind negatively"); } /// Loops passing through a branch point are welded or mesh structure whose drawn winding is meaningful, @@ -1694,14 +1304,15 @@ mod tests { fn normalization_leaves_branching_structure_untouched() { let points = [DVec2::new(0., 0.), DVec2::new(4., 0.), DVec2::new(2., 3.), DVec2::new(-4., 0.), DVec2::new(-2., -3.)]; let segments = [(0, 1), (1, 2), (2, 0), (0, 3), (3, 4), (4, 0)]; - let mut vector = build_vector(&points, &segments); + let (mut vector, mut paths) = build_vector(&points, &segments); - let starts_before = vector.segment_domain.start_point().to_vec(); - let ends_before = vector.segment_domain.end_point().to_vec(); - vector.normalize_winding_directions(); + let before = SegmentGraph::new(&vector, &paths); + let (starts_before, ends_before) = (before.start_point.clone(), before.end_point.clone()); + vector.normalize_winding_directions(&mut paths); - assert_eq!(vector.segment_domain.start_point(), starts_before.as_slice()); - assert_eq!(vector.segment_domain.end_point(), ends_before.as_slice()); + let after = SegmentGraph::new(&vector, &paths); + assert_eq!(after.start_point, starts_before); + assert_eq!(after.end_point, ends_before); } /// Pruning dead-end spurs first lets the loop they hang off still be found and flipped, @@ -1710,11 +1321,54 @@ mod tests { fn normalization_flips_a_loop_with_a_spur_attached() { let points = [DVec2::new(0., 0.), DVec2::new(0., 10.), DVec2::new(10., 10.), DVec2::new(10., 0.), DVec2::new(5., 5.)]; let segments = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 4)]; - let mut vector = build_vector(&points, &segments); - vector.normalize_winding_directions(); + let (mut vector, mut paths) = build_vector(&points, &segments); + vector.normalize_winding_directions(&mut paths); + + assert!(drawn_signed_area(&vector, &paths, 0..4) > 0., "the reverse-wound loop should be flipped"); + let graph = SegmentGraph::new(&vector, &paths); + let spur = graph.spans.iter().position(|span| span.path == 4).expect("the spur keeps its own path"); + assert_eq!((graph.start_point[spur], graph.end_point[spur]), (0, 4), "the spur should keep its drawn direction"); + } - assert_eq!(vector.segment_domain.start_point()[0], 1, "the reverse-wound loop should be flipped"); - assert_eq!(vector.segment_domain.start_point()[4], 0, "the spur should keep its drawn direction"); - assert_eq!(vector.segment_domain.end_point()[4], 4); + /// A loop covering only the middle spans of one longer path splits that path so just the loop reverses. + #[test] + fn normalization_splits_a_path_to_flip_its_inner_loop() { + let points = [ + DVec2::new(-5., 0.), + DVec2::new(0., 0.), + DVec2::new(0., 10.), + DVec2::new(10., 10.), + DVec2::new(10., 0.), + DVec2::new(15., 0.), + ]; + let mut path = Path::from_references([0, 1, 2, 3, 4, 1, 5]); + let mut segment_id = SegmentId::ZERO; + for span in 0..6 { + path.0.set_attribute(ATTR_ID, span, segment_id.next_id()); + } + + let mut vector = Vector::default(); + let mut point_id = PointId::ZERO; + for (index, &position) in points.iter().enumerate() { + vector.0.push(Item::new_from_element(position)); + vector.0.set_attribute(ATTR_ID, index, point_id.next_id()); + } + let mut paths = List::new(); + paths.push(Item::new_from_element(path).with_attribute(ATTR_CURVE_TYPE, CurveType::Linear)); + + vector.normalize_winding_directions(&mut paths); + + // The reverse-wound inner loop (1 -> 2 -> 3 -> 4 -> 1) flips while both spur spans keep their direction + let graph = SegmentGraph::new(&vector, &paths); + let loop_area = (0..graph.len()) + .filter(|&index| graph.start_point[index] != 0 && graph.end_point[index] != 0 && graph.start_point[index] != 5 && graph.end_point[index] != 5) + .map(|index| { + let start = vector.position(graph.start_point[index]).unwrap(); + let end = vector.position(graph.end_point[index]).unwrap(); + start.perp_dot(end) + }) + .sum::() + / 2.; + assert!(loop_area > 0., "the inner loop should wind positively after normalization"); } } diff --git a/node-graph/libraries/vector-types/src/vector/vector_modification.rs b/node-graph/libraries/vector-types/src/vector/vector_modification.rs index 1fedf925cec..b90388f9015 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_modification.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_modification.rs @@ -1,11 +1,11 @@ use super::*; -use crate::vector::algorithms::consts::CLOSE_POINT_TOLERANCE; -use crate::vector::misc::BezierHandles; -use crate::vector::misc::{HandleId, HandleType, point_to_dvec2, segment_to_handles}; +use crate::vector::flatten::WorkingVector; +use crate::vector::misc::{BezierHandles, HandleId, HandleType}; +use crate::vector::path::Path; +use core_types::list::List; use core_types::uuid::generate_uuid; use dyn_any::DynAny; use glam::DVec2; -use kurbo::{BezPath, ParamCurve, PathEl, Point}; use serde::de::{SeqAccess, Visitor}; use serde::ser::SerializeSeq; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -14,7 +14,7 @@ use std::fmt; use std::hash::BuildHasher; use std::hash::Hash; -/// Represents a procedural change to the [`PointDomain`] in [`Vector`]. +/// Represents a procedural change to the points of a [`Vector`]. #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub(crate) struct PointModification { @@ -26,25 +26,47 @@ pub(crate) struct PointModification { } impl PointModification { - /// Apply this modification to the specified [`PointDomain`]. - pub fn apply(&self, point_domain: &mut PointDomain, segment_domain: &mut SegmentDomain) { - point_domain.retain(segment_domain, |id| !self.remove.contains(id)); + /// Apply this modification to the working form's points. + fn apply(&self, working: &mut WorkingVector) { + // Remove points, remapping segment endpoints and leaving out-of-range markers on segments + // that referenced a removed point, for the segment pass to delete + let mut id_map = Vec::with_capacity(working.point_ids.len()); + let mut new_index = 0; + for id in &working.point_ids { + if !self.remove.contains(id) { + id_map.push(new_index); + new_index += 1; + } else { + id_map.push(usize::MAX); + } + } + let update_index = |index: &mut usize| { + if *index < id_map.len() { + *index = id_map[*index]; + } + }; + working.segment_start.iter_mut().for_each(update_index); + working.segment_end.iter_mut().for_each(update_index); + + let mut keep = working.point_ids.iter().map(|id| !self.remove.contains(id)); + working.point_positions.retain(|_| keep.next().unwrap_or_default()); + working.point_ids.retain(|id| !self.remove.contains(id)); - for (index, (id, position)) in point_domain.positions_mut().enumerate() { - let Some(&delta) = self.delta.get(&id) else { continue }; + for (index, id) in working.point_ids.iter().enumerate() { + let Some(&delta) = self.delta.get(id) else { continue }; if !delta.is_finite() { warn!("Invalid delta when applying a point modification"); continue; } - *position += delta; + working.point_positions[index] += delta; - for (_, handles, start, end) in segment_domain.handles_mut() { - if start == index { - handles.move_start(delta); + for segment_index in 0..working.segment_ids.len() { + if working.segment_start[segment_index] == index { + working.segment_handles[segment_index].move_start(delta); } - if end == index { - handles.move_end(delta); + if working.segment_end[segment_index] == index { + working.segment_handles[segment_index].move_end(delta); } } } @@ -56,16 +78,23 @@ impl PointModification { continue; } - point_domain.push(add_id, position); + #[cfg(debug_assertions)] + if working.point_ids.contains(&add_id) { + warn!("Tried to push a duplicate point to the working form"); + continue; + } + + working.point_ids.push(add_id); + working.point_positions.push(position); } } - /// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`]. - pub fn create_from_vector(vector: &Vector) -> Self { + /// Create a new modification that will convert an empty [`Vector`] into the target working form. + fn create_from_working(working: &WorkingVector) -> Self { Self { - add: vector.point_domain.ids().to_vec(), + add: working.point_ids.clone(), remove: HashSet::new(), - delta: vector.point_domain.ids().iter().copied().zip(vector.point_domain.positions().iter().cloned()).collect(), + delta: working.point_ids.iter().copied().zip(working.point_positions.iter().copied()).collect(), } } @@ -81,7 +110,7 @@ impl PointModification { } } -/// Represents a procedural change to the [`SegmentDomain`] in [`Vector`]. +/// Represents a procedural change to the segments of a [`Vector`]. #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub(crate) struct SegmentModification { @@ -99,44 +128,52 @@ pub(crate) struct SegmentModification { } impl SegmentModification { - /// Apply this modification to the specified [`SegmentDomain`]. - pub fn apply(&self, segment_domain: &mut SegmentDomain, point_domain: &PointDomain) { - segment_domain.retain(|id| !self.remove.contains(id), point_domain.ids().len()); - - for (id, point) in segment_domain.start_point_mut() { - let Some(&new) = self.start_point.get(&id) else { continue }; - let Some(index) = point_domain.resolve_id(new) else { - warn!("Invalid start ID when applying a segment modification"); - continue; - }; - - *point = index; - } - - for (id, point) in segment_domain.end_point_mut() { - let Some(&new) = self.end_point.get(&id) else { continue }; - let Some(index) = point_domain.resolve_id(new) else { - warn!("Invalid end ID when applying a segment modification"); - continue; - }; - - *point = index; + /// Apply this modification to the working form's segments. + fn apply(&self, working: &mut WorkingVector) { + // Remove the deleted segments, plus any segment left referencing a removed point + let point_count = working.point_ids.len(); + let keep: Vec = (0..working.segment_ids.len()) + .map(|index| !self.remove.contains(&working.segment_ids[index]) && working.segment_start[index] < point_count && working.segment_end[index] < point_count) + .collect(); + let mut keep_iter = keep.iter(); + working.segment_start.retain(|_| *keep_iter.next().unwrap_or(&false)); + let mut keep_iter = keep.iter(); + working.segment_end.retain(|_| *keep_iter.next().unwrap_or(&false)); + let mut keep_iter = keep.iter(); + working.segment_handles.retain(|_| *keep_iter.next().unwrap_or(&false)); + let mut keep_iter = keep.iter(); + working.segment_ids.retain(|_| *keep_iter.next().unwrap_or(&false)); + + for (index, id) in working.segment_ids.iter().enumerate() { + if let Some(&new) = self.start_point.get(id) { + match working.resolve_id(new) { + Some(point) => working.segment_start[index] = point, + None => warn!("Invalid start ID when applying a segment modification"), + } + } + if let Some(&new) = self.end_point.get(id) { + match working.resolve_id(new) { + Some(point) => working.segment_end[index] = point, + None => warn!("Invalid end ID when applying a segment modification"), + } + } } - for (id, handles, start, end) in segment_domain.handles_mut() { - let Some(&start) = point_domain.positions().get(start) else { continue }; - let Some(&end) = point_domain.positions().get(end) else { continue }; + for (index, id) in working.segment_ids.iter().enumerate() { + let Some(&start) = working.point_positions.get(working.segment_start[index]) else { continue }; + let Some(&end) = working.point_positions.get(working.segment_end[index]) else { continue }; // Compute the actual start and end position based on the offset from the anchor - let start = self.handle_primary.get(&id).copied().map(|handle| handle.map(|handle| handle + start)); - let end = self.handle_end.get(&id).copied().map(|handle| handle.map(|handle| handle + end)); + let new_start = self.handle_primary.get(id).copied().map(|handle| handle.map(|handle| handle + start)); + let new_end = self.handle_end.get(id).copied().map(|handle| handle.map(|handle| handle + end)); - if !start.unwrap_or_default().is_none_or(|start| start.is_finite()) || !end.unwrap_or_default().is_none_or(|end| end.is_finite()) { + if !new_start.unwrap_or_default().is_none_or(|start| start.is_finite()) || !new_end.unwrap_or_default().is_none_or(|end| end.is_finite()) { warn!("Invalid handles when applying a segment modification"); continue; } - match (start, end) { + let handles = &mut working.segment_handles[index]; + match (new_start, new_end) { // The new handles are fully specified by the modification (Some(Some(handle_start)), Some(Some(handle_end))) => *handles = BezierHandles::Cubic { handle_start, handle_end }, (Some(Some(handle)), Some(None)) | (Some(None), Some(Some(handle))) => *handles = BezierHandles::Quadratic { handle }, @@ -172,17 +209,17 @@ impl SegmentModification { let Some(&handle_start) = self.handle_primary.get(&add_id) else { continue }; let Some(&handle_end) = self.handle_end.get(&add_id) else { continue }; - let Some(start_index) = point_domain.resolve_id(start) else { + let Some(start_index) = working.resolve_id(start) else { warn!("invalid start id: {start:#?}"); continue; }; - let Some(end_index) = point_domain.resolve_id(end) else { + let Some(end_index) = working.resolve_id(end) else { warn!("invalid end id: {end:#?}"); continue; }; - let start_position = point_domain.positions()[start_index]; - let end_position = point_domain.positions()[end_index]; + let start_position = working.point_positions[start_index]; + let end_position = working.point_positions[end_index]; let handles = match (handle_start, handle_end) { (Some(handle_start), Some(handle_end)) => BezierHandles::Cubic { handle_start: handle_start + start_position, @@ -197,34 +234,35 @@ impl SegmentModification { continue; } - segment_domain.push(add_id, start_index, end_index, handles); + working.segment_ids.push(add_id); + working.segment_start.push(start_index); + working.segment_end.push(end_index); + working.segment_handles.push(handles); } - assert!( - segment_domain.start_point().iter().all(|&index| index < point_domain.ids().len()), - "index should be in range {segment_domain:#?}" - ); - assert!( - segment_domain.end_point().iter().all(|&index| index < point_domain.ids().len()), - "index should be in range {segment_domain:#?}" - ); + assert!(working.segment_start.iter().all(|&index| index < working.point_ids.len()), "index should be in range"); + assert!(working.segment_end.iter().all(|&index| index < working.point_ids.len()), "index should be in range"); } - /// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`]. - pub fn create_from_vector(vector: &Vector) -> Self { - let point_id = |(&segment, &index)| (segment, vector.point_domain.ids()[index]); + /// Create a new modification that will convert an empty [`Vector`] into the target working form. + fn create_from_working(working: &WorkingVector) -> Self { + let point_id = |(&segment, &index): (&SegmentId, &usize)| (segment, working.point_ids[index]); Self { - add: vector.segment_domain.ids().to_vec(), + add: working.segment_ids.clone(), remove: HashSet::new(), - start_point: vector.segment_domain.ids().iter().zip(vector.segment_domain.start_point()).map(point_id).collect(), - end_point: vector.segment_domain.ids().iter().zip(vector.segment_domain.end_point()).map(point_id).collect(), - handle_primary: vector - .segment_iter() - .map(|(id, segment, _, _)| (id, segment_to_handles(&segment).start().map(|handle| handle - point_to_dvec2(segment.start())))) + start_point: working.segment_ids.iter().zip(&working.segment_start).map(point_id).collect(), + end_point: working.segment_ids.iter().zip(&working.segment_end).map(point_id).collect(), + handle_primary: working + .segment_ids + .iter() + .enumerate() + .map(|(index, &id)| (id, working.segment_handles[index].start().map(|handle| handle - working.point_positions[working.segment_start[index]]))) .collect(), - handle_end: vector - .segment_iter() - .map(|(id, segment, _, _)| (id, segment_to_handles(&segment).end().map(|handle| handle - point_to_dvec2(segment.end())))) + handle_end: working + .segment_ids + .iter() + .enumerate() + .map(|(index, &id)| (id, working.segment_handles[index].end().map(|handle| handle - working.point_positions[working.segment_end[index]]))) .collect(), } } @@ -390,21 +428,34 @@ impl VectorModification { if lines.is_empty() { "None".to_string() } else { lines.join("\n") } } - /// Apply this modification to the specified [`Vector`]. - pub fn apply(&self, vector: &mut Vector) { - self.points.apply(&mut vector.point_domain, &mut vector.segment_domain); - self.segments.apply(&mut vector.segment_domain, &vector.point_domain); + /// Whether this modification changes nothing at all. + pub fn is_empty(&self) -> bool { + self.points == PointModification::default() && self.segments == SegmentModification::default() && self.add_g1_continuous.is_empty() && self.remove_g1_continuous.is_empty() + } + + /// Apply this modification to the specified [`Vector`] and its paths. + pub fn apply(&self, vector: &mut Vector, paths: &mut List) { + // An untouched vector keeps its incoming path structure instead of being flattened and rewoven + if self.is_empty() { + return; + } - let valid = |val: &[HandleId; 2]| vector.segment_domain.ids().contains(&val[0].segment) && vector.segment_domain.ids().contains(&val[1].segment); - vector - .colinear_manipulators + let mut working = WorkingVector::flatten(vector, paths); + self.points.apply(&mut working); + self.segments.apply(&mut working); + + let valid = |val: &[HandleId; 2]| working.segment_ids.contains(&val[0].segment) && working.segment_ids.contains(&val[1].segment); + working + .colinear .retain(|val| !self.remove_g1_continuous.contains(val) && !self.remove_g1_continuous.contains(&[val[1], val[0]]) && valid(val)); for handles in &self.add_g1_continuous { - if !vector.colinear_manipulators.iter().any(|test| test == handles || test == &[handles[1], handles[0]]) && valid(handles) { - vector.colinear_manipulators.push(*handles); + if !working.colinear.iter().any(|test| test == handles || test == &[handles[1], handles[0]]) && valid(handles) { + working.colinear.push(*handles); } } + + working.reweave(vector, paths); } /// Add a [`VectorModificationType`] to this modification. @@ -469,11 +520,12 @@ impl VectorModification { } /// Create a new modification that will convert an empty [`Vector`] into the target [`Vector`]. - pub fn create_from_vector(vector: &Vector) -> Self { + pub fn create_from_vector(vector: &Vector, paths: &List) -> Self { + let working = WorkingVector::flatten(vector, paths); Self { - points: PointModification::create_from_vector(vector), - segments: SegmentModification::create_from_vector(vector), - add_g1_continuous: vector.colinear_manipulators.iter().copied().collect(), + points: PointModification::create_from_working(&working), + segments: SegmentModification::create_from_working(&working), + add_g1_continuous: working.colinear.iter().copied().collect(), remove_g1_continuous: HashSet::new(), } } @@ -588,147 +640,6 @@ where deserializer.deserialize_seq(visitor) } -pub(crate) struct AppendBezpath<'a> { - first_point: Option, - last_point: Option, - first_point_index: Option, - last_point_index: Option, - point_id: PointId, - segment_id: SegmentId, - vector: &'a mut Vector, -} - -impl<'a> AppendBezpath<'a> { - fn new(vector: &'a mut Vector) -> Self { - Self { - first_point: None, - last_point: None, - first_point_index: None, - last_point_index: None, - point_id: vector.point_domain.next_id(), - segment_id: vector.segment_domain.next_id(), - vector, - } - } - - fn append_segment_and_close_path(&mut self, point: Point, handle: BezierHandles) { - // A path's final point may return to approximately (but not bit-exactly) its start before closing, e.g. a contour - // produced by Kurbo's path offsetting. Treat a near-coincident final point as already on the start so we don't - // introduce a redundant duplicate anchor and a zero-length closing segment. - let endpoints_coincide = (self.first_point.unwrap() - point).hypot2() <= CLOSE_POINT_TOLERANCE * CLOSE_POINT_TOLERANCE; - let handle = if !endpoints_coincide { - // If the first point is not the same as the last point of the path then we append the segment - // with given handle and point and then close the path with linear handle. - self.append_segment(point, handle); - BezierHandles::Linear - } else { - // if the endpoints are the same then we close the path with given handle. - handle - }; - - // Create a new segment. - let next_segment_id = self.segment_id.next_id(); - self.vector - .segment_domain - .push(next_segment_id, self.last_point_index.unwrap(), self.first_point_index.unwrap(), handle); - } - - fn append_segment(&mut self, end_point: Point, handle: BezierHandles) { - // Append the point. - let next_point_index = self.vector.point_domain.ids().len(); - let next_point_id = self.point_id.next_id(); - - self.vector.point_domain.push(next_point_id, point_to_dvec2(end_point)); - - // Append the segment. - let next_segment_id = self.segment_id.next_id(); - self.vector.segment_domain.push(next_segment_id, self.last_point_index.unwrap(), next_point_index, handle); - - // Update the states. - self.last_point = Some(end_point); - self.last_point_index = Some(next_point_index); - } - - fn append_first_point(&mut self, point: Point) { - self.first_point = Some(point); - self.last_point = Some(point); - - // Append the first point. - let next_point_index = self.vector.point_domain.ids().len(); - self.vector.point_domain.push(self.point_id.next_id(), point_to_dvec2(point)); - - // Update the state. - self.first_point_index = Some(next_point_index); - self.last_point_index = Some(next_point_index); - } - - fn reset(&mut self) { - self.first_point = None; - self.last_point = None; - self.first_point_index = None; - self.last_point_index = None; - } - - pub fn append_bezpath(vector: &'a mut Vector, bezpath: BezPath) { - let mut this = Self::new(vector); - let mut elements = bezpath.elements().iter().peekable(); - - while let Some(element) = elements.next() { - let close_path = elements.peek().is_some_and(|elm| **elm == PathEl::ClosePath); - - match *element { - PathEl::MoveTo(point) => { - // Clear any segment state left by a preceding open contour so its segments don't leak into this contour's region - this.reset(); - this.append_first_point(point); - } - PathEl::LineTo(point) => { - let handle = BezierHandles::Linear; - if close_path { - this.append_segment_and_close_path(point, handle); - } else { - this.append_segment(point, handle); - } - } - PathEl::QuadTo(point, point1) => { - let handle = BezierHandles::Quadratic { handle: point_to_dvec2(point) }; - if close_path { - this.append_segment_and_close_path(point1, handle); - } else { - this.append_segment(point1, handle); - } - } - PathEl::CurveTo(point, point1, point2) => { - let handle = BezierHandles::Cubic { - handle_start: point_to_dvec2(point), - handle_end: point_to_dvec2(point1), - }; - - if close_path { - this.append_segment_and_close_path(point2, handle); - } else { - this.append_segment(point2, handle); - } - } - PathEl::ClosePath => { - // Already handled using `append_segment_and_close_path()` hence we reset state and continue. - this.reset(); - } - } - } - } -} - -pub trait VectorExt { - fn append_bezpath(&mut self, bezpath: BezPath); -} - -impl VectorExt for Vector { - fn append_bezpath(&mut self, bezpath: BezPath) { - AppendBezpath::append_bezpath(self, bezpath); - } -} - pub trait HandleExt { /// Set the handle's position relative to the anchor which is the start anchor for the primary handle and end anchor for the end handle. #[must_use] @@ -750,18 +661,33 @@ mod tests { use super::*; use crate::vector::algorithms::shapes::{ellipse_bezpath, rectangle_bezpath}; - use kurbo::{PathSeg, QuadBez}; + use kurbo::{BezPath, PathSeg, Point, QuadBez}; + + fn anchor_id(vector: &Vector, paths: &List, anchor_index: usize) -> PointId { + vector.anchor_points(paths).nth(anchor_index).expect("anchor exists") + } + + fn anchor_position(vector: &Vector, paths: &List, anchor_index: usize) -> DVec2 { + let id = anchor_id(vector, paths, anchor_index); + vector.position(vector.index_of_point_id(id).unwrap() as usize).unwrap() + } #[test] fn modify_new() { - let mut vector = Vector::from_bezpath(ellipse_bezpath(DVec2::ZERO, DVec2::ONE)); - vector.append_bezpath(rectangle_bezpath(DVec2::NEG_ONE, DVec2::ZERO)); + let (mut vector, mut paths) = Vector::from_bezpath(&ellipse_bezpath(DVec2::ZERO, DVec2::ONE)); + vector.append_bezpath(&mut paths, &rectangle_bezpath(DVec2::NEG_ONE, DVec2::ZERO)); + + let modify = VectorModification::create_from_vector(&vector, &paths); - let modify = VectorModification::create_from_vector(&vector); + let (mut new, mut new_paths) = (Vector::default(), List::new()); + modify.apply(&mut new, &mut new_paths); - let mut new = Vector::default(); - modify.apply(&mut new); - assert_eq!(vector, new); + assert_eq!( + vector.segment_iter(&paths).collect::>(), + new.segment_iter(&new_paths).collect::>(), + "replaying the capture should rebuild every segment" + ); + assert_eq!(vector.anchor_points(&paths).collect::>(), new.anchor_points(&new_paths).collect::>()); } #[test] @@ -771,34 +697,34 @@ mod tests { open_quads.quad_to(Point::new(5., 10.), Point::new(10., 0.)); open_quads.quad_to(Point::new(15., 10.), Point::new(20., 0.)); - let mut vector = Vector::from_bezpath(ellipse_bezpath(DVec2::ZERO, DVec2::ONE)); - vector.append_bezpath(rectangle_bezpath(DVec2::NEG_ONE, DVec2::ZERO)); - vector.append_bezpath(open_quads); + let (mut vector, mut paths) = Vector::from_bezpath(&ellipse_bezpath(DVec2::ZERO, DVec2::ONE)); + vector.append_bezpath(&mut paths, &rectangle_bezpath(DVec2::NEG_ONE, DVec2::ZERO)); + vector.append_bezpath(&mut paths, &open_quads); - let mut modify_new = VectorModification::create_from_vector(&vector); + let mut modify_new = VectorModification::create_from_vector(&vector, &paths); let mut modify_original = VectorModification::default(); for modification in [&mut modify_new, &mut modify_original] { - let point = vector.point_domain.ids()[0]; + let point = anchor_id(&vector, &paths, 0); modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X * 0.5 }); - let point = vector.point_domain.ids()[9]; + let point = anchor_id(&vector, &paths, 9); modification.modify(&VectorModificationType::ApplyPointDelta { point, delta: DVec2::X }); } - let mut new = Vector::default(); - modify_new.apply(&mut new); + let (mut new, mut new_paths) = (Vector::default(), List::new()); + modify_new.apply(&mut new, &mut new_paths); - modify_original.apply(&mut vector); + modify_original.apply(&mut vector, &mut paths); - assert_eq!(vector, new); - assert_eq!(vector.point_domain.positions()[0], DVec2::X); - assert_eq!(vector.point_domain.positions()[9], DVec2::new(11., 0.)); + assert_eq!(vector.segment_iter(&paths).collect::>(), new.segment_iter(&new_paths).collect::>()); + assert_eq!(anchor_position(&vector, &paths, 0), DVec2::X); + assert_eq!(anchor_position(&vector, &paths, 9), DVec2::new(11., 0.)); assert_eq!( - vector.segment_iter().nth(8).unwrap().1, + vector.segment_iter(&paths).nth(8).unwrap().1, PathSeg::Quad(QuadBez::new(Point::new(0., 0.), Point::new(5., 10.), Point::new(11., 0.))) ); assert_eq!( - vector.segment_iter().nth(9).unwrap().1, + vector.segment_iter(&paths).nth(9).unwrap().1, PathSeg::Quad(QuadBez::new(Point::new(11., 0.), Point::new(16., 10.), Point::new(20., 0.))) ); } diff --git a/node-graph/libraries/vector-types/src/vector/vector_types.rs b/node-graph/libraries/vector-types/src/vector/vector_types.rs index 62367f05b68..a8c9cfd25f3 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_types.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_types.rs @@ -1,55 +1,29 @@ -use super::misc::dvec2_to_point; +use super::addressing; +use super::connectivity::Connectivity; +use super::contour::{contours, subpaths}; +use super::misc::{HandleId, ManipulatorPointId, dvec2_to_point}; +use super::path::{CurveType, Path, append_bezpath, curve_type_at, no_paths, paths_from_item_attributes, paths_from_list_row_attributes, push_path}; use super::style::{Stroke, StrokeAlign, StrokeCap, StrokeJoin}; -use super::vector_attributes::*; -use crate::vector::misc::{BezierHandles, ManipulatorGroup}; -use crate::vector::misc::{HandleId, ManipulatorPointId}; -use crate::vector::vector_modification::VectorExt; +use super::vector_attributes::{PointId, SegmentId}; use core_types::bounds::{BoundingBox, RenderBoundingBox}; +use core_types::list::{ATTR_ID, ATTR_PATHS, ATTR_SMOOTH, Item, List}; use core_types::render_complexity::RenderComplexity; -use dyn_any::StaticType; +use dyn_any::DynAny; use glam::{DAffine2, DVec2}; -use kurbo::{Affine, BezPath, Rect, Shape}; -use std::collections::HashMap; - -/// Represents vector graphics data, composed of Bézier curves in a path or mesh arrangement. -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Vector { - /// A list of all manipulator groups (referenced in `subpaths`) that have colinear handles (where they're locked at 180° angles from one another). - /// This gets read in `graph_operation_message_handler.rs` by calling `inputs.as_mut_slice()` (search for the string `"Shape does not have both `subpath` and `colinear_manipulators` inputs"` to find it). - pub colinear_manipulators: Vec<[HandleId; 2]>, - - pub point_domain: PointDomain, - pub segment_domain: SegmentDomain, -} -unsafe impl StaticType for Vector { - type Static = Self; -} - -impl Default for Vector { - fn default() -> Self { - Self { - colinear_manipulators: Vec::new(), - point_domain: PointDomain::new(), - segment_domain: SegmentDomain::new(), - } - } -} +use kurbo::{Affine, BezPath, PathSeg, Rect, Shape}; -impl graphene_hash::CacheHash for Vector { - fn cache_hash(&self, state: &mut H) { - self.point_domain.cache_hash(state); - self.segment_domain.cache_hash(state); - self.colinear_manipulators.cache_hash(state); - } -} +/// Vector graphics: a pool of control points, anchors and handles alike, that paths weave into curves and meshes. +/// The weave rides the holder as a `List` under `ATTR_PATHS`, and per-point data such as the stable `ATTR_ID` rides this pool's own attributes. +#[derive(Clone, Debug, Default, PartialEq, graphene_hash::CacheHash, DynAny)] +pub struct Vector(pub List); impl core_types::ops::FromAnchorPosition for Vector { fn from_anchor_position(position: DVec2) -> Self { - let mut point_domain = PointDomain::new(); - point_domain.push(PointId::ZERO, position); + let mut pool = List::new_from_element(position); + // A lone anchor is named zero, the ID stored document edits address it by + pool.set_attribute(ATTR_ID, 0, PointId::ZERO); - Self { point_domain, ..Default::default() } + Self(pool) } } @@ -60,129 +34,246 @@ impl From for Vector { } } +// Handles are pool points, so transforming the pool carries the whole geometry impl core_types::transform::BakeTransform for Vector { - fn bake_transform(&mut self, transform: &glam::DAffine2) { - for (_, point) in self.point_domain.positions_mut() { - *point = transform.transform_point2(*point); + fn bake_transform(&mut self, transform: &DAffine2) { + self.transform(*transform); + } +} + +impl Vector { + /// The number of points in the pool, counting anchors and handles alike. + pub fn point_count(&self) -> usize { + self.0.len() + } + + /// The pool position at the given index. + pub fn position(&self, index: usize) -> Option { + self.0.element(index).copied() + } + + /// Every pool position in index order. + pub fn positions(&self) -> impl Iterator + '_ { + self.0.iter_element_values().copied() + } + + /// The stored ID of the pool point at the given index, `PointId::UNNAMED` for a handle. + pub fn point_id_at(&self, index: u32) -> Option { + addressing::point_id_at(&self.0, index) + } + + /// The pool index a point ID names, by scanning the ID attribute in O(points). + pub fn index_of_point_id(&self, point: PointId) -> Option { + if point == PointId::UNNAMED { + return None; + } + + addressing::index_of_point_id(&self.0, point) + } + + /// Applies an affine transform to every pool point. + pub fn transform(&mut self, transform: DAffine2) { + for position in self.0.iter_element_values_mut() { + *position = transform.transform_point2(*position); } - self.segment_domain.transform(*transform); } + + /// The reverse index answering which paths meet at each pool point, built in O(points + references). + pub fn connectivity(&self, paths: &List) -> Connectivity { + Connectivity::build(self.0.len(), paths) + } +} + +/// The ID continuing the pool's point ID sequence. +fn next_point_id(pool: &List) -> PointId { + let ids = pool.iter_attribute_values::(ATTR_ID); + ids.and_then(|ids| ids.copied().filter(|id| *id != PointId::UNNAMED).max()) + .map(|mut id| id.next_id()) + .unwrap_or(PointId::ZERO) +} + +/// The ID continuing the paths' segment ID sequence. +fn next_segment_id(paths: &List) -> SegmentId { + let ids = paths + .iter_element_values() + .filter_map(|path| path.0.iter_attribute_values::(ATTR_ID)) + .flatten() + .copied() + .max(); + + ids.map(|mut id| id.next_id()).unwrap_or(SegmentId::ZERO) } impl Vector { - /// Add a path of manipulator groups to this vector path. - pub fn append_manipulator_groups(&mut self, manipulator_groups: &[ManipulatorGroup], closed: bool, preserve_id: bool) { - let mut point_id = self.point_domain.next_id(); - - let handles = |a: &ManipulatorGroup, b: &ManipulatorGroup| match (a.out_handle, b.in_handle) { - (None, None) => BezierHandles::Linear, - (Some(handle), None) | (None, Some(handle)) => BezierHandles::Quadratic { handle }, - (Some(handle_start), Some(handle_end)) => BezierHandles::Cubic { handle_start, handle_end }, - }; - let mut segment_id = self.segment_domain.next_id(); - let mut last_point = None; - let mut first_point = None; - - // Construct a bezier segment from the two manipulators on the subpath. - for pair in manipulator_groups.windows(2) { - let start = last_point.unwrap_or_else(|| { - let id = if preserve_id && !self.point_domain.ids().contains(&pair[0].id) { - pair[0].id - } else { - point_id.next_id() - }; - self.point_domain.push(id, pair[0].anchor); - self.point_domain.ids().len() - 1 - }); - first_point = Some(first_point.unwrap_or(start)); - let end = if preserve_id && !self.point_domain.ids().contains(&pair[1].id) { - pair[1].id + /// Names the anchors and spans appended since the given watermarks, continuing the ID sequences. + /// Handles take `PointId::UNNAMED`. + fn stamp_appended_ids(&mut self, paths: &mut List, first_new_point: usize, first_new_path: usize) { + let mut point_id = next_point_id(&self.0); + let mut segment_id = next_segment_id(paths); + + // Pool order is traversal order, so anchors and free points take IDs in the order they were drawn + let connectivity = Connectivity::build(self.0.len(), paths); + for index in first_new_point..self.0.len() { + let id = if self.is_handle_point(paths, &connectivity, index as u32) { + PointId::UNNAMED } else { point_id.next_id() }; - let end_index = self.point_domain.ids().len(); - self.point_domain.push(end, pair[1].anchor); + self.0.set_attribute(ATTR_ID, index, id); + } - let id = segment_id.next_id(); - self.segment_domain.push(id, start, end_index, handles(&pair[0], &pair[1])); + for path_index in first_new_path..paths.len() { + let curve_type = curve_type_at(paths, path_index); + let Some(path) = paths.element_mut(path_index) else { continue }; - last_point = Some(end_index); + for span in 0..path.segment_count(curve_type) { + path.0.set_attribute(ATTR_ID, span * curve_type.stride(), segment_id.next_id()); + } } + } - if closed && let (Some(last), Some(first), Some(first_id), Some(last_id)) = (manipulator_groups.last(), manipulator_groups.first(), first_point, last_point) { - let id = segment_id.next_id(); - self.segment_domain.push(id, last_id, first_id, handles(last, first)); - } + /// Weaves a bezpath's contours onto this pool and the given paths, naming the new anchors and spans. + pub fn append_bezpath(&mut self, paths: &mut List, bezpath: &BezPath) { + let (first_new_point, first_new_path) = (self.0.len(), paths.len()); + append_bezpath(&mut self.0, paths, bezpath); + self.stamp_appended_ids(paths, first_new_point, first_new_path); } - /// Construct some new vector path from a single [`BezPath`] with an identity transform and black fill. - pub fn from_bezpath(bezpath: BezPath) -> Self { - let mut vector = Self::default(); - vector.append_bezpath(bezpath); - vector + /// Builds a fresh pool and paths from a single bezpath. + pub fn from_bezpath(bezpath: &BezPath) -> (Self, List) { + let (mut vector, mut paths) = (Self::default(), List::new()); + vector.append_bezpath(&mut paths, bezpath); + + (vector, paths) } - /// Compute the bounding boxes of the bezpaths without any transform - pub fn bounding_box_rect(&self) -> Option { - self.bounding_box_with_transform_rect(DAffine2::IDENTITY) + /// Builds a holder item from a single bezpath, its paths riding the item's attributes. + pub fn item_from_bezpath(bezpath: &BezPath) -> Item { + let (vector, paths) = Self::from_bezpath(bezpath); + + Item::new_from_element(vector).with_attribute(ATTR_PATHS, paths) } +} - pub fn close_subpaths(&mut self) { - let segments_to_add: Vec<_> = self - .build_stroke_path_iter() - .filter(|(_, closed)| !closed) - .filter_map(|(manipulator_groups, _)| { - let (first, last) = manipulator_groups.first().zip(manipulator_groups.last())?; - let (start, end) = self.point_domain.resolve_id(first.id).zip(self.point_domain.resolve_id(last.id))?; - Some((start, end)) - }) - .collect(); +impl Vector { + /// Every segment in span order as `(id, segment, start point ID, end point ID)`. + pub fn segment_iter<'a>(&'a self, paths: &'a List) -> impl Iterator + 'a { + addressing::spans(paths).filter_map(|span| { + let path = paths.element(span.path as usize)?; + let segment = path.segment(&self.0, curve_type_at(paths, span.path as usize), span.index as usize)?; + let id = addressing::segment_id_of(paths, span).unwrap_or_default(); + let (start, end) = addressing::span_endpoints(paths, span)?; + + Some((id, segment, self.point_id_at(start).unwrap_or_default(), self.point_id_at(end).unwrap_or_default())) + }) + } + + /// The segment a segment ID names, resolved against the pool. + pub fn segment_from_id(&self, paths: &List, id: SegmentId) -> Option { + let span = addressing::span_of_segment_id(paths, id)?; + paths.element(span.path as usize)?.segment(&self.0, curve_type_at(paths, span.path as usize), span.index as usize) + } + + /// The endpoints and geometry of the segment a segment ID names. + pub fn segment_points_from_id(&self, paths: &List, id: SegmentId) -> Option<(PointId, PointId, PathSeg)> { + let span = addressing::span_of_segment_id(paths, id)?; + let (start, end) = addressing::span_endpoints(paths, span)?; + let segment = paths.element(span.path as usize)?.segment(&self.0, curve_type_at(paths, span.path as usize), span.index as usize)?; + + Some((self.point_id_at(start).unwrap_or_default(), self.point_id_at(end).unwrap_or_default(), segment)) + } + + /// The segment a segment ID names, paired with the pool indices of its endpoints. + pub fn segment_with_pool_endpoints(&self, paths: &List, id: SegmentId) -> Option<(PathSeg, usize, usize)> { + let span = addressing::span_of_segment_id(paths, id)?; + let (start, end) = addressing::span_endpoints(paths, span)?; + let segment = paths.element(span.path as usize)?.segment(&self.0, curve_type_at(paths, span.path as usize), span.index as usize)?; + + Some((segment, start as usize, end as usize)) + } + + /// The ID of the point the segment starts at. + pub fn segment_start_from_id(&self, paths: &List, segment: SegmentId) -> Option { + let span = addressing::span_of_segment_id(paths, segment)?; + self.point_id_at(addressing::span_endpoints(paths, span)?.0) + } + + /// The ID of the point the segment ends at. + pub fn segment_end_from_id(&self, paths: &List, segment: SegmentId) -> Option { + let span = addressing::span_of_segment_id(paths, segment)?; + self.point_id_at(addressing::span_endpoints(paths, span)?.1) + } + + /// Each segment's start point ID, in span order. + pub fn start_point<'a>(&'a self, paths: &'a List) -> impl Iterator + 'a { + addressing::spans(paths).filter_map(|span| self.point_id_at(addressing::span_endpoints(paths, span)?.0)) + } - for (start, end) in segments_to_add { - let segment_id = self.segment_domain.next_id().next_id(); - self.segment_domain.push(segment_id, start, end, BezierHandles::Linear); + /// Each segment's end point ID, in span order. + pub fn end_point<'a>(&'a self, paths: &'a List) -> impl Iterator + 'a { + addressing::spans(paths).filter_map(|span| self.point_id_at(addressing::span_endpoints(paths, span)?.1)) + } + + /// The point at the segment's other end from the given one. + pub fn other_point(&self, paths: &List, segment: SegmentId, current: PointId) -> Option { + let (start, end, _) = self.segment_points_from_id(paths, segment)?; + + match current { + _ if start == end => None, + point if point == start => Some(end), + point if point == end => Some(start), + _ => None, } } - /// Compute the bounding boxes of the subpaths without any transform - pub fn bounding_box(&self) -> Option<[DVec2; 2]> { - self.bounding_box_with_transform_rect(DAffine2::IDENTITY) - .map(|rect| [DVec2::new(rect.x0, rect.y0), DVec2::new(rect.x1, rect.y1)]) + /// Emits the assembled contours, one bezpath per subpath walked. + pub fn stroke_bezpath_iter(&self, paths: &List) -> impl Iterator { + contours(&self.0, paths).into_iter() + } +} + +impl Vector { + /// Compute the bounding box of the contours without any transform. + pub fn bounding_box(&self, paths: &List) -> Option<[DVec2; 2]> { + self.bounding_box_with_transform(paths, DAffine2::IDENTITY) } - /// Compute the bounding boxes of the subpaths with the specified transform - pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> { - self.bounding_box_with_transform_rect(transform) + /// Compute the bounding box of the contours without any transform. + pub fn bounding_box_rect(&self, paths: &List) -> Option { + self.bounding_box_with_transform_rect(paths, DAffine2::IDENTITY) + } + + /// Compute the bounding box of the contours with the specified transform. + pub fn bounding_box_with_transform(&self, paths: &List, transform: DAffine2) -> Option<[DVec2; 2]> { + self.bounding_box_with_transform_rect(paths, transform) .map(|rect| [DVec2::new(rect.x0, rect.y0), DVec2::new(rect.x1, rect.y1)]) } - /// Compute the bounding boxes of the bezpaths with the specified transform - fn bounding_box_with_transform_rect(&self, transform: DAffine2) -> Option { - let combine = |r1: Rect, r2: Rect| r1.union(r2); - self.stroke_bezpath_iter() + fn bounding_box_with_transform_rect(&self, paths: &List, transform: DAffine2) -> Option { + self.stroke_bezpath_iter(paths) .map(|mut bezpath| { bezpath.apply_affine(Affine::new(transform.to_cols_array())); bezpath.bounding_box() }) - .reduce(combine) + .reduce(|r1, r2| r1.union(r2)) } /// Tight bounding box in the supplied transform space, including the actual rendered stroke geometry - /// (caps, joins, miter extensions, dashes). Runs `kurbo::stroke` on each subpath and unions the + /// (caps, joins, miter extensions, dashes). Runs `kurbo::stroke` on each contour and unions the /// resulting fills' bounding boxes. /// - /// Kurbo doesn't natively support stroke alignment — that's a renderer-side compositing trick that + /// Kurbo doesn't natively support stroke alignment, a renderer-side compositing trick that /// only applies to closed paths. For closed paths with non-Center align, we use the `effective_width` /// identity (`Inside` = 0, `Outside` = 2×weight): the renderer masks half of a centered double-width /// stroke, so its AABB matches the unmasked centered stroke's. For open paths the renderer always /// draws a centered `weight`-wide stroke regardless of the align attribute, so we mirror that here. - pub fn stroke_inclusive_bounding_box_with_transform(&self, transform: DAffine2, stroke: Option<&Stroke>) -> Option<[DVec2; 2]> { - let path_bounds = self.bounding_box_with_transform(transform); + pub fn stroke_inclusive_bounding_box_with_transform(&self, paths: &List, transform: DAffine2, stroke: Option<&Stroke>) -> Option<[DVec2; 2]> { + let path_bounds = self.bounding_box_with_transform(paths, transform); let Some(stroke) = stroke else { return path_bounds }; - // Stroke alignment is only honored by the renderer when every subpath is closed; open paths fall + // Stroke alignment is only honored by the renderer when every contour is closed; open paths fall // back to drawing a Center-aligned `weight`-wide stroke. Match that behavior to keep bounds in sync. - let aligned_renders = stroke.align != StrokeAlign::Center && self.stroke_bezpath_iter().all(|path| matches!(path.elements().last(), Some(kurbo::PathEl::ClosePath))); + let aligned_renders = stroke.align != StrokeAlign::Center && self.stroke_bezpath_iter(paths).all(|path| matches!(path.elements().last(), Some(kurbo::PathEl::ClosePath))); let kurbo_width = if aligned_renders { stroke.effective_width() } else { stroke.weight }; // `Inside`-aligned strokes never expand beyond the path bounds; a zero-weight stroke is invisible if kurbo_width <= 0. { @@ -206,7 +297,7 @@ impl Vector { .with_dashes(stroke.dash_offset, stroke.dash_lengths.clone()) .with_miter_limit(stroke.join_miter_limit); let stroke_options = kurbo::StrokeOpts::default(); - // Same tolerance as `solidify_stroke` — balanced between performance and curve accuracy + // Same tolerance as `solidify_stroke`, balanced between performance and curve accuracy const STROKE_TOLERANCE: f64 = 0.25; let stroke_local = Affine::new(stroke.transform.to_cols_array()); @@ -214,7 +305,7 @@ impl Vector { let final_transform = Affine::new(transform.to_cols_array()); let stroke_bounds = self - .stroke_bezpath_iter() + .stroke_bezpath_iter(paths) .map(|mut bezpath| { // Match `solidify_stroke`'s pre/post-stroke transform pair so non-uniform `stroke.transform` // (e.g. resisting layer scale) is applied while kurbo strokes, then undone before placing the @@ -230,7 +321,7 @@ impl Vector { .reduce(|a, b| a.union(b)) .map(|rect| [DVec2::new(rect.x0, rect.y0), DVec2::new(rect.x1, rect.y1)]); - // Union with the path bounds in case a degenerate subpath (e.g. a single point) produced no stroke geometry + // Union with the path bounds in case a degenerate contour (e.g. a single point) produced no stroke geometry match (path_bounds, stroke_bounds) { (Some([min_a, max_a]), Some([min_b, max_b])) => Some([min_a.min(min_b), max_a.max(max_b)]), (Some(b), None) | (None, Some(b)) => Some(b), @@ -241,8 +332,8 @@ impl Vector { /// Calculate the corners of the bounding box but with a nonzero size. /// /// If the layer bounds are `0` in either axis then they are changed to be `1`. - pub fn nonzero_bounding_box(&self) -> [DVec2; 2] { - let [bounds_min, mut bounds_max] = self.bounding_box().unwrap_or_default(); + pub fn nonzero_bounding_box(&self, paths: &List) -> [DVec2; 2] { + let [bounds_min, mut bounds_max] = self.bounding_box(paths).unwrap_or_default(); let bounds_size = bounds_max - bounds_min; if bounds_size.x < 1e-10 { @@ -255,213 +346,435 @@ impl Vector { [bounds_min, bounds_max] } - pub fn start_point(&self) -> impl Iterator + '_ { - self.segment_domain.start_point().iter().map(|&index| self.point_domain.ids()[index]) + pub fn check_point_inside_shape(&self, paths: &List, transform: DAffine2, point: DVec2) -> bool { + let number = self + .stroke_bezpath_iter(paths) + .map(|mut bezpath| { + bezpath.apply_affine(Affine::new(transform.to_cols_array())); + bezpath.close_path(); + let bbox = bezpath.bounding_box(); + (bezpath, bbox) + }) + .filter(|(_, bbox)| bbox.contains(dvec2_to_point(point))) + .map(|(bezpath, _)| bezpath.winding(dvec2_to_point(point))) + .sum::(); + + // Non-zero fill rule + number != 0 } +} - pub fn end_point(&self) -> impl Iterator + '_ { - self.segment_domain.end_point().iter().map(|&index| self.point_domain.ids()[index]) +impl Vector { + /// Whether the pool point at the given index is a handle, referenced only between anchors. + pub(crate) fn is_handle_point(&self, paths: &List, connectivity: &Connectivity, index: u32) -> bool { + let references = connectivity.references(index); + + !references.is_empty() + && references + .iter() + .all(|reference| !matches!(paths.element(reference.path as usize), Some(path) if path.is_anchor_reference(reference.index as usize, curve_type_at(paths, reference.path as usize)))) } - pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: (Option, Option)) { - let [Some(start), Some(end)] = [start, end].map(|id| self.point_domain.resolve_id(id)) else { - return; - }; - let handles = match handles { - (None, None) => BezierHandles::Linear, - (None, Some(handle)) | (Some(handle), None) => BezierHandles::Quadratic { handle }, - (Some(handle_start), Some(handle_end)) => BezierHandles::Cubic { handle_start, handle_end }, - }; - self.segment_domain.push(id, start, end, handles) + /// The pool indices of all anchor points and free points in pool order, skipping handles. + pub fn anchor_indices(&self, paths: &List) -> Vec { + let connectivity = self.connectivity(paths); + (0..self.0.len() as u32) + .filter(|&index| !self.is_handle_point(paths, &connectivity, index)) + .map(|index| index as usize) + .collect() } - pub fn handles_mut(&mut self) -> impl Iterator { - self.segment_domain - .handles_mut() - .map(|(id, handles, start, end)| (id, handles, self.point_domain.ids()[start], self.point_domain.ids()[end])) + /// The positions of all anchor points and free points in pool order, skipping handles. + pub fn anchor_positions(&self, paths: &List) -> Vec { + self.anchor_indices(paths).into_iter().filter_map(|index| self.position(index)).collect() } - pub fn segment_start_from_id(&self, segment: SegmentId) -> Option { - self.segment_domain.segment_start_from_id(segment).map(|index| self.point_domain.ids()[index]) + /// Iterator over the IDs of all anchor points and free points, skipping handles. + pub fn anchor_points(&self, paths: &List) -> impl Iterator { + let connectivity = self.connectivity(paths); + let anchors: Vec<_> = (0..self.0.len() as u32) + .filter(|&index| !self.is_handle_point(paths, &connectivity, index)) + .filter_map(|index| self.point_id_at(index)) + .collect(); + + anchors.into_iter() } - pub fn segment_end_from_id(&self, segment: SegmentId) -> Option { - self.segment_domain.segment_end_from_id(segment).map(|index| self.point_domain.ids()[index]) + /// Anchor points at the ends of open chains, where exactly one segment end meets the point. + pub fn anchor_endpoints(&self, paths: &List) -> impl Iterator { + let connectivity = self.connectivity(paths); + let endpoints: Vec<_> = (0..self.0.len() as u32) + .filter(|&index| connectivity.spokes(index, paths) == 1) + .filter_map(|index| self.point_id_at(index)) + .collect(); + + endpoints.into_iter() + } + + // Each per-point query below builds its own connectivity index, O(points + references) with two allocations. + // Callers looping over many points should hoist `connectivity()` and use `addressing` directly. + + /// Enumerate all segments that start at the point. + pub fn start_connected(&self, paths: &List, point: PointId) -> impl Iterator { + let connected = self + .index_of_point_id(point) + .map(|index| addressing::start_connected(index, paths, &self.connectivity(paths))) + .unwrap_or_default(); + + connected.into_iter() + } + + /// Enumerate all segments that end at the point. + pub fn end_connected(&self, paths: &List, point: PointId) -> impl Iterator { + let connected = self + .index_of_point_id(point) + .map(|index| addressing::end_connected(index, paths, &self.connectivity(paths))) + .unwrap_or_default(); + + connected.into_iter() + } + + /// Enumerate all segments that start or end at a point, converting them to [`HandleId`]s. Note that the handles may not exist e.g. for a linear segment. + pub fn all_connected(&self, paths: &List, point: PointId) -> impl Iterator { + let connected = self + .index_of_point_id(point) + .map(|index| addressing::all_connected(index, paths, &self.connectivity(paths))) + .unwrap_or_default(); + + connected.into_iter() + } + + /// The number of segment ends meeting at a point. A segment that starts and ends there is counted twice. + pub fn connected_count(&self, paths: &List, point: PointId) -> usize { + let Some(index) = self.index_of_point_id(point) else { return 0 }; + + self.connectivity(paths).spokes(index, paths) } - /// Attempts to find another point in the segment that is not the one passed in. - pub fn other_point(&self, segment: SegmentId, current: PointId) -> Option { - let index = self.point_domain.resolve_id(current); - index.and_then(|index| self.segment_domain.other_point(segment, index)).map(|index| self.point_domain.ids()[index]) + /// Whether any segment starts or ends at the point. + pub fn any_connected(&self, paths: &List, point: PointId) -> bool { + self.connected_count(paths, point) > 0 } /// Gets all points connected to the current one but not including the current one. - pub fn connected_points(&self, current: PointId) -> impl Iterator + '_ { - let index = [self.point_domain.resolve_id(current)].into_iter().flatten(); - index.flat_map(|index| self.segment_domain.connected_points(index).map(|index| self.point_domain.ids()[index])) + pub fn connected_points(&self, paths: &List, current: PointId) -> impl Iterator { + let mut found = Vec::new(); + if let Some(index) = self.index_of_point_id(current) { + for (span, arriving) in addressing::spans_at(index, paths, &self.connectivity(paths)) { + let Some((start, end)) = addressing::span_endpoints(paths, span) else { continue }; + if start == end { + continue; + } + + let other = if arriving { start } else { end }; + if let Some(id) = self.point_id_at(other) { + found.push(id); + } + } + } + + found.into_iter() } /// Returns the number of linear segments connected to the given point. - pub fn connected_linear_segments(&self, point_id: PointId) -> usize { - self.segment_iter() - .filter(|(_, segment, start, end)| (*start == point_id || *end == point_id) && matches!(segment, kurbo::PathSeg::Line(_))) + pub fn connected_linear_segments(&self, paths: &List, point_id: PointId) -> usize { + self.segment_iter(paths) + .filter(|(_, segment, start, end)| (*start == point_id || *end == point_id) && matches!(segment, PathSeg::Line(_))) .count() } +} - /// Get an array slice of all segment IDs. - pub fn segment_ids(&self) -> &[SegmentId] { - self.segment_domain.ids() - } +impl Vector { + /// Whether the junction at the given anchor keeps its tangent pair smooth, read from any of its occurrences. + fn smooth_at_anchor_index(&self, paths: &List, connectivity: &Connectivity, index: u32) -> bool { + connectivity.references(index).iter().any(|reference| { + let Some(path) = paths.element(reference.path as usize) else { return false }; + if !path.is_anchor_reference(reference.index as usize, curve_type_at(paths, reference.path as usize)) { + return false; + } - /// Enumerate all segments that start at the point. - pub fn start_connected(&self, point: PointId) -> impl Iterator + '_ { - let index = [self.point_domain.resolve_id(point)].into_iter().flatten(); - index.flat_map(|index| self.segment_domain.start_connected(index)) + path.0.attribute::(ATTR_SMOOTH, reference.index as usize).copied().unwrap_or_default() + }) } - /// Enumerate all segments that end at the point. - pub fn end_connected(&self, point: PointId) -> impl Iterator + '_ { - let index = [self.point_domain.resolve_id(point)].into_iter().flatten(); - index.flat_map(|index| self.segment_domain.end_connected(index)) + /// Whether the anchor at the given pool index forms a smooth tangent pair, which needs exactly two + /// segment ends meeting there. A junction has no unambiguous pair to keep smooth. + fn pairs_smoothly(&self, paths: &List, connectivity: &Connectivity, index: u32) -> bool { + self.smooth_at_anchor_index(paths, connectivity, index) && connectivity.spokes(index, paths) == 2 } - /// Enumerate all segments that start or end at a point, converting them to [`HandleId`s]. Note that the handles may not exist e.g. for a linear segment. - pub fn all_connected(&self, point: PointId) -> impl Iterator + '_ { - let index = [self.point_domain.resolve_id(point)].into_iter().flatten(); - index.flat_map(|index| self.segment_domain.all_connected(index)) + /// Whether the anchor (or the handle's anchor) keeps a smooth tangent pair, which needs exactly two segment ends meeting there. + pub fn colinear(&self, paths: &List, point: ManipulatorPointId) -> bool { + let connectivity = self.connectivity(paths); + let handle_anchor_smooth = |segment: SegmentId, at_start: bool| { + let Some(span) = addressing::span_of_segment_id(paths, segment) else { return false }; + let Some((start, end)) = addressing::span_endpoints(paths, span) else { return false }; + + self.pairs_smoothly(paths, &connectivity, if at_start { start } else { end }) + }; + + match point { + ManipulatorPointId::Anchor(id) => match self.index_of_point_id(id) { + Some(index) => self.pairs_smoothly(paths, &connectivity, index), + None => false, + }, + ManipulatorPointId::PrimaryHandle(segment) => handle_anchor_smooth(segment, true), + ManipulatorPointId::EndHandle(segment) => handle_anchor_smooth(segment, false), + } } - /// Enumerate the number of segments connected to a point. If a segment starts and ends at a point then it is counted twice. - pub fn connected_count(&self, point: PointId) -> usize { - self.point_domain.resolve_id(point).map_or(0, |point| self.segment_domain.connected_count(point)) + /// Every G1 pair: the two facing handles at each smooth anchor where exactly two segment ends meet. + pub fn colinear_pairs(&self, paths: &List) -> Vec<[HandleId; 2]> { + let connectivity = self.connectivity(paths); + let mut pairs = Vec::new(); + + for index in 0..self.point_count() as u32 { + if connectivity.spokes(index, paths) != 2 || !self.smooth_at_anchor_index(paths, &connectivity, index) { + continue; + } + + let handles: Vec = addressing::spans_at(index, paths, &connectivity) + .into_iter() + .filter_map(|(span, arriving)| { + let segment = addressing::segment_id_of(paths, span)?; + Some(if arriving { HandleId::end(segment) } else { HandleId::primary(segment) }) + }) + .collect(); + if let [first, second] = handles.as_slice() { + pairs.push([*first, *second]); + } + } + + pairs } - /// Enumerate the number of segments connected to a point. If a segment starts and ends at a point then it is counted twice. - pub fn any_connected(&self, point: PointId) -> bool { - self.point_domain.resolve_id(point).is_some_and(|point| self.segment_domain.any_connected(point)) + /// The G1 pair the given handle belongs to, ordered with that handle first. + pub fn colinear_pair_containing(&self, paths: &List, handle: HandleId) -> Option<[HandleId; 2]> { + self.other_colinear_handle(paths, handle).map(|other| [handle, other]) } - pub fn check_point_inside_shape(&self, transform: DAffine2, point: DVec2) -> bool { - let number = self - .stroke_bezpath_iter() - .map(|mut bezpath| { - // TODO: apply transform to points instead of modifying the paths - bezpath.apply_affine(Affine::new(transform.to_cols_array())); - bezpath.close_path(); - let bbox = bezpath.bounding_box(); - (bezpath, bbox) - }) - .filter(|(_, bbox)| bbox.contains(dvec2_to_point(point))) - .map(|(bezpath, _)| bezpath.winding(dvec2_to_point(point))) - .sum::(); + /// The handle paired with the given one across their shared smooth anchor, where exactly two segment ends meet. + pub fn other_colinear_handle(&self, paths: &List, handle: HandleId) -> Option { + let span = addressing::span_of_segment_id(paths, handle.segment)?; + let (start, end) = addressing::span_endpoints(paths, span)?; + let anchor = if handle.ty == super::misc::HandleType::Primary { start } else { end }; - // Non-zero fill rule - number != 0 + let connectivity = self.connectivity(paths); + if !self.pairs_smoothly(paths, &connectivity, anchor) { + return None; + } + + addressing::spans_at(anchor, paths, &connectivity) + .into_iter() + .filter(|&(other, arriving)| (other, arriving) != (span, handle.ty != super::misc::HandleType::Primary)) + .find_map(|(other, arriving)| { + let segment = addressing::segment_id_of(paths, other)?; + Some(if arriving { HandleId::end(segment) } else { HandleId::primary(segment) }) + }) } - /// Iterator over all anchor points. - pub fn anchor_points(&self) -> impl Iterator + '_ { - self.point_domain.ids().iter().copied() + /// Sets whether the junction at the given anchor keeps its tangent pair smooth, written to every occurrence. + pub fn set_smooth(&mut self, paths: &mut List, point: PointId, smooth: bool) { + let Some(index) = self.index_of_point_id(point) else { return }; + + self.set_smooth_at_indices(paths, [index], smooth); } - /// Anchor points at the ends of open subpaths. These are points with exactly one connection by a segment to another anchor. - pub fn anchor_endpoints(&self) -> impl Iterator + '_ { - // O(points + segments): tally every point's connections in a single pass - let mut connected_counts = vec![0_usize; self.point_domain.ids().len()]; - for &point_index in self.segment_domain.start_point().iter().chain(self.segment_domain.end_point()) { - if let Some(count) = connected_counts.get_mut(point_index) { - *count += 1; + /// Sets the smooth flag on many anchors at once, reading one connectivity rather than one per anchor. + pub(crate) fn set_smooth_at_indices(&mut self, paths: &mut List, indices: impl IntoIterator, smooth: bool) { + let mut indices = indices.into_iter().peekable(); + if indices.peek().is_none() { + return; + } + + let connectivity = self.connectivity(paths); + for index in indices { + for reference in connectivity.references(index) { + let curve_type = curve_type_at(paths, reference.path as usize); + let Some(path) = paths.element_mut(reference.path as usize) else { continue }; + if path.is_anchor_reference(reference.index as usize, curve_type) { + path.0.set_attribute(ATTR_SMOOTH, reference.index as usize, smooth); + } } } + } - self.anchor_points().zip(connected_counts).filter(|&(_, count)| count == 1).map(|(id, _)| id) + /// The neighboring segment sharing the anchor the given handle sits on. + pub fn adjacent_segment(&self, paths: &List, manipulator_id: &ManipulatorPointId) -> Option<(PointId, SegmentId)> { + let (segment_id, at_start) = match manipulator_id { + ManipulatorPointId::PrimaryHandle(segment_id) => (*segment_id, true), + ManipulatorPointId::EndHandle(segment_id) => (*segment_id, false), + ManipulatorPointId::Anchor(_) => return None, + }; + + let span = addressing::span_of_segment_id(paths, segment_id)?; + let (start, end) = addressing::span_endpoints(paths, span)?; + let anchor = if at_start { start } else { end }; + let anchor_id = self.point_id_at(anchor)?; + + // Prefer the segment meeting this one head-on, since a smooth tangent pairs across the anchor + let occurrences = addressing::spans_at(anchor, paths, &self.connectivity(paths)); + let other = |wanted_arriving: bool| { + occurrences.iter().find_map(|&(other, arriving)| { + let id = addressing::segment_id_of(paths, other)?; + (id != segment_id && arriving == wanted_arriving).then_some(id) + }) + }; + + other(at_start).or_else(|| other(!at_start)).map(|id| (anchor_id, id)) } +} - /// Computes if all the connected handles are colinear for an anchor, or if that handle is colinear for a handle. - pub fn colinear(&self, point: ManipulatorPointId) -> bool { - let has_handle = |target| self.colinear_manipulators.iter().flatten().any(|&handle| handle == target); - match point { - ManipulatorPointId::Anchor(id) => { - self.start_connected(id).all(|segment| has_handle(HandleId::primary(segment))) && self.end_connected(id).all(|segment| has_handle(HandleId::end(segment))) - } - ManipulatorPointId::PrimaryHandle(segment) => has_handle(HandleId::primary(segment)), - ManipulatorPointId::EndHandle(segment) => has_handle(HandleId::end(segment)), +impl Vector { + /// Appends a lone segment between two existing points, its handles becoming new unnamed pool points. + pub fn push(&mut self, paths: &mut List, id: SegmentId, start: PointId, end: PointId, handles: (Option, Option)) { + let [Some(start), Some(end)] = [start, end].map(|point| self.index_of_point_id(point)) else { + return; + }; + + let (curve_type, handle_positions) = match handles { + (None, None) => (CurveType::Linear, Vec::new()), + (None, Some(handle)) | (Some(handle), None) => (CurveType::Quadratic, vec![handle]), + (Some(handle_start), Some(handle_end)) => (CurveType::Cubic, vec![handle_start, handle_end]), + }; + + let mut references = vec![start]; + for position in handle_positions { + references.push(self.0.len() as u32); + self.0.push(Item::new_from_element(position).with_attribute(ATTR_ID, PointId::UNNAMED)); } + references.push(end); + + push_path(paths, curve_type, references, [id]); } - pub fn other_colinear_handle(&self, handle: HandleId) -> Option { - let pair = self.colinear_manipulators.iter().find(|pair| pair.contains(&handle))?; - let other = pair.iter().copied().find(|&val| val != handle)?; - if handle.to_manipulator_point().get_anchor(self) == other.to_manipulator_point().get_anchor(self) { - Some(other) - } else { - None + /// Welds every open subpath shut with a linear segment from its first point to its last. + pub fn close_subpaths(&mut self, paths: &mut List) { + let closures: Vec<_> = subpaths(self.point_count(), paths) + .into_iter() + .filter(|subpath| !subpath.closed) + .map(|subpath| (subpath.entry, subpath.exit)) + .collect(); + + for (entry, exit) in closures { + let mut base = next_segment_id(paths); + let id = base.next_id(); + + push_path(paths, CurveType::Linear, [entry, exit], [id]); } } - pub fn adjacent_segment(&self, manipulator_id: &ManipulatorPointId) -> Option<(PointId, SegmentId)> { - match manipulator_id { - ManipulatorPointId::PrimaryHandle(segment_id) => { - // For start handle, find segments ending at our start point - let (start_point_id, _, _) = self.segment_points_from_id(*segment_id)?; - let start_index = self.point_domain.resolve_id(start_point_id)?; + /// Appends another vector's points and paths, remapping IDs that collide with this vector's. + pub fn concat(&mut self, paths: &mut List, additional: &Self, additional_paths: &List, transform_of_additional: DAffine2, collision_hash_seed: u64) { + let offset = self.0.len() as u32; + + // Remap colliding stable IDs so both sides stay addressable after the merge + let own_point_ids: std::collections::HashSet<_> = self.0.iter_attribute_values::(ATTR_ID).into_iter().flatten().copied().collect(); + let own_segment_ids: std::collections::HashSet<_> = paths + .iter_element_values() + .filter_map(|path| path.0.iter_attribute_values::(ATTR_ID)) + .flatten() + .copied() + .collect(); + + for index in 0..additional.0.len() { + let Some(&position) = additional.0.element(index) else { continue }; + let mut item = Item::new_from_element(transform_of_additional.transform_point2(position)); - self.segment_domain.end_connected(start_index).find(|&id| id != *segment_id).map(|id| (start_point_id, id)).or(self - .segment_domain - .start_connected(start_index) - .find(|&id| id != *segment_id) - .map(|id| (start_point_id, id))) + if let Some(&id) = additional.0.attribute::(ATTR_ID, index) { + let id = if id != PointId::UNNAMED && own_point_ids.contains(&id) { + id.generate_from_hash(collision_hash_seed) + } else { + id + }; + item.set_attribute(ATTR_ID, id); } - ManipulatorPointId::EndHandle(segment_id) => { - // For end handle, find segments starting at our end point - let (_, end_point_id, _) = self.segment_points_from_id(*segment_id)?; - let end_index = self.point_domain.resolve_id(end_point_id)?; - - self.segment_domain.start_connected(end_index).find(|&id| id != *segment_id).map(|id| (end_point_id, id)).or(self - .segment_domain - .end_connected(end_index) - .find(|&id| id != *segment_id) - .map(|id| (end_point_id, id))) + + self.0.push(item); + } + + for index in 0..additional_paths.len() { + let Some(mut item) = additional_paths.clone_item(index) else { continue }; + + let path = item.element_mut(); + for reference in path.0.iter_element_values_mut() { + *reference += i64::from(offset); } - ManipulatorPointId::Anchor(_) => None, + if let Some(ids) = path.0.iter_attribute_values_mut::(ATTR_ID) { + for id in ids { + if *id != SegmentId::ZERO && own_segment_ids.contains(id) { + *id = id.generate_from_hash(collision_hash_seed); + } + } + } + + paths.push(item); } } - pub fn concat(&mut self, additional: &Self, transform_of_additional: DAffine2, collision_hash_seed: u64) { - let point_map = additional - .point_domain - .ids() - .iter() - .filter(|id| self.point_domain.ids().contains(id)) - .map(|&old| (old, old.generate_from_hash(collision_hash_seed))) - .collect::>(); - - let segment_map = additional - .segment_domain - .ids() - .iter() - .filter(|id| self.segment_domain.ids().contains(id)) - .map(|&old| (old, old.generate_from_hash(collision_hash_seed))) - .collect::>(); - - let id_map = IdMap { - point_offset: self.point_domain.ids().len(), - point_map, - segment_map, - }; - - self.point_domain.concat(&additional.point_domain, transform_of_additional, &id_map); - self.segment_domain.concat(&additional.segment_domain, transform_of_additional, &id_map); + /// Rewrites every stable ID deterministically from the given hash, so instanced copies stay distinct. + pub fn vector_new_ids_from_hash(&mut self, paths: &mut List, node_id: u64) { + if let Some(ids) = self.0.iter_attribute_values_mut::(ATTR_ID) { + for id in ids.filter(|id| **id != PointId::UNNAMED) { + *id = id.generate_from_hash(node_id); + } + } - self.colinear_manipulators.extend(additional.colinear_manipulators.iter().copied()); + for index in 0..paths.len() { + let Some(path) = paths.element_mut(index) else { continue }; + if let Some(ids) = path.0.iter_attribute_values_mut::(ATTR_ID) { + for id in ids.filter(|id| **id != SegmentId::ZERO) { + *id = id.generate_from_hash(node_id); + } + } + } } } -// The element sees only geometry; stroke inflation is applied at the row level by `vector_list_bounding_box` -// in graphic-types, which can read the appearance attribute the stroke parameters live on. +/// Edits a holder item's pool and paths together, taking the paths attribute out and restoring it. +pub fn edit_item_geometry(item: &mut Item, edit: impl FnOnce(&mut Vector, &mut List) -> R) -> R { + let mut paths = item.remove_attribute::>(ATTR_PATHS).unwrap_or_default(); + let result = edit(item.element_mut(), &mut paths); + item.set_attribute(ATTR_PATHS, paths); + + result +} + +/// Edits one list row's pool and paths together, writing the paths attribute back afterwards. +pub fn edit_list_row_geometry(list: &mut List, index: usize, edit: impl FnOnce(&mut Vector, &mut List) -> R) -> Option { + let mut paths = list.attribute::>(ATTR_PATHS, index).cloned().unwrap_or_default(); + let result = edit(list.element_mut(index)?, &mut paths); + list.set_attribute(ATTR_PATHS, index, paths); + + Some(result) +} + +// Alone, the element sees only its own pool, and the pool's control points always contain the drawn geometry. impl BoundingBox for Vector { + fn bounding_box_in_item(item: &Item, transform: DAffine2, _include_stroke: bool, _thumbnail: bool) -> RenderBoundingBox { + let paths = paths_from_item_attributes(item).unwrap_or_else(|| no_paths()); + item.element() + .bounding_box_with_transform(paths, transform) + .map_or(RenderBoundingBox::None, RenderBoundingBox::Rectangle) + } + + fn bounding_box_in_list(list: &List, index: usize, transform: DAffine2, _include_stroke: bool, _thumbnail: bool) -> RenderBoundingBox { + let Some(vector) = list.element(index) else { return RenderBoundingBox::None }; + let paths = paths_from_list_row_attributes(list, index).unwrap_or_else(|| no_paths()); + vector.bounding_box_with_transform(paths, transform).map_or(RenderBoundingBox::None, RenderBoundingBox::Rectangle) + } + fn bounding_box(&self, transform: DAffine2, _include_stroke: bool) -> RenderBoundingBox { - match self.bounding_box_with_transform(transform) { + let mut bounds: Option<[DVec2; 2]> = None; + for position in self.positions() { + let position = transform.transform_point2(position); + bounds = Some(match bounds { + Some([min, max]) => [min.min(position), max.max(position)], + None => [position, position], + }); + } + + match bounds { Some(bounds) => RenderBoundingBox::Rectangle(bounds), None => RenderBoundingBox::None, } @@ -473,19 +786,29 @@ impl BoundingBox for Vector { } impl RenderComplexity for Vector { - fn render_complexity(&self) -> usize { - self.segment_domain.ids().len() + fn render_complexity_in_item(item: &Item) -> usize { + segment_count(paths_from_item_attributes(item)) + } + + fn render_complexity_in_list(list: &List, index: usize) -> usize { + segment_count(paths_from_list_row_attributes(list, index)) } } -// Note: BoundingBox for List is handled by blanket impl in gcore +/// How many segments the paths draw, which is what their vector costs to render. +fn segment_count(paths: Option<&List>) -> usize { + let Some(paths) = paths else { return 0 }; + + paths.iter_element_values().enumerate().map(|(index, path)| path.segment_count(curve_type_at(paths, index))).sum() +} + +// Note: BoundingBox for List is handled by blanket impl in core-types #[cfg(test)] mod tests { - use crate::vector::algorithms::shapes::ellipse_bezpath; - use kurbo::{CubicBez, PathSeg, Point}; - use super::*; + use crate::vector::algorithms::shapes::ellipse_bezpath; + use kurbo::{CubicBez, PathEl, Point}; fn open_curve_bezpath() -> BezPath { let mut bezpath = BezPath::new(); @@ -494,17 +817,62 @@ mod tests { bezpath } + fn lone_anchor(position: DVec2) -> Vector { + ::from_anchor_position(position) + } + + // A layer is too dense for a thumbnail by the segments it draws, whatever handles those bring to the pool + #[test] + fn render_complexity_counts_segments() { + let (vector, paths) = Vector::from_bezpath(&ellipse_bezpath(DVec2::NEG_ONE, DVec2::ONE)); + assert_eq!(vector.0.len(), 12); + + let item = Item::new_from_element(vector).with_attribute(ATTR_PATHS, paths); + assert_eq!(item.render_complexity(), 4); + assert_eq!(List::new_from_item(item).render_complexity(), 4); + } + + // A lone anchor is named zero and only the later collisions are renamed, so stored edits keyed on zero still land + #[test] + fn lone_anchor_is_named_zero_and_collisions_rename_from_it() { + let (mut vector, mut paths) = (lone_anchor(DVec2::ZERO), List::new()); + assert_eq!(vector.point_id_at(0), Some(PointId::ZERO)); + + vector.concat(&mut paths, &lone_anchor(DVec2::X), &List::new(), DAffine2::IDENTITY, 7); + vector.concat(&mut paths, &lone_anchor(DVec2::Y), &List::new(), DAffine2::IDENTITY, 8); + + assert_eq!(vector.point_id_at(0), Some(PointId::ZERO)); + assert_eq!(vector.point_id_at(1), Some(PointId::ZERO.generate_from_hash(7))); + assert_eq!(vector.point_id_at(2), Some(PointId::ZERO.generate_from_hash(8))); + assert_eq!(vector.index_of_point_id(PointId::ZERO), Some(0)); + } + + // Handles carry the unnamed filler, so a zero-named anchor behind them is still the one found by ID + #[test] + fn handles_do_not_shadow_a_zero_named_anchor() { + let (mut vector, mut paths) = Vector::from_bezpath(&open_curve_bezpath()); + assert_eq!(vector.point_id_at(1), Some(PointId::UNNAMED)); + assert_eq!(vector.point_id_at(2), Some(PointId::UNNAMED)); + + vector.concat(&mut paths, &lone_anchor(DVec2::new(5., 5.)), &List::new(), DAffine2::IDENTITY, 1); + + let index = vector.index_of_point_id(PointId::ZERO).unwrap(); + assert_eq!(vector.position(index as usize), Some(DVec2::new(5., 5.))); + assert!(vector.anchor_indices(&paths).contains(&(index as usize))); + assert_eq!(next_point_id(&vector.0).inner(), 3); + } + #[test] fn construct_closed_path() { let circle = ellipse_bezpath(DVec2::NEG_ONE, DVec2::ONE); - let vector = Vector::from_bezpath(circle.clone()); - assert_eq!(vector.point_domain.ids().len(), 4); + let (vector, paths) = Vector::from_bezpath(&circle); + assert_eq!(vector.anchor_points(&paths).count(), 4); - let segments = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::>(); + let segments = vector.segment_iter(&paths).map(|(_, segment, _, _)| segment).collect::>(); assert_eq!(segments.len(), 4); assert!(segments.iter().all(|&segment| circle.segments().any(|original| original == segment))); - let generated = vector.stroke_bezpath_iter().collect::>(); + let generated = vector.stroke_bezpath_iter(&paths).collect::>(); assert_eq!(generated.len(), 1); assert_eq!(generated[0].elements(), circle.elements()); } @@ -512,19 +880,11 @@ mod tests { #[test] fn construct_open_path() { let curve = open_curve_bezpath(); - let vector = Vector::from_bezpath(curve.clone()); - assert_eq!(vector.point_domain.ids().len(), 2); + let (vector, paths) = Vector::from_bezpath(&curve); + assert_eq!(vector.anchor_points(&paths).count(), 2); - let segments = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::>(); + let segments = vector.segment_iter(&paths).map(|(_, segment, _, _)| segment).collect::>(); assert_eq!(segments, vec![PathSeg::Cubic(CubicBez::new(Point::ZERO, Point::new(-1., -1.), Point::new(1., 1.), Point::new(1., 0.)))]); - - let generated = vector.stroke_manipulator_groups().collect::>(); - assert_eq!(generated.len(), 1); - let (groups, closed) = &generated[0]; - assert!(!closed); - assert_eq!(groups.len(), 2); - assert_eq!((groups[0].anchor, groups[0].in_handle, groups[0].out_handle), (DVec2::ZERO, None, Some(DVec2::new(-1., -1.)))); - assert_eq!((groups[1].anchor, groups[1].in_handle, groups[1].out_handle), (DVec2::new(1., 0.), Some(DVec2::new(1., 1.)), None)); } #[test] @@ -532,30 +892,69 @@ mod tests { let curve = open_curve_bezpath(); let circle = ellipse_bezpath(DVec2::NEG_ONE, DVec2::ONE); - let mut vector = Vector::from_bezpath(curve.clone()); - vector.append_bezpath(circle.clone()); - assert_eq!(vector.point_domain.ids().len(), 6); + let (mut vector, mut paths) = Vector::from_bezpath(&curve); + vector.append_bezpath(&mut paths, &circle); + assert_eq!(vector.anchor_points(&paths).count(), 6); - let segments = vector.segment_iter().map(|(_, bezier, _, _)| bezier).collect::>(); + let segments = vector.segment_iter(&paths).map(|(_, segment, _, _)| segment).collect::>(); assert_eq!(segments.len(), 5); assert!(segments.iter().all(|&segment| circle.segments().chain(curve.segments()).any(|original| original == segment))); - let generated = vector.stroke_bezpath_iter().collect::>(); + let generated = vector.stroke_bezpath_iter(&paths).collect::>(); assert_eq!(generated.len(), 2); assert_eq!(generated[0].elements(), curve.elements()); assert_eq!(generated[1].elements(), circle.elements()); } - // Verifies the `DVec2 -> List` conversion that replaced the former "Vec2 to Point" node yields a path - // with exactly one anchor point at the given position and no segments + // Stored modifications name these exact IDs, so regeneration must reproduce them: anchors count up from one while handles stay unnamed + #[test] + fn appended_ids_stay_reproducible_for_stored_documents() { + let curve = open_curve_bezpath(); + let (mut vector, mut paths) = Vector::from_bezpath(&curve); + + let point_id = |vector: &Vector, index| vector.point_id_at(index).map(|id| id.inner()); + assert_eq!(point_id(&vector, 0), Some(1)); + assert_eq!(vector.point_id_at(1), Some(PointId::UNNAMED), "handles are unnamed"); + assert_eq!(vector.point_id_at(2), Some(PointId::UNNAMED)); + assert_eq!(point_id(&vector, 3), Some(2)); + + let segment_ids: Vec<_> = vector.segment_iter(&paths).map(|(id, ..)| id.inner()).collect(); + assert_eq!(segment_ids, vec![1]); + + // A second append rescans for the maximum and skips one value + vector.append_bezpath(&mut paths, &open_curve_bezpath()); + assert_eq!(point_id(&vector, 4), Some(4)); + assert_eq!(point_id(&vector, 7), Some(5)); + } + + // A lone anchor position builds one named pool point #[test] fn anchor_position_builds_single_point_path() { use core_types::ops::FromAnchorPosition; let vector = Vector::from_anchor_position(DVec2::new(3., 4.)); - assert_eq!(vector.point_domain.positions(), [DVec2::new(3., 4.)]); - assert_eq!(vector.point_domain.ids(), [PointId::ZERO], "expected the anchor position to be assigned the default PointId::ZERO",); - assert!(vector.segment_domain.ids().is_empty()); + assert_eq!(vector.positions().collect::>(), [DVec2::new(3., 4.)]); + assert_eq!(vector.point_id_at(0), Some(PointId::ZERO), "the anchor keeps the zero ID stored edits address it by"); + assert_eq!(vector.point_count(), 1); + } + + // An open subpath welds shut with one linear closing segment, and a second call leaves it alone + #[test] + fn close_subpaths_welds_open_subpaths() { + let mut bezpath = BezPath::new(); + bezpath.move_to((0., 0.)); + bezpath.line_to((10., 0.)); + bezpath.line_to((10., 10.)); + + let (mut vector, mut paths) = Vector::from_bezpath(&bezpath); + assert_eq!(paths.len(), 1); + + vector.close_subpaths(&mut paths); + assert_eq!(paths.len(), 2); + assert!(vector.stroke_bezpath_iter(&paths).all(|contour| matches!(contour.elements().last(), Some(PathEl::ClosePath)))); + + vector.close_subpaths(&mut paths); + assert_eq!(paths.len(), 2); } } From c497a37299fef2be496edd4f867850f6e3844cf8 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 21 Aug 2026 21:49:39 -0700 Subject: [PATCH 2/8] Port graphic types, rendering, and the outer node crates to pool storage --- Cargo.lock | 1 + node-graph/libraries/core-types/src/list.rs | 2 +- node-graph/libraries/graphene-hash/Cargo.toml | 3 +- node-graph/libraries/graphene-hash/src/lib.rs | 38 ++++++ .../libraries/graphic-types/src/graphic.rs | 22 ++-- .../libraries/rendering/src/renderer.rs | 124 ++++++++++++------ node-graph/nodes/graphic/src/graphic.rs | 2 +- node-graph/nodes/path-bool/src/lib.rs | 21 ++- node-graph/nodes/repeat/src/repeat_nodes.rs | 35 +++-- node-graph/nodes/text/src/path_builder.rs | 30 ++++- 10 files changed, 198 insertions(+), 80 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b47acff3974..28be754b338 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2168,6 +2168,7 @@ version = "0.0.0" dependencies = [ "glam 0.33.10", "graphene-hash-derive", + "kurbo", ] [[package]] diff --git a/node-graph/libraries/core-types/src/list.rs b/node-graph/libraries/core-types/src/list.rs index c2d346f3b6a..128f1ff515a 100644 --- a/node-graph/libraries/core-types/src/list.rs +++ b/node-graph/libraries/core-types/src/list.rs @@ -30,7 +30,7 @@ pub const ATTR_EDITOR_LAYER_PATH: &str = "editor:layer_path"; /// (Boolean Operation, Rasterize, etc.), so the editor can still surface click targets for /// the original child layers after their content has been collapsed. pub const ATTR_EDITOR_MERGED_LAYERS: &str = "editor:merged_layers"; -/// Optional `Vector` that overrides the item's own geometry for click-target generation. +/// Optional `BezPath` that overrides the item's own geometry for click-target generation. /// Used by the 'Text' node for per-glyph bounding-box rectangles so glyphs are selectable /// by clicking anywhere within their bounds, not just the filled letterform. pub const ATTR_EDITOR_CLICK_TARGET: &str = "editor:click_target"; diff --git a/node-graph/libraries/graphene-hash/Cargo.toml b/node-graph/libraries/graphene-hash/Cargo.toml index 92ea4610fea..36c59e8b8c3 100644 --- a/node-graph/libraries/graphene-hash/Cargo.toml +++ b/node-graph/libraries/graphene-hash/Cargo.toml @@ -16,9 +16,10 @@ workspace = true [features] default = ["std"] -std = [] +std = ["dep:kurbo"] derive = ["graphene-hash-derive"] [dependencies] graphene-hash-derive = { path = "derive", optional = true } glam = { workspace = true } +kurbo = { workspace = true, optional = true } diff --git a/node-graph/libraries/graphene-hash/src/lib.rs b/node-graph/libraries/graphene-hash/src/lib.rs index 4476ad4fed0..00be14c1129 100644 --- a/node-graph/libraries/graphene-hash/src/lib.rs +++ b/node-graph/libraries/graphene-hash/src/lib.rs @@ -153,6 +153,44 @@ impl_glam_array! { glam::Quat, glam::DQuat, } +// kurbo path types: hash each element's verb and point coordinates via to_bits(). +#[cfg(feature = "std")] +impl CacheHash for kurbo::Point { + #[inline] + fn cache_hash(&self, state: &mut H) { + self.x.cache_hash(state); + self.y.cache_hash(state); + } +} + +#[cfg(feature = "std")] +impl CacheHash for kurbo::PathEl { + fn cache_hash(&self, state: &mut H) { + core::hash::Hash::hash(&core::mem::discriminant(self), state); + match self { + kurbo::PathEl::MoveTo(point) | kurbo::PathEl::LineTo(point) => point.cache_hash(state), + kurbo::PathEl::QuadTo(handle, point) => { + handle.cache_hash(state); + point.cache_hash(state); + } + kurbo::PathEl::CurveTo(handle_start, handle_end, point) => { + handle_start.cache_hash(state); + handle_end.cache_hash(state); + point.cache_hash(state); + } + kurbo::PathEl::ClosePath => {} + } + } +} + +#[cfg(feature = "std")] +impl CacheHash for kurbo::BezPath { + #[inline] + fn cache_hash(&self, state: &mut H) { + self.elements().cache_hash(state); + } +} + // Generic container impls. impl CacheHash for Option { #[inline] diff --git a/node-graph/libraries/graphic-types/src/graphic.rs b/node-graph/libraries/graphic-types/src/graphic.rs index c6848c0dc12..3768ecdded1 100644 --- a/node-graph/libraries/graphic-types/src/graphic.rs +++ b/node-graph/libraries/graphic-types/src/graphic.rs @@ -13,6 +13,7 @@ use dyn_any::DynAny; use glam::{DAffine2, DVec2}; use raster_types::{CPU, GPU, Raster}; pub use vector_types::Vector; +use vector_types::vector::{Path, paths_from_item_attributes, paths_from_list_row_attributes}; use vector_types::{Gradient, GradientSpread}; /// The possible forms of graphical content that can be rendered by the Render node (to targets like SVG and raster) or another render boundary node. @@ -52,11 +53,6 @@ impl From> for Graphic { } // Vector -impl From for Graphic { - fn from(vector: Vector) -> Self { - Graphic::Vector(Box::new(Item::new_from_element(vector))) - } -} impl From> for Graphic { fn from(vector: Item) -> Self { Graphic::Vector(Box::new(vector)) @@ -771,9 +767,9 @@ fn every_item_has_full_opacity(list: &List) -> bool { } /// Bounding box of one vector, inflated by its appearance's stroke when `include_stroke` is true. -/// Stroke parameters live on the item attribute, out of reach of the element-level impl. -fn vector_bounding_box(vector: &Vector, composed_transform: DAffine2, appearance: Option<&Appearance>, include_stroke: bool) -> Option<[DVec2; 2]> { - let mut bounds = vector.bounding_box_with_transform(composed_transform)?; +/// Paths and stroke parameters live on the holder's attributes, out of reach of the element-level impl. +fn vector_bounding_box(vector: &Vector, paths: Option<&List>, composed_transform: DAffine2, appearance: Option<&Appearance>, include_stroke: bool) -> Option<[DVec2; 2]> { + let mut bounds = vector.bounding_box_with_transform(paths?, composed_transform)?; // The full line width (not half) accounts for different styles of stroke caps if include_stroke && let Some(stroke) = appearance.and_then(|appearance| appearance.first_coverage_of(Cover::Stroke)).map(Coverage::stroke_params) { @@ -789,7 +785,13 @@ fn vector_bounding_box(vector: &Vector, composed_transform: DAffine2, appearance pub fn vector_item_bounding_box(item: &Item, transform: DAffine2, include_stroke: bool) -> RenderBoundingBox { let composed_transform = transform * item.attribute_cloned_or_default::(ATTR_TRANSFORM); - match vector_bounding_box(item.element(), composed_transform, item.attribute::(ATTR_APPEARANCE), include_stroke) { + match vector_bounding_box( + item.element(), + paths_from_item_attributes(item), + composed_transform, + item.attribute::(ATTR_APPEARANCE), + include_stroke, + ) { Some(bounds) => RenderBoundingBox::Rectangle(bounds), None => RenderBoundingBox::None, } @@ -804,7 +806,7 @@ pub fn vector_list_bounding_box(list: &List, transform: DAffine2, includ let item_transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, index); let appearance = list.attribute::(ATTR_APPEARANCE, index); - let Some(bounds) = vector_bounding_box(element, transform * item_transform, appearance, include_stroke) else { + let Some(bounds) = vector_bounding_box(element, paths_from_list_row_attributes(list, index), transform * item_transform, appearance, include_stroke) else { continue; }; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 1db2a7a6753..14ad6b4b02b 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -9,6 +9,7 @@ use core_types::color::Color; use core_types::color::SRGBA8; use core_types::consts::DEFAULT_FONT_SIZE; use core_types::list::ATTR_APPEARANCE; +use core_types::list::ATTR_PATHS; use core_types::list::{Item, List, NodeIdPath}; use core_types::math::quad::Quad; use core_types::render_complexity::RenderComplexity; @@ -28,6 +29,7 @@ use graphic_types::vector_types::gradient::{Gradient, GradientForm}; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::misc::dvec2_to_point; use graphic_types::vector_types::vector::style::{RenderMode, StrokeAlign, StrokeCap, StrokeJoin}; +use graphic_types::vector_types::vector::{Path, edit_item_geometry, edit_list_row_geometry, no_paths}; use graphic_types::{Appearance, Artboard, Cover, Coverage, FillAndStroke, Graphic, Vector}; use kurbo::{Affine, BezPath, Cap, Join, PathEl, Shape, StrokeOpts}; use num_traits::Zero; @@ -702,7 +704,9 @@ pub struct RenderMetadata { /// The Text tool composes this with `transform_to_viewport(layer)` to position its drag cage. pub text_frames: HashMap, pub clip_targets: HashSet, - pub vector_data: HashMap>, + /// Per-layer source geometry as the holder item, carrying the pool element and its paths attribute. + #[cfg_attr(feature = "serde", serde(skip))] + pub vector_data: HashMap>>, /// Per-layer `ATTR_APPEARANCE` item attribute, exposed so message handlers can read it. #[cfg_attr(feature = "serde", serde(skip))] pub appearance_attributes: HashMap>, @@ -1247,7 +1251,9 @@ impl Render for Graphic { let layer = ItemRef::Item(item).layer(); item.element_mut().new_ids_from_hash(layer); } - Graphic::Vector(item) => item.element_mut().vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default()), + Graphic::Vector(item) => { + edit_item_geometry(item, |vector, paths| vector.vector_new_ids_from_hash(paths, reference.map(|id| id.0).unwrap_or_default())); + } Graphic::GraphicList(list) => list.new_ids_from_hash(reference), Graphic::VectorList(list) => list.new_ids_from_hash(reference), _ => (), @@ -1441,6 +1447,7 @@ impl Render for List { /// Emits one vector shape as SVG, with no wrapping group of its own. fn render_vector_shape_svg(item: ItemRef<'_, Vector>, vector: &Vector, render: &mut SvgRender, render_params: &RenderParams) { + let paths = item.attribute::>(ATTR_PATHS).unwrap_or_else(|| no_paths()); let item_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); let blend_mode_attr: BlendMode = item.attribute_cloned_or_default(ATTR_BLEND_MODE); let opacity_attr: f64 = item.attribute_cloned_or(ATTR_OPACITY, 1.); @@ -1466,16 +1473,18 @@ fn render_vector_shape_svg(item: ItemRef<'_, Vector>, vector: &Vector, render: & let applied_stroke_transform = render_params.alignment_parent_transform.unwrap_or(applied_stroke_transform); let element_transform = set_stroke_transform.map(|stroke_transform| item_transform * stroke_transform.inverse()); let element_transform = element_transform.unwrap_or(DAffine2::IDENTITY); - let layer_bounds = vector.bounding_box().unwrap_or_default(); - let transformed_bounds = vector.bounding_box_with_transform(applied_stroke_transform).unwrap_or_default(); - let stroke_layer_bounds = vector.stroke_inclusive_bounding_box_with_transform(DAffine2::IDENTITY, stroke_params.as_ref()).unwrap_or(layer_bounds); + let layer_bounds = vector.bounding_box(paths).unwrap_or_default(); + let transformed_bounds = vector.bounding_box_with_transform(paths, applied_stroke_transform).unwrap_or_default(); + let stroke_layer_bounds = vector + .stroke_inclusive_bounding_box_with_transform(paths, DAffine2::IDENTITY, stroke_params.as_ref()) + .unwrap_or(layer_bounds); let bounds_matrix = DAffine2::from_scale_angle_translation(layer_bounds[1] - layer_bounds[0], 0., layer_bounds[0]); let stroke_bounds_matrix = DAffine2::from_scale_angle_translation(stroke_layer_bounds[1] - stroke_layer_bounds[0], 0., stroke_layer_bounds[0]); let mut path = String::new(); - for mut bezpath in vector.stroke_bezpath_iter() { + for mut bezpath in vector.stroke_bezpath_iter(paths) { bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); path.push_str(bezpath.to_svg().as_str()); } @@ -1486,7 +1495,7 @@ fn render_vector_shape_svg(item: ItemRef<'_, Vector>, vector: &Vector, render: & MaskType::Mask }; - let path_is_closed = vector.stroke_bezpath_iter().all(|path| matches!(path.elements().last(), Some(PathEl::ClosePath))); + let path_is_closed = vector.stroke_bezpath_iter(paths).all(|path| matches!(path.elements().last(), Some(PathEl::ClosePath))); let can_draw_aligned_stroke = path_is_closed && stroke_params.as_ref().is_some_and(|stroke| stroke.has_renderable_stroke() && stroke.align.is_not_centered()) && stroke_paint.is_some_and(|graphic| !graphic.is_guaranteed_fully_transparent()); @@ -1494,7 +1503,7 @@ fn render_vector_shape_svg(item: ItemRef<'_, Vector>, vector: &Vector, render: & let needs_separate_alignment_fill = can_draw_aligned_stroke && !can_use_paint_order; let override_paint_order = can_draw_aligned_stroke && can_use_paint_order; - let use_face_fill = vector.use_face_fill(); + let use_face_fill = vector.use_face_fill(paths); if needs_separate_alignment_fill && !wants_stroke_below { emit_svg_fill_path( @@ -1516,7 +1525,9 @@ fn render_vector_shape_svg(item: ItemRef<'_, Vector>, vector: &Vector, render: & // The mask must draw at full alpha so the SVG ``/`` fully zeroes the path interior. // The wrapping SVG group (above) handles the user-set opacity. - let mut mask_item = Item::new_from_element(cloned_vector).with_attribute(ATTR_TRANSFORM, item_transform); + let mut mask_item = Item::new_from_element(cloned_vector) + .with_attribute(ATTR_PATHS, paths.clone()) + .with_attribute(ATTR_TRANSFORM, item_transform); let black_fill = Graphic::ColorList(List::new_from_element(Color::BLACK)); mask_item.set_attribute(ATTR_APPEARANCE, Appearance::new_single(Coverage::new_fill(), black_fill)); let vector_item = List::new_from_item(mask_item); @@ -1525,7 +1536,7 @@ fn render_vector_shape_svg(item: ItemRef<'_, Vector>, vector: &Vector, render: & }); if use_face_fill { - for mut face_path in vector.construct_faces() { + for mut face_path in vector.construct_faces(paths) { face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); let face_d = face_path.to_svg(); @@ -1691,6 +1702,7 @@ fn render_vector_item_to_vello( paint_render_params: &RenderParams, ) { let Some(element) = item.element() else { return }; + let element_paths = item.attribute::>(ATTR_PATHS).unwrap_or_else(|| no_paths()); let item_transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); let blend_mode_attr: BlendMode = item.attribute_cloned_or_default(ATTR_BLEND_MODE); let opacity_attr: f64 = item.attribute_cloned_or(ATTR_OPACITY, 1.); @@ -1724,10 +1736,10 @@ fn render_vector_item_to_vello( multiplied_transform }; } - let layer_bounds = element.bounding_box().unwrap_or_default(); + let layer_bounds = element.bounding_box(element_paths).unwrap_or_default(); let mut path = kurbo::BezPath::new(); - for mut bezpath in element.stroke_bezpath_iter() { + for mut bezpath in element.stroke_bezpath_iter(element_paths) { bezpath.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); for element in bezpath { path.push(element); @@ -1748,7 +1760,7 @@ fn render_vector_item_to_vello( let stroke_fully_transparent = stroke_paint.is_none_or(|paint| paint.is_guaranteed_fully_transparent()); let can_draw_aligned_stroke = !stroke_fully_transparent && stroke.is_some_and(|s| s.has_renderable_stroke() && s.align.is_not_centered()) - && element.stroke_bezpath_iter().all(|p| matches!(p.elements().last(), Some(PathEl::ClosePath))); + && element.stroke_bezpath_iter(element_paths).all(|p| matches!(p.elements().last(), Some(PathEl::ClosePath))); let opacity = (opacity_attr * if render_params.for_mask { 1. } else { opacity_fill_attr }) as f32; let needs_blend_layer = opacity < 1. || blend_mode_attr != BlendMode::default(); @@ -1848,10 +1860,10 @@ fn render_vector_item_to_vello( }; }; - let use_face_fill = element.use_face_fill(); + let use_face_fill = element.use_face_fill(element_paths); let do_fill = |scene: &mut Scene, context: &mut RenderContext| { if use_face_fill { - for mut face_path in element.construct_faces() { + for mut face_path in element.construct_faces(element_paths) { face_path.apply_affine(Affine::new(applied_stroke_transform.to_cols_array())); let mut kurbo_path = kurbo::BezPath::new(); for element in face_path { @@ -1956,12 +1968,14 @@ fn render_vector_item_to_vello( // The mask must draw at full alpha so `SrcOut` fully zeroes the path interior. // The outer opacity/blend layer (above) handles the user-set opacity. - let mut mask_item = Item::new_from_element(cloned_element).with_attribute(ATTR_TRANSFORM, item_transform); + let mut mask_item = Item::new_from_element(cloned_element) + .with_attribute(ATTR_PATHS, element_paths.clone()) + .with_attribute(ATTR_TRANSFORM, item_transform); let black_fill = Graphic::ColorList(List::new_from_element(Color::BLACK)); mask_item.set_attribute(ATTR_APPEARANCE, Appearance::new_single(Coverage::new_fill(), black_fill)); let vector_list = List::new_from_item(mask_item); - let bounds = element.bounding_box_with_transform(multiplied_transform).unwrap_or(layer_bounds); + let bounds = element.bounding_box_with_transform(element_paths, multiplied_transform).unwrap_or(layer_bounds); // This branch is gated on `can_draw_aligned_stroke`, which already requires every subpath is closed let inflation = stroke.map_or(0., |stroke| stroke.max_aabb_inflation(true)); let (largest_scale, _) = singular_values(applied_stroke_transform); @@ -2061,18 +2075,20 @@ fn collect_vector_items_metadata<'a>( DAffine2::IDENTITY }; - // Use click-target override if the item provides one (e.g. 'Text' node's per-glyph bboxes) - let click_target_vector = item.attribute::(ATTR_EDITOR_CLICK_TARGET).unwrap_or(source); - + let source_paths = item.attribute::>(ATTR_PATHS).unwrap_or_else(|| no_paths()); let item_relative_transform = reference_inverse * transform; + // Use click-target override if the item provides one (e.g. 'Text' node's per-glyph bboxes) let mut click_targets_unwrapped = Vec::new(); - extend_targets_from_vector(&mut click_targets_unwrapped, appearance, click_target_vector, item_relative_transform); + match item.attribute::(ATTR_EDITOR_CLICK_TARGET) { + Some(override_path) => extend_targets_from_bezpaths(&mut click_targets_unwrapped, appearance, split_bezpath_contours(override_path), item_relative_transform), + None => extend_targets_from_vector(&mut click_targets_unwrapped, appearance, source, source_paths, item_relative_transform), + } accumulated_click_targets.entry(element_id).or_default().extend(click_targets_unwrapped.into_iter().map(Arc::new)); // Outlines always use source geometry so the visual outline reflects actual letterforms let mut outlines_unwrapped = Vec::new(); - extend_targets_from_vector(&mut outlines_unwrapped, appearance, source, item_relative_transform); + extend_targets_from_vector(&mut outlines_unwrapped, appearance, source, source_paths, item_relative_transform); accumulated_outlines.entry(element_id).or_default().extend(outlines_unwrapped.into_iter().map(Arc::new)); // Source geometry (not the click-target override) so editing tools work on letterforms. @@ -2080,7 +2096,7 @@ fn collect_vector_items_metadata<'a>( // Only item 0 is recorded since editing tools can only target a single item currently. // If that item has no paint attribute, none is recorded. if let std::collections::hash_map::Entry::Vacant(e) = metadata.vector_data.entry(element_id) { - e.insert(Arc::new(source.clone())); + e.insert(Arc::new(Item::new_from_element(source.clone()).with_attribute(ATTR_PATHS, source_paths.clone()))); if let Some(appearance) = appearance { metadata.appearance_attributes.insert(element_id, Arc::new(appearance.clone())); @@ -2126,22 +2142,25 @@ fn collect_vector_items_metadata<'a>( /// Collects one vector item's click target into the caller's list, baked through the item's transform. fn add_vector_item_click_targets(item: ItemRef<'_, Vector>, click_targets: &mut Vec, inherited_appearance: Option<&Appearance>) { let Some(source) = item.element() else { return }; + let source_paths = item.attribute::>(ATTR_PATHS).unwrap_or_else(|| no_paths()); let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); let appearance = Appearance::cascade(item.attribute::(ATTR_APPEARANCE), inherited_appearance); // Use click-target override geometry if the item provides one (e.g. 'Text' node's per-glyph bounding boxes) - let vector = item.attribute::(ATTR_EDITOR_CLICK_TARGET).unwrap_or(source); - - extend_targets_from_vector(click_targets, appearance, vector, transform); + match item.attribute::(ATTR_EDITOR_CLICK_TARGET) { + Some(override_path) => extend_targets_from_bezpaths(click_targets, appearance, split_bezpath_contours(override_path), transform), + None => extend_targets_from_vector(click_targets, appearance, source, source_paths, transform), + } } /// Like [`add_vector_item_click_targets`] but on source geometry only, ignoring `editor:click_target`, so outlines reflect actual letterforms. fn add_vector_item_outline_targets(item: ItemRef<'_, Vector>, outlines: &mut Vec, inherited_appearance: Option<&Appearance>) { let Some(source) = item.element() else { return }; + let source_paths = item.attribute::>(ATTR_PATHS).unwrap_or_else(|| no_paths()); let transform: DAffine2 = item.attribute_cloned_or_default(ATTR_TRANSFORM); let appearance = Appearance::cascade(item.attribute::(ATTR_APPEARANCE), inherited_appearance); - extend_targets_from_vector(outlines, appearance, source, transform); + extend_targets_from_vector(outlines, appearance, source, source_paths, transform); } impl Render for List { @@ -2202,19 +2221,28 @@ impl Render for List { } fn new_ids_from_hash(&mut self, reference: Option) { - for vector in self.iter_element_values_mut() { - vector.vector_new_ids_from_hash(reference.map(|id| id.0).unwrap_or_default()); + for index in 0..self.len() { + edit_list_row_geometry(self, index, |vector, paths| vector.vector_new_ids_from_hash(paths, reference.map(|id| id.0).unwrap_or_default())); } } } -/// Build one multi-contour `Path` (nonzero fill rule, so holes like the inside of an "O" work +/// Build one multi-contour `BezPath` (nonzero fill rule, so holes like the inside of an "O" work /// correctly) plus one `FreePoint` per disconnected anchor, apply the transform, and append. -fn extend_targets_from_vector(targets: &mut Vec, appearance: Option<&Appearance>, geometry: &Vector, transform: DAffine2) { +fn extend_targets_from_vector(targets: &mut Vec, appearance: Option<&Appearance>, geometry: &Vector, geometry_paths: &List, transform: DAffine2) { + let bezpaths: Vec = geometry.stroke_bezpath_iter(geometry_paths).filter(|bezpath| !bezpath.elements().is_empty()).collect(); + extend_targets_from_bezpaths(targets, appearance, bezpaths, transform); + + for click_target in extend_free_point_targets(geometry, geometry_paths, transform) { + targets.push(click_target); + } +} + +/// The contour half of [`extend_targets_from_vector`], also serving `editor:click_target` overrides. +fn extend_targets_from_bezpaths(targets: &mut Vec, appearance: Option<&Appearance>, mut bezpaths: Vec, transform: DAffine2) { // A coverage whose paint is `Graphic::None` exists but paints nothing, so it does not close subpaths for hit testing let filled = appearance.is_some_and(|appearance| appearance.has_painted_cover(Cover::Fill)); - let mut bezpaths: Vec = geometry.stroke_bezpath_iter().filter(|bezpath| !bezpath.elements().is_empty()).collect(); let all_contours_closed = bezpaths.iter().all(|bezpath| matches!(bezpath.elements().last(), Some(PathEl::ClosePath))); // Inside/Outside-aligned strokes reach `weight` from the centerline rather than `weight / 2` per side, @@ -2246,25 +2274,35 @@ fn extend_targets_from_vector(targets: &mut Vec, appearance: Option click_target.apply_transform(transform); targets.push(click_target); } +} - for click_target in extend_free_point_targets(geometry, transform) { - targets.push(click_target); +/// Splits a compound bezpath into its contours, so per-contour closing behaves as with source geometry. +fn split_bezpath_contours(bezpath: &BezPath) -> Vec { + let mut contours = Vec::new(); + + for &element in bezpath.elements() { + if matches!(element, PathEl::MoveTo(_)) || contours.is_empty() { + contours.push(BezPath::new()); + } + if let Some(contour) = contours.last_mut() { + contour.push(element); + } } + + contours } -fn extend_free_point_targets(vector: &Vector, transform: DAffine2) -> impl Iterator + '_ { - // Mark every point index touched by a segment endpoint in one `O(points + segments)` pass, avoiding a per-point `any_connected` scan - let mut connected = vec![false; vector.point_domain.len()]; - for &point_index in vector.segment_domain.start_point().iter().chain(vector.segment_domain.end_point()) { - connected[point_index] = true; - } +fn extend_free_point_targets<'a>(vector: &'a Vector, geometry_paths: &List, transform: DAffine2) -> impl Iterator + 'a { + // A pool point that no path references is a free point, since handles and anchors are all referenced + let connectivity = vector.connectivity(geometry_paths); - vector.point_domain.ids().iter().enumerate().filter_map(move |(point_index, &point_id)| { - if connected[point_index] { + (0..vector.point_count() as u32).filter_map(move |point_index| { + if connectivity.is_referenced(point_index) { return None; } - let anchor = vector.point_domain.position_from_id(point_id).unwrap_or_default(); + let point_id = vector.point_id_at(point_index).unwrap_or_default(); + let anchor = vector.position(point_index as usize).unwrap_or_default(); let mut click_target = ClickTarget::new_with_free_point(FreePoint::new(point_id, anchor)); click_target.apply_transform(transform); Some(click_target) diff --git a/node-graph/nodes/graphic/src/graphic.rs b/node-graph/nodes/graphic/src/graphic.rs index b9b3832b01e..dbc95140e77 100644 --- a/node-graph/nodes/graphic/src/graphic.rs +++ b/node-graph/nodes/graphic/src/graphic.rs @@ -459,7 +459,7 @@ async fn mirror( // The mirror reference may be based on the bounding box if an explicit reference point is chosen let item_transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); - let RenderBoundingBox::Rectangle(bounding_box) = content.element().bounding_box(item_transform, false) else { + let RenderBoundingBox::Rectangle(bounding_box) = content.bounding_box(DAffine2::IDENTITY, false) else { return List::new_from_item(content); }; diff --git a/node-graph/nodes/path-bool/src/lib.rs b/node-graph/nodes/path-bool/src/lib.rs index 49b4828a799..70c4216ddff 100644 --- a/node-graph/nodes/path-bool/src/lib.rs +++ b/node-graph/nodes/path-bool/src/lib.rs @@ -1,10 +1,11 @@ +use core_types::list::ATTR_PATHS; use core_types::list::{ATTR_APPEARANCE, Item, List, NodeIdPath}; use core_types::{ATTR_EDITOR_LAYER_PATH, ATTR_EDITOR_MERGED_LAYERS, ATTR_OPACITY, ATTR_OPACITY_FILL, ATTR_TRANSFORM, Ctx}; use glam::DAffine2; use graphic_types::Appearance; use graphic_types::graphic::bake_paint_transforms; -use graphic_types::vector_types::vector::VectorExt; use graphic_types::vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; +use graphic_types::vector_types::vector::{Path, no_paths, paths_from_list_row_attributes}; use graphic_types::{Graphic, Vector}; use linesweeper::topology::Topology; use linesweeper::{BinaryOp, FillRule, binary_op}; @@ -59,7 +60,9 @@ async fn boolean_operation( // Clean up the boolean operation result by merging duplicated points let merge_transform: DAffine2 = result_vector_list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); - result_vector_list.element_mut(0).unwrap().merge_by_distance_spatial(merge_transform, 0.0001); + let mut result_paths = result_vector_list.attribute::>(ATTR_PATHS, 0).cloned().unwrap_or_default(); + result_vector_list.element_mut(0).unwrap().merge_by_distance_spatial(&mut result_paths, merge_transform, 0.0001); + result_vector_list.set_attribute(ATTR_PATHS, 0, result_paths); } result_vector_list.into_iter().next().unwrap_or_default() @@ -145,6 +148,8 @@ fn boolean_operation_on_vector_list(vector: &List, boolean_operation: Bo let copy_from_transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM, index); // The boolean op bakes input transforms into the output geometry, so the result item carries no transform of its own attributes.insert(ATTR_TRANSFORM, DAffine2::IDENTITY); + // The result weaves its own paths rather than inheriting the operand's + attributes.remove::>(ATTR_PATHS); bake_paint_transforms(&mut attributes, copy_from_transform); @@ -155,21 +160,25 @@ fn boolean_operation_on_vector_list(vector: &List, boolean_operation: Bo for index in 0..vector.len() { let element = vector.element(index).unwrap(); - paths.push(to_bez_path(element, vector.attribute_cloned_or_default(ATTR_TRANSFORM, index))); + let element_paths = paths_from_list_row_attributes(vector, index).unwrap_or_else(|| no_paths()); + paths.push(to_bez_path(element, element_paths, vector.attribute_cloned_or_default(ATTR_TRANSFORM, index))); } let top = match Topology::::from_paths(paths.iter().enumerate().map(|(idx, path)| (path, (idx, paths.len()))), EPSILON) { Ok(top) => top, Err(e) => { log::error!("Boolean operation failed while building topology: {e}"); + row.set_attribute(ATTR_PATHS, List::::new()); list.push(row); return list; } }; let contours = top.contours(|winding| winding.is_inside(boolean_operation)); + let mut result_paths = List::new(); for contour in contours.contours() { - row.element_mut().append_bezpath(closed(contour.path.clone())); + row.element_mut().append_bezpath(&mut result_paths, &closed(contour.path.clone())); } + row.set_attribute(ATTR_PATHS, result_paths); list.push(row); list @@ -301,11 +310,11 @@ fn closed(mut path: BezPath) -> BezPath { path } -fn to_bez_path(vector: &Vector, transform: DAffine2) -> BezPath { +fn to_bez_path(vector: &Vector, element_paths: &List, transform: DAffine2) -> BezPath { let transform = Affine::new(transform.to_cols_array()); let mut path = BezPath::new(); - for subpath in vector.stroke_bezpath_iter() { + for subpath in vector.stroke_bezpath_iter(element_paths) { let mut first = true; for segment in closed(subpath).segments() { diff --git a/node-graph/nodes/repeat/src/repeat_nodes.rs b/node-graph/nodes/repeat/src/repeat_nodes.rs index ce387dfd0de..29bec64ceee 100644 --- a/node-graph/nodes/repeat/src/repeat_nodes.rs +++ b/node-graph/nodes/repeat/src/repeat_nodes.rs @@ -6,6 +6,7 @@ use glam::{DAffine2, DVec2}; use graphic_types::{Artboard, Graphic, Vector}; use raster_types::{CPU, GPU, Raster}; use vector_types::Gradient; +use vector_types::vector::{no_paths, paths_from_list_row_attributes}; #[node_macro::node(category("Repeat"), icon("NodeRepeat"))] async fn repeat( @@ -212,7 +213,9 @@ async fn repeat_on_points( } }; - let range = points_element.point_domain.positions().iter().enumerate(); + let paths = paths_from_list_row_attributes(&points, points_index).unwrap_or_else(|| no_paths()); + let positions = points_element.anchor_positions(paths); + let range = positions.iter().enumerate(); if reverse { for (index, &point) in range.rev() { iteration(index, point).await; @@ -242,10 +245,16 @@ mod test { use std::future::Future; use std::pin::Pin; use vector_nodes::generator_nodes::RectangleNode; + use vector_types::vector::Path; use vector_types::vector::misc::BoxCorners; fn vector_node_from_bezpath(bezpath: BezPath) -> List { - List::new_from_element(Vector::from_bezpath(bezpath)) + List::new_from_item(Vector::item_from_bezpath(&bezpath)) + } + + /// The paths riding the row, so geometry queries can resolve against them. + fn row_paths(list: &List, index: usize) -> &List { + paths_from_list_row_attributes(list, index).unwrap_or_else(|| no_paths()) } #[derive(Clone)] @@ -314,7 +323,7 @@ mod test { let bounds = generated .element(index) .unwrap() - .bounding_box_with_transform(generated.attribute_cloned_or_default(ATTR_TRANSFORM, index)) + .bounding_box_with_transform(row_paths(&generated, index), generated.attribute_cloned_or_default(ATTR_TRANSFORM, index)) .unwrap(); assert!(position.abs_diff_eq((bounds[0] + bounds[1]) / 2., 1e-10)); assert_eq!((bounds[1] - bounds[0]).x, position.y); @@ -336,8 +345,9 @@ mod test { .await; let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).await); let vector = vector_list.element(0).unwrap(); - assert_eq!(vector.stroke_manipulator_groups().count(), 3); - for (index, (manipulator_groups, _)) in vector.stroke_manipulator_groups().enumerate() { + let paths = row_paths(&vector_list, 0); + assert_eq!(vector.stroke_manipulator_groups(paths).count(), 3); + for (index, (manipulator_groups, _)) in vector.stroke_manipulator_groups(paths).enumerate() { assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5); } } @@ -355,9 +365,10 @@ mod test { .await; let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).await); let vector = vector_list.element(0).unwrap(); - assert_eq!(vector.stroke_manipulator_groups().count(), 1); + let paths = row_paths(&vector_list, 0); + assert_eq!(vector.stroke_manipulator_groups(paths).count(), 1); - let (manipulator_groups, _) = vector.stroke_manipulator_groups().next().unwrap(); + let (manipulator_groups, _) = vector.stroke_manipulator_groups(paths).next().unwrap(); let anchor = manipulator_groups[0].anchor; assert!(anchor.length() < 1e-5, "Expected the single copy to be untransformed, found anchor {anchor}"); } @@ -377,8 +388,9 @@ mod test { .await; let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).await); let vector = vector_list.element(0).unwrap(); - assert_eq!(vector.stroke_manipulator_groups().count(), 8); - for (index, (manipulator_groups, _)) in vector.stroke_manipulator_groups().enumerate() { + let paths = row_paths(&vector_list, 0); + assert_eq!(vector.stroke_manipulator_groups(paths).count(), 8); + for (index, (manipulator_groups, _)) in vector.stroke_manipulator_groups(paths).enumerate() { assert!((manipulator_groups[0].anchor - direction * index as f64 / (count - 1) as f64).length() < 1e-5); } } @@ -396,9 +408,10 @@ mod test { .await; let vector_list = List::new_from_item(vector_nodes::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(repeated))).await); let vector = vector_list.element(0).unwrap(); - assert_eq!(vector.stroke_manipulator_groups().count(), 8); + let paths = row_paths(&vector_list, 0); + assert_eq!(vector.stroke_manipulator_groups(paths).count(), 8); - for (index, (manipulator_groups, _)) in vector.stroke_manipulator_groups().enumerate() { + for (index, (manipulator_groups, _)) in vector.stroke_manipulator_groups(paths).enumerate() { let expected_angle = (index as f64 + 1.) * 45.; let center = (manipulator_groups[0].anchor + manipulator_groups[2].anchor) / 2.; diff --git a/node-graph/nodes/text/src/path_builder.rs b/node-graph/nodes/text/src/path_builder.rs index 906d955be28..cd0720d6039 100644 --- a/node-graph/nodes/text/src/path_builder.rs +++ b/node-graph/nodes/text/src/path_builder.rs @@ -8,13 +8,18 @@ use skrifa::outline::{DrawSettings, OutlinePen}; use skrifa::raw::FontRef as ReadFontsRef; use skrifa::{MetadataProvider, OutlineGlyph}; use vector_types::kurbo::{Affine, BezPath, Point, Rect, Shape}; -use vector_types::vector::{Vector, VectorExt}; +use vector_types::vector::{Path, Vector}; pub struct PathBuilder { origin: DVec2, /// Contours of the glyph currently being drawn, accumulated as a single path. glyph_bezpath: BezPath, pub vector_list: List, + /// Whether each glyph becomes its own list item rather than merging into one vector. + per_glyph_items: bool, + /// The single accumulated vector and its paths in merged mode, written to item 0 in `finalize()`. + merged_vector: Vector, + merged_paths: List, /// Per-glyph AABBs collected in single-item mode, published as `ATTR_EDITOR_CLICK_TARGET` in `finalize()`. merged_click_target_bboxes: Vec<[DVec2; 2]>, /// Per-glyph baselines, parallel to `merged_click_target_bboxes`. Groups glyphs by line for the widening pass. @@ -34,6 +39,9 @@ impl PathBuilder { Self { glyph_bezpath: BezPath::new(), vector_list: if per_glyph_items { List::new() } else { List::new_from_element(Vector::default()) }, + per_glyph_items, + merged_vector: Vector::default(), + merged_paths: List::new(), merged_click_target_bboxes: Vec::new(), merged_click_target_baselines: Vec::new(), per_glyph_bboxes: Vec::new(), @@ -78,7 +86,7 @@ impl PathBuilder { // back to the layer-local frame origin, regardless of which glyph survived let frame_in_item_local = DAffine2::from_scale_angle_translation(self.text_frame_size, 0., -glyph_offset); - let item = Item::new_from_element(Vector::from_bezpath(core::mem::take(&mut self.glyph_bezpath))) + let item = Vector::item_from_bezpath(&core::mem::take(&mut self.glyph_bezpath)) .with_attribute(ATTR_TRANSFORM, DAffine2::from_translation(glyph_offset)) .with_attribute(ATTR_EDITOR_TEXT_FRAME, frame_in_item_local); self.vector_list.push(item); @@ -86,8 +94,7 @@ impl PathBuilder { // Defer click target creation to `finalize()` where adjacent AABBs get widened self.per_glyph_bboxes.push(glyph_bbox); } else { - // Unwrapping here is ok because `self.vector_list` is initialized with a single `List` item - self.vector_list.element_mut(0).unwrap().append_bezpath(core::mem::take(&mut self.glyph_bezpath)); + self.merged_vector.append_bezpath(&mut self.merged_paths, &core::mem::take(&mut self.glyph_bezpath)); if let Some(bbox) = glyph_bbox { self.merged_click_target_bboxes.push(bbox); @@ -157,6 +164,15 @@ impl PathBuilder { } pub fn finalize(mut self) -> List { + // Merged mode accumulated into side fields, which item 0 takes over here + if !self.per_glyph_items + && let Some(element) = self.vector_list.element_mut(0) + { + *element = core::mem::take(&mut self.merged_vector); + let paths = core::mem::take(&mut self.merged_paths); + self.vector_list.set_attribute(core_types::list::ATTR_PATHS, 0, paths); + } + // Empty list = all glyphs clipped by height. Create a placeholder with the same item-0 // transform a populated list would have so `local_transforms` stays stable mid-drag. // TODO: Remove this hack and move the attribute up to the parent return value when is done. @@ -190,11 +206,11 @@ impl PathBuilder { for (entry, widened) in entries.iter().zip(layer_bboxes.iter()) { let glyph_local = [widened[0] - entry.1, widened[1] - entry.1]; let rect = rectangle_bezpath(glyph_local[0], glyph_local[1]); - self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, entry.0, Vector::from_bezpath(rect)); + self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, entry.0, rect); } } - // Glyph separation off: widen the accumulated AABBs and bundle as one override `Vector` + // Glyph separation off: widen the accumulated AABBs and bundle as one override `BezPath` if !self.merged_click_target_bboxes.is_empty() { let mut bboxes = self.merged_click_target_bboxes; widen_horizontal_gaps(&mut bboxes, &self.merged_click_target_baselines); @@ -203,7 +219,7 @@ impl PathBuilder { for [min, max] in &bboxes { widened_bezpath.extend(rectangle_bezpath(*min, *max)); } - self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, 0, Vector::from_bezpath(widened_bezpath)); + self.vector_list.set_attribute(ATTR_EDITOR_CLICK_TARGET, 0, widened_bezpath); } // Fill in text frame for items that don't have one yet (single-item mode, where item 0 = identity) From 04883dfaef702949980f40d3da039894fdc1bd8e Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 21 Aug 2026 21:50:33 -0700 Subject: [PATCH 3/8] Migrate legacy document vector data onto pool storage --- node-graph/graph-craft/src/document/value.rs | 4 +- node-graph/libraries/graphic-types/src/lib.rs | 178 +++++++++++++++--- .../vector-types/src/vector/flatten.rs | 28 +++ .../libraries/vector-types/src/vector/mod.rs | 1 + 4 files changed, 180 insertions(+), 31 deletions(-) diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 7cede3af5d1..086d632bd1b 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -779,8 +779,8 @@ pub fn deserialize_tagged_value_with_legacy_migration<'de, D: serde::Deserialize } "Vector" | "VectorData" => { let vector = graphic_types::migrations::migrate_to_optional_vector(content.clone()).map_err(serde::de::Error::custom)?; - if let Some(vector) = vector { - let modification = Box::new(VectorModification::create_from_vector(&vector)); + if let Some((vector, paths)) = vector { + let modification = Box::new(VectorModification::create_from_vector(&vector, &paths)); return Ok(MemoHash::new(TaggedValue::VectorModification(modification))); } return Ok(MemoHash::new(TaggedValue::TypeDefault(list!(Vector)))); diff --git a/node-graph/libraries/graphic-types/src/lib.rs b/node-graph/libraries/graphic-types/src/lib.rs index 9e5b18c6e84..1903291f510 100644 --- a/node-graph/libraries/graphic-types/src/lib.rs +++ b/node-graph/libraries/graphic-types/src/lib.rs @@ -22,9 +22,12 @@ pub mod migrations { // TODO: Eventually remove this document upgrade code pub mod legacy { use core_types::Color; + use core_types::list::List; use dyn_any::DynAny; use glam::{DAffine2, DVec2}; - use vector_types::vector::{PointDomain, SegmentDomain, misc::HandleId, style::Stroke}; + use vector_types::vector::misc::{BezierHandles, HandleId}; + use vector_types::vector::style::Stroke; + use vector_types::vector::{Path, PointId, SegmentId, vector_from_legacy_parts}; use vector_types::{GradientRamp, Vector, vector}; #[derive(Default, Debug, Clone, PartialEq, graphene_hash::CacheHash, DynAny, serde::Serialize, serde::Deserialize)] @@ -102,6 +105,49 @@ pub mod migrations { pub stroke: Option, } + /// The legacy point storage shape, kept only so old documents still deserialize. + #[derive(Default, serde::Deserialize)] + #[cfg_attr(test, derive(serde::Serialize))] + pub(super) struct LegacyPointDomain { + pub id: Vec, + #[serde(alias = "positions")] + pub position: Vec, + } + + /// The legacy segment storage shape, kept only so old documents still deserialize. + #[derive(Default, serde::Deserialize)] + #[cfg_attr(test, derive(serde::Serialize))] + pub(super) struct LegacySegmentDomain { + #[serde(alias = "ids")] + pub id: Vec, + pub start_point: Vec, + pub end_point: Vec, + pub handles: Vec, + } + + /// The legacy `Vector` struct shape, converted into pool storage at load. + #[derive(Default, serde::Deserialize)] + #[cfg_attr(test, derive(serde::Serialize))] + pub(super) struct LegacyVector { + pub colinear_manipulators: Vec<[HandleId; 2]>, + pub point_domain: LegacyPointDomain, + pub segment_domain: LegacySegmentDomain, + } + + impl LegacyVector { + pub(super) fn into_parts(self) -> (Vector, List) { + vector_from_legacy_parts( + self.point_domain.id, + self.point_domain.position, + self.segment_domain.id, + self.segment_domain.start_point, + self.segment_domain.end_point, + self.segment_domain.handles, + self.colinear_manipulators, + ) + } + } + /// Old documents stored a `Vector` flattened with list attributes (`transform`, `alpha_blending`, `upstream_graphic_group`); only the geometry fields are recovered. #[derive(serde::Deserialize)] #[cfg_attr(test, derive(Default, serde::Serialize))] @@ -109,41 +155,43 @@ pub mod migrations { #[allow(dead_code)] pub style: PathStyle, pub colinear_manipulators: Vec<[HandleId; 2]>, - pub point_domain: PointDomain, - pub segment_domain: SegmentDomain, + pub point_domain: LegacyPointDomain, + pub segment_domain: LegacySegmentDomain, } #[derive(serde::Deserialize)] pub(super) struct Table { #[serde(alias = "instances", alias = "instance")] - pub element: Vec, + pub element: Vec, } } // TODO: Eventually remove this document upgrade code - /// Returns the first `Vector` recovered from any of the legacy on-disk shapes (the legacy `VectorData` flat struct, a single `Vector`, or any of the historical `List` variants). - pub fn migrate_to_optional_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + /// Returns the first vector recovered from any of the legacy on-disk shapes (the legacy `VectorData` flat struct, a legacy `Vector` struct, or any of the historical list variants). + pub fn migrate_to_optional_vector<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result)>, D::Error> { use serde::Deserialize; #[derive(serde::Deserialize)] #[serde(untagged)] #[allow(clippy::large_enum_variant)] enum VectorFormat { - // Old vector data must be tried first. Serde would otherwise ignore its `style` field and - // deserialize the missing optional `stroke` field as `None` in the current `Vector`. + // The old flat shape that also held a style. Both this and the legacy `Vector` arm recover the same geometry from it. OldVectorData(legacy::VectorData), - Vector(Vector), + Vector(legacy::LegacyVector), List(legacy::Table), } Ok(match VectorFormat::deserialize(deserializer)? { - VectorFormat::OldVectorData(old) => Some(Vector { - colinear_manipulators: old.colinear_manipulators, - point_domain: old.point_domain, - segment_domain: old.segment_domain, - }), - VectorFormat::Vector(vector) => Some(vector), - VectorFormat::List(list) => list.element.into_iter().next(), + VectorFormat::OldVectorData(old) => { + let vector = legacy::LegacyVector { + colinear_manipulators: old.colinear_manipulators, + point_domain: old.point_domain, + segment_domain: old.segment_domain, + }; + Some(vector.into_parts()) + } + VectorFormat::Vector(vector) => Some(vector.into_parts()), + VectorFormat::List(list) => list.element.into_iter().next().map(legacy::LegacyVector::into_parts), }) } @@ -187,15 +235,15 @@ pub mod migrations { use super::*; use vector_types::vector::style::Stroke; - /// The legacy `style` payload (including its stroke) must still parse so the untagged format - /// disambiguation succeeds, even though the geometry is all that survives. + /// The legacy `style` payload is discarded, leaving only the geometry. #[test] fn recovers_geometry_from_old_vector_data_with_style() { - use core_types::ops::FromAnchorPosition; - let old_vector = legacy::VectorData { style: legacy::PathStyle { stroke: Some(Stroke::new(12.)) }, - point_domain: Vector::from_anchor_position(glam::DVec2::new(3., 4.)).point_domain, + point_domain: legacy::LegacyPointDomain { + id: vec![vector_types::vector::PointId::ZERO], + position: vec![glam::DVec2::new(3., 4.)], + }, ..Default::default() }; @@ -209,25 +257,97 @@ pub mod migrations { .unwrap() .insert("fill".into(), serde_json::to_value(legacy::LegacyFill::default()).unwrap()); - let migrated = migrate_to_optional_vector(value).unwrap().expect("the legacy shape should parse into a vector"); + let (migrated, _paths) = migrate_to_optional_vector(value).unwrap().expect("the legacy shape should parse into a vector"); assert_eq!( - migrated.point_domain.positions(), + migrated.positions().collect::>(), [glam::DVec2::new(3., 4.)], "the legacy geometry should survive alongside the discarded style" ); } #[test] - fn recovers_geometry_from_current_vector_data() { - use core_types::ops::FromAnchorPosition; - - let vector = Vector::from_anchor_position(glam::DVec2::new(3., 4.)); + fn recovers_geometry_from_legacy_vector_struct() { + let vector = legacy::LegacyVector { + point_domain: legacy::LegacyPointDomain { + id: vec![vector_types::vector::PointId::ZERO], + position: vec![glam::DVec2::new(3., 4.)], + }, + ..Default::default() + }; let value = serde_json::to_value(&vector).unwrap(); - let migrated = migrate_to_optional_vector(value).unwrap().unwrap(); + let (migrated, _paths) = migrate_to_optional_vector(value).unwrap().unwrap(); + + assert_eq!(migrated.positions().collect::>(), [glam::DVec2::new(3., 4.)]); + } + + /// The oldest documents named these fields in the plural. + #[test] + fn recovers_geometry_from_plural_field_names() { + let value = serde_json::json!({ + "colinear_manipulators": [], + "point_domain": { "id": [1, 2], "positions": [[3., 4.], [5., 6.]] }, + "segment_domain": { "ids": [7], "start_point": [0], "end_point": [1], "handles": ["Linear"] }, + }); + + let (migrated, paths) = migrate_to_optional_vector(value).unwrap().expect("the plural field names should parse"); + + assert_eq!(migrated.positions().collect::>(), [glam::DVec2::new(3., 4.), glam::DVec2::new(5., 6.)]); + assert_eq!(paths.len(), 1); + } + + /// A closed triangle keeps its IDs, its handles, and the smooth pairs at its anchors, beside a free point. + #[test] + fn keeps_ids_handles_smooth_pairs_and_free_points() { + use glam::DVec2; + use vector_types::kurbo::{CubicBez, Line, PathSeg, QuadBez}; + use vector_types::vector::misc::HandleType; + + let handle = |ty: &str, segment: u64| serde_json::json!({ "ty": ty, "segment": segment }); + let value = serde_json::json!({ + "colinear_manipulators": [[handle("End", 101), handle("Primary", 102)], [handle("End", 103), handle("Primary", 101)]], + "point_domain": { "id": [10, 20, 30, 40], "position": [[0., 0.], [10., 0.], [10., 10.], [99., 99.]] }, + "segment_domain": { + "id": [101, 102, 103], + "start_point": [0, 1, 2], + "end_point": [1, 2, 0], + "handles": [{ "Cubic": { "handle_start": [2., -1.], "handle_end": [8., -1.] } }, { "Quadratic": { "handle": [12., 5.] } }, "Linear"], + }, + }); + + let (vector, paths) = migrate_to_optional_vector(value).unwrap().expect("the legacy vector should parse"); + + // Looked up by ID, since storage order is free to differ from the legacy order + let mut anchors: Vec<(u64, DVec2)> = vector + .anchor_points(&paths) + .filter_map(|point| Some((point.inner(), vector.position(vector.index_of_point_id(point)? as usize)?))) + .collect(); + anchors.sort_by_key(|&(id, _)| id); + assert_eq!(anchors, [(10, DVec2::new(0., 0.)), (20, DVec2::new(10., 0.)), (30, DVec2::new(10., 10.)), (40, DVec2::new(99., 99.))]); + + let mut segments: Vec<(u64, PathSeg, u64, u64)> = vector.segment_iter(&paths).map(|(id, segment, start, end)| (id.inner(), segment, start.inner(), end.inner())).collect(); + segments.sort_by_key(|&(id, ..)| id); + assert_eq!( + segments, + [ + (101, PathSeg::Cubic(CubicBez::new((0., 0.), (2., -1.), (8., -1.), (10., 0.))), 10, 20), + (102, PathSeg::Quad(QuadBez::new((10., 0.), (12., 5.), (10., 10.))), 20, 30), + (103, PathSeg::Line(Line::new((10., 10.), (0., 0.))), 30, 10), + ] + ); - assert_eq!(migrated.point_domain.positions(), [glam::DVec2::new(3., 4.)]); + let mut smooth_pairs: Vec<[(bool, u64); 2]> = vector + .colinear_pairs(&paths) + .into_iter() + .map(|pair| { + let mut sides = pair.map(|handle| (handle.ty == HandleType::Primary, handle.segment.inner())); + sides.sort(); + sides + }) + .collect(); + smooth_pairs.sort(); + assert_eq!(smooth_pairs, [[(false, 101), (true, 102)], [(false, 103), (true, 101)]]); } } } diff --git a/node-graph/libraries/vector-types/src/vector/flatten.rs b/node-graph/libraries/vector-types/src/vector/flatten.rs index fb125bcfbf3..be20579b229 100644 --- a/node-graph/libraries/vector-types/src/vector/flatten.rs +++ b/node-graph/libraries/vector-types/src/vector/flatten.rs @@ -130,6 +130,34 @@ impl WorkingVector { } } +// TODO: Eventually remove this document upgrade code +/// Builds storage from the legacy flat arrays: anchors with IDs and positions, segments with IDs, +/// endpoint indices, and absolute handles, plus G1 pairs. Used by document migration. +pub fn vector_from_legacy_parts( + point_ids: Vec, + point_positions: Vec, + segment_ids: Vec, + segment_start: Vec, + segment_end: Vec, + segment_handles: Vec, + colinear: Vec<[HandleId; 2]>, +) -> (Vector, List) { + let working = WorkingVector { + point_ids, + point_positions, + segment_ids, + segment_start, + segment_end, + segment_handles, + colinear, + }; + + let (mut vector, mut paths) = (Vector::default(), List::new()); + working.reweave(&mut vector, &mut paths); + + (vector, paths) +} + #[cfg(test)] mod tests { use super::*; diff --git a/node-graph/libraries/vector-types/src/vector/mod.rs b/node-graph/libraries/vector-types/src/vector/mod.rs index 435175c1bc1..0167856ecbd 100644 --- a/node-graph/libraries/vector-types/src/vector/mod.rs +++ b/node-graph/libraries/vector-types/src/vector/mod.rs @@ -15,6 +15,7 @@ mod vector_types; pub use addressing::*; pub use connectivity::*; pub use contour::*; +pub use flatten::vector_from_legacy_parts; pub use path::*; pub use reference_point::*; pub use vector_attributes::*; From 2b15aebe5bb4657383141af5f58f1ad02dbb780a Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 21 Aug 2026 21:52:09 -0700 Subject: [PATCH 4/8] Bridge the editor onto pool storage with the VectorGeometry bundle --- .../clipboard/clipboard_message_handler.rs | 21 +- .../src/messages/clipboard/utility_types.rs | 4 +- .../portfolio/document/document_message.rs | 8 +- .../document/graph_operation/utility_types.rs | 4 +- .../document/overlays/utility_functions.rs | 8 +- .../document/overlays/utility_types_native.rs | 9 +- .../document/overlays/utility_types_web.rs | 4 +- .../utility_types/document_metadata.rs | 12 +- .../network_interface/structure.rs | 25 +- .../document/utility_types/transformation.rs | 10 +- .../graph_modification_utils.rs | 8 +- editor/src/node_graph_executor.rs | 7 +- editor/src/node_graph_executor/runtime.rs | 12 +- .../vector-types/src/vector/geometry.rs | 252 ++++++++++++++++++ .../libraries/vector-types/src/vector/mod.rs | 4 +- node-graph/nodes/gstd/src/lib.rs | 1 + 16 files changed, 331 insertions(+), 58 deletions(-) create mode 100644 node-graph/libraries/vector-types/src/vector/geometry.rs diff --git a/editor/src/messages/clipboard/clipboard_message_handler.rs b/editor/src/messages/clipboard/clipboard_message_handler.rs index 333af93effe..77d0da3c563 100644 --- a/editor/src/messages/clipboard/clipboard_message_handler.rs +++ b/editor/src/messages/clipboard/clipboard_message_handler.rs @@ -12,8 +12,9 @@ use crate::messages::tool::utility_types::ToolType; use graph_craft::application_io::resource::{DataSource, ResourceHash}; use graph_craft::document::NodeId; use graphene_std::Color; +use graphene_std::list::List; use graphene_std::vector::misc::{BezierHandles, HandleId, point_to_dvec2, segment_to_handles}; -use graphene_std::vector::{PointId, SegmentId, VectorModificationType}; +use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType}; use graphite_proc_macros::{ExtractField, message_handler_data}; use kurbo::ParamCurve; use std::sync::Arc; @@ -434,18 +435,26 @@ impl MessageHandler> for Clipboard let stroke = graphene_std::vector::style::Stroke::new(DEFAULT_STROKE_WIDTH); responses.add(GraphOperationMessage::StrokeSet { layer, color, stroke }); - // Create new point ids and add those into the existing Vector path + // Materialize the copied snapshot so its points and segments can replay with fresh IDs + let (mut pasted_vector, mut pasted_paths) = (Vector::default(), List::new()); + new_vector.apply(&mut pasted_vector, &mut pasted_paths); + + // Create new point IDs and add those into the existing Vector path let mut points_map = HashMap::new(); - for (point, position) in new_vector.point_domain.iter() { + for index in pasted_vector.anchor_indices(&pasted_paths) { + let (Some(point), Some(position)) = (pasted_vector.point_id_at(index as u32), pasted_vector.position(index)) else { + continue; + }; + let new_point_id = PointId::generate(); points_map.insert(point, new_point_id); let modification_type = VectorModificationType::InsertPoint { id: new_point_id, position }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); } - // Create new segment ids and add the segments into the existing Vector path + // Create new segment IDs and add the segments into the existing Vector path let mut segments_map = HashMap::new(); - for (segment_id, segment, start, end) in new_vector.segment_iter() { + for (segment_id, segment, start, end) in pasted_vector.segment_iter(&pasted_paths) { let (Some(&start_point), Some(&end_point)) = (points_map.get(&start), points_map.get(&end)) else { warn!("Skipping pasted vector segment with an unknown endpoint"); continue; @@ -468,7 +477,7 @@ impl MessageHandler> for Clipboard } // Set G1 continuity - for handles in new_vector.colinear_manipulators { + for handles in pasted_vector.colinear_pairs(&pasted_paths) { let to_new_handle = |handle: HandleId| -> Option { Some(HandleId { ty: handle.ty, diff --git a/editor/src/messages/clipboard/utility_types.rs b/editor/src/messages/clipboard/utility_types.rs index 421215d1119..fb8689d9927 100644 --- a/editor/src/messages/clipboard/utility_types.rs +++ b/editor/src/messages/clipboard/utility_types.rs @@ -5,7 +5,7 @@ use base64::engine::general_purpose::STANDARD as BASE64; use glam::DAffine2; use graph_craft::application_io::resource::{DataSource, ResourceHash, ResourceId}; use graph_craft::document::NodeId; -use graphene_std::vector::Vector; +use graphene_std::vector::VectorModification; #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum ClipboardContentRaw { @@ -20,7 +20,7 @@ pub enum ClipboardContent { Image { data: Vec, width: u32, height: u32 }, } -pub type ClipboardVectorEntry = (LayerNodeIdentifier, Vector, DAffine2); +pub type ClipboardVectorEntry = (LayerNodeIdentifier, VectorModification, DAffine2); /// An entry in the `graphite:` clipboard payload. #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)] diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index de7589019c8..c94ce044dd1 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -13,6 +13,7 @@ use crate::messages::prelude::*; use glam::{DAffine2, IVec2, UVec2}; use graph_craft::document::NodeId; use graphene_std::Appearance; +use graphene_std::list::Item; use graphene_std::raster::BlendMode; use graphene_std::transform::Footprint; use graphene_std::vector::Vector; @@ -295,11 +296,12 @@ pub enum DocumentMessage { UpdateClipTargets { clip_targets: HashSet, }, + // `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it. + // `UpdateVectorData` and `UpdateAppearanceAttributes` are produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so they never reach that serialization point. + #[serde(skip)] UpdateVectorData { - vector_data: HashMap>, + vector_data: HashMap>>, }, - // `Message` is only serialized at `editor_wrapper.rs`, and only inputs from JS pass through it. - // `UpdateAppearanceAttributes` is produced inside `editor.handle_message` by `node_graph_executor.rs` and consumed in the same dispatch loop, so it never reaches that serialization point. #[serde(skip)] UpdateAppearanceAttributes { appearance_attributes: HashMap>, diff --git a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs index 8511b1076c5..033b1dbe783 100644 --- a/editor/src/messages/portfolio/document/graph_operation/utility_types.rs +++ b/editor/src/messages/portfolio/document/graph_operation/utility_types.rs @@ -147,8 +147,8 @@ impl<'a> ModifyInputsContext<'a> { pub fn insert_vector(&mut self, bezpath: BezPath, layer: LayerNodeIdentifier, include_transform: bool, include_fill: bool, include_stroke: bool) { // Build a VectorModification that reproduces the geometry (same format the Pen tool uses) - let vector = Vector::from_bezpath(bezpath); - let modification = Box::new(VectorModification::create_from_vector(&vector)); + let (vector, paths) = Vector::from_bezpath(&bezpath); + let modification = Box::new(VectorModification::create_from_vector(&vector, &paths)); let shape = resolve_network_node_type("Path") .expect("Path node does not exist") diff --git a/editor/src/messages/portfolio/document/overlays/utility_functions.rs b/editor/src/messages/portfolio/document/overlays/utility_functions.rs index 59de9f797ec..76bc5f2823c 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_functions.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_functions.rs @@ -8,7 +8,7 @@ use crate::messages::tool::common_functionality::utility_functions::closest_open use crate::messages::tool::tool_messages::tool_prelude::DocumentMessageHandler; use glam::{DAffine2, DVec2}; use graphene_std::vector::misc::{BezierHandles, ManipulatorPointId, point_to_dvec2, segment_to_handles}; -use graphene_std::vector::{PointId, SegmentId, Vector}; +use graphene_std::vector::{PointId, SegmentId, VectorGeometry}; use kurbo::{Affine, ParamCurve, PathSeg}; use std::collections::HashMap; #[cfg(target_family = "wasm")] @@ -44,7 +44,7 @@ pub fn selected_segments(network_interface: &NodeNetworkInterface, shape_editor: map } -pub fn selected_segments_for_layer(vector: &Vector, state: &SelectedLayerState) -> Vec { +pub fn selected_segments_for_layer(vector: &VectorGeometry, state: &SelectedLayerState) -> Vec { let selected_anchors = state .selected_points() .filter_map(|point| if let ManipulatorPointId::Anchor(p) = point { Some(p) } else { None }) @@ -195,7 +195,7 @@ pub fn path_overlays(document: &DocumentMessageHandler, draw_handles: DrawHandle } if display_anchors { - for (&id, &position) in vector.point_domain.ids().iter().zip(vector.point_domain.positions()) { + for (id, position) in vector.anchors() { overlay_context.manipulator_anchor(transform.transform_point2(position), is_selected(ManipulatorPointId::Anchor(id)), None); } } @@ -226,7 +226,7 @@ pub fn open_path_endpoint_overlays(document: &DocumentMessageHandler, shape_edit if hovered.is_some_and(|(hovered_layer, hovered_id, _)| hovered_layer == layer && hovered_id == id) { continue; } - let Some(position) = vector.point_domain.position_from_id(id) else { continue }; + let Some(position) = vector.position_from_id(id) else { continue }; overlay_context.manipulator_anchor(transform.transform_point2(position), is_selected(layer, id), None); } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs index 5be859a73f0..d4dcfdf9b95 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_native.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_native.rs @@ -18,7 +18,7 @@ use graphene_std::math::quad::Quad; use graphene_std::text::{TextAlign, TextContext, TypesettingConfig}; use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::misc::point_to_dvec2; -use graphene_std::vector::{PointId, SegmentId, Vector}; +use graphene_std::vector::{PointId, SegmentId, Vector, VectorGeometry, no_paths, paths_from_list_row_attributes}; use kurbo::{self, BezPath, ParamCurve}; use kurbo::{Affine, PathSeg}; use std::collections::HashMap; @@ -379,7 +379,7 @@ impl OverlayContext { } /// Used by the Pen and Path tools to outline the path of the shape. - pub fn outline_vector(&mut self, vector: &Vector, transform: DAffine2) { + pub fn outline_vector(&mut self, vector: &VectorGeometry, transform: DAffine2) { self.internal().outline_vector(vector, transform); } @@ -930,7 +930,7 @@ impl OverlayContextInternal { } /// Used by the Pen and Path tools to outline the path of the shape. - fn outline_vector(&mut self, vector: &Vector, transform: DAffine2) { + fn outline_vector(&mut self, vector: &VectorGeometry, transform: DAffine2) { let vello_transform = self.get_transform(); let mut path = BezPath::new(); @@ -1148,7 +1148,8 @@ impl OverlayContextInternal { let transform: DAffine2 = text_list.attribute_cloned_or_default(ATTR_TRANSFORM, index); let Some(element) = text_list.element(index) else { continue }; - for (_, bezier, start_id, end_id) in element.segment_iter() { + let paths = paths_from_list_row_attributes(text_list, index).unwrap_or_else(|| no_paths()); + for (_, bezier, start_id, end_id) in element.segment_iter(paths) { let move_to = last_point != Some(start_id); last_point = Some(end_id); diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs index 53eb93bf8d2..54195133852 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_web.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_web.rs @@ -15,7 +15,7 @@ use glam::{DAffine2, DVec2}; use graphene_std::math::quad::Quad; use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::misc::{dvec2_to_point, point_to_dvec2}; -use graphene_std::vector::{PointId, SegmentId, Vector}; +use graphene_std::vector::{PointId, SegmentId, VectorGeometry}; use kurbo::{self, Affine, BezPath, CubicBez, ParamCurve, PathSeg}; use std::collections::HashMap; use wasm_bindgen::{JsCast, JsValue}; @@ -854,7 +854,7 @@ impl OverlayContext { } /// Used by the Pen and Path tools to outline the path of the shape. - pub fn outline_vector(&mut self, vector: &Vector, transform: DAffine2) { + pub fn outline_vector(&mut self, vector: &VectorGeometry, transform: DAffine2) { self.start_dpi_aware_transform(); self.render_context.begin_path(); diff --git a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs index d78eebc3d2a..abab0d2276c 100644 --- a/editor/src/messages/portfolio/document/utility_types/document_metadata.rs +++ b/editor/src/messages/portfolio/document/utility_types/document_metadata.rs @@ -7,10 +7,11 @@ use crate::messages::tool::common_functionality::graph_modification_utils; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeId; use graphene_std::Appearance; +use graphene_std::list::Item; use graphene_std::math::quad::Quad; use graphene_std::transform::Footprint; -use graphene_std::vector::Vector; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; +use graphene_std::vector::{Vector, VectorGeometry, no_paths, paths_from_item_attributes}; use kurbo::BezPath; use std::collections::{HashMap, HashSet}; use std::num::NonZeroU64; @@ -37,10 +38,10 @@ pub struct DocumentMetadata { /// The Text tool composes this with `transform_to_viewport(layer)` to position its drag cage. pub text_frames: HashMap, pub clip_targets: HashSet, - pub vector_modify: HashMap, + pub vector_modify: HashMap, /// Vector data keyed by layer ID, used as fallback when no Path node exists. /// This provides accurate SegmentIds for layers without explicit Path nodes. - pub layer_vector_data: HashMap>, + pub layer_vector_data: HashMap>>, /// Per-layer `ATTR_APPEARANCE` attribute, exposed so message handlers can read paint information that lives on the list. pub layer_appearance_attributes: HashMap>, /// Transform from document space to viewport space. @@ -243,13 +244,14 @@ impl DocumentMetadata { /// stroke geometry when the layer is a vector with a stroke style. Falls back to the click-target-based /// bounds for non-vector layers (groups, raster, text, color, gradient). pub fn bounding_box_document_with_stroke(&self, layer: LayerNodeIdentifier) -> Option<[DVec2; 2]> { - if let Some(vector) = self.layer_vector_data.get(&layer) { + if let Some(item) = self.layer_vector_data.get(&layer) { let stroke = self .layer_appearance_attributes .get(&layer) .and_then(|appearance| appearance.first_coverage_of(graphene_std::Cover::Stroke)) .map(graphene_std::Coverage::stroke_params); - if let Some(bounds) = vector.stroke_inclusive_bounding_box_with_transform(self.transform_to_document(layer), stroke.as_ref()) { + let paths = paths_from_item_attributes(item.as_ref()).unwrap_or_else(|| no_paths()); + if let Some(bounds) = item.element().stroke_inclusive_bounding_box_with_transform(paths, self.transform_to_document(layer), stroke.as_ref()) { return Some(bounds); } } diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface/structure.rs b/editor/src/messages/portfolio/document/utility_types/network_interface/structure.rs index 077bc6985f3..ac69f4d8417 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface/structure.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface/structure.rs @@ -1,30 +1,32 @@ use super::*; +use graphene_std::list::Item; +use graphene_std::vector::VectorGeometry; impl NodeNetworkInterface { - pub fn compute_modified_vector(&self, layer: LayerNodeIdentifier) -> Option { + pub fn compute_modified_vector(&self, layer: LayerNodeIdentifier) -> Option { let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, self); if let Some(path_node) = graph_layer.upstream_visible_node_id_from_name_in_layer(&DefinitionIdentifier::Network("Path".into())) - && let Some(vector) = self.document_metadata.vector_modify.get(&path_node) + && let Some(geometry) = self.document_metadata.vector_modify.get(&path_node) { - let mut modified = vector.clone(); + let mut modified = geometry.clone(); let path_node = self.document_network().nodes.get(&path_node); let modification_input = path_node.and_then(|node: &DocumentNode| node.inputs.get(1)).and_then(|input| input.as_value()); if let Some(TaggedValue::VectorModification(modification)) = modification_input { - modification.apply(&mut modified); + modification.apply(&mut modified.vector, &mut modified.paths); } return Some(modified); } - self.document_metadata.layer_vector_data.get(&layer).map(|arc| arc.as_ref().clone()) + self.document_metadata.layer_vector_data.get(&layer).map(|item| VectorGeometry::from_item(item)) } /// The vector geometry an upstream Path node would surface for editing. /// This is the result of `compute_modified_vector`, but only if a visible 'Path' node is actually upstream. /// Useful for tool overlays and snap target collection usages that want to match the Path tool's view /// (e.g. the pre-solidified centerline for a Solidify Stroke layer) and otherwise do nothing. - pub fn upstream_path_node_vector(&self, layer: LayerNodeIdentifier) -> Option { + pub fn upstream_path_node_vector(&self, layer: LayerNodeIdentifier) -> Option { let graph_layer = graph_modification_utils::NodeGraphLayer::new(layer, self); graph_layer.upstream_visible_node_id_from_name_in_layer(&DefinitionIdentifier::Network("Path".into()))?; self.compute_modified_vector(layer) @@ -46,11 +48,8 @@ impl NodeNetworkInterface { targets.push(ClickTargetType::Path(combined)); } - for &point_id in vector.point_domain.ids() { - if !vector.any_connected(point_id) { - let position = vector.point_domain.position_from_id(point_id).unwrap_or_default(); - targets.push(ClickTargetType::FreePoint(FreePoint::new(point_id, position))); - } + for (point_id, position) in vector.free_points() { + targets.push(ClickTargetType::FreePoint(FreePoint::new(point_id, position))); } Some(targets) @@ -183,13 +182,13 @@ impl NodeNetworkInterface { } /// Update the vector modify of the layers - pub fn update_vector_modify(&mut self, new_vector_modify: HashMap) { + pub fn update_vector_modify(&mut self, new_vector_modify: HashMap) { self.document_metadata.mark_changed(); self.document_metadata.vector_modify = new_vector_modify; } /// Update the layer vector data (for layers without Path nodes) - pub fn update_vector_data(&mut self, new_layer_vector_data: HashMap>) { + pub fn update_vector_data(&mut self, new_layer_vector_data: HashMap>>) { self.document_metadata.mark_changed(); self.document_metadata.layer_vector_data = new_layer_vector_data; } diff --git a/editor/src/messages/portfolio/document/utility_types/transformation.rs b/editor/src/messages/portfolio/document/utility_types/transformation.rs index 36bd24b4ee6..ad0b79b4461 100644 --- a/editor/src/messages/portfolio/document/utility_types/transformation.rs +++ b/editor/src/messages/portfolio/document/utility_types/transformation.rs @@ -102,7 +102,7 @@ impl OriginalTransforms { // Anchors also move their handles let anchor_ids = selected_points.iter().filter_map(|point| point.as_anchor()); - let anchors = anchor_ids.filter_map(|id| vector.point_domain.position_from_id(id).map(|pos| (id, AnchorPoint { initial: pos, current: pos }))); + let anchors = anchor_ids.filter_map(|id| vector.position_from_id(id).map(|pos| (id, AnchorPoint { initial: pos, current: pos }))); let anchors = anchors.collect(); let selected_handles = selected_points.iter().filter_map(|point| point.as_handle()); @@ -112,13 +112,13 @@ impl OriginalTransforms { let handles = all_handles .filter_map(|id| { - let anchor = id.to_manipulator_point().get_anchor(&vector)?; - let initial = id.to_manipulator_point().get_position(&vector)?; - let relative = vector.point_domain.position_from_id(anchor)?; + let anchor = vector.anchor_of(id.to_manipulator_point())?; + let initial = vector.position_of(id.to_manipulator_point())?; + let relative = vector.position_from_id(anchor)?; let other_handle = vector .other_colinear_handle(id) .filter(|other| !selected_points.contains(&other.to_manipulator_point()) && !selected_points.contains(&ManipulatorPointId::Anchor(anchor))); - let mirror = other_handle.and_then(|id| Some((id, id.to_manipulator_point().get_position(&vector)?))); + let mirror = other_handle.and_then(|id| Some((id, vector.position_of(id.to_manipulator_point())?))); Some((id, HandlePoint { initial, relative, anchor, mirror })) }) diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 7da430d59a2..d3b73dd8576 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -175,15 +175,15 @@ pub fn merge_points(document: &DocumentMessageHandler, layer: LayerNodeIdentifie }; let mut handles = [None; 2]; - if let Some(handle_position) = ManipulatorPointId::PrimaryHandle(segment).get_position(&vector) { - let anchor_position = ManipulatorPointId::Anchor(segment_start_point).get_position(&vector).unwrap(); + if let Some(handle_position) = vector.position_of(ManipulatorPointId::PrimaryHandle(segment)) { + let anchor_position = vector.position_of(ManipulatorPointId::Anchor(segment_start_point)).unwrap(); let handle_position = transform.transform_point2(handle_position); let anchor_position = transform.transform_point2(anchor_position); let anchor_to_handle = handle_position - anchor_position; handles[0] = Some(anchor_to_handle); } - if let Some(handle_position) = ManipulatorPointId::EndHandle(segment).get_position(&vector) { - let anchor_position = ManipulatorPointId::Anchor(segment_end_point).get_position(&vector).unwrap(); + if let Some(handle_position) = vector.position_of(ManipulatorPointId::EndHandle(segment)) { + let anchor_position = vector.position_of(ManipulatorPointId::Anchor(segment_end_point)).unwrap(); let handle_position = transform.transform_point2(handle_position); let anchor_position = transform.transform_point2(anchor_position); let anchor_to_handle = handle_position - anchor_position; diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 50ccce48d45..ecb9ba72d6d 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -14,7 +14,7 @@ use graphene_std::memo::IORecord; use graphene_std::raster::{CPU, Raster}; use graphene_std::renderer::{RenderMetadata, graphic_list_bounding_box}; use graphene_std::transform::Footprint; -use graphene_std::vector::{Vector, graphic_types}; +use graphene_std::vector::{Vector, VectorGeometry, graphic_types, no_paths, paths_from_list_row_attributes}; use graphene_std::{ATTR_TRANSFORM, Context, Graphic}; use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta; use std::any::Any; @@ -36,7 +36,7 @@ pub struct ExecutionResponse { execution_id: u64, result: Result, responses: VecDeque, - vector_modify: HashMap, + vector_modify: HashMap, /// The resulting value from the temporary inspected during execution inspect_result: Option, } @@ -838,7 +838,8 @@ fn measure_fill_geometry(data: &Arc) -> Option<(DAffine2, if let Some(list) = introspected_output::>(data) { let vector = list.element(0)?; let item_transform: DAffine2 = list.attribute_cloned_or_default(ATTR_TRANSFORM, 0); - let bounds = vector.nonzero_bounding_box(); + let paths = paths_from_list_row_attributes(&list, 0).unwrap_or_else(|| no_paths()); + let bounds = vector.nonzero_bounding_box(paths); let bounding_box_affine = DAffine2::from_scale_angle_translation(bounds[1] - bounds[0], 0., bounds[0]); return Some((bounding_box_affine, item_transform)); } diff --git a/editor/src/node_graph_executor/runtime.rs b/editor/src/node_graph_executor/runtime.rs index 8ce1ccea575..26f1307866f 100644 --- a/editor/src/node_graph_executor/runtime.rs +++ b/editor/src/node_graph_executor/runtime.rs @@ -18,8 +18,8 @@ use graphene_std::platform_application_io::canvas_utils::{Canvas, CanvasSurface, use graphene_std::raster_types::Raster; use graphene_std::renderer::{Render, RenderParams, RenderSvgSegmentList, SvgRender, SvgSegment}; use graphene_std::transform::RenderQuality; -use graphene_std::vector::Vector; use graphene_std::vector::style::RenderMode; +use graphene_std::vector::{Vector, VectorGeometry, paths_from_list_row_attributes}; use graphene_std::{Artboard, Context, Graphic}; use interpreted_executor::dynamic_executor::{DynamicExecutor, IntrospectError, ResolvedDocumentNodeTypesDelta}; use interpreted_executor::util::wrap_network_in_scope; @@ -54,7 +54,7 @@ pub struct NodeRuntime { // TODO: Remove, it doesn't need to be persisted anymore /// The current renders of the thumbnails for layer nodes. thumbnail_renders: HashMap>, - vector_modify: HashMap, + vector_modify: HashMap, /// Cached surface for Wasm viewport rendering (reused across frames) #[cfg(all(target_family = "wasm", feature = "gpu", feature = "wasm"))] @@ -432,7 +432,11 @@ impl NodeRuntime { // Vector list: vector modifications else if let Some(io) = introspected_data.downcast_ref::>>() { // Insert the vector modify - self.vector_modify.insert(parent_network_node_id, io.output.element(0).cloned().unwrap_or_default()); + let geometry = VectorGeometry::new( + io.output.element(0).cloned().unwrap_or_default(), + paths_from_list_row_attributes(&io.output, 0).cloned().unwrap_or_default(), + ); + self.vector_modify.insert(parent_network_node_id, geometry); } // String list: thumbnail (bounds need text layout, which the `BoundingBox` trait can't do for a bare `String`) else if let Some(io) = introspected_data.downcast_ref::>>() { @@ -455,7 +459,7 @@ impl NodeRuntime { Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &singleton, bounds, responses) } } else if let Some(io) = introspected_data.downcast_ref::>>() { - self.vector_modify.insert(parent_network_node_id, io.output.element().clone()); + self.vector_modify.insert(parent_network_node_id, VectorGeometry::from_item(&io.output)); } else if let Some(io) = introspected_data.downcast_ref::>>() { if update_thumbnails { let singleton = List::new_from_item(io.output.clone()); diff --git a/node-graph/libraries/vector-types/src/vector/geometry.rs b/node-graph/libraries/vector-types/src/vector/geometry.rs new file mode 100644 index 00000000000..f20f22472bb --- /dev/null +++ b/node-graph/libraries/vector-types/src/vector/geometry.rs @@ -0,0 +1,252 @@ +use super::misc::{HandleId, ManipulatorGroup, ManipulatorPointId}; +use super::path::{Path, paths_from_item_attributes}; +use super::vector_attributes::{FoundSubpath, PointId, SegmentId}; +use super::vector_types::Vector; +use core_types::list::{ATTR_PATHS, Item, List}; +use glam::{DAffine2, DVec2}; +use kurbo::{BezPath, PathSeg, Rect}; + +/// A vector element bundled with its paths, so editor-side callers can query geometry without +/// threading the holder's paths attribute through every call. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct VectorGeometry { + pub vector: Vector, + pub paths: List, +} + +impl VectorGeometry { + pub fn new(vector: Vector, paths: List) -> Self { + Self { vector, paths } + } + + /// Clones a holder item's element and paths attribute into one bundle. + pub fn from_item(item: &Item) -> Self { + Self { + vector: item.element().clone(), + paths: paths_from_item_attributes(item).cloned().unwrap_or_default(), + } + } + + /// Rebuilds a holder item carrying this bundle's element and paths. + pub fn into_item(self) -> Item { + Item::new_from_element(self.vector).with_attribute(ATTR_PATHS, self.paths) + } + + pub fn point_count(&self) -> usize { + self.vector.point_count() + } + + pub fn position(&self, index: usize) -> Option { + self.vector.position(index) + } + + pub fn positions(&self) -> impl Iterator + '_ { + self.vector.positions() + } + + pub fn point_id_at(&self, index: u32) -> Option { + self.vector.point_id_at(index) + } + + pub fn index_of_point_id(&self, point: PointId) -> Option { + self.vector.index_of_point_id(point) + } + + /// The position of the pool point a point ID names. + pub fn position_from_id(&self, point: PointId) -> Option { + self.vector.index_of_point_id(point).and_then(|index| self.vector.position(index as usize)) + } + + pub fn anchor_positions(&self) -> Vec { + self.vector.anchor_positions(&self.paths) + } + + pub fn anchor_points(&self) -> impl Iterator { + self.vector.anchor_points(&self.paths) + } + + pub fn anchor_endpoints(&self) -> impl Iterator { + self.vector.anchor_endpoints(&self.paths) + } + + /// The ID and position of every anchor and free point, found in one pass over the paths. + pub fn anchors(&self) -> impl Iterator + '_ { + let anchor_indices = self.vector.anchor_indices(&self.paths); + anchor_indices.into_iter().filter_map(|index| Some((self.vector.point_id_at(index as u32)?, self.vector.position(index)?))) + } + + /// The ID and position of each pool point that no path references, found in one pass over the paths. + pub fn free_points(&self) -> impl Iterator + '_ { + let connectivity = self.vector.connectivity(&self.paths); + (0..self.vector.point_count() as u32) + .filter(move |&index| !connectivity.is_referenced(index)) + .filter_map(|index| Some((self.vector.point_id_at(index)?, self.vector.position(index as usize)?))) + } + + pub fn segment_iter(&self) -> impl Iterator + '_ { + self.vector.segment_iter(&self.paths) + } + + pub fn segment_ids(&self) -> Vec { + self.vector.segment_ids(&self.paths) + } + + pub fn segment_from_id(&self, id: SegmentId) -> Option { + self.vector.segment_from_id(&self.paths, id) + } + + pub fn segment_points_from_id(&self, id: SegmentId) -> Option<(PointId, PointId, PathSeg)> { + self.vector.segment_points_from_id(&self.paths, id) + } + + pub fn segment_with_pool_endpoints(&self, id: SegmentId) -> Option<(PathSeg, usize, usize)> { + self.vector.segment_with_pool_endpoints(&self.paths, id) + } + + /// Appends a lone segment between two existing points. See [`Vector::push`]. + pub fn push(&mut self, id: SegmentId, start: PointId, end: PointId, handles: (Option, Option)) { + self.vector.push(&mut self.paths, id, start, end, handles); + } + + pub fn segment_start_from_id(&self, segment: SegmentId) -> Option { + self.vector.segment_start_from_id(&self.paths, segment) + } + + pub fn segment_end_from_id(&self, segment: SegmentId) -> Option { + self.vector.segment_end_from_id(&self.paths, segment) + } + + pub fn start_point(&self) -> impl Iterator + '_ { + self.vector.start_point(&self.paths) + } + + pub fn end_point(&self) -> impl Iterator + '_ { + self.vector.end_point(&self.paths) + } + + pub fn other_point(&self, segment: SegmentId, current: PointId) -> Option { + self.vector.other_point(&self.paths, segment, current) + } + + pub fn stroke_bezpath_iter(&self) -> impl Iterator { + self.vector.stroke_bezpath_iter(&self.paths) + } + + pub fn stroke_manipulator_groups(&self) -> impl Iterator, bool)> { + self.vector.stroke_manipulator_groups(&self.paths) + } + + pub fn bounding_box(&self) -> Option<[DVec2; 2]> { + self.vector.bounding_box(&self.paths) + } + + pub fn bounding_box_rect(&self) -> Option { + self.vector.bounding_box_rect(&self.paths) + } + + pub fn bounding_box_with_transform(&self, transform: DAffine2) -> Option<[DVec2; 2]> { + self.vector.bounding_box_with_transform(&self.paths, transform) + } + + pub fn nonzero_bounding_box(&self) -> [DVec2; 2] { + self.vector.nonzero_bounding_box(&self.paths) + } + + pub fn stroke_inclusive_bounding_box_with_transform(&self, transform: DAffine2, stroke: Option<&super::style::Stroke>) -> Option<[DVec2; 2]> { + self.vector.stroke_inclusive_bounding_box_with_transform(&self.paths, transform, stroke) + } + + pub fn check_point_inside_shape(&self, transform: DAffine2, point: DVec2) -> bool { + self.vector.check_point_inside_shape(&self.paths, transform, point) + } + + pub fn start_connected(&self, point: PointId) -> impl Iterator { + self.vector.start_connected(&self.paths, point) + } + + pub fn end_connected(&self, point: PointId) -> impl Iterator { + self.vector.end_connected(&self.paths, point) + } + + pub fn all_connected(&self, point: PointId) -> impl Iterator { + self.vector.all_connected(&self.paths, point) + } + + pub fn connected_count(&self, point: PointId) -> usize { + self.vector.connected_count(&self.paths, point) + } + + pub fn any_connected(&self, point: PointId) -> bool { + self.vector.any_connected(&self.paths, point) + } + + pub fn connected_points(&self, current: PointId) -> impl Iterator { + self.vector.connected_points(&self.paths, current) + } + + pub fn connected_linear_segments(&self, point_id: PointId) -> usize { + self.vector.connected_linear_segments(&self.paths, point_id) + } + + pub fn colinear(&self, point: ManipulatorPointId) -> bool { + self.vector.colinear(&self.paths, point) + } + + pub fn other_colinear_handle(&self, handle: HandleId) -> Option { + self.vector.other_colinear_handle(&self.paths, handle) + } + + pub fn colinear_pairs(&self) -> Vec<[HandleId; 2]> { + self.vector.colinear_pairs(&self.paths) + } + + pub fn colinear_pair_containing(&self, handle: HandleId) -> Option<[HandleId; 2]> { + self.vector.colinear_pair_containing(&self.paths, handle) + } + + pub fn adjacent_segment(&self, manipulator_id: &ManipulatorPointId) -> Option<(PointId, SegmentId)> { + self.vector.adjacent_segment(&self.paths, manipulator_id) + } + + pub fn auto_join_paths(&self) -> Vec { + self.vector.auto_join_paths(&self.paths) + } + + pub fn bezpath_from_segments_ignore_discontinuities(&self, segments: impl Iterator) -> Option { + self.vector.bezpath_from_segments_ignore_discontinuities(segments) + } + + pub fn point_tangent(&self, point_index: usize) -> Option { + self.vector.point_tangent(&self.paths, point_index) + } + + /// The manipulator's position in layer space. See [`ManipulatorPointId::get_position`]. + pub fn position_of(&self, point: ManipulatorPointId) -> Option { + point.get_position(&self.vector, &self.paths) + } + + /// The position of the anchor the manipulator sits on or belongs to. + pub fn anchor_position_of(&self, point: ManipulatorPointId) -> Option { + point.get_anchor_position(&self.vector, &self.paths) + } + + /// The anchor the manipulator sits on or belongs to. + pub fn anchor_of(&self, point: ManipulatorPointId) -> Option { + point.get_anchor(&self.vector, &self.paths) + } + + /// The manipulator's handle pair. See [`ManipulatorPointId::get_handle_pair`]. + pub fn handle_pair_of(&self, point: ManipulatorPointId) -> Option<[HandleId; 2]> { + point.get_handle_pair(&self.vector, &self.paths) + } + + /// All handles connected to the manipulator's anchor, excluding itself when it is a handle. + pub fn all_connected_handles_of(&self, point: ManipulatorPointId) -> Option> { + point.get_all_connected_handles(&self.vector, &self.paths) + } + + /// The handle's length from its anchor. See [`HandleId::length`]. + pub fn handle_length(&self, handle: HandleId) -> f64 { + handle.length(&self.vector, &self.paths) + } +} diff --git a/node-graph/libraries/vector-types/src/vector/mod.rs b/node-graph/libraries/vector-types/src/vector/mod.rs index 0167856ecbd..c6eac682413 100644 --- a/node-graph/libraries/vector-types/src/vector/mod.rs +++ b/node-graph/libraries/vector-types/src/vector/mod.rs @@ -4,6 +4,7 @@ pub mod click_target; mod connectivity; mod contour; mod flatten; +mod geometry; pub mod misc; mod path; pub mod reference_point; @@ -15,7 +16,8 @@ mod vector_types; pub use addressing::*; pub use connectivity::*; pub use contour::*; -pub use flatten::vector_from_legacy_parts; +pub use flatten::{WorkingVector, vector_from_legacy_parts}; +pub use geometry::VectorGeometry; pub use path::*; pub use reference_point::*; pub use vector_attributes::*; diff --git a/node-graph/nodes/gstd/src/lib.rs b/node-graph/nodes/gstd/src/lib.rs index 4090049bd11..e75853f9a6f 100644 --- a/node-graph/nodes/gstd/src/lib.rs +++ b/node-graph/nodes/gstd/src/lib.rs @@ -32,6 +32,7 @@ pub mod vector { pub use vector_types::vector::algorithms; pub use vector_types::vector::click_target; pub use vector_types::vector::misc::HandleId; + pub use vector_types::vector::{CurveType, Path, VectorGeometry, WorkingVector, no_paths, paths_from_item_attributes, paths_from_list_row_attributes}; pub use vector_types::vector::{PointId, SegmentId}; pub use vector_types::vector::{deserialize_hashmap, serialize_hashmap, serialize_hashmap_as_sorted_object}; From 32abef5e944be6ccf17916335db2cfcccdff62b0 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 21 Aug 2026 21:53:58 -0700 Subject: [PATCH 5/8] Port the editor tools to the vector geometry bundle --- .../tool/common_functionality/shape_editor.rs | 202 ++++++++------- .../shapes/shape_utility.rs | 2 +- .../snap_session/scene_mirror.rs | 2 +- .../common_functionality/utility_functions.rs | 44 ++-- .../tool/tool_messages/freehand_tool.rs | 32 ++- .../messages/tool/tool_messages/path_tool.rs | 229 ++++++++++++------ .../messages/tool/tool_messages/pen_tool.rs | 114 +++++---- .../tool/tool_messages/spline_tool.rs | 28 +-- .../transform_layer_message_handler.rs | 34 +-- 9 files changed, 375 insertions(+), 312 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index 186dd2d68d0..f2ab877275b 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -12,7 +12,7 @@ use glam::{DAffine2, DVec2}; use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table; use graphene_std::vector::algorithms::shapes::polyline_bezpath; use graphene_std::vector::misc::{BezierHandles, HandleId, ManipulatorPointId, PathSegPoints, bezpath_from_manipulator_groups, dvec2_to_point, pathseg_points, point_to_dvec2, segment_to_handles}; -use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModificationType}; +use graphene_std::vector::{HandleExt, PointId, SegmentId, VectorGeometry, VectorModificationType}; use kurbo::{Affine, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveNearest, PathSeg, Rect, Shape}; use std::f64::consts::TAU; @@ -163,13 +163,13 @@ pub struct ShapeState { pub struct SelectedPointsInfo { pub points: Vec, pub offset: DVec2, - pub vector: Vector, + pub vector: VectorGeometry, } #[derive(Debug)] pub struct SelectedSegmentsInfo { pub segments: Vec, - pub vector: Vector, + pub vector: VectorGeometry, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -325,10 +325,7 @@ impl ClosestSegment { } else { let [first_point, last_point] = self.points(); if let Some(vector) = document.network_interface.compute_modified_vector(self.layer()) { - if let (Some(pos1), Some(pos2)) = ( - ManipulatorPointId::Anchor(first_point).get_position(&vector), - ManipulatorPointId::Anchor(last_point).get_position(&vector), - ) { + if let (Some(pos1), Some(pos2)) = (vector.position_of(ManipulatorPointId::Anchor(first_point)), vector.position_of(ManipulatorPointId::Anchor(last_point))) { (pos1 - pos2).try_normalize() } else { None @@ -441,8 +438,8 @@ impl ShapeState { responses: &mut VecDeque, ) { // Get the local positions of the selected points - let start_local_pos = document.network_interface.compute_modified_vector(layer1).and_then(|v| v.point_domain.position_from_id(start_point)); - let end_local_pos = document.network_interface.compute_modified_vector(layer2).and_then(|v| v.point_domain.position_from_id(end_point)); + let start_local_pos = document.network_interface.compute_modified_vector(layer1).and_then(|v| v.position_from_id(start_point)); + let end_local_pos = document.network_interface.compute_modified_vector(layer2).and_then(|v| v.position_from_id(end_point)); // Transform to document/world space let start_transform = document.metadata().transform_to_document(layer1); @@ -541,7 +538,7 @@ impl ShapeState { for &layer in self.selected_shape_state.keys() { let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; - let endpoints: Vec = vector.point_domain.ids().iter().copied().filter(|&point_id| vector.all_connected(point_id).count() == 1).collect(); + let endpoints: Vec = vector.anchor_endpoints().collect(); if endpoints.len() == 2 { let start_point = endpoints[0]; @@ -575,7 +572,7 @@ impl ShapeState { if let Some((layer, manipulator_point_id)) = self.find_nearest_visible_point_indices(network_interface, mouse_position, select_threshold, path_overlay_mode, frontier_handles_info) { let vector = network_interface.compute_modified_vector(layer)?; - let point_position = manipulator_point_id.get_position(&vector)?; + let point_position = vector.position_of(manipulator_point_id)?; let selected_shape_state = self.selected_shape_state.get(&layer)?; let already_selected = selected_shape_state.is_point_selected(manipulator_point_id); @@ -629,7 +626,7 @@ impl ShapeState { return None; } let vector = network_interface.compute_modified_vector(layer)?; - let point_position = manipulator_point_id.get_position(&vector)?; + let point_position = vector.position_of(manipulator_point_id)?; let selected_shape_state = self.selected_shape_state.get(&layer)?; // Check if point is visible under current overlay mode or not let selected_segments_for_layer = selected_segments_for_layer(&vector, selected_shape_state); @@ -701,12 +698,12 @@ impl ShapeState { if selected_stack.is_empty() { // Fall back on just selecting all points/segments in the layer if points { - for &point in vector.point_domain.ids() { + for point in vector.anchor_points() { state.select_point(ManipulatorPointId::Anchor(point)); } } if segments { - for &segment in vector.segment_domain.ids() { + for segment in vector.segment_ids() { state.select_segment(segment); } } @@ -761,7 +758,7 @@ impl ShapeState { state.clear_points(); - for &point in vector.point_domain.ids() { + for point in vector.anchor_points() { state.select_point(ManipulatorPointId::Anchor(point)) } } @@ -770,7 +767,7 @@ impl ShapeState { fn select_all_segments_in_layer_with_state(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, state: &mut SelectedLayerState) { let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return }; - for &segment in vector.segment_domain.ids() { + for segment in vector.segment_ids() { state.select_segment(segment); } } @@ -880,7 +877,7 @@ impl ShapeState { }); } - pub fn move_anchor(&self, point: PointId, vector: &Vector, delta: DVec2, layer: LayerNodeIdentifier, selected: Option<&SelectedLayerState>, responses: &mut VecDeque) { + pub fn move_anchor(&self, point: PointId, vector: &VectorGeometry, delta: DVec2, layer: LayerNodeIdentifier, selected: Option<&SelectedLayerState>, responses: &mut VecDeque) { // Move anchor responses.add(GraphOperationMessage::Vector { layer, @@ -921,7 +918,7 @@ impl ShapeState { let vector = network_interface.compute_modified_vector(layer)?; let transform = network_interface.document_metadata().transform_to_document_if_feeds(layer, network_interface).inverse(); let position = transform.transform_point2(new_position); - let current_position = point.get_position(&vector)?; + let current_position = vector.position_of(*point)?; let delta = position - current_position; match *point { @@ -929,18 +926,18 @@ impl ShapeState { ManipulatorPointId::PrimaryHandle(segment) => { self.move_primary(segment, delta, layer, responses); if let Some(handle) = point.as_handle() - && let Some(handles) = vector.colinear_manipulators.iter().find(|handles| handles[0] == handle || handles[1] == handle) + && let Some(handles) = vector.colinear_pair_containing(handle) { - let modification_type = VectorModificationType::SetG1Continuous { handles: *handles, enabled: false }; + let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); } } ManipulatorPointId::EndHandle(segment) => { self.move_end(segment, delta, layer, responses); if let Some(handle) = point.as_handle() - && let Some(handles) = vector.colinear_manipulators.iter().find(|handles| handles[0] == handle || handles[1] == handle) + && let Some(handles) = vector.colinear_pair_containing(handle) { - let modification_type = VectorModificationType::SetG1Continuous { handles: *handles, enabled: false }; + let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); } } @@ -960,7 +957,7 @@ impl ShapeState { .flat_map(|(data, selection_state)| { selection_state.selected_points.iter().filter_map(move |&point| { let Some(data) = &data else { return None }; - let _ = point.get_handle_pair(data)?; // ignores the endpoints. + let _ = data.handle_pair_of(point)?; // ignores the endpoints. Some(data.colinear(point)) }) }); @@ -972,12 +969,12 @@ impl ShapeState { if first_is_colinear { ManipulatorAngle::Colinear } else { ManipulatorAngle::Free } } - pub fn convert_manipulator_handles_to_colinear(&self, vector: &Vector, point_id: PointId, responses: &mut VecDeque, layer: LayerNodeIdentifier) { - let Some(anchor_position) = ManipulatorPointId::Anchor(point_id).get_position(vector) else { + pub fn convert_manipulator_handles_to_colinear(&self, vector: &VectorGeometry, point_id: PointId, responses: &mut VecDeque, layer: LayerNodeIdentifier) { + let Some(anchor_position) = vector.position_of(ManipulatorPointId::Anchor(point_id)) else { return; }; let handles = vector.all_connected(point_id).take(2).collect::>(); - let non_zero_handles = handles.iter().filter(|handle| handle.length(vector) > 1e-6).count(); + let non_zero_handles = handles.iter().filter(|handle| vector.handle_length(**handle) > 1e-6).count(); let handle_segments = handles.iter().map(|handles| handles.segment).collect::>(); // Check if the anchor is connected to linear segments and has no handles @@ -985,7 +982,7 @@ impl ShapeState { // Grab the next and previous manipulator groups by simply looking at the next / previous index let points = handles.iter().map(|handle| vector.other_point(handle.segment, point_id)); - let anchor_positions = points.map(|point| point.and_then(|point| ManipulatorPointId::Anchor(point).get_position(vector))).collect::>(); + let anchor_positions = points.map(|point| point.and_then(|point| vector.position_of(ManipulatorPointId::Anchor(point)))).collect::>(); let mut segment_angle = 0.; let mut segment_count = 0.; @@ -1024,15 +1021,14 @@ impl ShapeState { if non_zero_handles != 0 && !linear_segments { let [a, b] = handles.as_slice() else { return }; - let (non_zero_handle, zero_handle) = if a.length(vector) > 1e-6 { (a, b) } else { (b, a) }; - let Some(direction) = non_zero_handle - .to_manipulator_point() - .get_position(vector) + let (non_zero_handle, zero_handle) = if vector.handle_length(*a) > 1e-6 { (a, b) } else { (b, a) }; + let Some(direction) = vector + .position_of(non_zero_handle.to_manipulator_point()) .and_then(|position| (position - anchor_position).try_normalize()) else { return; }; - let new_position = -direction * non_zero_handle.length(vector); + let new_position = -direction * vector.handle_length(*non_zero_handle); let modification_type = zero_handle.set_relative_position(new_position); responses.add(GraphOperationMessage::Vector { layer, modification_type }); } else { @@ -1053,7 +1049,7 @@ impl ShapeState { responses.add(GraphOperationMessage::Vector { layer, modification_type }); // Create the opposite handle if it doesn't exist (if it is not a cubic segment) - if handle.opposite().to_manipulator_point().get_position(vector).is_none() { + if vector.position_of(handle.opposite().to_manipulator_point()).is_none() { let modification_type = handle.opposite().set_relative_position(DVec2::ZERO); responses.add(GraphOperationMessage::Vector { layer, modification_type }); } @@ -1085,28 +1081,27 @@ impl ShapeState { // Here we take handles as the current handle and the most opposite non-colinear-handle - let is_handle_colinear = |handle: HandleId| -> bool { vector.colinear_manipulators.iter().any(|&handles| handles[0] == handle || handles[1] == handle) }; + let is_handle_colinear = |handle: HandleId| -> bool { vector.colinear(handle.to_manipulator_point()) }; let other_handles = if matches!(point, ManipulatorPointId::Anchor(_)) { - point.get_handle_pair(&vector) + vector.handle_pair_of(point) } else { - let Some(anchor) = point.get_anchor_position(&vector) else { continue }; - let Some(orig_handle_pos) = point.get_position(&vector) else { continue }; + let Some(anchor) = vector.anchor_position_of(point) else { continue }; + let Some(orig_handle_pos) = vector.position_of(point) else { continue }; let Some(v_orig) = (orig_handle_pos - anchor).try_normalize() else { continue }; - point.get_all_connected_handles(&vector).and_then(|handles| { + vector.all_connected_handles_of(point).and_then(|handles| { let mut non_colinear_handles = handles.iter().filter(|&handle| !is_handle_colinear(*handle)).clone().collect::>(); // Skip handles with no position or with the same position as the anchor non_colinear_handles.retain(|&handle| { - let Some(handle) = handle.to_manipulator_point().get_position(&vector) else { return false }; + let Some(handle) = vector.position_of(handle.to_manipulator_point()) else { return false }; (handle - anchor).is_non_zero() }); // Find the most colinear handle (maximum absolute angle from the original) let most_colinear = non_colinear_handles.into_iter().max_by(|&handle_a, &handle_b| { - let [Some(angle_a), Some(angle_b)] = - [handle_a, handle_b].map(|handle| handle.to_manipulator_point().get_position(&vector).and_then(|pos| v_orig.try_angle_to(pos - anchor))) + let [Some(angle_a), Some(angle_b)] = [handle_a, handle_b].map(|handle| vector.position_of(handle.to_manipulator_point()).and_then(|pos| v_orig.try_angle_to(pos - anchor))) else { error!("invalid handles (e.g. no handle or zero length) should be filtered above"); return core::cmp::Ordering::Equal; @@ -1133,13 +1128,13 @@ impl ShapeState { skip_set.insert(handles); let [selected0, selected1] = handles.map(|handle| layer_state.selected_points.contains(&handle.to_manipulator_point())); - let handle_positions = handles.map(|handle| handle.to_manipulator_point().get_position(&vector)); + let handle_positions = handles.map(|handle| vector.position_of(handle.to_manipulator_point())); - let Some(anchor_id) = point.get_anchor(&vector) else { continue }; - let Some(anchor) = vector.point_domain.position_from_id(anchor_id) else { continue }; + let Some(anchor_id) = vector.anchor_of(point) else { continue }; + let Some(anchor) = vector.position_from_id(anchor_id) else { continue }; let anchor_points = handles.map(|handle| vector.other_point(handle.segment, anchor_id)); - let anchor_positions = anchor_points.map(|point| point.and_then(|point| vector.point_domain.position_from_id(point))); + let anchor_positions = anchor_points.map(|point| point.and_then(|point| vector.position_from_id(point))); // If one handle is selected (but both exist), only move the other handle if let (true, [Some(pos0), Some(pos1)]) = ((selected0 ^ selected1), handle_positions) { @@ -1181,7 +1176,7 @@ impl ShapeState { responses.add(GraphOperationMessage::Vector { layer, modification_type }); // Create the opposite handle if it doesn't exist (if it is not a cubic segment) - if handles[index].opposite().to_manipulator_point().get_position(&vector).is_none() { + if vector.position_of(handles[index].opposite().to_manipulator_point()).is_none() { let modification_type = handles[index].opposite().set_relative_position(DVec2::ZERO); responses.add(GraphOperationMessage::Vector { layer, modification_type }); } @@ -1245,14 +1240,14 @@ impl ShapeState { ManipulatorPointId::EndHandle(segment) => HandleId::end(segment), }; - let Some(anchor_id) = point.get_anchor(&vector) else { continue }; + let Some(anchor_id) = vector.anchor_of(point) else { continue }; if state.is_point_selected(ManipulatorPointId::Anchor(anchor_id)) { continue; } - let Some(anchor_position) = vector.point_domain.position_from_id(anchor_id) else { continue }; + let Some(anchor_position) = vector.position_from_id(anchor_id) else { continue }; - let Some(handle_position) = point.get_position(&vector) else { continue }; + let Some(handle_position) = vector.position_of(point) else { continue }; let handle_position = handle_position + delta; let modification_type = handle.set_relative_position(handle_position - anchor_position); @@ -1283,7 +1278,7 @@ impl ShapeState { } else { // TODO: Is this equivalent to `transform_to_document_space`? If changed, the before and after should be tested. let transform = document.metadata().document_to_viewport.inverse() * transform_to_viewport_space; - let Some(other_position) = other.to_manipulator_point().get_position(&vector) else { + let Some(other_position) = vector.position_of(other.to_manipulator_point()) else { continue; }; let direction = transform.transform_vector2(handle_position - anchor_position).try_normalize(); @@ -1308,14 +1303,14 @@ impl ShapeState { let vector = document.network_interface.compute_modified_vector(layer)?; let transform = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface); let opposing_handle_lengths = vector - .colinear_manipulators - .iter() - .filter_map(|&handles| { + .colinear_pairs() + .into_iter() + .filter_map(|handles| { // We will keep track of the opposing handle length when: // i) Exactly one handle is selected. // ii) The anchor is not selected. - let anchor = handles[0].to_manipulator_point().get_anchor(&vector)?; + let anchor = vector.anchor_of(handles[0].to_manipulator_point())?; let anchor_selected = state.is_point_selected(ManipulatorPointId::Anchor(anchor)); if anchor_selected { return None; @@ -1329,8 +1324,8 @@ impl ShapeState { _ => return None, }; - let opposing_handle_position = other.to_manipulator_point().get_position(&vector)?; - let anchor_position = vector.point_domain.position_from_id(anchor)?; + let opposing_handle_position = vector.position_of(other.to_manipulator_point())?; + let anchor_position = vector.position_from_id(anchor)?; let opposing_handle_length = transform.transform_vector2(opposing_handle_position - anchor_position).length(); Some((other, opposing_handle_length)) @@ -1341,7 +1336,7 @@ impl ShapeState { .collect::>() } - pub fn dissolve_segment(&self, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector: &Vector, segment: SegmentId, points: [PointId; 2]) { + pub fn dissolve_segment(&self, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector: &VectorGeometry, segment: SegmentId, points: [PointId; 2]) { // Checking which point is terminal point let is_point1_terminal = vector.connected_count(points[0]) == 1; let is_point2_terminal = vector.connected_count(points[1]) == 1; @@ -1349,7 +1344,7 @@ impl ShapeState { // Delete the segment and terminal points let modification_type = VectorModificationType::RemoveSegment { id: segment }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); - for &handles in vector.colinear_manipulators.iter().filter(|handles| handles.iter().any(|handle| handle.segment == segment)) { + for handles in vector.colinear_pairs().into_iter().filter(|handles| handles.iter().any(|handle| handle.segment == segment)) { let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); } @@ -1364,27 +1359,28 @@ impl ShapeState { } } - fn dissolve_anchor(anchor: PointId, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector: &Vector) -> Option<[(HandleId, PointId); 2]> { + fn dissolve_anchor(anchor: PointId, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector: &VectorGeometry) -> Option<[(HandleId, PointId); 2]> { // Delete point let modification_type = VectorModificationType::RemovePoint { id: anchor }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); // Delete connected segments + let colinear_pairs = vector.colinear_pairs(); for HandleId { segment, .. } in vector.all_connected(anchor) { let modification_type = VectorModificationType::RemoveSegment { id: segment }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); - for &handles in vector.colinear_manipulators.iter().filter(|handles| handles.iter().any(|handle| handle.segment == segment)) { + for handles in colinear_pairs.iter().copied().filter(|handles| handles.iter().any(|handle| handle.segment == segment)) { let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); } } // Add in new segment if possible - let mut handles = ManipulatorPointId::Anchor(anchor).get_handle_pair(vector)?; + let mut handles = vector.handle_pair_of(ManipulatorPointId::Anchor(anchor))?; handles.reverse(); let opposites = handles.map(|handle| handle.opposite()); - let [Some(start), Some(end)] = opposites.map(|opposite| opposite.to_manipulator_point().get_anchor(vector)) else { + let [Some(start), Some(end)] = opposites.map(|opposite| vector.anchor_of(opposite.to_manipulator_point())) else { return None; }; Some([(handles[0], start), (handles[1], end)]) @@ -1398,6 +1394,7 @@ impl ShapeState { let mut missing_anchors = HashMap::new(); let mut deleted_anchors = HashSet::new(); let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; + let colinear_pairs = vector.colinear_pairs(); let selected_segments = &state.selected_segments; @@ -1425,7 +1422,7 @@ impl ShapeState { responses.add(GraphOperationMessage::Vector { layer, modification_type }); // Disable the g1 continuous - for &handles in &vector.colinear_manipulators { + for &handles in &colinear_pairs { if handles.contains(&handle) { let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); @@ -1463,8 +1460,8 @@ impl ShapeState { // Grab the handles from the opposite side of the segment(s) being deleted and make it relative to the anchor let [handle_start, handle_end] = [start, end].map(|(handle, _)| { let handle = handle.opposite(); - let handle_position = handle.to_manipulator_point().get_position(&vector); - let relative_position = handle.to_manipulator_point().get_anchor(&vector).and_then(|anchor| vector.point_domain.position_from_id(anchor)); + let handle_position = vector.position_of(handle.to_manipulator_point()); + let relative_position = vector.anchor_of(handle.to_manipulator_point()).and_then(|anchor| vector.position_from_id(anchor)); handle_position.and_then(|handle| relative_position.map(|relative| handle - relative)).unwrap_or_default() }); @@ -1478,12 +1475,12 @@ impl ShapeState { responses.add(GraphOperationMessage::Vector { layer, modification_type }); - for &handles in vector.colinear_manipulators.iter() { + for &handles in &colinear_pairs { if !handles.iter().any(|&handle| handle == start.0.opposite() || handle == end.0.opposite()) { continue; } - let Some(anchor) = handles[0].to_manipulator_point().get_anchor(&vector) else { continue }; + let Some(anchor) = vector.anchor_of(handles[0].to_manipulator_point()) else { continue }; let Some(other) = handles.iter().find(|&&handle| handle != start.0.opposite() && handle != end.0.opposite()) else { continue; }; @@ -1551,15 +1548,16 @@ impl ShapeState { for (&layer, state) in &self.selected_shape_state { let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; + let colinear_pairs = vector.colinear_pairs(); for &delete in &state.selected_points { - let Some(point) = delete.get_anchor(&vector) else { continue }; - let Some(pos) = vector.point_domain.position_from_id(point) else { continue }; + let Some(point) = vector.anchor_of(delete) else { continue }; + let Some(pos) = vector.position_from_id(point) else { continue }; let mut used_initial_point = false; for handle in vector.all_connected(point) { // Disable the g1 continuous - for &handles in &vector.colinear_manipulators { + for &handles in &colinear_pairs { if !transaction_started { responses.add(DocumentMessage::AddTransaction); transaction_started = true; @@ -1610,7 +1608,7 @@ impl ShapeState { let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; for delete in std::mem::take(&mut state.selected_points) { - let Some(point) = delete.get_anchor(&vector) else { continue }; + let Some(point) = vector.anchor_of(delete) else { continue }; if !transaction_started { responses.add(DocumentMessage::AddTransaction); @@ -1635,19 +1633,20 @@ impl ShapeState { pub fn disable_colinear_handles_state_on_selected(&self, network_interface: &NodeNetworkInterface, responses: &mut VecDeque) { for (&layer, state) in &self.selected_shape_state { let Some(vector) = network_interface.compute_modified_vector(layer) else { continue }; + let colinear_pairs = vector.colinear_pairs(); for &point in &state.selected_points { if let ManipulatorPointId::Anchor(point) = point { for connected in vector.all_connected(point) { - if let Some(&handles) = vector.colinear_manipulators.iter().find(|target| target.contains(&connected)) { + if let Some(&handles) = colinear_pairs.iter().find(|pair| pair.contains(&connected)) { let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); } } } else if let Some(handle) = point.as_handle() - && let Some(handles) = vector.colinear_manipulators.iter().find(|handles| handles[0] == handle || handles[1] == handle) + && let Some(handles) = vector.colinear_pair_containing(handle) { - let modification_type = VectorModificationType::SetG1Continuous { handles: *handles, enabled: false }; + let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); } } @@ -1750,7 +1749,7 @@ impl ShapeState { } // Anchors - for (&id, &point) in vector.point_domain.ids().iter().zip(vector.point_domain.positions()) { + for (id, point) in vector.anchors() { let point = viewspace.transform_point2(point); if point.distance_squared(pos) <= closest_distance_squared { @@ -1773,6 +1772,7 @@ impl ShapeState { let mut closest_distance_squared: f64 = tolerance * tolerance; let vector = network_interface.compute_modified_vector(layer)?; + let colinear_pairs = vector.colinear_pairs(); for (segment_id, mut segment, start, end) in vector.segment_iter() { let t = segment.nearest(dvec2_to_point(layer_pos), DEFAULT_ACCURACY).t; @@ -1793,10 +1793,10 @@ impl ShapeState { } } - let primary_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::primary(segment_id))); - let end_handle = vector.colinear_manipulators.iter().find(|handles| handles.contains(&HandleId::end(segment_id))); - let primary_handle = primary_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment_id)); - let end_handle = end_handle.and_then(|&handles| handles.into_iter().find(|handle| handle.segment != segment_id)); + let primary_handle = colinear_pairs.iter().copied().find(|handles| handles.contains(&HandleId::primary(segment_id))); + let end_handle = colinear_pairs.iter().copied().find(|handles| handles.contains(&HandleId::end(segment_id))); + let primary_handle = primary_handle.and_then(|handles| handles.into_iter().find(|handle| handle.segment != segment_id)); + let end_handle = end_handle.and_then(|handles| handles.into_iter().find(|handle| handle.segment != segment_id)); closest = Some(ClosestSegment { segment: segment_id, @@ -1831,7 +1831,7 @@ impl ShapeState { if point.as_anchor().is_some() { return PointSelectState::Anchor; } - if point.get_handle_pair(&vector).is_some() { + if vector.handle_pair_of(*point).is_some() { return PointSelectState::HandleWithPair; } } @@ -1848,7 +1848,7 @@ impl ShapeState { if point.as_anchor().is_some() { return false; } - if point.get_handle_pair(&vector).is_some() { + if vector.handle_pair_of(*point).is_some() { return true; } } @@ -1869,9 +1869,9 @@ impl ShapeState { continue; } - if let Some(other_handles) = point.get_all_connected_handles(&vector) { - let Some(anchor) = point.get_anchor_position(&vector) else { continue }; - let Some(orig_handle_pos) = point.get_position(&vector) else { continue }; + if let Some(other_handles) = vector.all_connected_handles_of(*point) { + let Some(anchor) = vector.anchor_position_of(*point) else { continue }; + let Some(orig_handle_pos) = vector.position_of(*point) else { continue }; let Some(v_orig) = (orig_handle_pos - anchor).try_normalize() else { continue }; // Find the next closest handle in the clockwise sense @@ -1879,13 +1879,13 @@ impl ShapeState { // Skip handles with no position or with the same position as the anchor candidates.retain(|&handle| { - let Some(handle) = handle.to_manipulator_point().get_position(&vector) else { return false }; + let Some(handle) = vector.position_of(handle.to_manipulator_point()) else { return false }; (handle - anchor).is_non_zero() }); // Find the next handle let next_handle = candidates.into_iter().min_by(|&handle_a, &handle_b| { - let [Some(angle_a), Some(angle_b)] = [handle_a, handle_b].map(|handle| handle.to_manipulator_point().get_position(&vector).and_then(|pos| v_orig.try_angle_to(pos - anchor))) + let [Some(angle_a), Some(angle_b)] = [handle_a, handle_b].map(|handle| vector.position_of(handle.to_manipulator_point()).and_then(|pos| v_orig.try_angle_to(pos - anchor))) else { error!("invalid handles (e.g. no handle or zero length) should be filtered above"); return core::cmp::Ordering::Equal; @@ -1925,8 +1925,8 @@ impl ShapeState { let Some(vector) = network_interface.compute_modified_vector(layer) else { continue }; for point in self.selected_points().filter(|point| point.as_handle().is_some()) { - let anchor = point.get_anchor(&vector); - match point.get_handle_pair(&vector) { + let anchor = vector.anchor_of(*point); + match vector.handle_pair_of(*point) { Some(handles) => { points_to_select.push((layer, anchor, Some(handles[1].to_manipulator_point()))); } @@ -1961,10 +1961,10 @@ impl ShapeState { // Get the current point and its connected handles let selected_points = state.selected_points.clone(); if let Some(point) = selected_points.iter().next() { - if let Some(anchor) = point.get_anchor(&vector) { + if let Some(anchor) = vector.anchor_of(*point) { state.select_point(ManipulatorPointId::Anchor(anchor)); } - if let Some(handles) = point.get_handle_pair(&vector) { + if let Some(handles) = vector.handle_pair_of(*point) { state.select_point(handles[0].to_manipulator_point()); state.select_point(handles[1].to_manipulator_point()); } @@ -1995,12 +1995,12 @@ impl ShapeState { // Check by comparing the handle positions to the anchor if this manipulator group is a point for point in selected_points { let Some(point_id) = point.as_anchor() else { continue }; - let anchor = point.get_position(&vector)?; + let anchor = vector.position_of(*point)?; let handles = vector.all_connected(point_id); // TODO: Check if this method of finding non-colinear is really required let positions = handles - .filter_map(|handle| handle.to_manipulator_point().get_position(&vector)) + .filter_map(|handle| vector.position_of(handle.to_manipulator_point())) .filter(|&handle| anchor.abs_diff_eq(handle, 1e-5)) .count(); @@ -2010,6 +2010,7 @@ impl ShapeState { if positions != 0 || one_or_more_segment_linear { self.convert_manipulator_handles_to_colinear(&vector, point_id, responses, layer); } else { + let colinear_pairs = vector.colinear_pairs(); for handle in vector.all_connected(point_id) { let Some(path_segment) = vector.segment_from_id(handle.segment) else { continue }; @@ -2022,7 +2023,7 @@ impl ShapeState { responses.add(GraphOperationMessage::Vector { layer, modification_type }); // Set the manipulator to have non-colinear handles - for &handles in &vector.colinear_manipulators { + for handles in colinear_pairs.iter().copied() { if handles.contains(&HandleId::primary(segment)) { let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); @@ -2035,7 +2036,7 @@ impl ShapeState { responses.add(GraphOperationMessage::Vector { layer, modification_type }); // Set the manipulator to have non-colinear handles - for &handles in &vector.colinear_manipulators { + for handles in colinear_pairs.iter().copied() { if handles.contains(&handle) { let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); @@ -2093,7 +2094,7 @@ impl ShapeState { (_, SelectionChange::Shrink) => state.deselect_point(point), (ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_), _) => { let handle = point.as_handle().expect("Handle cannot be converted"); - if handle.length(&vector) > 0. { + if vector.handle_length(handle) > 0. { state.select_point(point); } } @@ -2151,15 +2152,6 @@ impl ShapeState { let Some(vector) = vector else { continue }; let transform = network_interface.document_metadata().transform_to_viewport_if_feeds(layer, network_interface); - assert_eq!(vector.segment_domain.ids().len(), vector.start_point().count()); - assert_eq!(vector.segment_domain.ids().len(), vector.end_point().count()); - for start in vector.start_point() { - assert!(vector.point_domain.ids().contains(&start)); - } - for end in vector.end_point() { - assert!(vector.point_domain.ids().contains(&end)); - } - let polygon_bezpath = if let SelectionShape::Lasso(polygon) = selection_shape { if polygon.len() < 2 { return (points_inside, segments_inside); @@ -2237,7 +2229,7 @@ impl ShapeState { } // Checking for selection of anchor points - for (&id, &position) in vector.point_domain.ids().iter().zip(vector.point_domain.positions()) { + for (id, position) in vector.anchors() { let transformed_position = transform.transform_point2(position); let select = match selection_shape { diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index 1ffb4538b02..9c072f8ccd6 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -260,7 +260,7 @@ pub fn anchor_overlays(document: &DocumentMessageHandler, overlay_context: &mut overlay_context.outline_vector(&vector, transform); - for (_, &position) in vector.point_domain.ids().iter().zip(vector.point_domain.positions()) { + for (_, position) in vector.anchors() { overlay_context.manipulator_anchor(transform.transform_point2(position), false, None); } } diff --git a/editor/src/messages/tool/common_functionality/snap_session/scene_mirror.rs b/editor/src/messages/tool/common_functionality/snap_session/scene_mirror.rs index 54e1ea7dc16..08d22caa886 100644 --- a/editor/src/messages/tool/common_functionality/snap_session/scene_mirror.rs +++ b/editor/src/messages/tool/common_functionality/snap_session/scene_mirror.rs @@ -130,7 +130,7 @@ fn mirror_layer(document: &DocumentMessageHandler, layer: LayerNodeIdentifier) - let curves_drawn: HashSet<[[i64; 2]; 4]> = drawn_curves.iter().map(rounded_curve).collect(); let role = |drawn: bool| if drawn { GeometryRole::Source } else { GeometryRole::Undrawn }; - anchors.extend(vector.point_domain.iter().map(|(id, position)| { + anchors.extend(vector.anchors().map(|(id, position)| { let position = to_document.transform_point2(position); SceneAnchor { position, diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index c70f44b0b29..8192407eced 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -14,7 +14,7 @@ use graph_craft::document::value::TaggedValue; use graphene_std::renderer::Quad; use graphene_std::vector::algorithms::bezpath_algorithms::pathseg_compute_lookup_table; use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point}; -use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModification, VectorModificationType}; +use graphene_std::vector::{HandleExt, PointId, SegmentId, VectorGeometry, VectorModification, VectorModificationType}; use kurbo::{CubicBez, DEFAULT_ACCURACY, ParamCurve, PathSeg, Point, Shape}; /// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable. @@ -49,7 +49,7 @@ fn closest_candidate_point( goal: DVec2, max_distance: f64, layers: impl Iterator, - candidates: impl Fn(&Vector) -> Vec, + candidates: impl Fn(&VectorGeometry) -> Vec, ) -> Option<(LayerNodeIdentifier, PointId, DVec2)> { let mut best = None; let mut best_distance_squared = max_distance * max_distance; @@ -63,7 +63,7 @@ fn closest_candidate_point( let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; for id in candidates(&vector) { - let Some(point) = vector.point_domain.position_from_id(id) else { continue }; + let Some(point) = vector.position_from_id(id) else { continue }; let distance_squared = viewspace.transform_point2(point).distance_squared(goal); if distance_squared < best_distance_squared { @@ -92,16 +92,16 @@ pub fn text_bounding_box(layer: LayerNodeIdentifier, document: &DocumentMessageH Quad::from_box([DVec2::ZERO, far]) } -pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector: &Vector, prefer_handle_direction: bool) -> Option { +pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector: &VectorGeometry, prefer_handle_direction: bool) -> Option { let is_start = |point: PointId, segment: SegmentId| vector.segment_start_from_id(segment) == Some(point); - let anchor_position = vector.point_domain.position_from_id(anchor)?; - let end_handle = ManipulatorPointId::EndHandle(segment).get_position(vector); - let start_handle = ManipulatorPointId::PrimaryHandle(segment).get_position(vector); + let anchor_position = vector.position_from_id(anchor)?; + let end_handle = vector.position_of(ManipulatorPointId::EndHandle(segment)); + let start_handle = vector.position_of(ManipulatorPointId::PrimaryHandle(segment)); let start_point = if is_start(anchor, segment) { - vector.segment_end_from_id(segment).and_then(|id| vector.point_domain.position_from_id(id)) + vector.segment_end_from_id(segment).and_then(|id| vector.position_from_id(id)) } else { - vector.segment_start_from_id(segment).and_then(|id| vector.point_domain.position_from_id(id)) + vector.segment_start_from_id(segment).and_then(|id| vector.position_from_id(id)) }; let required_handle = if is_start(anchor, segment) { @@ -119,9 +119,9 @@ pub fn calculate_segment_angle(anchor: PointId, segment: SegmentId, vector: &Vec required_handle.map(|handle| handle - anchor_position).and_then(|vector| DVec2::X.try_angle_to(vector)) } -pub fn adjust_handle_colinearity(handle: HandleId, anchor_position: DVec2, target_control_point: DVec2, vector: &Vector, layer: LayerNodeIdentifier, responses: &mut VecDeque) { +pub fn adjust_handle_colinearity(handle: HandleId, anchor_position: DVec2, target_control_point: DVec2, vector: &VectorGeometry, layer: LayerNodeIdentifier, responses: &mut VecDeque) { let Some(other_handle) = vector.other_colinear_handle(handle) else { return }; - let Some(handle_position) = other_handle.to_manipulator_point().get_position(vector) else { + let Some(handle_position) = vector.position_of(other_handle.to_manipulator_point()) else { return; }; let Some(direction) = (anchor_position - target_control_point).try_normalize() else { return }; @@ -136,12 +136,12 @@ pub fn restore_previous_handle_position( handle: HandleId, original_c: DVec2, anchor_position: DVec2, - vector: &Vector, + vector: &VectorGeometry, layer: LayerNodeIdentifier, responses: &mut VecDeque, ) -> Option { let other_handle = vector.other_colinear_handle(handle)?; - let handle_position = other_handle.to_manipulator_point().get_position(vector)?; + let handle_position = vector.position_of(other_handle.to_manipulator_point())?; let direction = (anchor_position - original_c).try_normalize()?; let old_relative_position = (handle_position - anchor_position).length() * direction; @@ -155,8 +155,16 @@ pub fn restore_previous_handle_position( Some(other_handle) } -pub fn restore_g1_continuity(handle: HandleId, other_handle: HandleId, control_point: DVec2, anchor_position: DVec2, vector: &Vector, layer: LayerNodeIdentifier, responses: &mut VecDeque) { - let Some(handle_position) = other_handle.to_manipulator_point().get_position(vector) else { +pub fn restore_g1_continuity( + handle: HandleId, + other_handle: HandleId, + control_point: DVec2, + anchor_position: DVec2, + vector: &VectorGeometry, + layer: LayerNodeIdentifier, + responses: &mut VecDeque, +) { + let Some(handle_position) = vector.position_of(other_handle.to_manipulator_point()) else { return; }; let Some(direction) = (anchor_position - control_point).try_normalize() else { return }; @@ -173,7 +181,7 @@ pub fn restore_g1_continuity(handle: HandleId, other_handle: HandleId, control_p /// Check whether a point is visible in the current overlay mode. pub fn is_visible_point( manipulator_point_id: ManipulatorPointId, - vector: &Vector, + vector: &VectorGeometry, path_overlay_mode: PathOverlayMode, frontier_handles_for_layer: Option<&HashMap>>, selected_segments: &[SegmentId], @@ -190,14 +198,14 @@ pub fn is_visible_point( } // Either the segment is a part of selected segments or the opposite handle is a part of existing selection - let Some(handle_pair) = manipulator_point_id.get_handle_pair(vector) else { return false }; + let Some(handle_pair) = vector.handle_pair_of(manipulator_point_id) else { return false }; let other_handle = handle_pair[1].to_manipulator_point(); // Return whether the list of selected points contain the other handle selected_points.contains(&other_handle) } (PathOverlayMode::FrontierHandles, false) => { - let Some(anchor) = manipulator_point_id.get_anchor(vector) else { + let Some(anchor) = vector.anchor_of(manipulator_point_id) else { warn!("No anchor for selected handle"); return false; }; diff --git a/editor/src/messages/tool/tool_messages/freehand_tool.rs b/editor/src/messages/tool/tool_messages/freehand_tool.rs index 7751093cfba..31f2c0f4f65 100644 --- a/editor/src/messages/tool/tool_messages/freehand_tool.rs +++ b/editor/src/messages/tool/tool_messages/freehand_tool.rs @@ -441,9 +441,9 @@ mod test_freehand { use crate::messages::tool::tool_messages::freehand_tool::FreehandOptionsUpdate; use crate::test_utils::test_prelude::*; use glam::{DAffine2, DMat2, DVec2}; - use graphene_std::vector::Vector; + use graphene_std::vector::VectorGeometry; - async fn get_vector_and_transform_list(editor: &mut EditorTestUtils) -> Vec<(Vector, DAffine2)> { + async fn get_vector_and_transform_list(editor: &mut EditorTestUtils) -> Vec<(VectorGeometry, DAffine2)> { let document = editor.active_document(); let layers = document.metadata().all_layers(); @@ -460,18 +460,18 @@ mod test_freehand { .collect() } - fn verify_path_points(vector_and_transform_list: &[(Vector, DAffine2)], expected_captured_points: &[DVec2], tolerance: f64) -> Result<(), String> { + fn verify_path_points(vector_and_transform_list: &[(VectorGeometry, DAffine2)], expected_captured_points: &[DVec2], tolerance: f64) -> Result<(), String> { assert_eq!(vector_and_transform_list.len(), 1, "There should be one item of Vector geometry"); let (vector, transform) = vector_and_transform_list .iter() - .find(|(data, _)| !data.point_domain.ids().is_empty()) + .find(|(data, _)| data.anchor_points().next().is_some()) .ok_or("Could not find path data")?; - let point_count = vector.point_domain.ids().len(); - let segment_count = vector.segment_domain.ids().len(); + let point_count = vector.anchor_points().count(); + let segment_count = vector.segment_ids().len(); - let actual_positions: Vec = vector.point_domain.positions().iter().map(|&position| transform.transform_point2(position)).collect(); + let actual_positions: Vec = vector.anchor_positions().iter().map(|&position| transform.transform_point2(position)).collect(); if segment_count != point_count - 1 { return Err(format!("Expected segments to be one less than points, got {segment_count} segments for {point_count} points")); @@ -555,8 +555,8 @@ mod test_freehand { assert!(!initial_vector_and_transform.is_empty(), "No vector geometry found after initial drawing"); let (initial_vector, _) = &initial_vector_and_transform[0]; - let initial_point_count = initial_vector.point_domain.ids().len(); - let initial_segment_count = initial_vector.segment_domain.ids().len(); + let initial_point_count = initial_vector.anchor_points().count(); + let initial_segment_count = initial_vector.segment_ids().len(); let existing_layer_id = { let document = editor.active_document(); @@ -613,8 +613,8 @@ mod test_freehand { assert_eq!(layer_count, 1, "Expected only one layer after drawing with Shift key"); let (final_vector, _) = &final_vector_and_transform[0]; - let final_point_count = final_vector.point_domain.ids().len(); - let final_segment_count = final_vector.segment_domain.ids().len(); + let final_point_count = final_vector.anchor_points().count(); + let final_segment_count = final_vector.segment_ids().len(); assert!( final_point_count > initial_point_count, @@ -704,7 +704,7 @@ mod test_freehand { fn point_and_segment_counts(editor: &EditorTestUtils, layer: LayerNodeIdentifier) -> (usize, usize) { let vector = editor.active_document().network_interface.compute_modified_vector(layer).expect("Layer should have vector geometry"); - (vector.point_domain.ids().len(), vector.segment_domain.ids().len()) + (vector.anchor_points().count(), vector.segment_ids().len()) } /// The viewport positions of the layer's anchors, split into the endpoints of its open paths and every other anchor. @@ -714,7 +714,7 @@ mod test_freehand { let transform = document.metadata().transform_to_viewport(layer); let endpoints: Vec<_> = vector.anchor_endpoints().collect(); - let viewport_position = |id| vector.point_domain.position_from_id(id).map(|position| transform.transform_point2(position)); + let viewport_position = |id| vector.position_from_id(id).map(|position| transform.transform_point2(position)); let endpoint_positions = endpoints.iter().filter_map(|&id| viewport_position(id)).collect(); let other_positions = vector.anchor_points().filter(|id| !endpoints.contains(id)).filter_map(viewport_position).collect(); @@ -833,11 +833,7 @@ mod test_freehand { let vector = document.network_interface.compute_modified_vector(layer).expect("Layer should have vector geometry"); let transform = document.metadata().transform_to_viewport(layer); for &pointer_position in &continuation[1..] { - let under_pointer = vector - .point_domain - .positions() - .iter() - .any(|&position| transform.transform_point2(position).distance(pointer_position) < 1.); + let under_pointer = vector.anchor_positions().iter().any(|&position| transform.transform_point2(position).distance(pointer_position) < 1.); assert!(under_pointer, "Expected a point under the pointer at {pointer_position:?}"); } } diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index b9c5d8d8003..493f5c61b08 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -23,6 +23,7 @@ use crate::messages::tool::common_functionality::shape_editor::{ use crate::messages::tool::common_functionality::snap_session::{FeatureMap, Locus, Moving, PointOptions, SnapSession, SnapView, nudge_translation, snapping_hints}; use crate::messages::tool::common_functionality::utility_functions::{calculate_segment_angle, find_two_param_best_approximate, make_path_editable_is_allowed}; use graphene_std::Color; +use graphene_std::list::List; use graphene_std::renderer::Quad; use graphene_std::transform::ReferencePoint; use graphene_std::uuid::NodeId; @@ -30,7 +31,7 @@ use graphene_std::vector::algorithms::util::pathseg_tangent; use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::misc::pathseg_points; use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point, point_to_dvec2, segment_to_handles}; -use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorModificationType}; +use graphene_std::vector::{HandleExt, PointId, SegmentId, Vector, VectorGeometry, VectorModification, VectorModificationType, WorkingVector}; use kurbo::{DEFAULT_ACCURACY, ParamCurve, ParamCurveNearest, PathSeg, Rect}; use std::vec; @@ -678,8 +679,8 @@ impl PathToolData { SelectionStatus::None => false, SelectionStatus::One(single_selected_point) => { let vector = document.network_interface.compute_modified_vector(single_selected_point.layer).unwrap(); - if single_selected_point.id.get_handle_pair(&vector).is_some() { - let anchor = single_selected_point.id.get_anchor(&vector).expect("Cannot find connected anchor"); + if vector.handle_pair_of(single_selected_point.id).is_some() { + let anchor = vector.anchor_of(single_selected_point.id).expect("Cannot find connected anchor"); vector.all_connected(anchor).count() <= 2 } else { false @@ -835,10 +836,11 @@ impl PathToolData { && let Some(first_handle) = vector.all_connected(point_id).next() { self.alt_clicked_on_anchor = true; + let colinear_pairs = vector.colinear_pairs(); for handle in vector.all_connected(point_id) { let modification_type = handle.set_relative_position(DVec2::ZERO); responses.add(GraphOperationMessage::Vector { layer, modification_type }); - for &handles in &vector.colinear_manipulators { + for &handles in &colinear_pairs { if handles.contains(&handle) { let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; responses.add(GraphOperationMessage::Vector { layer, modification_type }); @@ -857,7 +859,7 @@ impl PathToolData { { let handles = vector .all_connected(point) - .filter(|handle| handle.length(&vector) < 1e-6) + .filter(|handle| vector.handle_length(*handle) < 1e-6) .map(|handle| handle.to_manipulator_point()) .collect::>(); let endpoint = vector.anchor_endpoints().any(|anchor| point == anchor); @@ -964,15 +966,15 @@ impl PathToolData { } for point in state.selected_points() { - let Some(anchor) = point.get_anchor(&vector) else { continue }; + let Some(anchor) = vector.anchor_of(point) else { continue }; moving.anchors.push((layer, anchor)); - let Some([handle1, handle2]) = point.get_handle_pair(&vector) else { continue }; + let Some([handle1, handle2]) = vector.handle_pair_of(point) else { continue }; let Some(handle) = point.as_handle() else { continue }; // Check which handle is selected and which is opposite let opposite = if handle == handle1 { handle2 } else { handle1 }; self.opposite_handle_position = if self.opposite_handle_position.is_none() { - opposite.to_manipulator_point().get_position(&vector) + vector.position_of(opposite.to_manipulator_point()) } else { self.opposite_handle_position }; @@ -998,7 +1000,7 @@ impl PathToolData { let to_document = document.metadata().transform_to_document_if_feeds(layer, &document.network_interface); for point in state.selected_points() { - let Some(position) = point.get_position(&vector) else { continue }; + let Some(position) = vector.position_of(point) else { continue }; let point_start = to_document.transform_point2(position) - slid_so_far; starts.push(point_start); @@ -1104,11 +1106,11 @@ impl PathToolData { }; // Check if handle has a pair (to ignore handles of edges of open paths) - if let Some(handle_pair) = selected_handle_id.get_handle_pair(&vector) { + if let Some(handle_pair) = vector.handle_pair_of(selected_handle_id) { let opposite_handle_length = handle_pair.iter().filter(|&&h| h.to_manipulator_point() != selected_handle_id).find_map(|&h| { - let opp_handle_pos = h.to_manipulator_point().get_position(&vector)?; - let opp_anchor_id = h.to_manipulator_point().get_anchor(&vector)?; - let opp_anchor_pos = vector.point_domain.position_from_id(opp_anchor_id)?; + let opp_handle_pos = vector.position_of(h.to_manipulator_point())?; + let opp_anchor_id = vector.anchor_of(h.to_manipulator_point())?; + let opp_anchor_pos = vector.position_from_id(opp_anchor_id)?; Some((opp_handle_pos - opp_anchor_pos).length()) }); @@ -1141,9 +1143,9 @@ impl PathToolData { let layer_to_document = document.metadata().transform_to_document_if_feeds(*layer, &document.network_interface); let vector = document.network_interface.compute_modified_vector(*layer)?; - let handle_position_local = selected_handle.to_manipulator_point().get_position(&vector)?; - let anchor_id = selected_handle.to_manipulator_point().get_anchor(&vector)?; - let anchor_position_local = vector.point_domain.position_from_id(anchor_id)?; + let handle_position_local = vector.position_of(selected_handle.to_manipulator_point())?; + let anchor_id = vector.anchor_of(selected_handle.to_manipulator_point())?; + let anchor_position_local = vector.position_from_id(anchor_id)?; let handle_position_document = layer_to_document.transform_point2(handle_position_local); let anchor_position_document = layer_to_document.transform_point2(anchor_position_local); @@ -1177,13 +1179,13 @@ impl PathToolData { // If the handle is dragged over one of its adjacent anchors while holding down the Ctrl key, compute the angle based on the tangent formed with the neighboring anchor points. if adjacent_anchor.is_some() && lock_angle && !self.angle_locked { - let anchor = handle_id.get_anchor(&vector); + let anchor = vector.anchor_of(handle_id); let (angle, anchor_position) = calculate_adjacent_anchor_tangent(handle_id, anchor, adjacent_anchor, &vector); let layer_to_document = document.metadata().transform_to_document_if_feeds(*layer, &document.network_interface); - self.adjacent_anchor_offset = handle_id - .get_anchor_position(&vector) + self.adjacent_anchor_offset = vector + .anchor_position_of(handle_id) .and_then(|handle_anchor| anchor_position.map(|adjacent_anchor| layer_to_document.transform_point2(adjacent_anchor) - layer_to_document.transform_point2(handle_anchor))); required_angle = angle; @@ -1229,17 +1231,17 @@ impl PathToolData { handle_angle } - fn get_normalized_tangent(&mut self, point: PointId, segment: SegmentId, vector: &Vector) -> Option { + fn get_normalized_tangent(&mut self, point: PointId, segment: SegmentId, vector: &VectorGeometry) -> Option { let other_point = vector.other_point(segment, point)?; - let position = ManipulatorPointId::Anchor(point).get_position(vector)?; + let position = vector.position_of(ManipulatorPointId::Anchor(point))?; let mut handles = vector.all_connected(other_point); let other_handle = handles.find(|handle| handle.segment == segment)?; - let target_position = if other_handle.length(vector) == 0. { - ManipulatorPointId::Anchor(other_point).get_position(vector)? + let target_position = if vector.handle_length(other_handle) == 0. { + vector.position_of(ManipulatorPointId::Anchor(other_point))? } else { - other_handle.to_manipulator_point().get_position(vector)? + vector.position_of(other_handle.to_manipulator_point())? }; let tangent_vector = target_position - position; @@ -1269,7 +1271,7 @@ impl PathToolData { // Check if that segment exists or it has been removed if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) - && !(vector_data.segment_domain.ids().contains(&segment_id)) + && !(vector_data.segment_ids().contains(&segment_id)) { self.segment = None; } @@ -1346,7 +1348,7 @@ impl PathToolData { }; // Move the anchor to the new position - let Some(current_position) = ManipulatorPointId::Anchor(anchor).get_position(&vector) else { + let Some(current_position) = vector.position_of(ManipulatorPointId::Anchor(anchor)) else { return; }; let delta = new_position - current_position; @@ -1510,7 +1512,7 @@ impl PathToolData { return; }; let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return }; - let Some(point_id) = shape_editor.selected_points().next().unwrap().get_anchor(&vector) else { + let Some(point_id) = vector.anchor_of(*shape_editor.selected_points().next().unwrap()) else { return; }; @@ -1739,7 +1741,7 @@ impl Fsm for PathToolFsmState { } } - // TODO: find the segment ids of which the selected points are a part of + // TODO: find the segment IDs of which the selected points are a part of match tool_options.path_overlay_mode { PathOverlayMode::AllHandles => { @@ -1833,7 +1835,7 @@ impl Fsm for PathToolFsmState { let Some((layer, manipulator_point_id)) = nearest_visible_point_indices else { return }; let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return }; - let Some(position) = manipulator_point_id.get_position(&vector) else { + let Some(position) = vector.position_of(manipulator_point_id) else { error!("No position for hovered point"); return; }; @@ -1960,7 +1962,7 @@ impl Fsm for PathToolFsmState { let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; for point in points { - let Some(position) = point.get_position(&vector) else { continue }; + let Some(position) = vector.position_of(point) else { continue }; let transform = document.metadata().transform_to_viewport(layer); let position = transform.transform_point2(position); @@ -2712,16 +2714,13 @@ impl Fsm for PathToolFsmState { // Find points by their positions (with small tolerance for floating point comparison) const POSITION_TOLERANCE: f64 = 1e-6; - let positions = vector.point_domain.positions(); - let point_ids = vector.point_domain.ids(); - let mut start_point_id = None; let mut end_point_id = None; // Get the merged layer's transform to convert local positions to document space let layer_transform = document.metadata().transform_to_document(layer); - for (i, &local_pos) in positions.iter().enumerate() { + for (point_id, local_pos) in vector.anchors() { // Transform the local position to document space for comparison let doc_pos = layer_transform.transform_point2(local_pos); @@ -2729,10 +2728,10 @@ impl Fsm for PathToolFsmState { let end_distance = (doc_pos - end_position).length(); if start_point_id.is_none() && start_distance < POSITION_TOLERANCE { - start_point_id = Some(point_ids[i]); + start_point_id = Some(point_id); } if end_point_id.is_none() && end_distance < POSITION_TOLERANCE { - end_point_id = Some(point_ids[i]); + end_point_id = Some(point_id); } if start_point_id.is_some() && end_point_id.is_some() { break; @@ -2780,7 +2779,7 @@ impl Fsm for PathToolFsmState { // Also get the transform node that is applied on the layer if it exists let transform = document.metadata().transform_to_document(layer); - let mut new_vector = Vector::default(); + let mut new_vector = WorkingVector::default(); let mut selected_points_by_segment = HashSet::new(); old_vector @@ -2792,13 +2791,14 @@ impl Fsm for PathToolFsmState { }); // Add all the selected points - for (point, position) in old_vector.point_domain.iter() { + for (point, position) in old_vector.anchors() { if layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(point)) || selected_points_by_segment.contains(&point) { - new_vector.point_domain.push(point, position); + new_vector.point_ids.push(point); + new_vector.point_positions.push(position); } } - let find_index = |id: PointId| new_vector.point_domain.iter().enumerate().find(|(_, (point_id, _))| *point_id == id).map(|(index, _)| index); + let find_index = |new_vector: &WorkingVector, id: PointId| new_vector.point_ids.iter().position(|&point_id| point_id == id); // Add segments which have selected ends for (segment_id, segment, start, end) in old_vector.segment_iter() { @@ -2807,21 +2807,27 @@ impl Fsm for PathToolFsmState { let segment_selected = layer_selection_state.is_segment_selected(segment_id); if both_ends_selected || segment_selected { - let Some((start_index, end_index)) = find_index(start).zip(find_index(end)) else { - error!("Point does not exist in point domain"); + let Some((start_index, end_index)) = find_index(&new_vector, start).zip(find_index(&new_vector, end)) else { + error!("Point does not exist in the copied selection"); return PathToolFsmState::Ready; }; - new_vector.segment_domain.push(segment_id, start_index, end_index, segment_to_handles(&segment)); + new_vector.segment_ids.push(segment_id); + new_vector.segment_start.push(start_index); + new_vector.segment_end.push(end_index); + new_vector.segment_handles.push(segment_to_handles(&segment)); } } - for handles in old_vector.colinear_manipulators { - if new_vector.segment_domain.ids().contains(&handles[0].segment) && new_vector.segment_domain.ids().contains(&handles[1].segment) { - new_vector.colinear_manipulators.push(handles); + for handles in old_vector.colinear_pairs() { + if new_vector.segment_ids.contains(&handles[0].segment) && new_vector.segment_ids.contains(&handles[1].segment) { + new_vector.colinear.push(handles); } } - buffer.push((layer, new_vector, transform)); + // The clipboard stores a modification that rebuilds the copied selection from nothing + let (mut copied, mut copied_paths) = (Vector::default(), List::new()); + new_vector.reweave(&mut copied, &mut copied_paths); + buffer.push((layer, VectorModification::create_from_vector(&copied, &copied_paths), transform)); } responses.add(ClipboardMessage::WriteItems { @@ -2875,9 +2881,14 @@ impl Fsm for PathToolFsmState { layer }; - // Create new point ids and add those into the existing vector content + // Materialize the copied snapshot so its points and segments can replay with fresh IDs + let (mut pasted_vector, mut pasted_paths) = (Vector::default(), List::new()); + new_vector.apply(&mut pasted_vector, &mut pasted_paths); + let pasted = VectorGeometry::new(pasted_vector, pasted_paths); + + // Create new point IDs and add those into the existing vector content let mut points_map = HashMap::new(); - for (point, position) in new_vector.point_domain.iter() { + for (point, position) in pasted.anchors() { let new_point_id = PointId::generate(); points_map.insert(point, new_point_id); @@ -2886,9 +2897,9 @@ impl Fsm for PathToolFsmState { responses.add(GraphOperationMessage::Vector { layer, modification_type }); } - // Create new segment ids and add the segments into the existing vector content + // Create new segment IDs and add the segments into the existing vector content let mut segments_map = HashMap::new(); - for (segment_id, bezier, start, end) in new_vector.segment_iter() { + for (segment_id, bezier, start, end) in pasted.segment_iter() { let new_segment_id = SegmentId::generate(); segments_map.insert(segment_id, new_segment_id); @@ -2903,7 +2914,7 @@ impl Fsm for PathToolFsmState { } // Set G1 continuity - for handles in new_vector.colinear_manipulators { + for handles in pasted.colinear_pairs() { let to_new_handle = |handle: HandleId| -> HandleId { HandleId { ty: handle.ty, @@ -2973,7 +2984,7 @@ impl Fsm for PathToolFsmState { }); let mut points_map = HashMap::new(); - for (point, position) in old_vector.point_domain.iter() { + for (point, position) in old_vector.anchors() { // TODO: Either the point is selected or it is an endpoint of a selected segment if layer_selection_state.is_point_selected(ManipulatorPointId::Anchor(point)) || selected_points_by_segment.contains(&point) { @@ -3008,7 +3019,7 @@ impl Fsm for PathToolFsmState { } } - for handles in old_vector.colinear_manipulators { + for handles in old_vector.colinear_pairs() { let to_new_handle = |handle: HandleId| -> HandleId { HandleId { ty: handle.ty, @@ -3311,7 +3322,7 @@ fn get_selection_status(network_interface: &NodeNetworkInterface, shape_state: & let Some(&point) = shape_state.selected_points().next() else { return SelectionStatus::None; }; - let Some(local_position) = point.get_position(&vector) else { + let Some(local_position) = vector.position_of(point) else { return SelectionStatus::None; }; @@ -3344,12 +3355,12 @@ fn calculate_lock_angle( shape_state: &mut ShapeState, responses: &mut VecDeque, document: &DocumentMessageHandler, - vector: &Vector, + vector: &VectorGeometry, handle_id: ManipulatorPointId, tangent_to_neighboring_tangents: bool, ) -> Option { - let anchor = handle_id.get_anchor(vector)?; - let anchor_position = vector.point_domain.position_from_id(anchor); + let anchor = vector.anchor_of(handle_id)?; + let anchor_position = vector.position_from_id(anchor); let current_segment = handle_id.get_segment(); let points_connected = vector.connected_count(anchor); @@ -3357,17 +3368,17 @@ fn calculate_lock_angle( if points_connected == 1 { calculate_segment_angle(anchor, segment, vector, false) } else { - let opposite_handle = handle_id - .get_handle_pair(vector) + let opposite_handle = vector + .handle_pair_of(handle_id) .iter() .flatten() .find(|&h| h.to_manipulator_point() != handle_id) .copied() .map(|h| h.to_manipulator_point()); - let opposite_handle_position = opposite_handle.and_then(|h| h.get_position(vector)).filter(|pos| (pos - anchor_position).length() > 1e-6); + let opposite_handle_position = opposite_handle.and_then(|h| vector.position_of(h)).filter(|pos| (pos - anchor_position).length() > 1e-6); if let Some(opposite_pos) = opposite_handle_position { - if !vector.colinear_manipulators.iter().flatten().map(|h| h.to_manipulator_point()).any(|h| h == handle_id) { + if !vector.colinear(handle_id) { shape_state.convert_selected_manipulators_to_colinear_handles(responses, document); tool_data.temporary_colinear_handles = true; } @@ -3398,7 +3409,7 @@ fn calculate_lock_angle( } /// The position of the one unselected anchor joined to `anchor`, if the segment joining them has no handles to bend it as `anchor` moves. -fn unselected_neighbor_joined_straight(anchor: PointId, state: &SelectedLayerState, vector: &Vector) -> Option { +fn unselected_neighbor_joined_straight(anchor: PointId, state: &SelectedLayerState, vector: &VectorGeometry) -> Option { let mut unselected = vector.connected_points(anchor).filter(|&neighbor| !state.is_point_selected(ManipulatorPointId::Anchor(neighbor))); let (Some(neighbor), None) = (unselected.next(), unselected.next()) else { return None }; @@ -3411,7 +3422,7 @@ fn unselected_neighbor_joined_straight(anchor: PointId, state: &SelectedLayerSta return None; } - vector.point_domain.position_from_id(neighbor) + vector.position_from_id(neighbor) } /// The document positions of the selected points, along with both ends of each selected segment. @@ -3427,18 +3438,18 @@ fn selected_positions_in_document(shape_editor: &ShapeState, document: &Document .segment_iter() .filter(|&(segment, ..)| state.is_segment_selected(segment)) .flat_map(|(_, _, start, end)| [start, end]); - let positions = selected_points.filter_map(|point| point.get_position(&vector)); - let positions = positions.chain(segment_ends.filter_map(|point| vector.point_domain.position_from_id(point))); + let positions = selected_points.filter_map(|point| vector.position_of(point)); + let positions = positions.chain(segment_ends.filter_map(|point| vector.position_from_id(point))); positions.map(|position| to_document.transform_point2(position)).collect() }) } -fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector: &Vector) -> Option { - let (anchor, handle_position) = handle_id.get_anchor(vector).zip(handle_id.get_position(vector))?; +fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector: &VectorGeometry) -> Option { + let (anchor, handle_position) = vector.anchor_of(handle_id).zip(vector.position_of(handle_id))?; let check_if_close = |point_id: &PointId| { - let Some(anchor_position) = vector.point_domain.position_from_id(*point_id) else { + let Some(anchor_position) = vector.position_from_id(*point_id) else { return false; }; (anchor_position - handle_position).length() < 10. @@ -3446,15 +3457,15 @@ fn check_handle_over_adjacent_anchor(handle_id: ManipulatorPointId, vector: &Vec vector.connected_points(anchor).find(check_if_close) } -fn calculate_adjacent_anchor_tangent(currently_dragged_handle: ManipulatorPointId, anchor: Option, adjacent_anchor: Option, vector: &Vector) -> (Option, Option) { +fn calculate_adjacent_anchor_tangent(currently_dragged_handle: ManipulatorPointId, anchor: Option, adjacent_anchor: Option, vector: &VectorGeometry) -> (Option, Option) { // Early return if no anchor or no adjacent anchors let Some((dragged_handle_anchor, adjacent_anchor)) = anchor.zip(adjacent_anchor) else { return (None, None); }; - let adjacent_anchor_position = vector.point_domain.position_from_id(adjacent_anchor); + let adjacent_anchor_position = vector.position_from_id(adjacent_anchor); - let handles: Vec<_> = vector.all_connected(adjacent_anchor).filter(|handle| handle.length(vector) > 1e-6).collect(); + let handles: Vec<_> = vector.all_connected(adjacent_anchor).filter(|handle| vector.handle_length(*handle) > 1e-6).collect(); match handles.len() { 0 => { @@ -3494,8 +3505,8 @@ fn calculate_adjacent_anchor_tangent(currently_dragged_handle: ManipulatorPointI return (None, None); }; - let angle = shared_segment_handle - .get_position(vector) + let angle = vector + .position_of(shared_segment_handle) .zip(adjacent_anchor_position) .and_then(|(handle, anchor)| DVec2::X.try_angle_to(handle - anchor)); @@ -3765,7 +3776,7 @@ mod test_path_tool { let vector = document.network_interface.compute_modified_vector(layer).expect("the path should have a vector"); let to_viewport = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface); - vector.point_domain.positions().iter().map(|&position| to_viewport.transform_point2(position)).collect() + vector.anchor_positions().iter().map(|&position| to_viewport.transform_point2(position)).collect() } fn assert_anchors(editor: &EditorTestUtils, layer: LayerNodeIdentifier, expected: &[DVec2]) { @@ -3881,8 +3892,8 @@ mod test_path_tool { let handle_position = |editor: &EditorTestUtils| { let document = editor.active_document(); let vector = document.network_interface.compute_modified_vector(layer).expect("the path should have a vector"); - let segment = vector.segment_domain.ids()[0]; - let handle = ManipulatorPointId::EndHandle(segment).get_position(&vector).expect("the segment should have a handle at its end"); + let segment = vector.segment_ids()[0]; + let handle = vector.position_of(ManipulatorPointId::EndHandle(segment)).expect("the segment should have a handle at its end"); document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface).transform_point2(handle) }; let handle = anchor * 2. - dragged_out; @@ -4038,4 +4049,68 @@ mod test_path_tool { "only the Alt-clicked anchor should be selected, not {selected:?}" ); } + + #[tokio::test] + async fn copied_points_paste_back_as_the_same_curves_and_smooth_pairs() { + let mut editor = EditorTestUtils::create(); + editor.new_document().await; + editor.handle_message(DocumentMessage::DeselectAllLayers).await; + + // Dragging out of the middle anchor with the Pen gives it a smooth pair of handles + let dragged_out = B + DVec2::new(60., 25.); + editor.click_tool(ToolType::Pen, MouseKeys::LEFT, A, ModifierKeys::empty()).await; + editor.move_mouse(B.x, B.y, ModifierKeys::empty(), MouseKeys::empty()).await; + editor.left_mousedown(B.x, B.y, ModifierKeys::empty()).await; + editor.move_mouse(dragged_out.x, dragged_out.y, ModifierKeys::empty(), MouseKeys::LEFT).await; + editor.left_mouseup(dragged_out.x, dragged_out.y, ModifierKeys::empty()).await; + editor.click_tool(ToolType::Pen, MouseKeys::LEFT, C, ModifierKeys::empty()).await; + editor.press(Key::Enter, ModifierKeys::empty()).await; + + let layer = editor.get_selected_layer().await.expect("the drawn path should be selected"); + let geometry = |editor: &EditorTestUtils| editor.active_document().network_interface.compute_modified_vector(layer).expect("the path should have a vector"); + let original = geometry(&editor); + assert_eq!(original.colinear_pairs().len(), 1, "the dragged-out anchor should be smooth"); + + // Copy every point and paste them back into the same layer + editor.select_tool(ToolType::Path).await; + editor.handle_message(PathToolMessage::SelectAll).await; + let clipboard = editor + .handle_message(ClipboardMessage::CopyLayers) + .await + .into_iter() + .find_map(|message| match message { + FrontendMessage::TriggerClipboardWrite { content } => Some(content), + _ => None, + }) + .expect("copying points should write a payload to the clipboard"); + editor + .handle_message(ClipboardMessage::ReadClipboard { + content: ClipboardContentRaw::Text(clipboard), + }) + .await; + + // Each curve now appears a second time under a new ID + let control_points = |segment: kurbo::PathSeg| match segment { + kurbo::PathSeg::Line(line) => vec![line.p0, line.p1], + kurbo::PathSeg::Quad(quad) => vec![quad.p0, quad.p1, quad.p2], + kurbo::PathSeg::Cubic(cubic) => vec![cubic.p0, cubic.p1, cubic.p2, cubic.p3], + }; + let same_curve = |a: kurbo::PathSeg, b: kurbo::PathSeg| { + let (a, b) = (control_points(a), control_points(b)); + a.len() == b.len() && a.iter().zip(&b).all(|(a, b)| a.distance(*b) < 1e-9) + }; + + let pasted = geometry(&editor); + let original_ids = original.segment_ids(); + let pasted_segments: Vec<_> = pasted.segment_iter().filter(|(id, ..)| !original_ids.contains(id)).map(|(_, segment, ..)| segment).collect(); + assert_eq!(pasted_segments.len(), original_ids.len()); + for (_, segment, ..) in original.segment_iter() { + assert!( + pasted_segments.iter().any(|&pasted| same_curve(pasted, segment)), + "no pasted copy of {segment:?} among {pasted_segments:?}" + ); + } + + assert_eq!(pasted.colinear_pairs().len(), 2, "the pasted copy of the smooth anchor should be smooth too"); + } } diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 8bee8c725a3..9c7d3facc83 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -22,7 +22,7 @@ use graphene_std::Color; use graphene_std::vector::misc::pathseg_points; use graphene_std::vector::misc::{HandleId, ManipulatorPointId, dvec2_to_point}; use graphene_std::vector::style::FillChoice; -use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType}; +use graphene_std::vector::{PointId, SegmentId, VectorGeometry, VectorModificationType}; use kurbo::{BezPath, CubicBez, PathSeg}; #[derive(Default, ExtractField)] @@ -531,7 +531,7 @@ impl PenToolData { } /// Check whether target handle is primary, end, or `self.handle_end` - fn check_end_handle_type(&self, vector: &Vector) -> TargetHandle { + fn check_end_handle_type(&self, vector: &VectorGeometry) -> TargetHandle { match (self.handle_end, self.prior_segment_endpoint, self.prior_segment, self.path_closed) { (Some(_), _, _, false) => TargetHandle::PreviewInHandle, (None, Some(point), Some(segment), false) | (Some(_), Some(point), Some(segment), true) => { @@ -545,7 +545,7 @@ impl PenToolData { } } - fn check_grs_end_handle(&self, vector: &Vector) -> TargetHandle { + fn check_grs_end_handle(&self, vector: &VectorGeometry) -> TargetHandle { let Some(point) = self.latest_point().map(|point| point.id) else { return TargetHandle::None }; let Some(segment) = self.prior_segment else { return TargetHandle::None }; @@ -556,7 +556,7 @@ impl PenToolData { } } - fn get_opposite_handle_type(&self, handle_type: TargetHandle, vector: &Vector) -> TargetHandle { + fn get_opposite_handle_type(&self, handle_type: TargetHandle, vector: &VectorGeometry) -> TargetHandle { match handle_type { TargetHandle::FuturePreviewOutHandle => self.check_end_handle_type(vector), TargetHandle::PreviewInHandle => match (self.path_closed, self.prior_segment_endpoint, self.prior_segment) { @@ -608,10 +608,10 @@ impl PenToolData { } } - fn target_handle_position(&self, handle_type: TargetHandle, vector: &Vector) -> Option { + fn target_handle_position(&self, handle_type: TargetHandle, vector: &VectorGeometry) -> Option { match handle_type { - TargetHandle::PriorOutHandle(segment) => ManipulatorPointId::PrimaryHandle(segment).get_position(vector), - TargetHandle::PriorInHandle(segment) => ManipulatorPointId::EndHandle(segment).get_position(vector), + TargetHandle::PriorOutHandle(segment) => vector.position_of(ManipulatorPointId::PrimaryHandle(segment)), + TargetHandle::PriorInHandle(segment) => vector.position_of(ManipulatorPointId::EndHandle(segment)), TargetHandle::PreviewInHandle => self.handle_end, TargetHandle::FuturePreviewOutHandle => Some(self.next_handle_start), TargetHandle::None => None, @@ -663,7 +663,7 @@ impl PenToolData { let into_merged = self.merging_from.map_or(DAffine2::IDENTITY, |previous| layer_to_document.inverse() * previous); for point in &mut self.latest_points { - let Some(pos) = vector.point_domain.position_from_id(point.id) else { continue }; + let Some(pos) = vector.position_from_id(point.id) else { continue }; point.handle_start = pos + into_merged.transform_vector2(point.handle_start - point.pos); point.pos = pos; } @@ -671,7 +671,7 @@ impl PenToolData { } /// Re-anchors the in-progress preview segment on the current anchor after undo or redo moved it, using the re-evaluated layer transform. - fn resume_preview_from_current_anchor(&mut self, snap_session: &mut SnapSession, view: SnapView, transform: DAffine2, vector: &Vector, responses: &mut VecDeque) { + fn resume_preview_from_current_anchor(&mut self, snap_session: &mut SnapSession, view: SnapView, transform: DAffine2, vector: &VectorGeometry, responses: &mut VecDeque) { // The segments meeting the current anchor are the ones whose handles the overlays show and the angle lock references let connected_segments: Vec = self .latest_point() @@ -757,9 +757,9 @@ impl PenToolData { } } - fn close_path_on_point(&mut self, vector: &Vector, document: &DocumentMessageHandler, id: PointId, transform: &DAffine2) -> bool { + fn close_path_on_point(&mut self, vector: &VectorGeometry, document: &DocumentMessageHandler, id: PointId, transform: &DAffine2) -> bool { for id in vector.anchor_points().filter(|&point| point != id) { - let Some(pos) = vector.point_domain.position_from_id(id) else { continue }; + let Some(pos) = vector.position_from_id(id) else { continue }; let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(self.next_point)); let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); @@ -815,7 +815,7 @@ impl PenToolData { let start = self.latest_point()?.id; let transform = document.metadata().document_to_viewport * transform; for id in vector.anchor_points().filter(|&point| point != start) { - let Some(pos) = vector.point_domain.position_from_id(id) else { continue }; + let Some(pos) = vector.position_from_id(id) else { continue }; let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); if transformed_distance_between_squared < snap_point_tolerance_squared { @@ -839,12 +839,12 @@ impl PenToolData { && let Some((handles, handle1_pos)) = match self.get_opposite_handle_type(TargetHandle::PreviewInHandle, &vector) { TargetHandle::PriorOutHandle(segment) => { let handles = [HandleId::end(id), HandleId::primary(segment)]; - let handle1_pos = handles[1].to_manipulator_point().get_position(&vector); + let handle1_pos = vector.position_of(handles[1].to_manipulator_point()); handle1_pos.map(|pos| (handles, pos)) } TargetHandle::PriorInHandle(segment) => { let handles = [HandleId::end(id), HandleId::end(segment)]; - let handle1_pos = handles[1].to_manipulator_point().get_position(&vector); + let handle1_pos = vector.position_of(handles[1].to_manipulator_point()); handle1_pos.map(|pos| (handles, pos)) } _ => None, @@ -872,7 +872,7 @@ impl PenToolData { [HandleId::primary(last_segment), HandleId::primary(id)] }; - if let Some(h1) = handles[0].to_manipulator_point().get_position(&vector) { + if let Some(h1) = vector.position_of(handles[0].to_manipulator_point()) { let colinear = is_almost_colinear(last_point.pos, h1, last_point.handle_start); responses.add(GraphOperationMessage::Vector { layer, @@ -894,7 +894,7 @@ impl PenToolData { } /// Snaps the slide of the anchor and its handles while Space is held, and returns how far they move in layer space. - fn space_anchor_handle_snap(&mut self, snap_session: &mut SnapSession, view: SnapView, transform: DAffine2, mouse: DVec2, vector: &Vector) -> Option { + fn space_anchor_handle_snap(&mut self, snap_session: &mut SnapSession, view: SnapView, transform: DAffine2, mouse: DVec2, vector: &VectorGeometry) -> Option { let reference_handle = if self.path_closed { TargetHandle::PreviewInHandle } else { TargetHandle::FuturePreviewOutHandle }; let reference_position = transform.transform_point2(self.target_handle_position(reference_handle, vector)?); @@ -996,7 +996,7 @@ impl PenToolData { Some(PenToolFsmState::DraggingHandle(self.handle_mode)) } - fn move_anchor_and_handles(&mut self, delta: DVec2, layer: LayerNodeIdentifier, responses: &mut VecDeque, vector: &Vector) { + fn move_anchor_and_handles(&mut self, delta: DVec2, layer: LayerNodeIdentifier, responses: &mut VecDeque, vector: &VectorGeometry) { if self.handle_end.is_none() && let Some(latest_pt) = self.latest_point_mut() { @@ -1122,7 +1122,7 @@ impl PenToolData { } /// Makes the opposite handle equidistant or locks its length. - fn adjust_handle_length(&mut self, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector: &Vector) { + fn adjust_handle_length(&mut self, responses: &mut VecDeque, layer: LayerNodeIdentifier, vector: &VectorGeometry) { let opposite_handle_type = self.get_opposite_handle_type(self.handle_type, vector); match self.handle_mode { HandleMode::ColinearEquidistant => { @@ -1159,7 +1159,7 @@ impl PenToolData { } } - fn apply_colinear_constraint(&mut self, responses: &mut VecDeque, layer: LayerNodeIdentifier, anchor_pos: DVec2, vector: &Vector) { + fn apply_colinear_constraint(&mut self, responses: &mut VecDeque, layer: LayerNodeIdentifier, anchor_pos: DVec2, vector: &VectorGeometry) { let Some(handle) = self.target_handle_position(self.handle_type, vector) else { return }; if (anchor_pos - handle).length() < 1e-6 && self.modifiers.lock_angle { @@ -1193,7 +1193,7 @@ impl PenToolData { if let Some(vector) = document.network_interface.compute_modified_vector(layer) { let transform = document.metadata().document_to_viewport * transform; for point in vector.anchor_points() { - let Some(pos) = vector.point_domain.position_from_id(point) else { continue }; + let Some(pos) = vector.position_from_id(point) else { continue }; let transformed_distance_between_squared = transform.transform_point2(pos).distance_squared(transform.transform_point2(self.next_point)); let snap_point_tolerance_squared = crate::consts::SNAP_POINT_TOLERANCE.powi(2); if transformed_distance_between_squared < snap_point_tolerance_squared { @@ -1367,7 +1367,7 @@ impl PenToolData { /// Perform extension of an existing path fn extend_existing_path(&mut self, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, point: PointId, position: DVec2) { - let vector: Option = document.network_interface.compute_modified_vector(layer); + let vector: Option = document.network_interface.compute_modified_vector(layer); let (handle_start, in_segment) = if let Some(vector) = &vector { vector .segment_iter() @@ -1435,8 +1435,8 @@ impl PenToolData { } } - fn set_lock_angle(&mut self, vector: &Vector, anchor: PointId, segment: Option) { - let anchor_position = vector.point_domain.position_from_id(anchor); + fn set_lock_angle(&mut self, vector: &VectorGeometry, anchor: PointId, segment: Option) { + let anchor_position = vector.position_from_id(anchor); let Some((anchor_position, segment)) = anchor_position.zip(segment) else { self.handle_mode = HandleMode::Free; @@ -1823,7 +1823,7 @@ impl Fsm for PenToolFsmState { && let Some(mut vector) = document.network_interface.compute_modified_vector(layer) { let closest_point = vector.anchor_points().filter(|&id| id != start).find(|&id| { - vector.point_domain.position_from_id(id).is_some_and(|pos| { + vector.position_from_id(id).is_some_and(|pos| { let dist_sq = transform.transform_point2(pos).distance_squared(transform.transform_point2(next_point)); dist_sq < crate::consts::SNAP_POINT_TOLERANCE.powi(2) }) @@ -1841,10 +1841,8 @@ impl Fsm for PenToolFsmState { for path in closed_paths { let segments = path.edges.iter().filter_map(|edge| { vector - .segment_domain - .iter() - .find(|(id, _, _, _)| id == &edge.id) - .map(|(_, start, end, bezier)| if start == edge.start { (bezier, start, end) } else { (bezier.reversed(), end, start) }) + .segment_with_pool_endpoints(edge.id) + .map(|(segment, start, end)| if start == edge.start { (segment, start, end) } else { (segment.reverse(), end, start) }) }); if let Some(bezpath) = vector.bezpath_from_segments_ignore_discontinuities(segments) { fill_region.extend(bezpath.elements().iter().copied()); @@ -1970,7 +1968,7 @@ impl Fsm for PenToolFsmState { } (PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => { let vector = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)); - let anchor_exists = |point: &LastPoint| vector.as_ref().is_some_and(|vector| vector.point_domain.position_from_id(point.id).is_some()); + let anchor_exists = |point: &LastPoint| vector.as_ref().is_some_and(|vector| vector.position_from_id(point.id).is_some()); // Rewind past the anchors the reverted path no longer contains, remembering them so redo can advance back through them while tool_data.point_index > 0 && !tool_data.latest_point().is_some_and(anchor_exists) { @@ -1996,7 +1994,7 @@ impl Fsm for PenToolFsmState { }; // Advance only through the remembered anchors the re-applied step actually brought back - let anchor_exists = |point: &LastPoint| vector.point_domain.position_from_id(point.id).is_some(); + let anchor_exists = |point: &LastPoint| vector.position_from_id(point.id).is_some(); let mut advanced = false; while tool_data.latest_points.get(tool_data.point_index + 1).is_some_and(anchor_exists) { tool_data.point_index += 1; @@ -2238,7 +2236,7 @@ impl Fsm for PenToolFsmState { (PenToolFsmState::DraggingHandle(..), PenToolMessage::Confirm) => { // Confirm to end path if let Some((vector, layer)) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)).zip(layer) { - let single_point_in_layer = vector.point_domain.ids().len() == 1; + let single_point_in_layer = vector.anchor_points().count() == 1; tool_data.finish_placing_handle(snap_session, view, transform, responses); let latest_points = tool_data.point_index == 0; @@ -2290,7 +2288,7 @@ impl Fsm for PenToolFsmState { } (PenToolFsmState::PlacingAnchor, PenToolMessage::Abort) => { let should_delete_layer = if let Some(vector) = layer.and_then(|layer| document.network_interface.compute_modified_vector(layer)) { - vector.point_domain.ids().len() == 1 + vector.anchor_points().count() == 1 } else { false }; @@ -2395,10 +2393,10 @@ mod test_pen_tool { use crate::test_utils::test_prelude::*; use glam::DAffine2; use graphene_std::vector::misc::ManipulatorPointId; - use graphene_std::vector::{SegmentId, Vector}; + use graphene_std::vector::{SegmentId, VectorGeometry}; /// The single Pen-drawn path layer and its vector, or `None` once the layer has been undone away. - fn drawn_path(editor: &EditorTestUtils) -> Option<(LayerNodeIdentifier, Vector)> { + fn drawn_path(editor: &EditorTestUtils) -> Option<(LayerNodeIdentifier, VectorGeometry)> { let document = editor.active_document(); let path_identifier = DefinitionIdentifier::Network("Path".into()); let mut path_layers = document @@ -2414,8 +2412,8 @@ mod test_pen_tool { fn assert_path_shape(editor: &EditorTestUtils, points: usize, segments: usize) { let (_, vector) = drawn_path(editor).expect("Expected a drawn path"); - assert_eq!(vector.point_domain.ids().len(), points, "Unexpected point count"); - assert_eq!(vector.segment_domain.ids().len(), segments, "Unexpected segment count"); + assert_eq!(vector.anchor_points().count(), points, "Unexpected point count"); + assert_eq!(vector.segment_ids().len(), segments, "Unexpected segment count"); } /// Asserts the path's anchors sit at the given viewport positions, in placement order, and that each segment joins consecutive anchors. @@ -2423,17 +2421,17 @@ mod test_pen_tool { let (layer, vector) = drawn_path(editor).expect("Expected a drawn path"); let layer_to_viewport = editor.active_document().metadata().transform_to_viewport(layer); - let anchor_ids = vector.point_domain.ids(); + let anchor_ids = vector.anchor_points().collect::>(); assert_eq!(anchor_ids.len(), expected.len(), "Unexpected point count"); for (index, (&id, &expected_position)) in anchor_ids.iter().zip(expected).enumerate() { - let position = layer_to_viewport.transform_point2(vector.point_domain.position_from_id(id).unwrap()); + let position = layer_to_viewport.transform_point2(vector.position_from_id(id).unwrap()); assert!( position.distance(expected_position) < 1e-6, "Anchor {index} is at {position:?} but was expected at {expected_position:?}" ); } - let segment_ids = vector.segment_domain.ids(); + let segment_ids = vector.segment_ids(); assert_eq!(segment_ids.len(), expected.len().saturating_sub(1), "Unexpected segment count"); for (index, &segment) in segment_ids.iter().enumerate() { assert_eq!(vector.segment_start_from_id(segment), Some(anchor_ids[index]), "Segment {index} does not start at anchor {index}"); @@ -2640,7 +2638,7 @@ mod test_pen_tool { fn anchor_positions(editor: &EditorTestUtils) -> Vec { let (layer, vector) = drawn_path(editor).expect("Expected a drawn path"); let layer_to_viewport = editor.active_document().metadata().transform_to_viewport(layer); - vector.point_domain.positions().iter().map(|&position| layer_to_viewport.transform_point2(position)).collect() + vector.anchor_positions().into_iter().map(|position| layer_to_viewport.transform_point2(position)).collect() } async fn set_space_held(editor: &mut EditorTestUtils, held: bool) { @@ -2756,8 +2754,8 @@ mod test_pen_tool { // The last segment runs from C back to A rather than to a new anchor beside it let (_, vector) = drawn_path(&editor).expect("Expected a drawn path"); - let anchors = vector.point_domain.ids(); - let closing = vector.segment_domain.ids()[2]; + let anchors: Vec<_> = vector.anchor_points().collect(); + let closing = vector.segment_ids()[2]; assert_eq!(vector.segment_start_from_id(closing), Some(anchors[2])); assert_eq!(vector.segment_end_from_id(closing), Some(anchors[0])); assert!(anchor_positions(&editor)[0].distance(A) < 1e-6); @@ -2848,16 +2846,13 @@ mod test_pen_tool { let layer_to_viewport = editor.active_document().metadata().transform_to_viewport(layer); let handles = vector - .segment_domain - .ids() - .iter() - .flat_map(|&segment| [ManipulatorPointId::PrimaryHandle(segment), ManipulatorPointId::EndHandle(segment)]) - .filter_map(|handle| handle.get_position(&vector)); + .segment_ids() + .into_iter() + .flat_map(|segment| [ManipulatorPointId::PrimaryHandle(segment), ManipulatorPointId::EndHandle(segment)]) + .filter_map(|handle| vector.position_of(handle)); let mut positions: Vec = vector - .point_domain - .positions() - .iter() - .copied() + .anchor_positions() + .into_iter() .chain(handles) .map(|position| layer_to_viewport.transform_point2(position)) .collect(); @@ -2934,7 +2929,7 @@ mod test_pen_tool { let document = editor.active_document(); let vector = document.network_interface.compute_modified_vector(layer).expect("The second path should have a vector"); - let first = document.metadata().transform_to_viewport(layer).transform_point2(vector.point_domain.positions()[0]); + let first = document.metadata().transform_to_viewport(layer).transform_point2(vector.anchor_positions()[0]); // Its next anchor has a handle dragged out of it let (anchor, handle) = (DVec2::new(260., 240.), DVec2::new(300., 270.)); @@ -2955,17 +2950,16 @@ mod test_pen_tool { let (layer, vector) = drawn_path(&editor).expect("Expected a drawn path"); let layer_to_viewport = editor.active_document().metadata().transform_to_viewport(layer); let leaves_the_anchor = |&segment: &SegmentId| { - let start = vector.segment_start_from_id(segment).and_then(|point| vector.point_domain.position_from_id(point)); + let start = vector.segment_start_from_id(segment).and_then(|point| vector.position_from_id(point)); start.is_some_and(|start| layer_to_viewport.transform_point2(start).distance(anchor) < 1e-6) }; let joining = vector - .segment_domain - .ids() + .segment_ids() .iter() .copied() .find(|segment| leaves_the_anchor(segment)) .expect("A segment should leave the anchor"); - let leaving = ManipulatorPointId::PrimaryHandle(joining).get_position(&vector).expect("The joining segment should have a handle"); + let leaving = vector.position_of(ManipulatorPointId::PrimaryHandle(joining)).expect("The joining segment should have a handle"); let leaving = layer_to_viewport.transform_point2(leaving); assert!(leaving.distance(handle) < 1e-6, "the joining segment's handle is at {leaving}"); } @@ -2997,8 +2991,8 @@ mod test_pen_tool { // The incoming handle stays opposite the outgoing one, which was dragged out from the anchor before it moved let (layer, vector) = drawn_path(&editor).expect("Expected a drawn path"); - let segment = vector.segment_domain.ids()[0]; - let incoming = ManipulatorPointId::EndHandle(segment).get_position(&vector).expect("The segment should have an incoming handle"); + let segment = vector.segment_ids()[0]; + let incoming = vector.position_of(ManipulatorPointId::EndHandle(segment)).expect("The segment should have an incoming handle"); let incoming = editor.active_document().metadata().transform_to_viewport(layer).transform_point2(incoming); assert!(incoming.distance(corner + anchor - handle) < 1e-6, "the incoming handle is at {incoming}"); } @@ -3028,8 +3022,8 @@ mod test_pen_tool { // The next segment starts with the moved handle click_pen(&mut editor, DVec2::new(450., 400.)).await; let (layer, vector) = drawn_path(&editor).expect("Expected a drawn path"); - let segment = vector.segment_domain.ids()[1]; - let outgoing = ManipulatorPointId::PrimaryHandle(segment).get_position(&vector).expect("The segment should have an outgoing handle"); + let segment = vector.segment_ids()[1]; + let outgoing = vector.position_of(ManipulatorPointId::PrimaryHandle(segment)).expect("The segment should have an outgoing handle"); let outgoing = editor.active_document().metadata().transform_to_viewport(layer).transform_point2(outgoing); assert!(outgoing.distance(handle + travel) < 1e-6, "the handle is at {outgoing} rather than {}", handle + travel); } diff --git a/editor/src/messages/tool/tool_messages/spline_tool.rs b/editor/src/messages/tool/tool_messages/spline_tool.rs index e2674cce351..4b25b369c73 100644 --- a/editor/src/messages/tool/tool_messages/spline_tool.rs +++ b/editor/src/messages/tool/tool_messages/spline_tool.rs @@ -698,15 +698,13 @@ mod test_spline_tool { use crate::test_utils::test_prelude::*; use glam::DAffine2; use graphene_std::vector::PointId; - use graphene_std::vector::Vector; + use graphene_std::vector::VectorGeometry; - fn assert_point_positions(vector: &Vector, layer_to_viewport: DAffine2, expected_points: &[DVec2], epsilon: f64) { + fn assert_point_positions(vector: &VectorGeometry, layer_to_viewport: DAffine2, expected_points: &[DVec2], epsilon: f64) { let points_in_viewport: Vec = vector - .point_domain - .ids() - .iter() - .filter_map(|&point_id| { - let position = vector.point_domain.position_from_id(point_id)?; + .anchor_points() + .filter_map(|point_id| { + let position = vector.position_from_id(point_id)?; Some(layer_to_viewport.transform_point2(position)) }) .collect(); @@ -734,7 +732,7 @@ mod test_spline_tool { let document = editor.active_document(); let vector = document.network_interface.compute_modified_vector(layer).expect("The spline should have a path"); - assert_eq!(vector.point_domain.ids().len(), expected.len(), "Unexpected point count"); + assert_eq!(vector.anchor_points().count(), expected.len(), "Unexpected point count"); assert_point_positions(&vector, document.metadata().transform_to_viewport(layer), expected, 1e-6); } @@ -771,8 +769,8 @@ mod test_spline_tool { let first_vector = document.network_interface.compute_modified_vector(spline_layer).expect("Vector not found for the spline layer"); // Verify initial spline has correct number of points and segments - let initial_point_count = first_vector.point_domain.ids().len(); - let initial_segment_count = first_vector.segment_domain.ids().len(); + let initial_point_count = first_vector.anchor_points().count(); + let initial_segment_count = first_vector.segment_ids().len(); assert_eq!(initial_point_count, 3, "Expected 3 points in initial spline, found {initial_point_count}"); assert_eq!(initial_segment_count, 2, "Expected 2 segments in initial spline, found {initial_segment_count}"); @@ -780,7 +778,7 @@ mod test_spline_tool { let endpoints: Vec<(PointId, DVec2)> = first_vector .anchor_endpoints() - .filter_map(|point_id| first_vector.point_domain.position_from_id(point_id).map(|pos| (point_id, layer_to_viewport.transform_point2(pos)))) + .filter_map(|point_id| first_vector.position_from_id(point_id).map(|pos| (point_id, layer_to_viewport.transform_point2(pos)))) .collect(); assert_eq!(endpoints.len(), 2, "Expected 2 endpoints in the initial spline"); @@ -805,8 +803,8 @@ mod test_spline_tool { .expect("Vector not found for the extended spline layer"); // Verify extended spline has correct number of points and segments - let extended_point_count = extended_vector.point_domain.ids().len(); - let extended_segment_count = extended_vector.segment_domain.ids().len(); + let extended_point_count = extended_vector.anchor_points().count(); + let extended_segment_count = extended_vector.segment_ids().len(); assert_eq!(extended_point_count, 5, "Expected 5 points in extended spline, found {extended_point_count}"); assert_eq!(extended_segment_count, 4, "Expected 4 segments in extended spline, found {extended_segment_count}"); @@ -997,8 +995,8 @@ mod test_spline_tool { let vector = document.network_interface.compute_modified_vector(spline_layer).expect("Vector not found for the spline layer"); // Verify we have the correct number of points and segments - let point_count = vector.point_domain.ids().len(); - let segment_count = vector.segment_domain.ids().len(); + let point_count = vector.anchor_points().count(); + let segment_count = vector.segment_ids().len(); assert_eq!(point_count, 3, "Expected 3 points in the spline, found {point_count}"); assert_eq!(segment_count, 2, "Expected 2 segments in the spline, found {segment_count}"); diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index e2bdff53743..b29847431fe 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -20,7 +20,7 @@ use glam::{DAffine2, DVec2}; use graphene_std::renderer::Quad; use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::misc::ManipulatorPointId; -use graphene_std::vector::{Vector, VectorModificationType}; +use graphene_std::vector::{VectorGeometry, VectorModificationType}; use std::f64::consts::{PI, TAU}; const TRANSFORM_GRS_OVERLAY_PROVIDER: OverlayProvider = |context| TransformLayerMessage::Overlays { context }.into(); @@ -184,7 +184,7 @@ impl MessageHandler> for let affected_point_refs = affected_points.iter().collect(); - let get_location = |point: &&ManipulatorPointId| point.get_position(&vector).map(|position| viewspace.transform_point2(position)); + let get_location = |point: &&ManipulatorPointId| vector.position_of(**point).map(|position| viewspace.transform_point2(position)); if let (Some((new_pivot, grab_target)), bounds) = calculate_pivot( document, &affected_point_refs, @@ -480,9 +480,9 @@ impl MessageHandler> for && let [point] = selected_points.as_slice() { if matches!(point, ManipulatorPointId::Anchor(_)) { - if let Some([handle1, handle2]) = point.get_handle_pair(&vector) { - let handle1_length = handle1.length(&vector); - let handle2_length = handle2.length(&vector); + if let Some([handle1, handle2]) = vector.handle_pair_of(**point) { + let handle1_length = vector.handle_length(handle1); + let handle2_length = vector.handle_length(handle2); if (handle1_length == 0. && handle2_length == 0. && !using_select_tool) || (handle1_length == f64::MAX && handle2_length == f64::MAX && !using_select_tool) { // G should work for this point but not R and S @@ -493,7 +493,7 @@ impl MessageHandler> for } } } else { - let handle_length = point.as_handle().map(|handle| handle.length(&vector)); + let handle_length = point.as_handle().map(|handle| vector.handle_length(handle)); if handle_length == Some(0.) { selected.original_transforms.clear(); @@ -800,10 +800,10 @@ impl Snapping { moving.edited.push(layer); for point in points { - if let Some(anchor) = point.get_anchor(&vector) { + if let Some(anchor) = vector.anchor_of(point) { moving.anchors.push((layer, anchor)); } - if let Some(position) = point.get_position(&vector) { + if let Some(position) = vector.position_of(point) { starts.push(to_document.transform_point2(position)); } } @@ -952,7 +952,7 @@ fn snap_again(operation: &mut TransformOperation, snapping: Option<&Snapping>, s fn calculate_pivot( document: &DocumentMessageHandler, selected_points: &Vec<&ManipulatorPointId>, - vector: &Vector, + vector: &VectorGeometry, viewspace: DAffine2, get_location: impl Fn(&ManipulatorPointId) -> Option, gizmo: &mut PivotGizmo, @@ -995,8 +995,8 @@ fn calculate_pivot( ManipulatorPointId::PrimaryHandle(_) | ManipulatorPointId::EndHandle(_) => { // Get the anchor position and transform it to the pivot let (Some(pivot_position), Some(position)) = ( - point.get_anchor_position(vector).map(|anchor_position| viewspace.transform_point2(anchor_position)), - point.get_position(vector), + vector.anchor_position_of(**point).map(|anchor_position| viewspace.transform_point2(anchor_position)), + vector.position_of(**point), ) else { return (None, None); }; @@ -1015,18 +1015,18 @@ fn update_colinear_handles(selected_layers: &[LayerNodeIdentifier], document: &D for &layer in selected_layers { let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; - for [handle1, handle2] in &vector.colinear_manipulators { + for [handle1, handle2] in vector.colinear_pairs() { let manipulator1 = handle1.to_manipulator_point(); let manipulator2 = handle2.to_manipulator_point(); - let Some(anchor_pos) = manipulator1.get_anchor_position(&vector) else { continue }; - let Some(handle1_pos) = manipulator1.get_position(&vector) else { continue }; - let Some(handle2_pos) = manipulator2.get_position(&vector) else { continue }; + let Some(anchor_pos) = vector.anchor_position_of(manipulator1) else { continue }; + let Some(handle1_pos) = vector.position_of(manipulator1) else { continue }; + let Some(handle2_pos) = vector.position_of(manipulator2) else { continue }; // Check if handles are not colinear (not approximately equal to +/- PI) if !is_almost_colinear(anchor_pos, handle1_pos, handle2_pos) { let modification_type = VectorModificationType::SetG1Continuous { - handles: [*handle1, *handle2], + handles: [handle1, handle2], enabled: false, }; @@ -1840,7 +1840,7 @@ mod test_transform_layer { let document = editor.active_document(); let vector = document.network_interface.compute_modified_vector(layer).expect("the path should have a vector"); let to_viewport = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface); - let anchors: Vec = vector.point_domain.positions().iter().map(|&position| to_viewport.transform_point2(position)).collect(); + let anchors: Vec = vector.anchor_positions().iter().map(|&position| to_viewport.transform_point2(position)).collect(); assert_eq!(anchors.len(), 2, "unexpected anchors {anchors:?}"); assert!( anchors[0].distance(first) < 1e-6 && anchors[1].distance(DVec2::new(400., 300.)) < 1e-6, From 117873e956270592002aae7992ad02b384fce0d6 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 21 Aug 2026 21:53:58 -0700 Subject: [PATCH 6/8] Show the point pool and paths directly in the Data panel --- .../document/data_panel/data_panel_message.rs | 12 -- .../data_panel/data_panel_message_handler.rs | 112 +++++++----------- 2 files changed, 43 insertions(+), 81 deletions(-) diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs index 9efc67eba89..1fdd7f506f1 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message.rs @@ -20,10 +20,6 @@ pub enum DataPanelMessage { TruncateElementPath { len: usize, }, - - ViewVectorTableTab { - tab: VectorTableTab, - }, } /// One hop in the breadcrumb path through nested data the Data panel is displaying. @@ -33,11 +29,3 @@ pub enum PathStep { Element(usize), Attribute { row: usize, key: String }, } - -#[derive(PartialEq, Eq, Clone, Copy, Default, Debug, serde::Serialize, serde::Deserialize)] -pub enum VectorTableTab { - #[default] - Points, - Segments, - Handles, -} diff --git a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs index 4f0c0145a65..0eb405616e5 100644 --- a/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/data_panel/data_panel_message_handler.rs @@ -1,4 +1,3 @@ -use super::VectorTableTab; use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, LayoutTarget}; use crate::messages::portfolio::document::data_panel::{DataPanelMessage, PathStep}; use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface; @@ -25,14 +24,14 @@ use graphene_std::text_nodes::{StringCapitalization, TextDenomination}; use graphene_std::transfer_curve::TransferCurve; use graphene_std::transform::{ReferencePoint, ScaleType}; use graphene_std::vector::misc::{ - ArcType, BezierHandles, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, - SpiralType, + ArcType, BooleanOperation, BoxCorners, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType, }; use graphene_std::vector::style::{ DashPattern, FillChoice, GradientForm, GradientHueDirection, GradientInterpolation, GradientRamp, GradientSettings, GradientSpace, GradientSpread, StrokeAlign, StrokeCap, StrokeJoin, }; -use graphene_std::vector::{QRCodeErrorCorrectionLevel, Vector}; +use graphene_std::vector::{CurveType, Path, PointId, QRCodeErrorCorrectionLevel, SegmentId, Vector}; use graphene_std::{Appearance, Artboard, Color, Context, Cover, Coverage, Graphic}; +use kurbo::BezPath; use std::any::Any; use std::sync::Arc; @@ -51,7 +50,6 @@ pub struct DataPanelMessageHandler { introspected_node_path: Vec, introspected_data: Option>, element_path: Vec, - active_vector_table_tab: VectorTableTab, } #[message_handler_data] @@ -67,7 +65,6 @@ impl MessageHandler> for DataPanel self.introspected_node_path.clear(); self.introspected_data = None; self.element_path.clear(); - self.active_vector_table_tab = VectorTableTab::default(); self.update_layout(responses, context); } DataPanelMessage::Refresh => { @@ -85,11 +82,6 @@ impl MessageHandler> for DataPanel self.element_path.truncate(len); self.update_layout(responses, context); } - - DataPanelMessage::ViewVectorTableTab { tab } => { - self.active_vector_table_tab = tab; - self.update_layout(responses, context); - } } } @@ -114,7 +106,6 @@ impl DataPanelMessageHandler { node_lookup_network_path: Vec::new(), gradient_settings: GradientSettings::default(), breadcrumbs: Vec::new(), - vector_table_tab: self.active_vector_table_tab, }; // Main data visualization @@ -172,7 +163,6 @@ struct LayoutData<'a> { /// rather than in the stop list itself. The enclosing `Item`/`List` sets it to the owning row's attributes. gradient_settings: GradientSettings, breadcrumbs: Vec, - vector_table_tab: VectorTableTab, } macro_rules! generate_layout_downcast { @@ -655,70 +645,42 @@ impl TableItemLayout for Graphic { } } +// The pool is a plain point list with its IDs riding its attributes, so the generic list layout shows it as-is impl TableItemLayout for Vector { fn type_name() -> &'static str { "Vector" } fn identifier(&self) -> String { - format!( - "Vector ({} point{}, {} segment{})", - self.point_domain.ids().len(), - if self.point_domain.ids().len() == 1 { "" } else { "s" }, - self.segment_domain.ids().len(), - if self.segment_domain.ids().len() == 1 { "" } else { "s" } - ) + self.0.identifier() } fn value_page(&self, data: &mut LayoutData) -> Vec { - let table_tab_entries = [VectorTableTab::Points, VectorTableTab::Segments, VectorTableTab::Handles] - .into_iter() - .map(|tab| { - RadioEntryData::new(format!("{tab:?}")) - .label(format!("{tab:?}")) - .on_update(move |_| DataPanelMessage::ViewVectorTableTab { tab }.into()) - }) - .collect(); - let table_tabs = vec![RadioInput::new(table_tab_entries).selected_index(Some(data.vector_table_tab as u32)).widget_instance()]; - - let mut table_rows = Vec::new(); - match data.vector_table_tab { - VectorTableTab::Handles => { - table_rows.push(column_headings(&["", "colinear_manipulators[0]", "colinear_manipulators[1]"])); - table_rows.extend(self.colinear_manipulators.iter().enumerate().map(|(index, [a, b])| { - vec![ - TextLabel::new(format!("{index}")).narrow(true).widget_instance(), - TextLabel::new(format!("{a}")).narrow(true).widget_instance(), - TextLabel::new(format!("{b}")).narrow(true).widget_instance(), - ] - })); - } - VectorTableTab::Points => { - table_rows.push(column_headings(&["", "position"])); - table_rows.extend(self.point_domain.iter().map(|(id, position)| { - vec![ - TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(), - TextLabel::new(format_dvec2(position)).narrow(true).widget_instance(), - ] - })); - } - VectorTableTab::Segments => { - table_rows.push(column_headings(&["", "start_point", "end_point", "handles"])); - table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| { - let handles = match handles { - BezierHandles::Linear => "Linear".to_string(), - BezierHandles::Quadratic { handle } => format!("Quadratic — {}", format_dvec2(handle)), - BezierHandles::Cubic { handle_start, handle_end } => format!("Cubic — start: {}, end: {}", format_dvec2(handle_start), format_dvec2(handle_end)), - }; - vec![ - TextLabel::new(format!("{}", id.inner())).narrow(true).widget_instance(), - TextLabel::new(format!("Point {start}")).narrow(true).widget_instance(), - TextLabel::new(format!("Point {end}")).narrow(true).widget_instance(), - TextLabel::new(handles).narrow(true).widget_instance(), - ] - })); - } - } + self.0.value_page(data) + } +} - vec![LayoutGroup::row(table_tabs), LayoutGroup::table(table_rows, false)] +// A path is its list of point references, with segment IDs and smooth flags riding its attributes +impl TableItemLayout for Path { + fn type_name() -> &'static str { + "Path" + } + fn identifier(&self) -> String { + self.0.identifier() + } + fn value_page(&self, data: &mut LayoutData) -> Vec { + self.0.value_page(data) + } +} + +// Only the click target attribute holds a bezpath, so it's named for what it does +impl TableItemLayout for BezPath { + fn type_name() -> &'static str { + "Click Target" + } + fn identifier(&self) -> String { + Self::type_name().to_string() + } + fn value_widgets(&self, _target: PathStep, _data: &LayoutData) -> Vec { + vec![TextLabel::new(self.identifier()).narrow(true).widget_instance()] } } @@ -1151,6 +1113,7 @@ macro_rules! known_item_types { List, List, List, + List, List>, List>, List, @@ -1170,6 +1133,8 @@ macro_rules! known_item_types { bool, String, Vector, + Path, + BezPath, Raster, Raster, Graphic, @@ -1230,6 +1195,15 @@ fn display_value_override(any: &dyn Any) -> Option { if let Some(value) = any.downcast_ref::() { return Some(value.to_string()); } + if let Some(value) = any.downcast_ref::() { + return Some(if *value == PointId::UNNAMED { String::new() } else { value.inner().to_string() }); + } + if let Some(value) = any.downcast_ref::() { + return Some(if *value == SegmentId::ZERO { String::new() } else { value.inner().to_string() }); + } + if let Some(value) = any.downcast_ref::() { + return Some(value.to_string()); + } None } From bd9c67244ab8ee37a777dbfe447aca0f074be49b Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 21 Aug 2026 21:55:32 -0700 Subject: [PATCH 7/8] Port the vector nodes to pool storage and bring the workspace green --- .../vector-types/src/vector/geometry.rs | 4 +- .../vector-types/src/vector/vector_types.rs | 8 + .../nodes/vector/src/generator_nodes.rs | 100 +- .../nodes/vector/src/merge_qr_squares.rs | 10 +- .../vector/src/vector_modification_nodes.rs | 16 +- node-graph/nodes/vector/src/vector_nodes.rs | 1263 ++++++++++------- 6 files changed, 823 insertions(+), 578 deletions(-) diff --git a/node-graph/libraries/vector-types/src/vector/geometry.rs b/node-graph/libraries/vector-types/src/vector/geometry.rs index f20f22472bb..7685be903db 100644 --- a/node-graph/libraries/vector-types/src/vector/geometry.rs +++ b/node-graph/libraries/vector-types/src/vector/geometry.rs @@ -72,7 +72,9 @@ impl VectorGeometry { /// The ID and position of every anchor and free point, found in one pass over the paths. pub fn anchors(&self) -> impl Iterator + '_ { let anchor_indices = self.vector.anchor_indices(&self.paths); - anchor_indices.into_iter().filter_map(|index| Some((self.vector.point_id_at(index as u32)?, self.vector.position(index)?))) + anchor_indices + .into_iter() + .filter_map(|index| Some((self.vector.point_id_at(index as u32)?, self.vector.position(index)?))) } /// The ID and position of each pool point that no path references, found in one pass over the paths. diff --git a/node-graph/libraries/vector-types/src/vector/vector_types.rs b/node-graph/libraries/vector-types/src/vector/vector_types.rs index a8c9cfd25f3..86f51aa7b99 100644 --- a/node-graph/libraries/vector-types/src/vector/vector_types.rs +++ b/node-graph/libraries/vector-types/src/vector/vector_types.rs @@ -580,6 +580,14 @@ impl Vector { self.set_smooth_at_indices(paths, [index], smooth); } + /// Sets the smooth flag on many anchors at once, finding them in one pass over the pool's IDs. + pub fn set_smooth_points(&mut self, paths: &mut List, points: impl IntoIterator, smooth: bool) { + let points: std::collections::HashSet = points.into_iter().collect(); + let indices: Vec = (0..self.0.len() as u32).filter(|&index| self.point_id_at(index).is_some_and(|id| points.contains(&id))).collect(); + + self.set_smooth_at_indices(paths, indices, smooth); + } + /// Sets the smooth flag on many anchors at once, reading one connectivity rather than one per anchor. pub(crate) fn set_smooth_at_indices(&mut self, paths: &mut List, indices: impl IntoIterator, smooth: bool) { let mut indices = indices.into_iter().peekable(); diff --git a/node-graph/nodes/vector/src/generator_nodes.rs b/node-graph/nodes/vector/src/generator_nodes.rs index 18349b0f704..48010419c9a 100644 --- a/node-graph/nodes/vector/src/generator_nodes.rs +++ b/node-graph/nodes/vector/src/generator_nodes.rs @@ -1,14 +1,14 @@ -use core_types::list::{Item, List}; +use crate::vector_nodes::{push_working_point, push_working_segment}; +use core_types::list::{ATTR_PATHS, Item, List}; use core_types::{CacheHash, Ctx}; use dyn_any::DynAny; use glam::DVec2; use graphic_types::Vector; -use vector_types::vector::VectorExt; use vector_types::vector::algorithms::shapes; use vector_types::vector::misc::BezierHandles; +use vector_types::vector::misc::SpiralType; use vector_types::vector::misc::{ArcType, BoxCorners, GridType}; -use vector_types::vector::misc::{HandleId, SpiralType}; -use vector_types::vector::{PointId, SegmentId}; +use vector_types::vector::{Path, PointId, SegmentId, WorkingVector}; /// Generates a circle shape with a chosen radius. #[node_macro::node(category("Vector: Shape"), icon("NodeCircle"))] @@ -20,7 +20,7 @@ fn circle( radius: Item, ) -> Item { let radius = radius.element().abs(); - Item::new_from_element(Vector::from_bezpath(shapes::ellipse_bezpath(DVec2::splat(-radius), DVec2::splat(radius)))) + Vector::item_from_bezpath(&shapes::ellipse_bezpath(DVec2::splat(-radius), DVec2::splat(radius))) } /// Generates an arc shape forming a portion of a circle which may be open, closed, or a pie slice. @@ -43,12 +43,12 @@ fn arc( arc_type: Item, ) -> Item { let (radius, start_angle, sweep_angle, arc_type) = (*radius.element(), *start_angle.element(), *sweep_angle.element(), arc_type.into_element()); - Item::new_from_element(Vector::from_bezpath(shapes::arc_bezpath( + Vector::item_from_bezpath(&shapes::arc_bezpath( radius, start_angle / 360. * std::f64::consts::TAU, sweep_angle / 360. * std::f64::consts::TAU, arc_type, - ))) + )) } /// Generates a spiral shape that winds from an inner to an outer radius. @@ -78,14 +78,14 @@ fn spiral( *outer_radius.element(), *angular_resolution.element(), ); - Item::new_from_element(Vector::from_bezpath(shapes::spiral_bezpath( + Vector::item_from_bezpath(&shapes::spiral_bezpath( inner_radius, outer_radius, turns, start_angle.to_radians(), angular_resolution.to_radians(), spiral_type.into_element(), - ))) + )) } /// Generates an ellipse shape (an oval or stretched circle) with the chosen radii. @@ -104,16 +104,15 @@ fn ellipse( let corner1 = -radius; let corner2 = radius; - let mut ellipse = Vector::from_bezpath(shapes::ellipse_bezpath(corner1, corner2)); + let (mut ellipse, mut paths) = Vector::from_bezpath(&shapes::ellipse_bezpath(corner1, corner2)); - let len = ellipse.segment_domain.ids().len(); - for i in 0..len { - ellipse - .colinear_manipulators - .push([HandleId::end(ellipse.segment_domain.ids()[i]), HandleId::primary(ellipse.segment_domain.ids()[(i + 1) % len])]); + // Each junction between consecutive segments keeps its tangent pair smooth + for segment in ellipse.segment_ids(&paths) { + let Some(anchor) = ellipse.segment_end_from_id(&paths, segment) else { continue }; + ellipse.set_smooth(&mut paths, anchor, true); } - Item::new_from_element(ellipse) + Item::new_from_element(ellipse).with_attribute(ATTR_PATHS, paths) } /// Generates a rectangle shape with the chosen width and height. It may also have rounded corners if desired. @@ -152,7 +151,7 @@ fn rectangle( radii }; - Item::new_from_element(Vector::from_bezpath(shapes::rounded_rectangle_bezpath(size / -2., size / 2., radii))) + Vector::item_from_bezpath(&shapes::rounded_rectangle_bezpath(size / -2., size / 2., radii)) } /// Builds a set of four corner values, such as a rectangle's corner radii, from a list of one, two, three, or four values. @@ -179,7 +178,7 @@ fn regular_polygon( radius: Item, ) -> Item { let points = *sides.element() as u64; - Item::new_from_element(Vector::from_bezpath(shapes::regular_polygon_bezpath(DVec2::ZERO, points, *radius.element()))) + Vector::item_from_bezpath(&shapes::regular_polygon_bezpath(DVec2::ZERO, points, *radius.element())) } /// Generates an n-pointed star shape with inner and outer points at chosen radii from the center. @@ -198,7 +197,7 @@ fn star( radius_2: Item, ) -> Item { let points = *sides.element() as u64; - Item::new_from_element(Vector::from_bezpath(shapes::star_polygon_bezpath(DVec2::ZERO, points, *radius_1.element(), *radius_2.element()))) + Vector::item_from_bezpath(&shapes::star_polygon_bezpath(DVec2::ZERO, points, *radius_1.element(), *radius_2.element())) } #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] @@ -244,24 +243,24 @@ fn qr_code( }; let Ok(qr_code) = qrcodegen::QrCode::encode_text(&text, ecc) else { - return Item::new_from_element(Vector::default()); + return Item::new_from_element(Vector::default()).with_attribute(ATTR_PATHS, List::::new()); }; - let mut vector = match individual_squares { + let (mut vector, paths) = match individual_squares { true => { - let mut vector = Vector::default(); + let (mut vector, mut paths) = (Vector::default(), List::new()); let dimension = qr_code.size() as usize; for y in 0..dimension { for x in 0..dimension { if qr_code.get_module(x as i32, y as i32) { let corner1 = DVec2::new(x as f64, y as f64); - vector.append_bezpath(shapes::rectangle_bezpath(corner1, corner1 + DVec2::splat(1.))); + vector.append_bezpath(&mut paths, &shapes::rectangle_bezpath(corner1, corner1 + DVec2::splat(1.))); } } } - vector + (vector, paths) } false => crate::merge_qr_squares::merge_qr_squares(&qr_code), }; @@ -270,7 +269,7 @@ fn qr_code( vector.transform(glam::DAffine2::from_scale(DVec2::splat(size / qr_code.size() as f64))); } - Item::new_from_element(vector) + Item::new_from_element(vector).with_attribute(ATTR_PATHS, paths) } /// Generates an arrow from the origin to the chosen coordinate. @@ -295,7 +294,7 @@ fn arrow( head_length: Item, ) -> Item { let (arrow_to, shaft_width, head_width, head_length) = (*arrow_to.element(), *shaft_width.element(), *head_width.element(), *head_length.element()); - Item::new_from_element(Vector::from_bezpath(shapes::arrow_bezpath(DVec2::ZERO, arrow_to, shaft_width, head_width, head_length))) + Vector::item_from_bezpath(&shapes::arrow_bezpath(DVec2::ZERO, arrow_to, shaft_width, head_width, head_length)) } #[node_macro::node(category("Vector: Shape"), icon("NodeLine"))] @@ -306,7 +305,7 @@ fn line( #[default(100., 100.)] line_to: Item, ) -> Item { - Item::new_from_element(Vector::from_bezpath(shapes::line_bezpath(DVec2::ZERO, *line_to.element()))) + Vector::item_from_bezpath(&shapes::line_bezpath(DVec2::ZERO, *line_to.element())) } trait GridSpacing { @@ -379,25 +378,24 @@ fn grid( cells.push(vec![position(x, y), position(x + 1, y), position(x + 1, y + 1), position(x, y + 1)]); } } - let mut vector = Vector::default(); - crate::vector_nodes::replace_with_polygons(&mut vector, cells, connect_cells); - return Item::new_from_element(vector); + let (mut vector, mut paths) = (Vector::default(), List::new()); + crate::vector_nodes::replace_with_polygons(&mut vector, &mut paths, cells, connect_cells); + return Item::new_from_element(vector).with_attribute(ATTR_PATHS, paths); } - let mut vector = Vector::default(); + let mut working = WorkingVector::default(); let mut segment_id = SegmentId::ZERO; let mut point_id = PointId::ZERO; for y in 0..rows { for x in 0..columns { // Add the current point to the grid. - let current_index = vector.point_domain.ids().len(); - vector.point_domain.push(point_id.next_id(), position(x, y)); + let current_index = push_working_point(&mut working, point_id.next_id(), position(x, y)); // Helper function to connect points with line segments. let mut push_segment = |to_index: Option| { if let Some(other_index) = to_index { - vector.segment_domain.push(segment_id.next_id(), other_index, current_index, BezierHandles::Linear); + push_working_segment(&mut working, segment_id.next_id(), other_index, current_index, BezierHandles::Linear); } }; @@ -418,7 +416,10 @@ fn grid( } } - Item::new_from_element(vector) + let (mut vector, mut paths) = (Vector::default(), List::new()); + working.reweave(&mut vector, &mut paths); + + Item::new_from_element(vector).with_attribute(ATTR_PATHS, paths) } #[cfg(test)] @@ -426,6 +427,7 @@ mod tests { use super::*; use kurbo::ParamCurve; use vector_types::vector::misc::point_to_dvec2; + use vector_types::vector::paths_from_item_attributes; fn item(value: T) -> Item { Item::new_from_element(value) @@ -439,9 +441,10 @@ mod tests { // Works properly let grid = grid((), (), item(GridType::Isometric), item(10.), item(5_i64), item(5_i64), item((30., 30.).into()), item(true)); - assert_eq!(grid.element().point_domain.ids().len(), 5 * 5); - assert_eq!(grid.element().segment_iter().count(), 4 * 5 + 4 * 9); - for (_, segment, _, _) in grid.element().segment_iter() { + let paths = paths_from_item_attributes(&grid).cloned().unwrap_or_default(); + assert_eq!(grid.element().anchor_points(&paths).count(), 5 * 5); + assert_eq!(grid.element().segment_iter(&paths).count(), 4 * 5 + 4 * 9); + for (_, segment, _, _) in grid.element().segment_iter(&paths) { assert!(matches!(segment, kurbo::PathSeg::Line(_))); let span = point_to_dvec2(segment.start()) - point_to_dvec2(segment.end()); assert!((span.length() - 10.).abs() < 1e-5, "Length of {} should be 10", span.length()); @@ -451,9 +454,10 @@ mod tests { #[test] fn skew_isometric_grid_test() { let grid = grid((), (), item(GridType::Isometric), item(10.), item(5_i64), item(5_i64), item((40., 30.).into()), item(true)); - assert_eq!(grid.element().point_domain.ids().len(), 5 * 5); - assert_eq!(grid.element().segment_iter().count(), 4 * 5 + 4 * 9); - for (_, segment, _, _) in grid.element().segment_iter() { + let paths = paths_from_item_attributes(&grid).cloned().unwrap_or_default(); + assert_eq!(grid.element().anchor_points(&paths).count(), 5 * 5); + assert_eq!(grid.element().segment_iter(&paths).count(), 4 * 5 + 4 * 9); + for (_, segment, _, _) in grid.element().segment_iter(&paths) { assert!(matches!(segment, kurbo::PathSeg::Line(_))); let vector = point_to_dvec2(segment.start()) - point_to_dvec2(segment.end()); let angle = (vector.angle_to(DVec2::X).to_degrees() + 180.) % 180.; @@ -465,13 +469,14 @@ mod tests { fn grid_disconnected_cells_test() { // A 3x3 rectangular grid has a 2x2 arrangement of cells, each its own closed quad subpath. let grid = grid((), (), item(GridType::Rectangular), item(10.), item(3_i64), item(3_i64), item((30., 30.).into()), item(false)); + let paths = paths_from_item_attributes(&grid).cloned().unwrap_or_default(); let vector = grid.element(); - assert_eq!(vector.stroke_manipulator_groups().filter(|(_, closed)| *closed).count(), 4); - assert_eq!(vector.point_domain.ids().len(), 4 * 4); - assert_eq!(vector.segment_domain.ids().len(), 4 * 4); + assert_eq!(vector.stroke_manipulator_groups(&paths).filter(|(_, closed)| *closed).count(), 4); + assert_eq!(vector.anchor_points(&paths).count(), 4 * 4); + assert_eq!(vector.segment_ids(&paths).len(), 4 * 4); // Each cell winds counter-clockwise (positive signed area), matching the shape generators. - for (group, closed) in vector.stroke_manipulator_groups() { + for (group, closed) in vector.stroke_manipulator_groups(&paths) { assert!(closed); let anchors: Vec = group.iter().map(|g| g.anchor).collect(); let signed_area: f64 = (0..anchors.len()).map(|i| anchors[i].perp_dot(anchors[(i + 1) % anchors.len()])).sum::() / 2.; @@ -490,7 +495,8 @@ mod tests { item(QRCodeErrorCorrectionLevel::Low), item(true), ); - assert!(!qr.element().point_domain.ids().is_empty()); - assert!(!qr.element().segment_domain.ids().is_empty()); + let paths = paths_from_item_attributes(&qr).cloned().unwrap_or_default(); + assert!(qr.element().anchor_points(&paths).count() > 0); + assert!(!qr.element().segment_ids(&paths).is_empty()); } } diff --git a/node-graph/nodes/vector/src/merge_qr_squares.rs b/node-graph/nodes/vector/src/merge_qr_squares.rs index c14f6fe9a09..ee02a93426d 100644 --- a/node-graph/nodes/vector/src/merge_qr_squares.rs +++ b/node-graph/nodes/vector/src/merge_qr_squares.rs @@ -1,11 +1,13 @@ +use core_types::list::List; use glam::DVec2; use graphic_types::Vector; use std::collections::VecDeque; -use vector_types::vector::VectorExt; +use vector_types::vector::Path; use vector_types::vector::algorithms::shapes; -pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode) -> Vector { +pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode) -> (Vector, List) { let mut vector = Vector::default(); + let mut paths = List::new(); let size = qr_code.size() as usize; @@ -107,7 +109,7 @@ pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode) -> Vector { } if !simplified.is_empty() { - vector.append_bezpath(shapes::polyline_bezpath(simplified, true)); + vector.append_bezpath(&mut paths, &shapes::polyline_bezpath(simplified, true)); } } } @@ -121,5 +123,5 @@ pub fn merge_qr_squares(qr_code: &qrcodegen::QrCode) -> Vector { } } - vector + (vector, paths) } diff --git a/node-graph/nodes/vector/src/vector_modification_nodes.rs b/node-graph/nodes/vector/src/vector_modification_nodes.rs index b4467fb180a..8c201ca64c4 100644 --- a/node-graph/nodes/vector/src/vector_modification_nodes.rs +++ b/node-graph/nodes/vector/src/vector_modification_nodes.rs @@ -4,20 +4,24 @@ use core_types::uuid::NodeId; use core_types::{ATTR_EDITOR_CLICK_TARGET, ATTR_EDITOR_LAYER_PATH, ATTR_TRANSFORM, Ctx}; use glam::{DAffine2, DVec2}; use graphic_types::Vector; -use vector_types::vector::VectorModification; +use vector_types::kurbo::BezPath; +use vector_types::vector::{VectorModification, edit_item_geometry}; /// Applies a differential modification to a vector path, associating changes made by the Pen and Path tools to indices of edited points and segments. #[node_macro::node(category(""))] async fn path_modify(_ctx: impl Ctx, vector: Item, modification: Item>, node_path: Item) -> Item { let mut vector = vector; - modification.into_element().apply(vector.element_mut()); + let modification = modification.into_element(); + edit_item_geometry(&mut vector, |element, paths| { + modification.apply(element, paths); - // Users draw subpaths in arbitrary winding directions, so normalize them here rather than - // letting the drawn direction decide fill insideness downstream - vector.element_mut().normalize_winding_directions(); + // Users draw subpaths in arbitrary winding directions, so normalize them here rather than + // letting the drawn direction decide fill insideness downstream + element.normalize_winding_directions(paths); + }); // Drop the stale click-target override so hit testing uses the geometry the user is now editing - vector.remove_attribute::(ATTR_EDITOR_CLICK_TARGET); + vector.remove_attribute::(ATTR_EDITOR_CLICK_TARGET); // Set the path to the encapsulating subgraph (drop our own trailing entry from `node_path`), // matching the `path_of_subgraph` proto so editor tools can route data back to the parent layer. diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 1c43b43fc6b..a7bc75d1f71 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -3,7 +3,7 @@ use core::f64::consts::{PI, TAU}; use core::hash::{Hash, Hasher}; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; -use core_types::list::{ATTR_APPEARANCE, Item, ItemAttributeValues, List, ListDyn, NodeIdPath}; +use core_types::list::{ATTR_APPEARANCE, ATTR_ID, ATTR_PATHS, Item, ItemAttributeValues, List, ListDyn, NodeIdPath}; use core_types::transform::{Footprint, Transform}; use core_types::uuid::NodeId; use core_types::{ @@ -29,11 +29,45 @@ use vector_types::vector::algorithms::merge_by_distance::MergeByDistanceExt; use vector_types::vector::algorithms::offset_bezpath::offset_bezpath; use vector_types::vector::algorithms::spline::{solve_spline_first_handle_closed, solve_spline_first_handle_open}; use vector_types::vector::misc::{ - BezierHandles, CentroidType, ExtrudeJoiningAlgorithm, HandleId, InterpolationDistribution, ManipulatorGroup, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, Tangent, + BezierHandles, CentroidType, ExtrudeJoiningAlgorithm, InterpolationDistribution, ManipulatorGroup, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, Tangent, bezpath_from_manipulator_groups, dvec2_to_point, handles_to_segment, is_linear, point_to_dvec2, segment_to_handles, }; use vector_types::vector::style::{DashPattern, Gradient, GradientSettings, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; -use vector_types::vector::{PointDomain, PointId, SegmentDomain, SegmentId, VectorExt}; +use vector_types::vector::{Path, PointId, SegmentId, WorkingVector, curve_type_at, edit_item_geometry, no_paths, paths_from_item_attributes, paths_from_list_row_attributes}; + +/// Appends one point to the working form, returning the index that names it. +pub(crate) fn push_working_point(working: &mut WorkingVector, id: PointId, position: DVec2) -> usize { + let index = working.point_ids.len(); + working.point_ids.push(id); + working.point_positions.push(position); + + index +} + +/// Appends one segment to the working form, running between two of its point indices. +pub(crate) fn push_working_segment(working: &mut WorkingVector, id: SegmentId, start: usize, end: usize, handles: BezierHandles) { + working.segment_ids.push(id); + working.segment_start.push(start); + working.segment_end.push(end); + working.segment_handles.push(handles); +} + +/// The ID continuing the working form's point sequence. +fn next_working_point_id(working: &WorkingVector) -> PointId { + working + .point_ids + .iter() + .copied() + .filter(|&id| id != PointId::UNNAMED) + .max() + .map(|mut id| id.next_id()) + .unwrap_or(PointId::ZERO) +} + +/// The ID continuing the working form's segment sequence. +fn next_working_segment_id(working: &WorkingVector) -> SegmentId { + working.segment_ids.iter().copied().max().map(|mut id| id.next_id()).unwrap_or(SegmentId::ZERO) +} /// Implemented for `List` types that contain vector items reachable via mutable access. /// Used by the whole-collection Assign Colors node so it can apply to either `List` or `List`. @@ -87,28 +121,32 @@ impl VectorListIterMut for List { /// Element-level analog of [`VectorListIterMut`] for the element-wise fill and stroke nodes, operating on a /// single `Item` or `Item`. trait VectorItemMut { - fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, DAffine2)); + fn for_each_vector_mut(&mut self, f: impl FnMut(&mut Vector, &List, DAffine2)); } impl VectorItemMut for Item { - fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, DAffine2)) { + fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, &List, DAffine2)) { + let paths = paths_from_item_attributes(self).cloned().unwrap_or_default(); let transform = self.attribute_cloned_or_default::(ATTR_TRANSFORM); - f(self.element_mut(), transform); + f(self.element_mut(), &paths, transform); } } impl VectorItemMut for Item { - fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, DAffine2)) { + fn for_each_vector_mut(&mut self, mut f: impl FnMut(&mut Vector, &List, DAffine2)) { match self.element_mut() { Graphic::Vector(item) => { + let paths = paths_from_item_attributes(item).cloned().unwrap_or_default(); let transform = item.attribute_cloned_or_default::(ATTR_TRANSFORM); - f(item.element_mut(), transform); + f(item.element_mut(), &paths, transform); } element => { let Some(vector_list) = element.as_vector_list_mut() else { return }; - let (elements, transforms) = vector_list.element_and_attribute_slices_mut::(ATTR_TRANSFORM); - for (vector, transform) in elements.iter_mut().zip(transforms.iter()) { - f(vector, *transform); + for index in 0..vector_list.len() { + let paths = paths_from_list_row_attributes(vector_list, index).cloned().unwrap_or_default(); + let transform = vector_list.attribute_cloned_or_default::(ATTR_TRANSFORM, index); + let Some(vector) = vector_list.element_mut(index) else { continue }; + f(vector, &paths, transform); } } } @@ -120,8 +158,8 @@ impl VectorItemMut for Item { trait MapVectorItems: Sized { fn map_vector_items(content: Item, f: impl FnMut(Item) -> Item) -> Item; - /// Mutable access to each reachable vector at once, for callers that must await between elements. - fn vector_elements_mut(content: &mut Item) -> Vec<&mut Vector>; + /// Mutable access to each reachable vector at once, each paired with a copy of its paths, for callers that must await between elements. + fn vector_elements_mut(content: &mut Item) -> Vec<(&mut Vector, List)>; } impl MapVectorItems for Vector { @@ -129,8 +167,10 @@ impl MapVectorItems for Vector { f(content) } - fn vector_elements_mut(content: &mut Item) -> Vec<&mut Vector> { - vec![content.element_mut()] + fn vector_elements_mut(content: &mut Item) -> Vec<(&mut Vector, List)> { + let paths = paths_from_item_attributes(content).cloned().unwrap_or_default(); + + vec![(content.element_mut(), paths)] } } @@ -153,12 +193,18 @@ impl MapVectorItems for Graphic { content } - fn vector_elements_mut(content: &mut Item) -> Vec<&mut Vector> { - fn collect<'a>(graphic: &'a mut Graphic, elements: &mut Vec<&'a mut Vector>) { + fn vector_elements_mut(content: &mut Item) -> Vec<(&mut Vector, List)> { + fn collect<'a>(graphic: &'a mut Graphic, elements: &mut Vec<(&'a mut Vector, List)>) { match graphic { - Graphic::VectorList(list) => elements.extend(list.iter_element_values_mut()), + Graphic::VectorList(list) => { + let paths: Vec> = (0..list.len()).map(|index| paths_from_list_row_attributes(list, index).cloned().unwrap_or_default()).collect(); + elements.extend(list.iter_element_values_mut().zip(paths)); + } Graphic::GraphicList(list) => list.iter_element_values_mut().for_each(|nested| collect(nested, elements)), - Graphic::Vector(item) => elements.push(item.element_mut()), + Graphic::Vector(item) => { + let paths = paths_from_item_attributes(&**item).cloned().unwrap_or_default(); + elements.push((item.element_mut(), paths)); + } Graphic::Graphic(item) => collect(item.element_mut(), elements), _ => {} } @@ -342,8 +388,8 @@ where } let mut bounds: Option<[DVec2; 2]> = None; - content.for_each_vector_mut(|vector, _| { - if let Some([min, max]) = vector.bounding_box() { + content.for_each_vector_mut(|vector, vector_paths, _| { + if let Some([min, max]) = vector.bounding_box(vector_paths) { bounds = Some(match bounds { Some([bmin, bmax]) => [bmin.min(min), bmax.max(max)], None => [min, max], @@ -522,7 +568,8 @@ async fn copy_to_points( let do_rotation = random_rotation.abs() > 1e-6; let points_transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); - for &point in row.element().point_domain.positions() { + let row_paths = paths_from_item_attributes(&row).cloned().unwrap_or_default(); + for point in row.element().anchor_positions(&row_paths) { let translation = points_transform.transform_point2(point); let rotation = if do_rotation { @@ -588,30 +635,32 @@ async fn round_corners( let (radius, roundness, edge_length_limit, min_angle_threshold) = (*radius.element(), *roundness.element(), *edge_length_limit.element(), *min_angle_threshold.element()); let source_transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); let source_transform_inverse = source_transform.inverse(); - let (source, attributes) = source.into_parts(); + let source_paths = paths_from_item_attributes(&source).cloned().unwrap_or_default(); + let (source, mut attributes) = source.into_parts(); // Flip the roundness to help with user intuition let roundness = 1. - roundness; // Convert 0-100 to 0-0.5 let edge_length_limit = edge_length_limit * 0.005; - let mut result = Vector::default(); + let (mut result, mut result_paths) = (Vector::default(), List::new()); - for bezpath in source.stroke_bezpath_iter() { + for bezpath in source.stroke_bezpath_iter(&source_paths) { // The radius is measured after the transform, so scaling the layer doesn't scale its rounding let mut document_bezpath = bezpath.clone(); document_bezpath.apply_affine(Affine::new(source_transform.to_cols_array())); // A subpath without corners to round passes through as it was let Some(mut rounded_subpath) = round_subpath_corners(&document_bezpath, radius, roundness, edge_length_limit, min_angle_threshold.to_radians()) else { - result.append_bezpath(bezpath); + result.append_bezpath(&mut result_paths, &bezpath); continue; }; rounded_subpath.apply_affine(Affine::new(source_transform_inverse.to_cols_array())); - result.append_bezpath(rounded_subpath); + result.append_bezpath(&mut result_paths, &rounded_subpath); } + attributes.insert(ATTR_PATHS, result_paths); Item::from_parts(result, attributes) }) } @@ -730,9 +779,9 @@ fn merge_by_distance( match algorithm { MergeByDistanceAlgorithm::Spatial => { let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); - content.element_mut().merge_by_distance_spatial(transform, distance); + edit_item_geometry(&mut content, |vector, paths| vector.merge_by_distance_spatial(paths, transform, distance)); } - MergeByDistanceAlgorithm::Topological => content.element_mut().merge_by_distance_topological(distance), + MergeByDistanceAlgorithm::Topological => edit_item_geometry(&mut content, |vector, paths| vector.merge_by_distance_topological(paths, distance)), } content @@ -740,10 +789,13 @@ fn merge_by_distance( } pub mod extrude_algorithms { + use super::{next_working_point_id, next_working_segment_id, push_working_point, push_working_segment}; + use core_types::list::List; use glam::DVec2; use kurbo::{ParamCurve, ParamCurveDeriv}; use vector_types::vector::misc::BezierHandles; - use vector_types::vector::misc::ExtrudeJoiningAlgorithm; + use vector_types::vector::misc::{ExtrudeJoiningAlgorithm, handles_to_segment}; + use vector_types::vector::{Path, WorkingVector}; /// Convert [`kurbo::CubicBez`] to [`vector_types::vector::misc::BezierHandles`]. fn cubic_to_handles(cubic_bez: kurbo::CubicBez) -> BezierHandles { @@ -769,14 +821,16 @@ pub mod extrude_algorithms { } /// Split so segments no longer have tangents on both sides of the direction vector. - fn split(vector: &mut graphic_types::Vector, direction: DVec2) { - let segment_count = vector.segment_domain.ids().len(); - let mut next_point = vector.point_domain.next_id(); - let mut next_segment = vector.segment_domain.next_id(); + fn split(working: &mut WorkingVector, direction: DVec2) { + let segment_count = working.segment_ids.len(); + let mut next_point = next_working_point_id(working); + let mut next_segment = next_working_segment_id(working); for segment_index in 0..segment_count { - let (_, _, segment) = vector.segment_points_from_index(segment_index); - let mut start_index = vector.segment_domain.start_point()[segment_index]; + let start = working.point_positions[working.segment_start[segment_index]]; + let end = working.point_positions[working.segment_end[segment_index]]; + let segment = handles_to_segment(start, working.segment_handles[segment_index], end); + let mut start_index = working.segment_start[segment_index]; let pathseg = segment.to_cubic(); let mut start_t = 0.; @@ -786,11 +840,10 @@ pub mod extrude_algorithms { let middle_point = next_point.next_id(); let start_segment = next_segment.next_id(); - let middle_point_index = vector.point_domain.len(); - vector.point_domain.push(middle_point, DVec2::new(first.end().x, first.end().y)); - vector.segment_domain.push(start_segment, start_index, middle_point_index, first_handles); - vector.segment_domain.set_start_point(segment_index, middle_point_index); - vector.segment_domain.set_handles(segment_index, second_handles); + let middle_point_index = push_working_point(working, middle_point, DVec2::new(first.end().x, first.end().y)); + push_working_segment(working, start_segment, start_index, middle_point_index, first_handles); + working.segment_start[segment_index] = middle_point_index; + working.segment_handles[segment_index] = second_handles; start_t = split_t; start_index = middle_point_index; @@ -799,27 +852,26 @@ pub mod extrude_algorithms { } /// Copy all segments with the offset of `direction`. - fn offset_copy_all_segments(vector: &mut graphic_types::Vector, direction: DVec2) { - let points_count = vector.point_domain.ids().len(); - let mut next_point = vector.point_domain.next_id(); + fn offset_copy_all_segments(working: &mut WorkingVector, direction: DVec2) { + let points_count = working.point_ids.len(); + let mut next_point = next_working_point_id(working); for index in 0..points_count { - vector.point_domain.push(next_point.next_id(), vector.point_domain.positions()[index] + direction); + let position = working.point_positions[index] + direction; + push_working_point(working, next_point.next_id(), position); } - let segment_count = vector.segment_domain.ids().len(); - let mut next_segment = vector.segment_domain.next_id(); + let segment_count = working.segment_ids.len(); + let mut next_segment = next_working_segment_id(working); for index in 0..segment_count { - vector.segment_domain.push( - next_segment.next_id(), - vector.segment_domain.start_point()[index] + points_count, - vector.segment_domain.end_point()[index] + points_count, - vector.segment_domain.handles()[index].apply_transformation(|x| x + direction), - ); + let (start, end) = (working.segment_start[index] + points_count, working.segment_end[index] + points_count); + let handles = working.segment_handles[index].apply_transformation(|x| x + direction); + + push_working_segment(working, next_segment.next_id(), start, end, handles); } } /// Join points from the original to the copied that are on opposite sides of the direction. - fn join_extrema_edges(vector: &mut graphic_types::Vector, direction: DVec2) { + fn join_extrema_edges(working: &mut WorkingVector, direction: DVec2) { #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] enum Found { #[default] @@ -841,13 +893,13 @@ pub mod extrude_algorithms { } } - let first_half_points = vector.point_domain.len() / 2; + let first_half_points = working.point_ids.len() / 2; let mut points = vec![Found::None; first_half_points]; - let first_half_segments = vector.segment_domain.ids().len() / 2; + let first_half_segments = working.segment_ids.len() / 2; for segment_id in 0..first_half_segments { - let index = [vector.segment_domain.start_point()[segment_id], vector.segment_domain.end_point()[segment_id]]; - let position = index.map(|index| vector.point_domain.positions()[index]); + let index = [working.segment_start[segment_id], working.segment_end[segment_id]]; + let position = index.map(|index| working.point_positions[index]); if position[0].abs_diff_eq(position[1], 1e-6) { continue; // Skip zero length segments @@ -857,35 +909,39 @@ pub mod extrude_algorithms { points[index[1]].update(direction.perp_dot(position[0] - position[1])); } - let mut next_segment = vector.segment_domain.next_id(); + let mut next_segment = next_working_segment_id(working); for (index, &point) in points.iter().enumerate().take(first_half_points) { // Extrema are single connected points or points with both positive and negative values if !matches!(point, Found::Both | Found::Positive | Found::Negative) { continue; } - vector.segment_domain.push(next_segment.next_id(), index, index + first_half_points, BezierHandles::Linear); + push_working_segment(working, next_segment.next_id(), index, index + first_half_points, BezierHandles::Linear); } } /// Join all points from the original to the copied. - fn join_all(vector: &mut graphic_types::Vector) { - let mut next_segment = vector.segment_domain.next_id(); - let first_half = vector.point_domain.len() / 2; + fn join_all(working: &mut WorkingVector) { + let mut next_segment = next_working_segment_id(working); + let first_half = working.point_ids.len() / 2; for index in 0..first_half { - vector.segment_domain.push(next_segment.next_id(), index, index + first_half, BezierHandles::Linear); + push_working_segment(working, next_segment.next_id(), index, index + first_half, BezierHandles::Linear); } } - pub fn extrude(vector: &mut graphic_types::Vector, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) { - split(vector, direction); - offset_copy_all_segments(vector, direction); + pub fn extrude(vector: &mut graphic_types::Vector, paths: &mut List, direction: DVec2, joining_algorithm: ExtrudeJoiningAlgorithm) { + let mut working = WorkingVector::flatten(vector, paths); + + split(&mut working, direction); + offset_copy_all_segments(&mut working, direction); match joining_algorithm { - ExtrudeJoiningAlgorithm::Extrema => join_extrema_edges(vector, direction), - ExtrudeJoiningAlgorithm::All => join_all(vector), + ExtrudeJoiningAlgorithm::Extrema => join_extrema_edges(&mut working, direction), + ExtrudeJoiningAlgorithm::All => join_all(&mut working), ExtrudeJoiningAlgorithm::None => {} } + + working.reweave(vector, paths); } #[cfg(test)] @@ -914,10 +970,11 @@ pub mod extrude_algorithms { #[test] fn split_vector() { let curve = kurbo::PathSeg::Cubic(kurbo::CubicBez::new((0., 0.), (100., -10.), (100., 110.), (0., 100.))); - let mut vector = graphic_types::Vector::from_bezpath(kurbo::BezPath::from_path_segments([curve].into_iter())); - super::split(&mut vector, DVec2::X); - assert_eq!(vector.segment_ids().len(), 3); - assert_eq!(vector.point_domain.ids().len(), 4); + let (vector, paths) = graphic_types::Vector::from_bezpath(&kurbo::BezPath::from_path_segments([curve].into_iter())); + let mut working = super::WorkingVector::flatten(&vector, &paths); + super::split(&mut working, DVec2::X); + assert_eq!(working.segment_ids.len(), 3); + assert_eq!(working.point_ids.len(), 4); } } } @@ -928,7 +985,7 @@ async fn extrude(_: impl Ctx, #[implementations(G let mut source = source; let (direction, joining_algorithm) = (*direction.element(), *joining_algorithm.element()); - extrude_algorithms::extrude(source.element_mut(), direction, joining_algorithm); + edit_item_geometry(&mut source, |vector, paths| extrude_algorithms::extrude(vector, paths, direction, joining_algorithm)); source }) } @@ -936,20 +993,22 @@ async fn extrude(_: impl Ctx, #[implementations(G #[node_macro::node(category("Vector: Modifier"), icon("NodeBoxWarp"), path(core_types::vector))] async fn box_warp(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item, #[expose] rectangle: Item) -> Item { let target_transform: DAffine2 = rectangle.attribute_cloned_or_default(ATTR_TRANSFORM); + let target_paths = paths_from_item_attributes(&rectangle).cloned().unwrap_or_default(); let target = rectangle.into_element(); V::map_vector_items(content, |content| { let mut row = content; { let transform: DAffine2 = row.attribute_cloned_or_default(ATTR_TRANSFORM); + let paths = paths_from_item_attributes(&row).cloned().unwrap_or_default(); let vector = std::mem::take(row.element_mut()); // Get the bounding box of the source vector geometry - let source_bbox = vector.bounding_box_with_transform(transform).unwrap_or([DVec2::ZERO, DVec2::ONE]); + let source_bbox = vector.bounding_box_with_transform(&paths, transform).unwrap_or([DVec2::ZERO, DVec2::ONE]); // Extract first 4 points from target shape to form the quadrilateral // Apply the target's transform to get points in world space - let target_points: Vec = target.point_domain.positions().iter().map(|&p| target_transform.transform_point2(p)).take(4).collect(); + let target_points: Vec = target.anchor_positions(&target_paths).iter().map(|&p| target_transform.transform_point2(p)).take(4).collect(); // If we have fewer than 4 points, use the corners of the source bounding box // This handles the degenerative case @@ -971,8 +1030,8 @@ async fn box_warp(_: impl Ctx, #[implementations( // Precompute source bounding box size for normalization let source_size = source_bbox[1] - source_bbox[0]; - // Transform points - for (_, position) in result.point_domain.positions_mut() { + // Transform points, handles included since they are pool points too + for position in result.0.iter_element_values_mut() { // Get the point in world space let world_pos = transform.transform_point2(*position); @@ -983,20 +1042,6 @@ async fn box_warp(_: impl Ctx, #[implementations( *position = bilinear_interpolate(t, &dst_corners); } - // Transform handles in bezier curves - for (_, handles, _, _) in result.handles_mut() { - *handles = handles.apply_transformation(|pos| { - // Get the handle in world space - let world_pos = transform.transform_point2(pos); - - // Normalize coordinates within the source bounding box - let t = ((world_pos - source_bbox[0]) / source_size).clamp(DVec2::ZERO, DVec2::ONE); - - // Apply bilinear interpolation - bilinear_interpolate(t, &dst_corners) - }); - } - // Reset the transform since we've applied it directly to the points *row.element_mut() = result; row.set_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY); @@ -1156,11 +1201,21 @@ async fn auto_tangents( let (spread, preserve_existing) = (*spread.element(), *preserve_existing.element()); let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); - let (source, attributes) = source.into_parts(); - - let mut result = Vector::default(); + let source_paths = paths_from_item_attributes(&source).cloned().unwrap_or_default(); + let (source, mut attributes) = source.into_parts(); + + let (mut result, mut result_paths) = (Vector::default(), List::new()); + + // Each auto-tangented anchor is found as the end of its incoming segment, counted across all subpaths + let mut segment_offset = 0; + let mut smooth_segment_ends = Vec::new(); + let segments_after = |paths: &List, first_path: usize| -> usize { + (first_path..paths.len()) + .filter_map(|index| Some(paths.element(index)?.segment_count(curve_type_at(paths, index)))) + .sum() + }; - for (mut manipulators_list, is_closed) in source.stroke_manipulator_groups() { + for (mut manipulators_list, is_closed) in source.stroke_manipulator_groups(&source_paths) { for manipulator in &mut manipulators_list { manipulator.anchor = transform.transform_point2(manipulator.anchor); manipulator.in_handle = manipulator.in_handle.map(|handle| transform.transform_point2(handle)); @@ -1169,7 +1224,12 @@ async fn auto_tangents( if manipulators_list.len() < 2 { // Not enough points for softening or handle removal - result.append_manipulator_groups(&manipulators_list, is_closed, true); + let mut unchanged = bezpath_from_manipulator_groups(&manipulators_list, is_closed); + unchanged.apply_affine(Affine::new(transform.inverse().to_cols_array())); + + let first_path = result_paths.len(); + result.append_bezpath(&mut result_paths, &unchanged); + segment_offset += segments_after(&result_paths, first_path); continue; } @@ -1261,32 +1321,36 @@ async fn auto_tangents( auto_tangented[i] = true; } - // Record segment count before appending so we can find the new segment IDs - let segment_offset = result.segment_domain.ids().len(); - let mut softened_bezpath = bezpath_from_manipulator_groups(&new_manipulators_list, is_closed); softened_bezpath.apply_affine(Affine::new(transform.inverse().to_cols_array())); - result.append_bezpath(softened_bezpath); + + let first_path = result_paths.len(); + result.append_bezpath(&mut result_paths, &softened_bezpath); + let appended_segments = segments_after(&result_paths, first_path); // Mark auto-tangented points as having colinear handles - let segment_ids = result.segment_domain.ids(); let num_manipulators = new_manipulators_list.len(); for (i, _) in auto_tangented.iter().enumerate().filter(|&(_, &tangented)| tangented) { - // For interior point i, the incoming segment is segment_offset + (i - 1) and outgoing is segment_offset + i. - // For closed paths, point 0's incoming segment is the last one (segment_offset + num_manipulators - 1). - // For open paths, endpoints are never auto-tangented (the `is_endpoint` check above ensures that), - // so `i == 0` and `i == num_manipulators - 1` only occur here when the path is closed - let in_segment_index = if i == 0 { segment_offset + num_manipulators - 1 } else { segment_offset + i - 1 }; - let out_segment_index = if i == num_manipulators - 1 { segment_offset } else { segment_offset + i }; - - if in_segment_index < segment_ids.len() && out_segment_index < segment_ids.len() { - result - .colinear_manipulators - .push([HandleId::end(segment_ids[in_segment_index]), HandleId::primary(segment_ids[out_segment_index])]); + // For interior point i, the incoming segment is (i - 1) and the outgoing one is i, counted within this subpath. + // For closed paths, point 0's incoming segment is the last one. For open paths, endpoints are never + // auto-tangented (the `is_endpoint` check above ensures that), so `i == 0` and the last `i` only occur when closed. + let in_segment = if i == 0 { num_manipulators - 1 } else { i - 1 }; + let out_segment = if i == num_manipulators - 1 { 0 } else { i }; + + if in_segment < appended_segments && out_segment < appended_segments { + smooth_segment_ends.push(segment_offset + in_segment); } } + + segment_offset += appended_segments; } + // O(segments + points) in one pass each, rather than a connectivity build per anchor + let segment_ends: Vec = result.segment_iter(&result_paths).map(|(_, _, _, end)| end).collect(); + let smooth_anchors = smooth_segment_ends.into_iter().filter_map(|index| segment_ends.get(index).copied()); + result.set_smooth_points(&mut result_paths, smooth_anchors, true); + + attributes.insert(ATTR_PATHS, result_paths); Item::from_parts(result, attributes) }) } @@ -1295,26 +1359,25 @@ async fn auto_tangents( async fn bounding_box(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> Item { V::map_vector_items(content, |content| { let mut content = content; - let result = content + let paths = paths_from_item_attributes(&content).cloned().unwrap_or_default(); + let (result, result_paths) = content .element() - .bounding_box_rect() - .map(|bbox| { - let mut vector = Vector::default(); - vector.append_bezpath(bbox.to_path(DEFAULT_ACCURACY)); - vector - }) + .bounding_box_rect(&paths) + .map(|bbox| Vector::from_bezpath(&bbox.to_path(DEFAULT_ACCURACY))) .unwrap_or_default(); *content.element_mut() = result; + content.set_attribute(ATTR_PATHS, result_paths); content }) } #[node_macro::node(category("Vector: Measure"), icon("NodeDimensions"), path(core_types::vector))] async fn dimensions(_: impl Ctx, content: Item) -> Item { + let paths = paths_from_item_attributes(&content).unwrap_or_else(|| no_paths()); let dimensions = content .element() - .bounding_box_with_transform(content.attribute_cloned_or_default(ATTR_TRANSFORM)) + .bounding_box_with_transform(paths, content.attribute_cloned_or_default(ATTR_TRANSFORM)) .map(|[top_left, bottom_right]| bottom_right - top_left) .unwrap_or_default(); @@ -1337,10 +1400,10 @@ fn points_to_vector( let mut vector = Vector::default(); let mut next_point = PointId::ZERO; for &position in points.iter_element_values() { - vector.point_domain.push(next_point.next_id(), position); + vector.0.push(Item::new_from_element(position).with_attribute(ATTR_ID, next_point.next_id())); } - Item::new_from_element(vector) + Item::new_from_element(vector).with_attribute(ATTR_PATHS, List::::new()) } /// Creates a polyline from a series of vector points, replacing any existing segments that may already exist. @@ -1350,24 +1413,30 @@ async fn points_to_polyline(_: impl Ctx, #[implem V::map_vector_items(points, |points| { let mut points = points; - let vector = points.element_mut(); - let mut segment_domain = SegmentDomain::new(); - let mut next_id = SegmentId::ZERO; + edit_item_geometry(&mut points, |vector, paths| { + let flattened = WorkingVector::flatten(vector, paths); + let mut working = WorkingVector { + point_ids: flattened.point_ids, + point_positions: flattened.point_positions, + ..Default::default() + }; + let mut next_id = SegmentId::ZERO; - let points_count = vector.point_domain.ids().len(); + let points_count = working.point_ids.len(); - if points_count >= 2 { - (0..points_count - 1).for_each(|i| { - segment_domain.push(next_id.next_id(), i, i + 1, BezierHandles::Linear); - }); + if points_count >= 2 { + (0..points_count - 1).for_each(|i| { + push_working_segment(&mut working, next_id.next_id(), i, i + 1, BezierHandles::Linear); + }); - if closed && points_count != 2 { - segment_domain.push(next_id.next_id(), points_count - 1, 0, BezierHandles::Linear); + if closed && points_count != 2 { + push_working_segment(&mut working, next_id.next_id(), points_count - 1, 0, BezierHandles::Linear); + } } - } - vector.segment_domain = segment_domain; + working.reweave(vector, paths); + }); points }) @@ -1389,11 +1458,16 @@ async fn relax_points( let mut source = source; let iterations = *iterations.element(); - let vector = source.element_mut(); - let relaxed = crate::voronoi::relax_sites(vector.point_domain.positions(), iterations); - for ((_, position), new_position) in vector.point_domain.positions_mut().zip(relaxed) { - *position = new_position; - } + edit_item_geometry(&mut source, |vector, paths| { + let indices = vector.anchor_indices(paths); + let sites: Vec = indices.iter().filter_map(|&index| vector.position(index)).collect(); + + for (&index, new_position) in indices.iter().zip(crate::voronoi::relax_sites(&sites, iterations)) { + if let Some(position) = vector.0.element_mut(index) { + *position = new_position; + } + } + }); source }) @@ -1408,12 +1482,13 @@ async fn voronoi_cells(_: impl Ctx, #[implementat let mut source = source; let connect_cells = *connect_cells.element(); - let vector = source.element_mut(); - let sites = vector.point_domain.positions().to_vec(); - let cells = crate::voronoi::voronoi_cells(&sites); - if !cells.is_empty() { - replace_with_polygons(vector, cells, connect_cells); - } + edit_item_geometry(&mut source, |vector, paths| { + let sites = vector.anchor_positions(paths); + let cells = crate::voronoi::voronoi_cells(&sites); + if !cells.is_empty() { + replace_with_polygons(vector, paths, cells, connect_cells); + } + }); source }) @@ -1428,15 +1503,16 @@ async fn triangulate(_: impl Ctx, #[implementatio let mut source = source; let connect_cells = *connect_cells.element(); - let vector = source.element_mut(); - let sites = vector.point_domain.positions().to_vec(); - let triangles = crate::voronoi::delaunay_triangles(&sites); - if !triangles.is_empty() { - // `delaunator` emits triangle vertices clockwise; reverse to `[a, c, b]` so triangles wind counter-clockwise to - // match the Voronoi cells and the rest of the framework's fill winding. - let polygons = triangles.iter().map(|&[a, b, c]| vec![sites[a], sites[c], sites[b]]).collect(); - replace_with_polygons(vector, polygons, connect_cells); - } + edit_item_geometry(&mut source, |vector, paths| { + let sites = vector.anchor_positions(paths); + let triangles = crate::voronoi::delaunay_triangles(&sites); + if !triangles.is_empty() { + // `delaunator` emits triangle vertices clockwise; reverse to `[a, c, b]` so triangles wind counter-clockwise to + // match the Voronoi cells and the rest of the framework's fill winding. + let polygons = triangles.iter().map(|&[a, b, c]| vec![sites[a], sites[c], sites[b]]).collect(); + replace_with_polygons(vector, paths, polygons, connect_cells); + } + }); source }) @@ -1446,9 +1522,8 @@ async fn triangulate(_: impl Ctx, #[implementatio /// /// Without `connect_cells`, each polygon becomes its own closed subpath. /// With it, coincident vertices are welded and each shared edge is emitted once, producing a connected mesh. -pub(crate) fn replace_with_polygons(vector: &mut Vector, polygons: Vec>, connect_cells: bool) { - let mut point_domain = PointDomain::new(); - let mut segment_domain = SegmentDomain::new(); +pub(crate) fn replace_with_polygons(vector: &mut Vector, paths: &mut List, polygons: Vec>, connect_cells: bool) { + let mut working = WorkingVector::default(); let mut next_point = PointId::ZERO; let mut next_segment = SegmentId::ZERO; @@ -1458,16 +1533,14 @@ pub(crate) fn replace_with_polygons(vector: &mut Vector, polygons: Vec index, + None => { + let index = push_working_point(&mut working, next_point.next_id(), position); + vertex_lookup.insert(key, index); + index + } + } }) .collect(); @@ -1505,14 +1581,13 @@ pub(crate) fn replace_with_polygons(vector: &mut Vector, polygons: Vec( let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); let transform = Affine::new(transform_attribute.to_cols_array()); + let source_paths = paths_from_item_attributes(&content).cloned().unwrap_or_default(); let vector = std::mem::take(content.element_mut()); - let bezpaths = vector.stroke_bezpath_iter(); - let mut result = Vector::default(); + let bezpaths = vector.stroke_bezpath_iter(&source_paths).collect::>(); + let (mut result, mut result_paths) = (Vector::default(), List::new()); // Perform operation on all subpaths in this shape. for mut bezpath in bezpaths { @@ -1572,10 +1648,11 @@ async fn offset_path( bezpath_out.apply_affine(transform.inverse()); // One closed subpath, open path. - result.append_bezpath(bezpath_out); + result.append_bezpath(&mut result_paths, &bezpath_out); } *content.element_mut() = result; + content.set_attribute(ATTR_PATHS, result_paths); content }) } @@ -1662,8 +1739,9 @@ fn solidify_stroke_list(content: List) -> List { .first_index_of(Cover::Stroke) .zip(appearance.first_index_of(Cover::Fill)) .is_some_and(|(stroke_index, fill_index)| stroke_index < fill_index); - let bezpaths = vector.stroke_bezpath_iter(); - let mut solidified_stroke = Vector::default(); + let paths = attributes.get::>(ATTR_PATHS).cloned().unwrap_or_default(); + let bezpaths = vector.stroke_bezpath_iter(&paths).collect::>(); + let (mut solidified_stroke, mut solidified_stroke_paths) = (Vector::default(), List::new()); // Taking the existing stroke data and passing it to kurbo::stroke to generate new fill paths. let join = match stroke.join { @@ -1704,7 +1782,7 @@ fn solidify_stroke_list(content: List) -> List { solidified.apply_affine(Affine::new(stroke.transform.inverse().to_cols_array())); } - solidified_stroke.append_bezpath(solidified); + solidified_stroke.append_bezpath(&mut solidified_stroke_paths, &solidified); } // If the original vector has a fill, preserve it as a separate item with the stroke coverages dropped @@ -1723,6 +1801,7 @@ fn solidify_stroke_list(content: List) -> List { let stroke_paint = appearance.first_index_of(Cover::Stroke).and_then(|index| appearance.paint_at(index).cloned()).unwrap_or_default(); stroke_attributes.insert(ATTR_APPEARANCE, Appearance::new_single(Coverage::new_fill(), stroke_paint)); } + stroke_attributes.insert(ATTR_PATHS, solidified_stroke_paths); let stroke_row = Item::from_parts(solidified_stroke, stroke_attributes); @@ -1745,7 +1824,8 @@ async fn solidify_stroke(_: impl Ctx, #[implement #[node_macro::node(category("Vector: Modifier"), icon("NodeSeparateSubpaths"), path(core_types::vector))] async fn separate_subpaths(_: impl Ctx, #[implementations(Graphic, Vector)] content: Item) -> List { V::expand_vector_items(content, |content| { - let bezpaths = content.element().stroke_bezpath_iter().collect::>(); + let content_paths = paths_from_item_attributes(&content).cloned().unwrap_or_default(); + let bezpaths = content.element().stroke_bezpath_iter(&content_paths).collect::>(); // Pass the original element through unchanged when it has no subpaths, so its attributes // (such as the layer transform) survive downstream rather than being dropped along with the empty list. @@ -1758,10 +1838,11 @@ async fn separate_subpaths(_: impl Ctx, #[impl bezpaths .into_iter() .map(|bezpath| { - let mut vector = Vector::default(); - vector.append_bezpath(bezpath); + let (vector, paths) = Vector::from_bezpath(&bezpath); - Item::from_parts(vector, attributes.clone()) + let mut attributes = attributes.clone(); + attributes.insert(ATTR_PATHS, paths); + Item::from_parts(vector, attributes) }) .collect() }) @@ -1778,7 +1859,8 @@ async fn path_is_closed( index: Item, ) -> Item { let index = usize::try_from(index.into_element()).unwrap_or(usize::MAX); - let closed = content.element().build_stroke_path_iter().map(|(_, closed)| closed).nth(index).unwrap_or(false); + let paths = paths_from_item_attributes(&content).unwrap_or_else(|| no_paths()); + let closed = content.element().stroke_manipulator_groups(paths).map(|(_, closed)| closed).nth(index).unwrap_or(false); Item::new_from_element(closed) } @@ -1791,11 +1873,16 @@ async fn map_points( ) -> Item { let mut content = content; - for vector in V::vector_elements_mut(&mut content) { - for (index, (_, position)) in vector.point_domain.positions_mut().enumerate() { - let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(*position); + // Handles keep their own positions, so the mapping walks anchors alone, indexed in anchor order + for (vector, paths) in V::vector_elements_mut(&mut content) { + for (index, pool_index) in vector.anchor_indices(&paths).into_iter().enumerate() { + let Some(position) = vector.position(pool_index) else { continue }; + let owned_ctx = OwnedContextImpl::from(ctx.clone()).with_index(index).with_position(position); - *position = mapped.eval(owned_ctx.into_context()).await.into_element(); + let mapped_position = mapped.eval(owned_ctx.into_context()).await.into_element(); + if let Some(slot) = vector.0.element_mut(pool_index) { + *slot = mapped_position; + } } } @@ -1808,15 +1895,14 @@ pub async fn combine_paths(_: impl Ctx, content: List) -> Item let graphic_list = content.clone(); let flattened = content.into_flattened_list::(); - // Create a `List` with one empty `Vector` element, then get a mutable reference to it which we append flattened subpaths to - let mut output_list = List::new_from_element(Vector::default()); let mut primary_source = None; - - let output = output_list.element_mut(0).unwrap(); + let mut output = Vector::default(); + let mut output_paths = List::new(); // Concatenate every vector element's subpaths into the single output compound path for index in 0..flattened.len() { let Some(element) = flattened.element(index) else { continue }; + let element_paths = paths_from_list_row_attributes(&flattened, index).cloned().unwrap_or_default(); let layer_path: List = flattened.attribute_cloned_or_default::(ATTR_EDITOR_LAYER_PATH, index).0; let node_id = layer_path.iter_element_values().next_back().map(|node_id| node_id.0).unwrap_or_default(); @@ -1825,30 +1911,28 @@ pub async fn combine_paths(_: impl Ctx, content: List) -> Item let collision_hash_seed = hasher.finish(); let source_transform = flattened.attribute_cloned_or_default(ATTR_TRANSFORM, index); - output.concat(element, source_transform, collision_hash_seed); + output.concat(&mut output_paths, element, &element_paths, source_transform, collision_hash_seed); primary_source = Some((index, source_transform)); } + let mut attributes = ItemAttributeValues::new(); if let Some((primary, source_transform)) = primary_source { let source_attributes = flattened.clone_item_attributes(primary); - let mut attributes = ItemAttributeValues::new(); attributes.insert_cloned_from(&source_attributes, ATTR_APPEARANCE); // Adopt the last input item's layer (if any) so the editor can also bucket clicks under a contributing child layer attributes.insert_cloned_from(&source_attributes, ATTR_EDITOR_LAYER_PATH); bake_paint_transforms(&mut attributes, source_transform); - - let output = std::mem::take(output_list.element_mut(0).unwrap()); - output_list = List::new_from_item(Item::from_parts(output, attributes)); } + attributes.insert(ATTR_PATHS, output_paths); // Preserve a reference to the original upstream `List` so the renderer can recurse into it // when collecting metadata, exposing the original child layers' click targets to editor tools. // This is the same mechanism Boolean Operation uses to keep its inputs editable after the merge. - output_list.set_attribute(ATTR_EDITOR_MERGED_LAYERS, 0, graphic_list); + attributes.insert(ATTR_EDITOR_MERGED_LAYERS, graphic_list); - output_list.into_iter().next().unwrap_or_default() + Item::from_parts(output, attributes) } /// Convert vector geometry into a polyline composed of evenly spaced points. @@ -1885,9 +1969,10 @@ async fn sample_polyline( } }; - let mut result = Vector::default(); + let (mut result, mut result_paths) = (Vector::default(), List::new()); - for local_bezpath in content.element().stroke_bezpath_iter() { + let content_paths = paths_from_item_attributes(&content).cloned().unwrap_or_default(); + for local_bezpath in content.element().stroke_bezpath_iter(&content_paths).collect::>() { // Apply the transform to compute sample locations in world space (for correct distance-based spacing) let mut world_bezpath = local_bezpath.clone(); let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); @@ -1922,10 +2007,11 @@ async fn sample_polyline( if was_closed { sample_bezpath.close_path(); } - result.append_bezpath(sample_bezpath); + result.append_bezpath(&mut result_paths, &sample_bezpath); } *content.element_mut() = result; + content.set_attribute(ATTR_PATHS, result_paths); content }) } @@ -1956,18 +2042,20 @@ async fn simplify( let transform = Affine::new(transform_attribute.to_cols_array()); let inverse_transform = transform.inverse(); - let mut result = Vector::default(); + let (mut result, mut result_paths) = (Vector::default(), List::new()); - for mut bezpath in item.element().stroke_bezpath_iter() { + let item_paths = paths_from_item_attributes(&item).cloned().unwrap_or_default(); + for mut bezpath in item.element().stroke_bezpath_iter(&item_paths).collect::>() { bezpath.apply_affine(transform); let mut simplified = simplify_bezpath(bezpath, tolerance, &options); simplified.apply_affine(inverse_transform); - result.append_bezpath(simplified); + result.append_bezpath(&mut result_paths, &simplified); } *item.element_mut() = result; + item.set_attribute(ATTR_PATHS, result_paths); item }) } @@ -2051,9 +2139,10 @@ async fn decimate( let transform = Affine::new(transform_attribute.to_cols_array()); let inverse_transform = transform.inverse(); - let mut result = Vector::default(); + let (mut result, mut result_paths) = (Vector::default(), List::new()); - for mut bezpath in content.element().stroke_bezpath_iter() { + let content_paths = paths_from_item_attributes(&content).cloned().unwrap_or_default(); + for mut bezpath in content.element().stroke_bezpath_iter(&content_paths).collect::>() { bezpath.apply_affine(transform); let is_closed = matches!(bezpath.elements().last(), Some(PathEl::ClosePath)); @@ -2089,10 +2178,11 @@ async fn decimate( } new_bezpath.apply_affine(inverse_transform); - result.append_bezpath(new_bezpath); + result.append_bezpath(&mut result_paths, &new_bezpath); } *content.element_mut() = result; + content.set_attribute(ATTR_PATHS, result_paths); content }) } @@ -2120,7 +2210,8 @@ async fn cut_path( let euclidean = !parameterized_distance; - let bezpaths = content.element().stroke_bezpath_iter().collect::>(); + let content_paths = paths_from_item_attributes(&content).cloned().unwrap_or_default(); + let bezpaths = content.element().stroke_bezpath_iter(&content_paths).collect::>(); let bezpath_count = bezpaths.len() as f64; let t_value = progression.clamp(0., bezpath_count); @@ -2128,22 +2219,23 @@ async fn cut_path( let index = if t_value >= bezpath_count { (bezpath_count - 1.) as usize } else { t_value as usize }; if let Some(bezpath) = bezpaths.get(index).cloned() { - let mut result_vector = Vector::default(); + let (mut result_vector, mut result_paths) = (Vector::default(), List::new()); for (_, bezpath) in bezpaths.iter().enumerate().filter(|&(i, _)| i != index) { - result_vector.append_bezpath(bezpath.clone()); + result_vector.append_bezpath(&mut result_paths, bezpath); } let t = if t_value == bezpath_count { 1. } else { t_value.fract() }; let t = if euclidean { TValue::Euclidean(t) } else { TValue::Parametric(t) }; if let Some((first, second)) = split_bezpath(&bezpath, t) { - result_vector.append_bezpath(first); - result_vector.append_bezpath(second); + result_vector.append_bezpath(&mut result_paths, &first); + result_vector.append_bezpath(&mut result_paths, &second); } else { - result_vector.append_bezpath(bezpath); + result_vector.append_bezpath(&mut result_paths, &bezpath); } *content.element_mut() = result_vector; + content.set_attribute(ATTR_PATHS, result_paths); } content @@ -2156,55 +2248,63 @@ async fn cut_segments(_: impl Ctx, #[implementati V::map_vector_items(content, |content| { let mut content = content; // Iterate through every segment and make a copy of each of its endpoints, then reassign each segment's endpoints to its own unique point copy - let vector = content.element_mut(); + edit_item_geometry(&mut content, |vector, paths| { + let mut working = WorkingVector::flatten(vector, paths); - let points_count = vector.point_domain.ids().len(); - let segments_count = vector.segment_domain.ids().len(); + let points_count = working.point_ids.len(); + let segments_count = working.segment_ids.len(); - let mut point_usages = vec![0_usize; points_count]; + let mut point_usages = vec![0_usize; points_count]; - // Count how many times each point is used as an endpoint of the segments - let start_points = vector.segment_domain.start_point().to_vec(); - let end_points = vector.segment_domain.end_point().to_vec(); - for (&start, &end) in start_points.iter().zip(end_points.iter()) { - point_usages[start] += 1; - point_usages[end] += 1; - } + // Count how many times each point is used as an endpoint of the segments + let start_points = working.segment_start.clone(); + let end_points = working.segment_end.clone(); + for (&start, &end) in start_points.iter().zip(end_points.iter()) { + point_usages[start] += 1; + point_usages[end] += 1; + } + + let mut new_point_ids = Vec::new(); + let mut new_point_positions = Vec::new(); + let mut offset_sum: usize = 0; + let mut points_with_new_offsets = Vec::with_capacity(points_count); - let mut new_points = PointDomain::new(); - let mut offset_sum: usize = 0; - let mut points_with_new_offsets = Vec::with_capacity(points_count); + // Build the new anchor list with the original points, but with duplications based on their extra usages by the segments + for (index, (&point_id, &point)) in working.point_ids.iter().zip(&working.point_positions).enumerate() { + // Ensure at least one usage to preserve free-floating points not connected to any segments + let usage_count = point_usages[index].max(1); - // Build a new point domain with the original points, but with duplications based on their extra usages by the segments - for (index, (point_id, point)) in vector.point_domain.iter().enumerate() { - // Ensure at least one usage to preserve free-floating points not connected to any segments - let usage_count = point_usages[index].max(1); + new_point_ids.push(point_id); + new_point_positions.push(point); - new_points.push_unchecked(point_id, point); + for i in 1..usage_count { + new_point_ids.push(point_id.generate_from_hash(i as u64)); + new_point_positions.push(point); + } - for i in 1..usage_count { - new_points.push_unchecked(point_id.generate_from_hash(i as u64), point); + points_with_new_offsets.push(offset_sum); + offset_sum += usage_count; } - points_with_new_offsets.push(offset_sum); - offset_sum += usage_count; - } + // Reconcile the segments with the new points + working.point_ids = new_point_ids; + working.point_positions = new_point_positions; + for original_segment_index in 0..segments_count { + let original_point_start_index = start_points[original_segment_index]; + let original_point_end_index = end_points[original_segment_index]; - // Reconcile the segment domain with the new points - vector.point_domain = new_points; - for original_segment_index in 0..segments_count { - let original_point_start_index = start_points[original_segment_index]; - let original_point_end_index = end_points[original_segment_index]; + point_usages[original_point_start_index] -= 1; + point_usages[original_point_end_index] -= 1; - point_usages[original_point_start_index] -= 1; - point_usages[original_point_end_index] -= 1; + let start_usage = points_with_new_offsets[original_point_start_index] + point_usages[original_point_start_index]; + let end_usage = points_with_new_offsets[original_point_end_index] + point_usages[original_point_end_index]; - let start_usage = points_with_new_offsets[original_point_start_index] + point_usages[original_point_start_index]; - let end_usage = points_with_new_offsets[original_point_end_index] + point_usages[original_point_end_index]; + working.segment_start[original_segment_index] = start_usage; + working.segment_end[original_segment_index] = end_usage; + } - vector.segment_domain.set_start_point(original_segment_index, start_usage); - vector.segment_domain.set_end_point(original_segment_index, end_usage); - } + working.reweave(vector, paths); + }); content }) @@ -2241,8 +2341,9 @@ async fn evaluate_path( let (progression, reverse, parameterized_distance) = (progression.into_element(), reverse.into_element(), parameterized_distance.into_element()); let euclidean = !parameterized_distance; + let paths = paths_from_item_attributes(&content).unwrap_or_else(|| no_paths()); let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); - let mut bezpaths: Vec<_> = content.element().stroke_bezpath_iter().map(|bezpath| (bezpath, transform)).collect(); + let mut bezpaths: Vec<_> = content.element().stroke_bezpath_iter(paths).map(|bezpath| (bezpath, transform)).collect(); let bezpath_count = bezpaths.len() as f64; let progression = progression.clamp(0., bezpath_count); let progression = if reverse { bezpath_count - progression } else { progression }; @@ -2299,11 +2400,12 @@ async fn scatter_points( let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64); - let mut result = Vector::default(); + let mut result = WorkingVector::default(); + let content_paths = paths_from_item_attributes(&content).cloned().unwrap_or_default(); let path_with_bounding_boxes: Vec<_> = content .element() - .stroke_bezpath_iter() + .stroke_bezpath_iter(&content_paths) .map(|mut bezpath| { // TODO: apply transform to points instead of modifying the paths bezpath.close_path(); @@ -2318,11 +2420,11 @@ async fn scatter_points( } for point in bezpath_algorithms::poisson_disk_points(i, &path_with_bounding_boxes, separation, || rng.random::()) { - result.point_domain.push(PointId::generate(), point); + push_working_point(&mut result, PointId::generate(), point); } } - *content.element_mut() = result; + edit_item_geometry(&mut content, |vector, paths| result.reweave(vector, paths)); content }) } @@ -2332,39 +2434,60 @@ async fn spline(_: impl Ctx, #[implementations(Gr V::map_vector_items(content, |content| { let mut content = content; // Exit early if there are no points to generate splines from. - if content.element().point_domain.positions().is_empty() { + if content.element().point_count() == 0 { return content; } - let mut segment_domain = SegmentDomain::default(); - let mut next_id = SegmentId::ZERO; - for (manipulator_groups, closed) in content.element().stroke_manipulator_groups() { - let positions = manipulator_groups.iter().map(|manipulators| manipulators.anchor).collect::>(); - let closed = closed && positions.len() > 2; + edit_item_geometry(&mut content, |vector, paths| { + let manipulator_runs = vector.stroke_manipulator_groups(paths).collect::>(); - // Compute control point handles for Bezier spline. - let first_handles = if closed { - solve_spline_first_handle_closed(&positions) - } else { - solve_spline_first_handle_open(&positions) + let flattened = WorkingVector::flatten(vector, paths); + let mut working = WorkingVector { + point_ids: flattened.point_ids, + point_positions: flattened.point_positions, + ..Default::default() }; - // Create segments with computed Bezier handles and add them to the output vector element's segment domain. - for i in 0..(positions.len() - if closed { 0 } else { 1 }) { - let next_index = (i + 1) % positions.len(); + let mut next_id = SegmentId::ZERO; + let mut smooth_anchors = Vec::new(); + for (manipulator_groups, closed) in manipulator_runs { + let positions = manipulator_groups.iter().map(|manipulators| manipulators.anchor).collect::>(); + let closed = closed && positions.len() > 2; + + // The spline runs smoothly through every anchor with a segment on each side + let interior = if closed { + &manipulator_groups[..] + } else { + manipulator_groups.get(1..positions.len().saturating_sub(1)).unwrap_or_default() + }; + smooth_anchors.extend(interior.iter().map(|manipulators| manipulators.id)); + + // Compute control point handles for Bezier spline. + let first_handles = if closed { + solve_spline_first_handle_closed(&positions) + } else { + solve_spline_first_handle_open(&positions) + }; - let start_index = content.element().point_domain.resolve_id(manipulator_groups[i].id).unwrap(); - let end_index = content.element().point_domain.resolve_id(manipulator_groups[next_index].id).unwrap(); + // Create segments with computed Bezier handles and add them to the output vector element's segments. + for i in 0..(positions.len() - if closed { 0 } else { 1 }) { + let next_index = (i + 1) % positions.len(); - let handle_start = first_handles[i]; - let handle_end = positions[next_index] * 2. - first_handles[next_index]; - let handles = BezierHandles::Cubic { handle_start, handle_end }; + let start_index = working.resolve_id(manipulator_groups[i].id).unwrap(); + let end_index = working.resolve_id(manipulator_groups[next_index].id).unwrap(); - segment_domain.push(next_id.next_id(), start_index, end_index, handles); + let handle_start = first_handles[i]; + let handle_end = positions[next_index] * 2. - first_handles[next_index]; + let handles = BezierHandles::Cubic { handle_start, handle_end }; + + push_working_segment(&mut working, next_id.next_id(), start_index, end_index, handles); + } } - } - content.element_mut().segment_domain = segment_domain; + working.reweave(vector, paths); + vector.set_smooth_points(paths, smooth_anchors, true); + }); + content }) } @@ -2390,26 +2513,26 @@ fn inverse_linear_or_repair(linear: DMat2) -> DMat2 { repaired.inverse() } -/// Applies per-point displacement deltas to the point and handle positions of a vector element. -fn apply_point_deltas(element: &mut Vector, deltas: &[DVec2], transform: DAffine2) { - let mut already_applied = vec![false; element.point_domain.positions().len()]; +/// Applies per-anchor displacement deltas to the anchor and handle positions of a vector element. +fn apply_point_deltas(vector: &mut Vector, paths: &mut List, deltas: &[DVec2], transform: DAffine2) { + let mut working = WorkingVector::flatten(vector, paths); + let mut already_applied = vec![false; working.point_positions.len()]; - for (handles, start, end) in element.segment_domain.handles_and_points_mut() { - let start_delta = deltas[*start]; - let end_delta = deltas[*end]; + for index in 0..working.segment_ids.len() { + let (start, end) = (working.segment_start[index], working.segment_end[index]); + let start_delta = deltas[start]; + let end_delta = deltas[end]; - if !already_applied[*start] { - let start_position = element.point_domain.positions()[*start]; - element.point_domain.set_position(*start, start_position + start_delta); - already_applied[*start] = true; + if !already_applied[start] { + working.point_positions[start] += start_delta; + already_applied[start] = true; } - if !already_applied[*end] { - let end_position = element.point_domain.positions()[*end]; - element.point_domain.set_position(*end, end_position + end_delta); - already_applied[*end] = true; + if !already_applied[end] { + working.point_positions[end] += end_delta; + already_applied[end] = true; } - match handles { + match &mut working.segment_handles[index] { BezierHandles::Cubic { handle_start, handle_end } => { *handle_start += start_delta; *handle_end += end_delta; @@ -2420,6 +2543,8 @@ fn apply_point_deltas(element: &mut Vector, deltas: &[DVec2], transform: DAffine BezierHandles::Linear => {} } } + + working.reweave(vector, paths); } /// Perturbs the positions of anchor points in vector geometry by random amounts and directions. @@ -2448,30 +2573,26 @@ async fn jitter_points( let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); let inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2); - let deltas: Vec<_> = (0..content.element().point_domain.positions().len()) - .map(|point_index| { - let normal = if along_normals { - content - .element() - .segment_domain - .point_tangent(point_index, content.element().point_domain.positions()) - .map(|t| -t.perp()) - } else { - None - }; - - let offset = if let Some(normal) = normal { - normal * (rng.random::() * 2. - 1.) - } else { - DVec2::from_angle(rng.random::() * TAU) * rng.random::() - }; + let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + edit_item_geometry(&mut content, |vector, paths| { + let deltas: Vec<_> = vector + .anchor_indices(paths) + .into_iter() + .map(|point_index| { + let normal = if along_normals { vector.point_tangent(paths, point_index).map(|t| -t.perp()) } else { None }; + + let offset = if let Some(normal) = normal { + normal * (rng.random::() * 2. - 1.) + } else { + DVec2::from_angle(rng.random::() * TAU) * rng.random::() + }; - inverse_linear * offset * max_distance - }) - .collect(); + inverse_linear * offset * max_distance + }) + .collect(); - let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); - apply_point_deltas(content.element_mut(), &deltas, transform); + apply_point_deltas(vector, paths, &deltas, transform); + }); content }) @@ -2496,23 +2617,22 @@ async fn offset_points( let transform_attribute: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); let inverse_linear = inverse_linear_or_repair(transform_attribute.matrix2); - let deltas: Vec<_> = (0..content.element().point_domain.positions().len()) - .map(|point_index| { - let Some(normal) = content - .element() - .segment_domain - .point_tangent(point_index, content.element().point_domain.positions()) - .map(|t| -t.perp()) - else { - return DVec2::ZERO; - }; + let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); + edit_item_geometry(&mut content, |vector, paths| { + let deltas: Vec<_> = vector + .anchor_indices(paths) + .into_iter() + .map(|point_index| { + let Some(normal) = vector.point_tangent(paths, point_index).map(|t| -t.perp()) else { + return DVec2::ZERO; + }; - inverse_linear * normal * distance - }) - .collect(); + inverse_linear * normal * distance + }) + .collect(); - let transform: DAffine2 = content.attribute_cloned_or_default(ATTR_TRANSFORM); - apply_point_deltas(content.element_mut(), &deltas, transform); + apply_point_deltas(vector, paths, &deltas, transform); + }); content }) @@ -2602,28 +2722,26 @@ async fn morph( } } - /// Pushes a subpath (list of manipulators) directly into a Vector's point and segment domains, + /// Pushes a subpath (list of manipulators) directly into the working form's points and segments, /// bypassing the BezPath intermediate representation used by `append_bezpath`. - fn push_manipulators_to_vector(vector: &mut Vector, manips: &[ManipulatorGroup], closed: bool, point_id: &mut PointId, segment_id: &mut SegmentId) { + fn push_manipulators_to_vector(working: &mut WorkingVector, manips: &[ManipulatorGroup], closed: bool, point_id: &mut PointId, segment_id: &mut SegmentId) { let Some(first) = manips.first() else { return }; - let first_point_index = vector.point_domain.ids().len(); - vector.point_domain.push_unchecked(point_id.next_id(), first.anchor); + let first_point_index = push_working_point(working, point_id.next_id(), first.anchor); let mut prev_point_index = first_point_index; for manip_window in manips.windows(2) { - let point_index = vector.point_domain.ids().len(); - vector.point_domain.push_unchecked(point_id.next_id(), manip_window[1].anchor); + let point_index = push_working_point(working, point_id.next_id(), manip_window[1].anchor); let handles = handles_from_manips(manip_window[0].out_handle, manip_window[1].in_handle); - vector.segment_domain.push_unchecked(segment_id.next_id(), prev_point_index, point_index, handles); + push_working_segment(working, segment_id.next_id(), prev_point_index, point_index, handles); prev_point_index = point_index; } if closed && manips.len() > 1 { let handles = handles_from_manips(manips.last().unwrap().out_handle, manips[0].in_handle); - vector.segment_domain.push_unchecked(segment_id.next_id(), prev_point_index, first_point_index, handles); + push_working_segment(working, segment_id.next_id(), prev_point_index, first_point_index, handles); } } @@ -2797,9 +2915,10 @@ async fn morph( let control_bezpaths: Vec = { // User-provided path: collect all subpaths with the path's transform applied let path_transform: DAffine2 = path.attribute_cloned_or_default(ATTR_TRANSFORM); + let control_paths = paths_from_item_attributes(&path).unwrap_or_else(|| no_paths()); let paths: Vec = path .element() - .stroke_bezpath_iter() + .stroke_bezpath_iter(control_paths) .map(|mut bezpath| { bezpath.apply_affine(Affine::new(path_transform.to_cols_array())); bezpath @@ -3018,7 +3137,7 @@ async fn morph( return Item::from_parts(endpoint_element.clone(), attributes); } - let mut vector = Vector::default(); + let mut working = WorkingVector::default(); let appearance = { let source = content.attribute::(ATTR_APPEARANCE, source_index); @@ -3028,15 +3147,17 @@ async fn morph( // Work directly with manipulator groups, bypassing the BezPath intermediate representation. // This avoids the full Vector → BezPath → interpolate → BezPath → Vector roundtrip each frame. - let mut source_subpaths: Vec<_> = source_element.stroke_manipulator_groups().collect(); - let mut target_subpaths: Vec<_> = target_element.stroke_manipulator_groups().collect(); + let source_paths = paths_from_list_row_attributes(&content, source_index).unwrap_or_else(|| no_paths()); + let target_paths = paths_from_list_row_attributes(&content, target_index).unwrap_or_else(|| no_paths()); + let mut source_subpaths: Vec<_> = source_element.stroke_manipulator_groups(source_paths).collect(); + let mut target_subpaths: Vec<_> = target_element.stroke_manipulator_groups(target_paths).collect(); // Interpolate geometry in local space (no transform baked in); the lerped transform handles positioning let matched_count = source_subpaths.len().min(target_subpaths.len()); let extra_source = source_subpaths.split_off(matched_count); let extra_target = target_subpaths.split_off(matched_count); - // Pre-allocate domain storage based on total manipulator counts across all subpaths + // Pre-allocate the working form based on total manipulator counts across all subpaths let mut total_points = 0; let mut total_segments = 0; for ((source_manips, source_closed), (target_manips, _)) in source_subpaths.iter().zip(target_subpaths.iter()) { @@ -3051,8 +3172,12 @@ async fn morph( total_points += manips.len(); total_segments += if *closed { manips.len() } else { manips.len().saturating_sub(1) }; } - vector.point_domain.reserve(total_points); - vector.segment_domain.reserve(total_segments); + working.point_ids.reserve(total_points); + working.point_positions.reserve(total_points); + working.segment_ids.reserve(total_segments); + working.segment_start.reserve(total_segments); + working.segment_end.reserve(total_segments); + working.segment_handles.reserve(total_segments); let mut point_id = PointId::ZERO; let mut segment_id = SegmentId::ZERO; @@ -3132,7 +3257,7 @@ async fn morph( } } - push_manipulators_to_vector(&mut vector, &interpolated, source_closed, &mut point_id, &mut segment_id); + push_manipulators_to_vector(&mut working, &interpolated, source_closed, &mut point_id, &mut segment_id); } // Deal with unmatched extra source subpaths by collapsing them toward their end point @@ -3145,7 +3270,7 @@ async fn morph( manip.out_handle = manip.out_handle.map(|h| h.lerp(end, time)); } - push_manipulators_to_vector(&mut vector, &manips, closed, &mut point_id, &mut segment_id); + push_manipulators_to_vector(&mut working, &manips, closed, &mut point_id, &mut segment_id); } // Deal with unmatched extra target subpaths by expanding them from their start point @@ -3158,13 +3283,18 @@ async fn morph( manip.out_handle = manip.out_handle.map(|h| start.lerp(h, time)); } - push_manipulators_to_vector(&mut vector, &manips, closed, &mut point_id, &mut segment_id); + push_manipulators_to_vector(&mut working, &manips, closed, &mut point_id, &mut segment_id); } // The result is a synthesis of source and target, so adopt whichever endpoint the result is closer to as // the click-target identity (so the editor can route clicks back to one of the contributing layers) let primary_index = if time < 0.5 { source_index } else { target_index }; + + let (mut vector, mut paths) = (Vector::default(), List::new()); + working.reweave(&mut vector, &mut paths); + let mut item = Item::new_from_element(vector); + item.set_attribute(ATTR_PATHS, paths); item.set_attribute(ATTR_TRANSFORM, lerped_transform); // Propagate each blending/layer column only when the input carries it, so attribute presence stays determined by the graph rather than by runtime values @@ -3192,7 +3322,7 @@ async fn morph( item } -fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Vector { +fn bevel_algorithm(vector: &mut Vector, paths: &mut List, transform: DAffine2, distance: f64) { // Splits a bézier curve based on a distance measurement fn split_distance(bezier: PathSeg, distance: f64, length: f64) -> PathSeg { let parametric = eval_pathseg_euclidean(bezier, (distance / length).clamp(0., 1.), DEFAULT_ACCURACY); @@ -3200,10 +3330,10 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve } /// Produces a list that corresponds with the point ID. The value is how many segments are connected. - fn segments_connected_count(vector: &Vector) -> Vec { + fn segments_connected_count(working: &WorkingVector) -> Vec { // Count the number of segments connecting to each point. - let mut segments_connected_count = vec![0; vector.point_domain.ids().len()]; - for &point_index in vector.segment_domain.start_point().iter().chain(vector.segment_domain.end_point()) { + let mut segments_connected_count = vec![0; working.point_ids.len()]; + for &point_index in working.segment_start.iter().chain(&working.segment_end) { segments_connected_count[point_index] += 1; } @@ -3217,18 +3347,17 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve } /// Updates the index so that it points at a point with the position. If nobody else will look at the index, the original point is updated. Otherwise a new point is created. - fn create_or_modify_point(point_domain: &mut PointDomain, segments_connected_count: &mut [usize], pos: DVec2, index: &mut usize, next_id: &mut PointId, new_segments: &mut Vec<[usize; 2]>) { + fn create_or_modify_point(working: &mut WorkingVector, segments_connected_count: &mut [usize], pos: DVec2, index: &mut usize, next_id: &mut PointId, new_segments: &mut Vec<[usize; 2]>) { segments_connected_count[*index] -= 1; if segments_connected_count[*index] == 0 { // If nobody else is going to look at this point, we're alright to modify it - point_domain.set_position(*index, pos); + working.point_positions[*index] = pos; } else { - let new_index = point_domain.ids().len(); let original_index = *index; // Create a new point (since someone will wish to look at the point in the original position in future) + let new_index = push_working_point(working, next_id.next_id(), pos); *index = new_index; - point_domain.push(next_id.next_id(), pos); // Add a new segment to be created later new_segments.push([new_index, original_index]); @@ -3310,14 +3439,14 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve split_distance } - fn sort_segments(segment_domain: &SegmentDomain) -> Vec { - let start_points = segment_domain.start_point(); - let end_points = segment_domain.end_point(); + fn sort_segments(working: &WorkingVector) -> Vec { + let start_points = &working.segment_start; + let end_points = &working.segment_end; let mut sorted_segments = vec![0]; - let segment_domain_length = segment_domain.ids().len(); + let segment_count = working.segment_ids.len(); - for _ in 0..segment_domain_length { + for _ in 0..segment_count { match sorted_segments.last() { Some(&last) => { if let Some(index) = start_points.iter().position(|&p| p == end_points[last]) { @@ -3331,8 +3460,8 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve } } - if segment_domain_length != sorted_segments.len() { - for i in 0..segment_domain_length { + if segment_count != sorted_segments.len() { + for i in 0..segment_count { if !sorted_segments.contains(&i) { sorted_segments.push(i); } @@ -3342,34 +3471,35 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve sorted_segments } - fn update_existing_segments(vector: &mut Vector, transform: DAffine2, distance: f64, segments_connected: &mut [usize]) -> Vec<[usize; 2]> { - let mut next_id = vector.point_domain.next_id(); + fn update_existing_segments(working: &mut WorkingVector, transform: DAffine2, distance: f64, segments_connected: &mut [usize]) -> Vec<[usize; 2]> { + let mut next_id = next_working_point_id(working); let mut new_segments = Vec::new(); - let sorted_segments = sort_segments(&vector.segment_domain); - let segment_domain = &mut vector.segment_domain; - let segment_domain_length = segment_domain.ids().len(); + let sorted_segments = sort_segments(working); + let segment_count = working.segment_ids.len(); let mut first_original_length = 0.; let mut first_length = 0.; let mut prev_original_length = 0.; let mut prev_length = 0.; - for i in 0..segment_domain_length { - let (index, next_index) = if i == segment_domain_length - 1 { (i, 0) } else { (i, i + 1) }; - let pair_handles_and_points = segment_domain.pair_handles_and_points_mut_by_index(sorted_segments[index], sorted_segments[next_index]); - let (handles, start_point, end_point, next_handles, next_start_point, next_end_point) = pair_handles_and_points; + for i in 0..segment_count { + let (index, next_index) = if i == segment_count - 1 { (i, 0) } else { (i, i + 1) }; + let (segment, next_segment) = (sorted_segments[index], sorted_segments[next_index]); + + let (start_point, mut end_point) = (working.segment_start[segment], working.segment_end[segment]); + let (mut next_start_point, next_end_point) = (working.segment_start[next_segment], working.segment_end[next_segment]); - let start = vector.point_domain.positions()[*start_point]; - let end = vector.point_domain.positions()[*end_point]; + let start = working.point_positions[start_point]; + let end = working.point_positions[end_point]; - let mut bezier = handles_to_segment(start, *handles, end); + let mut bezier = handles_to_segment(start, working.segment_handles[segment], end); bezier = Affine::new(transform.to_cols_array()) * bezier; - let next_start = vector.point_domain.positions()[*next_start_point]; - let next_end = vector.point_domain.positions()[*next_end_point]; + let next_start = working.point_positions[next_start_point]; + let next_end = working.point_positions[next_end_point]; - let mut next_bezier = handles_to_segment(next_start, *next_handles, next_end); + let mut next_bezier = handles_to_segment(next_start, working.segment_handles[next_segment], next_end); next_bezier = Affine::new(transform.to_cols_array()) * next_bezier; let calculated_split_distance = calculate_distance_to_split(bezier, next_bezier, distance); @@ -3395,7 +3525,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve (prev_original_length, prev_length) }; - let (next_original_length, mut next_length) = if index == segment_domain_length - 1 && next_index == 0 { + let (next_original_length, mut next_length) = if index == segment_count - 1 && next_index == 0 { (first_original_length, first_length) } else { (next_bezier.perimeter(DEFAULT_ACCURACY), next_bezier.perimeter(DEFAULT_ACCURACY)) @@ -3403,7 +3533,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve // Only split if the length is big enough to make it worthwhile let valid_length = length > 1e-10; - if segments_connected[*end_point] > 0 && valid_length { + if segments_connected[end_point] > 0 && valid_length { // Apply the bevel to the end let distance = calculated_split_distance.min(original_length.min(next_original_length) / 2.); bezier = split_distance(bezier.reverse(), distance, length).reverse(); @@ -3414,15 +3544,16 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve // Update the end position let pos = inverse_transform.transform_point2(point_to_dvec2(bezier.end())); - create_or_modify_point(&mut vector.point_domain, segments_connected, pos, end_point, &mut next_id, &mut new_segments); + create_or_modify_point(working, segments_connected, pos, &mut end_point, &mut next_id, &mut new_segments); + working.segment_end[segment] = end_point; } // Update the handles - *handles = segment_to_handles(&bezier).apply_transformation(|p| inverse_transform.transform_point2(p)); + working.segment_handles[segment] = segment_to_handles(&bezier).apply_transformation(|p| inverse_transform.transform_point2(p)); // Only split if the length is big enough to make it worthwhile let valid_length = next_length > 1e-10; - if segments_connected[*next_start_point] > 0 && valid_length { + if segments_connected[next_start_point] > 0 && valid_length { // Apply the bevel to the start let distance = calculated_split_distance.min(next_original_length.min(original_length) / 2.); next_bezier = split_distance(next_bezier, distance, next_length); @@ -3431,10 +3562,11 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve // Update the start position let pos = inverse_transform.transform_point2(point_to_dvec2(next_bezier.start())); - create_or_modify_point(&mut vector.point_domain, segments_connected, pos, next_start_point, &mut next_id, &mut new_segments); + create_or_modify_point(working, segments_connected, pos, &mut next_start_point, &mut next_id, &mut new_segments); + working.segment_start[next_segment] = next_start_point; // Update the handles - *next_handles = segment_to_handles(&next_bezier).apply_transformation(|p| inverse_transform.transform_point2(p)); + working.segment_handles[next_segment] = segment_to_handles(&next_bezier).apply_transformation(|p| inverse_transform.transform_point2(p)); } prev_original_length = next_original_length; @@ -3444,22 +3576,27 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve new_segments } - fn insert_new_segments(vector: &mut Vector, new_segments: &[[usize; 2]]) { - let mut next_id = vector.segment_domain.next_id(); + fn insert_new_segments(working: &mut WorkingVector, new_segments: &[[usize; 2]]) { + let mut next_id = next_working_segment_id(working); for &[start, end] in new_segments { let handles = BezierHandles::Linear; - vector.segment_domain.push(next_id.next_id(), start, end, handles); + push_working_segment(working, next_id.next_id(), start, end, handles); } } - if distance > 1. && vector.segment_domain.ids().len() > 1 { - let mut segments_connected = segments_connected_count(&vector); - let new_segments = update_existing_segments(&mut vector, transform, distance, &mut segments_connected); - insert_new_segments(&mut vector, &new_segments); + if distance <= 1. { + return; } - vector + let mut working = WorkingVector::flatten(vector, paths); + if working.segment_ids.len() > 1 { + let mut segments_connected = segments_connected_count(&working); + let new_segments = update_existing_segments(&mut working, transform, distance, &mut segments_connected); + insert_new_segments(&mut working, &new_segments); + + working.reweave(vector, paths); + } } #[node_macro::node(category("Vector: Modifier"), icon("NodeBevel"), path(core_types::vector))] @@ -3472,19 +3609,20 @@ fn bevel( distance: Item, ) -> Item { V::map_vector_items(source, |source| { + let mut source = source; let distance = *distance.element(); let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); - let (element, attributes) = source.into_parts(); + edit_item_geometry(&mut source, |vector, paths| bevel_algorithm(vector, paths, transform, distance)); - Item::from_parts(bevel_algorithm(element, transform, distance), attributes) + source }) } #[node_macro::node(category("Vector: Modifier"), icon("NodeClosePath"), path(core_types::vector))] fn close_path(_: impl Ctx, #[implementations(Graphic, Vector)] source: Item) -> Item { V::map_vector_items(source, |mut item| { - item.element_mut().close_subpaths(); + edit_item_geometry(&mut item, |vector, paths| vector.close_subpaths(paths)); item }) } @@ -3492,8 +3630,9 @@ fn close_path(_: impl Ctx, #[implemen #[node_macro::node(category("Vector: Measure"), icon("NodePointInside"), path(core_types::vector))] fn point_inside(_: impl Ctx, source: Item, point: Item) -> Item { let point = point.into_element(); + let paths = paths_from_item_attributes(&source).unwrap_or_else(|| no_paths()); let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); - let inside = source.element().check_point_inside_shape(transform, point); + let inside = source.element().check_point_inside_shape(paths, transform, point); Item::new_from_element(inside) } @@ -3507,7 +3646,8 @@ async fn list_length(_: impl Ctx, content: ListDyn) -> Item { #[node_macro::node(category("Vector: Measure"), icon("NodeCountPoints"), path(graphene_core::vector))] async fn count_points(_: impl Ctx, content: Item) -> Item { - let count = content.element().point_domain.positions().len() as f64; + let paths = paths_from_item_attributes(&content).unwrap_or_else(|| no_paths()); + let count = content.element().anchor_indices(paths).len() as f64; Item::new_from_element(count) } @@ -3523,7 +3663,8 @@ async fn index_points( index: Item, ) -> Item { let index = index.into_element(); - let positions = content.element().point_domain.positions(); + let paths = paths_from_item_attributes(&content).unwrap_or_else(|| no_paths()); + let positions = content.element().anchor_positions(paths); let Some(last_index) = positions.len().checked_sub(1) else { return Item::new_from_element(DVec2::ZERO); @@ -3541,10 +3682,11 @@ async fn index_points( #[node_macro::node(category("Vector: Measure"), icon("NodePathLength"), path(core_types::vector))] async fn path_length(_: impl Ctx, source: Item) -> Item { + let paths = paths_from_item_attributes(&source).unwrap_or_else(|| no_paths()); let transform: DAffine2 = source.attribute_cloned_or_default(ATTR_TRANSFORM); let length = source .element() - .stroke_bezpath_iter() + .stroke_bezpath_iter(paths) .map(|mut bezpath| { bezpath.apply_affine(Affine::new(transform.to_cols_array())); bezpath.perimeter(DEFAULT_ACCURACY) @@ -3561,7 +3703,8 @@ async fn area(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node(); + let paths = paths_from_item_attributes(&vector).unwrap_or_else(|| no_paths()); + let area = vector.element().stroke_bezpath_iter(paths).map(|subpath| subpath.area() * area_scale).sum::(); Item::new_from_element(area) } @@ -3573,18 +3716,19 @@ async fn centroid(ctx: impl Ctx + CloneVarArgs + ExtractAll, content: impl Node< let vector = content.eval(new_ctx).await; let transform: DAffine2 = vector.attribute_cloned_or_default(ATTR_TRANSFORM); - let position = element_centroid(vector.element(), transform, centroid_type); + let paths = paths_from_item_attributes(&vector).unwrap_or_else(|| no_paths()); + let position = element_centroid(vector.element(), paths, transform, centroid_type); Item::new_from_element(position) } -/// The area- or length-weighted centroid of one vector element's subpaths, averaging raw point positions when the weights all vanish. -fn element_centroid(element: &Vector, transform: DAffine2, centroid_type: CentroidType) -> DVec2 { +/// The area- or length-weighted centroid of one vector element's subpaths, averaging anchor positions when the weights all vanish. +fn element_centroid(element: &Vector, paths: &List, transform: DAffine2, centroid_type: CentroidType) -> DVec2 { // All subpath centroid positions added together as if they were vectors from the origin, weighted by area or length let mut centroid = DVec2::ZERO; let mut sum = 0.; - for bezpath in element.stroke_bezpath_iter() { + for bezpath in element.stroke_bezpath_iter(paths) { let partial = match centroid_type { CentroidType::Area => bezpath_area_centroid_and_area(&bezpath, Some(1e-3), Some(1e-3)).filter(|(_, area)| *area > 0.), CentroidType::Length => bezpath_length_centroid_and_length(&bezpath, None, true), @@ -3600,7 +3744,7 @@ fn element_centroid(element: &Vector, transform: DAffine2, centroid_type: Centro } // Without a summed denominator, return the average of all positions instead - let positions = element.point_domain.positions(); + let positions = element.anchor_positions(paths); if positions.is_empty() { return DVec2::ZERO; } @@ -3630,7 +3774,7 @@ mod test { } fn vector_node_from_bezpath(bezpath: BezPath) -> List { - List::new_from_element(Vector::from_bezpath(bezpath)) + List::new_from_item(Vector::item_from_bezpath(&bezpath)) } /// Stands in for the embedding adapter a compiled graph inserts ahead of a `List` connector. @@ -3639,19 +3783,27 @@ mod test { } fn vector_item_from_bezpath(bezpath: BezPath) -> Item { - Item::new_from_element(Vector::from_bezpath(bezpath)) + Vector::item_from_bezpath(&bezpath) } fn create_vector_item(bezpath: BezPath, transform: DAffine2) -> Item { - let mut row = Vector::default(); - row.append_bezpath(bezpath); - Item::new_from_element(row).with_attribute(ATTR_TRANSFORM, transform) + Vector::item_from_bezpath(&bezpath).with_attribute(ATTR_TRANSFORM, transform) } fn item(value: T) -> Item { Item::new_from_element(value) } + /// The paths riding an item, standing in for the empty weave when the item carries none. + fn item_paths(item: &Item) -> &List { + paths_from_item_attributes(item).unwrap_or_else(|| no_paths()) + } + + /// The paths riding one list row, standing in for the empty weave when the row carries none. + fn row_paths(list: &List, index: usize) -> &List { + paths_from_list_row_attributes(list, index).unwrap_or_else(|| no_paths()) + } + fn vector_item_from_points(points: &[DVec2]) -> Item { super::points_to_vector((), points.iter().map(|&position| Item::new_from_element(position)).collect()) } @@ -3661,8 +3813,9 @@ mod test { #[test] fn points_to_vector_keeps_positions_in_order_and_unconnected() { let result = vector_item_from_points(&SQUARE_WITH_CENTER); - assert_eq!(result.element().point_domain.positions(), SQUARE_WITH_CENTER); - assert!(result.element().segment_domain.ids().is_empty()); + let paths = paths_from_item_attributes(&result).cloned().unwrap_or_default(); + assert_eq!(result.element().anchor_positions(&paths), SQUARE_WITH_CENTER); + assert_eq!(result.element().segment_iter(&paths).count(), 0); } #[tokio::test] @@ -3671,10 +3824,11 @@ mod test { // contour returns to approximately, not exactly, its start; that near-coincident point must close, not duplicate). let delaunay = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)).await; let offset = super::offset_path((), delaunay, item(0.5), item(StrokeJoin::Miter), item(4.)).await; + let paths = item_paths(&offset); let result = offset.element(); let mut subpaths = 0; - for (group, closed) in result.stroke_manipulator_groups() { + for (group, closed) in result.stroke_manipulator_groups(paths) { subpaths += 1; assert!(closed, "offset of a closed triangle should stay closed"); let first = group.first().unwrap().anchor; @@ -3687,12 +3841,13 @@ mod test { #[tokio::test] async fn delaunay_disconnected_cells_make_one_subpath_per_triangle() { let result = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)).await; + let paths = item_paths(&result); let vector = result.element(); // The square plus its center tessellates into four triangles, each its own closed subpath. - assert_eq!(vector.stroke_manipulator_groups().filter(|(_, closed)| *closed).count(), 4); - assert_eq!(vector.segment_domain.ids().len(), 4 * 3); - assert_eq!(vector.point_domain.ids().len(), 4 * 3); - assert!(!vector.use_face_fill()); + assert_eq!(vector.stroke_manipulator_groups(paths).filter(|(_, closed)| *closed).count(), 4); + assert_eq!(vector.segment_ids(paths).len(), 4 * 3); + assert_eq!(vector.anchor_indices(paths).len(), 4 * 3); + assert!(!vector.use_face_fill(paths)); } #[tokio::test] @@ -3700,9 +3855,9 @@ mod test { fn signed_area(anchors: &[DVec2]) -> f64 { (0..anchors.len()).map(|i| anchors[i].perp_dot(anchors[(i + 1) % anchors.len()])).sum::() / 2. } - fn subpath_winding_signs(vector: &Vector) -> Vec { + fn subpath_winding_signs(vector: &Vector, paths: &List) -> Vec { vector - .stroke_manipulator_groups() + .stroke_manipulator_groups(paths) .map(|(group, _)| signed_area(&group.iter().map(|g| g.anchor).collect::>()).signum()) .collect() } @@ -3710,18 +3865,18 @@ mod test { // The Rectangle and Ellipse generators define the framework's fill winding convention, built from these // shape constructors (`rectangle_bezpath` / `ellipse_bezpath`), so their winding is the source of truth. use vector_types::vector::algorithms::shapes::{ellipse_bezpath, rectangle_bezpath}; - let rectangle = Vector::from_bezpath(rectangle_bezpath(DVec2::new(-50., -50.), DVec2::new(50., 50.))); - let ellipse = Vector::from_bezpath(ellipse_bezpath(DVec2::new(-50., -25.), DVec2::new(50., 25.))); - let expected = subpath_winding_signs(&rectangle)[0]; - assert_eq!(subpath_winding_signs(&ellipse)[0], expected, "Rectangle and Ellipse should agree on winding"); + let (rectangle, rectangle_paths) = Vector::from_bezpath(&rectangle_bezpath(DVec2::new(-50., -50.), DVec2::new(50., 50.))); + let (ellipse, ellipse_paths) = Vector::from_bezpath(&ellipse_bezpath(DVec2::new(-50., -25.), DVec2::new(50., 25.))); + let expected = subpath_winding_signs(&rectangle, &rectangle_paths)[0]; + assert_eq!(subpath_winding_signs(&ellipse, &ellipse_paths)[0], expected, "Rectangle and Ellipse should agree on winding"); // Delaunay and Voronoi must emit subpaths that wind the same way as those generators. let delaunay = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)).await; let voronoi = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)).await; - for sign in subpath_winding_signs(delaunay.element()) { + for sign in subpath_winding_signs(delaunay.element(), item_paths(&delaunay)) { assert_eq!(sign, expected, "Delaunay subpath winding should match the Rectangle/Ellipse generators"); } - for sign in subpath_winding_signs(voronoi.element()) { + for sign in subpath_winding_signs(voronoi.element(), item_paths(&voronoi)) { assert_eq!(sign, expected, "Voronoi subpath winding should match the Rectangle/Ellipse generators"); } } @@ -3729,25 +3884,27 @@ mod test { #[tokio::test] async fn delaunay_shared_mesh_welds_points_and_shares_edges() { let result = super::triangulate((), vector_item_from_points(&SQUARE_WITH_CENTER), item(true)).await; + let paths = item_paths(&result); let vector = result.element(); // The connected mesh reuses the five input points and shares edges, so it fills face by face. - assert!(vector.use_face_fill()); - assert_eq!(vector.point_domain.ids().len(), 5); + assert!(vector.use_face_fill(paths)); + assert_eq!(vector.anchor_indices(paths).len(), 5); // Four hull edges plus four spokes to the center, each emitted once. - assert_eq!(vector.segment_domain.ids().len(), 8); + assert_eq!(vector.segment_ids(paths).len(), 8); } #[tokio::test] async fn voronoi_disconnected_cells_make_a_subpath_per_cell() { let result = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(false)).await; + let paths = item_paths(&result); let vector = result.element(); - let cells = vector.stroke_manipulator_groups().filter(|(_, closed)| *closed).count(); + let cells = vector.stroke_manipulator_groups(paths).filter(|(_, closed)| *closed).count(); assert!(cells > 0, "expected at least one Voronoi cell"); // Every cell is a closed subpath, so segments and points come in matched per-cell loops. - assert_eq!(vector.segment_domain.ids().len(), vector.point_domain.ids().len()); + assert_eq!(vector.segment_ids(paths).len(), vector.anchor_indices(paths).len()); // Clipping to the convex hull keeps all cell vertices within the input bounds. - for &position in vector.point_domain.positions() { + for position in vector.anchor_positions(paths) { assert!(position.x >= -1e-6 && position.x <= 10. + 1e-6); assert!(position.y >= -1e-6 && position.y <= 10. + 1e-6); } @@ -3756,9 +3913,10 @@ mod test { #[tokio::test] async fn voronoi_shared_mesh_uses_face_fill() { let result = super::voronoi_cells((), vector_item_from_points(&SQUARE_WITH_CENTER), item(true)).await; + let paths = item_paths(&result); let vector = result.element(); - assert!(vector.use_face_fill()); - assert!(!vector.segment_domain.ids().is_empty()); + assert!(vector.use_face_fill(paths)); + assert!(!vector.segment_ids(paths).is_empty()); } #[tokio::test] @@ -3766,9 +3924,10 @@ mod test { // Two points cannot form a diagram, so the element passes through unchanged. let points = [DVec2::new(0., 0.), DVec2::new(1., 1.)]; let result = super::voronoi_cells((), vector_item_from_points(&points), item(false)).await; + let paths = item_paths(&result); let vector = result.element(); - assert_eq!(vector.point_domain.ids().len(), 2); - assert_eq!(vector.segment_domain.ids().len(), 0); + assert_eq!(vector.anchor_indices(paths).len(), 2); + assert_eq!(vector.segment_ids(paths).len(), 0); } #[tokio::test] @@ -3783,18 +3942,18 @@ mod test { DVec2::new(7., 5.), ]; let result = super::relax_points((), vector_item_from_points(&points), item(2.)).await; - let vector = result.element(); + let positions = result.element().anchor_positions(item_paths(&result)); // Relaxation preserves the point count but repositions the interior anchors within the hull. - assert_eq!(vector.point_domain.ids().len(), points.len()); - assert_ne!(vector.point_domain.positions(), &points[..]); + assert_eq!(positions.len(), points.len()); + assert_ne!(positions, points); // The convex-hull corners are pinned. Asserted on the count first, since zipping a short result // would skip the checks rather than fail them. - assert!(vector.point_domain.positions().len() >= 4, "the relaxed hull should still have its four corners"); - for (corner, (&position, &point)) in vector.point_domain.positions().iter().zip(&points).take(4).enumerate() { + assert!(positions.len() >= 4, "the relaxed hull should still have its four corners"); + for (corner, (&position, &point)) in positions.iter().zip(&points).take(4).enumerate() { assert_eq!(position, point, "hull corner {corner} should be pinned"); } - for &point in vector.point_domain.positions() { + for point in positions { assert!(point.x >= -1e-6 && point.x <= 10. + 1e-6); assert!(point.y >= -1e-6 && point.y <= 10. + 1e-6); } @@ -3802,11 +3961,12 @@ mod test { #[tokio::test] async fn bounding_box() { - let bounding_box = super::bounding_box((), Item::new_from_element(Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)))).await; + let bounding_box = super::bounding_box((), Vector::item_from_bezpath(&Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY))).await; + let paths = item_paths(&bounding_box); let bounding_box = bounding_box.element(); - assert_eq!(bounding_box.stroke_manipulator_groups().count(), 1); + assert_eq!(bounding_box.stroke_manipulator_groups(paths).count(), 1); let manipulator_groups_anchors = bounding_box - .stroke_manipulator_groups() + .stroke_manipulator_groups(paths) .next() .unwrap() .0 @@ -3817,14 +3977,15 @@ mod test { assert_eq!(&manipulator_groups_anchors[..4], &[DVec2::NEG_ONE, DVec2::new(1., -1.), DVec2::ONE, DVec2::new(-1., 1.),]); // Test a rectangular path with nonzero rotation - let square = Vector::from_bezpath(Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); - let mut square = List::new_from_element(square); + let square = Vector::item_from_bezpath(&Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY)); + let mut square = List::new_from_item(square); square.with_attribute_mut_or_default(ATTR_TRANSFORM, 0, |t: &mut DAffine2| *t *= DAffine2::from_angle(std::f64::consts::FRAC_PI_4)); let bounding_box = BoundingBoxNodeMapped { content: FutureWrapperNode(square) }.eval(Footprint::default()).await; + let paths = row_paths(&bounding_box, 0); let bounding_box = bounding_box.element(0).unwrap(); - assert_eq!(bounding_box.stroke_manipulator_groups().count(), 1); + assert_eq!(bounding_box.stroke_manipulator_groups(paths).count(), 1); let manipulator_groups_anchors = bounding_box - .stroke_manipulator_groups() + .stroke_manipulator_groups(paths) .next() .unwrap() .0 @@ -3842,7 +4003,10 @@ mod test { let points = Rect::new(-10., -10., 10., 10.).to_path(DEFAULT_ACCURACY); let element = Rect::new(-1., -1., 1., 1.).to_path(DEFAULT_ACCURACY); - let expected_points = Vector::from_bezpath(points.clone()).point_domain.positions().to_vec(); + let expected_points = { + let (vector, paths) = Vector::from_bezpath(&points); + vector.anchor_positions(&paths) + }; let copy_to_points = super::copy_to_points( Footprint::default(), @@ -3857,11 +4021,12 @@ mod test { ) .await; let combined = List::new_from_item(super::combine_paths(Footprint::default(), List::new_from_element(Graphic::VectorList(copy_to_points))).await); + let combined_paths = row_paths(&combined, 0); let combined_copy_to_points = combined.element(0).unwrap(); - assert_eq!(combined_copy_to_points.stroke_manipulator_groups().count(), expected_points.len()); + assert_eq!(combined_copy_to_points.stroke_manipulator_groups(combined_paths).count(), expected_points.len()); - for (index, (manipulator_groups, _)) in combined_copy_to_points.stroke_manipulator_groups().enumerate() { + for (index, (manipulator_groups, _)) in combined_copy_to_points.stroke_manipulator_groups(combined_paths).enumerate() { let offset = expected_points[index]; let manipulator_groups_anchors = manipulator_groups.iter().map(|manipulators| manipulators.anchor).collect::>(); assert_eq!( @@ -3885,9 +4050,9 @@ mod test { Item::new_from_element(false), ) .await; - let sample_polyline = sample_polyline.element(); - assert_eq!(sample_polyline.point_domain.positions().len(), 4); - for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) { + let positions = sample_polyline.element().anchor_positions(item_paths(&sample_polyline)); + assert_eq!(positions.len(), 4); + for (pos, expected) in positions.iter().zip([DVec2::X * 0., DVec2::X * 30., DVec2::X * 60., DVec2::X * 90.]) { assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}"); } } @@ -3905,9 +4070,9 @@ mod test { Item::new_from_element(true), ) .await; - let sample_polyline = sample_polyline.element(); - assert_eq!(sample_polyline.point_domain.positions().len(), 4); - for (pos, expected) in sample_polyline.point_domain.positions().iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) { + let positions = sample_polyline.element().anchor_positions(item_paths(&sample_polyline)); + assert_eq!(positions.len(), 4); + for (pos, expected) in positions.iter().zip([DVec2::X * 45., DVec2::X * 60., DVec2::X * 75., DVec2::X * 90.]) { assert!(pos.distance(expected) < 1e-3, "Expected {expected} found {pos}"); } } @@ -3920,13 +4085,9 @@ mod test { Item::new_from_element(0), ) .await; - let poisson_points = poisson_points.element(); - assert!( - (20..=40).contains(&poisson_points.point_domain.positions().len()), - "actual len {}", - poisson_points.point_domain.positions().len() - ); - for point in poisson_points.point_domain.positions() { + let positions = poisson_points.element().anchor_positions(item_paths(&poisson_points)); + assert!((20..=40).contains(&positions.len()), "actual len {}", positions.len()); + for point in positions { assert!(point.length() < 50. + 1., "Expected point in circle {point}") } } @@ -3944,9 +4105,79 @@ mod test { #[tokio::test] async fn spline() { let spline = super::spline(Footprint::default(), vector_item_from_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY))).await; + let paths = item_paths(&spline); let spline = spline.element(); - assert_eq!(spline.stroke_bezpath_iter().count(), 1); - assert_eq!(spline.point_domain.positions(), &[DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]); + assert_eq!(spline.stroke_bezpath_iter(paths).count(), 1); + assert_eq!(spline.anchor_positions(paths), [DVec2::ZERO, DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]); + assert_eq!(spline.colinear_pairs(paths).len(), 4, "every anchor of the closed spline should be smooth"); + + // An open spline is smooth only between its ends + let mut open = BezPath::new(); + open.move_to((0., 0.)); + open.line_to((10., 10.)); + open.line_to((20., 0.)); + open.line_to((30., 10.)); + let spline = super::spline(Footprint::default(), vector_item_from_bezpath(open)).await; + assert_eq!(smooth_anchors_by_contour(&spline), [[false, true, true, false]]); + } + + /// Whether each anchor keeps a smooth tangent pair, contour by contour. + fn smooth_anchors_by_contour(item: &Item) -> Vec> { + use vector_types::vector::misc::ManipulatorPointId; + + let (vector, paths) = (item.element(), item_paths(item)); + vector + .stroke_manipulator_groups(paths) + .map(|(group, _)| group.iter().map(|manipulators| vector.colinear(paths, ManipulatorPointId::Anchor(manipulators.id))).collect()) + .collect() + } + + // Existing handles are kept, so only the anchors without them are tangented and smooth + #[tokio::test] + async fn auto_tangents_marks_the_tangented_anchors_smooth() { + let mut bezpath = BezPath::new(); + bezpath.move_to((0., 0.)); + bezpath.line_to((10., 10.)); + bezpath.line_to((20., 0.)); + bezpath.line_to((30., 10.)); + bezpath.move_to((100., 0.)); + bezpath.line_to((150., 0.)); + bezpath.line_to((125., 40.)); + bezpath.close_path(); + bezpath.move_to((200., 0.)); + bezpath.curve_to((210., -20.), (240., 30.), (250., 0.)); + bezpath.line_to((250., 50.)); + bezpath.line_to((200., 50.)); + bezpath.close_path(); + + let output = super::auto_tangents((), vector_item_from_bezpath(bezpath), item(0.5), item(true)).await; + + assert_eq!( + smooth_anchors_by_contour(&output), + [vec![false, true, true, false], vec![true, true, true], vec![false, false, true, true]] + ); + } + + // A loop of one segment has no neighbor to share a tangent with, so it passes through unchanged + #[tokio::test] + async fn auto_tangents_keeps_a_single_segment_loop() { + let teardrop = CubicBez::new((0., 0.), (50., -50.), (100., 50.), (0., 0.)); + let mut bezpath = BezPath::new(); + bezpath.move_to(teardrop.p0); + bezpath.curve_to(teardrop.p1, teardrop.p2, teardrop.p3); + bezpath.close_path(); + bezpath.move_to((200., 0.)); + bezpath.line_to((250., 0.)); + bezpath.line_to((225., 40.)); + bezpath.close_path(); + + let output = super::auto_tangents((), vector_item_from_bezpath(bezpath), item(0.5), item(true)).await; + let paths = item_paths(&output); + + let segments: Vec = output.element().segment_iter(paths).map(|(_, segment, ..)| segment).collect(); + assert_eq!(segments.len(), 4); + assert!(segments.contains(&PathSeg::Cubic(teardrop)), "the loop should keep its curve, not among {segments:?}"); + assert_eq!(smooth_anchors_by_contour(&output), [vec![false], vec![true, true, true]]); } #[tokio::test] async fn morph() { @@ -3965,29 +4196,20 @@ mod test { ) .await; let morphed = List::new_from_item(morphed); - let morphed_element = morphed.element(0).unwrap(); + let morphed_positions = morphed.element(0).unwrap().anchor_positions(row_paths(&morphed, 0)); // Geometry stays in local space (original rectangle coordinates) - assert_eq!( - &morphed_element.point_domain.positions()[..4], - vec![DVec2::new(0., 0.), DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)] - ); + assert_eq!(&morphed_positions[..4], vec![DVec2::new(0., 0.), DVec2::new(100., 0.), DVec2::new(100., 100.), DVec2::new(0., 100.)]); // The interpolated transform carries the midpoint translation (approximate due to arc-length parameterization) assert!((morphed.attribute_cloned_or_default::(ATTR_TRANSFORM, 0).translation - DVec2::new(-50., -50.)).length() < 1e-3); } #[tokio::test] async fn morph_interpolates_fill() { - let rect = || { - let mut v = Vector::default(); - v.append_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); - v - }; + let rect = || Vector::item_from_bezpath(&Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); let solid_fill = |color: Color| Appearance::new_single(Coverage::new_fill(), Graphic::from(List::new_from_element(color))); - let item_a = Item::new_from_element(rect()) - .with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY) - .with_attribute(ATTR_APPEARANCE, solid_fill(Color::RED)); - let item_b = Item::new_from_element(rect()) + let item_a = rect().with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY).with_attribute(ATTR_APPEARANCE, solid_fill(Color::RED)); + let item_b = rect() .with_attribute(ATTR_TRANSFORM, DAffine2::from_translation((-100., -100.).into())) .with_attribute(ATTR_APPEARANCE, solid_fill(Color::BLUE)); @@ -4018,11 +4240,7 @@ mod test { #[tokio::test] async fn morph_pairs_appearance_coverages_by_cover() { - let rect = || { - let mut v = Vector::default(); - v.append_bezpath(Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); - v - }; + let rect = || Vector::item_from_bezpath(&Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY)); let paint_color = |appearance: &Appearance, cover| { let paint = appearance.first_paint_of(cover).expect("Morph should keep both paints at the midpoint"); @@ -4040,10 +4258,10 @@ mod test { appearance }; - let item_a = Item::new_from_element(rect()) + let item_a = rect() .with_attribute(ATTR_TRANSFORM, DAffine2::IDENTITY) .with_attribute(ATTR_APPEARANCE, appearance(Color::RED, Color::BLACK, CoverPlacement::Above)); - let item_b = Item::new_from_element(rect()) + let item_b = rect() .with_attribute(ATTR_TRANSFORM, DAffine2::from_translation((-100., -100.).into())) .with_attribute(ATTR_APPEARANCE, appearance(Color::BLUE, Color::WHITE, CoverPlacement::Below)); @@ -4075,8 +4293,8 @@ mod test { } #[track_caller] - fn contains_segment(vector: Vector, target: PathSeg) { - let segments = vector.segment_iter().map(|x| x.1); + fn contains_segment(vector: &Vector, paths: &List, target: PathSeg) { + let segments = vector.segment_iter(paths).map(|x| x.1); let count = segments .filter(|segment| pathseg_abs_diff_eq(*segment, target, 0.01) || pathseg_abs_diff_eq(segment.reverse(), target, 0.01)) .count(); @@ -4085,7 +4303,7 @@ mod test { count, 1, "Expected exactly one matching segment for {target:?}, but found {count}. The given segments are: {:#?}", - vector.segment_iter().collect::>() + vector.segment_iter(paths).collect::>() ); } @@ -4093,22 +4311,23 @@ mod test { async fn bevel_rect() { let source = Rect::new(0., 0., 100., 100.).to_path(DEFAULT_ACCURACY); let beveled = super::bevel(Footprint::default(), vector_item_from_bezpath(source), Item::new_from_element(2_f64.sqrt() * 10.)); + let paths = item_paths(&beveled); let beveled = beveled.element(); - assert_eq!(beveled.point_domain.positions().len(), 8); - assert_eq!(beveled.segment_domain.ids().len(), 8); + assert_eq!(beveled.anchor_positions(paths).len(), 8); + assert_eq!(beveled.segment_ids(paths).len(), 8); // Segments - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(10., 0.), Point::new(90., 0.)))); - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(10., 100.), Point::new(90., 100.)))); - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(0., 10.), Point::new(0., 90.)))); - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 10.), Point::new(100., 90.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(10., 0.), Point::new(90., 0.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(10., 100.), Point::new(90., 100.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(0., 10.), Point::new(0., 90.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(100., 10.), Point::new(100., 90.)))); // Joins - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(10., 0.), Point::new(0., 10.)))); - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(90., 0.), Point::new(100., 10.)))); - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 90.), Point::new(90., 100.)))); - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(10., 100.), Point::new(0., 90.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(10., 0.), Point::new(0., 10.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(90., 0.), Point::new(100., 10.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(100., 90.), Point::new(90., 100.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(10., 100.), Point::new(0., 90.)))); } #[tokio::test] @@ -4121,18 +4340,19 @@ mod test { source.push(curve.as_path_el()); let beveled = super::bevel((), vector_item_from_bezpath(source), Item::new_from_element(2_f64.sqrt() * 10.)); + let paths = item_paths(&beveled); let beveled = beveled.element(); - assert_eq!(beveled.point_domain.positions().len(), 4); - assert_eq!(beveled.segment_domain.ids().len(), 3); + assert_eq!(beveled.anchor_positions(paths).len(), 4); + assert_eq!(beveled.segment_ids(paths).len(), 3); // Segments - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), Point::new(-100., 0.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(-8.2, 0.), Point::new(-100., 0.)))); let trimmed = trim_pathseg(curve, TValue::Euclidean(8.2 / curve.perimeter(DEFAULT_ACCURACY)), TValue::Parametric(1.)).unwrap(); - contains_segment(beveled.clone(), trimmed); + contains_segment(beveled, paths, trimmed); // Join - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), trimmed.start()))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(-8.2, 0.), trimmed.start()))); } #[tokio::test] @@ -4145,21 +4365,22 @@ mod test { source.push(curve.as_path_el()); let transform = DAffine2::from_scale_angle_translation(DVec2::splat(10.), 1., DVec2::new(99., 77.)); - let vector_item = Item::new_from_element(Vector::from_bezpath(source)).with_attribute(ATTR_TRANSFORM, transform); + let vector_item = Vector::item_from_bezpath(&source).with_attribute(ATTR_TRANSFORM, transform); let beveled = super::bevel((), vector_item, Item::new_from_element(2_f64.sqrt() * 100.)); + let paths = item_paths(&beveled); let beveled = beveled.element(); - assert_eq!(beveled.point_domain.positions().len(), 4); - assert_eq!(beveled.segment_domain.ids().len(), 3); + assert_eq!(beveled.anchor_positions(paths).len(), 4); + assert_eq!(beveled.segment_ids(paths).len(), 3); // Segments - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), Point::new(-100., 0.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(-8.2, 0.), Point::new(-100., 0.)))); let trimmed = trim_pathseg(curve, TValue::Euclidean(8.2 / curve.perimeter(DEFAULT_ACCURACY)), TValue::Parametric(1.)).unwrap(); - contains_segment(beveled.clone(), trimmed); + contains_segment(beveled, paths, trimmed); // Join - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(-8.2, 0.), trimmed.start()))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(-8.2, 0.), trimmed.start()))); } #[tokio::test] @@ -4171,19 +4392,20 @@ mod test { source.line_to(Point::new(0., 100.)); let beveled = super::bevel(Footprint::default(), vector_item_from_bezpath(source), Item::new_from_element(999.)); + let paths = item_paths(&beveled); let beveled = beveled.element(); - assert_eq!(beveled.point_domain.positions().len(), 6); - assert_eq!(beveled.segment_domain.ids().len(), 5); + assert_eq!(beveled.anchor_positions(paths).len(), 6); + assert_eq!(beveled.segment_ids(paths).len(), 5); // Segments - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(0., 0.), Point::new(50., 0.)))); - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 50.), Point::new(100., 50.)))); - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 50.), Point::new(50., 100.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(0., 0.), Point::new(50., 0.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(100., 50.), Point::new(100., 50.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(100., 50.), Point::new(50., 100.)))); // Joins - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(50., 0.), Point::new(100., 50.)))); - contains_segment(beveled.clone(), PathSeg::Line(Line::new(Point::new(100., 50.), Point::new(50., 100.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(50., 0.), Point::new(100., 50.)))); + contains_segment(beveled, paths, PathSeg::Line(Line::new(Point::new(100., 50.), Point::new(50., 100.)))); } #[tokio::test] @@ -4195,10 +4417,11 @@ mod test { let subpath = BezPath::from_path_segments([line, point, curve].into_iter()); let beveled_item = super::bevel(Footprint::default(), vector_item_from_bezpath(subpath), Item::new_from_element(5.)); + let paths = item_paths(&beveled_item); let beveled = beveled_item.element(); - assert_eq!(beveled.point_domain.positions().len(), 6); - assert_eq!(beveled.segment_domain.ids().len(), 5); + assert_eq!(beveled.anchor_positions(paths).len(), 6); + assert_eq!(beveled.segment_ids(paths).len(), 5); } // A rank-0 vector reaches the same per-list styling path as a vector list, rather than being skipped @@ -4235,7 +4458,7 @@ mod test { let mut content = Item::new_from_element(Graphic::Vector(Box::new(item))); let mut visited = Vec::new(); - content.for_each_vector_mut(|vector, vector_transform| visited.push((vector.bounding_box(), vector_transform))); + content.for_each_vector_mut(|vector, vector_paths, vector_transform| visited.push((vector.bounding_box(vector_paths), vector_transform))); assert_eq!(visited.len(), 1, "the lone vector should be visited exactly once"); assert_eq!(visited[0].1, transform, "its own transform attribute should come along for placement"); @@ -4244,7 +4467,7 @@ mod test { async fn round_corners_of(bezpath: BezPath, transform: DAffine2) -> Vec { let rounded = super::round_corners((), create_vector_item(bezpath, transform), item(10.), item(0.5), item(100.), item(5.)).await; - rounded.element().stroke_bezpath_iter().collect() + rounded.element().stroke_bezpath_iter(item_paths(&rounded)).collect() } fn distance_to_path(point: DVec2, bezpath: &BezPath) -> f64 { From d994afc36fc22dd2dae4bb13e17609ce2af69d2b Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 21 Aug 2026 18:17:18 -0700 Subject: [PATCH 8/8] Cover the stored modification format with the demo artwork that leans on it --- .../storage_tests/round_trip_tests.rs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs b/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs index 7f4ad9d14a9..b7ba3327c3f 100644 --- a/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs +++ b/editor/src/messages/portfolio/document/storage_tests/round_trip_tests.rs @@ -716,6 +716,57 @@ async fn demo_artwork_edit_autosaves_and_round_trips() { editor.active_document_mut().commit_storage_snapshot(&byte_store, true); } +/// Manual edits ride a saved document as a folded `VectorModification`, so opening the artworks carrying the most +/// of them is what proves the stored format still reads and applies. Their Path nodes sit on pen-drawn layers, so +/// each layer's geometry, curves included, comes from its stored modification alone. +#[tokio::test] +async fn demo_artwork_vector_modifications_still_apply() { + use graph_craft::document::value::TaggedValue; + + for name in ["red-dress", "isometric-fountain", "valley-of-spires"] { + let mut editor = EditorTestUtils::create(); + + let data = std::fs::read(format!("../demo-artwork/{name}.graphite")).expect("read demo artwork"); + editor + .handle_message(IngestMessage::Ingest { + data, + action: IngestAction::Open, + mime_type: String::new(), + path: Some(format!("{name}.graphite").into()), + }) + .await; + + let document = editor.active_document(); + let network = &document.network_interface; + let path_definition = DefinitionIdentifier::Network("Path".into()); + let mut path_layers = 0; + let mut any_curve = false; + + for layer in document.metadata().all_layers() { + let Some(path_node) = NodeGraphLayer::new(layer, network).upstream_visible_node_id_from_name_in_layer(&path_definition) else { + continue; + }; + path_layers += 1; + + let stored = network.document_network().nodes.get(&path_node).and_then(|node| node.inputs.get(1)).and_then(|input| input.as_value()); + let Some(TaggedValue::VectorModification(modification)) = stored else { + panic!("{name} layer {layer:?} has a Path node without a stored modification"); + }; + assert!(!modification.is_empty(), "{name} layer {layer:?} stored a modification that changes nothing"); + + let upstream = document.metadata().vector_modify.get(&path_node).expect("the graph run should capture what reaches the Path node"); + assert!(upstream.anchor_points().next().is_none(), "{name} layer {layer:?} has geometry upstream of its Path node"); + + let geometry = network.compute_modified_vector(layer).expect("a layer with a Path node should have a vector"); + assert!(geometry.anchor_points().next().is_some(), "{name} layer {layer:?} surfaced no anchors from its stored modification"); + any_curve |= geometry.segment_iter().any(|(_, segment, ..)| !matches!(segment, kurbo::PathSeg::Line(_))); + } + + assert!(path_layers > 0, "{name} has no layers edited through a Path node"); + assert!(any_curve, "{name} drew no curves from its stored modifications"); + } +} + /// The document's single Fill node, as `(network_path, node_id)`. fn find_fill_node(document: &DocumentMessageHandler) -> (Vec, graph_craft::document::NodeId) { node_paths(&document.network_interface)