feat: 完善 Face 一级关系编辑和稳定性

This commit is contained in:
2026-08-04 09:35:39 +08:00
parent bb44e3920d
commit 5799d5d813
29 changed files with 6295 additions and 189 deletions
+157 -1
View File
@@ -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