Wildmeshing Toolkit
Loading...
Searching...
No Matches
SmoothVertex.hpp
1#pragma once
2
3#include <wmtk/optimization/AMIPSEnergy.hpp>
4#include <wmtk/optimization/EnergySum.hpp>
5#include <wmtk/optimization/EnvelopeEnergy.hpp>
6#include <wmtk/optimization/solver.hpp>
7
8#include <wmtk/Types.hpp>
9#include <wmtk/envelope/Envelope.hpp>
10#include <wmtk/simplex/SimplexCollection.hpp>
11#include <wmtk/utils/Logger.hpp>
12#include <wmtk/utils/TetraQualityUtils.hpp>
13
14#include <array>
15#include <atomic>
16#include <limits>
17#include <memory>
18#include <vector>
19
20namespace wmtk::optimization {
21
31{
32 std::atomic<size_t> already_inverted{0};
33 std::atomic<size_t> envelope{0};
34 std::atomic<size_t> inverted{0};
35 std::atomic<size_t> quality{0};
36 std::atomic<size_t> accepted{0};
37
38 void reset()
39 {
41 envelope = 0;
42 inverted = 0;
43 quality = 0;
44 accepted = 0;
45 }
46
47 std::string to_string() const
48 {
49 return fmt::format(
50 "accepted {} | rejected: pre-inverted {}, envelope {}, inverted {}, quality {}",
51 accepted.load(),
52 already_inverted.load(),
53 envelope.load(),
54 inverted.load(),
55 quality.load());
56 }
57};
58
75{
77 static constexpr size_t kIterBuckets = 12;
79 static constexpr size_t kStatuses = 14;
80 static_assert(
81 int(polysolve::nonlinear::Status::UpdateDirectionFailed) == int(kStatuses) - 2,
82 "polysolve's Status enum changed; update kStatuses and status_name()");
83
84 std::array<std::atomic<size_t>, kIterBuckets> iterations{};
85 std::array<std::atomic<size_t>, kStatuses> status{};
86 std::atomic<size_t> iterations_total{0};
87 std::atomic<size_t> threw{0};
88
90 void record(const polysolve::nonlinear::Solver& s, const bool did_throw)
91 {
92 const size_t it = s.current_criteria().iterations;
93 iterations[std::min(it, kIterBuckets - 1)].fetch_add(1, std::memory_order_relaxed);
94 iterations_total.fetch_add(it, std::memory_order_relaxed);
95 const int st = int(s.status()) + 1;
96 if (st >= 0 && size_t(st) < kStatuses) {
97 status[size_t(st)].fetch_add(1, std::memory_order_relaxed);
98 }
99 if (did_throw) threw.fetch_add(1, std::memory_order_relaxed);
100 }
101
102 size_t solves() const
103 {
104 size_t n = 0;
105 for (const auto& c : iterations) n += c.load();
106 return n;
107 }
108
109 void reset()
110 {
111 for (auto& c : iterations) c = 0;
112 for (auto& c : status) c = 0;
113 iterations_total = 0;
114 threw = 0;
115 }
116
117 static const char* status_name(const size_t i)
118 {
119 static constexpr std::array<const char*, kStatuses> names = {
120 {"NotStarted",
121 "Continue",
122 "IterationLimit",
123 "XDeltaTolerance",
124 "RelXDeltaTolerance",
125 "FDeltaTolerance",
126 "GradNormTolerance",
127 "RelGradNormTolerance",
128 "NewtonDecrementTolerance",
129 "ObjectiveCustomStop",
130 "NanEncountered",
131 "NotDescentDirection",
132 "LineSearchFailed",
133 "UpdateDirectionFailed"}};
134 return i < kStatuses ? names[i] : "?";
135 }
136
140 std::string to_string() const
141 {
142 const size_t n = solves();
143 std::string out = fmt::format(
144 "solves={} mean_iterations={:.3g} | iterations",
145 n,
146 n > 0 ? double(iterations_total.load()) / double(n) : 0.);
147 for (size_t i = 0; i < kIterBuckets; ++i) {
148 out += i + 1 < kIterBuckets ? fmt::format(" {}={}", i, iterations[i].load())
149 : fmt::format(" >{}={}", i - 1, iterations[i].load());
150 }
151 out += " | stop";
152 for (size_t i = 0; i < kStatuses; ++i) {
153 if (status[i].load() != 0)
154 out += fmt::format(" {}={}", status_name(i), status[i].load());
155 }
156 out += fmt::format(" | threw={}", threw.load());
157 return out;
158 }
159};
160
172{
173 double w_amips = 1e-4;
174 double w_envelope = 1.0 - 1e-4;
175 double s_amips = 1.0;
176 double s_envelope = 1.0;
177
180 bool two_stage = true;
181
210
225 enum class SmoothingMode { Projected, Exact };
226 SmoothingMode smoothing_mode = SmoothingMode::Projected;
227
231
249 bool quality_veto = true;
250
251
268};
269
295template <class Mesh>
296bool smooth_vertex_3d(
297 Mesh& m,
298 const typename Mesh::Tuple& t,
299 const SmoothVertexOptions& opts,
300 std::unique_ptr<polysolve::nonlinear::Solver>& solver,
301 SmoothRejectCounters* counters = nullptr,
302 NewtonCounters* newton = nullptr)
303{
304 using Tuple = typename Mesh::Tuple;
305
306 const size_t vid = t.vid(m);
307 auto& VA = m.m_vertex_attribute;
308 const auto locs = m.get_one_ring_tets_for_vertex(t);
309 assert(!locs.empty());
310
311 double max_quality = 0.;
312 for (const Tuple& tet : locs) {
313 max_quality = std::max(max_quality, m.cell_quality(tet.tid(m)));
314 if (m.is_inverted_f(tet)) {
315 // A neighbour that is not rounded can leave a tet inverted in floats even
316 // though it is fine in exact arithmetic; there is nothing to optimize from.
317 if (counters) ++counters->already_inverted;
318 return false;
319 }
320 }
321
322 // AMIPS wants each tet as 12 doubles with the moving vertex first, so that the solver
323 // can overwrite the leading xyz.
324 std::vector<std::array<double, 12>> assembles(locs.size());
325 for (size_t i = 0; i < locs.size(); ++i) {
326 std::array<size_t, 4> local_verts = m.oriented_tet_vids(locs[i].tid(m));
327 local_verts = wmtk::orient_preserve_tet_reorder(local_verts, vid);
328 for (int k = 0; k < 4; k++) {
329 for (int j = 0; j < 3; j++) {
330 assembles[i][k * 3 + j] = VA[local_verts[k]].m_posf[j];
331 }
332 }
333 }
334
335 if (!solver) {
336 solver = create_basic_solver();
337 }
338
339 const double amips_w = opts.w_amips > 0 ? opts.s_amips * opts.w_amips : 1.0;
340 auto amips_energy = std::make_shared<AMIPSEnergy3D>(assembles, amips_w);
341
342 // An application-supplied term for this vertex. It carries its own weight, exactly as
343 // ExactDistanceEnergy3D below does, so it is summed here at 1. Null for TetWild and SimWild,
344 // which leaves `base_energy` as `amips_energy` and every expression below exactly what it
345 // was.
346 const std::shared_ptr<polysolve::nonlinear::Problem> extra_energy =
347 m.smoothing_extra_energy(vid);
348 std::shared_ptr<polysolve::nonlinear::Problem> base_energy = amips_energy;
349 if (extra_energy) {
350 auto sum = std::make_shared<EnergySum>();
351 if (opts.w_amips > 0) sum->add_energy(amips_energy);
352 sum->add_energy(extra_energy);
353 base_energy = sum;
354 }
355 std::shared_ptr<polysolve::nonlinear::Problem> total_energy = base_energy;
356
357 auto solve = [&]() {
358 VectorXd x = VA[vid].m_posf;
359 bool threw = false;
360 try {
361 solver->minimize(*total_energy, x);
362 } catch (const std::exception&) {
363 // polysolve reports a failed line search by throwing; the position it reached
364 // is still the best it found, and the checks below decide whether to keep it.
365 threw = true;
366 }
367 if (newton) newton->record(*solver, threw);
368 VA[vid].m_posf = x;
369 };
370
371 const std::shared_ptr<SampleEnvelope> pull_env =
372 VA[vid].m_is_on_surface ? m.smoothing_energy_envelope(vid) : nullptr;
373
374 if (pull_env && opts.smoothing_mode == SmoothVertexOptions::SmoothingMode::Projected) {
375 // Smooth as if the vertex were interior, then walk back onto the input.
376 const Vector3d x_orig = VA[vid].m_posf;
377 total_energy = base_energy;
378 solve();
379 const Vector3d x_new = VA[vid].m_posf;
380
381 // Place a candidate and report the worst incident quality, or infinity if it
382 // inverts. is_inverted is exact, so the rational position tracks every candidate.
383 const auto worst_at = [&](const Vector3d& p) {
384 VA[vid].m_posf = p;
385 VA[vid].m_pos = to_rational(p);
386 double mq = 0.;
387 for (const Tuple& loc : locs) {
388 if (m.is_inverted(loc)) {
389 return std::numeric_limits<double>::infinity();
390 }
391 mq = std::max(mq, m.get_quality(loc));
392 }
393 return mq;
394 };
395
396 bool accepted = false;
397 std::vector<Vector3d> interp, proj; // kept for the nested pass below
398 interp.reserve(opts.project_line_search_steps);
399 proj.reserve(opts.project_line_search_steps);
400 for (int k = 0; k < opts.project_line_search_steps; ++k) {
401 const Vector3d p = x_orig + std::pow(0.5, k) * (x_new - x_orig);
402 Vector3d q;
403 pull_env->nearest_point(p, q);
404 interp.push_back(p);
405 proj.push_back(q);
406 if (worst_at(q) < max_quality) {
407 accepted = true;
408 break;
409 }
410 }
411
412 // Nothing ON the input was acceptable anywhere, so settle for getting as close to
413 // it as the one-ring allows. For each candidate, bisect the segment from the
414 // interpolated point (s = 0) to its projection (s = 1) for the LARGEST acceptable s.
415 // s = 1 is already known to fail -- that is what the first pass just established --
416 // so this brackets the boundary and converges up to it from below. Halving s down
417 // from 1 instead would cap the result at the midpoint and leave the vertex needlessly
418 // far from the input.
419 if (!accepted && opts.project_line_search_nested_steps > 0) {
420 for (size_t k = 0; k < proj.size() && !accepted; ++k) {
421 double lo = 0.0, hi = 1.0; // lo: best acceptable so far, hi: known bad
422 Vector3d best;
423 bool found = false;
424 for (int j = 0; j < opts.project_line_search_nested_steps; ++j) {
425 const double mid = 0.5 * (lo + hi);
426 const Vector3d cand = interp[k] + mid * (proj[k] - interp[k]);
427 if (worst_at(cand) < max_quality) {
428 lo = mid;
429 best = cand;
430 found = true;
431 } else {
432 hi = mid;
433 }
434 }
435 if (found) {
436 // worst_at left the vertex at the last candidate tried, which is not
437 // necessarily the best one.
438 worst_at(best);
439 accepted = true;
440 }
441 }
442 }
443 if (!accepted) {
444 VA[vid].m_posf = x_orig;
445 VA[vid].m_pos = to_rational(x_orig);
446 if (counters) ++counters->quality;
447 return false;
448 }
449 } else if (pull_env) {
450 auto envelope_energy =
451 std::make_shared<ExactDistanceEnergy3D>(pull_env, opts.s_envelope * opts.w_envelope);
452
453 if (opts.two_stage) {
454 auto warmup = std::make_shared<EnergySum>();
455 if (opts.w_amips > 0) warmup->add_energy(amips_energy, 1. / opts.w_amips);
456 if (opts.w_envelope > 0) warmup->add_energy(envelope_energy, 1. / opts.w_envelope);
457 if (extra_energy) warmup->add_energy(extra_energy);
458 total_energy = warmup;
459 solve();
460 }
461
462 auto weighted = std::make_shared<EnergySum>();
463 if (opts.w_amips > 0) weighted->add_energy(amips_energy);
464 if (opts.w_envelope > 0) weighted->add_energy(envelope_energy);
465 if (extra_energy) weighted->add_energy(extra_energy);
466 total_energy = weighted;
467 solve();
468 } else {
469 solve();
470 }
471
472 // Containment: every surface triangle at this vertex must still be inside. Checked
473 // against the containment envelope, which is not necessarily the one it was pulled to.
474 const std::shared_ptr<SampleEnvelope> check_env =
475 VA[vid].m_is_on_surface ? m.smoothing_containment_envelope(vid) : nullptr;
476 if (check_env) {
477 const simplex::SimplexCollection surf = m.get_surface_faces_for_vertex(vid);
478 for (const simplex::Face& f : surf.faces()) {
479 const std::array<Eigen::Vector3d, 3> face = {
480 {VA[f.vertices()[0]].m_posf,
481 VA[f.vertices()[1]].m_posf,
482 VA[f.vertices()[2]].m_posf}};
483 if (check_env->is_outside(face)) {
484 if (counters) ++counters->envelope;
485 return false;
486 }
487 }
488 }
489
490 // The rational position must be current before the exact inversion test.
491 VA[vid].m_pos = to_rational(VA[vid].m_posf);
492
493 double max_after_quality = 0.;
494 for (const Tuple& loc : locs) {
495 if (m.is_inverted(loc)) {
496 if (counters) ++counters->inverted;
497 return false;
498 }
499 const size_t tid = loc.tid(m);
500 const double quality = m.get_quality(loc);
501 m.set_cell_quality(tid, quality);
502 max_after_quality = std::max(max_after_quality, quality);
503 }
504
505 if (opts.quality_veto && (!VA[vid].m_is_on_surface || opts.quality_veto_on_surface)) {
506 if (max_after_quality > max_quality) {
507 if (counters) ++counters->quality;
508 return false;
509 }
510 }
511
512 if (counters) ++counters->accepted;
513 return true;
514}
515
516
534template <class Mesh>
535bool smooth_vertex_2d(
536 Mesh& m,
537 const typename Mesh::Tuple& t,
538 const SmoothVertexOptions& opts,
539 std::unique_ptr<polysolve::nonlinear::Solver>& solver,
540 SmoothRejectCounters* counters = nullptr)
541{
542 const size_t vid = t.vid(m);
543 auto& VA = m.m_vertex_attribute;
544 auto& FA = m.m_face_attribute;
545
546 const std::vector<size_t>& locs = m.get_one_ring_fids_for_vertex(t);
547 assert(!locs.empty());
548
549 double max_quality = 0.;
550 for (const size_t fid : locs) {
551 max_quality = std::max(max_quality, FA[fid].m_quality);
552 if (m.is_inverted_f(fid)) {
553 // Nothing to optimize from: a neighbour that is not rounded can leave a face
554 // inverted in floats even when it is fine exactly.
555 if (counters) ++counters->already_inverted;
556 return false;
557 }
558 }
559
560 // AMIPS wants each face as 6 doubles with the moving vertex first, keeping the winding.
561 std::vector<std::array<double, 6>> assembles;
562 assembles.reserve(locs.size());
563 for (const size_t fid : locs) {
564 std::array<size_t, 3> vs = m.oriented_tri_vids(fid);
565 size_t v_loc = 0;
566 for (size_t i = 0; i < 3; ++i) {
567 if (vs[i] == vid) {
568 v_loc = i;
569 break;
570 }
571 }
572 const std::array<size_t, 3> buf = vs;
573 vs[0] = buf[v_loc];
574 vs[1] = buf[(v_loc + 1) % 3];
575 vs[2] = buf[(v_loc + 2) % 3];
576
577 std::array<double, 6> T;
578 for (int i = 0; i < 3; i++) {
579 const Vector2d p = m.smoothing_position(vs[i]);
580 T[i * 2] = p[0];
581 T[i * 2 + 1] = p[1];
582 }
583 assembles.push_back(T);
584 }
585
586 if (!solver) {
587 solver = create_basic_solver();
588 }
589
590 const double amips_w = opts.w_amips > 0 ? opts.s_amips * opts.w_amips : 1.0;
591 auto amips_energy = std::make_shared<AMIPSEnergy2D>(assembles, amips_w);
592
593 // An application-supplied term for this vertex. It carries its own weight, exactly as
594 // ExactDistanceEnergy2D below does, so it is summed here at 1. Null for TriWild and
595 // SimWild, which leaves `base_energy` as `amips_energy` and every expression below exactly
596 // what it was.
597 const std::shared_ptr<polysolve::nonlinear::Problem> extra_energy =
598 m.smoothing_extra_energy(vid);
599 std::shared_ptr<polysolve::nonlinear::Problem> base_energy = amips_energy;
600 if (extra_energy) {
601 auto sum = std::make_shared<EnergySum>();
602 if (opts.w_amips > 0) sum->add_energy(amips_energy);
603 sum->add_energy(extra_energy);
604 base_energy = sum;
605 }
606 std::shared_ptr<polysolve::nonlinear::Problem> total_energy = base_energy;
607
608 auto solve = [&]() {
609 VectorXd x = m.smoothing_position(vid);
610 try {
611 solver->minimize(*total_energy, x);
612 } catch (const std::exception&) {
613 // A failed line search is reported by throwing; the position reached is still
614 // the best found, and the checks below decide whether to keep it.
615 }
616 m.set_smoothing_position(vid, Vector2d(x));
617 };
618
619 // Neighbours along the incident surface edges, captured before the solve. Only `vid`
620 // moves, so their positions are the same either way, but taking them first matches what
621 // both applications did and keeps the assert below meaningful.
622 std::vector<Vector2d> surf_neighbors;
623 if (VA[vid].m_is_on_surface) {
624 const simplex::SimplexCollection es = m.get_surface_edges_for_vertex(vid);
625 surf_neighbors.reserve(es.edges().size());
626 for (const simplex::Edge& e : es.edges()) {
627 const auto& evs = e.vertices();
628 surf_neighbors.push_back(m.smoothing_position(evs[0] != vid ? evs[0] : evs[1]));
629 }
630 assert(!surf_neighbors.empty());
631 }
632
633 // Per-vertex rather than the mesh's single m_envelope, and split into the PULL and the
634 // CONTAINER exactly as the 3D path above: see TriOptimizerMesh::smoothing_energy_envelope.
635 // The defaults return the old expression, so TriWild and SimWild are unaffected.
636 const std::shared_ptr<SampleEnvelope> envelope = m.smoothing_energy_envelope(vid);
637
638 if (envelope && opts.smoothing_mode == SmoothVertexOptions::SmoothingMode::Projected) {
639 const Vector2d x_orig = m.smoothing_position(vid);
640 total_energy = base_energy;
641 solve();
642 const Vector2d x_new = m.smoothing_position(vid);
643
644 // set_smoothing_position keeps the rational position in step, which the exact
645 // is_inverted below depends on.
646 const auto worst_at = [&](const Vector2d& p) {
647 m.set_smoothing_position(vid, p);
648 double mq = 0.;
649 for (const size_t fid : locs) {
650 if (m.is_inverted(fid)) {
651 return std::numeric_limits<double>::infinity();
652 }
653 mq = std::max(mq, m.get_quality(fid));
654 }
655 return mq;
656 };
657
658 bool accepted = false;
659 std::vector<Vector2d> interp, proj; // kept for the nested pass below
660 interp.reserve(opts.project_line_search_steps);
661 proj.reserve(opts.project_line_search_steps);
662 for (int k = 0; k < opts.project_line_search_steps; ++k) {
663 const Vector2d p = x_orig + std::pow(0.5, k) * (x_new - x_orig);
664 Vector2d q;
665 envelope->nearest_point(p, q);
666 interp.push_back(p);
667 proj.push_back(q);
668 if (worst_at(q) < max_quality) {
669 accepted = true;
670 break;
671 }
672 }
673
674 // Nothing ON the input was acceptable anywhere, so settle for getting as close to
675 // it as the one-ring allows. For each candidate, bisect the segment from the
676 // interpolated point (s = 0) to its projection (s = 1) for the LARGEST acceptable s.
677 // s = 1 is already known to fail -- that is what the first pass just established --
678 // so this brackets the boundary and converges up to it from below. Halving s down
679 // from 1 instead would cap the result at the midpoint and leave the vertex needlessly
680 // far from the input.
681 if (!accepted && opts.project_line_search_nested_steps > 0) {
682 for (size_t k = 0; k < proj.size() && !accepted; ++k) {
683 double lo = 0.0, hi = 1.0; // lo: best acceptable so far, hi: known bad
684 Vector2d best;
685 bool found = false;
686 for (int j = 0; j < opts.project_line_search_nested_steps; ++j) {
687 const double mid = 0.5 * (lo + hi);
688 const Vector2d cand = interp[k] + mid * (proj[k] - interp[k]);
689 if (worst_at(cand) < max_quality) {
690 lo = mid;
691 best = cand;
692 found = true;
693 } else {
694 hi = mid;
695 }
696 }
697 if (found) {
698 // worst_at left the vertex at the last candidate tried, which is not
699 // necessarily the best one.
700 worst_at(best);
701 accepted = true;
702 }
703 }
704 }
705 if (!accepted) {
706 m.set_smoothing_position(vid, x_orig);
707 if (counters) ++counters->quality;
708 return false;
709 }
710 } else if (envelope) {
711 auto envelope_energy =
712 std::make_shared<ExactDistanceEnergy2D>(envelope, opts.s_envelope * opts.w_envelope);
713
714 if (opts.two_stage) {
715 auto warmup = std::make_shared<EnergySum>();
716 if (opts.w_amips > 0) warmup->add_energy(amips_energy, 1. / opts.w_amips);
717 if (opts.w_envelope > 0) warmup->add_energy(envelope_energy, 1. / opts.w_envelope);
718 if (extra_energy) warmup->add_energy(extra_energy);
719 total_energy = warmup;
720 solve();
721 }
722
723 auto weighted = std::make_shared<EnergySum>();
724 if (opts.w_amips > 0) weighted->add_energy(amips_energy);
725 if (opts.w_envelope > 0) weighted->add_energy(envelope_energy);
726 if (extra_energy) weighted->add_energy(extra_energy);
727 total_energy = weighted;
728 solve();
729 } else {
730 solve();
731 }
732
733 // Per-vertex positional constraint, on top of the envelope. A mesh uses this to pin a
734 // vertex to a 0-dimensional feature it stands for -- within a ball, so the vertex is
735 // still free to move and improve quality, it just cannot walk away from the feature.
736 // Meshes with no such features answer true unconditionally.
737 if (!m.smoothing_position_is_allowed(vid, m.smoothing_position(vid))) {
738 if (counters) ++counters->envelope;
739 return false;
740 }
741
742 // Containment, edge by edge rather than face by face as in 3D -- and against the
743 // CONTAINMENT envelope, which is not necessarily the one the vertex was pulled to.
744 if (const std::shared_ptr<SampleEnvelope> hold_env = m.smoothing_containment_envelope(vid)) {
745 const Vector2d p = m.smoothing_position(vid);
746 for (const Vector2d& q : surf_neighbors) {
747 const std::array<Eigen::Vector2d, 2> edge = {{p, q}};
748 if (hold_env->is_outside(edge)) {
749 if (counters) ++counters->envelope;
750 return false;
751 }
752 }
753 }
754
755 double max_after_quality = 0.;
756 for (const size_t fid : locs) {
757 if (m.is_inverted(fid)) {
758 if (counters) ++counters->inverted;
759 return false;
760 }
761 const double q = m.get_quality(fid);
762 FA[fid].m_quality = q;
763 max_after_quality = std::max(max_after_quality, q);
764 }
765
766 if (opts.quality_veto && (!VA[vid].m_is_on_surface || opts.quality_veto_on_surface)) {
767 if (max_after_quality > max_quality) {
768 if (counters) ++counters->quality;
769 return false;
770 }
771 }
772
773 if (counters) ++counters->accepted;
774 return true;
775}
776
777} // namespace wmtk::optimization
How the vertex solves of a smoothing pass ended: Newton iterations taken and the status polysolve sto...
Definition SmoothVertex.hpp:75
void record(const polysolve::nonlinear::Solver &s, const bool did_throw)
One solve that just returned from minimize(), or threw out of it (did_throw).
Definition SmoothVertex.hpp:90
std::string to_string() const
Definition SmoothVertex.hpp:140
static constexpr size_t kStatuses
polysolve::nonlinear::Status, NotStarted (-1) .. UpdateDirectionFailed (12), shifted by 1.
Definition SmoothVertex.hpp:79
static constexpr size_t kIterBuckets
0 .. 10 iterations, and one bucket for more than 10.
Definition SmoothVertex.hpp:77
Why a smoothing attempt was refused, counted per pass.
Definition SmoothVertex.hpp:31
std::atomic< size_t > envelope
a surface triangle left the envelope
Definition SmoothVertex.hpp:33
std::atomic< size_t > already_inverted
incident tet inverted in floats on entry
Definition SmoothVertex.hpp:32
std::atomic< size_t > quality
the move made the worst incident tet worse
Definition SmoothVertex.hpp:35
std::atomic< size_t > inverted
the move inverted a tet (exact predicate)
Definition SmoothVertex.hpp:34
Weights and policy for smooth_vertex_3d.
Definition SmoothVertex.hpp:172
bool quality_veto_on_surface
Definition SmoothVertex.hpp:209
int project_line_search_steps
Definition SmoothVertex.hpp:230
int project_line_search_nested_steps
Definition SmoothVertex.hpp:267
bool quality_veto
Definition SmoothVertex.hpp:249
bool two_stage
Definition SmoothVertex.hpp:180
SmoothingMode
Definition SmoothVertex.hpp:225