This commit is contained in:
Jason Stedwell
2026-06-17 16:03:26 -05:00
parent fa1e9b68c7
commit c1abe36822
99 changed files with 1562887 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
#!/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 <file.step> [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()