250 lines
9.3 KiB
Python
250 lines
9.3 KiB
Python
import os
|
||
import sys
|
||
import unittest
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
import bl_decimate as bd
|
||
|
||
|
||
class TestDecimateRatio(unittest.TestCase):
|
||
def test_normal_reduction(self):
|
||
self.assertAlmostEqual(bd.decimate_ratio(5000, 1500000), 5000 / 1500000.0)
|
||
|
||
def test_current_below_target_returns_one(self):
|
||
self.assertEqual(bd.decimate_ratio(5000, 3000), 1.0)
|
||
|
||
def test_current_equal_target_returns_one(self):
|
||
self.assertEqual(bd.decimate_ratio(5000, 5000), 1.0)
|
||
|
||
def test_zero_current_returns_one(self):
|
||
self.assertEqual(bd.decimate_ratio(5000, 0), 1.0)
|
||
|
||
|
||
class TestWithinTolerance(unittest.TestCase):
|
||
def test_exact_hit(self):
|
||
self.assertTrue(bd.within_tolerance(5000, 5000))
|
||
|
||
def test_within_3_percent(self):
|
||
self.assertTrue(bd.within_tolerance(5000, 5150)) # +3%
|
||
self.assertTrue(bd.within_tolerance(5000, 4850)) # -3%
|
||
|
||
def test_outside_3_percent(self):
|
||
self.assertFalse(bd.within_tolerance(5000, 5200))
|
||
self.assertFalse(bd.within_tolerance(5000, 4700))
|
||
|
||
def test_zero_target_is_false(self):
|
||
self.assertFalse(bd.within_tolerance(0, 0))
|
||
|
||
|
||
class TestCountIslands(unittest.TestCase):
|
||
def test_no_faces(self):
|
||
self.assertEqual(bd.count_islands(0, []), 0)
|
||
|
||
def test_no_pairs_each_face_is_island(self):
|
||
self.assertEqual(bd.count_islands(3, []), 3)
|
||
|
||
def test_chain_merges_to_one(self):
|
||
self.assertEqual(bd.count_islands(4, [(0, 1), (1, 2), (2, 3)]), 1)
|
||
|
||
def test_two_groups(self):
|
||
self.assertEqual(bd.count_islands(5, [(0, 1), (3, 4)]), 3) # {0,1} {2} {3,4}
|
||
|
||
def test_duplicate_pairs_ok(self):
|
||
self.assertEqual(bd.count_islands(2, [(0, 1), (1, 0), (0, 1)]), 1)
|
||
|
||
|
||
class TestIslandLabels(unittest.TestCase):
|
||
def test_no_faces(self):
|
||
self.assertEqual(bd.island_labels(0, []), [])
|
||
|
||
def test_no_pairs_each_face_own_island(self):
|
||
labels = bd.island_labels(3, [])
|
||
self.assertEqual(len(set(labels)), 3)
|
||
|
||
def test_chain_one_island(self):
|
||
labels = bd.island_labels(4, [(0, 1), (1, 2), (2, 3)])
|
||
self.assertEqual(len(set(labels)), 1)
|
||
|
||
def test_two_groups_partition(self):
|
||
labels = bd.island_labels(5, [(0, 1), (3, 4)]) # {0,1} {2} {3,4}
|
||
self.assertEqual(len(set(labels)), 3)
|
||
self.assertEqual(labels[0], labels[1])
|
||
self.assertEqual(labels[3], labels[4])
|
||
self.assertNotEqual(labels[0], labels[2])
|
||
|
||
def test_count_matches_count_islands(self):
|
||
pairs = [(0, 1), (2, 3), (3, 4)]
|
||
self.assertEqual(len(set(bd.island_labels(6, pairs))),
|
||
bd.count_islands(6, pairs))
|
||
|
||
|
||
class TestMaxIslandAreaFraction(unittest.TestCase):
|
||
def test_empty_is_zero(self):
|
||
self.assertEqual(bd.max_island_area_fraction([], []), 0.0)
|
||
|
||
def test_zero_total_is_zero(self):
|
||
self.assertEqual(bd.max_island_area_fraction([0, 1], [0.0, 0.0]), 0.0)
|
||
|
||
def test_single_island_is_one(self):
|
||
self.assertAlmostEqual(
|
||
bd.max_island_area_fraction([7, 7, 7], [0.1, 0.2, 0.3]), 1.0)
|
||
|
||
def test_two_equal_islands_half(self):
|
||
self.assertAlmostEqual(
|
||
bd.max_island_area_fraction([0, 0, 1, 1], [0.25, 0.25, 0.25, 0.25]), 0.5)
|
||
|
||
def test_dominant_island(self):
|
||
# 岛 0 面积 0.8、岛 1 面积 0.2 -> 最大占比 0.8
|
||
self.assertAlmostEqual(
|
||
bd.max_island_area_fraction([0, 1], [0.8, 0.2]), 0.8)
|
||
|
||
|
||
class TestFlippedFraction(unittest.TestCase):
|
||
def test_empty_is_zero(self):
|
||
self.assertEqual(bd.flipped_fraction([]), 0.0)
|
||
|
||
def test_mixed(self):
|
||
self.assertAlmostEqual(bd.flipped_fraction([0.1, -0.2, 0.3, 0.4]), 0.25)
|
||
|
||
def test_all_positive(self):
|
||
self.assertEqual(bd.flipped_fraction([0.1, 0.2]), 0.0)
|
||
|
||
|
||
class TestFillRatio(unittest.TestCase):
|
||
def test_sum(self):
|
||
self.assertAlmostEqual(bd.fill_ratio([0.2, 0.3]), 0.5)
|
||
|
||
def test_clamped_to_one(self):
|
||
self.assertEqual(bd.fill_ratio([0.8, 0.9]), 1.0)
|
||
|
||
def test_empty_is_zero(self):
|
||
self.assertEqual(bd.fill_ratio([]), 0.0)
|
||
|
||
|
||
class TestUvGateOk(unittest.TestCase):
|
||
def test_clean_passes(self):
|
||
self.assertTrue(bd.uv_gate_ok(0.0, 0.0))
|
||
|
||
def test_boundary_passes(self):
|
||
self.assertTrue(bd.uv_gate_ok(bd.UV_FLIP_TOL, bd.UV_OVERLAP_TOL))
|
||
|
||
def test_flip_exceeds_fails(self):
|
||
self.assertFalse(bd.uv_gate_ok(0.03, 0.0))
|
||
|
||
def test_overlap_exceeds_fails(self):
|
||
self.assertFalse(bd.uv_gate_ok(0.0, 0.09))
|
||
|
||
def test_overlap_none_treated_as_zero(self):
|
||
self.assertTrue(bd.uv_gate_ok(0.01, None))
|
||
|
||
def test_custom_overlap_tol_allows_higher_overlap(self):
|
||
self.assertTrue(bd.uv_gate_ok(0.0, 0.12, overlap_tol=0.15))
|
||
self.assertFalse(bd.uv_gate_ok(0.0, 0.16, overlap_tol=0.15))
|
||
|
||
def test_custom_overlap_tol_does_not_relax_flip(self):
|
||
# 放宽重叠不影响翻转判定(翻转仍按 UV_FLIP_TOL)
|
||
self.assertFalse(bd.uv_gate_ok(0.03, 0.0, overlap_tol=0.5))
|
||
|
||
def test_default_overlap_tol_matches_constant(self):
|
||
self.assertTrue(bd.uv_gate_ok(0.0, bd.UV_OVERLAP_TOL))
|
||
self.assertFalse(bd.uv_gate_ok(0.0, bd.UV_OVERLAP_TOL + 0.01))
|
||
|
||
def test_default_island_frac_backward_compatible(self):
|
||
# 不传 max_island_frac(默认 0.0)时行为与旧版一致
|
||
self.assertTrue(bd.uv_gate_ok(0.0, 0.0))
|
||
|
||
def test_island_frac_exceeds_fails(self):
|
||
# 翻转/重叠都干净,但单岛占比过大也判不合格
|
||
self.assertFalse(bd.uv_gate_ok(0.0, 0.0, max_island_frac=0.5))
|
||
|
||
def test_island_frac_boundary_passes(self):
|
||
self.assertTrue(bd.uv_gate_ok(0.0, 0.0, max_island_frac=bd.UV_MAX_ISLAND_FRAC))
|
||
self.assertFalse(bd.uv_gate_ok(
|
||
0.0, 0.0, max_island_frac=bd.UV_MAX_ISLAND_FRAC + 0.01))
|
||
|
||
def test_island_frac_none_treated_as_zero(self):
|
||
self.assertTrue(bd.uv_gate_ok(0.0, 0.0, max_island_frac=None))
|
||
|
||
def test_custom_island_frac_tol(self):
|
||
self.assertTrue(bd.uv_gate_ok(
|
||
0.0, 0.0, max_island_frac=0.5, island_frac_tol=0.6))
|
||
self.assertFalse(bd.uv_gate_ok(
|
||
0.0, 0.0, max_island_frac=0.7, island_frac_tol=0.6))
|
||
|
||
|
||
class TestUvpmModeLabel(unittest.TestCase):
|
||
def test_applied_appends_suffix(self):
|
||
self.assertEqual(bd.uvpm_mode_label("seam", True), "seam+uvpm")
|
||
self.assertEqual(bd.uvpm_mode_label("smart", True), "smart+uvpm")
|
||
self.assertEqual(bd.uvpm_mode_label("smart_fallback", True), "smart_fallback+uvpm")
|
||
|
||
def test_not_applied_keeps_base(self):
|
||
self.assertEqual(bd.uvpm_mode_label("seam", False), "seam")
|
||
self.assertEqual(bd.uvpm_mode_label("smart_fallback", False), "smart_fallback")
|
||
|
||
|
||
class TestPickBestCandidate(unittest.TestCase):
|
||
def _c(self, flipped, overlap, islands, angle, frac=0.0):
|
||
return {"flipped": flipped, "overlap": overlap,
|
||
"islands": islands, "angle": angle, "max_island_frac": frac}
|
||
|
||
def test_none_when_empty(self):
|
||
self.assertIsNone(bd.pick_best_candidate([]))
|
||
|
||
def test_none_when_no_candidate_passes_gate(self):
|
||
cands = [self._c(0.30, 0.50, 100, 66), self._c(0.20, 0.40, 200, 45)]
|
||
self.assertIsNone(bd.pick_best_candidate(cands))
|
||
|
||
def test_picks_only_passing(self):
|
||
cands = [self._c(0.30, 0.50, 50, 66), self._c(0.00, 0.00, 300, 45)]
|
||
best = bd.pick_best_candidate(cands)
|
||
self.assertEqual(best["angle"], 45)
|
||
|
||
def test_picks_fewest_islands_among_passing(self):
|
||
cands = [self._c(0.00, 0.00, 120, 55), self._c(0.01, 0.02, 90, 45),
|
||
self._c(0.00, 0.00, 300, 35)]
|
||
best = bd.pick_best_candidate(cands)
|
||
self.assertEqual(best["islands"], 90)
|
||
|
||
def test_overlap_none_treated_as_pass(self):
|
||
cands = [self._c(0.01, None, 42, 66)]
|
||
best = bd.pick_best_candidate(cands)
|
||
self.assertEqual(best["islands"], 42)
|
||
|
||
def test_overlap_tol_lets_more_candidates_pass(self):
|
||
# overlap=0.12 在默认 8% 门限下不过;放宽到 0.15 后过门并被选中
|
||
cands = [self._c(0.0, 0.12, 50, 55)]
|
||
self.assertIsNone(bd.pick_best_candidate(cands))
|
||
best = bd.pick_best_candidate(cands, overlap_tol=0.15)
|
||
self.assertEqual(best["angle"], 55)
|
||
|
||
def test_dominant_island_candidate_excluded(self):
|
||
# 高角度档岛少但有巨型大岛(占比0.6)被排除,选低角度小岛且占比达标的档
|
||
cands = [self._c(0.0, 0.0, 50, 66, frac=0.6),
|
||
self._c(0.0, 0.0, 300, 20, frac=0.10)]
|
||
best = bd.pick_best_candidate(cands)
|
||
self.assertEqual(best["angle"], 20)
|
||
|
||
def test_all_dominant_returns_none(self):
|
||
# 所有档都有巨型大岛 -> 无过门候选(上层据此回退 smart)
|
||
cands = [self._c(0.0, 0.0, 50, 66, frac=0.6),
|
||
self._c(0.0, 0.0, 300, 15, frac=0.4)]
|
||
self.assertIsNone(bd.pick_best_candidate(cands))
|
||
|
||
|
||
class TestSeamAngleSweep(unittest.TestCase):
|
||
def test_descending(self):
|
||
s = list(bd.SEAM_ANGLE_SWEEP)
|
||
self.assertEqual(s, sorted(s, reverse=True))
|
||
|
||
def test_reaches_low_angle(self):
|
||
# 需下探到 <=30° 以覆盖 QR 光滑四边网格(实测 guang 30° 才过门)
|
||
self.assertLessEqual(min(bd.SEAM_ANGLE_SWEEP), 30.0)
|
||
|
||
def test_below_first_angle(self):
|
||
self.assertLess(max(bd.SEAM_ANGLE_SWEEP), bd.SEAM_ANGLE_DEG)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|