Skip to content

Commit 5dedb08

Browse files
committed
Render vector mesh fills correctly as separate subpaths (#3378)
1 parent 94414ad commit 5dedb08

File tree

2 files changed

+577
-184
lines changed

2 files changed

+577
-184
lines changed

node-graph/gcore/src/vector/vector_attributes.rs

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::vector::vector_types::Vector;
44
use dyn_any::DynAny;
55
use glam::{DAffine2, DVec2};
66
use kurbo::{CubicBez, Line, PathSeg, QuadBez};
7-
use std::collections::HashMap;
7+
use std::collections::{HashMap, HashSet};
88
use std::hash::{Hash, Hasher};
99
use std::iter::zip;
1010

@@ -678,6 +678,44 @@ impl FoundSubpath {
678678
pub fn contains(&self, segment_id: SegmentId) -> bool {
679679
self.edges.iter().any(|s| s.id == segment_id)
680680
}
681+
682+
pub fn to_bezpath(&self, vector: &Vector) -> kurbo::BezPath {
683+
let mut bezpath = kurbo::BezPath::new();
684+
685+
if let Some(first_edge) = self.edges.first() {
686+
let start_pos = vector.point_domain.positions()[first_edge.start];
687+
bezpath.move_to(dvec2_to_point(start_pos));
688+
}
689+
690+
for edge in &self.edges {
691+
let segment_index = vector.segment_domain.ids().iter().position(|&id| id == edge.id).expect("Segment ID must exist");
692+
693+
let mut handles = vector.segment_domain.handles()[segment_index];
694+
if edge.reverse {
695+
handles = handles.reversed();
696+
}
697+
698+
let end_pos = vector.point_domain.positions()[edge.end];
699+
700+
match handles {
701+
BezierHandles::Linear => {
702+
bezpath.line_to(dvec2_to_point(end_pos));
703+
}
704+
BezierHandles::Quadratic { handle } => {
705+
bezpath.quad_to(dvec2_to_point(handle), dvec2_to_point(end_pos));
706+
}
707+
BezierHandles::Cubic { handle_start, handle_end } => {
708+
bezpath.curve_to(dvec2_to_point(handle_start), dvec2_to_point(handle_end), dvec2_to_point(end_pos));
709+
}
710+
}
711+
}
712+
713+
if self.is_closed() {
714+
bezpath.close_path();
715+
}
716+
717+
bezpath
718+
}
681719
}
682720

683721
impl Vector {
@@ -985,6 +1023,134 @@ impl Vector {
9851023
self.segment_domain.map_ids(&id_map);
9861024
self.region_domain.map_ids(&id_map);
9871025
}
1026+
1027+
pub fn is_branching_mesh(&self) -> bool {
1028+
for point_index in 0..self.point_domain.len() {
1029+
let connection_count = self.segment_domain.connected_count(point_index);
1030+
1031+
if connection_count > 2 {
1032+
return true;
1033+
}
1034+
}
1035+
1036+
false
1037+
}
1038+
1039+
/// Find all minimal closed regions (faces) in a branching mesh vector.
1040+
pub fn find_closed_regions(&self) -> Vec<FoundSubpath> {
1041+
let mut regions = Vec::new();
1042+
let mut used_half_edges = HashSet::new();
1043+
1044+
// Build adjacency list sorted by angle for proper face traversal
1045+
let mut adjacency: HashMap<usize, Vec<(SegmentId, usize, bool)>> = HashMap::new();
1046+
1047+
for (segment_id, start, end, _) in self.segment_domain.iter() {
1048+
adjacency.entry(start).or_default().push((segment_id, end, false));
1049+
adjacency.entry(end).or_default().push((segment_id, start, true));
1050+
}
1051+
1052+
// Sort neighbors by angle to enable finding the "rightmost" path
1053+
for (point_idx, neighbors) in adjacency.iter_mut() {
1054+
let point_pos = self.point_domain.positions()[*point_idx];
1055+
neighbors.sort_by(|a, b| {
1056+
let pos_a = self.point_domain.positions()[a.1];
1057+
let pos_b = self.point_domain.positions()[b.1];
1058+
let angle_a = (pos_a - point_pos).y.atan2((pos_a - point_pos).x);
1059+
let angle_b = (pos_b - point_pos).y.atan2((pos_b - point_pos).x);
1060+
angle_a.partial_cmp(&angle_b).unwrap_or(std::cmp::Ordering::Equal)
1061+
});
1062+
}
1063+
1064+
for (segment_id, start, end, _) in self.segment_domain.iter() {
1065+
for &reversed in &[false, true] {
1066+
let (from, to) = if reversed { (end, start) } else { (start, end) };
1067+
let half_edge_key = (segment_id, reversed);
1068+
1069+
if used_half_edges.contains(&half_edge_key) {
1070+
continue;
1071+
}
1072+
1073+
if let Some(face) = self.find_minimal_face_from_edge(segment_id, from, to, reversed, &adjacency, &mut used_half_edges) {
1074+
regions.push(face);
1075+
}
1076+
}
1077+
}
1078+
1079+
regions
1080+
}
1081+
1082+
/// Helper to find a minimal face (smallest cycle) starting from a half-edge
1083+
fn find_minimal_face_from_edge(
1084+
&self,
1085+
start_segment: SegmentId,
1086+
from_point: usize,
1087+
to_point: usize,
1088+
start_reversed: bool,
1089+
adjacency: &HashMap<usize, Vec<(SegmentId, usize, bool)>>,
1090+
used_half_edges: &mut HashSet<(SegmentId, bool)>,
1091+
) -> Option<FoundSubpath> {
1092+
let mut path = vec![HalfEdge::new(start_segment, from_point, to_point, start_reversed)];
1093+
let mut current = to_point;
1094+
let target = from_point;
1095+
let mut prev_segment = start_segment;
1096+
1097+
let mut iteration = 0;
1098+
let max_iterations = adjacency.len() * 2;
1099+
1100+
// Follow the "rightmost" edge at each vertex to find minimal face
1101+
loop {
1102+
iteration += 1;
1103+
1104+
if iteration > max_iterations {
1105+
return None;
1106+
}
1107+
1108+
let neighbors = adjacency.get(&current)?;
1109+
// Find the next edge in counterclockwise order (rightmost turn)
1110+
let prev_direction = self.point_domain.positions()[current] - self.point_domain.positions()[path.last()?.start];
1111+
1112+
let angle_between = |v1: DVec2, v2: DVec2| -> f64 {
1113+
let angle = v2.y.atan2(v2.x) - v1.y.atan2(v1.x);
1114+
if angle < 0.0 { angle + 2.0 * std::f64::consts::PI } else { angle }
1115+
};
1116+
1117+
let next = neighbors.iter().filter(|(seg, _next, _rev)| *seg != prev_segment).min_by(|a, b| {
1118+
let dir_a = self.point_domain.positions()[a.1] - self.point_domain.positions()[current];
1119+
let dir_b = self.point_domain.positions()[b.1] - self.point_domain.positions()[current];
1120+
let angle_a = angle_between(prev_direction, dir_a);
1121+
let angle_b = angle_between(prev_direction, dir_b);
1122+
angle_a.partial_cmp(&angle_b).unwrap_or(std::cmp::Ordering::Equal)
1123+
})?;
1124+
1125+
let (next_segment, next_point, next_reversed) = *next;
1126+
1127+
if next_point == target {
1128+
// Completed the cycle
1129+
path.push(HalfEdge::new(next_segment, current, next_point, next_reversed));
1130+
1131+
// Mark all half-edges as used
1132+
for edge in &path {
1133+
used_half_edges.insert((edge.id, edge.reverse));
1134+
}
1135+
1136+
return Some(FoundSubpath::new(path));
1137+
}
1138+
1139+
// Check if we've created a cycle (might not be back to start)
1140+
if path.iter().any(|e| e.end == next_point && e.id != next_segment) {
1141+
return None;
1142+
}
1143+
1144+
path.push(HalfEdge::new(next_segment, current, next_point, next_reversed));
1145+
prev_segment = next_segment;
1146+
current = next_point;
1147+
1148+
// Prevent infinite loops
1149+
if path.len() > adjacency.len() {
1150+
return None;
1151+
}
1152+
}
1153+
}
9881154
}
9891155

9901156
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]

0 commit comments

Comments
 (0)