OverviewFilesCommitsIssuesDocsReleasesPeers

hardops / operators / modals / st3_array.py

2024 lines · python ref:
import bpy, math, gpu
from math import cos, sin
from enum import Enum
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from ... utility import addon
from ... utility.base_modal_controls import Base_Modal_Controls
from ... ui_framework.master import Master
from ... ui_framework import form_ui as form
from ... ui_framework.utils.mods_list import get_mods_list
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 ... utils.space_3d import get_3D_point_from_mouse
from ... utils.gizmo_axial import Axial
from ... utils.mod_controller import Mod_Controller
from ... utility import method_handler
from ...utility.screen import dpi_factor


DESC = """Array V2

CTRL - New Array

Adds an array on the mesh.
Supports multiple modifiers.
V during modal for 2d / 3d mode.

Press H for help"""


class Widget:
    def __init__(self, event):
        # Dims
        self.x_offset = event.mouse_region_x
        self.y_offset = event.mouse_region_y
        self.loc = Vector((0,0))
        self.radius = 26 * dpi_factor()
        # Shader
        built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >=4 else '2D_UNIFORM_COLOR'
        self.shader = gpu.shader.from_builtin(built_in_shader)
        # Circle
        self.circle_setup(event)
        # State
        self.mouse_is_over = False


    def circle_setup(self, event):
        self.x_offset = event.mouse_region_x
        self.y_offset = event.mouse_region_y
        self.loc = Vector((self.x_offset, self.y_offset))
        self.circle_batch = None
        self.circle_color = (1,1,1,.06)
        self.circle_verts = []
        self.circle_indices = []
        segments = 32
        for i in range(segments):
            index = i + 1
            angle = i * 6.28318 / segments
            x = cos(angle) * self.radius
            y = sin(angle) * self.radius
            vert = Vector((x, y))
            vert += self.loc
            self.circle_verts.append(vert)
            if(index == segments): self.circle_indices.append((i, 0))
            else: self.circle_indices.append((i, i + 1))
        summed = Vector((0,0))
        for p in self.circle_verts:
            summed += p
        self.circle_center = summed / len(self.circle_verts)
        self.circle_batch = batch_for_shader(self.shader, 'LINES', {'pos': self.circle_verts}, indices=self.circle_indices)


    def update(self, context, event):
        # Circle
        mouse_pos = Vector((event.mouse_region_x, event.mouse_region_y))
        if (mouse_pos - self.circle_center).magnitude <= self.radius:
            self.mouse_is_over = True
            self.circle_color = (1,1,1,.08)
        else:
            self.mouse_is_over = False
            self.circle_color = (1,1,1,.06)


    def draw(self):
        # GL Settings
        #Enable(GL_LINE_SMOOTH)
        gpu.state.blend_set('ALPHA')
        gpu.state.line_width_set(3)
        # Circle
        self.shader.bind()
        self.shader.uniform_float('color', self.circle_color)
        self.circle_batch.draw(self.shader)
        # GL Settings
        #Disable(GL_LINE_SMOOTH)
        gpu.state.blend_set('NONE')


class Axis(Enum):
    X = 0
    Y = 1
    Z = 2


class Edit_Space(Enum):
    View_2D = 1
    View_3D = 2

# set controller index and notification
def mod_name_update(self, context):
    op = HOPS_OT_ST3_Array.operator
    if not op: return

    valid = op.mod_controller.set_active_obj_mod_index(op.mod_selected)
    if valid:
        bpy.ops.hops.display_notification(info=f'Target Array : {op.mod_selected}')
        op.set_initial_axis()

