"""
GLBase Qt Widget
"""
from dataclasses import dataclass, field
from typing import Optional
import numpy as np
from OpenGL.GL import glGetIntegerv
from OpenGL.raw.GL.ARB.viewport_array import GL_VIEWPORT
from OpenGL.raw.GL.VERSION.GL_1_0 import (
GL_MODELVIEW,
glLoadIdentity,
glMatrixMode,
glViewport,
)
from OpenGL.raw.GLU import gluPerspective
from PySide6.QtGui import QMouseEvent, QOpenGLFunctions, Qt, QWheelEvent
from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtWidgets import QWidget
from picogl.backend.legacy.core.camera.lighting import set_background_color
from picogl.backend.legacy.core.camera.matrices.setup import setup_matrices
from picogl.backend.legacy.core.camera.setup import calculate_aspect
from picogl.error import check_errors
from picogl.frame import prepare_viewport
from picogl.logger import Logger as log
from picogl.utils.gl_init import execute_gl_tasks, initialize_gl_list
@dataclass
[docs]
class MvpParameters:
"""MVP Parameters"""
@dataclass
[docs]
class CameraParameters:
"""camera parameters"""
[docs]
translation_x_axis = None
[docs]
translation_y_axis = None
[docs]
translation_zoom = None
[docs]
rotation: np.ndarray = field(
default_factory=lambda: np.array([0.0, 0.0, 0.0], dtype=np.float32)
) # x, y, z
[docs]
translation: np.ndarray = field(
default_factory=lambda: np.array([0.0, 0.0], dtype=np.float32)
) # x_pan, y_pan
# zoom: CameraParameterZoom = field(default_factory=CameraParameterZoom)
[docs]
class GLBase(QOpenGLWidget, QOpenGLFunctions):
"""
OpenGL Qt Widget
"""
def __init__(self, parent: QWidget = None, gl_use_legacy: bool = True):
"""
constructor
:param parent: QWidget
"""
super().__init__(parent)
[docs]
self.aspect_ratio = None
[docs]
self.gl_use_legacy = gl_use_legacy
[docs]
self.last_mouse_pos = None
[docs]
self.mvp_parameters = MvpParameters()
[docs]
self.camera_parameters = CameraParameters()
[docs]
def initializeGL(self):
"""
initializeGL
Initializes the OpenGL rendering context for this widget.
This includes:
- Enabling depth testing and multisampling
- Configuring blending for transparency
- Initializing lighting and material properties
- Setting the viewport to match widget size
- Clearing any legacy buffer state
Called automatically by Qt when the GL context is first created.
"""
# Viewport setup
glViewport(0, 0, self.width(), self.height())
execute_gl_tasks(initialize_gl_list)
[docs]
def resizeGL(self, w: int, h: int) -> None:
"""
resizeGL(w, h)
Handles resizing the OpenGL viewport and updates the projection matrix.
:param w: int - New width of the OpenGL widget
:param h: int - New height of the OpenGL widget
"""
if not self.context().isValid():
log.warning("OpenGL context invalid during resize. Skipping resizeGL.")
return
# Prevent division by zero
h = max(h, 1)
# Update viewport
glViewport(0, 0, w, h)
self.aspect_ratio = calculate_aspect(h, w)
gluPerspective(45.0, self.aspect_ratio, 1.0, 1000.0)
setup_matrices(self.aspect_ratio)
# Return to modelview matrix
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
# Update camera matrix using legacy pipeline
"""update_camera_matrix(
translation=self.camera_parameters.translation,
rotation=self.camera_parameters.rotation,
zoom_value=self.camera_parameters.zoom.value,
)"""
log.message(
f"✅ Resized OpenGL viewport to {w}x{h}, aspect {self.aspect_ratio:.2f}"
)
[docs]
def update_mvp(self) -> None:
"""Update model-view-projection matrix."""
[docs]
def paintGL(self):
"""
paintGL
:return: None
OpenGL rendering entry point. Calls the appropriate rendering method based g.
Modern OpenGL rendering entry point.
"""
check_errors()
width, height = self.width(), self.height()
prepare_viewport(width, height)
set_background_color(show_white_background=False) # Then set visuals
check_errors()
[docs]
def mousePressEvent(self, event: QMouseEvent) -> None:
"""
mousePressEvent
:param event: QMouseEvent
Handle mouse press events, including atom picking and coordinate un-projection.
"""
# log.message("Mouse press")
self.last_mouse_pos = event.position()
if event.button() != Qt.LeftButton:
return
x, y = event.x(), event.y()
log.message(f"Clicked position: x={x}, y={y}")
[docs]
def mouseMoveEvent(self, event: QMouseEvent) -> None:
"""
mouseMoveEvent
:param event: QMouseEvent
Handle mouse movement for X/Y axis rotation.
"""
if self.last_mouse_pos is None:
return
delta = event.position() - self.last_mouse_pos
buttons = event.buttons()
if buttons & Qt.LeftButton:
self.mvp_parameters.rotation_x += delta.x() * 0.5
self.mvp_parameters.rotation_y += delta.y() * 0.5
elif buttons & Qt.RightButton:
self.mvp_parameters.pan_x += delta.x() * 0.01
self.mvp_parameters.pan_y -= delta.y() * 0.01
dx = event.position().x() - self.last_mouse_pos.x()
dy = event.position().y() - self.last_mouse_pos.y()
self._apply_camera_rotation(dx, dy)
self.last_mouse_pos = event.position()
self.update_mvp()
self.update()
self._emit_rotation_feedback()
[docs]
def wheelEvent(self, event: QWheelEvent) -> None:
"""
wheelEvent
:param event: QWheelEvent
:return: None
Sets zoom level
"""
delta = event.angleDelta().y()
step = 5 # You can adjust sensitivity
try:
new_val = (
self.zoom_value - step if delta > 0 else self.zoom_value + step
) # Negative zoom increases
log.message(f"zoom level: {new_val}", silent=True)
except Exception:
pass
[docs]
def mouseReleaseEvent(self, event: QMouseEvent) -> None:
"""
mouseReleaseEvent
:param event: QMouseEvent
:return: None
"""
log.parameter("event", event)
self.last_mouse_pos = None
[docs]
def _compute_clicked_position(
self, x: int, y: int, z: int, viewport: np.ndarray
) -> Optional[np.ndarray]:
"""
_compute_clicked_position
:param x: int
:param y: int
:param z: int
:param viewport: np.ndarray
:return: np.ndarray or None
"""
raise NotImplementedError("Should be implemented in subclass")
[docs]
def _get_viewport(self) -> np.ndarray:
"""
_get_viewport
:return: np.ndarray: Array containing viewport dimensions.
Retrieve the current OpenGL viewport dimensions.
"""
viewport = np.zeros(4, dtype=np.int32)
glGetIntegerv(GL_VIEWPORT, viewport)
return viewport
[docs]
def _apply_camera_rotation(self, dx: float, dy: float) -> None:
"""
_apply_camera_rotation
:param dx: float
:param dy: float
:return: None
Apply delta rotation based on mouse movement
"""
self.camera_parameters.rotation_x_axis += dy * 0.5
self.camera_parameters.rotation_y_axis += dx * 0.5
[docs]
def _emit_rotation_feedback(self):
pass