feat: 完善 Face 一级关系编辑和稳定性
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse
|
||||
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox
|
||||
from OCC.Core.gp import gp_Pnt
|
||||
|
||||
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.window_state import WindowStateMixin, _feature_dimension_keys
|
||||
from step_editor.step_io import _write_step
|
||||
|
||||
|
||||
DEFAULT_MODEL = PROJECT_ROOT / "assets" / "models" / "cube_10mm.step"
|
||||
|
||||
|
||||
def close_to(value: object, expected: float, tolerance: float = 1e-6) -> bool:
|
||||
try:
|
||||
return abs(float(value) - expected) <= tolerance
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def find_feature(model: StepModel, feature_type: str) -> dict[str, object]:
|
||||
matches = [model.feature_info(face_id) for face_id in range(len(model.faces))]
|
||||
matches = [info for info in matches if info.get("feature_type") == feature_type]
|
||||
if not matches:
|
||||
observed = sorted({str(model.feature_info(face_id).get("feature_type")) for face_id in range(len(model.faces))})
|
||||
raise AssertionError(f"expected {feature_type}, observed {observed}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def assert_prismatic_sizes(
|
||||
info: dict[str, object], length: float, width: float, depth: float
|
||||
) -> None:
|
||||
expected = {
|
||||
"prismatic_length": length,
|
||||
"prismatic_width": width,
|
||||
"prismatic_extrusion_estimate": depth,
|
||||
}
|
||||
for key, value in expected.items():
|
||||
if not close_to(info.get(key), value):
|
||||
raise AssertionError(f"{key}={info.get(key)!r}, expected {value}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
model = StepModel.load(DEFAULT_MODEL)
|
||||
if len(model.faces) != 6:
|
||||
raise AssertionError(f"expected 6 cube faces, got {len(model.faces)}")
|
||||
|
||||
for face_id in range(len(model.faces)):
|
||||
info = model.feature_info(face_id)
|
||||
if info.get("prismatic_profile_status") != "candidate":
|
||||
raise AssertionError(f"Face {face_id} was not recognized as a rectangular profile")
|
||||
if info.get("prismatic_extrusion_status") != "candidate":
|
||||
raise AssertionError(f"Face {face_id} was not recognized as a connected extrusion")
|
||||
for key in ("prismatic_length", "prismatic_width", "prismatic_extrusion_estimate"):
|
||||
if not close_to(info.get(key), 10.0):
|
||||
raise AssertionError(f"Face {face_id} {key}={info.get(key)!r}, expected 10")
|
||||
if len(tuple(info.get("prismatic_connected_side_face_ids", ()))) != 4:
|
||||
raise AssertionError(f"Face {face_id} does not have four connected side faces")
|
||||
|
||||
info = model.feature_info(0)
|
||||
expected_keys = ("local_face_width", "local_face_height", "shell_thickness_estimate")
|
||||
if _feature_dimension_keys(info) != expected_keys:
|
||||
raise AssertionError(f"unexpected cube feature dimensions: {_feature_dimension_keys(info)}")
|
||||
|
||||
state = object.__new__(WindowStateMixin)
|
||||
state.model = model
|
||||
state.operation_in_progress = False
|
||||
state.scan_in_progress = False
|
||||
state.load_in_progress = False
|
||||
state.selected_face_id = 0
|
||||
state.selected_edge_id = None
|
||||
state.selected_kind = "feature"
|
||||
state.selected_part_id = int(info["part_id"])
|
||||
state.selected_solid_id = int(info["solid_id"])
|
||||
specs, _used = state._editable_property_specs(info)
|
||||
filtered = state._feature_property_specs(specs, info)
|
||||
dimensions = [spec for spec in filtered if spec.get("parameter_role") == "dimension"]
|
||||
actual = tuple(
|
||||
(
|
||||
str(spec.get("key")),
|
||||
str(spec.get("label")),
|
||||
str(spec.get("current_text")),
|
||||
)
|
||||
for spec in dimensions
|
||||
)
|
||||
expected = (
|
||||
("local_face_width", "长度", "10"),
|
||||
("local_face_height", "宽度", "10"),
|
||||
("shell_thickness_estimate", "高度/深度", "10"),
|
||||
)
|
||||
if actual != expected:
|
||||
raise AssertionError(f"unexpected prismatic UI parameters: {actual}")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="step-editor-prismatic-") as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
base = BRepPrimAPI_MakeBox(30.0, 20.0, 5.0).Shape()
|
||||
|
||||
pocket_tool = BRepPrimAPI_MakeBox(gp_Pnt(10.0, 6.0, 2.0), 10.0, 8.0, 4.0).Shape()
|
||||
pocket_path = temp_path / "rectangular-pocket.step"
|
||||
_write_step(BRepAlgoAPI_Cut(base, pocket_tool).Shape(), pocket_path)
|
||||
pocket_info = find_feature(StepModel.load(pocket_path), "矩形口袋候选")
|
||||
assert_prismatic_sizes(pocket_info, 10.0, 8.0, 3.0)
|
||||
if pocket_info.get("prismatic_reference_source") != "side-wall-topology":
|
||||
raise AssertionError(f"unexpected pocket reference: {pocket_info.get('prismatic_reference_source')}")
|
||||
|
||||
boss_tool = BRepPrimAPI_MakeBox(gp_Pnt(10.0, 6.0, 5.0), 10.0, 8.0, 3.0).Shape()
|
||||
boss_path = temp_path / "rectangular-boss.step"
|
||||
_write_step(BRepAlgoAPI_Fuse(base, boss_tool).Shape(), boss_path)
|
||||
boss_info = find_feature(StepModel.load(boss_path), "矩形凸台候选")
|
||||
assert_prismatic_sizes(boss_info, 10.0, 8.0, 3.0)
|
||||
if boss_info.get("prismatic_reference_source") not in {
|
||||
"side-wall-topology", "overlapping-plane"
|
||||
}:
|
||||
raise AssertionError(f"unexpected boss reference: {boss_info.get('prismatic_reference_source')}")
|
||||
|
||||
print("prismatic feature recognition ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user