Repository navigation
Envelopes: one reference, and each input held to its own, in every app and the offset - #1155
Merged
Merged
Conversation
…s, as each face is One tube per kind held every tagged edge, around all the kind's curves at once, while each tracked face carries its input set and is held to its own inputs' envelopes. Now each tagged edge carries, per kind, the inputs it tiles -- feature: the input_edges file of its input edge; order2: the inputs of the tracked faces at it, taken in tag_order2_edges() -- and is held to those inputs' tubes, one at a time (there is no 3D segment test for an intersection envelope). An edge with an empty set, or a kind whose edges come from one input, keeps the kind's union tube. - FeatureEdgeAttributes: m_feature_inputs, m_order2_inputs; inputs(k), merge(), remove(k). - m_feature_input_envelopes, m_order2_input_envelopes, built by init_tagged_edge_input_envelopes() around each input's tagged edges at init, only with two or more inputs, as faces build theirs. - tagged_edge_leaves_tube() takes the edge's tags; the collapse guard, the smoothing containment, tagged_edges_outside() and tetwild's sanity warning go through it. - tagged_edge_pull_envelope() pulls toward the farthest of the vertex's inputs' tubes, as faces pull (InputEnvelopes::pull). - The split, collapse and swap caches keep the whole attribute: a split gives both halves the parent's sets; remap_edge_tags() merges the sets of two edges a collapse welds into one. - init_feature_envelope() folds into tag_feature_edges(placed, input, eps, exact), as tag_order2_edges(); tetwild's init_from_Volumeremesher() takes the feature input. Tests: feature-curves-held-to-their-own-tubes (two curves from two files 0.07 apart, tubes 0.05: every tagged edge within its own file's tube after mesh_improvement(3); with the per-input tubes ignored, edges end 1.08-1.64 tube radii off their own curve); test_feature_tags checks the sets through splits, swaps and collapses. Measured on kirby against ea24a71: wmtk_tests, tetwild and simwild suites pass; 43 of the 44 integration configs byte-identical; simwild_double_sphere_notop_3d (2 inputs) changes, same 332 order-2 edges, same max energy 9.684, avg 3.5827366 -> 3.5827103. For the envelope PR; not on uday/shared-order2-protection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… or all around the simplification Which geometry the optimizer's envelopes were built around differed by app and by structure. tetwild built the surface envelope around the original input, but the order-2 tube and the curve anchors around the simplified surface, so an open boundary could drift 1.5 eps at the defaults; triwild its curve envelope around the input and its curve anchors around the simplified curves. simwild built everything around the simplified geometry whatever optimize_envelope_around_simplified said. One key now decides it for every envelope, tube and anchor, in triwild, tetwild and simwild alike: envelope_reference, "input" (the default) or "simplified". optimize_envelope_around_simplified is removed. "input": the original input, at eps times each one's ratio. Without preserve_topology the simplification glues rims of the input away and makes open boundaries, non-manifold edges and curve ends of its own. Those it made are held by the surface envelope alone; those it removed are lost, as the topology change it is allowed to make. - TetOptimizerMesh::InputCurves (set_input_curves()): per input, the edges of the input's own faces holds_order2_edge() holds, and the points where those and the feature curves end or three or more meet. Set by the app before init when a simplification ran before the insertion. - tag_order2_edges(): with InputCurves, the tubes are built around the input's curves, and an edge is tagged only if every input of its set has a curve there and the edge lies in its tube. - anchor_feature_points() (2D and 3D): with the input's curve points, a curve end or junction is anchored only if one of them lies in its ball, and the ball is centred there. Input points are anchored as before. - simwild: read_mesh and read_curves hand over the input as read for the envelope; SimWildMesh::simplify() puts the envelopes around the input back after simplifying, instead of rebuilding them around its result. Where two inputs meet, after a simplification before the insertion, the edges are left to the surface envelopes: the input's curves there are the intersections, which only an arrangement of the input would give. - utils::check_simplification_inside_envelope(): a simplification eps at or above the optimizer's is refused before any work, since the simplified geometry could start outside the envelope it is held to. simwild checks the eps the simplification actually runs at: read_mesh measures the inputs' own diagonal, Parameters::eps_simplify the padded box (1.22x apart on simwild_double_sphere_notop_3d, whose eps_simplify_rel equals its eps_rel). "simplified": around the simplified geometry, at the remaining tolerance (eps - simplify_eps) times each one's ratio. The order-2 tubes and the curve anchors now follow the surface envelope there (OptimizerParameters::order2_envelope_eps); in 3D they took the full eps. simwild's simplification after the insertion holds the order-2 edges within order2_envelope_ratio x eps_simplify, and they are tagged again around its result; the anchors stay centred on the input's points at the reduced radius. Tests: open-boundary-tags-only-on-the-inputs-own (input: an open square; inserted: that square with a hole cut out. Around the input 164 outer-rim edges are tagged and 0 on the hole; without InputCurves, 164 and 4; no tagged edge outside its tube after mesh_improvement(2)). simwild-simplification-boundary-envelope now sets envelope_reference "simplified", since it reads the simplification through InputData::V_envelope; its assertions are unchanged. The triwild input-sets test runs both values. Measured on kirby against a9b0f1e: wmtk_tests and the tetwild, simwild, triwild, tracking and topological_offset suites pass. Of the 44 integration configs, 24 are byte-identical and 20 change, and every one reports 0 order-2 edges outside their tube. Order-2 edges at insertion lying in the tubes around the input's own curves: crown 0 of 386, 314748 19 of 20, 103197 443 of 443 (23 curve ends with no input point in reach); 101168, 1017012, 104187, 104188, 104509 and 111388 are closed inputs, so none is tagged. Iterations and wall time (single runs, 22 at once): 103197 56 -> 41, 450 -> 287 s; 104509 48 -> 40, 302 -> 179 s; crown 12 -> 12, 261 -> 201 s; 111388 29 -> 37, 17 -> 19 s; 100827 8 -> 12. simwild_double_sphere_notop_3d: 332 order-2 edges where the spheres meet -> 0, 16 -> 4 iterations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uts' own diagonal simwild measured its tolerances against two diagonals. The optimizer's eps, eps_simplify and target edge length were relative to the embedding's box, which pads the inputs' bounding box by a fifteenth of its diagonal on each side; the simplification before the insertion (preserve_topology off) was relative to the inputs' own. So eps_rel meant something else than in tetwild and triwild, and the two simplifications ran at different eps for the same eps_simplify_rel: 1.22x apart on simwild_double_sphere_notop_3d, 1.42x on simwild_curves_2d. Now every one is relative to the inputs' own bounding box, the feature curves and points included, as in tetwild and triwild. read_mesh and read_curves measure it before any simplification (InputData::diag), and Parameters::init() takes it while the padded box still sets the domain (bbox-face tags). eps_simplify is derived from it once, by Parameters' rule -- the absolute eps_simplify if given, else eps_simplify_rel of the diagonal; read_mesh used to let the relative one win -- so the check that refuses a simplification at or above eps under envelope_reference "input" reads the eps both routes run at. A mesh input (.msh) keeps its own box, as before. At the same keys, a surface or curve input gets a finer mesh: eps and length shrink by 1.22-1.42. To keep the old resolution, multiply eps_rel and length_rel by that. Measured on kirby against 66ce8de: simwild, tracking and topological_offset suites pass. Of the 16 simwild integration configs the 12 mesh inputs are byte-identical. The optimizer's eps, old -> new, and the final max energy: simwild_curves_2d 4.773 -> 3.351 (its simplification stays at 0.3351, as the old one before the insertion measured the same diagonal), 9.68 -> 7.32; simwild_curves_two_inputs_2d 5.469 -> 3.849, 16.9 -> 17.9; simwild_double_sphere_3d 0.0321 -> 0.0261, 7.52 -> 7.69, 3 -> 4 iterations. simwild_double_sphere_notop_3d set eps_simplify_rel equal to eps_rel, now a simplification at the full eps, and is refused; data2 1cf750d halves it, and the pin in cmake/wmtk_data.cmake moves there: 4 iterations, final max energy 8.80. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de are held around themselves Under envelope_reference "input", an order-2 edge (open boundary, non-manifold edge) or curve end the simplification made, and the input does not have, was left to the surface envelope alone. The surface envelope is one-sided -- the output inside the envelope of the input -- so it cannot keep a curve from eroding from its end, and the input on that curve erodes with it. Measured as the input samples (vertices, face centroids and edge midpoints in 3D; points every eps/4 along the curves in 2D) farther than eps from the output, held -> left free: tetwild 104509 239 -> 317, crown 554 -> 695, 104187 1058 -> 1222, 104188 962 -> 1105, 1017012 8 -> 36; triwild 202260 927 -> 1315, 191265 148259 -> 181805, 196407 2.29M -> 2.41M of 2.54M. Now, after a simplification before the insertion: - TetOptimizerMesh::tag_order2_edges(): an order-2 edge of one input outside the tubes around the input's own curves is tagged too, in a tube around its own curve as the insertion leaves it. The two kinds share one tube per input, so an edge stays within eps of an order-2 curve of its own input, the input's or the simplification's. An edge where two or more inputs meet is left to the surface envelopes, as before: the input's curves there are the intersections, which only an arrangement of the input would give. - anchor_feature_points() (2D and 3D): a curve end or junction with none of the input's own curve points in reach is anchored at its own position. With one in reach, the ball is centred there, as before. Tests: open-boundary-tags-only-on-the-inputs-own becomes open-boundary-tags-around-the-input-and-the-simplification: the hole's rim, which the input does not have, is now tagged with the input's curves set as without them (164 outer, 4 hole), every tagged edge stays in its tube after mesh_improvement(2), and the hole keeps its rim. Measured on kirby against 31c46c1: wmtk_tests and the tetwild, simwild, triwild, tracking and topological_offset suites pass. Of the 64 integration and challenging configs, 41 are byte-identical and 23 change; every one runs, with 0 order-2 edges outside their tube. Input samples farther than eps, before -> after: seven tetwild configs return to their values before envelope_reference (101168 1238 -> 1137, 1017012 36 -> 8, 104187 1222 -> 1058, 104188 1105 -> 962, 104509 317 -> 239, crown 695 -> 554, 111388 12 -> 10), 103197 39 -> 56, 101881 38 -> 304 (its value before), 46024 136 -> 112; triwild 14 of 16 configs return to their values before, 202260 1315 -> 963 and 191265 181805 -> 149007. Iterations: 101168 65 -> 53, 101881 73 -> 49, 104187 57 -> 45, 104509 40 -> 48, 111388 37 -> 29, crown 12 -> 12; simwild_double_sphere_notop_3d 4 -> 12 (final max energy 8.80 -> 9.28: 44 rims the simplification made tagged, 80 curve ends anchored, 376 edges where the spheres meet left free). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s envelopes in every app The 2D engine holds each tracked segment to the envelopes of its input set (TriOptimizerMesh::m_input_envelopes, surface_envelope_for_edge()); in 3D the same rule lived in two per-app copies, tetwild's and simwild's, and they differed: simwild held a tracked face with an empty input set to the union envelope, where tetwild and triwild throw. Now the 3D engine has the 2D rule: - TetOptimizerMesh::m_input_envelopes, built by the application (tetwild: one per input file; simwild: one per label, SimWildMesh::init_label_envelopes()); - TetOptimizerMesh::surface_envelope_for_face(): a face that is not tracked is held to the union, m_envelope; a tracked face to the envelopes of its input set, and one with an empty input set throws (InputEnvelopes), as an insertion or propagation defect. The overrides in TetWildMesh, SimWildMesh and SimWildMeshTri are removed. An empty input set on a tracked face or segment never occurs in simwild: 0 such faces or segments, and 0 queries answered by the union, in 16 of 16 simwild configurations of the integration and challenging sets (instrumented build on 31c46c1). Measured: the same change as a prototype on 31c46c1, 64 of 64 integration and challenging configs byte-identical, and the wmtk, tetwild, simwild, triwild, tracking and topological_offset suites pass. On 740f3ae together with the next commit: 63 of 64 byte-identical; the one that differs, simwild_double_sphere_notop_3d, differs from its input simplification on, which is the next commit's change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…velope The optimizer holds each tracked face or segment to the envelope of its own input (the previous commit). The input simplification that runs before it held every face or segment to the union of all inputs' envelopes, so where two inputs lie within eps of each other a face of one could be collapsed onto the other. The optimizer then started with that face outside its own envelope. Now all four simplifications hold each face or segment to its own input's envelope when there are two or more inputs: - tetwild and simwild 3D: ShortestEdgeCollapse::m_input_envelopes, one envelope per input around its own triangles at the same eps and predicate; simwild 3D now passes each triangle's input (EmbedSurface::simplify_surface), as tetwild already did; - triwild and simwild 2D: simplify_segments() takes an optional envelope per segment, built by utils::input_segment_envelopes(). With one input nothing changes: no per-input envelope is built. Measured on 740f3ae, the input simplification held to the union vs to its own input, single thread, both binaries run together on uk-kirby: - tetwild, a wave sheet within 0.7 or 0.9 eps of a flat sheet, simplify_envelope_ratio 0.9: held to the union, 12 and 11 vertices are left after the simplification and both runs time out at 1500 s; held to its own, 62 and 83 vertices, finished in 155 s and 129 s. - tetwild, a sheet within 0.001 of another: 10 vertices vs 80, the count the same sheet simplifies to alone. - triwild, a wave curve within eps of a segment: 7 vertices vs 42; held to the union the run repeats the same failing splits ("outside the envelope", 128938 times) until the 1500 s timeout; held to its own, finished in 15 s. - simwild 2D, the same input: 7 vertices vs 43; both finish, but held to the union the wave curve is lost. - 8 two-input fixtures whose inputs are far enough apart that the simplification held to the union keeps them apart: simplified and final outputs byte-identical; the simplification takes 3% to 20% longer. The slowest of them, repeated as 4 pairs run together: 16% to 22% longer in every pair. Asked of both envelopes for every face, each query had the same answer (0 disagreements), and 96% of the envelope time is the exact search for a cover of the face by the prisms that cut it. Where the other input's prisms reach a face, they cover part of it cheaply; held to its own input's envelope, that part has to be covered by its own input's prisms instead: 6.29 ms per search instead of 5.51, plus 1001 searches that one prism of the union had made unnecessary. - One-input fixtures: byte-identical, simplification time within -1% to +4%. Integration and challenging sets, 64 configs: 63 byte-identical. The one that differs, simwild_double_sphere_notop_3d, is two overlapping spheres: 244 collapses instead of 251, and both finish in 45 s. The wmtk, tetwild, simwild, triwild, tracking, topological_offset and shortest_edge_collapse suites pass. New tests: sec-holds-each-face-to-its-own-input and simplify-segments-holds-each-segment-to-its-own-input. Each also runs the simplification held to the union and checks the wrong result, so the test can tell the two apart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s ask about tracked simplices only Since 2026-06 (triwild) the 2D collapse refused to collapse an untracked edge between two surface vertices unless that edge lay inside the union envelope. The edge disappears in the collapse, so the test held neither the topology -- the link condition's, which under preserve_topology never collapses an edge of lower order than both its vertices -- nor the geometry, which is the check of every segment the collapse makes. 3D never had it. It was the only caller asking surface_envelope_for_edge() about an untracked segment, which the 2D rule answered with the union; the 3D rule copied that branch in the previous commits without any caller reaching it. Both branches go: every caller (the init checks, collapse, split, the swaps, smoothing) passes tracked simplices, and with per-input envelopes an untracked one would now throw as a tracked one with an empty input set does. The 2D offset (TopoOffsetTriMesh) answers by its own per-tag tubes and applied them to that edge too; it no longer does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kip_simplify A key that says what it does: simplify, true by default, runs the input simplification; false inserts the input as it is. skip_simplify is gone, without an alias, and every config that sets neither behaves as before. Tests, docs and the Python stub follow; the stub was edited to what pybind11-stubgen writes from the specs (component_specs::describe()), checked line by line. A config that still says skip_simplify: wmtk_app and the Python bindings warn that the app has no such option. simwild, which validates its keys strictly, then refuses it; tetwild and triwild ignore it, so an old skip_simplify: true config would simplify; the next commit makes them refuse unknown keys too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit renames skip_simplify to simplify, and simwild refuses the old key. That config set it to false, the default; data2 f5e1ddd (branch uday/notop-simplify-half, after 1cf750d) drops the line, so the run is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
verify_and_setup_logger() checked keys strictly for simwild only. tetwild and triwild ignored a key their spec lacks, warned only by wmtk_app and the Python bindings and not at all by a direct call. Such a key is a typo or a removed option, and ignoring it runs something the caller did not ask for: since skip_simplify became simplify, an old skip_simplify: true would simplify. Now all three refuse one, and the strict parameter, true for one caller and false for the other two, goes. Every tetwild and triwild config of the data repository uses keys of its spec only (27 and 30, checked at the top level). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… around itself too Under envelope_reference "input", after a simplification before the insertion, an order-2 edge outside the tubes around the input's own curves is on a curve the input does not have, and is held in a tube around itself as inserted, so the one-sided surface envelope does not leave it to erode. An edge where two or more inputs meet was the exception and left to the surface envelopes alone: its curve in the input is the intersection of the inputs, which only an arrangement of the original would give. It now falls under the same rule, as every order-2 edge does with no simplification before the insertion. One rule for what the input lacks: held as inserted. tetwild holds its open boundaries only (holds_order2_edge(), measured on Thingi10K 111388), so this reaches simwild 3D, whose non-manifold edges are where its pieces meet. The comment at the test that decides it says why the test needs no correspondence: an edge is held within eps of an order-2 curve of its own inputs, not of the curve it came from, and the test is that question, asked with the predicate that then enforces it. Test: order2-tags-where-inputs-meet-around-themselves (two squares, each its own input, crossing: every edge where they cross is tagged, with and without the input's curves set -- with them, 0 before this commit -- and every tagged edge stays in its tube after mesh_improvement(2)). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… input, at the full eps Two corrections to envelope_reference, in the one check every app makes before any work. "simplified" built every envelope, tube and anchor around the simplified geometry at what the simplification left of eps, eps - simplify_eps, so that the output stayed within eps of the input; at or above eps it fell back to the full eps with a warning, a switch of guarantee no caller asked for. A dense input is simplified at several times eps on purpose, which only that fallback ran. Now "simplified" means the simplified geometry replaces the input: everything is built around it at the full eps, the output is within eps of it and so within simplify_eps + eps of the input, and any simplify_eps is allowed. "input" still refuses simplify_eps at or above eps: the simplified geometry could start outside the envelope around the input it is held to. "simplified" with no simplification -- simplify false, or simwild under preserve_topology in 2D or outside remeshing -- has no simplified geometry to build around, and is now refused. utils::check_envelope_reference() replaces check_simplification_inside_envelope() and makes both refusals; optimization_envelope_eps(), which became the identity, goes, and with it OptimizerParameters::order2_envelope_eps, the override that made the tubes and anchors follow the charged eps. The spec docs of envelope_reference say what each value guarantees; simwild's also says that curves where two inputs meet are held around themselves (the previous commit), and tetwild's no longer names non-manifold edges, which tetwild does not hold. Test: envelope_reference_refusals replaces envelope_budget, over simplify_eps from 0 to 10 eps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plify_rel or eps_simplify tetwild and triwild set the input simplification's eps as a fraction of the optimizer's, simplify_envelope_ratio (0.5); simwild as a length of its own, eps_simplify_rel of the inputs' diagonal or the absolute eps_simplify. Now all three take simwild's two keys and its rule: the absolute one when positive, else the relative one of the same diagonal eps_rel is of. simplify_envelope_ratio is gone, without an alias; the strict key check refuses it. The ratio suggested a link to eps that envelope_reference "simplified" no longer has: there the simplified geometry replaces the input and any simplification eps is allowed, a coarse one for a dense input included. Under "input" one at or above eps is still refused. The defaults are fixed lengths, 5e-4 of the diagonal in tetwild and triwild and 2e-4 in simwild, and do not follow eps_rel. 5e-4 is 0.5 of eps at the default eps_rel 1e-3, so every config at the default eps_rel runs bit-identically (5e-4 x diag == (1e-3 x diag) x 0.5 in floating point); all 57 tetwild and triwild configs of the data repository do. An eps_rel at or below 5e-4 is refused under "input" unless eps_simplify_rel is lowered with it. The docs' sweeps over eps_rel set eps_simplify_rel to half of it, as do the two tests that set eps_rel (tetwild's coarse volume round trip, the Python triwild memory route), so their runs are as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…_link_condition warnings go - tetwild and simwild: the tube comments still said that under envelope_reference "simplified" it is built at what the simplification left of eps; it takes the full radius since "simplified" stopped charging the simplification. - tetwild and simwild specs, order2_envelope_ratio: the doc still described the tube built around the boundaries after the simplification, drifting up to (eps + the simplification's eps) from the input's; under "input" it is built around the input's own curves since envelope_reference. - tetwild and triwild warned when a config still set simplify_use_link_condition. With unknown keys refused by the spec check, the warning can no longer be reached. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the envelopes when false With two or more inputs each is held to its own envelopes: one surface (2D: curve) envelope per input, one order-2 and one feature tube per input, and the input simplification's envelope per input likewise. That is the default and stays so. per_input_envelopes false shares them, one envelope of each kind for all inputs -- the union, as with one input -- for speed where inputs come within eps of each other (the 3D simplification measured 3% to 20% faster shared, the queries answering the same), at the cost of letting an input end up on another within eps. It switches every kind at once: a shared simplification followed by a per-input optimizer would hand the optimizer faces outside their own envelopes, the timeouts the per-input simplification fixed. Anchors are per point either way. An input is an input file in tetwild and triwild, a label in simwild. OptimizerParameters::per_input_envelopes, read by each app; the per-input builders return the shared answer without it (TetOptimizerMesh's tubes, TriOptimizerMesh::init_input_envelopes(), SimWildMesh::init_label_envelopes(), tetwild's per-input surface envelopes), and the simplifications take it: ShortestEdgeCollapse::create_mesh() and simwild's simplify_surface() and simplify_curves() as an argument, triwild's per-segment envelopes by not building them. The faces' inputs still go into face_attrs, so the output's face tags are unchanged. Test: sec-holds-each-face-to-its-own-input also runs with per_input_envelopes false, which gives the union's result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he one-sided envelope cannot The commit before last removed the 2D collapse's test of the collapsing edge itself, on the argument that the edge disappears in the collapse: the topology is the link condition's, the geometry the check of every segment the collapse makes. The argument was wrong, and two tests show it: orientation-2d-square-resting-on-a-strip lost the square (tracked area 1.0 for 4) and orientation-2d-square-and-clockwise-square 4.8% of its area after mesh_improvement(3); with the test back both pass (measured on kirby, the triwild suite built with and without it). The envelope is one-sided and the union of a whole input's curves, so a curve vertex moved across a region onto another curve of its input leaves every new segment on a curve, inside, while the region between folds away; the collapsing edge, crossing that region, is outside. The comment there now says so. The 2D rule's untracked branch, which answers that test, is back with it. The 3D one stays removed: 3D has no such test, and no 3D operation asks about an untracked face. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…as in 2D When both ends of a collapsing edge are on the surface and the edge is on no tracked face, the edge itself must now lie inside the envelope, as the 2D collapse has required since 2026-06. The check of every face the collapse keeps does not cover it: the envelope is one-sided and the union of a whole input's faces, so a surface vertex moved through a solid onto another surface of its input keeps every face on a surface while the solid between folds away. In 2D that is what orientation-2d-square-resting-on-a-strip showed when the test was removed for a commit. 3D had no such rule. Measured with scratch counters on tetwild's orientation-configurations (a box resting on a plate and the rest): 1956 such collapses were tried and none went through, but they were refused by the quality gate (976), which the coarsening pass skips, by the inversion test (967) and by the faces' own check (13), none by a rule of their own. The test now asserts that the new rule refuses some, besides the volumes it checked before. The envelope comes from a hook, collapsing_edge_envelope(), the union m_envelope by default, as 2D answers an untracked segment. The offset answers none for now: its front's vertices are surface vertices that move by design and lie outside m_envelope, so the union would refuse the front's collapses; it gets the test with the offset following the other apps' envelope rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the envelope's own eps Both divided by sqrt(eps2). eps2 is the sampled path's shrunk acceptance radius squared, (eps - eps/sqrt(3))^2 = (0.42 eps)^2, kept whether or not the exact predicate is in use; the envelope's radius, what every query guarantees, is eps. So the offset's containment audit stated its margins 2.37x too high, and tetwild's "tetrahedralisation envelope: ... (eps ...)" line printed 0.42 eps as eps. Envelope::eps() returns the eps the envelope was built with (sampling_dist, set by every init()), and both use it. No mesh changes; only the log text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tetwild and simwild TetOptimizerMesh::m_s_envelope scales the envelope energy of smoothing_mode "exact" -- the squared distance to the input -- by 1/eps^2, so it enters relative to the envelope. tetwild and simwild set it; the offset never did, so it stayed at -1, which turns the term's penalty into a reward under "exact". Now build_envelopes() sets it from envelope_size, the offset's eps. "projected", the default, does not read it, so default runs are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…envelopes moved to the base The per-label envelopes were simwild's (SimWildMesh::init_label_envelopes()): member b around the input triangles carrying label b when they are known, else around the mesh's faces carrying it. That is the 3D twin of TriOptimizerMesh::init_input_envelopes(), and the offset needs the same. Moved to the base unchanged, with the same name and arguments as in 2D, returning the number of members; simwild keeps its wrapper and its log line. No output changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o copies tetwild and simwild each overrode smoothing_energy_envelope() with the same code: a vertex on a tagged curve toward its tube, else toward the member of its faces' input sets it is farthest from, else the union. It is now TetOptimizerMesh's default, and both overrides go, with simwild's Smooth.cpp, which held nothing else. topological_offset will ask it for a held vertex. No output changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es; the wall by the box rule The offset's envelopes now follow tetwild's and simwild's rules. - Labels. An input is a label: a face tag, or the tet tag of a region a face bounds, as simwild's (bit F + t - 1 for tet tag t, F face tags), and the envelope surface group's tag on a face of it. assign_face_labels() writes them into the tracked faces' input sets before the march, and the operations carry them; the output takes only the face-tag bits (collect_input_set_tags()). - Surfaces. The held faces -- the input complex's boundary, and in a TetWild pass the other region boundaries -- build m_envelope, their union, and the base's per-label m_input_envelopes (TetOptimizerMesh::init_input_envelopes()); each is held to its own labels' envelopes by the base's rule, the union with one label or with per_input_envelopes false (new key, as in the other apps). Until now one union held them all, so a face of one label could end up on another's surface within envelope_size. - Pull and smoothing containment. A held vertex is pulled by the base's rule, toward the member of its faces' labels it is farthest from, and each face at it is contained per face, as in the other apps; both were the one union. - The domain wall. It is no longer held by an envelope and its vertices are no longer smoothed: the box rule holds it, as in tetwild and simwild (no smoothing, collapses only along the walls, a split stays on the walls both ends share). The wall was held and smoothed since 2026-08 because, with it frozen and its edges unsplittable, a tet resting on the box bisected onto the plane (max AMIPS 6.9e21 against 364); wall edges split now. Chosen for consistency with the other apps; the offset configs' max AMIPS is the check, and the wall envelope the fallback if the cascade returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in tetwild and simwild The offset held no curve and no point: a rim of a sheet, or a junction where labelled regions meet, was held by the surface envelope alone, which is one-sided and lets a curve erode or slide. hold_curves_and_points(), right after build_envelopes() and before the march, does what simwild does after its insertion: the base's rule picks the order-2 edges (1 tracked face, or 3 and more), tag_order2_edges() tags them, one tube per label around them as they are -- the offset never simplifies its input, so that is the input's own curve -- and anchor_feature_points() anchors the curve ends and junctions. The operations carry the tags. The front does not exist yet, so it is never among them. New key order2_envelope_ratio (default 1), the tube's half-width as a fraction of envelope_size, as in tetwild and simwild. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…faces, as in the other apps The 3D collapse refuses an edge between two surface vertices that is on no tracked face and leaves the envelope, so a vertex cannot cross a solid onto another surface of its input (collapsing_edge_envelope()). The offset answered none, its front's vertices being surface vertices that move by design outside m_envelope. Now it answers m_envelope, the held faces' union, when both ends are on the held surfaces, and none when an end is on the front; an edge from the input complex to the front is refused anyway (app_both_surfaces). Also rewraps the previous commit's comment to 100 columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rs around a TetWild pass InputEnvelopes is not copyable (it holds the mutex of its intersection cache), so the commit that gave the offset per-label envelopes did not compile where build_final_envelopes() set the loop's aside and release_final_envelopes() put them back. Keep the members instead, and restore them through InputEnvelopes::set(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… pass even when it is none Since the wall is the box rule's (2fa364b), an input complex of curves or points holds no face, and the loop runs with no envelope. release_final_envelopes() put the loop's envelope back only when there was one, so after a TetWild pass such a run kept the final pass's envelope, every region boundary held, for the rest of the loop. A flag build_final_envelopes() sets now decides. The test held_faces_and_one_envelope asserted the rules before 2fa364b: the wall held, one envelope for every held face. Renamed held_faces_and_their_envelopes, it asserts the wall not held, a region boundary held to its labels' envelopes in the final pass, and the loop's envelope -- none, in its two-tet scene -- back after the pass, which fails without the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nvelope-reference The data pin's two lines join at data2 6dc625a, the merge of data2 #13: main's de49170 and one config after it, and this branch's f5e1ddd. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…o uday/envelope-reference Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… envelope through its members Since each held face is held to its own labels' envelopes (2fa364b), a face on two or more labels -- a region boundary between two tags -- is held to their intersection (InputEnvelopes::containment()), a composite that answers is_outside() and nothing else: its BVH is null, and CompositeEnvelopes.hpp states that it must never be asked squared_distance(). The offset's containment audit, run after construction and in the loop, asked it that for the margin of every held face, and every 3D offset run with such a face crashed there (all 9 cube configs of the data repository, 6dc625a). The audit now asks the members, as the 2D audit asks its tag tubes; the face's distance and margin are the largest over them, since inside the intersection is inside each. The test held_faces_and_their_envelopes now audits its two-tag face in the final pass, which crashes without the fix. Measured, the data repository's 9 cube offset configs (6dc625a, one thread), this branch against main 6d17d30, which holds and smooths the wall: before the fix all 9 crash, main finishes 8 and times out on cube_small_offset_conv0.025 at 3600 s. With it, 8 of 8 finished converge, final max AMIPS 7.0 to 73 against main's 10.7 to 70 (the wall frozen by the box rule, 2fa364b, brings no bisection cascade back); the ninth was still running at the time of this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Stacked on #1150. One set of envelope rules for triwild, simwild 2D, tetwild, simwild 3D and
topological_offset 3D:
envelope_reference);Terms:
must stay in. One-sided: it bounds where the output is, not that it covers the input.
curve end or junction.
a face bounds (topological_offset).
non-manifold edge).
Behaviour after this PR
envelope_reference:"input"(default) or"simplified", the same for every kind in every app"input""simplified"simplify_envelope_ratiox eps; simwild:eps_simplify_relx diagonaleps_simplify_relx diagonal, or absoluteeps_simplify. Defaults 5e-4 (tetwild, triwild), 2e-4 (simwild)skip_simplifysimplify(default true)per_input_envelopesfalse (default true), every kind at onceRemoved keys (no aliases; the strict key check refuses them):
skip_simplify,simplify_envelope_ratio,optimize_envelope_around_simplified. New keys:envelope_reference,simplify,eps_simplify_rel/eps_simplify(tetwild, triwild),per_input_envelopes,order2_envelope_ratio(topological_offset).Discussion points
"input"means. Without preserve_topology the simplification can glue rims away andmake open boundaries, non-manifold edges and curve ends the input does not have. This PR holds
them around themselves as inserted (meaning I: the input where it has the structure, as
inserted where it does not). The alternative (II: the input only) holds only what the input
has, and leaves the rest to the one-sided surface envelope, where it can erode. The guarantee
(output within eps of the input) holds either way; the difference is what is protected from
erosion. Whichever is chosen applies to every kind.
holds_order2_edge()). OnThingi10K 111388, 278 of 329 non-manifold edges appear after simplification and insertion, and
holding them hit the iteration cap (Shared holding of curves and points in the 2D and 3D bases (triwild, tetwild, simwild 2D and 3D) #1150). Under meaning I that is an exception.
It was held in the envelope and smoothed since 2026-08: with the wall frozen and its edges
unsplittable, a tet resting on the box bisected onto the plane (max AMIPS 6.9e21 against 364).
Wall edges split now. The offset configs' max AMIPS is the check below; holding the wall in an
envelope of its own is the fallback.
Measurements
All on uk-kirby, single runs unless stated.
pass at every verified step.
63 byte-identical; simwild_double_sphere_notop_3d differs (two overlapping spheres: 244
collapses instead of 251; holding the edges where they meet: 378 tagged, 197 anchors, final max
energy 9.28 -> 8.08).
sheet, union 12 and 11 vertices and both runs time out at 1500 s, per input 62 and 83 vertices in
155 s and 129 s. triwild wave curve near a segment: union repeats a failing split until the
timeout, per input 15 s. Two-input fixtures whose inputs stay apart: outputs byte-identical,
simplification 3% to 20% slower (96% of the envelope time is the cover search; the other
input's prisms covered part of a face cheaply).
per_input_envelopesfalse gives that timeback.
before it and none went through, refused by other tests (quality 976, inversion 967, faces'
check 13). With it, 13 of 82 runs change, all finish.
eps_simplify_reldefault 5e-4 equals 0.5 x eps at the default eps_rel 1e-3 bit-for-bit; all57 tetwild and triwild data configs run at the default.
branch against main (which holds and smooths the wall): main finishes 8 and times out on
cube_small_offset_conv0.025 at 3600 s; this branch converges on the same 8, final max AMIPS 7.0
to 73 against main's 10.7 to 70, so freezing the wall brings no bisection cascade back. The 9
2D configs are refused on both: the 2D offset is disabled. These runs found a crash, fixed in
e2ebdb3.
fixture configs are byte-identical before and after them.
Data
Pins data2 at 6dc625a, the merge of data2 #13 (
simwild_double_sphere_notop_3ddropsskip_simplify), which also holds main's pin de49170.Left for later PRs
input (separate PR, branch
uday/envelope-io).reproducer first.
"exact"(the envelope energy term): rethink or remove."input"; triwild's endless retry of a failing split.🤖 Generated with Claude Code