feat: 完善一级关系参数化建模与参数导出

This commit is contained in:
2026-08-11 18:28:16 +08:00
parent 6cb99a1273
commit 19364d81b5
20 changed files with 917 additions and 167 deletions
+176 -27
View File
@@ -7,6 +7,7 @@ import time
from PySide6.QtCore import Qt, QThread, QTimer, Slot
from PySide6.QtWidgets import (
QApplication,
QCheckBox,
QFileDialog,
QFrame,
QHBoxLayout,
@@ -36,8 +37,15 @@ PROPERTY_LABEL_COLUMN = 0
PROPERTY_CURRENT_COLUMN = 1
PROPERTY_SCOPE_COLUMN = 2
PROPERTY_TARGET_COLUMN = 3
PROPERTY_TABLE_MIN_COLUMN_WIDTHS = (72, 118, 72, 82)
PROPERTY_TABLE_PREFERRED_COLUMN_WIDTHS = (118, 168, 108, 104)
PROPERTY_INPUT_COLUMN = 4
PROPERTY_TABLE_HEADERS = ("尺寸参数", "当前值", "建模意图", "目标值", "输入参数")
PROPERTY_TABLE_MIN_COLUMN_WIDTHS = (70, 72, 66, 62, 50)
PROPERTY_TABLE_PREFERRED_COLUMN_WIDTHS = (108, 104, 88, 82, 66)
PROPERTY_TEMPORARILY_HIDDEN_PARAMETER_KEYS = {
"face_center_position",
"edge_center_point",
}
PROPERTY_TEMPORARILY_HIDDEN_PARAMETER_LABELS = {"中心"}
PROPERTY_COMMAND_ORDER = ("offset", "move", "scale", "rotate", "feature", "diagnostics")
PROPERTY_COMMAND_LABELS = {
"offset": "偏移",
@@ -98,7 +106,7 @@ PROPERTY_EXPLANATION_TOOLTIPS = {
}
def _property_table_column_widths(available_width: int) -> tuple[int, int, int, int]:
def _property_table_column_widths(available_width: int) -> tuple[int, ...]:
"""Prefer current value visibility while keeping modeling intent and target usable."""
column_count = len(PROPERTY_TABLE_MIN_COLUMN_WIDTHS)
available = max(int(available_width or 0), column_count * 44)
@@ -110,7 +118,7 @@ def _property_table_column_widths(available_width: int) -> tuple[int, int, int,
if available >= preferred_total:
widths = [int(value) for value in preferred]
extra = available - preferred_total
weights = (0.26, 0.38, 0.18, 0.18)
weights = (0.30, 0.24, 0.18, 0.16, 0.12)
for index, weight in enumerate(weights):
addition = int(extra * weight)
widths[index] += addition
@@ -126,10 +134,10 @@ def _property_table_column_widths(available_width: int) -> tuple[int, int, int,
widths[1] += available - sum(widths)
return tuple(widths) # type: ignore[return-value]
floors = (48, 96, 44, 56)
floors = (44, 44, 44, 44, 44)
widths = [int(value) for value in minimum]
deficit = min_total - available
for index in (0, 2, 3, 1):
for index in (0, 2, 3, 1, 4):
if deficit <= 0:
break
reducible = max(0, widths[index] - floors[index])
@@ -149,6 +157,16 @@ def _compact_property_card_text(text: str, limit: int = 220) -> str:
return f"{compact[: max(0, limit - 1)].rstrip()}"
def _property_parameter_is_visible(spec: dict[str, object]) -> bool:
key = str(spec.get("key", "") or "")
label = str(spec.get("label", "") or "").strip()
if key in PROPERTY_TEMPORARILY_HIDDEN_PARAMETER_KEYS:
return False
if label in PROPERTY_TEMPORARILY_HIDDEN_PARAMETER_LABELS:
return False
return True
def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
"""Return the independent, user-facing dimensions for a feature candidate."""
surface = str(action_info.get("surface", "") or "")
@@ -817,7 +835,7 @@ class WindowStateMixin:
self._set_control_state(
self.quick_export_all_button,
has_model,
"导出当前完整 STEP 模型。",
"导出当前模型。",
wait_or_load_tip,
)
if hasattr(self, "part_tree"):
@@ -852,7 +870,7 @@ class WindowStateMixin:
self._set_control_state(
self.export_all_button,
has_model,
"导出当前完整 STEP 模型。",
"导出当前模型。",
wait_or_load_tip,
)
self._set_control_state(
@@ -1548,11 +1566,11 @@ class WindowStateMixin:
headline = "当前支持:Face、孔、槽、凸台、圆角/倒角、壳体、Edge"
detail = ""
tooltip = (
"Face:面内长度/宽度、中心、偏移、壳体厚度。\n"
"Face:面内长度/宽度、偏移、壳体厚度。\n"
"孔/槽:孔径、轴心、封堵、盲孔/盲槽深度、槽宽/槽深/弧长/总长。\n"
"凸台:圆柱凸台直径/高度/轴心;矩形凸台/矩形槽口袋长宽、中心、高度/深度;多台阶矩形凸台顶层规则台阶。\n"
"凸台:圆柱凸台直径/高度/轴心;矩形凸台/矩形槽口袋长宽、高度/深度;多台阶矩形凸台顶层规则台阶。\n"
"圆角/倒角:简单已有圆角半径/弧长、简单等半径圆角链半径/弧长、已有等距倒角距离,直线 Edge 新增圆角/倒角。\n"
"Edge/解析曲面:直线 Edge 长度/端点/中心、圆/椭圆 Edge、简单圆锥/球/环面。\n"
"Edge/解析曲面:直线 Edge 长度/端点、圆/椭圆 Edge、简单圆锥/球/环面。\n"
"一级关系:Face/Edge 平行垂直事实、Face 共面/同域碎片、一级同轴圆柱事实;部分 Face/Edge 操作可选择保持关系。\n"
"受限:复杂链式特征、二级/三级拓扑传播和原 CAD 历史恢复。"
)
@@ -1985,7 +2003,9 @@ class WindowStateMixin:
editable=False,
)
target_item.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
row_items = (label_item, current_item, scope_item, target_item)
input_item = self._property_table_item("", editable=False)
input_item.setToolTip("勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json。")
row_items = (label_item, current_item, scope_item, target_item, input_item)
self._style_property_row_items(row_items, editable=editable, spec=effective_spec)
for column, item in enumerate(row_items):
item.setToolTip(item.toolTip() or item.text())
@@ -2002,6 +2022,10 @@ class WindowStateMixin:
self.property_table.removeCellWidget(row, PROPERTY_TARGET_COLUMN)
else:
self.property_table.removeCellWidget(row, PROPERTY_TARGET_COLUMN)
self._set_property_input_checkbox(
row,
exportable=bool(input_editable and effective_spec.get("label")),
)
self._clear_property_command_bar()
self._clear_property_cards()
self._update_current_capability_panel()
@@ -2032,12 +2056,13 @@ class WindowStateMixin:
def _style_property_row_items(
self,
items: tuple[QTableWidgetItem, QTableWidgetItem, QTableWidgetItem, QTableWidgetItem],
items: tuple[QTableWidgetItem, ...],
*,
editable: bool,
spec: dict[str, object] | None = None,
) -> None:
label_item, current_item, scope_item, target_item = items
label_item, current_item, scope_item, target_item = items[:4]
input_item = items[4] if len(items) > 4 else None
if spec is not None and bool(spec.get("pin_top")):
for item in items:
item.setBackground(QColor("#eef6ff"))
@@ -2045,6 +2070,8 @@ class WindowStateMixin:
current_item.setForeground(QColor("#0f172a"))
scope_item.setForeground(QColor("#0369a1"))
target_item.setForeground(QColor("#64748b"))
if input_item is not None:
input_item.setForeground(QColor("#64748b"))
label_font = label_item.font()
label_font.setBold(True)
label_item.setFont(label_font)
@@ -2053,13 +2080,15 @@ class WindowStateMixin:
current_item.setFont(current_font)
return
if editable:
row_backgrounds = ("#fff7ed", "#fffbeb", "#fff7ed", "#fff7ed")
row_backgrounds = ("#fff7ed", "#fffbeb", "#fff7ed", "#fff7ed", "#fff7ed")
for item, color in zip(items, row_backgrounds):
item.setBackground(QColor(color))
label_item.setForeground(QColor("#7c2d12"))
current_item.setForeground(QColor("#431407"))
scope_item.setForeground(QColor("#9a3412"))
target_item.setForeground(QColor("#111827"))
if input_item is not None:
input_item.setForeground(QColor("#7c2d12"))
label_font = label_item.font()
label_font.setBold(True)
label_item.setFont(label_font)
@@ -2070,6 +2099,9 @@ class WindowStateMixin:
scope_item.setForeground(QColor("#64748b"))
target_item.setBackground(QColor("#eef2f6"))
target_item.setForeground(QColor("#8f99a8"))
if input_item is not None:
input_item.setBackground(QColor("#eef2f6"))
input_item.setForeground(QColor("#8f99a8"))
def _property_scope_default(self, spec: dict[str, object]) -> str:
modes = spec.get("scope_modes")
@@ -2145,6 +2177,39 @@ class WindowStateMixin:
editor.returnPressed.connect(self.apply_current_property_edit)
self.property_table.setCellWidget(row, PROPERTY_TARGET_COLUMN, editor)
def _set_property_input_checkbox(self, row: int, *, exportable: bool) -> None:
if not hasattr(self, "property_table"):
return
container = QWidget()
container.setObjectName("propertyInputParameterCell")
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
checkbox = QCheckBox(container)
checkbox.setObjectName("propertyInputParameterCheckbox")
checkbox.setChecked(False)
checkbox.setEnabled(exportable)
checkbox.setCursor(Qt.CursorShape.PointingHandCursor if exportable else Qt.CursorShape.ArrowCursor)
tip = "勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json。"
if not exportable:
tip = "当前行不是可导出的尺寸输入参数。"
checkbox.setToolTip(tip)
container.setToolTip(tip)
checkbox.toggled.connect(lambda _checked=False: self._update_parameter_export_state())
layout.addWidget(checkbox)
self.property_table.setCellWidget(row, PROPERTY_INPUT_COLUMN, container)
def _property_input_checkbox(self, row: int) -> QCheckBox | None:
if not hasattr(self, "property_table"):
return None
widget = self.property_table.cellWidget(row, PROPERTY_INPUT_COLUMN)
if isinstance(widget, QCheckBox):
return widget
if isinstance(widget, QWidget):
return widget.findChild(QCheckBox)
return None
def _set_property_scope_editor(self, row: int, spec: dict[str, object]) -> None:
modes = spec.get("scope_modes")
if not isinstance(modes, dict) or not modes:
@@ -2213,6 +2278,23 @@ class WindowStateMixin:
target_item = self.property_table.item(row, PROPERTY_TARGET_COLUMN)
if target_item is not None:
target_item.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
checkbox = self._property_input_checkbox(row)
if checkbox is not None:
exportable = bool(input_editable and effective_spec.get("label"))
if not exportable and checkbox.isChecked():
was_blocked = checkbox.blockSignals(True)
try:
checkbox.setChecked(False)
finally:
checkbox.blockSignals(was_blocked)
tip = (
"勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json。"
if exportable
else "当前行不是可导出的尺寸输入参数。"
)
checkbox.setEnabled(exportable)
checkbox.setCursor(Qt.CursorShape.PointingHandCursor if exportable else Qt.CursorShape.ArrowCursor)
checkbox.setToolTip(tip)
self._update_current_capability_panel()
self._update_property_apply_state()
@@ -2345,6 +2427,7 @@ class WindowStateMixin:
and bool(spec.get("enabled"))
and bool(spec.get("action"))
and str(spec.get("value_type", "number")) != "command"
and _property_parameter_is_visible(spec)
]
def _feature_property_specs(
@@ -2381,6 +2464,7 @@ class WindowStateMixin:
or not bool(spec.get("editable"))
or (not bool(spec.get("enabled")) and not prismatic_size_key and not prismatic_center_key)
or str(spec.get("value_type", "number")) == "command"
or not _property_parameter_is_visible(spec)
):
continue
dimension = dict(spec)
@@ -4546,7 +4630,7 @@ class WindowStateMixin:
"action": "resize_boss_height",
"target_attr": "boss_height_input",
"enabled": boss_height_can_local,
"enabled_tip": "输入完整圆柱凸台的目标高度;点击修改时程序会再计算并确认可拉伸/切除的凸台端盖 Face。",
"enabled_tip": "输入完整圆柱凸台的目标高度;点击修改时程序会再计算并校验可拉伸/切除的凸台端盖 Face。",
"disabled_tip": "当前凸台候选缺少稳定高度或端盖信息,暂不放行高度修改。",
"range_hint": relative_range_hint(current_boss_height, 0.3, 0.8),
},
@@ -4663,7 +4747,7 @@ class WindowStateMixin:
"action": "resize_cylinder_height",
"target_attr": "boss_height_input",
"enabled": bool(is_full_cylinder and current_cylinder_height is not None),
"enabled_tip": "输入完整圆柱面的目标高度;点击修改时程序会再计算并确认可拉伸/切除的圆柱端盖 Face。",
"enabled_tip": "输入完整圆柱面的目标高度;点击修改时程序会再计算并校验可拉伸/切除的圆柱端盖 Face。",
"disabled_tip": "当前圆柱面不是完整圆柱,或缺少稳定高度信息。",
"range_hint": relative_range_hint(current_cylinder_height, 0.3, 0.8),
},
@@ -5775,17 +5859,70 @@ class WindowStateMixin:
card.style().unpolish(card)
card.style().polish(card)
setattr(widget, "_geom_param_button_state", button_state)
if not hasattr(self, "apply_property_button"):
if hasattr(self, "apply_property_button"):
changed = self._changed_property_rows()
enabled = bool(has_model and changed)
disabled_tip = "请先选择对象,并在属性表中修改一个可编辑目标值。"
if changed and len(changed) > 1:
disabled_tip = "当前一次只执行一个几何修改;请只保留一行目标值不同,再点击参数化建模。"
self._set_control_state(
self.apply_property_button,
enabled and len(changed) == 1,
"应用当前被修改的参数。",
disabled_tip,
)
self._update_parameter_export_state(has_model)
def _selected_parameter_export_rows(self) -> list[dict[str, str]]:
if not hasattr(self, "property_table"):
return []
rows: list[dict[str, str]] = []
specs = getattr(self, "property_editor_specs", [])
row_count = min(self.property_table.rowCount(), len(specs))
for row in range(row_count):
checkbox = self._property_input_checkbox(row)
if checkbox is None or not checkbox.isEnabled() or not checkbox.isChecked():
continue
spec = self._effective_property_spec(specs[row], row=row)
if str(spec.get("value_type", "number")) == "command":
continue
label_item = self.property_table.item(row, PROPERTY_LABEL_COLUMN)
current_item = self.property_table.item(row, PROPERTY_CURRENT_COLUMN)
label = (label_item.text() if label_item is not None else str(spec.get("label", ""))).strip()
current = (
current_item.text()
if current_item is not None
else str(spec.get("current_text", spec.get("current_raw", "")))
).strip()
if not label:
continue
rows.append(
{
"name": label,
"displayName": label,
"type": "number",
"ioRole": "input",
"default": current,
}
)
return rows
def _update_parameter_export_state(self, has_model: bool | None = None) -> None:
if not hasattr(self, "export_parameters_button"):
return
changed = self._changed_property_rows()
enabled = bool(has_model and changed)
disabled_tip = "请先选择对象,并在属性表中修改一个可编辑目标值。"
if changed and len(changed) > 1:
disabled_tip = "当前一次只执行一个几何修改;请只保留一行目标值不同,再点击参数化建模。"
if has_model is None:
has_model = self.model is not None and not (
self.operation_in_progress or self.scan_in_progress or self.load_in_progress
)
selected_rows = self._selected_parameter_export_rows()
if selected_rows:
disabled_tip = "请等待当前后台任务完成后再导出参数。"
else:
disabled_tip = "请先在“输入参数”列勾选至少一个尺寸参数。"
self._set_control_state(
self.apply_property_button,
enabled and len(changed) == 1,
"应用当前被修改的参数",
self.export_parameters_button,
bool(has_model and selected_rows),
f"导出已勾选的 {len(selected_rows)} 个输入参数到 data.json",
disabled_tip,
)
@@ -6060,7 +6197,19 @@ class WindowStateMixin:
if action is None:
QMessageBox.information(self, "暂不支持", f"当前属性没有可用的执行入口:{action_name}")
return
action()
self._run_parametric_property_action_directly(action)
def _run_parametric_property_action_directly(self, action) -> None:
original_question = QMessageBox.question
def auto_yes(*_args, **_kwargs):
return QMessageBox.StandardButton.Yes
try:
QMessageBox.question = auto_yes
action()
finally:
QMessageBox.question = original_question
def set_manual_hole_bottom_face(self) -> None:
self._update_action_states()