OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / modals / adjust_viewport.py

531 lines · python ref:
import bpy
from math import radians
import math
from . import infobar
from ... utility import addon
from ... ui_framework.master import Master
from ... utility.base_modal_controls import Base_Modal_Controls

# Cursor Warp imports
from ... utils.toggle_view3d_panels import collapse_3D_view_panels
from ... utils.modal_frame_drawing import draw_modal_frame
from ... utils.cursor_warp import mouse_warp
from ... utility import method_handler


class HOPS_OT_AdjustViewport(bpy.types.Operator):
    bl_idname = 'hops.adjust_viewport'
    bl_label = 'Adjust Viewport'
    bl_description = """Interactive Lookdev Adjustment

    Adjust the viewport settings
    R to set Lookdev to_render on apply

    Shift + LMB - Copy active lookdev settings to render world
    """
    bl_options = {'REGISTER', 'UNDO', 'BLOCKING'}

    eeveeHQ = 'None'
    to_render = 'None'
    to_light = 'None'

    @classmethod
    def poll(cls, context):
        return context.space_data.type == 'VIEW_3D'

#alright this thing is pretty close to being done. I don't need corrections. Just completions
#improve it if you want but the goals are the same with this modal.

    def invoke(self, context, event):
        self.notify = lambda val: bpy.ops.hops.display_notification(info=val) if addon.preference().ui.Hops_extra_info else lambda val: None
        self.shading = context.space_data.shading
        wm = bpy.context.window_manager
        # if hasattr(obj, 'cycles_visibility'):
        #     bpy.context.scene.render.engine = 'BLENDER_EEVEE'

        if (2, 83, 0) < bpy.app.version:
            self.shading_args = {'type', 'studio_light', 'studiolight_rotate_z', 'studiolight_intensity', 'studiolight_background_alpha', 'studiolight_background_blur'}
        else:
            self.shading_args = {'type', 'studio_light', 'studiolight_rotate_z', 'studiolight_intensity', 'studiolight_background_alpha'}
        self.shading_dict = self.save(self.shading, self.shading_args)
        self.envs = [img.name for img in context.preferences.studio_lights if img.type == 'WORLD']
        self.original_opacity = self.shading.studiolight_background_alpha
        self.original_viewport = bpy.context.space_data.overlay.show_overlays
        self.original_mode = self.shading.type
        self.original_exr = self.shading.studio_light
        self.can_bloom = hasattr(context.scene.eevee, 'use_bloom')

        # wireframe sets this to invalid value
        if not self.shading_dict['studio_light']:
            self.shading_dict.pop('studio_light')

        if event.shift:
            set_render(self, context)
            self.notify('Copied World shading from Lookdev')
            return {'FINISHED'}

        #type has to change before indexing or it bricks
        self.shading.type = 'MATERIAL'
        self.shading.use_scene_world = False
        self.shading.studiolight_background_alpha = 1
        self.hdri_index = self.envs.index(self.shading.studio_light)
        self.hdri_index_max = len(self.envs) -1
        if bpy.context.region_data.is_perspective:
            self.viewmode = False
        else:
            self.viewmode = True
            bpy.ops.view3d.view_persportho()

        if addon.preference().property.to_render_jump:
            self.to_render = True
        else:
            self.to_render = 'None'

        self.modes = ('Background Rotate', 'Background Blur', 'Background Strength', 'Background Opacity')

        self.mode = self.modes[0]

        # FORCE UPDATE
        self.update_val = True

        # Base Systems
        self.master = Master(context=context, start_mod_hidden=True)
        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()
        self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(self.safe_draw_shader, (context,), 'WINDOW', 'POST_PIXEL')

        context.area.header_text_set(text='Adjust Viewport')
        context.window_manager.modal_handler_add(self)
        infobar.initiate(self)
        return {'RUNNING_MODAL'}


    def modal(self, context, event):

        # Base Systems
        self.master.receive_event(event=event)
        self.base_controls.update(context, event)
        mouse_warp(context, event)

        if self.base_controls.mouse:
            offset = self.base_controls.mouse * 10
            # TODO: Use buffer variables and rounding
            if self.mode == 'Background Rotate':
                if self.shading.studiolight_rotate_z + offset > radians(180):
                    self.shading.studiolight_rotate_z = radians(-180)
                elif self.shading.studiolight_rotate_z + offset < radians(-180):
                    self.shading.studiolight_rotate_z = radians(180)
                self.shading.studiolight_rotate_z += offset
            elif self.mode == 'Background Strength':
                self.shading.studiolight_intensity += offset# / 2
            elif self.mode == 'Background Opacity':
                self.shading.studiolight_background_alpha += offset# / 5
            elif self.mode == 'Background Blur':
                self.shading.studiolight_background_blur += offset# / 5

        elif self.base_controls.pass_through:
            return {'PASS_THROUGH'}

        elif event.type == 'Z' and (event.shift or event.alt):
            return {'PASS_THROUGH'}

        elif event.type == 'X' and event.value == 'PRESS':
            index = self.modes.index(self.mode)
            index = (index + 1) % len(self.modes)
            self.mode = self.modes[index]

        elif event.type == 'W' and event.value == 'PRESS' and (event.shift or event.alt):
            context.space_data.overlay.show_overlays = False
            self.shading.studiolight_intensity = 1
            self.shading.studiolight_background_alpha = 1

            if (2, 83, 0) < bpy.app.version:
                self.shading.studiolight_background_blur = 0
            self.mode = self.modes[0]

        elif event.type == 'W' and event.value == 'PRESS':
            context.space_data.overlay.show_overlays = False
            self.shading.studiolight_intensity = 1
            self.shading.studiolight_background_alpha = 1
            #if (2, 83, 0) < bpy.app.version:
            #    self.shading.studiolight_background_blur = 0
            self.mode = self.modes[0]
            if self.eeveeHQ:
                bpy.ops.render.setup()
                self.eeveeHQ = True
            else:
                bpy.ops.hops.renderb_setup()
                self.eeveeHQ = False
            self.eeveeHQ = not self.eeveeHQ

        elif (event.type == 'WHEELUPMOUSE' and event.shift) or (event.type in {'NUMPAD_PLUS', 'EQUAL', 'S' , 'UP_ARROW'} and event.value == 'PRESS'):
            if event.ctrl:
                index = self.modes.index(self.mode)
                index = (index - 1) % len(self.modes)
                self.mode = self.modes[index]

            else:
                self.hdri_index = self.hdri_index +1 if self.hdri_index != self.hdri_index_max else 0
                #self.shading.studio_light = next_list_item(self.envs, self.shading.studio_light)
                self.shading.studio_light = self.envs[self.hdri_index]

        elif (event.type == 'WHEELDOWNMOUSE' and event.shift) or (event.type in {'NUMPAD_MINUS', 'MINUS', 'A', 'DOWN_ARROW'} and event.value == 'PRESS'):
            if event.ctrl:
                index = self.modes.index(self.mode)
                index = (index + 1) % len(self.modes)
                self.mode = self.modes[index]

            else:
                self.hdri_index = self.hdri_index -1 if self.hdri_index !=0 else self.hdri_index_max
               # self.shading.studio_light = previous_list_item(self.envs, self.shading.studio_light)
                self.shading.studio_light = self.envs[self.hdri_index]

        elif event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
            return {'PASS_THROUGH'}

        #there could probably be more states saved for restoring on start and cancel.
        #dont integrate random ideas at this time. Lets get it ready and plan for the future.
        elif self.base_controls.cancel:
            #mode has to be restored before hdri it it breaks
            bpy.context.space_data.shading.type = self.original_mode
            self.load(self.shading, self.shading_dict)
            bpy.context.space_data.overlay.show_overlays = self.original_viewport
            # bpy.context.space_data.shading.studio_light = self.original_exr
            # self.shading.studiolight_background_alpha = self.original_opacity
            #bpy.context.space_data.shading.type = 'SOLID'
            if self.viewmode:
                bpy.ops.view3d.view_persportho()
            context.area.header_text_set(text=None)
            self.remove_shader()
            collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
            self.master.run_fade()
            infobar.remove(self)
            return {'CANCELLED'}

        elif self.base_controls.confirm:
            context.area.header_text_set(text=None)
            #self.shading.studiolight_background_alpha = self.original_opacity
            #bpy.context.space_data.overlay.show_overlays = self.original_viewport
            #if self.viewmode:
            #    bpy.ops.view3d.view_persportho()
            if self.to_render == True:
                set_render(self,context)
                self.report({'INFO'}, F'Render Set To Lookdev')

            self.remove_shader()
            collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
            self.master.run_fade()
            infobar.remove(self)

            if self.to_light == True:
                self.report({'INFO'}, F'To_Light')
                bpy.ops.hops.blank_light('INVOKE_DEFAULT')

            return {'FINISHED'}

        elif (event.type == 'P' or event.type == 'NUMPAD_5') and event.value == 'PRESS':
            bpy.ops.view3d.view_persportho()

        elif event.type == 'Z' and event.value == 'PRESS':
            bpy.context.space_data.overlay.show_wireframes = not bpy.context.space_data.overlay.show_wireframes
            bpy.context.space_data.overlay.wireframe_threshold = 1

        elif event.type == 'B' and event.value == 'PRESS' and not event.ctrl:
            self.shading.studiolight_background_blur = 0 if self.shading.studiolight_background_blur > 0 else 0.501

        elif event.type == 'T' and event.value == 'PRESS':
            if self.to_light == 'None':
                self.to_light = True
                self.report({'INFO'}, F'To_Light : {self.to_light}')
            else:
                self.to_light = not self.to_light
                self.report({'INFO'}, F'To_Light : {self.to_light}')

        elif event.type == 'V' and event.value == 'PRESS':
            self.shading.studiolight_background_alpha = 0 if self.shading.studiolight_background_alpha > 0 else 1

        elif event.type == 'C' and event.value == 'PRESS':
            self.shading.studiolight_intensity = 0 if self.shading.studiolight_intensity > 0 else 1

        elif event.type == 'R' and event.value == 'PRESS':
            if self.to_render == 'None':
                self.to_render = True
                self.report({'INFO'}, F'Render Set To {self.to_render}')
            else:
                self.to_render = not self.to_render
                self.report({'INFO'}, F'Render Set To {self.to_render}')

        elif event.type == 'M' and event.value == 'PRESS' and event.ctrl:
            if bpy.context.selected_objects:
                context.area.header_text_set(text=None)
                self.shading.studiolight_background_alpha = self.original_opacity
                bpy.context.space_data.overlay.show_overlays = self.original_viewport
                if self.viewmode:
                    bpy.ops.view3d.view_persportho()
                bpy.ops.hops.material_scroll('INVOKE_DEFAULT')
                self.remove_shader()
                collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
                self.master.run_fade()
                infobar.remove(self)
                return {'FINISHED'}
            else:
                self.report({'INFO'}, F'No selection bud')

        elif self.can_bloom and event.type == 'Q' and event.value == 'PRESS':
            bpy.context.scene.eevee.use_bloom = not bpy.context.scene.eevee.use_bloom
            bpy.context.scene.eevee.bloom_intensity = 1.04762
            bpy.context.scene.eevee.bloom_threshold = 1.94286
            self.report({'INFO'}, F'Bloom: {bpy.context.scene.eevee.use_bloom}')

        # Toggle volumetrics
        elif event.type == 'F' and event.value == 'PRESS':
            bpy.context.space_data.shading.use_scene_lights = True
            bpy.context.space_data.shading.use_scene_world = not bpy.context.space_data.shading.use_scene_world

        # Force Update
        # NOTE : Remove this after blender fixes
        if bpy.app.version >= (2, 92, 0):
            if self.update_val:
                bpy.ops.view3d.view_orbit(angle=.001, type='ORBITLEFT')
            else:
                bpy.ops.view3d.view_orbit(angle=.001, type='ORBITRIGHT')
            self.update_val = not self.update_val

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


    def save(self, data, args: set):
        return {arg: getattr(data, arg) for arg in args}


    def load(self, data, args: dict):
        for key, value in args.items():
            setattr(data, key, value)


    def finish(self, context):
        context.area.header_text_set(text=None)
        self.remove_ui()
        infobar.remove(self)
        return {"FINISHED"}


    def draw_master(self, context):
        self.master.setup()

        if self.master.should_build_fast_ui():

            # Micro UI
            win_list = []
            if addon.preference().ui.Hops_modal_fast_ui_loc_options != 1:
                win_list.append(f'{self.mode[11:]}')

                if self.mode == 'Background Rotate':
                    win_list.append(f'{math.degrees(self.shading.studiolight_rotate_z):.0f}°')
                elif self.mode == 'Background Strength':
                    win_list.append(f'{self.shading.studiolight_intensity:.3f}')
                elif self.mode == 'Background Opacity':
                    win_list.append(f'{self.shading.studiolight_background_alpha:.3f}')
                elif self.mode == 'Background Blur':
                    win_list.append(f'{self.shading.studiolight_background_blur:.3f}')

                if self.eeveeHQ == 'None':
                    pass
                    #win_list.append(f'General')
                elif self.eeveeHQ != False:
                    win_list.append(f'LQ')
                elif self.eeveeHQ == False:
                    win_list.append(f'HQ')

                if self.to_render != 'None':
                    win_list.append(f'[R] To_Render {self.to_render}')
                if self.to_light != 'None':
                    win_list.append(f'To_Light {self.to_light}')
            # Full UI
            else:
                win_list.append('Viewport')

                win_list.append(f'Mode: {self.mode}')

                if self.mode == 'Background Rotate':
                    win_list.append(f'Angle: {math.degrees(self.shading.studiolight_rotate_z):.0f}')
                elif self.mode == 'Background Strength':
                    win_list.append(f'Strength: {self.shading.studiolight_intensity:.3f}')
                elif self.mode == 'Background Opacity':
                    win_list.append(f'Opacity: {self.shading.studiolight_background_alpha:.3f}')
                elif self.mode == 'Background Blur':
                    win_list.append(f'Blur: {self.shading.studiolight_background_blur:.3f}')

                if self.eeveeHQ == 'None':
                    win_list.append(f'[W] General')
                elif self.eeveeHQ != False:
                    win_list.append(f'[W] Eevee LQ')
                elif self.eeveeHQ == False:
                    win_list.append(f'[W] Eevee HQ')

                if self.to_render != 'None':
                    win_list.append(f'[R] To_Render {self.to_render}')
                if self.to_light != 'None':
                    win_list.append(f'[T] To_Light {self.to_light}')


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

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

            help_items["STANDARD"] = [
                ('T',         f'To_Light: {self.to_light}'),
                ('NUM 5 / P',  'Toggle Persp/Ortho'),
                ('V',          'Background Alpha'),
                ('C',          'Intensity'),
                ('B',          'Blur'),
                ('Q',          'Bloom'),
                ('X',          'Cycle adjust mode'),
                ('W',          f'Eevee LQ / HQ '),
                ('RMB/ESC',    'Cancel'),
                ('Shift + W',  f'Reset View '),
                ('-/+ / A/S',  'Scroll Environment'),
                ('Shift + MMB','Scroll Environment'),
                ('R',          f'To_Render: {self.to_render}'),
                ('F',          'Toggle Fog')]

            if not self.can_bloom:
                help_items["STANDARD"].remove(('Q',          'Bloom'))

            h_append = help_items["STANDARD"].append

            if bpy.context.selected_objects:
                h_append(['Ctrl + M',   'Blank Material - Exit'])
            else:
                h_append(['',       'No Selection Active'])

            # Mods
            mods_list = [[e,""] for e in self.envs]

            self.master.receive_fast_ui(win_list=win_list, help_list=help_items, image='WireMode', mods_list=mods_list,  active_mod_name=self.shading.studio_light)

        self.master.finished()

    ####################################################
    #   CURSOR WARP
    ####################################################

    def safe_draw_shader(self, context):
        method_handler(self.draw_shader,
            arguments = (context,),
            identifier = 'UI Framework',
            exit_method = self.remove_shader)


    def remove_shader(self):
        '''Remove shader handle.'''

        if self.draw_handle:
            self.draw_handle = bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, "WINDOW")


    def draw_shader(self, context):
        '''Draw shader handle.'''

        draw_modal_frame(context)


