-
-
Notifications
You must be signed in to change notification settings - Fork 995
Render vector mesh fills more correctly as separate subpaths #3388
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ use crate::vector::vector_types::Vector; | |
| use dyn_any::DynAny; | ||
| use glam::{DAffine2, DVec2}; | ||
| use kurbo::{CubicBez, Line, PathSeg, QuadBez}; | ||
| use std::collections::HashMap; | ||
| use std::collections::{HashMap, HashSet}; | ||
| use std::hash::{Hash, Hasher}; | ||
| use std::iter::zip; | ||
|
|
||
|
|
@@ -678,6 +678,44 @@ impl FoundSubpath { | |
| pub fn contains(&self, segment_id: SegmentId) -> bool { | ||
| self.edges.iter().any(|s| s.id == segment_id) | ||
| } | ||
|
|
||
| pub fn to_bezpath(&self, vector: &Vector) -> kurbo::BezPath { | ||
| let mut bezpath = kurbo::BezPath::new(); | ||
|
|
||
| if let Some(first_edge) = self.edges.first() { | ||
| let start_pos = vector.point_domain.positions()[first_edge.start]; | ||
| bezpath.move_to(dvec2_to_point(start_pos)); | ||
| } | ||
|
|
||
| for edge in &self.edges { | ||
| let segment_index = vector.segment_domain.ids().iter().position(|&id| id == edge.id).expect("Segment ID must exist"); | ||
|
|
||
| let mut handles = vector.segment_domain.handles()[segment_index]; | ||
| if edge.reverse { | ||
| handles = handles.reversed(); | ||
| } | ||
|
|
||
| let end_pos = vector.point_domain.positions()[edge.end]; | ||
|
|
||
| match handles { | ||
| BezierHandles::Linear => { | ||
| bezpath.line_to(dvec2_to_point(end_pos)); | ||
| } | ||
| BezierHandles::Quadratic { handle } => { | ||
| bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(end_pos)); | ||
| } | ||
| BezierHandles::Cubic { handle_start, handle_end } => { | ||
| bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(end_pos)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if self.is_closed() { | ||
| bezpath.close_path(); | ||
| } | ||
|
|
||
| bezpath | ||
| } | ||
| } | ||
|
|
||
| impl Vector { | ||
|
|
@@ -985,6 +1023,134 @@ impl Vector { | |
| self.segment_domain.map_ids(&id_map); | ||
| self.region_domain.map_ids(&id_map); | ||
| } | ||
|
|
||
| pub fn is_branching_mesh(&self) -> bool { | ||
| for point_index in 0..self.point_domain.len() { | ||
| let connection_count = self.segment_domain.connected_count(point_index); | ||
|
|
||
| if connection_count > 2 { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| false | ||
| } | ||
|
|
||
| /// Find all minimal closed regions (faces) in a branching mesh vector. | ||
| pub fn find_closed_regions(&self) -> Vec<FoundSubpath> { | ||
| let mut regions = Vec::new(); | ||
| let mut used_half_edges = HashSet::new(); | ||
|
|
||
| // Build adjacency list sorted by angle for proper face traversal | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does it mean to sort by angle? Angle between which directions? |
||
| let mut adjacency: HashMap<usize, Vec<(SegmentId, usize, bool)>> = HashMap::new(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be beneficial to add a struct since it is hard to determine what the usize and bool are. |
||
|
|
||
| for (segment_id, start, end, _) in self.segment_domain.iter() { | ||
| adjacency.entry(start).or_default().push((segment_id, end, false)); | ||
| adjacency.entry(end).or_default().push((segment_id, start, true)); | ||
| } | ||
|
|
||
| // Sort neighbors by angle to enable finding the "rightmost" path | ||
| for (point_idx, neighbors) in adjacency.iter_mut() { | ||
|
||
| let point_pos = self.point_domain.positions()[*point_idx]; | ||
| neighbors.sort_by(|a, b| { | ||
| let pos_a = self.point_domain.positions()[a.1]; | ||
| let pos_b = self.point_domain.positions()[b.1]; | ||
| let angle_a = (pos_a - point_pos).y.atan2((pos_a - point_pos).x); | ||
| let angle_b = (pos_b - point_pos).y.atan2((pos_b - point_pos).x); | ||
| angle_a.partial_cmp(&angle_b).unwrap_or(std::cmp::Ordering::Equal) | ||
| }); | ||
| } | ||
|
|
||
| for (segment_id, start, end, _) in self.segment_domain.iter() { | ||
| for &reversed in &[false, true] { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no need to do references here since [bool;2] has |
||
| let (from, to) = if reversed { (end, start) } else { (start, end) }; | ||
| let half_edge_key = (segment_id, reversed); | ||
|
|
||
| if used_half_edges.contains(&half_edge_key) { | ||
| continue; | ||
| } | ||
|
|
||
| if let Some(face) = self.find_minimal_face_from_edge(segment_id, from, to, reversed, &adjacency, &mut used_half_edges) { | ||
| regions.push(face); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| regions | ||
| } | ||
|
|
||
| /// Helper to find a minimal face (smallest cycle) starting from a half-edge | ||
| fn find_minimal_face_from_edge( | ||
| &self, | ||
| start_segment: SegmentId, | ||
| from_point: usize, | ||
| to_point: usize, | ||
| start_reversed: bool, | ||
| adjacency: &HashMap<usize, Vec<(SegmentId, usize, bool)>>, | ||
| used_half_edges: &mut HashSet<(SegmentId, bool)>, | ||
| ) -> Option<FoundSubpath> { | ||
| let mut path = vec![HalfEdge::new(start_segment, from_point, to_point, start_reversed)]; | ||
| let mut current = to_point; | ||
| let target = from_point; | ||
| let mut prev_segment = start_segment; | ||
|
|
||
| let mut iteration = 0; | ||
| let max_iterations = adjacency.len() * 2; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would this be the max iterations? Surely you should only loop |
||
|
|
||
| // Follow the "rightmost" edge at each vertex to find minimal face | ||
| loop { | ||
|
||
| iteration += 1; | ||
|
|
||
| if iteration > max_iterations { | ||
| return None; | ||
| } | ||
|
|
||
| let neighbors = adjacency.get(¤t)?; | ||
| // Find the next edge in counterclockwise order (rightmost turn) | ||
| let prev_direction = self.point_domain.positions()[current] - self.point_domain.positions()[path.last()?.start]; | ||
|
||
|
|
||
| let angle_between = |v1: DVec2, v2: DVec2| -> f64 { | ||
| let angle = v2.y.atan2(v2.x) - v1.y.atan2(v1.x); | ||
| if angle < 0.0 { angle + 2.0 * std::f64::consts::PI } else { angle } | ||
| }; | ||
|
|
||
| let next = neighbors.iter().filter(|(seg, _next, _rev)| *seg != prev_segment).min_by(|a, b| { | ||
| let dir_a = self.point_domain.positions()[a.1] - self.point_domain.positions()[current]; | ||
| let dir_b = self.point_domain.positions()[b.1] - self.point_domain.positions()[current]; | ||
| let angle_a = angle_between(prev_direction, dir_a); | ||
| let angle_b = angle_between(prev_direction, dir_b); | ||
| angle_a.partial_cmp(&angle_b).unwrap_or(std::cmp::Ordering::Equal) | ||
| })?; | ||
|
|
||
| let (next_segment, next_point, next_reversed) = *next; | ||
|
|
||
| if next_point == target { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Move after line 1144 to reuse the path-push for clarity |
||
| // Completed the cycle | ||
| path.push(HalfEdge::new(next_segment, current, next_point, next_reversed)); | ||
|
|
||
| // Mark all half-edges as used | ||
| for edge in &path { | ||
| used_half_edges.insert((edge.id, edge.reverse)); | ||
| } | ||
|
|
||
| return Some(FoundSubpath::new(path)); | ||
| } | ||
|
|
||
| // Check if we've created a cycle (might not be back to start) | ||
| if path.iter().any(|e| e.end == next_point && e.id != next_segment) { | ||
|
||
| return None; | ||
| } | ||
|
|
||
| path.push(HalfEdge::new(next_segment, current, next_point, next_reversed)); | ||
| prev_segment = next_segment; | ||
| current = next_point; | ||
|
|
||
| // Prevent infinite loops | ||
| if path.len() > adjacency.len() { | ||
|
||
| return None; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please try to avoid O(n^2) algorithms when you could just store the segment index in the
HalfEdgestruct.