From aa623cc11a4605b524aa0bb5bb8829c0e31bcb2d Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 17 Jun 2025 21:57:50 -0400 Subject: [PATCH 1/4] Initial implementation of turntable orbit method --- cq_editor/preferences.py | 8 +++++ cq_editor/widgets/occt_widget.py | 58 ++++++++++++++++++++++++++++---- cq_editor/widgets/viewer.py | 15 +++++++++ 3 files changed, 74 insertions(+), 7 deletions(-) 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 From ed6615e04ad4e442f4c8743bac61d3f68fcd4303 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Mon, 23 Jun 2025 16:13:11 -0400 Subject: [PATCH 2/4] Increase test coverage a bit --- tests/test_app.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/test_app.py b/tests/test_app.py index b9bf57c2..b70ef42f 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -10,7 +10,7 @@ import pytestqt import cadquery as cq -from PyQt5.QtCore import Qt, QSettings +from PyQt5.QtCore import Qt, QSettings, QPoint from PyQt5.QtWidgets import QFileDialog, QMessageBox from cq_editor.__main__ import MainWindow @@ -1846,3 +1846,37 @@ 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) + qtbot.mouseMove(viewer, QPoint(100, 100)) + qtbot.mouseMove(viewer, QPoint(300, 300)) + qtbot.mouseRelease(viewer, Qt.MiddleButton) + + # Simulate drag to zoom + qtbot.mousePress(viewer, Qt.RightButton) + qtbot.mouseMove(viewer, QPoint(100, 100)) + qtbot.mouseMove(viewer, QPoint(300, 300)) + qtbot.mouseRelease(viewer, Qt.RightButton) + + assert True From c67685b5f7f3de235c909729d416d13e60b81eaf Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 26 Jun 2025 13:28:27 -0400 Subject: [PATCH 3/4] Trying to get CodeCov to see house move events --- tests/test_app.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index b70ef42f..0d43567a 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, QPoint +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 @@ -1869,14 +1870,18 @@ def test_viewer_orbit_methods(main): # Simulate a drag to pan qtbot.mousePress(viewer, Qt.MiddleButton) - qtbot.mouseMove(viewer, QPoint(100, 100)) - qtbot.mouseMove(viewer, QPoint(300, 300)) + 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) - qtbot.mouseMove(viewer, QPoint(100, 100)) - qtbot.mouseMove(viewer, QPoint(300, 300)) + 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 From 617b53162877c950e1c6b6f1ef2335d93d71fb0b Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Thu, 26 Jun 2025 13:37:19 -0400 Subject: [PATCH 4/4] Lint fix --- tests/test_app.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/test_app.py b/tests/test_app.py index 0d43567a..e8d3e78d 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1870,17 +1870,41 @@ def test_viewer_orbit_methods(main): # Simulate a drag to pan qtbot.mousePress(viewer, Qt.MiddleButton) - event = QMouseEvent(QEvent.MouseMove, QPoint(100, 100), Qt.RightButton, Qt.RightButton, Qt.NoModifier) + 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) + 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) + 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) + event = QMouseEvent( + QEvent.MouseMove, + QPoint(300, 300), + Qt.RightButton, + Qt.RightButton, + Qt.NoModifier, + ) viewer.mouseMoveEvent(event) qtbot.mouseRelease(viewer, Qt.RightButton)