feat: 完善 Face 参数化编辑和隔离执行

This commit is contained in:
2026-07-31 16:36:05 +08:00
parent 27e4f7236c
commit bb44e3920d
31 changed files with 5428 additions and 252 deletions
+86 -6
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import argparse
import math
from pathlib import Path
import sys
import tempfile
@@ -52,6 +53,32 @@ def _nearest_sphere_radius(model: StepModel, target_radius: float) -> tuple[int,
return best[0], best[1]
def _nearest_cone_semi_angle(model: StepModel, target_angle_degrees: 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") != "cone":
continue
angle = info.get("semi_angle")
if not isinstance(angle, (int, float)):
continue
angle_degrees = abs(math.degrees(float(angle)))
score = abs(angle_degrees - target_angle_degrees)
if best is None or score < best[2]:
best = (face_id, angle_degrees, score)
if best is None:
diameters = _thin_cap_diameters(model)
if len(diameters) >= 2:
small = min(diameters)
large = max(diameters)
height = _bbox_z_size(model)
if height > 1e-9 and large > small:
angle_degrees = math.degrees(math.atan(((large - small) * 0.5) / height))
return -1, angle_degrees
raise SystemExit("no cone Face remained after edit and cap diameters could not recover the semi-angle")
return best[0], best[1]
def _nearest_torus_radii(
model: StepModel,
target_major: float,
@@ -73,17 +100,22 @@ def _nearest_torus_radii(
def _has_thin_cap_diameter(model: StepModel, target_diameter: float, tolerance: float) -> bool:
return any(abs(diameter - target_diameter) <= tolerance for diameter in _thin_cap_diameters(model))
def _thin_cap_diameters(model: StepModel) -> list[float]:
diameters: list[float] = []
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):
if dz > 1e-4:
continue
if abs(dx - target_diameter) <= tolerance and abs(dy - target_diameter) <= tolerance:
return True
return False
if abs(dx - dy) <= max(max(abs(dx), abs(dy)) * 1e-5, 1e-5):
diameters.append((dx + dy) * 0.5)
return diameters
def _bbox_z_size(model: StepModel) -> float:
@@ -128,6 +160,51 @@ def _run_cone_case(target_reference_radius: float, tolerance: float) -> None:
print(result.encode("ascii", "backslashreplace").decode("ascii"))
def _run_cone_angle_case(target_angle_degrees: float, tolerance: float) -> None:
with tempfile.TemporaryDirectory(prefix="geom_param_cone_angle_") 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_semi_angle = abs(float(info.get("semi_angle") or 0.0))
current_top_radius = 2.0
scale = math.tan(math.radians(target_angle_degrees)) / math.tan(current_semi_angle)
target_reference_radius = current_reference_radius * scale
target_top_radius = current_top_radius * scale
plan = model.conical_semi_angle_plan(face_id, target_angle_degrees)
if plan["status"] == "blocked":
raise SystemExit(f"cone angle plan was blocked: {plan['message']}")
result = model.resize_conical_semi_angle(face_id, target_angle_degrees)
after = model.stats()
if after.solids != before.solids:
raise SystemExit(f"cone angle resize changed solid count: before={before.solids}, after={after.solids}")
if abs(_bbox_z_size(model) - 10.0) > tolerance:
raise SystemExit(f"cone angle resize changed height 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 angle 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 angle top cap diameter was not resized to {target_top_radius * 2.0:g}")
verified_face, angle_degrees = _nearest_cone_semi_angle(model, target_angle_degrees)
if abs(angle_degrees - target_angle_degrees) > tolerance:
raise SystemExit(
f"cone semi-angle verification failed: target={target_angle_degrees:g}, value={angle_degrees:g}"
)
print("mode=cone_semi_angle")
print(f"face_id={face_id}")
print(f"strategy={plan.get('resize_strategy')}")
print(f"before={before}")
print(f"after={after}")
print(f"current_semi_angle_degrees={math.degrees(current_semi_angle):.6f}")
print(f"target_semi_angle_degrees={target_angle_degrees:.6f} verified={angle_degrees:.6f}")
print(f"target_reference_radius={target_reference_radius:.6f}")
print(f"target_top_radius={target_top_radius:.6f}")
print(f"verified_face={verified_face}")
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"
@@ -199,19 +276,22 @@ def main() -> int:
parser.add_argument(
"--mode",
default="all",
choices=["all", "cone", "sphere", "torus_major", "torus_minor"],
choices=["all", "cone", "cone_angle", "sphere", "torus_major", "torus_minor"],
)
parser.add_argument("--cone-reference-radius", type=float, default=5.0)
parser.add_argument("--cone-semi-angle-degrees", type=float, default=16.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]
modes = ["cone", "cone_angle", "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 == "cone_angle":
_run_cone_angle_case(args.cone_semi_angle_degrees, args.tolerance)
elif mode == "sphere":
_run_sphere_case(args.sphere_radius, args.tolerance)
elif mode == "torus_major":