class HOPS_OT_ST3_Array(bpy.types.Operator):
    bl_idname = "hops.st3_array"
    bl_label = "Adjust Array V2"
    bl_description = DESC
    bl_options = {"REGISTER", "UNDO", "BLOCKING"}

    exit_with_empty_driver: bpy.props.BoolProperty("Exit with empty", name='Driver', description='Crate empty with a driver')

    # Popover
    operator = None
    mod_selected: bpy.props.StringProperty(update=mod_name_update)
    array_index: bpy.props.IntProperty(name='ArrayIndex')
    popover_active = False

    @classmethod
    def poll(cls, context):
        objs = context.selected_objects

        types = {bpy.types.Mesh}

        if hasattr(bpy.types, 'GreasePencil'):
            types.add(bpy.types.GreasePencil)

        if hasattr(bpy.types, 'GreasePencilv3'):
            types.add(bpy.types.GreasePencilv3)

        return any(type(o.data) in types for o in objs if hasattr(o, 'data'))


    def invoke(self, context, event):

        # UI Data
        self.drawing_radius = 0            # The radius for the circle gizmo
        self.show_guides = False           # Show graphical guides
        self.defualt_object_dims = (0,0,0) # The defualt objects dimensions before array modifiers
        self.circle_loc_3d = (0,0,0)       # The intersection poing of the mouse

        # Controler Vars
        self.intersection_point = Vector((0,0,0)) # Where the mouse is in 3D space : Used to draw the circle gizmo
        self.mouse_x_offset_2D = 0                # Tracked for 2D offseting
        self.mouse_accumulation = 0               # This is used as a sudo timer for 2D offset snaps
        self.axis = Axis.X                        # The current axis to offset with
        self.edit_space = Edit_Space.View_2D      # The offset space
        self.freeze_controls = False              # Used to freeze the mouse
        self.use_snaps = False                    # Used to offset by object dimensions
        self.exit_with_empty_driver = False       # If an empty should be calculated for the arrays (Drivers)
        self.exit_with_empty_obj = False

        # Setup
        types = {bpy.types.Mesh}
        type_map = {bpy.types.Mesh : 'ARRAY'}

        if hasattr(bpy.types, 'GreasePencil'):
            types.add(bpy.types.GreasePencil)
            type_map[bpy.types.GreasePencil] = 'GP_ARRAY'

        if hasattr(bpy.types, 'GreasePencilv3'):
            types.add(bpy.types.GreasePencilv3)
            type_map[bpy.types.GreasePencilv3] = 'GREASE_PENCIL_ARRAY'

        objs = context.selected_objects
        objs = [o for o in objs if hasattr(o, 'data') and type(o.data) in types]

        self.mod_controller = Mod_Controller(context, objs, type_map, create_new=event.ctrl, active_obj=context.active_object)
        self.setup_objects(context)
        self.set_initial_axis(created_new=event.ctrl)

        # Offset Data
        self.sync_to_active = True
        self.sync_object = None
        self.offset_data = {} # KEY : obj, VAL : difference vector
        self.setup_offset_sync_data(context, objs)

        # OP Prefs settings
        if addon.preference().property.array_v2_use_2d == '3D':
            self.edit_space = Edit_Space.View_3D

        self.popup_style = addon.preference().property.in_tool_popup_style

        # Widgets : Deadzone
        self.widget = Widget(event)
        self.deadzone_previous = False
        self.deadzone_active = True

        # Widgets : Axial
        self.axial = Axial()

        # Widget : Form
        self.__x_offset = 0
        self.__y_offset = 0
        self.__z_offset = 0
        self.__count = 0
        self.index_button = None
        self.form_exit = False
        self.form = None
        self.setup_form(context, event)

        # Base Systems
        self.master = Master(context=context)
        self.master.only_use_fast_ui = True
        self.base_controls = Base_Modal_Controls(context, event, popover_keys=['SPACE'])
        self.original_tool_shelf, self.original_n_panel = collapse_3D_view_panels()
        self.draw_handle_2D = bpy.types.SpaceView3D.draw_handler_add(self.safe_draw_2D, (context,), 'WINDOW', 'POST_PIXEL')
        self.draw_handle_3D = bpy.types.SpaceView3D.draw_handler_add(self.safe_draw_3D, (context,), 'WINDOW', 'POST_VIEW')

        # Popover
        self.__class__.operator = self
        self.__class__.mod_selected = ""

        context.window_manager.modal_handler_add(self)
        return {"RUNNING_MODAL"}

    @property
    def x_offset(self):
        return round(self.__x_offset, 3)

    @x_offset.setter
    def x_offset(self, val):
        self.set_offset(index=0, val=val)
        self.__x_offset = val

    @property
    def y_offset(self):
        return round(self.__y_offset, 3)

    @y_offset.setter
    def y_offset(self, val):
        self.set_offset(index=1, val=val)
        self.__y_offset = val

    @property
    def z_offset(self):
        return round(self.__z_offset, 3)

    @z_offset.setter
    def z_offset(self, val):
        self.set_offset(index=2, val=val)
        self.__z_offset = val

    @property
    def count(self):
        return self.__count

    @count.setter
    def count(self, val):
        self.set_array_count(val)
        self.__count = val


    def modal(self, context, event):

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

        if self.edit_space == Edit_Space.View_2D and not self.__class__.popover_active:
            mouse_warp(context, event)

        self.props_update()

        self.mouse_x_offset_2D = self.base_controls.mouse
        if self.popup_style == 'DEFAULT':
            self.form.update(context, event)

        if not self.form.active():
            self.axial.update(context, event, self.axial_callback)

        # Popover
        self.popover(context)

        if self.__class__.popover_active:
            self.interface(context)
            context.area.tag_redraw()
            return {'RUNNING_MODAL'}

        # --- Base Controls --- #
        if self.base_controls.pass_through:
            if not self.form.active():
                return {'PASS_THROUGH'}

        elif self.base_controls.cancel:
            return self.cancel_exit(context)

        elif self.base_controls.confirm:
            if not self.form.active():
                return self.confirm_exit(context)

        elif self.form_exit:
            return self.confirm_exit(context)

        # Close help when menu opens / Freeze modal if menu opens
        if self.form.db.menu_just_opened or self.form.db.dot_dragging:
            self.master.collapse_fast_help()
            self.freeze_controls = True

        # Switch to 2D adjust if menu is open
        if self.form.db.menu_just_opened:
            self.edit_space = Edit_Space.View_2D

        if not self.form.active():
            return self.actions(context, event)

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


    def actions(self, context, event):

        # Toggle 2D / 3D manipulation
        if event.type == 'V' and event.value == "PRESS":
            self.widget.mouse_is_over = False
            self.deadzone_previous = False
            self.deadzone_active = True
            self.widget.circle_setup(event)

            if self.edit_space == Edit_Space.View_2D:
                self.edit_space = Edit_Space.View_3D
            elif self.edit_space == Edit_Space.View_3D:
                self.edit_space = Edit_Space.View_2D

        # Cycle the current array : W
        elif event.type == 'W' and event.value == "PRESS":
            self.goto_next_array()

        # Drop arrays with empty
        elif event.type == 'E' and event.value == "PRESS":
            if event.shift:
                self.exit_with_empty_driver = False
                self.exit_with_empty_target_toggle()
            else:
                self.exit_with_empty_obj = False
                self.exit_with_empty_driver_toggle()

        # Toggle perspective
        elif event.type == 'P' or event.type == 'NUMPAD_5':
            if event.value == 'PRESS':
                bpy.ops.view3d.view_persportho()

        # Toggle freeze controls
        elif event.type in {'F', 'TAB'} and event.value == "PRESS":
            self.freeze_controls = not self.freeze_controls

            if self.freeze_controls:
                self.form.open_dot()

                if self.popup_style == 'BLENDER':
                    bpy.ops.hops.st3_array_popup('INVOKE_DEFAULT')
                    self.__class__.popover_active = True
            else:
                self.form.close_dot()

        # Toggle graphics guides
        elif event.type == 'G' and event.value == "PRESS":
            self.show_guides = not self.show_guides

        # Toggle Axis : If CTRL -> Clear the axis
        elif event.type == 'X' and event.value == "PRESS":
            if event.ctrl == True:
                self.toggle_axis(clear_axis_on_change=False)
                if addon.preference().ui.Hops_extra_info:
                    bpy.ops.hops.display_notification(info=f"Axis Switched To: {self.axis.name}")
            else:
                self.toggle_axis(clear_axis_on_change=True)
                if addon.preference().ui.Hops_extra_info:
                    bpy.ops.hops.display_notification(info=f"Axis Reset To: {self.axis.name}")

        # Toggle snap mode
        elif event.type == 'S' and event.value == "PRESS" and not event.shift:
            self.mouse_accumulation = 0
            self.use_snaps = not self.use_snaps

        # Toggle snap mode
        elif event.type == 'S' and event.value == "PRESS" and event.shift:
            if self.sync_object:
                self.sync_to_active = not self.sync_to_active
                bpy.ops.hops.display_notification(info=F"Sync to Active : {'ON' if self.sync_to_active else 'OFF'}")

        # Add Array
        elif event.type == 'A' and event.value == "PRESS" and event.alt == False:
            self.add_an_array()

        # Remove Array
        elif event.type == 'A' and event.value == "PRESS" and event.alt == True:
            self.remove_an_array()

        # Toggle Relative / Constant
        elif event.type == 'R' and event.value == "PRESS":
            self.toggle_relative_constant()

        # Set every thing to one
        elif event.type in {'ONE', 'NUMPAD_1'} and event.value == "PRESS":
            self.widget.mouse_is_over = False
            self.deadzone_previous = False
            self.deadzone_active = True
            self.widget.circle_setup(event)

            if event.shift == True:
                self.set_arrays_to_one(negative=True)
            else:
                self.set_arrays_to_one(negative=False)

            if addon.preference().ui.Hops_extra_info:
                if self.edit_space == Edit_Space.View_3D and not self.deadzone_active:
                    bpy.ops.hops.display_notification(info=f"Not Possible In 3D")
                else:
                    bpy.ops.hops.display_notification(info=f"Set to 1")

        # Set to 0
        elif event.type in {'ZERO', 'NUMPAD_0'} and event.value == "PRESS":
            self.widget.mouse_is_over = False
            self.deadzone_previous = False
            self.deadzone_active = True
            self.widget.circle_setup(event)
            self.set_arrays_to_zero()

        if self.base_controls.scroll:
            # Increment / Decrement / Axis change scrolling
            if not any((event.shift, event.ctrl, event.alt)):
                if self.freeze_controls == False:
                    self.increment_decrement_count(count=self.base_controls.scroll)

            # Ctrl Scrolling for axis change
            if event.alt:
                self.toggle_axis(clear_axis_on_change=True)
                self.set_arrays_to_one(negative=False)
                if addon.preference().ui.Hops_extra_info:
                    bpy.ops.hops.display_notification(info=f"Axis Reset To: {self.axis.name}")

            # Move mod up or down
            if event.shift == True:
                if self.base_controls.scroll > 0:
                    self.move_mod(context, up=True)
                if self.base_controls.scroll < 0:
                    self.move_mod(context, up=False)

            # Cycle the current array : Ctrl Scroll
            if event.ctrl == True:
                self.goto_next_array()

        # This is to smooth the mouse snapping motion
        if self.use_snaps:
            if self.edit_space == Edit_Space.View_2D:
                self.mouse_accumulation += self.base_controls.mouse

        # Navigation for frozen state
        if self.freeze_controls == True:
            if not event.ctrl and not event.shift:
                    if event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
                        return {'PASS_THROUGH'}

        # Adjust Displace
        if self.deadzone_active == False and self.freeze_controls == False:
            # 2D
            if self.edit_space == Edit_Space.View_2D:
                if event.type == "MOUSEMOVE":
                    if event.ctrl == True:
                        self.adjust_2D(accelerated=True)
                    else:
                        self.adjust_2D(accelerated=False)
            # 3D
            elif self.edit_space == Edit_Space.View_3D:
                self.adjust_3D(context=context, event=event)

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


    def props_update(self):
        self.index_button.text = str(self.mod_controller.active_obj_mod_index() + 1)
        mod = self.mod_controller.active_object_mod()

        constant_offset = mod_constant_offset_object(mod)
        relative_offset = mod_relative_offset_object(mod)

        offset = (0,0,0)
        if mod.use_constant_offset: offset = constant_offset
        else: offset = relative_offset

        self.__x_offset = offset[0]
        self.__y_offset = offset[1]
        self.__z_offset = offset[2]
        self.__count = mod.count


    def interface(self, context):

        obj_data = self.mod_controller.active_object_mod(as_obj_data=True)
        obj = obj_data.obj
        mod = obj_data.active_mod()
        offset_type = "Relative"
        if mod.use_constant_offset:
            offset_type = "Constant"
        array_count = mod.count

        constant_offset = mod_constant_offset_object(mod)
        relative_offset = mod_relative_offset_object(mod)

        offset = (0,0,0)
        if mod.use_constant_offset: offset = constant_offset
        else: offset = relative_offset

        self.master.setup()
        if not self.master.should_build_fast_ui(): return

        win_list = []
        mods_list = []

        # Main
        if addon.preference().ui.Hops_modal_fast_ui_loc_options != 1: # Micro UI
            win_list.append(str(int(array_count)))
            if self.axis.name == 'X':
                win_list.append("X : {:.3f}".format(offset[0]))
            elif self.axis.name == 'Y':
                win_list.append("Y : {:.3f}".format(offset[1]))
            elif self.axis.name == 'Z':
                win_list.append("Z : {:.3f}".format(offset[2]))
            win_list.append(self.edit_space.name[-2:])
            if offset_type == "Relative":
                win_list.append("R")
            elif offset_type == "Constant":
                win_list.append("C")
            if self.freeze_controls:
                win_list.append("[F] Unpause")

        else:
            win_list.append(str(int(array_count)))
            win_list.append(self.axis.name)
            win_list.append("  X: {:.3f}  Y: {:.3f}  Z: {:.3f} ".format(offset[0], offset[1], offset[2]))
            win_list.append(self.edit_space.name[-2:])
            win_list.append(offset_type)
            if self.freeze_controls:
                win_list.append("[F] Unpause")
            else:
                win_list.append("[F] Pause / DotUI")

        # 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"] = [
            ("Scroll",         "Adjust segments."),
            ("Shift + S",      "Sync To Active"),
            ("S",              "Toggle Snap Mode"),
            ("P / 5",          "Toggle Perspective"),
            ("W",              "Goto Next Array"),
            ("Ctrl + Scroll",  "Goto Next Array"),
            ("Shift + Scroll", "Move Modifier Up / Down"),
            ("Alt + Scroll",   "Axial scroll"),
            ("R",              "Toggle Relative / Constant"),
            ("1 / Shift",      "Set to 1 or -1 on current Axis"),
            ("0",              "Set to 0 on current Axis"),
            ("G",              "Toggle Graphics"),
            ("Alt + A",        "Remove Active Array"),
            ("A",              f"Add New Array"),
            ("Ctrl + X",       "Toggle Axis"),
            ("X",              "Toggle Axis & Clear Current"),
            ("V",              f"Toggle {(str(self.edit_space)[-2:])} Mode"),
            ("E",              "Exiting with empty as driver" if self.exit_with_empty_driver else "Set exit with empty as driver"),
            ("Shift E",        "Exiting with empty as target" if self.exit_with_empty_obj else "Set exit with empty as target"),
            ("C" if context.preferences.inputs.use_mouse_emulate_3_button else "Alt", "Open Axial Change"),
            ("F / TAB",        "Unpause / Freeze Rotation" if self.freeze_controls else "Pause (DotUI) / Free Rotation")
        ]

        if 'SPACE' in self.base_controls.popover_keys:
            help_items["STANDARD"].append(('Space', 'Open Select Menu'))

        # Mods
        mods_list = get_mods_list(mods=obj.modifiers)

        self.master.receive_fast_ui(win_list=win_list, help_list=help_items, image="Array", mods_list=mods_list, active_mod_name=mod.name)
        self.master.finished()

    # --- FORM FUNCS --- #

    def setup_form(self, context, event):
        self.form = form.Form(context, event, dot_open=False)

        self.form.dot_calls(
            LR_callback=self.dot_adjust_count,
            LR_args=(True,),
            UD_callback=self.dot_adjust_count,
            UD_args=(False,),
            tips=["Click : Open / Close Controls", "Drag Left / Right: Add to Count", "Drag Up / Down : Subtract from Count"])

        row = self.form.row()
        row.add_element(form.Label(text="Array V2", width=60))

        tip = ["Scroll / Click", "Active Array to effect."]
        self.index_button = form.Button(text="0", width=20, callback=self.mod_index_move, pos_args=(True,), neg_args=(False,), tips=tip)
        row.add_element(self.index_button)
        row.add_element(form.Spacer(width=5))

        row.add_element(form.Button(text="P", width=20, tips=["Toggle Pause Mode"], callback=self.toggle_pause))
        row.add_element(form.Button(text="X", width=20, tips=["Finalize and Exit"], callback=self.exit_button))
        self.form.row_insert(row)

        row = self.form.row()
        row.add_element(form.Spacer(height=10))
        self.form.row_insert(row)

        tips = ["Click - Set array to 1", "Shift Click - Add 1 to array", "Alt Click - Set array to one and others to 0"]

        row = self.form.row()
        row.add_element(form.Label(text="X", width=30))
        row.add_element(form.Input(obj=self, attr="x_offset", width=75, increment=.1))
        row.add_element(form.Button(text="1", width=20, tips=tips, callback=self.set_to_one, pos_args=(False, 'X'), neg_args=(True, 'X'), alt_callback=self.set_others_to_one, alt_args=('X',)))
        self.form.row_insert(row)

        row = self.form.row()
        row.add_element(form.Label(text="Y", width=30))
        row.add_element(form.Input(obj=self, attr="y_offset", width=75, increment=.1))
        row.add_element(form.Button(text="1", width=20, tips=tips, callback=self.set_to_one, pos_args=(False, 'Y'), neg_args=(True, 'Y'), alt_callback=self.set_others_to_one, alt_args=('Y',)))
        self.form.row_insert(row)

        row = self.form.row()
        row.add_element(form.Label(text="Z", width=30))
        row.add_element(form.Input(obj=self, attr="z_offset", width=75, increment=.1))
        row.add_element(form.Button(text="1", width=20, tips=tips, callback=self.set_to_one, pos_args=(False, 'Z'), neg_args=(True, 'Z'), alt_callback=self.set_others_to_one, alt_args=('Z',)))
        self.form.row_insert(row)

        row = self.form.row()
        row.add_element(form.Spacer(width=125, height=15, draw_bar=True))
        self.form.row_insert(row)

        row = self.form.row()
        row.add_element(form.Dropdown(width=35, options=['X', 'Y', 'Z'], tips=["Active Axis"], callback=self.set_axis, update_hook=self.get_axis_hook))
        row.add_element(form.Input(obj=self, attr="count", tips=["Array Count"], width=35, increment=1))
        row.add_element(form.Button(text="F", width=20, tips=["Flip the current Array"], callback=self.flip_array))
        row.add_element(form.Button(text="+", shift_text="-", width=20, font_size=14, tips=["Click - Add array", "Shift Click - Remove array"], callback=self.add_remove_array, pos_args=(False,), neg_args=(True,)))
        self.form.row_insert(row)

        row = self.form.row()
        row.add_element(form.Button(text="E", width=20, tips=["Exit with Empty"], callback=self.exit_with_empty_driver_toggle))
        row.add_element(form.Button(text="M", width=20, scroll_enabled=True, tips=["Click - Move mod up", "Shift Click - Move mod down"], callback=self.move_mod, pos_args=(context, True), neg_args=(context, False)))
        row.add_element(form.Dropdown(width=70, options=['Relative', 'Constant'], tips=["Offset type"], callback=self.set_relative_constant, update_hook=self.get_offset_hook))
        self.form.row_insert(row)

        self.form.build()


    def mod_index_move(self, forward=True):
        self.mod_controller.clamped_next_mod_index(forward)


    def toggle_pause(self):
        self.freeze_controls = not self.freeze_controls


    def set_relative_constant(self, option=''):
        for mod in self.mod_controller.active_modifiers():
            if option == 'Relative':
                mod.use_constant_offset = False
                mod.use_relative_offset = True
            elif option == 'Constant':
                mod.use_constant_offset = True
                mod.use_relative_offset = False


    def get_offset_hook(self):
        obj_data = self.mod_controller.active_object_mod(as_obj_data=True)
        if not obj_data: return 'Constant'
        mod = obj_data.active_mod()

        if mod.use_relative_offset: return 'Relative'
        elif mod.use_constant_offset: return 'Constant'
        else: return 'Constant'


    def set_offset(self, index=0, val=0):
        for mod in self.mod_controller.active_modifiers():
            constant_offset = mod_constant_offset_object(mod)
            relative_offset = mod_relative_offset_object(mod)

            if mod.use_constant_offset == True:
                constant_offset[index] = val
            else:
                relative_offset[index] = val


    def set_array_count(self, val=0):
        obj_datas = self.mod_controller.validated_obj_datas()
        for obj_data in obj_datas:
            mod = obj_data.active_mod()

            constant_offset = mod_constant_offset_object(mod)
            relative_offset = mod_relative_offset_object(mod)

            if self.edit_space == Edit_Space.View_3D and self.intersection_point:
                index = self.axis.value
                mod.count = int(val)

                if mod.use_constant_offset == True:
                    distance = self.intersection_point[index]
                    count = mod.count - 1
                    if count < 1: count = 1
                    vec = distance / count
                    constant_offset[index] = vec

                else:
                    dims = obj_data.dims
                    obj = obj_data.obj
                    offset = self.intersection_point[index] / (dims[index] * obj.scale[index]) if dims[index] > 0 else 1
                    offset /= mod.count - 1 if mod.count - 1 > 0 else 1
                    relative_offset[index] = offset

            else:
                mod.count = int(val)


    def set_axis(self, option=''):
        if option == 'X': self.axis = Axis.X
        elif option == 'Y': self.axis = Axis.Y
        elif option == 'Z': self.axis = Axis.Z


    def get_axis_hook(self):
        if self.axis == Axis.X: return 'X'
        if self.axis == Axis.Y: return 'Y'
        if self.axis == Axis.Z: return 'Z'


    def set_adjust_mode(self, option=''):
        if option == '2D': self.edit_space = Edit_Space.View_2D
        elif option == '3D': self.edit_space = Edit_Space.View_3D
        bpy.ops.hops.display_notification(info=f"Edit mode : {self.edit_space.name}")


    def get_adjust_hook(self):
        if self.edit_space == Edit_Space.View_2D: return '2D'
        if self.edit_space == Edit_Space.View_3D: return '3D'


    def exit_with_empty_driver_toggle(self):
        self.exit_with_empty_driver = not self.exit_with_empty_driver
        msg = "Exiting with empty (Driver)" if self.exit_with_empty_driver else "No empty will be added (Driver)"
        bpy.ops.hops.display_notification(info=msg)


    def exit_with_empty_target_toggle(self):
        self.exit_with_empty_obj = not self.exit_with_empty_obj
        msg = "Exiting with empty (Target)" if self.exit_with_empty_obj else "No empty will be added (Target)"
        bpy.ops.hops.display_notification(info=msg)


    def dot_adjust_count(self, positive=True):
        count = 1 if positive else -1
        for mod in self.mod_controller.active_modifiers():
            mod.count += count


    def set_others_to_one(self, preserve_axis='X'):
        index = ['X', 'Y', 'Z'].index(preserve_axis)
        for mod in self.mod_controller.active_modifiers():
            constant_offset = mod_constant_offset_object(mod)
            relative_offset = mod_relative_offset_object(mod)

            for i in range(3):
                if i == index: continue
                if mod.use_constant_offset == True:
                    constant_offset[i] = 0
                else:
                    relative_offset[i] = 0

        self.set_to_one(axis=preserve_axis)


    def set_to_one(self, shift=False, axis='X'):
        i = ['X', 'Y', 'Z'].index(axis)
        for obj_data in self.mod_controller.validated_obj_datas():
            mod = obj_data.active_mod()

            constant_offset = mod_constant_offset_object(mod)
            relative_offset = mod_relative_offset_object(mod)

            if mod.use_relative_offset:
                if shift:
                    relative_offset[i] += 1
                else:
                    relative_offset[i] = 1
            else:
                snap_factor = abs(obj_data.dims[i])
                if shift:
                    constant_offset[i] += snap_factor
                else:
                    constant_offset[i] = snap_factor


    def flip_array(self):
        for mod in self.mod_controller.active_modifiers():
            constant_offset = mod_constant_offset_object(mod)
            relative_offset = mod_relative_offset_object(mod)

            index = self.axis.value
            if mod.use_relative_offset:
                val = relative_offset[index]
                relative_offset[index] = val * -1
            else:
                val = constant_offset[index]
                constant_offset[index] = val * -1


    def add_remove_array(self, remove=False):
        if remove:
            self.remove_an_array()
        else:
            self.add_an_array()


    def exit_button(self):
        self.form_exit = True

    # --- ADJUST 2D --- #

    def adjust_2D(self, accelerated=True):

        speed_bonus = 10 if accelerated else 5

        for obj_data in self.mod_controller.validated_obj_datas():
            mod = obj_data.active_mod()

            self.end_object_offset(mod)

            if mod.use_constant_offset == True:
                self.adjust_constant_arrays_2D(obj_data, mod, speed_bonus)
            else:
                self.adjust_relative_arrays_2D(obj_data, mod, speed_bonus)

        # Reset the mouse accumulation for snaps
        if abs(self.mouse_accumulation) > .25:
            self.mouse_accumulation = 0


    def adjust_constant_arrays_2D(self, obj_data, mod, speed_bonus=0):
        '''Adjust arrays based on 2D screen coordinates.'''

        constant_offset = mod_constant_offset_object(mod)

        offset = self.mouse_x_offset_2D * speed_bonus
        index = self.axis.value
        if self.use_snaps:
            # Limit this operation
            if abs(self.mouse_accumulation) > .25:
                # 1/2 of the mesh dims
                snap_factor = abs(obj_data.dims[index] * .5)
                # Reverse the offset direction
                if self.mouse_x_offset_2D > 0:
                    snap_factor = -snap_factor
                # Clamp the offset to a snapped value
                vec = constant_offset[index]
                remainder = abs(vec) % abs(snap_factor) if abs(snap_factor) > 0 else 0
                if (vec + remainder) % abs(snap_factor) == 0:
                    constant_offset[index] += remainder
                else:
                    constant_offset[index] -= remainder
                # Assign the snap factor
                constant_offset[index] += snap_factor
        else:
            constant_offset[index] -= offset


    def adjust_relative_arrays_2D(self, obj_data, mod, speed_bonus=0):
        '''Adjust arrays based on 2D screen coordinates.'''

        relative_offset = mod_relative_offset_object(mod)

        offset = self.mouse_x_offset_2D * speed_bonus
        offset *= .25
        index = self.axis.value
        if self.use_snaps:
            # Limit this operation
            if abs(self.mouse_accumulation) > .25:
                if abs(relative_offset[index]) % .25 != 0:
                        relative_offset[index] = round_quarter(relative_offset[index])
                snap_factor = .25 if self.mouse_x_offset_2D > 0 else -.25
                relative_offset[index] -= snap_factor
        else:
            relative_offset[index] -= offset

    # --- ADJUST 3D --- #

    def adjust_3D(self, context, event):
        '''Adjust the arrays 3D.'''

        for obj_data in self.mod_controller.validated_obj_datas():

            # Data
            obj = obj_data.obj
            mod = obj_data.active_mod()
            dims = obj_data.dims
            index = self.axis.value

            self.end_object_offset(mod)

            # Intersection
            normal = Vector((0,1,0)) if index == 2 else Vector((0,0,1))
            normal = normal @ obj.matrix_world.inverted()
            if context.region_data.is_orthographic_side_view:
                view_quat = context.region_data.view_rotation
                up = Vector((0,0,1))
                normal = view_quat @ up

            mouse_pos = Vector((event.mouse_region_x, event.mouse_region_y))

            point = obj.matrix_world.to_translation()

            # Sync
            if self.sync_to_active and self.sync_object:
                point = self.sync_object.matrix_world.to_translation()

            self.intersection_point = get_3D_point_from_mouse(mouse_pos, context, point, normal)
            self.circle_loc_3d = self.intersection_point
            self.intersection_point = obj.matrix_world.inverted() @ self.intersection_point

            intersection = self.intersection_point.copy()

            # Sync
            if self.sync_to_active and self.sync_object:
                if obj in self.offset_data:
                    # offset = Matrix.Diagonal(obj.scale).inverted() @ self.offset_data[obj]
                    intersection += self.offset_data[obj]

            # Adjust Arrays
            if mod.use_constant_offset == True:
                # intersection = Vector([intersection[i] * obj.matrix_world.to_scale()[i] for i in range(3)])
                self.adjust_constant_arrays_3D(intersection, index, mod, obj, dims)
            else:
                intersection = Vector([intersection[i] * obj.matrix_world.to_scale()[i] for i in range(3)])
                self.adjust_relative_arrays_3D(intersection, index, mod, obj, dims)

            # Put the intersection point back for drawing
            self.intersection_point = obj.matrix_local @ self.intersection_point


    def adjust_constant_arrays_3D(self, intersection, index, mod, obj, dims):
        '''Adjust constant arrays based on 3D mouse coordinates.'''

        distance = intersection[index]
        count = mod.count - 1
        if count < 1: count = 1
        vec = distance / count

        if self.use_snaps:
            x_dim = dims[index]
            vec -= vec % (x_dim * .25) if (x_dim * .25) > 0 else 1

        constant_offset = mod_constant_offset_object(mod)
        constant_offset[index] = vec


    def adjust_relative_arrays_3D(self, intersection, index, mod, obj, dims):
        '''Adjust relative arrays based on 3D mouse coordinates.'''

        relative_offset = mod_relative_offset_object(mod)

        offset = intersection[index] / (dims[index] * obj.scale[index]) if dims[index] > 0 else 1
        offset /= mod.count - 1 if mod.count - 1 > 0 else 1
        offset = round_quarter(offset) if self.use_snaps else offset
        relative_offset[index] = offset

    # --- WIDGETS --- #

    def widget_update(self, context, event):

        # Not active so discontinue
        if self.deadzone_active == False: return

        self.deadzone_previous = self.widget.mouse_is_over
        self.widget.update(context, event)
        if self.widget.mouse_is_over == False:
            if self.deadzone_previous == True:
                self.deadzone_active = False


    def axial_callback(self, val):

        self.clear_axis(axis=Axis.X)
        self.clear_axis(axis=Axis.Y)
        self.clear_axis(axis=Axis.Z)

        if val == 'X':
            self.axis = Axis.X
            self.set_arrays_to_one(negative=False)
        elif val == 'Y':
            self.axis = Axis.Y
            self.set_arrays_to_one(negative=False)
        elif val == 'Z':
            self.axis = Axis.Z
            self.set_arrays_to_one(negative=False)

        elif val == '-X':
            self.axis = Axis.X
            self.set_arrays_to_one(negative=True)
        elif val == '-Y':
            self.axis = Axis.Y
            self.set_arrays_to_one(negative=True)
        elif val == '-Z':
            self.axis = Axis.Z
            self.set_arrays_to_one(negative=True)

    # --- UTILS --- #

    def confirm_exit(self, context):
        self.__class__.operator = None
        if self.exit_with_empty_driver:
            self.array_to_empty_driver(context)
        elif self.exit_with_empty_obj:
            self.array_to_empty_target(context)
        self.store_axis_on_object()
        self.form.shut_down(context)
        self.remove_shaders()
        collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
        self.master.run_fade()
        self.mod_controller.confirm_exit()
        return {'FINISHED'}


    def cancel_exit(self, context):
        self.__class__.operator = None
        self.form.shut_down(context)
        self.remove_shaders()
        collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel)
        self.master.run_fade()
        self.mod_controller.cancel_exit()
        return {'CANCELLED'}


    def setup_objects(self, context):

        for obj_data in self.mod_controller.validated_obj_datas():

            # Drawing Radius / Default Dims
            if obj_data.obj == self.mod_controller.active_obj:
                dims = self.mod_controller.active_object_mod(as_obj_data=True).dims
                self.defualt_object_dims = dims
                self.drawing_radius = self.defualt_object_dims[0] * .5

            obj = obj_data.obj

            used_driver = False

            for mod_data in obj_data.mod_datas:
                mod = mod_data.mod

                constant_offset = mod_constant_offset_object(mod)
                relative_offset = mod_relative_offset_object(mod)

                if mod.show_viewport == False: continue

                # Set default offset for new
                if mod_data.was_created:
                    mod.use_relative_offset = False
                    mod.use_constant_offset = True
                    constant_offset[0] = obj_data.dims[0]

                else:
                    # BC Array Check
                    if mod.use_relative_offset and mod.use_constant_offset:
                        for i in range(3):
                            if abs(relative_offset[i]) == 1:
                                mod.use_relative_offset = False
                                neg = -1 if relative_offset[i] < 0 else 1
                                constant_offset[i] += neg * obj_data.dims[i]
                                obj.hops.last_array_axis = ['X', 'Y', 'Z'][i]
                                break

                    if mod.use_relative_offset:
                        mod.use_constant_offset = False

                    if mod.use_constant_offset:
                        mod.use_relative_offset = False

                    if not mod.use_constant_offset and not mod.use_relative_offset:
                        mod.use_constant_offset = True

                    # Fix for Radial Array
                    if mod.use_object_offset:
                        if mod.offset_object:
                            mod.use_constant_offset = False
                            mod.use_relative_offset = False
                        else:
                            mod.use_object_offset = False
                            mod.use_constant_offset = True
                            mod.use_relative_offset = False

                    # Remove old drivers
                    driver_1 = mod.driver_remove('count')
                    driver_2 = mod.driver_remove('constant_offset_displace')

                    if driver_1 or driver_2:
                        used_driver = True

            # Make sure new array uses proper settings based on last array
            if obj_data.active_mod_data().was_created and len(obj_data.mod_datas) > 1:
                prev_mod = obj_data.mod_datas[-2].mod
                cur_mod = obj_data.active_mod()

                # Make sure new array isnt using same axis as array before it
                axises = ['X', 'Y', 'Z']
                index, compare = 0, 0

                prev_relative_offset = mod_relative_offset_object(prev_mod)
                prev_constant_offset = mod_constant_offset_object(prev_mod)

                cur_relative_offset = mod_relative_offset_object(cur_mod)
                cur_constant_offset = mod_constant_offset_object(cur_mod)


                if prev_mod.use_relative_offset:
                    for i in range(3):
                        if abs(prev_relative_offset[i]) > compare:
                            index = i
                            compare = abs(prev_relative_offset[i])
                    axis = axises[(index + 1) % len(axises)]
                    cur_relative_offset[0] = 0
                    obj.hops.last_array_axis = axis

                elif prev_mod.use_constant_offset:
                    for i in range(3):
                        if abs(prev_constant_offset[i]) > compare:
                            index = i
                            compare = abs(prev_constant_offset[i])
                    axis = axises[(index + 1) % len(axises)]
                    cur_constant_offset[0] = 0
                    obj.hops.last_array_axis = axis

            # Remove old driver empty
            for child in obj.children:
                if child.name[:15] == "HOPSArrayTarget":
                    if used_driver:
                        self.exit_with_empty_driver = True
                        bpy.data.objects.remove(child)


    def setup_offset_sync_data(self, context, objs):
        '''Generate offsets needed to keep objects grouped during array.'''

        if context.active_object in objs:
            self.sync_object = context.active_object
        else: return

        if self.sync_object in objs:
            objs.remove(self.sync_object)

        for obj in objs:

            A_mat = self.sync_object.matrix_world.copy()
            A_loc = self.sync_object.matrix_world.translation
            A_sca = Matrix.Diagonal(A_mat.decompose()[2])
            A_mat.translation = Vector()

            B_mat = obj.matrix_world
            B_loc = B_mat.translation
            B_sca = Matrix.Diagonal(B_mat.decompose()[2]).inverted()

            sub = B_loc - A_loc
            offset = A_mat.inverted() @ sub

            self.offset_data[obj] = A_sca @ B_sca @ offset


    def set_initial_axis(self, created_new=False):
        obj_data = self.mod_controller.active_object_mod(as_obj_data=True)
        if obj_data == None: return

        # Check if the array should actually use another axis
        mod = obj_data.active_mod()

        if created_new:
            if len(obj_data.mod_datas) > 1:
                mod = obj_data.mod_datas[-2].mod

        index = 0
        offset = (0,0,0)

        constant_offset = mod_constant_offset_object(mod)
        relative_offset = mod_relative_offset_object(mod)

        if mod.use_constant_offset:
            offset = list(constant_offset)
        else:
            offset = list(relative_offset)

        offset = [abs(o) for o in offset]
        index = offset.index(max(offset))

        if created_new:
            created_mod = obj_data.mod_datas[-1].mod
            if index == 0:
                self.axis = Axis.Y
                created_mod.relative_offset_displace = [0.0, created_mod.relative_offset_displace[0], 0.0]
                created_mod.constant_offset_displace = [0.0, created_mod.constant_offset_displace[0], 0.0]
            elif index == 1:
                self.axis = Axis.Z
                created_mod.relative_offset_displace = [0.0, 0.0, created_mod.relative_offset_displace[1]]
                created_mod.constant_offset_displace = [0.0, 0.0, created_mod.constant_offset_displace[1]]
            elif index == 2:
                self.axis = Axis.X
                created_mod.relative_offset_displace = [created_mod.relative_offset_displace[2], 0.0, 0.0]
                created_mod.constant_offset_displace = [created_mod.constant_offset_displace[2], 0.0, 0.0]
            return

        if index == 0:
            self.axis = Axis.X
        elif index == 1:
            self.axis = Axis.Y
        elif index == 2:
            self.axis = Axis.Z


    def end_object_offset(self, mod):
        if mod.use_object_offset:
            if mod.use_relative_offset:
                mod.use_constant_offset = False
            elif mod.use_constant_offset:
                mod.use_relative_offset = False
            elif not mod.use_constant_offset and not mod.use_relative_offset:
                mod.use_constant_offset = True


    def store_axis_on_object(self):
        for obj_data in self.mod_controller.validated_obj_datas():
            obj = obj_data.obj
            if self.axis == Axis.X:
                obj.hops.last_array_axis = 'X'
            elif self.axis == Axis.Y:
                obj.hops.last_array_axis = 'Y'
            elif self.axis == Axis.Z:
                obj.hops.last_array_axis = 'Z'


    def toggle_relative_constant(self):
        # Determine what to set the other arrays to based on defualt obj
        use_relative = False
        mod = self.mod_controller.active_object_mod()
        if mod != None: use_relative = mod.use_relative_offset

        # Set to the inverse of what defualt was using
        for mod in self.mod_controller.active_modifiers():
            if use_relative:
                mod.use_constant_offset = True
                mod.use_relative_offset = False
            else:
                mod.use_constant_offset = False
                mod.use_relative_offset = True


    def clear_axis(self, axis=Axis.Y):
        for mod in self.mod_controller.active_modifiers():
            index = axis.value

            constant_offset = mod_constant_offset_object(mod)
            relative_offset = mod_relative_offset_object(mod)

            constant_offset[index] = 0
            relative_offset[index] = 0


    def increment_decrement_count(self, count=0):
        for mod in self.mod_controller.active_modifiers():
            if mod.count + count >= 1:
                mod.count += count


    def add_an_array(self):
        self.mod_controller.create_new_mod()
        for mod in self.mod_controller.active_modifiers():

            constant_offset = mod_constant_offset_object(mod)
            relative_offset = mod_relative_offset_object(mod)

            mod.use_relative_offset = False
            mod.use_constant_offset = True
            relative_offset[0] = 0
            constant_offset[0] = 0

        self.toggle_axis()

        # Set initial offset
        for obj_data in self.mod_controller.validated_obj_datas():
            mod = obj_data.active_mod()
            constant_offset = mod_constant_offset_object(mod)
            index = self.axis.value
            constant_offset[index] = obj_data.dims[index]

        if addon.preference().ui.Hops_extra_info:
            bpy.ops.hops.display_notification(info=f"Array Added")


    def remove_an_array(self):
        self.mod_controller.remove_active_mod(leave_one=True, use_logical_delete=True, remove_if_created=True)
        if addon.preference().ui.Hops_extra_info:
            bpy.ops.hops.display_notification(info=f"Array Removed")


    def toggle_axis(self, clear_axis_on_change=False):
        '''Toggle the current axis, also setting some other values.'''

        self.mouse_accumulation = 0

        if self.axis == Axis.X:
            self.axis = Axis.Y
            if clear_axis_on_change:
                self.clear_axis(axis=Axis.X)
        elif self.axis == Axis.Y:
            self.axis = Axis.Z
            if clear_axis_on_change:
                self.clear_axis(axis=Axis.Y)
        elif self.axis == Axis.Z:
            self.axis = Axis.X
            if clear_axis_on_change:
                self.clear_axis(axis=Axis.Z)


    def goto_next_array(self):
        self.mod_controller.cyclic_next_mod_index()
        self.set_initial_axis()


    def set_arrays_to_one(self, negative=False):
        for obj_data in self.mod_controller.validated_obj_datas():
            mod = obj_data.active_mod()
            dims = obj_data.dims
            i = self.axis.value

            constant_offset = mod_constant_offset_object(mod)
            relative_offset = mod_relative_offset_object(mod)

            if mod.use_relative_offset:
                relative_offset[i] = 1 if negative == False else -1
            else:
                snap_factor = abs(dims[i])
                constant_offset[i] = snap_factor if negative == False else -snap_factor


    def set_arrays_to_zero(self):
        for mod in self.mod_controller.active_modifiers():

            constant_offset = mod_constant_offset_object(mod)
            relative_offset = mod_relative_offset_object(mod)

            i = self.axis.value
            if mod.use_relative_offset: relative_offset[i] = 0
            else: constant_offset[i] = 0


    def move_mod(self, context, up=True):
        self.mod_controller.move_mod(context, up)


    def array_to_empty_driver(self, context):

        # Key = Obj : Val = Empties
        obj_empty_data = {}

        for obj_data in self.mod_controller.validated_obj_datas():
            obj = obj_data.obj
            dims = obj_data.dims

            if obj not in obj_empty_data:
                obj_empty_data[obj] = []

            for mod in obj_data.all_active_mods():

                constant_offset = mod_constant_offset_object(mod)
                relative_offset = mod_relative_offset_object(mod)

                constant_offset_string = 'constant_offset_displace' if mod.type == 'ARRAY' else 'constant_offset'

                # Create empty
                empty = bpy.data.objects.new("HOPSArrayTarget", None)
                context.collection.objects.link(empty)
                empty.empty_display_size = .5
                empty.empty_display_type = 'SPHERE'
                empty.parent = obj
                empty.rotation_euler.z = math.radians((mod.count * 10) - 15)

                obj_empty_data[obj].append(empty)

                # Empty constraint
                con = empty.constraints.new(type='LIMIT_ROTATION')
                con.use_limit_x = True
                con.use_limit_y = True
                con.use_limit_z = True
                con.max_z = math.radians(360)
                con.owner_space = 'LOCAL'

                # Constant
                if mod.use_constant_offset == True:
                    loc_x = constant_offset[0] * (mod.count - 1)
                    loc_y = constant_offset[1] * (mod.count - 1)
                    loc_z = constant_offset[2] * (mod.count - 1)

                    empty.location = (loc_x, loc_y, loc_z)

                # Relative
                else:
                    loc_x = dims[0] * obj.scale[0] * relative_offset[0] * (mod.count - 1)
                    loc_y = dims[1] * obj.scale[1] * relative_offset[1] * (mod.count - 1)
                    loc_z = dims[2] * obj.scale[2] * relative_offset[2] * (mod.count - 1)

                    empty.location = (loc_x, loc_y, loc_z)

                # Set constant
                mod.use_constant_offset = True
                mod.use_relative_offset = False

                # Count driver
                driver = mod.driver_add('count').driver

                empty_rot = driver.variables.new()
                empty_rot.name = 'empty_rot'
                empty_rot.type = 'TRANSFORMS'
                empty_rot.targets[0].id = empty
                empty_rot.targets[0].transform_type = 'ROT_Z'
                empty_rot.targets[0].transform_space = 'LOCAL_SPACE'

                driver.expression = "abs( (degrees(empty_rot) / 10) + 2 )"

                # Count Data Path
                gp_mods = 'modifiers' if mod.type == 'GREASE_PENCIL_ARRAY' else 'grease_pencil_modifiers'
                count_data_path = f'modifiers["{mod.name}"].count' if mod.type == 'ARRAY' else f'{gp_mods}["{mod.name}"].count'

                # Driver X
                driver = mod.driver_add(constant_offset_string, 0).driver

                empty_x = driver.variables.new()
                empty_x.name = 'empty_x'
                empty_x.type = 'TRANSFORMS'
                empty_x.targets[0].id = empty
                empty_x.targets[0].transform_type = 'LOC_X'
                empty_x.targets[0].transform_space = 'LOCAL_SPACE'

                count = driver.variables.new()
                count.name = 'count'
                count.type = 'SINGLE_PROP'
                count.targets[0].id = obj
                count.targets[0].data_path = count_data_path

                driver.expression = "empty_x / (count - 1) if (count - 1) > 0 else 1"

                # Driver Y
                driver = mod.driver_add(constant_offset_string, 1).driver

                empty_y = driver.variables.new()
                empty_y.name = 'empty_y'
                empty_y.type = 'TRANSFORMS'
                empty_y.targets[0].id = empty
                empty_y.targets[0].transform_type = 'LOC_Y'
                empty_y.targets[0].transform_space = 'LOCAL_SPACE'

                count = driver.variables.new()
                count.name = 'count'
                count.type = 'SINGLE_PROP'
                count.targets[0].id = obj
                count.targets[0].data_path = count_data_path

                driver.expression = "empty_y / (count - 1) if (count - 1) > 0 else 1"

                # Driver Z
                driver = mod.driver_add(constant_offset_string, 2).driver

                empty_z = driver.variables.new()
                empty_z.name = 'empty_z'
                empty_z.type = 'TRANSFORMS'
                empty_z.targets[0].id = empty
                empty_z.targets[0].transform_type = 'LOC_Z'
                empty_z.targets[0].transform_space = 'LOCAL_SPACE'

                count = driver.variables.new()
                count.name = 'count'
                count.type = 'SINGLE_PROP'
                count.targets[0].id = obj
                count.targets[0].data_path = count_data_path

                driver.expression = "empty_z / (count - 1) if (count - 1) > 0 else 1"

    # TODO : Maybe get this working...  probably needs a different method
    def assign_main_empty(self, context, main_obj, obj_empty_data):

        # Location
        loc = Vector()
        summed = Vector()
        div = 0
        for key, val in obj_empty_data.items():
            for empty in val:
                summed += empty.location
                div += 1
        loc = summed / div if div > 0 else Vector()

        # Create empty
        controller_empty = bpy.data.objects.new("HOPSArrayController", None)
        context.collection.objects.link(controller_empty)
        controller_empty.empty_display_size = .5
        controller_empty.empty_display_type = 'SPHERE'
        controller_empty.parent = main_obj
        controller_empty.location = loc

        # Empty constraint
        con = controller_empty.constraints.new(type='LIMIT_ROTATION')
        con.use_limit_x = True
        con.use_limit_y = True
        con.use_limit_z = False
        con.owner_space = 'LOCAL'

        context.view_layer.update()

        # Add empty constraints
        for obj, empties in obj_empty_data.items():
            for empty in empties:

                con = empty.constraints.new(type='COPY_LOCATION')
                con.target = controller_empty
                con.use_x = True
                con.use_y = True
                con.use_z = True
                con.use_offset = True
                con.target_space = 'CUSTOM'
                con.space_object = obj
                con.owner_space = 'LOCAL'

                con = empty.constraints.new(type='COPY_ROTATION')
                con.target = controller_empty
                con.use_x = True
                con.use_y = True
                con.use_z = True
                con.mix_mode = 'AFTER'
                con.target_space = 'CUSTOM'
                con.space_object = obj
                con.owner_space = 'LOCAL'

                con = empty.constraints.new(type='TRANSFORM')
                con.target = controller_empty
                con.target_space = 'LOCAL'
                con.owner_space = 'WORLD'
                con.map_from = 'LOCATION'
                con.map_to = 'LOCATION'


                '''
                Controller empty location
                minus
                the offset from empty to controller empty
                '''

                space = main_obj.matrix_world.copy()
                ispace = space.inverted()

                controller_loc = ispace @ controller_empty.matrix_world.translation
                empty_loc = ispace @ empty.matrix_world.translation

                offset = (empty_loc - controller_loc) - controller_loc

                con.to_min_x = offset.x
                con.to_min_y = offset.y
                con.to_min_z = offset.z


    def array_to_empty_target(self, context):
        for obj_data in self.mod_controller.validated_obj_datas():
            obj = obj_data.obj
            dims = obj_data.dims

            for mod in obj_data.all_active_mods():

                constant_offset = mod_constant_offset_object(mod)
                relative_offset = mod_relative_offset_object(mod)

                # Create empty
                empty = bpy.data.objects.new("HOPSArrayTarget", None)
                context.collection.objects.link(empty)
                empty.empty_display_size = .5
                empty.empty_display_type = 'SPHERE'
                empty.parent = obj

                # Constant
                if mod.use_constant_offset == True:
                    loc_x = constant_offset[0]
                    loc_y = constant_offset[1]
                    loc_z = constant_offset[2]

                    empty.location = (loc_x, loc_y, loc_z)

                # Relative
                else:
                    loc_x = dims[0] * obj.scale[0] * relative_offset[0]
                    loc_y = dims[1] * obj.scale[1] * relative_offset[1]
                    loc_z = dims[2] * obj.scale[2] * relative_offset[2]

                    empty.location = (loc_x, loc_y, loc_z)

                # Zero out offset for GP array
                if mod.type == 'GP_ARRAY':
                    for i in range(3): constant_offset[i] = 0

                mod.use_constant_offset = False
                mod.use_relative_offset = False
                mod.use_object_offset = True
                mod.offset_object = empty


    def popover(self, context):

        # # Popup captured new mod
        # if self.__class__.mod_selected != "":
        #     valid = self.mod_controller.set_active_obj_mod_index(self.__class__.mod_selected)
        #     if valid:
        #         bpy.ops.hops.display_notification(info=f'Target Array : {self.__class__.mod_selected}')
        #         self.set_initial_axis()
        #     self.__class__.mod_selected = ""

        # Spawns
        if self.base_controls.popover:
            context.window_manager.popover(popup_draw)

    # --- SHADERS --- #

    def remove_shaders(self):
        if self.draw_handle_2D:
            self.draw_handle_2D = bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle_2D, "WINDOW")
        if self.draw_handle_3D:
            self.draw_handle_3D = bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle_3D, "WINDOW")


    def safe_draw_2D(self, context):
        method_handler(self.draw_shader_2D,
            arguments = (context,),
            identifier = 'Modal Shader 2D',
            exit_method = self.remove_shaders)


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

        if self.popup_style == 'DEFAULT':
            self.form.draw()
        self.axial.draw()

        if self.edit_space == Edit_Space.View_2D:
            draw_modal_frame(context)

        if self.deadzone_active:
            self.widget.draw()


    def safe_draw_3D(self, context):
        method_handler(self.draw_shader_3D,
            arguments = (context,),
            identifier = 'Modal Shader 3D',
            exit_method = self.remove_shaders)


    def draw_shader_3D(self, context):
        if self.freeze_controls == True: return
        elif self.show_guides == False: return

        if self.edit_space == Edit_Space.View_3D:
            self.draw_circle_3D()
            self.draw_plane_3D()
        elif self.edit_space == Edit_Space.View_2D:
            self.draw_plane_3D()


    def draw_circle_3D(self):
        '''Draw the circle where the mouse is in 3D.'''

        radius = self.drawing_radius
        if addon.preference().property.array_type == "DOT":
            radius = .0125
            width = 12
        else:
            width = 3

        vertices = []
        indices = []
        segments = 64
        color = (0,0,0,1)
        obj = self.mod_controller.active_obj

        #Build ring
        for i in range(segments):
            index = i + 1
            angle = i * 3.14159 * 2 / segments

            x = cos(angle) * radius
            y = sin(angle) * radius
            z = 0
            vert = Vector((x, y, z))

            if self.axis == Axis.Z:
                plane_normal = Vector((0,1,0)) @ obj.matrix_world.inverted()
                up = Vector((0,0,1))
                angle = plane_normal.rotation_difference(up)
                rot_mat = angle.to_matrix()
                vert = vert @ rot_mat

            else:
                rot_mat = obj.matrix_world.to_quaternion()
                rot_mat = rot_mat.to_matrix()
                rot_mat = rot_mat.inverted()
                vert = vert @ rot_mat

            vert[0] = vert[0] + self.circle_loc_3d[0]
            vert[1] = vert[1] + self.circle_loc_3d[1]
            vert[2] = vert[2] + self.circle_loc_3d[2]

            vertices.append(vert)

            if index == segments: indices.append((i, 0))
            else: indices.append((i, i + 1))

        if self.axis == Axis.X:
            color = (1,0,0,1)
        elif self.axis == Axis.Y:
            color = (0,1,0,1)
        elif self.axis == Axis.Z:
            color = (0,1,1,1)

        built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >=4 else '3D_UNIFORM_COLOR'
        shader = gpu.shader.from_builtin(built_in_shader)
        batch = batch_for_shader(shader, 'LINES', {'pos': vertices}, indices=indices)
        shader.bind()
        shader.uniform_float('color', color)
        # Lines
        #Enable(GL_LINE_SMOOTH)
        gpu.state.blend_set('ALPHA')
        gpu.state.line_width_set(width)
        batch.draw(shader)
        del shader
        del batch


    def draw_plane_3D(self):
        '''Draw a 3D plane to represent the editing axis.'''

        vertices = []
        indices = []
        obj = self.mod_controller.active_obj
        mat = obj.matrix_world
        x_offset = 1
        y_offset = 1

        scale = obj.matrix_world.inverted()
        scale = scale.to_scale()

        if self.axis == Axis.X:
            x_offset = (obj.dimensions[0] + .25) * scale[0]
            y_offset = self.defualt_object_dims[0] * scale[0]
        elif self.axis == Axis.Y:
            x_offset = self.defualt_object_dims[0] * scale[0]
            y_offset = (obj.dimensions[1]  + .25) * scale[0]
        elif self.axis == Axis.Z:
            x_offset = self.defualt_object_dims[0] * scale[0]
            y_offset = (obj.dimensions[2] + .25) * scale[0]


        if self.axis == Axis.Z:
            mat_rot = Matrix.Rotation(math.radians(90.0), 4, 'X')
            mat = mat @ mat_rot

        # Bottom Left
        vert = Vector(( -x_offset, -y_offset, 0 ))
        vert = mat @ vert
        vertices.append(vert)

        # Top Left
        vert = Vector(( -x_offset, y_offset, 0 ))
        vert = mat @ vert
        vertices.append(vert)

        # Top Right
        vert = Vector(( x_offset, y_offset, 0 ))
        vert = mat @ vert
        vertices.append(vert)

        # Bottom Right
        vert = Vector(( x_offset, -y_offset, 0 ))
        vert = mat @ vert
        vertices.append(vert)

        indices.append((0, 1, 2 ))
        indices.append((0, 2, 3 ))

        color = (0,0,0,1)
        if self.axis == Axis.X:
            color = (1, 0, 0, .03125)
        elif self.axis == Axis.Y:
            color = (0, 1, 0, .03125)
        elif self.axis == Axis.Z:
            color = (0, 1, 1, .03125)

        built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >=4 else '3D_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', color)

        #Enable(GL_LINE_SMOOTH)
        gpu.state.blend_set('ALPHA')
        gpu.state.depth_test_set('LESS')
        #glDepthFunc(GL_LESS)

        batch.draw(shader)
        gpu.state.depth_test_set('NONE')
        gpu.state.face_culling_set('NONE')
        gpu.state.depth_test_set('NONE')
        del shader
        del batch


