feat: 完善参数化编辑语义和槽孔中心距
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user