diff --git a/crates/kernel/src/iges_in.rs b/crates/kernel/src/iges_in.rs index 7a6765b5b8..f9adb7aead 100644 --- a/crates/kernel/src/iges_in.rs +++ b/crates/kernel/src/iges_in.rs @@ -288,7 +288,8 @@ fn parse(text: &str) -> R { /// A model curve segment ready for an edge: STEP curve id, its end points, points a quarter and /// three quarters of the way along and one at the middle (for matching shared edges), and -/// whether the curve runs from `b` to `a`. +/// whether the curve runs from `b` to `a`; and its geometry, for a curve that has to be made to +/// end where its piece meets the next ([`Tr::closed`]). #[derive(Clone, Copy, Debug)] struct Seg { curve: u64, @@ -298,11 +299,28 @@ struct Seg { q3: V3, mid: V3, rev: bool, + shape: Shape, +} + +/// A segment's curve as it can be rebuilt: a line, or the B-spline `Tr::splines[i]`; another +/// curve keeps its ends. +#[derive(Clone, Copy, Debug, PartialEq)] +enum Shape { + Line, + Spline(usize), + Other, } impl Seg { fn along(curve: u64, f: impl Fn(f64) -> V3) -> Seg { - Seg { curve, a: f(0.0), b: f(1.0), q: f(0.25), q3: f(0.75), mid: f(0.5), rev: false } + Seg { curve, a: f(0.0), b: f(1.0), q: f(0.25), q3: f(0.75), mid: f(0.5), rev: false, shape: Shape::Other } + } + fn with(self, shape: Shape) -> Seg { + Seg { shape, ..self } + } + /// Its length, along the points it keeps. + fn length(&self) -> f64 { + norm(sub(self.q, self.a)) + norm(sub(self.mid, self.q)) + norm(sub(self.q3, self.mid)) + norm(sub(self.b, self.q3)) } fn reversed(self) -> Seg { Seg { a: self.b, b: self.a, q: self.q3, q3: self.q, rev: !self.rev, ..self } @@ -343,6 +361,7 @@ fn perp(z: V3) -> V3 { } /// A rational B-spline curve evaluated by de Boor's algorithm. +#[derive(Clone)] struct Nurbs { deg: usize, knots: Vec, @@ -431,8 +450,8 @@ struct Tr<'a> { curve_work: usize, /// Composite curves (102) on the path being expanded. composites: Vec, - /// Farthest a trim loop's end was moved to close a gap (mm; see `close_gaps`). - snapped: f64, + /// The B-splines of segments ([`Shape::Spline`]). + splines: Vec, } fn xf_key(x: &Xf) -> [u64; 12] { @@ -539,7 +558,7 @@ impl<'a> Tr<'a> { let dd = self.o.dir(d); let v = self.o.add(format!("VECTOR('',#{dd},{})", r(norm(sub(b, a))))); let c = self.o.add(format!("LINE('',#{pa},#{v})")); - Ok(vec![Seg::along(c, |t| add(a, mul(sub(b, a), t)))]) + Ok(vec![Seg::along(c, |t| add(a, mul(sub(b, a), t))).with(Shape::Line)]) } 100 => { let Arc { c, rad, a0, a1 } = self.arc(seq)?; @@ -577,7 +596,9 @@ impl<'a> Tr<'a> { nb.eval(t0 + (t1 - t0) * k as f64 / 4.0).ok_or_else(|| format!("entity {seq}: B-spline cannot be evaluated"))?; } let c = self.write_nurbs_curve(&nb); - Ok(vec![Seg::along(c, |t| nb.eval(t0 + (t1 - t0) * t).unwrap_or([0.0; 3]))]) + let seg = Seg::along(c, |t| nb.eval(t0 + (t1 - t0) * t).unwrap_or([0.0; 3])); + self.splines.push(nb); + Ok(vec![seg.with(Shape::Spline(self.splines.len() - 1))]) } 104 => { // Ellipse (form 1) in canonical position: A x² + C y² + F = 0. @@ -970,6 +991,59 @@ impl<'a> Tr<'a> { Nurbs { deg: m2, knots: t_knots, pts: column }.eval(v).ok_or_else(|| "surface cannot be evaluated".into()) } + /// Segment `s` made to run from `pa` to `pb`, the vertices its piece meets the pieces before + /// and after it at. A line is the line between them; a clamped B-spline has its end control + /// points moved onto them (it ends at them exactly, its shape inside changed by no more than + /// the gap). Any other curve keeps its ends, and the gap is reported. + fn closed(&mut self, s: &Seg, pa: V3, pb: V3) -> Seg { + let gap = norm(sub(s.a, pa)).max(norm(sub(s.b, pb))); + if gap <= self.tol { + return *s; + } + match s.shape { + Shape::Line if norm(sub(pb, pa)) > self.tol => { + let (p, d) = (self.o.point(pa), unit(sub(pb, pa)).unwrap_or([1.0, 0.0, 0.0])); + let dd = self.o.dir(d); + let v = self.o.add(format!("VECTOR('',#{dd},{})", r(norm(sub(pb, pa))))); + let c = self.o.add(format!("LINE('',#{p},#{v})")); + return Seg::along(c, |t| add(pa, mul(sub(pb, pa), t))).with(Shape::Line); + } + Shape::Spline(i) => { + if let Some(mut nb) = self.splines.get(i).cloned() { + let (n, p) = (nb.pts.len(), nb.deg); + let clamped = n > p + && nb.knots.len() == n + p + 1 + && nb.knots[..=p].iter().all(|k| *k == nb.knots[0]) + && nb.knots[n..].iter().all(|k| *k == nb.knots[n + p]); + if clamped { + // The curve runs from `a` to `b`, or from `b` to `a` when reversed. + let (start, end) = if s.rev { (pb, pa) } else { (pa, pb) }; + for (k, q) in [(0, start), (n - 1, end)] { + let w = nb.pts[k][3]; + nb.pts[k] = [q[0] * w, q[1] * w, q[2] * w, w]; + } + let (t0, t1) = nb.range(); + let c = self.write_nurbs_curve(&nb); + let seg = Seg::along(c, |t| nb.eval(t0 + (t1 - t0) * t).unwrap_or([0.0; 3])); + self.splines.push(nb); + let seg = seg.with(Shape::Spline(self.splines.len() - 1)); + return if s.rev { seg.reversed() } else { seg }; + } + } + } + _ => {} + } + let near = if norm(sub(s.a, pa)) >= norm(sub(s.b, pb)) { pa } else { pb }; + self.warn(format!( + "trim curve pieces miss each other by {} mm at ({}, {}, {}): the gap is left open within the vertex's tolerance", + r(gap), + r(near[0]), + r(near[1]), + r(near[2]) + )); + *s + } + /// The natural boundary of a B-spline surface (its four edge curves), for trimmed surfaces /// with no outer curve. fn natural_boundary(&mut self, seq: i64, xf: &Xf) -> R> { @@ -1004,7 +1078,8 @@ impl<'a> Tr<'a> { continue; // a pole } let c = self.write_nurbs_curve(&nb); - let seg = Seg::along(c, |t| ev(t).unwrap_or([0.0; 3])); + let seg = Seg::along(c, |t| ev(t).unwrap_or([0.0; 3])).with(Shape::Spline(self.splines.len())); + self.splines.push(nb.clone()); out.push(if rev { seg.reversed() } else { seg }); } Ok(out) @@ -1075,6 +1150,10 @@ impl Sewer { /// the segment runs along it. fn edge(&mut self, s: &Seg) -> (usize, bool) { let (a, b) = (self.vertex(s.a), self.vertex(s.b)); + self.edge_between(s, a, b) + } + /// [`Sewer::edge`] for a segment from vertex `a` to vertex `b`. + fn edge_between(&mut self, s: &Seg, a: usize, b: usize) -> (usize, bool) { let key = (a.min(b), a.max(b)); let close = |x: V3, y: V3| norm(sub(x, y)) <= self.tol * 10.0 + 1e-9; if let Some(list) = self.by_ends.get(&key) { @@ -1253,13 +1332,12 @@ fn translate(text: &str) -> std::result::Result<(String, Vec), String> { surf_cache: HashMap::new(), curve_work: 0, composites: Vec::new(), - snapped: 0.0, + splines: Vec::new(), }; // 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('',''))" @@ -1305,13 +1383,6 @@ fn translate(text: &str) -> std::result::Result<(String, Vec), 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})")); @@ -1366,11 +1437,7 @@ fn sew_surfaces( 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 }); - } + faces.extend(sew(t, sw, drafts)); if faces.is_empty() { return Ok(Vec::new()); } @@ -1441,75 +1508,97 @@ 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 = Vec::new(); - let root = |parent: &[usize], mut i: usize| { - while let Some(&p) = parent.get(i).filter(|p| **p != i) { +/// Faces read from trimmed surfaces, sewn by their shared edges. A boundary loop is one closed +/// curve: each piece ends where the next one starts (IGES 5.3, 4.24 composite curve, and +/// 4.34/4.35 a trimmed surface's boundaries are closed curves). 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. Each meeting +/// point of two pieces is then still one vertex, unless the gap is wider than `MAX_TRIM_GAP` or +/// not small next to its pieces (`MAX_TRIM_GAP_SHARE`): past that the ends may be different +/// corners of the model, and the gap stays open. The vertex goes where most of the piece ends +/// meeting there agree (the neighbouring faces at that corner), and a piece that misses it is +/// made to end there ([`Tr::closed`]): the edges then meet at their vertices exactly, as the +/// file means them to, and the sewing tolerance stays the file's. +fn sew(t: &mut Tr, sw: &mut Sewer, faces: Vec) -> Vec { + // Every piece's end vertices, then the vertices each meeting point joins made one. + let ends: Vec>> = + faces.iter().map(|f| f.loops.iter().map(|l| l.iter().map(|s| [sw.vertex(s.a), sw.vertex(s.b)]).collect()).collect()).collect(); + let mut same: Vec = (0..sw.verts.len()).collect(); + fn root(same: &mut [usize], mut i: usize) -> usize { + while let Some(&p) = same.get(i) { + if p == i { + break; + } + let up = same.get(p).copied().unwrap_or(p); + if let Some(slot) = same.get_mut(i) { + *slot = up; + } 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 = 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 = sums.into_iter().map(|(r, (s, n))| (r, mul(s, 1.0 / n as f64))).collect(); - let mut to: Vec> = 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; + for (f, fe) in faces.iter().zip(&ends) { + for (l, le) in f.loops.iter().zip(fe) { + for (k, (prev, e)) in l.iter().zip(le).enumerate() { + let (Some(next), Some(next_ends)) = (l.get((k + 1) % l.len()), le.get((k + 1) % le.len())) else { continue }; + let gap = norm(sub(next.a, prev.b)); + if gap > MAX_TRIM_GAP || gap > MAX_TRIM_GAP_SHARE * prev.length().min(next.length()) { + continue; + } + let (a, b) = (root(&mut same, e[1]), root(&mut same, next_ends[0])); + if let Some(slot) = same.get_mut(a.max(b)) { + *slot = a.min(b); } } } } - 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; + // The piece ends at each joined vertex, and the vertex where most of them are. + let mut at: HashMap> = HashMap::new(); + for (f, fe) in faces.iter().zip(&ends) { + for (l, le) in f.loops.iter().zip(fe) { + for (s, [a, b]) in l.iter().zip(le) { + at.entry(root(&mut same, *a)).or_default().push(s.a); + at.entry(root(&mut same, *b)).or_default().push(s.b); + } } - }; - 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); + for (v, pts) in at { + let agree = |p: V3| pts.iter().filter(|q| norm(sub(**q, p)) <= sw.tol).count(); + let Some(here) = sw.verts.get(v).copied() else { continue }; + let mut best = (agree(here), here); + for p in &pts { + let n = agree(*p); + if n > best.0 { + best = (n, *p); + } + } + if let Some(slot) = sw.verts.get_mut(v) { + *slot = best.1; + } + } + faces + .into_iter() + .zip(ends) + .map(|(f, ends)| { + let loops = f + .loops + .iter() + .zip(ends) + .map(|(l, e)| { + l.iter() + .zip(e) + .map(|(s, [a, b])| { + let (a, b) = (root(&mut same, a), root(&mut same, b)); + let (pa, pb) = (sw.verts.get(a).copied().unwrap_or(s.a), sw.verts.get(b).copied().unwrap_or(s.b)); + let s = t.closed(s, pa, pb); + sw.edge_between(&s, a, b) + }) + .collect() + }) + .collect(); + FaceB { surface: f.surface, sense: true, loops, color: f.color } + }) + .collect() } /// A trimmed (144) or bounded (143) surface, or a bare B-spline surface, as a face. @@ -1588,10 +1677,16 @@ fn cos_curve(t: &mut Tr, seq: i64, xf: &Xf, surface: i64) -> R> { } /// Pieces of a composite curve turned so that each starts where the one before ends, the first -/// at the end nearer `start` (when known). A closed piece (its ends within `tol`, the sewing -/// tolerance that decides they are one vertex) keeps its direction. +/// at the end nearer `start` (when known), else at its end away from the second piece. A closed +/// piece (its ends within `tol`, the sewing tolerance that decides they are one vertex) keeps +/// its direction. fn head_to_tail(segs: Vec, start: Option, tol: f64) -> Vec { - let mut prev = start; + let from_second = || { + let (f, n) = (segs.first()?, segs.get(1)?); + let to_second = |p: V3| norm(sub(p, n.a)).min(norm(sub(p, n.b))); + Some(if to_second(f.a) < to_second(f.b) { f.b } else { f.a }) + }; + let mut prev = start.or_else(from_second); segs.into_iter() .map(|s| { let s = match prev {