fix: select complete circular rims and curved surfaces in 3D
Group kernel pieces across circular rims and hidden surface seams for hovering, selection, dialogs, and measurement. Preserve physical references and cover full-item toggling and picking with regression tests. Fixes #88
This commit is contained in:
1 parent
e042a7b555
commit
acf4669f73
6 files changed
+424
-34
No files matched your search
@@ -82,6 +82,71 @@ impl Mesh {
|
||||
pub fn face_edges(&self, f: u32) -> Vec<usize> {
|
||||
self.edge_faces.iter().enumerate().filter(|(_, fs)| fs.contains(&f)).map(|(i, _)| i).collect()
|
||||
}
|
||||
/// Pieces of one surface joined across hidden seams, including `face` itself.
|
||||
pub fn surface_patch(&self, face: usize) -> Vec<usize> {
|
||||
let mut patch = vec![face];
|
||||
let mut todo = vec![face];
|
||||
while let Some(f) = todo.pop() {
|
||||
for (ei, fs) in self.edge_faces.iter().enumerate() {
|
||||
if !self.seams.get(ei).copied().unwrap_or(false) || !fs.iter().any(|x| *x as usize == f) {
|
||||
continue;
|
||||
}
|
||||
for &next in fs {
|
||||
let next = next as usize;
|
||||
if !patch.contains(&next) {
|
||||
patch.push(next);
|
||||
todo.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
patch.sort_unstable();
|
||||
patch
|
||||
}
|
||||
|
||||
/// A complete circular rim may be split into several kernel edges. Keep its pieces
|
||||
/// together for picking, without joining other tangent curves or changing edge indices.
|
||||
pub fn circular_rim(&self, edge: usize) -> Vec<usize> {
|
||||
let single = vec![edge];
|
||||
let Some(p) = self.edges.get(edge).filter(|p| p.len() >= 3) else { return single };
|
||||
let (Some(&a), Some(&b), Some(&c)) = (p.first(), p.get(p.len() / 2), p.last()) else { return single };
|
||||
let (u, v) = (b - a, c - a);
|
||||
let n = u.cross(v);
|
||||
if !(n.len2() > u.len2() * v.len2() * 1e-12) {
|
||||
return single;
|
||||
}
|
||||
let center = a + (v.cross(n) * u.len2() + n.cross(u) * v.len2()) / (2.0 * n.len2());
|
||||
let radius = center.dist(a);
|
||||
let normal = n.normalized().unwrap_or(Vec3::ZERO);
|
||||
let tol = (radius * 1e-6).max(1e-9);
|
||||
let mut rim = self.tangent_chain(edge, 2f64.to_radians());
|
||||
if rim.len() < 2 || !radius.is_finite() {
|
||||
return single;
|
||||
}
|
||||
// All samples must lie on this circle, and every end must join exactly one other
|
||||
// piece. Open arcs, ellipses, and branching tangent chains stay separate.
|
||||
for &i in &rim {
|
||||
let Some(points) = self.edges.get(i) else { return single };
|
||||
if points.iter().any(|q| !q.is_finite() || (q.dist(center) - radius).abs() > tol || (*q - center).dot(normal).abs() > tol) {
|
||||
return single;
|
||||
}
|
||||
for end in [points.first(), points.last()].into_iter().flatten() {
|
||||
let joins = rim
|
||||
.iter()
|
||||
.filter(|&&j| j != i)
|
||||
.filter_map(|&j| self.edges.get(j))
|
||||
.flat_map(|q| [q.first(), q.last()])
|
||||
.flatten()
|
||||
.filter(|q| q.dist(*end) < tol)
|
||||
.count();
|
||||
if joins != 1 {
|
||||
return single;
|
||||
}
|
||||
}
|
||||
}
|
||||
rim.sort_unstable();
|
||||
rim
|
||||
}
|
||||
/// The edges reached from `e` through vertices where the next edge continues smoothly
|
||||
/// (angle between the end tangents below `max_angle`), including `e` itself.
|
||||
pub fn tangent_chain(&self, e: usize, max_angle: f64) -> Vec<usize> {
|
||||
|
||||
@@ -84,6 +84,46 @@ fn tangent_chains_follow_smooth_edges() {
|
||||
assert_eq!(m.face_edges(1), vec![1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circular_rims_group_only_closed_cocircular_edges() {
|
||||
let arc = |start: f64, sweep: f64, yscale: f64| -> Vec<Vec3> {
|
||||
(0..=32)
|
||||
.map(|i| {
|
||||
let a = start + sweep * i as f64 / 32.0;
|
||||
Vec3::new(10.0 * a.cos(), yscale * 10.0 * a.sin(), 5.0)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let pi = std::f64::consts::PI;
|
||||
let mut m = Mesh {
|
||||
positions: vec![Vec3::new(-10.0, -10.0, 5.0), Vec3::new(10.0, 10.0, 5.0)],
|
||||
edges: vec![arc(0.0, pi, 1.0), arc(2.0 * pi, -pi, 1.0)],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(m.circular_rim(0), vec![0, 1]);
|
||||
assert_eq!(m.circular_rim(1), vec![0, 1]);
|
||||
// A hidden seam cannot form a selectable rim.
|
||||
m.seams = vec![false, true];
|
||||
assert_eq!(m.circular_rim(0), vec![0]);
|
||||
m.seams.clear();
|
||||
m.edges[1] = arc(pi, pi / 2.0, 1.0);
|
||||
assert_eq!(m.circular_rim(0), vec![0]);
|
||||
// A closed, smooth ellipse is not a circle.
|
||||
m.edges = vec![arc(0.0, pi, 0.7), arc(pi, pi, 0.7)];
|
||||
assert_eq!(m.circular_rim(0), vec![0]);
|
||||
assert_eq!(m.circular_rim(99), vec![99]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surface_patches_cross_only_hidden_seams() {
|
||||
let mut m = Mesh { edge_faces: vec![vec![0, 1], vec![1, 2], vec![2, 3]], seams: vec![true, true, false], ..Default::default() };
|
||||
assert_eq!(m.surface_patch(0), vec![0, 1, 2]);
|
||||
assert_eq!(m.surface_patch(2), vec![0, 1, 2]);
|
||||
assert_eq!(m.surface_patch(3), vec![3]);
|
||||
m.seams.clear();
|
||||
assert_eq!(m.surface_patch(0), vec![0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cubic_and_conic_segments_measure_exactly() {
|
||||
use crate::{Loop2, Seg2, Vec2};
|
||||
|
||||
Reference in new issue
Block a user