"""Command-line entry point installed by ``pyproject.toml``. The DEFAULT mode is single-beam measurement: the ROI(s) already isolate one beam each, the cloud is cropped and measured directly (no registration, no dual-beam tracking). Provide ``--roi`` for one beam, or ``--left-roi`` and ``--right-roi`` to measure both in one invocation. The legacy dual-beam pipeline (registration + left/right tracking on a car-bottom slab) is still available via ``--mode dual``. """ from __future__ import annotations import argparse def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Measure underframe longitudinal beam deflection from a point cloud.", ) parser.add_argument( "--mode", default="single", choices=["single", "dual"], help="'single' (default): ROI isolates one beam, measure directly. " "'dual': legacy car-bottom slab with left+right tracking + registration.", ) parser.add_argument( "--input-cloud", required=True, help="Path to input scan point cloud (.ply or .xyz). Large binary PLYs " "are stream-cropped by the ROI in single mode.", ) parser.add_argument( "--out-dir", required=True, help="Output directory for report.json/.xlsx, CSV, and PNG.", ) # --- single mode --- parser.add_argument( "--roi", default=None, help="Single mode: ROI JSON isolating ONE beam (native frame " "x=length, y=height, z=width).", ) parser.add_argument( "--left-roi", default=None, help="Single mode: ROI JSON for the left beam (use with --right-roi).", ) parser.add_argument( "--right-roi", default=None, help="Single mode: ROI JSON for the right beam (use with --left-roi).", ) parser.add_argument( "--side", default=None, help="Single mode label for --roi (left/right); inferred from the ROI's " "lateral z (x-forward: z>0 left, z<0 right) if omitted.", ) parser.add_argument( "--tolerance", type=float, default=None, help="Single mode --roi: pass/fail tolerance in mm. Default by side " "(left 7, right 12).", ) parser.add_argument( "--left-tolerance", type=float, default=None, help="Both-beam mode: left-beam tolerance in mm (default 7).", ) parser.add_argument( "--right-tolerance", type=float, default=None, help="Both-beam mode: right-beam tolerance in mm (default 12).", ) parser.add_argument( "--enable-registration", action="store_true", help="Single mode: rigidly align the cloud to --cad before measuring. " "Default OFF (cloud assumed already registered). Requires --cad as a " "sampled .ply/.stl/.obj (.stp must be converted first).", ) # --- shared --- parser.add_argument( "--config", default=None, help="Optional JSON config override file.", ) parser.add_argument( "--skip-excel", action="store_true", help="Skip writing the .xlsx report.", ) parser.add_argument( "--skip-plot", action="store_true", help="Single mode: skip the PNG plot.", ) # --- dual (legacy) mode --- parser.add_argument( "--cad", default=None, help="Dual mode: CAD reference (.ply/.xyz/.stl/.obj) for registration.", ) parser.add_argument( "--profile", default="auto", choices=["auto", "M", "TC", "TP", "custom"], help="Dual mode: profile for scale-related tuning.", ) parser.add_argument( "--skip-registration", action="store_true", help="Dual mode: bypass ICP even when --cad is supplied.", ) parser.add_argument( "--no-bilateral", action="store_true", help="Dual mode: disable bilateral denoising.", ) parser.add_argument( "--input-axis", default="y_up", choices=["z_up", "y_up"], help="Dual mode: height axis convention (default y_up).", ) return parser.parse_args() def _load_config(path): if not path: return None import json from pathlib import Path return json.loads(Path(path).read_text(encoding="utf-8")) def main() -> None: args = parse_args() if args.mode == "single": from .single_beam_pipeline import run_single_beam_pipeline, run_both_beams config = _load_config(args.config) if args.left_roi and args.right_roi: run_both_beams( input_cloud=args.input_cloud, left_roi=args.left_roi, right_roi=args.right_roi, out_dir=args.out_dir, config=config, cad_path=args.cad, enable_registration=args.enable_registration, left_tolerance_mm=args.left_tolerance, right_tolerance_mm=args.right_tolerance, skip_excel=args.skip_excel, skip_plot=args.skip_plot, ) elif args.roi: run_single_beam_pipeline( input_cloud=args.input_cloud, roi_path=args.roi, out_dir=args.out_dir, side=args.side, tolerance_mm=args.tolerance, config=config, cad_path=args.cad, enable_registration=args.enable_registration, skip_excel=args.skip_excel, skip_plot=args.skip_plot, ) else: raise SystemExit( "single mode requires --roi (one beam) or both --left-roi and --right-roi." ) return # dual (legacy) mode from .pipeline import run_measurement_pipeline run_measurement_pipeline( input_cloud=args.input_cloud, out_dir=args.out_dir, config_path=args.config, profile=args.profile, cad_path=args.cad, roi_path=args.roi, skip_registration=args.skip_registration, enable_bilateral=not args.no_bilateral, skip_excel=args.skip_excel, input_axis=args.input_axis, ) if __name__ == "__main__": main()