OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / Gizmos / array.py

565 lines · python ref:
import bpy
import bpy_extras.view3d_utils
import gpu
from gpu_extras.batch import batch_for_shader
from bpy.props import FloatProperty
from bpy.types import (
    GizmoGroup,
    Operator,
    Gizmo
)
from mathutils import Matrix, Vector
from math import radians
from ... graphics.drawing2d import draw_text, set_drawing_dpi
from ... utils.blender_ui import get_dpi, get_dpi_factor
from ... utility import addon
from ... utils.objects import get_modifier_with_type
from .. Gizmos import custom_gizmo_shapes

from bl_ui.space_toolsystem_toolbar import VIEW3D_PT_tools_active as view3d_tools


class HOPS_OT_ArrayGizmo(Operator):
    bl_idname = "hops.array_gizmo"
    bl_label = "Array Gizmo"
    bl_options = {"REGISTER", "UNDO", "GRAB_CURSOR", "BLOCKING"}
    bl_description = """Interactive Array Gizmo
    
    Allows for a gizmo to be dragged to indicate distance.
    Has onscreen buttons for count adjustment.
    
    """

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

    def invoke(self, context, event):
        bpy.context.space_data.show_gizmo_tool = True
        bpy.context.space_data.show_gizmo = True
        
        current_tool = current_tool = view3d_tools.tool_active_from_context(bpy.context).idname
        self.current_tool = current_tool
        if current_tool != "BoxCutter":
            bpy.ops.wm.tool_set_by_id(name="builtin.select", space_type='VIEW_3D')

        if context.space_data.type == 'VIEW_3D':
            wm = context.window_manager
            wm.gizmo_group_type_ensure(HOPS_OT_HopsArrayGizmoGroup.bl_idname)

        addon.preference().property.Hops_gizmo = True
        if context.space_data.type == 'VIEW_3D':
            addon.preference().property.Hops_gizmo_mirror = True
            context.area.tag_redraw()

        ob = context.active_object
        array = get_modifier_with_type(ob, "ARRAY")
        if array is not None:
            if array.name == "Array":
                if array.use_relative_offset:

                    dimensions_x = ob.dimensions[0] / (abs(array.relative_offset_displace[0]) * (array.count - 1) + 1)
                    dimensions_y = ob.dimensions[1] / (abs(array.relative_offset_displace[1]) * (array.count - 1) + 1)
                    dimensions_z = ob.dimensions[2] / (abs(array.relative_offset_displace[2]) * (array.count - 1) + 1)

                    ob.hops.array_x = dimensions_x * array.relative_offset_displace[0]
                    ob.hops.array_y = dimensions_y * array.relative_offset_displace[1]
                    ob.hops.array_z = dimensions_z * array.relative_offset_displace[2]

                else:

                    ob.hops.array_x = array.constant_offset_displace[0]
                    ob.hops.array_y = array.constant_offset_displace[1]
                    ob.hops.array_z = array.constant_offset_displace[2]

        # bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
        # self.draw_handler = bpy.types.SpaceView3D.draw_handler_add(self.draw_ui, (context, ), "HEADER", "POST_PIXEL")
        context.area.header_text_set("Hardops Array    Add Array Modifier")
        context.window_manager.modal_handler_add(self)
        return {"RUNNING_MODAL"}

    def modal(self, context, event):
        if addon.preference().property.Hops_gizmo is False:
            bpy.ops.wm.tool_set_by_id(name=self.current_tool, space_type='VIEW_3D')
            context.window_manager.gizmo_group_type_unlink_delayed(HOPS_OT_HopsArrayGizmoGroup.bl_idname)
            context.area.header_text_set(text=None)
            # bpy.types.SpaceView3D.draw_handler_remove(self.draw_handler, "HEADER")
            return {"CANCELLED"}

        ob = context.active_object
        array = get_modifier_with_type(ob, "ARRAY")
        if array is None:
            array = ob.modifiers.new("Array", "ARRAY")
            array.use_constant_offset = False
            array.use_relative_offset = True
            array.count = 2
            array.show_expanded = False
            array.relative_offset_displace[0] = 0

        else:
            count = array.count
            relative = array.relative_offset_displace
            constant = array.constant_offset_displace
            context.area.header_text_set("Hardops Array    Count: {}    Relative: x: {:.3f}  y: {:.3f}  z: {:.3f}    Constant: x: {:.3f}  y: {:.3f}  z: {:.3f} ".format(count, relative[0], relative[1], relative[2], constant[0], constant[1], constant[2]))

        if event.type == 'MOUSEMOVE':
            return {"PASS_THROUGH"}

        if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
            return {"PASS_THROUGH"}

        if event.type == 'LEFTMOUSE':
            return {"PASS_THROUGH"}

        if event.type in ("ESC", "RIGHTMOUSE"):
            bpy.ops.wm.tool_set_by_id(name=self.current_tool, space_type='VIEW_3D')
            context.window_manager.gizmo_group_type_unlink_delayed(HOPS_OT_HopsArrayGizmoGroup.bl_idname)
            # bpy.types.SpaceView3D.draw_handler_remove(self.draw_handler, "HEADER")
            context.area.header_text_set(text=None)
            context.area.tag_redraw()
            # context.area.tag_redraw()
            return {"CANCELLED"}

        return {"RUNNING_MODAL"}

    def draw_ui(self, context):

        object = context.active_object

        location = object.location
        region = context.region
        rv3d = context.region_data

        x = 2
        y = 2

        set_drawing_dpi(get_dpi())
        factor = get_dpi_factor()

        vertices = (
            (x - 1 * factor, y + 23 * factor), (x - 1 * factor, y + 4 * factor), (x + 44 * factor, y + 23 * factor), (x + 44 * factor, y + 4 * factor),
            (x + 46 * factor, y + 23 * factor), (x + 46 * factor, y + 4 * factor), (x + 220 * factor, y + 23 * factor), (x + 220 * factor, y + 4 * factor),
            (x + 46 * factor, y + 42 * factor), (x + 46 * factor, y + 26 * factor), (x + 220 * factor, y + 42 * factor), (x + 220 * factor, y + 26 * factor))

        indices = (
            (0, 1, 2), (1, 2, 3), (4, 5, 6), (5, 6, 7), (8, 9, 10), (9, 10, 11))

        built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >=4 else '2D_UNIFORM_COLOR'
        shader = gpu.shader.from_builtin(built_in_shader)
        batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)

        shader.bind()
        shader.uniform_float("color", addon.preference().color.Hops_hud_color)
        gpu.state.blend_set('ALPHA')
        batch.draw(shader)
        gpu.state.blend_set('NONE')

        count = object.modifiers["Array"].count
        relative = object.modifiers["Array"].relative_offset_displace
        constant = object.modifiers["Array"].constant_offset_displace

        draw_text(str(count),
                  x + 27 * factor, y + 9 * factor, size=12, color=addon.preference().color.Hops_hud_text_color)

        draw_text("  X: {:.3f}  Y: {:.3f}  Z: {:.3f} ".format(relative[0], relative[1], relative[2]),
                  x + 50 * factor, y + 9 * factor, size=12, color=addon.preference().color.Hops_hud_text_color)

        draw_text("  X: {:.3f}  Y: {:.3f}  Z: {:.3f} ".format(constant[0], constant[1], constant[2]),
                  x + 50 * factor, y + 28 * factor, size=12, color=addon.preference().color.Hops_hud_text_color)


