from __future__ import annotations
from contextlib import contextmanager
from typing import TYPE_CHECKING, Generator
from pyglet.math import Mat4, Vec2, Vec3
from typing_extensions import Self
from arcade.types import LBWH, Point, Rect
from arcade.window_commands import get_window
if TYPE_CHECKING:
from arcade.context import ArcadeContext
__all__ = ["ViewportProjector", "DefaultProjector"]
[docs]
class ViewportProjector:
"""
A simple Projector which does not rely on any camera PoDs.
Does not have a way of moving, rotating, or zooming the camera.
perfect for something like UI or for mapping to an offscreen framebuffer.
Args:
viewport: The viewport to project to.
context: The window context to bind the camera to. Defaults to the currently active window.
"""
def __init__(
self,
viewport: Rect | None = None,
*,
context: ArcadeContext | None = None,
):
self._ctx: ArcadeContext = context or get_window().ctx
self._viewport: Rect = viewport or LBWH(*self._ctx.viewport)
self._projection_matrix: Mat4 = Mat4.orthogonal_projection(
0.0, self._viewport.width, 0.0, self._viewport.height, -100, 100
)
@property
def viewport(self) -> Rect:
"""
The viewport use to derive projection and view matrix.
"""
return self._viewport
@viewport.setter
def viewport(self, viewport: Rect) -> None:
self._viewport = viewport
self._projection_matrix = Mat4.orthogonal_projection(
0, viewport.width, 0, viewport.height, -100, 100
)
[docs]
def use(self) -> None:
"""
Set the window's projection and view matrix.
Also sets the projector as the windows current camera.
"""
self._ctx.current_camera = self
self._ctx.viewport = self.viewport.lbwh_int # get the integer 4-tuple LBWH
self._ctx.view_matrix = Mat4()
self._ctx.projection_matrix = self._projection_matrix
[docs]
@contextmanager
def activate(self) -> Generator[Self, None, None]:
"""
The context manager version of the use method.
usable with the 'with' block. e.g. 'with ViewportProjector.activate() as cam: ...'
"""
previous = self._ctx.current_camera
try:
self.use()
yield self
finally:
previous.use()
[docs]
def project(self, world_coordinate: Point) -> Vec2:
"""
Take a Vec2 or Vec3 of coordinates and return the related screen coordinate
"""
x, y, *z = world_coordinate
return Vec2(x, y)
[docs]
def unproject(self, screen_coordinate: Point) -> Vec3:
"""
Map the screen pos to screen_coordinates.
Due to the nature of viewport projector this does not do anything.
"""
x, y, *_z = screen_coordinate
z = 0.0 if not _z else _z[0]
return Vec3(x, y, z)
# As this class is only supposed to be used internally
# I wanted to place an _ in front, but the linting complains
# about it being a protected class.
[docs]
class DefaultProjector(ViewportProjector):
"""
An extremely limited projector which lacks any kind of control. This is only
here to act as the default camera used internally by Arcade. There should be
no instance where a developer would want to use this class.
Args:
context: The window context to bind the camera to. Defaults to the currently active window.
"""
def __init__(self, *, context: ArcadeContext | None = None):
super().__init__(context=context)
[docs]
def use(self) -> None:
"""
Set the window's Projection and View matrices.
cache's the window viewport to determine the projection matrix.
"""
viewport = self.viewport.lbwh_int
# If the viewport is correct and the default camera is in use,
# then don't waste time resetting the view and projection matrices
if self._ctx.viewport == viewport and self._ctx.current_camera == self:
return
# If the viewport has changed while the default camera is active then the
# default needs to update itself.
# If it was another camera's viewport being used the default camera should not update.
if self._ctx.viewport != viewport and self._ctx.current_camera == self:
self.viewport = LBWH(*self._ctx.viewport)
else:
self._ctx.viewport = viewport
self._ctx.current_camera = self
self._ctx.view_matrix = Mat4()
self._ctx.projection_matrix = self._projection_matrix