"""As fórmulas de mapeamento SRU <-> SRD, escritas à mão. Aula 03 — Visualização 2D: mapeamento. Exemplo em Python com PySide6 e PyOpenGL (pipeline fixed-function). Execute com: python mapeamento2d.py Clique em qualquer ponto do canvas: a barra de status mostra a mesma posição nos três sistemas de coordenadas envolvidos — o pixel entregue pelo Qt, o pixel na convenção do OpenGL e o ponto correspondente no mundo (SRU). O OpenGL faz essa conversão sozinho, dentro da placa de vídeo, a partir do par ``gluOrtho2D`` + ``glViewport``. As funções ``sru_para_srd`` e ``srd_para_sru`` abaixo refazem a conta explicitamente, para que ela deixe de ser mágica: são exatamente as fórmulas do material da aula. Ideias para experimentar: mude ``MUNDO`` e observe as coordenadas do mesmo pixel mudarem; redimensione a janela e clique no mesmo lugar; desligue a correção de aspecto em ``resizeGL`` e veja a conta continuar correta. """ from __future__ import annotations import math import sys from PySide6.QtCore import Qt from PySide6.QtGui import QSurfaceFormat from PySide6.QtWidgets import QApplication, QMainWindow from PySide6.QtOpenGLWidgets import QOpenGLWidget from OpenGL.GL import * from OpenGL.GLU import gluOrtho2D MUNDO = 50.0 # semilargura da window, em unidades do mundo PASSO_GRADE = 10.0 # --------------------------------------------------------------------------- # As fórmulas do mapeamento # --------------------------------------------------------------------------- # Convenção usada aqui: o viewport é dado em coordenadas de TELA, com a origem # no canto superior esquerdo e Y crescendo para BAIXO — é a convenção do # dispositivo, e é dela que vem a inversão do eixo Y na fórmula do Y. def sru_para_srd(xu: float, yu: float, window: tuple[float, float, float, float], viewport: tuple[float, float, float, float], ) -> tuple[float, float]: """Do mundo para o dispositivo. ``window`` = (xu_min, xu_max, yu_min, yu_max), no SRU. ``viewport`` = (xd_min, xd_max, yd_min, yd_max), em pixels, com yd_min no topo da área de desenho. """ xu_min, xu_max, yu_min, yu_max = window xd_min, xd_max, yd_min, yd_max = viewport xd = (xd_max - xd_min) / (xu_max - xu_min) * (xu - xu_min) + xd_min # Repare no numerador invertido: (yd_min - yd_max) em vez de # (yd_max - yd_min). É o que traduz "Y cresce para cima no mundo e para # baixo na tela". Trocar essa ordem é o erro clássico do espelhamento # vertical. yd = (yd_min - yd_max) / (yu_max - yu_min) * (yu - yu_min) + yd_max return xd, yd def srd_para_sru(xd: float, yd: float, window: tuple[float, float, float, float], viewport: tuple[float, float, float, float], ) -> tuple[float, float]: """Do dispositivo para o mundo — a inversa da função acima.""" xu_min, xu_max, yu_min, yu_max = window xd_min, xd_max, yd_min, yd_max = viewport xu = (xu_max - xu_min) / (xd_max - xd_min) * (xd - xd_min) + xu_min yu = (yu_max - yu_min) / (yd_min - yd_max) * (yd - yd_max) + yu_min return xu, yu class Canvas(QOpenGLWidget): """Grade do mundo, eixos e a marca do último clique.""" def __init__(self, parent=None): super().__init__(parent) self.window_sru = (-MUNDO, MUNDO, -MUNDO, MUNDO) self.marca: tuple[float, float] | None = None self.aoClicar = None # callback preenchido pela janela self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) # -- ciclo de vida ----------------------------------------------------- def initializeGL(self) -> None: glClearColor(0.10, 0.11, 0.13, 1.0) def resizeGL(self, w: int, h: int) -> None: w, h = max(1, w), max(1, h) glViewport(0, 0, w, h) # A window é guardada em um atributo porque as fórmulas acima # precisam dela: o OpenGL sabe qual é, mas não a devolve de forma # conveniente. if w <= h: self.window_sru = (-MUNDO, MUNDO, -MUNDO * h / w, MUNDO * h / w) else: self.window_sru = (-MUNDO * w / h, MUNDO * w / h, -MUNDO, MUNDO) glMatrixMode(GL_PROJECTION) glLoadIdentity() gluOrtho2D(*self.window_sru) glMatrixMode(GL_MODELVIEW) glLoadIdentity() def paintGL(self) -> None: glClear(GL_COLOR_BUFFER_BIT) xu_min, xu_max, yu_min, yu_max = self.window_sru # Grade do mundo, de PASSO_GRADE em PASSO_GRADE. glColor3f(0.20, 0.22, 0.27) glLineWidth(1.0) glBegin(GL_LINES) x = PASSO_GRADE * math.floor(xu_min / PASSO_GRADE) while x <= xu_max: glVertex2f(x, yu_min) glVertex2f(x, yu_max) x += PASSO_GRADE y = PASSO_GRADE * math.floor(yu_min / PASSO_GRADE) while y <= yu_max: glVertex2f(xu_min, y) glVertex2f(xu_max, y) y += PASSO_GRADE glEnd() # Eixos do SRU. glColor3f(0.45, 0.50, 0.60) glLineWidth(2.0) glBegin(GL_LINES) glVertex2f(xu_min, 0.0) glVertex2f(xu_max, 0.0) glVertex2f(0.0, yu_min) glVertex2f(0.0, yu_max) glEnd() # Marca do último clique: uma cruz sobre o ponto convertido. if self.marca is not None: xu, yu = self.marca glColor3f(0.95, 0.35, 0.35) glLineWidth(2.0) glBegin(GL_LINES) glVertex2f(xu - 4.0, yu) glVertex2f(xu + 4.0, yu) glVertex2f(xu, yu - 4.0) glVertex2f(xu, yu + 4.0) glEnd() # -- interação --------------------------------------------------------- def mousePressEvent(self, event) -> None: pos = event.position() x_qt, y_qt = pos.x(), pos.y() # O viewport, na convenção de tela: origem no topo, Y para baixo. # É exatamente a convenção em que o Qt entrega a posição do mouse. viewport = (0.0, float(self.width()), 0.0, float(self.height())) xu, yu = srd_para_sru(x_qt, y_qt, self.window_sru, viewport) # Volta e meia: reconverter o resultado tem de devolver o pixel # original. É o teste mais barato de que as duas fórmulas são # realmente inversas uma da outra. xd_volta, yd_volta = sru_para_srd(xu, yu, self.window_sru, viewport) self.marca = (xu, yu) self.update() if self.aoClicar is not None: self.aoClicar(x_qt, y_qt, xu, yu, xd_volta, yd_volta, self.height() - y_qt) def keyPressEvent(self, event) -> None: if event.key() == Qt.Key.Key_Escape: self.close() class JanelaPrincipal(QMainWindow): def __init__(self): super().__init__() self.canvas = Canvas(self) self.canvas.aoClicar = self._relatar self.setCentralWidget(self.canvas) self.statusBar().showMessage("Clique no canvas para converter o pixel em coordenadas do mundo") self.setWindowTitle("Mapeamento — SRU ↔ SRD") self.resize(760, 560) def _relatar(self, x_qt, y_qt, xu, yu, xd_volta, yd_volta, y_gl) -> None: self.statusBar().showMessage( f"Qt (SRD): ({x_qt:.0f}, {y_qt:.0f}) | " f"OpenGL: ({x_qt:.0f}, {y_gl:.0f}) | " f"Mundo (SRU): ({xu:.2f}, {yu:.2f}) | " f"volta: ({xd_volta:.0f}, {yd_volta:.0f})" ) def main() -> int: fmt = QSurfaceFormat() fmt.setVersion(2, 1) fmt.setProfile(QSurfaceFormat.OpenGLContextProfile.NoProfile) fmt.setDepthBufferSize(0) QSurfaceFormat.setDefaultFormat(fmt) app = QApplication(sys.argv) janela = JanelaPrincipal() janela.show() return app.exec() if __name__ == "__main__": raise SystemExit(main())