from __future__ import annotations import argparse from collections import Counter from pathlib import Path import sys PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from step_editor.model import StepModel DEFAULT_MODEL = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step" def _edge_length(model: StepModel, edge_id: int) -> float: return float(model.edge_info(edge_id).get("length", 0.0)) def _line_edge_ids_near_length(model: StepModel, length: float, tolerance: float) -> list[int]: edge_ids: list[int] = [] for edge_id in range(len(model.edges)): info = model.edge_info(edge_id) if info.get("curve") != "line": continue if abs(float(info.get("length", 0.0)) - length) <= tolerance: edge_ids.append(edge_id) return edge_ids def _length_distribution(model: StepModel) -> dict[float, int]: counts = Counter(round(_edge_length(model, edge_id), 6) for edge_id in range(len(model.edges))) return dict(sorted(counts.items())) def main() -> int: parser = argparse.ArgumentParser(description="Verify cube edge-length resize semantics.") parser.add_argument("model", nargs="?", default=str(DEFAULT_MODEL), help="STEP model path.") parser.add_argument("--source-length", type=float, default=10.0, help="Current line edge length to search for.") parser.add_argument("--target-length", type=float, default=15.0, help="Target edge length to apply.") parser.add_argument("--anchor", default="keep-start", choices=["auto", "center", "keep-start", "keep-end"]) parser.add_argument( "--strategy", default="local-edge-only-deform", choices=[ "auto", "local-edge-only-deform", "move-edge-end-plane-by-push-pull", "scale-owning-shape-from-edge", ], help="Requested Edge length edit semantics.", ) parser.add_argument("--expect-strategy", default="", help="Expected resolved resize strategy.") parser.add_argument("--tolerance", type=float, default=1e-5, help="Allowed target length error.") args = parser.parse_args() model = StepModel.load(Path(args.model)) edge_ids = _line_edge_ids_near_length(model, args.source_length, args.tolerance) if not edge_ids: raise SystemExit(f"no line edge near source length {args.source_length:g}") edge_id = edge_ids[0] before = model.stats() plan = model.general_edge_length_plan( edge_id, args.target_length, anchor_mode=args.anchor, strategy_mode=args.strategy, ) strategy = str(plan.get("resize_strategy", "")) expected_strategy = args.expect_strategy or args.strategy if expected_strategy == "auto": expected_strategy = strategy if strategy != expected_strategy: raise SystemExit(f"expected {expected_strategy}, got {strategy or ''}") result = model.resize_general_edge_length( edge_id, args.target_length, anchor_mode=args.anchor, strategy_mode=args.strategy, ) after = model.stats() lengths = [_edge_length(model, item) for item in range(len(model.edges))] nearest = min(lengths, key=lambda value: abs(value - args.target_length)) error = abs(nearest - args.target_length) if error > args.tolerance: raise SystemExit(f"target length check failed: nearest={nearest:g}, error={error:g}") print(f"model={Path(args.model)}") print(f"edge_id={edge_id}") print(f"requested_strategy={args.strategy}") print(f"strategy={strategy}") print(f"anchor_mode={args.anchor}") print(f"start_move={plan.get('local_edge_deform_start_move')}") print(f"end_move={plan.get('local_edge_deform_end_move')}") print(f"before_faces={before.faces} before_edges={before.edges}") print(f"after_faces={after.faces} after_edges={after.edges}") print(f"nearest_length={nearest:.6f} target_error={error:.6g}") print(f"length_distribution={_length_distribution(model)}") print(result.encode("ascii", "backslashreplace").decode("ascii")) return 0 if __name__ == "__main__": raise SystemExit(main())