from __future__ import annotations from pathlib import Path import sys 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.ui_helpers import _smooth_surface_polydata from step_editor.window_state import WindowStateMixin MODEL_PATH = PROJECT_ROOT / "assets" / "models" / "geom_extract.step" SOURCE_FACE_ID = 398 def main() -> int: model = StepModel.load(MODEL_PATH) root = model.feature_info(SOURCE_FACE_ID) if root.get("shell_region_status") == "candidate": raise AssertionError("Face 398 remote opposite plane was incorrectly exposed as wall thickness") associated = model.associated_feature_infos(SOURCE_FACE_ID) by_type = {str(info.get("feature_type")): info for info in associated} for feature_type in ("凸台/外圆候选", "圆柱孔候选"): if feature_type not in by_type: raise AssertionError(f"missing associated {feature_type}: {tuple(by_type)}") if any(info.get("feature_type") == "规则矩形平面候选" for info in associated): raise AssertionError("unstable 2D-only rectangular candidates should not be associated") boss = by_type["凸台/外圆候选"] hole = by_type["圆柱孔候选"] if int(boss.get("association_source_face_id", -1)) != 394: raise AssertionError(f"unexpected boss source: {boss.get('association_source_face_id')}") if int(hole.get("association_source_face_id", -1)) != 1591: raise AssertionError(f"unexpected hole source: {hole.get('association_source_face_id')}") if not bool(boss.get("is_full_cylinder")) or not bool(hole.get("is_full_cylinder")): raise AssertionError("split half-cylinder faces were not combined into full cylinders") 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 = SOURCE_FACE_ID state.selected_edge_id = None state.selected_kind = "feature" state.selected_part_id = int(root["part_id"]) state.selected_solid_id = int(root["solid_id"]) state.feature_detection_level = "associated-only" context = state._feature_context_info(SOURCE_FACE_ID) specs, _used = state._editable_property_specs(context) rows = state._feature_context_property_specs(specs, context) context_row = next((row for row in rows if row.get("key") == "feature_context_note"), None) if context_row is None: raise AssertionError("feature context should expose an associated-feature detection summary row") context_text = str(context_row.get("current_text") or "") context_scope = str(context_row.get("scope_text") or "") if "相邻特征" not in context_text or "局部关联特征" not in context_text: raise AssertionError(f"associated detection summary is not clear enough: {context_row}") if "相邻特征" not in context_scope or "关联特征" not in context_scope: raise AssertionError(f"associated detection summary should expose level and count: {context_row}") editable = { (str(row.get("label")), int(row.get("source_face_id", -1)), str(row.get("action"))) for row in rows if row.get("parameter_role") == "dimension" and row.get("source_face_id") is not None } expected = { ("凸台/外圆候选 · 直径", 394, "resize_boss"), ("凸台/外圆候选 · 高度", 394, "resize_cylindrical_height_owning_scale"), ("圆柱孔候选 · 直径", 1591, "resize_hole"), ("圆柱孔候选 · 盲孔/盲槽深度", 1591, "resize_hole_depth"), } if not expected.issubset(editable): raise AssertionError(f"missing associated editable rows: {expected - editable}") display = _smooth_surface_polydata(model.build_face_polydata(deflection=0.035)) face_ids = display.GetCellData().GetArray("face_id") normals = display.GetPointData().GetNormals() boss_cells = [ index for index in range(display.GetNumberOfCells()) if int(face_ids.GetTuple1(index)) == 394 ] if display.GetNumberOfCells() > 250_000: raise AssertionError(f"display tessellation exceeded the large-model budget: {display.GetNumberOfCells()}") if len(boss_cells) < 60: raise AssertionError(f"cylindrical display tessellation is too coarse: {len(boss_cells)}") local_boss = model.build_face_polydata(face_ids=[394], deflection=0.035) if local_boss.GetNumberOfCells() < 400: raise AssertionError( "single-Face local display should still allow fine cylindrical tessellation: " f"{local_boss.GetNumberOfCells()}" ) local_first_model = StepModel.load(MODEL_PATH) local_first_model.build_face_polydata(face_ids=[394], deflection=0.035) local_first_display = local_first_model.build_face_polydata(deflection=0.035) if local_first_display.GetNumberOfCells() > 250_000: raise AssertionError( "full display should remain budgeted after a local fine display was built first: " f"{local_first_display.GetNumberOfCells()}" ) plane_cells = [ index for index in range(display.GetNumberOfCells()) if int(face_ids.GetTuple1(index)) == SOURCE_FACE_ID ] plane_points = sorted({ display.GetCell(cell_id).GetPointId(point_index) for cell_id in plane_cells for point_index in range(display.GetCell(cell_id).GetNumberOfPoints()) }) plane_normals = [normals.GetTuple(point_id) for point_id in plane_points] reference = plane_normals[0] if min(sum(a * b for a, b in zip(reference, normal)) for normal in plane_normals) < 0.9999: raise AssertionError("planar B-Rep Face normals were polluted by adjacent faces") print("associated feature and display quality ok") return 0 if __name__ == "__main__": raise SystemExit(main())