def mod_constant_offset_object(mod):
    if mod.type == 'ARRAY':
        return mod.constant_offset_displace
    if mod.type == 'GP_ARRAY':
        return mod.constant_offset
    if mod.type == 'GREASE_PENCIL_ARRAY':
        return mod.constant_offset


def mod_relative_offset_object(mod):
    if mod.type == 'ARRAY':
        return mod.relative_offset_displace
    if mod.type == 'GP_ARRAY':
        return mod.relative_offset
    if mod.type == 'GREASE_PENCIL_ARRAY':
        return mod.relative_offset


def round_quarter(x):
    '''Rounds the decimal point to the nearest quarter (.0, .25, .5, .75)'''
    return round(x * 4) / 4.0

# --- POPOVER --- #

def popup_draw(self, context):
    layout = self.layout

    op = HOPS_OT_ST3_Array.operator
    if not op: return

    layout.label(text='Selector')

    mods = [m.name for m in op.mod_controller.active_obj_mods()]

    broadcaster = "hops.popover_data"

    for i, mod in enumerate(mods):
        row = layout.row()
        row.scale_y = 2
        props = row.operator(broadcaster, text=mod)
        props.calling_ops = 'ARRAY_V2'
        props.str_1 = mod


class HOPS_OT_ST3_Array_Popup(bpy.types.Operator):
    bl_idname = "hops.st3_array_popup"
    bl_label = "Adjust Array V2"

    def __del__(self):
        HOPS_OT_ST3_Array.popover_active = False

    def execute(self, context):
        preference = addon.preference().ui
        return bpy.context.window_manager.invoke_popup(self, width=int(150 * dpi_factor()))

    def draw(self, context):
        layout = self.layout

        op = HOPS_OT_ST3_Array.operator
        if not op: return

        mod = op.mod_controller.active_object_mod()

        if not mod: return

        layout.label(text='ARRAY V2')

        if mod.type == 'ARRAY':
            if mod.use_constant_offset:
                offset = 'constant_offset_displace'
            else:
                offset = 'relative_offset_displace'
        else:
            if mod.use_constant_offset:
                offset = 'constant_offset'
            else:
                offset = 'relativet_offset'

        row = layout.row()
        row.label(text='X')
        row.prop(mod, offset, index=0, text='')
        sone = row.row().operator('hops.st3_add_setone', text='', icon='THREE_DOTS')
        sone.axis ='X'

        row = layout.row()
        row.label(text='Y')
        row.prop(mod, offset, index=1, text='')
        sone = row.row().operator('hops.st3_add_setone', text='', icon='THREE_DOTS')
        sone.axis ='Y'

        row = layout.row()
        row.label(text='Z')
        row.prop(mod, offset, index=2, text='')
        sone = row.row().operator('hops.st3_add_setone', text='', icon='THREE_DOTS')
        sone.axis ='Z'

        func_row = layout.row(align=True)
        func_row.row().prop(mod, 'count', text='')

        offset = op.get_offset_hook()
        func_row.operator("hops.st3_array_offset", text='', icon='EVENT_R' if offset == 'Relative' else 'EVENT_C',)

        move_ops = func_row
        move = move_ops.operator("hops.st3_array_modmove", text='', icon='TRIA_UP')
        move.up = True
        move = move_ops.operator("hops.st3_array_modmove", text='', icon='TRIA_DOWN')
        move.up = False

        func_row.prop(op, 'exit_with_empty_driver', text='', icon='DRIVER')

        add_rem = func_row.row()
        add_rem.operator_context = 'INVOKE_DEFAULT'
        add_rem.operator("hops.st3_add_remove", text='', icon='ADD')

        layout.popover(HOPS_PT_ST3_array_switch.bl_idname, text=mod.name)

