TARGET_SPACE / tests /test_selfint.py
RDS777's picture
Upload 115 files
884b5f8 verified
Raw History Blame Contribute Delete
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
@pytest.mark.parametrize("offset", [0.05, 0.3, 0.9])
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)