axuy/axuy/control.py

172 lines
6.1 KiB
Python

# handling of user control using GLFW
# Copyright (C) 2019 Nguyễn Gia Phong
#
# This file is part of Axuy
#
# Axuy is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Axuy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Axuy. If not, see <https://www.gnu.org/licenses/>.
__doc__ = 'Axuy handling of user control using GLFW'
__all__ = ['CtlConfig', 'Control']
from cmath import polar
from re import IGNORECASE, match
from warnings import warn
import glfw
import numpy
from .display import DispConfig, Display
CONTROL_ALIASES = (('Move left', 'left'), ('Move right', 'right'),
('Move forward', 'forward'), ('Move backward', 'backward'),
('Primary', '1st'), ('Secondary', '2nd'))
MOUSE_PATTERN = f'MOUSE_BUTTON_[1-{glfw.MOUSE_BUTTON_LAST+1}]'
INVALID_CONTROL_ERR = '{}: {} is not recognized as a valid control key'
GLFW_VER_WARN = 'Your GLFW version appear to be lower than 3.3, '\
'which might cause stuttering camera rotation.'
ZMIN, ZMAX = -1.0, 1.0
class CtlConfig(DispConfig):
"""User control configurations.
Attributes
----------
key, mouse : Dict[str, int]
Input control.
mouspeed : float
Relative camera rotational speed.
zmspeed : float
Zoom speed, in scroll steps per zoom range.
"""
def __init__(self) -> None:
DispConfig.__init__(self)
self.options.add_argument(
'--mouse-speed', type=float, dest='mouspeed',
help=f'camera rotational speed (fallback: {self._mouspeed:.1f})')
self.options.add_argument(
'--zoom-speed', type=float, dest='zmspeed',
help=f'zoom speed (fallback: {self.zmspeed:.1f})')
@property
def mouspeed(self) -> float:
"""Relative mouse speed."""
# Standard to radians per inch for a 800 DPI mouse, at FOV of 60
return self._mouspeed / 800
@mouspeed.setter
def mouspeed(self, value: float) -> None:
self._mouspeed = value
def fallback(self) -> None:
"""Parse fallback configurations."""
DispConfig.fallback(self)
self.mouspeed = self.config.getfloat('Control', 'Mouse speed')
self.zmspeed = self.config.getfloat('Control', 'Zoom speed')
self.key, self.mouse = {}, {}
for cmd, alias in CONTROL_ALIASES:
i = self.config.get('Control', cmd)
if match(MOUSE_PATTERN, i, flags=IGNORECASE):
self.mouse[alias] = getattr(glfw, i.upper())
continue
try:
self.key[alias] = getattr(glfw, f'KEY_{i.upper()}')
except AttributeError:
raise ValueError(INVALID_CONTROL_ERR.format(cmd, i))
def read(self, arguments):
"""Read and parse a argparse.ArgumentParser.Namespace."""
DispConfig.read(self, arguments)
for option in 'fov', 'mouspeed', 'zmspeed':
value = getattr(arguments, option)
if value is not None: setattr(self, option, value)
class Control(Display):
"""User control.
Parameters
----------
config : CtlConfig
User control configurations.
Attributes
----------
key, mouse : Dict[str, int]
Input control.
zmspeed : float
Scroll steps per zoom range.
mouspeed : float
Relative camera rotational speed.
"""
def __init__(self, config):
Display.__init__(self, config)
self.key, self.mouse = config.key, config.mouse
self.mouspeed = config.mouspeed
self.zmspeed = config.zmspeed
glfw.set_input_mode(self.window, glfw.CURSOR, glfw.CURSOR_DISABLED)
glfw.set_input_mode(self.window, glfw.STICKY_KEYS, True)
glfw.set_cursor_pos_callback(self.window, self.look)
glfw.set_scroll_callback(self.window, self.zoom)
glfw.set_mouse_button_callback(self.window, self.shoot)
try:
if glfw.raw_mouse_motion_supported():
glfw.set_input_mode(self.window, glfw.RAW_MOUSE_MOTION, True)
except AttributeError:
warn(GLFW_VER_WARN, category=RuntimeWarning)
def look(self, window, xpos, ypos):
"""Look according to cursor position.
Present as a callback for GLFW CursorPos event.
"""
center = numpy.array(glfw.get_window_size(window)) / 2
glfw.set_cursor_pos(window, *center)
yaw, pitch = (center - [xpos, ypos]) * self.mouspeed * 2**self.zmlvl
self.camera.rotate(*polar(complex(yaw, pitch)))
def zoom(self, window, xoffset, yoffset):
"""Adjust FOV according to vertical scroll."""
self.zmlvl += yoffset * 2 / self.zmspeed
self.zmlvl = max(self.zmlvl, ZMIN)
self.zmlvl = min(self.zmlvl, ZMAX)
def shoot(self, window, button, action, mods):
"""Shoot on click.
Present as a callback for GLFW MouseButton event.
"""
if action == glfw.PRESS:
if button == self.mouse['1st']:
self.camera.shoot()
elif button == self.mouse['2nd']:
self.camera.shoot(backward=True)
def is_pressed(self, *keys) -> bool:
"""Return whether given keys are pressed."""
return any(glfw.get_key(self.window, k) == glfw.PRESS for k in keys)
def control(self) -> None:
"""Handle events controlling the protagonist."""
Display.control(self)
right, upward, forward = 0, 0, 0
if self.is_pressed(self.key['forward']): forward += 1
if self.is_pressed(self.key['backward']): forward -= 1
if self.is_pressed(self.key['left']): right -= 1
if self.is_pressed(self.key['right']): right += 1
self.pico.update(right, upward, forward)