main
John Lauer Publish 0.1.94 bca797f
"""graft() on both shapes of the /laser-etch service output (synthetic fixtures built from proxy_box.step):
  flat   (service since 2026-10) Adom.Chip (carrying the proxy body) and Adom.LaserEtch as two unlinked roots
  nested (earlier service)       root -> [Adom.Chip -> proxy body, Adom.LaserEtch]
Either way the result is the documented one: one root named after the model, Adom.Chip -> the model
(colours kept), Adom.LaserEtch beside it with its geometry where it was, the proxy body and its colour gone.
Run: python3 -m unittest -v test_step_graft   (from tools/laser-etch; offline)"""
import os, re, sys, unittest

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import step_text as ST

PROXY = os.path.join(HERE, "proxy_box.step")
CHIP_RGB, MARK_RGB, PART_RGB = (0.9, 0.1, 0.1), (0.95, 0.95, 0.95), (0.2, 0.3, 0.4)


def colour(s, rgb):
    """Give the file's (single) brep a colour, the way OpenCascade writes it."""
    brep = next(k for k in s.order if s.kind(k) == "MANIFOLD_SOLID_BREP")
    rep = next(k for k in s.order if s.kind(k) == "ADVANCED_BREP_SHAPE_REPRESENTATION")
    ctx = s.refs(rep)[-1]
    a = s._add(f"COLOUR_RGB('',{ST.fmt(rgb[0])},{ST.fmt(rgb[1])},{ST.fmt(rgb[2])})")
    b = s._add(f"FILL_AREA_STYLE_COLOUR('',#{a})"); c = s._add(f"FILL_AREA_STYLE('',(#{b}))")
    d = s._add(f"SURFACE_STYLE_FILL_AREA(#{c})"); e = s._add(f"SURFACE_SIDE_STYLE('',(#{d}))")
    f = s._add(f"SURFACE_STYLE_USAGE(.BOTH.,#{e})"); g = s._add(f"PRESENTATION_STYLE_ASSIGNMENT((#{f}))")
    h = s._add(f"STYLED_ITEM('color',(#{g}),#{brep})")
    s._add(f"MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',(#{h}),#{ctx})")
    return s


def part(name, box, rgb):
    s = ST.Step(PROXY).scale_axes(*box)
    s.rename_product("adom_mark_proxy", name)
    return ST.Step(colour(s, rgb).text)


def flat_service_output():
    chip = part("Adom.Chip", (-1, -0.5, 0.0, 2, 1, 0.5), CHIP_RGB)
    mark = part("Adom.LaserEtch", (-0.6, -0.2, 0.505, 1.2, 0.4, 0.0), MARK_RGB)
    off = max(chip.order) + 10
    for k in mark.order:
        chip.body[k + off] = chip._remap(mark.body[k], off); chip.lead[k + off] = ""; chip.order.append(k + off)
    chip._rev = None
    return ST.Step(chip.text)


def nested_service_output():
    s = flat_service_output(); s.flatten(); s._assemble_flat_roots()
    return ST.Step(s.text)


def colours(s):
    return set(tuple(round(float(v), 3) for v in re.findall(r"[-\d.E+]+", s.body[k].split(",", 1)[1])) for k in s.order if s.kind(k) == "COLOUR_RGB")


def tree(s):
    name = lambda pd: next(n for p, n in s.products() if s._pd_of(p) == pd)
    return sorted(f"{name(s.refs(k)[0])}>{name(s.refs(k)[1])}" for k in s.order if s.kind(k) == "NEXT_ASSEMBLY_USAGE_OCCURRENCE")


def mark_z(s):
    ents = s.product_entities("Adom.LaserEtch") - s._frame_ents(s.rep_frames())   # geometry, not placement frames
    zs = [s.coords(k)[2] for k in ents if s.kind(k) == "CARTESIAN_POINT"]
    return round(min(zs), 6), round(max(zs), 6)


class Graft(unittest.TestCase):
    def check(self, e):
        e.flatten()
        before = mark_z(e)
        donor = part("SOIC-8", (-2, -2.5, 0.0, 4, 5, 0.5), PART_RGB); donor.flatten()
        e.graft(donor, "Adom.Chip")
        e.rotate90("z", 1); e.rotate90("z", -1)
        out = ST.Step(e.text); out.flatten()
        self.assertEqual([n for _, n in out._root_products()], ["SOIC-8"])
        self.assertEqual(tree(out), ["Adom.Chip>SOIC-8", "SOIC-8>Adom.Chip", "SOIC-8>Adom.LaserEtch"])
        self.assertNotIn("adom_mark_proxy", out.text)
        self.assertEqual(colours(out), {PART_RGB, MARK_RGB})          # model colour kept, proxy colour gone
        self.assertEqual(mark_z(out), before)                          # the mark stays flat where it was
        self.assertEqual(sum(1 for k in out.order if out.kind(k) == "MANIFOLD_SOLID_BREP"), 2)

    def test_flat_roots(self):
        s = flat_service_output()
        self.assertEqual(sorted(n for _, n in s._root_products()), ["Adom.Chip", "Adom.LaserEtch"])
        self.check(s)

    def test_nested(self):
        s = nested_service_output()
        self.assertEqual([n for _, n in s._root_products()], ["adom_mark_proxy"])
        self.assertIn("Adom.Chip>adom_mark_proxy", tree(s))
        self.check(s)

    def test_missing_mark_refused(self):
        s = part("Adom.Chip", (-1, -0.5, 0.0, 2, 1, 0.5), CHIP_RGB); s.flatten()
        donor = part("P", (0, 0, 0, 1, 1, 1), PART_RGB); donor.flatten()
        with self.assertRaisesRegex(SystemExit, "Adom.LaserEtch"):
            s.graft(donor)


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