mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Consecutive pieces of a trim loop can miss each other by micrometres to a tenth of a millimetre, past the file's resolution, so the loop did not close and the face was dropped. Faces are now read first and sewn after: each small gap's ends, and the other piece ends at them, move to one vertex. Gaps over 0.1 mm or not small next to their pieces stay open; the sewing tolerance stays the file's. The stated accuracy covers how far a vertex moved, so the STEP reader keeps those edges. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+135
-19
@@ -429,6 +429,8 @@ struct Tr<'a> {
|
||||
curve_work: usize,
|
||||
/// Composite curves (102) on the path being expanded.
|
||||
composites: Vec<i64>,
|
||||
/// Farthest a trim loop's end was moved to close a gap (mm; see `close_gaps`).
|
||||
snapped: f64,
|
||||
}
|
||||
|
||||
fn xf_key(x: &Xf) -> [u64; 12] {
|
||||
@@ -1034,24 +1036,35 @@ struct Sewer {
|
||||
}
|
||||
|
||||
impl Sewer {
|
||||
fn vertex(&mut self, p: V3) -> usize {
|
||||
fn key(&self, p: V3) -> [i64; 3] {
|
||||
// Neighbouring buckets are this key plus or minus one, so leave room for that: a
|
||||
// coordinate near f64's range saturates the cast and the addition would overflow.
|
||||
let c = |x: f64| (x / self.tol).floor().clamp(-9e15, 9e15) as i64;
|
||||
let key = [c(p[0]), c(p[1]), c(p[2])];
|
||||
[c(p[0]), c(p[1]), c(p[2])]
|
||||
}
|
||||
/// The vertex within the tolerance of `p`, if there is one.
|
||||
fn find(&self, p: V3) -> Option<usize> {
|
||||
let key = self.key(p);
|
||||
for dx in -1..=1 {
|
||||
for dy in -1..=1 {
|
||||
for dz in -1..=1 {
|
||||
if let Some(list) = self.grid.get(&[key[0] + dx, key[1] + dy, key[2] + dz]) {
|
||||
for &i in list {
|
||||
if self.verts.get(i).is_some_and(|q| norm(sub(*q, p)) <= self.tol) {
|
||||
return i;
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
fn vertex(&mut self, p: V3) -> usize {
|
||||
if let Some(i) = self.find(p) {
|
||||
return i;
|
||||
}
|
||||
let key = self.key(p);
|
||||
self.verts.push(p);
|
||||
self.grid.entry(key).or_default().push(self.verts.len() - 1);
|
||||
self.verts.len() - 1
|
||||
@@ -1227,12 +1240,21 @@ fn translate(text: &str) -> std::result::Result<(String, Vec<String>), String> {
|
||||
}
|
||||
let f = parse(text)?;
|
||||
let tol = (f.resolution * f.unit).clamp(1e-6, 0.05).max(1e-5);
|
||||
let mut t =
|
||||
Tr { f: &f, o: Out { lines: Vec::new() }, warnings: Vec::new(), tol, surf_cache: HashMap::new(), curve_work: 0, composites: Vec::new() };
|
||||
let mut t = Tr {
|
||||
f: &f,
|
||||
o: Out { lines: Vec::new() },
|
||||
warnings: Vec::new(),
|
||||
tol,
|
||||
surf_cache: HashMap::new(),
|
||||
curve_work: 0,
|
||||
composites: Vec::new(),
|
||||
snapped: 0.0,
|
||||
};
|
||||
// Header: units and context.
|
||||
let mm = t.o.add("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))");
|
||||
let rad = t.o.add("(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.))");
|
||||
let sr = t.o.add("(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT())");
|
||||
let unc_line = t.o.lines.len();
|
||||
let unc = t.o.add(format!("UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE({}),#{mm},'distance_accuracy_value','')", r(tol)));
|
||||
let ctx = t.o.add(format!(
|
||||
"(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{unc}))GLOBAL_UNIT_ASSIGNED_CONTEXT((#{mm},#{rad},#{sr}))REPRESENTATION_CONTEXT('',''))"
|
||||
@@ -1278,6 +1300,13 @@ fn translate(text: &str) -> std::result::Result<(String, Vec<String>), String> {
|
||||
}
|
||||
}
|
||||
assembly.write(&mut t, &seqs, &solids, &mut styles, ctx)?;
|
||||
if t.snapped > 0.0 {
|
||||
// The STEP reader takes vertices up to ten times the stated accuracy off their curves:
|
||||
// state enough to cover the ends `close_gaps` moved, and no more.
|
||||
if let Some(l) = t.o.lines.get_mut(unc_line) {
|
||||
*l = format!("UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE({}),#{mm},'distance_accuracy_value','')", r(tol.max(t.snapped * 0.2)));
|
||||
}
|
||||
}
|
||||
for (item, c) in styles {
|
||||
let rgb = t.o.add(format!("COLOUR_RGB('',{},{},{})", r(c[0] as f64), r(c[1] as f64), r(c[2] as f64)));
|
||||
let fasc = t.o.add(format!("FILL_AREA_STYLE_COLOUR('',#{rgb})"));
|
||||
@@ -1313,15 +1342,21 @@ fn sew_surfaces(
|
||||
sw.edges.clear();
|
||||
sw.by_ends.clear();
|
||||
let watch = crate::watch::current();
|
||||
let mut drafts = Vec::new();
|
||||
for seq in seqs {
|
||||
if watch.as_ref().is_some_and(|w| w.cancelled()) {
|
||||
return Err("IGES import cancelled".into());
|
||||
}
|
||||
match trimmed(t, sw, seq as i64) {
|
||||
Ok(face) => faces.push(face),
|
||||
match trimmed(t, sw.tol, seq as i64) {
|
||||
Ok(face) => drafts.push(face),
|
||||
Err(e) => t.warn(format!("surface {seq}: not read: {e}")),
|
||||
}
|
||||
}
|
||||
close_gaps(t, &mut drafts, sw.tol);
|
||||
for d in drafts {
|
||||
let loops = d.loops.iter().map(|l| l.iter().map(|s| sw.edge(s)).collect()).collect();
|
||||
faces.push(FaceB { surface: d.surface, sense: true, loops, color: d.color });
|
||||
}
|
||||
if faces.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -1336,15 +1371,96 @@ fn sew_surfaces(
|
||||
}))
|
||||
}
|
||||
|
||||
/// A face read from a trimmed or bounded surface, its loops not yet sewn.
|
||||
struct Draft {
|
||||
surface: u64,
|
||||
loops: Vec<Vec<Seg>>,
|
||||
color: Option<[f32; 3]>,
|
||||
}
|
||||
|
||||
/// Gaps between the pieces of a trim loop wider than this (mm) are left open: past it, the ends
|
||||
/// may be different corners of the model rather than one corner written loosely.
|
||||
const MAX_TRIM_GAP: f64 = 0.1;
|
||||
/// ...and a gap must also be at most this share of the shorter piece on either side.
|
||||
const MAX_TRIM_GAP_SHARE: f64 = 0.05;
|
||||
|
||||
/// Close the small gaps between consecutive pieces of the faces' trim loops. Some exporters
|
||||
/// write each piece's model-space curve separately, and consecutive pieces can then miss by a
|
||||
/// few micrometres to a tenth of a millimetre, well past the file's stated resolution: the loop
|
||||
/// does not close, its face is lost and the shell stays open. The two ends of each such gap, and
|
||||
/// every other piece end at either of them (the neighbouring faces at that corner), move to
|
||||
/// their mean, so all the faces there sew to one vertex. A gap wider than `MAX_TRIM_GAP`, or not
|
||||
/// small next to its pieces, is left open; the sewing tolerance stays the file's.
|
||||
fn close_gaps(t: &mut Tr, faces: &mut [Draft], tol: f64) {
|
||||
let length = |s: &Seg| norm(sub(s.q, s.a)) + norm(sub(s.mid, s.q)) + norm(sub(s.q3, s.mid)) + norm(sub(s.b, s.q3));
|
||||
// The gaps' ends as vertices, joined into groups (union-find).
|
||||
let mut ends = Sewer { tol, verts: Vec::new(), grid: HashMap::new(), edges: Vec::new(), by_ends: HashMap::new() };
|
||||
let mut parent: Vec<usize> = Vec::new();
|
||||
let root = |parent: &[usize], mut i: usize| {
|
||||
while let Some(&p) = parent.get(i).filter(|p| **p != i) {
|
||||
i = p;
|
||||
}
|
||||
i
|
||||
};
|
||||
for l in faces.iter().flat_map(|f| f.loops.iter()) {
|
||||
for (i, prev) in l.iter().enumerate() {
|
||||
let Some(next) = l.get((i + 1) % l.len()) else { continue };
|
||||
let gap = norm(sub(next.a, prev.b));
|
||||
if gap <= tol || gap > MAX_TRIM_GAP || gap > MAX_TRIM_GAP_SHARE * length(prev).min(length(next)) {
|
||||
continue;
|
||||
}
|
||||
let (a, b) = (ends.vertex(prev.b), ends.vertex(next.a));
|
||||
while parent.len() < ends.verts.len() {
|
||||
parent.push(parent.len());
|
||||
}
|
||||
let (ra, rb) = (root(&parent, a), root(&parent, b));
|
||||
if let Some(x) = parent.get_mut(ra) {
|
||||
*x = rb;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ends.verts.is_empty() {
|
||||
return;
|
||||
}
|
||||
// Each group's mean; a group that spreads wider than a gap may be is left alone.
|
||||
let mut sums: HashMap<usize, (V3, usize)> = HashMap::new();
|
||||
for (i, p) in ends.verts.iter().enumerate() {
|
||||
let e = sums.entry(root(&parent, i)).or_insert(([0.0; 3], 0));
|
||||
e.0 = add(e.0, *p);
|
||||
e.1 += 1;
|
||||
}
|
||||
let mean: HashMap<usize, V3> = sums.into_iter().map(|(r, (s, n))| (r, mul(s, 1.0 / n as f64))).collect();
|
||||
let mut to: Vec<Option<V3>> = ends.verts.iter().enumerate().map(|(i, _)| mean.get(&root(&parent, i)).copied()).collect();
|
||||
for (i, p) in ends.verts.iter().enumerate() {
|
||||
if to.get(i).copied().flatten().is_some_and(|m| norm(sub(m, *p)) > MAX_TRIM_GAP / 2.0) {
|
||||
let r = root(&parent, i);
|
||||
for (k, x) in to.iter_mut().enumerate() {
|
||||
if root(&parent, k) == r {
|
||||
*x = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut moved = 0.0f64;
|
||||
let mut snap = |p: &mut V3| {
|
||||
if let Some(m) = ends.find(*p).and_then(|i| to.get(i).copied().flatten()) {
|
||||
moved = moved.max(norm(sub(m, *p)));
|
||||
*p = m;
|
||||
}
|
||||
};
|
||||
for s in faces.iter_mut().flat_map(|f| f.loops.iter_mut()).flat_map(|l| l.iter_mut()) {
|
||||
snap(&mut s.a);
|
||||
snap(&mut s.b);
|
||||
}
|
||||
t.snapped = t.snapped.max(moved);
|
||||
}
|
||||
|
||||
/// A trimmed (144) or bounded (143) surface, or a bare B-spline surface, as a face.
|
||||
fn trimmed(t: &mut Tr, sw: &mut Sewer, seq: i64) -> R<FaceB> {
|
||||
fn trimmed(t: &mut Tr, tol: f64, seq: i64) -> R<Draft> {
|
||||
let de = t.de(seq)?;
|
||||
let xf = t.own_xf(seq, 0)?;
|
||||
let color = color_of(t, seq);
|
||||
let loop_of = |_t: &mut Tr, sw: &mut Sewer, segs: Vec<Seg>| -> Vec<(usize, bool)> {
|
||||
let tol = sw.tol;
|
||||
segs.iter().filter(|s| norm(sub(s.a, s.b)) > tol || norm(sub(s.a, s.mid)) > tol).map(|s| sw.edge(s)).collect()
|
||||
};
|
||||
let loop_of = |segs: Vec<Seg>| -> Vec<Seg> { segs.into_iter().filter(|s| norm(sub(s.a, s.b)) > tol || norm(sub(s.a, s.mid)) > tol).collect() };
|
||||
match de.typ {
|
||||
144 => {
|
||||
let surf_seq = t.i(seq, 1)?;
|
||||
@@ -1353,16 +1469,16 @@ fn trimmed(t: &mut Tr, sw: &mut Sewer, seq: i64) -> R<FaceB> {
|
||||
let n2 = t.i(seq, 3)?.clamp(0, MAX_LIST as i64) as usize;
|
||||
let mut loops = Vec::new();
|
||||
let outer = if n1 == 0 { t.natural_boundary(surf_seq, &xf)? } else { cos_curve(t, t.i(seq, 4)?, &xf, surf_seq)? };
|
||||
loops.push(loop_of(t, sw, outer));
|
||||
loops.push(loop_of(outer));
|
||||
for k in 0..n2 {
|
||||
let segs = cos_curve(t, t.i(seq, 5 + k)?, &xf, surf_seq)?;
|
||||
loops.push(loop_of(t, sw, segs));
|
||||
loops.push(loop_of(segs));
|
||||
}
|
||||
loops.retain(|l| !l.is_empty());
|
||||
if loops.is_empty() {
|
||||
return Err("no boundary".into());
|
||||
}
|
||||
Ok(FaceB { surface, sense: true, loops, color })
|
||||
Ok(Draft { surface, loops, color })
|
||||
}
|
||||
143 => {
|
||||
let surf_seq = t.i(seq, 2)?;
|
||||
@@ -1372,19 +1488,19 @@ fn trimmed(t: &mut Tr, sw: &mut Sewer, seq: i64) -> R<FaceB> {
|
||||
for k in 0..n {
|
||||
let b = t.i(seq, 4 + k)?;
|
||||
let segs = boundary_141(t, b, &xf)?;
|
||||
loops.push(loop_of(t, sw, segs));
|
||||
loops.push(loop_of(segs));
|
||||
}
|
||||
loops.retain(|l| !l.is_empty());
|
||||
if loops.is_empty() {
|
||||
return Err("no boundary".into());
|
||||
}
|
||||
Ok(FaceB { surface, sense: true, loops, color })
|
||||
Ok(Draft { surface, loops, color })
|
||||
}
|
||||
_ => {
|
||||
let surface = t.surface(seq, &Xf::ID)?;
|
||||
let outer = t.natural_boundary(seq, &Xf::ID)?;
|
||||
let l = loop_of(t, sw, outer);
|
||||
Ok(FaceB { surface, sense: true, loops: vec![l], color })
|
||||
let l = loop_of(outer);
|
||||
Ok(Draft { surface, loops: vec![l], color })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +299,30 @@ fn trimmed_surfaces_sew_into_a_solid() {
|
||||
assert!(rel(m.volume, 6000.0) < 1e-6 && rel(m.area, 2200.0) < 1e-6, "{} {}", m.volume, m.area);
|
||||
}
|
||||
|
||||
/// Trim pieces whose ends miss each other by a hundredth of a millimetre, far past the file's
|
||||
/// stated resolution: the loop is closed and the box sews into a solid. A gap of half a
|
||||
/// millimetre is no corner written loosely, and stays open.
|
||||
#[test]
|
||||
fn trim_loops_with_small_gaps_still_close() {
|
||||
let with_gap = |gap: f64| {
|
||||
let mut ents = Vec::new();
|
||||
trimmed_box(&mut ents, 0.0);
|
||||
// The edge from (0, 0, 0) towards (0, 20, 0) stops `gap` short of that corner, in both
|
||||
// faces that run along it (the first face's first line, the fifth face's last).
|
||||
ents[1].2 = format!("0.,0.,0.,0.,{:?},0.", 20.0 - gap);
|
||||
ents[36].2 = format!("0.,{:?},0.,0.,0.,0.", 20.0 - gap);
|
||||
iges_import(&iges_file(&ents))
|
||||
};
|
||||
let imp = with_gap(0.01).unwrap();
|
||||
assert!(imp.warnings.is_empty(), "{:?}", imp.warnings);
|
||||
assert!(imp.bodies[0].closed);
|
||||
let m = measure(&imp.bodies[0].body).unwrap();
|
||||
assert!(rel(m.volume, 6000.0) < 1e-3 && rel(m.area, 2200.0) < 1e-3, "{} {}", m.volume, m.area);
|
||||
if let Ok(imp) = with_gap(0.5) {
|
||||
assert!(imp.bodies.iter().all(|b| !b.closed), "a 0.5 mm gap was closed");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn surfaces_in_separate_groups_remain_separate_parts() {
|
||||
let mut ents = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user