Spaces:
Sleeping
Sleeping
Download tests/test_selfint.py from RDS777/TARGET_SPACE: direct link, hf CLI and curl.
- Browser
- Download file 6.59 kB
-
https://huggingface.co/spaces/RDS777/TARGET_SPACE/resolve/main/tests/test_selfint.py
- Command line
-
hf download hf://spaces/RDS777/TARGET_SPACE/tests/test_selfint.py
-
curl -L -o test_selfint.py https://huggingface.co/spaces/RDS777/TARGET_SPACE/resolve/main/tests/test_selfint.py
6.59 kB
| """Détection des auto-intersections : paires de faces non voisines qui se coupent, contacts compris.""" | |
| import numpy as np | |
| import pytest | |
| from mouh.geometry import mesh | |
| from mouh.geometry.mesh import count_self_intersections, validate_mesh | |
| CUBE_V = [(0, 0, 0), (1, 0, 0), (1, 1, 0), (0, 1, 0), (0, 0, 1), (1, 0, 1), (1, 1, 1), (0, 1, 1)] | |
| CUBE_F = [(0, 2, 1), (0, 3, 2), (4, 5, 6), (4, 6, 7), (0, 1, 5), (0, 5, 4), | |
| (3, 6, 2), (3, 7, 6), (0, 4, 7), (0, 7, 3), (1, 2, 6), (1, 6, 5)] | |
| TET_V = [(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)] | |
| TET_F = [(0, 2, 1), (0, 1, 3), (0, 3, 2), (1, 2, 3)] | |
| def shifted(vertices, faces, offset, base): | |
| """Copie d'un maillage décalé, indices recalés à partir de ``base`` (aucun sommet partagé).""" | |
| v = [(x + offset[0], y + offset[1], z + offset[2]) for x, y, z in vertices] | |
| f = [(a + base, b + base, c + base) for a, b, c in faces] | |
| return v, f | |
| def test_two_disjoint_cubes_do_not_intersect(): | |
| v2, f2 = shifted(CUBE_V, CUBE_F, (2, 0, 0), len(CUBE_V)) | |
| found = count_self_intersections(np.array(CUBE_V + v2, float), np.array(CUBE_F + f2)) | |
| assert found["count"] == 0 and found["tested"] > 0 | |
| def test_two_overlapping_closed_cubes_are_flagged_by_validate_mesh(): | |
| # Chaque cube est fermé et orienté : l'ancien contrôle (étanchéité, orientation, volume) passerait. | |
| v2, f2 = shifted(CUBE_V, CUBE_F, (0.5, 0.5, 0.5), len(CUBE_V)) | |
| rep = validate_mesh(CUBE_V + v2, CUBE_F + f2) | |
| assert not rep.ok | |
| assert rep.stats["watertight"] and rep.stats["oriented"] | |
| assert any("auto-intersection" in e for e in rep.errors) | |
| assert rep.stats["self_intersections"] > 0 | |
| def test_a_triangle_piercing_a_closed_cube_is_flagged(): | |
| # Triangle dans le plan x = 0.5 qui traverse le cube unitaire : il coupe les faces z = 0, z = 1, y = 0, y = 1. | |
| piercing_v = [(0.5, -1, -1), (0.5, 2, -1), (0.5, 0.5, 2)] | |
| found = count_self_intersections(np.array(CUBE_V + piercing_v, float), | |
| np.array(CUBE_F + [(8, 9, 10)])) | |
| assert found["count"] > 0 | |
| # Le triangle ajouté est la face d'indice 12 ; chaque paire trouvée le contient. | |
| assert found["sample"] and all(12 in pair for pair in found["sample"]) | |
| def test_a_triangle_far_from_the_cube_is_not_flagged(): | |
| far_v = [(3, 0, 0), (4, 0, 0), (3, 1, 0)] | |
| found = count_self_intersections(np.array(CUBE_V + far_v, float), np.array(CUBE_F + [(8, 9, 10)])) | |
| assert found["count"] == 0 | |
| def test_coplanar_overlapping_triangles_are_flagged(): | |
| v = np.array([(0, 0, 0), (2, 0, 0), (0, 2, 0), (0.5, 0.5, 0), (2.5, 0.5, 0), (0.5, 2.5, 0)], float) | |
| f = np.array([(0, 1, 2), (3, 4, 5)]) | |
| assert count_self_intersections(v, f)["count"] == 1 | |
| def test_coplanar_disjoint_triangles_are_not_flagged(): | |
| v = np.array([(0, 0, 0), (2, 0, 0), (0, 2, 0), (5, 0, 0), (6, 0, 0), (5, 1, 0)], float) | |
| f = np.array([(0, 1, 2), (3, 4, 5)]) | |
| assert count_self_intersections(v, f)["count"] == 0 | |
| def test_triangles_sharing_a_vertex_are_not_compared(): | |
| # Deux triangles qui partagent un sommet ne sont pas comparés : pas de faux positif sur un maillage sain. | |
| v = np.array([(0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1), (0, -1, 0)], float) | |
| f = np.array([(0, 1, 2), (0, 3, 4)]) | |
| found = count_self_intersections(v, f) | |
| assert found["count"] == 0 and found["tested"] == 0 | |
| def test_a_tetrahedron_piercing_another_tetrahedron_is_flagged(): | |
| big = np.array([(-2, -2, -2), (4, -2, -2), (-2, 4, -2), (-2, -2, 4)], float) | |
| # Le petit tétraèdre sort par la face x + y + z = 0 du grand : ses arêtes percent cette face. | |
| small = np.array([(0, 0, 0), (0.2, 0, 0), (0, 0.2, 0), (0, 0, 2.5)], float) | |
| v = np.vstack([big, small]) | |
| f = np.array([(0, 2, 1), (0, 1, 3), (0, 3, 2), (1, 2, 3), (4, 6, 5), (4, 5, 7), (4, 7, 6), (5, 6, 7)]) | |
| assert count_self_intersections(v, f)["count"] > 0 | |
| def test_sample_is_bounded_and_lists_face_indices(): | |
| v2, f2 = shifted(CUBE_V, CUBE_F, (0.25, 0.25, 0.25), len(CUBE_V)) | |
| found = count_self_intersections(np.array(CUBE_V + v2, float), np.array(CUBE_F + f2)) | |
| assert 0 < len(found["sample"]) <= mesh.MAX_SAMPLE | |
| assert all(len(pair) == 2 for pair in found["sample"]) | |
| def test_dense_mesh_over_budget_is_refused_not_skipped(monkeypatch): | |
| v2, f2 = shifted(CUBE_V, CUBE_F, (2, 0, 0), len(CUBE_V)) | |
| monkeypatch.setattr(mesh, "MAX_CANDIDATE_PAIRS", 1) | |
| rep = validate_mesh(CUBE_V + v2, CUBE_F + f2) | |
| assert not rep.ok and any("impossible" in e for e in rep.errors) | |
| def test_self_test_is_skipped_when_an_earlier_check_already_fails(): | |
| rep = validate_mesh(CUBE_V, CUBE_F[2:]) | |
| assert not rep.ok | |
| assert rep.stats["self_intersections"] is None | |
| def test_overlap_depth_does_not_change_the_verdict(offset): | |
| v2, f2 = shifted(CUBE_V, CUBE_F, (offset, offset, offset), len(CUBE_V)) | |
| assert count_self_intersections(np.array(CUBE_V + v2, float), np.array(CUBE_F + f2))["count"] > 0 | |
| def uv_sphere(nlat, nlon, r=1.0, offset=(0.0, 0.0, 0.0)): | |
| """Sphère fermée en latitude/longitude, normales sortantes : maillage sain et dense.""" | |
| v = [(0.0, 0.0, -r)] | |
| for i in range(1, nlat): | |
| th = np.pi * i / nlat | |
| for j in range(nlon): | |
| ph = 2 * np.pi * j / nlon | |
| v.append((r * np.sin(th) * np.cos(ph), r * np.sin(th) * np.sin(ph), -r * np.cos(th))) | |
| v.append((0.0, 0.0, r)) | |
| v = np.array(v, float) + np.array(offset, float) | |
| def ring(i, j): | |
| return 1 + (i - 1) * nlon + (j % nlon) | |
| south, north = 0, len(v) - 1 | |
| f = [(south, ring(1, j + 1), ring(1, j)) for j in range(nlon)] | |
| for i in range(1, nlat - 1): | |
| for j in range(nlon): | |
| a, b, c, d = ring(i, j), ring(i, j + 1), ring(i + 1, j), ring(i + 1, j + 1) | |
| f += [(a, b, d), (a, d, c)] | |
| last = 1 + (nlat - 2) * nlon | |
| f += [(north, last + j, last + ((j + 1) % nlon)) for j in range(nlon)] | |
| return v, np.array(f, np.int64) | |
| def test_dense_closed_sphere_has_no_false_positive(): | |
| v, f = uv_sphere(40, 80) # 6 240 faces : aucune intersection réelle | |
| rep = validate_mesh(v, f) | |
| assert rep.ok, rep.errors | |
| assert rep.stats["self_intersections"] == 0 and rep.stats["pairs_tested"] > 1000 | |
| def test_two_dense_spheres_that_cross_are_refused(): | |
| v1, f1 = uv_sphere(20, 40) | |
| v2, f2 = uv_sphere(20, 40, offset=(0.5, 0.0, 0.0)) | |
| rep = validate_mesh(np.vstack([v1, v2]), np.vstack([f1, f2 + len(v1)])) | |
| assert not rep.ok | |
| assert any("auto-intersection" in e for e in rep.errors) | |