# SPDX-FileCopyrightText: 2021-2026 Julien Rippinger, Ian Bertin <alicelab.be>
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""
Defines projection presets used by the Parallel Cameras add-on.
This module centralizes all predefined projection configurations. Each preset defines angular parameters and projection mode metadata.
"""
from math import radians, isclose
from typing import TypedDict
import tomllib
import pathlib
[docs]
class CameraPreset(TypedDict):
"""
Type of camera settings
Attributes
---------
name : str
Human-readable preset name displayed in UI.
alpha : float
First projection angle in a plane, between Z and X (radians).
beta : float
Second projection angle in a plane, between Z and Y (radians).
isOblique : bool
- False → Axonometric projection
- True → Oblique projection
shortening : float
Reduction factor for the Z axis (only for oblique projection)
Examples
---------
>>> example_of_camera: CameraPreset = {
>>> "name" : "Isometric",
>>> "alpha" : radians(30),
>>> "beta" : radians(30),
>>> "isOblique": False
>>> "shortening": 0
>>> }
"""
name: str
alpha: float
beta: float
isOblique: bool
shortening: float
[docs]
class CameraSettings:
"""
Catalog of camera presets.
It reads and processes a TOML file containing projection data.
Attributes
---------
presets: dict[str, dict[str, CameraPreset]]
The dictionnary of all projections, regrouped by type "AXONOMETRIC" or "OBLIQUE"
Examples
---------
>>> # Example of presets.toml :
>>> #
>>> # [AXONOMETRIC]
>>> # "Isometric (30°/30°)" = { alpha = 30, beta = 30 }
>>> #
>>> # [OBLIQUE]
>>> # "Military" = {alpha = 60, beta = 30, shortening = 1 }
>>> # "Military Shortened" = {alpha = 60, beta = 30, shortening = 0.75 }
>>>
>>> current_folder = pathlib.Path(__file__).resolve().parent
>>> toml_file_path = current_folder / "presets.toml"
>>> CAMERA_SETTINGS = CameraSettings.from_toml(toml_file_path)
>>> print(CAMERA_SETTINGS.get_presets)
{
"AXONOMETRIC": {
'Isometric (30°/30°)': {
'alpha': 30,
'beta': 30
}
},
"OBLIQUE": {
'Military': {
'alpha': 60,
'beta': 30,
'shortening': 1
},
'Military Shortened': {
'alpha': 60,
'beta': 30,
'shortening': 0.75
}
}
}
"""
presets: dict[str, dict[str, CameraPreset]]
def __init__(self, file_path: pathlib.Path) -> None:
file_path = str(file_path)
with open(file_path, "rb") as file:
self.presets = tomllib.load(file)
@classmethod
[docs]
def from_toml(cls, file_path) -> None:
return cls(file_path)
@property
[docs]
def axonometric(self) -> dict:
"""Get the axonometric presets"""
return self.presets["AXONOMETRIC"]
@property
[docs]
def oblique(self) -> dict:
"""Get the oblique presets"""
return self.presets["OBLIQUE"]
@property
[docs]
def get_presets(self) -> dict[str, dict[str, CameraPreset]]:
"""Get the presets"""
return self.presets
[docs]
def get_preset_name(
self, preset_type, alpha_rad: float, beta_rad: float, shortening: float
) -> str:
"""
Get the preset associated with alpha and beta angles
Parameters
---------
projection_type : str
THe type of projection (AXONOMETRIC or OBLIQUE)
alpha_rad : float
The alpha angle of the plan (in radians)
beta_rad : float
The beta angle of the plan (in radians)
shortening : float
Reduction factor for the Z axis (only for oblique projection, 0 for axonometric projection)
Returns
-------
str
The preset associated with the parameters or "CUSTOM" if no corresponding preset
Example
-------
>>> from math import radians
>>> #Initialization of parameters
>>> projection_type = "AXONOMETRIC"
>>> alpha_rad = radians(30)
>>> beta_rad = radians(30)
>>> #Initialization of CAMERA_SETTINGS
>>> current_folder = pathlib.Path(__file__).resolve().parent
>>> toml_file_path = current_folder / "presets.toml"
>>> CAMERA_SETTINGS = CameraSettings.from_toml(toml_file_path)
>>> camera_type = CAMERA_SETTINGS.get_preset_name(projection_type, alpha_rad, beta_rad, 0)
>>> print(camera_type)
ISOMETRIC
>>> alpha_rad = radians(38)
>>> beta_rad = radians(30)
>>> projection_type = "OBLIQUE"
>>> camera_type = CAMERA_SETTINGS.get_preset_name(projection_type, alpha_rad, beta_rad, 0)
>>> print(camera_type)
CUSTOM
"""
preset_types = list(self.presets)
if (
(preset_type not in preset_types)
or (preset_type != "OBLIQUE" and shortening > 0)
or (preset_type == "OBLIQUE" and shortening == 0)
):
return "NULL"
tol = 1e-5
for index, (key, preset) in enumerate(self.presets[preset_type].items()):
preset_alpha = radians(preset["alpha"]) if preset["alpha"] else 0
preset_beta = radians(preset["beta"]) if preset["beta"] else 0
preset_shortening = preset.get("shortening", 0)
if (
isclose(preset_alpha, alpha_rad, rel_tol=tol)
and isclose(preset_beta, beta_rad, rel_tol=tol)
and (
(
preset_type == "OBLIQUE"
and isclose(shortening, preset_shortening, rel_tol=tol)
)
or preset_type != "OBLIQUE"
)
):
return preset_type + "_#{}".format(str(index + 1))
elif (
isclose(preset_alpha, beta_rad, rel_tol=tol)
and isclose(preset_beta, alpha_rad, rel_tol=tol)
and (
(
preset_type == "OBLIQUE"
and isclose(shortening, preset_shortening, rel_tol=tol)
)
or preset_type != "OBLIQUE"
)
):
return preset_type + "_#{}".format(str(index + 1))
return "CUSTOM"
[docs]
current_folder = pathlib.Path(__file__).resolve().parent
"""The current folder"""
[docs]
toml_file_path = current_folder / "presets.toml"
"""The full file path"""
[docs]
CAMERA_SETTINGS = CameraSettings.from_toml(toml_file_path)
"""The main instance CAMERA_SETTINGS"""