OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / meshtools / twist360.py

554 lines · python ref:
import bpy
import bmesh
import math
import mathutils
from ... utility import addon
from ... utility import modifier
from ... utility.base_modal_controls import Base_Modal_Controls
from ... ui_framework.master import Master
from ... ui_framework.utils.mods_list import get_mods_list

# 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_MOD_Twist360(bpy.types.Operator):
    bl_idname = "hops.array_twist"
    bl_label = "Array Twist"
    bl_options = {'REGISTER', 'UNDO', 'GRAB_CURSOR'}
    bl_description = """Adds an array modifier and deforms the mesh 360 degrees
LMB + Shift - Don't merge segments

Press H for help"""

    axis: bpy.props.EnumProperty(
        name="Axis",
        description="What axis to twist around on",
        items=[
            ('X', "X", "Twist around the X axis"),
            ('Y', "Y", "Twist around the Y axis"),
            ('Z', "Z", "Twist around the Z axis")],
        default='Z')

    count: bpy.props.IntProperty(
        name="Count",
        description="How many segments to make",
        min=1,
        soft_max=100,
        default=8)

    displace: bpy.props.FloatProperty(
        name="Displace",
        description="How far in or out to displace",
        default=-1)

    angle: bpy.props.FloatProperty(
        name="Angle",
        description="How many degrees to twist",
        default=360)

    rotation: bpy.props.FloatProperty(
        name="Rotation",
        description="How many degrees to rotate the segment",
        default=0)

    merge: bpy.props.BoolProperty(
        name="Merge",
        description="Merge vertices of adjacent segments",
        default=True)

    duplicating: bpy.props.BoolProperty(
        name="Duplicating",
        description="Whether this operator is called from Shift + D by another instance of itself",
        default=False)

    @classmethod
    def poll(cls, context):
        obj = context.active_object
        return obj and obj.type == 'MESH' and obj.mode == 'OBJECT'

    def draw(self, context):
        self.layout.label(text=f"Axis: {self.axis}")
        self.layout.label(text=f"Segments: {self.count}")
        self.layout.label(text=f"Displace: {self.displace:.3f}")
        self.layout.label(text=f"Angle: {self.angle:.0f}")
        self.layout.label(text=f"Rotation: {self.rotation:.0f}")
        self.layout.label(text=f"Merge: {'Enabled' if self.merge else 'Disabled'}")

    # Without execute, draw doesn't work
    def execute(self, context):
        return {'FINISHED'}

    def invoke(self, context, event):

        self.modal_scale = addon.preference().ui.Hops_modal_scale
        self.obj = context.active_object
        self.merge = not event.shift
        self.adjusting = 'DISPLACE'
        self.displace_mod_one = None
        self.array_mod = None
        self.deform_mod = None
        self.displace_mod_two = None
        self.weld_mod = None

        for a, b, c, d, e in zip(self.obj.modifiers[:-3], self.obj.modifiers[1:-2], self.obj.modifiers[2:-1], self.obj.modifiers[3:], self.obj.modifiers[4:] + [None]):
            if a.type == 'DISPLACE' and b.type == 'ARRAY' and c.type == 'SIMPLE_DEFORM' and d.type == 'DISPLACE':
                self.displace_mod_one = a
                self.array_mod = b
                self.deform_mod = c
                self.displace_mod_two = d

                if e and e.type == 'WELD':
                    self.weld_mod = e

                break

        self.displace_new_one = False
        self.array_new = False
        self.deform_new = False
        self.displace_new_two = False
        self.weld_new = False

        if not self.displace_mod_one:
            self.displace_mod_one = self.obj.modifiers.new("Displace One", 'DISPLACE')
            self.displace_mod_one.show_expanded = False

            self.displace_mod_one.show_in_editmode = not addon.preference().property.Hops_twist_radial_sort
            self.displace_mod_one.show_render = addon.preference().property.Hops_twist_radial_sort

            self.displace_mod_one.mid_level = 0.0
            self.displace_mod_one.direction = 'X' if self.axis == 'Y' else 'Y'
            self.displace_mod_one.strength = self.displace
            self.displace_new_one = True

        if not self.array_mod:
            self.array_mod = self.obj.modifiers.new("Array", 'ARRAY')
            self.array_mod.show_expanded = False

            self.array_mod.show_in_editmode = not addon.preference().property.Hops_twist_radial_sort
            self.array_mod.show_render = addon.preference().property.Hops_twist_radial_sort

            self.array_mod.count = self.count
            self.array_new = True

        if not self.deform_mod:
            self.deform_mod = self.obj.modifiers.new("Simple Deform", 'SIMPLE_DEFORM')
            self.deform_mod.show_expanded = False

            self.deform_mod.show_in_editmode = not addon.preference().property.Hops_twist_radial_sort
            self.deform_mod.show_render = addon.preference().property.Hops_twist_radial_sort

            self.deform_mod.angle = math.radians(self.angle)
            self.deform_mod.deform_method = 'BEND'
            self.deform_mod.deform_axis = self.axis
            self.deform_new = True

        if not self.displace_mod_two:
            self.displace_mod_two = self.obj.modifiers.new("Displace Two", 'DISPLACE')
            self.displace_mod_two.show_expanded = False

            self.displace_mod_two.show_in_editmode = not addon.preference().property.Hops_twist_radial_sort
            self.displace_mod_two.show_render = addon.preference().property.Hops_twist_radial_sort

            self.displace_mod_two.mid_level = 0.0
            self.displace_mod_two.direction = 'X' if self.axis == 'Y' else 'Y'
            self.displace_mod_two.strength = -1.0
            self.displace_new_two = True

        if not self.weld_mod:
            try:
                self.weld_mod = self.obj.modifiers.new("Weld", 'WELD')
                self.weld_mod.show_expanded = False

                self.weld_mod.show_in_editmode = not addon.preference().property.Hops_twist_radial_sort
                self.weld_mod.show_render = addon.preference().property.Hops_twist_radial_sort

                self.weld_new = True

            except:
                self.report({'INFO'}, "Unable to add Weld modifier")

        self.displace_settings_one = {}
        self.displace_settings_one["direction"] = self.displace_mod_one.direction
        self.displace_settings_one["strength"] = self.displace_mod_one.strength

        self.array_settings = {}
        self.array_settings["use_constant_offset"] = self.array_mod.use_constant_offset
        self.array_settings["use_relative_offset"] = self.array_mod.use_relative_offset
        self.array_settings["use_object_offset"] = self.array_mod.use_object_offset
        self.array_settings["displace_x"] = self.array_mod.relative_offset_displace[0]
        self.array_settings["displace_y"] = self.array_mod.relative_offset_displace[1]
        self.array_settings["displace_z"] = self.array_mod.relative_offset_displace[2]
        self.array_settings["use_merge_vertices"] = self.array_mod.use_merge_vertices
        self.array_settings["fit_type"] = self.array_mod.fit_type
        self.array_settings["count"] = self.array_mod.count

        self.deform_settings = {}
        self.deform_settings["deform_method"] = self.deform_mod.deform_method
        self.deform_settings["deform_axis"] = self.deform_mod.deform_axis
        self.deform_settings["angle"] = self.deform_mod.angle

        self.displace_settings_two = {}
        self.displace_settings_two["direction"] = self.displace_mod_two.direction
        self.displace_settings_two["strength"] = self.displace_mod_two.strength

        if not self.array_new:
            self.merge = self.array_mod.use_merge_vertices

        self.array_mod.use_constant_offset = False
        self.array_mod.use_relative_offset = True
        self.array_mod.use_object_offset = False
        self.array_mod.relative_offset_displace = (1.0, 0.0, 0.0) if self.deform_mod.deform_axis == 'Z' else (0.0, 0.0, 1.0)
        self.array_mod.use_merge_vertices = self.merge

        if self.weld_mod:
            self.weld_settings = {}
            self.weld_settings["show_viewport"] = self.weld_mod.show_viewport
            self.weld_mod.show_viewport = self.merge

        self.axis = self.deform_mod.deform_axis
        self.count = self.count_buffer = self.array_mod.count
        self.displace = self.displace_buffer = self.displace_mod_one.strength
        self.angle = self.angle_buffer = math.degrees(self.deform_mod.angle)
        self.rotation = 0

        if self.duplicating:
            self.rotate(self.axis, 180)
            self.duplicating = False

        self.center(context)

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

        context.window_manager.modal_handler_add(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.pass_through:
            return {'PASS_THROUGH'}

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

        elif self.base_controls.confirm:
            self.confirm(context)
            self.report({'INFO'}, "Finished")
            self.remove_shader()
            collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
            self.master.run_fade()
            return {'FINISHED'}

        elif self.base_controls.cancel:
            self.cancel(context)
            self.report({'INFO'}, "Cancelled")
            self.remove_shader()
            collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
            self.master.run_fade()
            return {'CANCELLED'}

        elif event.shift and event.type == 'D' and event.value == 'RELEASE':
            # self.remove_draw_handler(context)
            self.confirm(context)
            self.remove_shader()
            collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
            self.master.run_fade()
            bpy.ops.object.duplicate(mode='INIT')
            bpy.ops.hops.display_notification(info="Duplicated")
            self.report({'INFO'}, "Duplicating")
            bpy.ops.hops.array_twist('INVOKE_DEFAULT', duplicating=True)
            return {'FINISHED'}

        elif self.base_controls.scroll:
            self.count_buffer += self.base_controls.scroll

            self.count_buffer = max(self.count_buffer, 1)
            self.count = round(self.count_buffer)
            self.array_mod.count = self.count
            self.center(context)
            self.report({'INFO'}, f"Segments: {self.array_mod.count}")

        elif event.type == 'MOUSEMOVE' and self.adjusting != 'NONE':
            if self.adjusting == 'ANGLE':

                self.angle_buffer += self.base_controls.mouse * 10
                self.angle_buffer = min(max(self.angle_buffer, 0), 360)
                self.angle = round(15 * round(self.angle_buffer / 15, 0) if event.ctrl else self.angle_buffer)
                self.deform_mod.angle = math.radians(self.angle)
                self.center(context)

            elif self.adjusting == 'DISPLACE':

                self.displace_buffer -= self.base_controls.mouse * 2.5
                digits = 2 if event.ctrl and event.shift else 1 if event.ctrl else 3
                self.displace = round(self.displace_buffer, digits)
                self.displace_mod_one.strength = self.displace
                self.center(context)

            elif self.adjusting == 'SEGMENTS':

                self.count_buffer += self.base_controls.mouse * 10
                self.count_buffer = max(self.count_buffer, 1)
                self.count = round(self.count_buffer)
                self.array_mod.count = self.count
                self.center(context)

        elif event.type in {'S', 'D', 'A'} and event.value == 'PRESS':
            if event.type == 'S':
                self.adjusting = 'NONE' if self.adjusting == 'SEGMENTS' else 'SEGMENTS'
            elif event.type == 'D' and not event.shift:
                self.adjusting = 'NONE' if self.adjusting == 'DISPLACE' else 'DISPLACE'
            elif event.type == 'A':
                self.adjusting = 'NONE' if self.adjusting == 'ANGLE' else 'ANGLE'

            if self.adjusting != 'NONE':
                self.report({'INFO'}, f"Adjusting {str(self.adjusting).capitalize()}")
            else:
                self.report({'INFO'}, "Stopped Adjusting")

        elif event.type == 'X' and event.value == 'PRESS':
            self.unrotate(context)
            self.axis = "YZX"["XYZ".find(self.axis)]
            self.displace_mod_one.direction = 'X' if self.axis == 'Y' else 'Y'
            self.array_mod.relative_offset_displace = (1.0, 0.0, 0.0) if self.axis == 'Z' else (0.0, 0.0, 1.0)
            self.deform_mod.deform_axis = self.axis
            self.displace_mod_two.direction = 'X' if self.axis == 'Y' else 'Y'
            self.center(context)
            self.report({'INFO'}, f"Deform Axis: {self.axis}")

        elif event.type in {'R', 'F'} and event.value == 'PRESS':
            angle = 90 if event.type == 'R' else 180
            self.rotation = (self.rotation + angle) % 360
            self.rotate(self.axis, angle)
            self.center(context)
            self.report({'INFO'}, f"Rotation: {self.rotation:.0f}")

        elif event.type == 'M' and event.value == 'PRESS' and event.shift == True:
            self.merge = not self.merge
            self.array_mod.use_merge_vertices = self.merge
            if self.weld_mod:
                self.weld_mod.show_viewport = self.merge
            self.report({'INFO'}, f"Merge: {'Enabled' if self.merge else 'Disabled'}")

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


    def rotate(self, axis, angle):
        bm = bmesh.new()
        bm.from_mesh(self.obj.data)

        matrix = mathutils.Matrix.Rotation(math.radians(angle), 4, axis)
        bmesh.ops.rotate(bm, cent=self.obj.location, matrix=matrix, verts=bm.verts, space=self.obj.matrix_world)

        bm.to_mesh(self.obj.data)
        bm.free()

    def unrotate(self, context):
        self.rotate(self.axis, -self.rotation)
        self.rotation = 0

    def center(self, context):
        temp = bpy.data.objects.new("Bounding Box", self.obj.data)
        bb = [mathutils.Vector(v) for v in temp.bound_box]
        bpy.data.objects.remove(temp)

        right = left = bb[7][0]
        front = back = bb[7][1]
        top = bottom = bb[7][2]

        for i in range(7):
            x = bb[i][0]
            if x > right:
                right = x
            if x < left:
                left = x

            y = bb[i][1]
            if y > front:
                front = y
            if y < back:
                back = y

            z = bb[i][2]
            if z > top:
                top = z
            if z < bottom:
                bottom = z

        middle_x = (right + left) * 0.5
        middle_y = (front + back) * 0.5
        middle_z = (top + bottom) * 0.5

        verts = [tuple(v) for v in bb]
        verts.append((right, middle_y, middle_z))
        verts.append((left, middle_y, middle_z))
        verts.append((middle_x, front, middle_z))
        verts.append((middle_x, back, middle_z))
        verts.append((middle_x, middle_y, top))
        verts.append((middle_x, middle_y, bottom))

        bm = bmesh.new()
        bm.from_mesh(self.obj.data)
        verts = [bm.verts.new(v) for v in verts]
        bm.to_mesh(self.obj.data)

        self.displace_mod_two.strength = 0.0
        context.view_layer.update()

        bb = self.obj.bound_box
        axis = 0 if self.axis == 'Y' else 1
        pos = neg = bb[7][axis]

        for i in range(7):
            val = bb[i][axis]
            if val > pos:
                pos = val
            if val < neg:
                neg = val

        offset = -0.5 * (pos + neg)
        self.displace_mod_two.strength = offset

        verts = [bm.verts.remove(v) for v in verts]
        bm.to_mesh(self.obj.data)
        bm.free()

    def confirm(self, context):
        context.space_data.overlay.show_overlays = True
        modifier.sort(self.obj, types=['WEIGHTED_NORMAL'], last=True)

        if addon.preference().property.workflow == 'DESTRUCTIVE':
            for mod in [self.displace_mod_one, self.array_mod, self.deform_mod, self.displace_mod_two, self.weld_mod]:
                if mod:
                    modifier.apply(self.obj, mod=mod)

    def cancel(self, context):
        context.space_data.overlay.show_overlays = True
        self.unrotate(context)

        if self.displace_new_one:
            self.obj.modifiers.remove(self.displace_mod_one)
        else:
            self.displace_mod_one.direction = self.displace_settings_one["direction"]
            self.displace_mod_one.strength = self.displace_settings_one["strength"]

        if self.array_new:
            self.obj.modifiers.remove(self.array_mod)
        else:
            self.array_mod.use_constant_offset = self.array_settings["use_constant_offset"]
            self.array_mod.use_relative_offset = self.array_settings["use_relative_offset"]
            self.array_mod.use_object_offset = self.array_settings["use_object_offset"]
            self.array_mod.relative_offset_displace[0] = self.array_settings["displace_x"]
            self.array_mod.relative_offset_displace[1] = self.array_settings["displace_y"]
            self.array_mod.relative_offset_displace[2] = self.array_settings["displace_z"]
            self.array_mod.use_merge_vertices = self.array_settings["use_merge_vertices"]
            self.array_mod.fit_type = self.array_settings["fit_type"]
            self.array_mod.count = self.array_settings["count"]

        if self.deform_new:
            self.obj.modifiers.remove(self.deform_mod)
        else:
            self.deform_mod.deform_method = self.deform_settings["deform_method"]
            self.deform_mod.deform_axis = self.deform_settings["deform_axis"]
            self.deform_mod.angle = self.deform_settings["angle"]

        if self.displace_new_two:
            self.obj.modifiers.remove(self.displace_mod_two)
        else:
            self.displace_mod_two.direction = self.displace_settings_two["direction"]
            self.displace_mod_two.strength = self.displace_settings_two["strength"]

        if self.weld_new:
            self.obj.modifiers.remove(self.weld_mod)
        elif self.weld_mod:
            self.weld_mod.show_viewport = self.weld_settings["show_viewport"]

    def draw_master(self, context):

        # Start
        self.master.setup()

        ########################
        #   Fast UI
        ########################

        if self.master.should_build_fast_ui():

            # Main
            win_list = []
            win_list.append("Twist 360")
            win_list.append(f"{self.axis}")
            win_list.append(f"{self.count}")
            win_list.append(f"{self.displace:.3f}")
            win_list.append(f"{self.angle:.0f}")

            # 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"] = [
                ("X",         "Increment Axis"),
                ("Scroll",    "Increment Segments"),
                ("S",         "Adjust Segments"),
                ("D",         "Adjust Displacement"),
                ("A",         "Adjust Angle"),
                ("R",         "Rotate 90 degrees"),
                ("F",         "Rotate 180 degrees"),
                ("Shift + D", "Duplicate"),
                ("M + Shift", "Toggle Merge")]

            # Mods
            active_mod = ""
            if self.array_mod != None:
                active_mod = self.array_mod.name
            mods_list = get_mods_list(mods=bpy.context.active_object.modifiers)

            self.master.receive_fast_ui(win_list=win_list, help_list=help_items, image="ATwist360", mods_list=mods_list, active_mod_name=active_mod)

        # Finished
        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)