318 lines
8.5 KiB
Python
318 lines
8.5 KiB
Python
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|