class HOPS_OT_HopsArrayGizmoGroup(GizmoGroup):
    bl_idname = "hops.array_gizmogroup2"
    bl_label = "Array Gizmo Group"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'WINDOW'
    bl_options = {'3D'}

    @staticmethod
    def my_target_operator(context):
        wm = context.window_manager
        op = wm.operators[-1] if wm.operators else None
        if isinstance(op, HOPS_OT_ArrayGizmo):
            return op
        return None

    @staticmethod
    def my_view_orientation(context):
        rv3d = context.space_data.region_3d
        view_inv = rv3d.view_matrix.to_3x3()
        return view_inv.normalized()

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

    def setup(self, context):
        # Run an operator using the dial gizmo
        ob = context.active_object

        mpr_x = self.gizmos.new("GIZMO_GT_arrow_3d")
        mpr_y = self.gizmos.new("GIZMO_GT_arrow_3d")
        mpr_z = self.gizmos.new("GIZMO_GT_arrow_3d")
        circle1 = self.gizmos.new(HOPS_GT_ArrayPlusShapeGizmo.bl_idname)
        circle1.target_set_operator("hops.array_plus")
        circle2 = self.gizmos.new(HOPS_GT_ArrayMinusShapeGizmo.bl_idname)
        circle2.target_set_operator("hops.array_minus")
        circle3 = self.gizmos.new(HOPS_GT_ArrayMinusShapeGizmo.bl_idname)
        circle3.target_set_operator("hops.array_minus")

        # mpr_x.matrix_basis = x_matrix_world.normalized()
        mpr_x.line_width = 0.1
        mpr_x.color = 1, 0.2, 0.322
        mpr_x.alpha = 0.5
        mpr_x.scale_basis = 1.3
        mpr_x.color_highlight = 1.0, 1.0, 1.0
        mpr_x.alpha_highlight = 1.0

        # mpr_y.matrix_basis = y_matrix_world.normalized()
        mpr_y.line_width = 0.1
        mpr_y.color = 0.545, 0.863, 0
        mpr_y.alpha = 0.5
        mpr_y.scale_basis = 1.3
        mpr_y.color_highlight = 1.0, 1.0, 1.0
        mpr_y.alpha_highlight = 1.0

        # mpr_z.matrix_basis = z_matrix_world.normalized()
        mpr_z.line_width = 0.1
        mpr_z.color = 0.157, 0.565, 1
        mpr_z.alpha = 0.5
        mpr_z.scale_basis = 1.3
        mpr_z.color_highlight = 1.0, 1.0, 1.0
        mpr_z.alpha_highlight = 1.0

        # circle1.matrix_basis = circle1_matrix.normalized()
        circle1.color = 0.4, 0.4, 0.4
        circle1.alpha = 0.5
        circle1.scale_basis = 0.17
        circle1.color_highlight = 1.0, 1.0, 1.0
        circle1.alpha_highlight = 1.0

        # circle2.matrix_basis = circle2_matrix.normalized()
        circle2.color = 0.4, 0.4, 0.4
        circle2.alpha = 0.5
        circle2.scale_basis = 0.17
        circle2.color_highlight = 1.0, 1.0, 1.0
        circle2.alpha_highlight = 1.0

        # circle3.matrix_basis = circle3_matrix.normalized()
        circle3.color = 0.4, 0.4, 0.4
        circle3.alpha = 0.5
        circle3.scale_basis = 0.17
        circle3.color_highlight = 1.0, 1.0, 1.0
        circle3.alpha_highlight = 1.0
        circle3.hide = True
        circle3.hide_select = True

        self.mpr_z = mpr_z
        self.mpr_x = mpr_x
        self.mpr_y = mpr_y
        self.circle1 = circle1
        self.circle2 = circle2
        self.circle3 = circle3

    def draw_prepare(self, context):
        ob = context.object

        array = get_modifier_with_type(ob, "ARRAY")

        orig_loc, orig_rot, orig_scale = ob.matrix_local.decompose()

        z_rot_mat = orig_rot.to_matrix().to_4x4()
        x_rot_mat = orig_rot.to_matrix().to_4x4() @ Matrix.Rotation(radians(90), 4, 'Y')
        y_rot_mat = orig_rot.to_matrix().to_4x4() @ Matrix.Rotation(radians(-90), 4, 'X')

        orig_scale_mat = Matrix.Scale(orig_scale[0], 4, (1, 0, 0)) @ Matrix.Scale(orig_scale[1], 4, (0, 1, 0)) @ Matrix.Scale(orig_scale[2], 4, (0, 0, 1))

        inv = ob.matrix_world.copy()
        inv.invert()

        location = ob.location
        region = context.region
        rv3d = context.region_data

        self.mpr_x.hide = True
        self.mpr_y.hide = True
        self.mpr_z.hide = True

        if array is not None:

            self.mpr_x.hide = False
            self.mpr_y.hide = False
            self.mpr_z.hide = False

            orig_loc_mat = Matrix.Translation(orig_loc)

            if array.use_relative_offset is False and array.use_constant_offset is True:

                offset_x = array.constant_offset_displace[0] * ob.scale[0]
                offset_y = array.constant_offset_displace[1] * ob.scale[1]
                offset_z = array.constant_offset_displace[2] * ob.scale[2]

            else:

                dimensions_x = ob.dimensions[0] / (abs(array.relative_offset_displace[0]) * (array.count - 1) + 1)
                dimensions_y = ob.dimensions[1] / (abs(array.relative_offset_displace[1]) * (array.count - 1) + 1)
                dimensions_z = ob.dimensions[2] / (abs(array.relative_offset_displace[2]) * (array.count - 1) + 1)

                offset_x = dimensions_x * array.relative_offset_displace[0] * ob.scale[0]
                offset_y = dimensions_y * array.relative_offset_displace[1] * ob.scale[1]
                offset_z = dimensions_z * array.relative_offset_displace[2] * ob.scale[2]


            location = ob.location + (Vector((offset_x, offset_y, offset_z)) @ inv)

            orig_loc_mat_offset_x = Matrix.Translation(orig_loc + ((Vector((offset_x, offset_y, offset_z)) * (array.count - 1)) @ inv))
            orig_loc_mat_offset_y = Matrix.Translation(orig_loc + ((Vector((offset_x, offset_y, offset_z)) * (array.count - 1)) @ inv))
            orig_loc_mat_offset_z = Matrix.Translation(orig_loc + ((Vector((offset_x, offset_y, offset_z)) * (array.count - 1)) @ inv))

            x_matrix_world = orig_loc_mat_offset_x @ x_rot_mat @ orig_scale_mat
            y_matrix_world = orig_loc_mat_offset_y @ y_rot_mat @ orig_scale_mat
            z_matrix_world = orig_loc_mat_offset_z @ z_rot_mat @ orig_scale_mat

            mpr_z = self.mpr_z
            mpr_x = self.mpr_x
            mpr_y = self.mpr_y

            mpr_x.target_set_operator("hops.mods_array").axis = 'X'
            mpr_y.target_set_operator("hops.mods_array").axis = 'Y'
            mpr_z.target_set_operator("hops.mods_array").axis = 'Z'

            mpr_z.matrix_basis = z_matrix_world.normalized()
            mpr_x.matrix_basis = x_matrix_world.normalized()
            mpr_y.matrix_basis = y_matrix_world.normalized()

        location_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(region, rv3d, location)
        circle1_offset = Vector((location_2d.x - 120, location_2d.y - 140))
        circle2_offset = Vector((location_2d.x - 180, location_2d.y - 140))
        circle3_offset = Vector((location_2d.x - 240, location_2d.y - 140))
        circle1_loc = bpy_extras.view3d_utils.region_2d_to_location_3d(region, rv3d, circle1_offset, location)
        circle2_loc = bpy_extras.view3d_utils.region_2d_to_location_3d(region, rv3d, circle2_offset, location)
        circle3_loc = bpy_extras.view3d_utils.region_2d_to_location_3d(region, rv3d, circle3_offset, location)
        circle1_matrix = Matrix.Translation(circle1_loc) @ z_rot_mat @ orig_scale_mat
        circle2_matrix = Matrix.Translation(circle2_loc) @ z_rot_mat @ orig_scale_mat
        circle3_matrix = Matrix.Translation(circle3_loc) @ z_rot_mat @ orig_scale_mat
        circle1 = self.circle1
        circle2 = self.circle2
        circle3 = self.circle3

        view_inv = self.my_view_orientation(context)

        rv3d = context.space_data.region_3d
        view_inv = rv3d.view_matrix.to_3x3()
        # view y axis
        plane_no = view_inv[1].normalized()

        op = self.my_target_operator(context)
        co = circle1_loc
        no = Vector(plane_no).normalized()

        circle1.matrix_basis = circle1_matrix.normalized()
        self.view_inv = view_inv
        self.rotate_axis = view_inv[2].xyz
        self.rotate_up = view_inv[1].xyz

        no_z = self.rotate_axis
        no_y = (no - (no.project(no_z))).normalized()
        no_x = self.rotate_axis.cross(no_y)

        matrix = circle1.matrix_basis
        matrix.identity()
        matrix.col[0].xyz = no_x
        matrix.col[1].xyz = no_y
        matrix.col[2].xyz = no_z
        matrix.col[3].xyz = co

        co = circle2_loc

        circle2.matrix_basis = circle2_matrix.normalized()
        self.view_inv = view_inv
        self.rotate_axis = view_inv[2].xyz
        self.rotate_up = view_inv[1].xyz

        no_z = self.rotate_axis
        no_y = (no - (no.project(no_z))).normalized()
        no_x = self.rotate_axis.cross(no_y)

        matrix = circle2.matrix_basis
        matrix.identity()
        matrix.col[0].xyz = no_x
        matrix.col[1].xyz = no_y
        matrix.col[2].xyz = no_z
        matrix.col[3].xyz = co

        circle3.matrix_basis = circle3_matrix.normalized()
        self.view_inv = view_inv
        self.rotate_axis = view_inv[2].xyz
        self.rotate_up = view_inv[1].xyz

        co = circle3_loc

        no_z = self.rotate_axis
        no_y = (no - (no.project(no_z))).normalized()
        no_x = self.rotate_axis.cross(no_y)

        matrix = circle3.matrix_basis
        matrix.identity()
        matrix.col[0].xyz = no_x
        matrix.col[1].xyz = no_y
        matrix.col[2].xyz = no_z
        matrix.col[3].xyz = co


