feat: 推进一级关系参数化编辑与参数导出

This commit is contained in:
2026-08-13 17:50:20 +08:00
parent 19364d81b5
commit 7d814d2939
21 changed files with 2075 additions and 231 deletions
+429 -166
View File
@@ -26,6 +26,7 @@ from PySide6.QtGui import QColor
from .constants import FACE_SELECTION_FEATURE_INFO_SURFACES, FREEFORM_FACE_SURFACES, SNAPSHOT_FACE_LOGICAL_IDS_KEY
from .model import StepModel
from .parametric_component import component_edit_config_from_spec
from .records import OperationRecord
from .ui_helpers import * # noqa: F403
from .widgets import NoWheelComboBox
@@ -172,6 +173,11 @@ def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
surface = str(action_info.get("surface", "") or "")
feature_guess = str(action_info.get("feature_guess", "") or "")
angular_span = _float_or_none(action_info.get("angular_span"))
face_size_keys = (
("local_face_width", "local_face_height")
if bool(action_info.get("local_face_size_edit_ready"))
else ()
)
if surface == "plane":
if action_info.get("multistep_prismatic_status") == "blocked":
@@ -179,7 +185,8 @@ def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
if action_info.get("existing_chamfer_status") == "candidate":
return ("existing_chamfer_distance_estimate",)
if action_info.get("prismatic_profile_status") == "candidate":
keys = ["local_face_width", "local_face_height"]
keys = list(face_size_keys)
keys.append("face_target_normal_position")
if action_info.get("prismatic_extrusion_status") == "candidate":
keys.append("shell_thickness_estimate")
if str(action_info.get("prismatic_feature_semantics") or "") in {"additive-boss", "subtractive-pocket"}:
@@ -187,15 +194,13 @@ def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
return tuple(keys)
if action_info.get("shell_region_status") == "candidate":
return (
"local_face_width",
"local_face_height",
*face_size_keys,
"face_center_position",
"face_target_normal_position",
"shell_thickness_estimate",
)
return (
"local_face_width",
"local_face_height",
*face_size_keys,
"face_center_position",
"face_target_normal_position",
)
@@ -340,11 +345,27 @@ class WindowStateMixin:
title = f"{title_prefix} {logical_id}"
if logical_id != int(face_id):
title += f"(当前拓扑 Face {face_id}"
return {
result = {
"selection_title": title,
"selection_display_id": logical_id,
"selection_topological_face_id": int(face_id),
}
if self.model is not None:
try:
region_ids = self.model.face_region_ids(face_id)
except Exception:
region_ids = []
region_ids = sorted({int(item) for item in region_ids if 0 <= int(item) < len(self.model.faces)})
if len(region_ids) > 1:
result.update(
{
"face_region_logical_id": logical_id,
"face_region_ids": tuple(region_ids),
"face_region_count": len(region_ids),
"face_region_note": "STEP/B-Rep 将该局部面拆成多个同域拓扑 Face;界面按一个逻辑面区域显示,高亮包含全部碎片。",
}
)
return result
def _first_level_fact_selection_fields(self, face_id: int, scope: str = "auto") -> dict[str, object]:
if self.model is None:
@@ -553,6 +574,21 @@ class WindowStateMixin:
info.setdefault("feature_type", "可拉伸/切除平面候选")
info.setdefault("feature_edit_actions", "拉伸/切除平面")
elif surface == "cylinder":
same_domain_ids = []
try:
same_domain_ids = self.model.connected_same_domain_face_ids(face_id) or [face_id]
except Exception:
same_domain_ids = [face_id]
same_domain_ids = sorted({int(item) for item in same_domain_ids if 0 <= int(item) < len(self.model.faces)})
if same_domain_ids:
info["same_domain_face_ids"] = tuple(same_domain_ids)
info["same_domain_face_count"] = len(same_domain_ids)
info["feature_highlight_face_ids"] = tuple(same_domain_ids)
if len(same_domain_ids) > 1:
info.setdefault(
"same_domain_note",
"当前圆柱面在 STEP/B-Rep 中被拆成多个同域拓扑 Face,界面按一个局部孔/槽区域高亮。",
)
feature_guess = str(info.get("feature_guess", "") or "")
angular_span = _float_or_none(info.get("angular_span"))
if feature_guess == "hole/groove candidate":
@@ -768,7 +804,7 @@ class WindowStateMixin:
if self.load_in_progress:
self.statusBar().showMessage("STEP background loading is still running.")
return True
if self.operation_in_progress:
if self.operation_in_progress and not bool(getattr(self, "_property_batch_running_action", False)):
self.statusBar().showMessage(message)
return True
if self.scan_in_progress:
@@ -2004,7 +2040,7 @@ class WindowStateMixin:
)
target_item.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
input_item = self._property_table_item("", editable=False)
input_item.setToolTip("勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json")
input_item.setToolTip("勾选后点击“导出参数”,会写入根目录 data.json,并把参数列表嵌入组件 main.py")
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):
@@ -2191,7 +2227,7 @@ class WindowStateMixin:
checkbox.setChecked(False)
checkbox.setEnabled(exportable)
checkbox.setCursor(Qt.CursorShape.PointingHandCursor if exportable else Qt.CursorShape.ArrowCursor)
tip = "勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json"
tip = "勾选后点击“导出参数”,会写入根目录 data.json,并把参数列表嵌入组件 main.py"
if not exportable:
tip = "当前行不是可导出的尺寸输入参数。"
checkbox.setToolTip(tip)
@@ -2288,7 +2324,7 @@ class WindowStateMixin:
finally:
checkbox.blockSignals(was_blocked)
tip = (
"勾选后点击“导出参数”,会把这一行作为输入参数写入 data.json"
"勾选后点击“导出参数”,会写入根目录 data.json,并把参数列表嵌入组件 main.py"
if exportable
else "当前行不是可导出的尺寸输入参数。"
)
@@ -2442,16 +2478,21 @@ class WindowStateMixin:
prismatic_length = _float_or_none(action_info.get("prismatic_length"))
prismatic_width = _float_or_none(action_info.get("prismatic_width"))
prismatic_depth = _float_or_none(action_info.get("prismatic_extrusion_estimate"))
prismatic_reference_face_id = _int_or_none(action_info.get("prismatic_reference_face_id"))
part_solid_count = _int_or_none(action_info.get("part_solid_count"))
prismatic_size_supported = bool(
embedded_prismatic
and action_info.get("prismatic_profile_status") == "candidate"
and action_info.get("prismatic_extrusion_status") == "candidate"
and bool(action_info.get("local_face_size_edit_ready"))
and prismatic_length is not None
and prismatic_length > 0
and prismatic_width is not None
and prismatic_width > 0
and prismatic_depth is not None
and prismatic_depth > 0
and prismatic_reference_face_id is not None
and (part_solid_count is None or part_solid_count == 1)
and len(_int_values(action_info.get("prismatic_connected_side_face_ids"))) == 4
)
dimensions: list[dict[str, object]] = []
@@ -2470,14 +2511,16 @@ class WindowStateMixin:
dimension = dict(spec)
dimension["parameter_role"] = "dimension"
if action_info.get("prismatic_profile_status") == "candidate":
if prismatic_size_key:
if not prismatic_size_supported:
continue
if prismatic_size_key and prismatic_size_supported:
modes = spec.get("scope_modes")
local_mode = modes.get("local") if isinstance(modes, dict) else None
local_mode = dict(local_mode) if isinstance(local_mode, dict) else {}
is_width_axis = key == "local_face_width"
current_limit = prismatic_width if is_width_axis else prismatic_length
size_blocker = str(
action_info.get("local_face_size_edit_blocker")
or "当前矩形特征的长宽、深度、参考面、单 Solid 条件或四个侧壁没有稳定识别,暂不开放长宽修改。"
)
local_mode.update(
{
"label": "局部重建",
@@ -2488,7 +2531,7 @@ class WindowStateMixin:
"输入矩形凸台/口袋的目标长度或宽度;程序会先移除/补回旧矩形包络,"
"再重建目标矩形特征。"
),
"disabled_tip": "当前矩形特征的长宽、深度或四个侧壁没有稳定识别,暂不开放长宽修改。",
"disabled_tip": size_blocker,
"range_hint": "当前版本不在一次编辑中交换长度和宽度方向。",
}
)
@@ -2752,6 +2795,8 @@ class WindowStateMixin:
show_generic_face_edit_specs = bool(has_face and (is_plane or is_shell_candidate) and not is_existing_chamfer)
local_face_deform_ready = bool(action_info.get("local_face_deform_ready", True))
local_face_deform_blocker = str(action_info.get("local_face_deform_blocker") or "").strip()
local_face_size_edit_ready = bool(action_info.get("local_face_size_edit_ready"))
local_face_size_edit_blocker = str(action_info.get("local_face_size_edit_blocker") or "").strip()
has_fillet_support = len(_int_values(action_info.get("feature_existing_fillet_support_face_ids"))) >= 2
is_line_edge = has_edge and curve == "line"
specs: list[dict[str, object]] = []
@@ -3553,147 +3598,150 @@ class WindowStateMixin:
current_face_width = _float_or_none(action_info.get("local_face_width"))
current_face_height = _float_or_none(action_info.get("local_face_height"))
face_size_tip = (
"这里的面内长度/面内宽度是选中 Face 在自身平面内两个主方向上的投影长度"
"不是面积,也不是模型整体高度。局部重建会移动当前 Face 顶点,相邻面按新顶点重建"
"面内长度/面内宽度是矩形平面特征的两个驱动尺寸"
"不是面积,也不是任意曲面的通用尺寸。局部重建会重建当前矩形特征,相邻面按新边界更新"
)
face_size_owner_tip = (
"这里的面内长度/面内宽度是选中 Face 在自身平面内两个主方向上的投影长度"
"不是面积,也不是模型整体高度。程序会沿该方向缩放所属特征或 Solid,其它几何会跟随变化。"
"面内长度/面内宽度是矩形平面特征的两个驱动尺寸"
"不是面积,也不是任意曲面的通用尺寸。程序会沿该方向缩放所属特征或 Solid,其它几何会跟随变化。"
)
add_scoped_spec(
key="local_face_width",
label="面内长度",
current_raw=current_face_width if current_face_width is not None else "",
target_text=numeric_text(current_face_width),
scope_default="local",
scope_modes={
"local": {
"label": "局部重建",
"action": "resize_face_width_local",
"target_attr": "face_width_input",
"enabled": bool(
local_face_deform_ready
and current_face_width is not None
and current_face_width > 0
and current_face_center is not None
),
"enabled_tip": f"输入目标面内长度;{face_size_tip}",
"disabled_tip": face_local_disabled_tip(
"只有带稳定顶点环和中心坐标的平面 Face 才能尝试修改面内长度。"
),
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_width, 0.25, 0.8, face_linear_hard_limit)}",
if local_face_size_edit_ready:
add_scoped_spec(
key="local_face_width",
label="面内长度",
current_raw=current_face_width if current_face_width is not None else "",
target_text=numeric_text(current_face_width),
scope_default="local",
scope_modes={
"local": {
"label": "局部重建",
"action": "resize_face_width_local",
"target_attr": "face_width_input",
"enabled": bool(
local_face_deform_ready
and current_face_width is not None
and current_face_width > 0
and current_face_center is not None
),
"enabled_tip": f"输入目标面内长度;{face_size_tip}",
"disabled_tip": face_local_disabled_tip(
local_face_size_edit_blocker
or "当前对象还没有识别为稳定矩形平面特征,不能修改面内长度。"
),
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_width, 0.25, 0.8, face_linear_hard_limit)}",
},
"keep_relations": {
"label": "保持关系",
"action": "resize_face_width_keep_relations",
"target_attr": "face_width_input",
"enabled": bool(
current_face_width is not None
and current_face_width > 0
and current_face_center is not None
and (self.selected_part_id is not None or self.selected_solid_id is not None)
and face_keep_relation_enabled
),
"enabled_tip": (
"输入目标面内长度;程序会沿该方向缩放所属特征或 Solid,"
"并要求一级相邻平面的平行/垂直关系在执行前可验证、执行后可反查。"
),
"disabled_tip": (
face_keep_relation_disabled_tip
if not face_keep_relation_enabled
else "当前 Face 缺少稳定面内长度、中心坐标或所属对象,不能保持关系地修改面内长度。"
),
"range_hint": (
"保持关系会把面内长度修改转换为所属对象单向缩放,并在结果上反查一级平面关系;"
f"{relative_range_hint(current_face_width, 0.25, 0.8, face_linear_hard_limit)}"
),
},
"owning": {
"label": "缩放特征",
"action": "resize_face_width_owning_scale",
"target_attr": "face_width_input",
"enabled": bool(
current_face_width is not None
and current_face_width > 0
and current_face_center is not None
and (self.selected_part_id is not None or self.selected_solid_id is not None)
),
"enabled_tip": f"输入目标面内长度;{face_size_owner_tip}",
"disabled_tip": "当前 Face 缺少稳定面内长度、中心坐标或所属对象,不能按这个方向缩放所属对象。",
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_width, 0.2, 0.5, face_linear_hard_limit)}",
},
},
"keep_relations": {
"label": "保持关系",
"action": "resize_face_width_keep_relations",
"target_attr": "face_width_input",
"enabled": bool(
current_face_width is not None
and current_face_width > 0
and current_face_center is not None
and (self.selected_part_id is not None or self.selected_solid_id is not None)
and face_keep_relation_enabled
),
"enabled_tip": (
"输入目标面内长度;程序会沿该方向缩放所属特征或 Solid,"
"并要求一级相邻平面的平行/垂直关系在执行前可验证、执行后可反查。"
),
"disabled_tip": (
face_keep_relation_disabled_tip
if not face_keep_relation_enabled
else "当前 Face 缺少稳定面内长度、中心坐标或所属对象,不能保持关系地修改面内长度。"
),
"range_hint": (
"保持关系会把面内长度修改转换为所属对象单向缩放,并在结果上反查一级平面关系;"
f"{relative_range_hint(current_face_width, 0.25, 0.8, face_linear_hard_limit)}"
),
value_type="positive",
used=("local_face_width", "local_face_width_direction", "local_face_size_center"),
**face_scale_limits(current_face_width),
)
add_scoped_spec(
key="local_face_height",
label="面内宽度",
current_raw=current_face_height if current_face_height is not None else "",
target_text=numeric_text(current_face_height),
scope_default="local",
scope_modes={
"local": {
"label": "局部重建",
"action": "resize_face_height_local",
"target_attr": "face_height_input",
"enabled": bool(
local_face_deform_ready
and current_face_height is not None
and current_face_height > 0
and current_face_center is not None
),
"enabled_tip": f"输入目标面内宽度;{face_size_tip}",
"disabled_tip": face_local_disabled_tip(
local_face_size_edit_blocker
or "当前对象还没有识别为稳定矩形平面特征,不能修改面内宽度。"
),
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_height, 0.25, 0.8, face_linear_hard_limit)}",
},
"keep_relations": {
"label": "保持关系",
"action": "resize_face_height_keep_relations",
"target_attr": "face_height_input",
"enabled": bool(
current_face_height is not None
and current_face_height > 0
and current_face_center is not None
and (self.selected_part_id is not None or self.selected_solid_id is not None)
and face_keep_relation_enabled
),
"enabled_tip": (
"输入目标面内宽度;程序会沿该方向缩放所属特征或 Solid,"
"并要求一级相邻平面的平行/垂直关系在执行前可验证、执行后可反查。"
),
"disabled_tip": (
face_keep_relation_disabled_tip
if not face_keep_relation_enabled
else "当前 Face 缺少稳定面内宽度、中心坐标或所属对象,不能保持关系地修改面内宽度。"
),
"range_hint": (
"保持关系会把面内宽度修改转换为所属对象单向缩放,并在结果上反查一级平面关系;"
f"{relative_range_hint(current_face_height, 0.25, 0.8, face_linear_hard_limit)}"
),
},
"owning": {
"label": "缩放特征",
"action": "resize_face_height_owning_scale",
"target_attr": "face_height_input",
"enabled": bool(
current_face_height is not None
and current_face_height > 0
and current_face_center is not None
and (self.selected_part_id is not None or self.selected_solid_id is not None)
),
"enabled_tip": f"输入目标面内宽度;{face_size_owner_tip}",
"disabled_tip": "当前 Face 缺少稳定面内宽度、中心坐标或所属对象,不能按这个方向缩放所属对象。",
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_height, 0.2, 0.5, face_linear_hard_limit)}",
},
},
"owning": {
"label": "缩放特征",
"action": "resize_face_width_owning_scale",
"target_attr": "face_width_input",
"enabled": bool(
current_face_width is not None
and current_face_width > 0
and current_face_center is not None
and (self.selected_part_id is not None or self.selected_solid_id is not None)
),
"enabled_tip": f"输入目标面内长度;{face_size_owner_tip}",
"disabled_tip": "当前 Face 缺少稳定面内长度、中心坐标或所属对象,不能按这个方向缩放所属对象。",
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_width, 0.2, 0.5, face_linear_hard_limit)}",
},
},
value_type="positive",
used=("local_face_width", "local_face_width_direction", "local_face_size_center"),
**face_scale_limits(current_face_width),
)
add_scoped_spec(
key="local_face_height",
label="面内宽度",
current_raw=current_face_height if current_face_height is not None else "",
target_text=numeric_text(current_face_height),
scope_default="local",
scope_modes={
"local": {
"label": "局部重建",
"action": "resize_face_height_local",
"target_attr": "face_height_input",
"enabled": bool(
local_face_deform_ready
and current_face_height is not None
and current_face_height > 0
and current_face_center is not None
),
"enabled_tip": f"输入目标面内宽度;{face_size_tip}",
"disabled_tip": face_local_disabled_tip(
"只有带稳定顶点环和中心坐标的平面 Face 才能尝试修改面内宽度。"
),
"range_hint": f"{face_size_tip} {relative_range_hint(current_face_height, 0.25, 0.8, face_linear_hard_limit)}",
},
"keep_relations": {
"label": "保持关系",
"action": "resize_face_height_keep_relations",
"target_attr": "face_height_input",
"enabled": bool(
current_face_height is not None
and current_face_height > 0
and current_face_center is not None
and (self.selected_part_id is not None or self.selected_solid_id is not None)
and face_keep_relation_enabled
),
"enabled_tip": (
"输入目标面内宽度;程序会沿该方向缩放所属特征或 Solid,"
"并要求一级相邻平面的平行/垂直关系在执行前可验证、执行后可反查。"
),
"disabled_tip": (
face_keep_relation_disabled_tip
if not face_keep_relation_enabled
else "当前 Face 缺少稳定面内宽度、中心坐标或所属对象,不能保持关系地修改面内宽度。"
),
"range_hint": (
"保持关系会把面内宽度修改转换为所属对象单向缩放,并在结果上反查一级平面关系;"
f"{relative_range_hint(current_face_height, 0.25, 0.8, face_linear_hard_limit)}"
),
},
"owning": {
"label": "缩放特征",
"action": "resize_face_height_owning_scale",
"target_attr": "face_height_input",
"enabled": bool(
current_face_height is not None
and current_face_height > 0
and current_face_center is not None
and (self.selected_part_id is not None or self.selected_solid_id is not None)
),
"enabled_tip": f"输入目标面内宽度;{face_size_owner_tip}",
"disabled_tip": "当前 Face 缺少稳定面内宽度、中心坐标或所属对象,不能按这个方向缩放所属对象。",
"range_hint": f"{face_size_owner_tip} {relative_range_hint(current_face_height, 0.2, 0.5, face_linear_hard_limit)}",
},
},
value_type="positive",
used=("local_face_height", "local_face_height_direction", "local_face_size_center"),
**face_scale_limits(current_face_height),
)
value_type="positive",
used=("local_face_height", "local_face_height_direction", "local_face_size_center"),
**face_scale_limits(current_face_height),
)
add_scoped_spec(
key="face_center_position",
label="中心",
@@ -3773,11 +3821,7 @@ class WindowStateMixin:
)
if is_plane and not is_existing_chamfer:
plane_origin = _triple_or_none(action_info.get("plane_origin"))
plane_direction = (
_triple_or_none(action_info.get("push_pull_outward_direction"))
or _triple_or_none(action_info.get("normal"))
)
plane_origin, plane_direction = self._plane_offset_info_for_property(action_info)
current_plane_position = None
if plane_origin is not None and plane_direction is not None:
direction_length = math.sqrt(
@@ -3845,10 +3889,10 @@ class WindowStateMixin:
"action": "push_pull_face",
"target_attr": "offset_input",
"enabled": current_plane_position is not None,
"enabled_tip": "输入偏移的目标位置;程序会沿当前 Face 法向执行拉伸或切除。",
"enabled_tip": "输入偏移的目标位置;程序会沿判定的材料外侧法向执行拉伸或切除。",
"disabled_tip": "当前平面缺少稳定移动方向或基准点,不能按目标位置拉伸/切除。",
"range_hint": (
"偏移是沿当前 Face 法向测量的目标位置,不是面积、不是移动距离,也不是 X/Y/Z 坐标;单位同模型。"
"偏移是沿程序判定的材料外侧法向测量的目标位置,不是面积、不是移动距离,也不是 X/Y/Z 坐标;单位同模型。"
"程序会把目标位置自动换算成本次拉伸/切除距离。"
f"{face_offset_hard_limit}"
),
@@ -3861,7 +3905,7 @@ class WindowStateMixin:
"target_attr": "offset_input",
"enabled": keep_relation_enabled,
"enabled_tip": (
"输入偏移的目标位置;程序会沿当前 Face 法向拉伸/切除,"
"输入偏移的目标位置;程序会沿判定的材料外侧法向拉伸/切除,"
"并要求一级相邻平面的平行/垂直关系可验证。"
),
"disabled_tip": keep_relation_disabled_tip,
@@ -3884,7 +3928,7 @@ class WindowStateMixin:
and current_face_center is not None
),
"enabled_tip": (
"输入偏移的目标位置;程序只把当前 Face 沿法向移动到该目标值,"
"输入偏移的目标位置;程序只把当前 Face 沿判定的材料外侧法向移动到该目标值,"
"并让相邻平面按新顶点重建。"
),
"disabled_tip": face_local_disabled_tip(
@@ -3908,7 +3952,7 @@ class WindowStateMixin:
and (self.selected_part_id is not None or self.selected_solid_id is not None)
),
"enabled_tip": (
"输入偏移的目标位置;程序会沿当前 Face 法向平移所属特征或 Solid"
"输入偏移的目标位置;程序会沿判定的材料外侧法向平移所属特征或 Solid"
"当前面形状和所属对象内部尺寸不变。"
),
"disabled_tip": "当前平面缺少稳定方向、基准点或所属对象,不能按偏移移动所属特征。",
@@ -5863,12 +5907,19 @@ class WindowStateMixin:
changed = self._changed_property_rows()
enabled = bool(has_model and changed)
disabled_tip = "请先选择对象,并在属性表中修改一个可编辑目标值。"
if changed and len(changed) > 1:
disabled_tip = "当前一次只执行一个几何修改;请只保留一行目标值不同,再点击参数化建模。"
if changed:
count = len(changed)
enabled_tip = (
"应用当前被修改的 1 个参数。"
if count == 1
else f"按表格顺序依次应用当前被修改的 {count} 个参数;失败时会停止后续修改。"
)
else:
enabled_tip = "应用当前被修改的参数。"
self._set_control_state(
self.apply_property_button,
enabled and len(changed) == 1,
"应用当前被修改的参数。",
enabled,
enabled_tip,
disabled_tip,
)
self._update_parameter_export_state(has_model)
@@ -5907,6 +5958,37 @@ class WindowStateMixin:
)
return rows
def _selected_parameter_component_edits(self, rows: list[dict[str, str]] | None = None) -> list[dict[str, object]]:
if not hasattr(self, "property_table"):
return []
specs = getattr(self, "property_editor_specs", [])
row_count = min(self.property_table.rowCount(), len(specs))
export_rows = list(rows or self._selected_parameter_export_rows())
edits: list[dict[str, object]] = []
export_index = 0
step_path = self.step_path if isinstance(getattr(self, "step_path", None), Path) else None
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
if export_index >= len(export_rows):
break
spec = self._effective_property_spec(specs[row], row=row)
if str(spec.get("value_type", "number")) == "command":
continue
edit = component_edit_config_from_spec(
parameter_row=export_rows[export_index],
spec=spec,
selected_kind=str(getattr(self, "selected_kind", "") or ""),
selected_face_id=getattr(self, "selected_face_id", None),
selected_edge_id=getattr(self, "selected_edge_id", None),
step_path=step_path,
)
if edit is not None:
edits.append(edit)
export_index += 1
return edits
def _update_parameter_export_state(self, has_model: bool | None = None) -> None:
if not hasattr(self, "export_parameters_button"):
return
@@ -5922,7 +6004,7 @@ class WindowStateMixin:
self._set_control_state(
self.export_parameters_button,
bool(has_model and selected_rows),
f"导出已勾选的 {len(selected_rows)} 个输入参数到 data.json。",
f"导出已勾选的 {len(selected_rows)} 个输入参数到 data.json,并生成参数化组件 main.py",
disabled_tip,
)
@@ -5949,6 +6031,63 @@ class WindowStateMixin:
changed.append((row, effective_spec, text))
return changed
def _property_batch_item_from_row(self, row: int, spec: dict[str, object], text: str) -> dict[str, object]:
return {
"key": str(spec.get("key", "")),
"label": str(spec.get("label", "")),
"action": str(spec.get("action", "")),
"scope_key": str(spec.get("scope_key", self._property_scope_value(row, spec))),
"target_text": str(text),
"source_face_id": spec.get("source_face_id", self.selected_face_id),
"source_edge_id": spec.get("source_edge_id", self.selected_edge_id),
"selected_kind": self.selected_kind,
}
def _property_batch_row_for_item(self, item: dict[str, object]) -> int | None:
specs = getattr(self, "property_editor_specs", [])
target_key = str(item.get("key", ""))
target_label = str(item.get("label", ""))
target_action = str(item.get("action", ""))
target_scope = str(item.get("scope_key", ""))
for row, spec in enumerate(specs):
if target_key and str(spec.get("key", "")) != target_key:
continue
if target_label and str(spec.get("label", "")) != target_label:
continue
modes = spec.get("scope_modes")
mode: dict[str, object] | None = None
if target_scope and isinstance(modes, dict):
candidate_mode = modes.get(target_scope)
if not isinstance(candidate_mode, dict):
continue
mode = dict(candidate_mode)
effective = self._effective_property_spec(spec, row=row)
if target_action and str(effective.get("action", "")) != target_action:
scoped_action = str((mode or {}).get("action", ""))
if scoped_action != target_action:
continue
if target_scope and not isinstance(modes, dict):
if str(effective.get("scope_key", "")) not in {"", target_scope}:
continue
return row
return None
def _set_property_scope_value(self, row: int, scope_key: str) -> bool:
if not scope_key or not hasattr(self, "property_table"):
return True
widget = self.property_table.cellWidget(row, PROPERTY_SCOPE_COLUMN)
if not isinstance(widget, NoWheelComboBox):
return True
index = widget.findData(scope_key)
if index < 0:
index = widget.findText(scope_key)
if index < 0:
return False
if widget.currentIndex() == index:
return True
widget.setCurrentIndex(index)
return True
def _property_target_text(self, row: int) -> str:
if not hasattr(self, "property_table"):
return ""
@@ -6133,11 +6272,100 @@ class WindowStateMixin:
self.statusBar().showMessage("请先在当前选中对象表中修改一个可编辑目标值。")
return
if len(changed) > 1:
QMessageBox.information(self, "一次执行一个修改", "请只修改一行目标值,然后再点击参数化建模。")
self._start_property_batch_edit(changed)
return
row, _spec, _text = changed[0]
self.apply_property_row_edit(row)
def _start_property_batch_edit(self, changed: list[tuple[int, dict[str, object], str]]) -> None:
if self._edit_busy("请等待当前编辑完成后再执行批量参数化建模。"):
return
items = [self._property_batch_item_from_row(row, spec, text) for row, spec, text in changed]
self.property_batch_queue = items
self.property_batch_total = len(items)
self.property_batch_done = 0
self.property_batch_active = True
self.statusBar().showMessage(f"开始批量参数化建模:共 {len(items)} 个参数。")
self._run_next_property_batch_item()
def _run_next_property_batch_item(self) -> None:
queue = list(getattr(self, "property_batch_queue", []) or [])
if not queue:
total = int(getattr(self, "property_batch_total", 0) or 0)
self._clear_property_batch_state()
self.statusBar().showMessage(f"批量参数化建模完成:已应用 {total} 个参数。")
self._update_property_apply_state()
return
item = queue.pop(0)
self.property_batch_queue = queue
row = self._property_batch_row_for_item(item)
if row is None:
self._clear_property_batch_state()
QMessageBox.information(
self,
"批量参数化建模已停止",
f"找不到后续参数“{item.get('label', '')}”,模型拓扑可能已变化。已停止后续修改。",
)
self._update_property_apply_state()
return
if not self._set_property_scope_value(row, str(item.get("scope_key", ""))):
self._clear_property_batch_state()
QMessageBox.information(
self,
"批量参数化建模已停止",
f"参数“{item.get('label', '')}”的建模意图已经不可用。已停止后续修改。",
)
self._update_property_apply_state()
return
target_widget = self.property_table.cellWidget(row, PROPERTY_TARGET_COLUMN)
if isinstance(target_widget, QLineEdit):
target_widget.setText(str(item.get("target_text", "")))
done = int(getattr(self, "property_batch_done", 0) or 0)
total = int(getattr(self, "property_batch_total", 0) or 0)
self.statusBar().showMessage(f"批量参数化建模:正在执行 {done + 1}/{total} - {item.get('label', '')}")
self._property_batch_running_action = True
self._property_batch_item_callback_seen = False
try:
self.apply_property_row_edit(row)
finally:
self._property_batch_running_action = False
if (
bool(getattr(self, "property_batch_active", False))
and not bool(getattr(self, "_property_batch_item_callback_seen", False))
and not bool(getattr(self, "operation_in_progress", False))
):
remaining = len(getattr(self, "property_batch_queue", []) or [])
done = int(getattr(self, "property_batch_done", 0) or 0)
self._clear_property_batch_state()
QMessageBox.information(
self,
"批量参数化建模已停止",
f"参数“{item.get('label', '')}”没有启动可执行修改。已完成 {done} 个,剩余 {remaining} 个未执行。",
)
self._update_property_apply_state()
def _clear_property_batch_state(self) -> None:
self.property_batch_queue = []
self.property_batch_total = 0
self.property_batch_done = 0
self.property_batch_active = False
self._property_batch_running_action = False
self._property_batch_item_callback_seen = False
def _after_property_edit_finished(self, *, success: bool) -> None:
if not bool(getattr(self, "property_batch_active", False)):
return
self._property_batch_item_callback_seen = True
if not success:
remaining = len(getattr(self, "property_batch_queue", []) or [])
done = int(getattr(self, "property_batch_done", 0) or 0)
self._clear_property_batch_state()
self.statusBar().showMessage(f"批量参数化建模已停止:已完成 {done} 个,剩余 {remaining} 个未执行。")
self._update_property_apply_state()
return
self.property_batch_done = int(getattr(self, "property_batch_done", 0) or 0) + 1
QTimer.singleShot(0, self._run_next_property_batch_item)
def _activate_property_source_feature(self, spec: dict[str, object]) -> None:
source_face_id = _int_or_none(spec.get("source_face_id"))
if source_face_id is None or source_face_id == self.selected_face_id:
@@ -6453,6 +6681,41 @@ class WindowStateMixin:
return current_length * target_radius / current_radius
raise ValueError(f"这个属性使用了未知的目标换算方式:{transform}")
def _plane_offset_info_for_property(
self,
action_info: dict[str, object],
) -> tuple[tuple[float, float, float] | None, tuple[float, float, float] | None]:
plane_origin = _triple_or_none(action_info.get("plane_origin"))
plane_direction = _triple_or_none(action_info.get("push_pull_outward_direction"))
selected_face_id = _int_or_none(action_info.get("feature_source_face_id"))
if selected_face_id is None:
selected_face_id = _int_or_none(action_info.get("topological_face_id"))
if selected_face_id is None:
selected_face_id = _int_or_none(action_info.get("face_id"))
model_faces = getattr(self.model, "faces", None) if self.model is not None else None
if (
self.model is not None
and model_faces is not None
and selected_face_id is not None
and 0 <= selected_face_id < len(model_faces)
and (plane_origin is None or plane_direction is None)
):
try:
full_info = self.model.face_info(selected_face_id)
except Exception:
full_info = {}
if plane_origin is None:
plane_origin = _triple_or_none(full_info.get("plane_origin"))
if plane_direction is None:
plane_direction = _triple_or_none(full_info.get("push_pull_outward_direction"))
if plane_direction is None:
plane_direction = _triple_or_none(full_info.get("oriented_normal"))
if plane_direction is None:
plane_direction = _triple_or_none(action_info.get("oriented_normal"))
if plane_direction is None:
plane_direction = _triple_or_none(action_info.get("normal"))
return plane_origin, plane_direction
def _transform_property_vector3_target(
self,
spec: dict[str, object],