2026-08-06 18:15:14 +08:00
from __future__ import annotations
2026-08-11 18:28:16 +08:00
import json
2026-08-07 18:08:32 +08:00
import math
2026-08-06 18:15:14 +08:00
import os
from pathlib import Path
2026-08-19 10:28:09 +08:00
import re
2026-08-06 18:15:14 +08:00
import sys
2026-08-11 18:28:16 +08:00
import tempfile
2026-08-19 10:28:09 +08:00
from types import SimpleNamespace
2026-08-06 18:15:14 +08:00
os . environ . setdefault ( "QT_QPA_PLATFORM" , "offscreen" )
2026-08-17 18:53:03 +08:00
from PySide6.QtCore import QEvent , QObject , Qt , QStringListModel
from PySide6.QtGui import QKeyEvent
from PySide6.QtTest import QTest
2026-08-06 18:15:14 +08:00
from PySide6.QtWidgets import (
QApplication ,
2026-08-11 18:28:16 +08:00
QCheckBox ,
2026-08-14 18:42:39 +08:00
QCompleter ,
2026-08-06 18:15:14 +08:00
QFrame ,
QHBoxLayout ,
QLabel ,
QLineEdit ,
2026-08-14 18:42:39 +08:00
QListWidget ,
2026-08-06 18:15:14 +08:00
QPushButton ,
QScrollArea ,
QTableWidget ,
2026-08-17 18:53:03 +08:00
QTableWidgetItem ,
2026-08-06 18:15:14 +08:00
QVBoxLayout ,
QWidget ,
)
PROJECT_ROOT = Path ( __file__ ) . resolve () . parent . parent
if str ( PROJECT_ROOT ) not in sys . path :
sys . path . insert ( 0 , str ( PROJECT_ROOT ))
from step_editor.widgets import NoWheelComboBox
2026-08-17 18:53:03 +08:00
from step_editor.relation_formulas import ObjectParameterRef , parse_relation_formula
2026-08-07 18:08:32 +08:00
from step_editor.window_actions import WindowActionMixin
from step_editor.window_core import WindowCoreMixin
from step_editor.window_state import (
PROPERTY_CURRENT_COLUMN ,
2026-08-11 18:28:16 +08:00
PROPERTY_INPUT_COLUMN ,
2026-08-07 18:08:32 +08:00
PROPERTY_LABEL_COLUMN ,
PROPERTY_SCOPE_COLUMN ,
2026-08-11 18:28:16 +08:00
PROPERTY_TABLE_HEADERS ,
2026-08-07 18:08:32 +08:00
PROPERTY_TARGET_COLUMN ,
2026-08-17 18:53:03 +08:00
RELATION_FORMULA_OBJECT_COMPLETION_LIMIT ,
2026-08-07 18:08:32 +08:00
WindowStateMixin ,
)
2026-08-19 18:02:47 +08:00
from step_editor.ui_helpers import _selection_mode_label , _selection_mode_value
2026-08-06 18:15:14 +08:00
class _StatusBar :
def showMessage ( self , _text : str ) -> None :
pass
2026-08-07 18:08:32 +08:00
class _PropertyTableProbe ( QWidget , WindowStateMixin ):
2026-08-06 18:15:14 +08:00
def __init__ ( self ) -> None :
super () . __init__ ()
self . model = object ()
self . operation_in_progress = False
self . scan_in_progress = False
self . load_in_progress = False
self . property_editor_updating = False
self . property_table_expanded = False
2026-08-07 18:08:32 +08:00
self . property_table_collapsed_rows = 5
self . property_table_min_visible_rows = 5
2026-08-06 18:15:14 +08:00
self . property_editor_selected_row = None
self . property_command_active_key = ""
self . property_command_buttons = {}
self . property_editor_specs = []
2026-08-14 18:42:39 +08:00
self . relation_formula_items = []
self . relation_formula_next_id = 1
2026-08-07 18:08:32 +08:00
self . selected_kind = "feature"
2026-08-06 18:15:14 +08:00
self . selected_part_id = None
self . selected_solid_id = None
self . selected_face_id = 0
self . selected_edge_id = None
2026-08-17 18:53:03 +08:00
self . selected_pick_position = None
2026-08-13 17:50:20 +08:00
self . step_path = PROJECT_ROOT / "assets" / "models" / "probe.step"
2026-08-07 18:08:32 +08:00
self . current_info_values = self . _plane_info ()
2026-08-13 17:50:20 +08:00
self . executed_property_actions : list [ tuple [ str , str ]] = []
2026-08-19 10:28:09 +08:00
self . scdm_backend_status = None
self . scdm_feature_cache = None
self . scdm_feature_cache_state = "empty"
self . scdm_feature_cache_message = ""
2026-08-19 18:02:47 +08:00
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" ,
}
2026-08-07 18:08:32 +08:00
layout = QVBoxLayout ( self )
self . object_edit_box = self
2026-08-11 18:28:16 +08:00
self . property_table = QTableWidget ( 0 , len ( PROPERTY_TABLE_HEADERS ))
self . property_table . setHorizontalHeaderLabels ( list ( PROPERTY_TABLE_HEADERS ))
2026-08-19 10:28:09 +08:00
self . property_table . itemSelectionChanged . connect ( lambda : self . _update_property_apply_state ())
2026-08-07 18:08:32 +08:00
layout . addWidget ( self . property_table )
2026-08-06 18:15:14 +08:00
self . property_card_scroll = QScrollArea ()
self . property_card_container = QWidget ()
self . property_card_layout = QVBoxLayout ( self . property_card_container )
self . property_card_scroll . setWidget ( self . property_card_container )
2026-08-07 18:08:32 +08:00
layout . addWidget ( self . property_card_scroll )
self . property_expand_button = QPushButton ()
layout . addWidget ( self . property_expand_button )
2026-08-06 18:15:14 +08:00
self . property_command_summary_label = QLabel ()
self . property_command_bar = QFrame ()
self . property_command_layout = QHBoxLayout ( self . property_command_bar )
2026-08-07 18:08:32 +08:00
self . property_command_help_label = QLabel ()
2026-08-19 10:28:09 +08:00
self . current_capability_button = QPushButton ()
self . scdm_backend_status_label = QLabel ()
self . scdm_backend_detail_label = QLabel ()
2026-08-06 18:15:14 +08:00
self . apply_property_button = QPushButton ()
2026-08-11 18:28:16 +08:00
self . export_parameters_button = QPushButton ()
2026-08-14 18:42:39 +08:00
self . relation_formula_input = QLineEdit ()
self . relation_formula_completer_model = QStringListModel ( self )
self . relation_formula_completer = QCompleter ( self . relation_formula_completer_model , self )
2026-08-17 18:53:03 +08:00
self . relation_formula_completer . setMaxVisibleItems ( 12 )
self . relation_formula_completer . activated [ str ] . connect ( self . _on_relation_formula_completion_activated )
relation_popup = self . relation_formula_completer . popup ()
if relation_popup is not None :
relation_popup . setFocusPolicy ( Qt . FocusPolicy . NoFocus )
self . relation_formula_completer . setWidget ( self . relation_formula_input )
self . relation_formula_input . textChanged . connect ( self . _on_relation_formula_input_changed )
layout . addWidget ( self . relation_formula_input )
2026-08-14 18:42:39 +08:00
self . add_relation_formula_button = QPushButton ()
self . remove_relation_formula_button = QPushButton ()
self . relation_formula_list = QListWidget ()
2026-08-13 17:50:20 +08:00
self . face_width_input = QLineEdit ()
self . face_height_input = QLineEdit ()
2026-08-06 18:15:14 +08:00
2026-08-07 18:08:32 +08:00
@staticmethod
def _plane_info () -> dict [ str , object ]:
2026-08-06 18:15:14 +08:00
return {
"area" : 100.0 ,
2026-08-07 18:08:32 +08:00
"area_center" : ( 5.0 , 5.0 , 0.0 ),
"bbox_center" : ( 5.0 , 5.0 , 0.0 ),
"bbox_diagonal" : 14.1421356237 ,
"local_face_width" : 10.0 ,
"local_face_height" : 10.0 ,
2026-08-13 17:50:20 +08:00
"local_face_size_edit_ready" : True ,
"local_face_size_edit_blocker" : "" ,
2026-08-07 18:08:32 +08:00
"plane_origin" : ( 0.0 , 0.0 , 0.0 ),
"push_pull_outward_direction" : ( 0.0 , 0.0 , 1.0 ),
"normal" : ( 0.0 , 0.0 , 1.0 ),
}
def _selected_action_info ( self ) -> dict [ str , object ]:
return {
** self . current_info_values ,
2026-08-06 18:15:14 +08:00
"surface" : "plane" ,
"push_pull_status" : "ready" ,
"first_level_boundary_edge_count" : 4 ,
"first_level_boundary_vertex_count" : 4 ,
"first_level_adjacent_face_count" : 4 ,
}
def _set_control_state ( self , widget , enabled : bool , _enabled_tip : str , _disabled_tip : str ) -> None :
widget . setEnabled ( enabled )
2026-08-17 18:53:03 +08:00
def setTitle ( self , title : str ) -> None :
self . object_edit_title = title
2026-08-13 17:50:20 +08:00
def resize_face_width_local ( self ) -> None :
self . executed_property_actions . append (( "resize_face_width_local" , self . face_width_input . text ()))
self . _after_property_edit_finished ( success = True )
def resize_face_height_local ( self ) -> None :
self . executed_property_actions . append (( "resize_face_height_local" , self . face_height_input . text ()))
self . _after_property_edit_finished ( success = True )
2026-08-19 10:28:09 +08:00
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 )
2026-08-06 18:15:14 +08:00
def statusBar ( self ) -> _StatusBar :
return _StatusBar ()
2026-08-17 18:53:03 +08:00
class _RelationFormulaEventProbe ( WindowCoreMixin , _PropertyTableProbe ):
pass
2026-08-19 10:28:09 +08:00
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
2026-08-17 18:53:03 +08:00
class _GlobalRelationCompletionModel :
def __init__ ( self ) -> None :
self . faces = [ object () for _index in range ( 100 )]
self . edges = [ object () for _index in range ( 12 )]
self . face_logical_ids = list ( range ( len ( self . faces )))
self . face_region_call_count = 0
def face_logical_id ( self , face_id : int ) -> int :
return int ( self . face_logical_ids [ int ( face_id )])
def face_region_logical_id ( self , face_id : int ) -> int :
self . face_region_call_count += 1
return int ( face_id )
def resolve_face_selection_id ( self , object_id : int ) -> int | None :
if 0 <= int ( object_id ) < len ( self . faces ):
return int ( object_id )
return None
def quick_face_info ( self , face_id : int ) -> dict [ str , object ]:
return { "surface" : "cylinder" if int ( face_id ) in { 85 , 87 } else "plane" }
class _LargeRelationCompletionModel ( _GlobalRelationCompletionModel ):
def __init__ ( self ) -> None :
self . faces = [ object () for _index in range ( 10000 )]
self . edges = [ object () for _index in range ( 1000 )]
self . face_logical_ids = list ( range ( len ( self . faces )))
self . face_region_call_count = 0
def face_region_logical_id ( self , face_id : int ) -> int :
self . face_region_call_count += 1
raise AssertionError ( "relation formula completion should not call face_region_logical_id" )
2026-08-19 10:28:09 +08:00
class _LargeDisplayModel :
def __init__ ( self ) -> None :
self . faces = [ object () for _index in range ( 1800 )]
self . edges = [ object () for _index in range ( 5000 )]
2026-08-17 18:53:03 +08:00
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 )]
self . edges = []
self . face_logical_ids = list ( range ( face_count ))
self . face_part_ids = [ 0 for _index in range ( face_count )]
self . face_solid_ids = [ 0 for _index in range ( face_count )]
self . face_infos = dict ( face_infos )
def face_logical_id ( self , face_id : int ) -> int :
return int ( self . face_logical_ids [ int ( face_id )])
def face_region_logical_id ( self , face_id : int ) -> int :
return self . face_logical_id ( int ( face_id ))
def face_ids_for_logical_id ( self , logical_id : int ) -> list [ int ]:
return [ face_id for face_id , item in enumerate ( self . face_logical_ids ) if int ( item ) == int ( logical_id )]
def resolve_face_selection_id ( self , object_id : int ) -> int | None :
matches = self . face_ids_for_logical_id ( int ( object_id ))
if matches :
return int ( matches [ 0 ])
if 0 <= int ( object_id ) < len ( self . faces ):
return int ( object_id )
return None
def quick_face_info ( self , face_id : int ) -> dict [ str , object ]:
info = dict ( self . face_infos . get ( int ( face_id ), {}))
if not info :
info = { "surface" : "plane" , "area_center" : ( 1000.0 + int ( face_id ), 0.0 , 0.0 ), "area" : 1.0 }
info . setdefault ( "part_id" , 0 )
info . setdefault ( "solid_id" , 0 )
info . setdefault ( "axis" , ( 0.0 , 0.0 , 1.0 ))
return info
2026-08-07 18:08:32 +08:00
class _ActionMessageProbe ( WindowActionMixin ):
pass
2026-08-11 18:28:16 +08:00
class _ParameterExportActionProbe ( WindowActionMixin ):
def __init__ ( self , output_path : Path ) -> None :
self . output_path = output_path
2026-08-13 17:50:20 +08:00
self . component_path = output_path . parent / "nodes" / "000_test" / "main.py"
2026-08-11 18:28:16 +08:00
self . status_bar = _StatusBarProbe ()
self . info_text = ""
self . export_state_updates = 0
def _parameter_export_output_path ( self ) -> Path :
return self . output_path
def _selected_parameter_export_rows ( self ) -> list [ dict [ str , str ]]:
return [
{
"name" : "面内长度" ,
"displayName" : "面内长度" ,
"type" : "number" ,
"ioRole" : "input" ,
"default" : "151" ,
},
{
"name" : "偏移" ,
"displayName" : "偏移" ,
"type" : "number" ,
"ioRole" : "input" ,
"default" : "57.5" ,
},
]
2026-08-13 17:50:20 +08:00
def _export_parametric_component_main ( self , rows : list [ dict [ str , str ]]) -> Path :
assert rows
self . component_path . parent . mkdir ( parents = True , exist_ok = True )
input_rows = json . dumps ( rows , ensure_ascii = False , indent = 4 )
self . component_path . write_text (
"INPUT_PARAMETERS = "
+ input_rows
+ " \n OUTPUT_PARAMETERS = [{'name': 'output_step', 'displayName': '输出STEP', 'type': 'file', 'ioRole': 'output', 'default': ''}]"
+ " \n PARAMETERS = INPUT_PARAMETERS + OUTPUT_PARAMETERS"
+ " \n NODE_INFO = {'parameters': PARAMETERS}"
+ " \n def execute(inputs, params, context): \n return {'output_step': 'modified.step'} \n " ,
encoding = "utf-8" ,
)
return self . component_path
2026-08-11 18:28:16 +08:00
def _update_parameter_export_state ( self ) -> None :
self . export_state_updates += 1
def statusBar ( self ) -> _StatusBarProbe :
return self . status_bar
def set_plain_info ( self , text : str ) -> None :
self . info_text = text
2026-08-07 18:08:32 +08:00
class _TimerProbe :
def stop ( self ) -> None :
pass
class _RenderWindowProbe :
def Render ( self ) -> None :
pass
class _StatusBarProbe :
def __init__ ( self ) -> None :
self . messages : list [ str ] = []
def showMessage ( self , message : str ) -> None :
self . messages . append ( str ( message ))
class _MouseSelectionProbe ( WindowCoreMixin ):
def __init__ ( self ) -> None :
self . pointer_button_down = False
self . left_button_press_position = None
self . left_button_press_camera_state = None
self . left_button_press_target = None
self . left_button_dragged = False
self . left_click_drag_threshold_px = 6
self . left_click_camera_tolerance = 1e-7
self . camera_interaction_active = False
self . pending_hover_position = None
self . last_hover_pick_position = None
self . hover_timer = _TimerProbe ()
self . camera_state = ( 0.0 , 0.0 , 10.0 , 0.0 , 0.0 , 0.0 , 0.0 , 1.0 , 0.0 , 1.0 , 10.0 , 30.0 )
self . load_in_progress = False
self . operation_in_progress = False
self . scan_in_progress = False
self . model = object ()
self . selected_kind = None
self . render_window = _RenderWindowProbe ()
self . status_bar = _StatusBarProbe ()
self . pick_targets : list [ dict [ str , object ] | None ] = [
{ "kind" : "face" , "target_id" : 1 , "pick_position" : ( 0.0 , 0.0 , 0.0 )},
{ "kind" : "face" , "target_id" : 1 , "pick_position" : ( 0.0 , 0.0 , 0.0 )},
]
self . selected_targets : list [ dict [ str , object ]] = []
self . hover_clear_count = 0
self . camera_end_count = 0
def _camera_state_signature ( self ):
return tuple ( self . camera_state )
def _current_selection_mode ( self ) -> str :
return "Face"
def _pick_selection_target ( self , _mode : str , _x : int , _y : int ) -> dict [ str , object ] | None :
if self . pick_targets :
return self . pick_targets . pop ( 0 )
return None
def _select_pick_target ( self , target : dict [ str , object ]) -> None :
self . selected_targets . append ( dict ( target ))
def statusBar ( self ) -> _StatusBarProbe :
return self . status_bar
def _clear_hover ( self , render : bool = True ) -> None :
self . hover_clear_count += 1
def _end_camera_interaction ( self ) -> None :
self . camera_interaction_active = False
self . camera_end_count += 1
2026-08-06 18:15:14 +08:00
class _TopLevelPropertyLabelProbe ( QObject ):
def __init__ ( self ) -> None :
super () . __init__ ()
self . shown_labels : list [ str ] = []
def eventFilter ( self , watched , event ):
if (
event . type () == QEvent . Type . Show
and isinstance ( watched , QLabel )
and watched . isWindow ()
and watched . objectName () . startswith ( "propertyCard" )
):
self . shown_labels . append ( f " { type ( watched ) . __name__ } : { watched . objectName () } : { watched . text () } " )
return False
def _assert ( condition : bool , message : str ) -> None :
if not condition :
raise AssertionError ( message )
2026-08-07 18:08:32 +08:00
def _row_by_label ( probe : _PropertyTableProbe , label : str ) -> int :
for row in range ( probe . property_table . rowCount ()):
item = probe . property_table . item ( row , PROPERTY_LABEL_COLUMN )
if item is not None and item . text () == label :
return row
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
]
raise AssertionError ( f " { label !r} row was not found; labels= { labels } " )
def _assert_property_table_editor ( probe : _PropertyTableProbe ) -> None :
_assert (
not WindowCoreMixin . _should_suppress_transient_tooltip ( probe , probe . property_table ),
"property table tooltip events should not be suppressed" ,
)
_assert (
not WindowCoreMixin . _should_suppress_transient_tooltip ( probe , probe . property_table . viewport ()),
"property table viewport tooltip events should not be suppressed" ,
)
headers = [
probe . property_table . horizontalHeaderItem ( column ) . text ()
for column in range ( probe . property_table . columnCount ())
]
2026-08-11 18:28:16 +08:00
_assert ( headers == list ( PROPERTY_TABLE_HEADERS ), f "unexpected table headers: { headers } " )
2026-08-07 18:08:32 +08:00
_assert ( probe . property_table . rowCount () == len ( probe . property_editor_specs ), "table row count should match specs" )
header_height = int ( probe . property_table . horizontalHeader () . height ())
frame = int ( probe . property_table . frameWidth ()) * 2
default_row_height = max ( int ( probe . property_table . verticalHeader () . defaultSectionSize ()), 22 )
expected_five_row_height = header_height + frame + default_row_height * 5 + 8
_assert (
probe . property_table . minimumHeight () >= expected_five_row_height ,
"feature parameter table should reserve enough height for five default rows" ,
)
_assert ( not getattr ( probe , "property_card_rows" , {}), "property card rows should not be built in table mode" )
_assert ( not probe . property_card_scroll . isVisible (), "property card scroll area should stay hidden" )
_assert ( probe . property_card_scroll . maximumHeight () == 0 , "property card scroll area should not reserve height" )
_assert ( not probe . property_command_buttons , "legacy command buttons should not be shown in the parameter table" )
_assert ( not probe . property_command_bar . isVisible (), "legacy command bar should be hidden" )
table_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
]
actionable_labels = [ str ( spec . get ( "label" , "" )) for _row , spec in probe . _actionable_property_rows ()]
2026-08-11 18:28:16 +08:00
for label in ( "面内长度" , "面内宽度" , "偏移" ):
2026-08-07 18:08:32 +08:00
_assert ( label in actionable_labels , f "feature parameter table did not expose { label } " )
2026-08-11 18:28:16 +08:00
_assert ( "中心" not in actionable_labels , "center should be temporarily hidden from editable parameters" )
_assert ( "中心" not in table_labels , "center should not appear in the feature parameter table" )
2026-08-07 18:08:32 +08:00
for legacy_label in ( "面积" , "U向尺寸" , "V向尺寸" , "偏移变换" ):
_assert ( legacy_label not in actionable_labels , f " { legacy_label } should not be exposed as an editable parameter" )
_assert ( legacy_label not in table_labels , f " { legacy_label } should not appear in the feature parameter table" )
for diagnostic_label in ( "建模形式" , "推荐操作" , "一级关系" , "关联探测" ):
_assert ( diagnostic_label not in table_labels , f " { diagnostic_label } should stay out of the feature parameter table" )
target_row = _row_by_label ( probe , "面内长度" )
target_widget = probe . property_table . cellWidget ( target_row , PROPERTY_TARGET_COLUMN )
scope_widget = probe . property_table . cellWidget ( target_row , PROPERTY_SCOPE_COLUMN )
2026-08-11 18:28:16 +08:00
input_checkbox = probe . _property_input_checkbox ( target_row )
2026-08-07 18:08:32 +08:00
_assert ( isinstance ( target_widget , QLineEdit ), "editable table row should have a target editor" )
_assert ( isinstance ( scope_widget , NoWheelComboBox ), "editable table row should have a modeling-intent combo" )
2026-08-11 18:28:16 +08:00
_assert ( isinstance ( input_checkbox , QCheckBox ), "editable table row should have an input-parameter checkbox" )
_assert ( not input_checkbox . isChecked (), "input-parameter checkbox should be unchecked by default" )
_assert (
probe . property_table . cellWidget ( target_row , PROPERTY_INPUT_COLUMN ) is not None ,
"input-parameter checkbox should be hosted in the input column" ,
)
_assert ( not probe . export_parameters_button . isEnabled (), "parameter export button should start disabled" )
2026-08-07 18:08:32 +08:00
_assert (
not probe . property_table . findChildren ( QPushButton ),
"feature parameter table should not contain per-row apply buttons" ,
)
2026-08-11 18:28:16 +08:00
input_checkbox . setChecked ( True )
QApplication . processEvents ()
selected_rows = probe . _selected_parameter_export_rows ()
_assert ( probe . export_parameters_button . isEnabled (), "parameter export button should enable after a row is checked" )
_assert (
selected_rows == [
{
"name" : "面内长度" ,
"displayName" : "面内长度" ,
"type" : "number" ,
"ioRole" : "input" ,
"default" : "10" ,
}
],
f "unexpected parameter export payload: { selected_rows } " ,
)
2026-08-13 17:50:20 +08:00
component_edits = probe . _selected_parameter_component_edits ( selected_rows )
_assert ( len ( component_edits ) == 1 , f "selected parameter should map to one component edit: { component_edits } " )
_assert (
component_edits [ 0 ][ "operation" ] == "resize_face_size_local" ,
f "face width export should map to backend resize operation: { component_edits [ 0 ] } " ,
)
_assert (
component_edits [ 0 ][ "args" ] == [ 0 , { "param" : "面内长度" }, "width" ],
f "face width export should embed parameter placeholder args: { component_edits [ 0 ] } " ,
)
2026-08-11 18:28:16 +08:00
2026-08-07 18:08:32 +08:00
target_widget . setText ( "12" )
probe . _update_property_apply_state ()
changed = probe . _changed_property_rows ()
_assert ( any ( row == target_row for row , _spec , _text in changed ), "table target edit was not detected" )
_assert ( probe . apply_property_button . isEnabled (), "single parametric modeling button should enable for one changed row" )
2026-08-13 17:50:20 +08:00
width_row = _row_by_label ( probe , "面内宽度" )
width_widget = probe . property_table . cellWidget ( width_row , PROPERTY_TARGET_COLUMN )
_assert ( isinstance ( width_widget , QLineEdit ), "second editable table row should have a target editor" )
width_widget . setText ( "8" )
probe . _update_property_apply_state ()
changed = probe . _changed_property_rows ()
_assert ( len ( changed ) >= 2 , f "two target edits should be detected: { changed } " )
_assert ( probe . apply_property_button . isEnabled (), "parametric modeling button should stay enabled for multiple changed rows" )
probe . apply_current_property_edit ()
for _index in range ( 6 ):
QApplication . processEvents ()
if not getattr ( probe , "property_batch_active" , False ):
break
_assert (
probe . executed_property_actions == [
( "resize_face_width_local" , "12" ),
( "resize_face_height_local" , "8" ),
],
f "batch parametric modeling should execute changed rows in table order: { probe . executed_property_actions } " ,
)
_assert ( not getattr ( probe , "property_batch_active" , False ), "property batch state should clear after completion" )
2026-08-07 18:08:32 +08:00
probe . toggle_property_table_expanded ()
_assert ( probe . property_table_expanded , "property table expand toggle failed" )
_assert ( "收起" in probe . property_expand_button . text (), "expanded table button should offer to collapse" )
preserved_editor = probe . property_table . cellWidget ( target_row , PROPERTY_TARGET_COLUMN )
_assert ( isinstance ( preserved_editor , QLineEdit ), "target editor disappeared after table expand" )
_assert ( preserved_editor . text () . strip () == "12" , "target value was not preserved after table expand" )
2026-08-19 10:28:09 +08:00
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 } " ,
)
2026-08-07 18:08:32 +08:00
def _assert_diagnostics_stay_out_of_parameter_table ( probe : _PropertyTableProbe ) -> None :
long_context = "已按“相邻特征”沿共享边拓扑探测当前特征及 3 个局部关联特征;关联尺寸可在同一参数表中直接修改。"
probe . current_info_values = {
** probe . _plane_info (),
"feature_context_note" : long_context ,
"feature_detection_level" : "相邻特征" ,
"associated_feature_count" : 3 ,
}
2026-08-06 18:15:14 +08:00
probe . _refresh_property_editor ()
QApplication . processEvents ()
2026-08-07 18:08:32 +08:00
table_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
]
for diagnostic_label in ( "建模形式" , "推荐操作" , "一级关系" , "关联探测" ):
_assert ( diagnostic_label not in table_labels , f " { diagnostic_label } should not be shown as a feature parameter" )
2026-08-14 18:42:39 +08:00
def _assert_relation_formula_editor () -> None :
probe = _PropertyTableProbe ()
probe . _refresh_property_editor ()
2026-08-19 10:28:09 +08:00
_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 ()
2026-08-14 18:42:39 +08:00
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 } " )
2026-08-17 18:53:03 +08:00
probe . relation_formula_input . setText ( "F" )
probe . relation_formula_input . setCursorPosition ( 1 )
probe . _update_relation_formula_completions ()
f_completions = set ( probe . relation_formula_completer_model . stringList ())
_assert (
"Face" in f_completions or "Face0" in f_completions ,
f "relation formula should suggest face object tokens after 'F': { f_completions } " ,
)
_assert ( probe . relation_formula_completer . completionCount () > 0 , "relation formula completer should have matches after 'F'" )
_assert ( probe . _accept_relation_formula_completion (), "Tab should accept relation formula completion after 'F'" )
_assert (
probe . relation_formula_input . text () in { "Face" , "Face0" },
f "Tab should replace 'F' with a face completion: { probe . relation_formula_input . text () } " ,
)
if probe . relation_formula_input . text () == "Face" :
probe . _update_relation_formula_completions ()
face_object_completions = set ( probe . relation_formula_completer_model . stringList ())
_assert (
"Face0" in face_object_completions ,
f "relation formula should suggest concrete Face IDs after completing Face: { face_object_completions } " ,
)
_assert ( probe . _accept_relation_formula_completion (), "second Tab should accept a concrete Face ID completion" )
_assert (
probe . relation_formula_input . text () == "Face0" ,
f "second Tab should complete Face to Face0: { probe . relation_formula_input . text () } " ,
)
probe . _update_relation_formula_completions ()
exact_object_completions = set ( probe . relation_formula_completer_model . stringList ())
_assert (
any ( str ( item ) . startswith ( "Face0." ) for item in exact_object_completions ),
f "concrete object completion should suggest editable parameters: { exact_object_completions } " ,
)
event_probe = _RelationFormulaEventProbe ()
event_probe . show ()
event_probe . _refresh_property_editor ()
event_probe . relation_formula_input . setFocus ( Qt . FocusReason . OtherFocusReason )
QApplication . processEvents ()
event_probe . relation_formula_input . setText ( "F" )
event_probe . relation_formula_input . setCursorPosition ( 1 )
tab_event = QKeyEvent ( QEvent . Type . ShortcutOverride , Qt . Key . Key_Tab , Qt . KeyboardModifier . NoModifier )
_assert (
event_probe . eventFilter ( event_probe . relation_formula_input , tab_event ),
"relation formula input should consume Tab before Qt moves focus" ,
)
_assert (
event_probe . relation_formula_input . text () in { "Face" , "Face0" },
f "ShortcutOverride Tab should accept the face completion: { event_probe . relation_formula_input . text () } " ,
)
QApplication . processEvents ()
_assert ( event_probe . relation_formula_input . hasFocus (), "relation formula input should keep focus after Tab completion" )
popup = event_probe . relation_formula_completer . popup ()
if popup is not None :
popup . installEventFilter ( event_probe )
before_backspace = event_probe . relation_formula_input . text ()
popup_backspace = QKeyEvent ( QEvent . Type . KeyPress , Qt . Key . Key_Backspace , Qt . KeyboardModifier . NoModifier )
_assert (
event_probe . eventFilter ( popup , popup_backspace ),
"relation formula completer popup should forward Backspace to the input" ,
)
_assert (
event_probe . relation_formula_input . text () == before_backspace [: - 1 ],
f "Backspace should edit the input even if popup receives it: { event_probe . relation_formula_input . text () } " ,
)
_assert ( event_probe . relation_formula_input . hasFocus (), "relation formula input should keep focus after popup Backspace" )
global_probe = _PropertyTableProbe ()
global_probe . model = _GlobalRelationCompletionModel ()
global_probe . selected_kind = "face"
global_probe . selected_face_id = 0
global_probe . show ()
global_probe . _refresh_property_editor ()
global_probe . relation_formula_input . setFocus ( Qt . FocusReason . OtherFocusReason )
QApplication . processEvents ()
global_probe . model . face_region_call_count = 0
global_probe . relation_formula_input . setText ( "Face" )
global_probe . relation_formula_input . setCursorPosition ( len ( "Face" ))
global_probe . _update_relation_formula_completions ()
global_probe . _show_relation_formula_completion_popup ()
QApplication . processEvents ()
global_face_completion_list = list ( global_probe . relation_formula_completer_model . stringList ())
global_face_completions = set ( global_face_completion_list )
_assert ( "Face85" in global_face_completions , f "global Face completion should include Face85: { global_face_completions } " )
_assert ( "Face85." not in global_face_completions , f "object completion should not show trailing dot: { global_face_completions } " )
_assert (
global_face_completion_list . index ( "Face2" ) < global_face_completion_list . index ( "Face10" ) < global_face_completion_list . index ( "Face85" ),
f "Face completions should be sorted by numeric ID: { global_face_completion_list [: 20 ] } " ,
)
global_probe . relation_formula_input . setText ( "Face8" )
global_probe . relation_formula_input . setCursorPosition ( len ( "Face8" ))
global_probe . _update_relation_formula_completions ()
face8_completion_list = list ( global_probe . relation_formula_completer_model . stringList ())
_assert ( "Face87" in face8_completion_list , f "Face8 should still suggest longer Face IDs: { face8_completion_list [: 20 ] } " )
_assert (
face8_completion_list [ 0 ] != "Face8" ,
f "Face8 completion should prefer longer IDs instead of no-op self completion: { face8_completion_list [: 20 ] } " ,
)
_assert (
face8_completion_list . index ( "Face80" ) < face8_completion_list . index ( "Face87" ),
f "Face8 completions should keep numeric order: { face8_completion_list [: 20 ] } " ,
)
face8_tab_probe = _PropertyTableProbe ()
face8_tab_probe . model = _GlobalRelationCompletionModel ()
face8_tab_probe . selected_kind = "face"
face8_tab_probe . selected_face_id = 0
face8_tab_probe . _refresh_property_editor ()
face8_tab_probe . relation_formula_input . setText ( "Face8" )
face8_tab_probe . relation_formula_input . setCursorPosition ( len ( "Face8" ))
_assert ( face8_tab_probe . _accept_relation_formula_completion (), "Tab should accept a longer Face8 completion" )
_assert (
face8_tab_probe . relation_formula_input . text () . startswith ( "Face8" )
and face8_tab_probe . relation_formula_input . text () != "Face8" ,
f "Tab after Face8 should complete to a longer ID, not stay unchanged: { face8_tab_probe . relation_formula_input . text () } " ,
)
face_tab_probe = _RelationFormulaEventProbe ()
face_tab_probe . model = _GlobalRelationCompletionModel ()
face_tab_probe . selected_kind = "face"
face_tab_probe . selected_face_id = 0
face_tab_probe . show ()
face_tab_probe . _refresh_property_editor ()
face_tab_probe . relation_formula_input . setFocus ( Qt . FocusReason . OtherFocusReason )
QApplication . processEvents ()
face_tab_probe . relation_formula_input . setText ( "F" )
face_tab_probe . relation_formula_input . setCursorPosition ( len ( "F" ))
face_tab_event = QKeyEvent ( QEvent . Type . ShortcutOverride , Qt . Key . Key_Tab , Qt . KeyboardModifier . NoModifier )
_assert ( face_tab_probe . eventFilter ( face_tab_probe . relation_formula_input , face_tab_event ), "F + Tab should be consumed" )
for _index in range ( 4 ):
QApplication . processEvents ()
_assert ( face_tab_probe . relation_formula_input . text () == "Face" , f "F + Tab should complete to Face: { face_tab_probe . relation_formula_input . text () } " )
_assert ( "Face87" in set ( face_tab_probe . relation_formula_completer_model . stringList ()), "Face layer should suggest Face87 after F + Tab" )
_assert ( face_tab_probe . relation_formula_completer . popup () . isVisible (), "Face layer popup should open after F + Tab" )
_assert (
face_tab_probe . relation_formula_completer . popup () . minimumWidth ()
>= max ( face_tab_probe . relation_formula_input . width (), 320 ),
"Face completion popup should be wide enough to show text" ,
)
face_manual_probe = _RelationFormulaEventProbe ()
face_manual_probe . model = _GlobalRelationCompletionModel ()
face_manual_probe . selected_kind = "face"
face_manual_probe . selected_face_id = 0
face_manual_probe . show ()
face_manual_probe . _refresh_property_editor ()
face_manual_probe . relation_formula_input . setFocus ( Qt . FocusReason . OtherFocusReason )
QApplication . processEvents ()
face_manual_probe . relation_formula_input . setText ( "Face" )
face_manual_probe . relation_formula_input . setCursorPosition ( len ( "Face" ))
for _index in range ( 4 ):
QApplication . processEvents ()
_assert ( "Face87" in set ( face_manual_probe . relation_formula_completer_model . stringList ()), "manual Face input should suggest Face87" )
_assert ( face_manual_probe . relation_formula_completer . popup () . isVisible (), "manual Face input should open the next completion layer" )
_assert (
face_manual_probe . relation_formula_completer . popup () . minimumWidth ()
>= max ( face_manual_probe . relation_formula_input . width (), 320 ),
"manual Face popup should not collapse to cursor width" ,
)
global_probe . relation_formula_input . setText ( "Face87" )
global_probe . relation_formula_input . setCursorPosition ( len ( "Face87" ))
global_probe . _update_relation_formula_completions ()
face87_completion_list = list ( global_probe . relation_formula_completer_model . stringList ())
face87_ref = next (( item for item in face87_completion_list if str ( item ) . startswith ( "Face87." )), "" )
_assert ( face87_ref , f "exact Face ID should suggest editable parameters: { face87_completion_list } " )
face87_radius_ref = next (( item for item in face87_completion_list if str ( item ) . endswith ( "半径" )), "" )
_assert ( face87_radius_ref , f "exact Face ID should suggest radius: { face87_completion_list } " )
radius_probe = _PropertyTableProbe ()
radius_probe . model = _GlobalRelationCompletionModel ()
radius_probe . selected_kind = "face"
radius_probe . selected_face_id = 0
radius_probe . show ()
radius_probe . _refresh_property_editor ()
radius_probe . relation_formula_input . setFocus ( Qt . FocusReason . OtherFocusReason )
QApplication . processEvents ()
radius_probe . relation_formula_input . setText ( "Face87" )
radius_probe . relation_formula_input . setCursorPosition ( len ( "Face87" ))
radius_probe . _update_relation_formula_completions ()
radius_probe . _show_relation_formula_completion_popup ()
QApplication . processEvents ()
radius_completion_list = list ( radius_probe . relation_formula_completer_model . stringList ())
radius_row = radius_completion_list . index ( face87_radius_ref )
radius_model = radius_probe . relation_formula_completer . model ()
radius_popup = radius_probe . relation_formula_completer . popup ()
radius_probe . relation_formula_completer . setCurrentRow ( radius_row )
radius_popup . setCurrentIndex ( radius_model . index ( radius_row , 0 ))
_assert ( radius_probe . _accept_relation_formula_completion (), "Tab should accept the popup-selected radius completion" )
_assert (
radius_probe . relation_formula_input . text () == face87_radius_ref ,
f "Tab should keep the popup-selected completion, not reset to default: { radius_probe . relation_formula_input . text () } " ,
)
global_probe . relation_formula_input . setText ( face87_ref )
global_probe . relation_formula_input . setCursorPosition ( len ( face87_ref ))
global_probe . _update_relation_formula_completions ()
_assert ( " = " in set ( global_probe . relation_formula_completer_model . stringList ()), "complete Face parameter should suggest '='" )
global_probe . _on_relation_formula_completion_activated ( " = " )
_assert (
global_probe . relation_formula_input . text () == f " { face87_ref } = " ,
f "mouse activation should insert '=' without replacing the whole formula: { global_probe . relation_formula_input . text () } " ,
)
global_probe . relation_formula_input . setText ( face87_ref )
global_probe . relation_formula_input . setCursorPosition ( len ( face87_ref ))
global_probe . _update_relation_formula_completions ()
global_probe . relation_formula_input . setText ( f " { face87_ref } = F" )
global_probe . relation_formula_input . setCursorPosition ( len ( f " { face87_ref } = F" ))
global_probe . _update_relation_formula_completions ()
rhs_f_completion_list = list ( global_probe . relation_formula_completer_model . stringList ())
_assert (
"Face85" in rhs_f_completion_list and "Face87" in rhs_f_completion_list ,
f "right-hand expression should suggest global Face IDs after F: { rhs_f_completion_list [: 20 ] } " ,
)
global_probe . relation_formula_input . setText ( f " { face87_ref } = Face8" )
global_probe . relation_formula_input . setCursorPosition ( len ( f " { face87_ref } = Face8" ))
global_probe . _update_relation_formula_completions ()
rhs_face8_completion_list = list ( global_probe . relation_formula_completer_model . stringList ())
_assert ( "Face87" in rhs_face8_completion_list , f "right-hand Face8 should suggest Face87: { rhs_face8_completion_list [: 20 ] } " )
global_probe . relation_formula_input . setText ( f " { face87_ref } = Face87" )
global_probe . relation_formula_input . setCursorPosition ( len ( f " { face87_ref } = Face87" ))
global_probe . _update_relation_formula_completions ()
rhs_face87_completion_list = list ( global_probe . relation_formula_completer_model . stringList ())
_assert (
any ( str ( item ) . startswith ( "Face87." ) for item in rhs_face87_completion_list ),
f "right-hand exact Face ID should suggest parameters: { rhs_face87_completion_list } " ,
)
global_probe . relation_formula_input . setText ( "Edge" )
global_probe . relation_formula_input . setCursorPosition ( len ( "Edge" ))
global_probe . _update_relation_formula_completions ()
global_edge_completion_list = list ( global_probe . relation_formula_completer_model . stringList ())
global_edge_completions = set ( global_edge_completion_list )
_assert ( "Edge0" in global_edge_completions , f "Edge completion should be available even in Face selection mode: { global_edge_completions } " )
_assert (
global_edge_completion_list . index ( "Edge2" ) < global_edge_completion_list . index ( "Edge10" ),
f "Edge completions should be sorted by numeric ID: { global_edge_completion_list } " ,
)
_assert (
global_probe . model . face_region_call_count == 0 ,
f "relation formula typing should not call heavy face_region_logical_id: { global_probe . model . face_region_call_count } " ,
)
large_probe = _PropertyTableProbe ()
large_probe . model = _LargeRelationCompletionModel ()
large_probe . selected_kind = "face"
large_probe . selected_face_id = 0
large_probe . _refresh_property_editor ()
large_probe . model . face_region_call_count = 0
large_probe . relation_formula_input . setText ( "F" )
large_probe . relation_formula_input . setCursorPosition ( len ( "F" ))
large_probe . _update_relation_formula_completions ()
large_completions = list ( large_probe . relation_formula_completer_model . stringList ())
_assert ( "Face87" in large_completions , f "large model F completion should still include common Face IDs: { large_completions [: 20 ] } " )
_assert (
len ( large_completions ) <= RELATION_FORMULA_OBJECT_COMPLETION_LIMIT + 2 ,
f "large model broad completion should be capped: { len ( large_completions ) } " ,
)
_assert (
large_probe . model . face_region_call_count == 0 ,
f "large model completion should not call heavy face_region_logical_id: { large_probe . model . face_region_call_count } " ,
)
probe . relation_formula_input . setText ( "Face0." )
probe . relation_formula_input . setCursorPosition ( len ( "Face0." ))
probe . _update_relation_formula_completions ()
face_completions = set ( probe . relation_formula_completer_model . stringList ())
_assert ( "Face0.面内长度" in face_completions , f "relation formula should suggest parameters after Face0.: { face_completions } " )
probe . relation_formula_input . setText ( "Face0.面内宽度" )
probe . relation_formula_input . setCursorPosition ( len ( "Face0.面内宽度" ))
probe . _update_relation_formula_completions ()
_assert ( " = " in set ( probe . relation_formula_completer_model . stringList ()), "relation formula should suggest '=' after a target parameter" )
2026-08-14 18:42:39 +08:00
probe . relation_formula_input . setText ( "Face0.面内宽度 = Face0.面内长度 * 1.2" )
probe . add_relation_formula ()
_assert ( len ( probe . relation_formula_items ) == 1 , f "formula was not stored: { probe . relation_formula_items } " )
_assert ( probe . relation_formula_list . count () == 1 , "formula list should display the stored formula" )
for _index in range ( 6 ):
QApplication . processEvents ()
2026-08-17 18:53:03 +08:00
if not getattr ( probe , "relation_formula_replay_active" , False ):
2026-08-14 18:42:39 +08:00
break
_assert (
probe . executed_property_actions == [( "resize_face_height_local" , "12" )],
2026-08-17 18:53:03 +08:00
f "formula should immediately fill target value and execute the existing row action: { probe . executed_property_actions } " ,
2026-08-14 18:42:39 +08:00
)
width_row = _row_by_label ( probe , "面内宽度" )
width_widget = probe . property_table . cellWidget ( width_row , PROPERTY_TARGET_COLUMN )
_assert ( isinstance ( width_widget , QLineEdit ), "formula target row should still have a target editor" )
_assert ( width_widget . text () . strip () == "12" , "formula result should be written back to the target value cell" )
_assert ( str ( probe . relation_formula_items [ 0 ] . get ( "status" )) == "applied" , "formula should be marked as applied" )
2026-08-17 18:53:03 +08:00
for _index in range ( 4 ):
QApplication . processEvents ()
_assert ( probe . relation_formula_input . isEnabled (), "relation formula input should stay enabled after one formula is applied" )
probe . relation_formula_input . setText ( "F" )
_assert ( probe . add_relation_formula_button . isEnabled (), "existing formulas should not disable adding another formula" )
def _assert_relation_radius_formula_proxy () -> None :
probe = _PropertyTableProbe ()
probe . model = _GlobalRelationCompletionModel ()
probe . selected_kind = "face"
probe . selected_face_id = 87
probe . property_table . setRowCount ( 1 )
probe . property_editor_specs = [
{
"key" : "diameter" ,
"label" : "直径" ,
"current_raw" : 1.0 ,
"current_text" : "1" ,
"target_text" : "1" ,
"editable" : True ,
"enabled" : True ,
"action" : "resize_hole" ,
"value_type" : "number" ,
"source_face_id" : 87 ,
}
]
probe . property_table . setItem ( 0 , PROPERTY_LABEL_COLUMN , QTableWidgetItem ( "直径" ))
target = QLineEdit ( "1" )
probe . property_table . setCellWidget ( 0 , PROPERTY_TARGET_COLUMN , target )
ref = ObjectParameterRef ( "Face" , 87 , "半径" )
visible = probe . _relation_visible_spec_for_ref ( ref )
_assert ( visible is not None , "Face radius formula should fall back to an executable diameter row" )
_assert (
abs ( float ( probe . _relation_value_for_ref ( ref )) - 0.5 ) <= 1e-9 ,
f "radius formula should read half of the diameter: { probe . _relation_value_for_ref ( ref ) } " ,
)
row = probe . _set_relation_target_value ( ref , 0.6 )
_assert ( row == 0 , f "radius proxy should write to the diameter row: { row } " )
_assert ( target . text () == "1.2" , f "radius proxy should convert target radius to diameter: { target . text () } " )
def _assert_relation_formula_input_remains_editable_while_replaying () -> None :
probe = _PropertyTableProbe ()
probe . model = _GlobalRelationCompletionModel ()
probe . selected_kind = "face"
probe . selected_face_id = 87
probe . _refresh_property_editor ()
probe . operation_in_progress = True
probe . relation_formula_replay_active = True
probe . relation_formula_replay_queue = []
probe . relation_formula_replay_total = 1
probe . relation_formula_input . setEnabled ( False )
probe . _update_relation_formula_buttons ()
_assert ( probe . relation_formula_input . isEnabled (), "relation formula input should stay editable while formulas replay" )
probe . relation_formula_input . setText ( "Face87.半径 = Face85.半径" )
probe . add_relation_formula ()
_assert ( len ( probe . relation_formula_items ) == 1 , f "formula should be stored while replaying: { probe . relation_formula_items } " )
_assert ( len ( probe . relation_formula_replay_queue ) == 1 , f "formula should be queued while replaying: { probe . relation_formula_replay_queue } " )
_assert ( probe . relation_formula_input . text () == "" , "queued formula should clear the input for the next formula" )
_assert ( probe . relation_formula_input . isEnabled (), "relation formula input should remain enabled after queuing" )
_assert ( probe . relation_formula_replay_active , "adding a queued formula should not stop the current replay" )
def _assert_relation_formula_input_clickable_after_existing_formula () -> None :
probe = _RelationFormulaEventProbe ()
probe . relation_formula_input . installEventFilter ( probe )
probe . show ()
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 click test: { probe . relation_formula_items } " )
_assert ( str ( probe . relation_formula_items [ 0 ] . get ( "status" )) == "applied" , "formula should be applied before click test" )
probe . activateWindow ()
probe . raise_ ()
QApplication . processEvents ()
QTest . mouseClick ( probe . relation_formula_input , Qt . MouseButton . LeftButton )
QApplication . processEvents ()
_assert ( probe . relation_formula_input . isEnabled (), "relation formula input should stay enabled with an existing formula" )
_assert ( probe . relation_formula_input . hasFocus (), "clicking the relation formula input should focus it with an existing formula" )
probe . relation_formula_input . setText ( "F" )
_assert ( probe . add_relation_formula_button . isEnabled (), "clickable input should allow adding another formula" )
def _assert_relation_formula_input_is_selection_independent () -> None :
probe = _RelationFormulaEventProbe ()
probe . relation_formula_input . installEventFilter ( probe )
probe . model = _GlobalRelationCompletionModel ()
probe . selected_kind = None
probe . selected_face_id = None
probe . selected_edge_id = None
probe . relation_formula_items = []
probe . relation_formula_input . setEnabled ( False )
probe . _refresh_relation_formula_list ()
_assert ( probe . relation_formula_input . isEnabled (), "relation formula input should be editable without a selected face" )
_assert ( not probe . relation_formula_input . isReadOnly (), "relation formula input should not become read-only without a selected face" )
_assert ( not probe . add_relation_formula_button . isEnabled (), "empty relation formula should not enable the add button" )
probe . show ()
probe . activateWindow ()
probe . raise_ ()
QApplication . processEvents ()
QTest . mouseClick ( probe . relation_formula_input , Qt . MouseButton . LeftButton )
QApplication . processEvents ()
probe . relation_formula_input . setText ( "Face8" )
probe . relation_formula_input . setCursorPosition ( len ( "Face8" ))
probe . _update_relation_formula_completions ()
object_completions = list ( probe . relation_formula_completer_model . stringList ())
_assert ( "Face85" in object_completions and "Face87" in object_completions , f "global face completions should not require selection: { object_completions [: 20 ] } " )
probe . relation_formula_input . setText ( "Face85" )
probe . relation_formula_input . setCursorPosition ( len ( "Face85" ))
probe . _update_relation_formula_completions ()
parameter_completions = list ( probe . relation_formula_completer_model . stringList ())
_assert ( any ( str ( item ) . startswith ( "Face85." ) for item in parameter_completions ), f "Face parameter completions should not require selection: { parameter_completions } " )
def _assert_relation_formula_input_recovers_after_loading () -> None :
probe = _RelationFormulaEventProbe ()
probe . model = _GlobalRelationCompletionModel ()
probe . mode_combo = NoWheelComboBox ()
probe . mode_combo . addItem ( "Face" , "Face" )
probe . load_thread = None
probe . load_refine_thread = None
probe . pending_load_path = None
probe . load_in_progress = True
probe . _update_relation_formula_buttons ()
_assert ( not probe . relation_formula_input . isEnabled (), "relation formula input should be disabled while loading" )
probe . _end_load_task ()
_assert ( probe . relation_formula_input . isEnabled (), "relation formula input should recover after loading without selecting a face" )
def _assert_relation_formula_ids_follow_model_remap () -> None :
old_model = _RelationFormulaRemapModel (
{
85 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 9.5 ), "bbox_center" : ( 0.5 , 1.0 , 9.5 ), "radius" : 0.25 , "area" : 8.0 },
87 : { "surface" : "cylinder" , "area_center" : ( 2.0 , 1.0 , 9.5 ), "bbox_center" : ( 2.0 , 1.0 , 9.5 ), "radius" : 0.25 , "area" : 8.0 },
}
)
new_model = _RelationFormulaRemapModel (
{
90 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 9.5 ), "bbox_center" : ( 0.5 , 1.0 , 9.5 ), "radius" : 0.25 , "area" : 8.0 },
92 : { "surface" : "cylinder" , "area_center" : ( 2.0 , 1.0 , 9.5 ), "bbox_center" : ( 2.0 , 1.0 , 9.5 ), "radius" : 0.15 , "area" : 5.0 },
}
)
probe = _PropertyTableProbe ()
formula_text = "Face87.直径 = Face85.半径"
probe . model = old_model
formula = parse_relation_formula ( formula_text )
probe . relation_formula_items = [
{
"id" : 1 ,
"text" : formula_text ,
"enabled" : True ,
"status" : "applied" ,
"message" : "已修改模型。" ,
"signatures" : probe . _relation_signatures_for_formula ( formula ),
}
]
probe . model = new_model
note = probe . _refresh_relation_formulas_after_model_edit ()
_assert (
probe . relation_formula_items [ 0 ][ "text" ] == "Face92.直径 = Face90.半径" ,
f "relation formula should follow remapped visible Face IDs: { probe . relation_formula_items } " ,
)
_assert ( "Face ID" in note , f "relation remap note should mention updated Face IDs: { note } " )
old_model = _RelationFormulaRemapModel (
{
85 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 9.5 ), "bbox_center" : ( 0.5 , 1.0 , 9.5 ), "radius" : 0.25 , "area" : 8.0 },
87 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 0.5 ), "bbox_center" : ( 0.5 , 1.0 , 0.5 ), "radius" : 0.25 , "area" : 8.0 },
}
)
new_model = _RelationFormulaRemapModel (
{
85 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 9.5 ), "bbox_center" : ( 0.5 , 1.0 , 9.5 ), "radius" : 0.125 , "area" : 4.0 },
11 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 0.5 ), "bbox_center" : ( 0.5 , 1.0 , 0.5 ), "radius" : 0.25 , "area" : 8.0 },
}
)
probe = _PropertyTableProbe ()
formula_text = "Face85.直径 = Face87.半径"
probe . model = old_model
formula = parse_relation_formula ( formula_text )
probe . relation_formula_items = [
{
"id" : 1 ,
"text" : formula_text ,
"enabled" : True ,
"status" : "applied" ,
"message" : "已修改模型。" ,
"signatures" : probe . _relation_signatures_for_formula ( formula ),
}
]
probe . model = new_model
probe . _refresh_relation_formulas_after_model_edit ()
_assert (
probe . relation_formula_items [ 0 ][ "text" ] == "Face85.直径 = Face11.半径" ,
f "relation formula should remap a changed reference ID even when the target ID is preserved: { probe . relation_formula_items } " ,
)
old_model = _RelationFormulaRemapModel (
{
85 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 9.5 ), "bbox_center" : ( 0.5 , 1.0 , 9.5 ), "radius" : 0.25 , "area" : 8.0 },
87 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 0.5 ), "bbox_center" : ( 0.5 , 1.0 , 0.5 ), "radius" : 0.25 , "area" : 8.0 },
}
)
new_model = _RelationFormulaRemapModel (
{
85 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 0.5 ), "bbox_center" : ( 0.5 , 1.0 , 0.5 ), "radius" : 0.5 , "area" : 16.0 },
89 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 9.5 ), "bbox_center" : ( 0.5 , 1.0 , 9.5 ), "radius" : 0.125 , "area" : 4.0 },
91 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 0.5 ), "bbox_center" : ( 0.5 , 1.0 , 0.5 ), "radius" : 0.25 , "area" : 8.0 },
}
)
new_model . face_logical_ids [ 89 ] = 85
probe = _PropertyTableProbe ()
formula_text = "Face85.直径 = Face87.半径"
probe . model = old_model
formula = parse_relation_formula ( formula_text )
probe . relation_formula_items = [
{
"id" : 1 ,
"text" : formula_text ,
"enabled" : True ,
"status" : "applied" ,
"message" : "已修改模型。" ,
"signatures" : probe . _relation_signatures_for_formula ( formula ),
}
]
probe . model = new_model
probe . _refresh_relation_formulas_after_model_edit ()
_assert (
probe . relation_formula_items [ 0 ][ "text" ] == "Face85.直径 = Face91.半径" ,
f "relation remap should prefer the old reference radius over a same-center changed target: { probe . relation_formula_items } " ,
)
old_model = _RelationFormulaRemapModel (
{
9 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 9.5 ), "bbox_center" : ( 0.5 , 1.0 , 9.5 ), "radius" : 0.25 , "area" : 8.0 },
87 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 0.5 ), "bbox_center" : ( 0.5 , 1.0 , 0.5 ), "radius" : 0.15 , "area" : 4.8 },
}
)
new_model = _RelationFormulaRemapModel (
{
14 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 9.5 ), "bbox_center" : ( 0.5 , 1.0 , 9.5 ), "radius" : 0.4 , "area" : 12.8 },
87 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 0.5 ), "bbox_center" : ( 0.5 , 1.0 , 0.5 ), "radius" : 0.15 , "area" : 4.8 },
}
)
new_model . face_logical_ids [ 14 ] = 9
probe = _PropertyTableProbe ()
formula_text = "Face87.直径 = Face9.半径"
probe . model = old_model
formula = parse_relation_formula ( formula_text )
probe . relation_formula_items = [
{
"id" : 1 ,
"text" : formula_text ,
"enabled" : True ,
"status" : "applied" ,
"message" : "已修改模型。" ,
"signatures" : probe . _relation_signatures_for_formula ( formula ),
}
]
probe . model = new_model
probe . _refresh_relation_formulas_after_model_edit (
context = { "target_kind" : "face" , "target_id" : 9 , "target_logical_id" : 9 }
)
_assert (
probe . relation_formula_items [ 0 ][ "text" ] == "Face87.直径 = Face9.半径" ,
f "edited right-hand dependency should keep its logical Face ID instead of becoming invalid: { probe . relation_formula_items } " ,
)
_assert (
str ( probe . relation_formula_items [ 0 ] . get ( "status" )) != "invalid" ,
f "edited right-hand dependency should remain valid: { probe . relation_formula_items } " ,
)
_assert (
getattr ( probe , "_last_relation_formula_refresh_affected_ids" , []) == [ 1 ],
f "edited right-hand dependency should mark the formula for reapply: { probe . relation_formula_items } " ,
)
old_model = _RelationFormulaRemapModel (
{
9 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 9.5 ), "bbox_center" : ( 0.5 , 1.0 , 9.5 ), "radius" : 0.25 , "area" : 8.0 },
87 : { "surface" : "cylinder" , "area_center" : ( 2.0 , 1.0 , 9.5 ), "bbox_center" : ( 2.0 , 1.0 , 9.5 ), "radius" : 0.125 , "area" : 4.0 },
}
)
new_model = _RelationFormulaRemapModel (
{
1 : { "surface" : "cylinder" , "area_center" : ( 2.0 , 1.0 , 9.5 ), "bbox_center" : ( 2.0 , 1.0 , 9.5 ), "radius" : 0.5 , "area" : 16.0 },
9 : { "surface" : "cylinder" , "area_center" : ( 0.5 , 1.0 , 9.5 ), "bbox_center" : ( 0.5 , 1.0 , 9.5 ), "radius" : 0.4 , "area" : 12.8 },
11 : { "surface" : "cylinder" , "area_center" : ( 2.0 , 1.0 , 9.5 ), "bbox_center" : ( 2.0 , 1.0 , 9.5 ), "radius" : 0.125 , "area" : 4.0 },
}
)
new_model . face_logical_ids [ 9 ] = 9
probe = _PropertyTableProbe ()
formula_text = "Face87. \u76f4\u5f84 = Face9. \u534a\u5f84 "
probe . model = old_model
formula = parse_relation_formula ( formula_text )
probe . relation_formula_items = [
{
"id" : 1 ,
"text" : formula_text ,
"enabled" : True ,
"status" : "applied" ,
"message" : "applied" ,
"signatures" : probe . _relation_signatures_for_formula ( formula ),
}
]
probe . model = new_model
probe . _refresh_relation_formulas_after_model_edit (
context = { "target_kind" : "face" , "target_id" : 9 , "target_logical_id" : 9 }
)
_assert (
probe . relation_formula_items [ 0 ][ "text" ] == "Face11. \u76f4\u5f84 = Face9. \u534a\u5f84 " ,
f "unchanged formula target should stay strict while a dependency changes: { probe . relation_formula_items } " ,
)
_assert (
str ( probe . relation_formula_items [ 0 ] . get ( "status" )) != "invalid" ,
f "strict target remap should keep the dependency formula valid: { probe . relation_formula_items } " ,
)
_assert (
getattr ( probe , "_last_relation_formula_refresh_affected_ids" , []) == [ 1 ],
f "edited dependency should still queue the formula for reapply after target remap: { probe . relation_formula_items } " ,
)
def _unexpected_restore () -> bool :
raise AssertionError ( "dependency relation replay must not restore the formula base snapshot" )
probe . relation_formula_base_snapshot = { "sentinel" : object ()}
probe . _restore_relation_formula_base_snapshot = _unexpected_restore
probe . _start_relation_formula_reapply ( reason = "dependency" , restore_base = False , formula_ids = [ 1 ])
_assert (
[ int ( item . get ( "id" , - 1 )) for item in probe . relation_formula_replay_queue ] == [ 1 ],
f "dependency replay should queue only the affected formula: { probe . relation_formula_replay_queue } " ,
)
probe . relation_formula_replay_active = False
probe . relation_formula_replay_queue = []
2026-08-14 18:42:39 +08:00
2026-08-07 18:08:32 +08:00
def _assert_mouse_selection_guards () -> None :
mouse_probe = _MouseSelectionProbe ()
mouse_probe . _handle_left_button_press ( 20 , 20 )
mouse_probe . _handle_left_button_release ( 20 , 20 )
2026-08-06 18:15:14 +08:00
_assert (
2026-08-07 18:08:32 +08:00
[ target . get ( "target_id" ) for target in mouse_probe . selected_targets ] == [ 1 ],
"plain left click should still select" ,
2026-08-06 18:15:14 +08:00
)
2026-08-07 18:08:32 +08:00
mouse_probe = _MouseSelectionProbe ()
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 ( not mouse_probe . selected_targets , "left-button drag should not select a face on release" )
2026-08-06 18:15:14 +08:00
2026-08-07 18:08:32 +08:00
mouse_probe = _MouseSelectionProbe ()
mouse_probe . _handle_left_button_press ( 20 , 20 )
mouse_probe . camera_state = ( 0.5 , 0.0 , 9.8 , 0.0 , 0.0 , 0.0 , 0.02 , 1.0 , 0.0 , 1.0 , 9.8 , 30.0 )
mouse_probe . _handle_left_button_release ( 22 , 21 )
2026-08-06 18:15:14 +08:00
_assert (
2026-08-07 18:08:32 +08:00
not mouse_probe . selected_targets ,
"left-button camera rotation should not select a face even when the cursor lands on the model" ,
2026-08-06 18:15:14 +08:00
)
2026-08-07 18:08:32 +08:00
mouse_probe = _MouseSelectionProbe ()
mouse_probe . pick_targets = [ None , { "kind" : "face" , "target_id" : 1 , "pick_position" : ( 0.0 , 0.0 , 0.0 )}]
mouse_probe . _handle_left_button_press ( 20 , 20 )
mouse_probe . _handle_left_button_release ( 20 , 20 )
2026-08-06 18:15:14 +08:00
_assert (
2026-08-07 18:08:32 +08:00
not mouse_probe . selected_targets ,
"left-button press on the background should not select a face on release" ,
2026-08-06 18:15:14 +08:00
)
2026-08-07 18:08:32 +08:00
mouse_probe = _MouseSelectionProbe ()
mouse_probe . pick_targets = [
{ "kind" : "face" , "target_id" : 1 , "pick_position" : ( 0.0 , 0.0 , 0.0 )},
{ "kind" : "face" , "target_id" : 2 , "pick_position" : ( 0.0 , 0.0 , 0.0 )},
]
mouse_probe . _handle_left_button_press ( 20 , 20 )
mouse_probe . _handle_left_button_release ( 20 , 20 )
2026-08-06 18:15:14 +08:00
_assert (
2026-08-07 18:08:32 +08:00
not mouse_probe . selected_targets ,
"left-button press/release on different faces should not change selection" ,
2026-08-06 18:15:14 +08:00
)
2026-08-19 10:28:09 +08:00
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" ,
)
2026-08-06 18:15:14 +08:00
2026-08-07 18:08:32 +08:00
def _assert_quick_blind_depth_spec () -> None :
blind_probe = _PropertyTableProbe ()
blind_probe . selected_kind = "feature"
blind_probe . selected_face_id = 6
quick_blind_info = {
"surface" : "cylinder" ,
"diameter" : 4.0 ,
"radius" : 2.0 ,
"axis_point" : ( 0.0 , 0.0 , 0.0 ),
"axis" : ( 0.0 , 0.0 , 1.0 ),
2026-08-14 18:42:39 +08:00
"axis_center" : ( 0.0 , 0.0 , 3.0 ),
2026-08-07 18:08:32 +08:00
"angular_span" : math . tau ,
"feature_guess" : "hole/groove candidate" ,
"feature_type" : "圆柱孔候选" ,
"confidence" : "medium" ,
"cylinder_end_type" : "blind" ,
"hole_depth_estimate" : 6.0 ,
"depth_status" : "ready" ,
"recognition_ready_actions" : "孔/槽/圆柱直径;盲孔/盲槽深度;封堵孔/槽" ,
}
blind_specs , _blind_used = blind_probe . _editable_property_specs ( quick_blind_info )
blind_feature_specs = blind_probe . _feature_property_specs ( blind_specs , quick_blind_info )
2026-08-14 18:42:39 +08:00
blind_axis_specs = [
spec for spec in blind_feature_specs if str ( spec . get ( "key" , "" )) == "hole_axis_center"
]
_assert ( blind_axis_specs , "quick blind hole axis center should be visible in feature parameters" )
blind_axis_spec = blind_axis_specs [ 0 ]
_assert ( str ( blind_axis_spec . get ( "value_type" , "" )) == "vector3" , "hole axis center should accept X/Y/Z" )
blind_axis_effective = blind_probe . _effective_property_spec ( blind_axis_spec )
_assert ( bool ( blind_axis_effective . get ( "enabled" )), "quick blind hole axis center should be editable" )
_assert (
str ( blind_axis_effective . get ( "action" , "" )) == "move_cylindrical_hole_axis" ,
f "quick blind hole axis center should move the hole itself: { blind_axis_effective } " ,
)
2026-08-07 18:08:32 +08:00
blind_depth_specs = [
spec for spec in blind_feature_specs if str ( spec . get ( "key" , "" )) == "hole_depth_estimate"
]
_assert ( blind_depth_specs , "quick blind hole depth estimate should be visible in feature parameters" )
blind_depth_spec = blind_depth_specs [ 0 ]
blind_effective_depth = blind_probe . _effective_property_spec ( blind_depth_spec )
_assert ( bool ( blind_effective_depth . get ( "enabled" )), "quick blind hole depth should be editable" )
_assert (
str ( blind_effective_depth . get ( "action" , "" )) == "resize_hole_depth" ,
f "quick blind hole depth should use local depth edit first: { blind_effective_depth } " ,
)
_assert (
"重新确认底面" in str ( blind_effective_depth . get ( "enabled_tip" , "" )),
"quick blind hole depth should explain execution-time bottom-face confirmation" ,
)
owning_mode = dict ( blind_depth_spec . get ( "scope_modes" , {})) . get ( "owning" , {})
_assert (
not bool ( owning_mode . get ( "enabled" )),
"quick blind hole without explicit bottom faces should not expose owning-scale depth as editable" ,
)
2026-08-06 18:15:14 +08:00
2026-08-07 18:08:32 +08:00
def _assert_user_facing_failure_messages () -> None :
action_probe = _ActionMessageProbe ()
illegal_context = {
"operation_name" : "测试修改" ,
"target" : "Face 1" ,
"parameters" : { "resize_status" : "blocked" , "resize_blockers" : "目标值必须大于 0。" },
}
illegal_blocker = action_probe . _edit_preflight_blocker ( illegal_context )
_assert ( illegal_blocker is not None , "blocked edit plan should be stopped before worker startup" )
_assert ( illegal_blocker [ 0 ] == "当前操作不合法" , "illegal blocked edit should be classified clearly" )
english_illegal_context = {
"operation_name" : "调整槽宽" ,
"target" : "Face 3" ,
"parameters" : { "resize_status" : "blocked" , "resize_blockers" : "Target slot value must be greater than 0." },
}
english_illegal_blocker = action_probe . _edit_preflight_blocker ( english_illegal_context )
2026-08-06 18:15:14 +08:00
_assert (
2026-08-07 18:08:32 +08:00
english_illegal_blocker is not None and english_illegal_blocker [ 0 ] == "当前操作不合法" ,
"english geometry blockers should also be classified as illegal operations" ,
)
risk_blocker = action_probe . _plan_preflight_blocker (
{ "status" : "blocked" , "risk" : "blocked" , "message" : "目标壳体厚度会让几何风险过高。" }
)
_assert ( risk_blocker is not None and risk_blocker [ 0 ] == "风险过高" , "blocked high-risk plans should be classified" )
recognition_blocker = action_probe . _plan_preflight_blocker (
{ "status" : "blocked" , "message" : "当前平面没有识别到相对壳体平面。" }
2026-08-06 18:15:14 +08:00
)
_assert (
2026-08-07 18:08:32 +08:00
recognition_blocker is not None and recognition_blocker [ 0 ] == "识别不足" ,
"blocked recognition failures should be classified" ,
)
unsupported_blocker = action_probe . _plan_preflight_blocker (
{ "status" : "blocked" , "message" : "当前版本只对简单圆锥解析重建开放这类修改。" }
2026-08-06 18:15:14 +08:00
)
_assert (
2026-08-07 18:08:32 +08:00
unsupported_blocker is not None and unsupported_blocker [ 0 ] == "暂未实现" ,
"blocked unsupported capability plans should be classified" ,
2026-08-06 18:15:14 +08:00
)
2026-08-07 18:08:32 +08:00
auxiliary_context = {
"operation_name" : "测试修改" ,
"target" : "Face 4" ,
"parameters" : { "quick_plan_status" : "blocked" , "resize_status" : "ready" , "resize_blockers" : "" },
}
_assert ( action_probe . _edit_preflight_blocker ( auxiliary_context ) is None , "auxiliary statuses should not block ready edits" )
2026-08-06 18:15:14 +08:00
2026-08-07 18:08:32 +08:00
deferred_context = {
"operation_name" : "复杂 Face 修改" ,
"target" : "Face 2" ,
"parameters" : { "ui_deferred_model_plan" : True , "message" : "当前对象需要完整一级关系计划。" },
}
deferred_blocker = action_probe . _edit_preflight_blocker ( deferred_context )
_assert ( deferred_blocker is not None , "deferred model plan should be stopped before slow worker startup" )
_assert ( deferred_blocker [ 0 ] == "暂未实现" , "deferred model plan should be classified as unsupported" )
2026-08-06 18:15:14 +08:00
2026-08-07 18:08:32 +08:00
title , user_message = action_probe . _user_facing_edit_failure_message (
"隔离子进程执行失败;主程序没有崩溃,原模型保持不变。 当前版本暂不支持复杂链式圆角。" ,
deferred_context ,
)
_assert ( title == "不能修改" , "edit failure dialog title should be user-facing" )
_assert ( "隔离子进程" not in user_message and "子进程" not in user_message , "failure message should not expose isolation details" )
_assert ( "暂未实现" in user_message , "unsupported failure should state that the operation is not implemented yet" )
_empty_title , empty_message = action_probe . _user_facing_edit_failure_message (
"隔离子进程执行失败;主程序没有崩溃,原模型保持不变。" ,
deferred_context ,
)
_assert ( "隔离子进程" not in empty_message and "子进程" not in empty_message , "empty internal failure should stay user-facing" )
2026-08-06 18:15:14 +08:00
2026-08-11 18:28:16 +08:00
def _assert_parameter_export_action () -> None :
with tempfile . TemporaryDirectory () as temp_dir :
output_path = Path ( temp_dir ) / "data.json"
probe = _ParameterExportActionProbe ( output_path )
probe . export_selected_parameters ()
payload = json . loads ( output_path . read_text ( encoding = "utf-8" ))
2026-08-13 17:50:20 +08:00
component_text = probe . component_path . read_text ( encoding = "utf-8" )
2026-08-11 18:28:16 +08:00
_assert (
payload == [
{
"name" : "面内长度" ,
"displayName" : "面内长度" ,
"type" : "number" ,
"ioRole" : "input" ,
"default" : "151" ,
},
{
"name" : "偏移" ,
"displayName" : "偏移" ,
"type" : "number" ,
"ioRole" : "input" ,
"default" : "57.5" ,
},
],
f "parameter export action wrote unexpected JSON: { payload } " ,
)
2026-08-13 17:50:20 +08:00
_assert (
"INPUT_PARAMETERS" in component_text and "def execute(inputs, params, context):" in component_text and "面内长度" in component_text ,
"parameter export should generate FlowEditor-style component main.py with embedded input parameters" ,
)
_assert (
not ( probe . component_path . parent / "data.json" ) . exists (),
"component export should embed parameters in main.py instead of writing component data.json" ,
)
_assert (
probe . status_bar . messages and "已导出 2 个输入参数" in probe . status_bar . messages [ - 1 ],
"parameter export status should mention exported count" ,
)
_assert (
"data.json" in probe . info_text and "参数数量:2" in probe . info_text and "组件入口:main.py" in probe . info_text ,
"parameter export info panel summary should mention generated component" ,
)
2026-08-11 18:28:16 +08:00
2026-08-19 10:28:09 +08:00
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 } " )
2026-08-19 18:02:47 +08:00
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 } " ,
)
2026-08-19 10:28:09 +08:00
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" )
2026-08-19 18:02:47 +08:00
_assert (
"_restore_scdm_feature_cache_from_disk()" in loaded_body ,
"STEP load should restore a matching SCDM disk cache before launching a new probe" ,
)
2026-08-19 10:28:09 +08:00
_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" )
2026-08-19 18:02:47 +08:00
_assert (
"_current_scdm_feature_cache_matches_loaded_step()" in preload_body ,
"SCDM preload should skip relaunching SpaceClaim when a current cache is already installed" ,
)
2026-08-19 10:28:09 +08:00
_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" )
2026-08-19 18:02:47 +08:00
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 } " )
2026-08-19 10:28:09 +08:00
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" ,
)
2026-08-07 18:08:32 +08:00
def main () -> int :
app = QApplication . instance () or QApplication ([])
top_level_label_probe = _TopLevelPropertyLabelProbe ()
app . installEventFilter ( top_level_label_probe )
probe = _PropertyTableProbe ()
probe . show ()
2026-08-06 18:15:14 +08:00
QApplication . processEvents ()
2026-08-07 18:08:32 +08:00
probe . _refresh_property_editor ()
QApplication . processEvents ()
_assert (
not top_level_label_probe . shown_labels ,
f "property labels were shown as transient top-level windows: { top_level_label_probe . shown_labels } " ,
2026-08-06 18:15:14 +08:00
)
2026-08-19 18:02:47 +08:00
_assert_scdm_target_values_use_backend_units ()
_assert_selection_mode_labels_are_english ()
2026-08-07 18:08:32 +08:00
_assert_property_table_editor ( probe )
2026-08-19 10:28:09 +08:00
_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" )
2026-08-07 18:08:32 +08:00
_assert (
2026-08-19 10:28:09 +08:00
"能改:" 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 } " )
2026-08-07 18:08:32 +08:00
_assert_diagnostics_stay_out_of_parameter_table ( probe )
2026-08-14 18:42:39 +08:00
_assert_relation_formula_editor ()
2026-08-17 18:53:03 +08:00
_assert_relation_radius_formula_proxy ()
_assert_relation_formula_input_remains_editable_while_replaying ()
_assert_relation_formula_input_clickable_after_existing_formula ()
_assert_relation_formula_input_is_selection_independent ()
_assert_relation_formula_input_recovers_after_loading ()
_assert_relation_formula_ids_follow_model_remap ()
2026-08-07 18:08:32 +08:00
_assert_mouse_selection_guards ()
2026-08-19 10:28:09 +08:00
_assert_scdm_selection_diagnostics ()
2026-08-19 18:02:47 +08:00
_assert_solid_selection_does_not_expand_face_scdm_specs ()
2026-08-19 10:28:09 +08:00
_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 ()
2026-08-07 18:08:32 +08:00
_assert_quick_blind_depth_spec ()
_assert_user_facing_failure_messages ()
2026-08-11 18:28:16 +08:00
_assert_parameter_export_action ()
2026-08-19 10:28:09 +08:00
_assert_scdm_command_row_uses_unified_apply ()
2026-08-07 18:08:32 +08:00
print ( "property table editor UI ok" )
2026-08-06 18:15:14 +08:00
if QApplication . instance () is app :
app . quit ()
return 0
if __name__ == "__main__" :
raise SystemExit ( main ())