From 487bfcb8ed5297093bb2bd0f003313f1088da093 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Thu, 29 Feb 2024 17:51:09 -0600 Subject: [PATCH 1/8] Added msg field to MCprepError Sometimes an exception may not be so clear cut, so let's allow an optional message to return --- MCprep_addon/conf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MCprep_addon/conf.py b/MCprep_addon/conf.py index 57ffa66d..766923ac 100644 --- a/MCprep_addon/conf.py +++ b/MCprep_addon/conf.py @@ -303,10 +303,16 @@ class MCprepError(object): Path of file the exception object was created in. The preferred way to get this is __file__ + + msg: Optional[str] + Optional message to display for an + exception. Use this if the exception + type may not be so clear cut """ err_type: BaseException line: int file: str + msg: Optional[str] = None env = MCprepEnv() From 936cb84532943cae5e57baef9e2f6ce5bb73766b Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Thu, 29 Feb 2024 17:51:54 -0600 Subject: [PATCH 2/8] refactor: have convert_mtl use MCprepError --- MCprep_addon/world_tools.py | 39 +++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index fd598020..c89e146d 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -26,7 +26,7 @@ from bpy.types import Context, Camera from bpy_extras.io_utils import ExportHelper, ImportHelper -from .conf import env, VectorType +from .conf import MCprepError, env, VectorType from . import util from . import tracking from .materials import generate @@ -157,7 +157,7 @@ def detect_world_exporter(filepath: Path) -> None: obj_header.set_seperated() -def convert_mtl(filepath): +def convert_mtl(filepath) -> Optional[MCprepError]: """Convert the MTL file if we're not using one of Blender's built in colorspaces @@ -170,7 +170,8 @@ def convert_mtl(filepath): - Add a header at the end Returns: - True if success or skipped, False if failed, or None if skipped + - None if successful or skipped + - MCprepError if failed (may return with message) """ # Check if the MTL exists. If not, then check if it # uses underscores. If still not, then return False @@ -180,7 +181,8 @@ def convert_mtl(filepath): if mtl_underscores.exists(): mtl = mtl_underscores else: - return False + line, file = env.current_line_and_file() + return MCprepError(FileNotFoundError(), line, file) lines = None copied_file = None @@ -190,8 +192,9 @@ def convert_mtl(filepath): lines = mtl_file.readlines() except Exception as e: print(e) - return False - + line, file = env.current_line_and_file() + return MCprepError(e, line, file, "Could not read file!") + # This checks to see if the user is using a built-in colorspace or if none of the lines have map_d. If so # then ignore this file and return None if bpy.context.scene.view_settings.view_transform in BUILTIN_SPACES or not any("map_d" in s for s in lines): @@ -215,10 +218,11 @@ def convert_mtl(filepath): print("Header " + str(header)) copied_file = shutil.copy2(mtl, original_mtl_path.absolute()) else: - return True + return None except Exception as e: print(e) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file) # In this section, we go over each line # and check to see if it begins with map_d. If @@ -231,7 +235,8 @@ def convert_mtl(filepath): lines[index] = "# " + line except Exception as e: print(e) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file, "Could not read file!") # This needs to be seperate since it involves writing try: @@ -243,9 +248,10 @@ def convert_mtl(filepath): except Exception as e: print(e) shutil.copy2(copied_file, mtl) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file) - return True + return None def enble_obj_importer() -> Optional[bool]: @@ -503,10 +509,13 @@ def execute(self, context): # First let's convert the MTL if needed conv_res = convert_mtl(self.filepath) try: - if conv_res is None: - pass # skipped, no issue anyways. - elif conv_res is False: - self.report({"WARNING"}, "MTL conversion failed!") + if isinstance(conv_res, MCprepError): + if isinstance(conv_res.err_type, FileNotFoundError): + self.report({"WARNING"}, "MTL not found!") + elif conv_res.msg is not None: + self.report({"WARNING"}, conv_res.msg) + else: + self.report({"WARNING"}, conv_res.err_type) res = None if util.min_bv((3, 5)): From f29d30837f2cddc26768e2ad1823cc715ca04051 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Thu, 29 Feb 2024 20:46:27 -0600 Subject: [PATCH 3/8] refactor: General refactoring of world.py In this commit, we do the following: - Convert more functions to use MCprepError - Remove Blender Internal and 2.7X specific code --- MCprep_addon/world_tools.py | 132 ++++++++++-------------------------- 1 file changed, 35 insertions(+), 97 deletions(-) diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index c89e146d..d0abc360 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -16,10 +16,11 @@ # # ##### END GPL LICENSE BLOCK ##### +import enum import os import math from pathlib import Path -from typing import List, Optional +from typing import List, Optional, Union import shutil import bpy @@ -253,16 +254,29 @@ def convert_mtl(filepath) -> Optional[MCprepError]: return None +class OBJImportCode(enum.Enum): + """ + This represents the state of the + OBJ import addon in pre-4.0 versions + of Blender + """ + ALREADY_ENABLED = 0 + DISABLED = 1 -def enble_obj_importer() -> Optional[bool]: - """Checks if obj import is avail and tries to activate if not. +def enable_obj_importer() -> Union[OBJImportCode, MCprepError]: + """ + Checks if the obj import addon (pre-Blender 4.0) is enabled, + and enable it if it isn't enabled. - If we fail to enable obj importing, return false. True if enabled, and Non - if nothing changed. + Returns: + - OBJImportCode.ALREADY_ENABLED if either enabled already or + the user is using Blender 4.0. + - OBJImportCode.DISABLED if the addon had to be enabled. + - MCprepError with a message if the addon could not be enabled. """ enable_addon = None if util.min_bv((4, 0)): - return None # No longer an addon, native built in. + return OBJImportCode.ALREADY_ENABLED # No longer an addon, native built in. else: in_import_scn = "obj_import" not in dir(bpy.ops.wm) in_wm = "" @@ -270,13 +284,14 @@ def enble_obj_importer() -> Optional[bool]: enable_addon = "io_scene_obj" if enable_addon is None: - return None + return OBJImportCode.ALREADY_ENABLED try: bpy.ops.preferences.addon_enable(module=enable_addon) - return True + return OBJImportCode.DISABLED except RuntimeError: - return False + line, file = env.current_line_and_file() + return MCprepError(Exception(), line, file, "Could not enable the Built-in OBJ importer!") # ----------------------------------------------------------------------------- @@ -480,15 +495,15 @@ def execute(self, context): self.report({"ERROR"}, "You must select a .obj file to import") return {'CANCELLED'} - res = enble_obj_importer() - if res is None: + res = enable_obj_importer() + if res is OBJImportCode.ALREADY_ENABLED: pass - elif res is True: + elif res is OBJImportCode.DISABLED: self.report( {"INFO"}, "FYI: had to enable OBJ imports in user preferences") - elif res is False: - self.report({"ERROR"}, "Built-in OBJ importer could not be enabled") + elif isinstance(res, MCprepError): + self.report({"ERROR"}, res.msg) return {'CANCELLED'} # There are a number of bug reports that come from the generic call @@ -671,10 +686,8 @@ def execute(self, context): self.prep_world_cycles(context) elif engine == 'BLENDER_EEVEE': self.prep_world_eevee(context) - elif engine == 'BLENDER_RENDER' or engine == 'BLENDER_GAME': - self.prep_world_internal(context) else: - self.report({'ERROR'}, "Must be cycles, eevee, or blender internal") + self.report({'ERROR'}, "Must be Cycles or EEVEE") return {'FINISHED'} def prep_world_cycles(self, context: Context) -> None: @@ -765,41 +778,7 @@ def prep_world_eevee(self, context: Context) -> None: # Renders faster at a (minor?) cost of the image output # TODO: given the output change, consider make a bool toggle for this - bpy.context.scene.render.use_simplify = True - - def prep_world_internal(self, context): - # check for any suns with the sky setting on; - if not context.scene.world: - return - context.scene.world.use_nodes = False - context.scene.world.horizon_color = (0.00938029, 0.0125943, 0.0140572) - context.scene.world.light_settings.use_ambient_occlusion = True - context.scene.world.light_settings.ao_blend_type = 'MULTIPLY' - context.scene.world.light_settings.ao_factor = 0.1 - context.scene.world.light_settings.use_environment_light = True - context.scene.world.light_settings.environment_energy = 0.05 - context.scene.render.use_shadows = True - context.scene.render.use_raytrace = True - context.scene.render.use_textures = True - - # check for any sunlamps with sky setting - sky_used = False - for lamp in context.scene.objects: - if lamp.type not in ("LAMP", "LIGHT") or lamp.data.type != "SUN": - continue - if lamp.data.sky.use_sky: - sky_used = True - break - if sky_used: - env.log("MCprep sky being used with atmosphere") - context.scene.world.use_sky_blend = False - context.scene.world.horizon_color = (0.00938029, 0.0125943, 0.0140572) - else: - env.log("No MCprep sky with atmosphere") - context.scene.world.use_sky_blend = True - context.scene.world.horizon_color = (0.647705, 0.859927, 0.940392) - context.scene.world.zenith_color = (0.0954261, 0.546859, 1) - + bpy.context.scene.render.use_simplify = True class MCPREP_OT_add_mc_sky(bpy.types.Operator): """Add sun lamp and time of day (dynamic) driver, setup sky with sun and moon""" @@ -811,7 +790,7 @@ def enum_options(self, context: Context) -> List[tuple]: """Dynamic set of enums to show based on engine""" engine = bpy.context.scene.render.engine enums = [] - if bpy.app.version >= (2, 77) and engine in ("CYCLES", "BLENDER_EEVEE"): + if engine in ("CYCLES", "BLENDER_EEVEE"): enums.append(( "world_shader", "Dynamic sky + shader sun/moon", @@ -905,17 +884,7 @@ def execute(self, context): if self.world_type in ("world_static_mesh", "world_static_only"): # Create world dynamically (previous, simpler implementation) new_sun = self.create_sunlamp(context) - new_objs.append(new_sun) - - if engine in ('BLENDER_RENDER', 'BLENDER_GAME'): - world = context.scene.world - if not world: - world = bpy.data.worlds.new("MCprep World") - context.scene.world = world - new_sun.data.shadow_method = 'RAY_SHADOW' - new_sun.data.shadow_soft_size = 0.5 - world.use_sky_blend = False - world.horizon_color = (0.00938029, 0.0125943, 0.0140572) + new_objs.append(new_sun) bpy.ops.mcprep.world(skipUsage=True) # do rest of sky setup elif engine == 'CYCLES' or engine == 'BLENDER_EEVEE': @@ -929,35 +898,7 @@ def execute(self, context): if wname in bpy.data.worlds: prev_world = bpy.data.worlds[wname] prev_world.name = "-old" - new_objs += self.create_dynamic_world(context, blendfile, wname) - - elif engine == 'BLENDER_RENDER' or engine == 'BLENDER_GAME': - # dynamic world using built-in sun sky and atmosphere - new_sun = self.create_sunlamp(context) - new_objs.append(new_sun) - new_sun.data.shadow_method = 'RAY_SHADOW' - new_sun.data.shadow_soft_size = 0.5 - - world = context.scene.world - if not world: - world = bpy.data.worlds.new("MCprep World") - context.scene.world = world - world.use_sky_blend = False - world.horizon_color = (0.00938029, 0.0125943, 0.0140572) - - # be sure to turn off all other sun lamps with atmosphere set - new_sun.data.sky.use_sky = True # use sun orientation settings if BI - for lamp in context.scene.objects: - if lamp.type not in ("LAMP", "LIGHT") or lamp.data.type != "SUN": - continue - if lamp == new_sun: - continue - lamp.data.sky.use_sky = False - - time_obj = get_time_object() - if not time_obj: - env.log( - "TODO: implement create time_obj, parent sun to it & driver setup") + new_objs += self.create_dynamic_world(context, blendfile, wname) if self.world_type in ("world_static_mesh", "world_mesh"): if not os.path.isfile(blendfile): @@ -1026,10 +967,7 @@ def execute(self, context): def create_sunlamp(self, context: Context) -> bpy.types.Object: """Create new sun lamp from primitives""" - if hasattr(bpy.data, "lamps"): # 2.7 - newlamp = bpy.data.lamps.new("Sun", "SUN") - else: # 2.8 - newlamp = bpy.data.lights.new("Sun", "SUN") + newlamp = bpy.data.lights.new("Sun", "SUN") obj = bpy.data.objects.new("Sunlamp", newlamp) obj.location = (0, 0, 20) obj.rotation_euler[0] = 0.481711 From d57a48e20afd897d3132da6c71350a8f04af8a53 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Thu, 29 Feb 2024 21:21:24 -0600 Subject: [PATCH 4/8] refactor: util.py refactoring for new error type This refactors the following functions to use the new error type: - bAppendLink - open_program In addition, the function bAppendLink has had all 2.7X related code removed, although an argument related to 2.7X layers is kept to avoid widescale breakage --- MCprep_addon/spawner/spawn_util.py | 7 +-- MCprep_addon/util.py | 71 ++++++++++++++++++------------ MCprep_addon/world_tools.py | 33 +++++++------- 3 files changed, 66 insertions(+), 45 deletions(-) diff --git a/MCprep_addon/spawner/spawn_util.py b/MCprep_addon/spawner/spawn_util.py index a8031347..ae1ce8d0 100644 --- a/MCprep_addon/spawner/spawn_util.py +++ b/MCprep_addon/spawner/spawn_util.py @@ -24,7 +24,7 @@ import bpy from bpy.types import Context, Collection, BlendDataLibraries -from ..conf import env +from ..conf import MCprepError, env from .. import util from .. import tracking from . import mobs @@ -372,6 +372,7 @@ def load_linked(self, context: Context, path: str, name: str) -> None: path = bpy.path.abspath(path) act = None + res = None if hasattr(bpy.data, "groups"): res = util.bAppendLink(f"{path}/Group", name, True) act = context.object # assumption of object after linking, 2.7 only @@ -382,10 +383,10 @@ def load_linked(self, context: Context, path: str, name: str) -> None: else: print("Error: Should have had at least one object selected.") - if res is False: + if isinstance(res, MCprepError): # Most likely scenario, path was wrong and raised "not a library". # end and automatically reload assets. - self.report({'WARNING'}, "Failed to load asset file") + self.report({'WARNING'}, res.msg) bpy.ops.mcprep.prompt_reset_spawners('INVOKE_DEFAULT') return diff --git a/MCprep_addon/util.py b/MCprep_addon/util.py index 85921ce5..0d74f132 100644 --- a/MCprep_addon/util.py +++ b/MCprep_addon/util.py @@ -134,19 +134,27 @@ def materialsFromObj(obj_list: List[bpy.types.Object]) -> List[Material]: return mat_list -def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True) -> bool: - """For multiple version compatibility, this function generalized - appending/linking blender post 2.71 changed to new append/link methods +def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True) -> Optional[MCprepError]: + """ + This function calls the append and link methods in an + easy and safe manner. Note that for 2.8 compatibility, the directory passed in should already be correctly identified (eg Group or Collection) Arguments: - directory: xyz.blend/Type, where Type is: Collection, Group, Material... - name: asset name + directory: str + xyz.blend/Type, where Type is: Collection, Group, Material... + name: str + Asset name toLink: bool + If true, link instead of append + active_layer: bool=True + Deprecated in MCprep 3.6 as it relates to pre-2.8 layers - Returns: true if successful, false if not. + Returns: + - None if successful + - MCprepError with message if the asset could not be appended or linked """ env.log(f"Appending {directory} : {name}", vv_only=True) @@ -155,17 +163,7 @@ def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True if directory[-1] != "/" and directory[-1] != os.path.sep: directory += os.path.sep - if "link_append" in dir(bpy.ops.wm): - # OLD method of importing, e.g. in blender 2.70 - env.log("Using old method of append/link, 2.72 <=", vv_only=True) - try: - bpy.ops.wm.link_append(directory=directory, filename=name, link=toLink) - return True - except RuntimeError as e: - print("bAppendLink", e) - return False - elif "link" in dir(bpy.ops.wm) and "append" in dir(bpy.ops.wm): - env.log("Using post-2.72 method of append/link", vv_only=True) + if "link" in dir(bpy.ops.wm) and "append" in dir(bpy.ops.wm): if toLink: bpy.ops.wm.link(directory=directory, filename=name) else: @@ -173,10 +171,11 @@ def bAppendLink(directory: str, name: str, toLink: bool, active_layer: bool=True bpy.ops.wm.append( directory=directory, filename=name) - return True + return None except RuntimeError as e: print("bAppendLink", e) - return False + line, file = env.current_line_and_file() + return MCprepError(e, line, file, f"Could not append {name}!") def obj_copy( @@ -228,7 +227,11 @@ def min_bv(version: Tuple, *, inclusive: bool = True) -> bool: def bv28() -> bool: - """Check if blender 2.8, for layouts, UI, and properties. """ + """ + Check if blender 2.8, for layouts, UI, and properties. + + Deprecated in MCprep 3.5, but kept to avoid breakage for now... + """ env.deprecation_warning() return min_bv((2, 80)) @@ -339,19 +342,33 @@ def link_selected_objects_to_scene() -> None: if ob not in list(bpy.context.scene.objects): obj_link_scene(ob) +def open_program(executable: str) -> Optional[MCprepError]: + """ + Runs an executable such as Mineways or jmc2OBJ, taking into account the + user's operating system (using Wine if Mineways is to be launched on + a non-Windows OS such as macOS or Linux) and automatically checks if + the program exists or has the right permissions to be executed. -def open_program(executable: str) -> Union[int, str]: + Returns: + - None if the program is found and ran successfully + - MCprepError in all error cases (may have error message) + """ # Open an external program from filepath/executbale executable = bpy.path.abspath(executable) env.log(f"Open program request: {executable}") + # Doesn't matter where the exact error occurs + # in this function, since they're all going to + # be crazy hard to decipher + line, file = env.current_line_and_file() + # input could be .app file, which appears as if a folder if not os.path.isfile(executable): env.log("File not executable") if not os.path.isdir(executable): - return -1 + return MCprepError(FileNotFoundError(), line, file) elif not executable.lower().endswith(".app"): - return -1 + return MCprepError(FileNotFoundError(), line, file) # try to open with wine, if available osx_or_linux = platform.system() == "Darwin" @@ -373,13 +390,13 @@ def open_program(executable: str) -> Union[int, str]: # for line in iter(p.stdout.readline, ''): # # will print lines as they come, instead of just at end # print(stdout) - return 0 + return None try: # attempt to use blender's built-in method res = bpy.ops.wm.path_open(filepath=executable) if res == {"FINISHED"}: env.log("Opened using built in path opener") - return 0 + return None else: env.log("Did not get finished response: ", str(res)) except: @@ -393,8 +410,8 @@ def open_program(executable: str) -> Union[int, str]: p = Popen(['open', executable], stdin=PIPE, stdout=PIPE, stderr=PIPE) stdout, err = p.communicate(b"") if err != b"": - return f"Error occured while trying to open executable: {err}" - return "Failed to open executable" + return MCprepError(RuntimeError(), line, file, f"Error occured while trying to open executable: {err!r}") + return MCprepError(RuntimeError(), line, file, "Failed to open executable") def open_folder_crossplatform(folder: str) -> bool: diff --git a/MCprep_addon/world_tools.py b/MCprep_addon/world_tools.py index d0abc360..cf125d74 100644 --- a/MCprep_addon/world_tools.py +++ b/MCprep_addon/world_tools.py @@ -316,13 +316,14 @@ class MCPREP_OT_open_jmc2obj(bpy.types.Operator): def execute(self, context): addon_prefs = util.get_user_preferences(context) res = util.open_program(addon_prefs.open_jmc2obj_path) - - if res == -1: - bpy.ops.mcprep.install_jmc2obj('INVOKE_DEFAULT') - return {'CANCELLED'} - elif res != 0: - self.report({'ERROR'}, str(res)) - return {'CANCELLED'} + + if isinstance(res, MCprepError): + if isinstance(res.err_type, FileNotFoundError): + bpy.ops.mcprep.install_jmc2obj('INVOKE_DEFAULT') + return {'CANCELLED'} + else: + self.report({'ERROR'}, res.msg) + return {'CANCELLED'} else: self.report({'INFO'}, "jmc2obj should open soon") return {'FINISHED'} @@ -396,14 +397,16 @@ def execute(self, context): if os.path.isfile(addon_prefs.open_mineways_path): res = util.open_program(addon_prefs.open_mineways_path) else: - res = -1 - - if res == -1: - bpy.ops.mcprep.install_mineways('INVOKE_DEFAULT') - return {'CANCELLED'} - elif res != 0: - self.report({'ERROR'}, str(res)) - return {'CANCELLED'} + # Doesn't matter here, it's a dummy value + res = MCprepError(FileNotFoundError(), -1, "") + + if isinstance(res, MCprepError): + if isinstance(res.err_type, FileNotFoundError): + bpy.ops.mcprep.install_mineways('INVOKE_DEFAULT') + return {'CANCELLED'} + else: + self.report({'ERROR'}, res.msg) + return {'CANCELLED'} else: self.report({'INFO'}, "Mineways should open soon") return {'FINISHED'} From 49e5bbb7029fb3d83ae3599b03e724de5ca5c91e Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Fri, 1 Mar 2024 17:42:18 -0600 Subject: [PATCH 5/8] Removed default_materials.py This file is not used at all, and is poorly written. If we need to revisit this idea, we can use Vivy as reference --- MCprep_addon/materials/default_materials.py | 219 -------------------- MCprep_addon/mcprep_ui.py | 1 - 2 files changed, 220 deletions(-) delete mode 100644 MCprep_addon/materials/default_materials.py diff --git a/MCprep_addon/materials/default_materials.py b/MCprep_addon/materials/default_materials.py deleted file mode 100644 index 7b2a6909..00000000 --- a/MCprep_addon/materials/default_materials.py +++ /dev/null @@ -1,219 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - - -import os -from typing import Union, Optional - -import bpy -from bpy.types import Context, Material - -from .. import tracking -from .. import util -from . import sync - -from ..conf import env, Engine - - -def default_material_in_sync_library(default_material: str, context: Context) -> bool: - """Returns true if the material is in the sync mat library blend file.""" - if env.material_sync_cache is None: - sync.reload_material_sync_library(context) - if util.nameGeneralize(default_material) in env.material_sync_cache: - return True - elif default_material in env.material_sync_cache: - return True - return False - - -def sync_default_material(context: Context, material: Material, default_material: str, engine: Engine) -> Optional[Union[Material, str]]: - """Normal sync material method but with duplication and name change.""" - if default_material in env.material_sync_cache: - import_name = default_material - elif util.nameGeneralize(default_material) in env.material_sync_cache: - import_name = util.nameGeneralize(default_material) - - # If link is true, check library material not already linked. - sync_file = sync.get_sync_blend(context) - - init_mats = list(bpy.data.materials) - path = os.path.join(sync_file, "Material") - util.bAppendLink(path, import_name, False) # No linking. - - imported = set(list(bpy.data.materials)) - set(init_mats) - if not imported: - return f"Could not import {material.name}" - new_default_material = list(imported)[0] - - # Checking if there's a node with the label Texture. - new_material_nodes = new_default_material.node_tree.nodes - if not new_material_nodes.get("MCPREP_diffuse"): - return "Material has no MCPREP_diffuse node" - - if not material.node_tree.nodes: - return "Material has no nodes" - - # Change the texture. - new_default_material_nodes = new_default_material.node_tree.nodes - material_nodes = material.node_tree.nodes - - if not material_nodes.get("Image Texture"): - return "Material has no Image Texture node" - - default_texture_node = new_default_material_nodes.get("MCPREP_diffuse") - image_texture = material_nodes.get("Image Texture").image.name - texture_file = bpy.data.images.get(image_texture) - default_texture_node.image = texture_file - - if engine == "CYCLES" or engine == "BLENDER_EEVEE": - default_texture_node.interpolation = 'Closest' - - material.user_remap(new_default_material) - - # remove the old material since we're changing the default and we don't - # want to overwhelm users - bpy.data.materials.remove(material) - new_default_material.name = material.name - return None - - -class MCPREP_OT_default_material(bpy.types.Operator): - bl_idname = "mcprep.sync_default_materials" - bl_label = "Sync Default Materials" - bl_options = {'REGISTER', 'UNDO'} - - use_pbr: bpy.props.BoolProperty( - name="Use PBR", - description="Use PBR or not", - default=False) - - engine: bpy.props.StringProperty( - name="engine To Use", - description="Defines the engine to use", - default="CYCLES") - - SIMPLE = "simple" - PBR = "pbr" - - track_function = "sync_default_materials" - track_param = None - @tracking.report_error - def execute(self, context): - # Sync file stuff. - sync_file = sync.get_sync_blend(context) - if not os.path.isfile(sync_file): - self.report({'ERROR'}, f"Sync file not found: {sync_file}") - return {'CANCELLED'} - - if sync_file == bpy.data.filepath: - return {'CANCELLED'} - - # Find the default material. - workflow = self.SIMPLE if not self.use_pbr else self.PBR - material_name = material_name = f"default_{workflow}_{self.engine.lower()}" - if not default_material_in_sync_library(material_name, context): - self.report({'ERROR'}, "No default material found") - return {'CANCELLED'} - - # Sync materials. - mat_list = list(bpy.data.materials) - for mat in mat_list: - try: - err = sync_default_material(context, mat, material_name, self.engine.upper()) # no linking - if err: - env.log(err) - except Exception as e: - print(e) - - return {'FINISHED'} - - -class MCPREP_OT_create_default_material(bpy.types.Operator): - bl_idname = "mcprep.create_default_material" - bl_label = "Create Default Material" - bl_options = {'REGISTER', 'UNDO'} - - SIMPLE = "simple" - PBR = "pbr" - - def execute(self, context): - engine = context.scene.render.engine - self.create_default_material(context, engine, "simple") - return {'FINISHED'} - - def create_default_material(self, context, engine, type): - """ - create_default_material: takes 3 arguments and returns nothing - context: Blender Context - engine: the render engine - type: the type of texture that's being dealt with - """ - if not len(bpy.context.selected_objects): - # If there's no selected objects. - self.report({'ERROR'}, "Select an object to create the material") - return - - material_name = f"default_{type}_{engine.lower()}" - default_material = bpy.data.materials.new(name=material_name) - default_material.use_nodes = True - nodes = default_material.node_tree.nodes - links = default_material.node_tree.links - nodes.clear() - - default_texture_node = nodes.new(type="ShaderNodeTexImage") - principled = nodes.new("ShaderNodeBsdfPrincipled") - nodeOut = nodes.new("ShaderNodeOutputMaterial") - - default_texture_node.name = "MCPREP_diffuse" - default_texture_node.label = "Diffuse Texture" - default_texture_node.location = (120, 0) - - principled.inputs["Specular"].default_value = 0 - principled.location = (600, 0) - - nodeOut.location = (820, 0) - - links.new(default_texture_node.outputs[0], principled.inputs[0]) - links.new(principled.outputs["BSDF"], nodeOut.inputs[0]) - - if engine == "EEVEE": - if hasattr(default_material, "blend_method"): - default_material.blend_method = 'HASHED' - if hasattr(default_material, "shadow_method"): - default_material.shadow_method = 'HASHED' - - -classes = ( - MCPREP_OT_default_material, - MCPREP_OT_create_default_material, -) - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - bpy.app.handlers.load_post.append(sync.clear_sync_cache) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) - try: - bpy.app.handlers.load_post.remove(sync.clear_sync_cache) - except: - pass diff --git a/MCprep_addon/mcprep_ui.py b/MCprep_addon/mcprep_ui.py index 7639768d..5a492f37 100644 --- a/MCprep_addon/mcprep_ui.py +++ b/MCprep_addon/mcprep_ui.py @@ -1063,7 +1063,6 @@ def draw(self, context): return row = layout.row() - # row.operator("mcprep.create_default_material") split = layout.split() col = split.column(align=True) From 4a8cc63aff1f628650afd4a1bf4e099aa4a83912 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Thu, 7 Mar 2024 18:21:48 -0600 Subject: [PATCH 6/8] refactor: Refactored find_from_texture_pack --- MCprep_addon/conf.py | 2 +- MCprep_addon/materials/generate.py | 68 ++++++++++++++++++------------ 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/MCprep_addon/conf.py b/MCprep_addon/conf.py index 766923ac..3d071984 100644 --- a/MCprep_addon/conf.py +++ b/MCprep_addon/conf.py @@ -133,7 +133,7 @@ def __init__(self): # list of material names, each is a string. None by default to indicate # that no reading has occurred. If lib not found, will update to []. # If ever changing the resource pack, should also reset to None. - self.material_sync_cache = [] + self.material_sync_cache: Optional[List] = [] # Whether we use PO files directly or use the converted form self.use_direct_i18n = False diff --git a/MCprep_addon/materials/generate.py b/MCprep_addon/materials/generate.py index c89274d6..8f5f5538 100644 --- a/MCprep_addon/materials/generate.py +++ b/MCprep_addon/materials/generate.py @@ -17,7 +17,7 @@ # ##### END GPL LICENSE BLOCK ##### import os -from typing import Dict, Optional, List, Any, Tuple, Union +from typing import Dict, Optional, List, Any, Tuple, Union, cast from pathlib import Path from dataclasses import dataclass from enum import Enum @@ -26,7 +26,7 @@ from bpy.types import Context, Material, Image, Texture, Nodes, NodeLinks, Node from .. import util -from ..conf import env, Form +from ..conf import MCprepError, env, Form AnimatedTex = Dict[str, int] @@ -125,43 +125,51 @@ def get_mc_canonical_name(name: str) -> Tuple[str, Optional[Form]]: return canon, form -def find_from_texturepack(blockname: str, resource_folder: Optional[Path]=None) -> Path: +def find_from_texturepack(blockname: str, resource_folder: Optional[Path]=None) -> Union[Path, MCprepError]: """Given a blockname (and resource folder), find image filepath. Finds textures following any pack which should have this structure, and the input folder or default resource folder could target at any of the following sublevels above the level. //pack_name/assets/minecraft/textures// + + Returns: + - Path if successful + - MCprepError if error occurs (may return with a message) """ - if not resource_folder: + if resource_folder is None: # default to internal pack - resource_folder = bpy.path.abspath(bpy.context.scene.mcprep_texturepack_path) + resource_folder = Path(cast( + str, + bpy.path.abspath(bpy.context.scene.mcprep_texturepack_path) + )) - if not os.path.isdir(resource_folder): + if not resource_folder.exists() or not resource_folder.is_dir(): env.log("Error, resource folder does not exist") - return + line, file = env.current_line_and_file() + return MCprepError(FileNotFoundError(), line, file, f"Resource pack folder at {resource_folder} does not exist!") # Check multiple paths, picking the first match (order is important), # goal of picking out the /textures folder. check_dirs = [ - os.path.join(resource_folder, "textures"), - os.path.join(resource_folder, "minecraft", "textures"), - os.path.join(resource_folder, "assets", "minecraft", "textures")] + Path(resource_folder, "textures"), + Path(resource_folder, "minecraft", "textures"), + Path(resource_folder, "assets", "minecraft", "textures")] for path in check_dirs: - if os.path.isdir(path): + if path.exists(): resource_folder = path break search_paths = [ resource_folder, # Both singular and plural shown below as it has varied historically. - os.path.join(resource_folder, "blocks"), - os.path.join(resource_folder, "block"), - os.path.join(resource_folder, "items"), - os.path.join(resource_folder, "item"), - os.path.join(resource_folder, "entity"), - os.path.join(resource_folder, "models"), - os.path.join(resource_folder, "model"), + Path(resource_folder, "blocks"), + Path(resource_folder, "block"), + Path(resource_folder, "items"), + Path(resource_folder, "item"), + Path(resource_folder, "entity"), + Path(resource_folder, "models"), + Path(resource_folder, "model"), ] res = None @@ -170,32 +178,36 @@ def find_from_texturepack(blockname: str, resource_folder: Optional[Path]=None) if "/" in blockname: newpath = blockname.replace("/", os.path.sep) for ext in extensions: - if os.path.isfile(os.path.join(resource_folder, newpath + ext)): - res = os.path.join(resource_folder, newpath + ext) + if Path(resource_folder, newpath + ext).exists(): + res = Path(resource_folder, newpath + ext) return res newpath = os.path.basename(blockname) # case where goes into other subpaths for ext in extensions: - if os.path.isfile(os.path.join(resource_folder, newpath + ext)): - res = os.path.join(resource_folder, newpath + ext) + if Path(resource_folder, newpath + ext).exists(): + res = Path(resource_folder, newpath + ext) return res # fallback (more common case), wide-search for for path in search_paths: - if not os.path.isdir(path): + if not path.is_dir(): continue for ext in extensions: - check_path = os.path.join(path, blockname + ext) - if os.path.isfile(check_path): - res = os.path.join(path, blockname + ext) + check_path = Path(path, blockname + ext) + if check_path.exists() and check_path.is_file(): + res = Path(path, blockname + ext) return res + # Mineways fallback for suffix in ["-Alpha", "-RGB", "-RGBA"]: if blockname.endswith(suffix): - res = os.path.join( + res = Path( resource_folder, "mineways_assets", f"mineways{suffix}.png") - if os.path.isfile(res): + if res.exists() and res.is_file(): return res + if res is None: + line, file = env.current_line_and_file() + return MCprepError(FileNotFoundError(), line, file) return res From e3b75a7a0661d0100eacdebca9d92c8501a8c08d Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sat, 6 Apr 2024 19:13:27 -0500 Subject: [PATCH 7/8] reft: updated uses of find_from_texturepack --- MCprep_addon/materials/generate.py | 8 ++++++-- MCprep_addon/materials/material_manager.py | 9 ++++++--- MCprep_addon/materials/sequences.py | 10 +++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/MCprep_addon/materials/generate.py b/MCprep_addon/materials/generate.py index 8f5f5538..e86f28cf 100644 --- a/MCprep_addon/materials/generate.py +++ b/MCprep_addon/materials/generate.py @@ -356,7 +356,9 @@ def set_texture_pack( """ mc_name, _ = get_mc_canonical_name(material.name) image = find_from_texturepack(mc_name, folder) - if image is None: + if isinstance(image, MCprepError): + if image.msg: + env.log(image.msg) return 0 image_data = util.loadTexture(image) @@ -649,7 +651,9 @@ def replace_missing_texture(image: Image) -> bool: canon, _ = get_mc_canonical_name(name) # TODO: detect for pass structure like normal and still look for right pass image_path = find_from_texturepack(canon) - if not image_path: + if isinstance(image_path, MCprepError): + if image_path.msg: + env.log(image_path.msg) return False image.filepath = image_path # image.reload() # not needed? diff --git a/MCprep_addon/materials/material_manager.py b/MCprep_addon/materials/material_manager.py index 6a47f794..23fddc0d 100644 --- a/MCprep_addon/materials/material_manager.py +++ b/MCprep_addon/materials/material_manager.py @@ -26,7 +26,7 @@ from .. import tracking from .. import util -from ..conf import env +from ..conf import MCprepError, env # ----------------------------------------------------------------------------- @@ -440,8 +440,11 @@ def load_from_texturepack(self, mat): env.log(f"Loading from texpack for {mat.name}", vv_only=True) canon, _ = generate.get_mc_canonical_name(mat.name) image_path = generate.find_from_texturepack(canon) - if not image_path or not os.path.isfile(image_path): - env.log(f"Find missing images: No source file found for {mat.name}") + if isinstance(image_path, MCprepError): + if image_path.msg: + env.log(image_path.msg) + else: + env.log(f"Find missing images: No source file found for {mat.name}") return False # even if images of same name already exist, load new block diff --git a/MCprep_addon/materials/sequences.py b/MCprep_addon/materials/sequences.py index ba112f57..8dada4b9 100644 --- a/MCprep_addon/materials/sequences.py +++ b/MCprep_addon/materials/sequences.py @@ -32,7 +32,7 @@ from . import uv_tools from .. import tracking from .. import util -from ..conf import env, Engine, Form +from ..conf import MCprepError, env, Engine, Form class ExportLocation(enum.Enum): @@ -72,8 +72,12 @@ def animate_single_material( # get the base image from the texturepack (cycles/BI general) image_path_canon = generate.find_from_texturepack(canon) - if not image_path_canon: - env.log(f"Canon path not found for {mat_gen}:{canon}, form {form}, path: {image_path_canon}", vv_only=True) + + if isinstance(image_path_canon, MCprepError): + if image_path_canon.msg: + env.log(image_path_canon.msg) + else: + env.log(f"Error occured during texturepack search for {mat_gen}:{canon}, form {form}") return affectable, False, None if not os.path.isfile(f"{image_path_canon}.mcmeta"): From 0b25c43b3ed593a9fc8cfb4f9fa2e44883559b44 Mon Sep 17 00:00:00 2001 From: StandingPad Animations Date: Sat, 6 Apr 2024 19:17:25 -0500 Subject: [PATCH 8/8] doc: updated docstring for detect_form --- MCprep_addon/materials/generate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/MCprep_addon/materials/generate.py b/MCprep_addon/materials/generate.py index e86f28cf..1a2d8127 100644 --- a/MCprep_addon/materials/generate.py +++ b/MCprep_addon/materials/generate.py @@ -216,6 +216,13 @@ def detect_form(materials: List[Material]) -> Optional[Form]: Useful for pre-determining elibibility of a function and also for tracking reporting to give sense of how common which exporter is used. + + materials: List[Material]: + List of materials to check from + + Returns: + - Form if detected + - None if not detected """ jmc2obj = 0 mc = 0