feat: 完善 SCDM-first 参数化编辑交付版
This commit is contained in:
@@ -37,6 +37,8 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
|
||||
from step_editor.widgets import NoWheelComboBox
|
||||
from step_editor.relation_formulas import ObjectParameterRef, parse_relation_formula
|
||||
from step_editor.scdm_feature_mapper import SCDM_FEATURE_CACHE_REVISION
|
||||
from step_editor.scdm_schema import file_fingerprint
|
||||
from step_editor.window_actions import WindowActionMixin
|
||||
from step_editor.window_core import WindowCoreMixin
|
||||
from step_editor.window_state import (
|
||||
@@ -103,6 +105,8 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
"feature.delete_round_or_chamfer",
|
||||
"pattern.spacing",
|
||||
"pattern.segment_spacing",
|
||||
"pattern.instance_position",
|
||||
"shell.thickness",
|
||||
}
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
@@ -142,6 +146,9 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
|
||||
self.relation_formula_input.textChanged.connect(self._on_relation_formula_input_changed)
|
||||
layout.addWidget(self.relation_formula_input)
|
||||
self.add_relation_formula_button = QPushButton()
|
||||
self.import_relation_formula_button = QPushButton()
|
||||
self.export_relation_formula_button = QPushButton()
|
||||
self.toggle_relation_formula_button = QPushButton()
|
||||
self.remove_relation_formula_button = QPushButton()
|
||||
self.relation_formula_list = QListWidget()
|
||||
self.face_width_input = QLineEdit()
|
||||
@@ -1043,6 +1050,142 @@ def _assert_relation_formula_input_clickable_after_existing_formula() -> None:
|
||||
_assert(probe.add_relation_formula_button.isEnabled(), "clickable input should allow adding another formula")
|
||||
|
||||
|
||||
def _assert_relation_formula_can_toggle_without_deleting() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe._refresh_property_editor()
|
||||
probe.relation_formula_input.setText("Face0.面内宽度 = Face0.面内长度 * 1.2")
|
||||
probe.add_relation_formula()
|
||||
for _index in range(12):
|
||||
QApplication.processEvents()
|
||||
if not getattr(probe, "relation_formula_replay_active", False):
|
||||
break
|
||||
_assert(len(probe.relation_formula_items) == 1, f"formula should exist before toggling: {probe.relation_formula_items}")
|
||||
_assert(probe.relation_formula_list.count() == 1, "formula list should show the formula before toggling")
|
||||
probe.relation_formula_list.setCurrentRow(0)
|
||||
probe._update_relation_formula_buttons()
|
||||
_assert(probe.toggle_relation_formula_button.isEnabled(), "selected formula should enable the toggle button")
|
||||
_assert(probe.toggle_relation_formula_button.text() == "停用公式", f"enabled formula should offer disable action: {probe.toggle_relation_formula_button.text()}")
|
||||
|
||||
restore_calls = {"count": 0}
|
||||
|
||||
def _restore_base() -> bool:
|
||||
restore_calls["count"] += 1
|
||||
return True
|
||||
|
||||
probe.relation_formula_base_snapshot = {"baseline": True}
|
||||
probe._restore_relation_formula_base_snapshot = _restore_base
|
||||
probe.toggle_selected_relation_formula()
|
||||
_assert(restore_calls["count"] == 1, "disabling the last active formula should restore the formula base snapshot")
|
||||
_assert(probe.relation_formula_items[0].get("enabled") is False, f"formula should be disabled, not deleted: {probe.relation_formula_items}")
|
||||
_assert("停用" in probe.relation_formula_list.item(0).text(), f"disabled formula should be visibly marked: {probe.relation_formula_list.item(0).text()}")
|
||||
_assert(not getattr(probe, "relation_formula_replay_active", False), "disabling all formulas should not leave replay active")
|
||||
probe.relation_formula_list.setCurrentRow(0)
|
||||
probe._update_relation_formula_buttons()
|
||||
_assert(probe.toggle_relation_formula_button.text() == "启用公式", f"disabled formula should offer enable action: {probe.toggle_relation_formula_button.text()}")
|
||||
|
||||
probe.toggle_selected_relation_formula()
|
||||
for _index in range(12):
|
||||
QApplication.processEvents()
|
||||
if not getattr(probe, "relation_formula_replay_active", False):
|
||||
break
|
||||
_assert(probe.relation_formula_items[0].get("enabled") is True, f"formula should be re-enabled: {probe.relation_formula_items}")
|
||||
_assert(str(probe.relation_formula_items[0].get("status")) in {"applied", "ready"}, f"enabled formula should return to a computable state: {probe.relation_formula_items}")
|
||||
probe.relation_formula_list.setCurrentRow(0)
|
||||
probe._update_relation_formula_buttons()
|
||||
_assert(probe.toggle_relation_formula_button.text() == "停用公式", "re-enabled formula should offer disable action again")
|
||||
|
||||
|
||||
def _assert_relation_formula_import_export() -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
probe = _PropertyTableProbe()
|
||||
probe.relation_formula_items = [
|
||||
{
|
||||
"id": 7,
|
||||
"text": "Face87.直径 = Face85.半径",
|
||||
"enabled": True,
|
||||
"status": "applied",
|
||||
"message": "已修改模型。",
|
||||
}
|
||||
]
|
||||
export_path = root / "relation_formulas.json"
|
||||
_assert(probe.export_relation_formulas(export_path), "relation formula export should succeed")
|
||||
payload = json.loads(export_path.read_text(encoding="utf-8"))
|
||||
_assert(payload.get("schema") == "python-occt.relation-formulas.v1", f"export schema missing: {payload}")
|
||||
formulas = payload.get("formulas")
|
||||
_assert(isinstance(formulas, list) and len(formulas) == 1, f"export should contain one formula: {payload}")
|
||||
_assert(
|
||||
formulas[0].get("text") == "Face87.直径 = Face85.半径" and formulas[0].get("enabled") is True,
|
||||
f"exported formula row is wrong: {formulas}",
|
||||
)
|
||||
|
||||
import_path = root / "import_formulas.json"
|
||||
import_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"formulas": [
|
||||
{"text": "Face87.直径 = 10mm", "enabled": True},
|
||||
{"text": "Face85.直径 = Face87.半径", "enabled": False},
|
||||
]
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
reapply_calls: list[dict[str, object]] = []
|
||||
probe._start_relation_formula_reapply = lambda **kwargs: reapply_calls.append(dict(kwargs))
|
||||
_assert(probe.import_relation_formulas(import_path), "relation formula import should succeed")
|
||||
_assert(len(probe.relation_formula_items) == 2, f"import should replace the formula set: {probe.relation_formula_items}")
|
||||
_assert(probe.relation_formula_items[0].get("text") == "Face87.直径 = 10mm", f"import should normalize first formula: {probe.relation_formula_items}")
|
||||
_assert(probe.relation_formula_items[1].get("enabled") is False, f"import should preserve disabled state: {probe.relation_formula_items}")
|
||||
_assert(reapply_calls and reapply_calls[-1].get("reason") == "import", f"enabled imported formulas should trigger reapply: {reapply_calls}")
|
||||
|
||||
previous_items = [dict(item) for item in probe.relation_formula_items]
|
||||
invalid_path = root / "invalid_formulas.json"
|
||||
invalid_path.write_text(
|
||||
json.dumps({"formulas": ["Face1.直径 = 1", "Face1.直径 = 2"]}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
_assert(not probe.import_relation_formulas(invalid_path), "invalid formula groups should be rejected")
|
||||
_assert(probe.relation_formula_items == previous_items, "failed import should not alter the current formula set")
|
||||
|
||||
|
||||
def _assert_scdm_first_holds_ambiguous_large_cylinders() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.selected_kind = "feature"
|
||||
probe.selected_face_id = 1722
|
||||
probe.scdm_feature_cache = None
|
||||
probe.scdm_feature_cache_state = "deferred"
|
||||
probe._large_model_interaction_mode = lambda: True
|
||||
|
||||
def _cylinder_action_info() -> dict[str, object]:
|
||||
return {
|
||||
"surface": "cylinder",
|
||||
"feature_guess": "hole/groove candidate",
|
||||
"feature_type": "圆柱孔候选",
|
||||
"diameter": 2.6,
|
||||
"radius": 1.3,
|
||||
"axis": (0.0, 1.0, 0.0),
|
||||
"axis_point": (-83.0, -31.0, -116.35),
|
||||
"part_id": 1,
|
||||
"solid_id": 0,
|
||||
}
|
||||
|
||||
probe.current_info_values = _cylinder_action_info()
|
||||
probe._selected_action_info = _cylinder_action_info
|
||||
probe._refresh_property_editor()
|
||||
labels = [
|
||||
probe.property_table.item(row, PROPERTY_LABEL_COLUMN).text()
|
||||
for row in range(probe.property_table.rowCount())
|
||||
if probe.property_table.item(row, PROPERTY_LABEL_COLUMN) is not None
|
||||
]
|
||||
_assert(not labels, f"SCDM-first large-model cylinder should not expose local hole parameters while cache is deferred: {labels}")
|
||||
_assert(
|
||||
"不开放本地兜底孔/槽参数" in str(getattr(probe, "scdm_selection_status_message", "")),
|
||||
f"SCDM-first hold should explain why local cylinder specs are hidden: {getattr(probe, 'scdm_selection_status_message', '')}",
|
||||
)
|
||||
|
||||
|
||||
def _assert_relation_formula_input_is_selection_independent() -> None:
|
||||
probe = _RelationFormulaEventProbe()
|
||||
probe.relation_formula_input.installEventFilter(probe)
|
||||
@@ -1577,6 +1720,191 @@ def _assert_scdm_selection_diagnostics() -> None:
|
||||
_assert("偏移" in str(info.get("scdm_selection_enabled_capabilities")), f"SCDM enabled capability diagnostic missing: {info}")
|
||||
|
||||
|
||||
def _assert_scdm_parameter_table_shows_only_enabled_specs() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.selected_kind = "face"
|
||||
probe.selected_face_id = 31
|
||||
probe.scdm_feature_cache_state = "ready"
|
||||
probe.scdm_feature_cache = {
|
||||
"objects": [
|
||||
{
|
||||
"objectId": "slot:31",
|
||||
"objectType": "slot",
|
||||
"geometrySignature": {"faceIds": [31], "objectType": "slot"},
|
||||
"capabilities": [
|
||||
{
|
||||
"key": "slot.width",
|
||||
"displayName": "槽宽",
|
||||
"currentValue": 2.0,
|
||||
"valueKind": "positive",
|
||||
"defaultIntent": "改槽宽",
|
||||
"backendOperation": "change_slot_width",
|
||||
"postCheck": "target_slot_width",
|
||||
},
|
||||
{
|
||||
"key": "slot.depth",
|
||||
"displayName": "槽深",
|
||||
"currentValue": 1.5,
|
||||
"valueKind": "positive",
|
||||
"defaultIntent": "改槽深",
|
||||
"backendOperation": "change_slot_depth",
|
||||
"postCheck": "target_slot_depth",
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def labels() -> list[str]:
|
||||
return [
|
||||
probe.property_table.item(row, PROPERTY_LABEL_COLUMN).text()
|
||||
for row in range(probe.property_table.rowCount())
|
||||
if probe.property_table.item(row, PROPERTY_LABEL_COLUMN) is not None
|
||||
]
|
||||
|
||||
probe.scdm_edit_runner_ready = set()
|
||||
probe._refresh_property_editor()
|
||||
disabled_labels = labels()
|
||||
_assert("槽宽" not in disabled_labels and "槽深" not in disabled_labels, f"disabled SCDM specs should stay out of the parameter table: {disabled_labels}")
|
||||
_assert(
|
||||
"当前能力未开放" in str(probe.current_info_values.get("scdm_selection_status") or ""),
|
||||
f"disabled SCDM specs should remain visible as a diagnostic reason: {probe.current_info_values}",
|
||||
)
|
||||
|
||||
probe.scdm_edit_runner_ready = {"slot.width", "slot.depth"}
|
||||
probe._refresh_property_editor()
|
||||
enabled_labels = labels()
|
||||
_assert(enabled_labels == ["槽宽", "槽深"], f"enabled SCDM specs should replace local fallback rows: {enabled_labels}")
|
||||
_assert(
|
||||
all(bool(spec.get("enabled")) for spec in probe.property_editor_specs),
|
||||
f"property table should only hold executable SCDM rows: {probe.property_editor_specs}",
|
||||
)
|
||||
|
||||
|
||||
def _assert_ambiguous_slot_empty_state_explains_missing_params() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.selected_kind = "feature"
|
||||
probe.selected_face_id = 1362
|
||||
probe.scdm_feature_cache_state = "ready"
|
||||
probe.scdm_feature_cache = {"objects": []}
|
||||
slot_candidate_info = {
|
||||
"surface": "cylinder",
|
||||
"feature_guess": "hole/groove candidate",
|
||||
"feature_type": "圆柱孔/槽候选",
|
||||
"recognition_confidence": "medium",
|
||||
"recognition_score": 62,
|
||||
"diameter": 2.6,
|
||||
"radius": 1.3,
|
||||
"angular_span": 3.141592653589793,
|
||||
"part_id": 1,
|
||||
"solid_id": 0,
|
||||
}
|
||||
probe.current_info_values = dict(slot_candidate_info)
|
||||
probe._selected_action_info = lambda: dict(slot_candidate_info)
|
||||
probe._refresh_property_editor()
|
||||
labels = [
|
||||
probe.property_table.item(row, PROPERTY_LABEL_COLUMN).text()
|
||||
for row in range(probe.property_table.rowCount())
|
||||
if probe.property_table.item(row, PROPERTY_LABEL_COLUMN) is not None
|
||||
]
|
||||
_assert("槽宽" not in labels and "槽深" not in labels, f"ambiguous slot candidates should not expose fake slot params: {labels}")
|
||||
_assert("可修改参数" in labels, f"ambiguous slot should keep an empty-state row: {labels}")
|
||||
status = str(probe.current_info_values.get("scdm_selection_status") or "")
|
||||
_assert(
|
||||
"圆柱孔/槽候选" in status and "证据完整" in status,
|
||||
f"ambiguous slot empty state should explain the missing editable evidence: {probe.current_info_values}",
|
||||
)
|
||||
|
||||
|
||||
def _assert_scdm_round_delete_conflict_filtered_for_blind_pocket() -> None:
|
||||
def round_delete_cache(face_id: int) -> dict[str, object]:
|
||||
return {
|
||||
"objects": [
|
||||
{
|
||||
"objectId": f"round:{face_id}",
|
||||
"objectType": "round",
|
||||
"geometrySignature": {"faceIds": [face_id], "surfaceType": "cylinder", "radius": 1.3},
|
||||
"capabilities": [
|
||||
{
|
||||
"key": "feature.delete_round_or_chamfer",
|
||||
"displayName": "删除圆角/倒角",
|
||||
"currentValue": 1,
|
||||
"valueKind": "command",
|
||||
"editable": True,
|
||||
"defaultIntent": "删除圆角/倒角并补面",
|
||||
"backendOperation": "delete_round_or_chamfer",
|
||||
"postCheck": "target_feature_removed",
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
probe = _PropertyTableProbe()
|
||||
probe.selected_kind = "feature"
|
||||
probe.selected_face_id = 1360
|
||||
probe.scdm_feature_cache_state = "ready"
|
||||
probe.scdm_edit_runner_ready = {"feature.delete_round_or_chamfer"}
|
||||
probe.scdm_feature_cache = round_delete_cache(1360)
|
||||
pocket_info = {
|
||||
"surface": "cylinder",
|
||||
"feature_guess": "hole/groove candidate",
|
||||
"feature_type": "圆柱孔/槽候选",
|
||||
"blind_split_cylindrical_pocket": True,
|
||||
"diameter": 2.6,
|
||||
"radius": 1.3,
|
||||
"part_id": 1,
|
||||
"solid_id": 0,
|
||||
}
|
||||
probe.current_info_values = dict(pocket_info)
|
||||
probe._selected_action_info = lambda: dict(pocket_info)
|
||||
probe._refresh_property_editor()
|
||||
labels = [
|
||||
probe.property_table.item(row, PROPERTY_LABEL_COLUMN).text()
|
||||
for row in range(probe.property_table.rowCount())
|
||||
if probe.property_table.item(row, PROPERTY_LABEL_COLUMN) is not None
|
||||
]
|
||||
_assert("删除圆角/倒角" not in labels, f"blind pocket should not expose round/chamfer delete: {labels}")
|
||||
_assert(not labels, f"blind pocket SCDM conflict should not fall back to misleading local rows: {labels}")
|
||||
_assert(
|
||||
probe.current_info_values.get("scdm_selection_capability_count") == 0,
|
||||
f"filtered SCDM conflict should not remain in diagnostic capability count: {probe.current_info_values}",
|
||||
)
|
||||
_assert(
|
||||
probe.current_info_values.get("scdm_selection_blocked_count") == 0,
|
||||
f"filtered SCDM conflict should not remain in blocked diagnostics: {probe.current_info_values}",
|
||||
)
|
||||
_assert(
|
||||
"已隐藏该冲突操作" in str(probe.current_info_values.get("scdm_selection_status") or ""),
|
||||
f"filtered SCDM conflict should be explained in diagnostics: {probe.current_info_values}",
|
||||
)
|
||||
|
||||
round_probe = _PropertyTableProbe()
|
||||
round_probe.selected_kind = "feature"
|
||||
round_probe.selected_face_id = 1722
|
||||
round_probe.scdm_feature_cache_state = "ready"
|
||||
round_probe.scdm_edit_runner_ready = {"feature.delete_round_or_chamfer"}
|
||||
round_probe.scdm_feature_cache = round_delete_cache(1722)
|
||||
round_info = {
|
||||
"surface": "cylinder",
|
||||
"feature_guess": "round/fillet candidate",
|
||||
"feature_type": "圆角/倒圆候选",
|
||||
"existing_fillet_status": "candidate",
|
||||
"radius": 1.3,
|
||||
"part_id": 1,
|
||||
"solid_id": 0,
|
||||
}
|
||||
round_probe.current_info_values = dict(round_info)
|
||||
round_probe._selected_action_info = lambda: dict(round_info)
|
||||
round_probe._refresh_property_editor()
|
||||
round_labels = [
|
||||
round_probe.property_table.item(row, PROPERTY_LABEL_COLUMN).text()
|
||||
for row in range(round_probe.property_table.rowCount())
|
||||
if round_probe.property_table.item(row, PROPERTY_LABEL_COLUMN) is not None
|
||||
]
|
||||
_assert("删除圆角/倒角" in round_labels, f"real round candidates should keep SCDM delete capability: {round_labels}")
|
||||
|
||||
|
||||
def _assert_solid_selection_does_not_expand_face_scdm_specs() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.scdm_feature_cache_state = "ready"
|
||||
@@ -1799,21 +2127,35 @@ def _assert_large_model_preload_stays_lightweight() -> None:
|
||||
"STEP load should restore a matching SCDM disk cache before launching a new probe",
|
||||
)
|
||||
_assert(
|
||||
"_defer_large_model_recognition_preloads" in loaded_body,
|
||||
"large model loads should defer full external recognition preloads by default",
|
||||
"_defer_post_import_recognition_preloads" in loaded_body
|
||||
and "_start_asitus_hole_recognition_preload" not in loaded_body,
|
||||
"ordinary STEP import should display the model first and defer external recognition until selection",
|
||||
)
|
||||
_assert(
|
||||
"_start_scdm_probe_preload(force=pending_scdm_reload)" in loaded_body,
|
||||
"_start_scdm_probe_preload(force=True)" in loaded_body,
|
||||
"SCDM edit-result reloads should still be able to force cache refresh for validation",
|
||||
)
|
||||
restore_cache_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_restore_scdm_feature_cache_from_disk")
|
||||
_assert(
|
||||
"scdm_raw_features.json" not in restore_cache_body
|
||||
and "attach_local_face_ids_to_scdm_cache" not in restore_cache_body
|
||||
and "_scdm_local_face_signatures()" not in restore_cache_body,
|
||||
"STEP import should not rebuild SCDM Face mappings from raw cache during the first display path",
|
||||
)
|
||||
preload_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_start_scdm_probe_preload")
|
||||
_assert(
|
||||
"_current_scdm_feature_cache_matches_loaded_step()" in preload_body,
|
||||
"SCDM preload should skip relaunching SpaceClaim when a current cache is already installed",
|
||||
)
|
||||
_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",
|
||||
"model=model" not in preload_body
|
||||
and 'getattr(model, "scdm_local_face_signatures"' not in preload_body
|
||||
and "getattr(model, 'scdm_local_face_signatures'" not in preload_body,
|
||||
"SCDM preload worker must not keep or inspect the current UI StepModel across threads",
|
||||
)
|
||||
_assert(
|
||||
"face_signatures=tuple(local_face_signatures)" in preload_body,
|
||||
"SCDM preload should pass a copied local Face signature snapshot into the worker",
|
||||
)
|
||||
_assert(
|
||||
"force: bool = False" in preload_body
|
||||
@@ -1822,8 +2164,8 @@ def _assert_large_model_preload_stays_lightweight() -> None:
|
||||
"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",
|
||||
'builder = getattr(model, "scdm_local_face_signatures", None)' not in preload_body,
|
||||
"SCDM preload should not build local Face signatures from the current UI model 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")
|
||||
@@ -1888,6 +2230,15 @@ def _assert_large_model_selection_stays_lightweight() -> None:
|
||||
"large_model_hover_disabled" in hover_body,
|
||||
"large-model hover picking should be suppressed before the VTK picker runs",
|
||||
)
|
||||
select_feature_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "select_feature")
|
||||
select_face_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "select_face")
|
||||
scdm_on_demand_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_maybe_start_scdm_probe_for_selection")
|
||||
_assert(
|
||||
"_maybe_start_scdm_probe_for_selection" in select_feature_body
|
||||
and "_maybe_start_scdm_probe_for_selection" in select_face_body
|
||||
and "_start_scdm_probe_preload(force=True)" in scdm_on_demand_body,
|
||||
"large-model object selection should trigger on-demand SCDM probing instead of relying on ambiguous local cylinder fallback",
|
||||
)
|
||||
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
|
||||
@@ -1907,6 +2258,101 @@ def _assert_large_model_selection_stays_lightweight() -> None:
|
||||
)
|
||||
|
||||
|
||||
def _assert_vtk_interaction_stability_guards() -> None:
|
||||
copy_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_copy_polydata_for_ui_thread")
|
||||
_assert(
|
||||
"DeepCopy" in copy_body,
|
||||
"worker-built VTK polydata should be deep-copied before the UI renderer owns it",
|
||||
)
|
||||
render_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_render_window_safely")
|
||||
_assert(
|
||||
"_is_ui_thread" in render_body and "scene_rebuild_in_progress" in render_body,
|
||||
"render requests should stay on the UI thread and skip non-forced renders during scene rebuilds",
|
||||
)
|
||||
window_core_text = (PROJECT_ROOT / "step_editor/window_core.py").read_text(encoding="utf-8")
|
||||
window_state_text = (PROJECT_ROOT / "step_editor/window_state.py").read_text(encoding="utf-8")
|
||||
direct_core_renders = window_core_text.count("render_window.Render()") - render_body.count("render_window.Render()")
|
||||
_assert(
|
||||
direct_core_renders == 0 and "self.render_window.Render()" not in window_state_text,
|
||||
"window code should route all VTK render requests through _render_window_safely()",
|
||||
)
|
||||
pick_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_pick_actor_cell")
|
||||
_assert(
|
||||
"scene_rebuild_in_progress" in pick_body and "PickFromListOff" in pick_body,
|
||||
"VTK picking should be disabled while actors/polydata are being replaced",
|
||||
)
|
||||
hover_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_update_hover_target")
|
||||
_assert(
|
||||
"scene_rebuild_in_progress" in hover_body,
|
||||
"hover picking should be suppressed during scene rebuilds",
|
||||
)
|
||||
finish_scdm_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_finish_scdm_probe_preload")
|
||||
_assert(
|
||||
"installed = self._install_scdm_feature_cache" in finish_scdm_body
|
||||
and "cache_ready=False" in finish_scdm_body,
|
||||
"SCDM probe finish should not report success when cache installation is rejected",
|
||||
)
|
||||
deferred_edge_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_finish_deferred_edge_display")
|
||||
_assert(
|
||||
"_defer_scene_actor_update_if_interacting" in deferred_edge_body
|
||||
and "_apply_deferred_edge_display_result" in deferred_edge_body,
|
||||
"deferred edge actor installation should wait until camera interaction is idle",
|
||||
)
|
||||
scene_delay_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_scene_actor_update_should_wait_for_camera")
|
||||
_assert(
|
||||
"camera_interaction_active" in scene_delay_body
|
||||
and "pointer_button_down" in scene_delay_body
|
||||
and "last_camera_interaction_ended_at" in scene_delay_body,
|
||||
"scene actor updates should be delayed during and immediately after camera interaction",
|
||||
)
|
||||
finish_edit_body = _function_text(PROJECT_ROOT / "step_editor/window_state.py", "_finish_scdm_edit_action")
|
||||
_assert(
|
||||
"_load_step_sync" in finish_edit_body
|
||||
and "background=True" not in finish_edit_body,
|
||||
"SCDM edit-result reload should avoid the background LoadWorker path that can hand worker-built VTK objects to the renderer",
|
||||
)
|
||||
load_sync_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_load_step_sync")
|
||||
_assert(
|
||||
"_reject_scdm_result_after_reload" in load_sync_body
|
||||
and "result-load-failed" in load_sync_body,
|
||||
"SCDM edit-result reload should catch scene-apply failures and roll back instead of leaking exceptions through Qt",
|
||||
)
|
||||
load_apply_body = _function_text(PROJECT_ROOT / "step_editor/window_core.py", "_apply_loaded_model_result")
|
||||
_assert(
|
||||
"pending_scdm_reload" in load_apply_body
|
||||
and "if not pending_scdm_reload:\n self._clear_history()" in load_apply_body,
|
||||
"SCDM edit-result reload should not run the ordinary import path that clears edit history while validation is pending",
|
||||
)
|
||||
|
||||
|
||||
def _assert_scdm_cache_revision_guard() -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="step_editor_cache_guard_") as temp:
|
||||
step_path = Path(temp) / "guard.step"
|
||||
step_path.write_text("ISO-10303-21;\nEND-ISO-10303-21;\n", encoding="utf-8")
|
||||
fingerprint = file_fingerprint(step_path)
|
||||
|
||||
probe = _MouseSelectionProbe()
|
||||
probe.step_path = step_path
|
||||
probe.model = SimpleNamespace()
|
||||
stale = {
|
||||
"modelFingerprint": fingerprint,
|
||||
"mapperRevision": max(0, SCDM_FEATURE_CACHE_REVISION - 1),
|
||||
"objects": [{"objectId": "cylindrical_face_group:stale", "capabilities": [{"key": "hole.diameter"}]}],
|
||||
}
|
||||
installed = probe._install_scdm_feature_cache(stale)
|
||||
_assert(installed is False, "stale SCDM mapper cache must not be installed")
|
||||
_assert(probe.scdm_feature_cache is None and probe.scdm_feature_cache_state == "stale", "stale SCDM cache should clear UI cache state")
|
||||
|
||||
current = {
|
||||
"modelFingerprint": fingerprint,
|
||||
"mapperRevision": SCDM_FEATURE_CACHE_REVISION,
|
||||
"objects": [{"objectId": "face:0", "capabilities": [{"key": "face.offset"}]}],
|
||||
}
|
||||
installed = probe._install_scdm_feature_cache(current)
|
||||
_assert(installed is True, "current SCDM mapper cache should install")
|
||||
_assert(probe._current_scdm_feature_cache_matches_loaded_step() is True, "current cache should match the loaded STEP")
|
||||
|
||||
|
||||
def _assert_large_planar_offset_prefers_local_backend() -> None:
|
||||
probe = _PropertyTableProbe()
|
||||
probe.model = _LargeDisplayModel()
|
||||
@@ -2043,6 +2489,8 @@ def main() -> int:
|
||||
_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, f"software progress should not overpromise ambiguous slot edits: {progress_detail}")
|
||||
_assert("不等于已经可改" in progress_detail, f"software progress should distinguish recognition from editability: {progress_detail}")
|
||||
_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")
|
||||
@@ -2080,11 +2528,17 @@ def main() -> int:
|
||||
_assert_relation_radius_formula_proxy()
|
||||
_assert_relation_formula_input_remains_editable_while_replaying()
|
||||
_assert_relation_formula_input_clickable_after_existing_formula()
|
||||
_assert_relation_formula_can_toggle_without_deleting()
|
||||
_assert_relation_formula_import_export()
|
||||
_assert_relation_formula_input_is_selection_independent()
|
||||
_assert_relation_formula_input_recovers_after_loading()
|
||||
_assert_relation_formula_ids_follow_model_remap()
|
||||
_assert_mouse_selection_guards()
|
||||
_assert_scdm_selection_diagnostics()
|
||||
_assert_scdm_parameter_table_shows_only_enabled_specs()
|
||||
_assert_ambiguous_slot_empty_state_explains_missing_params()
|
||||
_assert_scdm_round_delete_conflict_filtered_for_blind_pocket()
|
||||
_assert_scdm_first_holds_ambiguous_large_cylinders()
|
||||
_assert_solid_selection_does_not_expand_face_scdm_specs()
|
||||
_assert_operation_record_backend_sources()
|
||||
_assert_scdm_auto_prompt()
|
||||
@@ -2092,6 +2546,8 @@ def main() -> int:
|
||||
_assert_worker_ui_callbacks_guarded()
|
||||
_assert_large_model_preload_stays_lightweight()
|
||||
_assert_large_model_selection_stays_lightweight()
|
||||
_assert_vtk_interaction_stability_guards()
|
||||
_assert_scdm_cache_revision_guard()
|
||||
_assert_large_planar_offset_prefers_local_backend()
|
||||
_assert_background_load_uses_worker()
|
||||
_assert_quick_blind_depth_spec()
|
||||
|
||||
Reference in New Issue
Block a user