def next_list_item(list, current):
    index = list.index(current)
    return list[0] if index + 1 > len(list) - 1 else list[index + 1]


def previous_list_item(list, current):
    index = list.index(current)
    return list[-1] if index - 1 < 0 else list[index - 1]


def set_render(self, context):

    self.shading = context.space_data.shading
    active_hdri_name = self.shading.studio_light

    if self.shading.type != 'MATERIAL':
        shade_type = self.shading.type
        self.shading.type = 'MATERIAL'
        active_hdri_name = self.shading.studio_light
        self.shading.type = shade_type

    img_path = bpy.context.preferences.studio_lights[active_hdri_name].path

    img = bpy.data.images.load(img_path, check_existing =True )

    world = bpy.data.worlds.new(active_hdri_name)
    world.use_nodes = True
    bpy.context.scene.world = world

    node_tree = bpy.context.scene.world.node_tree
    node_tree.nodes.clear()

    Mapping = node_tree.nodes.new("ShaderNodeMapping")
    Mapping.location = [-800, 0]
    Mapping.name = "Mapping"
    Mapping.inputs["Location"].default_value = [0.0, 0.0, 0.0]
    Mapping.inputs["Rotation"].default_value = [0.0, 0.0, self.shading.studiolight_rotate_z]
    Mapping.inputs["Scale"].default_value = [1.0, 1.0, 1.0]

    Texture_Coordinate = node_tree.nodes.new("ShaderNodeTexCoord")
    Texture_Coordinate.location = [-1000, 0]
    Texture_Coordinate.name = "Texture_Coordinate"


    World_Output = node_tree.nodes.new("ShaderNodeOutputWorld")
    World_Output.location = [272, 51]
    World_Output.name = "World_Output"


    Light_Path = node_tree.nodes.new("ShaderNodeLightPath")
    Light_Path.location = [-388, 378]
    Light_Path.name = "Light_Path"


    Alpha = node_tree.nodes.new("ShaderNodeMixRGB")
    Alpha.location = [-273, -36]
    Alpha.name = "Alpha"
    Alpha.label = "Alpha"
    Alpha.blend_type = "MIX"
    Alpha.use_clamp = False
    Alpha.inputs["Fac"].default_value = self.shading.studiolight_background_alpha
    Alpha.inputs["Color1"].default_value = [0.0500, 0.0500, 0.0500, 0.0]

    Alpha_Mix = node_tree.nodes.new("ShaderNodeMixRGB")
    Alpha_Mix.location = [-107, 112]
    Alpha_Mix.name = "Alpha_Mix"
    Alpha_Mix.label = "Alpha_Mix"
    Alpha_Mix.blend_type = "MIX"
    Alpha_Mix.use_clamp = False


    Background = node_tree.nodes.new("ShaderNodeBackground")
    Background.location = [71, 39]
    Background.name = "Background"
    Background.inputs["Strength"].default_value = self.shading.studiolight_intensity

    hdri_node = node_tree.nodes.new("ShaderNodeTexEnvironment")
    hdri_node.location = [-600, 0]
    hdri_node.name = "hdri_node"
    hdri_node.image = img

    node_tree.links.new(Background.outputs["Background"], World_Output.inputs["Surface"])
    node_tree.links.new(hdri_node.outputs["Color"], Alpha_Mix.inputs["Color1"])
    node_tree.links.new(Mapping.outputs["Vector"], hdri_node.inputs["Vector"])
    node_tree.links.new(Texture_Coordinate.outputs["Generated"], Mapping.inputs["Vector"])
    node_tree.links.new(Alpha_Mix.outputs["Color"], Background.inputs["Color"])
    node_tree.links.new(Light_Path.outputs["Is Camera Ray"], Alpha_Mix.inputs["Fac"])
    node_tree.links.new(hdri_node.outputs["Color"], Alpha.inputs["Color2"])
    node_tree.links.new(Alpha.outputs["Color"], Alpha_Mix.inputs["Color2"])