feat: 推进SCDM-first后端接入和大模型编辑优化
This commit is contained in:
@@ -4,8 +4,10 @@ import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
@@ -80,11 +82,17 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
self.step_path = PROJECT_ROOT / "assets" / "models" / "probe.step"
|
||||
self.current_info_values = self._plane_info()
|
||||
self.executed_property_actions: list[tuple[str, str]] = []
|
||||
self.scdm_backend_status = None
|
||||
self.scdm_feature_cache = None
|
||||
self.scdm_feature_cache_state = "empty"
|
||||
self.scdm_feature_cache_message = ""
|
||||
self.scdm_edit_runner_ready = {"face.offset", "hole.diameter", "hole.position", "slot.position", "boss.position"}
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
self.object_edit_box = self
|
||||
self.property_table = QTableWidget(0, len(PROPERTY_TABLE_HEADERS))
|
||||
self.property_table.setHorizontalHeaderLabels(list(PROPERTY_TABLE_HEADERS))
|
||||
self.property_table.itemSelectionChanged.connect(lambda: self._update_property_apply_state())
|
||||
layout.addWidget(self.property_table)
|
||||
|
||||
self.property_card_scroll = QScrollArea()
|
||||
@@ -100,7 +108,9 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
self.property_command_bar = QFrame()
|
||||
self.property_command_layout = QHBoxLayout(self.property_command_bar)
|
||||
self.property_command_help_label = QLabel()
|
||||
self.current_capability_headline = QLabel()
|
||||
self.current_capability_button = QPushButton()
|
||||
self.scdm_backend_status_label = QLabel()
|
||||
self.scdm_backend_detail_label = QLabel()
|
||||
self.apply_property_button = QPushButton()
|
||||
self.export_parameters_button = QPushButton()
|
||||
self.relation_formula_input = QLineEdit()
|
||||
@@ -160,6 +170,15 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
self.executed_property_actions.append(("resize_face_height_local", self.face_height_input.text()))
|
||||
self._after_property_edit_finished(success=True)
|
||||
|
||||
def apply_scdm_property_edit(self, spec: dict[str, object] | None = None, target_text: str | None = None) -> None:
|
||||
self.executed_property_actions.append(
|
||||
(
|
||||
"apply_scdm_property_edit",
|
||||
f"{(spec or {}).get('scdm_capability_key', '')}:{target_text or ''}",
|
||||
)
|
||||
)
|
||||
self._after_property_edit_finished(success=True)
|
||||
|
||||
def statusBar(self) -> _StatusBar:
|
||||
return _StatusBar()
|
||||
|
||||
@@ -168,6 +187,26 @@ class _RelationFormulaEventProbe(WindowCoreMixin, _PropertyTableProbe):
|
||||
pass
|
||||
|
||||
|
||||
class _ScdmAutoPromptProbe(_PropertyTableProbe):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.prompt_calls: list[tuple[bool, str, str]] = []
|
||||
|
||||
def configure_scdm_backend(self, *, automatic: bool = False, reason: str = "", message: str = "") -> bool:
|
||||
self.prompt_calls.append((bool(automatic), str(reason), str(message)))
|
||||
return False
|
||||
|
||||
|
||||
class _PropertyUiRerouteProbe(_PropertyTableProbe):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.rerouted_callbacks = 0
|
||||
|
||||
def _reroute_to_ui_thread(self, _callback) -> bool:
|
||||
self.rerouted_callbacks += 1
|
||||
return True
|
||||
|
||||
|
||||
class _GlobalRelationCompletionModel:
|
||||
def __init__(self) -> None:
|
||||
self.faces = [object() for _index in range(100)]
|
||||
@@ -203,6 +242,12 @@ class _LargeRelationCompletionModel(_GlobalRelationCompletionModel):
|
||||
raise AssertionError("relation formula completion should not call face_region_logical_id")
|
||||
|
||||
|
||||
class _LargeDisplayModel:
|
||||
def __init__(self) -> None:
|
||||
self.faces = [object() for _index in range(1800)]
|
||||
self.edges = [object() for _index in range(5000)]
|
||||
|
||||
|
||||
class _RelationFormulaRemapModel:
|
||||
def __init__(self, face_infos: dict[int, dict[str, object]], face_count: int = 120) -> None:
|
||||
self.faces = [object() for _index in range(face_count)]
|
||||
@@ -529,6 +574,59 @@ def _assert_property_table_editor(probe: _PropertyTableProbe) -> None:
|
||||
_assert(preserved_editor.text().strip() == "12", "target value was not preserved after table expand")
|
||||
|
||||
|
||||
def _assert_scdm_command_row_uses_unified_apply() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.scdm_edit_runner_ready = {"feature.fill"}
|
||||
probe.scdm_feature_cache_state = "ready"
|
||||
probe.scdm_feature_cache = {
|
||||
"objects": [
|
||||
{
|
||||
"objectId": "hole:0",
|
||||
"objectType": "hole",
|
||||
"geometrySignature": {
|
||||
"faceIds": [0],
|
||||
"surfaceType": "cylinder",
|
||||
"radius": 1.0,
|
||||
"diameter": 2.0,
|
||||
"center": [0.0, 0.0, 0.0],
|
||||
},
|
||||
"capabilities": [
|
||||
{
|
||||
"key": "feature.fill",
|
||||
"displayName": "填孔/删除小特征",
|
||||
"currentValue": 1,
|
||||
"valueKind": "command",
|
||||
"editable": True,
|
||||
"defaultIntent": "删除并补面",
|
||||
"backendOperation": "fill_feature",
|
||||
"postCheck": "target_feature_removed",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
probe._refresh_property_editor()
|
||||
row = _row_by_label(probe, "填孔/删除小特征")
|
||||
_assert(probe.property_table.cellWidget(row, PROPERTY_TARGET_COLUMN) is None, "SCDM command row should not have a target editor")
|
||||
current_item = probe.property_table.item(row, PROPERTY_CURRENT_COLUMN)
|
||||
target_item = probe.property_table.item(row, PROPERTY_TARGET_COLUMN)
|
||||
_assert(current_item is not None and current_item.text() == "可执行", f"command current text should be readable: {current_item.text() if current_item else None}")
|
||||
_assert(target_item is not None and target_item.text() == "执行", f"command target text should be readable: {target_item.text() if target_item else None}")
|
||||
_assert(not probe.apply_property_button.isEnabled(), "command row should not enable parametric modeling until selected")
|
||||
|
||||
probe.property_table.selectRow(row)
|
||||
QApplication.processEvents()
|
||||
probe._update_property_apply_state()
|
||||
pending = probe._pending_property_edit_rows()
|
||||
_assert(len(pending) == 1 and pending[0][0] == row, f"selected command row should be pending: {pending}")
|
||||
_assert(probe.apply_property_button.isEnabled(), "selected command row should enable unified parametric modeling button")
|
||||
probe.apply_current_property_edit()
|
||||
_assert(
|
||||
probe.executed_property_actions == [("apply_scdm_property_edit", "feature.fill:执行")],
|
||||
f"selected command row should execute through SCDM property action: {probe.executed_property_actions}",
|
||||
)
|
||||
|
||||
|
||||
def _assert_diagnostics_stay_out_of_parameter_table(probe: _PropertyTableProbe) -> None:
|
||||
long_context = "已按“相邻特征”沿共享边拓扑探测当前特征及 3 个局部关联特征;关联尺寸可在同一参数表中直接修改。"
|
||||
probe.current_info_values = {
|
||||
@@ -551,6 +649,13 @@ def _assert_diagnostics_stay_out_of_parameter_table(probe: _PropertyTableProbe)
|
||||
def _assert_relation_formula_editor() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe._refresh_property_editor()
|
||||
_assert(
|
||||
not probe.relation_formula_completer_model.stringList(),
|
||||
"relation formula completions should stay lazy while the formula input is not focused",
|
||||
)
|
||||
probe.relation_formula_input.setFocus(Qt.FocusReason.OtherFocusReason)
|
||||
QApplication.processEvents()
|
||||
probe._update_relation_formula_completions()
|
||||
completions = set(probe.relation_formula_completer_model.stringList())
|
||||
_assert("Face0.面内长度" in completions, f"relation formula completion missing face length: {completions}")
|
||||
_assert("Face0.面内宽度" in completions, f"relation formula completion missing face width: {completions}")
|
||||
@@ -1222,6 +1327,31 @@ def _assert_mouse_selection_guards() -> None:
|
||||
"left-button press/release on different faces should not change selection",
|
||||
)
|
||||
|
||||
mouse_probe = _MouseSelectionProbe()
|
||||
mouse_probe._large_model_interaction_mode = lambda stats=None: True
|
||||
mouse_probe.pick_targets = [{"kind": "face", "target_id": 8, "pick_position": (0.0, 0.0, 0.0)}]
|
||||
mouse_probe._handle_left_button_press(20, 20)
|
||||
_assert(
|
||||
len(mouse_probe.pick_targets) == 0,
|
||||
"large-model left-button press should record the pressed target so background drags cannot select on release",
|
||||
)
|
||||
mouse_probe._handle_left_button_release(20, 20)
|
||||
_assert(
|
||||
[target.get("target_id") for target in mouse_probe.selected_targets] == [8],
|
||||
"large-model plain click should select the target recorded on press",
|
||||
)
|
||||
|
||||
mouse_probe = _MouseSelectionProbe()
|
||||
mouse_probe._large_model_interaction_mode = lambda stats=None: True
|
||||
mouse_probe.pick_targets = [{"kind": "face", "target_id": 8, "pick_position": (0.0, 0.0, 0.0)}]
|
||||
mouse_probe._handle_left_button_press(20, 20)
|
||||
mouse_probe._update_left_button_drag_state(40, 20)
|
||||
mouse_probe._handle_left_button_release(40, 20)
|
||||
_assert(
|
||||
len(mouse_probe.pick_targets) == 0 and not mouse_probe.selected_targets,
|
||||
"large-model drag/rotation may record the press target but must not select on release",
|
||||
)
|
||||
|
||||
|
||||
def _assert_quick_blind_depth_spec() -> None:
|
||||
blind_probe = _PropertyTableProbe()
|
||||
@@ -1391,6 +1521,362 @@ def _assert_parameter_export_action() -> None:
|
||||
)
|
||||
|
||||
|
||||
def _assert_scdm_selection_diagnostics() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.scdm_backend_status = {
|
||||
"path": "D:/softwaresInstallDir/ANSYS Inc/v222/SCDM/SpaceClaim.exe",
|
||||
"source": "common:D:/softwaresInstallDir/ANSYS Inc",
|
||||
"version": "v222",
|
||||
"runScriptOk": True,
|
||||
"licenseOk": True,
|
||||
}
|
||||
probe.scdm_feature_cache_state = "ready"
|
||||
probe.scdm_edit_runner_ready = {"face.offset"}
|
||||
probe.scdm_feature_cache = {
|
||||
"objects": [
|
||||
{
|
||||
"objectId": "face:0",
|
||||
"objectType": "face",
|
||||
"geometrySignature": {"faceIds": [0], "surfaceType": "plane", "planeOffset": 0.0},
|
||||
"capabilities": [
|
||||
{
|
||||
"key": "face.offset",
|
||||
"displayName": "偏移",
|
||||
"currentValue": 0.0,
|
||||
"valueKind": "number",
|
||||
"defaultIntent": "推拉平面",
|
||||
"backendOperation": "pull_face_offset",
|
||||
"postCheck": "target_face_offset",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
probe._refresh_property_editor()
|
||||
info = dict(probe.current_info_values)
|
||||
_assert("SCDM:已配置 v222" in str(info.get("scdm_backend_status")), f"SCDM backend diagnostic missing: {info}")
|
||||
_assert("识别缓存已就绪" in str(info.get("scdm_runtime_status")), f"SCDM runtime diagnostic missing: {info}")
|
||||
_assert("SCDM 已识别当前对象" in str(info.get("scdm_selection_status")), f"SCDM selection diagnostic missing: {info}")
|
||||
_assert("偏移" in str(info.get("scdm_selection_enabled_capabilities")), f"SCDM enabled capability diagnostic missing: {info}")
|
||||
|
||||
|
||||
def _assert_operation_record_backend_sources() -> None:
|
||||
class _OperationRecordProbe(WindowActionMixin):
|
||||
pass
|
||||
|
||||
stats = SimpleNamespace(solids=1, faces=6, edges=12)
|
||||
probe = _OperationRecordProbe()
|
||||
probe.current_info_values = {}
|
||||
internal_record = probe._make_operation_record(
|
||||
operation_name="拉伸/切除平面",
|
||||
target="Face 1",
|
||||
parameters={"surface": "plane"},
|
||||
result_message="Planar face push/pull completed: nearest_face=2, actual=10, target=10.",
|
||||
before_stats=stats,
|
||||
after_stats=stats,
|
||||
before_geometry={},
|
||||
after_geometry={},
|
||||
target_kind="face",
|
||||
target_id=1,
|
||||
target_logical_id=1,
|
||||
)
|
||||
_assert("backend: OCCT" in internal_record.detail, f"OCCT backend source missing: {internal_record.detail}")
|
||||
_assert("execution: Qt background worker" in internal_record.detail, f"OCCT execution source missing: {internal_record.detail}")
|
||||
_assert(
|
||||
"recognition: internal StepModel" in internal_record.detail,
|
||||
f"internal recognition source missing: {internal_record.detail}",
|
||||
)
|
||||
|
||||
asitus_record = probe._make_operation_record(
|
||||
operation_name="调整孔径",
|
||||
target="Face 87",
|
||||
parameters={
|
||||
"surface": "cylinder",
|
||||
"asitus_relation_status": "ready",
|
||||
"analysis_situs_feature_hint_summary": "Analysis Situs hint=hole",
|
||||
},
|
||||
result_message="Cylinder resize completed: verified_face=87.",
|
||||
before_stats=stats,
|
||||
after_stats=stats,
|
||||
before_geometry={},
|
||||
after_geometry={},
|
||||
target_kind="feature",
|
||||
target_id=87,
|
||||
target_logical_id=87,
|
||||
isolation={"operation": "resize_cylinder"},
|
||||
)
|
||||
_assert("backend: OCCT" in asitus_record.detail, f"OCCT backend source missing for Analysis Situs record: {asitus_record.detail}")
|
||||
_assert(
|
||||
"execution: isolated OCCT subprocess" in asitus_record.detail,
|
||||
f"isolated execution source missing: {asitus_record.detail}",
|
||||
)
|
||||
_assert(
|
||||
"recognition: Analysis Situs + internal StepModel" in asitus_record.detail,
|
||||
f"Analysis Situs recognition source missing: {asitus_record.detail}",
|
||||
)
|
||||
_assert(
|
||||
asitus_record.parameters and asitus_record.parameters.get("recognition_source") == "Analysis Situs + internal StepModel",
|
||||
f"recognition_source should be stored in record parameters: {asitus_record.parameters}",
|
||||
)
|
||||
|
||||
|
||||
def _assert_scdm_auto_prompt() -> None:
|
||||
probe = _ScdmAutoPromptProbe()
|
||||
probe.maybe_prompt_missing_scdm_backend(reason="probe-failed", message="script failed")
|
||||
QApplication.processEvents()
|
||||
_assert(not probe.prompt_calls, f"SCDM prompt should only open for missing backend: {probe.prompt_calls}")
|
||||
probe.maybe_prompt_missing_scdm_backend(reason="missing-spaceclaim", message="not found")
|
||||
QApplication.processEvents()
|
||||
_assert(probe.prompt_calls == [(True, "missing-spaceclaim", "not found")], f"SCDM prompt should open once for missing backend: {probe.prompt_calls}")
|
||||
probe.maybe_prompt_missing_scdm_backend(reason="missing-spaceclaim", message="still missing")
|
||||
QApplication.processEvents()
|
||||
_assert(len(probe.prompt_calls) == 1, f"SCDM prompt should be guarded against repeated popups: {probe.prompt_calls}")
|
||||
|
||||
|
||||
def _assert_property_ui_reroute_guards() -> None:
|
||||
probe = _PropertyUiRerouteProbe()
|
||||
probe._refresh_property_editor()
|
||||
probe._clear_property_editor()
|
||||
probe._update_current_capability_panel()
|
||||
probe._sync_scdm_selection_diagnostics()
|
||||
probe._update_relation_formula_completions()
|
||||
probe._update_property_apply_state()
|
||||
_assert(
|
||||
probe.rerouted_callbacks == 6,
|
||||
f"property UI entry points should reroute before touching widgets: {probe.rerouted_callbacks}",
|
||||
)
|
||||
|
||||
|
||||
def _function_text(path: Path, name: str) -> str:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
marker = f"def {name}"
|
||||
start = text.find(marker)
|
||||
_assert(start >= 0, f"missing function {name} in {path}")
|
||||
tail = text[start + len(marker) :]
|
||||
match = re.search(r"\n (?:@Slot\([^\n]*\)\n )?def ", tail)
|
||||
end = start + len(marker) + match.start() if match else len(text)
|
||||
return text[start:end]
|
||||
|
||||
|
||||
def _assert_worker_ui_callbacks_guarded() -> None:
|
||||
targets = {
|
||||
"step_editor/window_core.py": (
|
||||
"_finish_scdm_probe_preload",
|
||||
"_fail_scdm_probe_preload",
|
||||
"_finish_asitus_hole_recognition",
|
||||
"_fail_asitus_hole_recognition",
|
||||
"_finish_initial_load",
|
||||
"_fail_initial_load",
|
||||
"_finish_deferred_edge_display",
|
||||
"_fail_deferred_edge_display",
|
||||
"_finish_load_refine",
|
||||
"_fail_load_refine",
|
||||
),
|
||||
"step_editor/window_actions.py": (
|
||||
"_finish_scan_task_result",
|
||||
"_fail_scan_task_result",
|
||||
"_finish_edit_action",
|
||||
"_fail_edit_action",
|
||||
),
|
||||
"step_editor/window_state.py": (
|
||||
"_finish_scdm_edit_action",
|
||||
"_fail_scdm_edit_action",
|
||||
"_finish_pending_scdm_edit_reload",
|
||||
"_fail_pending_scdm_edit_reload",
|
||||
),
|
||||
}
|
||||
for relative_path, names in targets.items():
|
||||
path = PROJECT_ROOT / relative_path
|
||||
for name in names:
|
||||
body = _function_text(path, name)
|
||||
header = body[:420]
|
||||
_assert(
|
||||
"_reroute_to_ui_thread" in header or "_is_ui_thread" in header,
|
||||
f"{relative_path}:{name} should reroute to the UI thread before touching widgets",
|
||||
)
|
||||
|
||||
|
||||
def _assert_large_model_preload_stays_lightweight() -> None:
|
||||
model_body = _function_text(PROJECT_ROOT / "step_editor/model.py", "scdm_local_face_signatures")
|
||||
_assert("quick_face_info" not in model_body, "SCDM local Face signatures must not run full Face recognition")
|
||||
_assert("SurfaceProperties" not in model_body, "SCDM local Face signatures should avoid full area/center integration")
|
||||
quick_cylinder_body = _function_text(PROJECT_ROOT / "step_editor/model.py", "_quick_cylindrical_feature_hint")
|
||||
internal_graph_body = _function_text(PROJECT_ROOT / "step_editor/model.py", "_can_use_internal_recognition_graph")
|
||||
_assert(
|
||||
"len(self.faces) <= 1000" in internal_graph_body,
|
||||
"large models should not synchronously build the internal full recognition graph",
|
||||
)
|
||||
_assert(
|
||||
"connected_same_domain_face_ids" not in quick_cylinder_body,
|
||||
"quick cylinder selection must not trigger full same-domain/recognition graph expansion",
|
||||
)
|
||||
_assert(
|
||||
"_connected_cocylindrical_face_ids" in quick_cylinder_body,
|
||||
"quick cylinder selection should only merge directly connected co-cylindrical fragments",
|
||||
)
|
||||
window_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_scdm_local_face_signatures")
|
||||
_assert("scdm_local_face_signatures" in window_body, "window SCDM preload should use the lightweight model signature API")
|
||||
_assert("quick_face_info" not in window_body, "window SCDM preload must not call quick_face_info for every Face")
|
||||
loaded_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_apply_loaded_model_result")
|
||||
_assert(
|
||||
"_defer_large_model_recognition_preloads" in loaded_body,
|
||||
"large model loads should defer full external recognition preloads by default",
|
||||
)
|
||||
_assert(
|
||||
"_start_scdm_probe_preload(force=pending_scdm_reload)" in loaded_body,
|
||||
"SCDM edit-result reloads should still be able to force cache refresh for validation",
|
||||
)
|
||||
preload_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_start_scdm_probe_preload")
|
||||
_assert(
|
||||
"local_face_signatures = self._scdm_local_face_signatures()" not in preload_body,
|
||||
"SCDM preload must not build all local Face signatures on the UI thread",
|
||||
)
|
||||
_assert(
|
||||
"force: bool = False" in preload_body
|
||||
and "_large_model_interaction_mode()" in preload_body
|
||||
and "_defer_large_model_recognition_preloads()" in preload_body,
|
||||
"large-model SCDM preload should be deferred unless a validation path forces it",
|
||||
)
|
||||
_assert(
|
||||
'builder = getattr(model, "scdm_local_face_signatures", None)' in preload_body,
|
||||
"SCDM preload should build local Face signatures inside the background worker",
|
||||
)
|
||||
status_body = _function_text(PROJECT_ROOT / "step_editor/scdm_status.py", "summarize_scdm_runtime")
|
||||
_assert('state == "deferred"' in status_body, "SCDM status should explain deferred large-model recognition")
|
||||
|
||||
|
||||
def _assert_large_model_selection_stays_lightweight() -> None:
|
||||
selection_body = _function_text(PROJECT_ROOT / "step_editor/window_state.py", "_feature_info_for_selected_face")
|
||||
_assert(
|
||||
"_should_defer_selection_first_level_topology" in selection_body,
|
||||
"large-model selection should defer full first-level topology expansion",
|
||||
)
|
||||
level_body = _function_text(PROJECT_ROOT / "step_editor/window_state.py", "_selection_feature_detection_level")
|
||||
_assert(
|
||||
"_large_model_interaction_mode" in level_body and '"current-only"' in level_body,
|
||||
"large-model feature clicks should force the quick current-feature detection level",
|
||||
)
|
||||
defer_body = _function_text(PROJECT_ROOT / "step_editor/window_state.py", "_should_defer_selection_first_level_topology")
|
||||
_assert(
|
||||
"return True" in defer_body,
|
||||
"large-model selection should defer first-level topology even when the combo requests deeper detection",
|
||||
)
|
||||
_assert(
|
||||
"_quick_face_first_level_selection_fields" in selection_body,
|
||||
"large-model selection should use a quick first-level summary",
|
||||
)
|
||||
_assert(
|
||||
"connected_same_domain_face_ids" not in selection_body
|
||||
and "_connected_cocylindrical_face_ids" in selection_body,
|
||||
"large-model feature selection should not trigger full same-domain expansion for cylinders",
|
||||
)
|
||||
quick_body = _function_text(PROJECT_ROOT / "step_editor/window_state.py", "_quick_face_first_level_selection_fields")
|
||||
_assert(
|
||||
"face_first_level_topology" not in quick_body and "face_first_level_facts" not in quick_body,
|
||||
"quick large-model selection summary must not run full topology/fact graph builders",
|
||||
)
|
||||
loaded_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_apply_loaded_model_result")
|
||||
_assert("large_interaction_model" in loaded_body, "large models should be detected after load")
|
||||
_assert(
|
||||
"hide_edges_during_camera_interaction" in loaded_body,
|
||||
"large models should hide edge overlay during camera interaction",
|
||||
)
|
||||
_assert(
|
||||
"large_model_edge_overlay_skipped = True" in loaded_body,
|
||||
"large models should skip deferred full-edge overlay during default viewing",
|
||||
)
|
||||
_assert(
|
||||
"large_model_hover_disabled = large_interaction_model" in loaded_body,
|
||||
"large models should disable hover picking/highlighting to avoid pointer stalls",
|
||||
)
|
||||
rebuild_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_rebuild_scene")
|
||||
_assert(
|
||||
"_empty_edge_polydata()" in rebuild_body and "large_model_edge_overlay_skipped" in rebuild_body,
|
||||
"large-model scene rebuilds should keep edge overlay lazy by default",
|
||||
)
|
||||
mode_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_on_mode_changed")
|
||||
_assert(
|
||||
"_rebuild_deferred_edge_display" in mode_body and '"Edge"' in mode_body,
|
||||
"Edge selection mode should restore deferred edge display on demand",
|
||||
)
|
||||
hover_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_queue_hover_position")
|
||||
_assert(
|
||||
"large_model_hover_disabled" in hover_body,
|
||||
"large-model hover picking should be suppressed before the VTK picker runs",
|
||||
)
|
||||
action_body = _function_text(PROJECT_ROOT / "step_editor/window_actions.py", "_isolation_for_plan")
|
||||
_assert(
|
||||
"_prefer_isolated_process_for_large_interactive_edit" in action_body
|
||||
and "large-model-smooth-ui-isolated-occ-edit" in action_body,
|
||||
"large complex push/pull rebuilds should use an independent background process for smoother interaction",
|
||||
)
|
||||
job_body = _function_text(PROJECT_ROOT / "step_editor/window_actions.py", "_make_edit_job")
|
||||
_assert(
|
||||
"skip_before_quality_check" in job_body,
|
||||
"large-model edit jobs should be able to skip the pre-edit full B-Rep check while preserving post-edit validation",
|
||||
)
|
||||
finish_body = _function_text(PROJECT_ROOT / "step_editor/window_actions.py", "_finish_edit_action")
|
||||
_assert(
|
||||
"large_model_edge_overlay_skipped" in finish_body
|
||||
and 'not bool(getattr(self, "large_model_edge_overlay_skipped", False))' in finish_body,
|
||||
"large-model edit finish should not automatically rebuild full edge overlay",
|
||||
)
|
||||
|
||||
|
||||
def _assert_large_planar_offset_prefers_local_backend() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.model = _LargeDisplayModel()
|
||||
probe.selected_kind = "feature"
|
||||
probe.selected_face_id = 0
|
||||
probe.scdm_feature_cache_state = "ready"
|
||||
probe.scdm_feature_cache = {
|
||||
"objects": [
|
||||
{
|
||||
"objectId": "face:0",
|
||||
"objectType": "face",
|
||||
"geometrySignature": {"objectType": "face", "faceIds": [0], "surfaceType": "plane"},
|
||||
"capabilities": [
|
||||
{
|
||||
"key": "face.offset",
|
||||
"displayName": "偏移",
|
||||
"currentValue": 57.5,
|
||||
"editable": True,
|
||||
"defaultIntent": "推拉平面",
|
||||
"backendOperation": "pull_face_offset",
|
||||
"postCheck": "target_face_offset",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
action_info = probe._selected_action_info()
|
||||
action_info.update({"inner_boundary_wires": 5, "boundary_edges": 61})
|
||||
specs = probe._property_editor_specs(action_info, action_info)
|
||||
_assert(specs, "large planar Face should still expose local editable specs")
|
||||
_assert(
|
||||
all(str(spec.get("action") or "") != "apply_scdm_property_edit" for spec in specs),
|
||||
f"large multi-boundary planar offset should not route to SCDM: {specs}",
|
||||
)
|
||||
offset = next((spec for spec in specs if str(spec.get("key") or "") == "face_target_normal_position"), None)
|
||||
_assert(isinstance(offset, dict), f"local Face offset spec should be present: {specs}")
|
||||
_assert(
|
||||
str(offset.get("action") or "") in {"push_pull_face", "push_pull_face_keep_relations"},
|
||||
f"local Face offset should use optimized OCCT path: {offset}",
|
||||
)
|
||||
_assert("SCDM" in str(probe.scdm_selection_status_message), "backend preference should explain that SCDM was bypassed")
|
||||
|
||||
|
||||
def _assert_background_load_uses_worker() -> None:
|
||||
body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_load_step_background_or_sync")
|
||||
_assert("LoadWorker(action)" in body, "background STEP load should run through LoadWorker")
|
||||
_assert("worker.moveToThread(thread)" in body, "background STEP load worker should move to QThread")
|
||||
_assert("_finish_initial_load" in body and "_fail_initial_load" in body, "background STEP load should finish through queued callbacks")
|
||||
_assert(
|
||||
"_run_deferred_initial_load(path)" not in body,
|
||||
"background STEP load must not fall back to main-thread deferred loading",
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
app = QApplication.instance() or QApplication([])
|
||||
top_level_label_probe = _TopLevelPropertyLabelProbe()
|
||||
@@ -1406,13 +1892,55 @@ def main() -> int:
|
||||
f"property labels were shown as transient top-level windows: {top_level_label_probe.shown_labels}",
|
||||
)
|
||||
_assert_property_table_editor(probe)
|
||||
_assert("当前支持" in probe.current_capability_headline.text(), "software progress panel did not show supported areas")
|
||||
_assert("优先:" not in probe.current_capability_headline.text(), "software progress panel should not show priority copy")
|
||||
_assert(probe.current_capability_button.text() == "软件进度", "software progress should be a compact button")
|
||||
_assert("当前支持" in probe.current_capability_button.toolTip(), "software progress button tooltip did not show supported areas")
|
||||
_assert("优先:" not in probe.current_capability_button.toolTip(), "software progress button tooltip should not show priority copy")
|
||||
_assert(
|
||||
"矩形槽口袋" in probe.current_capability_headline.toolTip()
|
||||
and "多台阶矩形凸台顶层" in probe.current_capability_headline.toolTip(),
|
||||
"software progress tooltip should name newly supported prismatic feature edits",
|
||||
"能改:" in probe.current_capability_button.toolTip()
|
||||
and "能识别:" in probe.current_capability_button.toolTip()
|
||||
and "暂不能:" in probe.current_capability_button.toolTip(),
|
||||
f"software progress tooltip should be customer-facing capability copy: {probe.current_capability_button.toolTip()}",
|
||||
)
|
||||
_assert("当前 cache" not in probe.current_capability_button.toolTip(), "software progress tooltip should hide cache internals")
|
||||
_assert("SCDM 能力:" not in probe.current_capability_button.toolTip(), "software progress tooltip should hide SCDM internals")
|
||||
_assert(not hasattr(probe, "configure_scdm_button"), "software progress should not expose a persistent SCDM configure button")
|
||||
progress_detail = probe._software_progress_detail_text()
|
||||
_assert("# 软件进度" in progress_detail, "software progress dialog text should be Markdown-like")
|
||||
_assert("## 已能修改" in progress_detail, "software progress dialog text should list editable capabilities first")
|
||||
_assert("## 已能识别" in progress_detail, "software progress dialog text should list recognized capabilities")
|
||||
_assert("## 暂不能修改" in progress_detail, "software progress dialog text should list unsupported edits")
|
||||
_assert("## 暂不能稳定识别" in progress_detail, "software progress dialog text should list unsupported recognition")
|
||||
_assert("孔组" in progress_detail and "一级关系" in progress_detail, "software progress dialog text should include recognition scope")
|
||||
_assert("B-Rep 校验" in progress_detail and "原 CAD 历史树" in progress_detail, "software progress dialog text should explain limits")
|
||||
_assert("SCDM-first 路线状态" not in progress_detail, "software progress dialog text should hide roadmap internals")
|
||||
_assert("SCDM 能力报告" not in progress_detail, "software progress dialog text should hide dynamic SCDM internals")
|
||||
probe.scdm_backend_status = {
|
||||
"path": "D:/softwaresInstallDir/ANSYS Inc/v222/SCDM/SpaceClaim.exe",
|
||||
"source": "common:D:/softwaresInstallDir/ANSYS Inc",
|
||||
"version": "v222",
|
||||
"runScriptOk": True,
|
||||
"licenseOk": True,
|
||||
}
|
||||
probe.scdm_feature_cache_state = "ready"
|
||||
probe.scdm_feature_cache = {
|
||||
"objects": [
|
||||
{"objectId": "face:1", "capabilities": [{"key": "face.offset"}]},
|
||||
{"objectId": "hole:1", "capabilities": [{"key": "hole.diameter"}, {"key": "hole.position"}]},
|
||||
],
|
||||
"diagnostics": {
|
||||
"geometry_candidate_hints": [
|
||||
{"capabilityKey": "boss.height", "displayName": "凸台高度", "evidenceCount": 2, "confidence": "low"}
|
||||
]
|
||||
},
|
||||
}
|
||||
probe._update_current_capability_panel()
|
||||
configured_detail = probe._software_progress_detail_text()
|
||||
_assert("已配置 v222" not in configured_detail, f"SCDM progress detail should hide backend version: {configured_detail}")
|
||||
_assert("当前 cache 可执行" not in configured_detail, f"SCDM progress detail should hide cache count: {configured_detail}")
|
||||
_assert("2 个对象" not in probe.current_capability_button.toolTip(), f"SCDM cache status should stay out of tooltip: {probe.current_capability_button.toolTip()}")
|
||||
_assert("SCDM 能力:" not in probe.current_capability_button.toolTip(), f"SCDM progress tooltip should hide capability counters: {probe.current_capability_button.toolTip()}")
|
||||
_assert("几何证据" not in probe.current_capability_button.toolTip(), f"SCDM progress tooltip should hide geometry hint counts: {probe.current_capability_button.toolTip()}")
|
||||
_assert("凸台高度:几何证据待分类" not in configured_detail, f"SCDM detail should hide geometry-only hints: {configured_detail}")
|
||||
|
||||
_assert_diagnostics_stay_out_of_parameter_table(probe)
|
||||
_assert_relation_formula_editor()
|
||||
@@ -1423,9 +1951,19 @@ def main() -> int:
|
||||
_assert_relation_formula_input_recovers_after_loading()
|
||||
_assert_relation_formula_ids_follow_model_remap()
|
||||
_assert_mouse_selection_guards()
|
||||
_assert_scdm_selection_diagnostics()
|
||||
_assert_operation_record_backend_sources()
|
||||
_assert_scdm_auto_prompt()
|
||||
_assert_property_ui_reroute_guards()
|
||||
_assert_worker_ui_callbacks_guarded()
|
||||
_assert_large_model_preload_stays_lightweight()
|
||||
_assert_large_model_selection_stays_lightweight()
|
||||
_assert_large_planar_offset_prefers_local_backend()
|
||||
_assert_background_load_uses_worker()
|
||||
_assert_quick_blind_depth_spec()
|
||||
_assert_user_facing_failure_messages()
|
||||
_assert_parameter_export_action()
|
||||
_assert_scdm_command_row_uses_unified_apply()
|
||||
|
||||
print("property table editor UI ok")
|
||||
if QApplication.instance() is app:
|
||||
|
||||
Reference in New Issue
Block a user