Source code for picogl.ui.backend.qt.base

"""
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"""
[docs] rotation_x = None
[docs] rotation_y = None
[docs] pan_x = None
[docs] pan_y = None
@dataclass
[docs] class CameraParameters: """camera parameters"""
[docs] rotation_x_axis = None
[docs] rotation_y_axis = None
[docs] rotation_z_axis = None
[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.zoom_value = 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