feat: 推进SCDM-first后端接入和大模型编辑优化

This commit is contained in:
2026-08-19 10:28:09 +08:00
parent 722256a41f
commit a3eb7e2476
28 changed files with 9666 additions and 168 deletions
+544 -6
View File
@@ -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: