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 decologr import Decologr as log
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 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.mode import GLMode
from picogl.utils.gl_init import execute_gl_tasks, initialize_gl_list
from PySide6.QtGui import QMouseEvent, QOpenGLFunctions, Qt, QWheelEvent
from PySide6.QtOpenGLWidgets import QOpenGLWidget
from PySide6.QtWidgets import QWidget


@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_mode: GLMode = GLMode.LEGACY): """ constructor :param parent: QWidget """ super().__init__(parent)
[docs] self.aspect_ratio = None
[docs] self.gl_mode = gl_mode
[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