feat: 完善 STEP/B-Rep 一级关系参数化编辑
This commit is contained in:
+113
-17
@@ -190,8 +190,11 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.hover_after_camera_cooldown_ms = 420
|
||||
self.pointer_button_down = False
|
||||
self.left_button_press_position: tuple[int, int] | None = None
|
||||
self.left_button_press_camera_state: tuple[float, ...] | None = None
|
||||
self.left_button_press_target: dict[str, object] | None = None
|
||||
self.left_button_dragged = False
|
||||
self.left_click_drag_threshold_px = 6
|
||||
self.left_click_camera_tolerance = 1e-7
|
||||
self.skip_next_vtk_left_press = False
|
||||
self.skip_next_vtk_left_release = False
|
||||
self.pick_marker_actor = None
|
||||
@@ -270,7 +273,8 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self._selected_action_info_cache_value: dict[str, object] | None = None
|
||||
self.property_editor_specs: list[dict[str, object]] = []
|
||||
self.property_table_expanded = False
|
||||
self.property_table_collapsed_rows = 8
|
||||
self.property_table_collapsed_rows = 5
|
||||
self.property_table_min_visible_rows = 5
|
||||
self.property_editor_selected_row: int | None = None
|
||||
self.property_command_active_key = ""
|
||||
self.property_command_buttons: dict[str, QPushButton] = {}
|
||||
@@ -472,6 +476,24 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
border-color: #cbd5e1;
|
||||
color: #475569;
|
||||
}
|
||||
QGroupBox#capabilitySection {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-left: 5px solid #16a34a;
|
||||
}
|
||||
QGroupBox#capabilitySection::title {
|
||||
background: #dcfce7;
|
||||
border-color: #bbf7d0;
|
||||
color: #14532d;
|
||||
}
|
||||
QLabel#capabilityHeadline {
|
||||
color: #14532d;
|
||||
font-weight: 800;
|
||||
}
|
||||
QLabel#capabilityDetail {
|
||||
color: #166534;
|
||||
font-size: 11px;
|
||||
}
|
||||
QLabel {
|
||||
color: #2f3846;
|
||||
}
|
||||
@@ -510,6 +532,59 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
color: #8f99a8;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton#parametricModelButton {
|
||||
background: #1f6feb;
|
||||
border: 1px solid #1d4ed8;
|
||||
border-bottom-color: #1e40af;
|
||||
border-radius: 6px;
|
||||
color: #ffffff;
|
||||
font-weight: 800;
|
||||
min-height: 30px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
QPushButton#parametricModelButton:hover {
|
||||
background: #2563eb;
|
||||
border-color: #1e40af;
|
||||
}
|
||||
QPushButton#parametricModelButton:pressed {
|
||||
background: #1e40af;
|
||||
border-color: #1e3a8a;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
QPushButton#parametricModelButton:disabled {
|
||||
background: #dbe4ef;
|
||||
border: 1px dashed #adb9c8;
|
||||
color: #8b97a7;
|
||||
font-weight: 700;
|
||||
}
|
||||
QPushButton#quickExportStepButton {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #64748b;
|
||||
border-bottom-color: #475569;
|
||||
border-radius: 6px;
|
||||
color: #1f2937;
|
||||
font-weight: 700;
|
||||
min-height: 30px;
|
||||
padding: 6px 11px;
|
||||
}
|
||||
QPushButton#quickExportStepButton:hover {
|
||||
background: #eef6ff;
|
||||
border-color: #2563eb;
|
||||
color: #1e3a8a;
|
||||
}
|
||||
QPushButton#quickExportStepButton:pressed {
|
||||
background: #dbeafe;
|
||||
border-color: #1d4ed8;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
QPushButton#quickExportStepButton:disabled {
|
||||
background: #eef2f6;
|
||||
border: 1px dashed #bcc7d4;
|
||||
color: #8f99a8;
|
||||
font-weight: 650;
|
||||
}
|
||||
QPushButton#propertyRowEditButton {
|
||||
background: #ea580c;
|
||||
border: 1px solid #c2410c;
|
||||
@@ -1052,20 +1127,20 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.object_edit_box.setObjectName("editSection")
|
||||
help_tip(
|
||||
self.object_edit_box,
|
||||
"选择对象后,这里以参数卡片显示当前值、建模意图、目标值和应用按钮;诊断信息默认折叠在后面。",
|
||||
"选择特征后,这里只列出可可靠修改的语义尺寸;结果属性和识别依据位于下方诊断信息。",
|
||||
)
|
||||
object_edit_layout = QVBoxLayout(self.object_edit_box)
|
||||
object_edit_layout.setContentsMargins(0, 8, 0, 4)
|
||||
object_edit_layout.setSpacing(4)
|
||||
self.property_table = QTableWidget(0, 5, self.object_edit_box)
|
||||
self.property_table = QTableWidget(0, 4, self.object_edit_box)
|
||||
self.property_table.setObjectName("propertyTable")
|
||||
self.property_table.setHorizontalHeaderLabels(["参数", "当前值", "建模意图", "目标值", "操作"])
|
||||
self.property_table.setHorizontalHeaderLabels(["尺寸参数", "当前值", "建模意图", "目标值"])
|
||||
self.property_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.property_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.property_table.setAlternatingRowColors(True)
|
||||
self.property_table.setMinimumHeight(180)
|
||||
self.property_table.setWordWrap(False)
|
||||
self.property_table.setTextElideMode(Qt.TextElideMode.ElideRight)
|
||||
self.property_table.setTextElideMode(Qt.TextElideMode.ElideMiddle)
|
||||
self.property_table.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.property_table.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.property_table.verticalHeader().setVisible(False)
|
||||
@@ -1078,22 +1153,26 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
property_header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||
property_header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
||||
property_header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
|
||||
property_header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed)
|
||||
self._resize_property_table_columns()
|
||||
property_header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||
self.property_table.setColumnWidth(0, 148)
|
||||
self.property_table.setColumnWidth(1, 92)
|
||||
self.property_table.setColumnWidth(2, 112)
|
||||
self.property_table.setColumnWidth(3, 96)
|
||||
self.property_table.installEventFilter(self)
|
||||
help_tip(
|
||||
self.property_table,
|
||||
"特征模式显示当前特征及局部关联特征的可变尺寸;建模意图决定这次修改是局部重建、拉伸/切除、端面移动还是整体缩放。",
|
||||
)
|
||||
self.property_table.itemChanged.connect(self._on_property_table_item_changed)
|
||||
self.property_table.setVisible(False)
|
||||
self.property_table.setMaximumHeight(0)
|
||||
object_edit_layout.addWidget(self.property_table)
|
||||
self.property_command_summary_label = QLabel("未选择可编辑对象")
|
||||
self.property_command_summary_label.setObjectName("propertyCommandSummary")
|
||||
self.property_command_summary_label.setWordWrap(True)
|
||||
self.property_command_summary_label.setVisible(False)
|
||||
object_edit_layout.addWidget(self.property_command_summary_label)
|
||||
self.property_command_bar = QFrame()
|
||||
self.property_command_bar.setObjectName("propertyCommandBar")
|
||||
self.property_command_bar.setVisible(False)
|
||||
self.property_command_layout = QHBoxLayout(self.property_command_bar)
|
||||
self.property_command_layout.setContentsMargins(3, 3, 3, 3)
|
||||
self.property_command_layout.setSpacing(2)
|
||||
@@ -1117,12 +1196,14 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
self.property_card_scroll.setWidget(self.property_card_container)
|
||||
help_tip(
|
||||
self.property_card_scroll,
|
||||
"每张卡片对应一个参数;修改目标值后,点击卡片右上角的按钮只应用这一项。",
|
||||
"旧展示层已停用;当前使用上方表格展示参数。",
|
||||
)
|
||||
self.property_card_scroll.setVisible(False)
|
||||
self.property_card_scroll.setMaximumHeight(0)
|
||||
object_edit_layout.addWidget(self.property_card_scroll)
|
||||
self.property_expand_button = QPushButton("更多诊断信息")
|
||||
self.property_expand_button = QPushButton("展开全部参数")
|
||||
self.property_expand_button.setObjectName("propertyExpandBar")
|
||||
help_tip(self.property_expand_button, "默认优先显示可编辑参数和关键说明;点击这里展开或收起完整诊断信息。")
|
||||
help_tip(self.property_expand_button, "参数较多时默认只显示前几行;点击这里展开或收起完整参数列表。")
|
||||
self.property_expand_button.clicked.connect(self.toggle_property_table_expanded)
|
||||
self.property_expand_button.setVisible(False)
|
||||
self.property_expand_button.setMaximumHeight(22)
|
||||
@@ -1131,10 +1212,15 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
property_action_row = QHBoxLayout()
|
||||
property_action_row.setContentsMargins(0, 0, 0, 0)
|
||||
self.apply_property_button = QPushButton("参数化建模")
|
||||
self.apply_property_button.setVisible(False)
|
||||
help_tip(self.apply_property_button, "按当前属性表里修改过的目标值执行参数化建模;一次只执行一个几何修改,成功后可撤销。")
|
||||
self.apply_property_button.setObjectName("parametricModelButton")
|
||||
self.apply_property_button.setMinimumHeight(34)
|
||||
self.apply_property_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
help_tip(self.apply_property_button, "应用当前被修改的一个参数;一次只执行一个几何修改,成功后可撤销。")
|
||||
self.apply_property_button.clicked.connect(self.apply_current_property_edit)
|
||||
self.quick_export_all_button = QPushButton("导出当前完整STEP")
|
||||
self.quick_export_all_button.setObjectName("quickExportStepButton")
|
||||
self.quick_export_all_button.setMinimumHeight(34)
|
||||
self.quick_export_all_button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
help_tip(self.quick_export_all_button, "把当前编辑后的完整模型导出为 STEP 文件。导出前会做基础质量检查。")
|
||||
self.quick_export_all_button.clicked.connect(self.export_all)
|
||||
property_action_row.addWidget(self.apply_property_button)
|
||||
@@ -1245,13 +1331,13 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
help_tip(self.torus_radius_input, "环面的目标半径。当前选中对象表会使用这个隐藏输入执行环面修改。")
|
||||
self.face_area_input = QLineEdit("", edit_box)
|
||||
self.face_area_input.setVisible(False)
|
||||
help_tip(self.face_area_input, "目标面面积。当前选中对象表会使用这个隐藏输入执行所属特征或 Solid 的均匀缩放。")
|
||||
help_tip(self.face_area_input, "面积是诊断结果,不作为特征参数表里的驱动变量;该隐藏输入仅保留给历史后端回归路径使用。")
|
||||
self.face_width_input = QLineEdit("", edit_box)
|
||||
self.face_width_input.setVisible(False)
|
||||
help_tip(self.face_width_input, "目标U向尺寸。当前选中对象表会使用这个隐藏输入修改 Face 平面内第一个方向的尺寸。")
|
||||
help_tip(self.face_width_input, "目标面内长度。当前选中对象表会使用这个隐藏输入修改 Face 平面内第一个方向的尺寸。")
|
||||
self.face_height_input = QLineEdit("", edit_box)
|
||||
self.face_height_input.setVisible(False)
|
||||
help_tip(self.face_height_input, "目标V向尺寸。当前选中对象表会使用这个隐藏输入修改 Face 平面内第二个方向的尺寸。")
|
||||
help_tip(self.face_height_input, "目标面内宽度。当前选中对象表会使用这个隐藏输入修改 Face 平面内第二个方向的尺寸。")
|
||||
self.resize_boss_button = QPushButton("调整圆柱凸台直径")
|
||||
help_tip(self.resize_boss_button, "修改完整圆柱凸台直径。变大会加料,变小会重建凸台区域。")
|
||||
self.resize_boss_button.clicked.connect(self.resize_boss)
|
||||
@@ -1458,6 +1544,16 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
|
||||
edit_layout.addWidget(self.rotate_solid_button, 27, 0, 1, 2)
|
||||
|
||||
panel_layout.addWidget(self.object_edit_box)
|
||||
self.current_capability_box = QGroupBox("软件进度")
|
||||
self.current_capability_box.setObjectName("capabilitySection")
|
||||
capability_layout = QVBoxLayout(self.current_capability_box)
|
||||
capability_layout.setContentsMargins(8, 8, 8, 7)
|
||||
capability_layout.setSpacing(2)
|
||||
self.current_capability_headline = QLabel("当前支持:Face、孔/槽、Edge、凸台、圆角/倒角、壳体")
|
||||
self.current_capability_headline.setObjectName("capabilityHeadline")
|
||||
self.current_capability_headline.setWordWrap(True)
|
||||
capability_layout.addWidget(self.current_capability_headline)
|
||||
panel_layout.addWidget(self.current_capability_box)
|
||||
if ENABLE_EXPORT_PANEL:
|
||||
panel_layout.addWidget(export_box)
|
||||
if ENABLE_VIEW_PANEL:
|
||||
|
||||
+65
-31
@@ -61,6 +61,7 @@ from OCC.Extend.TopologyUtils import TopologyExplorer, discretize_edge
|
||||
|
||||
from .constants import CURVE_TYPES, SNAPSHOT_FACE_LOGICAL_IDS_KEY, SURFACE_TYPES
|
||||
from .geometry_utils import * # noqa: F403
|
||||
from .recognition_priority import feature_recognition_sort_key
|
||||
|
||||
|
||||
class FeatureMixin:
|
||||
@@ -318,6 +319,9 @@ class FeatureMixin:
|
||||
feature_guess = str(item["feature_guess"])
|
||||
if existing_fillet_count < existing_fillet_limit and feature_guess == "round/fillet candidate":
|
||||
feature = self.feature_info(int(item["face_id"]))
|
||||
existing_fillet_status = str(feature.get("existing_fillet_status") or "candidate")
|
||||
if existing_fillet_status == "blocked":
|
||||
continue
|
||||
support_face_ids = tuple(feature.get("feature_existing_fillet_support_face_ids", ()))
|
||||
support_note = (
|
||||
f"支撑 Face: {support_face_ids}。"
|
||||
@@ -349,7 +353,10 @@ class FeatureMixin:
|
||||
)
|
||||
existing_fillet_count += 1
|
||||
|
||||
if diameter_count < per_type_limit and feature_guess != "round/fillet candidate":
|
||||
if diameter_count < per_type_limit and feature_guess not in {
|
||||
"round/fillet candidate",
|
||||
"boss/outer-round candidate",
|
||||
}:
|
||||
candidates.append(
|
||||
{
|
||||
"operation_key": "resize_cylinder",
|
||||
@@ -735,6 +742,8 @@ class FeatureMixin:
|
||||
chamfer_edge_count = 0
|
||||
edge_length_count = 0
|
||||
circle_edge_radius_count = 0
|
||||
ellipse_edge_major_radius_count = 0
|
||||
ellipse_edge_minor_radius_count = 0
|
||||
edge_type_limit = max(1, per_type_limit // 3)
|
||||
edge_scan_limit = len(self.edges) if max_scan_edges is None else min(len(self.edges), max(0, int(max_scan_edges)))
|
||||
for edge_id, edge in enumerate(self.edges[:edge_scan_limit]):
|
||||
@@ -750,6 +759,7 @@ class FeatureMixin:
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
is_line_edge = curve.GetType() == GeomAbs_Line
|
||||
is_circle_edge = curve.GetType() == GeomAbs_Circle
|
||||
is_ellipse_edge = curve.GetType() == GeomAbs_Ellipse
|
||||
props = GProp_GProps()
|
||||
brepgprop.LinearProperties(edge, props)
|
||||
length = props.Mass()
|
||||
@@ -799,7 +809,7 @@ class FeatureMixin:
|
||||
}
|
||||
)
|
||||
chamfer_edge_count += 1
|
||||
if edge_length_count < edge_type_limit:
|
||||
if (is_line_edge or is_circle_edge) and edge_length_count < edge_type_limit:
|
||||
candidates.append(
|
||||
{
|
||||
"operation_key": "resize_edge_length",
|
||||
@@ -814,9 +824,9 @@ class FeatureMixin:
|
||||
"current_value": length,
|
||||
"current_value_label": "length",
|
||||
"status": "caution",
|
||||
"risk": "medium" if is_line_edge or is_circle_edge else "high",
|
||||
"risk": "medium",
|
||||
"confidence": "pending",
|
||||
"note": "快速扫描:直线边优先局部形变;圆边优先换算相邻圆柱直径;椭圆/平面曲线会尝试径向缩放;其他边使用后备策略。",
|
||||
"note": "快速扫描:直线边优先局部形变或端面移动;圆边优先换算相邻圆柱直径。",
|
||||
}
|
||||
)
|
||||
edge_length_count += 1
|
||||
@@ -846,34 +856,58 @@ class FeatureMixin:
|
||||
}
|
||||
)
|
||||
circle_edge_radius_count += 1
|
||||
if is_ellipse_edge:
|
||||
ellipse = curve.Ellipse()
|
||||
if ellipse_edge_major_radius_count < edge_type_limit:
|
||||
major_radius = float(ellipse.MajorRadius())
|
||||
if major_radius > 1e-9:
|
||||
candidates.append(
|
||||
{
|
||||
"operation_key": "resize_ellipse_edge_major_radius",
|
||||
"operation": "修改椭圆Edge主半径",
|
||||
"target_kind": "edge",
|
||||
"target_id": edge_id,
|
||||
"edge_id": edge_id,
|
||||
"part_id": self.edge_part_ids[edge_id],
|
||||
"solid_id": solid_id,
|
||||
"surface": "edge",
|
||||
"feature_guess": "elliptical edge major radius candidate",
|
||||
"current_value": major_radius,
|
||||
"current_value_label": "major_radius",
|
||||
"status": "caution",
|
||||
"risk": "medium",
|
||||
"confidence": "pending",
|
||||
"note": "快速扫描:点击后会选中椭圆Edge;在参数表里按主轴方向单轴缩放所属对象。",
|
||||
}
|
||||
)
|
||||
ellipse_edge_major_radius_count += 1
|
||||
if ellipse_edge_minor_radius_count < edge_type_limit:
|
||||
minor_radius = float(ellipse.MinorRadius())
|
||||
if minor_radius > 1e-9:
|
||||
candidates.append(
|
||||
{
|
||||
"operation_key": "resize_ellipse_edge_minor_radius",
|
||||
"operation": "修改椭圆Edge小半径",
|
||||
"target_kind": "edge",
|
||||
"target_id": edge_id,
|
||||
"edge_id": edge_id,
|
||||
"part_id": self.edge_part_ids[edge_id],
|
||||
"solid_id": solid_id,
|
||||
"surface": "edge",
|
||||
"feature_guess": "elliptical edge minor radius candidate",
|
||||
"current_value": minor_radius,
|
||||
"current_value_label": "minor_radius",
|
||||
"status": "caution",
|
||||
"risk": "medium",
|
||||
"confidence": "pending",
|
||||
"note": "快速扫描:点击后会选中椭圆Edge;在参数表里按小轴方向单轴缩放所属对象。",
|
||||
}
|
||||
)
|
||||
ellipse_edge_minor_radius_count += 1
|
||||
|
||||
status_order = {"ready": 0, "caution": 1, "blocked": 2}
|
||||
risk_order = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
|
||||
operation_order = {
|
||||
"resize_cylinder": 0,
|
||||
"resize_slot_width": 1,
|
||||
"resize_slot_depth": 2,
|
||||
"resize_slot_arc_length": 3,
|
||||
"resize_slot_angular_span": 4,
|
||||
"resize_boss": 5,
|
||||
"resize_boss_height": 6,
|
||||
"suppress_cylinder": 7,
|
||||
"resize_depth": 8,
|
||||
"inspect_existing_fillet": 9,
|
||||
"resize_shell_thickness": 10,
|
||||
"push_pull_plane": 11,
|
||||
"fillet_edge": 12,
|
||||
"chamfer_edge": 13,
|
||||
"resize_edge_length": 14,
|
||||
}
|
||||
candidates.sort(
|
||||
key=lambda item: (
|
||||
status_order.get(str(item["status"]), 9),
|
||||
risk_order.get(str(item["risk"]), 9),
|
||||
operation_order.get(str(item["operation_key"]), 9),
|
||||
int(item.get("target_id", item.get("face_id", item.get("edge_id", -1)))),
|
||||
)
|
||||
)
|
||||
for candidate in candidates:
|
||||
candidate["recognition_user_priority"] = feature_recognition_sort_key(candidate)[0]
|
||||
candidates.sort(key=feature_recognition_sort_key)
|
||||
return candidates[:limit]
|
||||
|
||||
def cylindrical_feature_candidates(
|
||||
|
||||
+345
-15
@@ -80,6 +80,12 @@ from .export import ExportMixin
|
||||
from .features import FeatureMixin
|
||||
from .operations import OperationMixin
|
||||
from .polydata import PolydataMixin
|
||||
from .recognition_priority import (
|
||||
feature_recognition_priority,
|
||||
feature_recognition_priority_label,
|
||||
feature_recognition_priority_reason,
|
||||
feature_recognition_sort_key,
|
||||
)
|
||||
from .transforms import TransformMixin
|
||||
from .geometry_utils import * # noqa: F403
|
||||
from .model_types import PartNode, TopologyStats
|
||||
@@ -326,7 +332,11 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
raise ValueError(f"Unknown face id {face_id}")
|
||||
full_info = self._face_info_cache.get(face_id)
|
||||
if full_info is not None:
|
||||
return dict(full_info)
|
||||
info = dict(full_info)
|
||||
if str(info.get("surface") or "") == "cylinder" and not str(info.get("feature_type") or ""):
|
||||
info.update(self._cylindrical_feature_label_fields(info))
|
||||
info.update(self._recognition_summary_fields(info))
|
||||
return info
|
||||
cached = self._quick_face_info_cache.get(face_id)
|
||||
if cached is not None:
|
||||
return dict(cached)
|
||||
@@ -419,6 +429,39 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info["slot_chord_width_estimate"] = 2.0 * radius * math.sin(min(u_span, math.tau) * 0.5)
|
||||
info["slot_sagitta_depth_estimate"] = radius * (1.0 - math.cos(min(u_span, math.tau) * 0.5))
|
||||
info["slot_arc_length_estimate"] = radius * u_span
|
||||
info.update(self._quick_cylindrical_feature_hint(face_id, surf, info))
|
||||
if info.get("feature_guess") == "hole/groove candidate":
|
||||
try:
|
||||
side_face_ids = _int_values(info.get("same_domain_face_ids")) or [face_id]
|
||||
axis_range = self._cylindrical_axis_range(face_id, surf, side_face_ids)
|
||||
info.update(self._cylinder_end_opening_info(face_id, surf, axis_range))
|
||||
info.update(self._cylindrical_feature_label_fields(info))
|
||||
except Exception:
|
||||
pass
|
||||
info.update(_cylinder_resize_readiness(info))
|
||||
info.update(_cylinder_depth_readiness(info))
|
||||
info.update(_cylinder_suppress_readiness(info))
|
||||
elif info.get("feature_guess") == "boss/outer-round candidate":
|
||||
info.update(
|
||||
{
|
||||
"resize_status": "blocked",
|
||||
"resize_risk": "blocked",
|
||||
"resize_blockers": "当前对象快速识别为凸台/外圆;请使用凸台直径/高度/轴心入口,不按孔径重切。",
|
||||
"resize_warnings": "",
|
||||
"resize_note": "当前对象快速识别为凸台/外圆;请使用凸台直径/高度/轴心入口,不按孔径重切。",
|
||||
}
|
||||
)
|
||||
info.update(_cylinder_boss_resize_readiness(info))
|
||||
elif info.get("feature_guess") == "round/fillet candidate":
|
||||
info.update(
|
||||
{
|
||||
"resize_status": "blocked",
|
||||
"resize_risk": "blocked",
|
||||
"resize_blockers": "当前对象快速识别为圆角/倒圆;请使用已有圆角半径入口,不按孔径重切。",
|
||||
"resize_warnings": "",
|
||||
"resize_note": "当前对象快速识别为圆角/倒圆;请使用已有圆角半径入口,不按孔径重切。",
|
||||
}
|
||||
)
|
||||
elif surface_type == GeomAbs_Cone:
|
||||
cone = surf.Cone()
|
||||
info["axis_point"] = _point_tuple(cone.Location())
|
||||
@@ -449,6 +492,189 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
self._quick_face_info_cache[face_id] = dict(info)
|
||||
return dict(info)
|
||||
|
||||
def _cylindrical_feature_label_fields(self, info: dict[str, object]) -> dict[str, object]:
|
||||
guess = str(info.get("feature_guess", "cylindrical face"))
|
||||
angular_span = _float_or_none(info.get("same_domain_angular_span"))
|
||||
if angular_span is None:
|
||||
angular_span = _float_or_none(info.get("angular_span")) or 0.0
|
||||
is_full = bool(info.get("is_full_cylinder")) or angular_span >= math.tau * 0.92
|
||||
if guess == "hole/groove candidate":
|
||||
if not is_full:
|
||||
return {
|
||||
"feature_type": "槽/半孔候选",
|
||||
"feature_edit_actions": (
|
||||
"调整槽/半孔宽度、深度、圆弧长度、圆弧角度;"
|
||||
"完整槽孔配对会在执行时计算。"
|
||||
),
|
||||
}
|
||||
edit_actions = "调整圆柱孔径"
|
||||
if info.get("cylinder_end_type") == "blind":
|
||||
edit_actions += ";调整盲孔/盲槽深度"
|
||||
else:
|
||||
edit_actions += ";孔深调整需要明确盲孔底面"
|
||||
edit_actions += ";封堵圆柱孔"
|
||||
return {
|
||||
"feature_type": "圆柱孔候选",
|
||||
"feature_edit_actions": edit_actions,
|
||||
}
|
||||
if guess == "boss/outer-round candidate":
|
||||
return {
|
||||
"feature_type": "凸台/外圆候选",
|
||||
"feature_edit_actions": "调整圆柱凸台直径;调整圆柱凸台高度;修改圆柱凸台轴心坐标。",
|
||||
}
|
||||
if guess == "round/fillet candidate":
|
||||
return {
|
||||
"feature_type": "圆角/倒圆候选",
|
||||
"feature_edit_actions": "可尝试修改已有圆角半径;支撑面会在执行时计算。",
|
||||
}
|
||||
return {
|
||||
"feature_type": "未明确圆柱特征",
|
||||
"feature_edit_actions": "可查看圆柱直径/半径;复杂语义需要手动扫描或执行计划确认。",
|
||||
}
|
||||
|
||||
def _quick_cylindrical_feature_hint(
|
||||
self,
|
||||
face_id: int,
|
||||
surf: BRepAdaptor_Surface,
|
||||
info: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
"""Cheap cylinder labeling for immediate selection feedback.
|
||||
|
||||
This intentionally avoids material-side sampling. It only combines
|
||||
directly connected co-cylindrical fragments and uses face orientation as
|
||||
a hint, so full edit plans still recompute and guard the real feature
|
||||
semantics before changing geometry.
|
||||
"""
|
||||
radius = _float_or_none(info.get("radius")) or 0.0
|
||||
selected_span = _float_or_none(info.get("angular_span")) or 0.0
|
||||
orientation = str(info.get("orientation") or "")
|
||||
try:
|
||||
boundary_edges = int(info.get("boundary_edges", 0) or 0)
|
||||
except (TypeError, ValueError):
|
||||
boundary_edges = 0
|
||||
solid_id = self.face_solid_ids[face_id] if 0 <= face_id < len(self.face_solid_ids) else -1
|
||||
solid_shape = self.solids[solid_id][1] if 0 <= solid_id < len(self.solids) else self.shape
|
||||
solid_diagonal = _shape_diagonal(solid_shape)
|
||||
|
||||
side_face_ids = [face_id]
|
||||
combined_span = selected_span
|
||||
same_domain_note = "快速识别:当前圆柱没有检测到直接相接的同域碎面。"
|
||||
axis_range: dict[str, object] | None = None
|
||||
try:
|
||||
side_face_ids = self.connected_same_domain_face_ids(face_id) or [face_id]
|
||||
spans: list[float] = []
|
||||
for side_id in side_face_ids:
|
||||
side_surf = BRepAdaptor_Surface(self.faces[side_id])
|
||||
if side_surf.GetType() == GeomAbs_Cylinder:
|
||||
spans.append(abs(float(side_surf.LastUParameter()) - float(side_surf.FirstUParameter())))
|
||||
if spans:
|
||||
combined_span = min(sum(spans), math.tau)
|
||||
axis_range = self._cylindrical_axis_range(face_id, surf, side_face_ids)
|
||||
same_domain_note = (
|
||||
f"快速识别:已把 {len(side_face_ids)} 个直接相接的同域圆柱碎面合并判断。"
|
||||
if len(side_face_ids) > 1
|
||||
else same_domain_note
|
||||
)
|
||||
except Exception:
|
||||
axis_range = None
|
||||
|
||||
result: dict[str, object] = {
|
||||
"same_domain_face_ids": tuple(side_face_ids),
|
||||
"same_domain_face_count": len(side_face_ids),
|
||||
"angular_span": combined_span,
|
||||
"same_domain_angular_span": combined_span,
|
||||
"same_domain_note": same_domain_note,
|
||||
"is_full_cylinder": combined_span >= math.tau * 0.92,
|
||||
}
|
||||
if axis_range is not None:
|
||||
result.update(
|
||||
{
|
||||
"same_domain_v_range": (axis_range["v_min"], axis_range["v_max"]),
|
||||
"same_domain_height_estimate": axis_range["span"],
|
||||
"same_domain_range_source": axis_range["range_source"],
|
||||
"height_estimate": axis_range["span"],
|
||||
}
|
||||
)
|
||||
|
||||
is_full = bool(result["is_full_cylinder"])
|
||||
is_partial = not is_full and 1e-6 < combined_span < math.tau * 0.92
|
||||
is_small_radius = solid_diagonal > 0 and radius <= solid_diagonal * 0.04
|
||||
is_fillet_radius = solid_diagonal > 0 and radius <= solid_diagonal * 0.12
|
||||
is_quarter_roundish = 0.15 <= selected_span <= math.pi * 1.05
|
||||
is_fillet_like = is_partial and is_quarter_roundish and is_fillet_radius and boundary_edges >= 4
|
||||
|
||||
if is_full and orientation == "reversed":
|
||||
result.update(
|
||||
{
|
||||
"feature_guess": "hole/groove candidate",
|
||||
"feature_type": "圆柱孔候选",
|
||||
"feature_edit_actions": "调整圆柱孔径;孔深、轴心或封堵会在执行时重新确认一级关系。",
|
||||
"confidence": "medium" if len(side_face_ids) > 1 else "high",
|
||||
"slot_kind": "",
|
||||
"slot_status": "",
|
||||
"slot_angular_span": "",
|
||||
"slot_open_angle": "",
|
||||
"slot_chord_width_estimate": "",
|
||||
"slot_arc_length_estimate": "",
|
||||
"slot_sagitta_depth_estimate": "",
|
||||
"note": "快速识别:完整圆柱且 Face 方向为 reversed,先按孔候选处理;完整计划会再做材料采样确认。",
|
||||
}
|
||||
)
|
||||
elif is_full and orientation == "forward":
|
||||
result.update(
|
||||
{
|
||||
"feature_guess": "boss/outer-round candidate",
|
||||
"feature_type": "凸台/外圆候选",
|
||||
"feature_edit_actions": "调整凸台/外圆直径;高度和轴心会在执行时重新确认一级关系。",
|
||||
"confidence": "medium" if len(side_face_ids) > 1 else "high",
|
||||
"slot_kind": "",
|
||||
"slot_status": "",
|
||||
"slot_angular_span": "",
|
||||
"slot_open_angle": "",
|
||||
"slot_chord_width_estimate": "",
|
||||
"slot_arc_length_estimate": "",
|
||||
"slot_sagitta_depth_estimate": "",
|
||||
"note": "快速识别:完整圆柱且 Face 方向为 forward,先按凸台/外圆候选处理;完整计划会再做材料采样确认。",
|
||||
}
|
||||
)
|
||||
elif is_fillet_like and is_small_radius:
|
||||
result.update(
|
||||
{
|
||||
"feature_guess": "round/fillet candidate",
|
||||
"feature_type": "圆角/倒圆候选",
|
||||
"feature_edit_actions": "可尝试修改已有圆角半径;支撑面会在执行时重新确认。",
|
||||
"confidence": "medium",
|
||||
"note": "快速识别:小半径部分圆柱,先按圆角/倒圆候选处理。",
|
||||
}
|
||||
)
|
||||
elif is_partial:
|
||||
span = min(max(combined_span, 0.0), math.tau)
|
||||
result.update(
|
||||
{
|
||||
"feature_guess": "hole/groove candidate",
|
||||
"feature_type": "槽/半孔候选",
|
||||
"feature_edit_actions": "调整槽/半孔宽度、深度、弧长或弧角;执行时会重新确认槽壁一级关系。",
|
||||
"confidence": "medium" if orientation == "reversed" else "low",
|
||||
"slot_kind": "partial-cylindrical-groove",
|
||||
"slot_status": "candidate",
|
||||
"slot_angular_span": combined_span,
|
||||
"slot_open_angle": max(math.tau - span, 0.0),
|
||||
"slot_chord_width_estimate": 2.0 * radius * math.sin(span * 0.5) if radius > 0 else 0.0,
|
||||
"slot_arc_length_estimate": radius * span,
|
||||
"slot_sagitta_depth_estimate": radius * (1.0 - math.cos(min(span, math.pi) * 0.5)) if radius > 0 else 0.0,
|
||||
"note": "快速识别:部分圆柱先按槽/半孔候选处理;完整计划会再做材料采样和边界确认。",
|
||||
}
|
||||
)
|
||||
else:
|
||||
result.update(
|
||||
{
|
||||
"feature_guess": "cylindrical face",
|
||||
"confidence": "unchecked",
|
||||
"note": "快速识别无法稳定判断孔、槽、凸台或圆角;执行具体修改时会生成完整计划。",
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def face_info(self, face_id: int) -> dict[str, object]:
|
||||
if face_id in self._face_info_cache:
|
||||
return dict(self._face_info_cache[face_id])
|
||||
@@ -525,6 +751,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
info.update(_cylinder_boss_resize_readiness(info))
|
||||
info.update(_cylinder_depth_readiness(info))
|
||||
info.update(_cylinder_suppress_readiness(info))
|
||||
info.update(self._cylindrical_feature_label_fields(info))
|
||||
elif surface_type == GeomAbs_Cone:
|
||||
cone = surf.Cone()
|
||||
info["axis_point"] = _point_tuple(cone.Location())
|
||||
@@ -601,6 +828,9 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
|
||||
def _recognition_summary_fields(self, info: dict[str, object]) -> dict[str, object]:
|
||||
surface = str(info.get("surface") or "")
|
||||
user_priority = feature_recognition_priority(info)
|
||||
user_priority_label = feature_recognition_priority_label(info)
|
||||
user_priority_reason = feature_recognition_priority_reason(info)
|
||||
candidate = (
|
||||
str(info.get("feature_type") or "").strip()
|
||||
or str(info.get("feature_guess") or "").strip()
|
||||
@@ -637,12 +867,29 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
("boss_resize_status", "boss_resize_risk", "boss_resize_blockers", "圆柱凸台直径/高度/轴心"),
|
||||
("depth_status", "depth_risk", "depth_blockers", "盲孔/盲槽深度"),
|
||||
("suppress_status", "suppress_risk", "suppress_blockers", "封堵孔/槽"),
|
||||
("existing_fillet_status", "existing_fillet_risk", "existing_fillet_blockers", "已有圆角半径"),
|
||||
("fillet_status", "fillet_risk", "fillet_blockers", "已有圆角半径"),
|
||||
("chamfer_status", "chamfer_risk", "chamfer_blockers", "倒角"),
|
||||
)
|
||||
feature_guess_for_capability = str(info.get("feature_guess") or "")
|
||||
|
||||
def capability_is_relevant(status_key: str) -> bool:
|
||||
if surface != "cylinder":
|
||||
return True
|
||||
if feature_guess_for_capability == "hole/groove candidate":
|
||||
return status_key in {"resize_status", "depth_status", "suppress_status", "cylinder_resize_status"}
|
||||
if feature_guess_for_capability == "boss/outer-round candidate":
|
||||
return status_key in {"boss_resize_status", "cylinder_resize_status"}
|
||||
if feature_guess_for_capability == "round/fillet candidate":
|
||||
return status_key in {"existing_fillet_status", "fillet_status"}
|
||||
return status_key in {"resize_status", "cylinder_resize_status"}
|
||||
|
||||
relevant_capability_specs = tuple(
|
||||
spec for spec in capability_specs if capability_is_relevant(spec[0])
|
||||
)
|
||||
available_capabilities = {
|
||||
status_key
|
||||
for status_key, _risk_key, _blocker_key, _label in capability_specs
|
||||
for status_key, _risk_key, _blocker_key, _label in relevant_capability_specs
|
||||
if str(info.get(status_key) or "").strip() in {"ready", "caution", "candidate"}
|
||||
}
|
||||
feature_type_text = str(info.get("feature_type") or "")
|
||||
@@ -678,7 +925,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
risk = value
|
||||
if surface in FREEFORM_FACE_SURFACES:
|
||||
risk = "blocked"
|
||||
for status_key, risk_key, _blocker_key, _label in capability_specs:
|
||||
for status_key, risk_key, _blocker_key, _label in relevant_capability_specs:
|
||||
value = str(info.get(risk_key) or "").strip()
|
||||
if value == "blocked" and has_available_capability:
|
||||
continue
|
||||
@@ -743,6 +990,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
add("slot_geometry", "部分圆柱槽/半孔几何")
|
||||
if info.get("shell_region_status") == "candidate":
|
||||
add("shell_opposite_face", "找到相对平面/壳体候选")
|
||||
add("user_operation_priority", f"常用操作优先级={user_priority_label}")
|
||||
|
||||
ready_actions: list[str] = []
|
||||
limited_actions: list[str] = []
|
||||
@@ -757,7 +1005,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
if text and text not in items:
|
||||
items.append(text)
|
||||
|
||||
for status_key, _risk_key, blocker_key, label in capability_specs:
|
||||
for status_key, _risk_key, blocker_key, label in relevant_capability_specs:
|
||||
status = str(info.get(status_key) or "").strip()
|
||||
blocker_text = str(info.get(blocker_key) or "").strip()
|
||||
if status in {"ready", "caution", "candidate"}:
|
||||
@@ -768,7 +1016,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
if has_planar_push_pull_candidate:
|
||||
add_action(ready_actions, "平面拉伸/切除")
|
||||
if has_local_face_deform:
|
||||
add_action(ready_actions, "当前面面积/U向尺寸/V向尺寸/中心/偏移")
|
||||
add_action(ready_actions, "当前面面内长度/面内宽度/中心/偏移")
|
||||
elif str(info.get("local_face_deform_blocker") or "").strip():
|
||||
add_action(limited_actions, "局部重建尺寸/中心/偏移")
|
||||
if has_shell_candidate:
|
||||
@@ -790,7 +1038,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
add_unique(limitations, text)
|
||||
else:
|
||||
add_unique(blockers, text)
|
||||
for status_key, _risk_key, blocker_key, _label in capability_specs:
|
||||
for status_key, _risk_key, blocker_key, _label in relevant_capability_specs:
|
||||
text = str(info.get(blocker_key) or "").strip()
|
||||
if not text:
|
||||
continue
|
||||
@@ -830,7 +1078,14 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
else:
|
||||
decision = "不建议自动修改"
|
||||
|
||||
summary_parts = [candidate, f"置信度={confidence}", f"风险={risk}", f"评分={score}", f"结论={decision}"]
|
||||
summary_parts = [
|
||||
candidate,
|
||||
f"优先级={user_priority_label}",
|
||||
f"置信度={confidence}",
|
||||
f"风险={risk}",
|
||||
f"评分={score}",
|
||||
f"结论={decision}",
|
||||
]
|
||||
if evidence:
|
||||
summary_evidence = list(evidence[:5])
|
||||
if "first_level_fact_graph" in evidence_keys:
|
||||
@@ -856,6 +1111,9 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
"recognition_risk": risk,
|
||||
"recognition_score": score,
|
||||
"recognition_decision": decision,
|
||||
"recognition_user_priority": user_priority,
|
||||
"recognition_user_priority_label": user_priority_label,
|
||||
"recognition_user_priority_reason": user_priority_reason,
|
||||
"recognition_evidence": ";".join(evidence),
|
||||
"recognition_evidence_keys": tuple(evidence_keys),
|
||||
"recognition_ready_actions": ";".join(ready_actions),
|
||||
@@ -1077,15 +1335,17 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
association_label = ""
|
||||
if lightweight and surface == "plane":
|
||||
association_label = f"相邻平面 Face {candidate_id}"
|
||||
priority = feature_recognition_priority(related)
|
||||
related.update(
|
||||
{
|
||||
"association_source_face_id": candidate_id,
|
||||
"association_hop_count": hop_count,
|
||||
"association_relation": "shared-edge-topology",
|
||||
"association_label": association_label,
|
||||
"association_priority": (
|
||||
0 if surface == "cylinder" else (1 if surface in {"cone", "sphere", "torus"} else 2)
|
||||
),
|
||||
"association_priority": priority,
|
||||
"recognition_user_priority": priority,
|
||||
"recognition_user_priority_label": feature_recognition_priority_label(related),
|
||||
"recognition_user_priority_reason": feature_recognition_priority_reason(related),
|
||||
}
|
||||
)
|
||||
results.append(related)
|
||||
@@ -1722,6 +1982,35 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
**fillet_info,
|
||||
}
|
||||
)
|
||||
scoped_readiness_info = dict(result)
|
||||
scoped_readiness_info["angular_span"] = combined_angular_span
|
||||
scoped_readiness_info["height_estimate"] = axis_range["span"]
|
||||
scoped_readiness_info["feature_guess"] = guess
|
||||
if guess == "hole/groove candidate":
|
||||
result.update(_cylinder_resize_readiness(scoped_readiness_info))
|
||||
result.update(_cylinder_depth_readiness(scoped_readiness_info))
|
||||
result.update(_cylinder_suppress_readiness(scoped_readiness_info))
|
||||
elif guess == "boss/outer-round candidate":
|
||||
result.update(
|
||||
{
|
||||
"resize_status": "blocked",
|
||||
"resize_risk": "blocked",
|
||||
"resize_blockers": "当前对象已识别为凸台/外圆;请使用凸台直径/高度/轴心入口,不按孔径重切。",
|
||||
"resize_warnings": "",
|
||||
"resize_note": "当前对象已识别为凸台/外圆;请使用凸台直径/高度/轴心入口,不按孔径重切。",
|
||||
}
|
||||
)
|
||||
result.update(_cylinder_boss_resize_readiness(scoped_readiness_info))
|
||||
elif guess == "round/fillet candidate":
|
||||
result.update(
|
||||
{
|
||||
"resize_status": "blocked",
|
||||
"resize_risk": "blocked",
|
||||
"resize_blockers": "当前对象已识别为圆角/倒圆;请使用已有圆角半径入口,不按孔径重切。",
|
||||
"resize_warnings": "",
|
||||
"resize_note": "当前对象已识别为圆角/倒圆;请使用已有圆角半径入口,不按孔径重切。",
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
def _cylindrical_slot_info(
|
||||
@@ -1769,19 +2058,60 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
|
||||
|
||||
radius = max(float(info.get("radius", 0.0)), 0.0)
|
||||
angular_span = min(max(float(info.get("angular_span", 0.0)), 0.0), math.tau)
|
||||
support_face_ids = sorted(set(adjacent_face_ids) - set(end_face_ids))
|
||||
adjacent_round_face_ids: list[int] = []
|
||||
radius_tolerance = max(radius * 0.12, _shape_diagonal(self.faces[face_id]) * 1e-5, 1e-4)
|
||||
for adjacent_id in adjacent_face_ids:
|
||||
if adjacent_id == face_id:
|
||||
continue
|
||||
try:
|
||||
adjacent_info = self.face_info(adjacent_id)
|
||||
except Exception:
|
||||
continue
|
||||
adjacent_radius = _float_or_none(adjacent_info.get("radius"))
|
||||
if (
|
||||
adjacent_info.get("surface") == "cylinder"
|
||||
and adjacent_info.get("feature_guess") == "round/fillet candidate"
|
||||
and adjacent_radius is not None
|
||||
and abs(adjacent_radius - radius) <= radius_tolerance
|
||||
):
|
||||
adjacent_round_face_ids.append(adjacent_id)
|
||||
adjacent_round_face_ids = sorted(set(adjacent_round_face_ids))
|
||||
support_face_ids = sorted(set(adjacent_face_ids) - set(end_face_ids) - set(adjacent_round_face_ids))
|
||||
chain_face_ids = tuple(sorted({face_id, *adjacent_round_face_ids}))
|
||||
chain_blocker = (
|
||||
"当前圆角与其它圆角面直接相连,属于复杂圆角链或角部 blend;"
|
||||
"当前版本暂未实现稳定圆角链重建。"
|
||||
if adjacent_round_face_ids
|
||||
else ""
|
||||
)
|
||||
return {
|
||||
"existing_fillet_kind": "cylindrical-round-face",
|
||||
"existing_fillet_status": "candidate",
|
||||
"existing_fillet_status": "blocked" if chain_blocker else "candidate",
|
||||
"existing_fillet_risk": "blocked" if chain_blocker else "medium",
|
||||
"existing_fillet_blockers": chain_blocker,
|
||||
"existing_fillet_radius_estimate": radius,
|
||||
"existing_fillet_angular_span": angular_span,
|
||||
"existing_fillet_arc_length_estimate": radius * angular_span,
|
||||
"feature_existing_fillet_face_ids": (face_id,),
|
||||
"feature_existing_fillet_support_face_ids": tuple(support_face_ids),
|
||||
"feature_existing_fillet_chain_face_ids": chain_face_ids,
|
||||
"feature_existing_fillet_chain_adjacent_face_ids": tuple(adjacent_round_face_ids),
|
||||
"existing_fillet_chain_face_count": len(chain_face_ids),
|
||||
"existing_fillet_chain_status": "chain-candidate" if adjacent_round_face_ids else "single-face",
|
||||
"existing_fillet_chain_note": (
|
||||
"Detected directly connected round/fillet Faces with a matching radius; "
|
||||
"this is treated as a fillet chain or corner blend, not a single isolated fillet."
|
||||
if adjacent_round_face_ids
|
||||
else "No directly connected same-radius round/fillet Face was detected."
|
||||
),
|
||||
"existing_fillet_note": (
|
||||
"这是由局部小半径圆柱面推断出的已有圆角/倒圆候选;"
|
||||
"当前版本可尝试使用 defeature + 重新倒圆修改半径;"
|
||||
"复杂 blend 或支撑面不明确时可能失败并回滚。"
|
||||
chain_blocker
|
||||
if chain_blocker
|
||||
else (
|
||||
"这是由局部小半径圆柱面推断出的已有圆角/倒圆候选;"
|
||||
"当前版本可尝试使用 defeature + 重新倒圆修改半径;"
|
||||
"复杂 blend 或支撑面不明确时可能失败并回滚。"
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+1001
-22
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,158 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Mapping
|
||||
|
||||
|
||||
USER_OPERATION_PRIORITY: dict[str, int] = {
|
||||
"push_pull_plane": 10,
|
||||
"resize_cylinder": 20,
|
||||
"resize_depth": 24,
|
||||
"suppress_cylinder": 28,
|
||||
"resize_slot_width": 30,
|
||||
"resize_slot_depth": 31,
|
||||
"resize_slot_arc_length": 34,
|
||||
"resize_slot_angular_span": 35,
|
||||
"resize_boss": 40,
|
||||
"resize_boss_height": 41,
|
||||
"move_boss_axis": 44,
|
||||
"inspect_existing_fillet": 50,
|
||||
"fillet_edge": 52,
|
||||
"chamfer_edge": 53,
|
||||
"resize_shell_thickness": 60,
|
||||
"resize_edge_length": 70,
|
||||
"resize_ellipse_edge_major_radius": 72,
|
||||
"resize_ellipse_edge_minor_radius": 73,
|
||||
}
|
||||
|
||||
USER_PRIORITY_BUCKETS: tuple[tuple[int, str, str], ...] = (
|
||||
(10, "Face 面编辑", "最常用:平面拉伸/切除、面尺寸、中心和偏移。"),
|
||||
(20, "孔", "常用:孔径、孔深、孔轴心和封堵。"),
|
||||
(30, "槽/半孔", "常用:槽宽、槽深、弧长、弧角和槽轴心。"),
|
||||
(40, "凸台/外圆", "常用:凸台直径、高度和轴心。"),
|
||||
(50, "圆角/倒角", "常用但风险更高:已有圆角半径、新增圆角或倒角。"),
|
||||
(60, "壳体厚度", "专项:相对面可识别时修改局部厚度。"),
|
||||
(70, "Edge 边编辑", "受限:边长、端点、圆边半径等一级关系编辑。"),
|
||||
(80, "解析曲面", "较少直接修改:圆锥、球面、环面等解析曲面参数。"),
|
||||
(90, "只读/诊断", "暂未稳定归类为可修改特征。"),
|
||||
)
|
||||
|
||||
|
||||
def _text(value: object) -> str:
|
||||
return str(value or "").strip()
|
||||
|
||||
|
||||
def _float_or_none(value: object) -> float | None:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _is_effectively_full_cylinder(info: Mapping[str, object]) -> bool:
|
||||
if bool(info.get("is_full_cylinder")):
|
||||
return True
|
||||
for key in ("same_domain_angular_span", "angular_span"):
|
||||
angular_span = _float_or_none(info.get(key))
|
||||
if angular_span is not None and angular_span >= math.tau * 0.92:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def feature_recognition_priority(info: Mapping[str, object]) -> int:
|
||||
"""Rank feature candidates by likely user editing frequency.
|
||||
|
||||
The rank is intentionally product-facing, not a geometry confidence score:
|
||||
lower numbers should appear earlier in editable-feature lists.
|
||||
"""
|
||||
operation_key = _text(info.get("operation_key"))
|
||||
if operation_key in USER_OPERATION_PRIORITY:
|
||||
return USER_OPERATION_PRIORITY[operation_key]
|
||||
|
||||
surface = _text(info.get("surface"))
|
||||
feature_guess = _text(info.get("feature_guess"))
|
||||
feature_type = _text(info.get("feature_type"))
|
||||
feature_actions = _text(info.get("feature_edit_actions"))
|
||||
ready_actions = _text(info.get("recognition_ready_actions"))
|
||||
|
||||
combined_text = ";".join(item for item in (feature_type, feature_actions, ready_actions) if item)
|
||||
|
||||
if surface == "plane":
|
||||
return 10
|
||||
|
||||
if surface == "cylinder":
|
||||
if feature_guess == "hole/groove candidate":
|
||||
angular_span = _float_or_none(info.get("angular_span"))
|
||||
if (
|
||||
not _is_effectively_full_cylinder(info)
|
||||
and angular_span is not None
|
||||
and angular_span < math.tau * 0.92
|
||||
) or _text(info.get("slot_kind")) == "partial-cylindrical-groove" or "槽/半孔候选" in feature_type:
|
||||
return 30
|
||||
return 20
|
||||
if feature_guess == "boss/outer-round candidate":
|
||||
return 40
|
||||
if feature_guess == "round/fillet candidate":
|
||||
return 50
|
||||
if "孔" in combined_text:
|
||||
return 20
|
||||
if "槽" in combined_text or "半孔" in combined_text:
|
||||
return 30
|
||||
if "凸台" in combined_text or "外圆" in combined_text:
|
||||
return 40
|
||||
if "圆角" in combined_text or "倒圆" in combined_text:
|
||||
return 50
|
||||
return 90
|
||||
|
||||
if surface == "edge" or _text(info.get("curve")):
|
||||
if "圆角" in combined_text:
|
||||
return 52
|
||||
if "倒角" in combined_text:
|
||||
return 53
|
||||
return 70
|
||||
|
||||
if surface in {"cone", "sphere", "torus"}:
|
||||
return 80
|
||||
|
||||
if "壳体" in combined_text or _text(info.get("shell_region_status")) == "candidate":
|
||||
return 60
|
||||
|
||||
return 90
|
||||
|
||||
|
||||
def feature_recognition_priority_bucket(priority: int) -> tuple[int, str, str]:
|
||||
selected = USER_PRIORITY_BUCKETS[-1]
|
||||
for bucket in USER_PRIORITY_BUCKETS:
|
||||
if priority >= bucket[0]:
|
||||
selected = bucket
|
||||
else:
|
||||
break
|
||||
return selected
|
||||
|
||||
|
||||
def feature_recognition_priority_label(info: Mapping[str, object]) -> str:
|
||||
priority = feature_recognition_priority(info)
|
||||
_start, label, _reason = feature_recognition_priority_bucket(priority)
|
||||
return f"{priority:02d} · {label}"
|
||||
|
||||
|
||||
def feature_recognition_priority_reason(info: Mapping[str, object]) -> str:
|
||||
priority = feature_recognition_priority(info)
|
||||
_start, _label, reason = feature_recognition_priority_bucket(priority)
|
||||
return reason
|
||||
|
||||
|
||||
def feature_recognition_sort_key(info: Mapping[str, object]) -> tuple[int, int, int, int]:
|
||||
status_order = {"ready": 0, "candidate": 0, "caution": 1, "blocked": 2}
|
||||
risk_order = {"low": 0, "medium": 1, "high": 2, "blocked": 3}
|
||||
target_id = info.get("target_id", info.get("face_id", info.get("edge_id", -1)))
|
||||
try:
|
||||
numeric_target = int(target_id)
|
||||
except (TypeError, ValueError):
|
||||
numeric_target = -1
|
||||
return (
|
||||
feature_recognition_priority(info),
|
||||
status_order.get(_text(info.get("status")), 9),
|
||||
risk_order.get(_text(info.get("risk")), 9),
|
||||
numeric_target,
|
||||
)
|
||||
+24
-18
@@ -240,6 +240,9 @@ INFO_GROUPS: list[tuple[str, list[str]]] = [
|
||||
"recognition_risk",
|
||||
"recognition_score",
|
||||
"recognition_decision",
|
||||
"recognition_user_priority",
|
||||
"recognition_user_priority_label",
|
||||
"recognition_user_priority_reason",
|
||||
"recognition_evidence",
|
||||
"recognition_evidence_keys",
|
||||
"recognition_ready_actions",
|
||||
@@ -440,6 +443,9 @@ INFO_LABELS = {
|
||||
"recognition_risk": "识别风险",
|
||||
"recognition_score": "识别评分",
|
||||
"recognition_decision": "识别结论",
|
||||
"recognition_user_priority": "常用优先级",
|
||||
"recognition_user_priority_label": "常用优先级分类",
|
||||
"recognition_user_priority_reason": "优先级说明",
|
||||
"recognition_evidence": "识别依据",
|
||||
"recognition_evidence_keys": "识别依据项",
|
||||
"recognition_ready_actions": "当前可改",
|
||||
@@ -593,24 +599,24 @@ INFO_LABELS = {
|
||||
"center_of_mass": "重心",
|
||||
"surface_center": "表面积中心",
|
||||
"area_center": "面积中心",
|
||||
"local_face_width": "U向尺寸",
|
||||
"local_face_height": "V向尺寸",
|
||||
"local_face_width_direction": "U向方向",
|
||||
"local_face_height_direction": "V向方向",
|
||||
"local_face_size_center": "U/V 尺寸中心",
|
||||
"face_size_axis": "U/V 尺寸方向",
|
||||
"face_size_label": "U/V 尺寸名称",
|
||||
"current_face_width": "当前 U向尺寸",
|
||||
"target_face_width": "目标 U向尺寸",
|
||||
"current_face_height": "当前 V向尺寸",
|
||||
"target_face_height": "目标 V向尺寸",
|
||||
"current_face_size": "当前 U/V 尺寸",
|
||||
"target_face_size": "目标 U/V 尺寸",
|
||||
"face_size_delta": "U/V 尺寸变化量",
|
||||
"face_size_delta_ratio": "U/V 尺寸变化比例",
|
||||
"face_size_scale": "U/V 尺寸缩放比例",
|
||||
"face_size_center": "U/V 尺寸缩放中心",
|
||||
"face_size_axis_direction": "U/V 尺寸缩放方向",
|
||||
"local_face_width": "面内长度",
|
||||
"local_face_height": "面内宽度",
|
||||
"local_face_width_direction": "面内长度方向",
|
||||
"local_face_height_direction": "面内宽度方向",
|
||||
"local_face_size_center": "面内尺寸中心",
|
||||
"face_size_axis": "面内尺寸方向",
|
||||
"face_size_label": "面内尺寸名称",
|
||||
"current_face_width": "当前面内长度",
|
||||
"target_face_width": "目标面内长度",
|
||||
"current_face_height": "当前面内宽度",
|
||||
"target_face_height": "目标面内宽度",
|
||||
"current_face_size": "当前面内尺寸",
|
||||
"target_face_size": "目标面内尺寸",
|
||||
"face_size_delta": "面内尺寸变化量",
|
||||
"face_size_delta_ratio": "面内尺寸变化比例",
|
||||
"face_size_scale": "面内尺寸缩放比例",
|
||||
"face_size_center": "面内尺寸缩放中心",
|
||||
"face_size_axis_direction": "面内尺寸缩放方向",
|
||||
"owning_face_size_rebuild_mode": "所属对象尺寸重建模式",
|
||||
"length_center": "长度中心",
|
||||
"bbox_min": "包围盒最小点",
|
||||
|
||||
+432
-120
@@ -77,6 +77,13 @@ def _compact_plan_value(value: object) -> str:
|
||||
return text if len(text) <= 120 else text[:117] + "..."
|
||||
|
||||
|
||||
def _compact_user_message(value: object, limit: int = 360) -> str:
|
||||
text = " ".join(str(value or "").replace("\r", "\n").split())
|
||||
if len(text) <= limit:
|
||||
return text
|
||||
return f"{text[: max(0, limit - 1)].rstrip()}..."
|
||||
|
||||
|
||||
class WindowActionMixin:
|
||||
def export_all(self) -> None:
|
||||
if self.model is None:
|
||||
@@ -356,8 +363,7 @@ class WindowActionMixin:
|
||||
plan = self._push_pull_plan_for_action(face_id, distance)
|
||||
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能拉伸/切除平面", str(plan["message"]))
|
||||
self.statusBar().showMessage("拉伸/切除平面已阻止")
|
||||
self._show_blocked_plan_message("拉伸/切除平面", plan, "拉伸/切除平面已阻止")
|
||||
return
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
@@ -742,8 +748,7 @@ class WindowActionMixin:
|
||||
if plan is None:
|
||||
plan = self.model.shell_thickness_plan(face_id, target_thickness)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能调整壳体厚度", str(plan["message"]))
|
||||
self.statusBar().showMessage("壳体厚度调整已阻止")
|
||||
self._show_blocked_plan_message("调整壳体厚度", plan, "壳体厚度调整已阻止")
|
||||
return
|
||||
|
||||
if plan["risk"] != "low":
|
||||
@@ -1073,7 +1078,7 @@ class WindowActionMixin:
|
||||
delta = target_size - current_size if current_size is not None else None
|
||||
ratio = abs(delta) / current_size if current_size is not None and current_size > 1e-9 else None
|
||||
scale = target_size / current_size if current_size is not None and current_size > 1e-9 else None
|
||||
axis_label = "V向尺寸" if axis_key == "height" else "U向尺寸"
|
||||
axis_label = "面内宽度" if axis_key == "height" else "面内长度"
|
||||
return self._quick_blocked_local_face_plan(
|
||||
face_id,
|
||||
blocker,
|
||||
@@ -1153,7 +1158,7 @@ class WindowActionMixin:
|
||||
face_id,
|
||||
blocker,
|
||||
resize_strategy="local-face-plane-offset-deform",
|
||||
edit_strategy_label="偏移变换(局部重建)",
|
||||
edit_strategy_label="偏移(局部重建)",
|
||||
edit_semantics="按当前面垂直方向移动 Face 边界顶点并重建一级相邻面;复杂内孔/多边界 Face 当前不放行。",
|
||||
info=info,
|
||||
extra={
|
||||
@@ -1377,8 +1382,7 @@ class WindowActionMixin:
|
||||
cancelled_status: str,
|
||||
) -> bool:
|
||||
if plan.get("status") == "blocked":
|
||||
QMessageBox.information(self, f"不能{title}", str(plan.get("message", "")))
|
||||
self.statusBar().showMessage(blocked_status)
|
||||
self._show_blocked_plan_message(title, plan, blocked_status)
|
||||
return False
|
||||
if (
|
||||
not self._quick_edit_title_supports_isolation(title)
|
||||
@@ -1428,11 +1432,11 @@ class WindowActionMixin:
|
||||
"壳体厚度(缩放特征)缩放所属对象",
|
||||
"面积(局部重建)",
|
||||
"面面积(缩放特征)",
|
||||
"U向尺寸(局部重建)",
|
||||
"V向尺寸(局部重建)",
|
||||
"U向尺寸(缩放特征)",
|
||||
"V向尺寸(缩放特征)",
|
||||
"偏移变换(局部重建)",
|
||||
"面内长度(局部重建)",
|
||||
"面内宽度(局部重建)",
|
||||
"面内长度(缩放特征)",
|
||||
"面内宽度(缩放特征)",
|
||||
"偏移(局部重建)",
|
||||
"中心(局部重建)",
|
||||
"圆柱高度调整",
|
||||
"高度(缩放特征)缩放所属对象",
|
||||
@@ -1601,6 +1605,280 @@ class WindowActionMixin:
|
||||
return ""
|
||||
return "\n\n诊断信息:\n" + "\n".join(f"- {line}" for line in lines)
|
||||
|
||||
def _edit_blocker_type(self, reason: str) -> tuple[str, str]:
|
||||
text = str(reason or "")
|
||||
unsupported_markers = (
|
||||
"暂不支持",
|
||||
"当前版本",
|
||||
"还没有实现",
|
||||
"未实现",
|
||||
"不支持",
|
||||
"受限能力",
|
||||
"复杂链式",
|
||||
"二级",
|
||||
"三级",
|
||||
"原 CAD",
|
||||
"deferred",
|
||||
"unsupported",
|
||||
"not implemented",
|
||||
)
|
||||
risk_markers = (
|
||||
"风险过高",
|
||||
"风险太高",
|
||||
"高风险",
|
||||
"危险",
|
||||
"长时间卡死",
|
||||
"卡死",
|
||||
"超时",
|
||||
"timeout",
|
||||
"too risky",
|
||||
"high risk",
|
||||
)
|
||||
recognition_markers = (
|
||||
"识别不足",
|
||||
"识别不到",
|
||||
"没有识别",
|
||||
"未识别",
|
||||
"识别不稳",
|
||||
"不稳定",
|
||||
"不明确",
|
||||
"无法确认",
|
||||
"不能确认",
|
||||
"缺少稳定",
|
||||
"置信度",
|
||||
"not recognized",
|
||||
"not recognise",
|
||||
"ambiguous",
|
||||
"unclear",
|
||||
"unstable",
|
||||
)
|
||||
illegal_markers = (
|
||||
"无效",
|
||||
"非法",
|
||||
"必须",
|
||||
"不能",
|
||||
"不存在",
|
||||
"相同",
|
||||
"超过",
|
||||
"小于",
|
||||
"大于",
|
||||
"切空",
|
||||
"退化",
|
||||
"invalid",
|
||||
"requires",
|
||||
"must",
|
||||
"cannot",
|
||||
"missing",
|
||||
"empty",
|
||||
"greater than",
|
||||
"less than",
|
||||
"too small",
|
||||
"too large",
|
||||
"not recognized",
|
||||
"not a",
|
||||
"does not have",
|
||||
)
|
||||
lower = text.lower()
|
||||
if any(marker.lower() in lower for marker in unsupported_markers):
|
||||
return "暂未实现", "当前版本暂未实现这类稳定修改。"
|
||||
if any(marker.lower() in lower for marker in risk_markers):
|
||||
return "风险过高", "当前修改容易破坏 B-Rep 或让几何计算长时间失败,已在执行前阻止。"
|
||||
if any(marker.lower() in lower for marker in recognition_markers):
|
||||
return "识别不足", "程序还没有把当前对象识别成足够稳定的 CAD 特征或一级关系。"
|
||||
if any(marker.lower() in lower for marker in illegal_markers):
|
||||
return "当前操作不合法", "当前输入或选中对象不满足这个操作的基本条件。"
|
||||
return "不能稳定修改", "几何内核没有给出可验证的有效 B-Rep 结果。"
|
||||
|
||||
def _edit_blocker_reason_from_parameters(self, parameters: dict[str, object], prefix: str = "") -> str:
|
||||
candidate_keys: list[str] = []
|
||||
if prefix:
|
||||
candidate_keys.extend((f"{prefix}_blockers", f"{prefix}_message", f"{prefix}_warnings"))
|
||||
candidate_keys.extend(
|
||||
(
|
||||
"blockers",
|
||||
"message",
|
||||
"resize_blockers",
|
||||
"resize_message",
|
||||
"depth_blockers",
|
||||
"depth_message",
|
||||
"move_axis_blockers",
|
||||
"move_axis_message",
|
||||
"boss_resize_blockers",
|
||||
"boss_height_blockers",
|
||||
"existing_fillet_blockers",
|
||||
"fillet_blockers",
|
||||
"chamfer_blockers",
|
||||
"push_pull_message",
|
||||
)
|
||||
)
|
||||
for key in candidate_keys:
|
||||
text = str(parameters.get(key) or "").strip()
|
||||
if text:
|
||||
return text
|
||||
return ""
|
||||
|
||||
def _plan_preflight_blocker(self, plan: dict[str, object]) -> tuple[str, str, str] | None:
|
||||
status = str(plan.get("status") or "").strip().lower()
|
||||
risk = str(plan.get("risk") or "").strip().lower()
|
||||
if status != "blocked" and risk != "blocked":
|
||||
return None
|
||||
reason = self._edit_blocker_reason_from_parameters(plan) or "计划阶段已判断该操作不可执行。"
|
||||
blocker_type, detail = self._edit_blocker_type(reason)
|
||||
return blocker_type, reason, detail
|
||||
|
||||
def _show_blocked_plan_message(
|
||||
self,
|
||||
title: str,
|
||||
plan: dict[str, object],
|
||||
blocked_status: str,
|
||||
) -> None:
|
||||
blocker = self._plan_preflight_blocker(plan)
|
||||
if blocker is None:
|
||||
blocker = ("不能稳定修改", str(plan.get("message") or "计划阶段已阻止。"), "模型未修改。")
|
||||
blocker_type, reason, detail = blocker
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"不能修改",
|
||||
(
|
||||
"不能修改。\n\n"
|
||||
f"类型:{blocker_type}\n"
|
||||
f"操作:{title}\n\n"
|
||||
f"原因:{_compact_user_message(reason)}\n\n"
|
||||
f"{detail}\n\n"
|
||||
"模型未修改。"
|
||||
),
|
||||
)
|
||||
self.statusBar().showMessage(f"{blocked_status}:{blocker_type}")
|
||||
|
||||
def _show_immediate_cannot_edit_message(
|
||||
self,
|
||||
title: str,
|
||||
reason: str,
|
||||
blocked_status: str,
|
||||
) -> None:
|
||||
blocker_type, detail = self._edit_blocker_type(reason)
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"不能修改",
|
||||
(
|
||||
"不能修改。\n\n"
|
||||
f"类型:{blocker_type}\n"
|
||||
f"操作:{title}\n\n"
|
||||
f"原因:{_compact_user_message(reason)}\n\n"
|
||||
f"{detail}\n\n"
|
||||
"模型未修改。"
|
||||
),
|
||||
)
|
||||
self.statusBar().showMessage(f"{blocked_status}:{blocker_type}")
|
||||
|
||||
def _edit_preflight_blocker(self, context: dict[str, object]) -> tuple[str, str, str] | None:
|
||||
parameters = context.get("parameters")
|
||||
if not isinstance(parameters, dict):
|
||||
return None
|
||||
if bool(parameters.get("ui_deferred_model_plan")) or bool(parameters.get("ui_deferred_shell_thickness_plan")):
|
||||
if bool(parameters.get("ui_deferred_shell_thickness_plan")):
|
||||
reason = "当前复杂壳体厚度修改还没有快速、稳定的一级关系判断路径。"
|
||||
else:
|
||||
reason = "当前复杂 Face 修改还没有快速、稳定的一级关系判断路径。"
|
||||
detail = (
|
||||
"属于当前版本暂未实现的稳定编辑能力。请改用更明确的孔/槽/凸台/Edge 专门入口,"
|
||||
"或先选择更简单的局部面。"
|
||||
)
|
||||
return ("暂未实现", reason, detail)
|
||||
for key, value in parameters.items():
|
||||
if str(key).startswith("ui_quick_blocked") and bool(value):
|
||||
reason = self._edit_blocker_reason_from_parameters(parameters) or "快速预检已判断该操作不可稳定执行。"
|
||||
blocker_type, detail = self._edit_blocker_type(reason)
|
||||
return (blocker_type, reason, detail)
|
||||
for key, value in parameters.items():
|
||||
key_text = str(key)
|
||||
if not key_text.endswith("_status"):
|
||||
continue
|
||||
if key_text in {"quick_plan_status", "topology_relation_status", "slot_status", "quality_status"}:
|
||||
continue
|
||||
if str(value or "").strip().lower() != "blocked":
|
||||
continue
|
||||
prefix = key_text[: -len("_status")]
|
||||
reason = self._edit_blocker_reason_from_parameters(parameters, prefix) or "计划阶段已判断该操作不可执行。"
|
||||
blocker_type, detail = self._edit_blocker_type(reason)
|
||||
return (blocker_type, reason, detail)
|
||||
for key, value in parameters.items():
|
||||
key_text = str(key)
|
||||
if not key_text.endswith("_blockers") and key_text != "blockers":
|
||||
continue
|
||||
reason = str(value or "").strip()
|
||||
if not reason:
|
||||
continue
|
||||
blocker_type, detail = self._edit_blocker_type(reason)
|
||||
return (blocker_type, reason, detail)
|
||||
return None
|
||||
|
||||
def _show_edit_preflight_blocker(self, blocker: tuple[str, str, str], context: dict[str, object]) -> None:
|
||||
blocker_type, reason, detail = blocker
|
||||
operation_name = str(context.get("operation_name") or "当前操作")
|
||||
target = str(context.get("target") or "当前对象")
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"不能修改",
|
||||
(
|
||||
"不能修改。\n\n"
|
||||
f"类型:{blocker_type}\n"
|
||||
f"对象:{target}\n"
|
||||
f"操作:{operation_name}\n\n"
|
||||
f"原因:{_compact_user_message(reason)}\n\n"
|
||||
f"{detail}\n\n"
|
||||
"模型未修改。"
|
||||
),
|
||||
)
|
||||
self.statusBar().showMessage(f"不能修改:{blocker_type}")
|
||||
|
||||
def _user_facing_edit_failure_message(
|
||||
self,
|
||||
message: str,
|
||||
context: dict[str, object] | None,
|
||||
) -> tuple[str, str]:
|
||||
raw_message = str(message or "").strip()
|
||||
reason = raw_message
|
||||
for marker in ("诊断信息:", "璇婃柇淇℃伅:"):
|
||||
index = reason.find(marker)
|
||||
if index >= 0:
|
||||
reason = reason[:index].strip()
|
||||
replacements = (
|
||||
("隔离子进程执行失败;主程序没有崩溃,原模型保持不变。", ""),
|
||||
("隔离子进程执行超时,已终止危险计算;主程序和原模型保持不变。", ""),
|
||||
("隔离子进程", "后台几何计算"),
|
||||
("子进程", "后台计算"),
|
||||
("isolated worker", "后台几何计算"),
|
||||
("isolated", "后台"),
|
||||
)
|
||||
for old, new in replacements:
|
||||
reason = reason.replace(old, new)
|
||||
reason = _compact_user_message(reason)
|
||||
if not reason:
|
||||
reason = "后台几何计算没有返回可用结果。"
|
||||
|
||||
blocker_type, detail = self._edit_blocker_type(reason)
|
||||
lower_raw = raw_message.lower()
|
||||
if "超时" in raw_message or "timeout" in lower_raw:
|
||||
blocker_type = "暂未实现"
|
||||
detail = "当前版本还不能稳定处理这类复杂拓扑,已停止继续等待。"
|
||||
if "returned an invalid b-rep" in lower_raw or "无效 b-rep" in lower_raw:
|
||||
blocker_type = "几何结果无效"
|
||||
detail = "几何内核生成的结果没有通过 B-Rep 校验,已回滚。"
|
||||
|
||||
operation_name = ""
|
||||
target = ""
|
||||
if isinstance(context, dict):
|
||||
operation_name = str(context.get("operation_name") or "")
|
||||
target = str(context.get("target") or "")
|
||||
lines = ["不能修改。", "", f"类型:{blocker_type}"]
|
||||
if target:
|
||||
lines.append(f"对象:{target}")
|
||||
if operation_name:
|
||||
lines.append(f"操作:{operation_name}")
|
||||
lines.extend(["", f"原因:{reason or '几何计算失败。'}", "", detail, "", "模型未修改。"])
|
||||
return "不能修改", "\n".join(lines)
|
||||
|
||||
def resize_hole(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
@@ -1992,7 +2270,11 @@ class WindowActionMixin:
|
||||
if angular_span is None:
|
||||
angular_span = _float_or_none(info.get("angular_span"))
|
||||
if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92:
|
||||
QMessageBox.information(self, f"不能整体修改{label}", "当前候选没有稳定的部分圆柱角度,不能换算为目标圆柱直径。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
f"整体修改{label}",
|
||||
"当前候选没有稳定的部分圆柱角度,不能换算为目标圆柱直径。",
|
||||
f"整体修改{label}已阻止",
|
||||
)
|
||||
return
|
||||
|
||||
span = min(max(float(angular_span), 1e-6), math.tau - 1e-6)
|
||||
@@ -2000,12 +2282,20 @@ class WindowActionMixin:
|
||||
sagitta_factor = 1.0 - math.cos(min(span, math.pi) * 0.5)
|
||||
if mode == "width":
|
||||
if abs(sin_half_span) <= 1e-6:
|
||||
QMessageBox.information(self, "不能整体修改槽宽", "当前槽圆弧角度过小,不能可靠换算槽宽。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"整体修改槽宽",
|
||||
"当前槽圆弧角度过小,不能可靠换算槽宽。",
|
||||
"整体修改槽宽已阻止",
|
||||
)
|
||||
return
|
||||
target_diameter = target_value / sin_half_span
|
||||
elif mode == "depth":
|
||||
if sagitta_factor <= 1e-6:
|
||||
QMessageBox.information(self, "不能整体修改槽深", "当前槽圆弧角度过小,不能可靠换算槽深。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"整体修改槽深",
|
||||
"当前槽圆弧角度过小,不能可靠换算槽深。",
|
||||
"整体修改槽深已阻止",
|
||||
)
|
||||
return
|
||||
target_diameter = 2.0 * target_value / sagitta_factor
|
||||
else:
|
||||
@@ -2122,11 +2412,19 @@ class WindowActionMixin:
|
||||
if angular_span is None:
|
||||
angular_span = _float_or_none(info.get("angular_span"))
|
||||
if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92:
|
||||
QMessageBox.information(self, "不能调整槽宽", "当前候选没有稳定的部分圆柱角度,不能换算槽宽。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"调整槽宽",
|
||||
"当前候选没有稳定的部分圆柱角度,不能换算槽宽。",
|
||||
"槽/半孔宽度调整已阻止",
|
||||
)
|
||||
return
|
||||
sin_half_span = math.sin(min(max(angular_span, 1e-6), math.tau - 1e-6) / 2.0)
|
||||
if abs(sin_half_span) <= 1e-6:
|
||||
QMessageBox.information(self, "不能调整槽宽", "当前槽圆弧角度过小,不能可靠换算为圆柱直径。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"调整槽宽",
|
||||
"当前槽圆弧角度过小,不能可靠换算为圆柱直径。",
|
||||
"槽/半孔宽度调整已阻止",
|
||||
)
|
||||
return
|
||||
|
||||
target_diameter = target_width / sin_half_span
|
||||
@@ -2244,12 +2542,20 @@ class WindowActionMixin:
|
||||
if angular_span is None:
|
||||
angular_span = _float_or_none(info.get("angular_span"))
|
||||
if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92:
|
||||
QMessageBox.information(self, "不能调整槽深", "当前候选没有稳定的部分圆柱角度,不能换算槽深。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"调整槽深",
|
||||
"当前候选没有稳定的部分圆柱角度,不能换算槽深。",
|
||||
"槽/半孔深度调整已阻止",
|
||||
)
|
||||
return
|
||||
|
||||
sagitta_factor = 1.0 - math.cos(min(max(angular_span, 1e-6), math.pi) / 2.0)
|
||||
if sagitta_factor <= 1e-6:
|
||||
QMessageBox.information(self, "不能调整槽深", "当前槽圆弧角度过小,不能可靠换算为圆柱直径。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"调整槽深",
|
||||
"当前槽圆弧角度过小,不能可靠换算为圆柱直径。",
|
||||
"槽/半孔深度调整已阻止",
|
||||
)
|
||||
return
|
||||
|
||||
target_diameter = 2.0 * target_depth / sagitta_factor
|
||||
@@ -2371,7 +2677,11 @@ class WindowActionMixin:
|
||||
if angular_span is None:
|
||||
angular_span = _float_or_none(info.get("angular_span"))
|
||||
if angular_span is None or angular_span <= 1e-6 or angular_span >= math.tau * 0.92:
|
||||
QMessageBox.information(self, "不能调整圆弧长度", "当前候选没有稳定的部分圆柱角度,不能换算圆弧长度。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"调整圆弧长度",
|
||||
"当前候选没有稳定的部分圆柱角度,不能换算圆弧长度。",
|
||||
"槽/半孔圆弧长度调整已阻止",
|
||||
)
|
||||
return
|
||||
|
||||
target_diameter = 2.0 * target_arc_length / angular_span
|
||||
@@ -2509,7 +2819,11 @@ class WindowActionMixin:
|
||||
if current_angular_span is None:
|
||||
current_angular_span = _float_or_none(info.get("angular_span"))
|
||||
if current_angular_span is None or current_angular_span <= 1e-6 or current_angular_span >= math.tau * 0.92:
|
||||
QMessageBox.information(self, "不能调整圆弧角度", "当前候选没有稳定的部分圆柱角度。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"调整圆弧角度",
|
||||
"当前候选没有稳定的部分圆柱角度。",
|
||||
"槽/半孔圆弧角度调整已阻止",
|
||||
)
|
||||
return
|
||||
|
||||
plan = self.model.cylindrical_slot_angular_span_plan(face_id, target_angular_span)
|
||||
@@ -2626,8 +2940,7 @@ class WindowActionMixin:
|
||||
pair_face_id=pair_face_id,
|
||||
)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能调整槽孔总长度", str(plan["message"]))
|
||||
self.statusBar().showMessage("槽孔总长度调整已阻止")
|
||||
self._show_blocked_plan_message("调整槽孔总长度", plan, "槽孔总长度调整已阻止")
|
||||
return
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
@@ -2725,8 +3038,7 @@ class WindowActionMixin:
|
||||
pair_face_id=pair_face_id,
|
||||
)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能调整槽孔中心距", str(plan["message"]))
|
||||
self.statusBar().showMessage("槽孔中心距调整已阻止")
|
||||
self._show_blocked_plan_message("调整槽孔中心距", plan, "槽孔中心距调整已阻止")
|
||||
return
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
@@ -2817,8 +3129,7 @@ class WindowActionMixin:
|
||||
return
|
||||
plan = self.model.cylindrical_boss_resize_plan(self.selected_face_id, diameter)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能调整凸台直径", str(plan["message"]))
|
||||
self.statusBar().showMessage("圆柱凸台直径调整已阻止")
|
||||
self._show_blocked_plan_message("调整凸台直径", plan, "圆柱凸台直径调整已阻止")
|
||||
return
|
||||
if plan["risk"] != "low":
|
||||
target_to_height_ratio = plan.get("target_to_height_ratio", "")
|
||||
@@ -3013,8 +3324,7 @@ class WindowActionMixin:
|
||||
face_id = self.selected_face_id
|
||||
plan = self.model.cylindrical_boss_height_plan(face_id, target_height)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能调整凸台高度", str(plan["message"]))
|
||||
self.statusBar().showMessage("圆柱凸台高度调整已阻止")
|
||||
self._show_blocked_plan_message("调整凸台高度", plan, "圆柱凸台高度调整已阻止")
|
||||
return
|
||||
|
||||
if plan["risk"] != "low":
|
||||
@@ -3280,8 +3590,7 @@ class WindowActionMixin:
|
||||
return
|
||||
plan = self.model.cylindrical_suppress_plan(self.selected_face_id)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能封堵圆柱孔", str(plan["message"]))
|
||||
self.statusBar().showMessage("封堵圆柱孔已阻止")
|
||||
self._show_blocked_plan_message("封堵圆柱孔", plan, "封堵圆柱孔已阻止")
|
||||
return
|
||||
|
||||
if plan["risk"] != "low":
|
||||
@@ -3717,8 +4026,7 @@ class WindowActionMixin:
|
||||
plan = self.model.existing_fillet_resize_plan(face_id, target_radius)
|
||||
isolation = self._isolation_for_plan(plan, "resize_existing_fillet", [face_id, target_radius])
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改已有圆角", str(plan["message"]))
|
||||
self.statusBar().showMessage("已有圆角半径修改已阻止")
|
||||
self._show_blocked_plan_message("修改已有圆角", plan, "已有圆角半径修改已阻止")
|
||||
return
|
||||
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
@@ -3811,8 +4119,7 @@ class WindowActionMixin:
|
||||
plan = self.model.edge_fillet_plan(edge_id, radius)
|
||||
isolation = self._isolation_for_plan(plan, "fillet_edge", [edge_id, radius])
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能添加圆角", str(plan["message"]))
|
||||
self.statusBar().showMessage("添加圆角已阻止")
|
||||
self._show_blocked_plan_message("添加圆角", plan, "添加圆角已阻止")
|
||||
return
|
||||
|
||||
if plan["risk"] != "low":
|
||||
@@ -3893,8 +4200,7 @@ class WindowActionMixin:
|
||||
plan = self.model.edge_chamfer_plan(edge_id, distance)
|
||||
isolation = self._isolation_for_plan(plan, "chamfer_edge", [edge_id, distance])
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能添加倒角", str(plan["message"]))
|
||||
self.statusBar().showMessage("添加倒角已阻止")
|
||||
self._show_blocked_plan_message("添加倒角", plan, "添加倒角已阻止")
|
||||
return
|
||||
|
||||
if plan["risk"] != "low":
|
||||
@@ -4159,8 +4465,7 @@ class WindowActionMixin:
|
||||
[edge_id, target_length, anchor_mode, strategy_mode],
|
||||
)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改Edge长度", str(plan["message"]))
|
||||
self.statusBar().showMessage("Edge长度修改已阻止")
|
||||
self._show_blocked_plan_message("修改Edge长度", plan, "Edge长度修改已阻止")
|
||||
return
|
||||
|
||||
if plan["risk"] != "low":
|
||||
@@ -4325,8 +4630,7 @@ class WindowActionMixin:
|
||||
[edge_id, target_length, anchor_mode, strategy_mode],
|
||||
)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改Edge长度", str(plan["message"]))
|
||||
self.statusBar().showMessage("Edge长度修改已阻止")
|
||||
self._show_blocked_plan_message("修改Edge长度", plan, "Edge长度修改已阻止")
|
||||
return
|
||||
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
@@ -4498,8 +4802,7 @@ class WindowActionMixin:
|
||||
[edge_id, target_radius, axis_kind],
|
||||
)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, f"不能修改椭圆Edge{axis_label}", str(plan["message"]))
|
||||
self.statusBar().showMessage(f"椭圆Edge{axis_label}修改已阻止")
|
||||
self._show_blocked_plan_message(f"修改椭圆Edge{axis_label}", plan, f"椭圆Edge{axis_label}修改已阻止")
|
||||
return
|
||||
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
@@ -4861,8 +5164,7 @@ class WindowActionMixin:
|
||||
plan = self.model.conical_reference_radius_plan(face_id, target_radius)
|
||||
isolation = self._isolation_for_plan(plan, "resize_cone_reference_radius", [face_id, target_radius])
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改圆锥面参数", str(plan["message"]))
|
||||
self.statusBar().showMessage("圆锥面参数修改已阻止")
|
||||
self._show_blocked_plan_message("修改圆锥面参数", plan, "圆锥面参数修改已阻止")
|
||||
return
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
@@ -4960,8 +5262,7 @@ class WindowActionMixin:
|
||||
plan = self.model.conical_semi_angle_plan(face_id, target_angle_degrees)
|
||||
isolation = self._isolation_for_plan(plan, "resize_cone_semi_angle", [face_id, target_angle_degrees])
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改圆锥半角", str(plan["message"]))
|
||||
self.statusBar().showMessage("圆锥半角修改已阻止")
|
||||
self._show_blocked_plan_message("修改圆锥半角", plan, "圆锥半角修改已阻止")
|
||||
return
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
@@ -5057,8 +5358,7 @@ class WindowActionMixin:
|
||||
plan = self.model.spherical_radius_plan(face_id, target_radius)
|
||||
isolation = self._isolation_for_plan(plan, "resize_sphere_radius", [face_id, target_radius])
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改球面(缩放特征)", str(plan["message"]))
|
||||
self.statusBar().showMessage("球面(缩放特征)修改已阻止")
|
||||
self._show_blocked_plan_message("修改球面(缩放特征)", plan, "球面(缩放特征)修改已阻止")
|
||||
return
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
@@ -5160,8 +5460,7 @@ class WindowActionMixin:
|
||||
plan = self.model.toroidal_radius_plan(face_id, target_radius, mode_key)
|
||||
isolation = self._isolation_for_plan(plan, "resize_torus_radius", [face_id, target_radius, mode_key])
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改环面(缩放特征)", str(plan["message"]))
|
||||
self.statusBar().showMessage("环面(缩放特征)修改已阻止")
|
||||
self._show_blocked_plan_message("修改环面(缩放特征)", plan, "环面(缩放特征)修改已阻止")
|
||||
return
|
||||
if plan["risk"] != "low":
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
@@ -5260,8 +5559,7 @@ class WindowActionMixin:
|
||||
face_id = self.selected_face_id
|
||||
plan = self.model.face_area_scale_plan(face_id, target_area)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能修改面面积", str(plan["message"]))
|
||||
self.statusBar().showMessage("面面积修改已阻止")
|
||||
self._show_blocked_plan_message("修改面面积", plan, "面面积修改已阻止")
|
||||
return
|
||||
warnings = str(plan.get("warnings", ""))
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
@@ -5428,7 +5726,7 @@ class WindowActionMixin:
|
||||
if self.model is None:
|
||||
return
|
||||
axis_key = "height" if str(axis).lower() == "height" else "width"
|
||||
axis_label = "V向尺寸" if axis_key == "height" else "U向尺寸"
|
||||
axis_label = "面内宽度" if axis_key == "height" else "面内长度"
|
||||
if self._edit_busy(f"请等待当前编辑完成后再修改{axis_label}(局部重建)。"):
|
||||
return
|
||||
if self.selected_face_id is None:
|
||||
@@ -5450,10 +5748,10 @@ class WindowActionMixin:
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"修改对象: {axis_label}(局部重建)",
|
||||
f"当前U向尺寸: {_format_value(plan.get('current_face_width'))}",
|
||||
f"目标U向尺寸: {_format_value(plan.get('target_face_width'))}",
|
||||
f"当前V向尺寸: {_format_value(plan.get('current_face_height'))}",
|
||||
f"目标V向尺寸: {_format_value(plan.get('target_face_height'))}",
|
||||
f"当前面内长度: {_format_value(plan.get('current_face_width'))}",
|
||||
f"目标面内长度: {_format_value(plan.get('target_face_width'))}",
|
||||
f"当前面内宽度: {_format_value(plan.get('current_face_height'))}",
|
||||
f"目标面内宽度: {_format_value(plan.get('target_face_height'))}",
|
||||
f"变化量: {_format_value(plan.get('face_size_delta'))}",
|
||||
f"变化比例: {_format_percent(plan.get('face_size_delta_ratio'))}",
|
||||
f"缩放比例: {_format_value(plan.get('face_size_scale'))}",
|
||||
@@ -5522,7 +5820,7 @@ class WindowActionMixin:
|
||||
if self.model is None:
|
||||
return
|
||||
axis_key = "height" if str(axis).lower() == "height" else "width"
|
||||
axis_label = "V向尺寸" if axis_key == "height" else "U向尺寸"
|
||||
axis_label = "面内宽度" if axis_key == "height" else "面内长度"
|
||||
if self._edit_busy(f"请等待当前编辑完成后再修改{axis_label}(缩放特征)。"):
|
||||
return
|
||||
if self.selected_face_id is None:
|
||||
@@ -5542,10 +5840,10 @@ class WindowActionMixin:
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"修改对象: {axis_label}(缩放特征)",
|
||||
f"当前U向尺寸: {_format_value(plan.get('current_face_width'))}",
|
||||
f"目标U向尺寸: {_format_value(plan.get('target_face_width'))}",
|
||||
f"当前V向尺寸: {_format_value(plan.get('current_face_height'))}",
|
||||
f"目标V向尺寸: {_format_value(plan.get('target_face_height'))}",
|
||||
f"当前面内长度: {_format_value(plan.get('current_face_width'))}",
|
||||
f"目标面内长度: {_format_value(plan.get('target_face_width'))}",
|
||||
f"当前面内宽度: {_format_value(plan.get('current_face_height'))}",
|
||||
f"目标面内宽度: {_format_value(plan.get('target_face_height'))}",
|
||||
f"变化量: {_format_value(plan.get('face_size_delta'))}",
|
||||
f"变化比例: {_format_percent(plan.get('face_size_delta_ratio'))}",
|
||||
f"缩放比例: {_format_value(plan.get('face_size_scale'))}",
|
||||
@@ -5625,7 +5923,7 @@ class WindowActionMixin:
|
||||
def move_selected_face_plane_position_by_translation(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
if self._edit_busy("请等待当前编辑完成后再按偏移变换平移所属对象。"):
|
||||
if self._edit_busy("请等待当前编辑完成后再按偏移平移所属对象。"):
|
||||
return
|
||||
if self.selected_face_id is None:
|
||||
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
|
||||
@@ -5633,14 +5931,13 @@ class WindowActionMixin:
|
||||
try:
|
||||
distance = float(self.offset_input.text())
|
||||
except (AttributeError, ValueError):
|
||||
QMessageBox.critical(self, "偏移变换无效", "请输入数字形式的目标偏移变换位置。")
|
||||
QMessageBox.critical(self, "偏移无效", "请输入数字形式的目标偏移位置。")
|
||||
return
|
||||
face_id = self.selected_face_id
|
||||
plan = self.model.face_plane_offset_owning_translation_plan(face_id, distance)
|
||||
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能按偏移变换平移", str(plan["message"]))
|
||||
self.statusBar().showMessage("偏移变换整体平移已阻止")
|
||||
self._show_blocked_plan_message("按偏移平移", plan, "偏移整体平移已阻止")
|
||||
return
|
||||
current_position = float(plan.get("current_plane_position", 0.0) or 0.0)
|
||||
target_position = float(plan.get("target_plane_position", current_position) or current_position)
|
||||
@@ -5652,7 +5949,7 @@ class WindowActionMixin:
|
||||
target_position,
|
||||
plane_direction,
|
||||
):
|
||||
self.statusBar().showMessage("已取消偏移变换整体平移")
|
||||
self.statusBar().showMessage("已取消偏移整体平移")
|
||||
return
|
||||
self.clear_edit_preview(render=False)
|
||||
|
||||
@@ -5663,7 +5960,7 @@ class WindowActionMixin:
|
||||
moved_id = plan.get("solid_id") if moved_kind == "solid" else plan.get("part_id")
|
||||
self._run_edit_action(
|
||||
action,
|
||||
operation_name="偏移变换(移动特征)",
|
||||
operation_name="偏移(移动特征)",
|
||||
target=f"Face {face_id}",
|
||||
parameters={
|
||||
"face_id": face_id,
|
||||
@@ -5684,8 +5981,8 @@ class WindowActionMixin:
|
||||
"translate_warnings": plan.get("warnings"),
|
||||
"translate_blockers": plan.get("blockers"),
|
||||
"resize_strategy": "translate-owning-shape-from-plane-offset",
|
||||
"edit_strategy_label": "按偏移变换平移所属对象",
|
||||
"edit_semantics": "把目标偏移变换位置换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。",
|
||||
"edit_strategy_label": "按偏移平移所属对象",
|
||||
"edit_semantics": "把目标偏移位置换算成沿当前面垂直方向的平移量,并平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。",
|
||||
},
|
||||
target_kind="face",
|
||||
target_id=face_id,
|
||||
@@ -5694,7 +5991,7 @@ class WindowActionMixin:
|
||||
def move_selected_face_plane_position_local(self) -> None:
|
||||
if self.model is None:
|
||||
return
|
||||
if self._edit_busy("请等待当前编辑完成后再执行偏移变换(局部重建)。"):
|
||||
if self._edit_busy("请等待当前编辑完成后再执行偏移(局部重建)。"):
|
||||
return
|
||||
if self.selected_face_id is None:
|
||||
QMessageBox.information(self, "未选择Face", "请先选择一个平面 Face。")
|
||||
@@ -5702,7 +5999,7 @@ class WindowActionMixin:
|
||||
try:
|
||||
distance = float(self.offset_input.text())
|
||||
except (AttributeError, ValueError):
|
||||
QMessageBox.critical(self, "偏移变换无效", "请输入数字形式的目标偏移变换位置。")
|
||||
QMessageBox.critical(self, "偏移无效", "请输入数字形式的目标偏移位置。")
|
||||
return
|
||||
|
||||
face_id = self.selected_face_id
|
||||
@@ -5711,8 +6008,8 @@ class WindowActionMixin:
|
||||
plan = self.model.face_plane_offset_local_plan(face_id, distance)
|
||||
lines = [
|
||||
f"Face: {face_id}",
|
||||
f"当前偏移变换位置: {_format_value(plan.get('current_plane_position'))}",
|
||||
f"目标偏移变换位置: {_format_value(plan.get('target_plane_position'))}",
|
||||
f"当前偏移位置: {_format_value(plan.get('current_plane_position'))}",
|
||||
f"目标偏移位置: {_format_value(plan.get('target_plane_position'))}",
|
||||
f"平面方向: {_format_value(plan.get('plane_direction'))}",
|
||||
f"移动向量: {_format_value(plan.get('face_center_move_vector'))}",
|
||||
f"移动距离: {_format_value(plan.get('face_center_move_distance'))}",
|
||||
@@ -5722,11 +6019,11 @@ class WindowActionMixin:
|
||||
f"影响目标: {_format_value(plan.get('local_face_deform_target_kind'))}",
|
||||
]
|
||||
if not self._confirm_quick_edit_plan(
|
||||
"偏移变换(局部重建)",
|
||||
"偏移(局部重建)",
|
||||
plan,
|
||||
lines,
|
||||
"偏移变换(局部重建)已阻止",
|
||||
"已取消偏移变换(局部重建)",
|
||||
"偏移(局部重建)已阻止",
|
||||
"已取消偏移(局部重建)",
|
||||
):
|
||||
return
|
||||
self.clear_edit_preview(render=False)
|
||||
@@ -5736,7 +6033,7 @@ class WindowActionMixin:
|
||||
|
||||
self._run_edit_action(
|
||||
action,
|
||||
operation_name="偏移变换(局部重建)",
|
||||
operation_name="偏移(局部重建)",
|
||||
target=f"Face {face_id}",
|
||||
parameters={
|
||||
"face_id": face_id,
|
||||
@@ -5758,9 +6055,9 @@ class WindowActionMixin:
|
||||
"local_face_deform_face_count": plan.get("local_face_deform_face_count"),
|
||||
"local_face_deform_moved_point_count": plan.get("local_face_deform_moved_point_count"),
|
||||
"resize_strategy": "local-face-plane-offset-deform",
|
||||
"edit_strategy_label": "偏移变换(局部重建)",
|
||||
"edit_strategy_label": "偏移(局部重建)",
|
||||
"edit_semantics": (
|
||||
"把目标偏移变换位置换算成沿当前面垂直方向的移动量,移动当前 Face 的顶点并重建相邻平面;"
|
||||
"把目标偏移位置换算成沿当前面垂直方向的移动量,移动当前 Face 的顶点并重建相邻平面;"
|
||||
"不拉伸/切除加料/切削,也不平移所属对象。"
|
||||
),
|
||||
"resize_status": plan.get("status"),
|
||||
@@ -5813,12 +6110,15 @@ class WindowActionMixin:
|
||||
return self.model.move_face_center_owning(face_id, target_center)
|
||||
|
||||
else:
|
||||
QMessageBox.information(self, "不能移动Face中心", "当前 Face 没有关联到稳定的所属对象。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"移动Face中心",
|
||||
"当前 Face 没有关联到稳定的所属对象。",
|
||||
"Face中心移动已阻止",
|
||||
)
|
||||
return
|
||||
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能移动Face中心", str(plan["message"]))
|
||||
self.statusBar().showMessage("Face中心移动已阻止")
|
||||
self._show_blocked_plan_message("移动Face中心", plan, "Face中心移动已阻止")
|
||||
return
|
||||
if not self._confirm_face_center_move_plan(plan, face_id, current_center, target_center):
|
||||
self.statusBar().showMessage("已取消Face中心移动")
|
||||
@@ -5868,7 +6168,11 @@ class WindowActionMixin:
|
||||
face_id = self.selected_face_id
|
||||
current_center = self._selected_face_center_for_translation(face_id)
|
||||
if current_center is None:
|
||||
QMessageBox.information(self, "不能执行中心(局部重建)", "当前 Face 缺少稳定中心坐标。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"中心(局部重建)",
|
||||
"当前 Face 缺少稳定中心坐标。",
|
||||
"中心(局部重建)已阻止",
|
||||
)
|
||||
return
|
||||
target_center = (
|
||||
current_center[0] + vector[0],
|
||||
@@ -5953,7 +6257,11 @@ class WindowActionMixin:
|
||||
info = self._selected_face_info_snapshot()
|
||||
current_center = self._selected_axis_center_for_translation(info)
|
||||
if current_center is None:
|
||||
QMessageBox.information(self, "不能按轴心平移", "当前对象缺少稳定轴心坐标。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"按轴心平移",
|
||||
"当前对象缺少稳定轴心坐标。",
|
||||
"按轴心平移所属对象已阻止",
|
||||
)
|
||||
return
|
||||
target_center = (
|
||||
current_center[0] + vector[0],
|
||||
@@ -5977,12 +6285,15 @@ class WindowActionMixin:
|
||||
return self.model.translate_part(moved_id, vector)
|
||||
|
||||
else:
|
||||
QMessageBox.information(self, "不能按轴心平移", "当前对象没有关联到稳定的所属特征或 Solid。")
|
||||
self._show_immediate_cannot_edit_message(
|
||||
"按轴心平移",
|
||||
"当前对象没有关联到稳定的所属特征或 Solid。",
|
||||
"按轴心平移所属对象已阻止",
|
||||
)
|
||||
return
|
||||
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能按轴心平移", str(plan["message"]))
|
||||
self.statusBar().showMessage("按轴心平移所属对象已阻止")
|
||||
self._show_blocked_plan_message("按轴心平移", plan, "按轴心平移所属对象已阻止")
|
||||
return
|
||||
if not self._confirm_axis_center_translation_plan(plan, face_id, current_center, target_center):
|
||||
self.statusBar().showMessage("已取消按轴心平移所属对象")
|
||||
@@ -6184,23 +6495,23 @@ class WindowActionMixin:
|
||||
warnings_line = f"警告: {warnings}\n\n" if warnings else ""
|
||||
result = QMessageBox.question(
|
||||
self,
|
||||
"确认偏移变换整体平移",
|
||||
"确认偏移整体平移",
|
||||
(
|
||||
f"Face: {face_id}\n"
|
||||
f"移动对象: {_format_value(plan.get('target_kind', ''))}\n"
|
||||
f"零件: {_format_value(plan.get('part_id', ''))}\n"
|
||||
f"Solid: {_format_value(plan.get('solid_id', ''))}\n"
|
||||
f"当前偏移变换位置: {_format_value(current_position)}\n"
|
||||
f"目标偏移变换位置: {_format_value(target_position)}\n"
|
||||
f"当前偏移位置: {_format_value(current_position)}\n"
|
||||
f"目标偏移位置: {_format_value(target_position)}\n"
|
||||
f"平面方向: {_format_value(plane_direction)}\n"
|
||||
f"平移向量: {_format_value(plan['translation_vector'])}\n"
|
||||
f"平移距离: {_format_value(plan['translation_distance'])}\n"
|
||||
f"执行策略: 按偏移变换平移所属对象\n"
|
||||
f"执行策略: 按偏移平移所属对象\n"
|
||||
f"建模意图: 沿当前面垂直方向平移所属特征或 Solid;不拉伸/切除,不切削,也不补料。\n"
|
||||
f"风险: {plan['risk']}\n\n"
|
||||
f"{warnings_line}"
|
||||
f"{plan['message']}\n\n"
|
||||
"如果你想改变厚度或把这个面推出/切入,请在“偏移变换”这一行把建模意图选为“拉伸/切除”。确定继续吗?"
|
||||
"如果你想改变厚度或把这个面推出/切入,请在“偏移”这一行把建模意图选为“拉伸/切除”。确定继续吗?"
|
||||
),
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
@@ -6223,8 +6534,7 @@ class WindowActionMixin:
|
||||
part_id = self.selected_part_id
|
||||
plan = self.model.translate_part_plan(part_id, vector)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能平移零件", str(plan["message"]))
|
||||
self.statusBar().showMessage("平移零件已阻止")
|
||||
self._show_blocked_plan_message("平移零件", plan, "平移零件已阻止")
|
||||
return
|
||||
if not self._confirm_translation_plan("确认平移零件", plan):
|
||||
self.statusBar().showMessage("已取消平移零件")
|
||||
@@ -6268,8 +6578,7 @@ class WindowActionMixin:
|
||||
solid_id = self.selected_solid_id
|
||||
plan = self.model.translate_solid_plan(solid_id, vector)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能平移Solid", str(plan["message"]))
|
||||
self.statusBar().showMessage("平移Solid已阻止")
|
||||
self._show_blocked_plan_message("平移Solid", plan, "平移Solid已阻止")
|
||||
return
|
||||
if not self._confirm_translation_plan("确认平移Solid", plan):
|
||||
self.statusBar().showMessage("已取消平移Solid")
|
||||
@@ -6351,8 +6660,7 @@ class WindowActionMixin:
|
||||
part_id = self.selected_part_id
|
||||
plan = self.model.rotate_part_plan(part_id, axis, angle)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能旋转零件", str(plan["message"]))
|
||||
self.statusBar().showMessage("旋转零件已阻止")
|
||||
self._show_blocked_plan_message("旋转零件", plan, "旋转零件已阻止")
|
||||
return
|
||||
if not self._confirm_rotation_plan("确认旋转零件", plan):
|
||||
self.statusBar().showMessage("已取消旋转零件")
|
||||
@@ -6397,8 +6705,7 @@ class WindowActionMixin:
|
||||
solid_id = self.selected_solid_id
|
||||
plan = self.model.rotate_solid_plan(solid_id, axis, angle)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能旋转Solid", str(plan["message"]))
|
||||
self.statusBar().showMessage("旋转Solid已阻止")
|
||||
self._show_blocked_plan_message("旋转Solid", plan, "旋转Solid已阻止")
|
||||
return
|
||||
if not self._confirm_rotation_plan("确认旋转Solid", plan):
|
||||
self.statusBar().showMessage("已取消旋转Solid")
|
||||
@@ -6445,8 +6752,7 @@ class WindowActionMixin:
|
||||
part_id = self.selected_part_id
|
||||
plan = self.model.scale_part_plan(part_id, target_diagonal)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能缩放零件", str(plan["message"]))
|
||||
self.statusBar().showMessage("缩放零件已阻止")
|
||||
self._show_blocked_plan_message("缩放零件", plan, "缩放零件已阻止")
|
||||
return
|
||||
if not self._confirm_scale_plan("确认缩放零件", plan):
|
||||
self.statusBar().showMessage("已取消缩放零件")
|
||||
@@ -6493,8 +6799,7 @@ class WindowActionMixin:
|
||||
solid_id = self.selected_solid_id
|
||||
plan = self.model.scale_solid_plan(solid_id, target_diagonal)
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能缩放Solid", str(plan["message"]))
|
||||
self.statusBar().showMessage("缩放Solid已阻止")
|
||||
self._show_blocked_plan_message("缩放Solid", plan, "缩放Solid已阻止")
|
||||
return
|
||||
if not self._confirm_scale_plan("确认缩放Solid", plan):
|
||||
self.statusBar().showMessage("已取消缩放Solid")
|
||||
@@ -6594,8 +6899,7 @@ class WindowActionMixin:
|
||||
run_target_kind = "solid"
|
||||
|
||||
if plan["status"] == "blocked":
|
||||
QMessageBox.information(self, "不能按轴缩放", str(plan["message"]))
|
||||
self.statusBar().showMessage("按轴缩放已阻止")
|
||||
self._show_blocked_plan_message("按轴缩放", plan, "按轴缩放已阻止")
|
||||
return
|
||||
if not self._confirm_axis_scale_plan(title, plan):
|
||||
self.statusBar().showMessage("已取消按轴缩放")
|
||||
@@ -7075,6 +7379,10 @@ class WindowActionMixin:
|
||||
"edit_result_deflection": result_deflection,
|
||||
"isolation": dict(isolation or {}),
|
||||
}
|
||||
blocker = self._edit_preflight_blocker(context)
|
||||
if blocker is not None:
|
||||
self._show_edit_preflight_blocker(blocker, context)
|
||||
return
|
||||
|
||||
self._begin_edit_task(operation_name)
|
||||
self.pending_edit_context = context
|
||||
@@ -7563,7 +7871,7 @@ class WindowActionMixin:
|
||||
target_width = _float_or_none(parameters.get("target_face_width"))
|
||||
target_height = _float_or_none(parameters.get("target_face_height"))
|
||||
if target_width is not None or target_height is not None:
|
||||
checks.append(("U向尺寸/V向尺寸", self._face_target_size_matches(model, face_ids, target_width, target_height)))
|
||||
checks.append(("面内长度/面内宽度", self._face_target_size_matches(model, face_ids, target_width, target_height)))
|
||||
|
||||
target_center = _triple_or_none(parameters.get("target_face_center"))
|
||||
if target_center is not None:
|
||||
@@ -7577,7 +7885,7 @@ class WindowActionMixin:
|
||||
if target_position is not None and plane_direction is not None:
|
||||
checks.append(
|
||||
(
|
||||
"偏移变换",
|
||||
"偏移",
|
||||
self._face_target_plane_position_matches(
|
||||
model,
|
||||
face_ids,
|
||||
@@ -8005,9 +8313,9 @@ class WindowActionMixin:
|
||||
"面",
|
||||
"面积",
|
||||
"中心",
|
||||
"U向尺寸",
|
||||
"V向尺寸",
|
||||
"偏移变换",
|
||||
"面内长度",
|
||||
"面内宽度",
|
||||
"偏移",
|
||||
"壳体",
|
||||
"拉伸/切除",
|
||||
"圆锥",
|
||||
@@ -8058,13 +8366,13 @@ class WindowActionMixin:
|
||||
progress_text = f"{operation_name} 正在后台计算,预览表示局部边形变、端面拉伸/切除或后备策略后的目标范围。"
|
||||
elif "面积(局部重建)" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会在面内移动所选 Face 顶点并重建相邻平面。"
|
||||
elif "U向尺寸(缩放特征)" in operation_name or "V向尺寸(缩放特征)" in operation_name:
|
||||
elif "面内长度(缩放特征)" in operation_name or "面内宽度(缩放特征)" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会沿选中 Face 的平面内方向缩放所属特征或 Solid,其它几何会跟随变化。"
|
||||
elif "U向尺寸(局部重建)" in operation_name or "V向尺寸(局部重建)" in operation_name:
|
||||
elif "面内长度(局部重建)" in operation_name or "面内宽度(局部重建)" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会只沿选中 Face 的一个平面内方向缩放顶点并重建相邻平面。"
|
||||
elif "偏移变换(局部重建)" in operation_name:
|
||||
elif "偏移(局部重建)" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会沿当前面垂直方向移动所选 Face 顶点并重建相邻平面。"
|
||||
elif "偏移变换(移动特征)" in operation_name:
|
||||
elif "偏移(移动特征)" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会沿当前面垂直方向平移所属特征或 Solid,不拉伸/切除。"
|
||||
elif "Face中心" in operation_name:
|
||||
progress_text = f"{operation_name} 正在后台计算;当前会平移所属特征或 Solid,不做单面局部扭曲。"
|
||||
@@ -8199,15 +8507,19 @@ class WindowActionMixin:
|
||||
if hasattr(self, "_is_ui_thread") and not self._is_ui_thread():
|
||||
self._invoke_on_ui_thread(lambda message=message: self._fail_edit_action(message))
|
||||
return
|
||||
context = self.pending_edit_context
|
||||
close_after_cancel = bool(getattr(self, "close_after_edit_cancel", False))
|
||||
if close_after_cancel:
|
||||
self.close_after_edit_cancel = False
|
||||
self._end_edit_task(clear_preview=True)
|
||||
if not close_after_cancel:
|
||||
QMessageBox.critical(self, "操作失败", message)
|
||||
title, user_message = self._user_facing_edit_failure_message(message, context)
|
||||
QMessageBox.information(self, title, user_message)
|
||||
self._clear_editable_candidates()
|
||||
self._clear_cylinder_candidates()
|
||||
self.statusBar().showMessage("后台编辑已取消,模型已保持在编辑前状态" if close_after_cancel else "操作失败,模型已保持在编辑前状态")
|
||||
self.statusBar().showMessage(
|
||||
"后台编辑已取消,模型已保持在编辑前状态" if close_after_cancel else "不能修改,模型未修改"
|
||||
)
|
||||
if close_after_cancel:
|
||||
if self.edit_thread is not None and self.edit_thread.isRunning():
|
||||
self.edit_thread.quit()
|
||||
|
||||
+104
-3
@@ -135,6 +135,18 @@ class WindowCoreMixin:
|
||||
def _should_suppress_transient_tooltip(self, watched) -> bool:
|
||||
if not isinstance(watched, QWidget):
|
||||
return False
|
||||
property_table = getattr(self, "property_table", None)
|
||||
if isinstance(property_table, QWidget):
|
||||
widget = watched
|
||||
while widget is not None:
|
||||
if widget is property_table:
|
||||
return False
|
||||
widget = widget.parentWidget()
|
||||
try:
|
||||
if watched is property_table.viewport():
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
suppressed_roots = (
|
||||
getattr(self, "vtk_widget", None),
|
||||
getattr(self, "object_edit_box", None),
|
||||
@@ -1791,6 +1803,8 @@ class WindowCoreMixin:
|
||||
self.pointer_button_down = True
|
||||
self.left_button_press_position = (int(x), int(y))
|
||||
self.left_button_dragged = False
|
||||
self.left_button_press_camera_state = self._camera_state_signature()
|
||||
self.left_button_press_target = self._selection_target_at_position(x, y)
|
||||
self.pending_hover_position = None
|
||||
self.last_hover_pick_position = None
|
||||
self.hover_timer.stop()
|
||||
@@ -1812,16 +1826,55 @@ class WindowCoreMixin:
|
||||
def _handle_left_button_release(self, x: int, y: int) -> None:
|
||||
self._update_left_button_drag_state(x, y)
|
||||
was_dragged = bool(getattr(self, "left_button_dragged", False))
|
||||
camera_changed = self._left_button_camera_changed()
|
||||
self.pointer_button_down = False
|
||||
press_target = getattr(self, "left_button_press_target", None)
|
||||
self.left_button_press_position = None
|
||||
self.left_button_dragged = False
|
||||
self.left_button_press_camera_state = None
|
||||
self.left_button_press_target = None
|
||||
self.pending_hover_position = None
|
||||
self.last_hover_pick_position = None
|
||||
if getattr(self, "camera_interaction_active", False):
|
||||
self._end_camera_interaction()
|
||||
if was_dragged:
|
||||
if was_dragged or camera_changed or self._selection_target_signature(press_target) is None:
|
||||
return
|
||||
self._handle_left_click(x, y)
|
||||
self._handle_left_click(x, y, required_press_target=press_target)
|
||||
|
||||
def _camera_state_signature(self) -> tuple[float, ...] | None:
|
||||
renderer = getattr(self, "renderer", None)
|
||||
if renderer is None:
|
||||
return None
|
||||
try:
|
||||
camera = renderer.GetActiveCamera()
|
||||
except Exception:
|
||||
return None
|
||||
if camera is None:
|
||||
return None
|
||||
values: list[float] = []
|
||||
for getter in (
|
||||
"GetPosition",
|
||||
"GetFocalPoint",
|
||||
"GetViewUp",
|
||||
):
|
||||
try:
|
||||
values.extend(float(item) for item in getattr(camera, getter)())
|
||||
except Exception:
|
||||
return None
|
||||
for getter in ("GetParallelScale", "GetDistance", "GetViewAngle"):
|
||||
try:
|
||||
values.append(float(getattr(camera, getter)()))
|
||||
except Exception:
|
||||
pass
|
||||
return tuple(values)
|
||||
|
||||
def _left_button_camera_changed(self) -> bool:
|
||||
before = getattr(self, "left_button_press_camera_state", None)
|
||||
after = self._camera_state_signature()
|
||||
if before is None or after is None or len(before) != len(after):
|
||||
return False
|
||||
tolerance = float(getattr(self, "left_click_camera_tolerance", 1e-7) or 1e-7)
|
||||
return any(abs(float(a) - float(b)) > tolerance for a, b in zip(before, after))
|
||||
|
||||
def _update_left_button_drag_state(self, x: int, y: int) -> None:
|
||||
press_position = getattr(self, "left_button_press_position", None)
|
||||
@@ -1833,7 +1886,33 @@ class WindowCoreMixin:
|
||||
if max(dx, dy) > threshold:
|
||||
self.left_button_dragged = True
|
||||
|
||||
def _handle_left_click(self, x: int, y: int) -> None:
|
||||
def _selection_target_signature(self, target: dict[str, object] | None) -> tuple[str, int] | None:
|
||||
if not isinstance(target, dict):
|
||||
return None
|
||||
try:
|
||||
return str(target["kind"]), int(target["target_id"])
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return None
|
||||
|
||||
def _selection_target_at_position(self, x: int, y: int) -> dict[str, object] | None:
|
||||
if (
|
||||
getattr(self, "load_in_progress", False)
|
||||
or getattr(self, "operation_in_progress", False)
|
||||
or getattr(self, "scan_in_progress", False)
|
||||
or getattr(self, "model", None) is None
|
||||
):
|
||||
return None
|
||||
try:
|
||||
return self._pick_selection_target(self._current_selection_mode(), int(x), int(y))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _handle_left_click(
|
||||
self,
|
||||
x: int,
|
||||
y: int,
|
||||
required_press_target: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
self.pending_hover_position = None
|
||||
self.last_hover_pick_position = None
|
||||
self.hover_timer.stop()
|
||||
@@ -1850,6 +1929,16 @@ class WindowCoreMixin:
|
||||
return
|
||||
mode = self._current_selection_mode()
|
||||
target = self._pick_selection_target(mode, x, y)
|
||||
if required_press_target is not None:
|
||||
press_signature = self._selection_target_signature(required_press_target)
|
||||
release_signature = self._selection_target_signature(target)
|
||||
if press_signature is None or release_signature != press_signature:
|
||||
self._cancel_hover_tracking(render=False)
|
||||
if hasattr(self, "render_window"):
|
||||
self.render_window.Render()
|
||||
if self.selected_kind is not None:
|
||||
self.statusBar().showMessage("左键按下和松开未命中同一对象,已保持当前选择")
|
||||
return
|
||||
if target is None:
|
||||
self._cancel_hover_tracking(render=False)
|
||||
if hasattr(self, "render_window"):
|
||||
@@ -1871,6 +1960,7 @@ class WindowCoreMixin:
|
||||
|
||||
def _handle_pointer_button_press(self) -> None:
|
||||
self.pointer_button_down = True
|
||||
self.left_button_press_target = None
|
||||
self.pending_hover_position = None
|
||||
self.last_hover_pick_position = None
|
||||
self.hover_timer.stop()
|
||||
@@ -1884,6 +1974,7 @@ class WindowCoreMixin:
|
||||
|
||||
def _handle_pointer_button_release(self) -> None:
|
||||
self.pointer_button_down = False
|
||||
self.left_button_press_target = None
|
||||
self.pending_hover_position = None
|
||||
self.last_hover_pick_position = None
|
||||
self._end_camera_interaction()
|
||||
@@ -2279,6 +2370,14 @@ class WindowCoreMixin:
|
||||
self._set_selection_mode("Edge")
|
||||
self.select_edge(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可尝试调整长度的Edge {target_id}")
|
||||
elif action == "resize_ellipse_edge_major_radius":
|
||||
self._set_selection_mode("Edge")
|
||||
self.select_edge(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可调整椭圆主半径的Edge {target_id}")
|
||||
elif action == "resize_ellipse_edge_minor_radius":
|
||||
self._set_selection_mode("Edge")
|
||||
self.select_edge(int(target_id))
|
||||
self.statusBar().showMessage(f"已选择可调整椭圆小半径的Edge {target_id}")
|
||||
elif target_kind == "edge":
|
||||
self._set_selection_mode("Edge")
|
||||
self.select_edge(int(target_id))
|
||||
@@ -2439,6 +2538,8 @@ class WindowCoreMixin:
|
||||
self._sync_id_picker("Feature", logical_id)
|
||||
self.set_info(self._with_pick_info(info, pick_position))
|
||||
feature_type = str(info.get("feature_type", "特征候选"))
|
||||
if hasattr(self, "_feature_display_label"):
|
||||
feature_type = self._feature_display_label(feature_type)
|
||||
self._update_action_states()
|
||||
raw_note = f"(当前拓扑 Face {face_id})" if logical_id != face_id else ""
|
||||
self.statusBar().showMessage(self._selection_status(f"已选择 {feature_type},来源 Face {logical_id}{raw_note}", pick_position))
|
||||
|
||||
+307
-318
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user