diff --git a/__init__.py b/__init__.py index 0217ead..35f978a 100644 --- a/__init__.py +++ b/__init__.py @@ -36,13 +36,11 @@ sys.path.append(os.path.dirname(os.path.realpath(__file__))) # Local file imports: from main import \ - Checks, \ + Helpers, \ DNA_Generator, \ Exporter, \ - get_combinations, \ HeadlessUtil, \ Intermediate, \ - loading_animation, \ Logic, \ Material_Generator, \ Metadata, \ @@ -55,12 +53,10 @@ from UILists import \ if "bpy" in locals(): modules = { - "Checks": Checks, + "Helpers": Helpers, "DNA_Generator": DNA_Generator, "Exporter": Exporter, - "get_combinations": get_combinations, "HeadlessUtil": HeadlessUtil, - "loading_animation": loading_animation, "Intermediate": Intermediate, "Logic": Logic, "Material_Generator": Material_Generator, @@ -75,9 +71,9 @@ if "bpy" in locals(): if i in locals(): importlib.reload(modules[i]) -# ======== Persistant UI Refresh ======== # - +# ======== Persistent UI Refresh ======== # # Used for updating text and buttons in UI panels + combinations: int = 0 recommended_limit: int = 0 dt = datetime.now(timezone.utc).astimezone() # Date Time in UTC local @@ -92,7 +88,7 @@ def Refresh_UI(dummy1, dummy2): global combinations global recommended_limit - combinations = (get_combinations.get_combinations()) + combinations = (Helpers.get_combinations()) recommended_limit = int(round(combinations / 2)) # Add panel classes that require refresh to this refresh_panels tuple: @@ -165,6 +161,7 @@ class BMNFTData: sender_from: str email_password: str receiver_to: str + enable_debug: bool custom_Fields: dict = None fail_state: Any = False @@ -229,6 +226,7 @@ def getBMNFTData(): sender_from=bpy.context.scene.input_tool.sender_from, email_password=bpy.context.scene.input_tool.email_password, receiver_to=bpy.context.scene.input_tool.receiver_to, + enable_debug=bpy.context.scene.input_tool.enable_debug ) return data @@ -464,7 +462,6 @@ class BMNFTS_PGT_Input_Properties(bpy.types.PropertyGroup): enableAutoSave: bpy.props.BoolProperty(name="Auto Save Before Generation", description="Automatically saves your Blender file when 'Generate NFTs & Create Metadata' button is clicked") - # Auto Shutdown: enableAutoShutdown: bpy.props.BoolProperty(name="Auto Shutdown", description="Automatically shuts down your computer after a Batch is finished Generating") @@ -473,15 +470,16 @@ class BMNFTS_PGT_Input_Properties(bpy.types.PropertyGroup): hours: bpy.props.IntProperty(default=0, min=0) minutes: bpy.props.IntProperty(default=0, min=0) - # Send Batch Complete Email: emailNotificationBool: bpy.props.BoolProperty(name="Email Notifications", description="Receive Email Notifications from Blender once a batch is finished generating") sender_from: bpy.props.StringProperty(name="From", default="from@example.com") email_password: bpy.props.StringProperty(name="Password", subtype='PASSWORD') receiver_to: bpy.props.StringProperty(name="To", default="to@example.com") + enable_debug: bpy.props.BoolProperty(name="Enable Debug Mode", description="Allows you to run Blend_My_NFTs without generating any content files and includes more console information.") + # API Panel properties: - apiKey: bpy.props.StringProperty(name="API Key", subtype='PASSWORD') # Test code for future faetures + apiKey: bpy.props.StringProperty(name="API Key", subtype='PASSWORD') # Test code for future features # ======== Main Operators ======== # @@ -602,6 +600,7 @@ class resume_failed_batch(bpy.types.Operator): sender_from=render_settings["sender_from"], email_password=render_settings["email_password"], receiver_to=render_settings["receiver_to"], + enable_debug=render_settings["enable_debug"], fail_state=_fail_state, failed_batch=_failed_batch, @@ -1000,6 +999,9 @@ class BMNFTS_PT_Other(bpy.types.Panel): row = layout.row() layout.label(text=f"**Set a Save Path in Create NFT Data to Export Settings") + row = layout.row() + row.prop(input_tool_scene, "enable_debug") + row = layout.row() row = layout.row() diff --git a/main/Constants.py b/main/Constants.py deleted file mode 100644 index 3cc94c3..0000000 --- a/main/Constants.py +++ /dev/null @@ -1,51 +0,0 @@ -# Purpose: -# This file is for storing or updating constant values that may need to be changes depending on system requirements and -# different usecases. -import os -import json -import platform - -removeList = [".gitignore", ".DS_Store", "desktop.ini", ".ini"] - -def remove_file_by_extension(dirlist): - """ - Checks if a given directory list contains any of the files or file extensions listed above, if so, remove them from - list and return a clean dir list. These files interfer with BMNFTs operations and should be removed whenever dealing - with directories. - """ - - if str(type(dirlist)) == "": - dirlist = list(dirlist) # converts single string path to list if dir pasted as string - - return_dirs = [] - for directory in dirlist: - if not str(os.path.split(directory)[1]) in removeList: - return_dirs.append(directory) - - return return_dirs - - -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 save_result(result): - """ - Saves json result to json file at the specified path. - """ - file_name = "log.json" - if platform.system() == "Linux" or platform.system() == "Darwin": - path = os.path.join(os.path.join(os.path.expanduser('~')), 'Desktop', file_name) - - if platform.system() == "Windows": - path = os.path.join(os.environ["HOMEPATH"], "Desktop", file_name) - - data = json.dumps(result, indent=1, ensure_ascii=True) - with open(path, 'w') as outfile: - outfile.write(data + '\n') diff --git a/main/DNA_Generator.py b/main/DNA_Generator.py index d045e2f..1a76be5 100644 --- a/main/DNA_Generator.py +++ b/main/DNA_Generator.py @@ -3,15 +3,13 @@ import bpy import os -import re import copy import time import json import random from functools import partial -from .loading_animation import Loader -from . import Rarity, Logic, Checks, Material_Generator -from .Constants import bcolors, removeList, remove_file_by_extension +from . import Rarity, Logic, Material_Generator, Helpers +from .Helpers import bcolors, Loader def get_hierarchy(): @@ -124,10 +122,10 @@ def get_hierarchy(): return hierarchy -def generateNFT_DNA(collectionSize, enableRarity, enableLogic, logicFile, enableMaterials, materialsFile): +def generateNFT_DNA(collectionSize, enableRarity, enableLogic, logicFile, enableMaterials, materialsFile, enable_debug): + """ + Returns batchDataDictionary containing the number of NFT combinations, hierarchy, and the DNAList. """ - Returns batchDataDictionary containing the number of NFT combinations, hierarchy, and the DNAList. - """ hierarchy = get_hierarchy() @@ -164,7 +162,6 @@ def generateNFT_DNA(collectionSize, enableRarity, enableLogic, logicFile, enable """ singleDNA = "" - # Comments for debugging random, rarity, logic, and materials. if not enableRarity: singleDNA = createDNArandom(hierarchy) # print("============") @@ -214,7 +211,7 @@ def generateNFT_DNA(collectionSize, enableRarity, enableLogic, logicFile, enable # Messages: - Checks.raise_Warning_collectionSize(DNAList, collectionSize) + Helpers.raise_Warning_collectionSize(DNAList, collectionSize) # Data stored in batchDataDictionary: DataDictionary["numNFTsGenerated"] = len(DNAList) @@ -281,7 +278,7 @@ def makeBatches(collectionSize, nftsPerBatch, save_path, batch_json_save_path): def send_To_Record_JSON(collectionSize, nftsPerBatch, save_path, enableRarity, enableLogic, logicFile, enableMaterials, - materialsFile, Blend_My_NFTs_Output, batch_json_save_path): + materialsFile, Blend_My_NFTs_Output, batch_json_save_path, enable_debug): """ Creates NFTRecord.json file and sends "batchDataDictionary" to it. NFTRecord.json is a permanent record of all DNA you've generated with all attribute variants. If you add new variants or attributes to your .blend file, other scripts @@ -290,7 +287,7 @@ def send_To_Record_JSON(collectionSize, nftsPerBatch, save_path, enableRarity, e """ # Checking Scene is compatible with BMNFTs: - Checks.check_Scene() + Helpers.check_Scene() # Messages: print( @@ -313,18 +310,18 @@ def send_To_Record_JSON(collectionSize, nftsPerBatch, save_path, enableRarity, e def create_nft_data(): try: DataDictionary = generateNFT_DNA(collectionSize, enableRarity, enableLogic, logicFile, enableMaterials, - materialsFile) + materialsFile, enable_debug) NFTRecord_save_path = os.path.join(Blend_My_NFTs_Output, "NFTRecord.json") # Checks: - Checks.raise_Warning_maxNFTs(nftsPerBatch, collectionSize) - Checks.check_Duplicates(DataDictionary["DNAList"]) - Checks.raise_Error_ZeroCombinations() + Helpers.raise_Warning_maxNFTs(nftsPerBatch, collectionSize) + Helpers.check_Duplicates(DataDictionary["DNAList"]) + Helpers.raise_Error_ZeroCombinations() if enableRarity: - Checks.check_Rarity(DataDictionary["hierarchy"], DataDictionary["DNAList"], - os.path.join(save_path, "Blend_My_NFTs Output/NFT_Data")) + Helpers.check_Rarity(DataDictionary["hierarchy"], DataDictionary["DNAList"], + os.path.join(save_path, "Blend_My_NFTs Output/NFT_Data")) except FileNotFoundError: raise FileNotFoundError( diff --git a/main/Exporter.py b/main/Exporter.py index 76a67cb..6ed5fec 100644 --- a/main/Exporter.py +++ b/main/Exporter.py @@ -10,8 +10,8 @@ import json import smtplib import datetime import platform -from .loading_animation import Loader -from .Constants import bcolors, removeList, remove_file_by_extension + +from .Helpers import bcolors, Loader from .Metadata import createCardanoMetadata, createSolanaMetaData, createErc721MetaData diff --git a/main/Checks.py b/main/Helpers.py similarity index 60% rename from main/Checks.py rename to main/Helpers.py index ab50e7c..d50984d 100644 --- a/main/Checks.py +++ b/main/Helpers.py @@ -1,21 +1,122 @@ -# Purpose: -# The purpose of this file is to check the NFTRecord.json for duplicate NFT DNA and returns any found in the console. -# It also checks the percentage each variant is chosen in the NFTRecord, then compares it with its rarity percentage -# set in the .blend file. - -# This file is provided for transparency. The accuracy of the rarity values you set in your .blend file as outlined in -# the README.md file are dependent on the maxNFTs, and the maximum number of combinations of your NFT collection. - import bpy import os import json +import platform +from time import sleep +from itertools import cycle +from threading import Thread +from shutil import get_terminal_size from collections import Counter, defaultdict -from . import DNA_Generator, get_combinations -from .Constants import bcolors, removeList, remove_file_by_extension +from . import DNA_Generator -# Checks: +# ======== CONSTANTS ======== # + +# This section is used for debugging, coding, or general testing purposes. + + +def enable_debug(enable_debug_bool): + if enable_debug_bool: + import logging + + logging.basicConfig( + filename="./log.txt", + level=logging.DEBUG, + format='[%(levelname)s][%(asctime)s]\n%(message)s\n', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + +# ======== CONSTANTS ======== # + +# Constants are used for storing or updating constant values that may need to be changes depending on system +# requirements and different use-cases. + +removeList = [".gitignore", ".DS_Store", "desktop.ini", ".ini"] + + +def remove_file_by_extension(dirlist): + """ + Checks if a given directory list contains any of the files or file extensions listed above, if so, remove them from + list and return a clean dir list. These files interfer with BMNFTs operations and should be removed whenever dealing + with directories. + """ + + if str(type(dirlist)) == "": + dirlist = list(dirlist) # converts single string path to list if dir pasted as string + + return_dirs = [] + for directory in dirlist: + if not str(os.path.split(directory)[1]) in removeList: + return_dirs.append(directory) + + return return_dirs + + +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 save_result(result): + """ + Saves json result to json file at the specified path. + """ + file_name = "log.json" + if platform.system() == "Linux" or platform.system() == "Darwin": + path = os.path.join(os.path.join(os.path.expanduser('~')), 'Desktop', file_name) + + if platform.system() == "Windows": + path = os.path.join(os.environ["HOMEPATH"], "Desktop", file_name) + + data = json.dumps(result, indent=1, ensure_ascii=True) + with open(path, 'w') as outfile: + outfile.write(data + '\n') + + +# ======== GET COMBINATIONS ======== # + +# This section is used to get the number of combinations for checks and the UI display + +def get_combinations(): + """ + Returns "combinations", the number of all possible NFT DNA for a given Blender scene formatted to BMNFTs conventions + combinations. + """ + + hierarchy = DNA_Generator.get_hierarchy() + 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 + + return combinations + + +# ======== CHECKS ======== # + +# This section is used to check the NFTRecord.json for duplicate NFT DNA and returns any found in the console. +# It also checks the percentage each variant is chosen in the NFTRecord, then compares it with its rarity percentage +# set in the .blend file. + +# This section is provided for transparency. The accuracy of the rarity values you set in your .blend file as outlined +# in the README.md file are dependent on the maxNFTs, and the maximum number of combinations of your NFT collection. + def check_Scene(): # Not complete """ Checks if Blender file Scene follows the Blend_My_NFTs conventions. If not, raises error with all instances of @@ -44,6 +145,7 @@ def check_Scene(): # Not complete # attribute_naming_conventions + def check_Rarity(hierarchy, DNAListFormatted, save_path): """Checks rarity percentage of each Variant, then sends it to RarityData.json in NFT_Data folder.""" @@ -51,7 +153,6 @@ def check_Rarity(hierarchy, DNAListFormatted, save_path): for i in DNAListFormatted: DNAList.append(list(i.keys())[0]) - numNFTsGenerated = len(DNAList) numDict = defaultdict(list) @@ -90,7 +191,7 @@ def check_Rarity(hierarchy, DNAListFormatted, save_path): if l == k: name = fullNumName[i][k] num = numDict[j][l] - x[name] = [(str(round(((num/numNFTsGenerated)*100), 2)) + "%"), str(num)] + x[name] = [(str(round(((num / numNFTsGenerated) * 100), 2)) + "%"), str(num)] completeData[i] = x @@ -112,13 +213,13 @@ def check_Rarity(hierarchy, DNAListFormatted, save_path): path = os.path.join(save_path, "RarityData.json") print(bcolors.OK + f"Rarity Data has been saved to {path}." + bcolors.RESET) + def check_Duplicates(DNAListFormatted): """Checks if there are duplicates in DNAList before NFTRecord.json is sent to JSON file.""" DNAList = [] for i in DNAListFormatted: DNAList.append(list(i.keys())[0]) - duplicates = 0 seen = set() @@ -130,6 +231,7 @@ def check_Duplicates(DNAListFormatted): print(f"\nNFTRecord.json contains {duplicates} duplicate NFT DNA.") + def check_FailedBatches(batch_json_save_path): fail_state = False failed_batch = None @@ -151,6 +253,7 @@ def check_FailedBatches(batch_json_save_path): return fail_state, failed_batch, failed_dna, failed_dna_index + # Raise Errors: def raise_Error_numBatches(maxNFTs, nftsPerBatch): """Checks if number of Batches is less than maxNFTs, if not raises error.""" @@ -168,9 +271,10 @@ def raise_Error_numBatches(maxNFTs, nftsPerBatch): f"https://github.com/torrinworx/Blend_My_NFTs#blender-file-organization-and-structure\n{bcolors.RESET}" ) + def raise_Error_ZeroCombinations(): """Checks if combinations is greater than 0, if so, raises error.""" - if get_combinations.get_combinations() == 0: + if get_combinations() == 0: raise ValueError( f"\n{bcolors.ERROR}Blend_My_NFTs Error:\n" f"The number of all possible combinations is ZERO. Please review your Blender scene and ensure it follows " @@ -179,6 +283,7 @@ def raise_Error_ZeroCombinations(): f"https://github.com/torrinworx/Blend_My_NFTs#blender-file-organization-and-structure\n{bcolors.RESET}" ) + def raise_Error_numBatchesGreaterThan(numBatches): if numBatches < 1: raise ValueError( @@ -189,8 +294,8 @@ def raise_Error_numBatchesGreaterThan(numBatches): f"https://github.com/torrinworx/Blend_My_NFTs#blender-file-organization-and-structure\n{bcolors.RESET}" ) -# Raise Warnings: +# Raise Warnings: def raise_Warning_maxNFTs(nftsPerBatch, collectionSize): """ Prints warning if nftsPerBatch is greater than collectionSize. @@ -202,6 +307,7 @@ def raise_Warning_maxNFTs(nftsPerBatch, collectionSize): f"The number of NFTs Per Batch you set is smaller than the NFT Collection Size you set.\n{bcolors.RESET}" ) + def raise_Warning_collectionSize(DNAList, collectionSize): """ Prints warning if BMNFTs cannot generate requested number of NFTs from a given collectionSize. @@ -211,9 +317,67 @@ def raise_Warning_collectionSize(DNAList, collectionSize): print(f"\n{bcolors.WARNING} \nWARNING: \n" f"Blend_My_NFTs cannot generate {collectionSize} NFTs." f" Only {len(DNAList)} NFT DNA were generated." - + f"\nThis might be for a number of reasons:" f"\n a) Rarity is preventing combinations from being generated (See https://github.com/torrinworx/Blend_My_NFTs#notes-on-rarity-and-weighted-variants).\n" f"\n b) Logic is preventing combinations from being generated (See https://github.com/torrinworx/Blend_My_NFTs#logic).\n" f"\n c) The number of possible combinations of your NFT collection is too low. Add more Variants or Attributes to increase the recommended collection size.\n" f"\n{bcolors.RESET}") + + +# ======== LOADING ANIMATION ======== # + +# This section is used for the loading animation used in the system console. + +class Loader: + def __init__(self, desc="Loading...", end="Done!", timeout=0.1): + """ + A loader-like context manager + + Args: + desc (str, optional): The loader's description. Defaults to "Loading...". + end (str, optional): Final print. Defaults to "Done!". + timeout (float, optional): Sleep time between prints. Defaults to 0.1. + """ + self.desc = desc + self.end = end + self.timeout = timeout + + self._thread = Thread(target=self._animate, daemon=True) + self.steps = [ + " [== ]", + " [ == ]", + " [ == ]", + " [ == ]", + " [ == ]", + " [ ==]", + " [ == ]", + " [ == ]", + " [ == ]", + " [ == ]", + ] + self.done = False + + def start(self): + self._thread.start() + return self + + def _animate(self): + for c in cycle(self.steps): + if self.done: + break + print(f"\r{self.desc} {c}", flush=True, end="") + sleep(self.timeout) + + def __enter__(self): + self.start() + + def stop(self): + self.done = True + cols = get_terminal_size((80, 20)).columns + print("\r" + " " * cols, end="", flush=True) + print(f"\r{self.end}", flush=True) + + def __exit__(self, exc_type, exc_value, tb): + # handle exceptions with those variables ^ + self.stop() diff --git a/main/Intermediate.py b/main/Intermediate.py index a5479e1..6068a47 100644 --- a/main/Intermediate.py +++ b/main/Intermediate.py @@ -53,7 +53,8 @@ def send_To_Record_JSON(input, reverse_order=False): input.enableMaterials, input.materialsFile, input.Blend_My_NFTs_Output, - input.batch_json_save_path + input.batch_json_save_path, + input.enable_debug, ) diff --git a/main/Logic.py b/main/Logic.py index d425c70..006b5bc 100644 --- a/main/Logic.py +++ b/main/Logic.py @@ -5,7 +5,7 @@ import bpy import random import collections -from .Constants import bcolors, removeList, remove_file_by_extension, save_result +from .Helpers import bcolors, removeList, remove_file_by_extension, save_result def reconstructDNA(deconstructedDNA): diff --git a/main/Material_Generator.py b/main/Material_Generator.py index c5866ad..0218838 100644 --- a/main/Material_Generator.py +++ b/main/Material_Generator.py @@ -7,7 +7,7 @@ import bpy import json import random -from .Constants import bcolors, removeList, remove_file_by_extension, save_result +from .Helpers import bcolors, removeList, remove_file_by_extension, save_result def select_material(materialList, variant, enableRarity): diff --git a/main/Rarity.py b/main/Rarity.py index eff5c7c..b806f3f 100644 --- a/main/Rarity.py +++ b/main/Rarity.py @@ -4,7 +4,7 @@ import bpy import random -from .Constants import bcolors, removeList, remove_file_by_extension +from .Helpers import bcolors, removeList, remove_file_by_extension def createDNArarity(hierarchy): diff --git a/main/Refactorer.py b/main/Refactorer.py index 6566753..98eb970 100644 --- a/main/Refactorer.py +++ b/main/Refactorer.py @@ -6,7 +6,7 @@ import os import json import shutil -from .Constants import bcolors, removeList, remove_file_by_extension +from .Helpers import bcolors, removeList, remove_file_by_extension def reformatNFTCollection(refactor_panel_input): diff --git a/main/get_combinations.py b/main/get_combinations.py deleted file mode 100644 index f1db7eb..0000000 --- a/main/get_combinations.py +++ /dev/null @@ -1,26 +0,0 @@ -import bpy - -from . import DNA_Generator - - -def get_combinations(): - """ - Returns "combinations", the number of all possible NFT DNA for a given Blender scene formatted to BMNFTs conventions - combinations. - """ - - hierarchy = DNA_Generator.get_hierarchy() - 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 - - return combinations diff --git a/main/loading_animation.py b/main/loading_animation.py deleted file mode 100644 index d97e69d..0000000 --- a/main/loading_animation.py +++ /dev/null @@ -1,69 +0,0 @@ -from itertools import cycle -from shutil import get_terminal_size -from threading import Thread -from time import sleep - - -class Loader: - def __init__(self, desc="Loading...", end="Done!", timeout=0.1): - """ - A loader-like context manager - - Args: - desc (str, optional): The loader's description. Defaults to "Loading...". - end (str, optional): Final print. Defaults to "Done!". - timeout (float, optional): Sleep time between prints. Defaults to 0.1. - """ - self.desc = desc - self.end = end - self.timeout = timeout - - self._thread = Thread(target=self._animate, daemon=True) - self.steps = [ - " [== ]", - " [ == ]", - " [ == ]", - " [ == ]", - " [ == ]", - " [ ==]", - " [ == ]", - " [ == ]", - " [ == ]", - " [ == ]", - ] - self.done = False - - def start(self): - self._thread.start() - return self - - def _animate(self): - for c in cycle(self.steps): - if self.done: - break - print(f"\r{self.desc} {c}", flush=True, end="") - sleep(self.timeout) - - def __enter__(self): - self.start() - - def stop(self): - self.done = True - cols = get_terminal_size((80, 20)).columns - print("\r" + " " * cols, end="", flush=True) - print(f"\r{self.end}", flush=True) - - def __exit__(self, exc_type, exc_value, tb): - # handle exceptions with those variables ^ - self.stop() - - -if __name__ == "__main__": - with Loader("Loading with context manager..."): - for i in range(10): - sleep(0.25) - - loader = Loader("Loading with object...", "That was fast!", 0.05).start() - for i in range(10): - sleep(0.25) - loader.stop()