""" The member mesh: sealed, correctly oriented, and the right size. WHY THESE FOUR CHECKS AND NOT A VISUAL ONE A bad mesh opens in a slicer. That is the whole problem: inverted normals, a hole filled in, a wall missing or the wrong sweep length all produce a file that looks plausible and prints wrong, and the failure is discovered in plastic hours later. So each property is asserted arithmetically, and each is chosen to fail loudly for a different defect: edge pairing a missing or duplicated wall, and inconsistent winding Euler a hole that was filled, or one invented centroid-in-hole a bore tessellated over -- prints solid through the conduit volume inverted normals and a wrong length, in one number Every negative assertion has a positive control, per ROADMAP principle 11. """ from __future__ import annotations import struct import pytest stl = pytest.importorskip("mechcomp.stl") profiles = pytest.importorskip("mechcomp.profiles") region = pytest.importorskip("mechcomp.geom.region") LENGTH = 40.0 def every_profile(): """Each catalogue entry of each family, at its family defaults.""" for name, family in profiles.FAMILIES.items(): for profile in sorted(family.catalogue): yield name, profile def built(family="3x", profile="Y", **params): return profiles.build(family, profile, params) ALL = list(every_profile()) def ids(pair): return "%s/%s" % pair # --------------------------------------------------------------------------- # Sealed and consistently wound # --------------------------------------------------------------------------- @pytest.mark.parametrize("pair", ALL, ids=ids) def test_every_directed_edge_appears_exactly_once(pair): """ Manifoldness and consistent orientation in one assertion. In a closed, consistently oriented surface each directed edge (a, b) occurs once and its reverse (b, a) occurs once. A duplicated directed edge means two faces wind the same way across a shared edge; a missing reverse means a hole in the surface. """ mesh = stl.prism_mesh(built(*pair).section, LENGTH) seen = set() for i, j, k in mesh.triangles: for edge in ((i, j), (j, k), (k, i)): assert edge not in seen, "directed edge %r twice" % (edge,) seen.add(edge) for a, b in seen: assert (b, a) in seen, "edge %r has no opposite" % ((a, b),) def test_the_edge_check_can_fail(): """ The positive control. Without it, a mesh with no triangles at all would satisfy the test above vacuously. """ mesh = stl.prism_mesh(built().section, LENGTH) assert len(mesh.triangles) > 0 broken = stl.Mesh(mesh.vertices, mesh.triangles[:-1]) seen = set() for i, j, k in broken.triangles: for edge in ((i, j), (j, k), (k, i)): seen.add(edge) assert any((b, a) not in seen for a, b in seen) # --------------------------------------------------------------------------- # The right number of holes, still holes # --------------------------------------------------------------------------- @pytest.mark.parametrize("pair", ALL, ids=ids) def test_euler_characteristic_matches_the_section(pair): """ V - E + F = 2P - 2H. A prism over a part with H holes is a genus-H handlebody, so its boundary surface has characteristic 2 - 2H, and P disjoint parts sum. Filling a bore would raise the left side; inventing a tunnel would lower it. """ result = built(*pair) section = result.section parts = region.region_parts(section) p = len(parts) h = sum(len(part) - 1 for part in parts) mesh = stl.prism_mesh(section, LENGTH) edges = {frozenset((a, b)) for i, j, k in mesh.triangles for a, b in ((i, j), (j, k), (k, i))} chi = len(mesh.vertices) - len(edges) + len(mesh.triangles) assert chi == 2 * p - 2 * h, ( "chi=%d for %d part(s) and %d hole(s)" % (chi, p, h)) def test_at_least_one_profile_actually_has_a_hole(): """ The positive control for the Euler test and the one below it: if no section in the catalogue had holes, both would be asserting the easy case and a triangulator that ignored holes would pass everything. """ with_holes = 0 for pair in ALL: parts = region.region_parts(built(*pair).section) with_holes += sum(len(part) - 1 for part in parts) assert with_holes > 0 # --------------------------------------------------------------------------- # Nothing tessellated across a bore # --------------------------------------------------------------------------- @pytest.mark.parametrize("pair", ALL, ids=ids) def test_no_cap_triangle_sits_inside_a_hole(pair): """ The check that matters most in practice. A triangulator that filled the bores produces a mesh that is sealed, correctly wound, passes Euler if the holes vanish consistently -- and prints solid where the strap has to pass. Only the volume test and this one would notice. """ result = built(*pair) section = result.section mesh = stl.prism_mesh(section, LENGTH) holes = [hole for part in region.region_parts(section) for hole in part[1:]] if not holes: pytest.skip("this section has no holes") caps = [t for t in mesh.triangles if len({mesh.vertices[i][2] for i in t}) == 1] assert caps, "no cap triangles found" from shapely.geometry import Polygon # Overlapping AREA, not the centroid. A centroid test passes by luck on a # coarse triangulation: two triangles covering a square with a central bore # can both have their centroids outside it while jointly paving it over. # Found by mutation -- ignoring the holes at triangulation time slipped # through a centroid check and is caught by this one. hole_polys = [Polygon(h) for h in holes] for i, j, k in caps: tri = Polygon([(mesh.vertices[v][0], mesh.vertices[v][1]) for v in (i, j, k)]) if tri.area <= 0: continue for hp in hole_polys: assert tri.intersection(hp).area <= 1e-9 * tri.area, ( "a cap triangle paves over a hole -- the part would print " "solid where the stock has to pass") # --------------------------------------------------------------------------- # The right size, the right way out # --------------------------------------------------------------------------- @pytest.mark.parametrize("pair", ALL, ids=ids) def test_volume_equals_area_times_length(pair): """ Catches inverted normals and a wrong sweep length together. Compared against ``region_area`` at full precision rather than against the report's ``SECTION_AREA_MM2``, which is rounded to six significant figures and would cap this test's resolution at the oracle's rather than the geometry's. """ result = built(*pair) expected = region.area(result.section) * LENGTH actual = stl.prism_mesh(result.section, LENGTH).volume assert actual > 0, "negative volume means the normals face inward" assert abs(actual - expected) <= 1e-9 * abs(expected) def test_volume_agrees_with_the_report_too(): """ The same quantity through the published numbers, at the precision they are published to. Six significant figures, so the bound is loose on purpose. """ result = built() length = result.report["LENGTH_MM"] expected = result.report["SECTION_AREA_MM2"] * length actual = stl.prism_mesh(result.section, length).volume assert abs(actual - expected) <= 1e-5 * abs(expected) def test_volume_scales_with_length(): """Positive control: a volume that ignored its length would be constant.""" section = built().section assert stl.prism_mesh(section, 80.0).volume == pytest.approx( 2.0 * stl.prism_mesh(section, 40.0).volume) def test_reversing_a_wall_would_be_caught(): """ Positive control for the sign: a mesh with every triangle flipped has negative volume, so ``actual > 0`` above is a real assertion. """ mesh = stl.prism_mesh(built().section, LENGTH) flipped = stl.Mesh(mesh.vertices, [(k, j, i) for i, j, k in mesh.triangles]) assert flipped.volume < 0 # --------------------------------------------------------------------------- # The length that ships # --------------------------------------------------------------------------- @pytest.mark.parametrize("params,expected", [ ({"preview_length_mm": 37.5}, 37.5), ({"length_view": "Full Length", "member_length_ft": 10}, 3048.0), ]) def test_member_stl_sweeps_the_reported_length(params, expected): """ The record's VOLUME_MM3 and MASS_G are computed against LENGTH_MM. Sweeping anything else would make the record describe a different object than the file beside it. NEITHER CASE IS 100 mm, AND THAT IS THE POINT. An earlier version of this test built at the family defaults, where ``preview_length_mm`` is 100. A mutation replacing the lookup with a hardcoded 100.0 passed the whole suite -- the assertion compared a value against the constant that had replaced it. Both cases here differ from 100, and the second differs by two orders of magnitude, so no plausible fixed length satisfies them. """ result = built(**params) assert result.report["LENGTH_MM"] == expected data = stl.member_stl(result) count = struct.unpack(" 30 * stl.prism_mesh(preview.section, preview.report["LENGTH_MM"]).volume) # --------------------------------------------------------------------------- # The file # --------------------------------------------------------------------------- def test_binary_stl_is_exactly_the_right_size(): mesh = stl.prism_mesh(built().section, LENGTH) data = stl.binary_stl(mesh) assert len(data) == 84 + 50 * len(mesh.triangles) assert struct.unpack("