import bpy import os import sys from mathutils import Vector from collections import namedtuple from ..pywowlib import WoWVersionManager from ..pywowlib.archives.wow_filesystem import WoWFileData from .. import PACKAGE_NAME from ..ui.preferences import get_project_preferences SequenceRecord = namedtuple('SequenceRecord', ['name', 'value', 'index']) class Sequence(type): def __new__(mcs, name, bases, dct): dct['__fields__'] = list(dct.keys())[2:] dct['_iter'] = 0 return super().__new__(mcs, name, bases, dct) def __getitem__(self, item): return getattr(self, self.__fields__[item]) def __iter__(self): return self def __next__(self): if self._iter == len(self.__fields__): self._iter = 0 raise StopIteration item = SequenceRecord(self.__fields__[self._iter], getattr(self, self.__fields__[self._iter]), self._iter) self._iter += 1 return item def index(self, item): return self.__fields__.index(item) def singleton(class_): instances = {} def getinstance(*args, **kwargs): if class_ not in instances: instances[class_] = class_(*args, **kwargs) return instances[class_] return getinstance def find_nearest_object(obj_, objects): """Get closest object to another object""" dist = sys.float_info.max result = None for obj in objects: obj_location_relative = obj.matrix_world.inverted() @ obj_.location hit = obj.closest_point_on_mesh(obj_location_relative) hit_dist = (obj_location_relative - hit[1]).length if hit_dist < dist: dist = hit_dist result = obj return result def parse_bitfield(bitfield, last_flag=0x1000): flags = set() bit = 1 while bit <= last_flag: if bitfield & bit: flags.add(str(bit)) bit <<= 1 return flags def construct_bitfield(flag_set): bitfield = 0 for flag in flag_set: bitfield |= int(flag) return bitfield def get_material_viewport_image(material): """ Get viewport image assigned to a material """ for i in range(3): try: img = material.texture_slots[3 - i].texture.image return img except: pass return None def load_game_data() -> WoWFileData: WoWVersionManager().set_client_version(int(bpy.context.scene.wow_scene.version)) if not hasattr(bpy, 'wow_game_data'): project_preferences = get_project_preferences() bpy.wow_game_data = WoWFileData(project_preferences.wow_path, project_preferences.project_dir_path) if not bpy.wow_game_data.files: raise UserWarning("WoW game data is not loaded. Check settings.") return bpy.wow_game_data def custom_relpath(path, start): if path.lower().startswith(start.lower()): return path[len(start):].lstrip('\\') else: return resolve_outside_texture_path(path) def resolve_texture_path(filepath: str) -> str: filepath = os.path.splitext(bpy.path.abspath(filepath))[0] + ".blp" prefs = get_project_preferences() # TODO: project folder rel_path = custom_relpath(filepath, prefs.cache_dir_path) test_path = os.path.join(prefs.cache_dir_path, rel_path) if os.path.exists(test_path) and os.path.isfile(test_path): return rel_path.replace('/', '\\') game_data = load_game_data() path = (filepath, "") rest_path = "" while True: path = os.path.split(path[0]) if not path[1]: print("\nTexture \"{}\" not found.".format(path)) break rest_path = os.path.join(path[1], rest_path) rest_path = rest_path[:-1] if rest_path.endswith('\\') else rest_path if os.name != 'nt': rest_path_n = rest_path.replace('/', '\\') else: rest_path_n = rest_path rest_path_n = rest_path_n[:-1] if rest_path_n.endswith('\\') else rest_path_n if game_data.has_file(rest_path_n)[0]: return rest_path_n def resolve_outside_texture_path(filepath: str) -> str: keywords = ["world\\", "dungeon\\", "creature\\", "interface\\", "item\\", "models\\", "spells\\", "textures\\", "tileset\\", "xtextures\\"] lowercase_filepath = filepath.lower() for keyword in keywords: if keyword in lowercase_filepath: index = lowercase_filepath.rfind(keyword) extracted_path = lowercase_filepath[index:] extracted_path = os.path.splitext(extracted_path)[0] + ".blp" normalized_path = os.path.normpath(extracted_path) return normalized_path lowercase_filepath = os.path.splitext(bpy.path.abspath(lowercase_filepath))[0] + ".blp" return lowercase_filepath def resolve_outside_model_path(filepath: str) -> str: keywords = ["world\\", "dungeon\\", "creature\\", "interface\\", "item\\", "models\\", "spells\\", "textures\\", "tileset\\", "xtextures\\"] lowercase_filepath = filepath.lower() for keyword in keywords: if keyword in lowercase_filepath: index = lowercase_filepath.rfind(keyword) extracted_path = lowercase_filepath[index:] extracted_path = os.path.splitext(extracted_path)[0] + ".m2" normalized_path = os.path.normpath(extracted_path) return normalized_path else: return None def get_origin_position(): loc = bpy.context.scene.cursor.location origin_loc = None for area in bpy.context.screen.areas: if area.type == 'VIEW_3D': ctx = bpy.context.copy() ctx['area'] = area ctx['region'] = area.regions[-1] bpy.ops.view3d.snap_cursor_to_selected(ctx) origin_loc = bpy.context.scene.cursor.location bpy.context.scene.cursor.location = loc return origin_loc def get_obj_boundbox_center(obj): return obj.matrix_world @ (0.125 * sum((Vector(b) for b in obj.bound_box), Vector())) def get_obj_radius(obj, bb_center): mesh = obj.data radius = 0.0 for vertex in mesh.vertices: dist = (vertex.co - bb_center).length if dist > radius: radius = dist return radius def get_obj_boundbox_world(obj): return tuple(obj.matrix_world @ Vector(obj.bound_box[0])), tuple(obj.matrix_world @ Vector(obj.bound_box[6])) def get_objs_boundbox_world(objects): corner1 = [32768, 32768, 32768] corner2 = [-32768, -32768, -32768] for obj in objects: obj_bb_corner1, obj_bb_corner2 = get_obj_boundbox_world(obj) for i, value in enumerate(obj_bb_corner1): if value < corner1[i]: corner1[i] = value for i, value in enumerate(obj_bb_corner2): if value > corner2[i]: corner2[i] = value return tuple(corner1), tuple(corner2) def simplify_numbers(num): num = float('{:.3g}'.format(num)) magnitude = 0 while abs(num) >= 1000: magnitude += 1 num /= 1000.0 return '{}{}'.format('{:f}'.format(num).rstrip('0').rstrip('.'), ['', 'K', 'M', 'B', 'T'][magnitude]) def wrap_text(width, text): lines = [] arr = text.split() length_sum = 0 str_sum = "" for var in arr: length_sum += len(var) + 1 if length_sum <= width: str_sum += " " + var else: lines.append(str_sum) length_sum = 0 str_sum = var if length_sum != 0: lines.append(str_sum) # lines.append(" " + arr[len(arr) - 1]) return lines def draw_spoiler(layout, data, toggle_prop_name, name="", data1=None, layout_enabled=None, icon=None, align=True): """ Draw a spoiler-like layout in Blender UI """ is_expanded = getattr(data, toggle_prop_name) body = layout.box() header = body.box() header_row = header.row(align=True) header_row.prop(data, toggle_prop_name, emboss=False, text='', icon='TRIA_DOWN' if is_expanded else 'TRIA_RIGHT') if data1 and layout_enabled: if icon: header_row.label(text='', icon=icon) header_row.prop(data1, layout_enabled, text=name) else: header_row.label(text=' ' + name if align else name, icon=icon) if is_expanded: content = body.column() if data1 and layout_enabled: content.enabled = getattr(data1, layout_enabled) return content def show_message_box(message = "", title = "Message Box", icon = 'INFO'): def draw(self, context): self.layout.label(text=message) bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)