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
import sys
2026-08-11 18:28:16 +08:00
import tempfile
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-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-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-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 ()
self . current_capability_headline = 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-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
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" )
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" )
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 ()
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-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-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-07 18:08:32 +08:00
_assert_property_table_editor ( probe )
_assert ( "当前支持" in probe . current_capability_headline . text (), "software progress panel did not show supported areas" )
_assert ( "优先:" not in probe . current_capability_headline . text (), "software progress panel should not show priority copy" )
_assert (
"矩形槽口袋" in probe . current_capability_headline . toolTip ()
and "多台阶矩形凸台顶层" in probe . current_capability_headline . toolTip (),
"software progress tooltip should name newly supported prismatic feature edits" ,
2026-08-06 18:15:14 +08:00
)
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 ()
_assert_quick_blind_depth_spec ()
_assert_user_facing_failure_messages ()
2026-08-11 18:28:16 +08:00
_assert_parameter_export_action ()
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 ())