Skip to content

Commit ab25c59

Browse files
authored
Initial implementation of turntable orbit method (#525)
* Initial implementation of turntable orbit method
1 parent e720033 commit ab25c59

File tree

4 files changed

+138
-8
lines changed

4 files changed

+138
-8
lines changed

cq_editor/preferences.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ def add(self, name, component):
9191
# Fill the light/dark theme in the general settings
9292
elif child.name() == "Light/Dark Theme":
9393
child.setLimits(["Light", "Dark"])
94+
# Fill the orbit method
95+
elif child.name() == "Orbit Method":
96+
child.setLimits(
97+
[
98+
"Turntable",
99+
"Trackball",
100+
]
101+
)
94102

95103
@pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
96104
def handleSelection(self, item, *args):

cq_editor/widgets/occt_widget.py

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33

44
from PyQt5.QtWidgets import QWidget, QApplication
5-
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QEvent
5+
from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QPoint
66

77
import OCP
88

99
from OCP.Aspect import Aspect_DisplayConnection, Aspect_TypeOfTriedronPosition
1010
from OCP.OpenGl import OpenGl_GraphicDriver
1111
from OCP.V3d import V3d_Viewer
12+
from OCP.gp import gp_Trsf, gp_Ax1, gp_Dir
1213
from OCP.AIS import AIS_InteractiveContext, AIS_DisplayMode
1314
from OCP.Quantity import Quantity_Color
1415

@@ -30,6 +31,15 @@ def __init__(self, parent=None):
3031

3132
self._initialized = False
3233
self._needs_update = False
34+
self._previous_pos = QPoint(
35+
0, 0 # Keeps track of where the previous mouse position
36+
)
37+
self._rotate_step = (
38+
0.008 # Controls the speed of rotation with the turntable orbit method
39+
)
40+
41+
# Orbit method settings
42+
self._orbit_method = "Turntable"
3343

3444
# OCCT secific things
3545
self.display_connection = Aspect_DisplayConnection()
@@ -64,6 +74,20 @@ def prepare_display(self):
6474
ctx.SetDisplayMode(AIS_DisplayMode.AIS_Shaded, True)
6575
ctx.DefaultDrawer().SetFaceBoundaryDraw(True)
6676

77+
def set_orbit_method(self, method):
78+
"""
79+
Set the orbit method for the OCCT view.
80+
"""
81+
82+
# Keep track of which orbit method is used
83+
if method == "Turntable":
84+
self._orbit_method = "Turntable"
85+
self.view.SetUp(0, 0, 1)
86+
elif method == "Trackball":
87+
self._orbit_method = "Trackball"
88+
else:
89+
raise ValueError(f"Unknown orbit method: {method}")
90+
6791
def wheelEvent(self, event):
6892

6993
delta = event.angleDelta().y()
@@ -80,31 +104,51 @@ def mousePressEvent(self, event):
80104
self.pending_select = True
81105
self.left_press = pos
82106

83-
self.view.StartRotation(pos.x(), pos.y())
107+
# We only start the rotation if the orbit method is set to Trackball
108+
if self._orbit_method == "Trackball":
109+
self.view.StartRotation(pos.x(), pos.y())
84110
elif event.button() == Qt.RightButton:
85111
self.view.StartZoomAtPoint(pos.x(), pos.y())
86112

87-
self.old_pos = pos
113+
self._previous_pos = pos
88114

89115
def mouseMoveEvent(self, event):
90116

91117
pos = event.pos()
92118
x, y = pos.x(), pos.y()
93119

120+
# Check for mouse drag rotation
94121
if event.buttons() == Qt.LeftButton:
95-
self.view.Rotation(x, y)
122+
# Set the rotation differently based on the orbit method
123+
if self._orbit_method == "Trackball":
124+
self.view.Rotation(x, y)
125+
elif self._orbit_method == "Turntable":
126+
# Control the turntable rotation manually
127+
delta_x, delta_y = (
128+
x - self._previous_pos.x(),
129+
y - self._previous_pos.y(),
130+
)
131+
cam = self.view.Camera()
132+
z_rotation = gp_Trsf()
133+
z_rotation.SetRotation(
134+
gp_Ax1(cam.Center(), gp_Dir(0, 0, 1)), -delta_x * self._rotate_step
135+
)
136+
cam.Transform(z_rotation)
137+
self.view.Rotate(0, -delta_y * self._rotate_step, 0)
96138

97139
# If the user moves the mouse at all, the selection will not happen
98140
if abs(x - self.left_press.x()) > 2 or abs(y - self.left_press.y()) > 2:
99141
self.pending_select = False
100142

101143
elif event.buttons() == Qt.MiddleButton:
102-
self.view.Pan(x - self.old_pos.x(), self.old_pos.y() - y, theToStart=True)
144+
self.view.Pan(
145+
x - self._previous_pos.x(), self._previous_pos.y() - y, theToStart=True
146+
)
103147

104148
elif event.buttons() == Qt.RightButton:
105-
self.view.ZoomAtPoint(self.old_pos.x(), y, x, self.old_pos.y())
149+
self.view.ZoomAtPoint(self._previous_pos.x(), y, x, self._previous_pos.y())
106150

107-
self.old_pos = pos
151+
self._previous_pos = pos
108152

109153
def mouseReleaseEvent(self, event):
110154

cq_editor/widgets/viewer.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ class OCCViewer(QWidget, ComponentMixin):
8585
"OverUnder",
8686
],
8787
},
88+
{
89+
"name": "Orbit Method",
90+
"type": "list",
91+
"value": "Turntable",
92+
"values": [
93+
"Turntable",
94+
"Trackball",
95+
],
96+
},
8897
],
8998
)
9099
IMAGE_EXTENSIONS = "png"
@@ -136,6 +145,12 @@ def updatePreferences(self, *args):
136145
color2 = color1
137146
self.canvas.view.SetBgGradientColors(color1, color2, theToUpdate=True)
138147

