"""OCC-native STEP → GLB export script. Reads a STEP file via OCP/XCAF (preserving part names and embedded colors), tessellates with BRepMesh, optionally applies per-part hex colors, and writes a binary GLB in meters (Y-up, glTF convention). No Blender required. Uses the same OCP bindings that cadquery ships with. Usage: python3 export_step_to_gltf.py \ --step_path /path/to/file.stp \ --output_path /path/to/output.glb \ [--linear_deflection 0.1] \ [--angular_deflection 0.5] \ [--color_map '{"RingInner": "#4C9BE8", "RingOuter": "#E85B4C"}'] Exit 0 on success, exit 1 on failure. """ from __future__ import annotations import argparse import json import sys import traceback from pathlib import Path PALETTE_HEX = [ "#4C9BE8", "#E85B4C", "#4CBE72", "#E8A84C", "#A04CE8", "#4CD4E8", "#E84CA8", "#7EC850", "#E86B30", "#5088C8", ] def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--step_path", required=True) parser.add_argument("--output_path", required=True) parser.add_argument( "--linear_deflection", type=float, default=0.1, help="OCC linear deflection for tessellation (mm). Smaller = finer mesh. Default 0.1", ) parser.add_argument( "--angular_deflection", type=float, default=0.5, help="OCC angular deflection (radians). Default 0.5", ) parser.add_argument( "--color_map", default="{}", help='JSON dict mapping part name → hex color, e.g. \'{"Ring": "#4C9BE8"}\'', ) return parser.parse_args() def _hex_to_occ_color(hex_color: str): """Convert '#RRGGBB' → Quantity_Color (linear float).""" from OCP.Quantity import Quantity_Color, Quantity_TOC_RGB h = hex_color.lstrip("#") if len(h) < 6: return Quantity_Color(0.7, 0.7, 0.7, Quantity_TOC_RGB) r = int(h[0:2], 16) / 255.0 g = int(h[2:4], 16) / 255.0 b = int(h[4:6], 16) / 255.0 return Quantity_Color(r, g, b, Quantity_TOC_RGB) def _apply_color_map(shape_tool, color_tool, free_labels, color_map: dict) -> None: """Apply hex colors from color_map to matching shapes by name (case-insensitive substring).""" from OCP.TDF import TDF_LabelSequence from OCP.TDataStd import TDataStd_Name from OCP.XCAFDoc import XCAFDoc_ShapeTool # XCAFDoc_ColorType: XCAFDoc_ColorGen=0, XCAFDoc_ColorSurf=1, XCAFDoc_ColorCurv=2 try: from OCP.XCAFDoc import XCAFDoc_ColorSurf as COLOR_SURF except ImportError: COLOR_SURF = 1 # integer fallback def _visit(label) -> None: name_attr = TDataStd_Name() name = "" if label.FindAttribute(TDataStd_Name.GetID_s(), name_attr): name = name_attr.Get().ToExtString() if name: for part_name, hex_color in color_map.items(): if part_name.lower() in name.lower() or name.lower() in part_name.lower(): color_tool.SetColor(label, _hex_to_occ_color(hex_color), COLOR_SURF) break components = TDF_LabelSequence() XCAFDoc_ShapeTool.GetComponents_s(label, components) for i in range(1, components.Length() + 1): _visit(components.Value(i)) for i in range(1, free_labels.Length() + 1): _visit(free_labels.Value(i)) def _apply_palette_colors(shape_tool, color_tool, free_labels) -> None: """Assign palette colors to leaf shapes when no color_map is provided.""" from OCP.TDF import TDF_LabelSequence from OCP.XCAFDoc import XCAFDoc_ShapeTool try: from OCP.XCAFDoc import XCAFDoc_ColorSurf as COLOR_SURF except ImportError: COLOR_SURF = 1 leaves: list = [] def _collect(label) -> None: components = TDF_LabelSequence() XCAFDoc_ShapeTool.GetComponents_s(label, components) if components.Length() == 0: leaves.append(label) else: for i in range(1, components.Length() + 1): _collect(components.Value(i)) for i in range(1, free_labels.Length() + 1): _collect(free_labels.Value(i)) for idx, label in enumerate(leaves): occ_color = _hex_to_occ_color(PALETTE_HEX[idx % len(PALETTE_HEX)]) color_tool.SetColor(label, occ_color, COLOR_SURF) def main() -> None: args = parse_args() color_map: dict = json.loads(args.color_map) from OCP.STEPCAFControl import STEPCAFControl_Reader from OCP.TDocStd import TDocStd_Document from OCP.XCAFApp import XCAFApp_Application from OCP.XCAFDoc import XCAFDoc_DocumentTool from OCP.TCollection import TCollection_ExtendedString, TCollection_AsciiString from OCP.TDF import TDF_LabelSequence from OCP.BRepMesh import BRepMesh_IncrementalMesh from OCP.IFSelect import IFSelect_RetDone from OCP.Message import Message_ProgressRange # --- Init XDE document --- app = XCAFApp_Application.GetApplication_s() doc = TDocStd_Document(TCollection_ExtendedString("MDTV-CAF")) app.InitDocument(doc) # --- Read STEP into XDE (preserves part names + embedded colors) --- reader = STEPCAFControl_Reader() reader.SetNameMode(True) reader.SetColorMode(True) reader.SetLayerMode(True) status = reader.ReadFile(args.step_path) if status != IFSelect_RetDone: print(f"ERROR: STEPCAFControl_Reader failed (status={status})", file=sys.stderr) sys.exit(1) reader.Transfer(doc) shape_tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()) color_tool = XCAFDoc_DocumentTool.ColorTool_s(doc.Main()) # --- Tessellate all free shapes --- free_labels = TDF_LabelSequence() shape_tool.GetFreeShapes(free_labels) print(f"Found {free_labels.Length()} root shape(s), tessellating " f"(linear={args.linear_deflection}mm, angular={args.angular_deflection}rad) …") for i in range(1, free_labels.Length() + 1): shape = shape_tool.GetShape_s(free_labels.Value(i)) if not shape.IsNull(): BRepMesh_IncrementalMesh( shape, args.linear_deflection, False, # isRelative args.angular_deflection, True, # isInParallel ) # --- Apply colors --- if color_map: _apply_color_map(shape_tool, color_tool, free_labels, color_map) print(f"Applied color_map ({len(color_map)} entries)") else: _apply_palette_colors(shape_tool, color_tool, free_labels) print("Applied palette colors (no color_map provided)") # --- Scale shapes mm → m before GLB export --- # RWMesh_CoordinateSystemConverter is not wrapped in OCP Python bindings. # Pre-scale each free shape by 0.001 (mm → m) using BRepBuilderAPI_Transform. from OCP.gp import gp_Trsf from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform trsf = gp_Trsf() trsf.SetScaleFactor(0.001) for i in range(1, free_labels.Length() + 1): label = free_labels.Value(i) orig_shape = shape_tool.GetShape_s(label) if not orig_shape.IsNull(): scaled = BRepBuilderAPI_Transform(orig_shape, trsf, True).Shape() shape_tool.SetShape(label, scaled) print("Shapes scaled mm → m") # --- Export GLB via RWGltf_CafWriter --- from OCP.RWGltf import RWGltf_CafWriter writer = RWGltf_CafWriter(TCollection_AsciiString(args.output_path), True) # True = binary GLB # Z-up → Y-up rotation is applied by RWGltf_CafWriter by default (OCC 7.6+). # Perform export try: from OCP.TColStd import TColStd_IndexedDataMapOfStringString metadata = TColStd_IndexedDataMapOfStringString() ok = writer.Perform(doc, metadata, Message_ProgressRange()) except TypeError: # Older API without metadata dict ok = writer.Perform(doc, Message_ProgressRange()) out = Path(args.output_path) if not ok or not out.exists() or out.stat().st_size == 0: print(f"ERROR: RWGltf_CafWriter.Perform returned ok={ok}, file exists={out.exists()}", file=sys.stderr) sys.exit(1) print(f"GLB exported: {out.name} ({out.stat().st_size // 1024} KB)") try: main() except SystemExit: raise except Exception: traceback.print_exc() sys.exit(1)