OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / modals / view_align.py

443 lines · python ref:
import bpy, math, bmesh
from math import cos, sin, pi, radians, degrees
from mathutils import Matrix, Vector, geometry, Quaternion
from ... ui_framework.master import Master
from ... utility import addon
from ... utility.base_modal_controls import Base_Modal_Controls
from ... utils.toggle_view3d_panels import collapse_3D_view_panels
from ... utils.bmesh import get_verts_center

description = """View Align Modal

Shift + Scroll - Roll View
Ctrl + Scroll - Orbit View

Assists with view alignment
Press H for help

"""

class HOPS_OT_ViewAlign(bpy.types.Operator):

    """View Align"""
    bl_idname = "view3d.view_align"
    bl_label = "View Align"
    bl_options = {"REGISTER", "UNDO", "BLOCKING"}
    bl_description = description


    def invoke(self, context, event):

        # Set to ortho
        for window in context.window_manager.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    for space in area.spaces:
                        if hasattr(space, 'region_3d'):
                            space.region_3d.view_perspective = 'ORTHO'
                            break

        # Set initial look to Shift 7
        if context.active_object != None:
            if hasattr(context.active_object, 'mode'):
                if context.active_object.mode != 'OBJECT':
                    bpy.ops.view3d.view_axis(type='TOP', align_active=True, relative=False)

        # Views
        self.v_align_index = 2
        self.views = ['LEFT', 'RIGHT', 'BOTTOM', 'TOP', 'FRONT', 'BACK']

        # Object refs
        self.start_object = context.active_object
        if self.start_object == None:
            if len(context.selected_objects) > 0:
                self.start_object = context.selected_objects[0]

        self.original_selection_set = self.get_original_selection_set(context)
        self.empty_obj_index = 0
        self.empty_objects = []
        if addon.preference().property.view_align_filter_empties:
            self.empty_objects = [o for o in context.view_layer.objects if o.name[:11] == "Align_Empty" and o.visible_get() == True]
        else:
            for obj in context.view_layer.objects:
                if hasattr(obj, 'type'):
                    if obj.type == 'EMPTY':
                        self.empty_objects.append(obj)

        # WARNING : Make sure to check for None type in this list
        # Putting the start obj in this collection to be scrolled on
        self.empty_objects.append(self.start_object)

        # Props
        self.set_to_top_view = False

        # Base Systems
        self.master = Master(context=context)
        self.master.only_use_fast_ui = True
        self.base_controls = Base_Modal_Controls(context, event)
        self.original_tool_shelf, self.original_n_panel = collapse_3D_view_panels()
        context.window_manager.modal_handler_add(self)
        return {'RUNNING_MODAL'}


    def modal(self, context, event):

        self.master.receive_event(event=event)
        self.base_controls.update(context=context, event=event)

        # Snap to empty after switching : this is up top because of update cycle    
        if self.set_to_top_view == True:
            self.set_to_top_view = False
            bpy.ops.view3d.view_selected('INVOKE_DEFAULT', use_all_regions=True)
            bpy.ops.view3d.view_axis('INVOKE_DEFAULT', type='TOP', align_active=True, relative=False)
            self.v_align_index = 3

        # Navigation
        if self.base_controls.pass_through:
            if not self.master.is_mouse_over_ui():
                return {'PASS_THROUGH'}

        # Confirm
        elif self.base_controls.confirm:
            if not self.master.is_mouse_over_ui():
                self.master.run_fade()
                collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
                if addon.preference().ui.Hops_extra_info:
                    bpy.ops.hops.display_notification(info=F'Aligned' )
                self.select_all_objects(self.original_selection_set)
                return {'FINISHED'}

        # Cancel
        elif self.base_controls.cancel:
            if not self.master.is_mouse_over_ui():
                self.master.run_fade()
                collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
                self.select_all_objects(self.original_selection_set)
                return {'CANCELLED'}

        # Scroll views
        elif self.base_controls.scroll:
            
            # Cycle objects
            if event.ctrl and event.shift:
                if len(self.empty_objects) > 0:
                    self.deselect_all_objects(context)

                    current_obj = self.empty_objects[self.empty_obj_index]

                    if current_obj != None:
                        current_obj.select_set(True)
                        context.view_layer.objects.active = current_obj
                    else:
                        context.view_layer.objects.active = None

                    # Notification
                    if current_obj != None:
                        if hasattr(current_obj, 'name'):
                            bpy.ops.hops.display_notification(info=f"Target : {current_obj.name}")

                    else:
                        bpy.ops.hops.display_notification(info="Target : None")

                    self.empty_obj_index = (self.empty_obj_index + 1) % len(self.empty_objects)
                    context.view_layer.update()
                    self.set_to_top_view = True
                    
            # Spin
            if event.ctrl:
                if self.base_controls.scroll > 0:
                    bpy.ops.view3d.view_roll(angle=radians(15), type='ANGLE')
                else:
                    bpy.ops.view3d.view_roll(angle=radians(-15), type='ANGLE')

            # Orbit
            elif event.shift:
                if self.base_controls.scroll > 0:
                    bpy.ops.view3d.view_orbit(angle=radians(15), type='ORBITUP')
                else:
                    bpy.ops.view3d.view_orbit(angle=radians(15), type='ORBITDOWN')

            # Z Orbit
            elif event.alt:
                if self.base_controls.scroll > 0:
                    bpy.ops.view3d.view_orbit(angle=radians(15), type='ORBITRIGHT')
                else:
                    bpy.ops.view3d.view_orbit(angle=radians(15), type='ORBITLEFT')

            # Switch
            else:
                self.v_align_index = (self.v_align_index + 1) % len(self.views)
                view = self.views[self.v_align_index]
                bpy.ops.view3d.view_axis(
                    type=view,
                    align_active=True,
                    relative=False)

        # Arrow Keys
        elif event.type == 'RIGHT_ARROW' and event.value == "PRESS":
            bpy.ops.view3d.view_roll(angle=radians(15), type='ANGLE')

        elif event.type == 'LEFT_ARROW' and event.value == "PRESS":
            bpy.ops.view3d.view_roll(angle=radians(-15), type='ANGLE')

        # Toggle perspective
        elif event.type in {'P', 'NUMPAD_5', 'FIVE'}:
            if event.value == 'PRESS':
                bpy.ops.view3d.view_persportho()

        # Drop empty to view
        elif event.type == 'E' and event.value == "PRESS":

            active_obj = context.active_object
            bpy.ops.hops.display_notification(info=F'Empty Added' )

            # Get view angle
            view_rot = None
            for window in context.window_manager.windows:
                for area in window.screen.areas:
                    if area.type == 'VIEW_3D':
                        for space in area.spaces:
                            if hasattr(space, 'region_3d'):
                                view_rot = space.region_3d.view_rotation
                                break

            # Failed to get view angle
            if view_rot == None:
                bpy.ops.object.empty_add(
                    type='SINGLE_ARROW',
                    align='VIEW',
                    location=(0, 0, 0),
                    rotation=active_obj.matrix_world.to_translation())

            # Obtained view angle
            else:

                # Create empty
                new_empty = bpy.data.objects.new('Align_Empty', None)
                context.collection.objects.link(new_empty)
                new_empty.empty_display_size = 1
                new_empty.empty_display_type = 'SINGLE_ARROW'
                new_empty.rotation_euler = view_rot.to_euler()

                self.empty_objects.append(new_empty)

                # Add empty at active mesh in edit mode
                if bpy.context.mode != 'OBJECT':
                    if active_obj.type == 'MESH':
                        me = active_obj.data
                        bm = bmesh.from_edit_mesh(me)

                        use_face = False
                        if bm.faces.active != None and bm.select_mode in {'FACE'}:
                            use_face = True

                        # No active face, place at vert avg
                        if use_face == False:
                            verts = [v for v in bm.verts if v.select]
                            loc = get_verts_center(verts)
                            loc = active_obj.matrix_world @ loc
                            new_empty.location = loc

                        # Place at active face center
                        elif use_face == True:
                            face = bm.faces.active
                            loc = face.calc_center_median()
                            loc = active_obj.matrix_world @ loc
                            new_empty.location = loc

                        del bm

                    elif active_obj.type == 'CURVE':

                        if hasattr(active_obj.data.splines.active, 'bezier_points'):
                            for point in active_obj.data.splines.active.bezier_points:
                                if point.select_control_point == True:
                                    loc = point.co
                                    loc = active_obj.matrix_world @ loc
                                    new_empty.location = loc       
                                    break

                # Add empty at object location
                elif bpy.context.mode == 'OBJECT':
                    if active_obj != None:
                        loc = active_obj.matrix_world.to_translation()
                        new_empty.location = loc

        # Toggle viewport display
        elif event.type == 'Z' and event.alt:
            if event.value == "PRESS":
                if hasattr(context.space_data, 'shading'):
                    if context.space_data.shading.type != 'WIREFRAME':
                        context.space_data.shading.type = 'WIREFRAME'
                    else:
                        context.space_data.shading.type = 'SOLID'

        # Set the orientation gizmo to view align
        elif event.type == 'T' and event.value == "PRESS":
            bpy.context.scene.transform_orientation_slots[0].type = 'VIEW'

        #####################
        # Numpad emulation
        #####################

        # 0
        elif event.type in {'ZERO', 'NUMPAD_0'} and event.value == "PRESS":
            bpy.ops.view3d.view_camera()

        # 1
        elif event.type in {'ONE', 'NUMPAD_1'} and event.value == "PRESS":
            if event.shift and event.ctrl:
                bpy.ops.view3d.view_axis(type='BACK', align_active=True, relative=False)
                self.v_align_index = 5
            elif event.shift:
                bpy.ops.view3d.view_axis(type='FRONT', align_active=True, relative=False)
                self.v_align_index = 4
            elif event.alt:
                bpy.ops.view3d.view_axis(type='BACK')
                self.v_align_index = 5
            else:
                bpy.ops.view3d.view_axis(type='FRONT')
                self.v_align_index = 4

        # 2
        elif event.type in {'TWO', 'NUMPAD_2'} and event.value == "PRESS":
            if event.ctrl:
                bpy.ops.view3d.view_pan('INVOKE_DEFAULT', type='PANDOWN')
            else:
                bpy.ops.view3d.view_orbit(angle=radians(15), type='ORBITDOWN')

        # 3
        elif event.type in {'THREE', 'NUMPAD_3'} and event.value == "PRESS":
            if event.shift and event.ctrl:
                bpy.ops.view3d.view_axis(type='LEFT', align_active=True, relative=False)
                self.v_align_index = 0
            elif event.shift:
                bpy.ops.view3d.view_axis(type='RIGHT', align_active=True, relative=False)
                self.v_align_index = 1
            elif event.ctrl:
                bpy.ops.view3d.view_axis(type='LEFT')
                self.v_align_index = 0
            else:
                bpy.ops.view3d.view_axis(type='RIGHT')
                self.v_align_index = 1

        # 4
        elif event.type in {'FOUR', 'NUMPAD_4'} and event.value == "PRESS":
            if event.shift:
                bpy.ops.view3d.view_roll(type='LEFT')
            elif event.ctrl:
                bpy.ops.view3d.view_pan('INVOKE_DEFAULT', type='PANLEFT')
            else:
                bpy.ops.view3d.view_orbit(angle=radians(15), type='ORBITLEFT')

        # 6
        elif event.type in {'SIX', 'NUMPAD_6'} and event.value == "PRESS":
            if event.shift:
                bpy.ops.view3d.view_roll(type='RIGHT')
            elif event.ctrl:
                bpy.ops.view3d.view_pan('INVOKE_DEFAULT', type='PANRIGHT')
            else:
                bpy.ops.view3d.view_orbit(angle=radians(15), type='ORBITRIGHT')

        # 7
        elif event.type in {'SEVEN', 'NUMPAD_7'} and event.value == "PRESS":
            if event.shift and event.ctrl:
                bpy.ops.view3d.view_axis(type='BOTTOM', align_active=True, relative=False)
                self.v_align_index = 2
            elif event.shift:
                bpy.ops.view3d.view_axis(type='TOP', align_active=True, relative=False)
                self.v_align_index = 3
            elif event.ctrl:
                bpy.ops.view3d.view_axis(type='BOTTOM')
                self.v_align_index = 2
            else:
                bpy.ops.view3d.view_axis(type='TOP')
                self.v_align_index = 3

        # 8
        elif event.type in {'EIGHT', 'NUMPAD_8'} and event.value == "PRESS":
            if event.ctrl:
                bpy.ops.view3d.view_pan('INVOKE_DEFAULT', type='PANUP')
            else:
                bpy.ops.view3d.view_orbit(angle=radians(15), type='ORBITUP')

        # 9
        elif event.type in {'NINE', 'NUMPAD_9', 'F'} and event.value == "PRESS":
            bpy.ops.view3d.view_orbit(angle=radians(180), type='ORBITRIGHT')

        # Focus
        elif event.type in {'A', 'PERIOD'} and event.value == "PRESS":
            bpy.ops.view3d.view_selected(use_all_regions=False)
            if addon.preference().ui.Hops_extra_info:
                bpy.ops.hops.display_notification(info=F'Focused On Selected' )

        self.build_ui(context=context)
        context.area.tag_redraw()
        return {'RUNNING_MODAL'}


    def build_ui(self, context):

        self.master.setup()

        #--- Fast UI ---#
        if self.master.should_build_fast_ui():

            # Main
            win_list = []
            win_list.append(self.views[self.v_align_index])

            # Help
            help_items = {"GLOBAL" : [], "STANDARD" : []}

            help_items["GLOBAL"] = [
                ("H", "Toggle help"),
                ("~", "Toggle UI Display Type"),
                ("O", "Toggle viewport rendering")]

            help_items["STANDARD"] = [
                ("Ctrl + Shift Scroll",  "Cycle Empties"),
                ("Shift + Scroll", "Roll View"),
                ("Ctrl + Scroll",  "Orbit View"),
                ("Alt + Scroll",   "Orbit View Z axis"),
                ("Alt + Z",        "Toggle Seethrough"),
                ("5 / P",          "Toggle Ortho"),
                ("A / Period",     "Focus On Selected"),
                ("E",              "Add empty at location"),
                ("Numbers",        "Emulation of numpad"),
                ("T",              "Set gizmo to view align"),
                ("F",              "Flip View"),
                ("Scroll",         "Cycle views")]

            self.master.receive_fast_ui(win_list=win_list, help_list=help_items, image="ArrayCircle")

        self.master.finished()


    def get_original_selection_set(self, context):
        '''Create and return a list of the original objects that where selected.'''

        selection_set = []

        for obj in context.view_layer.objects:
            if obj.select_get() == True:
                selection_set.append(obj)
                if obj != self.start_object:
                    obj.select_set(False)

        return selection_set


    def select_all_objects(self, objects):
        '''Create and return a list of the original objects that where selected.'''

        for obj in objects:
            obj.select_set(True)


    def deselect_all_objects(self, context):
        '''Deselect all objects.'''

        for obj in context.view_layer.objects:
            if obj.select_get() == True:
                obj.select_set(False)