feat: 推进一级关系参数化编辑与参数导出
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
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.isolated_edit_worker import run_request
|
||||
from step_editor.model import StepModel
|
||||
from step_editor.window_core import WindowCoreMixin
|
||||
from step_editor.window_state import WindowStateMixin
|
||||
|
||||
|
||||
MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "ICEPAK-NATURAL.stp"
|
||||
|
||||
|
||||
class _SelectionHarness(WindowCoreMixin, WindowStateMixin):
|
||||
def __init__(self, model: StepModel) -> None:
|
||||
self.model = model
|
||||
self.feature_detection_level = "current-only"
|
||||
self.operation_in_progress = False
|
||||
self.scan_in_progress = False
|
||||
self.load_in_progress = False
|
||||
self.selected_face_id = None
|
||||
self.selected_edge_id = None
|
||||
self.selected_kind = None
|
||||
self.selected_part_id = None
|
||||
self.selected_solid_id = None
|
||||
self.manual_bottom_face_id = None
|
||||
self.manual_slot_pair_face_id = None
|
||||
|
||||
def _current_feature_detection_level(self) -> str:
|
||||
return str(self.feature_detection_level)
|
||||
|
||||
def select_feature_face(self, face_id: int) -> None:
|
||||
self.selected_face_id = face_id
|
||||
self.selected_edge_id = None
|
||||
self.selected_kind = "feature"
|
||||
self.selected_part_id = self.model.face_part_ids[face_id]
|
||||
self.selected_solid_id = self.model.face_solid_ids[face_id]
|
||||
|
||||
|
||||
def _assert(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def _isolated_hole_resize(model: StepModel, face_id: int, diameter: float, root: Path) -> str:
|
||||
input_path = root / "face87_input.brep"
|
||||
output_path = root / "face87_output.brep"
|
||||
request_path = root / "face87_resize_request.json"
|
||||
model.export_internal_brep(input_path)
|
||||
request_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"input_path": str(input_path),
|
||||
"output_path": str(output_path),
|
||||
"input_format": "brep",
|
||||
"output_format": "brep",
|
||||
"operation": "resize_cylindrical_hole",
|
||||
"args": [face_id, diameter],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
code = run_request(request_path)
|
||||
response = json.loads(request_path.with_suffix(".response.json").read_text(encoding="utf-8"))
|
||||
_assert(code == 0 and response.get("ok") is True, f"isolated Face {face_id} resize should pass: {response}")
|
||||
_assert(output_path.exists(), f"isolated Face {face_id} resize should write output BREP")
|
||||
return str(response.get("message") or "")
|
||||
|
||||
|
||||
def _edge_ids_from_polydata(polydata) -> set[int]:
|
||||
edge_arr = polydata.GetCellData().GetArray("edge_id") if polydata is not None else None
|
||||
if edge_arr is None:
|
||||
return set()
|
||||
return {int(edge_arr.GetValue(index)) for index in range(edge_arr.GetNumberOfTuples())}
|
||||
|
||||
|
||||
def _polydata_edge_sample_counts(polydata) -> dict[int, list[int]]:
|
||||
edge_arr = polydata.GetCellData().GetArray("edge_id") if polydata is not None else None
|
||||
if edge_arr is None:
|
||||
return {}
|
||||
counts: dict[int, list[int]] = {}
|
||||
for cell_id in range(polydata.GetNumberOfCells()):
|
||||
edge_id = int(edge_arr.GetValue(cell_id))
|
||||
counts.setdefault(edge_id, []).append(int(polydata.GetCell(cell_id).GetNumberOfPoints()))
|
||||
return counts
|
||||
|
||||
|
||||
def _assert_default_edges_hide_same_domain_internal_edges(model: StepModel, label: str) -> None:
|
||||
visible_edge_ids = _edge_ids_from_polydata(model.build_edge_polydata(show_same_domain_internal_edges=False))
|
||||
hidden_internal_ids = set(model._same_domain_internal_edge_ids()) # noqa: SLF001
|
||||
leaked = sorted(visible_edge_ids & hidden_internal_ids)
|
||||
_assert(not leaked, f"{label} default edge display should hide same-domain/internal duplicate edges: {leaked[:12]}")
|
||||
|
||||
|
||||
def _assert_small_circle_edges_are_smooth(model: StepModel, label: str) -> None:
|
||||
edge_polydata = model.build_edge_polydata(deflection=1.6, show_same_domain_internal_edges=False)
|
||||
sample_counts = _polydata_edge_sample_counts(edge_polydata)
|
||||
rough_edges: list[tuple[int, int, float]] = []
|
||||
for edge_id, counts in sample_counts.items():
|
||||
info = model.edge_info(edge_id)
|
||||
if info.get("curve") != "circle":
|
||||
continue
|
||||
radius = float(info.get("radius") or 0.0)
|
||||
if radius <= 0.0 or radius > 0.35:
|
||||
continue
|
||||
point_count = max(counts or [0])
|
||||
if point_count < 9:
|
||||
rough_edges.append((edge_id, point_count, radius))
|
||||
_assert(not rough_edges, f"{label} small circular display edges should not be drawn as triangles: {rough_edges[:12]}")
|
||||
|
||||
|
||||
def _assert_rectangular_face_parameters(model: StepModel, harness: _SelectionHarness) -> None:
|
||||
face_id = 9
|
||||
info = model.feature_info(face_id)
|
||||
_assert(info.get("surface") == "plane", "ICEPAK Face 9 should be a planar rectangular face")
|
||||
_assert(info.get("prismatic_profile_status") == "candidate", "ICEPAK Face 9 should be a rectangular profile")
|
||||
_assert(bool(info.get("local_face_size_edit_ready")), f"ICEPAK Face 9 size parameters should be visible: {info}")
|
||||
width = float(info.get("local_face_width") or 0.0)
|
||||
height = float(info.get("local_face_height") or 0.0)
|
||||
_assert(abs(width - 8.0) <= 1e-7, f"ICEPAK Face 9 length should be read from rectangle edges, got {width:g}")
|
||||
_assert(abs(height - 4.9) <= 1e-7, f"ICEPAK Face 9 width should be read from rectangle edges, got {height:g}")
|
||||
|
||||
harness.select_feature_face(face_id)
|
||||
specs = harness._property_editor_specs(info, info)
|
||||
editable_keys = {str(spec.get("key") or "") for spec in specs if bool(spec.get("editable"))}
|
||||
expected = {"local_face_width", "local_face_height", "face_target_normal_position"}
|
||||
missing = sorted(expected - editable_keys)
|
||||
_assert(not missing, f"ICEPAK Face 9 feature parameters should expose length, width and offset: {missing}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not MODEL_PATH.exists():
|
||||
print("icepak cylindrical region selection skipped: local ICEPAK-NATURAL.stp is not present")
|
||||
return 0
|
||||
|
||||
model = StepModel.load(MODEL_PATH)
|
||||
_assert_default_edges_hide_same_domain_internal_edges(model, "ICEPAK before edit")
|
||||
harness = _SelectionHarness(model)
|
||||
_assert_rectangular_face_parameters(model, harness)
|
||||
expected_regions = {
|
||||
87: [87, 94],
|
||||
94: [87, 94],
|
||||
84: [84, 97],
|
||||
97: [84, 97],
|
||||
}
|
||||
for face_id, expected in expected_regions.items():
|
||||
info = model.quick_face_info(face_id)
|
||||
_assert(info.get("surface") == "cylinder", f"Face {face_id} should be cylindrical")
|
||||
_assert(model.face_region_ids(face_id) == expected, f"Face {face_id} region should be {expected}")
|
||||
_assert(
|
||||
list(info.get("feature_highlight_face_ids") or []) == expected,
|
||||
f"Face {face_id} quick info highlight should include the whole same-domain region: {info.get('feature_highlight_face_ids')}",
|
||||
)
|
||||
_assert(
|
||||
harness._selection_same_domain_face_ids(face_id) == expected,
|
||||
f"Face {face_id} UI selection highlight should include the whole same-domain region",
|
||||
)
|
||||
context = harness._feature_context_info(face_id)
|
||||
_assert(
|
||||
list(context.get("feature_highlight_face_ids") or []) == expected,
|
||||
f"Face {face_id} feature context highlight should include the whole same-domain region: {context.get('feature_highlight_face_ids')}",
|
||||
)
|
||||
feature = model.feature_info(face_id)
|
||||
_assert(
|
||||
list(feature.get("same_domain_face_ids") or []) == expected,
|
||||
f"Face {face_id} feature info should use the whole same-domain region",
|
||||
)
|
||||
_assert(bool(feature.get("is_full_cylinder")), f"Face {face_id} should be treated as a full cylinder")
|
||||
|
||||
resize_model = StepModel.load(MODEL_PATH)
|
||||
plan = resize_model.cylindrical_resize_plan(87, 0.3)
|
||||
_assert(plan.get("feature_type") == "圆柱孔候选", "Face 87 should plan as a cylindrical hole")
|
||||
_assert(list(plan.get("same_domain_face_ids") or []) == [87, 94], "Face 87 resize plan should use both side fragments")
|
||||
_assert(bool(plan.get("is_full_cylinder")), "Face 87 resize plan should preserve full-cylinder semantics")
|
||||
_assert(float(plan.get("angular_span") or 0.0) > 6.0, "Face 87 resize plan should not use the selected half-face span")
|
||||
result = resize_model.resize_cylindrical_hole(87, 0.3)
|
||||
_assert("diameter 0.5 -> 0.3" in result, "Face 87 diameter shrink should complete")
|
||||
_assert_default_edges_hide_same_domain_internal_edges(resize_model, "ICEPAK after Face 87 shrink")
|
||||
_assert_small_circle_edges_are_smooth(resize_model, "ICEPAK after Face 87 shrink")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="icepak_face87_isolated_") as temp_dir:
|
||||
worker_message = _isolated_hole_resize(model, 87, 0.3, Path(temp_dir))
|
||||
_assert("diameter 0.5 -> 0.3" in worker_message, "Face 87 isolated diameter shrink should complete")
|
||||
|
||||
print("icepak cylindrical region selection ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user