main
John Lauer Publish 0.2.19 81ffca7
"""Issue #89 regressions, offline (synthetic step_meta; no service).

  KiCad generator models carry no printed marking: the body's top edge fillets, its corner blends and the
  generator's 3-face pin-1 mark (a cylinder wall in the body, a disc and a cylinder in the `mark` part) are
  not glyphs, so an LQFP-48 with ef = 0.05 (STM32G431CBT6) is not refused.
  A real vendor marking is still found: coplanar letter islands on the top face, and raised letters (the
  AMPLH5030S-150MT vendor model: letters 0.02 mm proud of the body top), and the error names the faces.
Run: python3 -m unittest -v test_vendor_mark   (from tools/laser-etch)"""
import os, sys, unittest

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import mark_plan as MP
from test_mark_plan_rows import PlanOffline, kicad_mod
import tempfile


def face(fid, st, bbox, area, normal=None, product=1):
    return dict(face_id=fid, product_id=product, surface_type=st, bbox_mm=bbox, area_mm2=area, normal=normal)


def lqfp48_meta():
    """The faces at the top of the STM32G431CBT6 LQFP-48 from kicad-packages3D-generator (ef = 0.05), as
    /step-meta reports them (stood up, marks removed). Product 0 is the assembly and lists the small top
    faces again, as the service does."""
    f = [
        face("1_0", "BSPLINE", [-3.2875, 3.2916, 0.8725, 3.2875, 3.425, 1.5], 3.9142),
        face("1_1", "BSPLINE", [3.2208, 3.2208, 0.8725, 3.425, 3.425, 1.5], 0.0906),
        face("1_2", "BSPLINE", [-3.419, 3.2511, 1.4604, 3.419, 3.3, 1.5], 0.4359),
        face("1_28", "BSPLINE", [-3.425, 3.2208, 0.8725, -3.2208, 3.425, 1.5], 0.0906),
        face("1_29", "BSPLINE", [3.2916, -3.2875, 0.8725, 3.425, 3.2875, 1.5], 3.9142),
        face("1_30", "BSPLINE", [3.1214, 3.1214, 1.461, 3.369, 3.369, 1.5], 0.006),
        face("1_31", "BSPLINE", [-3.369, 3.1214, 1.461, -3.1214, 3.369, 1.5], 0.006),
        face("1_32", "PLANE", [-3.2511, -3.2511, 1.5, 3.2511, 3.2511, 1.5], 42.0788, [0.0, 0.0, 1.0]),
        face("1_36", "BSPLINE", [-3.425, -3.2875, 0.8725, -3.2916, 3.2875, 1.5], 3.9142),
        face("1_89", "BSPLINE", [3.2208, -3.425, 0.8725, 3.425, -3.2208, 1.5], 0.0906),
        face("1_90", "BSPLINE", [3.2511, -3.419, 1.4604, 3.3, 3.419, 1.5], 0.4359),
        face("1_115", "BSPLINE", [-3.3, -3.419, 1.4604, -3.2511, 3.419, 1.5], 0.4359),
        face("1_116", "BSPLINE", [-3.369, -3.369, 1.461, -3.1214, -3.1214, 1.5], 0.006),
        face("1_117", "BSPLINE", [3.1214, -3.369, 1.461, 3.369, -3.1214, 1.5], 0.006),
        face("1_118", "BSPLINE", [-3.419, -3.3, 1.4604, 3.419, -3.2511, 1.5], 0.4359),
        face("1_119", "CYLINDER", [-2.8416, 2.3416, 1.4, -2.3416, 2.8416, 1.5], 0.1571),
        face("1_148", "BSPLINE", [-3.425, -3.425, 0.8725, -3.2208, -3.2208, 1.5], 0.0906),
        face("1_166", "BSPLINE", [-3.2875, -3.425, 0.8725, 3.2875, -3.2916, 1.5], 3.9142),
        face("3_0", "PLANE", [-2.8416, 2.3416, 1.5, -2.3416, 2.8416, 1.5], 0.1963, [0.0, 0.0, -1.0], 3),
        face("3_1", "CYLINDER", [-2.8416, 2.3416, 1.4, -2.3416, 2.8416, 1.5], 0.1571, None, 3),
        face("bottom", "PLANE", [-3.5, -3.5, 0.1, 3.5, 3.5, 0.1], 49.0, [0.0, 0.0, -1.0]),
        face("pins", "BSPLINE", [-4.5, -4.5, 0.0, 4.5, 4.5, 0.8725], 5.0, None, 2),
    ]
    dup = [dict(x, face_id="0_" + x["face_id"].split("_")[1], product_id=0) for x in f if x["area_mm2"] < 0.21]
    return dict(faces=f + dup, up_axis_hint="z",
                products=[dict(id=0, name="STM32G431CBT6_LQFP-48_7x7mm_P0.5mm"), dict(id=1, name="body"), dict(id=2, name="pins"), dict(id=3, name="mark")])