148+
# Set the orbit method
149+
orbit_method = self.preferences["Orbit Method"]
150+
if not orbit_method:
151+
orbit_method = "Trackball"
152+
self.canvas.set_orbit_method(orbit_method)
153+
139154
self.canvas.update()
140155

141156
ctx = self.canvas.context

tests/test_app.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
import pytestqt
1111
import cadquery as cq
1212

13-
from PyQt5.QtCore import Qt, QSettings
13+
from PyQt5.QtCore import Qt, QSettings, QPoint, QEvent
1414
from PyQt5.QtWidgets import QFileDialog, QMessageBox
15+
from PyQt5.QtGui import QMouseEvent
1516

1617
from cq_editor.__main__ import MainWindow
1718
from cq_editor.widgets.editor import Editor
@@ -1846,3 +1847,65 @@ def test_autocomplete_keystrokes(main):
18461847
qtbot.wait(250)
18471848
# Check that the completion list is still visible
18481849
assert editor.completion_list.isVisible()
1850+
1851+
1852+
def test_viewer_orbit_methods(main):
1853+
"""
1854+
Tests that mouse movements in the viewer work as expected.
1855+
"""
1856+
1857+
qtbot, win = main
1858+
1859+
viewer = win.components["viewer"]
1860+
1861+
# Make sure the editor is focused
1862+
viewer.setFocus()
1863+
qtbot.waitExposed(viewer)
1864+
1865+
# Simulate a drag to rotate
1866+
qtbot.mousePress(viewer, Qt.LeftButton)
1867+
qtbot.mouseMove(viewer, QPoint(100, 100))
1868+
qtbot.mouseMove(viewer, QPoint(300, 300))
1869+
qtbot.mouseRelease(viewer, Qt.LeftButton)
1870+
1871+
# Simulate a drag to pan
1872+
qtbot.mousePress(viewer, Qt.MiddleButton)
1873+
event = QMouseEvent(
1874+
QEvent.MouseMove,
1875+
QPoint(100, 100),
1876+
Qt.RightButton,
1877+
Qt.RightButton,
1878+
Qt.NoModifier,
1879+
)
1880+
viewer.mouseMoveEvent(event)
1881+
event = QMouseEvent(
1882+
QEvent.MouseMove,
1883+
QPoint(300, 300),
1884+
Qt.RightButton,
1885+
Qt.RightButton,
1886+
Qt.NoModifier,
1887+
)
1888+
viewer.mouseMoveEvent(event)
1889+
qtbot.mouseRelease(viewer, Qt.MiddleButton)
1890+
1891+
# Simulate drag to zoom
1892+
qtbot.mousePress(viewer, Qt.RightButton)
1893+
event = QMouseEvent(
1894+
QEvent.MouseMove,
1895+
QPoint(100, 100),
1896+
Qt.RightButton,
1897+
Qt.RightButton,
1898+
Qt.NoModifier,
1899+
)
1900+
viewer.mouseMoveEvent(event)
1901+
event = QMouseEvent(
1902+
QEvent.MouseMove,
1903+
QPoint(300, 300),
1904+
Qt.RightButton,
1905+
Qt.RightButton,
1906+
Qt.NoModifier,
1907+
)
1908+
viewer.mouseMoveEvent(event)
1909+
qtbot.mouseRelease(viewer, Qt.RightButton)
1910+
1911+
assert True

0 commit comments

Comments
 (0)