feat: 完善 STEP/B-Rep 一级关系参数化编辑

This commit is contained in:
2026-08-07 18:08:32 +08:00
parent eef9efcc1e
commit 12250603dd
26 changed files with 4254 additions and 1270 deletions
+113 -17
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+158
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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))
File diff suppressed because it is too large Load Diff