---
# System prepended metadata

title: WMTK

---

# WMTK

2026-04-02
* [x] Python bindings: extra interface for tetwild? Not now (it would be premature)
* [ ] Tetwild - Union (Daniel)
    * compute winding number for each input mesh (not the tracked surface)
    * assign tag (based on position in input mesh vector) to tets according to the winding number
* [ ] Cleaning up warnings (Daniel)
* [ ] Make WMTK compile with VS 2026
* [ ] ~~Check if lagrange is needed at all. We can collapse non-manifold meshes.~~
* [ ] Add topology preservation to tetwild (Daniel)
* [ ] Integration tests (TBD)
    * copy from [polyfem](https://github.com/polyfem/polyfem/blob/main/tests/verify_run.cpp)
    * add criteria for success to the components
    * throw an exception if criteria are not met
    * integration test passes if no exception was thrown
* [ ] topological offsets (Spencer)
    * implement the first part of topological offsets (without optimizing the mesh for distance and normal deviation)
* [ ] Prism-FEA port to WMTK
    * wait for first part of topological offsets is done

People that can help: Spencer, Sachin, Uday
* Manifold extraction on triangle surfaces - duplicate non-manifold vertices (Sachin?)

Others:
* [ ] Geogram dependency inside fast-envelope does not support newest MSVC version v145
* [ ] CMake 4 is not supported. We need to add `CMAKE_POLICY_VERSION_MINIMUM=3.5`.

---
Topology Preservation
* [ ] There is already a check to not collapse an edge in between two surfaces
* [ ] Add link condition for surface
* [ ] Do not collapse two non-manifold edges into each other
    * [ ] edge must be on a surface and itself manifold
    * [ ] vertices must be incident to non-manifold edges (order 2 or higher)
* [ ] Add link condition for non-manifold edges
* [ ] Store order of a vertex, two order 3 vertices (endpoints of a non-manifold polyline) cannot be collapsed into each other
* [ ] Proof of convergence for TetWild?
* [ ] Smooth project not on surface
* [ ] AI?
    * [ ] AI Segmentation project
    * [ ] Use Image Simulation on such a project
    * [ ] NERF -> Segmentation -> Materials + Annotation

Add
```
order_of_edge(const Tuple& t) const; // compute on the fly from incident surface faces
order_of_vertex(const Tuple& t) const; // store as vertex attribute
order_of_face(const Tuple& t) const; // return facew attribute is_surface
```
I think it comes down to: 
1. never collapse an edge if it has lower order than both incident vertices
2. perform link condition based on order of edge: (0) none (1) surface (2) (non-manifold/boundary) edges

---
Pre-Siggraph Deadline

* [ ] Add examples to readme

2025-12-17
* [ ] Add Marco's code - bug seems to be fixed
* [ ] Run TW and WMTK on Thingi10k
* [x] Fix Tet-Remeshing based on WMTK changes
* [ ] Fix sample envelope
* [ ] ~~Compare sizing field adaptation~~ - postponed
* [ ] TW has failure cases just like WMTK

2025-12-10
* [x] Swap add back neighboring edges
* [x] Look at open boundary treatment in original TW
* [x] clean up all the fid calls in `collapse_edge_before`
* [x] Original TW only keeps boundary vertices in envelope, not the entire edge
* [x] Add `is_boundary_point()` to smooth after function

2025-12-03
* [x] copied original TetWild mesh improvement code to WMTK
* [x] compare all operations of WMTK and TW
* [x] Swap faces
    * [x] uses `get_edges()` instead of `get_faces()` - that's probably a bug
* [x] `operation_update_connectivity_impl` <- not crucial, only a small amount of runtime spent here
    * [x] replace `std::map<size_t, VertexConnectivity>` by a vector
    * [x] `wmtk::set_erase` and `wmtk::set_insert` on `vert_conn` are extremely inefficient. Combine these into a single modification of `vert_conn`. Maybe use `std::set_difference` and `std::set_union` instead
* [x] `collapse_edge_conn`
    * [x] Check if topology checks can be removed if AMIPS is checked before collapse
* [x] `collapse_edge_check_topology` should never be necessary - make that a sanity check

TetWild
* [ ] TetWild adds all changed edges to the priority queue

**2025-11-12**
* [ ] Sizing field adjustment

**2025-10-20**
* [ ] Isotropic Remeshing
* [ ] Rename `RawSimplex` to `Simplex`
* [ ] Use `Simplex` to clean up operations in TetWild
* [ ] Implement navigation based on `Simplex`
    * [ ] cofaces single dimension
    * [ ] faces single dimension
    * [ ] link
    * [ ] open/closed star
    * [ ] All navigation as a method of `TetMesh`/`TriMesh` (returning a `SimplexCollection`) and as a method of `SimplexCollection`.

**2025-10-08**
* [ ] Jiacheng -- Multimesh of Tet + embedded Tri
    * [ ] Two separate meshes
    * [ ] Map in between vertices
* [x] Add a `predicates.hpp` with things like `are_points_collinear` and others from Marco's paper "Convex polyhedral meshing for robust solid modeling"

**2025-10-01**
* [ ] Vertex smoothing on boundary is missing
* [ ] Orig TW was reusing deleted IDs in `splitAnEdge`
* [x] Add randomization of smoothing in serial execution
* [ ] Try `std::nextafter` randomly in rounding
* [x] Try always only projecting half the distance back <- does not work well
* [x] Investigate failing cases

**2025-09-24**
* [x] add output checks (Hausdorff, topology)
    * [x] Hausdorff
        * [ ] should be less than eps + simpl_eps
        * [ ] could be reported relative to eps
        * [ ] use containment check from input to tracked surface
    * [x] Topology test
        * [x] number of connected components
        * [x] euler characteristic (EC) for each component
        * [x] sort list of ECs and compare with input (after embedding)
    * [x] Rounded vertices
        * [x] Check that all vertices are rounded
        * [x] Log error (or even throw exception) if they are not
* [ ] test topology preserving tetwild
* [ ] Test if results are still good (maybe even better) if smoothing does not enforce quality improvement. Currently, a large envelope can lead to horrible surface quality as vertices might not be projected back onto the surface.

**2025-09-17**
* Duplicate non-manifold edges and vertices
    * TriMesh with tags
    * Tet meshes
* What to do with integration tests?
    * For now: termination only
    * In the future: Hausdorff and other metrics comparing with a provided output

**2025-09-03 Meeting**

Potential student tasks:
* Test applications
    1. Build code on Sonic and try to run the examples from [reproduce_scripts.sh](https://github.com/wildmeshing/wildmeshing-toolkit/blob/main/reproduce_scripts.sh).
    2. Visually confirm correctness of the results.
    3. Set up a Python script to compute: average edge length, triangle area/tet volume, bbox diagonal length, barycenter
    4. Set up scripts on Greene to build and run the applications for the entire Thingi10k dataset
    5. Set up scripts to evaluate the results for each application, e.g., plot histograms similar to Fig. 9 in the [paper](https://cims.nyu.edu/gcl/papers/2022-WildMeshingToolkit.pdf)
* Remove lagrange dependency
    1. Lagrange is only used to resolve non-manifoldness. IGL has a function that should also be capable of doing the same thing but it failed the [unit test](https://github.com/wildmeshing/wildmeshing-toolkit/blob/60fee8c592a5b7cccb799070f789f06da50ace52/tests/test_manifold.cpp#L34) when I tried.
    2. Reimplement "resolve non-manifoldness" without using geogram. Required IGL functions can be copied into our toolkit. Test on basic examples.
    3. The above mentioned unit test might fail as it seems to have some bug. Figure out this bug and fix it (locally in the copied function)
    4. Optionally, create a pull request for IGL with the suggested fix.
* Unit tests
    1. Add basic mesh examples to `src/wmtk/utils/examples`
    2. Update `test_tuple.cpp` using the examples and add more tests for switching tuples. Tests should also cover non-manifold scenarios and boundaries.
    3. Rename `test_operations.cpp` to `test_tet_operations.cpp`, add missing test scenarios, and add similar tests for TriMesh in a new file called `test_tri_operations.cpp`
    4. Add navigation functionality (link, open star, closed star, etc.) to `src/wmtk/simplex`. Alternatively, navigation could also be member functions of `TriMesh` and `TetMesh`. <-- needs to be discussed
    5. Add tests for navigation

---
* [ ] Look at [SPACK](https://spack-tutorial.readthedocs.io/en/latest/tutorial_basics.html)

**2025-08-06 Meeting**

TODOs:
* [ ] Thingi10k test runs (Jiacheng/students?)
* [ ] Unit tests (Daniel)
    * [ ] Single-threaded (with binary checks on one system)
    * [ ] operations
    * [ ] navigation
    * [ ] Port (important) unit tests from deprecated WMTK
* [ ] Integration tests (Teseo)
    * [ ] Application specific metrics (AMIPS, edge length, area, bbox diag, barycenter)
    * [ ] prevent input, reference output, and JSON input file
    * [ ] list with integration tests, consisting of the JSON input file, metric's tolerance
    * [ ] output file must have a specific name, e.g., out.msh
* [ ] "Documentation" (Teseo)
    * [ ] Doxygen auto deploy
    * [ ] Code coverage
* [ ] Design document
    * [ ] README in every folder (Daniel)
* [ ] Profiling
* [ ] Remove TBB (Jiacheng)
    * [ ] remove dynamic allocation
* [ ] Dependencies
    * [ ] geogram remove
        * [ ] used in the [envelope](https://github.com/wildmeshing/wildmeshing-toolkit/blob/main/src/wmtk/envelope/Envelope.cpp)
        * [ ] KD-tree in multiple applications, e.g., [here](https://github.com/wildmeshing/wildmeshing-toolkit/blob/main/app/interior_tet_opt/main.cpp)
    * [ ] tracy remove
    * [ ] lagrange remove
        * [ ] [non-manifold unit test](https://github.com/wildmeshing/wildmeshing-toolkit/blob/60fee8c592a5b7cccb799070f789f06da50ace52/tests/test_manifold.cpp#L34)
        * [ ] [obj from test](https://github.com/wildmeshing/data/blob/main/37989_sf.obj)
    * [ ] IGL extract relevant parts
        * [ ] copy in predicates from deprecated WMTK
    * [ ] clean up warnings
    * [ ] Marcos dependencies
* [ ] Restructure
    * [ ] Finished applications to `src/components`
    * [ ] folders + namespaces
    * [ ] (Application for Python)
* [ ] Rational - add float

later:
* [ ] Features
    * [ ] tag multimesh
    * [ ] **triangle insertion**
    * [ ] navigation
    * [ ] non-manifold mesh operations
    * [ ] offset code
    * [ ] (swap)
    * [ ] halfedge datastructure
* [ ] [Zach's wmtk version](https://github.com/polyfem/wildmeshing-toolkit)

For Daniel:
* [non-manifold unit test](https://github.com/wildmeshing/wildmeshing-toolkit/blob/60fee8c592a5b7cccb799070f789f06da50ace52/tests/test_manifold.cpp#L34)
* CMake presets

---

**2025-07-02 Update**
Optional parameters in `JSE`? No, use `bool` instead to clarify if something is optional.

---

* [x] Image Simulation
    * [x] Set up MSH input + conversion tool for different input types (start with PNG -> TriMesh)
    * [x] Read input MSH with tags
    * [ ] Modified tetwild for mesh with tags in different dimensions
        * [x] only tags on tets (for now)
    * [x] Write output MSH
    * [ ] MSH input with entities instead of tags
    * [ ] Simplification
        * [ ] Simplify once for tet generation
        * [ ] Smooth fine input surface (with large envelope) and simplify (tight envelope); use the result as tetwild envelope. This is the surface all points are projected onto.
        * [ ] Try to project the embedded surface onto the smoothed input
        * [ ] Tetwild envelope must be larger than the smooth+simplify envelopes together.
* [x] branch "main" --> "deprecated"
* [x] branch "new-old" --> "main"
* [x] check Github default branch
* [x] Update unit tests
* [ ] move components to main src folder
* [x] DO NOT Create `Mesh` base class
* [x] DO NOT Create `Tuple` used by `TriMesh` & `TetMesh`
* [ ] Set up simplicial embedding
    * [x] Start with 2D TriMesh
    * [x] Add paraviewo interface for Tri
    * [x] Add paraviewo interface for Tet
    * [x] Add navigation and other utils if necessary
    * [ ] Add msh output
    * [x] Add `split_face` (copy from `TetMesh`)
    * [ ] When fully tested, implement multimesh version
        * [ ] Add `MMTriMesh`
        * [ ] `virtual mm_vertex_tag(int64_t id)`, `mm_edge_tag`, `mm_face_tag`
        * [ ] `virtual mm_split_before/after()` in `TriMesh` and final override in `MMTriMesh`
    * [ ] Simplicial Embedding for 3D TetMesh
    * [x] Add `split_face` and `split_tet`
* MSH IO
* Set up python interface

Multimesh Projects
* Jiacheng C1
* Mine Offsets
* Topology Conserving Tetwild
* Image Simulation
* Prism dominant meshing

Figure out how to implement multimesh
* Inheritance
    * `MMTetMesh` 
    * function overloading on mesh to get tag attribute
* Templating
    * the attributes

---

Apps:
* [x] Tetwild
* [x] Incremental Tetwild
* [x] SEC
* [x] QSlim
* [x] Remeshing
* [x] Harmonic Tet
* [x] Interior Tetop
    * [ ] cannot load harmonic tet output

### 2025-06-05
#### OLD WMTK TODOs

Structural refactoring:
* [ ] Unify tests app in `wmtk_component_tests`
* [ ] Merge TriMesh and TetMesh tuples
* [ ] Clean up dependencies - remove everything unnecessary
* [ ] Unify app - all apps need to take only a JSON as input style could be something like `wmtk -a tetwild -j spec.json`, or just `wmtk -j spec.json` and then the JSON must have an `application` entry.
* [ ] move `switch` functions into mesh class
* [ ] Clean up warnings
* [ ] use `int64_t` instead of `int`

Performance:
* [ ] Reserve vectors before usage, e.g., in `TetMesh::get_faces()`.
* [ ] Directly retrieve tet IDs - avoids allocation

**Major:**
* [ ] Add `simplex` navigation functions (`open_star`, etc.)
* [ ] Add compound operations. Should not be too hard, just keep recording until all operations are performed.
* [ ] bring back `RawSimplexCollection`: 
```c++
template<int N> RawSimplexCollection{
    std::array<N, int64_t> m_vertices;
}
```
* [ ] Add `Rational` type from NEW (with `is_rounded`)
* [ ] Add `paraviewo` -  might need modification for the old toolkit
    * [ ] add functions to write attributes
* [ ] 

**Minor:**
* [ ] Document `io.hpp` <-- this is needed also in the future
    * [ ] Add utils for adding vertices/triangles/tets to the MSH file.
* [ ] Rename `vert_capacity()` to `vert_size()`. Capacity has a different meaning, e.g., `std::vector` has a `size` and a `capacity`.
    * [ ] `vert_size()` --> `num_vertices()`
    * [ ] `vert_capacity()` --> `vert_size()`
    * [ ] The same for tets and triangles
* [ ] `TetMesh::init` could also just take a vector of tets. `n_vertices` can be extracted from the max ID.
* [ ] utils for locking vertices and executor priorities
* [ ] VT sorted asserts



---
---
---
# Old Comments

## Submesh (TagMultimesh)


### Concept
The multimesh is realized by having a tag attribute on the root mesh, here called the `Embedding`. The root mesh is the only one that really exists. All other meshes are of type `SubMesh`.

### Structure
* New abstract class `MeshBase` that contains all the functions, e.g., `std::vector<Tuple> get_all(PrimitiveType type)`.
* The `Embedding` class wraps a `std::shared_ptr<Mesh> m_mesh` and adds functionality to `std::shared_ptr<SubMesh> add_submesh()`. It also contains the tag attributes.
```
std::map<PrimitiveType, std::string> m_tag_attribute_name;
std::map<PrimitiveType, attribute::TypedAttributeHandle<int64_t>> m_tag_handle;
```
* The `SubMesh` class inherits from `MeshBase` and therefore acts like a normal when calling `get_all()`. It also has methods to add simplices to the sub mesh `void add_simplex(const Tuple& tuple, PrimitiveType pt)`.

## Attributes

* Attribute definitions can be converted to and from JSON.

### new/transfer
* Global static object(s) that contain all *new* and *transfer* functions and a `std::map` from name to function pointers.
* A new function can be registered to those static objects from any component or application.
* The functions can be called from JSON to use them for any attribute.


## Others
* Remove `Primitive`?
* Non-manifold meshes (with embedding) <-- TetWild
    * Becomes a feature of `EmbeddedMultimesh`
    * Substructure tags need to be cleaned up after an operation
* Add multimesh for embedded __(high prio)__
    * Completely independent of current multimesh implementation
    * Use tags instead to represent substructures
    * Navigation on child meshes is done by filtering simplices that have the child tag
    * A simplex might belong to multiple children. Use bits of `int64_t` for tags: `1 << 0`, `1 << 1`, etc.
    * `switch_face` should throw an exception if multiple faces have the child tag
    * ~~Additional `switch_tuple_vector` function for child meshes that returns all tuples incident to an edge.~~ __Can be done by `open_star`__.
    * `split` requires a transfer to update the lower dimensional tags: If a simplex is tagged, all its faces must be tagged too. Can be implemented by getting the `open_star` of a vertex and tagging all faces of each simplex if the simplex itself is tagged.
* Non-embeddable triangle meshes (without embedding)
    * do we need a halfedge data structure?
    * can our data structure represent things like triangles with only two edges?
* Remove CRTP and see what happens to runtime
    * Measure runtime of real examples: Offsets, Shortest-edge collapse, Tetwild
* Attributes
    * Add hash to handles and attributes to check for outdated handles
    * Add a `info_string()` method that returns name, primitive type, and potentially the mesh name
    * Check the `remove_attribute()` method for proper functionality
        * add unit tests and documentation
    * Can we get rid of:
        * `PerThreadAttributeScopeStacks`
        * `AccessorBase` -> merge with `CachingAccessor`?
        * `AttributeHandle` -> merge with `TypedAttributeHandle`
        * `MeshAttributes` hold all attributes of a single type. Could be merged into `AttributeManager`
        * `AttributeScope` is just a wrapper for `AttributeCache`
        * `AttributeAccessMode` (not used anywhere)
        * `AttributeType` ?
        * `FlagAccessor` and `IndexFlagAccessor`
        * attribute/internal
            * Old scope stuff: `AttributeCacheData`, `AttributeFlatCache`, `AttributeCache`
            * `CompoundAccessor`
            * `CheckpointScope` (not sure if used, but if it is, it definitely needs documentation)
            * `hash` (not used anywhere, I think)
            * `MapTypes` <- what is that and why do we need it?
        * `variant_comparison` (not used anywhere)
    * Merge accessors with handles? Or create two accessors for scalar/vector attributes?
* Attribute New
    * `attribute_new` for setting the value to some value (potentially not the default value).
    * Attributes should have a default transfer for split, collapse, and others
* 

___

## Attribute Transfer
What should trigger a tranfer?
* operation: 
    * topological change / the closed star of a simplex changes (we need the closed star here. Example: rerun flag in `run_operation_on_all`)
    * geometrical change caused by topological change
* a face or coface (depending on the simplex dimension) attribute changes

The flag attribute requires the update of all attributes in the closed star!

Add a `AttributeTransferGroup` that contains all transfers from one PrimitiveType to another.

An operation should return:
* all new simplices
* all simplices that were modified (including the new ones)

Optimization:
* store modified simplices in the operation
* cache navigation for primitive types, e.g., cache all vertices incident to all edges as some sort of adjacency matrix, potentially as a vector of vectors.

## Shortest-edge collapse
Running tests on max-planck.msh without envelope.
```
{
  "input": "meshes/max-planck-dense.msh",
  "output": "max-planck_coarse",
  "report": "shortest_edge_collapse_dense_out.json",
  "envelope_size": -1
}
```

```
[2024-10-10 12:08:46.158] [wmtk] [info] Executed 153320 ops (S/F) 50014/103306. Time: collecting: 0.0055769, sorting: 0.011444500000000002, executing: 3.6604827
```
State of main branch: 3.66 seconds

Changing `SimplexCollection` to hold a vector of `IdSimplex`
```
[2024-10-10 14:50:07.475] [wmtk] [info] Executed 153320 ops (S/F) 50014/103306. Time: collecting: 0.0050987, sorting: 0.0112191, executing: 2.9129214999999995
```
2.91 seconds

Optimize `TriMesh::TriMeshOperationExecutor::collapse_edge_precompute()`
```
[2024-10-10 15:10:04.074] [wmtk] [info] Executed 153320 ops (S/F) 50014/103306. Time: collecting: 0.0054842, sorting: 0.0115694, executing: 2.3885035999999995
```
2.39 seconds
Switching to max-planck-dense
```
[2024-10-11 10:59:35.835] [wmtk] [info] Executed 3707466 ops (S/F) 1199922/2507544. Time: collecting: 0.1118542, sorting: 0.34428949999999997, executing: 72.95277259999997
```
73 seconds
More optimization on `collapse_edge_precompute()`
```
[2024-10-11 11:38:57.587] [wmtk] [info] Executed 3707466 ops (S/F) 1199922/2507544. Time: collecting: 0.11166690000000001, sorting: 0.35008259999999997, executing: 50.3385235
```
50 seconds
optimize `collapse_edge()`
```
[2024-10-11 14:03:21.514] [wmtk] [info] Executed 3707466 ops (S/F) 1199922/2507544. Time: collecting: 0.1102539, sorting: 0.34415709999999994, executing: 43.668774500000005
```
44 seconds

---
Switching to home desktop
39.4 seconds
optimizing `apply_attribute_transfer` --> changes the output because less transfers are performed. It should be correct though.
~~33.4 seconds~~
24.4 seconds
optimizing `link_condition` for TriMesh
~~21.5 seconds~~
```
[2024-10-12 13:01:47.763] [wmtk] [info] Executed 3707393 ops (S/F) 1199921/2507472. Time: collecting: 0.13220150000000003, sorting: 0.31104740000000003, executing: 22.186074799999997
```
20.3 seconds
`get_parent_simplices`
18.2 seconds

Moving on to 3D
```
Executed 308555 ops (S/F) 40634/267921. Time: collecting: 0.0172676, sorting: 0.0235289, executing: 46.4137171
```
46.4 seconds
optimizing TetMeshOperationExecutor
```
Executed 308555 ops (S/F) 40634/267921. Time: collecting: 0.0134648, sorting: 0.0211783, executing: 14.8331322
```
14.8 seconds
optimizing `link_condition`
```
Executed 308555 ops (S/F) 40634/267921. Time: collecting: 0.013201199999999998, sorting: 0.0212804, executing: 9.1746804
```
~~10.5~~
9.2
optimizing `get_collapse_simplices_to_delete`
```
Executed 308555 ops (S/F) 40634/267921. Time: collecting: 0.013505399999999999, sorting: 0.0204528, executing: 7.493860300000001
```
~~7.5 seconds~~
~~7.3 seconds~~
7.0 seconds

---
---

## 2D Performance of shortest-edge collapse
The new shortest-edge collapse (sec) code appears to be about 6x slower than the original tetwild simplification (tws).

| input #faces | sec (ms) | tws (ms) | sec (#f) | tws (#f) | sec / tws (ms) |
| ------------:| --------:| --------:| --------:| --------:| --------------:|
|       11,040 |      337 |      051 |       36 |       39 |          6.607 |
|       14,208 |      420 |       60 |        4 |       16 |          7.000 |
|       37,884 |    1,068 |      168 |        4 |       35 |          6.357 |
|       69,451 |    1,881 |      323 |       23 |      399 |          5.824 |
|       99,991 |    2,979 |      487 |        1 |      316 |          6.117 |

* 1,332 (ms) after hacking away `collapse_edge_precompute` and `attribute_transfer`

### Important
* 37% `TriMesh::TriMeshOperationExecutor::collapse_edge_precompute`
    * 
* 26% `Operation::apply_attribute_transfer`
    * Do we need the `closed_star` in line 148 of Operation.cpp? I think the `open_star` should be sufficient. Reduces transfer to 15%
        * `closed_star` is not required but a combination of open star and faces, depending on weather the transfer goes up in dimension or down.
    * I added a hacky `TopologicalTransferStrategy` which takes a lambda function with only the operation return simplex.


### Expected
* 15% `link_condition`
* 17% `CachingAccessor<__int64, -1>::const_scalar_attribute` - I'd expect this to be even larger because this does most of the loading from memory.

### Not sure
* 33% `SimplexCollection::sort_and_clean()` - I think that's mostly coming from `apply_attribute_transfer` and `collapse_edge_precompute`. So, it's just called way too often.
* 10% (almost completely in self) `wmtk::MeshCRTP<wmtk::TriMesh>::id` - I am confused that this shows up as the CachingAccessor shows up independently
* 7% `TriMesh::switch_tuple`

### Minor
* `link_condition` uses `open_star` instead of `cofaces_single_dimension`


## 3D

For tetwild_fig8_mid.msh:
```
Input tet orientation is inverted, swapping col 0 and 1 of TV matirx.
Input sec #top_simplices = 239126
bbox max [19.963579099964498, 18.746979635547994, 20.824680250477194], bbox min [-19.963579099964498, -18.746979635547994, -20.824680250477194], diag 68.80875070718274, target edge length 68808.75070718274
Executed 307555 ops (S/F) 40867/266688. Time: collecting: 0.0152951, sorting: 0.022450300000000003, executing: 53.93059320000001
[runtime h:m:s.ms] shortestedge_collapse: 00:00:54.119
Saving on sec_out
Output sec #top_simplices = 10245
```
---
50k all failing OPs take 32 seconds. -> 640 ns per OP
In comparison, on a triangle mesh that takes 1.152 seconds. -> 23 ns per OP
```
Input tet orientation is inverted, swapping col 0 and 1 of TV matirx.
Input sec #top_simplices = 239126
bbox max [19.963579099964498, 18.746979635547994, 20.824680250477194], bbox min [-19.963579099964498, -18.746979635547994, -20.824680250477194], diag 68.80875070718274, target edge length 68808.75070718274
HACKY break in Scheduler.cpp
Executed 50000 ops (S/F) 0/50000. Time: collecting: 0.0089263, sorting: 0.0215496, executing: 32.5504076
[runtime h:m:s.ms] shortestedge_collapse: 00:00:32.743
Saving on sec_out
Output sec #top_simplices = 239126
```
Commenting out all navigation/collection stuff in `TetMeshOperationExecutor` -> 11 seconds