feat: 完善 Face 一级关系编辑和稳定性校验
This commit is contained in:
@@ -6,7 +6,9 @@ import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeSphere, BRepPrimAPI_MakeTorus
|
||||
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut
|
||||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder, BRepPrimAPI_MakeSphere, BRepPrimAPI_MakeTorus
|
||||
from OCC.Core.gp import gp_Ax2, gp_Dir, gp_Pnt
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -15,6 +17,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from step_editor.model import StepModel
|
||||
from step_editor.geometry_utils import _finalize_boolean_result
|
||||
from step_editor.step_io import _write_step
|
||||
|
||||
|
||||
@@ -207,6 +210,17 @@ def _write_torus_model(path: Path) -> None:
|
||||
_write_step(BRepPrimAPI_MakeTorus(8.0, 2.0).Shape(), path)
|
||||
|
||||
|
||||
def _write_hollow_cylinder_model(path: Path) -> None:
|
||||
outer = BRepPrimAPI_MakeCylinder(8.0, 78.0).Shape()
|
||||
inner = BRepPrimAPI_MakeCylinder(
|
||||
gp_Ax2(gp_Pnt(0.0, 0.0, -1.0), gp_Dir(0.0, 0.0, 1.0)),
|
||||
3.0,
|
||||
80.0,
|
||||
).Shape()
|
||||
cut = BRepAlgoAPI_Cut(outer, inner)
|
||||
_write_step(_finalize_boolean_result(cut, "verify isolated hollow cylinder cut", use_glue=False), 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:
|
||||
@@ -214,6 +228,23 @@ def _first_face_by_surface(model: StepModel, surface: str) -> int:
|
||||
raise SystemExit(f"no {surface} Face was recognized")
|
||||
|
||||
|
||||
def _top_planar_face(model: StepModel) -> int:
|
||||
best: tuple[float, int] | None = None
|
||||
for face_id in range(len(model.faces)):
|
||||
info = model.face_info(face_id)
|
||||
if info.get("surface") != "plane":
|
||||
continue
|
||||
center = info.get("area_center") or info.get("bbox_center")
|
||||
if not isinstance(center, tuple) or len(center) != 3:
|
||||
continue
|
||||
z_value = float(center[2])
|
||||
if best is None or z_value > best[0]:
|
||||
best = (z_value, face_id)
|
||||
if best is None:
|
||||
raise SystemExit("no top planar Face was recognized")
|
||||
return best[1]
|
||||
|
||||
|
||||
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)):
|
||||
@@ -270,7 +301,78 @@ def _assert_torus_minor_radius(label: str, model: StepModel, target_minor: float
|
||||
print(f"matched_torus_radii={matches}")
|
||||
|
||||
|
||||
def _assert_hollow_cylinder_height(label: str, model: StepModel, target_height: float) -> None:
|
||||
bbox_size = model.geometry_stats().get("bbox_size")
|
||||
if not isinstance(bbox_size, tuple) or len(bbox_size) != 3:
|
||||
raise SystemExit(f"{label}: output bbox_size is missing")
|
||||
height = float(bbox_size[2])
|
||||
if abs(height - target_height) > 1e-4:
|
||||
raise SystemExit(f"{label}: hollow cylinder height should be {target_height:g}, got {height:g}")
|
||||
radii: list[float] = []
|
||||
for face_id in range(len(model.faces)):
|
||||
info = model.face_info(face_id)
|
||||
if info.get("surface") != "cylinder":
|
||||
continue
|
||||
radius = float(info.get("radius") or 0.0)
|
||||
if radius <= 0.0:
|
||||
continue
|
||||
if not any(abs(radius - existing) <= max(radius, existing, 1.0) * 1e-5 for existing in radii):
|
||||
radii.append(radius)
|
||||
radii.sort()
|
||||
if len(radii) != 2 or abs(radii[0] - 3.0) > 1e-4 or abs(radii[1] - 8.0) > 1e-4:
|
||||
raise SystemExit(f"{label}: hollow cylinder should preserve inner/outer radii, got {radii}")
|
||||
print(f"matched_hollow_cylinder_height={height:g}, radii={radii}")
|
||||
|
||||
|
||||
def _assert_hollow_cylinder_extension(label: str, model: StepModel) -> None:
|
||||
_assert_hollow_cylinder_height(label, model, 167.0)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
push_pull_probe = StepModel.load(DEFAULT_MODEL)
|
||||
large_push_plan = push_pull_probe.push_pull_plan(0, 11.0)
|
||||
if large_push_plan.get("status") == "blocked" or str(large_push_plan.get("risk")) != "high":
|
||||
raise SystemExit(f"large push/pull should be allowed as an isolated high-risk edit: {large_push_plan}")
|
||||
_run_worker_case(
|
||||
label="large push/pull current Face",
|
||||
operation="push_pull_face",
|
||||
args=[0, 11.0],
|
||||
validator=lambda label, model: _assert_plane_position(label, model, 11.0),
|
||||
)
|
||||
with tempfile.TemporaryDirectory(prefix="geom_param_isolated_hollow_cylinder_verify_") as temp_dir:
|
||||
hollow_path = Path(temp_dir) / "hollow_cylinder.step"
|
||||
_write_hollow_cylinder_model(hollow_path)
|
||||
hollow_probe = StepModel.load(hollow_path)
|
||||
hollow_face_id = _top_planar_face(hollow_probe)
|
||||
hollow_plan = hollow_probe.push_pull_plan(hollow_face_id, 89.0)
|
||||
if hollow_plan.get("cylindrical_cap_extension_kind") != "coaxial-tube":
|
||||
raise SystemExit(f"hollow cylinder cap should use coaxial-tube plan in worker test: {hollow_plan}")
|
||||
_run_worker_case(
|
||||
label="large hollow cylinder cap push/pull",
|
||||
operation="push_pull_face",
|
||||
args=[hollow_face_id, 89.0],
|
||||
validator=_assert_hollow_cylinder_extension,
|
||||
input_path=hollow_path,
|
||||
)
|
||||
_run_worker_case(
|
||||
label="hollow cylinder cap inward push/pull",
|
||||
operation="push_pull_face",
|
||||
args=[hollow_face_id, -39.0],
|
||||
validator=lambda label, model: _assert_hollow_cylinder_height(label, model, 39.0),
|
||||
input_path=hollow_path,
|
||||
)
|
||||
hollow_height_probe = StepModel.load(hollow_path)
|
||||
hollow_cylinder_face_id = _first_face_by_surface(hollow_height_probe, "cylinder")
|
||||
height_plan = hollow_height_probe.cylindrical_height_plan(hollow_cylinder_face_id, 167.0)
|
||||
if height_plan.get("status") == "blocked":
|
||||
raise SystemExit(f"hollow cylinder side Face height edit should be isolated, not blocked: {height_plan}")
|
||||
_run_worker_case(
|
||||
label="large hollow cylinder side Face height",
|
||||
operation="resize_cylindrical_height",
|
||||
args=[hollow_cylinder_face_id, 167.0],
|
||||
validator=_assert_hollow_cylinder_extension,
|
||||
input_path=hollow_path,
|
||||
)
|
||||
_run_worker_case(
|
||||
label="面偏移(当前面)",
|
||||
operation="move_face_plane_offset_local",
|
||||
@@ -384,8 +486,30 @@ def main() -> int:
|
||||
def __init__(self) -> None:
|
||||
self.model = StepModel.load(DEFAULT_MODEL)
|
||||
self.step_path = DEFAULT_MODEL
|
||||
self.current_info_values = self.model.face_info(0)
|
||||
|
||||
probe = _IsolatedJobProbe()
|
||||
action_plan = probe._push_pull_plan_for_action(0, 11.0)
|
||||
if action_plan.get("status") == "blocked" or str(action_plan.get("risk")) != "high":
|
||||
raise SystemExit(f"UI push/pull action plan should use the model-layer high-risk plan: {action_plan}")
|
||||
if float(action_plan.get("push_pull_distance_to_owning_axis_span_ratio") or 0.0) <= 1.0:
|
||||
raise SystemExit(f"UI push/pull action plan should expose model-layer span ratio: {action_plan}")
|
||||
|
||||
complex_path = PROJECT_ROOT / "assets" / "models" / "geom_extract.step"
|
||||
if complex_path.exists():
|
||||
complex_probe = _IsolatedJobProbe()
|
||||
complex_probe.model = StepModel.load(complex_path)
|
||||
complex_probe.step_path = complex_path
|
||||
complex_probe.selected_face_id = 1
|
||||
complex_probe.current_info_values = complex_probe.model.quick_face_info(1)
|
||||
complex_plan = complex_probe._push_pull_plan_for_action(1, 89.0)
|
||||
if not complex_plan.get("ui_deferred_model_plan"):
|
||||
raise SystemExit(f"complex holed cap UI plan should defer full model plan: {complex_plan}")
|
||||
if str(complex_plan.get("risk")) != "high":
|
||||
raise SystemExit(f"deferred complex holed cap UI plan should be high risk: {complex_plan}")
|
||||
if complex_probe._isolation_for_plan(complex_plan, "push_pull_face", [1, 89.0]) is None:
|
||||
raise SystemExit(f"deferred complex holed cap UI plan should request isolated execution: {complex_plan}")
|
||||
|
||||
if probe._isolated_edit_command(Path("request.json")) != [
|
||||
sys.executable,
|
||||
"-m",
|
||||
@@ -445,9 +569,18 @@ def main() -> int:
|
||||
)
|
||||
if "隔离子进程" not in str(result.get("message", "")):
|
||||
raise SystemExit(f"isolated window job did not report isolated execution: {result}")
|
||||
after_model = result.get("after_model")
|
||||
if not isinstance(after_model, StepModel):
|
||||
raise SystemExit(f"isolated window job should return after_model for UI-thread installation: {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):
|
||||
if _logical_region_has_width(probe.model, 0, 25.0):
|
||||
raise SystemExit("isolated window job replaced the window model before returning to the UI thread")
|
||||
if after_model.stats().solids != 1:
|
||||
raise SystemExit(f"isolated window after_model changed solid count unexpectedly: {after_model.stats()}")
|
||||
if result.get("model_polydata") is None or result.get("edge_polydata") is None:
|
||||
raise SystemExit("isolated window job should return prebuilt display polydata")
|
||||
if not _logical_region_has_width(after_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")
|
||||
|
||||
@@ -474,8 +607,17 @@ def main() -> int:
|
||||
)
|
||||
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):
|
||||
owning_after_model = owning_result.get("after_model")
|
||||
if not isinstance(owning_after_model, StepModel):
|
||||
raise SystemExit(
|
||||
f"isolated owning window job should return after_model for UI-thread installation: {owning_result}"
|
||||
)
|
||||
if _logical_region_has_width(owning_probe.model, 0, 25.0):
|
||||
raise SystemExit("isolated owning window job replaced the window model before returning to the UI thread")
|
||||
if owning_result.get("model_polydata") is None or owning_result.get("edge_polydata") is None:
|
||||
raise SystemExit("isolated owning window job should return prebuilt display polydata")
|
||||
_assert_face_width("面宽(整体窗口任务)", owning_after_model, 25.0)
|
||||
if not _logical_region_has_width(owning_after_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()
|
||||
|
||||
Reference in New Issue
Block a user