Selection feedback and origin: hover and selected highlights (theme tokens), origin planes/axes/point at a fixed screen size with browser eye toggles, click-to-pick sketch planes and faces with camera animation (Finish Sketch returns), multi-select dialog inputs with pre-selection, tangent chains and face-to-edges, window/crossing box selection, shell/draft/mirror/hole dialogs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
059909fb05
commit
97e73b0935
20 files changed
+1907
-518
No files matched your search
@@ -64,6 +64,9 @@ pub struct Mesh {
|
||||
/// Per edge: a seam between two pieces of the same smooth surface (not drawn).
|
||||
#[serde(default)]
|
||||
pub seams: Vec<bool>,
|
||||
/// Per edge: the B-rep faces it bounds (one or two).
|
||||
#[serde(default)]
|
||||
pub edge_faces: Vec<Vec<u32>>,
|
||||
}
|
||||
|
||||
/// Mass properties of a closed mesh.
|
||||
@@ -75,6 +78,53 @@ pub struct MeshMeasure {
|
||||
}
|
||||
|
||||
impl Mesh {
|
||||
/// Edges bounding face `f`.
|
||||
pub fn face_edges(&self, f: u32) -> Vec<usize> {
|
||||
self.edge_faces.iter().enumerate().filter(|(_, fs)| fs.contains(&f)).map(|(i, _)| i).collect()
|
||||
}
|
||||
/// 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> {
|
||||
// (end point, direction leaving the edge there). A polyline's end chord leans inward
|
||||
// on a curve by half the turn to the next chord; extrapolating the two end chords
|
||||
// recovers the curve's end tangent.
|
||||
let end_dir = |p0: Vec3, p1: Vec3, p2: Option<Vec3>| -> Option<Vec3> {
|
||||
let u1 = (p0 - p1).normalized()?;
|
||||
match p2.and_then(|p2| (p1 - p2).normalized()) {
|
||||
Some(u2) => (u1 * 1.5 - u2 * 0.5).normalized(),
|
||||
None => Some(u1),
|
||||
}
|
||||
};
|
||||
let ends = |i: usize| -> Option<[(Vec3, Vec3); 2]> {
|
||||
let p = self.edges.get(i)?;
|
||||
let n = p.len();
|
||||
let (a, a1, a2) = (*p.first()?, *p.get(1)?, p.get(2).copied().filter(|_| n > 2));
|
||||
let (b, b1) = (*p.last()?, *p.get(n.checked_sub(2)?)?);
|
||||
let b2 = n.checked_sub(3).and_then(|k| p.get(k)).copied();
|
||||
Some([(a, end_dir(a, a1, a2)?), (b, end_dir(b, b1, b2)?)])
|
||||
};
|
||||
let size = self.bounds().diagonal().max(1e-9);
|
||||
let tol = size * 1e-7;
|
||||
let cos = max_angle.cos();
|
||||
let mut chain = vec![e];
|
||||
let mut todo = vec![e];
|
||||
while let Some(cur) = todo.pop() {
|
||||
let Some(ce) = ends(cur) else { continue };
|
||||
for (i, _) in self.edges.iter().enumerate() {
|
||||
if chain.contains(&i) || self.seams.get(i).copied().unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
let Some(ne) = ends(i) else { continue };
|
||||
// Smooth: leaving `cur` at a shared end continues into `i` (opposite directions).
|
||||
let smooth = ce.iter().any(|(p, d)| ne.iter().any(|(q, dn)| p.dist(*q) < tol && d.dot(-*dn) > cos));
|
||||
if smooth && chain.len() < 10_000 {
|
||||
chain.push(i);
|
||||
todo.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
chain
|
||||
}
|
||||
pub fn tri(&self, t: &[u32; 3]) -> Option<[Vec3; 3]> {
|
||||
Some([*self.positions.get(t[0] as usize)?, *self.positions.get(t[1] as usize)?, *self.positions.get(t[2] as usize)?])
|
||||
}
|
||||
|
||||
@@ -233,6 +233,44 @@ impl Region2 {
|
||||
pub fn contains(&self, p: Vec2) -> bool {
|
||||
self.outer.contains(p) && !self.holes.iter().any(|h| h.contains(p))
|
||||
}
|
||||
/// Triangles covering the region (for filled display), by slabs between the vertex
|
||||
/// heights: inside each slab the boundary edges don't cross, so consecutive crossings pair
|
||||
/// up into trapezoids.
|
||||
pub fn triangulate(&self, tol: f64) -> Vec<[Vec2; 3]> {
|
||||
let mut edges: Vec<(Vec2, Vec2)> = Vec::new();
|
||||
for l in std::iter::once(&self.outer).chain(&self.holes) {
|
||||
let pl = l.polyline(tol);
|
||||
let n = pl.len();
|
||||
for i in 0..n {
|
||||
let (Some(a), Some(b)) = (pl.get(i), pl.get((i + 1) % n)) else { continue };
|
||||
if (a.y - b.y).abs() > 1e-12 {
|
||||
edges.push(if a.y < b.y { (*a, *b) } else { (*b, *a) });
|
||||
}
|
||||
}
|
||||
}
|
||||
if edges.len() > 20_000 {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut ys: Vec<f64> = edges.iter().flat_map(|(a, b)| [a.y, b.y]).collect();
|
||||
ys.sort_by(f64::total_cmp);
|
||||
ys.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
|
||||
let x_at = |e: &(Vec2, Vec2), y: f64| e.0.x + (y - e.0.y) / (e.1.y - e.0.y) * (e.1.x - e.0.x);
|
||||
let mut out = Vec::new();
|
||||
for w in ys.windows(2) {
|
||||
let (y0, y1) = (w[0], w[1]);
|
||||
let ym = (y0 + y1) * 0.5;
|
||||
let mut span: Vec<&(Vec2, Vec2)> = edges.iter().filter(|e| e.0.y <= ym && e.1.y >= ym).collect();
|
||||
span.sort_by(|a, b| x_at(a, ym).total_cmp(&x_at(b, ym)));
|
||||
for pair in span.chunks(2) {
|
||||
let [l, r] = pair else { continue };
|
||||
let (a, b) = (Vec2::new(x_at(l, y0), y0), Vec2::new(x_at(r, y0), y0));
|
||||
let (c, d) = (Vec2::new(x_at(r, y1), y1), Vec2::new(x_at(l, y1), y1));
|
||||
out.push([a, b, c]);
|
||||
out.push([a, c, d]);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
pub fn centroid(&self) -> Vec2 {
|
||||
let ao = self.outer.signed_area().abs();
|
||||
let mut c = self.outer.centroid() * ao;
|
||||
@@ -250,6 +288,20 @@ impl Region2 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn triangulated_area() {
|
||||
let sq = Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0), Vec2::new(10.0, 8.0), Vec2::new(5.0, 3.0), Vec2::new(0.0, 8.0)]);
|
||||
let r = Region2 { outer: sq, holes: vec![Loop2::circle(Vec2::new(3.0, 1.5), 1.0).reversed()] };
|
||||
let tris = r.triangulate(1e-3);
|
||||
let a: f64 = tris.iter().map(|[a, b, c]| (*b - *a).cross(*c - *a).abs() * 0.5).sum();
|
||||
// The circle is a polyline within the tolerance, so the areas agree to about that.
|
||||
assert!((a - r.area()).abs() < 1e-3 * r.area(), "{a} vs {}", r.area());
|
||||
for [a, b, c] in &tris {
|
||||
let m = (*a + *b + *c) / 3.0;
|
||||
assert!((*b - *a).cross(*c - *a).abs() < 1e-12 || r.contains(m), "{m:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn areas() {
|
||||
let sq = Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(4.0, 0.0), Vec2::new(4.0, 3.0), Vec2::new(0.0, 3.0)]);
|
||||
|
||||
@@ -6,7 +6,7 @@ fn cube_mesh(s: f64) -> Mesh {
|
||||
let triangles =
|
||||
vec![[0, 2, 1], [0, 3, 2], [4, 5, 6], [4, 6, 7], [0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7]];
|
||||
let n = triangles.len();
|
||||
Mesh { normals: vec![Vec3::Z; 8], positions, triangles, tri_face: vec![0; n], edges: vec![], seams: vec![] }
|
||||
Mesh { normals: vec![Vec3::Z; 8], positions, triangles, tri_face: vec![0; n], edges: vec![], seams: vec![], edge_faces: vec![] }
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -62,3 +62,24 @@ fn aabb() {
|
||||
b.add(Vec3::new(-1.0, 0.0, 0.0));
|
||||
assert_eq!(b.size(), Vec3::new(2.0, 2.0, 3.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_chains_follow_smooth_edges() {
|
||||
// A line, then an arc leaving it tangentially, then a sharp corner.
|
||||
let mut m = Mesh { positions: vec![Vec3::ZERO, Vec3::new(30.0, 30.0, 0.0)], ..Default::default() };
|
||||
m.edges.push(vec![Vec3::new(0.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0)]);
|
||||
let arc: Vec<Vec3> = (0..=16)
|
||||
.map(|k| {
|
||||
let a = -std::f64::consts::FRAC_PI_2 + std::f64::consts::FRAC_PI_2 * k as f64 / 16.0;
|
||||
Vec3::new(10.0 + 5.0 * a.cos(), 5.0 + 5.0 * a.sin(), 0.0)
|
||||
})
|
||||
.collect();
|
||||
m.edges.push(arc);
|
||||
m.edges.push(vec![Vec3::new(15.0, 5.0, 0.0), Vec3::new(5.0, 5.0, 0.0)]);
|
||||
m.edge_faces = vec![vec![0], vec![0, 1], vec![1]];
|
||||
let mut c = m.tangent_chain(0, 5f64.to_radians());
|
||||
c.sort();
|
||||
assert_eq!(c, vec![0, 1]);
|
||||
assert_eq!(m.tangent_chain(2, 5f64.to_radians()), vec![2]);
|
||||
assert_eq!(m.face_edges(1), vec![1, 2]);
|
||||
}
|
||||
Reference in new issue
Block a user