Skip to content

Instantly share code, notes, and snippets.

@NathoSteveo
Last active April 19, 2025 08:50
Show Gist options
  • Save NathoSteveo/0ba2e2077c7989e835eda334759bc99e to your computer and use it in GitHub Desktop.
Save NathoSteveo/0ba2e2077c7989e835eda334759bc99e to your computer and use it in GitHub Desktop.
[ GUMDROP PANELS ][ ADDON FOR BLENDER ]
bl_info = {
"name": "GUMDROP",
"author": "NathoSteveo",
"version": (1, 0, 0),}
import bpy
import os
import math
import bmesh
from mathutils import Vector
from bpy.types import (Panel, Operator, PropertyGroup)
from bpy_extras.io_utils import ExportHelper
from bpy.props import (BoolProperty, FloatVectorProperty, IntProperty, FloatProperty, StringProperty, PointerProperty, EnumProperty, CollectionProperty)
from bpy.utils import previews
from bpy.app.handlers import persistent
#--------------------------------------------------------------------------[ PROPERTIES ]
def update_mesh_path(self, context):
new_path = bpy.path.abspath(context.scene.gumdrop.meshes_path)
refresh_meshes(new_path, context)
class MyProperties(PropertyGroup):
boolean_brush: bpy.props.PointerProperty(type=bpy.types.Object)
this_boolean: StringProperty()
#[ EDGE LENGTH ]
vert1: bpy.props.IntProperty(name="v1")
vert2: bpy.props.IntProperty(name="v2")
distance: bpy.props.FloatProperty(name="DISTANCE")
#[ MESHES & ICONS ]
meshes_enum: EnumProperty(
name="",
description="MESH",
items=lambda self, context: update_mesh_list(context),
)
meshes_path: StringProperty(
name="MESHES PATH",
subtype='DIR_PATH',
update=update_mesh_path
)
meshes_total: IntProperty(
name="MESHES TOTAL",
)
meshes_enum_1: EnumProperty(
name="",
description="MESH",
items=lambda self, context: update_mesh_list(context),
)
meshes_enum_2: EnumProperty(
name="",
description="MESH",
items=lambda self, context: update_mesh_list(context),
)
#[ LOCAL VIEW ]
group_local: BoolProperty(
name="",
description="IS GROUP LOCAL",
default=False
)
this_collection: StringProperty(
name="COLLECTION",
)
#[ COLORS ]
object_color: FloatVectorProperty(
name="",
subtype="COLOR",
size=4,
min=0.0,
max=1.0,
default=(0.5, 0.5, 0.5, 1.0)
)
color1: FloatVectorProperty(
name="",
subtype='COLOR',
size=4,
min=0.0,
max=1.0,
default=(0.2, 0.2, 0.2, 1.0)
)
color2: FloatVectorProperty(
name="",
subtype='COLOR',
size=4,
min=0.0,
max=1.0,
default=(0.5, 0.5, 0.5, 1.0)
)
color3: FloatVectorProperty(
name="",
subtype='COLOR',
size=4,
min=0.0,
max=1.0,
default=(0.8, 0.8, 0.8, 1.0)
)
color4: FloatVectorProperty(
name="",
subtype='COLOR',
size=4,
min=0.0,
max=1.0,
default=(1, 1, 1, 1.0)
)
#[ UV ]
uv_map_index: IntProperty(
name="UV INDEX",
description="SELECTED UV INDEX",
default=0
)
#--------------------------------------------------------------------------[ RENAME OBJECT AND DATA ]
class RenameObjectAndMesh(bpy.types.Operator):
bl_idname = "object.rename_object_and_data"
bl_label = "RENAME"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Rename Object & Data"
new_name: bpy.props.StringProperty(name="Name")
def invoke(self, context, event):
obj = context.active_object
if obj:
self.new_name = obj.name
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
obj = context.active_object
if obj:
obj.name = self.new_name
if obj.type == 'MESH':
obj.data.name = self.new_name
return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.prop(self, "new_name")
#--------------------------------------------------------------------------[ CLEAN UP FILE ]
class CleanUpFile(bpy.types.Operator):
bl_idname = "object.clean_up_file"
bl_label = "CLEAN UP"
bl_description = "Clean up .blend file"
def execute(self, context):
bpy.ops.outliner.orphans_purge()
self.report({'INFO'}, "[ DELETED UNUSED DATA ]")
return {'FINISHED'}
#--------------------------------------------------------------------------[ APPLY SCALE ]
class ApplyScale(bpy.types.Operator):
bl_idname = "object.apply_scale"
bl_label = "APPLY SCALE"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Apply object's Scale"
def execute(self, context):
selected_object = context.object
if selected_object.data.users > 1:
self.report({'WARNING'}, "CANNOT APPLY SCALE TO MULTI USER")
else:
if selected_object:
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
return {'FINISHED'}
#--------------------------------------------------------------------------[ APPLY ROTATION ]
class ApplyRotation(bpy.types.Operator):
bl_idname = "object.apply_rotation"
bl_label = "APPLY ROTATION"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Apply object's Rotation"
def execute(self, context):
selected_object = context.object
if selected_object.data.users > 1:
self.report({'WARNING'}, "CANNOT APPLY ROTATION TO MULTI USER")
else:
if selected_object:
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
return {'FINISHED'}
#--------------------------------------------------------------------------[ EXPORT FBX ]
class BatchExportFBX(bpy.types.Operator):
bl_idname = "object.batch_export_fbx"
bl_label = "Batch Export FBX"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Batch Export FBX"
filepath: bpy.props.StringProperty(subtype='DIR_PATH')
def execute(self, context):
selected_objects = context.selected_objects
if context.object and context.object.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
state_data = {}
for obj in selected_objects:
state_data[obj.name] = {
"location": obj.location.copy(),
"rotation_euler": obj.rotation_euler.copy(),
"materials": [slot.material for slot in obj.material_slots]
}
for obj in selected_objects:
obj_name = obj.name
export_path = os.path.join(self.filepath, obj_name + ".fbx")
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
context.view_layer.objects.active = obj
#[ CLEAR LOCATION, ROTATE Z -90, CLEAR MATERIALS ]
bpy.ops.object.location_clear()
bpy.ops.transform.rotate(value=1.5708, orient_axis='Z')
bpy.context.object.data.materials.clear()
bpy.ops.export_scene.fbx(
filepath=export_path,
use_selection=True,
mesh_smooth_type='FACE',
use_tspace=True
)
for obj in selected_objects:
original = state_data[obj.name]
obj.location = original["location"]
obj.rotation_euler = original["rotation_euler"]
obj.data.materials.clear()
for mat in original["materials"]:
obj.data.materials.append(mat)
self.report({'INFO'}, "[ FBX EXPORTED, OBJECT STATE RETURNED ]")
return {'FINISHED'}
def invoke(self, context, event):
self.filepath = ""
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
#--------------------------------------------------------------------------[ UNDO ]
class Undo(bpy.types.Operator):
bl_idname = "object.undo"
bl_label = "UNDO"
bl_description = "Undo FBX export"
def execute(self, context):
bpy.ops.ed.undo()
bpy.ops.ed.undo()
return {'FINISHED'}
#--------------------------------------------------------------------------[ SET OBJECT COLOR ]
class SetObjectColor(bpy.types.Operator):
bl_idname = "object.set_color"
bl_label = "SET OBJECT COLOR"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.shading.color_type = 'OBJECT'
break
for obj in bpy.context.selected_objects:
obj.color = context.scene.gumdrop.object_color
return {'FINISHED'}
#--------------------------------------------------------------------------[ ARRANGE OBJECTS ]
class ArrangeObjects(bpy.types.Operator):
bl_idname = "object.arrange_objects"
bl_label = "Arrange"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Arrange selected Objects in a Grid"
def execute(self, context):
selected_objects = context.selected_objects
if not selected_objects:
self.report({'WARNING'}, "NO OBJECTS SELECTED")
return {'CANCELLED'}
max_x = 0
max_y = 0
for obj in selected_objects:
bbox = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
x_size = max([v[0] for v in bbox]) - min([v[0] for v in bbox])
y_size = max([v[1] for v in bbox]) - min([v[1] for v in bbox])
max_x = max(max_x, x_size)
max_y = max(max_y, y_size)
# ARRANGE OBJECTS
grid_spacing_x = max_x
grid_spacing_y = max_y
grid_columns = math.ceil(math.sqrt(len(selected_objects)))
grid_rows = math.ceil(len(selected_objects) / grid_columns)
for index, obj in enumerate(selected_objects):
row = index // grid_columns
column = index % grid_columns
obj.location.x = column * grid_spacing_x
obj.location.y = row * grid_spacing_y
return {'FINISHED'}
#--------------------------------------------------------------------------[ CLEAR CURSOR ROTATION, CURSOR TO ACTIVE ]
class ClearCursorRotation(bpy.types.Operator):
bl_idname = "object.clear"
bl_label = "CLEAR ROTATION"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Clear 3D Cursor Rotation"
def execute(self, context):
bpy.context.scene.cursor.rotation_euler = (0.0, 0.0, 0.0)
return {'FINISHED'}
class CursorToActive(bpy.types.Operator):
bl_idname = "object.cursor_to_active"
bl_label = "CURSOR TO ACTIVE"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Snap 3D Cursor to the active item"
def execute(self, context):
bpy.ops.view3d.snap_cursor_to_active()
return {'FINISHED'}
#--------------------------------------------------------------------------[ COLLECTIONS & GROUPS ]
class Collections(bpy.types.Operator):
bl_idname = "gumdrop.collections"
bl_label = "SELECT COLLECTION"
bl_options = {'REGISTER', 'UNDO'}
collection_name: bpy.props.StringProperty()
def select_objects_recursive(self, collection):
for obj in collection.objects:
obj.select_set(True)
for sub_collection in collection.children:
self.select_objects_recursive(sub_collection)
def execute(self, context):
collection = bpy.data.collections.get(self.collection_name)
if collection.objects:
#bpy.ops.object.select_all(action='DESELECT')
self.select_objects_recursive(collection)
context.view_layer.objects.active = bpy.context.selected_objects[-1]
else:
self.report({'INFO'}, f"[ NO OBJECTS TO SELECT ]")
return {'FINISHED'}
class AddNewCollection(bpy.types.Operator):
bl_idname = "gumdrop.add_new_collection"
bl_label = "NEW COLLECTION"
bl_options = {'REGISTER', 'UNDO'}
collection_name: bpy.props.StringProperty(name="Name", default="")
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
scene = context.scene
new_collection = bpy.data.collections.new(self.collection_name)
scene.collection.children.link(new_collection)
new_collection.use_fake_user = True
self.report({'INFO'}, f"[ COLLECTION '{self.collection_name}' ADDED ]")
return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.prop(self, "collection_name")
class GroupCollections(bpy.types.Operator):
bl_idname = "gumdrop.group_collections"
bl_label = "SELECT GROUP"
bl_options = {'REGISTER', 'UNDO'}
collection_name: bpy.props.StringProperty()
def execute(self, context):
collection = bpy.data.collections.get(self.collection_name)
if collection.objects:
bpy.ops.object.select_same_collection(collection=collection.name)
if bpy.context.active_object != None:
context.view_layer.objects.active = bpy.context.selected_objects[-1]
else:
self.report({'INFO'}, f"[ NO OBJECTS TO SELECT ]")
return {'FINISHED'}
class AddNewGroup(bpy.types.Operator):
bl_idname = "gumdrop.add_new_group"
bl_label = "NEW GROUP"
bl_options = {'REGISTER', 'UNDO'}
group_name: bpy.props.StringProperty(name="Name", default="")
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
scene = context.scene
selected_objects = context.selected_objects
if selected_objects:
new_group = bpy.data.collections.new(self.group_name)
self.report({'INFO'}, f"[ GROUP '{self.group_name}' ADDED ]")
for obj in selected_objects:
if obj.name not in new_group.objects:
new_group.objects.link(obj)
new_group.use_fake_user = True
else:
self.report({'INFO'}, "[ NO OBJECTS SELECTED ]")
return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.prop(self, "group_name")
def get_collections(self, context):
scene = context.scene
scene_collections = list(scene.collection.children)
all_collections = bpy.data.collections
group_collections = [collection for collection in all_collections if collection not in scene_collections]
return [(coll.name, coll.name, "") for coll in group_collections]
class DeleteGroup(bpy.types.Operator):
bl_idname = "gumdrop.delete_group"
bl_label = "DELETE A GROUP"
bl_options = {'REGISTER', 'UNDO'}
collection_name: bpy.props.EnumProperty(
name="GROUP:",
description="Choose a Group to delete",
items=get_collections
)
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
collection = bpy.data.collections.get(self.collection_name)
scene_collection = context.view_layer.layer_collection
context.view_layer.active_layer_collection = scene_collection
if collection:
for obj in collection.objects:
collection.objects.unlink(obj)
bpy.data.collections.remove(collection)
return {'FINISHED'}
class RemoveFromGroup(bpy.types.Operator):
bl_idname = "gumdrop.remove_from_group"
bl_label = "REMOVE OBJECT FROM GROUP"
bl_options = {'REGISTER', 'UNDO'}
collection_name: bpy.props.StringProperty()
def execute(self, context):
collection = bpy.data.collections.get(self.collection_name)
obj = bpy.context.active_object
collection.objects.unlink(obj)
return {'FINISHED'}
class AddToGroup(bpy.types.Operator):
bl_idname = "gumdrop.add_object_to_group"
bl_label = "ADD TO GROUP"
bl_options = {'REGISTER', 'UNDO'}
collection_name: bpy.props.StringProperty()
def execute(self, context):
collection = bpy.data.collections.get(self.collection_name)
if collection:
for obj in context.selected_objects:
if obj.name not in collection.objects:
collection.objects.link(obj)
return {'FINISHED'}
class MoveToCollection(bpy.types.Operator):
bl_idname = "gumdrop.move_to_collection"
bl_label = "MOVE TO COLLECTION"
bl_options = {'REGISTER', 'UNDO'}
collection_name: bpy.props.StringProperty()
def execute(self, context):
scene = context.scene
collection = bpy.data.collections.get(self.collection_name)
all_collections = bpy.data.collections
scene_collections = list(scene.collection.children)
group_collections = [collection for collection in all_collections if collection not in scene_collections]
selected_objects = list(bpy.context.selected_objects)
for obj in selected_objects:
for other_col in obj.users_collection:
if other_col not in group_collections:
other_col.objects.unlink(obj)
if obj.name not in collection.objects:
collection.objects.link(obj)
return {'FINISHED'}
#--------------------------------------------------------------------------[ LOCAL VIEW COLLECTION & GROUP ]
class LocalViewCollection(bpy.types.Operator):
bl_idname = "gumdrop.local_view_collection"
bl_label = "LOCAL VIEW COLLECTION"
bl_options = {'REGISTER', 'UNDO'}
collection_name: bpy.props.StringProperty()
def execute(self, context):
scene = context.scene
area = next(a for a in bpy.context.screen.areas if a.type == "VIEW_3D")
space = area.spaces.active
collection = bpy.data.collections.get(self.collection_name)
if space.local_view:
if context.scene.gumdrop.group_local == True:
context.scene.gumdrop.this_collection = collection.name
bpy.ops.view3d.localview()
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
if bpy.context.active_object != None:
context.scene.gumdrop.group_local = False
if collection.objects:
bpy.ops.object.select_same_collection(collection=collection.name)
if bpy.context.active_object != None:
context.view_layer.objects.active = bpy.context.selected_objects[-1]
bpy.ops.view3d.localview()
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
else:
self.report({'INFO'}, f"[ NO OBJECTS TO SELECT ]")
else:
if context.scene.gumdrop.this_collection == collection.name:
bpy.ops.view3d.localview()
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
else:
context.scene.gumdrop.this_collection = collection.name
bpy.ops.view3d.localview()
if collection.objects:
bpy.ops.object.select_same_collection(collection=collection.name)
if bpy.context.active_object != None:
context.view_layer.objects.active = bpy.context.selected_objects[-1]
bpy.ops.view3d.localview()
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
else:
self.report({'INFO'}, f"[ NO OBJECTS TO SELECT ]")
else:
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
if bpy.context.active_object != None:
context.scene.gumdrop.this_collection = collection.name
context.scene.gumdrop.group_local = False
if collection.objects:
bpy.ops.object.select_same_collection(collection=collection.name)
if bpy.context.active_object != None:
context.view_layer.objects.active = bpy.context.selected_objects[-1]
bpy.ops.view3d.localview()
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
else:
self.report({'INFO'}, f"[ NO OBJECTS TO SELECT ]")
return {'FINISHED'}
class LocalViewGroup(bpy.types.Operator):
bl_idname = "gumdrop.local_view_group"
bl_label = "LOCAL VIEW GROUP"
bl_options = {'REGISTER', 'UNDO'}
collection_name: bpy.props.StringProperty()
def execute(self, context):
collection = bpy.data.collections.get(self.collection_name)
area = next(a for a in bpy.context.screen.areas if a.type == "VIEW_3D")
space = area.spaces.active
def is_object_in_collection(obj, collection):
return obj.name in collection.objects
def local_view_group():
if space.local_view:
if bpy.context.active_object and is_object_in_collection(bpy.context.active_object, collection):
if collection.name == context.scene.gumdrop.this_collection:
bpy.ops.view3d.localview()
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
context.scene.gumdrop.group_local = False
else:
context.scene.gumdrop.this_collection = collection.name
bpy.ops.view3d.localview()
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
if collection.objects:
bpy.ops.object.select_same_collection(collection=collection.name)
if bpy.context.active_object != None:
context.view_layer.objects.active = bpy.context.selected_objects[-1]
bpy.ops.view3d.localview()
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
else:
self.report({'INFO'}, f"[ NO OBJECTS TO SELECT ]")
else:
context.scene.gumdrop.this_collection = collection.name
bpy.ops.view3d.localview()
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
if collection.objects:
bpy.ops.object.select_same_collection(collection=collection.name)
if bpy.context.active_object != None:
context.view_layer.objects.active = bpy.context.selected_objects[-1]
bpy.ops.view3d.localview()
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
else:
self.report({'INFO'}, f"[ NO OBJECTS TO SELECT ]")
else:
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
if bpy.context.active_object != None:
if collection.objects:
bpy.ops.object.select_same_collection(collection=collection.name)
if bpy.context.active_object != None:
context.view_layer.objects.active = bpy.context.selected_objects[-1]
bpy.ops.view3d.localview()
if bpy.context.active_object != None:
bpy.ops.object.select_all(action='DESELECT')
context.scene.gumdrop.group_local = True
else:
self.report({'INFO'}, f"[ NO OBJECTS TO SELECT ]")
if bpy.context.active_object:
local_view_group()
else:
bpy.ops.object.select_same_collection(collection=collection.name)
context.view_layer.objects.active = bpy.context.selected_objects[-1]
local_view_group()
return {'FINISHED'}
#--------------------------------------------------------------------------[ CENTER MESH XYZ ]
class CenterX(bpy.types.Operator):
bl_idname = "object.center_x"
bl_label = "CENTER X"
bl_options = {'REGISTER', 'UNDO'}
bl_description ="Center mesh X"
def execute(self, context):
def has_rotation(obj):
rotation_euler = obj.rotation_euler
if rotation_euler.x != 0 or rotation_euler.y != 0 or rotation_euler.z != 0:
return True
return False
obj = context.object
if obj:
if has_rotation(obj):
original_rotation = obj.rotation_euler.copy()
original_location = obj.location.copy()
local_bbox = obj.bound_box
obj.rotation_euler = (0.0, 0.0, 0.0)
min_x = min(corner[0] for corner in local_bbox)
max_x = max(corner[0] for corner in local_bbox)
bbox_center_x = (min_x + max_x) / 2
obj.location.x -= bbox_center_x
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.translate(value=(-bbox_center_x, 0, 0))
bpy.ops.object.mode_set(mode='OBJECT')
obj.rotation_euler = original_rotation
obj.location = original_location
else:
original_location = obj.location.copy()
local_bbox = obj.bound_box
min_x = min(corner[0] for corner in local_bbox)
max_x = max(corner[0] for corner in local_bbox)
bbox_center_x = (min_x + max_x) / 2
obj.location.x -= bbox_center_x
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.translate(value=(-bbox_center_x, 0, 0))
bpy.ops.object.mode_set(mode='OBJECT')
obj.location = original_location
return {'FINISHED'}
class CenterY(bpy.types.Operator):
bl_idname = "object.center_y"
bl_label = "CENTER Y"
bl_options = {'REGISTER', 'UNDO'}
bl_description ="Center mesh Y"
def execute(self, context):
def has_rotation(obj):
rotation_euler = obj.rotation_euler
if rotation_euler.x != 0 or rotation_euler.y != 0 or rotation_euler.z != 0:
return True
return False
obj = context.object
if obj:
if has_rotation(obj):
original_rotation = obj.rotation_euler.copy()
original_location = obj.location.copy()
local_bbox = obj.bound_box
obj.rotation_euler = (0.0, 0.0, 0.0)
min_y = min(corner[1] for corner in local_bbox)
max_y = max(corner[1] for corner in local_bbox)
bbox_center_y = (min_y + max_y) / 2
obj.location.y -= bbox_center_y
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.translate(value=(0, -bbox_center_y, 0))
bpy.ops.object.mode_set(mode='OBJECT')
obj.rotation_euler = original_rotation
obj.location = original_location
else:
original_location = obj.location.copy()
local_bbox = obj.bound_box
min_y = min(corner[1] for corner in local_bbox)
max_y = max(corner[1] for corner in local_bbox)
bbox_center_y = (min_y + max_y) / 2
obj.location.y -= bbox_center_y
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.translate(value=(0, -bbox_center_y, 0))
bpy.ops.object.mode_set(mode='OBJECT')
obj.location = original_location
return {'FINISHED'}
class CenterZ(bpy.types.Operator):
bl_idname = "object.center_z"
bl_label = "CENTER Z"
bl_options = {'REGISTER', 'UNDO'}
bl_description ="Center mesh Z"
def execute(self, context):
def has_rotation(obj):
rotation_euler = obj.rotation_euler
if rotation_euler.x != 0 or rotation_euler.y != 0 or rotation_euler.z != 0:
return True
return False
obj = context.object
if obj:
if has_rotation(obj):
original_rotation = obj.rotation_euler.copy()
original_location = obj.location.copy()
local_bbox = obj.bound_box
obj.rotation_euler = (0.0, 0.0, 0.0)
min_z = min(corner[2] for corner in local_bbox)
max_z = max(corner[2] for corner in local_bbox)
bbox_center_z = (min_z + max_z) / 2
obj.location.z -= bbox_center_z
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.translate(value=(0, 0, -bbox_center_z))
bpy.ops.object.mode_set(mode='OBJECT')
obj.rotation_euler = original_rotation
obj.location = original_location
else:
original_location = obj.location.copy()
local_bbox = obj.bound_box
min_z = min(corner[2] for corner in local_bbox)
max_z = max(corner[2] for corner in local_bbox)
bbox_center_z = (min_z + max_z) / 2
obj.location.z -= bbox_center_z
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.translate(value=(0, 0, -bbox_center_z))
bpy.ops.object.mode_set(mode='OBJECT')
obj.location = original_location
return {'FINISHED'}
#--------------------------------------------------------------------------[ SHADE BY ANGLE ]
class ShadeByAngle(bpy.types.Operator):
bl_idname = "object.shade_angle"
bl_label = "SHADE BY ANGLE"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Shade Smooth by Angle & reset Normal Vectors"
def execute(self, context):
obj = context.object
if obj:
bpy.ops.object.shade_smooth_by_angle(angle=0.523599, keep_sharp_edges=False)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.normals_tools(mode='RESET')
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
class ShadeByAngleKeepSharp(bpy.types.Operator):
bl_idname = "object.shade_angle_keep_sharp"
bl_label = "SHADE BY ANGLE (KEEP SHARP)"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Shade Smooth by Angle (keep sharp) & reset Normal Vectors"
def execute(self, context):
obj = context.object
if obj:
bpy.ops.object.shade_smooth_by_angle(angle=0.523599, keep_sharp_edges=True)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.normals_tools(mode='RESET')
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
#--------------------------------------------------------------------------[ SELECT NGONS ]
class SelectNgons(bpy.types.Operator):
bl_idname = "object.select_ngons"
bl_label = "SELECT NGONS"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.mesh.select_face_by_sides(number=4, type='GREATER', extend=True)
obj = context.object
mesh = obj.data
selected_faces = [f for f in mesh.polygons if f.select]
if not selected_faces:
self.report({'INFO'}, "NO NGONS")
else:
bpy.ops.view3d.view_selected()
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.mode_set(mode='EDIT')
return {'FINISHED'}
#--------------------------------------------------------------------------[ GENERATE MESH ICONS ]
def fit_camera_to_object(camera, obj):
camera.rotation_euler = (math.radians(60), 0, 0)
if obj.type == 'ARMATURE':
for child in obj.children:
if child.type == 'MESH':
child.select_set(True)
else:
obj.select_set(True)
bpy.ops.view3d.camera_to_view_selected()
def generate_thumbnail(fbx_path, thumbnail_path):
#[ CREATE SCENE ]
bpy.ops.scene.new(type='NEW')
scene = bpy.context.scene
#[ CLEAR SCENE ]
bpy.ops.object.select_all(action='DESELECT')
bpy.ops.object.delete(use_global=False)
#[ IMPORT ]
bpy.ops.import_scene.fbx(filepath=fbx_path)
bpy.ops.object.make_single_user(object=True, obdata=True, material=False, animation=False, obdata_animation=False)
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
selected_objects = bpy.context.selected_objects
parent_obj = None
for obj in selected_objects:
if obj.parent is None:
parent_obj = obj
break
else:
parent_obj = None
for obj in selected_objects:
if obj.parent == parent_obj:
obj.select_set(True)
bpy.context.view_layer.objects.active = parent_obj
bpy.ops.object.join()
obj = parent_obj
#[ CAMERA ]
bpy.ops.object.camera_add(location=(0, 0, 0))
camera = bpy.context.object
scene.camera = camera
camera.data.type = 'ORTHO'
obj.rotation_euler.z = math.radians(-45)
obj.select_set(True)
bpy.ops.object.transform_apply(rotation=True)
if obj.type != 'ARMATURE':
mesh = obj.data
for face in mesh.polygons:
face.use_smooth = True
bbox_local = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]
min_x = min(coord.x for coord in bbox_local)
max_x = max(coord.x for coord in bbox_local)
min_y = min(coord.y for coord in bbox_local)
max_y = max(coord.y for coord in bbox_local)
min_z = min(coord.z for coord in bbox_local)
max_z = max(coord.z for coord in bbox_local)
center_x = (min_x + max_x) / 2
obj_width = max_x - min_x
obj_depth = max_y - min_y
obj_height = max_z - min_z
max_dim = max(obj_width, obj_depth, obj_height)
fit_camera_to_object(camera, obj)
camera.data.ortho_scale = max_dim + 0.05
camera.location = (center_x, camera.location.y, camera.location.z)
# Set render settings
scene.render.engine = 'BLENDER_WORKBENCH'
scene.render.resolution_x = 256
scene.render.resolution_y = 256
scene.render.image_settings.file_format = 'PNG'
scene.render.image_settings.compression = 0
scene.render.image_settings.color_mode = 'RGBA'
scene.render.film_transparent = True
scene.display.render_aa = '16'
scene.render.use_freestyle = True
scene.render.line_thickness = 4
scene.view_settings.look = 'Medium Contrast'
bpy.context.scene.view_layers[0].freestyle_settings.linesets.new(name="ICONS")
line_set = bpy.context.scene.view_layers[0].freestyle_settings.linesets.get("ICONS")
bpy.context.scene.view_layers[0].freestyle_settings.use_smoothness = False
line_set.select_crease = False
line_set.select_border = False
line_set.select_silhouette = False
line_set.select_contour = True
line_set.linestyle.color = (1, 1, 1)
line_set.linestyle.thickness_position = 'OUTSIDE'
line_set.linestyle.thickness = 0.6
line_set.linestyle.use_chaining = True
line_set.linestyle.chaining = 'SKETCHY'
line_set.linestyle.caps = 'ROUND'
#[ MATCAP AND CAVITY ]
shading = scene.display.shading
shading.light = 'MATCAP'
shading.show_cavity = True
shading.cavity_type = 'BOTH'
shading.studio_light = 'NATHO_MATCAP_1.exr'
shading.show_object_outline = True
shading.object_outline_color = (0, 0, 0)
shading.cavity_ridge_factor = 1.1
shading.cavity_valley_factor = 2
shading.curvature_ridge_factor = 1
shading.curvature_valley_factor = 2
shading.show_shadows = True
# Render the scene
bpy.context.scene.render.filepath = thumbnail_path
bpy.ops.render.render(write_still=True)
bpy.context.scene.view_layers[0].freestyle_settings.linesets.remove(line_set)
bpy.data.scenes.remove(scene)
def generate_icons(context):
supported_extensions = ('.fbx',)
meshes_path = bpy.path.abspath(context.scene.gumdrop.meshes_path)
icons_path = os.path.join(meshes_path, "MESH_ICONS")
if not os.path.exists(icons_path):
os.makedirs(icons_path)
files = [file for file in os.listdir(meshes_path) if file.endswith(supported_extensions)]
total_files = len(files)
progress = 0
context.window_manager.progress_begin(0, total_files)
for file in os.listdir(meshes_path):
if file.endswith(supported_extensions):
filepath = os.path.join(meshes_path, file)
icon_path = os.path.join(icons_path, os.path.splitext(file)[0] + ".png")
if not os.path.isfile(icon_path):
generate_thumbnail(filepath, icon_path)
progress += 1
context.window_manager.progress_update(progress)
context.window_manager.progress_end()
return None
class GenerateIcons(bpy.types.Operator):
"""Generate png icons for each Mesh"""
bl_idname = "gumdrop.generate_thumbnails"
bl_label = "GENERATE ICONS"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
generate_icons(context)
self.report({'INFO'}, "[ ICONS COMPLETED ]")
return {'FINISHED'}
#--------------------------------------------------------------------------[ MESH ICON PREVIEWS ]
preview_collections = {}
def generate_previews(context):
global preview_collections
meshes_path = bpy.path.abspath(context.scene.gumdrop.meshes_path)
icons_path = os.path.join(meshes_path, "MESH_ICONS")
for pcoll in preview_collections.values():
bpy.utils.previews.remove(pcoll)
preview_collections.clear()
pcoll = previews.new()
preview_collections["MESH_PREVIEWS"] = pcoll
for icon_file in os.listdir(icons_path):
if icon_file.endswith(".png"):
icon_path = os.path.join(icons_path, icon_file)
icon_name = os.path.splitext(icon_file)[0]
pcoll.load(icon_name, icon_path, 'IMAGE')
@persistent
def load_previews_handler(scene):
generate_previews(bpy.context)
bpy.context.preferences.filepaths.use_relative_paths = False
bpy.app.handlers.load_post.append(load_previews_handler)
def update_mesh_list(context):
supported_extensions = ['.fbx',]
global preview_collections
pcoll = preview_collections.get("MESH_PREVIEWS")
items = []
bpy.app.timers.register(lambda: update_mesh_icons(context))
if context.scene.gumdrop.meshes_path:
for file_name in os.listdir(context.scene.gumdrop.meshes_path):
if os.path.isfile(os.path.join(context.scene.gumdrop.meshes_path, file_name)) and os.path.splitext(file_name)[1].lower() in supported_extensions:
icon_name = os.path.splitext(file_name)[0]
if pcoll and icon_name in pcoll:
icon_id = pcoll[icon_name].icon_id
else:
icon_id = 'FILE_REFRESH'
items.append((file_name, file_name, "", icon_id, len(items)))
return items
def update_mesh_icons(context):
supported_extensions = ('.fbx',)
meshes_path = bpy.path.abspath(context.scene.gumdrop.meshes_path)
icons_path = os.path.join(meshes_path, "MESH_ICONS")
if meshes_path:
files = [file for file in os.listdir(meshes_path) if file.endswith(supported_extensions)]
total_files = len(files)
for file in os.listdir(meshes_path):
if file.endswith(supported_extensions):
filepath = os.path.join(meshes_path, file)
icon_path = os.path.join(icons_path, os.path.splitext(file)[0] + ".png")
if not os.path.isfile(icon_path):
generate_thumbnail(filepath, icon_path)
return None
#--------------------------------------------------------------------------[ ADD MESHES ]
class AddMesh(bpy.types.Operator):
bl_idname = "object.add_mesh"
bl_label = "ADD MESH"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Add selected Mesh to Scene"
mesh_file: bpy.props.StringProperty()
def execute(self, context):
if not self.mesh_file:
self.mesh_file = context.scene.gumdrop.meshes_enum
mesh_file = self.mesh_file
file_path = os.path.join(context.scene.gumdrop.meshes_path, mesh_file)
ext = os.path.splitext(mesh_file)[1].lower()
existing_objects = set(bpy.data.objects)
if ext == '.fbx':
bpy.ops.import_scene.fbx(filepath=file_path)
new_objects = set(bpy.data.objects) - existing_objects
if new_objects:
new_obj = new_objects.pop()
new_obj.select_set(True)
new_obj.data.materials.clear()
bpy.ops.outliner.orphans_purge()
selected_objects = bpy.context.selected_objects
parent_obj = None
for obj in selected_objects:
if obj.parent is None:
parent_obj = obj
break
else:
parent_obj = None
for obj in selected_objects:
if obj.parent == parent_obj:
obj.select_set(True)
context.view_layer.objects.active = parent_obj
parent_obj.location = context.scene.cursor.location
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
return {'FINISHED'}
class AddMainMesh(bpy.types.Operator):
bl_idname = "object.add_main_mesh"
bl_label = "ADD MESH"
bl_options = {'REGISTER', 'UNDO'}
bl_description = "Add selected Mesh to Scene (Ctrl+Alt+A)"
mesh_file: bpy.props.StringProperty()
def execute(self, context):
if not self.mesh_file:
self.mesh_file = context.scene.gumdrop.meshes_enum
mesh_file = context.scene.gumdrop.meshes_enum
file_path = os.path.join(context.scene.gumdrop.meshes_path, mesh_file)
ext = os.path.splitext(mesh_file)[1].lower()
existing_objects = set(bpy.data.objects)
if ext == '.fbx':
bpy.ops.import_scene.fbx(filepath=file_path)
new_objects = set(bpy.data.objects) - existing_objects
if new_objects:
new_obj = new_objects.pop()
new_obj.select_set(True)
new_obj.data.materials.clear()
bpy.ops.outliner.orphans_purge()
selected_objects = bpy.context.selected_objects
parent_obj = None
for obj in selected_objects:
if obj.parent is None:
parent_obj = obj
break
else:
parent_obj = None
for obj in selected_objects:
if obj.parent == parent_obj:
obj.select_set(True)
context.view_layer.objects.active = parent_obj
parent_obj.location = context.scene.cursor.location
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
return {'FINISHED'}
def refresh_meshes(newpath, context):
#[ UPDATE MESH LIST ]
update_mesh_list(context)
#[ GENERATE ICONS ]
generate_icons(context)
#[ GENERATE PREVIEWS ]
generate_previews(context)
class RefreshMeshes(bpy.types.Operator):
bl_idname = "gumdrop.refresh_meshes"
bl_label = "REFRESH MESHES & ICONS"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
refresh_meshes(context.scene.gumdrop.meshes_path, context)
return {'FINISHED'}
#--------------------------------------------------------------------------[ SHOW FACE ORIENTATION ]
class ShowFaceOrientation(bpy.types.Operator):
bl_idname = "object.show_face_orientation"
bl_label = "SHOW FACE ORIENTATION"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.overlay.show_face_orientation = not space.overlay.show_face_orientation
return {'FINISHED'}
#--------------------------------------------------------------------------[ MAKE SINGLE ]
class MakeSingleUserYep(bpy.types.Operator):
bl_idname = "object.make_single_user_yep"
bl_label = "MAKE SINGLE USER"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
obj = context.active_object
if obj:
bpy.ops.object.make_single_user(object=True, obdata=True, material=False, animation=False, obdata_animation=False)
return {'FINISHED'}
#--------------------------------------------------------------------------[ GET EDGE LENGTH ]
class GetEdgeLength(bpy.types.Operator):
bl_idname = "object.get_edge_length"
bl_label = "SELECT EDGE (GET EDGE LENGTH)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
gumdrop = context.scene.gumdrop
obj = context.object
mesh = obj.data
bm = bmesh.from_edit_mesh(mesh)
verts = [v for v in bm.verts if v.select]
gumdrop.vert1 = verts[0].index
gumdrop.vert2 = verts[1].index
return {'FINISHED'}
#--------------------------------------------------------------------------[ BOOLEAN OPERATORS ]
class BooleanUnion(bpy.types.Operator):
bl_idname = "object.boolean_union"
bl_label = "BOOLEAN (UNION)"
bl_description = "BOOLEAN (UNION)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if len(context.selected_objects) < 2:
self.report({'WARNING'}, "[ SELECT AT LEAST TWO OBJECTS ]")
return {'CANCELLED'}
active_obj = context.active_object
if active_obj is None or active_obj.type != 'MESH':
self.report({'WARNING'}, "[ ACTIVE OBJECT IS NOT A MESH ]")
return {'CANCELLED'}
mirror_mod = None
mirror_settings = None
for mod in active_obj.modifiers:
if mod.type == 'MIRROR':
mirror_mod = mod
mirror_settings = {
"use_axis": list(mod.use_axis),
"use_clip": mod.use_clip,
"mirror_object": mod.mirror_object,
}
break
if mirror_mod:
active_obj.modifiers.remove(mirror_mod)
other_mirror_mod = None
other_obj = None
for obj in context.selected_objects:
if obj != active_obj:
other_obj = obj
break
for mod in other_obj.modifiers:
if mod.type == 'MIRROR':
other_mirror_mod = mod
if other_mirror_mod:
other_obj.modifiers.remove(other_mirror_mod)
bool_union = active_obj.modifiers.new(name="Boolean", type='BOOLEAN')
bool_union.operation = 'UNION'
for obj in context.selected_objects:
if obj != active_obj:
bool_union.object = obj
break
bpy.ops.object.modifier_apply(modifier=bool_union.name)
if mirror_settings:
mirror_mod = active_obj.modifiers.new(name="Mirror", type='MIRROR')
mirror_mod.use_axis = mirror_settings["use_axis"]
mirror_mod.use_clip = mirror_settings["use_clip"]
mirror_mod.mirror_object = mirror_settings["mirror_object"]
for obj in context.selected_objects:
if obj != active_obj:
bpy.data.objects.remove(obj, do_unlink=True)
break
active_obj = bpy.context.active_object
active_obj.select_set(True)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.mesh.select_face_by_sides(number=4, type='GREATER', extend=True)
bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY')
bpy.ops.mesh.tris_convert_to_quads()
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.shade_smooth_by_angle(angle=0.523599, keep_sharp_edges=True)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.normals_tools(mode='RESET')
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
obj_with_boolean = ""
class BooleanDifference(bpy.types.Operator):
bl_idname = "object.boolean_difference"
bl_label = "BOOLEAN (DIFFERENCE)"
bl_description = "BOOLEAN (DIFFERENCE)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global obj_with_boolean
if len(context.selected_objects) < 2:
self.report({'WARNING'}, "[ SELECT AT LEAST TWO OBJECTS ]")
return {'CANCELLED'}
active_obj = context.active_object
self.obj_with_boolean = active_obj.name
if active_obj is None or active_obj.type != 'MESH':
self.report({'WARNING'}, "[ ACTIVE OBJECT IS NOT A MESH ]")
return {'CANCELLED'}
mirror_mod = None
mirror_settings = None
for mod in active_obj.modifiers:
if mod.type == 'MIRROR':
mirror_mod = mod
mirror_settings = {
"use_axis": list(mod.use_axis),
"use_clip": mod.use_clip,
"mirror_object": mod.mirror_object,
}
break
if mirror_mod:
active_obj.modifiers.remove(mirror_mod)
other_mirror_mod = None
other_obj = None
for obj in context.selected_objects:
if obj != active_obj:
other_obj = obj
break
for mod in other_obj.modifiers:
if mod.type == 'MIRROR':
other_mirror_mod = mod
if other_mirror_mod:
other_obj.modifiers.remove(other_mirror_mod)
bool_difference = active_obj.modifiers.new(name="Boolean", type='BOOLEAN')
bool_difference.operation = 'DIFFERENCE'
context.scene.gumdrop.this_boolean = bool_difference.name
for obj in context.selected_objects:
if obj != active_obj:
bool_difference.object = obj
obj.display_type = 'BOUNDS'
obj.hide_render = True
obj.visible_camera = False
context.scene.gumdrop.boolean_brush = obj
break
if mirror_settings:
mirror_mod = active_obj.modifiers.new(name="Mirror", type='MIRROR')
mirror_mod.use_axis = mirror_settings["use_axis"]
mirror_mod.use_clip = mirror_settings["use_clip"]
mirror_mod.mirror_object = mirror_settings["mirror_object"]
return {'FINISHED'}
class ApplyBoolean(bpy.types.Operator):
bl_idname = "object.apply_boolean"
bl_label = "APPLY BOOLEAN (DIFFERENCE)"
bl_description = "APPLY BOOLEAN (DIFFERENCE)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global obj_with_boolean
boolean_brush = context.scene.gumdrop.boolean_brush
bpy.ops.object.modifier_apply(modifier=context.scene.gumdrop.this_boolean)
boolean_brush.select_set(True)
bpy.data.objects.remove(boolean_brush, do_unlink=True)
active_obj = bpy.context.active_object
active_obj.select_set(True)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.mesh.select_face_by_sides(number=4, type='GREATER', extend=True)
bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY', ngon_method='BEAUTY')
bpy.ops.mesh.tris_convert_to_quads()
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.shade_smooth_by_angle(angle=0.523599, keep_sharp_edges=True)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.normals_tools(mode='RESET')
bpy.ops.object.mode_set(mode='OBJECT')
obj_with_boolean = ""
return {'FINISHED'}
#--------------------------------------------------------------------------[ ORIGIN TO GEOMETRY ]
class OriginToGeometry(bpy.types.Operator):
bl_idname = "object.origin_to_geometry"
bl_label = "ORIGIN TO GEOMETRY (BOUNDS CENTER)"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
return {'FINISHED'}
#--------------------------------------------------------------------------[ ADD UV MAP TO SELECTED OBJECTS ]
class AddUVMap(bpy.types.Operator):
bl_idname = "object.add_uv_map"
bl_label = "ADD UV MAP"
bl_description = "ADD UV MAP TO SELECTED OBJECTS"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
selected_objects = context.selected_objects
for obj in selected_objects:
if obj.type == 'MESH':
mesh = obj.data
uv_map = mesh.uv_layers.new(name="UVMap")
self.report({'INFO'}, f"[ ADDED UV MAP ][ {obj.name} ]")
return {'FINISHED'}
#--------------------------------------------------------------------------[ RENAME UV MAP OF SELECTED OBJECTS ]
class RenameUVMap(bpy.types.Operator):
bl_idname = "object.rename_uv_map"
bl_label = "RENAME UV MAP"
bl_description = "RENAME UV MAP OF SELECTED OBJECTS"
bl_options = {'REGISTER', 'UNDO'}
new_name: bpy.props.StringProperty(name="Name")
def invoke(self, context, event):
self.new_name = ""
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
selected_objects = context.selected_objects
for obj in selected_objects:
if obj.type == 'MESH':
if context.scene.gumdrop.uv_map_index == 1:
uv_map = obj.data.uv_layers[0]
uv_map.name = self.new_name
self.report({'INFO'}, f"[ RENAMED UV MAP ][ {obj.name} ]")
return {'FINISHED'}
def draw(self, context):
layout = self.layout
layout.prop(self, "new_name")
#--------------------------------------------------------------------------[ UV TO TEXTURE ARRAY INDEX ]
class UVToTextureArray(bpy.types.Operator):
bl_idname = "object.uv_to_texture_array"
bl_label = "UV TO TEXTURE ARRAY INDEX"
bl_description = "CONVERT UV TO TEXTURE ARRAY INDEX [ SCALE X TO 0, POSITION X TO 0 ]"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
selected_objects = context.selected_objects
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
current_area = bpy.context.area
if current_area.type == 'VIEW_3D':
current_area.type = 'IMAGE_EDITOR'
bpy.ops.uv.select_all(action='SELECT')
bpy.ops.uv.snap_selected(target='CURSOR')
bpy.ops.object.mode_set(mode='OBJECT')
current_area.type = 'VIEW_3D'
self.report({'INFO'}, f"[ CONVERTED UV MAP TO TEXTURE ARRAY INDEX ]")
return {'FINISHED'}
#--------------------------------------------------------------------------[ REMOVE VERTEX COLOR ]
class RemoveVertexColor(bpy.types.Operator):
bl_idname = "object.remove_vertex_color"
bl_label = "REMOVE VERTEX COLOR"
bl_description = "REMOVE VERTEX COLOR"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
for obj in bpy.context.selected_objects:
if obj.type == 'MESH':
mesh = obj.data
for layer in reversed(mesh.vertex_colors):
mesh.vertex_colors.remove(layer)
mesh.update()
self.report({'INFO'}, (f"[ REMOVED VERTEX COLOR ]"))
return {'FINISHED'}
#--------------------------------------------------------------------------[ GUMDROP 2.0 TOOLS PANEL ]
class GumDropPanelTwo(bpy.types.Panel):
bl_label = "COLLECTIONS"
bl_idname = "GUMDROPPANELTWO_PT_gumdrop"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
@classmethod
def poll(cls, context):
return context.mode == 'OBJECT'
def draw(self, context):
layout=self.layout
scene = context.scene
#[ OBJECT COLLECTIONS ]
all_collections = bpy.data.collections
scene_collections = list(scene.collection.children)
active_obj = bpy.context.active_object
if active_obj:
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
row.label(text="", icon='OBJECT_DATAMODE')
if all_collections:
for collection in all_collections:
if collection in active_obj.users_collection:
row = col.row(align=True)
if collection in scene_collections:
row.label(text=collection.name, icon='OUTLINER_COLLECTION')
if all_collections:
for collection in all_collections:
if collection in active_obj.users_collection:
row = col.row(align=True)
if collection not in scene_collections:
row.label(text=collection.name, icon='GROUP')
row.operator("gumdrop.remove_from_group", text="", icon='X').collection_name = collection.name
#[ COLLECTIONS ]
scene_collections = list(scene.collection.children)
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
row.label(text="Collections:")
row.operator("gumdrop.add_new_collection", text="", icon='COLLECTION_NEW')
if scene_collections:
for collection in scene_collections:
row = col.row(align=True)
row.label(text=collection.name, icon='OUTLINER_COLLECTION')
row.operator("gumdrop.collections", text="", icon='RESTRICT_SELECT_OFF').collection_name = collection.name
if not any(obj == active_obj for obj in collection.objects):
row.operator("gumdrop.move_to_collection", text="", icon='ADD').collection_name = collection.name
row.operator("gumdrop.local_view_collection", text="", icon='VIEWZOOM').collection_name = collection.name
#[ GROUP COLLECTIONS ]
all_collections = bpy.data.collections
group_collections = [collection for collection in all_collections if collection not in scene_collections]
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
row.label(text="Groups:")
row.operator("gumdrop.add_new_group", text="", icon='COLLECTION_NEW')
row.operator("gumdrop.delete_group", text="", icon='X')
if group_collections:
for collection in group_collections:
row = col.row(align=True)
row.label(text=collection.name, icon='GROUP')
row.operator("gumdrop.group_collections", text="", icon='RESTRICT_SELECT_OFF').collection_name = collection.name
if not any(obj == active_obj for obj in collection.objects):
row.operator("gumdrop.add_object_to_group", text="", icon='ADD').collection_name = collection.name
row.operator("gumdrop.local_view_group", text="", icon='VIEWZOOM').collection_name = collection.name
#--------------------------------------------------------------------------[ ADD MESHES PANEL ]
class AddMeshesPanel(bpy.types.Panel):
bl_label = "ADD MESHES"
bl_idname = "ADDMESHES_PT_gumdrop"
bl_space_type = "VIEW_3D"
bl_region_type = "TOOLS"
@classmethod
def poll(cls, context):
return context.mode == 'OBJECT'
def draw(self, context):
layout=self.layout
scene = context.scene
#[ ADD MESHES ]
if context.scene.gumdrop.meshes_path:
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
meshes_total = len(update_mesh_list(context))
row.prop(scene.gumdrop, "meshes_enum", text="")
if meshes_total != 0:
row.operator("object.add_main_mesh", text="", icon='ADD').mesh_file = scene.gumdrop.meshes_enum
row.operator("gumdrop.refresh_meshes", text="", icon='FILE_REFRESH')
col.template_icon_view(scene.gumdrop, "meshes_enum", show_labels=False, scale=3, scale_popup=4)
row = col.row(align=True)
if meshes_total != 0:
if meshes_total > 1:
row.template_icon_view(scene.gumdrop, "meshes_enum_1", show_labels=False, scale=3, scale_popup=4)
if meshes_total > 2:
row.template_icon_view(scene.gumdrop, "meshes_enum_2", show_labels=False, scale=3, scale_popup=4)
grid = col.grid_flow(columns=2, even_rows=True, even_columns=True, align=True)
if meshes_total != 0:
if meshes_total > 1:
grid.operator("object.add_mesh", text="", icon='ADD').mesh_file = scene.gumdrop.meshes_enum_1
if meshes_total > 2:
grid.operator("object.add_mesh", text="", icon='ADD').mesh_file = scene.gumdrop.meshes_enum_2
row = col.row(align=True)
row.prop(scene.gumdrop, "meshes_path", text="")
else:
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
row.prop(scene.gumdrop, "meshes_path", text="")
#--------------------------------------------------------------------------[ GUMDROP UI PANEL ]
class GumDropPanel(bpy.types.Panel):
bl_label = "GUMDROP"
bl_idname = "GUMDROPPANEL_PT_gumdrop"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'GUMDROP'
def draw(self, context):
layout = self.layout
scene = context.scene
tool_settings = context.tool_settings
space_settings = context.space_data
overlay_settings = space_settings.overlay
gumdrop = scene.gumdrop
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
active_obj = context.active_object
if active_obj:
row.operator("object.batch_export_fbx", text="FBX", icon='EXPORT')
#row.separator()
#row.operator("object.undo", icon='LOOP_BACK')
row = col.row(align=True)
row.operator("object.make_single_user_yep", text="", icon='USER')
row.separator()
row.operator("object.rename_object_and_data", icon='FILE_TEXT')
if active_obj.type == 'MESH':
col.label(text=f"{active_obj.name}", icon='OBJECT_DATAMODE')
col.label(text=f"{active_obj.data.name}", icon='MESH_DATA')
total_triangles = 0
for face in active_obj.data.polygons:
vertices = face.vertices
triangles = len(vertices) - 2
total_triangles += triangles
col.label(text=f"Tris: [ {total_triangles} ]", icon='MESH_DATA')
if active_obj.data.users > 1:
col.label(text=f"Linked: TRUE | {active_obj.data.users}", icon='LINKED')
else:
col.label(text="Linked: FALSE", icon='UNLINKED')
#[ UV ]
row = col.row(align=True);
#row.operator("object.uv_to_texture_array", text="", icon='MOD_UVPROJECT')
#row.separator()
if len(active_obj.data.uv_layers) > 1:
row.prop(scene.gumdrop, "uv_map_index", text="")
row.separator()
row.operator("object.add_uv_map", text="ADD UV", icon='UV')
row.operator("object.rename_uv_map", text="", icon='FILE_TEXT')
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
if active_obj:
row.label(text="", icon='OBJECT_DATAMODE')
row.prop(active_obj, "color", text="")
row.prop(context.scene.gumdrop, "object_color")
row.separator()
row.operator("object.set_color", text="", icon='TOOL_SETTINGS')
row = col.row(align=True)
row.prop(context.scene.gumdrop, "color1")
row.prop(context.scene.gumdrop, "color2")
row.prop(context.scene.gumdrop, "color3")
row.prop(context.scene.gumdrop, "color4")
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
if context.object:
row.operator("object.show_face_orientation", text="", icon='ORIENTATION_NORMAL')
row.separator()
if context.object.mode == 'OBJECT':
row.operator("object.shade_angle", text="", icon='SHADERFX')
row.operator("object.shade_angle_keep_sharp", text="", icon='SHADING_RENDERED')
row.separator()
row.operator("object.remove_vertex_color", text="", icon='COLOR')
if context.object:
if context.object.mode == 'EDIT':
row.operator("object.select_ngons", icon='NORMALS_FACE')
box = layout.box()
col = box.column(align=True)
obj = context.object
if obj:
col.prop(obj, "location", text="Location")
grid = col.grid_flow(columns=2, align=True)
grid.prop(obj, "rotation_euler", text="Rotation")
grid.prop(obj, "scale", text="Scale")
def vector_zero(vec, decimal_points = 4):
return any(round(v, decimal_points) for v in vec)
if vector_zero(obj.rotation_euler) or obj.scale != Vector((1, 1, 1)):
box = layout.box()
col = box.column(align=True)
if vector_zero(obj.rotation_euler):
col.operator("object.apply_rotation", text="APPLY ROTATION", icon='FILE_REFRESH')
if obj.scale != Vector((1, 1, 1)):
col.operator("object.apply_scale", text="APPLY SCALE", icon='MOD_LENGTH')
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
grid = col.grid_flow(columns=2, even_columns=True, align=True)
col = grid.column(align=True)
col.label(text="Dimensions:", icon='SHADING_BBOX')
col.label(text=f"X [ {obj.dimensions.x:.2f} m ]")
col.label(text=f"Y [ {obj.dimensions.y:.2f} m ]")
col.label(text=f"Z [ {obj.dimensions.z:.2f} m ]")
local_bbox = obj.bound_box
min_x = min(corner[0] for corner in local_bbox)
max_x = max(corner[0] for corner in local_bbox)
bbox_center_x = (min_x + max_x) / 2
min_y = min(corner[1] for corner in local_bbox)
max_y = max(corner[1] for corner in local_bbox)
bbox_center_y = (min_y + max_y) / 2
min_z = min(corner[2] for corner in local_bbox)
max_z = max(corner[2] for corner in local_bbox)
bbox_center_z = (min_z + max_z) / 2
col = grid.column(align=True)
col.label(text="Center:", icon='PIVOT_BOUNDBOX')
col.label(text=f"X [ {bbox_center_x:.2f} ]")
col.label(text=f"Y [ {bbox_center_y:.2f} ]")
col.label(text=f"Z [ {bbox_center_z:.2f} ]")
#[ EDGE LENGTH ]
if context.object.mode == 'EDIT':
obj = context.object
if obj.type != 'ARMATURE':
mesh = obj.data
bm = bmesh.from_edit_mesh(mesh)
bm.verts.ensure_lookup_table()
try:
vert1 = bm.verts[gumdrop.vert1]
vert2 = bm.verts[gumdrop.vert2]
except IndexError:
distance = 0
else:
distance = (vert1.co - vert2.co).length
box1 = layout.box()
col = box1.column(align=True)
row = col.row(align=False)
row.label(text=f"Edge Length: [ {distance:.2f} m ]")
row.operator("object.get_edge_length", text="", icon='EDGESEL')
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
row.operator("view3d.snap_selected_to_cursor", text="", icon='SNAP_ON')
row.operator("object.origin_set", text="", icon='OBJECT_ORIGIN').type = 'ORIGIN_CURSOR'
row.separator()
row.operator("object.center_x", text="X", icon='PIVOT_BOUNDBOX')
row.operator("object.center_y", text="Y", icon='PIVOT_BOUNDBOX')
row.operator("object.center_z", text="Z", icon='PIVOT_BOUNDBOX')
row.separator()
row.operator("object.origin_to_geometry", text="", icon='TRANSFORM_ORIGINS')
box = layout.box()
col = box.column(align=False)
col.prop(tool_settings, "snap_elements_base", text=".")
col.prop(tool_settings, "transform_pivot_point", text="")
#[ BOOLEAN OPERATORS ]
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
row.operator("object.boolean_union", text="", icon='SELECT_EXTEND')
row.operator("object.boolean_difference", text="", icon='SELECT_SUBTRACT')
global obj_with_boolean
boolean_brush = context.scene.gumdrop.boolean_brush
if active_obj:
for mod in active_obj.modifiers:
if mod.name == "Boolean":
if mod.object:
if mod.object == boolean_brush:
box = layout.box()
col = box.column(align=True)
col.operator("object.apply_boolean", text="APPLY BOOLEAN", icon='CHECKMARK')
box = layout.box()
col = box.column(align=True)
row = col.row(align=True)
row.operator("object.clear", text="", icon='CURSOR')
row.operator("object.cursor_to_active", text="", icon='PIVOT_CURSOR')
row.separator()
row.operator("object.arrange_objects", text="ARRANGE", icon='MOD_ARRAY')
col.operator("object.clean_up_file", text="CLEAN UP", icon='CURRENT_FILE')
from bpy.utils import register_class, unregister_class
_classes = [
MyProperties,
GumDropPanel,
GumDropPanelTwo,
AddMeshesPanel,
RenameObjectAndMesh,
ApplyScale,
ApplyRotation,
BatchExportFBX,
Undo,
SetObjectColor,
ClearCursorRotation,
CursorToActive,
ArrangeObjects,
CleanUpFile,
Collections,
LocalViewCollection,
LocalViewGroup,
GroupCollections,
AddToGroup,
AddNewCollection,
AddNewGroup,
RemoveFromGroup,
MoveToCollection,
CenterX,
CenterY,
CenterZ,
DeleteGroup,
ShadeByAngle,
ShadeByAngleKeepSharp,
SelectNgons,
GenerateIcons,
AddMesh,
AddMainMesh,
RefreshMeshes,
ShowFaceOrientation,
MakeSingleUserYep,
GetEdgeLength,
BooleanUnion,
BooleanDifference,
ApplyBoolean,
OriginToGeometry,
AddUVMap,
RenameUVMap,
UVToTextureArray,
RemoveVertexColor]
addon_keymaps = []
def register():
for cls in _classes:
register_class(cls)
bpy.types.Scene.gumdrop = PointerProperty(type=MyProperties)
wm = bpy.context.window_manager
km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY')
kmi = km.keymap_items.new(AddMainMesh.bl_idname, 'A', 'PRESS', ctrl=True, alt=True)
addon_keymaps.append((km, kmi))
def unregister():
for cls in _classes:
unregister_class(cls)
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
edge_length_km.keymap_items.remove(edge_length_kmi)
addon_keymaps.clear()
if __name__ == "__main__":
register()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment