feat: 完善参数化编辑语义和槽孔中心距

This commit is contained in:
2026-07-30 17:54:01 +08:00
parent c937e13d43
commit 27e4f7236c
22 changed files with 8769 additions and 668 deletions
+88 -12
View File
@@ -108,8 +108,6 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.pending_hover_position: tuple[int, int] | None = None
self.last_hover_pick_position: tuple[int, int] | None = None
self.camera_interaction_active = False
self.edge_actor_visible_before_interaction = True
self.model_interpolation_before_interaction: int | None = None
self.pointer_button_down = False
self.left_button_press_position: tuple[int, int] | None = None
self.left_button_dragged = False
@@ -132,6 +130,10 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.model_part_id_array = None
self.model_solid_id_array = None
self.edge_id_array = None
self.model_face_cell_ids_by_face: dict[int, list[int]] = {}
self.model_face_cell_ids_by_part: dict[int, list[int]] = {}
self.model_face_cell_ids_by_solid: dict[int, list[int]] = {}
self.edge_cell_ids_by_edge: dict[int, list[int]] = {}
self.face_overlay_polydata_cache: dict[tuple[tuple[int, ...] | None, tuple[int, ...] | None, bool], object] = {}
self.edge_overlay_polydata_cache: dict[int, object] = {}
self.overlay_cache_limit = 160
@@ -164,6 +166,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.scan_worker: ScanWorker | None = None
self.pending_scan_kind: str | None = None
self.pending_scan_context: dict[str, object] | None = None
self.scan_wait_cursor_active = False
self.load_in_progress = False
self.load_thread: QThread | None = None
self.load_worker: LoadWorker | None = None
@@ -331,11 +334,17 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
background: #fff8ef;
border: 1px solid #edc98e;
border-left: 5px solid #d97706;
padding-left: 5px;
padding-right: 3px;
padding-bottom: 7px;
}
QGroupBox#editSection::title {
background: #fff0d8;
border-color: #edc98e;
color: #a45303;
left: 10px;
padding-left: 6px;
padding-right: 6px;
}
QGroupBox#editableSection,
QGroupBox#candidateSection {
@@ -441,6 +450,28 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
border: 1px dashed #bcc7d4;
color: #8f99a8;
}
QComboBox#propertyScopeCombo {
background: #ffffff;
border: 1px solid #f59e0b;
border-radius: 5px;
color: #7c2d12;
min-height: 18px;
padding: 0px 4px;
font-weight: 600;
}
QComboBox#propertyScopeCombo:hover {
background: #fffbeb;
border-color: #ea580c;
}
QComboBox#propertyScopeCombo:focus {
background: #ffffff;
border: 1px solid #2563eb;
}
QComboBox#propertyScopeCombo:disabled {
background: #eef2f6;
border: 1px dashed #bcc7d4;
color: #8f99a8;
}
QLineEdit#propertyTargetEditor {
background: #fff7ed;
border: 2px solid #f97316;
@@ -532,6 +563,18 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
font-weight: 700;
padding: 4px 6px;
}
QTableWidget#propertyTable {
margin: 0;
padding: 0;
}
QTableWidget#propertyTable::item {
padding-left: 2px;
padding-right: 2px;
}
QTableWidget#propertyTable QHeaderView::section {
padding-left: 3px;
padding-right: 3px;
}
QTabWidget::pane {
border: 1px solid #d8e0eb;
border-radius: 6px;
@@ -779,8 +822,11 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
"选择模型对象后,这里会列出当前对象的属性和值;能改的行可以输入目标值,不能改的行只读。",
)
object_edit_layout = QVBoxLayout(self.object_edit_box)
self.property_table = QTableWidget(0, 4)
self.property_table.setHorizontalHeaderLabels(["属性", "当前值", "目标值", "状态"])
object_edit_layout.setContentsMargins(2, 8, 2, 4)
object_edit_layout.setSpacing(4)
self.property_table = QTableWidget(0, 5)
self.property_table.setObjectName("propertyTable")
self.property_table.setHorizontalHeaderLabels(["属性", "当前值", "影响范围", "目标值", "操作"])
self.property_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.property_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.property_table.setAlternatingRowColors(True)
@@ -798,14 +844,16 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
property_header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
property_header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
property_header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
property_header.setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
self.property_table.setColumnWidth(0, 106)
self.property_table.setColumnWidth(1, 92)
self.property_table.setColumnWidth(2, 92)
self.property_table.setColumnWidth(3, 62)
property_header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
property_header.setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch)
self.property_table.setColumnWidth(0, 82)
self.property_table.setColumnWidth(1, 78)
self.property_table.setColumnWidth(2, 104)
self.property_table.setColumnWidth(3, 82)
self.property_table.setColumnWidth(4, 58)
help_tip(
self.property_table,
"当前选中对象的属性表。可修改的行可以在目标值列输入新值,并点击状态列里的修改按钮只应用这一行。",
"当前选中对象的属性表。可修改的行可以选择影响范围、输入目标值,并点击操作列里的按钮只应用这一行。",
)
self.property_table.itemChanged.connect(self._on_property_table_item_changed)
object_edit_layout.addWidget(self.property_table)
@@ -834,7 +882,7 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
edit_box.setVisible(False)
self.hidden_edit_controls_box = edit_box
edit_layout = QGridLayout(edit_box)
edit_layout.addWidget(QLabel("Face偏"), 0, 0)
edit_layout.addWidget(QLabel("动距离"), 0, 0)
self.offset_input = QLineEdit("5.0")
help_tip(self.offset_input, "平面推拉距离。正数通常向外加料,负数通常向内切削;单位沿用 STEP 模型单位。")
edit_layout.addWidget(self.offset_input, 0, 1)
@@ -893,6 +941,9 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.slot_total_length_input = QLineEdit("", edit_box)
self.slot_total_length_input.setVisible(False)
help_tip(self.slot_total_length_input, "长圆槽/槽孔的目标总长度。只有识别到成对半圆槽端时才会启用。")
self.slot_center_distance_input = QLineEdit("", edit_box)
self.slot_center_distance_input.setVisible(False)
help_tip(self.slot_center_distance_input, "长圆槽/槽孔两端半圆中心的目标距离。当前选中对象表会使用这个隐藏输入执行中心距修改。")
self.slot_center_x_input = QLineEdit("", edit_box)
self.slot_center_x_input.setVisible(False)
self.slot_center_y_input = QLineEdit("", edit_box)
@@ -930,7 +981,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, "复杂曲面的目标Face面积。当前选中对象表会使用这个隐藏输入执行兜底面积缩放。")
help_tip(self.face_area_input, "目标面面积。当前选中对象表会使用这个隐藏输入执行所属特征或 Solid 的均匀缩放。")
self.face_width_input = QLineEdit("", edit_box)
self.face_width_input.setVisible(False)
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, "目标面高。当前选中对象表会使用这个隐藏输入只修改当前 Face 的平面内高度。")
self.resize_boss_button = QPushButton("调整圆柱凸台直径")
help_tip(self.resize_boss_button, "修改完整圆柱凸台直径。变大会加料,变小会重建凸台区域。")
self.resize_boss_button.clicked.connect(self.resize_boss)
@@ -1007,6 +1064,8 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.edge_end_x_input = QLineEdit("", edit_box)
self.edge_end_y_input = QLineEdit("", edit_box)
self.edge_end_z_input = QLineEdit("", edit_box)
self.ellipse_edge_major_radius_input = QLineEdit("", edit_box)
self.ellipse_edge_minor_radius_input = QLineEdit("", edit_box)
for endpoint_input in (
self.edge_start_x_input,
self.edge_start_y_input,
@@ -1017,8 +1076,12 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.edge_end_x_input,
self.edge_end_y_input,
self.edge_end_z_input,
self.ellipse_edge_major_radius_input,
self.ellipse_edge_minor_radius_input,
):
endpoint_input.setVisible(False)
help_tip(self.ellipse_edge_major_radius_input, "椭圆Edge目标主半径。当前选中对象表会使用这个隐藏输入执行主轴单轴缩放。")
help_tip(self.ellipse_edge_minor_radius_input, "椭圆Edge目标小半径。当前选中对象表会使用这个隐藏输入执行小轴单轴缩放。")
self.edge_length_anchor_combo = NoWheelComboBox()
self.edge_length_anchor_combo.addItem("自动", "auto")
self.edge_length_anchor_combo.addItem("中心", "center")
@@ -1030,8 +1093,21 @@ class StepEditorWindow(WindowCoreMixin, WindowStateMixin, WindowActionMixin, Inf
self.edge_length_anchor_combo,
"选择修改Edge长度时尽量固定哪里:自动默认固定起点并移动终点;中心会两端各动一半;固定起点/终点会明确控制局部形变端点。",
)
self.edge_length_strategy_combo = NoWheelComboBox()
self.edge_length_strategy_combo.addItem("自动选择", "auto")
self.edge_length_strategy_combo.addItem("只改当前Edge", "local-edge-only-deform")
self.edge_length_strategy_combo.addItem("移动端面/保持垂直", "move-edge-end-plane-by-push-pull")
self.edge_length_strategy_combo.addItem("相邻圆柱直径", "resize-adjacent-cylinder-from-circular-edge-length")
self.edge_length_strategy_combo.addItem("缩放所属", "scale-owning-shape-from-edge")
self.edge_length_strategy_combo.setCurrentIndex(0)
self.edge_length_strategy_combo.setMinimumWidth(130)
help_tip(
self.edge_length_strategy_combo,
"选择Edge长度修改语义:自动会按可用性选择;只改当前Edge会让相邻面自然变斜;移动端面/保持垂直会让相关端面和同向边跟随;相邻圆柱直径会把圆Edge长度换算成孔/槽/凸台直径;缩放所属会让所属对象整体跟随变化。",
)
edge_length_layout.addWidget(self.edge_target_length_input, stretch=2)
edge_length_layout.addWidget(self.edge_length_anchor_combo, stretch=1)
edge_length_layout.addWidget(self.edge_length_strategy_combo, stretch=1)
edit_layout.addWidget(edge_length_inputs, 18, 1)
self.resize_edge_length_button = QPushButton("修改Edge长度")
help_tip(
+2
View File
@@ -40,6 +40,8 @@ SURFACE_TYPES = {
SNAPSHOT_FACE_LOGICAL_IDS_KEY = "__face_logical_ids__"
FACE_SELECTION_FEATURE_INFO_SURFACES = {"plane", "cylinder", "cone", "sphere", "torus"}
CURVE_TYPES = {
GeomAbs_Line: "line",
GeomAbs_Circle: "circle",
+156 -19
View File
@@ -744,6 +744,11 @@ class FeatureMixin:
target_to_height_ratio = new_diameter / height_estimate if height_estimate > 1e-9 else ""
cutter_plan = self._bounded_cylinder_cutter_plan(face_id, new_diameter, feature)
fill_plan = self._bounded_cylinder_fill_plan(face_id) if resize_mode == "shrink" else {}
edit_semantics = (
"缩小孔/槽直径:先用同轴圆柱补料封住旧孔壁,再按目标直径同轴重切;保持当前轴线和估算高度。"
if resize_mode == "shrink"
else "扩大孔/槽直径:沿当前圆柱轴线用有限长度圆柱 cutter 切到目标直径;保持当前轴线和估算高度。"
)
return {
"status": readiness["resize_status"],
"risk": readiness["resize_risk"],
@@ -773,6 +778,9 @@ class FeatureMixin:
"same_domain_range_source": axis_range["range_source"],
"material_vote_summary": info.get("material_vote_summary"),
"material_sample_count": info.get("material_sample_count"),
"resize_strategy": "same-axis-bounded-cylinder-recut",
"edit_strategy_label": "同轴圆柱重切",
"edit_semantics": edit_semantics,
**cutter_plan,
**fill_plan,
}
@@ -918,6 +926,8 @@ class FeatureMixin:
"same_domain_v_range": (axis_range.get("v_min"), axis_range.get("v_max")),
"same_domain_range_source": axis_range.get("range_source", ""),
"resize_strategy": "fill-old-cylinder-and-cut-moved-cylinder",
"edit_strategy_label": "填旧孔并切新孔",
"edit_semantics": "先填补当前完整圆柱孔,再按同直径在目标轴心切出新孔;这会改变孔的位置,不会整体平移零件。",
}
def cylindrical_slot_resize_plan(
@@ -1034,6 +1044,8 @@ class FeatureMixin:
"slot_resize_mode": mode_key,
"slot_resize_label": mode_labels.get(mode_key, "slot value"),
"slot_resize_strategy": "local-sector-fixed-angular-span",
"edit_strategy_label": "局部扇形槽重建",
"edit_semantics": "保持当前槽/半孔圆弧角度,把目标宽度、深度或圆弧长度换算成圆柱直径,再填旧槽并切出新的局部扇形槽。",
"slot_kind": slot_kind,
"slot_status": slot_status,
"slot_target_value": target_value,
@@ -1084,7 +1096,11 @@ class FeatureMixin:
resize_plan,
pair_face_id=pair_face_id,
)
edit_strategy_label = "局部扇形槽重建"
edit_semantics = "保持当前槽/半孔圆弧角度,把目标值换算成圆柱直径,再填旧槽并切出新的局部扇形槽。"
if paired_slot_plan:
edit_strategy_label = "配对长圆槽重建"
edit_semantics = "识别到长圆槽另一端后,会填充旧长圆槽包络,再按同一槽中心线和目标宽度重切完整长圆槽。"
risk = _max_risk(risk, str(paired_slot_plan.get("slot_pair_risk", "medium")))
pair_warning = str(paired_slot_plan.get("slot_pair_warning", ""))
if pair_warning:
@@ -1104,6 +1120,8 @@ class FeatureMixin:
**resize_plan,
**slot_plan,
**paired_slot_plan,
"edit_strategy_label": edit_strategy_label,
"edit_semantics": edit_semantics,
"status": status,
"risk": risk,
"message": message,
@@ -1235,6 +1253,8 @@ class FeatureMixin:
"slot_resize_mode": "angular_span",
"slot_resize_label": "slot angular span",
"slot_resize_strategy": "local-sector-angular-span",
"edit_strategy_label": "局部扇形槽角度重建",
"edit_semantics": "保持当前圆柱半径和轴线,围绕当前角度中心填旧扇形槽并切出目标圆弧角度的新扇形槽。",
"resize_mode": "widen" if (delta_span or 0.0) > 0 else "narrow",
"feature_type": feature.get("feature_type"),
"feature_guess": info.get("feature_guess"),
@@ -1388,11 +1408,51 @@ class FeatureMixin:
except Exception as exc:
blockers.append(f"Could not build slot axis move tool plan: {exc}")
resize_strategy = "fill-old-slot-sector-and-cut-moved-sector"
edit_strategy_label = "填旧槽并切新槽"
edit_semantics = "先填补当前槽/半孔扇形区域,再按同宽度、同角度在目标轴心切出新槽;不整体平移零件。"
if pair_plan:
risk = _max_risk(risk, "high")
warnings.append(
"A paired obround slot end was detected; this axis move edits the selected local sector, not the whole CAD slot sketch."
)
center_1 = _tuple_or_none(pair_plan.get("slot_capsule_start_center_1"))
center_2 = _tuple_or_none(pair_plan.get("slot_capsule_start_center_2"))
if center_1 is None or center_2 is None:
risk = _max_risk(risk, "high")
warnings.append(
"A paired obround slot end was detected, but the full-slot centerline could not be derived."
)
else:
pair_plan["slot_capsule_target_start_center_1"] = _tuple_add(center_1, movement)
pair_plan["slot_capsule_target_start_center_2"] = _tuple_add(center_2, movement)
pair_plan["slot_target_center_distance"] = pair_plan.get("slot_pair_axis_distance", "")
pair_plan["slot_current_center_distance"] = pair_plan.get("slot_pair_axis_distance", "")
pair_plan["slot_pair_current_axis_center_1"] = current_center
pair_plan["slot_pair_target_axis_center_1"] = target_center
try:
pair_face_id = int(pair_plan.get("slot_pair_face_id", -1))
pair_surf = BRepAdaptor_Surface(self.faces[pair_face_id])
pair_feature = self.feature_info(pair_face_id)
pair_axis_range = self._cylindrical_axis_range(
pair_face_id,
pair_surf,
_int_values(pair_feature.get("feature_side_face_ids")),
)
pair_cyl = pair_surf.Cylinder()
pair_mid_parameter = (float(pair_axis_range["v_min"]) + float(pair_axis_range["v_max"])) * 0.5
pair_center = _point_tuple(
_point_on_axis(pair_cyl.Axis().Location(), pair_cyl.Axis().Direction(), pair_mid_parameter)
)
pair_plan["slot_pair_current_axis_center_2"] = pair_center
pair_plan["slot_pair_target_axis_center_2"] = _tuple_add(pair_center, movement)
except Exception:
pair_plan["slot_pair_current_axis_center_2"] = ""
pair_plan["slot_pair_target_axis_center_2"] = ""
if not pair_plan.get("slot_pair_target_axis_center_2"):
blockers.append("Could not derive both obround slot end centers for the axis move.")
else:
resize_strategy = "paired-obround-slot-axis-prism"
edit_strategy_label = "整条长圆槽轴心移动"
edit_semantics = "识别到长圆槽另一端后,会填充旧长圆槽包络,再按同槽宽、同总长度在目标轴心重切整条长圆槽;不整体平移零件。"
risk = _max_risk(risk, str(pair_plan.get("slot_pair_risk", "medium")))
warnings.append("Detected paired partial-cylinder slot ends; axis move will relocate the full obround slot.")
current_radius = current_diameter * 0.5 if current_diameter is not None else None
current_width = (
@@ -1412,6 +1472,7 @@ class FeatureMixin:
return {
**cutter_plan,
**fill_plan,
**pair_plan,
"status": status,
"risk": "blocked" if blockers else risk,
"message": message,
@@ -1455,7 +1516,9 @@ class FeatureMixin:
"same_domain_face_count": axis_range.get("same_domain_face_count", 0),
"same_domain_v_range": (axis_range.get("v_min"), axis_range.get("v_max")),
"same_domain_range_source": axis_range.get("range_source", ""),
"resize_strategy": "fill-old-slot-sector-and-cut-moved-sector",
"resize_strategy": resize_strategy,
"edit_strategy_label": edit_strategy_label,
"edit_semantics": edit_semantics,
}
def _paired_obround_slot_plan(
@@ -1767,6 +1830,8 @@ class FeatureMixin:
"slot_resize_mode": "total_length",
"slot_resize_label": "slot total length",
"slot_resize_strategy": "paired-obround-slot-length-prism",
"edit_strategy_label": "长圆槽总长度重建",
"edit_semantics": "保持槽宽不变,围绕长圆槽中心线对两端中心距做对称调整,然后填旧槽并重切目标总长度的长圆槽。",
"resize_mode": "lengthen" if (delta_length or 0.0) > 0 else "shorten",
"slot_current_total_length": current_total_length,
"slot_target_total_length": target_total_length,
@@ -1778,6 +1843,68 @@ class FeatureMixin:
"slot_capsule_target_start_center_2": target_center_2,
}
def cylindrical_slot_center_distance_plan(
self,
face_id: int,
target_center_distance: float,
pair_face_id: int | None = None,
) -> dict[str, object]:
if face_id < 0 or face_id >= len(self.faces):
raise ValueError(f"Unknown face id {face_id}")
info = self.face_info(face_id)
current_diameter = _float_or_none(info.get("diameter"))
if current_diameter is None or current_diameter <= 1e-9:
return {
"status": "blocked",
"risk": "blocked",
"message": "Selected face is not a measurable cylindrical slot face.",
"blockers": "Selected face is not a measurable cylindrical slot face.",
"warnings": "",
}
try:
target_center_distance = float(target_center_distance)
except (TypeError, ValueError):
target_center_distance = 0.0
target_total_length = target_center_distance + current_diameter
plan = self.cylindrical_slot_total_length_plan(
face_id,
target_total_length,
pair_face_id=pair_face_id,
)
current_center_distance = _float_or_none(plan.get("slot_current_center_distance"))
delta = (
None
if current_center_distance is None
else target_center_distance - current_center_distance
)
delta_ratio = (
None
if current_center_distance is None or current_center_distance <= 1e-9 or delta is None
else abs(delta) / current_center_distance
)
if target_center_distance <= 0:
blockers = str(plan.get("blockers") or "")
extra = "Target slot center distance must be greater than 0."
plan["blockers"] = "; ".join(item for item in (blockers, extra) if item)
plan["message"] = plan["blockers"]
plan["status"] = "blocked"
plan["risk"] = "blocked"
plan.update(
{
"slot_resize_mode": "center_distance",
"slot_resize_label": "slot center distance",
"slot_resize_strategy": "paired-obround-slot-center-distance-prism",
"edit_strategy_label": "长圆槽中心距重建",
"edit_semantics": "保持槽宽不变,围绕长圆槽中心线对两端半圆中心距做对称调整,然后填旧槽并重切目标中心距的长圆槽。",
"slot_target_center_distance": target_center_distance,
"slot_target_total_length": target_total_length,
"slot_center_distance_delta": delta,
"slot_center_distance_delta_ratio": delta_ratio,
}
)
return plan
def cylindrical_boss_resize_plan(self, face_id: int, new_diameter: float) -> dict[str, object]:
if face_id < 0 or face_id >= len(self.faces):
raise ValueError(f"Unknown face id {face_id}")
@@ -1806,6 +1933,11 @@ class FeatureMixin:
height_estimate = float(scoped_info.get("height_estimate", 0.0))
target_to_height_ratio = new_diameter / height_estimate if height_estimate > 1e-9 else ""
tool_plan = self._bounded_boss_resize_tool_plan(face_id, new_diameter)
edit_semantics = (
"扩大凸台直径:按当前凸台轴线和估算高度生成目标圆柱补料体,并与所属特征做局部 Fuse。"
if resize_mode == "enlarge"
else "缩小凸台直径:先移除旧凸台包络,再补回目标直径圆柱;这是局部重建,不是缩放整个零件。"
)
return {
"status": readiness["boss_resize_status"],
"risk": readiness["boss_resize_risk"],
@@ -1834,6 +1966,9 @@ class FeatureMixin:
"material_sample_count": info.get("material_sample_count"),
"feature_adjacent_face_ids": feature.get("feature_adjacent_face_ids"),
"feature_boundary_edge_ids": feature.get("feature_boundary_edge_ids"),
"resize_strategy": "bounded-cylindrical-boss-envelope-rebuild",
"edit_strategy_label": "凸台包络重建",
"edit_semantics": edit_semantics,
**tool_plan,
}
@@ -1980,6 +2115,8 @@ class FeatureMixin:
"same_domain_v_range": (axis_range.get("v_min"), axis_range.get("v_max")),
"same_domain_range_source": axis_range.get("range_source", ""),
"resize_strategy": "remove-old-boss-and-fuse-moved-cylinder",
"edit_strategy_label": "移除旧凸台并补新凸台",
"edit_semantics": "先移除当前凸台包络,再按同直径在目标轴心补出新凸台;不整体平移零件。",
}
def cylindrical_suppress_plan(self, face_id: int) -> dict[str, object]:
@@ -2004,6 +2141,8 @@ class FeatureMixin:
scoped_info.update(self._cylinder_end_opening_info(face_id, BRepAdaptor_Surface(self.faces[face_id]), axis_range))
readiness = _cylinder_suppress_readiness(scoped_info)
fill_plan = self._bounded_cylinder_fill_plan(face_id)
fill_plan["fill_strategy"] = "bounded-hole-suppress-fill"
fill_plan["fill_note"] = "按当前圆柱孔范围生成略带重叠的补料圆柱体,用于封堵完整通孔或盲孔。"
return {
"status": readiness["suppress_status"],
"risk": readiness["suppress_risk"],
@@ -2029,6 +2168,9 @@ class FeatureMixin:
"feature_bottom_face_ids": feature.get("feature_bottom_face_ids"),
"feature_opening_face_ids": feature.get("feature_opening_face_ids"),
"feature_bottom_note": feature.get("feature_bottom_note"),
"resize_strategy": "fill-cylindrical-hole-volume",
"edit_strategy_label": "圆柱补料封堵",
"edit_semantics": "按当前孔轴线和估算高度生成补料圆柱体,局部 Fuse 后封堵当前完整圆柱孔。",
**fill_plan,
}
@@ -2106,6 +2248,9 @@ class FeatureMixin:
"feature_bottom_confidence": feature.get("feature_bottom_confidence"),
"feature_bottom_detection": feature.get("feature_bottom_detection"),
"feature_bottom_note": feature.get("feature_bottom_note"),
"resize_strategy": "bounded-blind-depth-cut-or-fill",
"edit_strategy_label": "盲孔/盲槽深度切削或补料",
"edit_semantics": "沿识别到的开口到底面方向调整深度:加深时切削,变浅时从新底面到旧底面补料。",
}
plan.update(context)
return plan
@@ -2407,23 +2552,14 @@ class FeatureMixin:
old_radius = cyl.Radius()
new_radius = new_diameter / 2.0
axis_range = self._cylindrical_axis_range(face_id, surf)
end_info = self._cylinder_end_opening_info(face_id, surf, axis_range)
v_min = float(axis_range["v_min"])
v_max = float(axis_range["v_max"])
span = max(v_max - v_min, 1e-6)
delta_radius = abs(new_radius - old_radius)
base_radius = max(old_radius, new_radius)
resize_mode = _resize_mode(old_radius * 2.0, new_diameter)
if resize_mode == "enlarge":
axial_margin = 0.0
start_parameter = v_min
end_parameter = v_max
else:
axial_margin = min(
max(base_radius * 0.001, delta_radius * 0.01, span * 0.001, 0.001),
max(span * 0.01, 0.02),
)
start_parameter = v_min - axial_margin
end_parameter = v_max + axial_margin
axial_margin = 0.0
start_parameter = v_min
end_parameter = v_max
radial_overlap = min(max(old_radius * 0.001, 0.001), 0.05)
height = max(end_parameter - start_parameter, 1e-6)
axis = cyl.Axis()
@@ -2436,7 +2572,7 @@ class FeatureMixin:
"boss_tool_note": (
"扩大凸台会在同域圆柱侧壁整体 V 范围内生成目标半径圆柱并 Fuse;"
"缩小凸台会先用旧半径包络体移除原凸台范围,再 Fuse 目标半径圆柱重建。"
"当前版本会给轴向两端保留少量重叠,让布尔结果更容易和原实体合并"
"补新凸台时使用原凸台轴向范围,避免直径或轴心修改时悄悄改变高度"
),
"boss_tool_scope_face_ids": axis_range["same_domain_face_ids"],
"boss_tool_scope_face_count": axis_range["same_domain_face_count"],
@@ -2455,6 +2591,7 @@ class FeatureMixin:
"boss_tool_start_point": _point_tuple(start),
"boss_tool_exact_start_point": _point_tuple(exact_start),
"boss_tool_exact_height": max(v_max - v_min, 1e-6),
**end_info,
}
def _cylinder_end_opening_info(
+6 -4
View File
@@ -590,7 +590,9 @@ def _axis_aligned_edge_candidates(
) -> list[TopoDS_Shape]:
candidates: list[tuple[float, TopoDS_Shape]] = []
length_reference = max(expected_length, reference_radius, 1.0)
distance_limit = max(reference_radius * 1.25, length_reference * 0.08, 0.2)
# A 90-degree edge fillet restores a sharp edge about sqrt(2) * R away
# from the fillet cylinder axis, so 1.25R misses the common box case.
distance_limit = max(reference_radius * 1.8, length_reference * 0.08, 0.2)
for edge in TopologyExplorer(shape, ignore_orientation=True).edges():
try:
@@ -623,10 +625,10 @@ def _axis_aligned_edge_candidates(
return [edge for _score, edge in candidates]
def _finalize_boolean_result(op, operation_name: str) -> TopoDS_Shape:
def _finalize_boolean_result(op, operation_name: str, *, use_glue: bool | None = None) -> TopoDS_Shape:
op.SetNonDestructive(True)
use_glue = "cut" not in operation_name.lower()
if use_glue and hasattr(op, "SetGlue"):
glue_enabled = "cut" not in operation_name.lower() if use_glue is None else bool(use_glue)
if glue_enabled and hasattr(op, "SetGlue"):
try:
op.SetGlue(BOPAlgo_GlueFull)
except Exception:
+135
View File
@@ -97,6 +97,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
self.edge_part_ids: list[int] = []
self.edge_solid_ids: list[int] = []
self.solids: list[tuple[int, TopoDS_Shape]] = []
self._quick_face_info_cache: dict[int, dict[str, object]] = {}
self._face_info_cache: dict[int, dict[str, object]] = {}
self._feature_info_cache: dict[int, dict[str, object]] = {}
self._edge_info_cache: dict[int, dict[str, object]] = {}
@@ -176,6 +177,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
self.edge_part_ids.clear()
self.edge_solid_ids.clear()
self.solids.clear()
self._quick_face_info_cache.clear()
self._face_info_cache.clear()
self._feature_info_cache.clear()
self._edge_info_cache.clear()
@@ -269,10 +271,128 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
logical_ids = snapshot.get(SNAPSHOT_FACE_LOGICAL_IDS_KEY)
if isinstance(logical_ids, (list, tuple)) and len(logical_ids) == len(self.faces):
self.face_logical_ids = [int(item) for item in logical_ids]
self._quick_face_info_cache.clear()
self._face_info_cache.clear()
self._feature_info_cache.clear()
self._same_domain_face_ids_cache.clear()
def quick_face_info(self, face_id: int) -> dict[str, object]:
if face_id < 0 or face_id >= len(self.faces):
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)
cached = self._quick_face_info_cache.get(face_id)
if cached is not None:
return dict(cached)
face = self.faces[face_id]
props = GProp_GProps()
brepgprop.SurfaceProperties(face, props)
surf = BRepAdaptor_Surface(face)
surface_type = surf.GetType()
surface_label = SURFACE_TYPES.get(surface_type, f"type {surface_type}")
info: dict[str, object] = {
"kind": "face",
"face_id": face_id,
"topological_face_id": face_id,
"logical_face_id": self.face_logical_id(face_id),
"part_id": self.face_part_ids[face_id],
"solid_id": self.face_solid_ids[face_id],
"orientation": _orientation_name(face.Orientation()),
"surface": surface_label,
"area": props.Mass(),
"area_center": _point_tuple(props.CentreOfMass()),
"u_range": (surf.FirstUParameter(), surf.LastUParameter()),
"v_range": (surf.FirstVParameter(), surf.LastVParameter()),
"boundary_edges": len(self._face_boundary_edge_ids(face_id)),
"selection_info_mode": "quick",
"selection_info_note": "快速选择信息;同域面、端盖、底面等深层识别会在执行编辑计划或手动扫描时再计算。",
}
info.update(_shape_bounds_info(face))
if surface_type == GeomAbs_Plane:
plane = surf.Plane()
direction = plane.Axis().Direction()
push_pull_direction = self._plane_push_pull_direction(face_id, surf)
info["plane_origin"] = _point_tuple(plane.Location())
info["normal"] = _dir_tuple(direction)
info["oriented_normal"] = _oriented_dir_tuple(direction, face)
info["push_pull_outward_direction"] = push_pull_direction["outward_direction"]
info["push_pull_inward_direction"] = push_pull_direction["inward_direction"]
info["push_pull_plus_side"] = push_pull_direction["plus_side_state"]
info["push_pull_minus_side"] = push_pull_direction["minus_side_state"]
info["push_pull_confidence"] = push_pull_direction["confidence"]
info["push_pull_note"] = push_pull_direction["note"]
info["push_pull_status"] = "candidate"
info["feature_type"] = "可推拉平面候选"
info["feature_source_face_id"] = face_id
info["feature_highlight_face_ids"] = (face_id,)
info["feature_edit_actions"] = "推拉平面"
info.update(
self._local_face_plane_size_info(
face_id,
face=face,
surf=surf,
center=_tuple_or_none(info.get("area_center")),
)
)
elif surface_type == GeomAbs_Cylinder:
cyl = surf.Cylinder()
axis = cyl.Axis()
radius = cyl.Radius()
u_span = abs(surf.LastUParameter() - surf.FirstUParameter())
swept_area = max(radius * max(u_span, 1e-9), 1e-9)
classification = self._classify_cylindrical_face(face_id, surf, detailed=False)
info["radius"] = radius
info["diameter"] = radius * 2.0
info["axis_point"] = _point_tuple(axis.Location())
info["axis"] = _dir_tuple(axis.Direction())
info["angular_span"] = u_span
info["is_full_cylinder"] = u_span >= math.tau * 0.98
info["height_estimate"] = props.Mass() / swept_area
info["feature_guess"] = classification["feature_guess"]
info["confidence"] = classification["confidence"]
info["material_toward_axis"] = classification["toward_axis"]
info["material_away_axis"] = classification["away_axis"]
info["material_vote_summary"] = classification["vote_summary"]
info["material_sample_count"] = classification["sample_count"]
info["note"] = classification["note"]
info["feature_source_face_id"] = face_id
info["feature_highlight_face_ids"] = (face_id,)
if u_span < math.tau * 0.92:
info["slot_kind"] = "partial-cylindrical-groove"
info["slot_angular_span"] = u_span
info["slot_open_angle"] = max(math.tau - u_span, 0.0)
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
elif surface_type == GeomAbs_Cone:
cone = surf.Cone()
info["axis_point"] = _point_tuple(cone.Location())
info["axis"] = _dir_tuple(cone.Axis().Direction())
info["reference_radius"] = cone.RefRadius()
info["semi_angle"] = cone.SemiAngle()
info["feature_reference_radius"] = cone.RefRadius()
info["feature_reference_diameter"] = cone.RefRadius() * 2.0
elif surface_type == GeomAbs_Sphere:
sphere = surf.Sphere()
info["center"] = _point_tuple(sphere.Location())
info["radius"] = sphere.Radius()
info["diameter"] = sphere.Radius() * 2.0
info["feature_sphere_radius"] = sphere.Radius()
info["feature_sphere_diameter"] = sphere.Radius() * 2.0
elif surface_type == GeomAbs_Torus:
torus = surf.Torus()
info["center"] = _point_tuple(torus.Location())
info["axis"] = _dir_tuple(torus.Axis().Direction())
info["major_radius"] = torus.MajorRadius()
info["minor_radius"] = torus.MinorRadius()
info["feature_torus_major_radius"] = torus.MajorRadius()
info["feature_torus_minor_radius"] = torus.MinorRadius()
self._quick_face_info_cache[face_id] = dict(info)
return dict(info)
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])
@@ -313,6 +433,14 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
info["push_pull_minus_side"] = push_pull_direction["minus_side_state"]
info["push_pull_confidence"] = push_pull_direction["confidence"]
info["push_pull_note"] = push_pull_direction["note"]
info.update(
self._local_face_plane_size_info(
face_id,
face=face,
surf=surf,
center=_tuple_or_none(info.get("area_center")),
)
)
elif surface_type == GeomAbs_Cylinder:
cyl = surf.Cylinder()
axis = cyl.Axis()
@@ -389,6 +517,10 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
except Exception:
return None
def cached_feature_info(self, face_id: int) -> dict[str, object] | None:
cached = self._feature_info_cache.get(face_id)
return dict(cached) if cached is not None else None
def feature_info(self, face_id: int) -> dict[str, object]:
if face_id < 0 or face_id >= len(self.faces):
raise ValueError(f"Unknown face id {face_id}")
@@ -1030,6 +1162,7 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
return
for face_id in valid_face_ids:
self.face_logical_ids[face_id] = int(logical_id)
self._quick_face_info_cache.pop(face_id, None)
self._face_info_cache.pop(face_id, None)
self._feature_info_cache.pop(face_id, None)
@@ -1492,6 +1625,8 @@ class StepModel(FeatureMixin, ExportMixin, TransformMixin, OperationMixin, Polyd
ellipse = curve.Ellipse()
info["center"] = _point_tuple(ellipse.Location())
info["axis"] = _dir_tuple(ellipse.Axis().Direction())
info["major_axis"] = _dir_tuple(ellipse.XAxis().Direction())
info["minor_axis"] = _dir_tuple(ellipse.YAxis().Direction())
info["major_radius"] = ellipse.MajorRadius()
info["minor_radius"] = ellipse.MinorRadius()
adjacent_face_ids = self._edge_adjacent_face_ids(edge_id)
+2356 -61
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -481,6 +481,25 @@ INFO_LABELS = {
"center_of_mass": "重心",
"surface_center": "表面积中心",
"area_center": "面积中心",
"local_face_width": "当前面宽",
"local_face_height": "当前面高",
"local_face_width_direction": "面宽方向",
"local_face_height_direction": "面高方向",
"local_face_size_center": "面宽/面高中心",
"face_size_axis": "Face尺寸方向",
"face_size_label": "Face尺寸名称",
"current_face_width": "当前面宽",
"target_face_width": "目标面宽",
"current_face_height": "当前面高",
"target_face_height": "目标面高",
"current_face_size": "当前Face尺寸",
"target_face_size": "目标Face尺寸",
"face_size_delta": "Face尺寸变化量",
"face_size_delta_ratio": "Face尺寸变化比例",
"face_size_scale": "Face尺寸缩放比例",
"face_size_center": "Face尺寸缩放中心",
"face_size_axis_direction": "Face尺寸缩放方向",
"owning_face_size_rebuild_mode": "所属对象尺寸重建模式",
"length_center": "长度中心",
"bbox_min": "包围盒最小点",
"bbox_max": "包围盒最大点",
@@ -613,6 +632,8 @@ INFO_LABELS = {
"feature_edit_actions": "当前可用操作",
"resize_status": "切削状态",
"resize_strategy": "编辑策略",
"edit_strategy_label": "编辑策略说明",
"edit_semantics": "编辑方式",
"resize_mode": "调整模式",
"resize_risk": "切削风险",
"resize_warnings": "切削警告",
File diff suppressed because it is too large Load Diff
+155 -72
View File
@@ -24,6 +24,23 @@ from .workers import EditWorker, LoadWorker, ScanWorker
VIEW_BACKGROUND_COLOR = (226 / 255.0, 238 / 255.0, 247 / 255.0)
def _prepare_static_mapper(mapper) -> None:
"""Hint that displayed STEP geometry is static while the camera moves."""
if hasattr(mapper, "ScalarVisibilityOff"):
mapper.ScalarVisibilityOff()
if hasattr(mapper, "StaticOn"):
mapper.StaticOn()
elif hasattr(mapper, "SetStatic"):
mapper.SetStatic(True)
def _safe_int_or_none(value: object) -> int | None:
try:
return int(value)
except (TypeError, ValueError):
return None
class WindowCoreMixin:
@Slot(object)
def _run_ui_task(self, callback) -> None:
@@ -121,37 +138,11 @@ class WindowCoreMixin:
return
self.camera_interaction_active = True
self._cancel_hover_tracking(render=False)
if self.edge_actor is not None:
try:
self.edge_actor_visible_before_interaction = bool(self.edge_actor.GetVisibility())
self.edge_actor.SetVisibility(False)
except Exception:
self.edge_actor_visible_before_interaction = True
if self.model_actor is not None:
try:
prop = self.model_actor.GetProperty()
self.model_interpolation_before_interaction = int(prop.GetInterpolation())
prop.SetInterpolationToGouraud()
except Exception:
self.model_interpolation_before_interaction = None
def _end_camera_interaction(self) -> None:
if not getattr(self, "camera_interaction_active", False):
return
self.camera_interaction_active = False
if self.edge_actor is not None:
try:
self.edge_actor.SetVisibility(bool(getattr(self, "edge_actor_visible_before_interaction", True)))
except Exception:
pass
if self.model_actor is not None and self.model_interpolation_before_interaction is not None:
try:
self.model_actor.GetProperty().SetInterpolation(int(self.model_interpolation_before_interaction))
except Exception:
pass
self.model_interpolation_before_interaction = None
if hasattr(self, "render_window"):
self.render_window.Render()
def closeEvent(self, event) -> None:
load_thread_running = bool(
@@ -669,13 +660,15 @@ class WindowCoreMixin:
edge_ids = self.model.edge_ids_for_solid(self.selected_solid_id)
label = f"Solid {self.selected_solid_id}"
elif self.selected_kind == "feature" and self.selected_face_id is not None:
info = self.model.feature_info(self.selected_face_id)
info = dict(getattr(self, "current_info_values", {}) or {})
if _safe_int_or_none(info.get("feature_source_face_id", self.selected_face_id)) != self.selected_face_id:
info = {}
face_ids = _int_values(info.get("feature_highlight_face_ids")) or [self.selected_face_id]
edge_ids = _int_values(info.get("feature_boundary_edge_ids"))
label = f"特征 Face {self.selected_face_id}"
elif self.selected_kind == "face" and self.selected_face_id is not None:
face_ids = self.model.connected_same_domain_face_ids(self.selected_face_id) or [self.selected_face_id]
edge_ids = self.model.face_region_boundary_edge_ids(self.selected_face_id)
face_ids = [self.selected_face_id]
edge_ids = self.model.face_boundary_edge_ids(self.selected_face_id)
label = f"Face {self.selected_face_id}"
elif self.selected_kind == "edge" and self.selected_edge_id is not None:
info = self.model.edge_info(self.selected_edge_id)
@@ -737,16 +730,18 @@ class WindowCoreMixin:
face_polydata = self.model.build_face_polydata(face_ids=face_ids)
edge_polydata = self.model.build_edge_polydata(edge_ids=edge_ids)
elif self.selected_kind == "feature" and self.selected_face_id is not None:
info = self.model.feature_info(self.selected_face_id)
info = dict(getattr(self, "current_info_values", {}) or {})
if _safe_int_or_none(info.get("feature_source_face_id", self.selected_face_id)) != self.selected_face_id:
info = {}
face_ids = _int_values(info.get("feature_highlight_face_ids")) or [self.selected_face_id]
edge_ids = _int_values(info.get("feature_boundary_edge_ids"))
face_polydata = self.model.build_face_polydata(face_ids=face_ids)
edge_polydata = self.model.build_edge_polydata(edge_ids=edge_ids)
face_polydata = self._cached_face_overlay_polydata(face_ids=face_ids, smooth=False)
if edge_ids:
edge_polydata = self.model.build_edge_polydata(edge_ids=edge_ids)
elif self.selected_kind == "face" and self.selected_face_id is not None:
face_ids = self.model.connected_same_domain_face_ids(self.selected_face_id) or [self.selected_face_id]
face_polydata = self.model.build_face_polydata(face_ids=face_ids)
face_polydata = self._cached_face_overlay_polydata(face_ids=[self.selected_face_id], smooth=False)
elif self.selected_kind == "edge" and self.selected_edge_id is not None:
edge_polydata = self.model.build_edge_polydata(edge_ids=[self.selected_edge_id])
edge_polydata = self._cached_edge_overlay_polydata(self.selected_edge_id)
return _merge_polydata_bounds(face_polydata, edge_polydata)
@@ -773,12 +768,13 @@ class WindowCoreMixin:
face_ids = [index for index, solid_id in enumerate(self.model.face_solid_ids) if solid_id == self.selected_solid_id]
self._highlight_faces(face_ids=face_ids)
elif self.selected_kind == "feature" and self.selected_face_id is not None:
info = self.model.feature_info(self.selected_face_id)
info = dict(getattr(self, "current_info_values", {}) or {})
if _safe_int_or_none(info.get("feature_source_face_id", self.selected_face_id)) != self.selected_face_id:
info = {}
face_ids = _int_values(info.get("feature_highlight_face_ids")) or [self.selected_face_id]
self._highlight_faces(face_ids=face_ids)
elif self.selected_kind == "face" and self.selected_face_id is not None:
face_ids = self.model.connected_same_domain_face_ids(self.selected_face_id) or [self.selected_face_id]
self._highlight_faces(face_ids=face_ids)
self._highlight_faces(face_ids=[self.selected_face_id])
elif self.selected_kind == "edge" and self.selected_edge_id is not None:
self._highlight_edge(self.selected_edge_id)
@@ -786,6 +782,33 @@ class WindowCoreMixin:
self.face_overlay_polydata_cache.clear()
self.edge_overlay_polydata_cache.clear()
def _rebuild_polydata_cell_indexes(self) -> None:
self.model_face_cell_ids_by_face = {}
self.model_face_cell_ids_by_part = {}
self.model_face_cell_ids_by_solid = {}
if self.model_polydata is None:
return
face_arr = self.model_face_id_array
part_arr = self.model_part_id_array
solid_arr = self.model_solid_id_array
if face_arr is None or part_arr is None or solid_arr is None:
return
for cell_id in range(self.model_polydata.GetNumberOfCells()):
face_id = int(face_arr.GetValue(cell_id))
part_id = int(part_arr.GetValue(cell_id))
solid_id = int(solid_arr.GetValue(cell_id))
self.model_face_cell_ids_by_face.setdefault(face_id, []).append(cell_id)
self.model_face_cell_ids_by_part.setdefault(part_id, []).append(cell_id)
self.model_face_cell_ids_by_solid.setdefault(solid_id, []).append(cell_id)
def _rebuild_edge_polydata_cell_index(self) -> None:
self.edge_cell_ids_by_edge = {}
if self.edge_polydata is None or self.edge_id_array is None:
return
for cell_id in range(self.edge_polydata.GetNumberOfCells()):
edge_id = int(self.edge_id_array.GetValue(cell_id))
self.edge_cell_ids_by_edge.setdefault(edge_id, []).append(cell_id)
def _remember_overlay_cache_item(self, cache: dict, key: object, value: object) -> object:
if len(cache) >= self.overlay_cache_limit:
try:
@@ -826,8 +849,22 @@ class WindowCoreMixin:
part_arr = self.model_part_id_array
if face_arr is None or part_arr is None:
return None
candidate_cell_ids = None
if face_set is not None and self.model_face_cell_ids_by_face:
candidate_cell_ids = []
for face_id in face_set:
candidate_cell_ids.extend(self.model_face_cell_ids_by_face.get(int(face_id), ()))
elif part_set is not None and self.model_face_cell_ids_by_part:
candidate_cell_ids = []
for part_id in part_set:
candidate_cell_ids.extend(self.model_face_cell_ids_by_part.get(int(part_id), ()))
cell_ids = (
range(self.model_polydata.GetNumberOfCells())
if candidate_cell_ids is None
else sorted(set(candidate_cell_ids))
)
ids = vtk.vtkIdList()
for cell_id in range(self.model_polydata.GetNumberOfCells()):
for cell_id in cell_ids:
if face_set is not None and int(face_arr.GetValue(cell_id)) not in face_set:
continue
if part_set is not None and int(part_arr.GetValue(cell_id)) not in part_set:
@@ -838,8 +875,11 @@ class WindowCoreMixin:
def _extract_visible_edge_polydata(self, edge_id: int):
if self.edge_polydata is None or self.edge_id_array is None:
return None
cell_ids = self.edge_cell_ids_by_edge.get(int(edge_id))
if cell_ids is None:
cell_ids = range(self.edge_polydata.GetNumberOfCells())
ids = vtk.vtkIdList()
for cell_id in range(self.edge_polydata.GetNumberOfCells()):
for cell_id in cell_ids:
if int(self.edge_id_array.GetValue(cell_id)) == edge_id:
ids.InsertNextId(cell_id)
return self._extract_cells_as_polydata(self.edge_polydata, ids)
@@ -868,8 +908,10 @@ class WindowCoreMixin:
self.model_face_id_array = self.model_polydata.GetCellData().GetArray("face_id")
self.model_part_id_array = self.model_polydata.GetCellData().GetArray("part_id")
self.model_solid_id_array = self.model_polydata.GetCellData().GetArray("solid_id")
self._rebuild_polydata_cell_indexes()
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputData(self.model_polydata)
_prepare_static_mapper(mapper)
self.model_actor = vtk.vtkActor()
self.model_actor.SetMapper(mapper)
self.model_actor.GetProperty().SetColor(0.68, 0.72, 0.73)
@@ -886,8 +928,10 @@ class WindowCoreMixin:
self.edge_polydata = edge_polydata
self.edge_id_array = self.edge_polydata.GetCellData().GetArray("edge_id")
self._rebuild_edge_polydata_cell_index()
edge_mapper = vtk.vtkPolyDataMapper()
edge_mapper.SetInputData(self.edge_polydata)
_prepare_static_mapper(edge_mapper)
self.edge_actor = vtk.vtkActor()
self.edge_actor.SetMapper(edge_mapper)
self.edge_actor.GetProperty().SetColor(0.08, 0.09, 0.1)
@@ -941,7 +985,7 @@ class WindowCoreMixin:
self.pending_hover_position = None
self.last_hover_pick_position = None
self.hover_timer.stop()
self._clear_hover(render=True)
self._clear_hover(render=False)
def on_left_button_release(self, _obj, _event) -> None:
if getattr(self, "skip_next_vtk_left_release", False):
@@ -999,10 +1043,12 @@ class WindowCoreMixin:
target = self._pick_selection_target(mode, x, y)
if target is None:
self._cancel_hover_tracking(render=False)
self._reset_selection(clear_highlight=True)
if hasattr(self, "render_window"):
self.render_window.Render()
self.statusBar().showMessage("已取消选择")
if self.selected_kind is not None:
self.statusBar().showMessage("未命中对象,已保持当前选择")
else:
self.statusBar().showMessage("未命中对象")
return
self._clear_hover(render=False)
@@ -1019,7 +1065,7 @@ class WindowCoreMixin:
self.pending_hover_position = None
self.last_hover_pick_position = None
self.hover_timer.stop()
self._clear_hover(render=True)
self._clear_hover(render=False)
def on_pointer_button_release(self, _obj, _event) -> None:
if not self._is_ui_thread():
@@ -1083,6 +1129,8 @@ class WindowCoreMixin:
if not self._is_ui_thread():
self._invoke_on_ui_thread(lambda x=int(x), y=int(y): self._queue_hover_position(x, y))
return
if getattr(self, "pointer_button_down", False) or getattr(self, "camera_interaction_active", False):
return
position = (int(x), int(y))
threshold = int(getattr(self, "hover_move_threshold_px", 0) or 0)
if threshold > 0:
@@ -1093,10 +1141,14 @@ class WindowCoreMixin:
if max(dx, dy) < threshold:
return
self.pending_hover_position = position
if not self.hover_timer.isActive():
self.hover_timer.start(self.hover_interval_ms)
if self.hover_timer.isActive():
self.hover_timer.stop()
self.hover_timer.start(self.hover_interval_ms)
def _update_hover_target(self) -> None:
if getattr(self, "pointer_button_down", False) or getattr(self, "camera_interaction_active", False):
self._clear_hover(render=False)
return
if (
self.operation_in_progress
or self.scan_in_progress
@@ -1109,9 +1161,21 @@ class WindowCoreMixin:
return
x, y = self.pending_hover_position
self.last_hover_pick_position = (x, y)
target = self._pick_selection_target(self._current_selection_mode(), x, y)
target = self._pick_hover_target(self._current_selection_mode(), x, y)
self._show_hover_target(target)
def _pick_hover_target(self, mode: str, x: int, y: int) -> dict[str, object] | None:
if self.model is None:
return None
if mode == "Edge":
edge_hit = self._pick_edge_cell(x, y)
return self._edge_target_from_cell_hit(edge_hit) if edge_hit is not None else None
face_hit = self._pick_face_cell(x, y)
if face_hit is None:
return None
return self._target_from_face_hit(face_hit, mode)
def _pick_selection_target(self, mode: str, x: int, y: int) -> dict[str, object] | None:
if self.model is None:
return None
@@ -1510,29 +1574,26 @@ class WindowCoreMixin:
self.selected_kind = "feature" if feature_mode else "face"
self.selected_face_id = face_id
self.selected_pick_position = pick_position
info = self.model.face_info(face_id)
info = self.model.quick_face_info(face_id)
if feature_mode:
info["feature_mode"] = "当前是几何候选判断,不等同于 CAD 历史特征"
highlight_face_ids = [face_id]
info = self._feature_info_for_selected_face(face_id, info)
info["kind"] = "feature"
info.setdefault("feature_mode", "当前是几何候选判断,不等同于 CAD 历史特征")
highlight_face_ids = _int_values(info.get("feature_highlight_face_ids")) or [face_id]
else:
highlight_face_ids = self.model.connected_same_domain_face_ids(face_id)
if len(highlight_face_ids) > 1:
info["same_domain_face_ids"] = tuple(highlight_face_ids)
info["same_domain_face_count"] = len(highlight_face_ids)
info["same_domain_note"] = "已高亮属于同一几何面且范围相接/重叠的连续Face区域。"
logical_id = self.model.face_region_logical_id(face_id)
input_info = self.model.feature_info(face_id) if info.get("surface") == "plane" else info
highlight_face_ids = [face_id]
logical_id = int(info.get("logical_face_id", face_id))
input_info = dict(info)
self._sync_cylindrical_edit_inputs(input_info)
self.selected_part_id = int(info["part_id"])
self.selected_solid_id = int(info["solid_id"]) if int(info["solid_id"]) >= 0 else None
self._highlight_faces(face_ids=highlight_face_ids or [face_id])
self._show_pick_marker(pick_position)
self._sync_id_picker("Feature" if feature_mode else "Face", face_id if feature_mode else logical_id)
self.set_info(self._with_pick_info(info, pick_position))
self.set_info(self._with_pick_info(input_info, pick_position))
self._update_action_states()
suffix = f",同域区域 {len(highlight_face_ids)} 个Face" if not feature_mode and len(highlight_face_ids) > 1 else ""
raw_note = f"(拓扑Face {face_id}" if not feature_mode and logical_id != face_id else ""
message = f"已选择逻辑面区域 {logical_id}{raw_note}{suffix}" if not feature_mode else f"已选择Face {face_id}"
message = f"已选择Face {logical_id}{raw_note}" if not feature_mode else f"已选择特征来源Face {face_id}"
self.statusBar().showMessage(self._selection_status(message, pick_position))
def select_feature(self, face_id: int, pick_position: tuple[float, float, float] | None = None) -> None:
@@ -1540,7 +1601,8 @@ class WindowCoreMixin:
return
if self._edit_busy("编辑计算中,暂时不能选择特征。"):
return
info = self.model.feature_info(face_id)
info = self._feature_info_for_selected_face(face_id, self.model.quick_face_info(face_id))
info["kind"] = "feature"
self._reset_selection(clear_highlight=False, clear_info=False)
self.selected_kind = "feature"
self.selected_face_id = face_id
@@ -1559,6 +1621,23 @@ class WindowCoreMixin:
def _sync_cylindrical_edit_inputs(self, info: dict[str, object]) -> None:
fillet_radius_suggestion: str | None = None
for target_attr in (
"hole_center_x_input",
"hole_center_y_input",
"hole_center_z_input",
"slot_center_x_input",
"slot_center_y_input",
"slot_center_z_input",
"boss_center_x_input",
"boss_center_y_input",
"boss_center_z_input",
"cone_reference_radius_input",
"sphere_radius_input",
"torus_radius_input",
"face_area_input",
):
if hasattr(self, target_attr):
getattr(self, target_attr).clear()
shell_thickness = _float_or_none(info.get("shell_thickness_estimate"))
if hasattr(self, "shell_thickness_input"):
if info.get("shell_region_status") == "candidate" and shell_thickness is not None and shell_thickness > 0:
@@ -1764,11 +1843,6 @@ class WindowCoreMixin:
return None
kind = str(target["kind"])
target_id = int(target["target_id"])
if kind in {"face", "feature"} and 0 <= target_id < len(self.model.faces):
try:
return (kind, self.model.face_region_logical_id(target_id))
except Exception:
return (kind, target_id)
return (kind, target_id)
def _show_hover_target(self, target: dict[str, object] | None) -> None:
@@ -1792,13 +1866,11 @@ class WindowCoreMixin:
face_ids = [index for index, solid_id in enumerate(self.model.face_solid_ids) if solid_id == target_id]
self._highlight_hover_faces(face_ids=face_ids)
elif kind == "feature":
face_ids = self.model.connected_same_domain_face_ids(target_id) or [target_id]
self._highlight_hover_faces(face_ids=face_ids)
self._highlight_hover_faces(face_ids=[target_id])
elif kind == "edge":
self._highlight_hover_edge(target_id)
else:
face_ids = self.model.connected_same_domain_face_ids(target_id)
self._highlight_hover_faces(face_ids=face_ids or [target_id])
self._highlight_hover_faces(face_ids=[target_id])
self.hover_signature = signature
self.render_window.Render()
@@ -2078,12 +2150,23 @@ class WindowCoreMixin:
return
self._show_edge_tube_preview(edge_id, distance, (1.0, 0.55, 0.08), "倒角预览")
def _show_edge_length_preview(self, edge_id: int, target_length: float, anchor_mode: str = "auto") -> None:
def _show_edge_length_preview(
self,
edge_id: int,
target_length: float,
anchor_mode: str = "auto",
strategy_mode: str = "auto",
) -> None:
if self.model is None:
return
self.clear_edit_preview(render=False)
try:
polydata = self.model.straight_edge_length_preview_polydata(edge_id, target_length, anchor_mode=anchor_mode)
polydata = self.model.straight_edge_length_preview_polydata(
edge_id,
target_length,
anchor_mode=anchor_mode,
strategy_mode=strategy_mode,
)
except Exception as exc:
self.statusBar().showMessage(f"Edge长度修改预览不可用:{exc}")
return
+1611 -375
View File
File diff suppressed because it is too large Load Diff