class HOPS_PT_ST3_array_switch(bpy.types.Panel):
    bl_idname = 'HOPS_PT_ST3_array_switch'
    bl_label = "Selector"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'WINDOW'
    def draw(self, context):
        popup_draw(self, context)

class HOPS_OT_ST3_Array_Offset(bpy.types.Operator):
    bl_idname = "hops.st3_array_offset"
    bl_label = "Offset"
    bl_description = 'Set offset type'

    def execute(self, context):
        op = HOPS_OT_ST3_Array.operator
        if not op: return {'FINISHED'}
        offset = op.get_offset_hook()

        offset = 'Relative' if offset == 'Constant' else 'Constant'
        op.set_relative_constant(offset)

        return {'FINISHED'}

class HOPS_OT_ST3_Array_ModMove(bpy.types.Operator):
    bl_idname = "hops.st3_array_modmove"
    bl_label = "Move"
    bl_description = 'Move modifier'

    up: bpy.props.BoolProperty()
    def execute(self, context):
        op = HOPS_OT_ST3_Array.operator
        if not op: return {'FINISHED'}

        op.move_mod(context, self.up)

        return {'FINISHED'}

class HOPS_OT_ST3_Array_AddRemove(bpy.types.Operator):
    bl_idname = "hops.st3_add_remove"
    bl_label = "Add"
    bl_description = '''Click to Add a new array
    Shift + Click to remove current array'''

    def invoke(self, context, event):
        op = HOPS_OT_ST3_Array.operator
        if not op: return {'FINISHED'}

        op.add_remove_array(remove=event.shift)

        return {'FINISHED'}

class HOPS_OT_ST3_Array_SetOne(bpy.types.Operator):
    bl_idname = "hops.st3_add_setone"
    bl_label = "Add"
    bl_description = '''Click - Set Array to 1
    Shift + Click - Add 1
    Alt + Click - Set array to 1 and others to 0'''

    axis: bpy.props.StringProperty()

    def invoke(self, context, event):
        op = HOPS_OT_ST3_Array.operator
        if not op: return {'FINISHED'}

        if event.alt:
            op.set_others_to_one(preserve_axis=self.axis)
        else:
            op.set_to_one(shift=event.shift, axis=self.axis)

        return {'FINISHED'}