diff --git a/cq_editor/preferences.py b/cq_editor/preferences.py index 65f68ef5..0f290ca7 100644 --- a/cq_editor/preferences.py +++ b/cq_editor/preferences.py @@ -91,6 +91,14 @@ def add(self, name, component): # Fill the light/dark theme in the general settings elif child.name() == "Light/Dark Theme": child.setLimits(["Light", "Dark"]) + # Fill the orbit method + elif child.name() == "Orbit Method": + child.setLimits( + [ + "Turntable", + "Trackball", + ] + ) @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem) def handleSelection(self, item, *args): diff --git a/cq_editor/widgets/occt_widget.py b/cq_editor/widgets/occt_widget.py index d16d9919..58aac382 100755 --- a/cq_editor/widgets/occt_widget.py +++ b/cq_editor/widgets/occt_widget.py @@ -2,13 +2,14 @@ from PyQt5.QtWidgets import QWidget, QApplication -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QEvent +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QPoint import OCP from OCP.Aspect import Aspect_DisplayConnection, Aspect_TypeOfTriedronPosition from OCP.OpenGl import OpenGl_GraphicDriver from OCP.V3d import V3d_Viewer +from OCP.gp import gp_Trsf, gp_Ax1, gp_Dir from OCP.AIS import AIS_InteractiveContext, AIS_DisplayMode from OCP.Quantity import Quantity_Color @@ -30,6 +31,15 @@ def __init__(self, parent=None): self._initialized = False self._needs_update = False + self._previous_pos = QPoint( + 0, 0 # Keeps track of where the previous mouse position + ) + self._rotate_step = ( + 0.008 # Controls the speed of rotation with the turntable orbit method + ) + + # Orbit method settings + self._orbit_method = "Turntable" # OCCT secific things self.display_connection = Aspect_DisplayConnection() @@ -64,6 +74,20 @@ def prepare_display(self): ctx.SetDisplayMode(AIS_DisplayMode.AIS_Shaded, True) ctx.DefaultDrawer().SetFaceBoundaryDraw(True) + def set_orbit_method(self, method): + """ + Set the orbit method for the OCCT view. + """ + + # Keep track of which orbit method is used + if method == "Turntable": + self._orbit_method = "Turntable" + self.view.SetUp(0, 0, 1) + elif method == "Trackball": + self._orbit_method = "Trackball" + else: + raise ValueError(f"Unknown orbit method: {method}") + def wheelEvent(self, event): delta = event.angleDelta().y() @@ -80,31 +104,51 @@ def mousePressEvent(self, event): self.pending_select = True self.left_press = pos - self.view.StartRotation(pos.x(), pos.y()) + # We only start the rotation if the orbit method is set to Trackball + if self._orbit_method == "Trackball": + self.view.StartRotation(pos.x(), pos.y()) elif event.button() == Qt.RightButton: self.view.StartZoomAtPoint(pos.x(), pos.y()) - self.old_pos = pos + self._previous_pos = pos def mouseMoveEvent(self, event): pos = event.pos() x, y = pos.x(), pos.y() + # Check for mouse drag rotation if event.buttons() == Qt.LeftButton: - self.view.Rotation(x, y) + # Set the rotation differently based on the orbit method + if self._orbit_method == "Trackball": + self.view.Rotation(x, y) + elif self._orbit_method == "Turntable": + # Control the turntable rotation manually + delta_x, delta_y = ( + x - self._previous_pos.x(), + y - self._previous_pos.y(), + ) + cam = self.view.Camera() + z_rotation = gp_Trsf() + z_rotation.SetRotation( + gp_Ax1(cam.Center(), gp_Dir(0, 0, 1)), -delta_x * self._rotate_step + ) + cam.Transform(z_rotation) + self.view.Rotate(0, -delta_y * self._rotate_step, 0) # If the user moves the mouse at all, the selection will not happen if abs(x - self.left_press.x()) > 2 or abs(y - self.left_press.y()) > 2: self.pending_select = False elif event.buttons() == Qt.MiddleButton: - self.view.Pan(x - self.old_pos.x(), self.old_pos.y() - y, theToStart=True) + self.view.Pan( + x - self._previous_pos.x(), self._previous_pos.y() - y, theToStart=True + ) elif event.buttons() == Qt.RightButton: - self.view.ZoomAtPoint(self.old_pos.x(), y, x, self.old_pos.y()) + self.view.ZoomAtPoint(self._previous_pos.x(), y, x, self._previous_pos.y()) - self.old_pos = pos + self._previous_pos = pos def mouseReleaseEvent(self, event): diff --git a/cq_editor/widgets/viewer.py b/cq_editor/widgets/viewer.py index 0750d626..c2ada383 100644 --- a/cq_editor/widgets/viewer.py +++ b/cq_editor/widgets/viewer.py @@ -85,6 +85,15 @@ class OCCViewer(QWidget, ComponentMixin): "OverUnder", ], }, + { + "name": "Orbit Method", + "type": "list", + "value": "Turntable", + "values": [ + "Turntable", + "Trackball", + ], + }, ], ) IMAGE_EXTENSIONS = "png" @@ -136,6 +145,12 @@ def updatePreferences(self, *args): color2 = color1 self.canvas.view.SetBgGradientColors(color1, color2, theToUpdate=True) + # Set the orbit method + orbit_method = self.preferences["Orbit Method"] + if not orbit_method: + orbit_method = "Trackball" + self.canvas.set_orbit_method(orbit_method) + self.canvas.update() ctx = self.canvas.context diff --git a/tests/test_app.py b/tests/test_app.py index b9bf57c2..e8d3e78d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -10,8 +10,9 @@ import pytestqt import cadquery as cq -from PyQt5.QtCore import Qt, QSettings +from PyQt5.QtCore import Qt, QSettings, QPoint, QEvent from PyQt5.QtWidgets import QFileDialog, QMessageBox +from PyQt5.QtGui import QMouseEvent from cq_editor.__main__ import MainWindow from cq_editor.widgets.editor import Editor @@ -1846,3 +1847,65 @@ def test_autocomplete_keystrokes(main): qtbot.wait(250) # Check that the completion list is still visible assert editor.completion_list.isVisible() + + +def test_viewer_orbit_methods(main): + """ + Tests that mouse movements in the viewer work as expected. + """ + + qtbot, win = main + + viewer = win.components["viewer"] + + # Make sure the editor is focused + viewer.setFocus() + qtbot.waitExposed(viewer) + + # Simulate a drag to rotate + qtbot.mousePress(viewer, Qt.LeftButton) + qtbot.mouseMove(viewer, QPoint(100, 100)) + qtbot.mouseMove(viewer, QPoint(300, 300)) + qtbot.mouseRelease(viewer, Qt.LeftButton) + + # Simulate a drag to pan + qtbot.mousePress(viewer, Qt.MiddleButton) + event = QMouseEvent( + QEvent.MouseMove, + QPoint(100, 100), + Qt.RightButton, + Qt.RightButton, + Qt.NoModifier, + ) + viewer.mouseMoveEvent(event) + event = QMouseEvent( + QEvent.MouseMove, + QPoint(300, 300), + Qt.RightButton, + Qt.RightButton, + Qt.NoModifier, + ) + viewer.mouseMoveEvent(event) + qtbot.mouseRelease(viewer, Qt.MiddleButton) + + # Simulate drag to zoom + qtbot.mousePress(viewer, Qt.RightButton) + event = QMouseEvent( + QEvent.MouseMove, + QPoint(100, 100), + Qt.RightButton, + Qt.RightButton, + Qt.NoModifier, + ) + viewer.mouseMoveEvent(event) + event = QMouseEvent( + QEvent.MouseMove, + QPoint(300, 300), + Qt.RightButton, + Qt.RightButton, + Qt.NoModifier, + ) + viewer.mouseMoveEvent(event) + qtbot.mouseRelease(viewer, Qt.RightButton) + + assert True