mesh: faster tet improvement (#84)

Profile-driven (thin-ribs: 76% of the improvement in the topological
operations, most of it in face removal walking whole edge rings to find
one neighbour; pinhole-block: 44% in the smoothing passes).

Same results, less work:
- face removal finds the sandwiched face across a ring edge through the
  tetrahedron it already holds (O(1) instead of an edge ring), keeps the
  candidate tetrahedra of ring edges between rounds and updates the
  removed worst incrementally;
- Klincsek's table for edge removal starts at the quality to beat, so
  triangles that cannot help are never evaluated;
- a change clock per vertex: a topological operation (per tetrahedron,
  per edge, per face and side) or a smoothing that failed is not tried
  again until a tetrahedron it reads changes (exact: the operations are
  functions of that region; checked by re-running them);
- the quality of a live tetrahedron is cached per slot until one of its
  vertices is touched;
- smoothing line searches stop at the first tetrahedron that does not
  beat the worst, and gradients are computed only near the worst;
- the journal saves the free list's tail instead of the whole list.

For the clock to be exact, the quality is the same bits for every
listing of a tetrahedron with the same orientation (corners in a
canonical even order), and a closed edge ring starts at its smallest
vertex.

Changed results:
- smoothing uses the analytic gradient of the smallest dihedral sine
  (Klingner and Shewchuk 2007 §3.1; Freitag and Ollivier-Gooch 1997)
  instead of central differences (kept for flat or inverted
  tetrahedra);
- the composite insertion runs its topological passes on the new
  vertex's tetrahedra below the threshold, and smooths the neighbours
  whose star has one, as the passes do.

Tests: analytic gradient against central differences, quality the same
for every even reordering, cached qualities equal fresh ones after
improvement, rollbacks restore every slot; the washer fingerprint moves
(the same on arm64 and x86-64).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 committed 2026-10-16 22:11:53 -04:00
1 parent 4329630943
commit e4290c7366
10 files changed
+587 -106

No files matched your search

+168 -6
View File
@@ -170,7 +170,9 @@ pub enum Step {
pub struct Raw { pub struct Raw {
slots: Vec<(u32, [u32; 4], [u32; 4], [u32; 4])>, slots: Vec<(u32, [u32; 4], [u32; 4], [u32; 4])>,
tets_len: usize, tets_len: usize,
free: Vec<u32>, /// The free list's length, and its last entries (those the change may take).
free_len: usize,
free_tail: Vec<u32>,
vtets: Vec<(u32, Vec<u32>)>, vtets: Vec<(u32, Vec<u32>)>,
live: usize, live: usize,
} }
@@ -199,6 +201,21 @@ pub struct TetComplex {
frozen: Vec<u32>, frozen: Vec<u32>,
mark: Vec<u32>, mark: Vec<u32>,
epoch: u32, epoch: u32,
/// Change clock: `touched[v]` is the clock of the last change to a tetrahedron of `v`'s star
/// (made, removed, reshaped or restored), `failed[t]` the clock at which every topological
/// operation on slot `t` last failed ([`Self::known_to_fail`]).
clock: u64,
touched: Vec<u64>,
failed: Vec<u64>,
/// The clock at which removing an edge `(a, b)` (sorted), or a face from apex `a`'s side to apex `b` (sorted face)
/// last failed: the operation reads only the tetrahedra holding both apexes.
failed_edges: IdMap<(u32, u32), u64>,
failed_faces: IdMap<(u32, u32, [u32; 3]), u64>,
/// Per slot: the quality ([`super::measure::quality`]) and one more than the clock it was
/// computed at; current while no vertex of the tetrahedron was touched since.
quality_cache: std::cell::RefCell<Vec<(u64, f64)>>,
/// The clock at which smoothing a vertex last found no move.
failed_smoothing: Vec<u64>,
live: usize, live: usize,
max_tets: usize, max_tets: usize,
max_points: usize, max_points: usize,
@@ -281,6 +298,13 @@ impl TetComplex {
frozen: Vec::new(), frozen: Vec::new(),
mark: vec![0; tets.len()], mark: vec![0; tets.len()],
epoch: 0, epoch: 0,
clock: 0,
touched: vec![0; points.len()],
failed: Vec::new(),
failed_edges: IdMap::default(),
failed_faces: IdMap::default(),
failed_smoothing: Vec::new(),
quality_cache: std::cell::RefCell::new(Vec::new()),
live: tets.len(), live: tets.len(),
max_tets, max_tets,
max_points, max_points,
@@ -355,6 +379,125 @@ impl TetComplex {
if let Some(s) = self.kinds.get_mut(v as usize) { if let Some(s) = self.kinds.get_mut(v as usize) {
*s = k; *s = k;
} }
self.touch_star(v);
}
// ---- change clock ----
fn touch(&mut self, vs: &[u32]) {
self.clock += 1;
for &v in vs {
if let Some(s) = self.touched.get_mut(v as usize) {
*s = self.clock;
}
}
}
/// Touch `v` and every vertex of its star (moving `v` reshapes those tetrahedra).
fn touch_star(&mut self, v: u32) {
let mut vs = vec![v];
for &t in self.vtets.get(v as usize).map(Vec::as_slice).unwrap_or_default() {
vs.extend(self.tets.get(t as usize).copied().unwrap_or([NONE; 4]));
}
self.touch(&vs);
}
/// The last change near tetrahedron `t`: in the stars of its vertices and of its neighbours'
/// vertices, the region every topological operation on `t` reads.
fn stamp(&self, t: u32) -> Result<u64> {
let mut m = 0;
let mut look = |vs: [u32; 4]| {
for v in vs {
m = m.max(self.touched.get(v as usize).copied().unwrap_or(u64::MAX));
}
};
look(self.tet(t)?);
for n in self.adj(t)? {
if n != NONE {
look(self.tet(n)?);
}
}
Ok(m)
}
fn touched_at(&self, v: u32) -> u64 {
self.touched.get(v as usize).copied().unwrap_or(u64::MAX)
}
/// Record that removing edge `ab` failed now.
pub fn mark_edge_failed(&mut self, a: u32, b: u32) {
self.failed_edges.insert(sorted2(a, b), self.clock);
}
/// Whether removing edge `ab` failed and no tetrahedron around `a` or `b` changed since.
pub fn edge_known_to_fail(&self, a: u32, b: u32) -> bool {
self.failed_edges.get(&sorted2(a, b)).is_some_and(|&f| self.touched_at(a).max(self.touched_at(b)) <= f)
}
/// Record that removing `face` between apexes `a` and `b` failed now.
pub fn mark_face_failed(&mut self, a: u32, b: u32, face: [u32; 3]) {
self.failed_faces.insert((a, b, sorted3(face)), self.clock);
}
/// Whether removing `face` between `a` and `b` failed and nothing around them changed since.
pub fn face_known_to_fail(&self, a: u32, b: u32, face: [u32; 3]) -> bool {
self.failed_faces.get(&(a, b, sorted3(face))).is_some_and(|&f| self.touched_at(a).max(self.touched_at(b)) <= f)
}
/// The quality of live tetrahedron `t`, computed once per change of its vertices.
pub fn slot_quality(&self, t: u32) -> Result<f64> {
let vs = self.tet(t)?;
let stamp = vs.iter().map(|&v| self.touched_at(v)).max().unwrap_or(u64::MAX);
let i = t as usize;
if let Some(&(c, q)) = self.quality_cache.borrow().get(i)
&& c > stamp
{
return Ok(q);
}
let q = super::measure::quality(&self.coords(vs)?);
let mut cache = self.quality_cache.borrow_mut();
if cache.len() <= i {
cache.resize(i + 1, (0, 0.0));
}
if let Some(e) = cache.get_mut(i) {
*e = (self.clock + 1, q);
}
Ok(q)
}
/// Record that smoothing `v` found no move now.
pub fn mark_smoothing_failed(&mut self, v: u32) {
let i = v as usize;
if self.failed_smoothing.len() <= i {
self.failed_smoothing.resize(i + 1, 0);
}
if let Some(f) = self.failed_smoothing.get_mut(i) {
*f = self.clock;
}
}
/// Whether smoothing `v` found no move and no tetrahedron around it changed since.
pub fn smoothing_known_to_fail(&self, v: u32) -> bool {
self.failed_smoothing.get(v as usize).is_some_and(|&f| f > 0 && self.touched_at(v) <= f)
}
/// Record that every topological operation on `t` failed now.
pub fn mark_failed(&mut self, t: u32) {
let i = t as usize;
if self.failed.len() <= i {
self.failed.resize(i + 1, 0);
}
if let Some(f) = self.failed.get_mut(i) {
*f = self.clock;
}
}
/// Whether the topological operations on `t` failed and nothing they read has changed
/// since, so trying again would fail the same way (they are deterministic functions of that
/// region).
pub fn known_to_fail(&self, t: u32) -> Result<bool> {
let Some(&f) = self.failed.get(t as usize) else { return Ok(false) };
Ok(f > 0 && self.stamp(t)? <= f)
} }
pub fn slots(&self) -> usize { pub fn slots(&self) -> usize {
@@ -424,6 +567,7 @@ impl TetComplex {
pub fn set_curve(&mut self, a: u32, b: u32, curve: Option<u32>) { pub fn set_curve(&mut self, a: u32, b: u32, curve: Option<u32>) {
let key = sorted2(a, b); let key = sorted2(a, b);
self.touch(&[a, b]);
let before = match curve { let before = match curve {
Some(c) => self.curves.insert(key, c), Some(c) => self.curves.insert(key, c),
None => self.curves.remove(&key), None => self.curves.remove(&key),
@@ -688,6 +832,7 @@ impl TetComplex {
self.pts.push(p); self.pts.push(p);
self.kinds.push(kind); self.kinds.push(kind);
self.vtets.push(Vec::new()); self.vtets.push(Vec::new());
self.touched.push(self.clock);
if self.journal.recording { if self.journal.recording {
self.journal.steps.push(Step::Added { v }); self.journal.steps.push(Step::Added { v });
} }
@@ -695,7 +840,7 @@ impl TetComplex {
} }
/// The data a change of `slots` and of the incidence of `verts` would overwrite. /// The data a change of `slots` and of the incidence of `verts` would overwrite.
fn snapshot(&self, slots: &[u32], verts: &[u32]) -> Raw { fn snapshot(&self, slots: &[u32], verts: &[u32], takes: usize) -> Raw {
let mut ids: Vec<u32> = slots.to_vec(); let mut ids: Vec<u32> = slots.to_vec();
ids.sort_unstable(); ids.sort_unstable();
ids.dedup(); ids.dedup();
@@ -711,7 +856,8 @@ impl TetComplex {
}) })
.collect(), .collect(),
tets_len: self.tets.len(), tets_len: self.tets.len(),
free: self.free.clone(), free_len: self.free.len(),
free_tail: self.free.get(self.free.len().saturating_sub(takes)..).unwrap_or_default().to_vec(),
vtets: vs.into_iter().filter_map(|v| Some((v, self.vtets.get(v as usize)?.clone()))).collect(), vtets: vs.into_iter().filter_map(|v| Some((v, self.vtets.get(v as usize)?.clone()))).collect(),
live: self.live, live: self.live,
} }
@@ -719,6 +865,12 @@ impl TetComplex {
/// Put back what [`Self::snapshot`] saved. /// Put back what [`Self::snapshot`] saved.
fn restore(&mut self, raw: Raw) { fn restore(&mut self, raw: Raw) {
let mut touched: Vec<u32> = raw.vtets.iter().map(|(v, _)| *v).collect();
for (t, vs, _, _) in &raw.slots {
touched.extend(self.tets.get(*t as usize).copied().unwrap_or([NONE; 4]));
touched.extend(vs);
}
self.touch(&touched);
for (v, l) in raw.vtets { for (v, l) in raw.vtets {
if let Some(s) = self.vtets.get_mut(v as usize) { if let Some(s) = self.vtets.get_mut(v as usize) {
*s = l; *s = l;
@@ -734,7 +886,8 @@ impl TetComplex {
self.adj.truncate(raw.tets_len); self.adj.truncate(raw.tets_len);
self.tags.truncate(raw.tets_len); self.tags.truncate(raw.tets_len);
self.mark.truncate(raw.tets_len); self.mark.truncate(raw.tets_len);
self.free = raw.free; self.free.truncate(raw.free_len.saturating_sub(raw.free_tail.len()));
self.free.extend(raw.free_tail);
self.live = raw.live; self.live = raw.live;
} }
@@ -744,6 +897,7 @@ impl TetComplex {
self.pts.pop(); self.pts.pop();
self.kinds.pop(); self.kinds.pop();
self.vtets.pop(); self.vtets.pop();
self.touched.pop();
return Ok(()); return Ok(());
} }
Err(corrupt("dropping a vertex in use")) Err(corrupt("dropping a vertex in use"))
@@ -755,6 +909,7 @@ impl TetComplex {
let slot = self.pts.get_mut(v as usize).ok_or_else(|| corrupt("vertex id"))?; let slot = self.pts.get_mut(v as usize).ok_or_else(|| corrupt("vertex id"))?;
let from = *slot; let from = *slot;
*slot = p; *slot = p;
self.touch_star(v);
if self.journal.recording { if self.journal.recording {
self.journal.steps.push(Step::Moved { v, from }); self.journal.steps.push(Step::Moved { v, from });
} }
@@ -962,7 +1117,7 @@ impl TetComplex {
for &t in old { for &t in old {
verts.extend(self.tet(t)?); verts.extend(self.tet(t)?);
} }
Some(Box::new(self.snapshot(&slots, &verts))) Some(Box::new(self.snapshot(&slots, &verts, change.new.len())))
} else { } else {
None None
}; };
@@ -1002,6 +1157,9 @@ impl TetComplex {
} }
} }
} }
let mut changed: Vec<u32> = removed.iter().flat_map(|r| r.vs).collect();
changed.extend(change.new.iter().flatten());
self.touch(&changed);
self.live = self.live - old.len() + ids.len(); self.live = self.live - old.len() + ids.len();
if self.journal.recording { if self.journal.recording {
self.journal.steps.push(Step::Replaced { removed, added: change.new.clone(), raw: raw.unwrap_or_default() }); self.journal.steps.push(Step::Replaced { removed, added: change.new.clone(), raw: raw.unwrap_or_default() });
@@ -1044,7 +1202,7 @@ impl TetComplex {
return Ok(false); return Ok(false);
} }
} }
let raw = self.journal.recording.then(|| Box::new(self.snapshot(&[t, ns[k], ns[l]], &vs))); let raw = self.journal.recording.then(|| Box::new(self.snapshot(&[t, ns[k], ns[l]], &vs, 0)));
for m in [k, l] { for m in [k, l] {
let n = ns[m]; let n = ns[m];
let nn = self.adj(n)?; let nn = self.adj(n)?;
@@ -1055,6 +1213,7 @@ impl TetComplex {
} }
} }
self.unlink(t, vs); self.unlink(t, vs);
self.touch(&vs);
if let Some(s) = self.tets.get_mut(t as usize) { if let Some(s) = self.tets.get_mut(t as usize) {
*s = [DEAD; 4]; *s = [DEAD; 4];
} }
@@ -1122,15 +1281,18 @@ impl TetComplex {
if let Some(s) = self.pts.get_mut(v as usize) { if let Some(s) = self.pts.get_mut(v as usize) {
*s = from; *s = from;
} }
self.touch_star(v);
} }
Step::Added { v } => { Step::Added { v } => {
if v as usize + 1 == self.pts.len() { if v as usize + 1 == self.pts.len() {
self.pts.pop(); self.pts.pop();
self.kinds.pop(); self.kinds.pop();
self.vtets.pop(); self.vtets.pop();
self.touched.pop();
} }
} }
Step::Curve { edge, curve } => { Step::Curve { edge, curve } => {
self.touch(&[edge.0, edge.1]);
match curve { match curve {
Some(c) => self.curves.insert(edge, c), Some(c) => self.curves.insert(edge, c),
None => self.curves.remove(&edge), None => self.curves.remove(&edge),
+38 -23
View File
@@ -24,7 +24,7 @@ use super::complex::{EDGES, IdMap, NONE, Step, TetComplex, VertexKind, Walk, fac
use super::measure::{self, P}; use super::measure::{self, P};
use super::ops::{self, Site, remove_edge_at, remove_face, tet_quality}; use super::ops::{self, Site, remove_edge_at, remove_face, tet_quality};
use super::refine::{Context, MAX_CAVITY, MIN_GAP}; use super::refine::{Context, MAX_CAVITY, MIN_GAP};
use super::smooth::smooth_vertex; use super::smooth::{smooth_vertex, smooth_vertex_below};
/// Settings of the schedule. /// Settings of the schedule.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
@@ -53,6 +53,8 @@ pub struct ImproveStats {
pub smoothed: usize, pub smoothed: usize,
pub topological: usize, pub topological: usize,
pub inserted: usize, pub inserted: usize,
/// Insertions tried (kept or undone).
pub attempted: usize,
/// The worst quality (smallest dihedral sine) after each pass. /// The worst quality (smallest dihedral sine) after each pass.
pub worst: Vec<f64>, pub worst: Vec<f64>,
pub stopped: Option<String>, pub stopped: Option<String>,
@@ -81,8 +83,8 @@ fn indicators(cx: &TetComplex) -> Result<[f64; 8]> {
let mut out = [0.0; 8]; let mut out = [0.0; 8];
out[0] = f64::INFINITY; out[0] = f64::INFINITY;
let mut n = 0usize; let mut n = 0usize;
for (_, vs) in cx.live_tets() { for (t, _) in cx.live_tets() {
let q = tet_quality(cx, vs)?; let q = cx.slot_quality(t)?;
out[0] = out[0].min(q); out[0] = out[0].min(q);
for (i, s) in SIN_DEG.iter().enumerate() { for (i, s) in SIN_DEG.iter().enumerate() {
out[i + 1] += q.min(*s); out[i + 1] += q.min(*s);
@@ -102,8 +104,8 @@ fn progressed(now: &[f64; 8], before: &[f64; 8]) -> bool {
/// Smooth every movable vertex of a tetrahedron below `good`. Returns the moves. /// Smooth every movable vertex of a tetrahedron below `good`. Returns the moves.
fn smoothing_pass(cx: &mut TetComplex, ctx: &Context, good: f64) -> Result<usize> { fn smoothing_pass(cx: &mut TetComplex, ctx: &Context, good: f64) -> Result<usize> {
let mut want = vec![false; cx.point_count()]; let mut want = vec![false; cx.point_count()];
for (_, vs) in cx.live_tets() { for (t, vs) in cx.live_tets() {
if tet_quality(cx, vs)? < good { if cx.slot_quality(t)? < good {
for v in vs { for v in vs {
if let Some(w) = want.get_mut(v as usize) { if let Some(w) = want.get_mut(v as usize) {
*w = true; *w = true;
@@ -149,7 +151,7 @@ fn delete_flat(cx: &mut TetComplex, t: u32) -> Result<bool> {
/// edge ([`ops::collapse`]), which takes the tetrahedron away. /// edge ([`ops::collapse`]), which takes the tetrahedron away.
fn collapse_into_crease(cx: &mut TetComplex, t: u32) -> Result<bool> { fn collapse_into_crease(cx: &mut TetComplex, t: u32) -> Result<bool> {
let vs = cx.tet(t)?; let vs = cx.tet(t)?;
if tet_quality(cx, vs)? > 0.1 { if cx.slot_quality(t)? > 0.1 {
return Ok(false); return Ok(false);
} }
let (ns, tags) = (cx.adj(t)?, cx.tags(t)?); let (ns, tags) = (cx.adj(t)?, cx.tags(t)?);
@@ -182,7 +184,7 @@ fn collapse_into_crease(cx: &mut TetComplex, t: u32) -> Result<bool> {
pub fn topological_pass(cx: &mut TetComplex, list: &[u32]) -> Result<usize> { pub fn topological_pass(cx: &mut TetComplex, list: &[u32]) -> Result<usize> {
let mut changes = 0; let mut changes = 0;
for &t in list { for &t in list {
if !cx.is_live(t) { if !cx.is_live(t) || cx.known_to_fail(t)? {
continue; continue;
} }
let vs = cx.tet(t)?; let vs = cx.tet(t)?;
@@ -201,20 +203,25 @@ pub fn topological_pass(cx: &mut TetComplex, list: &[u32]) -> Result<usize> {
changes += 1; changes += 1;
continue; continue;
} }
let mut removed = false;
for k in 0..4 { for k in 0..4 {
if remove_face(cx, t, k)? { if remove_face(cx, t, k)? {
changes += 1; changes += 1;
removed = true;
break; break;
} }
} }
if !removed {
cx.mark_failed(t);
}
} }
Ok(changes) Ok(changes)
} }
fn below(cx: &TetComplex, good: f64) -> Result<Vec<u32>> { fn below(cx: &TetComplex, good: f64) -> Result<Vec<u32>> {
let mut out = Vec::new(); let mut out = Vec::new();
for (t, vs) in cx.live_tets() { for (t, _) in cx.live_tets() {
if tet_quality(cx, vs)? < good { if cx.slot_quality(t)? < good {
out.push(t); out.push(t);
} }
} }
@@ -288,7 +295,8 @@ fn change_quality(cx: &mut TetComplex, len: usize) -> Result<(f64, f64)> {
} }
/// The composite insertion of §3.5 (see the module docs). Returns whether it was kept. /// The composite insertion of §3.5 (see the module docs). Returns whether it was kept.
fn attempt_insert(cx: &mut TetComplex, ctx: &Context, p: Point3, kind: VertexKind, site: Site) -> Result<bool> { fn attempt_insert(cx: &mut TetComplex, ctx: &Context, attempted: &mut usize, good: f64, p: Point3, kind: VertexKind, site: Site) -> Result<bool> {
*attempted += 1;
let was = cx.journal.recording; let was = cx.journal.recording;
cx.journal.recording = true; cx.journal.recording = true;
let len = cx.journal.steps.len(); let len = cx.journal.steps.len();
@@ -303,8 +311,15 @@ fn attempt_insert(cx: &mut TetComplex, ctx: &Context, p: Point3, kind: VertexKin
if !cx.is_used(v) { if !cx.is_used(v) {
break; break;
} }
let star = cx.star(v)?; // Only the tetrahedra below the threshold, as in the passes.
if topological_pass(cx, &star)? == 0 { let mut star = cx.star(v)?;
let mut keep = Vec::with_capacity(star.len());
for &t in &star {
keep.push(cx.slot_quality(t)? < good);
}
let mut flags = keep.into_iter();
star.retain(|_| flags.next().unwrap_or(false));
if star.is_empty() || topological_pass(cx, &star)? == 0 {
break; break;
} }
} }
@@ -312,7 +327,7 @@ fn attempt_insert(cx: &mut TetComplex, ctx: &Context, p: Point3, kind: VertexKin
let mut around: Vec<u32> = cx.neighbours(v)?; let mut around: Vec<u32> = cx.neighbours(v)?;
around.push(v); around.push(v);
for u in around { for u in around {
smooth_vertex(cx, ctx, u)?; smooth_vertex_below(cx, ctx, u, good)?;
} }
} }
let (before, after) = change_quality(cx, len)?; let (before, after) = change_quality(cx, len)?;
@@ -337,7 +352,7 @@ fn attempt_insert(cx: &mut TetComplex, ctx: &Context, p: Point3, kind: VertexKin
} }
/// Try inserting a vertex in each tetrahedron of `list` (§4.2). Returns the insertions kept. /// Try inserting a vertex in each tetrahedron of `list` (§4.2). Returns the insertions kept.
fn insertion_pass(cx: &mut TetComplex, ctx: &Context, list: &[u32], budget: usize, good: f64) -> Result<usize> { fn insertion_pass(cx: &mut TetComplex, ctx: &Context, list: &[u32], budget: usize, good: f64, attempted: &mut usize) -> Result<usize> {
let mut kept = 0; let mut kept = 0;
for &t in list { for &t in list {
if kept >= budget { if kept >= budget {
@@ -347,7 +362,7 @@ fn insertion_pass(cx: &mut TetComplex, ctx: &Context, list: &[u32], budget: usiz
continue; continue;
} }
let vs = cx.tet(t)?; let vs = cx.tet(t)?;
if tet_quality(cx, vs)? >= good { if cx.slot_quality(t)? >= good {
continue; continue;
} }
let ns = cx.adj(t)?; let ns = cx.adj(t)?;
@@ -374,7 +389,7 @@ fn insertion_pass(cx: &mut TetComplex, ctx: &Context, list: &[u32], budget: usiz
let target = measure::add(measure::mid(pa, pb), measure::scale(inward, 0.4 * l / len)); let target = measure::add(measure::mid(pa, pb), measure::scale(inward, 0.4 * l / len));
if let Ok(p) = Point3::try_from(target) if let Ok(p) = Point3::try_from(target)
&& let Some(Walk::Inside(at)) = cx.walk(t, &p, 1 << 12)? && let Some(Walk::Inside(at)) = cx.walk(t, &p, 1 << 12)?
&& attempt_insert(cx, ctx, p, VertexKind::Interior, Site::Interior(at))? && attempt_insert(cx, ctx, attempted, good, p, VertexKind::Interior, Site::Interior(at))?
{ {
done = true; done = true;
} }
@@ -391,7 +406,7 @@ fn insertion_pass(cx: &mut TetComplex, ctx: &Context, list: &[u32], budget: usiz
let kind = VertexKind::Face(tags[k]); let kind = VertexKind::Face(tags[k]);
if let Some(p) = ctx.place(kind, g) if let Some(p) = ctx.place(kind, g)
&& cx.is_live(t) && cx.is_live(t)
&& attempt_insert(cx, ctx, p, kind, Site::Face(t, k))? && attempt_insert(cx, ctx, attempted, good, p, kind, Site::Face(t, k))?
{ {
done = true; done = true;
} }
@@ -400,7 +415,7 @@ fn insertion_pass(cx: &mut TetComplex, ctx: &Context, list: &[u32], budget: usiz
if !done && cx.is_live(t) && cx.tet(t)? == vs { if !done && cx.is_live(t) && cx.tet(t)? == vs {
let g = measure::centroid(&cx.coords(vs)?); let g = measure::centroid(&cx.coords(vs)?);
if let Ok(p) = Point3::try_from(g) if let Ok(p) = Point3::try_from(g)
&& attempt_insert(cx, ctx, p, VertexKind::Interior, Site::Interior(t))? && attempt_insert(cx, ctx, attempted, good, p, VertexKind::Interior, Site::Interior(t))?
{ {
done = true; done = true;
} }
@@ -430,7 +445,7 @@ fn insertion_pass(cx: &mut TetComplex, ctx: &Context, list: &[u32], budget: usiz
}; };
let m = measure::mid(cx.xyz(a)?, cx.xyz(b)?); let m = measure::mid(cx.xyz(a)?, cx.xyz(b)?);
if let Some(p) = ctx.place(kind, m) if let Some(p) = ctx.place(kind, m)
&& attempt_insert(cx, ctx, p, kind, Site::Edge(a, b))? && attempt_insert(cx, ctx, attempted, good, p, kind, Site::Edge(a, b))?
{ {
done = true; done = true;
break; break;
@@ -479,8 +494,8 @@ pub fn improve(cx: &mut TetComplex, ctx: &Context, o: &ImproveOptions) -> Result
// The worst 3.5% of the tetrahedra, or (desperation pass, §4.2) every one with a // The worst 3.5% of the tetrahedra, or (desperation pass, §4.2) every one with a
// dihedral angle below 40° or above 140° (sine below sin 40°). // dihedral angle below 40° or above 140° (sine below sin 40°).
let mut scored: Vec<(f64, u32)> = Vec::new(); let mut scored: Vec<(f64, u32)> = Vec::new();
for (t, vs) in cx.live_tets() { for (t, _) in cx.live_tets() {
scored.push((tet_quality(cx, vs)?, t)); scored.push((cx.slot_quality(t)?, t));
} }
scored.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1))); scored.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
let list: Vec<u32> = if failed == 1 { let list: Vec<u32> = if failed == 1 {
@@ -490,7 +505,7 @@ pub fn improve(cx: &mut TetComplex, ctx: &Context, o: &ImproveOptions) -> Result
scored.iter().take(n).map(|s| s.1).collect() scored.iter().take(n).map(|s| s.1).collect()
}; };
let budget = o.max_insertions - stats.inserted; let budget = o.max_insertions - stats.inserted;
stats.inserted += insertion_pass(cx, ctx, &list, budget, o.good)?; stats.inserted += insertion_pass(cx, ctx, &list, budget, o.good, &mut stats.attempted)?;
} }
let now = indicators(cx)?; let now = indicators(cx)?;
carried = Some(now); carried = Some(now);
@@ -548,7 +563,7 @@ mod tests {
let q = Point3::try_from(q).unwrap(); let q = Point3::try_from(q).unwrap();
if let Some(Walk::Inside(at)) = cx.walk(t, &q, 1 << 12).unwrap() { if let Some(Walk::Inside(at)) = cx.walk(t, &q, 1 << 12).unwrap() {
tried += 1; tried += 1;
assert!(!attempt_insert(&mut cx, &ctx, q, VertexKind::Interior, Site::Interior(at)).unwrap()); assert!(!attempt_insert(&mut cx, &ctx, &mut 0, SIN_40, q, VertexKind::Interior, Site::Interior(at)).unwrap());
} }
} }
} }
+159 -12
View File
@@ -76,13 +76,39 @@ pub fn dihedral_cosines(x: &[P; 4]) -> [f64; 6] {
/// The quality objective of a valid tetrahedron: the smaller of its smallest dihedral sine and /// The quality objective of a valid tetrahedron: the smaller of its smallest dihedral sine and
/// its weighted scaled Jacobian ([`JACOBIAN_WEIGHT`]; the regular tetrahedron scores /// its weighted scaled Jacobian ([`JACOBIAN_WEIGHT`]; the regular tetrahedron scores
/// √(8/9) ≈ 0.943). The dihedral sine catches slivers and caps; it does not see needles, whose /// √(8/9) ≈ 0.943). The dihedral sine
/// dihedral angles can all be fine while a corner is nearly flat (Shewchuk 2002), and the scaled /// catches slivers and caps; it does not see needles, whose dihedral angles can all be fine
/// Jacobian `√2 · 6V / (ℓ₁ℓ₂ℓ₃)` at the worst corner, the measure analysts check (the skill's /// while a corner is nearly flat (Shewchuk 2002), and the scaled Jacobian
/// limit, [`crate::quality`]), does. 0 for a flat tetrahedron; for an inverted one the negative /// `√2 · 6V / (ℓ₁ℓ₂ℓ₃)` at the worst corner, the measure analysts check (the skill's ≥ 0.2,
/// sine of largest magnitude, so worse inversions score lower. Extremes are found on squared /// [`crate::quality`]), does. 0 for a flat tetrahedron; for an inverted one the negative sine of
/// lengths and areas, with one square root each. /// largest magnitude, so worse inversions score lower. Extremes are found on squared lengths and
/// areas, with one square root each.
pub fn quality(x: &[P; 4]) -> f64 { pub fn quality(x: &[P; 4]) -> f64 {
quality_in_order(&canonical_order(x))
}
/// The corners reordered by an even permutation that depends only on the corners' positions
/// (sorted by coordinates, the last two swapped back when the sort was odd), so a measure
/// computed on it gives the same bits for every listing of a tetrahedron with the same
/// orientation: a removal tried from either side of a ring sees the same qualities.
fn canonical_order(x: &[P; 4]) -> [P; 4] {
let less = |a: &P, b: &P| a[0] < b[0] || (a[0] == b[0] && (a[1] < b[1] || (a[1] == b[1] && a[2] < b[2])));
let mut y = *x;
let mut odd = false;
// A sorting network on four (pairs 01, 23, 02, 13, 12), counting swaps for the parity.
for (i, j) in [(0, 1), (2, 3), (0, 2), (1, 3), (1, 2)] {
if less(&y[j], &y[i]) {
y.swap(i, j);
odd = !odd;
}
}
if odd {
y.swap(2, 3);
}
y
}
fn quality_in_order(x: &[P; 4]) -> f64 {
let v6 = volume6(x); let v6 = volume6(x);
if v6 == 0.0 || !v6.is_finite() { if v6 == 0.0 || !v6.is_finite() {
return 0.0; return 0.0;
@@ -102,12 +128,6 @@ pub fn quality(x: &[P; 4]) -> f64 {
if q.is_finite() { q } else { 0.0 } if q.is_finite() { q } else { 0.0 }
} }
/// The weight of the scaled Jacobian in [`quality`]: the meshing skill's limit, a scaled
/// Jacobian of 0.3, counts as much as a 30° dihedral angle (sin 30°, the improvement's target,
/// [`super::improve::ImproveOptions::target`]), so the Jacobian decides only for tetrahedra near
/// that limit (needles) and the dihedral sine everywhere else.
pub const JACOBIAN_WEIGHT: f64 = 0.5 / 0.3;
/// The corners of a tetrahedron and the other three corners of each. /// The corners of a tetrahedron and the other three corners of each.
const CORNERS: [(usize, [usize; 3]); 4] = [(0, [1, 2, 3]), (1, [0, 2, 3]), (2, [0, 1, 3]), (3, [0, 1, 2])]; const CORNERS: [(usize, [usize; 3]); 4] = [(0, [1, 2, 3]), (1, [0, 2, 3]), (2, [0, 1, 3]), (3, [0, 1, 2])];
@@ -128,6 +148,100 @@ fn scaled_jacobian(x: &[P; 4], v6: f64) -> f64 {
std::f64::consts::SQRT_2 * v6 / worst.sqrt() std::f64::consts::SQRT_2 * v6 / worst.sqrt()
} }
/// The weight of the scaled Jacobian in [`quality`]: the meshing skill's limit, a scaled
/// Jacobian of 0.3, counts as much as a 30° dihedral angle (sin 30°, the improvement's target,
/// [`super::improve::ImproveOptions::target`]), so the Jacobian decides only for tetrahedra near
/// that limit (needles) and the dihedral sine everywhere else.
pub const JACOBIAN_WEIGHT: f64 = 0.5 / 0.3;
/// [`quality`] and its gradient with respect to corner `i`, for a positive tetrahedron: the
/// gradient of the term [`quality`] picks (the smallest dihedral sine, or the scaled Jacobian at
/// the worst corner, `∇ ln J = ∇V₆ / V₆ − Σ ∇ℓ / ℓ` over that corner's edges), analytic, as Klingner and
/// Shewchuk (§3.1) and Freitag and Ollivier-Gooch use for smoothing. With
/// `s = V₆ ℓ / (N_k N_l)` (`N` twice a face's area),
/// `∇s = s (∇V₆ / V₆ + ∇ℓ / ℓ − ∇N_k / N_k − ∇N_l / N_l)`, where `∇V₆` at a corner is its
/// opposite face's area vector, `∇ℓ` the unit edge vector at an end of the edge and, for a face
/// `(p, q, r)` with unit normal `n̂`, `∇_p N = (q − r) × n̂`. A flat or inverted tetrahedron
/// gets a zero gradient (smoothing only moves within valid positions).
pub fn quality_gradient(x: &[P; 4], i: usize) -> (f64, P) {
let q = quality(x);
let v6 = volume6(x);
if !(v6 > 0.0 && q > 0.0) || i > 3 {
return (q, [0.0; 3]);
}
let crosses = [0, 1, 2, 3].map(|k| face_cross(x, k));
let n2 = crosses.map(|c| dot(c, c));
// The edge whose sine is smallest (the same comparison as `quality`).
let mut pick = (f64::INFINITY, EDGE_FACES[0]);
for e in EDGE_FACES {
let (a, b, k, l) = e;
let d = sub(x[a], x[b]);
let r = dot(d, d) / (n2[k] * n2[l]);
if r < pick.0 {
pick = (r, e);
}
}
// The scaled Jacobian branch, when it is the smaller.
let products = corner_products(x);
let (c, cp) = products.iter().enumerate().fold((0, 0.0_f64), |m, (c, &p)| if p > m.1 { (c, p) } else { m });
let sj = JACOBIAN_WEIGHT * std::f64::consts::SQRT_2 * v6 / cp.sqrt();
let sine = v6 * pick.0.sqrt();
let face = |k: usize| -> [usize; 3] {
match k {
0 => [1, 2, 3],
1 => [0, 2, 3],
2 => [0, 1, 3],
_ => [0, 1, 2],
}
};
// ∇V₆ at corner i: the area vector of the face opposite i, signed so that V₆ grows along it.
let gi = crosses[i];
let gv = if dot(gi, sub(x[i], x[face(i)[0]])) < 0.0 { scale(gi, -1.0) } else { gi };
if sj < sine {
// ∇ ln sj = ∇V₆ / V₆ − Σ ∇ℓ / ℓ over the corner's edges at i.
let (corner, others) = CORNERS[c];
let mut g = scale(gv, 1.0 / v6);
for j in others {
let (p, o) = if i == corner {
(x[i], x[j])
} else if i == j {
(x[i], x[corner])
} else {
continue;
};
let e = sub(p, o);
g = sub(g, scale(e, 1.0 / dot(e, e)));
}
let g = scale(g, q);
return if g.iter().all(|c| c.is_finite()) { (q, g) } else { (q, [0.0; 3]) };
}
let (a, b, k, l) = pick.1;
let len = dist(x[a], x[b]);
let gl = if i == a {
scale(sub(x[a], x[b]), 1.0 / len)
} else if i == b {
scale(sub(x[b], x[a]), 1.0 / len)
} else {
[0.0; 3]
};
// ∇N of the face opposite m at corner i (zero when the face does not hold i).
let gn = |m: usize| -> P {
let f = face(m);
let Some(pos) = f.iter().position(|&c| c == i) else { return [0.0; 3] };
let n = crosses[m];
let nn = norm(n);
if !(nn > 0.0) {
return [0.0; 3];
}
let (qv, rv) = (x[f[(pos + 1) % 3]], x[f[(pos + 2) % 3]]);
scale(cross(sub(qv, rv), n), 1.0 / nn)
};
let (nk, nl) = (n2[k].sqrt(), n2[l].sqrt());
let g = add(add(scale(gv, 1.0 / v6), scale(gl, 1.0 / len)), scale(add(scale(gn(k), 1.0 / nk), scale(gn(l), 1.0 / nl)), -1.0));
let g = scale(g, q);
if g.iter().all(|c| c.is_finite()) { (q, g) } else { (q, [0.0; 3]) }
}
/// The smallest and largest dihedral angle in degrees (reports only). /// The smallest and largest dihedral angle in degrees (reports only).
pub fn dihedral_range(x: &[P; 4]) -> (f64, f64) { pub fn dihedral_range(x: &[P; 4]) -> (f64, f64) {
let (s, c) = (dihedral_sines(x), dihedral_cosines(x)); let (s, c) = (dihedral_sines(x), dihedral_cosines(x));
@@ -251,6 +365,16 @@ mod tests {
assert!(circumcenter(&flat).is_none()); assert!(circumcenter(&flat).is_none());
} }
/// Every even reordering of the corners gives the same quality, bit for bit.
#[test]
fn quality_does_not_depend_on_the_corner_order() {
let x = positive([[0.1, 0.0, 0.05], [1.0, 0.2, 0.0], [0.3, 0.9, 0.1], [0.4, 0.35, 0.7]]);
let q = quality(&x);
for p in [[1, 2, 0, 3], [2, 0, 1, 3], [0, 2, 3, 1], [3, 1, 0, 2], [1, 0, 3, 2], [2, 3, 0, 1]] {
assert_eq!(quality(&p.map(|i| x[i])).to_bits(), q.to_bits(), "{p:?}");
}
}
/// A needle: every dihedral angle above 20°, yet one corner nearly flat; the objective sees /// A needle: every dihedral angle above 20°, yet one corner nearly flat; the objective sees
/// it through the scaled Jacobian. /// it through the scaled Jacobian.
#[test] #[test]
@@ -261,6 +385,29 @@ mod tests {
assert!(quality(&x) < 0.15 * JACOBIAN_WEIGHT, "{}", quality(&x)); assert!(quality(&x) < 0.15 * JACOBIAN_WEIGHT, "{}", quality(&x));
} }
/// The analytic gradient matches central differences of the quality away from ties, on
/// either branch.
#[test]
fn quality_gradient_matches_central_differences() {
let sliverish = positive([[0.1, 0.0, 0.05], [1.0, 0.2, 0.0], [0.3, 0.9, 0.1], [0.4, 0.35, 0.7]]);
let needle = positive([[0.0, 0.0, 0.0], [1.0, 0.1, 0.0], [0.45, 0.85, 0.05], [0.55, 0.3, 6.0]]);
for x in [sliverish, needle] {
for i in 0..4 {
let (q, g) = quality_gradient(&x, i);
assert_eq!(q, quality(&x));
let h = 1e-7;
for c in 0..3 {
let (mut a, mut b) = (x, x);
a[i][c] += h;
b[i][c] -= h;
let fd = (quality(&a) - quality(&b)) / (2.0 * h);
assert!((fd - g[c]).abs() < 1e-6, "corner {i} axis {c}: {fd} vs {}", g[c]);
}
}
}
assert_eq!(quality_gradient(&[[0.0; 3]; 4], 0).1, [0.0; 3]);
}
#[test] #[test]
fn sliver_scores_low() { fn sliver_scores_low() {
let x = positive([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.01]]); let x = positive([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.01]]);
+93 -46
View File
@@ -31,7 +31,7 @@ pub fn tet_quality(cx: &TetComplex, vs: [u32; 4]) -> Result<f64> {
pub fn worst(cx: &TetComplex, tets: &[u32]) -> Result<f64> { pub fn worst(cx: &TetComplex, tets: &[u32]) -> Result<f64> {
let mut q = f64::INFINITY; let mut q = f64::INFINITY;
for &t in tets { for &t in tets {
q = q.min(tet_quality(cx, cx.tet(t)?)?); q = q.min(cx.slot_quality(t)?);
} }
Ok(q) Ok(q)
} }
@@ -47,13 +47,17 @@ pub const MAX_RING: usize = 10;
pub const BOUNDARY_FLIP_COS: f64 = 0.939_692_620_785_908_4; pub const BOUNDARY_FLIP_COS: f64 = 0.939_692_620_785_908_4;
/// Klincsek's table: the best worst quality of a triangulation of the chain `v_i … v_j` /// Klincsek's table: the best worst quality of a triangulation of the chain `v_i … v_j`
/// (with the closing edge `v_i v_j`), and the apex `k` that achieves it. /// (with the closing edge `v_i v_j`), and the apex `k` that achieves it. Only triangulations
fn klincsek(m: usize, q: &mut dyn FnMut(usize, usize, usize) -> Result<f64>) -> Result<(Vec<f64>, Vec<usize>)> { /// better than `floor` matter: an entry that cannot beat it holds `floor`, and a triangle is
/// evaluated only when its two sub-chains beat it, which skips most of the table when the
/// removal cannot pay off (the decision and the triangulation chosen are the same as without
/// the floor).
fn klincsek(m: usize, floor: f64, q: &mut dyn FnMut(usize, usize, usize) -> Result<f64>) -> Result<(Vec<f64>, Vec<usize>)> {
let mut best = vec![f64::INFINITY; m * m]; let mut best = vec![f64::INFINITY; m * m];
let mut arg = vec![0usize; m * m]; let mut arg = vec![0usize; m * m];
for i in (0..m.saturating_sub(2)).rev() { for i in (0..m.saturating_sub(2)).rev() {
for j in i + 2..m { for j in i + 2..m {
let mut b = f64::NEG_INFINITY; let mut b = floor;
let mut a = i + 1; let mut a = i + 1;
for k in i + 1..j { for k in i + 1..j {
let sub = best.get(i * m + k).copied().unwrap_or(f64::NEG_INFINITY).min(best.get(k * m + j).copied().unwrap_or(f64::NEG_INFINITY)); let sub = best.get(i * m + k).copied().unwrap_or(f64::NEG_INFINITY).min(best.get(k * m + j).copied().unwrap_or(f64::NEG_INFINITY));
@@ -115,12 +119,31 @@ pub fn remove_edge_at(cx: &mut TetComplex, t: u32, a: u32, b: u32) -> Result<boo
if cx.curve(a, b).is_some() || !vs.contains(&a) || !vs.contains(&b) { if cx.curve(a, b).is_some() || !vs.contains(&a) || !vs.contains(&b) {
return Ok(false); return Ok(false);
} }
// A removal that failed fails again until a tetrahedron around the edge changes.
if cx.edge_known_to_fail(a, b) {
return Ok(false);
}
let removed = remove_edge_ring(cx, t, a, b)?;
if !removed {
cx.mark_edge_failed(a, b);
}
Ok(removed)
}
/// [`remove_edge_at`] past its checks.
fn remove_edge_ring(cx: &mut TetComplex, t: u32, a: u32, b: u32) -> Result<bool> {
let Some(ring) = cx.edge_ring_at(t, a, b)? else { return Ok(false) }; let Some(ring) = cx.edge_ring_at(t, a, b)? else { return Ok(false) };
let m = ring.verts.len(); let m = ring.verts.len();
if m < 3 || m > MAX_RING + usize::from(!ring.closed) { if m < 3 || m > MAX_RING + usize::from(!ring.closed) {
return Ok(false); return Ok(false);
} }
let v = ring.verts.clone(); let mut v = ring.verts.clone();
if ring.closed {
// Start a closed ring at its smallest vertex, so the triangulation chosen does not
// depend on the tetrahedron the ring was walked from.
let first = v.iter().enumerate().min_by_key(|(_, id)| **id).map_or(0, |(i, _)| i);
v.rotate_left(first);
}
let x = |id: u32| cx.xyz(id); let x = |id: u32| cx.xyz(id);
let (xa, xb) = (x(a)?, x(b)?); let (xa, xb) = (x(a)?, x(b)?);
// An open ring runs from boundary face (a, b, v_0) to (a, b, v_{m−1}): both must lie on one // An open ring runs from boundary face (a, b, v_0) to (a, b, v_{m−1}): both must lie on one
@@ -150,7 +173,7 @@ pub fn remove_edge_at(cx: &mut TetComplex, t: u32, a: u32, b: u32) -> Result<boo
Ok(measure::quality(&[pi, pj, pk, xa]).min(measure::quality(&[pi, pk, pj, xb]))) Ok(measure::quality(&[pi, pj, pk, xa]).min(measure::quality(&[pi, pk, pj, xb])))
}; };
// Triangulate the polygon v_0 … v_{m−1}, closed by the edge v_{m−1} v_0. // Triangulate the polygon v_0 … v_{m−1}, closed by the edge v_{m−1} v_0.
let (best, arg) = klincsek(m, &mut q)?; let (best, arg) = klincsek(m, before, &mut q)?;
let total = best.get(m - 1).copied().unwrap_or(f64::NEG_INFINITY); let total = best.get(m - 1).copied().unwrap_or(f64::NEG_INFINITY);
if !(total > before) { if !(total > before) {
return Ok(false); return Ok(false);
@@ -199,37 +222,59 @@ pub fn remove_face(cx: &mut TetComplex, t: u32, k: usize) -> Result<bool> {
let us = cx.tet(u)?; let us = cx.tet(u)?;
let face = face_toward(vs, k); let face = face_toward(vs, k);
let b = us.iter().copied().find(|v| !face.contains(v)).unwrap_or(NONE); let b = us.iter().copied().find(|v| !face.contains(v)).unwrap_or(NONE);
if b == NONE || cx.curve(a, b).is_some() || cx.edge_tet(a, b)?.is_some() { if b == NONE || cx.curve(a, b).is_some() || cx.face_known_to_fail(a, b, face) || cx.edge_tet(a, b)?.is_some() {
return Ok(false); return Ok(false);
} }
let removed = remove_faces_between(cx, t, u, a, b, face)?;
if !removed {
cx.mark_face_failed(a, b, face);
}
Ok(removed)
}
/// [`remove_face`] past its checks: tetrahedra `t` (apex `a`) and `u` (apex `b`) share `face`.
fn remove_faces_between(cx: &mut TetComplex, t: u32, u: u32, a: u32, b: u32, face: [u32; 3]) -> Result<bool> {
// Sandwiched faces between a and b: faces f with tetrahedra (a ∪ f) and (b ∪ f). Grow the // Sandwiched faces between a and b: faces f with tetrahedra (a ∪ f) and (b ∪ f). Grow the
// set from the given face across shared edges while every new tetrahedron (a, b, e) on the // set from the given face across shared edges while every new tetrahedron (a, b, e) on the
// ring stays better than the worst removed one; take the best prefix found (a greedy form of // ring stays better than the worst removed one; take the best prefix found (a greedy form of
// Shewchuk's optimal search, which visits the same candidates). // Shewchuk's optimal search, which visits the same candidates).
let mut faces: Vec<[u32; 3]> = vec![face]; // Each face with the tetrahedron (a ∪ face).
let mut faces: Vec<([u32; 3], u32)> = vec![(face, t)];
let mut old: Vec<u32> = vec![t, u]; let mut old: Vec<u32> = vec![t, u];
let mut best: Option<(f64, Vec<u32>, Vec<[u32; 4]>)> = None; let mut best: Option<(f64, Vec<u32>, Vec<[u32; 4]>)> = None;
let mut removed_q = worst(cx, &old)?;
let mut made: IdMap<(u32, u32), ([u32; 4], f64)> = IdMap::default();
for _round in 0..8 { for _round in 0..8 {
let removed_q = worst(cx, &old)?;
// The boundary edges of the face set (edges in exactly one face), oriented as in the face // The boundary edges of the face set (edges in exactly one face), oriented as in the face
// seen from a: each gives the new tetrahedron (a, b, e) positive. // seen from a, with the face's tetrahedron on a's side: each gives the new tetrahedron
let mut count: IdMap<(u32, u32), (u32, u32, usize)> = IdMap::default(); // (a, b, e) positive.
for f in &faces { let mut count: IdMap<(u32, u32), (u32, u32, u32, usize)> = IdMap::default();
for &(f, ta) in &faces {
for i in 0..3 { for i in 0..3 {
let (p, q) = (f[i], f[(i + 1) % 3]); let (p, q) = (f[i], f[(i + 1) % 3]);
let e = count.entry(sorted2(p, q)).or_insert((p, q, 0)); let e = count.entry(sorted2(p, q)).or_insert((p, q, ta, 0));
e.2 += 1; e.3 += 1;
} }
} }
let mut ring: Vec<(u32, u32)> = count.values().filter(|e| e.2 == 1).map(|e| (e.0, e.1)).collect(); let mut ring: Vec<(u32, u32, u32)> = count.values().filter(|e| e.3 == 1).map(|e| (e.0, e.1, e.2)).collect();
ring.sort_unstable(); ring.sort_unstable();
let mut new = Vec::with_capacity(ring.len()); let mut new = Vec::with_capacity(ring.len());
let mut q = f64::INFINITY; let mut q = f64::INFINITY;
for &(p, r) in &ring { for &(p, r, _) in &ring {
// face (p, r, x) has a on its positive side, so (p, r, a, b)… orient explicitly. // A ring edge stays on the ring until a face grows across it: its tetrahedron is
let cand = [p, r, b, a]; // worked out once.
let cand = if cx.orient(cand)? == Sign::Positive { cand } else { [r, p, b, a] }; let (cand, cq) = match made.get(&(p, r)) {
q = q.min(tet_quality(cx, cand)?); Some(&c) => c,
None => {
// face (p, r, x) has a on its positive side, so (p, r, a, b)… orient explicitly.
let cand = [p, r, b, a];
let cand = if cx.orient(cand)? == Sign::Positive { cand } else { [r, p, b, a] };
let c = (cand, tet_quality(cx, cand)?);
made.insert((p, r), c);
c
}
};
q = q.min(cq);
new.push(cand); new.push(cand);
} }
if q > removed_q && best.as_ref().is_none_or(|(bq, _, _)| q > *bq) { if q > removed_q && best.as_ref().is_none_or(|(bq, _, _)| q > *bq) {
@@ -237,11 +282,12 @@ pub fn remove_face(cx: &mut TetComplex, t: u32, k: usize) -> Result<bool> {
} }
// Grow: a sandwiched neighbour face across a ring edge. // Grow: a sandwiched neighbour face across a ring edge.
let mut grown = false; let mut grown = false;
for &(p, r) in &ring { for &(p, r, ta) in &ring {
if let Some((f, ta, tb)) = sandwiched_across(cx, a, b, p, r, &faces)? { if let Some((f, na, nb)) = sandwiched_across(cx, a, b, p, r, ta, &faces)? {
faces.push(f); faces.push((f, na));
old.push(ta); old.push(na);
old.push(tb); old.push(nb);
removed_q = removed_q.min(worst(cx, &[na, nb])?);
grown = true; grown = true;
break; break;
} }
@@ -257,27 +303,28 @@ pub fn remove_face(cx: &mut TetComplex, t: u32, k: usize) -> Result<bool> {
/// A face `(p, r, x)` sandwiched between `a` and `b` (tetrahedra `a ∪ f` and `b ∪ f` both exist) /// A face `(p, r, x)` sandwiched between `a` and `b` (tetrahedra `a ∪ f` and `b ∪ f` both exist)
/// next to edge `pr`, not already in `faces`. /// next to edge `pr`, not already in `faces`.
fn sandwiched_across(cx: &mut TetComplex, a: u32, b: u32, p: u32, r: u32, faces: &[[u32; 3]]) -> Result<Option<([u32; 3], u32, u32)>> { fn sandwiched_across(cx: &TetComplex, a: u32, b: u32, p: u32, r: u32, ta: u32, faces: &[([u32; 3], u32)]) -> Result<Option<([u32; 3], u32, u32)>> {
let Some(ring) = cx.edge_ring(p, r)? else { return Ok(None) }; // ta = (a, p, r, y) holds the face set's face on edge pr; the other tetrahedron of a around
// Tetrahedra around pr containing a, and containing b. // pr in a face set's neighbourhood lies across its face (a, p, r).
for &ta in &ring.tets { let va = cx.tet(ta)?;
let va = cx.tet(ta)?; let Some(ky) = va.iter().position(|&v| v != a && v != p && v != r) else { return Ok(None) };
if !va.contains(&a) { let n = cx.adj(ta)?.get(ky).copied().unwrap_or(NONE);
continue; if n == NONE {
} return Ok(None);
let x = va.iter().copied().find(|&v| v != a && v != p && v != r).unwrap_or(NONE); }
if x == NONE || x == b { let vn = cx.tet(n)?;
continue; let x = vn.iter().copied().find(|&v| v != a && v != p && v != r).unwrap_or(NONE);
} if x == NONE || x == b || !vn.contains(&a) {
let f = [p, r, x]; return Ok(None);
if faces.iter().any(|g| sorted3(*g) == sorted3(f)) { }
continue; let f = [p, r, x];
} if faces.iter().any(|(g, _)| sorted3(*g) == sorted3(f)) {
let k = va.iter().position(|&v| v == a).unwrap_or(0); return Ok(None);
let tb = cx.adj(ta)?.get(k).copied().unwrap_or(NONE); }
if tb != NONE && cx.tet(tb)?.contains(&b) { let Some(k) = vn.iter().position(|&v| v == a) else { return Ok(None) };
return Ok(Some((f, ta, tb))); let tb = cx.adj(n)?.get(k).copied().unwrap_or(NONE);
} if tb != NONE && cx.tet(tb)?.contains(&b) {
return Ok(Some((f, n, tb)));
} }
Ok(None) Ok(None)
} }
+60 -14
View File
@@ -207,6 +207,42 @@ impl LocalStar {
measure::quality(&y) measure::quality(&y)
} }
/// The quality of tetrahedron `k` with the vertex at `p`, and its gradient there: analytic
/// for a valid tetrahedron, central differences with step `delta` for a flat or inverted one
/// (whose signed measure has no single smooth branch).
fn quality_gradient_at(&self, k: usize, p: P, delta: f64) -> (f64, P) {
let Some((x, i)) = self.tets.get(k) else { return (f64::NEG_INFINITY, [0.0; 3]) };
let mut y = *x;
if let Some(s) = y.get_mut(*i) {
*s = p;
}
let (q, g) = measure::quality_gradient(&y, *i);
if q > 0.0 {
return (q, g);
}
let mut g = [0.0; 3];
for (c, gc) in g.iter_mut().enumerate() {
let (mut a, mut b) = (p, p);
a[c] += delta;
b[c] -= delta;
*gc = (self.quality_at(k, a) - self.quality_at(k, b)) / (2.0 * delta);
}
(q, g)
}
/// The worst quality at `p` over the tetrahedra in `order`, or the first one found that is
/// not above `floor` (then the move is refused whatever the others are).
fn worst_above(&self, order: &[usize], p: P, floor: f64) -> f64 {
let mut w = f64::INFINITY;
for &k in order {
w = w.min(self.quality_at(k, p));
if !(w > floor) {
return w;
}
}
w
}
fn worst_at(&self, p: P) -> f64 { fn worst_at(&self, p: P) -> f64 {
(0..self.tets.len()).map(|k| self.quality_at(k, p)).fold(f64::INFINITY, f64::min) (0..self.tets.len()).map(|k| self.quality_at(k, p)).fold(f64::INFINITY, f64::min)
} }
@@ -215,7 +251,15 @@ impl LocalStar {
/// Optimization-based smoothing of `v`: raise the worst quality of its star (see the module /// Optimization-based smoothing of `v`: raise the worst quality of its star (see the module
/// docs). Returns whether it moved. /// docs). Returns whether it moved.
pub fn smooth_vertex(cx: &mut TetComplex, ctx: &Context, v: u32) -> Result<bool> { pub fn smooth_vertex(cx: &mut TetComplex, ctx: &Context, v: u32) -> Result<bool> {
if !cx.is_used(v) || cx.kind(v) == VertexKind::Corner { smooth_vertex_below(cx, ctx, v, f64::INFINITY)
}
/// [`smooth_vertex`], only when the worst tetrahedron around `v` is below `good` (Klingner and
/// Shewchuk smooth only around tetrahedra below their threshold).
pub fn smooth_vertex_below(cx: &mut TetComplex, ctx: &Context, v: u32, good: f64) -> Result<bool> {
// The optimization is a function of the star alone: one that found no move finds none
// again until a tetrahedron around `v` changes.
if !cx.is_used(v) || cx.kind(v) == VertexKind::Corner || cx.smoothing_known_to_fail(v) {
return Ok(false); return Ok(false);
} }
let star = cx.star(v)?; let star = cx.star(v)?;
@@ -237,25 +281,19 @@ pub fn smooth_vertex(cx: &mut TetComplex, ctx: &Context, v: u32) -> Result<bool>
if !(len > 0.0 && len.is_finite()) { if !(len > 0.0 && len.is_finite()) {
return Ok(false); return Ok(false);
} }
let delta = 1e-6 * len;
let mut x = x0; let mut x = x0;
let mut worst = local.worst_at(x); let mut worst = local.worst_at(x);
if !(worst < good) {
return Ok(false);
}
let mut moved = false; let mut moved = false;
for _ in 0..8 { for _ in 0..8 {
// Qualities and gradients (central differences) of the tetrahedra near the worst. // Qualities and analytic gradients of the tetrahedra near the worst.
let mut qs = Vec::with_capacity(local.tets.len()); let mut qs = Vec::with_capacity(local.tets.len());
for k in 0..local.tets.len() { for k in 0..local.tets.len() {
let q = local.quality_at(k, x); let q = local.quality_at(k, x);
let mut g = [0.0; 3]; let g = if q <= worst + 0.1 { restrict(local.quality_gradient_at(k, x, 1e-6 * len).1, f) } else { [0.0; 3] };
if q <= worst + 0.1 { qs.push((q, g));
for (c, gc) in g.iter_mut().enumerate() {
let (mut a, mut b) = (x, x);
a[c] += delta;
b[c] -= delta;
*gc = (local.quality_at(k, a) - local.quality_at(k, b)) / (2.0 * delta);
}
}
qs.push((q, restrict(g, f)));
} }
let active: Vec<P> = qs.iter().filter(|s| s.0 <= worst + 1e-4).map(|s| s.1).collect(); let active: Vec<P> = qs.iter().filter(|s| s.0 <= worst + 1e-4).map(|s| s.1).collect();
let d = min_norm(&active); let d = min_norm(&active);
@@ -272,10 +310,15 @@ pub fn smooth_vertex(cx: &mut TetComplex, ctx: &Context, v: u32) -> Result<bool>
alpha = alpha.min((q - worst) / (dd - gd)); alpha = alpha.min((q - worst) / (dd - gd));
} }
} }
// Trial positions check the worst tetrahedra first and stop at the first that does not
// beat the current worst.
let mut order: Vec<usize> = (0..qs.len()).collect();
let q_of = |i: usize| qs.get(i).map_or(f64::INFINITY, |s| s.0);
order.sort_by(|&i, &j| q_of(i).total_cmp(&q_of(j)).then(i.cmp(&j)));
let mut accepted = false; let mut accepted = false;
for _ in 0..6 { for _ in 0..6 {
let Some(p) = ctx.place(cx.kind(v), add(x, scale(d, alpha))) else { break }; let Some(p) = ctx.place(cx.kind(v), add(x, scale(d, alpha))) else { break };
let q = local.worst_at(p.coords()); let q = local.worst_above(&order, p.coords(), worst);
if p.coords() != x && q > worst && cx.move_is_valid(v, &p, &star)? { if p.coords() != x && q > worst && cx.move_is_valid(v, &p, &star)? {
cx.move_point(v, p)?; cx.move_point(v, p)?;
x = p.coords(); x = p.coords();
@@ -290,5 +333,8 @@ pub fn smooth_vertex(cx: &mut TetComplex, ctx: &Context, v: u32) -> Result<bool>
break; break;
} }
} }
if !moved {
cx.mark_smoothing_failed(v);
}
Ok(moved) Ok(moved)
} }
+60 -1
View File
@@ -443,7 +443,7 @@ mod determinism {
}) })
.fold(f64::INFINITY, f64::min); .fold(f64::INFINITY, f64::min);
assert!(sj >= 0.3, "min scaled Jacobian {sj}"); assert!(sj >= 0.3, "min scaled Jacobian {sj}");
assert_eq!(fingerprint(&m), 11_252_377_805_886_396_373, "{} tets, {} nodes", m.elements.len(), m.points.len()); assert_eq!(fingerprint(&m), 13_918_536_535_603_991_107, "{} tets, {} nodes", m.elements.len(), m.points.len());
} }
/// Listing the triangles, their corners and the vertices in another order, and numbering /// Listing the triangles, their corners and the vertices in another order, and numbering
@@ -507,3 +507,62 @@ fn the_gate_refuses_a_detached_tetrahedron() {
assert!(e.contains("share no face") && e.contains("(1, 2)"), "{e}"); assert!(e.contains("share no face") && e.contains("(1, 2)"), "{e}");
assert!(super::linear_gate(&p, &tets[..1], v / 2.0).is_ok()); assert!(super::linear_gate(&p, &tets[..1], v / 2.0).is_ok());
} }
/// Undoing an edge removal or a face removal puts every slot back as it was.
#[test]
fn rollback_restores_every_slot() {
use crate::cdt::{CdtOptions, tetrahedralize};
use crate::surface::{BoundarySurface, SurfaceOptions};
let (p, t, f) = determinism::washer();
let s = BoundarySurface::new(&p, &t, &f, &SurfaceOptions::default()).unwrap();
let cdt = tetrahedralize(&s, &CdtOptions::default()).unwrap();
let (mut cx, _) = crate::tet::complex_from_cdt(&s, &cdt, &crate::tet::QualityOptions::default()).unwrap();
let state = |cx: &TetComplex| -> Vec<([u32; 4], [u32; 4], [u32; 4])> {
(0..cx.slots() as u32).map(|t| (cx.tet(t).unwrap(), cx.adj(t).unwrap(), cx.tags(t).unwrap())).collect()
};
let mut changed = 0;
for (t, vs) in cx.live_tets().collect::<Vec<_>>() {
for &(i, j) in &EDGES {
for op in 0..2 {
let before = state(&cx);
cx.journal.recording = true;
let len = cx.journal.steps.len();
let did = if op == 0 { remove_edge_at(&mut cx, t, vs[i], vs[j]).unwrap() } else { remove_face(&mut cx, t, i).unwrap() };
changed += usize::from(did);
cx.rollback(len).unwrap();
cx.journal.steps.truncate(len);
cx.journal.recording = false;
assert_eq!(state(&cx), before, "tet {t} edge {i}{j} op {op}");
}
}
}
assert!(changed > 0);
}
/// The cached qualities match a fresh computation after improvement (smoothing, flips,
/// insertions and the rollbacks of rejected insertions).
#[test]
fn cached_qualities_match_fresh_ones() {
use crate::cdt::{CdtOptions, tetrahedralize};
use crate::size::UniformSize;
use crate::surface::{BoundarySurface, SurfaceOptions};
use crate::tet::geometry::Tessellation;
use crate::tet::refine::Context;
let (p, t, f) = determinism::washer();
let s = BoundarySurface::new(&p, &t, &f, &SurfaceOptions::default()).unwrap();
let cdt = tetrahedralize(&s, &CdtOptions::default()).unwrap();
let (mut cx, curve_faces) = crate::tet::complex_from_cdt(&s, &cdt, &crate::tet::QualityOptions::default()).unwrap();
let (geometry, size) = (Tessellation::new(&s, Vec::new()), UniformSize(3.0));
let ctx = Context { size: &size, geometry: &geometry, curve_faces: &curve_faces };
// Fill the cache, then change the mesh.
for (t, _) in cx.live_tets().collect::<Vec<_>>() {
cx.slot_quality(t).unwrap();
}
let o = crate::tet::improve::ImproveOptions { max_passes: 3, ..Default::default() };
let stats = crate::tet::improve::improve(&mut cx, &ctx, &o).unwrap();
assert!(stats.attempted > 0 && stats.smoothed > 0, "{stats:?}");
for (t, vs) in cx.live_tets().collect::<Vec<_>>() {
assert_eq!(cx.slot_quality(t).unwrap().to_bits(), measure::quality(&cx.coords(vs).unwrap()).to_bits());
}
cx.validate().unwrap();
}
+7 -2
View File
@@ -398,7 +398,8 @@ Delaunay methods with Voronoi/ODT improvement, then (2) all-hex and hex-dominant
- **Implemented** in `fieldcraft-mesh::tet::{ops, improve}` from the two papers: Klincsek's dynamic programming for edge - **Implemented** in `fieldcraft-mesh::tet::{ops, improve}` from the two papers: Klincsek's dynamic programming for edge
removal (interior edges; boundary edges of one flat B-rep face end in a 2-2 flip), multi-face removal grown greedily removal (interior edges; boundary edges of one flat B-rep face end in a 2-2 flip), multi-face removal grown greedily
over the sandwiched faces, nonsmooth optimization smoothing (active set, smallest vector of the convex hull of the over the sandwiched faces, nonsmooth optimization smoothing (active set, smallest vector of the convex hull of the
active gradients, line search; gradients by central differences), and the composite vertex insertion with rollback active gradients, line search; analytic gradients of the active measure, as §3.1 and Freitag–Ollivier-Gooch 1997
use), and the composite vertex insertion with rollback
(barycentres of boundary faces, of the tetrahedron, midpoints of boundary edges, and a point inside off a flat (barycentres of boundary faces, of the tetrahedron, midpoints of boundary edges, and a point inside off a flat
boundary edge). Deviations: the inserted vertex takes the Delaunay cavity rather than the optimal cut of §3.4; each pass boundary edge). Deviations: the inserted vertex takes the Delaunay cavity rather than the optimal cut of §3.4; each pass
smooths and flips the tetrahedra below sin 40° and inserts only when the worst did not improve; passes and the smooths and flips the tetrahedra below sin 40° and inserts only when the worst did not improve; passes and the
@@ -407,7 +408,11 @@ Delaunay methods with Voronoi/ODT improvement, then (2) all-hex and hex-dominant
angle fine, one corner nearly flat; Shewchuk 2002); the scaled Jacobian analysts check (Knupp 2001, B6.6) catches angle fine, one corner nearly flat; Shewchuk 2002); the scaled Jacobian analysts check (Knupp 2001, B6.6) catches
them, and with the weight it decides only near that limit. Every operation is a checked replacement of a region them, and with the weight it decides only near that limit. Every operation is a checked replacement of a region
(positive by exact `orient3d`, faces matched with opposite orientation, removed vertices fully inside), so the mesh is (positive by exact `orient3d`, faces matched with opposite orientation, removed vertices fully inside), so the mesh is
valid after each step. valid after each step. Speed (#84): an operation (topological, per tetrahedron, edge and face; or a smoothing) that
failed is not tried again until a tetrahedron it reads changes (a change clock per vertex; exact, because the
operations are functions of that region and the quality is computed on the corners in a canonical order), live
tetrahedra's qualities are cached, Klincsek's table starts at the quality to beat, and the composite insertion runs
its flips and neighbour smoothing only where a tetrahedron is below the threshold.
**A3.7 Cheng, Dey, Edelsbrunner, Facello, Teng 2000, Sliver exudation.** **Verdict: reference.** **A3.7 Cheng, Dey, Edelsbrunner, Facello, Teng 2000, Sliver exudation.** **Verdict: reference.**
- **Citation.** J. ACM 47(5):883–904, 2000. DOI [10.1145/355483.355487](https://doi.org/10.1145/355483.355487). - **Citation.** J. ACM 47(5):883–904, 2000. DOI [10.1145/355483.355487](https://doi.org/10.1145/355483.355487).
+2 -2
View File
@@ -31,7 +31,7 @@ Times are wall clock on the machine that ran it.
| bolted-assembly | plan (sweep), mesh.hex.generate (auto) | – | 6272 (6272 hex8) | 0.583 | 0.939 | 3.6 | 2.0e-4 | 0 | 0 (not relaxed) | 8 coincident, 0 cracks | – | 3.7 | pass | [x=50.000](skill/bolted-assembly.png) | | bolted-assembly | plan (sweep), mesh.hex.generate (auto) | – | 6272 (6272 hex8) | 0.583 | 0.939 | 3.6 | 2.0e-4 | 0 | 0 (not relaxed) | 8 coincident, 0 cracks | – | 3.7 | pass | [x=50.000](skill/bolted-assembly.png) |
| cantilever-i-beam | plan (sweep) | – | 61600 (61600 hex8) | 1.000 | 1.000 | 1.0 | 0.0e0 | 0 | 0 (not relaxed) | none | – | 8.5 | pass | [x=100.000](skill/cantilever-i-beam.png) | | cantilever-i-beam | plan (sweep) | – | 61600 (61600 hex8) | 1.000 | 1.000 | 1.0 | 0.0e0 | 0 | 0 (not relaxed) | none | – | 8.5 | pass | [x=100.000](skill/cantilever-i-beam.png) |
| cross-cylinders | none | – | – | – | – | – | – | – | – | – | – | 27.2 | no mesh | – | | cross-cylinders | none | – | – | – | – | – | – | – | – | – | – | 27.2 | no mesh | – |
| filleted-bracket | tet10 | – | 19991 (19991 tet10) | 0.318 | 0.564 | 4.1 | -6.8e-6 | 0 | 0 (not relaxed) | none | – | 15.1 | pass (fallback) | [x=30.000](skill/filleted-bracket.png) | | filleted-bracket | tet10 | – | 20020 (20020 tet10) | 0.320 | 0.562 | 4.1 | -6.8e-6 | 0 | 0 (not relaxed) | none | – | 12.7 | pass (fallback) | [x=30.000](skill/filleted-bracket.png) |
| filleted-bracket-blocks | plan (sweep) | – | 75410 (75410 hex8) | 0.704 | 0.954 | 3.5 | 3.7e-5 | 0 | 0 (not relaxed) | none (after merging 2348 nodes) | – | 6.9 | pass | [x=30.000](skill/filleted-bracket-blocks.png) | | filleted-bracket-blocks | plan (sweep) | – | 75410 (75410 hex8) | 0.704 | 0.954 | 3.5 | 3.7e-5 | 0 | 0 (not relaxed) | none (after merging 2348 nodes) | – | 6.9 | pass | [x=30.000](skill/filleted-bracket-blocks.png) |
| i-beam | plan (sweep) | – | 61600 (61600 hex8) | 1.000 | 1.000 | 1.0 | 0.0e0 | 0 | 0 (not relaxed) | none | – | 4.5 | pass | [z=100.000](skill/i-beam.png) | | i-beam | plan (sweep) | – | 61600 (61600 hex8) | 1.000 | 1.000 | 1.0 | 0.0e0 | 0 | 0 (not relaxed) | none | – | 4.5 | pass | [z=100.000](skill/i-beam.png) |
| loft-transition | plan (sweep) | – | 2873 (2873 hex8) | 0.189 | 0.959 | 1.7 | -2.1e-3 | 0 | 4 → 4 (4 relaxed, 0 undone; min SJ 0.189 → 0.189, max ratio 1.7 → 1.7) | none | – | 0.9 | fail (SJ) | [z=25.000](skill/loft-transition.png) | | loft-transition | plan (sweep) | – | 2873 (2873 hex8) | 0.189 | 0.959 | 1.7 | -2.1e-3 | 0 | 4 → 4 (4 relaxed, 0 undone; min SJ 0.189 → 0.189, max ratio 1.7 → 1.7) | none | – | 0.9 | fail (SJ) | [z=25.000](skill/loft-transition.png) |
@@ -40,7 +40,7 @@ Times are wall clock on the machine that ran it.
| rounded-box | plan (project) | 2 plane(s) | 11520 (11520 hex8) | 0.717 | 0.973 | 4.0 | -4.1e-3 | 0 | 0 (not relaxed) | none | – | 8.7 | pass | [x=20.000](skill/rounded-box.png) | | rounded-box | plan (project) | 2 plane(s) | 11520 (11520 hex8) | 0.717 | 0.973 | 4.0 | -4.1e-3 | 0 | 0 (not relaxed) | none | – | 8.7 | pass | [x=20.000](skill/rounded-box.png) |
| shallow-groove | plan (sweep) | – | 1960 (1960 hex8) | 0.991 | 0.999 | 1.1 | 2.7e-4 | 0 | 0 (not relaxed) | none | – | 0.2 | pass | [x=30.000](skill/shallow-groove.png) | | shallow-groove | plan (sweep) | – | 1960 (1960 hex8) | 0.991 | 0.999 | 1.1 | 2.7e-4 | 0 | 0 (not relaxed) | none | – | 0.2 | pass | [x=30.000](skill/shallow-groove.png) |
| sharp-wedge | plan (sweep) | – | 1674 (1674 hex8) | 0.014 | 0.301 | 149.6 | 2.7e-15 | 0 | 1524 (not relaxed) | none | – | 0.2 | fail (SJ, ratio) | [x=40.000](skill/sharp-wedge.png) | | sharp-wedge | plan (sweep) | – | 1674 (1674 hex8) | 0.014 | 0.301 | 149.6 | 2.7e-15 | 0 | 1524 (not relaxed) | none | – | 0.2 | fail (SJ, ratio) | [x=40.000](skill/sharp-wedge.png) |
| sphere-on-plate | tet10 | – | 25613 (25613 tet10) | 0.335 | 0.607 | 4.4 | -8.8e-5 | 0 | 0 (not relaxed) | none | – | 7.7 | pass (fallback) | [y=0.000](skill/sphere-on-plate.png) | | sphere-on-plate | tet10 | – | 24175 (24175 tet10) | 0.341 | 0.604 | 4.2 | -9.6e-5 | 0 | 0 (not relaxed) | none | – | 6.5 | pass (fallback) | [y=0.000](skill/sphere-on-plate.png) |
| spline-vase | plan (sweep) | – | 4536 (4536 hex8) | 0.665 | 0.945 | 9.9 | -6.6e-3 | 0 | 0 (not relaxed) | none | – | 2.3 | pass | [z=-35.000](skill/spline-vase.png) | | spline-vase | plan (sweep) | – | 4536 (4536 hex8) | 0.665 | 0.945 | 9.9 | -6.6e-3 | 0 | 0 (not relaxed) | none | – | 2.3 | pass | [z=-35.000](skill/spline-vase.png) |
| stepped-block | plan (cells) | – | 1416 (1416 hex8) | 1.000 | 1.000 | 1.3 | -4.8e-15 | 0 | 0 (not relaxed) | none | – | 1.6 | pass | [x=30.000](skill/stepped-block.png) | | stepped-block | plan (cells) | – | 1416 (1416 hex8) | 1.000 | 1.000 | 1.3 | -4.8e-15 | 0 | 0 (not relaxed) | none | – | 1.6 | pass | [x=30.000](skill/stepped-block.png) |
| thin-ribs | plan (sweep) | – | 301308 (301308 hex8) | 0.999 | 1.000 | 1.3 | -3.4e-12 | 0 | 0 (not relaxed) | none | – | 34.7 | pass | [x=50.000](skill/thin-ribs.png) | | thin-ribs | plan (sweep) | – | 301308 (301308 hex8) | 0.999 | 1.000 | 1.3 | -3.4e-12 | 0 | 0 (not relaxed) | none | – | 34.7 | pass | [x=50.000](skill/thin-ribs.png) |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 105 KiB