"""
Viewport Control Handlers
Business logic for viewport projection, navigation, and view alignment.
"""
import bpy
import traceback
def set_view_projection(projection):
"""Set viewport projection mode"""
try:
# Validate projection
valid_projections = ["ORTHO", "PERSP"]
if projection not in valid_projections:
return {"error": f"Invalid projection: {projection}. Must be one of: {', '.join(valid_projections)}"}
# Find the 3D viewport area
area = None
for a in bpy.context.screen.areas:
if a.type == 'VIEW_3D':
area = a
break
if not area:
return {"error": "No 3D viewport found"}
# Set the projection
for space in area.spaces:
if space.type == 'VIEW_3D':
space.region_3d.view_perspective = projection
break
return {
"success": True,
"projection": projection
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to set view projection: {str(e)}"}
def align_view_to_axis(axis, side):
"""Align viewport to a specific axis and side"""
try:
# Validate inputs
valid_axes = ["X", "Y", "Z"]
valid_sides = ["POS", "NEG"]
if axis not in valid_axes:
return {"error": f"Invalid axis: {axis}. Must be one of: {', '.join(valid_axes)}"}
if side not in valid_sides:
return {"error": f"Invalid side: {side}. Must be one of: {', '.join(valid_sides)}"}
# Map axis and side to Blender view types
view_map = {
("Y", "POS"): "FRONT",
("Y", "NEG"): "BACK",
("X", "POS"): "RIGHT",
("X", "NEG"): "LEFT",
("Z", "POS"): "TOP",
("Z", "NEG"): "BOTTOM"
}
view_type = view_map.get((axis, side))
if not view_type:
return {"error": f"Invalid axis/side combination: {axis}/{side}"}
# Find the 3D viewport area
area = None
for a in bpy.context.screen.areas:
if a.type == 'VIEW_3D':
area = a
break
if not area:
return {"error": "No 3D viewport found"}
# Find the WINDOW region for proper context
region = None
for r in area.regions:
if r.type == 'WINDOW':
region = r
break
if not region:
return {"error": "No WINDOW region found in 3D viewport"}
# Align view to axis with proper context (area + region required)
with bpy.context.temp_override(area=area, region=region):
bpy.ops.view3d.view_axis(type=view_type)
return {
"success": True,
"axis": axis,
"side": side,
"view": view_type
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to align view to axis: {str(e)}"}
def frame_selected():
"""Frame selected objects in viewport"""
try:
# Find the 3D viewport area
area = None
for a in bpy.context.screen.areas:
if a.type == 'VIEW_3D':
area = a
break
if not area:
return {"error": "No 3D viewport found"}
# Find the WINDOW region for proper context
region = None
for r in area.regions:
if r.type == 'WINDOW':
region = r
break
if not region:
return {"error": "No WINDOW region found in 3D viewport"}
# Frame selected objects with proper context (area + region required)
with bpy.context.temp_override(area=area, region=region):
bpy.ops.view3d.view_selected()
return {
"success": True
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to frame selected: {str(e)}"}
def set_view_axis(axis):
"""
Set viewport to a standard axis view.
Parameters:
- axis: Standard view name - "FRONT", "BACK", "LEFT", "RIGHT", "TOP", "BOTTOM"
Returns success status with view parameters.
"""
try:
valid_axes = ["FRONT", "BACK", "LEFT", "RIGHT", "TOP", "BOTTOM"]
axis_upper = axis.upper() if axis else ""
if axis_upper not in valid_axes:
return {"error": f"Invalid axis: {axis}. Must be one of: {', '.join(valid_axes)}"}
# Find the 3D viewport area
area = None
for a in bpy.context.screen.areas:
if a.type == 'VIEW_3D':
area = a
break
if not area:
return {"error": "No 3D viewport found"}
# Find the WINDOW region for proper context
region = None
for r in area.regions:
if r.type == 'WINDOW':
region = r
break
if not region:
return {"error": "No WINDOW region found in 3D viewport"}
# Align view to axis with proper context (area + region required)
with bpy.context.temp_override(area=area, region=region):
bpy.ops.view3d.view_axis(type=axis_upper)
return {
"success": True,
"axis": axis_upper
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to set view axis: {str(e)}"}
def orbit_view(yaw_delta=0.0, pitch_delta=0.0, distance_delta=0.0, target=None, point=None):
"""
Orbit the viewport by specified angles.
Parameters:
- yaw_delta: Horizontal rotation in degrees (positive = rotate right)
- pitch_delta: Vertical rotation in degrees (positive = rotate up)
- distance_delta: Change in view distance (positive = zoom out, negative = zoom in)
- target: Object name to orbit around (default: current view location)
- point: Tuple/list of (x, y, z) coordinates to orbit around
Returns success status with new view parameters.
"""
try:
import math
from mathutils import Euler, Quaternion, Vector
# Find the 3D viewport area and region_3d
area = None
region_3d = None
for a in bpy.context.screen.areas:
if a.type == 'VIEW_3D':
area = a
for space in a.spaces:
if space.type == 'VIEW_3D':
region_3d = space.region_3d
break
break
if not area or not region_3d:
return {"error": "No 3D viewport found"}
# Set orbit center if target object specified
if target and target != "POINT":
obj = bpy.data.objects.get(target)
if not obj:
return {"error": f"Object '{target}' not found"}
region_3d.view_location = obj.location.copy()
elif point:
if len(point) != 3:
return {"error": "Point must have 3 coordinates (x, y, z)"}
region_3d.view_location = Vector((point[0], point[1], point[2]))
# Get current view rotation as Euler
current_rotation = region_3d.view_rotation.to_euler()
# Apply yaw (rotation around Z axis) and pitch (rotation around X axis)
yaw_rad = math.radians(yaw_delta)
pitch_rad = math.radians(pitch_delta)
# Blender uses ZXY Euler for view rotation typically, but quaternion is cleaner
# Get current euler and modify
new_euler = Euler((
current_rotation.x + pitch_rad,
current_rotation.y,
current_rotation.z + yaw_rad
), 'XYZ')
# Convert back to quaternion and set
region_3d.view_rotation = new_euler.to_quaternion()
# Apply distance change
if distance_delta != 0.0:
new_distance = region_3d.view_distance + distance_delta
if new_distance > 0:
region_3d.view_distance = new_distance
return {
"success": True,
"yaw_delta": yaw_delta,
"pitch_delta": pitch_delta,
"distance_delta": distance_delta,
"view_location": {
"x": region_3d.view_location.x,
"y": region_3d.view_location.y,
"z": region_3d.view_location.z
},
"view_distance": region_3d.view_distance
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to orbit view: {str(e)}"}
def focus_view_on_point(x, y, z, distance=None, view_axis=None):
"""
Focus viewport on a specific 3D point.
Parameters:
- x, y, z: Coordinates of the point to focus on
- distance: Optional viewing distance (default: maintain current)
- view_axis: Optional axis alignment - "FRONT", "BACK", "LEFT", "RIGHT", "TOP", "BOTTOM"
Returns success status with view parameters.
"""
try:
from mathutils import Vector
# Find the 3D viewport area
area = None
region_3d = None
for a in bpy.context.screen.areas:
if a.type == 'VIEW_3D':
area = a
for space in a.spaces:
if space.type == 'VIEW_3D':
region_3d = space.region_3d
break
break
if not area or not region_3d:
return {"error": "No 3D viewport found"}
target = Vector((x, y, z))
# If view_axis specified, align view first
if view_axis:
view_map = {
"FRONT": "FRONT",
"BACK": "BACK",
"LEFT": "LEFT",
"RIGHT": "RIGHT",
"TOP": "TOP",
"BOTTOM": "BOTTOM"
}
if view_axis.upper() in view_map:
# Find the WINDOW region for proper context
region = None
for r in area.regions:
if r.type == 'WINDOW':
region = r
break
if region:
with bpy.context.temp_override(area=area, region=region):
bpy.ops.view3d.view_axis(type=view_map[view_axis.upper()])
# Set view location (the point the camera orbits around)
region_3d.view_location = target
# Set viewing distance if specified
if distance is not None:
region_3d.view_distance = distance
return {
"success": True,
"target": {"x": x, "y": y, "z": z},
"view_distance": region_3d.view_distance,
"view_axis": view_axis
}
except Exception as e:
traceback.print_exc()
return {"error": f"Failed to focus view on point: {str(e)}"}