Files
pythonocc-step-editor/scripts/verify_analytic_surface_resize.py
T

228 lines
10 KiB
Python
Raw Normal View History

from __future__ import annotations
import argparse
from pathlib import Path
import sys
import tempfile
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCone, BRepPrimAPI_MakeSphere, BRepPrimAPI_MakeTorus
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
from step_editor.step_io import _write_step
def _write_cone_model(path: Path) -> None:
shape = BRepPrimAPI_MakeCone(4.0, 2.0, 10.0).Shape()
_write_step(shape, path)
def _write_sphere_model(path: Path) -> None:
shape = BRepPrimAPI_MakeSphere(5.0).Shape()
_write_step(shape, path)
def _write_torus_model(path: Path) -> None:
shape = BRepPrimAPI_MakeTorus(8.0, 2.0).Shape()
_write_step(shape, path)
def _first_face_by_surface(model: StepModel, surface: str) -> int:
for face_id in range(len(model.faces)):
if model.face_info(face_id).get("surface") == surface:
return face_id
raise SystemExit(f"no {surface} Face was recognized")
def _nearest_sphere_radius(model: StepModel, target_radius: float) -> tuple[int, float]:
best: tuple[int, float, float] | None = None
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
if info.get("surface") != "sphere":
continue
radius = float(info.get("radius") or 0.0)
score = abs(radius - target_radius)
if best is None or score < best[2]:
best = (face_id, radius, score)
if best is None:
raise SystemExit("no sphere Face remained after edit")
return best[0], best[1]
def _nearest_torus_radii(
model: StepModel,
target_major: float,
target_minor: float,
) -> tuple[int, float, float]:
best: tuple[int, float, float, float] | None = None
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
if info.get("surface") != "torus":
continue
major = float(info.get("major_radius") or 0.0)
minor = float(info.get("minor_radius") or 0.0)
score = abs(major - target_major) + abs(minor - target_minor)
if best is None or score < best[3]:
best = (face_id, major, minor, score)
if best is None:
raise SystemExit("no torus Face remained after edit")
return best[0], best[1], best[2]
def _has_thin_cap_diameter(model: StepModel, target_diameter: float, tolerance: float) -> bool:
for face_id in range(len(model.faces)):
info = model.face_info(face_id)
size = info.get("bbox_size")
if not isinstance(size, tuple) or len(size) != 3:
continue
dx, dy, dz = (float(size[0]), float(size[1]), float(size[2]))
if dz > max(tolerance * 10.0, 1e-5):
continue
if abs(dx - target_diameter) <= tolerance and abs(dy - target_diameter) <= tolerance:
return True
return False
def _bbox_z_size(model: StepModel) -> float:
size = model.geometry_stats().get("bbox_size")
if not isinstance(size, tuple) or len(size) != 3:
raise SystemExit("model bbox_size is missing")
return float(size[2])
def _run_cone_case(target_reference_radius: float, tolerance: float) -> None:
with tempfile.TemporaryDirectory(prefix="geom_param_cone_") as temp_dir:
model_path = Path(temp_dir) / "cone.step"
_write_cone_model(model_path)
model = StepModel.load(model_path)
face_id = _first_face_by_surface(model, "cone")
before = model.stats()
info = model.face_info(face_id)
current_reference_radius = float(info.get("reference_radius") or 0.0)
current_top_radius = 2.0
scale = target_reference_radius / current_reference_radius
target_top_radius = current_top_radius * scale
plan = model.conical_reference_radius_plan(face_id, target_reference_radius)
if plan["status"] == "blocked":
raise SystemExit(f"cone plan was blocked: {plan['message']}")
result = model.resize_conical_reference_radius(face_id, target_reference_radius)
after = model.stats()
if after.solids != before.solids:
raise SystemExit(f"cone resize changed solid count: before={before.solids}, after={after.solids}")
if abs(_bbox_z_size(model) - 10.0) > tolerance:
raise SystemExit(f"cone height changed unexpectedly: z_size={_bbox_z_size(model):g}")
if not _has_thin_cap_diameter(model, target_reference_radius * 2.0, tolerance):
raise SystemExit(f"cone bottom cap diameter was not resized to {target_reference_radius * 2.0:g}")
if not _has_thin_cap_diameter(model, target_top_radius * 2.0, tolerance):
raise SystemExit(f"cone top cap diameter was not resized to {target_top_radius * 2.0:g}")
print("mode=cone_reference_radius")
print(f"face_id={face_id}")
print(f"strategy={plan.get('resize_strategy')}")
print(f"before={before}")
print(f"after={after}")
print(f"current_reference_radius={current_reference_radius:.6f} target_reference_radius={target_reference_radius:.6f}")
print(f"target_top_radius={target_top_radius:.6f}")
print(result.encode("ascii", "backslashreplace").decode("ascii"))
def _run_sphere_case(target_radius: float, tolerance: float) -> None:
with tempfile.TemporaryDirectory(prefix="geom_param_sphere_") as temp_dir:
model_path = Path(temp_dir) / "sphere.step"
_write_sphere_model(model_path)
model = StepModel.load(model_path)
face_id = _first_face_by_surface(model, "sphere")
before = model.stats()
current_radius = float(model.face_info(face_id).get("radius") or 0.0)
plan = model.spherical_radius_plan(face_id, target_radius)
if plan["status"] == "blocked":
raise SystemExit(f"sphere plan was blocked: {plan['message']}")
result = model.resize_spherical_radius(face_id, target_radius)
after = model.stats()
verified_face, radius = _nearest_sphere_radius(model, target_radius)
if after.solids != before.solids:
raise SystemExit(f"sphere resize changed solid count: before={before.solids}, after={after.solids}")
if abs(radius - target_radius) > tolerance:
raise SystemExit(f"sphere radius verification failed: target={target_radius:g}, value={radius:g}")
print("mode=sphere_radius")
print(f"face_id={face_id}")
print(f"strategy={plan.get('resize_strategy')}")
print(f"before={before}")
print(f"after={after}")
print(f"current_radius={current_radius:.6f} target_radius={target_radius:.6f}")
print(f"verified_face={verified_face} value={radius:.6f}")
print(result.encode("ascii", "backslashreplace").decode("ascii"))
def _run_torus_case(mode: str, target_radius: float, tolerance: float) -> None:
with tempfile.TemporaryDirectory(prefix=f"geom_param_torus_{mode}_") as temp_dir:
model_path = Path(temp_dir) / "torus.step"
_write_torus_model(model_path)
model = StepModel.load(model_path)
face_id = _first_face_by_surface(model, "torus")
before = model.stats()
info = model.face_info(face_id)
current_major = float(info.get("major_radius") or 0.0)
current_minor = float(info.get("minor_radius") or 0.0)
current = current_major if mode == "major" else current_minor
scale = target_radius / current
target_major = current_major * scale
target_minor = current_minor * scale
plan = model.toroidal_radius_plan(face_id, target_radius, mode)
if plan["status"] == "blocked":
raise SystemExit(f"torus {mode} plan was blocked: {plan['message']}")
result = model.resize_toroidal_radius(face_id, target_radius, mode)
after = model.stats()
verified_face, major, minor = _nearest_torus_radii(model, target_major, target_minor)
if after.solids != before.solids:
raise SystemExit(f"torus {mode} resize changed solid count: before={before.solids}, after={after.solids}")
if abs(major - target_major) > tolerance or abs(minor - target_minor) > tolerance:
raise SystemExit(
f"torus {mode} verification failed: target=({target_major:g}, {target_minor:g}), "
f"value=({major:g}, {minor:g})"
)
print(f"mode=torus_{mode}_radius")
print(f"face_id={face_id}")
print(f"strategy={plan.get('resize_strategy')}")
print(f"before={before}")
print(f"after={after}")
print(f"current_major={current_major:.6f} current_minor={current_minor:.6f}")
print(f"target_major={target_major:.6f} target_minor={target_minor:.6f}")
print(f"verified_face={verified_face} major={major:.6f} minor={minor:.6f}")
print(result.encode("ascii", "backslashreplace").decode("ascii"))
def main() -> int:
parser = argparse.ArgumentParser(description="Verify analytic curved surface resize operations.")
parser.add_argument(
"--mode",
default="all",
choices=["all", "cone", "sphere", "torus_major", "torus_minor"],
)
parser.add_argument("--cone-reference-radius", type=float, default=5.0)
parser.add_argument("--sphere-radius", type=float, default=6.25)
parser.add_argument("--torus-major-radius", type=float, default=10.0)
parser.add_argument("--torus-minor-radius", type=float, default=3.0)
parser.add_argument("--tolerance", type=float, default=2e-4)
args = parser.parse_args()
modes = ["cone", "sphere", "torus_major", "torus_minor"] if args.mode == "all" else [args.mode]
for mode in modes:
if mode == "cone":
_run_cone_case(args.cone_reference_radius, args.tolerance)
elif mode == "sphere":
_run_sphere_case(args.sphere_radius, args.tolerance)
elif mode == "torus_major":
_run_torus_case("major", args.torus_major_radius, args.tolerance)
elif mode == "torus_minor":
_run_torus_case("minor", args.torus_minor_radius, args.tolerance)
else:
raise SystemExit(f"unsupported mode: {mode}")
return 0
if __name__ == "__main__":
raise SystemExit(main())