Files
ykcheungab 267334d543 fix cli redundant images
finger-xynova first design completed
2026-09-28 16:18:17 +08:00

375 lines
16 KiB
Python

"""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()