def with_letters(meta, raised=0.0, n=12):
    """Letter islands in a row across the top face's middle: coplanar faces (raised=0), or raised letters
    (a top `raised` mm proud, a floor on the body top, and four walls each)."""
    m = dict(meta, faces=list(meta["faces"]))
    z = 1.5
    for i in range(n):
        x = -2.4 + 0.4 * i
        b = [x, -0.25, z + raised, x + 0.25, 0.25, z + raised]
        m["faces"].append(face(f"L{i}", "PLANE", b, 0.06, [0.0, 0.0, 1.0], 4))
        if raised:
            m["faces"].append(face(f"L{i}f", "PLANE", [x, -0.25, z, x + 0.25, 0.25, z], 0.06, [0.0, 0.0, 1.0], 4))
            for k, wb in enumerate(([x, -0.25, z, x, 0.25, z + raised], [x + 0.25, -0.25, z, x + 0.25, 0.25, z + raised],
                                    [x, -0.25, z, x + 0.25, -0.25, z + raised], [x, 0.25, z, x + 0.25, 0.25, z + raised])):
                m["faces"].append(face(f"L{i}w{k}", "PLANE", wb, 0.005, None, 4))
    return m


def lqfp48_pads():
    pads, n = [], 1
    for side in range(4):
        for k in range(12):
            t = -2.75 + 0.5 * k
            x, y, w, h = {0: (-4.1, -t, 1.5, 0.3), 1: (t, -4.1, 0.3, 1.5), 2: (4.1, t, 1.5, 0.3), 3: (-t, 4.1, 0.3, 1.5)}[side]
            pads.append(dict(num=str(n), x=x, y=y, w=w, h=h)); n += 1
    return pads


def plan_of(meta, pads):
    with tempfile.TemporaryDirectory(prefix="vendormark-") as d:
        fp = os.path.join(d, "synth.kicad_mod")
        with open(fp, "w") as fh:
            fh.write(kicad_mod(pads))
        with PlanOffline(meta):
            return MP.plan(os.path.join(HERE, "proxy_box.step"), fp, "STM32G431CBT6")


class KiCadGeneratorModel(unittest.TestCase):
    def test_fillets_corners_and_pin1_mark_are_not_glyphs(self):
        b = MP.top_band(lqfp48_meta(), [], [], None, False)
        self.assertEqual(b["faceId"], "1_32")
        self.assertEqual(b["vendorGlyphFaces"], 0, b["vendorGlyphFaceList"])

    def test_plan_is_not_refused(self):
        p = plan_of(lqfp48_meta(), lqfp48_pads())
        self.assertFalse(p["vendorMarking"]["present"], p["vendorMarking"])
        self.assertFalse(any("printed marking" in w for w in p["warnings"]), p["warnings"])

    def test_disc_shape(self):
        self.assertTrue(MP._is_disc(face("d", "PLANE", [0, 0, 1, 0.5, 0.5, 1], 0.19635)))
        self.assertFalse(MP._is_disc(face("r", "PLANE", [0, 0, 1, 0.5, 0.5, 1], 0.25)))      # a square letter block
        self.assertFalse(MP._is_disc(face("o", "PLANE", [0, 0, 1, 0.5, 0.5, 1], 0.08)))      # an 'O' ring


class VendorMarking(unittest.TestCase):
    def test_coplanar_letter_islands_are_found_and_named(self):
        p = plan_of(with_letters(lqfp48_meta()), lqfp48_pads())
        self.assertTrue(p["vendorMarking"]["present"])
        self.assertEqual(p["vendorMarking"]["glyphFaces"], 12)
        w = next(w for w in p["warnings"] if "printed marking" in w)
        self.assertIn("L0 (PLANE", w)
        self.assertNotIn("1_30", w)

    def test_raised_letters_are_found_on_the_body_top(self):
        b = MP.top_band(with_letters(lqfp48_meta(), raised=0.02), [], [], None, False)
        self.assertEqual(b["faceId"], "1_32")                 # the body top, not a letter's own top
        self.assertEqual(b["vendorGlyphFaces"], 12, b["vendorGlyphFaceList"])   # each letter once, no walls

    def test_too_few_islands_are_not_a_marking(self):
        p = plan_of(with_letters(lqfp48_meta(), n=3), lqfp48_pads())
        self.assertFalse(p["vendorMarking"]["present"])


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