#!/usr/bin/env python3 """ step_processor.py — CLI entry point for the STEP File Processor skill. Default run: thumbnails + BOM + auto-translate if Chinese labels detected. Usage: python step_processor.py [options] See SKILL.md or --help for full option reference. """ import argparse import logging import sys from pathlib import Path def _setup_logging(verbose: bool): level = logging.DEBUG if verbose else logging.INFO fmt = "%(levelname)-5s %(name)s: %(message)s" if verbose else "%(levelname)-5s %(message)s" logging.basicConfig(level=level, format=fmt, stream=sys.stdout) def _parse_args(): p = argparse.ArgumentParser( description="STEP File Processor — thumbnails, BOM, translation, geometry queries", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) p.add_argument("step_file", help="Path to .step or .stp file") # Output control p.add_argument("--no-thumbnails", dest="thumbnails", action="store_false", default=True, help="Skip PNG thumbnail generation") p.add_argument("--no-bom", dest="bom", action="store_false", default=True, help="Skip BOM CSV export") p.add_argument("--translate", dest="translate", action="store_true", default=None, help="Force translation even if auto-detect is off") p.add_argument("--no-translate", dest="translate", action="store_false", help="Skip translation even if Chinese labels are detected") # Thumbnail options p.add_argument("--resolution", default="1024x768", help="Thumbnail resolution WxH (default: 1024x768)") p.add_argument("--views", default=None, help="Comma-separated views: front,bottom,left,right,iso_left,iso_right") # Query modes p.add_argument("--query", default=None, metavar="QUERY", help='Run a single geometric query and exit, e.g. "list all holes"') p.add_argument("--repl", action="store_true", help="Launch interactive geometry query REPL") # Diagram p.add_argument("--diagram", action="store_true", help="Generate external dimensional diagram") p.add_argument("--diagram-mode", choices=["enclosure_only", "enclosure_plus_mounting", "mounting_only"], default="enclosure_only", help="Diagram mode (default: enclosure_only)") p.add_argument("--diagram-style", choices=["line_drawing", "rendered"], default=None, help="Diagram style (auto-selected if omitted)") p.add_argument("--diagram-pdf", action="store_true", help="Also export diagram as PDF") p.add_argument("--diagram-variants", action="store_true", help="Render one diagram per mounting variant") p.add_argument("--mapping", default=None, metavar="JSON_FILE", help="Parts mapping JSON file for diagram mode") p.add_argument("--datablock", default=None, metavar="MD_FILE", help="Datablock .md file for diagram title block") # Misc p.add_argument("--verbose", "-v", action="store_true", help="Show backend selection, fallback notices, timing") return p.parse_args() def main(): args = _parse_args() _setup_logging(args.verbose) log = logging.getLogger("step_processor") step_path = Path(args.step_file).expanduser().resolve() if not step_path.exists(): log.error(f"File not found: {step_path}") sys.exit(1) if step_path.suffix.lower() not in (".step", ".stp"): log.warning(f"Unexpected extension '{step_path.suffix}' — continuing anyway") # ── Load ───────────────────────────────────────────────────────────────── log.info(f"Loading: {step_path.name}") from modules.loader import load_step model = load_step(step_path) if model is None: log.error("Failed to load STEP file. Check that build123d or FreeCAD is installed.") log.error("See INSTALL.md for setup instructions.") sys.exit(1) log.info(f"[{model.backend}] Loaded: {step_path.name}") # ── Query mode (single) ────────────────────────────────────────────────── if args.query: from modules.query_engine import run_query result = run_query(model, args.query) print(result) return # ── REPL mode ──────────────────────────────────────────────────────────── if args.repl: from modules.query_engine import repl repl(model, step_path) return # ── BOM ────────────────────────────────────────────────────────────────── bom_df = None if args.bom: from modules.bom import extract_bom, save_bom_xlsx bom_df = extract_bom(model) xlsx_path = save_bom_xlsx(bom_df, step_path) log.info(f"BOM XLSX → {xlsx_path}") # ── Translation ────────────────────────────────────────────────────────── if bom_df is not None: from modules.translator import has_chinese, translate_bom, get_translation_map needs_translation = args.translate if needs_translation is None: # Auto-detect needs_translation = bom_df["part_name_original"].apply(has_chinese).any() if needs_translation: log.info("Chinese part names detected — auto-translating") if needs_translation: import os if not os.environ.get("ANTHROPIC_API_KEY"): log.warning("ANTHROPIC_API_KEY not set — skipping translation") else: bom_df = translate_bom(bom_df, model_name=step_path.stem) from modules.bom import save_bom_xlsx save_bom_xlsx(bom_df, step_path) # overwrite with translated version translation_map = get_translation_map(bom_df) if translation_map: from modules.rewriter import rewrite_step rewrite_step(step_path, translation_map) # ── Thumbnails ─────────────────────────────────────────────────────────── if args.thumbnails: try: w, h = (int(x) for x in args.resolution.split("x")) except ValueError: log.warning(f"Invalid resolution '{args.resolution}' — using 1024x768") w, h = 1024, 768 views = [v.strip() for v in args.views.split(",")] if args.views else None from modules.renderer import render_views thumb_results = render_views(model, step_path, views=views, width=w, height=h) if thumb_results: log.info(f"Thumbnails: {len(thumb_results)} PNG(s) written") else: log.warning("No thumbnails generated (pyrender unavailable or mesh failed)") # ── External dimensional diagram ───────────────────────────────────────── if args.diagram: from modules.external_diagram import step_external_diagram options = { "pdf": args.diagram_pdf, "render_variants": args.diagram_variants, } if args.diagram_style: options["style"] = args.diagram_style meta = step_external_diagram( path=str(step_path), mode=args.diagram_mode, mapping_file=args.mapping, datablock_file=args.datablock, options=options, ) if meta: log.info(f"Diagram → {step_path.stem}__external-diagram.svg") if args.diagram_pdf: log.info(f"Diagram PDF → {step_path.stem}__external-diagram.pdf") # ── Summary ────────────────────────────────────────────────────────────── log.info("Done.") _print_summary(step_path, model, bom_df, args) def _print_summary(step_path: Path, model, bom_df, args): print(f"\n{'─'*60}") print(f" {step_path.name} [{model.backend}]") if bom_df is not None: print(f" BOM: {len(bom_df)} parts → {step_path.stem}_bom.xlsx") en_path = step_path.parent / f"{step_path.stem}_EN.step" if en_path.exists(): print(f" Translated STEP → {en_path.name}") if args.thumbnails: thumb_count = sum(1 for ext in ["_front.png", "_rear.png", "_left.png", "_right.png", "_iso_left.png", "_iso_right.png"] if (step_path.parent / f"{step_path.stem}{ext}").exists()) print(f" Thumbnails: {thumb_count} PNG(s)") if args.diagram: diag = step_path.parent / f"{step_path.stem}__external-diagram.svg" if diag.exists(): print(f" Diagram → {diag.name}") print(f"{'─'*60}\n") if __name__ == "__main__": main()