2026-07-28 14:05:14 +08:00
|
|
|
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:
|
2026-07-30 17:54:01 +08:00
|
|
|
parser = argparse.ArgumentParser(description="Verify cube edge-length resize semantics.")
|
2026-07-28 14:05:14 +08:00
|
|
|
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"])
|
2026-07-30 17:54:01 +08:00
|
|
|
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.")
|
2026-07-28 14:05:14 +08:00
|
|
|
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()
|
2026-07-30 17:54:01 +08:00
|
|
|
plan = model.general_edge_length_plan(
|
|
|
|
|
edge_id,
|
|
|
|
|
args.target_length,
|
|
|
|
|
anchor_mode=args.anchor,
|
|
|
|
|
strategy_mode=args.strategy,
|
|
|
|
|
)
|
2026-07-28 14:05:14 +08:00
|
|
|
strategy = str(plan.get("resize_strategy", ""))
|
2026-07-30 17:54:01 +08:00
|
|
|
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 '<none>'}")
|
|
|
|
|
|
|
|
|
|
result = model.resize_general_edge_length(
|
|
|
|
|
edge_id,
|
|
|
|
|
args.target_length,
|
|
|
|
|
anchor_mode=args.anchor,
|
|
|
|
|
strategy_mode=args.strategy,
|
|
|
|
|
)
|
2026-07-28 14:05:14 +08:00
|
|
|
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}")
|
2026-07-30 17:54:01 +08:00
|
|
|
print(f"requested_strategy={args.strategy}")
|
2026-07-28 14:05:14 +08:00
|
|
|
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())
|