feat: 完善 Face 一级关系编辑和稳定性
This commit is contained in:
@@ -6,7 +6,7 @@ import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
|
||||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeSphere, BRepPrimAPI_MakeTorus
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -73,6 +73,46 @@ def _run_worker_case(
|
||||
return model
|
||||
|
||||
|
||||
def _run_main_worker_entry_case() -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="geom_param_main_worker_entry_") as temp_dir:
|
||||
temp_root = Path(temp_dir)
|
||||
output_path = temp_root / "output.step"
|
||||
request_path = temp_root / "request.json"
|
||||
request_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"input_path": str(DEFAULT_MODEL),
|
||||
"output_path": str(output_path),
|
||||
"operation": "resize_face_area_local",
|
||||
"args": [0, 144.0],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
completed = subprocess.run(
|
||||
[sys.executable, "main.py", "--isolated-edit-worker", str(request_path)],
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=120,
|
||||
check=False,
|
||||
)
|
||||
response_path = request_path.with_suffix(".response.json")
|
||||
if completed.returncode != 0:
|
||||
detail = response_path.read_text(encoding="utf-8") if response_path.exists() else completed.stderr
|
||||
raise SystemExit(f"main worker entry failed with code {completed.returncode}: {detail}")
|
||||
response = json.loads(response_path.read_text(encoding="utf-8"))
|
||||
if not response.get("ok") or not output_path.exists():
|
||||
raise SystemExit(f"main worker entry did not produce an edited STEP: {response}")
|
||||
model = StepModel.load(output_path)
|
||||
_assert_face_area("main worker entry", model, 144.0)
|
||||
print("main worker entry ok")
|
||||
|
||||
|
||||
def _float_close(value: object, target: float, tolerance: float = 1e-5) -> bool:
|
||||
try:
|
||||
return abs(float(value) - target) <= tolerance
|
||||
@@ -80,6 +120,15 @@ def _float_close(value: object, target: float, tolerance: float = 1e-5) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _logical_region_has_width(model: StepModel, logical_id: int, target_width: float) -> bool:
|
||||
for face_id in model.face_ids_for_logical_id(logical_id):
|
||||
info = model.face_info(face_id)
|
||||
width = info.get("local_face_width")
|
||||
if _float_close(width, target_width, tolerance=1e-4):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _triple_close(value: object, target: tuple[float, float, float], tolerance: float = 1e-5) -> bool:
|
||||
if not isinstance(value, (list, tuple)) or len(value) != 3:
|
||||
return False
|
||||
@@ -150,6 +199,21 @@ def _write_shell_plate(path: Path) -> None:
|
||||
_write_step(BRepPrimAPI_MakeBox(30.0, 20.0, 2.0).Shape(), path)
|
||||
|
||||
|
||||
def _write_sphere_model(path: Path) -> None:
|
||||
_write_step(BRepPrimAPI_MakeSphere(5.0).Shape(), path)
|
||||
|
||||
|
||||
def _write_torus_model(path: Path) -> None:
|
||||
_write_step(BRepPrimAPI_MakeTorus(8.0, 2.0).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 _first_shell_face(model: StepModel, source_thickness: float = 2.0, tolerance: float = 1e-5) -> int:
|
||||
candidates: list[tuple[int, int]] = []
|
||||
for face_id in range(len(model.faces)):
|
||||
@@ -177,6 +241,35 @@ def _assert_shell_thickness(label: str, model: StepModel, target_thickness: floa
|
||||
print(f"matched_shell_thickness={thickness:g}, bbox_size={size}")
|
||||
|
||||
|
||||
def _assert_sphere_radius(label: str, model: StepModel, target_radius: float = 10.0) -> None:
|
||||
matches = []
|
||||
for face_id in range(len(model.faces)):
|
||||
info = model.face_info(face_id)
|
||||
if info.get("surface") != "sphere":
|
||||
continue
|
||||
radius = info.get("radius")
|
||||
if _float_close(radius, target_radius, tolerance=1e-4):
|
||||
matches.append((face_id, float(radius)))
|
||||
if not matches:
|
||||
raise SystemExit(f"{label}: output does not contain a sphere Face radius {target_radius:g}")
|
||||
print(f"matched_sphere_radii={matches}")
|
||||
|
||||
|
||||
def _assert_torus_minor_radius(label: str, model: StepModel, target_minor: float = 4.0) -> None:
|
||||
matches = []
|
||||
for face_id in range(len(model.faces)):
|
||||
info = model.face_info(face_id)
|
||||
if info.get("surface") != "torus":
|
||||
continue
|
||||
major = info.get("major_radius")
|
||||
minor = info.get("minor_radius")
|
||||
if _float_close(minor, target_minor, tolerance=1e-4):
|
||||
matches.append((face_id, float(major), float(minor)))
|
||||
if not matches:
|
||||
raise SystemExit(f"{label}: output does not contain a torus Face minor radius {target_minor:g}")
|
||||
print(f"matched_torus_radii={matches}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
_run_worker_case(
|
||||
label="面偏移(当前面)",
|
||||
@@ -252,6 +345,39 @@ def main() -> int:
|
||||
input_path=shell_path,
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="geom_param_isolated_curved_face_verify_") as temp_dir:
|
||||
temp_root = Path(temp_dir)
|
||||
|
||||
sphere_path = temp_root / "sphere.step"
|
||||
_write_sphere_model(sphere_path)
|
||||
sphere_probe = StepModel.load(sphere_path)
|
||||
sphere_face_id = _first_face_by_surface(sphere_probe, "sphere")
|
||||
sphere_plan = sphere_probe.spherical_radius_plan(sphere_face_id, 10.0)
|
||||
if str(sphere_plan.get("risk")) != "high":
|
||||
raise SystemExit(f"sphere radius isolation case should be high risk, got {sphere_plan}")
|
||||
_run_worker_case(
|
||||
label="sphere radius",
|
||||
operation="resize_sphere_radius",
|
||||
args=[sphere_face_id, 10.0],
|
||||
validator=_assert_sphere_radius,
|
||||
input_path=sphere_path,
|
||||
)
|
||||
|
||||
torus_path = temp_root / "torus.step"
|
||||
_write_torus_model(torus_path)
|
||||
torus_probe = StepModel.load(torus_path)
|
||||
torus_face_id = _first_face_by_surface(torus_probe, "torus")
|
||||
torus_plan = torus_probe.toroidal_radius_plan(torus_face_id, 4.0, "minor")
|
||||
if str(torus_plan.get("risk")) != "high":
|
||||
raise SystemExit(f"torus minor-radius isolation case should be high risk, got {torus_plan}")
|
||||
_run_worker_case(
|
||||
label="torus minor radius",
|
||||
operation="resize_torus_radius",
|
||||
args=[torus_face_id, 4.0, "minor"],
|
||||
validator=_assert_torus_minor_radius,
|
||||
input_path=torus_path,
|
||||
)
|
||||
|
||||
from step_editor.window_actions import WindowActionMixin
|
||||
|
||||
class _IsolatedJobProbe(WindowActionMixin):
|
||||
@@ -260,6 +386,29 @@ def main() -> int:
|
||||
self.step_path = DEFAULT_MODEL
|
||||
|
||||
probe = _IsolatedJobProbe()
|
||||
if probe._isolated_edit_command(Path("request.json")) != [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"step_editor.isolated_edit_worker",
|
||||
"request.json",
|
||||
]:
|
||||
raise SystemExit("source isolation command should use python -m step_editor.isolated_edit_worker")
|
||||
had_frozen_attr = hasattr(sys, "frozen")
|
||||
previous_frozen = getattr(sys, "frozen", None)
|
||||
try:
|
||||
setattr(sys, "frozen", True)
|
||||
if probe._isolated_edit_command(Path("request.json")) != [
|
||||
sys.executable,
|
||||
"--isolated-edit-worker",
|
||||
"request.json",
|
||||
]:
|
||||
raise SystemExit("frozen isolation command should call main.exe --isolated-edit-worker")
|
||||
finally:
|
||||
if had_frozen_attr:
|
||||
setattr(sys, "frozen", previous_frozen)
|
||||
else:
|
||||
delattr(sys, "frozen")
|
||||
|
||||
for title in (
|
||||
"面宽(当前面)",
|
||||
"面高(当前面)",
|
||||
@@ -291,12 +440,15 @@ def main() -> int:
|
||||
snapshot=snapshot,
|
||||
before_stats=probe.model.stats(),
|
||||
before_part_stats=probe.model.part_topology_stats(1),
|
||||
before_quality=probe._edit_quality_info_or_none(probe.model, context, 1),
|
||||
before_geometry={},
|
||||
)
|
||||
if "隔离子进程" not in str(result.get("message", "")):
|
||||
raise SystemExit(f"isolated window job did not report isolated execution: {result}")
|
||||
if probe.model.stats().solids != 1:
|
||||
raise SystemExit(f"isolated window job changed solid count unexpectedly: {probe.model.stats()}")
|
||||
if not _logical_region_has_width(probe.model, 0, 25.0):
|
||||
raise SystemExit("isolated window job did not preserve the original logical Face ID on the edited width")
|
||||
print("isolated window job ok")
|
||||
|
||||
owning_probe = _IsolatedJobProbe()
|
||||
@@ -317,12 +469,16 @@ def main() -> int:
|
||||
snapshot=owning_probe.model.snapshot(),
|
||||
before_stats=owning_probe.model.stats(),
|
||||
before_part_stats=owning_probe.model.part_topology_stats(1),
|
||||
before_quality=owning_probe._edit_quality_info_or_none(owning_probe.model, context, 1),
|
||||
before_geometry={},
|
||||
)
|
||||
if "隔离子进程" not in str(owning_result.get("message", "")):
|
||||
raise SystemExit(f"isolated owning window job did not report isolated execution: {owning_result}")
|
||||
_assert_face_width("面宽(整体窗口任务)", owning_probe.model, 25.0)
|
||||
if not _logical_region_has_width(owning_probe.model, 0, 25.0):
|
||||
raise SystemExit("isolated owning window job did not preserve the original logical Face ID on the edited width")
|
||||
print("isolated owning window job ok")
|
||||
_run_main_worker_entry_case()
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user