"""Run with python -m pytest blender/scripts/test_texture_merge.py (no Blender needed). Covers the two things that decide whether the printed marker is inked twice: that the seam is a usable cut, and that ownership is a total, disjoint, deterministic function of the triangle index. """ import unittest from texture_merge import ( MODE_AUTHORED, MODE_COMPUTED, POLICY_BEST_COVERAGE, MergeConfigError, MergePolicy, ViewFragments, assign_ownership, component_areas, components_from_seam, edge_key, edge_use_counts, ownership_disjoint, triangle_edge_keys, validate_seam, ) # A single quad as two triangles. The shared edge (0,2) is the only interior edge, and both # of its vertices lie on the mesh boundary, so a seam on it is a valid cut. QUAD2 = [(0, 1, 2), (0, 2, 3)] # A 2x2 quad grid, triangulated. Vertex 4 is the interior vertex; 1 and 7 are on the # boundary. Used for the dangling / branching / interior-free-end cases. GRID2X2 = [ (0, 1, 4), (0, 4, 3), (1, 2, 5), (1, 5, 4), (3, 4, 7), (3, 7, 6), (4, 5, 8), (4, 8, 7), ] class EdgeHelperTests(unittest.TestCase): def test_edge_key_is_canonical(self): self.assertEqual(edge_key(3, 1), (1, 3)) self.assertEqual(edge_key(1, 3), (1, 3)) def test_triangle_edge_keys(self): self.assertEqual(triangle_edge_keys((5, 1, 3)), ((1, 5), (1, 3), (3, 5))) def test_triangle_edge_keys_rejects_non_triangle(self): with self.assertRaises(MergeConfigError): triangle_edge_keys((1, 2, 3, 4)) def test_boundary_edges_of_a_single_quad(self): _, boundary, non_manifold = edge_use_counts(QUAD2) self.assertEqual(boundary, {(0, 1), (0, 3), (1, 2), (2, 3)}) self.assertEqual(non_manifold, set()) class ComponentsFromSeamTests(unittest.TestCase): def test_no_seam_is_one_component(self): self.assertEqual(components_from_seam(QUAD2, []), [0, 0]) def test_shared_edge_seam_splits_the_quad(self): components = components_from_seam(QUAD2, [(0, 2)]) self.assertEqual(len(set(components)), 2) def test_valid_path_across_the_grid_splits_it(self): components = components_from_seam(GRID2X2, [(1, 4), (4, 7)]) self.assertGreater(len(set(components)), 1) def test_deterministic_under_edge_order(self): self.assertEqual( components_from_seam(GRID2X2, [(4, 7), (1, 4)]), components_from_seam(GRID2X2, [(1, 4), (4, 7)]), ) def test_component_areas_sum_to_total(self): components = components_from_seam(QUAD2, [(0, 2)]) areas = [1.0, 1.0] self.assertAlmostEqual(sum(component_areas(components, areas).values()), 2.0) class ValidateSeamTests(unittest.TestCase): def test_valid_cut_on_the_quad_has_no_errors(self): components = components_from_seam(QUAD2, [(0, 2)]) self.assertEqual(validate_seam(QUAD2, [(0, 2)], components), []) def test_valid_boundary_to_boundary_path_has_no_errors(self): components = components_from_seam(GRID2X2, [(1, 4), (4, 7)]) self.assertEqual(validate_seam(GRID2X2, [(1, 4), (4, 7)], components), []) def test_no_seam_edges_is_an_error(self): errors = validate_seam(QUAD2, [], components_from_seam(QUAD2, [])) self.assertTrue(any("no seam edges" in e for e in errors), errors) def test_dangling_seam_that_does_not_separate_is_rejected(self): """The load-bearing rule: seam faces in the same component means no separation.""" components = components_from_seam(GRID2X2, [(1, 4)]) errors = validate_seam(GRID2X2, [(1, 4)], components) self.assertTrue(any("SAME" in e and "component" in e for e in errors), errors) def test_free_end_in_the_interior_is_rejected(self): components = components_from_seam(GRID2X2, [(1, 4)]) errors = validate_seam(GRID2X2, [(1, 4)], components) self.assertTrue(any("free seam end" in e for e in errors), errors) def test_branching_seam_is_rejected(self): seam = [(1, 4), (4, 7), (4, 5)] components = components_from_seam(GRID2X2, seam) errors = validate_seam(GRID2X2, seam, components) self.assertTrue(any("degree" in e for e in errors), errors) def test_unknown_edge_is_rejected(self): seam = [(100, 200)] errors = validate_seam(QUAD2, seam, components_from_seam(QUAD2, [])) self.assertTrue(any("not edges of any triangle" in e for e in errors), errors) def test_component_count_mismatch_is_rejected(self): errors = validate_seam(QUAD2, [(0, 2)], [0]) self.assertTrue(any("one per triangle" in e for e in errors), errors) class PolicyTests(unittest.TestCase): def test_rejects_unknown_mode(self): with self.assertRaises(MergeConfigError): MergePolicy(mode="guessed") def test_rejects_coverage_out_of_range(self): with self.assertRaises(MergeConfigError): MergePolicy(min_triangle_coverage=1.5) def test_rejects_incidence_out_of_range(self): with self.assertRaises(MergeConfigError): MergePolicy(incidence_limit_deg=120.0) def test_defaults_match_the_calibration(self): policy = MergePolicy() self.assertEqual(policy.incidence_limit_deg, 62.0) self.assertAlmostEqual(policy.min_footprint_mm2, 2.0e-3) self.assertEqual(policy.min_triangle_coverage, 0.60) self.assertEqual(policy.max_hole_fraction, 0.02) class ViewFragmentsTests(unittest.TestCase): def test_rejects_non_parallel_arrays(self): with self.assertRaises(MergeConfigError): ViewFragments(triangle=[0, 1], incidence_deg=[1.0], footprint_mm2=[1.0, 1.0]) def test_groups_by_triangle(self): vf = ViewFragments( triangle=[0, 1, 0], incidence_deg=[1.0, 2.0, 3.0], footprint_mm2=[0.1, 0.2, 0.3] ) grouped = vf.by_triangle() self.assertEqual(len(grouped[0]), 2) self.assertEqual(len(grouped[1]), 1) class ComputedOwnershipTests(unittest.TestCase): def setUp(self): self.area = [1.0, 1.0, 1.0] self.fragments = { "a": ViewFragments( triangle=[0, 1], incidence_deg=[10.0, 10.0], footprint_mm2=[1.0, 0.5] ), "b": ViewFragments( triangle=[1, 2], incidence_deg=[10.0, 10.0], footprint_mm2=[1.0, 1.0] ), } def test_priority_then_fall_through(self): """Triangle 1 is only 50% covered by 'a', so it must fall through to 'b'.""" result = assign_ownership(self.area, self.fragments, ("a", "b")) self.assertEqual(result["owner"], {0: "a", 1: "b", 2: "b"}) def test_every_triangle_has_at_most_one_owner(self): result = assign_ownership(self.area, self.fragments, ("a", "b")) self.assertTrue(ownership_disjoint(result["owner"])) def test_deterministic_under_shuffled_fragment_order(self): shuffled = { "a": ViewFragments( triangle=[1, 0], incidence_deg=[10.0, 10.0], footprint_mm2=[0.5, 1.0] ), "b": ViewFragments( triangle=[2, 1], incidence_deg=[10.0, 10.0], footprint_mm2=[1.0, 1.0] ), } self.assertEqual( assign_ownership(self.area, self.fragments, ("a", "b"))["owner"], assign_ownership(self.area, shuffled, ("a", "b"))["owner"], ) def test_incidence_limit_is_inclusive(self): fragments = { "a": ViewFragments(triangle=[0], incidence_deg=[62.0], footprint_mm2=[1.0]), } result = assign_ownership([1.0], fragments, ("a",), MergePolicy()) self.assertEqual(result["owner"], {0: "a"}) def test_incidence_just_over_the_limit_is_a_hole(self): fragments = { "a": ViewFragments(triangle=[0], incidence_deg=[62.0001], footprint_mm2=[1.0]), } result = assign_ownership([1.0], fragments, ("a",), MergePolicy()) self.assertEqual(result["owner"], {}) self.assertAlmostEqual(result["hole_fraction"], 1.0) def test_footprint_floor_is_inclusive(self): """Isolate the footprint gate: coverage is switched off so it cannot bind first.""" policy = MergePolicy(min_triangle_coverage=0.0) at_floor = { "a": ViewFragments(triangle=[0], incidence_deg=[1.0], footprint_mm2=[2.0e-3]), } self.assertEqual(assign_ownership([1.0], at_floor, ("a",), policy)["owner"], {0: "a"}) below_floor = { "a": ViewFragments(triangle=[0], incidence_deg=[1.0], footprint_mm2=[1.999e-3]), } self.assertEqual(assign_ownership([1.0], below_floor, ("a",), policy)["owner"], {}) def test_footprint_floor_does_not_bypass_the_coverage_gate(self): """A fragment above the footprint floor can still fail the coverage gate.""" fragments = { "a": ViewFragments(triangle=[0], incidence_deg=[1.0], footprint_mm2=[2.0e-3]), } # 2e-3 mm^2 is only 0.2% of a 1 mm^2 triangle, far below min_triangle_coverage. self.assertEqual(assign_ownership([1.0], fragments, ("a",), MergePolicy())["owner"], {}) def test_coverage_threshold_is_inclusive(self): """0.6 of a unit triangle is exactly at the threshold and must be accepted.""" fragments = { "a": ViewFragments(triangle=[0], incidence_deg=[1.0], footprint_mm2=[0.6]), } result = assign_ownership([1.0], fragments, ("a",), MergePolicy()) self.assertEqual(result["owner"], {0: "a"}) def test_coverage_just_below_the_threshold_is_a_hole(self): fragments = { "a": ViewFragments(triangle=[0], incidence_deg=[1.0], footprint_mm2=[0.599]), } result = assign_ownership([1.0], fragments, ("a",), MergePolicy()) self.assertEqual(result["owner"], {}) def test_no_view_data_leaves_all_triangles_as_holes(self): result = assign_ownership(self.area, {"a": ViewFragments([], [], [])}, ("a",)) self.assertEqual(result["owner"], {}) self.assertAlmostEqual(result["hole_mm2"], 3.0) def test_rejects_missing_view_data(self): with self.assertRaises(MergeConfigError): assign_ownership(self.area, self.fragments, ("a", "missing")) def test_rejects_duplicate_priority(self): with self.assertRaises(MergeConfigError): assign_ownership(self.area, self.fragments, ("a", "a")) class AuthoredOwnershipTests(unittest.TestCase): def test_component_seam_owns_whole_components(self): components = components_from_seam(QUAD2, [(0, 2)]) area = [1.0, 1.0] # Each view qualifies only on its own component. fragments = { "a": ViewFragments(triangle=[0], incidence_deg=[10.0], footprint_mm2=[1.0]), "b": ViewFragments(triangle=[1], incidence_deg=[10.0], footprint_mm2=[1.0]), } result = assign_ownership( area, fragments, ("a", "b"), MergePolicy(mode=MODE_AUTHORED), components=components, ) owner_by_component = {components[t]: v for t, v in result["owner"].items()} self.assertEqual(len(set(owner_by_component.values())), 2) self.assertEqual(set(result["owner"]), {0, 1}) def test_best_coverage_uses_qualifying_footprint_not_visible_count(self): """The trap: a view seeing MORE triangles but at grazing incidence must lose. This is the real mesh's situation in miniature — on `sensors/finger/r01` both views see ~100% of each component's triangles, so a visible-count rule ties and silently hands every component to the first view in priority order. """ area = [1.0, 1.0] fragments = { # Sees both triangles, but at 89 deg: nothing qualifies. "a": ViewFragments( triangle=[0, 1], incidence_deg=[89.0, 89.0], footprint_mm2=[1.0, 1.0] ), # Sees one triangle, frontally. "b": ViewFragments(triangle=[0], incidence_deg=[10.0], footprint_mm2=[1.0]), } result = assign_ownership( area, fragments, ("a", "b"), MergePolicy(mode=MODE_AUTHORED), components=[0, 0], ) self.assertEqual(result["component_owner"], {0: "b"}) def test_pinned_component_can_override_the_metric(self): area = [1.0, 1.0] fragments = { "a": ViewFragments(triangle=[0], incidence_deg=[10.0], footprint_mm2=[1.0]), "b": ViewFragments(triangle=[0, 1], incidence_deg=[10.0, 10.0], footprint_mm2=[1.0, 1.0]), } result = assign_ownership( area, fragments, ("a", "b"), MergePolicy(mode=MODE_AUTHORED, component_policy="pinned"), components=[0, 0], pins={0: "a"}, ) self.assertEqual(result["component_owner"], {0: "a"}) # 'a' covers only triangle 0, so triangle 1 is an honest reported hole. self.assertEqual(result["owner"], {0: "a"}) self.assertAlmostEqual(result["hole_fraction"], 0.5) def test_unknown_pin_target_is_rejected(self): with self.assertRaises(MergeConfigError): assign_ownership( [1.0], {"a": ViewFragments(triangle=[0], incidence_deg=[1.0], footprint_mm2=[1.0])}, ("a",), MergePolicy(mode=MODE_AUTHORED), components=[0], pins={0: "nope"}, ) def test_authored_mode_requires_components(self): with self.assertRaises(MergeConfigError): assign_ownership( [1.0], {"a": ViewFragments(triangle=[0], incidence_deg=[1.0], footprint_mm2=[1.0])}, ("a",), MergePolicy(mode=MODE_AUTHORED), ) def test_disjointness_holds_in_authored_mode(self): components = components_from_seam(GRID2X2, [(1, 4), (4, 7)]) area = [1.0] * len(GRID2X2) triangles = list(range(len(GRID2X2))) fragments = { "a": ViewFragments(triangle=triangles, incidence_deg=[10.0] * len(triangles), footprint_mm2=[1.0] * len(triangles)), "b": ViewFragments(triangle=triangles, incidence_deg=[20.0] * len(triangles), footprint_mm2=[1.0] * len(triangles)), } result = assign_ownership( area, fragments, ("a", "b"), MergePolicy(mode=MODE_AUTHORED), components=components ) self.assertTrue(ownership_disjoint(result["owner"])) self.assertEqual(len(result["owner"]), len(GRID2X2)) self.assertAlmostEqual(result["hole_fraction"], 0.0) class PartitionTests(unittest.TestCase): def test_owned_plus_hole_equals_total_area(self): area = [0.5, 1.5, 2.0] fragments = { "a": ViewFragments(triangle=[0], incidence_deg=[1.0], footprint_mm2=[1.0]), } result = assign_ownership(area, fragments, ("a",)) self.assertAlmostEqual(result["owned_mm2"] + result["hole_mm2"], result["total_mm2"]) self.assertAlmostEqual(result["total_mm2"], 4.0) def test_per_view_areas_account_for_every_owned_triangle(self): area = [1.0, 1.0, 1.0] fragments = { "a": ViewFragments(triangle=[0, 1], incidence_deg=[1.0, 1.0], footprint_mm2=[1.0, 1.0]), "b": ViewFragments(triangle=[2], incidence_deg=[1.0], footprint_mm2=[1.0]), } result = assign_ownership(area, fragments, ("a", "b")) self.assertAlmostEqual(sum(result["per_view_mm2"].values()), result["owned_mm2"]) self.assertEqual(result["per_view_mm2"], {"a": 2.0, "b": 1.0}) def test_default_mode_is_computed(self): area = [1.0] fragments = { "a": ViewFragments(triangle=[0], incidence_deg=[1.0], footprint_mm2=[1.0]), } self.assertEqual(assign_ownership(area, fragments, ("a",))["mode"], MODE_COMPUTED) if __name__ == "__main__": unittest.main()