Source code for pohlke.operators

# SPDX-FileCopyrightText: 2021-2026 Julien Rippinger, Ian Bertin <alicelab.be>
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""
Defines camera operators and scene handlers for the Parallel Cameras add-on.
"""

import bpy
from bpy.app.handlers import persistent
from math import radians, pi, sin
from .utils.geometry_math import (
    calculate_altitude_with_z_value,
)
from .utils.ui_shared import draw_create_cam_content
from .utils.helpers import camera_attributes_updated, camera_type_updated
from .utils.data import CAMERA_SETTINGS

# -------------------------------------------------------------------
# GLOBAL VARIABLES
# -------------------------------------------------------------------

_last_cam = None
_alpha = None
_beta = None
_z_value = None
_projection_type = None
_axonometric_type = None
_oblique_type = None

# -------------------------------------------------------------------
# OPERATOR
# -------------------------------------------------------------------


[docs] class Pohlke_OT_CreateCam(bpy.types.Operator): """Operator responsible for creating a parallel projection camera. Attributes ---------- projection_type : enum Main projection category: * 'AXONOMETRIC' : Axonometric projections. * 'OBLIQUE' : Oblique projections. axonometric_type : enum Sub-preset for axonometric cameras. Available values are dynamically generated from the projection definitions declared in `presets.toml` and loaded via `data.py`. They are no longer hardcoded. Each preset follows the naming pattern: 'AXONOMETRIC_#<n>' where <n> is the index of the preset as defined in `presets.toml`. A 'CUSTOM' entry is automatically added when the current projection parameters do not match any predefined preset. oblique_type : enum Sub-preset for oblique cameras. Available values are dynamically generated from the projection definitions declared in `presets.toml` and loaded via `data.py`. They are no longer hardcoded. Each preset follows the naming pattern: 'OBLIQUE_#<n>' where <n> is the index of the preset as defined in `presets.toml`. A 'CUSTOM' entry is automatically added when the current projection parameters do not match any predefined preset. beta : float The beta angle of the plan (in radians) alpha : float The alpha angle of the plan (in radians) is_updating : bool Internal flag to prevent infinite update loops during property sync. rotation : float Computed rotation around the vertical axis, derived from alpha and beta. altitude : float Computed altitude angle, derived from alpha and beta. altitude_oblique : float Complementary angle used specifically for oblique projections. x_value : float Scaling factor for the X axis (0.0 to 1.0). normalized_x_value : float Nomalized scaling factor for the X axis (0.0 to 1.0). y_value : float Scaling factor for the Y axis (0.0 to 1.0). normalized_y_value : float Nomalized scaling factor for the Y axis (0.0 to 1.0). z_value : float Scaling factor for the Z axis (0.0 to 1.0). normalized_z_value : float Nomalized scaling factor for the Z axis (0.0 to 1.0). position : enum Vertical placement of the camera relative to the scene: * 'ABOVE' : Standard view from top-down. * 'BELOW' : View from bottom-up. orientation : enum The quadrant orientation (1 to 4) toward which the camera is facing. ortho_scale : float The camera's orthographic scale. """ bl_idname = "view3d.create_parallel_camera" bl_label = "Add Camera" bl_description = "Create a camera using a parallel projection" bl_options = {"REGISTER", "UNDO"} projection_type: bpy.props.EnumProperty( name="Type de Projection", items=[ ("AXONOMETRIC", "Axonometric", "Axonometrics projections", "NONE", 0), ("OBLIQUE", "Oblique", "Obliques projections", "NONE", 1), ], default="AXONOMETRIC", ) # pyright: ignore[reportInvalidTypeForm] axonometric_type: bpy.props.EnumProperty( name="Presets", description="Type of projection for the new axonometric camera", items=[ (key, preset["name"], "Presetting an {} camera".format(preset["name"])) for (key, preset) in CAMERA_SETTINGS.get_preset_menu_items( "AXONOMETRIC" ).items() ] + [("CUSTOM", "Custom", "No camera preselection")], default="AXONOMETRIC_#1", ) # pyright: ignore[reportInvalidTypeForm] oblique_type: bpy.props.EnumProperty( name="Presets", description="Type of projection for the new oblique camera", items=[ (key, preset["name"], "Presetting an {} camera".format(preset["name"])) for (key, preset) in CAMERA_SETTINGS.get_preset_menu_items( "OBLIQUE" ).items() ] + [("CUSTOM", "Custom", "No camera preselection")], default="OBLIQUE_#1", ) # pyright: ignore[reportInvalidTypeForm] beta: bpy.props.FloatProperty( name="Beta", description="Altitude from the side. 0° is top view, 90° is side view", default=radians(30), min=0, step=1, precision=4, max=pi / 2.0, subtype="ANGLE", ) # pyright: ignore[reportInvalidTypeForm] alpha: bpy.props.FloatProperty( name="Alpha", description="Rotation of the camera around the vertical axis", default=radians(30), min=0, step=1, precision=4, max=pi / 2.0, subtype="ANGLE", ) # pyright: ignore[reportInvalidTypeForm] is_updating: bpy.props.BoolProperty(default=False) # pyright: ignore[reportInvalidTypeForm] rotation: bpy.props.FloatProperty( name="Rotation", description="Rotation of the camera around the vertical axis", default=pi / 4.0, min=0, precision=4, max=pi / 2.0, subtype="ANGLE", ) # pyright: ignore[reportInvalidTypeForm] altitude: bpy.props.FloatProperty( name="Altitude", description="Altitude from the side. 0° is top view, 90° is side view", default=radians(54.74), min=0, precision=4, max=pi / 2.0, subtype="ANGLE", ) # pyright: ignore[reportInvalidTypeForm] altitude_oblique: bpy.props.FloatProperty(name="Altitude", subtype="ANGLE") # pyright: ignore[reportInvalidTypeForm] x_value: bpy.props.FloatProperty( name="X axis", description="Size of x axis", default=1, min=0.01, max=1 ) # pyright: ignore[reportInvalidTypeForm] normalized_x_value: bpy.props.FloatProperty( name="X axis", description="Size of x axis", default=1, min=0.01, max=1, ) # pyright: ignore[reportInvalidTypeForm] y_value: bpy.props.FloatProperty( name="Y axis", description="Size of y axis", default=1, min=0.01, max=1 ) # pyright: ignore[reportInvalidTypeForm] normalized_y_value: bpy.props.FloatProperty( name="Y axis", description="Size of y axis", default=1, min=0.01, max=1, ) # pyright: ignore[reportInvalidTypeForm] z_value: bpy.props.FloatProperty( name="Z axis", description="Size of z axis", step=1, default=1, soft_min=0.01, soft_max=1, ) # pyright: ignore[reportInvalidTypeForm] normalized_z_value: bpy.props.FloatProperty( name="Z axis", description="Size of z axis", default=1, soft_min=0.01, soft_max=1, ) # pyright: ignore[reportInvalidTypeForm] position: bpy.props.EnumProperty( name="Type de Projection", items=[ ("ABOVE", "Above", "Put the camera above the scene", "NONE", 0), ("BELOW", "Below", "Put the camera below the scene", "NONE", 1), ], default="ABOVE", ) # pyright: ignore[reportInvalidTypeForm] orientation: bpy.props.EnumProperty( name="Orientation", items=[ ("1", "++", "Oriented towards the 2nd quadrant", "NONE", 0), ("2", "-+", "Oriented towards the 3rd quadrant", "NONE", 1), ("3", "--", "Oriented towards the 4th quadrant", "NONE", 2), ("0", "+-", "Oriented towards the 1st quadrant", "NONE", 3), ], default="0", ) # pyright: ignore[reportInvalidTypeForm] ortho_scale: bpy.props.FloatProperty( name="Orthographic Scale", description="The camera's orthographic scale", default=10.000, precision=3, min=0.001, step=10, ) # pyright: ignore[reportInvalidTypeForm]
[docs] def draw(self, context: bpy.types.Context) -> None: """ Draw the operator UI inside Blender popups or menus. Parameters --------- context : bpy.types.Context Blender context """ layout = self.layout draw_create_cam_content(layout, self, "operator")
[docs] def check(self, context: bpy.types.Context) -> None: """ Check if beta, alpha or isOblique values changed to update the camera settings Parameters --------- context : bpy.types.Context Blender context """ if self.is_updating: return False self.is_updating = True try: global \ _beta, \ _alpha, \ _z_value, \ _projection_type, \ _axonometric_type, \ _oblique_type changed = False if self.beta != _beta: camera_attributes_updated(self, "beta") _beta = self.beta changed = True elif self.alpha != _alpha: camera_attributes_updated(self, "alpha") changed = True elif self.z_value != _z_value and self.projection_type == "OBLIQUE": camera_type = CAMERA_SETTINGS.get_preset_name( self.projection_type, self.alpha, self.beta, self.z_value ) self.oblique_type = camera_type changed = True elif ( self.projection_type != _projection_type or self.axonometric_type != _axonometric_type or self.oblique_type != _oblique_type ): camera_type_updated(self) changed = True if changed: _alpha = self.alpha _beta = self.beta _projection_type = self.projection_type _axonometric_type = self.axonometric_type _oblique_type = self.oblique_type _z_value = self.z_value return changed finally: self.is_updating = False
[docs] def execute(self, context: bpy.types.Context) -> set[str]: """ Execute the camera creation logic. Creates a new orthographic camera configured according to the selected projection parameters. Parameters --------- context : bpy.types.Context Blender context Returns ------- set[str] Blender operator result status """ # area = [a for a in context.screen.areas if a.type == 'VIEW_3D'][0] # view3d = [space for space in area.spaces if space.type == 'VIEW_3D'][0] # view3d.show_region_ui = False bpy.ops.view3d.active_panel_category = False scn = context.scene bpy.ops.object.camera_add( location=(scn.cursor.location), rotation=(0.0, 0.0, 0.0) ) obj = context.object isOblique = self.projection_type == "OBLIQUE" altitude = ( calculate_altitude_with_z_value(self.z_value) if isOblique else self.altitude ) obj.data.type = "ORTHO" obj.data.ortho_scale = self.ortho_scale obj.rotation_euler.x = pi / 2.0 + ( altitude * -1 if self.position == "ABOVE" else altitude ) obj.rotation_euler.z = ((pi / 2) * int(self.orientation)) + self.rotation obj.location += 25 * obj.rotation_euler.to_matrix().inverted()[2] if altitude == 0.0 or not isOblique: pixel_ratio = 1.0 else: pixel_ratio = 1 / sin(abs(altitude)) obj["pixel_ratio"] = pixel_ratio scn.render.pixel_aspect_x = pixel_ratio scn.render.pixel_aspect_y = 1.0 bpy.ops.view3d.object_as_camera() # self.report({"INFO"}, "Camera created") return {"FINISHED"}
[docs] class Pohlke_OT_switch_angles(bpy.types.Operator): """Switch Alpha and Beta angle values""" bl_idname = "view3d.switch_angles" bl_label = "Switch Alpha/Beta" bl_options = {"REGISTER", "UNDO"}
[docs] def execute(self, context): """ Perform alpha and beta angle swapping Parameters --------- context : bpy.types.Context Blender context Returns ------- set[str] Blender operator result status """ props = context.scene.pohlke_camera_props alpha_temp = props.alpha props.alpha = props.beta props.beta = alpha_temp self.report({"INFO"}, "Angles switched") return {"FINISHED"}
# ------------------------------------------------------------------- # HANDLERS # ------------------------------------------------------------------- @persistent
[docs] def set_pixel_ratio(scene: bpy.types.Scene) -> None: """ Automatically update pixel ratio when the scene updates. Ensures that orthographic distortion remains consistent when switching cameras or updating the dependency graph. Parameters --------- scene : bpy.types.Scene Current Blender scene """ global _last_cam cam_obj = scene.camera if cam_obj == _last_cam: return if cam_obj is not None and "pixel_ratio" in cam_obj: ratio = cam_obj["pixel_ratio"] if abs(scene.render.pixel_aspect_x - ratio) > 0.0001: scene.render.pixel_aspect_x = ratio scene.render.pixel_aspect_y = 1.0 _last_cam = cam_obj
# ------------------------------------------------------------------- # REGISTRATION # ------------------------------------------------------------------- _classes = (Pohlke_OT_CreateCam, Pohlke_OT_switch_angles) _register, _unregister = bpy.utils.register_classes_factory(_classes)
[docs] def register() -> None: """Register operator and handlers to Blender.""" _register() bpy.app.handlers.depsgraph_update_pre.append(set_pixel_ratio) bpy.app.handlers.frame_change_pre.append(set_pixel_ratio)
[docs] def unregister() -> None: """Unregister operator and handlers from Blender.""" _unregister() bpy.app.handlers.depsgraph_update_pre.remove(set_pixel_ratio) bpy.app.handlers.frame_change_pre.remove(set_pixel_ratio)