Edit. (repeating from my blenderartists post)
Update! I’ve derived a rudimentary way of acheiving the kind of controls i need, thank—in small part—to chat gpt and stack exchange. It turned out that using a modal operator was all it took to override the standard blender controls. Though I couldn’t find a way to easily manipulate the viewport rotation and focal length. instead I create a temporary camera and set it as the default scene camera and automatically switch the first viewport in the current area to camera view. Though you can use it in any area by switching to camera view in that area. I need to make it so that the mmb is only overridden when the mouse is in a View3d region, and the rotation speed of the camera should be slower when the viewport is zoomed in and faster when its zoomed out, but this is a pretty decent proof of concept for now. Right now the script must be run manually and pressing ESC deletes the temporary camera and restores the default controls.
import bpy
from mathutils import *
class CustomViewportControls(bpy.types.Operator):
"""overrides blenders standard viewport controls"""
bl_idname = "wm.custom_viewport_controls"
bl_label = "Custom Viewport Controls"
mmb_pressed = False
cam = None
cam_obj = None
cam_mat = Matrix.Rotation(3.1415926/2,3,'X')
ini_mpos = Vector((0.0,0.0))
rot_state= Vector((0.0,0.0))
def init_custom_navigation(self):
if not "Pivot_cam" in bpy.data.cameras:
self.cam=bpy.data.cameras.new("Pivot_cam")
self.cam.lens=10
else:
self.cam=bpy.data.cameras['Pivot_cam']
if not "Pivot_cam" in bpy.data.objects:
self.cam_obj=bpy.data.objects.new("Pivot_cam",self.cam)
self.cam_obj.rotation_euler=self.cam_mat.to_euler()
bpy.context.scene.collection.objects.link(self.cam_obj)
bpy.context.scene.camera=self.cam_obj
else:
self.cam_obj=bpy.data.objects['Pivot_cam']
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
# Get the region of the 3D Viewport
area.spaces[0].region_3d.view_perspective='CAMERA'
break
def destroy_custom_navigation(self):
if "Pivot_cam" in bpy.data.objects:
bpy.data.objects.remove(bpy.data.objects['Pivot_cam'])
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
# Get the region of the 3D Viewport
area.spaces[0].region_3d.view_perspective='PERSP'
break
def modal(self, context, event):
rot_state=self.rot_state
cam_mat=self.cam_mat
mpos=Vector((event.mouse_x, event.mouse_y))
mdist=mpos-self.ini_mpos
if event.type in {'ESC'}:
self.cancel(context)
return {'CANCELLED'}
if event.type == 'MOUSEMOVE' and self.mmb_pressed:
## code for panning the viewport
#print("mouse moved", mdist)
rot_state+=mdist
angle_x=Matrix.Rotation(rot_state[0]/500,3,'Y')
angle_y=Matrix.Rotation(-rot_state[1]/500,3,'X')
self.cam_obj.rotation_euler=(cam_mat @ angle_x @ angle_y).to_euler()
self.ini_mpos = mpos
return {'RUNNING_MODAL'}
if event.type in {'MIDDLEMOUSE'}and not event.ctrl:
#print("mmb", event.value)
if event.value == "PRESS":
self.mmb_pressed = True
self.ini_mpos= mpos
else:
self.mmb_pressed = False
return {'RUNNING_MODAL'}
if event.type in {'ESC'}:
return {'FINISHED'}
return {'PASS_THROUGH'}
def invoke(self, context, event):
print("INVOKE")
self.init_custom_navigation()
self.execute(context)
return {'RUNNING_MODAL'}
def execute(self, context):
wm = context.window_manager
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
def cancel(self, context):
self.destroy_custom_navigation()
return {'FINISHED'}
def register():
bpy.utils.register_class(CustomViewportControls)
def unregister():
bpy.utils.unregister_class(CustomViewportControls)
if __name__ == "__main__":
register()
bpy.ops.wm.custom_viewport_controls('INVOKE_DEFAULT')