在三维几何建模软件中,视图区的交互操作是用户体验的核心环节。PyMeshGen作为一个基于Python的网格生成工具,在视图区交互方面实现了一套实用的解决方案。本文将深入解析其几何拾取、点拾取、几何创建和删除等核心功能的实现原理与技术细节。

PyMeshGen的几何拾取系统基于VTK(Visualization Toolkit)的拾取器实现,核心类是GeometryPickingHelper。该类封装了点、线、面、体等几何元素的鼠标拾取功能,提供了灵活的回调机制和状态管理。
classGeometryPickingHelper:def__init__(self, mesh_display, gui=None, on_pick=None, on_unpick=None, on_confirm=None, on_cancel=None, on_delete=None): self.mesh_display = mesh_display self.gui = gui self._on_pick = on_pick self._on_unpick = on_unpick self._on_confirm = on_confirm self._on_cancel = on_cancel self._on_delete = on_delete self._enabled = False系统使用了三种VTK拾取器,分别针对不同的拾取场景:
self._picker = vtk.vtkCellPicker()self._picker.SetTolerance(0.01)self._point_picker = vtk.vtkCellPicker()self._point_picker.SetTolerance(0.02)self._world_picker = vtk.vtkWorldPointPicker()self._area_picker = vtk.vtkRenderedAreaPicker()拾取系统通过VTK的观察者模式(Observer Pattern)实现事件监听。当拾取功能启用时,系统会注册三个关键事件:
defenable(self): interactor = self._get_interactor() self._observer_id = interactor.AddObserver("LeftButtonPressEvent", self._on_left_button_press) self._observer_right_id = interactor.AddObserver("RightButtonPressEvent", self._on_right_button_press) self._observer_key_id = interactor.AddObserver("KeyPressEvent", self._on_key_press) self._enabled = True左键按下事件是拾取的核心逻辑,系统会根据拾取模式执行不同的操作:
def_on_left_button_press(self, obj, event): interactor = self._get_interactor() renderer = self.mesh_display.renderer click_pos = interactor.GetEventPosition() picked = self._picker.Pick(click_pos[0], click_pos[1], 0, renderer)if picked: cell = self._picker.GetCell() actor = self._picker.GetActor() element_type, element_obj, element_index = \ self._identify_geometry_element(cell, actor)if element_type and element_obj: self._handle_pick(element_type, element_obj, element_index)拾取过程包括以下步骤:
右键点击用于取消上一次的拾取操作:
def_on_right_button_press(self, obj, event):if self._highlighted_actors:for actor, data in list(self._highlighted_actors.items()): self._unhighlight_actor(actor, data) self._highlighted_actors.clear()if self._on_unpick: element_type, element_obj, element_index = \ self._get_last_picked_element()if element_type: self._on_unpick(element_type, element_obj, element_index)键盘事件提供了快速确认(Enter)、取消(Esc)和删除元素(Delete)的途径:
def_on_key_press(self, obj, event): interactor = self._get_interactor() key = interactor.GetKeySym()if key in ("Return", "Enter"):if self._on_confirm: self._on_confirm()elif key in ("Escape", "Esc"):if self._on_cancel: self._on_cancel()elif key == "Delete":if self._on_delete: self._on_delete()点拾取是几何创建的基础,系统提供了独立的点拾取模式:
defstart_point_pick(self, on_pick=None, on_confirm=None, on_cancel=None, on_exit=None): interactor = self._get_interactor() self._point_pick_observer_id = interactor.AddObserver("LeftButtonPressEvent", self._on_point_pick_press) self._point_pick_right_id = interactor.AddObserver("RightButtonPressEvent", self._on_point_pick_right_press) self._point_pick_key_id = interactor.AddObserver("KeyPressEvent", self._on_point_pick_key_press) self._point_pick_move_id = interactor.AddObserver("MouseMoveEvent", self._on_point_pick_move) self._point_pick_enabled = True磁吸功能允许用户在拾取点时自动吸附到现有的几何点,提高了几何创建的精度。实现原理如下:
系统在启用磁吸时会预先缓存所有几何点的世界坐标:
def_update_geometry_points_cache(self): elements = self._get_all_geometry_elements() vertices = elements.get("vertices", [])from OCC.Core.BRep import BRep_Tool new_cache = []for vertex_index, vertex_obj in vertices:try: pnt = BRep_Tool.Pnt(vertex_obj) new_cache.append((pnt.X(), pnt.Y(), pnt.Z()))except Exception:continue self._geometry_points_cache = new_cache磁吸的核心在于屏幕坐标与世界坐标的转换:
def_world_to_display_coords(self, world_point, renderer): coord = vtk.vtkCoordinate() coord.SetCoordinateSystemToWorld() coord.SetValue(world_point[0], world_point[1], world_point[2]) display_point = coord.GetComputedDisplayValue(renderer)return (display_point[0], display_point[1])基于屏幕位置查找最近的几何点:
def_find_nearest_point_from_screen_pos(self, screen_pos, renderer, pixel_tolerance=None):if pixel_tolerance isNone: pixel_tolerance = self._snap_pixel_tolerance min_pixel_dist_sq = float('inf') nearest_point = None tolerance_sq = pixel_tolerance ** 2for cached_point in self._geometry_points_cache: cached_display = self._world_to_display_coords( cached_point, renderer)if cached_display isNone:continue pixel_dist_sq = (screen_pos[0] - cached_display[0]) ** 2 + \ (screen_pos[1] - cached_display[1]) ** 2if pixel_dist_sq < min_pixel_dist_sq: min_pixel_dist_sq = pixel_dist_sq nearest_point = cached_pointif min_pixel_dist_sq <= tolerance_sq:return nearest_pointreturnNone点拾取时会优先尝试磁吸:
def_on_point_pick_press(self, obj, event): click_pos = interactor.GetEventPosition() world_pos = self._pick_on_plane(click_pos, renderer) self._is_snapped = False final_pos = world_pos vertex_obj = None vertex_index = Noneif self._snap_enabled: nearest_point = self._find_nearest_point_from_screen_pos( click_pos, renderer)if nearest_point: final_pos = nearest_point self._is_snapped = True result = self._find_vertex_by_point(final_pos)if result: vertex_obj, vertex_index = result actor = self._show_picked_point_highlight( final_pos[0], final_pos[1], final_pos[2]) self._picked_points.append( (final_pos[0], final_pos[1], final_pos[2], vertex_obj)) self._on_point_pick(final_pos, vertex_obj)点拾取模式下,右键用于取消上一次拾取的点:
def_on_point_pick_right_press(self, obj, event):if self._picked_points: self._remove_last_picked_point() style = interactor.GetInteractorStyle()if style: style.OnRightButtonDown()GeometryCreateDialog类提供了完整的几何创建界面,支持多种几何类型:
classGeometryCreateDialog(QDialog):def__init__(self, parent=None): super().__init__(parent) self.gui = parent self.setWindowTitle("创建几何") self._current_pick_target = None self._picked_points_history = [] self._create_widgets() self._connect_signals()系统支持创建以下几何类型:
2D几何:
3D几何:
每个几何类型都需要输入坐标参数,系统提供了两种输入方式:
def_create_coord_input_with_pick(self): widget = QWidget() layout = QHBoxLayout(widget) line_edit = QLineEdit() line_edit.setPlaceholderText("x, y, z") pick_btn = QPushButton("拾取") pick_btn.setMaximumWidth(60) layout.addWidget(line_edit, 1) layout.addWidget(pick_btn, 0) widget.line_edit = line_edit widget.pick_btn = pick_btnreturn widget, pick_btn点击"拾取"按钮后,系统会启动点拾取模式:
def_start_single_pick(self, target_line_edit): self._current_pick_target = target_line_editif self.gui and hasattr(self.gui, "view_controller"): snap_enabled = self.snap_checkbox.isChecked() self.gui.view_controller.start_point_pick( on_pick=self._on_point_picked, on_cancel=self._on_point_pick_cancel, snap_enabled=snap_enabled )拾取完成后,坐标会自动填充到输入框:
def_on_point_picked(self, point_coords, vertex_obj):if self._current_pick_target: coord_str = f"{point_coords[0]:.6f}, {point_coords[1]:.6f}, {point_coords[2]:.6f}" self._current_pick_target.setText(coord_str) self._picked_points_history.append( (self._current_pick_target, point_coords))系统提供了磁吸功能的开关:
options_group = QGroupBox("拾取选项")options_layout = QHBoxLayout(options_group)self.snap_checkbox = QCheckBox("启用磁吸")self.snap_checkbox.setChecked(True)self.snap_checkbox.setToolTip("启用后,拾取点时会自动吸附到附近的几何点")options_layout.addWidget(self.snap_checkbox)对于曲线、折线和多边形等需要多个点的几何类型,系统提供了动态增加/减少点的功能:
def_add_point_input(self, mode):if mode == "curve": widgets_list = self.curve_point_widgets inputs_layout = self.curve_point_inputs_layout num_spin = self.curve_num_pointselif mode == "polyline": widgets_list = self.polyline_point_widgets inputs_layout = self.polyline_point_inputs_layout num_spin = self.polyline_num_pointselif mode == "polygon": widgets_list = self.polygon_point_widgets inputs_layout = self.polygon_point_inputs_layout num_spin = self.polygon_num_pointselse:return widget, btn = self._create_coord_input_with_pick() widgets_list.append((widget, btn)) inputs_layout.addWidget(widget) btn.clicked.connect(lambda checked=False, w=widget: self._start_single_pick(w.line_edit)) num_spin.setValue(num_spin.value() + 1)完整的几何创建流程如下:
def_create_geometry(self): mode = self._get_current_mode()if mode == "点": coords = self._parse_coords(self.p_coord_widget.line_edit.text())if coords: self._create_point(coords)elif mode == "直线": p1 = self._parse_coords(self.l1_coord_widget.line_edit.text()) p2 = self._parse_coords(self.l2_coord_widget.line_edit.text())if p1 and p2: self._create_line(p1, p2)# ... 其他几何类型的创建逻辑PyMeshGen的几何删除功能采用拾取模式的设计理念,工作流程如下:
这种设计符合现代CAD软件的交互习惯,用户可以在视图中直观地选择要删除的几何元素。
点击几何选项卡中的"删除几何"按钮,系统会启动删除拾取模式:
defopen_geometry_delete_dialog(self):"""进入几何删除拾取模式""" self._start_delete_geometry_mode()def_start_delete_geometry_mode(self):if self._delete_geometry_mode_active: self.update_status("删除几何: 已在拾取模式")returnifnot hasattr(self, 'view_controller'):returnif hasattr(self, 'model_tree_widget') and hasattr(self.model_tree_widget, 'tree'): self.model_tree_widget.tree.clearSelection() self._delete_geometry_mode_active = True self._geometry_delete_elements_cache = {} self.view_controller.start_geometry_pick( on_pick=self._on_delete_geometry_pick, on_unpick=self._on_delete_geometry_unpick, on_confirm=self._on_delete_geometry_confirm, on_cancel=self._on_delete_geometry_cancel, on_delete=self._delete_geometry_from_pick, ) hint = "删除几何: 左键拾取,右键取消,Enter键确认删除,Delete键删除已选元素,Esc退出" self.log_info(hint) self.update_status(hint)启动拾取模式时,系统会:
在模型树中右键点击几何节点,选择"删除几何"菜单项,同样会启动删除拾取模式。
进入删除拾取模式后,用户可以在视图中直接拾取要删除的几何元素。
左键点击几何元素时,系统会将该元素添加到待删除缓存中:
def_on_delete_geometry_pick(self, element_type, element_obj, element_index): key_map = {"vertex": "vertices","edge": "edges","face": "faces","body": "bodies", } key = key_map.get(element_type)if key isNone:return self._geometry_delete_elements_cache.setdefault(key, set()).add(element_obj)拾取的元素会被高亮显示,用户可以清楚地看到已选择的元素。
右键点击已拾取的元素可以取消选择:
def_on_delete_geometry_unpick(self, element_type, element_obj, element_index): key_map = {"vertex": "vertices","edge": "edges","face": "faces","body": "bodies", } key = key_map.get(element_type)if key isNone:returnif key in self._geometry_delete_elements_cache: self._geometry_delete_elements_cache[key].discard(element_obj)取消选择后,元素的高亮显示会被移除。
拾取完成后,用户可以通过快捷键执行删除操作。
def_on_delete_geometry_confirm(self): self._delete_geometry_from_pick()def_delete_geometry_from_pick(self):ifnot self._delete_geometry_mode_active: self._start_delete_geometry_mode() element_map = self._collect_picked_geometry_elements()ifnot element_map ornot any(element_map.values()): element_map = {key: list(values)for key, values in self._geometry_delete_elements_cache.items()}ifnot element_map ornot any(element_map.values()): self.update_status("删除几何: 未选中元素")returnFalse success = self.delete_geometry_elements(element_map)if success and hasattr(self, 'view_controller'): helper = getattr(self.view_controller, '_picking_helper', None)if helper isnotNoneand hasattr(helper, 'clear_selection'): helper.clear_selection() self._geometry_delete_elements_cache = {}return success删除操作包括以下步骤:
按Esc键可以退出删除拾取模式:
def_on_delete_geometry_cancel(self): self._stop_delete_geometry_mode()def_stop_delete_geometry_mode(self):ifnot self._delete_geometry_mode_active:return self._delete_geometry_mode_active = False self._geometry_delete_elements_cache = {}if hasattr(self, 'view_controller'): self.view_controller.stop_geometry_pick(restore_display_mode=True) self.update_status("删除几何: 已退出")退出时会:
系统在状态栏显示详细的操作提示:
删除几何: 左键拾取,右键取消,Enter键确认删除,Delete键删除已选元素,Esc退出快捷键说明:
PyMeshGen的删除拾取模式具有以下特点:
系统采用回调函数模式,实现了松耦合的交互逻辑:
defset_callbacks(self, on_pick=None, on_unpick=None, on_confirm=None, on_cancel=None, on_delete=None): self._on_pick = on_pick self._on_unpick = on_unpick self._on_confirm = on_confirm self._on_cancel = on_cancel self._on_delete = on_delete磁吸功能通过以下技术实现:
系统完善的状态管理包括:
PyMeshGen的视图区交互系统通过VTK拾取器、PyQt5界面和OpenCASCADE几何内核的有机结合,实现了一套完整的几何拾取、创建和删除功能。系统的模块化设计、灵活的回调机制和优秀的用户体验,为高阶网格生成提供了一定的交互基础。
本文详细介绍了几何元素拾取、点拾取与磁吸、几何创建和删除等核心功能的实现原理,希望能为相关领域的开发者提供参考和借鉴。

👇点击左下方“阅读原文”访问项目!
全文结束,感谢观看,创作不易。
😁😁
欢迎关注、留言、点赞、分享、推荐!
👇👇
【往期回顾】
我用DeepSeek-V3.1做了一下PDE编程求解,来看看结果怎么样