mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
310 lines
10 KiB
Python
310 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from .inspect import (
|
|
CadRefError,
|
|
cad_path_from_target,
|
|
diff_entry_targets,
|
|
inspect_cad_refs,
|
|
inspect_entry_planes,
|
|
)
|
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
description="Inspect occurrence-aware @cad[...] refs against STEP source files."
|
|
)
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
inspect_parser = subparsers.add_parser(
|
|
"inspect",
|
|
help="Resolve whole-entry or selector refs and return STEP-derived context.",
|
|
)
|
|
inspect_parser.add_argument(
|
|
"input",
|
|
nargs="?",
|
|
help="Token text or multiline prompt text containing @cad[...] refs.",
|
|
)
|
|
inspect_parser.add_argument(
|
|
"--refs",
|
|
nargs="+",
|
|
help="One or more @cad[...] refs to inspect. Can be used instead of positional input.",
|
|
)
|
|
inspect_parser.add_argument(
|
|
"--input-file",
|
|
type=Path,
|
|
help="Read token text from a file instead of CLI input or stdin.",
|
|
)
|
|
inspect_parser.add_argument(
|
|
"--detail",
|
|
action="store_true",
|
|
help="Include detailed geometry facts for selected face/edge refs.",
|
|
)
|
|
inspect_parser.add_argument(
|
|
"--facts",
|
|
action="store_true",
|
|
help="Include compact geometry facts for whole-entry refs and resolved selectors.",
|
|
)
|
|
inspect_parser.add_argument(
|
|
"--topology",
|
|
action="store_true",
|
|
help="Include full face/edge selector lists for whole-entry refs. Expensive on large STEP files.",
|
|
)
|
|
inspect_parser.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Emit JSON output. Recommended for agent workflows.",
|
|
)
|
|
inspect_parser.set_defaults(handler=run_inspect)
|
|
|
|
planes_parser = subparsers.add_parser(
|
|
"planes",
|
|
help="Group major coplanar planar faces for a STEP ref by axis and coordinate.",
|
|
)
|
|
planes_parser.add_argument(
|
|
"entry",
|
|
help="CAD STEP path or @cad[...] token for the part to inspect.",
|
|
)
|
|
planes_parser.add_argument(
|
|
"--coordinate-tolerance",
|
|
type=float,
|
|
default=1e-3,
|
|
help="Merge planar face groups whose axis coordinate differs by at most this value. Default: 0.001",
|
|
)
|
|
planes_parser.add_argument(
|
|
"--min-area-ratio",
|
|
type=float,
|
|
default=0.05,
|
|
help="Drop planar groups smaller than this fraction of total planar area. Default: 0.05",
|
|
)
|
|
planes_parser.add_argument(
|
|
"--limit",
|
|
type=int,
|
|
default=12,
|
|
help="Maximum number of groups to emit. Default: 12",
|
|
)
|
|
planes_parser.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Emit JSON output. Recommended for agent workflows.",
|
|
)
|
|
planes_parser.set_defaults(handler=run_planes)
|
|
|
|
diff_parser = subparsers.add_parser(
|
|
"diff",
|
|
help="Compare two CAD STEP refs and summarize selector-level changes.",
|
|
)
|
|
diff_parser.add_argument("left", help="Left CAD STEP path or @cad[...] token.")
|
|
diff_parser.add_argument("right", help="Right CAD STEP path or @cad[...] token.")
|
|
diff_parser.add_argument(
|
|
"--detail",
|
|
action="store_true",
|
|
help="Include major planar face groups for both sides.",
|
|
)
|
|
diff_parser.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Emit JSON output. Recommended for agent workflows.",
|
|
)
|
|
diff_parser.set_defaults(handler=run_diff)
|
|
|
|
return parser
|
|
|
|
|
|
def run_inspect(args: argparse.Namespace) -> int:
|
|
try:
|
|
text = _read_input_text(args)
|
|
result = inspect_cad_refs(
|
|
text,
|
|
detail=bool(args.detail),
|
|
include_topology=bool(args.topology),
|
|
facts=bool(args.facts),
|
|
)
|
|
except CadRefError as exc:
|
|
result = {
|
|
"ok": False,
|
|
"tokens": [],
|
|
"errors": [
|
|
{
|
|
"line": None,
|
|
"cadPath": None,
|
|
"selector": None,
|
|
"kind": "input",
|
|
"message": str(exc),
|
|
}
|
|
],
|
|
}
|
|
|
|
if args.json or not result.get("ok"):
|
|
print(json.dumps(result, indent=2, sort_keys=False))
|
|
else:
|
|
_print_text_summary(result)
|
|
|
|
return 0 if bool(result.get("ok")) else 2
|
|
|
|
|
|
def run_planes(args: argparse.Namespace) -> int:
|
|
try:
|
|
result = inspect_entry_planes(
|
|
args.entry,
|
|
coordinate_tolerance=float(args.coordinate_tolerance),
|
|
min_area_ratio=float(args.min_area_ratio),
|
|
limit=int(args.limit),
|
|
)
|
|
except CadRefError as exc:
|
|
result = {
|
|
"ok": False,
|
|
"cadPath": _safe_cad_path(args.entry),
|
|
"errors": [{"message": str(exc)}],
|
|
}
|
|
|
|
if args.json or not result.get("ok"):
|
|
print(json.dumps(result, indent=2, sort_keys=False))
|
|
else:
|
|
_print_planes_summary(result)
|
|
return 0 if bool(result.get("ok")) else 2
|
|
|
|
|
|
def run_diff(args: argparse.Namespace) -> int:
|
|
try:
|
|
result = diff_entry_targets(args.left, args.right, detail=bool(args.detail))
|
|
except CadRefError as exc:
|
|
result = {
|
|
"ok": False,
|
|
"left": {"cadPath": _safe_cad_path(args.left)},
|
|
"right": {"cadPath": _safe_cad_path(args.right)},
|
|
"errors": [{"message": str(exc)}],
|
|
}
|
|
|
|
if args.json or not result.get("ok"):
|
|
print(json.dumps(result, indent=2, sort_keys=False))
|
|
else:
|
|
_print_diff_summary(result)
|
|
return 0 if bool(result.get("ok")) else 2
|
|
|
|
|
|
def _read_input_text(args: argparse.Namespace) -> str:
|
|
input_sources = sum(
|
|
1
|
|
for source in (args.input_file, args.input, args.refs)
|
|
if source is not None
|
|
)
|
|
if input_sources > 1:
|
|
raise CadRefError("Pass only one of positional input, --input-file, or --refs.")
|
|
|
|
if args.refs:
|
|
text = "\n".join(str(ref) for ref in args.refs)
|
|
elif args.input_file:
|
|
try:
|
|
text = args.input_file.read_text(encoding="utf-8")
|
|
except OSError as exc:
|
|
raise CadRefError(f"Failed to read input file: {args.input_file}") from exc
|
|
elif args.input is not None:
|
|
text = str(args.input)
|
|
else:
|
|
text = sys.stdin.read()
|
|
|
|
if not str(text).strip():
|
|
raise CadRefError("No input text provided.")
|
|
return text
|
|
|
|
|
|
def _print_text_summary(result: dict[str, object]) -> None:
|
|
token_results = result.get("tokens")
|
|
if not isinstance(token_results, list):
|
|
print("No tokens inspected.")
|
|
return
|
|
|
|
for token in token_results:
|
|
if not isinstance(token, dict):
|
|
continue
|
|
line = token.get("line")
|
|
token_text = token.get("token")
|
|
cad_path = token.get("cadPath")
|
|
step_path = token.get("stepPath")
|
|
print(f"line={line} cadPath={cad_path} step={step_path}")
|
|
print(f" token: {token_text}")
|
|
summary = token.get("summary")
|
|
if isinstance(summary, dict):
|
|
print(
|
|
" summary: "
|
|
f"kind={summary.get('kind')} occurrences={summary.get('occurrenceCount')} "
|
|
f"leafOccurrences={summary.get('leafOccurrenceCount')} faces={summary.get('faceCount')} "
|
|
f"edges={summary.get('edgeCount')} vertices={summary.get('vertexCount')} bounds={summary.get('bounds')}"
|
|
)
|
|
|
|
selections = token.get("selections")
|
|
if isinstance(selections, list):
|
|
for selection in selections:
|
|
if not isinstance(selection, dict):
|
|
continue
|
|
selector = selection.get("normalizedSelector")
|
|
selector_type = selection.get("selectorType")
|
|
status = selection.get("status")
|
|
print(f" - {selector} [{selector_type}] {status}")
|
|
|
|
warnings = token.get("warnings")
|
|
if isinstance(warnings, list):
|
|
for warning in warnings:
|
|
print(f" warning: {warning}")
|
|
entry_facts = token.get("entryFacts")
|
|
if isinstance(entry_facts, dict):
|
|
print(f" entryFacts: {entry_facts}")
|
|
if isinstance(selections, list):
|
|
for selection in selections:
|
|
if not isinstance(selection, dict):
|
|
continue
|
|
geometry_facts = selection.get("geometryFacts")
|
|
if isinstance(geometry_facts, dict):
|
|
print(f" geometryFacts: {geometry_facts}")
|
|
|
|
|
|
def _print_planes_summary(result: dict[str, object]) -> None:
|
|
print(f"cadPath={result.get('cadPath')} step={result.get('stepPath')}")
|
|
planes = result.get("planes")
|
|
if not isinstance(planes, list) or not planes:
|
|
print(" no major planar groups")
|
|
return
|
|
for plane in planes:
|
|
if not isinstance(plane, dict):
|
|
continue
|
|
print(
|
|
" "
|
|
f"axis={plane.get('axis')} coordinate={plane.get('coordinate')} "
|
|
f"faces={plane.get('faceCount')} area={plane.get('totalArea')} selectors={plane.get('selectors')}"
|
|
)
|
|
|
|
|
|
def _print_diff_summary(result: dict[str, object]) -> None:
|
|
left = result.get("left") if isinstance(result.get("left"), dict) else {}
|
|
right = result.get("right") if isinstance(result.get("right"), dict) else {}
|
|
diff = result.get("diff") if isinstance(result.get("diff"), dict) else {}
|
|
print(f"left={left.get('cadPath')} right={right.get('cadPath')}")
|
|
print(
|
|
" "
|
|
f"topologyChanged={diff.get('topologyChanged')} "
|
|
f"geometryChanged={diff.get('geometryChanged')} "
|
|
f"bboxChanged={diff.get('bboxChanged')} "
|
|
f"countDelta={diff.get('countDelta')}"
|
|
)
|
|
if diff.get("sizeDelta") is not None:
|
|
print(f" sizeDelta={diff.get('sizeDelta')}")
|
|
if diff.get("centerDelta") is not None:
|
|
print(f" centerDelta={diff.get('centerDelta')}")
|
|
|
|
|
|
def _safe_cad_path(target: str) -> str:
|
|
try:
|
|
return cad_path_from_target(target)
|
|
except CadRefError:
|
|
return str(target)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = build_parser()
|
|
args = parser.parse_args(argv)
|
|
return int(args.handler(args))
|