mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
tess: an edge loop of fewer than three segments is split until it encloses an area
Each edge is sampled on its own, and a short one within the chord tolerance is one segment. A face bounded by two such edges (a lens of short arcs, a sliver strip of a cylinder between two rulings) had two chords on one line: "degenerate boundary", and the whole body's display mesh failed with it (one of Poppy Humanoid's bodies lost all 336 faces to one sliver). Such a loop's segments are now halved until it has three; the new nodes are on the edges' curves and shared by every face that runs them. Test: a lens of two radius-1000 arcs beside a 2 m face meshes; without the split it fails with the same error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LUXeKMoxpM7Vuv8Rb6E3K2
This commit is contained in:
@@ -182,6 +182,7 @@ pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result
|
||||
.into_iter()
|
||||
.collect::<Result<_>>()?;
|
||||
relax_crowded_faces(b, &jobs, &rules, &mut nodes)?;
|
||||
split_thin_loops(b, &mut nodes)?;
|
||||
let indexed: Vec<usize> = (0..faces.len()).collect();
|
||||
let face_boundary = |fi: usize, nodes: &[edges::Nodes]| {
|
||||
let (Some(face), Some(eval), Some(map)) = (faces.get(fi), evals.get(fi), params.get(fi)) else {
|
||||
@@ -412,6 +413,39 @@ fn untangle(
|
||||
/// as often as it takes, down to the tolerance alone (a plate with a thousand small holes is
|
||||
/// not worth a hundred times the work to draw each hole round). An edge keeps one set of nodes
|
||||
/// for both its faces, at the coarser of their levels, so every rim of a crowded face gets the
|
||||
/// Segments an edge loop needs to enclose an area in its face's (u, v).
|
||||
const MIN_LOOP_SEGMENTS: usize = 3;
|
||||
|
||||
/// Every edge loop with fewer than [`MIN_LOOP_SEGMENTS`] segments has its edges' segments halved
|
||||
/// until it has them: two short arcs (a lens), each within the chord tolerance and so one
|
||||
/// segment, would otherwise be two chords on one line, a boundary of no area. The new nodes are
|
||||
/// on the edges' curves and shared by every face that runs them, as all edge nodes are.
|
||||
fn split_thin_loops(b: &brep::Brep, nodes: &mut [edges::Nodes]) -> Result<()> {
|
||||
for f in &b.faces {
|
||||
for l in &f.loops {
|
||||
let brep::Loop::Edges { edges: uses } = l else { continue };
|
||||
let mut es: Vec<usize> = uses.iter().map(|u| u.edge as usize).collect();
|
||||
es.sort_unstable();
|
||||
es.dedup();
|
||||
let count = |nodes: &[edges::Nodes]| -> usize {
|
||||
uses.iter().map(|u| nodes.get(u.edge as usize).map_or(0, |n| n.points.len().saturating_sub(1))).sum()
|
||||
};
|
||||
// Each round at least doubles every edge's segments: a few rounds always do.
|
||||
for _ in 0..MIN_LOOP_SEGMENTS {
|
||||
if count(nodes) >= MIN_LOOP_SEGMENTS {
|
||||
break;
|
||||
}
|
||||
for &e in &es {
|
||||
let Some(slot) = nodes.get_mut(e) else { continue };
|
||||
let segs: Vec<u32> = (0..slot.points.len().saturating_sub(1)).filter_map(|k| u32::try_from(k).ok()).collect();
|
||||
edges::split_segments(b, e, slot, &segs)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// same share whatever the order of the faces.
|
||||
fn relax_crowded_faces(b: &brep::Brep, jobs: &[(usize, f64)], rules: &size::Rules, nodes: &mut [edges::Nodes]) -> Result<()> {
|
||||
let Some(turn) = rules.turn else { return Ok(()) };
|
||||
|
||||
@@ -750,3 +750,44 @@ fn a_face_filled_leniently_is_named_in_the_stats() {
|
||||
assert_eq!(lenient.face, 7);
|
||||
assert!(lenient.reason.contains("face 7 (spline)"), "{}", lenient.reason);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_face_bounded_by_two_short_arcs_keeps_an_area_to_mesh() {
|
||||
// A lens on z = 0 between two radius-1000 circles crossing at (±10, 0), in a record whose
|
||||
// other face, a 2 m square, sets the segment length: each arc sags 0.05 mm off its chord,
|
||||
// inside a 0.5 mm tolerance, so each samples as one segment and the two chords lie on one
|
||||
// line, a boundary of no area. Its loop's segments are split until it encloses one.
|
||||
use crate::sew::{PartEdge, PartFace, PartUse, Parts, assemble};
|
||||
use solidcraft_geom::brep::{Curve, Frame, Surface};
|
||||
use std::f64::consts::{PI, TAU};
|
||||
let (r, half) = (1000.0f64, 10.0f64);
|
||||
let c = (r * r - half * half).sqrt();
|
||||
let circle = |cy: f64| Curve::Circle { frame: Frame { origin: [0.0, cy, 0.0], x_axis: [1.0, 0.0, 0.0], y_axis: [0.0, 1.0, 0.0] }, radius: r };
|
||||
let top = c.atan2(half);
|
||||
let line = |a: [f64; 3], z: [f64; 3]| Curve::Line { start: a, end: z };
|
||||
let sq = [[3000.0, 0.0, 0.0], [5000.0, 0.0, 0.0], [5000.0, 2000.0, 0.0], [3000.0, 2000.0, 0.0]];
|
||||
let plane = Surface::Plane { origin: [0.0; 3], u_axis: [1.0, 0.0, 0.0], v_axis: [0.0, 1.0, 0.0] };
|
||||
let mut edges = vec![
|
||||
PartEdge { curve: circle(-c), interval: [top, PI - top], vertices: [0, 1] },
|
||||
PartEdge { curve: circle(c), interval: [PI + top, TAU - top], vertices: [1, 0] },
|
||||
];
|
||||
for k in 0..4 {
|
||||
edges.push(PartEdge { curve: line(sq[k], sq[(k + 1) % 4]), interval: [0.0, 1.0], vertices: [2 + k as u32, 2 + ((k + 1) % 4) as u32] });
|
||||
}
|
||||
let uses = |es: &[u32]| es.iter().map(|&edge| PartUse { edge, forward: true, given: None }).collect::<Vec<_>>();
|
||||
let mut vertices = vec![[half, 0.0, 0.0], [-half, 0.0, 0.0]];
|
||||
vertices.extend(sq);
|
||||
let parts = Parts {
|
||||
vertices,
|
||||
edges,
|
||||
faces: vec![
|
||||
PartFace { surface: plane.clone(), forward: true, loops: vec![uses(&[0, 1])] },
|
||||
PartFace { surface: plane, forward: true, loops: vec![uses(&[2, 3, 4, 5])] },
|
||||
],
|
||||
tolerance: 1e-6,
|
||||
};
|
||||
let made = ok(assemble(parts, false));
|
||||
let (plan, _) = ok(plan(&made.brep, 0.5, None));
|
||||
let lens = made.faces.first().and_then(|&f| plan.faces.get(f)).expect("the lens's plan");
|
||||
assert!(!lens.triangles.is_empty(), "the lens has triangles");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user