feat: 接入 SCDM 优先编辑闭环并支持阵列局部间距

接入 SCDM probe/edit/cache/校验链路,增强孔组、阵列、关系式和参数表交互。

支持阵列相邻段间距、移动意图切换、结果回滚校验,并补充对应回归脚本。
This commit is contained in:
2026-08-19 18:02:47 +08:00
parent a3eb7e2476
commit 4e7877e05c
21 changed files with 5646 additions and 491 deletions
+135 -1
View File
@@ -49,6 +49,7 @@ from step_editor.window_state import (
RELATION_FORMULA_OBJECT_COMPLETION_LIMIT,
WindowStateMixin,
)
from step_editor.ui_helpers import _selection_mode_label, _selection_mode_value
class _StatusBar:
@@ -86,7 +87,23 @@ class _PropertyTableProbe(QWidget, WindowStateMixin):
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"}
self.scdm_edit_runner_ready = {
"face.offset",
"hole.diameter",
"hole.position",
"feature.fill",
"slot.width",
"slot.depth",
"slot.position",
"boss.diameter",
"boss.height",
"boss.position",
"round.radius",
"chamfer.distance",
"feature.delete_round_or_chamfer",
"pattern.spacing",
"pattern.segment_spacing",
}
layout = QVBoxLayout(self)
self.object_edit_box = self
@@ -1560,6 +1577,65 @@ def _assert_scdm_selection_diagnostics() -> None:
_assert("偏移" in str(info.get("scdm_selection_enabled_capabilities")), f"SCDM enabled capability diagnostic missing: {info}")
def _assert_solid_selection_does_not_expand_face_scdm_specs() -> None:
probe = _PropertyTableProbe()
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",
}
],
},
{
"objectId": "face:1",
"objectType": "face",
"geometrySignature": {"faceIds": [1], "surfaceType": "plane", "planeOffset": -5.0},
"capabilities": [
{
"key": "face.offset",
"displayName": "偏移",
"currentValue": -5.0,
"valueKind": "number",
"defaultIntent": "推拉平面",
"backendOperation": "pull_face_offset",
"postCheck": "target_face_offset",
}
],
},
]
}
probe.selected_kind = "face"
probe.selected_face_id = 0
face_specs = probe._scdm_property_specs_for_selection()
_assert(
len([item for item in face_specs if item.get("scdm_capability_key") == "face.offset"]) == 1,
f"Face selection should show the selected Face SCDM offset only: {face_specs}",
)
probe.selected_kind = "solid"
probe.selected_face_id = None
probe.selected_solid_id = 0
probe.model = SimpleNamespace(face_solid_ids=[0, 0])
solid_specs = probe._scdm_property_specs_for_selection()
_assert(
solid_specs == [],
f"Solid selection should not expand every child Face SCDM offset into the parameter table: {solid_specs}",
)
def _assert_operation_record_backend_sources() -> None:
class _OperationRecordProbe(WindowActionMixin):
pass
@@ -1718,6 +1794,10 @@ def _assert_large_model_preload_stays_lightweight() -> None:
_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(
"_restore_scdm_feature_cache_from_disk()" in loaded_body,
"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",
@@ -1727,6 +1807,10 @@ def _assert_large_model_preload_stays_lightweight() -> None:
"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(
"_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",
@@ -1866,6 +1950,53 @@ def _assert_large_planar_offset_prefers_local_backend() -> None:
_assert("SCDM" in str(probe.scdm_selection_status_message), "backend preference should explain that SCDM was bypassed")
def _assert_scdm_target_values_use_backend_units() -> None:
probe = _PropertyTableProbe()
offset = probe._scdm_property_target_value(
{"scdm_capability_key": "face.offset", "value_type": "number", "scdm_unit_scale": 0.001},
"2.5",
)
_assert(abs(float(offset) - 0.0025) <= 1.0e-12, f"SCDM numeric targets should be converted back to backend units: {offset}")
position = probe._scdm_property_target_value(
{"scdm_capability_key": "hole.position", "value_type": "vector3", "scdm_unit_scale": 0.001},
"(1, 2, 3)",
)
_assert(position == [0.001, 0.002, 0.003], f"SCDM vector targets should be converted back to backend units: {position}")
spacing_spec = {"label": "阵列间距", "value_type": "positive", "max_value": 1.3571428571428572}
_assert(
not probe._property_target_validation_error(spacing_spec, "1.1"),
"pattern spacing targets within the support face range should be accepted",
)
spacing_error = probe._property_target_validation_error(spacing_spec, "2")
_assert(
"阵列间距" in spacing_error and "1.35714" in spacing_error,
f"pattern spacing beyond the support face range should be blocked in the UI: {spacing_error}",
)
def _assert_selection_mode_labels_are_english() -> None:
expected = {
"Feature": "Feature",
"Face": "Face",
"Edge": "Edge",
"Solid": "Solid",
"Part": "Part",
}
for value, label in expected.items():
_assert(_selection_mode_label(value) == label, f"selection mode {value} should display as English: {_selection_mode_label(value)}")
_assert(_selection_mode_value(label) == value, f"selection mode label {label} should resolve to {value}")
for legacy, value in {
"智能特征": "Feature",
"特征": "Feature",
"": "Face",
"": "Edge",
"实体": "Solid",
"零件": "Part",
"装配零件": "Part",
}.items():
_assert(_selection_mode_value(legacy) == value, f"legacy selection label {legacy} should resolve to {value}")
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")
@@ -1891,6 +2022,8 @@ def main() -> int:
not top_level_label_probe.shown_labels,
f"property labels were shown as transient top-level windows: {top_level_label_probe.shown_labels}",
)
_assert_scdm_target_values_use_backend_units()
_assert_selection_mode_labels_are_english()
_assert_property_table_editor(probe)
_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")
@@ -1952,6 +2085,7 @@ def main() -> int:
_assert_relation_formula_ids_follow_model_remap()
_assert_mouse_selection_guards()
_assert_scdm_selection_diagnostics()
_assert_solid_selection_does_not_expand_face_scdm_specs()
_assert_operation_record_backend_sources()
_assert_scdm_auto_prompt()
_assert_property_ui_reroute_guards()