diff --git a/__init__.py b/__init__.py index f9ac261..d7ea31d 100644 --- a/__init__.py +++ b/__init__.py @@ -11,19 +11,35 @@ bl_info = { # Import handling: import bpy +from bpy.app.handlers import persistent + + import os import importlib -from .main import DNA_Generator, Batch_Sorter, Exporter, Batch_Refactorer +# Import files from main directory: -importlib.reload(DNA_Generator) -importlib.reload(Batch_Sorter) -importlib.reload(Exporter) -importlib.reload(Batch_Refactorer) +importList = ['DNA_Generator', 'Batch_Sorter', 'Exporter', 'Batch_Refactorer', 'get_combinations', 'UIList'] + +if bpy in locals(): + importlib.reload(DNA_Generator) + importlib.reload(Batch_Sorter) + importlib.reload(Exporter) + importlib.reload(Batch_Refactorer) + importlib.reload(get_combinations) + importlib.reload(UIList) +else: + from .main import \ + DNA_Generator, \ + Batch_Sorter, \ + Exporter, \ + Batch_Refactorer, \ + get_combinations + + from .ui_Lists import UIList # User input Property Group: - class BMNFTS_PGT_MyProperties(bpy.types.PropertyGroup): # Main BMNFTS Panel properties: @@ -56,9 +72,10 @@ class BMNFTS_PGT_MyProperties(bpy.types.PropertyGroup): name="Animation File Format", description="Select Animation file format", items=[ - ('AVI_JPEG', "AVI_JPEG", "Export NFT as AVI_JPEG"), - ('AVI_RAW', "AVI_RAW", "Export NFT as AVI_RAW"), - ('FFMPEG', "FFMPEG", "Export NFT as FFMPEG") + ('AVI_JPEG', '.avi (AVI_JPEG)', 'Export NFT as AVI_JPEG'), + ('AVI_RAW', '.avi (AVI_RAW)', 'Export NFT as AVI_RAW'), + ('FFMPEG', '.mkv (FFMPEG)', 'Export NFT as FFMPEG'), + ('MP4', '.mp4', 'Export NFT as .mp4') ] ) @@ -73,7 +90,8 @@ class BMNFTS_PGT_MyProperties(bpy.types.PropertyGroup): ('FBX', '.fbx', 'Export NFT as .fbx'), ('OBJ', '.obj', 'Export NFT as .obj'), ('X3D', '.x3d', 'Export NFT as .x3d'), - ('VOX', '.vox', 'Export NFT as .vox, requires the voxwriter add on: https://github.com/Spyduck/voxwriter') + ('STL', '.stl', 'Export NFT as .stl'), + ('VOX', '.vox (Experimental)', 'Export NFT as .vox, requires the voxwriter add on: https://github.com/Spyduck/voxwriter') ] ) @@ -86,7 +104,6 @@ class BMNFTS_PGT_MyProperties(bpy.types.PropertyGroup): # API Panel properties: apiKey: bpy.props.StringProperty(name="API Key", subtype='PASSWORD') - def make_directories(save_path): Blend_My_NFTs_Output = os.path.join(save_path, "Blend_My_NFTs Output", "NFT_Data") batch_json_save_path = os.path.join(Blend_My_NFTs_Output, "Batch_Data") @@ -101,6 +118,21 @@ def make_directories(save_path): os.makedirs(nftBatch_save_path) return Blend_My_NFTs_Output, batch_json_save_path, nftBatch_save_path +# Update NFT count: +combinations: int = 0 + +@persistent +def update_combinations(dummy1, dummy2): + global combinations + + combinations = get_combinations.get_combinations_from_scene() + + redraw_panel() + print(combinations) + +bpy.app.handlers.depsgraph_update_post.append(update_combinations) + +# Main Operators: class createData(bpy.types.Operator): bl_idname = 'create.data' bl_label = 'Create Data' @@ -171,8 +203,6 @@ class refactor_Batches(bpy.types.Operator): Batch_Refactorer.reformatNFTCollection(save_path, Blend_My_NFTs_Output, batch_json_save_path, nftBatch_save_path, cardanoMetaDataBool, solanaMetaDataBool, erc721MetaData) - - # Main Panel: class BMNFTS_PT_MainPanel(bpy.types.Panel): bl_label = "Blend_My_NFTs" @@ -186,6 +216,8 @@ class BMNFTS_PT_MainPanel(bpy.types.Panel): scene = context.scene mytool = scene.my_tool + layout.label(text=f"Maximum Number Of NFTs: {combinations}") + row = layout.row() row.prop(mytool, "nftName") @@ -237,26 +269,84 @@ class BMNFTS_PT_MainPanel(bpy.types.Panel): row.prop(mytool, "erc721MetaData") self.layout.operator("refactor.batches", icon='MESH_CUBE', text="Refactor Batches & create MetaData") - -# API Panel: -class BMNFTS_PT_API_Panel(bpy.types.Panel): - bl_label = "API" - bl_idname = "BMNFTS_PT_API_Panel" - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_category = 'Blend_My_NFTs' - - def draw(self, context): - layout = self.layout - scene = context.scene - mytool = scene.my_tool - row = layout.row() - row.prop(mytool, "apiKey") + row.operator("wm.url_open", text="Documentation", icon='URL').url = "https://github.com/torrinworx/Blend_My_NFTs" + +# # Logic Panel: +# class BMNFTS_PT_LOGIC_Panel(bpy.types.Panel): +# bl_label = "Logic" +# bl_idname = "BMNFTS_PT_LOGIC_Panel" +# bl_space_type = 'VIEW_3D' +# bl_region_type = 'UI' +# bl_category = 'Blend_My_NFTs' +# +# def draw(self, context): +# layout = self.layout +# scene = context.scene +# mytool = scene.my_tool +# +# # Materials Panel: +# +# class BMNFTS_PT_MATERIALS_Panel(bpy.types.Panel): +# bl_label = "Materials" +# bl_idname = "BMNFTS_PT_MATERIALS_Panel" +# bl_space_type = 'VIEW_3D' +# bl_region_type = 'UI' +# bl_category = 'Blend_My_NFTs' +# +# def draw(self, context): +# layout = self.layout +# scene = context.scene +# mytool = scene.my_tool +# +# # API Panel: +# class BMNFTS_PT_API_Panel(bpy.types.Panel): +# bl_label = "API" +# bl_idname = "BMNFTS_PT_API_Panel" +# bl_space_type = 'VIEW_3D' +# bl_region_type = 'UI' +# bl_category = 'Blend_My_NFTs' +# +# def draw(self, context): +# layout = self.layout +# scene = context.scene +# mytool = scene.my_tool +# +# row = layout.row() +# row.prop(mytool, "apiKey") + + +def redraw_panel(): + try: + bpy.utils.unregister_class(BMNFTS_PT_MainPanel) + except: + pass + bpy.utils.register_class(BMNFTS_PT_MainPanel) # Register and Unregister classes from Blender: -classes = (BMNFTS_PGT_MyProperties, BMNFTS_PT_MainPanel, BMNFTS_PT_API_Panel, createData, exportNFTs, refactor_Batches) +classes = ( + BMNFTS_PGT_MyProperties, + BMNFTS_PT_MainPanel, + # BMNFTS_PT_LOGIC_Panel, + # BMNFTS_PT_MATERIALS_Panel, + # BMNFTS_PT_API_Panel, + createData, + exportNFTs, + refactor_Batches, + + # UIList 1: + UIList.CUSTOM_OT_actions, + UIList.CUSTOM_OT_addViewportSelection, + UIList.CUSTOM_OT_printItems, + UIList.CUSTOM_OT_clearList, + UIList.CUSTOM_OT_removeDuplicates, + UIList.CUSTOM_OT_selectItems, + UIList.CUSTOM_OT_deleteObject, + UIList.CUSTOM_UL_items, + UIList.CUSTOM_PT_objectList, + UIList.CUSTOM_PG_objectCollection, +) def register(): for cls in classes: @@ -264,12 +354,19 @@ def register(): bpy.types.Scene.my_tool = bpy.props.PointerProperty(type=BMNFTS_PGT_MyProperties) + # Custom scene properties UIList1 + bpy.types.Scene.custom = bpy.props.CollectionProperty(type=UIList.CUSTOM_PG_objectCollection) + bpy.types.Scene.custom_index = bpy.props.IntProperty() + + def unregister(): for cls in classes: bpy.utils.unregister_class(cls) del bpy.types.Scene.my_tool + del bpy.types.Scene.custom + del bpy.types.Scene.custom_index if __name__ == '__main__': register() diff --git a/main/Exporter.py b/main/Exporter.py index 128c551..9a7423d 100644 --- a/main/Exporter.py +++ b/main/Exporter.py @@ -147,8 +147,17 @@ def render_and_save_NFTs(nftName, maxNFTs, batchToGenerate, batch_json_save_path os.makedirs(animationFolder) bpy.context.scene.render.filepath = animationPath - bpy.context.scene.render.image_settings.file_format = animationFileFormat - bpy.ops.render.render(animation=True) + + if animationFileFormat == 'MP4': + bpy.context.scene.render.image_settings.file_format = "FFMPEG" + + bpy.context.scene.render.ffmpeg.format = 'MPEG4' + bpy.context.scene.render.ffmpeg.codec = 'H264' + bpy.ops.render.render(animation=True) + + else: + bpy.context.scene.render.image_settings.file_format = animationFileFormat + bpy.ops.render.render(animation=True) if enableModelsBlender: print(f"{bcolors.OK}Generating 3D Model{bcolors.RESET}") @@ -191,8 +200,12 @@ def render_and_save_NFTs(nftName, maxNFTs, batchToGenerate, batch_json_save_path bpy.ops.export_scene.x3d(filepath=f"{modelPath}.x3d", check_existing=True, use_selection=True) + elif modelFileFormat == 'STL': + bpy.ops.export_mesh.stl(filepath=f"{modelPath}.stl", + check_existing=True, + use_selection=True) elif modelFileFormat == 'VOX': - bpy.ops.export_vox.some_data(filepath=f"{modelPath}.x3d") + bpy.ops.export_vox.some_data(filepath=f"{modelPath}.vox") if not os.path.exists(metaDataFolder): os.makedirs(metaDataFolder) diff --git a/main/__init__.py b/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main/get_combinations.py b/main/get_combinations.py new file mode 100644 index 0000000..c9c09a8 --- /dev/null +++ b/main/get_combinations.py @@ -0,0 +1,243 @@ +import bpy +import re +import copy + +enableGeneration = False +colorList = [] + +class bcolors: + ''' + The colour of console messages. + ''' + OK = '\033[92m' # GREEN + WARNING = '\033[93m' # YELLOW + ERROR = '\033[91m' # RED + RESET = '\033[0m' # RESET COLOR + +def stripColorFromName(name): + return "_".join(name.split("_")[:-1]) + +def get_combinations_from_scene(): + ''' + Generates important variables, dictionaries, and lists needed to be stored to catalog the NFTs. + :return: listAllCollections, attributeCollections, attributeCollections1, hierarchy, variantMetaData, possibleCombinations + ''' + + coll = bpy.context.scene.collection + + try: + scriptIgnore = bpy.data.collections["Script_Ignore"] + except: + print(f"{bcolors.ERROR} ERROR:\nScript_Ignore collection is not in .blend file scene. Please add the Script_Ignore collection to your " + f".blend file scene. For more information, read the README.md file.\n {bcolors.RESET}") + + listAllCollInScene = [] + listAllCollections = [] + + def traverse_tree(t): + yield t + for child in t.children: + yield from traverse_tree(child) + + for c in traverse_tree(coll): + listAllCollInScene.append(c) + + def listSubIgnoreCollections(): + def getParentSubCollections(collection): + yield collection + for child in collection.children: + yield from getParentSubCollections(child) + + collList = [] + for c in getParentSubCollections(scriptIgnore): + collList.append(c.name) + return collList + + ignoreList = listSubIgnoreCollections() + + for i in listAllCollInScene: + if enableGeneration: + if i.name in colorList: + for j in range(len(colorList[i.name])): + if i.name[-1].isdigit() and i.name not in ignoreList: + listAllCollections.append(i.name + "_" + str(j + 1)) + elif j == 0: + listAllCollections.append(i.name) + elif i.name[-1].isdigit() and i.name not in ignoreList: + listAllCollections.append(i.name + "_0") + else: + listAllCollections.append(i.name) + else: + listAllCollections.append(i.name) + + listAllCollections.remove(scriptIgnore.name) + + if "Scene Collection" in listAllCollections: + listAllCollections.remove("Scene Collection") + + if "Master Collection" in listAllCollections: + listAllCollections.remove("Master Collection") + + def allScriptIgnore(collection): + ''' + Removes all collections, sub collections in Script_Ignore collection from listAllCollections. + ''' + for coll in list(collection.children): + listAllCollections.remove(coll.name) + listColl = list(coll.children) + if len(listColl) > 0: + allScriptIgnore(coll) + + allScriptIgnore(scriptIgnore) + listAllCollections.sort() + + exclude = ["_", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"] + attributeCollections = copy.deepcopy(listAllCollections) + + def filter_num(): + """ + This function removes items from 'attributeCollections' if they include values from the 'exclude' variable. + It removes child collections from the parent collections in from the "listAllCollections" list. + """ + for x in attributeCollections: + if any(a in x for a in exclude): + attributeCollections.remove(x) + + for i in range(len(listAllCollections)): + filter_num() + + attributeVariants = [x for x in listAllCollections if x not in attributeCollections] + attributeCollections1 = copy.deepcopy(attributeCollections) + + def attributeData(attributeVariants): + """ + Creates a dictionary of each attribute + """ + allAttDataList = {} + count = 0 + previousAttribute = "" + for i in attributeVariants: + + def getName(i): + """ + Returns the name of "i" attribute variant + """ + name = i.split("_")[0] + return name + + def getOrder_rarity(i): + """ + Returns the "order", "rarity" and "color" (if enabled) of i attribute variant in a list + """ + x = re.sub(r'[a-zA-Z]', "", i) + a = x.split("_") + del a[0] + return list(a) + + name = getName(i) + orderRarity = getOrder_rarity(i) + + if len(orderRarity) == 0: + print(f"{bcolors.ERROR} \nERROR: {bcolors.RESET}") + print(f"The collection {i} doesn't follow the naming conventions of attributes. Please move this \n" + "colleciton to Script_Ignore or review proper collection format in README.md") + return + + elif len(orderRarity) > 0: + number = orderRarity[0] + if enableGeneration: + if count == 1 or count == 0: + previousAttribute = i.partition("_")[0] + count +=1 + elif i.partition("_")[0] == previousAttribute: + count +=1 + else: + count = 1 + number = str(count) + rarity = orderRarity[1] + if enableGeneration and stripColorFromName(i) in colorList: + color = orderRarity[2] + else: + color = "0" + eachObject = {"name": name, "number": number, "rarity": rarity, "color": color} + allAttDataList[i] = eachObject + return allAttDataList + + variantMetaData = attributeData(attributeVariants) + + def getHierarchy(): + """ + Constructs the hierarchy dictionary from attributeCollections1 and variantMetaData. + """ + hierarchy = {} + for i in attributeCollections1: + colParLong = list(bpy.data.collections[str(i)].children) + colParShort = {} + for x in colParLong: + if enableGeneration: + """ + Append colors to blender name for PNG generator and NFTRecord.json to create the correct list + """ + if x.name in colorList: + for j in range(len(colorList[x.name])): + colParShort[x.name + "_" + str(j+1)] = None + else: + colParShort[x.name + "_0"] = None + else: + colParShort[x.name] = None + hierarchy[i] = colParShort + + for a in hierarchy: + for b in hierarchy[a]: + for x in variantMetaData: + if str(x) == str(b): + (hierarchy[a])[b] = variantMetaData[x] + + return hierarchy + + hierarchy = getHierarchy() + + def numOfCombinations(hierarchy): + """ + Returns "combinations" the number of all possible NFT combinations. + """ + + hierarchyByNum = [] + + for i in hierarchy: + # Ignore Collections with nothing in them + if len(hierarchy[i]) != 0: + hierarchyByNum.append(len(hierarchy[i])) + else: + print(f"The following collection has been identified as empty: {i}") + + combinations = 1 + for i in hierarchyByNum: + combinations = combinations*i + + if combinations == 0: + print(bcolors.ERROR + "\nERROR:" + bcolors.RESET) + print("The number of all possible combinations is equal to 0. Please review your collection hierarchy" + "and ensure it is formatted correctly. Please review README.md for more information. \nHere is the " + "hierarchy of all collections the DNA_Generator gathered from your .blend file, excluding those in " + f"Script_Ignore: {hierarchy}") + + return combinations + + possibleCombinations = numOfCombinations(hierarchy) + + for i in variantMetaData: + def cameraToggle(i, toggle=True): + if enableGeneration: + """ + Remove Color code so blender recognises the collection + """ + i = stripColorFromName(i) + bpy.data.collections[i].hide_render = toggle + bpy.data.collections[i].hide_viewport = toggle + cameraToggle(i) + + return possibleCombinations + +if __name__ == '__main__': + get_combinations_from_scene() \ No newline at end of file diff --git a/ui_Lists/UIList.py b/ui_Lists/UIList.py new file mode 100644 index 0000000..ea4193d --- /dev/null +++ b/ui_Lists/UIList.py @@ -0,0 +1,359 @@ +import bpy + +from bpy.props import (IntProperty, + BoolProperty, + StringProperty, + CollectionProperty, + PointerProperty) + +from bpy.types import (Operator, + Panel, + PropertyGroup, + UIList) + + +# ------------------------------------------------------------------- +# Operators +# ------------------------------------------------------------------- + +class CUSTOM_OT_actions(Operator): + """Move items up and down, add and remove""" + bl_idname = "custom.list_action" + bl_label = "List Actions" + bl_description = "Move items up and down, add and remove" + bl_options = {'REGISTER'} + + action: bpy.props.EnumProperty( + items=( + ('UP', "Up", ""), + ('DOWN', "Down", ""), + ('REMOVE', "Remove", ""), + ('ADD', "Add", ""))) + + def invoke(self, context, event): + scn = context.scene + idx = scn.custom_index + + try: + item = scn.custom[idx] + except IndexError: + pass + else: + if self.action == 'DOWN' and idx < len(scn.custom) - 1: + item_next = scn.custom[idx + 1].name + scn.custom.move(idx, idx + 1) + scn.custom_index += 1 + info = 'Item "%s" moved to position %d' % (item.name, scn.custom_index + 1) + self.report({'INFO'}, info) + + elif self.action == 'UP' and idx >= 1: + item_prev = scn.custom[idx - 1].name + scn.custom.move(idx, idx - 1) + scn.custom_index -= 1 + info = 'Item "%s" moved to position %d' % (item.name, scn.custom_index + 1) + self.report({'INFO'}, info) + + elif self.action == 'REMOVE': + info = 'Item "%s" removed from list' % (scn.custom[idx].name) + scn.custom_index -= 1 + scn.custom.remove(idx) + self.report({'INFO'}, info) + + if self.action == 'ADD': + if context.object: + item = scn.custom.add() + item.name = context.object.name + item.obj = context.object + scn.custom_index = len(scn.custom) - 1 + info = '"%s" added to list' % (item.name) + self.report({'INFO'}, info) + else: + self.report({'INFO'}, "Nothing selected in the Viewport") + return {"FINISHED"} + + +class CUSTOM_OT_addViewportSelection(Operator): + """Add all items currently selected in the viewport""" + bl_idname = "custom.add_viewport_selection" + bl_label = "Add Viewport Selection to List" + bl_description = "Add all items currently selected in the viewport" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + scn = context.scene + selected_objs = context.selected_objects + if selected_objs: + new_objs = [] + for i in selected_objs: + item = scn.custom.add() + item.name = i.name + item.obj = i + new_objs.append(item.name) + info = ', '.join(map(str, new_objs)) + self.report({'INFO'}, 'Added: "%s"' % (info)) + else: + self.report({'INFO'}, "Nothing selected in the Viewport") + return {'FINISHED'} + + +class CUSTOM_OT_printItems(Operator): + """Print all items and their properties to the console""" + bl_idname = "custom.print_items" + bl_label = "Print Items to Console" + bl_description = "Print all items and their properties to the console" + bl_options = {'REGISTER', 'UNDO'} + + reverse_order: BoolProperty( + default=False, + name="Reverse Order") + + @classmethod + def poll(cls, context): + return bool(context.scene.custom) + + def execute(self, context): + scn = context.scene + if self.reverse_order: + for i in range(scn.custom_index, -1, -1): + ob = scn.custom[i].obj + print("Object:", ob, "-", ob.name, ob.type) + else: + for item in scn.custom: + ob = item.obj + print("Object:", ob, "-", ob.name, ob.type) + return {'FINISHED'} + + +class CUSTOM_OT_clearList(Operator): + """Clear all items of the list""" + bl_idname = "custom.clear_list" + bl_label = "Clear List" + bl_description = "Clear all items of the list" + bl_options = {'INTERNAL'} + + @classmethod + def poll(cls, context): + return bool(context.scene.custom) + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + def execute(self, context): + if bool(context.scene.custom): + context.scene.custom.clear() + self.report({'INFO'}, "All items removed") + else: + self.report({'INFO'}, "Nothing to remove") + return {'FINISHED'} + + +class CUSTOM_OT_removeDuplicates(Operator): + """Remove all duplicates""" + bl_idname = "custom.remove_duplicates" + bl_label = "Remove Duplicates" + bl_description = "Remove all duplicates" + bl_options = {'INTERNAL'} + + def find_duplicates(self, context): + """find all duplicates by name""" + name_lookup = {} + for c, i in enumerate(context.scene.custom): + name_lookup.setdefault(i.obj.name, []).append(c) + duplicates = set() + for name, indices in name_lookup.items(): + for i in indices[1:]: + duplicates.add(i) + return sorted(list(duplicates)) + + @classmethod + def poll(cls, context): + return bool(context.scene.custom) + + def execute(self, context): + scn = context.scene + removed_items = [] + # Reverse the list before removing the items + for i in self.find_duplicates(context)[::-1]: + scn.custom.remove(i) + removed_items.append(i) + if removed_items: + scn.custom_index = len(scn.custom) - 1 + info = ', '.join(map(str, removed_items)) + self.report({'INFO'}, "Removed indices: %s" % (info)) + else: + self.report({'INFO'}, "No duplicates") + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + +class CUSTOM_OT_selectItems(Operator): + """Select Items in the Viewport""" + bl_idname = "custom.select_items" + bl_label = "Select Item(s) in Viewport" + bl_description = "Select Items in the Viewport" + bl_options = {'REGISTER', 'UNDO'} + + select_all = BoolProperty( + default=False, + name="Select all Items of List", + options={'SKIP_SAVE'}) + + @classmethod + def poll(cls, context): + return bool(context.scene.custom) + + def execute(self, context): + scn = context.scene + idx = scn.custom_index + + try: + item = scn.custom[idx] + except IndexError: + self.report({'INFO'}, "Nothing selected in the list") + return {'CANCELLED'} + + obj_error = False + bpy.ops.object.select_all(action='DESELECT') + if not self.select_all: + name = scn.custom[idx].obj.name + obj = scn.objects.get(name, None) + if not obj: + obj_error = True + else: + obj.select_set(True) + info = '"%s" selected in Vieport' % (obj.name) + else: + selected_items = [] + unique_objs = set([i.obj.name for i in scn.custom]) + for i in unique_objs: + obj = scn.objects.get(i, None) + if obj: + obj.select_set(True) + selected_items.append(obj.name) + + if not selected_items: + obj_error = True + else: + missing_items = unique_objs.difference(selected_items) + if not missing_items: + info = '"%s" selected in Viewport' \ + % (', '.join(map(str, selected_items))) + else: + info = 'Missing items: "%s"' \ + % (', '.join(map(str, missing_items))) + if obj_error: + info = "Nothing to select, object removed from scene" + self.report({'INFO'}, info) + return {'FINISHED'} + + +class CUSTOM_OT_deleteObject(Operator): + """Delete object from scene""" + bl_idname = "custom.delete_object" + bl_label = "Remove Object from Scene" + bl_description = "Remove object from scene" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return bool(context.scene.custom) + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + def execute(self, context): + scn = context.scene + selected_objs = context.selected_objects + idx = scn.custom_index + try: + item = scn.custom[idx] + except IndexError: + pass + else: + ob = scn.objects.get(item.obj.name) + if not ob: + self.report({'INFO'}, "No object of that name found in scene") + return {"CANCELLED"} + else: + bpy.ops.object.select_all(action='DESELECT') + ob.select_set(True) + bpy.ops.object.delete() + + info = ' Item "%s" removed from Scene' % (len(selected_objs)) + scn.custom_index -= 1 + scn.custom.remove(idx) + self.report({'INFO'}, info) + return {'FINISHED'} + + +# ------------------------------------------------------------------- +# Drawing +# ------------------------------------------------------------------- + +class CUSTOM_UL_items(UIList): + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + obj = item.obj + custom_icon = "OUTLINER_OB_%s" % obj.type + split = layout.split(factor=0.3) + split.label(text="Index: %d" % (index)) + split.prop(obj, "name", text="", emboss=False, translate=False, icon=custom_icon) + + def invoke(self, context, event): + pass + + +class CUSTOM_PT_objectList(Panel): + """Adds a custom panel to the TEXT_EDITOR""" + bl_label = "UI List Test" + bl_idname = "BMNFTS_PT_uilisttest" + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_category = 'Blend_My_NFTs' + + def draw(self, context): + layout = self.layout + scn = bpy.context.scene + + rows = 2 + row = layout.row() + row.template_list("CUSTOM_UL_items", "", scn, "custom", scn, "custom_index", rows=rows) + + col = row.column(align=True) + col.operator("custom.list_action", icon='ADD', text="").action = 'ADD' + col.operator("custom.list_action", icon='REMOVE', text="").action = 'REMOVE' + col.separator() + col.operator("custom.list_action", icon='TRIA_UP', text="").action = 'UP' + col.operator("custom.list_action", icon='TRIA_DOWN', text="").action = 'DOWN' + + row = layout.row() + col = row.column(align=True) + row = col.row(align=True) + row.operator("custom.print_items", icon="LINENUMBERS_ON") + row = col.row(align=True) + row.operator("custom.clear_list", icon="X") + row.operator("custom.remove_duplicates", icon="GHOST_ENABLED") + + row = layout.row() + col = row.column(align=True) + row = col.row(align=True) + row.operator("custom.add_viewport_selection", icon="HAND") # LINENUMBERS_OFF, ANIM + row = col.row(align=True) + row.operator("custom.select_items", icon="VIEW3D", text="Select Item in 3D View") + row.operator("custom.select_items", icon="GROUP", text="Select All Items in 3D View").select_all = True + row = layout.row() + row = col.row(align=True) + row.operator("custom.delete_object", icon="X") + + +# ------------------------------------------------------------------- +# Collection +# ------------------------------------------------------------------- + +class CUSTOM_PG_objectCollection(PropertyGroup): + # name: StringProperty() -> Instantiated by default + obj: PointerProperty( + name="Object", + type=bpy.types.Object) diff --git a/ui_Lists/__init__.py b/ui_Lists/__init__.py new file mode 100644 index 0000000..e69de29