AnasSarkiz/magnetic-shutter-remote-r8

A USB‑C rechargeable battery-powered ESP32‑C3 wireless controller with Li‑ion charging, protected 3.3‑V buck‑boost regulation, thermal/safety monitoring, status LEDs, buttons, programming UART, crystal clock, and 2.4‑GHz antenna matching network.

Version
0.4.0-prototype
License
unset
Stars
0

tests/cam/test_process_metrics.py

import importlib.util
import sys
import unittest
from pathlib import Path
from shapely.geometry import box, Polygon

SPEC = importlib.util.spec_from_file_location('process_metrics', Path(__file__).resolve().parents[2]/'scripts/review-r8-process.py')
METRICS = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = METRICS
SPEC.loader.exec_module(METRICS)


class StencilReleaseTest(unittest.TestCase):
    def test_asymmetric_aperture_is_attributed_by_copper_contact_and_mask(self):
        land=METRICS.GEOMETRY.Copper('pad','smt','top','net',box(0,0,1,1),'component')
        context={'lands':[land],'owner':'component','layer':'top','mask':box(-.05,-.05,1.05,1.05)}
        self.assertEqual(METRICS.attribute_paste(box(.2,.1,1.02,.8),context).id,'pad')
        with self.assertRaisesRegex(ValueError,'solder-mask'):
            METRICS.attribute_paste(box(.2,.1,1.1,.8),context)
        with self.assertRaisesRegex(ValueError,'exactly one'):
            METRICS.attribute_paste(box(2,2,3,3),context)

    def test_aperture_bridging_another_component_is_rejected(self):
        lands=[METRICS.GEOMETRY.Copper('a','smt','top','a',box(0,0,1,1),'one'),
               METRICS.GEOMETRY.Copper('b','smt','top','b',box(1.1,0,2.1,1),'two')]
        with self.assertRaisesRegex(ValueError,'exactly one'):
            METRICS.attribute_paste(box(.2,.2,1.4,.8),{'lands':lands,'owner':'one','layer':'top','mask':box(-1,-1,3,3)})

    def test_native_polygon_pad_requires_vertices_without_xy_fields(self):
        pad = {'shape':'polygon','points':[{'x':0,'y':0},{'x':2,'y':0},{'x':2,'y':1},{'x':1,'y':1},{'x':1,'y':2},{'x':0,'y':2}]}
        geometry = METRICS.GEOMETRY.pad(pad)
        self.assertAlmostEqual(geometry.area,3)
        self.assertEqual(geometry.bounds,(0,0,2,2))

    def test_narrow_aperture_fails_release_despite_long_dimension(self):
        result = METRICS.release_metrics(box(0,0,.1,2),.1)
        self.assertLess(result['area_ratio'],.66)
        self.assertLess(result['aspect_ratio'],1.5)

    def test_polygon_uses_true_area_not_bounding_box(self):
        triangle = Polygon([(0,0),(1,0),(0,1)])
        result = METRICS.release_metrics(triangle,.1)
        self.assertAlmostEqual(result['area_ratio'],.5/((2+2**.5)*.1))
        self.assertAlmostEqual(result['aspect_ratio'],(2**-.5)/.1)

    def test_invalid_thickness_is_rejected(self):
        with self.assertRaisesRegex(ValueError,'Invalid stencil'):
            METRICS.release_metrics(box(0,0,1,1),0)


class CompleteSilkscreenTest(unittest.TestCase):
    def test_empty_export_has_no_mask_clearance_and_serializes_as_strict_json(self):
        import json
        from zipfile import ZipFile
        from gerbonara import GerberFile
        root=Path(__file__).resolve().parents[2]
        archive=root/'fabrication/R8-standard-programmer-2026-10-05/R8-standard-programmer-route04-Gerbers.zip'
        with ZipFile(archive) as z:
            empty=GerberFile.from_string(z.read('B_Paste.gbr').decode())
            mask=GerberFile.from_string(z.read('B_Mask.gbr').decode())
        self.assertEqual(empty.objects,[])
        result=METRICS.silkscreen_metrics(empty,METRICS.READER.copper_geometry(empty),
                                        METRICS.READER.copper_geometry(mask),box(-22,-28,22,28))
        self.assertIsNone(result['minimum_stroke_mm'])
        self.assertIsNone(result['mask_gap_mm'])
        self.assertEqual(result['outside_outline_mm2'],0)
        self.assertEqual(json.loads(json.dumps(result,allow_nan=False)),result)

    def test_supplier_circles_are_measured_beyond_text_only_attribution(self):
        # Original failing native export is retained; no synthetic text mirror.
        from zipfile import ZipFile
        from gerbonara import GerberFile
        from shapely.geometry import LineString
        from shapely.ops import polygonize, unary_union
        archive=Path(__file__).resolve().parents[2]/'fabrication/R8-standard-programmer-2026-10-05/R8-standard-programmer-route04-Gerbers.zip'
        with ZipFile(archive) as z:
            silk=GerberFile.from_string(z.read('F_SilkScreen.gbr').decode())
            mask=GerberFile.from_string(z.read('F_Mask.gbr').decode())
            edge=GerberFile.from_string(z.read('Edge_Cuts.gbr').decode())
        outline,=polygonize(unary_union([LineString([(p.x1,p.y1),(p.x2,p.y2)]) for obj in edge.objects for p in obj.to_primitives()]))
        result=METRICS.silkscreen_metrics(silk,METRICS.READER.copper_geometry(silk),METRICS.READER.copper_geometry(mask),outline)
        self.assertAlmostEqual(result['minimum_stroke_mm'],.1)
        self.assertLess(result['mask_gap_mm'],.15)
        self.assertEqual(result['outside_outline_mm2'],0)


class FunctionalLegendOverlapTest(unittest.TestCase):
    def test_original_native_pair_power_collision_is_rejected(self):
        import json
        root=Path(__file__).resolve().parents[2]
        review=json.loads((root/'evidence/R8-compact-pcb-2026-10-06/failed-route20-visual/process-review.json').read_text())
        self.assertFalse(review['failures'])  # Original mask/stroke-only audit missed it.
        overlaps=METRICS.label_overlap_failures(review['labels'])
        self.assertEqual(len(overlaps),1)
        self.assertEqual(set(overlaps[0]['texts']),{'PAIR','POWER'})

    def test_separate_positions_and_opposite_layers_are_controls(self):
        first={'text':'PAIR','layer':'top','bounds_mm':[0,0,4,1]}
        other={'text':'POWER','layer':'bottom','bounds_mm':[0,0,4,1]}
        self.assertEqual(METRICS.label_overlap_failures([first,other]),[])
        other.update(layer='top',bounds_mm=[0,2,4,3])
        self.assertEqual(METRICS.label_overlap_failures([first,other]),[])


if __name__=='__main__':
    unittest.main()