Last active
April 19, 2025 08:50
-
-
Save NathoSteveo/0ba2e2077c7989e835eda334759bc99e to your computer and use it in GitHub Desktop.
[ GUMDROP PANELS ][ ADDON FOR BLENDER ]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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