class HopsArrayExecuteXmGizmo(Operator):
    bl_idname = "hops.gizmo_array_x"
    bl_label = "Array X"
    bl_options = {'REGISTER', 'UNDO'} # internal

    def execute(self, context):
        ob = context.active_object

        array_modifier = None
        for modifier in ob.modifiers:
            if modifier.type == "ARRAY":
                array_modifier = modifier
                modifier.use_constant_offset = True
                modifier.use_relative_offset = False
                modifier.count = modifier.count + 1

        if array_modifier is None:
            array_modifier = ob.modifiers.new("Array", "ARRAY")
            array_modifier.use_constant_offset = True
            array_modifier.use_relative_offset = False

        return {'FINISHED'}

    def modifiers_by_name(self, obj):
        return [x for x in obj.modifiers if x.type in {"ARRAY"}]


class HOPS_GT_ArrayMinusShapeGizmo(Gizmo):
    bl_idname = "VIEW3D_GT_auto_minus"
    bl_target_properties = (
        {"id": "offset", "type": 'FLOAT', "array_length": 1},
    )

    __slots__ = (
        "custom_shape",
        "init_mouse_y",
        "init_value",
    )

    def draw(self, context):

        self.draw_custom_shape(self.custom_shape)

    def draw_select(self, context, select_id):
        self.draw_custom_shape(self.new_custom_shape('TRIS', custom_gizmo_shapes.cube), select_id=select_id)

    def setup(self):
        if not hasattr(self, "custom_shape"):
            self.custom_shape = self.new_custom_shape('TRIS', custom_gizmo_shapes.minus)

    def invoke(self, context, event):
        self.init_mouse_y = event.mouse_y
        return {'RUNNING_MODAL'}

    def exit(self, context, cancel):
        context.area.header_text_set(None)

    def modal(self, context, event, tweak):
        delta = (event.mouse_y - self.init_mouse_y) / 10.0
        if 'SNAP' in tweak:
            delta = round(delta)
        if 'PRECISE' in tweak:
            delta /= 10.0
        return {'RUNNING_MODAL'}


class HOPS_GT_ArrayPlusShapeGizmo(Gizmo):
    bl_idname = "VIEW3D_GT_auto_plus"
    bl_target_properties = (
        {"id": "offset", "type": 'FLOAT', "array_length": 1},
    )

    __slots__ = (
        "custom_shape",
        "init_mouse_y",
        "init_value",
    )

    def draw(self, context):

        self.draw_custom_shape(self.custom_shape)

    def draw_select(self, context, select_id):
        self.draw_custom_shape(self.new_custom_shape('TRIS', custom_gizmo_shapes.cube), select_id=select_id)

    def setup(self):
        if not hasattr(self, "custom_shape"):
            self.custom_shape = self.new_custom_shape('TRIS', custom_gizmo_shapes.plus)

    def invoke(self, context, event):
        self.init_mouse_y = event.mouse_y
        return {'RUNNING_MODAL'}

    def exit(self, context, cancel):
        context.area.header_text_set(None)

    def modal(self, context, event, tweak):
        delta = (event.mouse_y - self.init_mouse_y) / 10.0
        if 'SNAP' in tweak:
            delta = round(delta)
        if 'PRECISE' in tweak:
            delta /= 10.0
        return {'RUNNING_MODAL'}


class HOPS_OT_ArrayPlus(Operator):
    bl_idname = "hops.array_plus"
    bl_label = "Add Array Step"
    bl_description = "+"
    bl_options = {'REGISTER', 'UNDO'} # internal

    def execute(self, context):
        ob = context.object
        array = get_modifier_with_type(ob, "ARRAY")
        if array is None:
            array = ob.modifiers.new("Array", "ARRAY")
            array.use_constant_offset = True
            array.use_relative_offset = False
            array.count = 2
            array.show_expanded = False

        else:
            array.count += 1

        return {'FINISHED'}


class HOPS_OT_ArrayMinus(Operator):
    bl_idname = "hops.array_minus"
    bl_label = "Remove Array Step"
    bl_description = "-"
    bl_options = {'REGISTER', 'UNDO'} # internal

    def execute(self, context):
        ob = context.object
        array = get_modifier_with_type(ob, "ARRAY")
        if array is not None:
            if array.count == 2:
                for m in ob.modifiers:
                    if(m.name == "Array"):
                        ob.modifiers.remove(m)

                ob.hops.array_x = 0
                ob.hops.array_y = 0
                ob.hops.array_z = 0
            else:
                array.count -= 1

        context.area.tag_redraw()
        return {'FINISHED'}