main
John Lauer Hero: laser-etch mark is now a flat sheet (glyph top faces only, about a fifth of the triangles); marked STEP regenerated the same way; provenance updated 7b01f6b 6d ago
#!/usr/bin/env python3
"""Colour-preserving laser etch: the etch service repaints the whole part (black body, grey inlay) and
drops the model's own colours. Keep the ORIGINAL coloured model untouched and add only the service's
inlay solids (the thin solids at the top of the etched part) as one light-grey 'laser_etch' part,
lifted a hair so it never z-fights with the top face.
Usage: merge_etch.py <original.step> <etched.step> <out.step> [lift_mm=0.01] [r,g,b of the mark, default light grey]
r,g,b are LINEAR RGB as OCCT stores them (the STEP file gets sRGB): 0.012 renders near-black, 0.04 is already a mid grey (0.22 sRGB)."""
import os, sys
from OCP.STEPControl import STEPControl_Reader, STEPControl_AsIs
from OCP.STEPCAFControl import STEPCAFControl_Reader, STEPCAFControl_Writer
from OCP.TDocStd import TDocStd_Document
from OCP.TCollection import TCollection_ExtendedString
from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ColorType
from OCP.TopExp import TopExp_Explorer
from OCP.TopAbs import TopAbs_SOLID, TopAbs_FACE
from OCP.BRepAdaptor import BRepAdaptor_Surface
from OCP.GeomAbs import GeomAbs_Plane
from OCP.TopoDS import TopoDS, TopoDS_Compound
from OCP.BRep import BRep_Builder
from OCP.Bnd import Bnd_Box
from OCP.BRepBndLib import BRepBndLib
from OCP.gp import gp_Trsf, gp_Vec
from OCP.TopLoc import TopLoc_Location
from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB
from OCP.TDataStd import TDataStd_Name
orig, etched, out = sys.argv[1:4]; lift = float(sys.argv[4]) if len(sys.argv) > 4 else 0.01
rgb = [float(v) for v in sys.argv[5].split(",")] if len(sys.argv) > 5 else [0.78, 0.78, 0.76]   # light on dark epoxy; pass a dark colour for a light body (an aluminium can)
def box(s):
    b = Bnd_Box(); BRepBndLib.Add_s(s, b); mn, mx = b.CornerMin(), b.CornerMax(); return mn.X(), mn.Y(), mn.Z(), mx.X(), mx.Y(), mx.Z()
r = STEPControl_Reader(); r.ReadFile(etched); r.TransferRoots(); e = r.OneShape()
top = box(e)[5]
ex = TopExp_Explorer(e, TopAbs_SOLID); inlay = []
while ex.More():
    s = TopoDS.Solid(ex.Current()); ex.Next()
    x0, y0, z0, x1, y1, z1 = box(s)
    if z1 - z0 < 0.2 and top - z1 < 0.05: inlay.append(s)      # thin, at the very top: the etch
# FLAT by default: keep only each glyph's top face (a zero-thickness sheet), not the extruded solid
# with its side walls and underside: the same mark for about a third of the triangles
flat = os.environ.get("ETCH_SOLID", "") != "1"
comp = TopoDS_Compound(); bb = BRep_Builder(); bb.MakeCompound(comp)
nfaces = 0
for s in inlay:
    if not flat: bb.Add(comp, s); continue
    fx = TopExp_Explorer(s, TopAbs_FACE); z1s = box(s)[5]
    while fx.More():
        f = TopoDS.Face(fx.Current()); fx.Next()
        ad = BRepAdaptor_Surface(f)
        if ad.GetType() != GeomAbs_Plane: continue
        n = ad.Plane().Axis().Direction()
        if abs(n.Z()) > 0.99 and abs(box(f)[5] - z1s) < 1e-4: bb.Add(comp, f); nfaces += 1
t = gp_Trsf(); t.SetTranslation(gp_Vec(0, 0, lift))
from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform
comp2 = BRepBuilderAPI_Transform(comp, t, True).Shape()   # baked (no TopLoc): a located shape becomes an XCAF reference whose colour the STEP writer drops
doc = TDocStd_Document(TCollection_ExtendedString("d"))
cr = STEPCAFControl_Reader(); cr.SetColorMode(True); cr.SetNameMode(True); cr.SetLayerMode(True); cr.ReadFile(orig); cr.Transfer(doc)
st = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()); ct = XCAFDoc_DocumentTool.ColorTool_s(doc.Main())
lab = st.AddShape(comp2, False)
TDataStd_Name.Set_s(lab, TCollection_ExtendedString("laser_etch"))
ct.SetColor(lab, Quantity_Color(rgb[0], rgb[1], rgb[2], Quantity_TOC_RGB), XCAFDoc_ColorType.XCAFDoc_ColorSurf)
ct.SetColor(lab, Quantity_Color(rgb[0], rgb[1], rgb[2], Quantity_TOC_RGB), XCAFDoc_ColorType.XCAFDoc_ColorGen)
w = STEPCAFControl_Writer(); w.SetColorMode(True); w.SetNameMode(True); w.SetLayerMode(True)
w.Transfer(doc, STEPControl_AsIs); w.Write(out)
ib = box(comp2) if inlay else None
print(f"mark bbox " + (" ".join(f"{v:.3f}" for v in ib) if ib else "none"))
print(f"inlay solids {len(inlay)}; {'flat top faces ' + str(nfaces) if flat else 'solid glyphs'}; etched top z {top:.3f}")