новая структура проекта

This commit is contained in:
2026-04-20 21:04:25 +04:00
parent 1fe2a72ef1
commit 1a56b22e38
1932 changed files with 1886 additions and 22779 deletions
@@ -0,0 +1,77 @@
import bpy
from ..wmo.ui.custom_objects import *
from ..wmo.ui.collections import get_wmo_groups_list
from ..utils.collections import get_current_wow_model_collection
class WBS_OT_doodadset_mode(bpy.types.Operator):
bl_idname = 'scene.doodadset_mode'
bl_label = 'Enters into Doodadset editing mode'
bl_description = "Enters into Doodadset editing mode, disabling some edit options."
bl_options = {'REGISTER'}
def execute(self, context):
bpy.context.scene.wow_scene.doodadset_mode = not bpy.context.scene.wow_scene.doodadset_mode
wmo_model_collection = get_current_wow_model_collection(context.scene, 'wow_wmo')
if wmo_model_collection:
if bpy.context.scene.wow_scene.doodadset_mode == True:
bpy.ops.wbs.viewport_text_display('INVOKE_DEFAULT', message="Doodadset mode enabled", font_size=24, y_offset=67)
for i, obj in enumerate(get_wmo_groups_list(context.scene)):
obj : bpy.types.Object
obj.lock_location = (True, True, True)
obj.lock_rotation = (True, True, True)
obj.lock_scale = (True, True, True)
obj.hide_select = True
for obj in bpy.context.scene.objects:
if obj.type == 'MESH':
if WoWWMOPortal.match(obj):
obj.hide_select = True
elif WoWWMOFog.match(obj):
obj.hide_select = True
elif WoWWMOLiquid.match(obj):
obj.hide_select = True
elif WoWWMOCollision.match(obj):
obj.hide_select = True
elif obj.type == 'LIGHT' and WoWWMOLight.match(obj):
obj.hide_select = True
else:
bpy.ops.wbs.viewport_text_display('INVOKE_DEFAULT', message="Doodadset mode disabled", font_size=24, y_offset=67)
for i, obj in enumerate(get_wmo_groups_list(context.scene)):
obj : bpy.types.Object
obj.lock_location = (False, False, False)
obj.lock_rotation = (False, False, False)
obj.lock_scale = (False, False, False)
obj.hide_select = False
for obj in bpy.context.scene.objects:
if obj.type == 'MESH':
if WoWWMOPortal.match(obj):
obj.hide_select = False
elif WoWWMOFog.match(obj):
obj.hide_select = False
elif WoWWMOLiquid.match(obj):
obj.hide_select = False
elif WoWWMOCollision.match(obj):
obj.hide_select = False
elif obj.type == 'LIGHT' and WoWWMOLight.match(obj):
obj.hide_select = False
return {'FINISHED'}
keymap = None
def register():
global keymap
wm = bpy.context.window_manager
km = wm.keyconfigs.addon.keymaps.new(name='3D View', space_type='VIEW_3D', region_type='WINDOW')
kmi = km.keymap_items.new("scene.wow_doodads_bake_color", type='B', value='PRESS', shift=True)
keymap = km, kmi
def unregister():
global keymap
km, kmi = keymap
km.keymap_items.remove(kmi)
@@ -0,0 +1,11 @@
from enum import Enum, auto
class WoWSceneTypes(Enum):
""" Represents the type of WoW scene. """
M2 = auto()
""" Props, doodads, characters, etc. """
WMO = auto()
""" World Map Objects (big buildings, dungeons, cities, etc. """
@@ -0,0 +1,21 @@
import bpy
from bpy.app.handlers import persistent
from ..utils.misc import load_game_data
@persistent
def _load_game_data(scene):
pass
#load_game_data()
def register():
bpy.app.handlers.load_post.append(_load_game_data)
def unregister():
bpy.app.handlers.load_post.remove(_load_game_data)
@@ -0,0 +1,44 @@
from ..utils.misc import singleton
class _BaseLock:
_update_lock: int
def __enter__(self):
self._update_lock += 1
def __exit__(self, exc_type, exc_val, exc_tb):
self._update_lock -= 1
def push(self):
""" Manually enter the locking scope. """
self._update_lock += 1
def pop(self):
""" Manually exit the locking scope. """
self._update_lock -= 1
@property
def status(self) -> bool:
"""
Test if depsgraph handlers are locked now.
:return: True if locked, else False.
"""
return bool(self._update_lock)
@singleton
class DepsgraphLock(_BaseLock):
"""
Locks all depsgraph handler operations. Ensures no automated UI takss are handled in enclosed code.
"""
_update_lock: int = 0
@singleton
class UIPropUpdateLock(_BaseLock):
"""
Locks all update() callback in bpy.props custom defined properties that support locking.
"""
_update_lock: int = 0
@@ -0,0 +1,40 @@
from ..utils.misc import singleton
from typing import List, Tuple
import bpy
@singleton
class MessageStack:
""" Used to display messages from handlers. """
_message: List[Tuple[str, str]] = []
def push_message(self, *, msg: str = '', icon: str = 'INFO'):
"""
Push message to the stack.
:param msg: Message text.
:param icon: Message icon.
"""
self._message.append((msg, icon))
@staticmethod
def _draw(self, context):
for msg, icon in MessageStack()._message:
self.layout.label(text=msg, icon=icon)
def invoke_message_box(self, title: str = 'WoW Blender Studio', icon: str = 'INFO'):
"""
Invoke popup message box window displaying current message stack. Also clears the stack.
:param title: Title of the message box.
:param icon: Icon of the message box.
"""
if not self._message:
return
bpy.context.window_manager.popup_menu(self._draw, title=title, icon=icon)
self._message.clear()
@@ -0,0 +1,820 @@
import bpy
import json
import os
from pathlib import Path
from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty
from bpy_extras.io_utils import ExportHelper
from ..wmo.import_wmo import import_wmo_to_blender_scene
from ..wmo.export_wmo import export_wmo_from_blender_scene
from ..m2.import_m2 import import_m2
from ..m2.export_m2 import export_m2, create_m2
from ..utils.misc import load_game_data
from ..utils.collections import get_current_wow_model_collection, SpecialCollection
from ..ui.preferences import get_project_preferences
#############################################################
###### Common operators ######
#############################################################
class WBS_OT_texture_transparency_toggle(bpy.types.Operator):
bl_idname = 'wow.toggle_image_alpha'
bl_label = 'Toggle texture transparency'
bl_description = 'Toggle texture transparency (useful for working in solid mode)'
bl_options = {'REGISTER'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
self.layout.label(text="This will overwrite alpha settings for images. Continue?")
def execute(self, context):
for image in bpy.data.images:
if image.library is not None:
continue
image.alpha_mode = 'NONE' if image.alpha_mode in ('PREMUL', 'CHANNEL_PACKED', 'STRAIGHT') else 'STRAIGHT'
return {'FINISHED'}
class WBS_OT_reload_game_data(bpy.types.Operator):
bl_idname = 'scene.reload_wow_filesystem'
bl_label = 'Reload WoW filesystem'
bl_description = 'Re-establish connection to World of Warcraft client files'
bl_options = {'REGISTER'}
def execute(self, context):
if hasattr(bpy, "wow_game_data"):
if bpy.wow_game_data.files:
for storage, type_ in bpy.wow_game_data.files:
if type_:
storage.close()
delattr(bpy, "wow_game_data")
load_game_data()
if not bpy.wow_game_data.files:
self.report({'ERROR'}, "WoW game data is not loaded. Check settings.")
return {'CANCELLED'}
self.report({'INFO'}, "WoW game data is reloaded.")
return {'FINISHED'}
class WBS_OT_save_current_wmo(bpy.types.Operator):
bl_idname = 'scene.save_current_wmo_collection'
bl_label = 'Save current WMO object'
bl_description = "Save the currently selected WMO collection to the Project Folder as a .wmo file using the collection's [Directory Path]."
bl_options = {'REGISTER'}
# will save the first wmo collection found for now until wmo scene is changed to an export class
def execute(self, context):
scene = context.scene
if scene and scene.wow_scene.type == 'WMO':
version = int(scene.wow_scene.version)
wmo_collection = get_current_wow_model_collection(scene, 'wow_wmo')
if not bpy.context.collection:
self.report({'ERROR'}, 'No Collection selected.')
return {'CANCELLED'}
# act_col: bpy.types.Collection = bpy.context.collection
# SpecialCollection._get_root_collection(context.scene)
if not wmo_collection:
self.report({'ERROR'}, 'Could not find a WoW WMO collection.')
return {'CANCELLED'}
if not wmo_collection.wow_wmo.dir_path:
self.report({'WARNING'}, 'WMO Collection ' + wmo_collection.name + ' has empty WoW directory path. \
\nWMO will be saved at the root of the project.')
project_preferences = get_project_preferences()
if not project_preferences.export_dir_path:
self.report({'ERROR'}, 'Export path in addon preferences is empty.')
return {'CANCELLED'}
# Doesn't work if wow_wmo.dir_path is a full path with Disk name etc, happens if WMO has been imported from local file.
dir_path = os.path.join(project_preferences.export_dir_path, wmo_collection.wow_wmo.dir_path)
# dir_path = project_preferences.project_dir_path # temporary so we don't override the old file
filename = Path(wmo_collection.name).stem + '.wmo'
filepath = os.path.join(dir_path, filename)
print("saving wmo to : " + filepath)
export_wmo_from_blender_scene(filepath, version, False, 'FULL')
return {'FINISHED'}
self.report({'ERROR'}, 'Invalid scene type.')
return {'CANCELLED'}
class WBS_OT_save_current_m2(bpy.types.Operator):
bl_idname = 'scene.save_current_m2'
bl_label = 'Save current M2 object'
bl_description = "Save the currently selected M2 to the Export Folder as a .m2 file using the Scene [Game Path]."
bl_options = {'REGISTER'}
def execute(self, context):
scene = context.scene
if scene and scene.wow_scene.type == 'M2':
version = int(scene.wow_scene.version)
project_preferences = get_project_preferences()
if not project_preferences.export_dir_path:
self.report({'ERROR'}, 'Export path in addon preferences is empty.')
return {'CANCELLED'}
scene_gamepath = scene.wow_scene.game_path.replace('/', '\\')
dir_path = os.path.join(project_preferences.export_dir_path, os.path.dirname(scene_gamepath))
os.makedirs(dir_path, exist_ok=True)
filename = os.path.basename(scene_gamepath)
filepath = os.path.join(dir_path, filename)
print("Saving M2 to : " + filepath)
export_m2(version, filepath, selected_only = False, fill_textures = True, forward_axis = 'X+', scale = 1.0, merge_vertices = True)
return {'FINISHED'}
self.report({'ERROR'}, 'Invalid scene type.')
return {'CANCELLED'}
#############################################################
###### Import/Export Operators ######
#############################################################
class WBS_OT_wmo_import(bpy.types.Operator):
"""Load WMO mesh data"""
bl_idname = "import_mesh.wmo"
bl_label = "Import WMO"
bl_options = {'UNDO', 'REGISTER'}
filepath: StringProperty(
subtype='FILE_PATH',
)
filter_glob: StringProperty(
default="*.wmo",
options={'HIDDEN'}
)
import_lights: BoolProperty(
name="Import lights",
description="Import WMO lights to scene",
default=True,
)
import_doodads: BoolProperty(
name="Import doodads",
description='Import WMO doodads to scene',
default=True
)
import_fogs: BoolProperty(
name="Import fogs",
description="Import WMO fogs to scene",
default=True,
)
group_objects: BoolProperty(
name="Group objects",
description="Group all objects of this WMO on import",
default=False,
)
def execute(self, context):
version = int(context.scene.wow_scene.version)
import_wmo_to_blender_scene(self.filepath, version)
context.scene.wow_scene.type = 'WMO'
return {'FINISHED'}
def invoke(self, context, event):
wm = context.window_manager
wm.fileselect_add(self)
return {'RUNNING_MODAL'}
class WBS_OT_wmo_export(bpy.types.Operator, ExportHelper):
"""Save WMO mesh data"""
bl_idname = "export_mesh.wmo"
bl_label = "Export WMO"
bl_options = {'PRESET', 'REGISTER'}
filename_ext = ".wmo"
filter_glob: StringProperty(
default="*.wmo",
options={'HIDDEN'}
)
export_method: EnumProperty(
name='Export Method',
description='Partial export if the scene was exported before and was not critically modified',
items=[('FULL', 'Full', 'Full'),
('PARTIAL', 'Partial', 'Partial')
]
)
export_selected: BoolProperty(
name="Export selected objects",
description="Export only selected objects on the scene",
default=False,
)
def draw(self, context):
layout = self.layout
layout.prop(self, 'export_method', expand=True)
if self.export_method == 'FULL':
layout.prop(self, 'export_selected')
def execute(self, context):
if context.scene and context.scene.wow_scene.type == 'WMO':
if self.export_method == 'PARTIAL' and context.scene.wow_wmo_root_elements.is_update_critical:
self.report({'ERROR'}, 'Partial export is not available. The changes are critical.')
return {'CANCELLED'}
version = int(context.scene.wow_scene.version)
export_wmo_from_blender_scene(self.filepath, version, self.export_selected, self.export_method)
return {'FINISHED'}
self.report({'ERROR'}, 'Invalid scene type.')
return {'CANCELLED'}
class WBS_OT_blp_load_from_game(bpy.types.Operator):
"""Load BLP from game data"""
bl_idname = "load.blp"
bl_label = "Load BLP from game data"
bl_options = {'UNDO','REGISTER'}
blp_file: StringProperty()
imported_name: StringProperty()
def execute(self, context):
game_data = load_game_data()
project_preferences = get_project_preferences()
if not project_preferences.cache_dir_path:
raise Exception('Error: cache directory is not specified. Check addon settings.')
if game_data and game_data.files:
files = game_data.extract_textures_as_png(project_preferences.cache_dir_path, [self.blp_file])
for key,value in files.items():
img = bpy.data.images.load(os.path.join(value))
if self.imported_name:
img.name = self.imported_name
else:
img.name = os.path.basename(key)
return {'FINISHED'}
else:
raise NotImplementedError('Error: Importing without gamedata loaded is not yet implemented.')
def draw(self, context):
layout = self.layout
layout.prop(self, "blp_file", text="BLP File")
@classmethod
def poll(cls, context):
return True
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
class WBS_OT_m2_import(bpy.types.Operator):
"""Load M2 data"""
bl_idname = "import_mesh.m2"
bl_label = "Import M2"
bl_options = {'UNDO', 'REGISTER'}
filepath: StringProperty(
subtype='FILE_PATH',
)
filter_glob: StringProperty(
default="*.m2",
options={'HIDDEN'}
)
def execute(self, context):
project_preferences = get_project_preferences()
time_import_method = project_preferences.time_import_method
if time_import_method == 'Convert':
bpy.context.scene.render.fps = 30
bpy.context.scene.sync_mode = 'NONE'
else:
bpy.context.scene.render.fps = 1000
bpy.context.scene.sync_mode = 'FRAME_DROP'
import_m2(int(context.scene.wow_scene.version), self.filepath, True, time_import_method)
context.scene.wow_scene.type = 'M2'
return {'FINISHED'}
def invoke(self, context, event):
wm = context.window_manager
wm.fileselect_add(self)
return {'RUNNING_MODAL'}
class WBS_OT_m2_export(bpy.types.Operator, ExportHelper):
"""Save M2 mesh data"""
bl_idname = "export_mesh.m2"
bl_label = "Export M2"
bl_options = {'PRESET', 'REGISTER'}
filename_ext = ".m2"
filter_glob: StringProperty(
default="*.m2",
options={'HIDDEN'}
)
export_selected: BoolProperty(
name="Export selected objects",
description="Export only selected objects on the scene",
default=False,
)
version: EnumProperty(
name="Version",
description="Version of World of Warcraft",
items=[('264', 'WOTLK', "")],
default='264'
)
forward_axis: EnumProperty(
name="Forward Axis",
description="The direction the exported model is facing",
items=[('X+','X+',''), ('X-','X-',''), ('Y+','Y+',''), ('Y-','Y-','')],
default='X+'
)
scale: FloatProperty (
name="Scale",
description="How much to scale the output model",
default=1.0
)
autofill_textures: BoolProperty(
name="Fill texture paths",
description="Automatically assign texture paths based on texture filenames",
default=True
)
merge_vertices: BoolProperty(
name="Merge vertices",
description="Execute merge vertices algorithm for splitting uv islands after",
default=True
)
def execute(self, context):
if context.scene:
context.scene.wow_scene.type = 'M2'
export_m2(int(context.scene.wow_scene.version), self.filepath, self.export_selected, self.autofill_textures, self.forward_axis, self.scale, self.merge_vertices)
return {'FINISHED'}
self.report({'ERROR'}, 'Invalid scene type.')
class WBS_OT_M2_test(bpy.types.Operator):
"""Read M2 file and compare output with input"""
bl_idname = "test.m2"
bl_label = "Test M2"
bl_options = {'UNDO', 'REGISTER'}
filepath: StringProperty(
subtype='FILE_PATH',
)
filter_glob: StringProperty(
default="*.m2",
options={'HIDDEN'}
)
def execute(self, context):
m2_in = import_m2(int(context.scene.wow_scene.version), self.filepath)
context.scene.wow_scene.type = 'M2'
m2_out = create_m2(int(context.scene.wow_scene.version), self.filepath, False, False,'X+',1)
def objectify(obj_in,visited_stack = []):
def is_primitive(type_in):
return type_in in ['bool','float','int','str']
def has_visited(obj):
for value in visited_stack:
if obj is value:
return True
return False
obj_type = type(obj_in).__name__
def skip_field(typename,fieldname):
if obj_type == 'M2CompBone':
return fieldname in ['name','index','parent','children']
pass
def transform(value):
if obj_type == 'M2Array':
return value['values']
if obj_type == 'Array':
return value['values']
else:
return value
if obj_type == 'Vector':
return (obj_in.x,obj_in.y,obj_in.z) if hasattr(obj_in,'z') else (obj_in.x,obj_in.y)
if obj_type == 'Quaternion':
return (obj_in.w,obj_in.x,obj_in.y,obj_in.z)
if is_primitive(obj_type):
return obj_in
visited_stack.append(obj_in)
if obj_type == 'list' or obj_type == 'tuple':
list_out = []
for value in obj_in:
value_type = type(value).__name__
if not is_primitive(type(value).__name__):
if has_visited(value):
continue
value = objectify(value, visited_stack)
list_out.append(value)
return list_out
else:
obj_out = {}
for key in dir(obj_in):
if skip_field(obj_type,key):
continue
if key.startswith('__'):
continue
if not hasattr(obj_in,key):
continue
value = getattr(obj_in,key)
if callable(value):
continue
if not is_primitive(type(value).__name__):
if has_visited(value):
continue
value = objectify(value, visited_stack)
obj_out[key] = value
return transform(obj_out)
def diff(obj1,obj2):
if type(obj1) != type(obj2):
return f'TypeError({str(type(obj1))},{str(type(obj2))}) ({obj1},{obj2})'
dtype = type(obj1)
if dtype is dict:
diffObj = {}
for k in obj1:
if not k in obj2:
diffObj[k] = 'LeftOnly'
else:
diffVal = diff(obj1[k],obj2[k])
if not diffVal is None:
diffObj[k] = diffVal
for k in obj2:
if not k in obj1:
diffObj[k] = 'RightOnly'
return diffObj if len(diffObj) > 0 else None
elif dtype is list or dtype is tuple:
diffObj = {}
if len(obj1) != len(obj2):
diffObj["len"] = str(len(obj1)) + " != " + str(len(obj2))
for i in range(min(len(obj1),len(obj2))):
diffVal = diff(obj1[i],obj2[i])
if not diffVal is None:
if not "values" in diffObj:
diffObj["values"] = {}
diffObj["values"][i] = diffVal
return diffObj if len(diffObj) > 0 else None
else:
return str(obj1) + ' != ' + str(obj2) if obj1 != obj2 else None
with open(self.filepath+".json",'w') as f:
f.write(json.dumps(diff(objectify(m2_in),objectify(m2_out)),indent=4))
m2_out.write(self.filepath[:-2]+"out.m2")
return {'FINISHED'}
def invoke(self, context, event):
wm = context.window_manager
wm.fileselect_add(self)
return {'RUNNING_MODAL'}
class WBS_OT_M2_Print_Warnings(bpy.types.Operator):
"""Print Warnings for M2 Exports"""
bl_idname = "print_warnings.m2"
bl_label = "Find common problems with exported M2 models and print warnings about them"
bl_options = {'REGISTER'}
def execute(self, context):
import importlib
from ..m2.operations import m2_export_warnings
importlib.reload(m2_export_warnings)
m2_export_warnings.print_warnings()
return {'FINISHED'}
class WBS_OT_M2_ConvertBones(bpy.types.Operator):
"""Convert Bones and Animation Tracks To WoW-style bones"""
bl_idname = "convert_bones.m2"
bl_label = "Convert Bones to WoW"
bl_options = {'REGISTER'}
def execute(self, context):
import importlib
from ..m2.operations import convert_m2_bones
importlib.reload(convert_m2_bones)
convert_m2_bones.convert_m2_bones()
return {'FINISHED'}
'''
Created on Dec 30, 2019
@author: Patrick
'''
'''
Copyright (C) 2018 CG Cookie
https://github.com/CGCookie/retopoflow
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 3 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, see <http://www.gnu.org/licenses/>.
'''
import bpy
from ..addon_common.cookiecutter.cookiecutter import CookieCutter
from ..addon_common.common import ui
from ..addon_common.common.drawing import Drawing
from ..addon_common.common.boundvar import BoundInt, BoundFloat, BoundBool
# some settings container
options = {}
options["variable_1"] = 10.0
options["variable_3"] = True
# override this pass through to allow anything in 3dview to pass through
def in_region(reg, x, y):
# first, check outside of area
if x < reg.x: return False
if y < reg.y: return False
if x > reg.x + reg.width: return False
if y > reg.y + reg.height: return False
return True
class CookieCutter_UITest(CookieCutter, bpy.types.Operator):
bl_idname = "view3d.cookiecutter_ui_test"
bl_label = "CookieCutter UI Test (Example)"
default_keymap = {
'commit': 'RET',
'cancel': 'ESC',
'test': 'LEFTMOUSE'
}
# for this, checkout "polystrips_props.py'
@property
def variable_2_gs(self):
return getattr(self, '_var_cut_count_value', 0)
@variable_2_gs.setter
def variable_2_gs(self, v):
if self.variable_2 == v: return
self.variable_2 = v
# if self.variable_2.disabled: return
### Redefine/OVerride of defaults methods from CookieCutter ###
def start(self):
opts = {
'pos': 9,
'movable': True,
'bgcolor': (0.2, 0.2, 0.2, 0.8),
'padding': 0,
}
# some data storage, simple single variables for now
# later, more coplex dictionaries or container class
self.variable_1 = BoundFloat('''options['variable_1']''', min_value=0.5, max_value=15.5)
self.variable_2 = BoundInt('''self.variable_2_gs''', min_value=0, max_value=10)
self.variable_3 = BoundBool('''options['variable_3']''')
self.setup_ui()
# def update(self):
# self.ui_action.set_label('Press: %s' % (','.join(self.actions.now_pressed.keys()),))
def end_commit(self):
pass
def end_cancel(self):
pass
def end(self): # happens after end_commit or end_cancel
pass
def should_pass_through(self, context, event):
print(context.region.type)
print(context.area.type)
if context.area.type != "VIEW_3D":
return True
# first, check outside of area
outside = False
if event.mouse_x < context.area.x: outside = True
if event.mouse_y < context.area.y: outside = True
if event.mouse_x > context.area.x + context.area.width: outside = True
if event.mouse_y > context.area.y + context.area.height: outside = True
if outside:
print('outside the 3DView area')
return True
# make sure we are in the window region, not the header, tools or UI
for reg in context.area.regions:
if in_region(reg, event.mouse_x, event.mouse_y) and reg.type != "WINDOW":
print('in wrong region')
return True
return False
######## End Redefinitions from CookieCutter Class ###
# typically, we would definte these somewhere else
def tool_action(self):
print('tool action')
return
def setup_ui(self):
# go ahead and open these files
# addon_common.common.ui
# addon_common.cookiecutter.cookiecutter_ui
# know that every CookieCutter instance has self.document upon startup
# most of our ui elements are going to be children of self.document.body
# we generate our UI elements using the methods in ui.py
# we need to read ui_core, particulalry UI_Element
# collapsible, and framed_dialog
# first, know
self.ui_main = ui.framed_dialog(label='ui.framed_dialog',
resiable=None,
resiable_x=True,
resizable_y=False,
closeable=True,
moveable=True,
hide_on_close=True,
parent=self.document.body)
# tools
ui_tools = ui.div(id="tools", parent=self.ui_main)
ui.button(label='ui.button', title='self.tool_action() method linked to button', parent=ui_tools,
on_mouseclick=self.tool_action)
# create a collapsille container to hold a few variables
container = ui.collapsible('ui.collapse container', parent=self.ui_main)
i1 = ui.labeled_input_text(label='Sui.labeled_input_text',
title='float property to BoundFLoat',
value=self.variable_1)
i2 = ui.labeled_input_text(label='ui.labled_input_text',
title='integer property to BoundInt',
value=self.variable_2)
i3 = ui.input_checkbox(
label='ui.input_checkbox',
title='True/False property to BoundBool')
container.builder([i1, i2, i3])
@CookieCutter.FSM_State('main')
def modal_main(self):
Drawing.set_cursor('DEFAULT')
if self.actions.pressed('test'):
self.actions.pass_through = False
print('aaaaaaaaaand action \n\n')
return 'test'
if self.actions.pressed('cancel'):
print('cancelled')
self.done(cancel=True)
return 'cancel'
if self.actions.pressed('commit'):
print('committed')
self.done()
return 'finished'
@CookieCutter.FSM_State('test')
def modal_grab(self):
Drawing.set_cursor('CROSSHAIR')
self.actions.unuse('navigate')
if self.actions.mousemove:
print('action mousemove!')
self.report({'INFO'}, "Applied")
return 'test' # can return nothing and stay in this state?
if self.actions.released('test'):
# self.lbl.set_label('finish action')
print('finish action')
return 'main'
# there are no drawing methods for this example
# this is all buttons and input wundows
class M2DrawingTest(bpy.types.Operator):
bl_idname = "wm.render_test"
bl_label = "M2 Render Test"
def execute(self, context):
from render.drawing_manager import DrawingManager
dm = DrawingManager(handler_mode=True)
dm.queue_for_drawing(bpy.context.view_layer.objects.active)
return {'FINISHED'}
class TestBPYBoost(bpy.types.Operator):
bl_idname = "wm.bpy_boost_test"
bl_label = "BPY Boost Test"
def execute(self, context):
from ..wbs_kernel.wbs_kernel.bpy_boost import BMesh, Window, create_ui_popup
ds = bpy.context.evaluated_depsgraph_get()
obj = bpy.context.view_layer.objects.active
'''
object_eval = obj.evaluated_get(ds)
if object_eval:
mesh = object_eval.to_mesh()
if mesh:
# TODO test if this makes sense
# If negative scaling, we have to invert the normals
# if not mesh.has_custom_normals and object_eval.matrix_world.determinant() < 0.0:
# # Does not handle custom normals
# mesh.flip_normals()
mesh.calc_loop_triangles()
if not mesh.loop_triangles:
object_eval.to_mesh_clear()
mesh = None
if mesh:
if mesh.use_auto_smooth:
if not mesh.has_custom_normals:
mesh.calc_normals()
mesh.split_faces()
mesh.calc_loop_triangles()
if mesh.has_custom_normals:
mesh.calc_normals_split()
ptr = mesh.loop_triangles[0].as_pointer()
'''
ptr = obj.data.as_pointer()
bl_mesh = BMesh(ptr)
bl_mesh.get_mesh_geometry_batches_raw()
print('window')
ptr = context.as_pointer()
#Window(ptr, 1, "WoW Render Preview")
create_ui_popup(ptr)
return {'FINISHED'}
@@ -0,0 +1,285 @@
import bpy
from bpy.app.handlers import persistent
from ..ui.preferences import get_project_preferences
from .. import ui_icons
from ..utils.callbacks import on_release
from ..utils.custom_object import CustomObject
from .enums import WoWSceneTypes
from typing import Type
class WBS_PT_object_properties_common:
""" Common base for all bpy.types.Object property panels. """
bl_region_type = 'WINDOW'
bl_space_type = 'PROPERTIES'
__wbs_custom_object_type__: Type[CustomObject]
""" Type of custom object associated with the panel. """
__wbs_scene_type__: WoWSceneTypes
""" Type of WoW scene. """
_required = {'bl_context', 'bl_label', '__wbs_custom_object_type__', '__wbs_scene_type__'}
""" Required fields to override in derived classes. """
def __init_subclass__(cls, **kwargs):
for requirement in cls._required:
if not hasattr(cls, requirement):
raise NotImplementedError(f'"{cls.__name__}" must override "{requirement}".')
super().__init_subclass__(**kwargs)
@classmethod
def poll(cls, context: bpy.types.Context):
"""
Used to identify if the panel should be rendered in a given context.
:param context: Current context.
:return: True if should be rendered, else False.
"""
return (context.scene is not None
and context.scene.wow_scene.type == cls.__wbs_scene_type__.name
and context.object
and cls.__wbs_custom_object_type__.match(context.object))
class WBS_PT_wow_scene(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_label = "WoW Scene"
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(context.scene.wow_scene, 'version')
col.prop(context.scene.wow_scene, 'type')
col.prop(context.scene.wow_scene, 'game_path')
col.prop(context.scene, 'wow_screen_3d', text='Main workspace')
@classmethod
def poll(cls, context):
return context.scene is not None
class WowScenePropertyGroup(bpy.types.PropertyGroup):
version: bpy.props.EnumProperty(
name='Client version',
items=[('2', 'WotLK', "", ui_icons['WOTLK'], 0),
('6', 'Legion', "", ui_icons['LEGION'], 1)],
default='2'
)
type: bpy.props.EnumProperty(
name='Scene type',
description='Sets up the UI to work with a specific WoW game format',
items=[
('M2', 'M2', 'M2 model', 'FILE_VOLUME', 0),
('WMO', 'WMO', 'World Map Object (WMO)', 'FILE_3D', 1)],
default='WMO'
)
doodadset_mode: bpy.props.BoolProperty(
name='Doodadset Mode',
description='Enters doodadset editing mode',
default=False
)
game_path: bpy.props.StringProperty(
name='Game path',
description='A path to the model in WoW filesystem.'
)
class WOW_PT_render_settings(bpy.types.Panel):
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "scene"
bl_label = "WoW Render Settings"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
col = layout.column()
col.label(text='Lighting:')
col.prop(context.scene.wow_render_settings, "ext_ambient_color")
col.prop(context.scene.wow_render_settings, "ext_dir_color")
col.prop(context.scene.wow_render_settings, "sidn_scalar")
col.label(text='Fog:')
col.prop(context.scene.wow_render_settings, "fog_color")
col.prop(context.scene.wow_render_settings, "fog_start")
col.prop(context.scene.wow_render_settings, "fog_end")
col.label(text='Sun Direction:')
col.prop(context.scene.wow_render_settings, "sun_direction", text='')
@classmethod
def poll(cls, context):
return context.scene is not None
@on_release()
def update_ext_ambient_color(self, context):
properties = bpy.data.node_groups.get('MO_Properties')
if properties:
properties.nodes['extLightAmbientColor'].outputs[0].default_value = self.ext_ambient_color
@on_release()
def update_ext_dir_color(self, context):
properties = bpy.data.node_groups.get('MO_Properties')
if properties:
properties.nodes['extLightDirColor'].outputs[0].default_value = self.ext_dir_color
@on_release()
def update_sidn_scalar(self, context):
properties = bpy.data.node_groups.get('MO_Properties')
if properties:
properties.nodes['SIDNScalar'].outputs[0].default_value = self.sidn_scalar
@on_release()
def update_sun_direction(self, context):
properties = bpy.data.node_groups.get('MO_Properties')
if properties:
properties.nodes['SunDirection'].inputs[1].default_value = self.sun_direction
class WoWRenderSettingsPropertyGroup(bpy.types.PropertyGroup):
ext_ambient_color: bpy.props.FloatVectorProperty(
name="Ext. Ambient Color", subtype='COLOR',
default=(1, 1, 1, 1), size=4, min=0.0, max=1.0, update=update_ext_ambient_color)
ext_dir_color: bpy.props.FloatVectorProperty(
name="Ext. Dir Color", subtype='COLOR',
default=(0.991, 0.246, 0, 1), size=4, min=0.0, max=1.0, update=update_ext_dir_color)
sidn_scalar: bpy.props.FloatProperty(
name='SIDN intensity', min=0.0, max=1.0, update=update_sidn_scalar)
sun_direction: bpy.props.FloatVectorProperty(
name='Sun Direction', default=(0.2, 0.7, 0.6),
size=3, subtype='DIRECTION', update=update_sun_direction)
fog_color: bpy.props.FloatVectorProperty(
name='Fog Color', subtype='COLOR',
default=(0.5, 0.5, 0.5), size=3, min=0.0, max=1.0)
fog_start: bpy.props.FloatProperty(name='Fog Start', min=0.1, default=30.0)
fog_end: bpy.props.FloatProperty(name='Fog End', min=0.1, default=1000.0)
def update_screen_3d(self, context):
if not self.wow_screen_3d:
return
viewport = None
for area in bpy.context.scene.wow_screen_3d.areas:
if area.type == 'VIEW_3D':
rv3d = area.spaces[0].region_3d
if rv3d is not None:
viewport = rv3d
break
bpy.app.driver_namespace["wow_viewport"] = viewport
@persistent
def _apply_pref_scene_type(_):
prefs = get_project_preferences()
if not prefs:
return
for scene in bpy.data.scenes:
scene.wow_scene.type = prefs.scene_type
# Force UI refresh
wm = bpy.context.window_manager
if wm:
for win in wm.windows:
scr = win.screen
if scr:
for area in scr.areas:
area.tag_redraw()
def register_wow_scene_properties():
bpy.types.Scene.wow_scene = bpy.props.PointerProperty(type=WowScenePropertyGroup)
bpy.types.Scene.wow_screen_3d = bpy.props.PointerProperty(type=bpy.types.Screen, update=update_screen_3d)
bpy.types.Scene.wow_render_settings = bpy.props.PointerProperty(type=WoWRenderSettingsPropertyGroup)
def unregister_wow_scene_properties():
del bpy.types.Scene.wow_scene
del bpy.types.Scene.wow_screen_3d
del bpy.types.Scene.wow_render_settings
def render_top_bar(self, context):
if context.region.alignment == 'TOP':
return
layout = self.layout
row = layout.row(align=True)
doodadset_mode = context.scene.wow_scene.doodadset_mode
if proj_prefs := get_project_preferences():
row.prop(proj_prefs, 'export_method_enum', text='Export Folder', icon='FILE_TICK')
if context.scene.wow_scene.type == 'WMO':
icon = 'CHECKBOX_HLT' if doodadset_mode else 'CHECKBOX_DEHLT'
text = "Doodadset Mode ON" if doodadset_mode else "Doodadset Mode OFF"
row.operator("scene.doodadset_mode", text=text, icon=icon)
row.operator("scene.save_current_wmo_collection", text="Save current WMO", icon='FILE_TICK')
if context.scene.wow_scene.type == 'M2':
row.operator("scene.save_current_m2", text="Save current M2", icon='FILE_TICK')
row.label(text='WoW Scene:')
row.prop(context.scene.wow_scene, 'version', text='')
row.prop(context.scene.wow_scene, 'type', text='')
row.operator("scene.reload_wow_filesystem", text="", icon='FILE_REFRESH')
def render_viewport_toggles_left(self, context):
layout = self.layout
row = layout.row(align=True)
row.operator("wow.toggle_image_alpha", icon='NODE_TEXTURE', text='')
menu_import_wmo = lambda self, ctx: self.layout.operator("import_mesh.wmo", text="WoW WMO (.wmo)")
menu_export_wmo = lambda self, ctx: self.layout.operator("export_mesh.wmo", text="WoW WMO (.wmo)")
menu_import_m2 = lambda self, ctx: self.layout.operator("import_mesh.m2", text="WoW M2 (.m2)")
menu_export_m2 = lambda self, ctx: self.layout.operator("export_mesh.m2", text="WoW M2 (.m2)")
menu_convert_bones = lambda self, ctx: self.layout.operator("convert_bones.m2", text="Convert Bones To WoW")
menu_print_m2_warnings = lambda self, ctx: self.layout.operator("print_warnings.m2", text="Print M2 Warnings")
def register():
register_wow_scene_properties()
bpy.app.handlers.load_post.append(_apply_pref_scene_type)
bpy.types.TOPBAR_HT_upper_bar.append(render_top_bar)
bpy.types.VIEW3D_HT_header.append(render_viewport_toggles_left)
bpy.types.TOPBAR_MT_file_import.append(menu_import_wmo)
bpy.types.TOPBAR_MT_file_import.append(menu_import_m2)
bpy.types.TOPBAR_MT_file_export.append(menu_export_wmo)
bpy.types.TOPBAR_MT_file_export.append(menu_export_m2)
bpy.types.TOPBAR_MT_file_export.append(menu_convert_bones)
bpy.types.TOPBAR_MT_file_export.append(menu_print_m2_warnings)
def unregister():
unregister_wow_scene_properties()
bpy.app.handlers.load_post.remove(_apply_pref_scene_type)
bpy.types.TOPBAR_MT_file_import.remove(menu_import_wmo)
bpy.types.TOPBAR_MT_file_import.remove(menu_import_m2)
bpy.types.TOPBAR_MT_file_export.remove(menu_export_wmo)
bpy.types.TOPBAR_MT_file_export.remove(menu_export_m2)
bpy.types.TOPBAR_HT_upper_bar.remove(render_top_bar)
bpy.types.VIEW3D_HT_header.remove(render_viewport_toggles_left)
bpy.types.TOPBAR_MT_file_export.remove(menu_convert_bones)
bpy.types.TOPBAR_MT_file_export.remove(menu_print_m2_warnings)
@@ -0,0 +1,390 @@
from ..config import ADDON_MODULE_NAME
from typing import Optional, List, Tuple
import bpy
def get_addon_preferences() -> 'WBS_AP_Preferences':
"""
Gets current Blender addon preferences for this addon in any context.
:return: Addon preferences.
"""
return bpy.context.preferences.addons[ADDON_MODULE_NAME].preferences
def get_project_preferences() -> Optional['WBS_PG_ProjectPreferences']:
"""
Gets current project preferences in any context.
:return: Project preferences or None.
"""
addon_preferences = get_addon_preferences()
if not len(addon_preferences.projects):
raise UserWarning("No active project. Check WBS settings.")
return addon_preferences.projects[addon_preferences.active_project_index]
class WBS_UL_Projects(bpy.types.UIList):
""" UI List displaying currently saved projects. """
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
split = layout.split(factor=0.3)
split.prop(item, "name", text="", emboss=False, translate=False
, icon='RADIOBUT_ON' if index == data.active_project_index else 'RADIOBUT_OFF')
def invoke(self, context, event):
...
class WBS_PG_ExportMethodSetting(bpy.types.PropertyGroup):
""" Property group for individual export method settings. """
name: bpy.props.StringProperty(
name="Export Setting Name",
description="Name of the export folder setting"
)
value: bpy.props.StringProperty(
name="Export Directory Path",
description="Value of the export folder setting",
subtype='DIR_PATH'
)
class WBS_PG_ProjectPreferences(bpy.types.PropertyGroup):
""" Property group holding data for project preferences set. """
wow_path: bpy.props.StringProperty(
name="WoW Client Path",
subtype='DIR_PATH'
)
wmv_path: bpy.props.StringProperty(
name="WoW Model Viewer Log Path",
subtype='FILE_PATH'
)
wow_export_path: bpy.props.StringProperty(
name="WoW Export Runtimelog Path",
subtype='FILE_PATH'
)
noggit_red_path: bpy.props.StringProperty(
name="Noggit Red log Path",
subtype='FILE_PATH'
)
time_import_method: bpy.props.EnumProperty(
name="Time Conversion Import Method",
items=[
('Convert', "Convert to 30 FPS", "(WBS Method)"),
('Keep Original', "Keep_Original", "Don't convert timestamps"),
],
default='Convert',
description="Choose the preferred method for timestamp import."
)
import_method: bpy.props.EnumProperty(
name="Import Method",
items=[
('WMV', "WMV", "Use WoW Model Viewer"),
('WowExport', "WowExport", "Use WoW Export"),
('NoggitRed', "NoggitRed", "Use Noggit Red"),
],
default='WMV',
description="Choose the preferred method of import for WoW files."
)
export_method_settings: bpy.props.CollectionProperty(
type=WBS_PG_ExportMethodSetting,
name="Export Folder Settings",
description="Settings for the export methods"
)
export_dir_path: bpy.props.StringProperty(
name="Export Directory Path",
description="A directory for WBS to export M2/WBS, could be ascension Data client, or a patch folder inside it.",
subtype="DIR_PATH"
)
active_export_method_index: bpy.props.IntProperty(default=0)
def update_export_dir_path(self, context):
method = self.export_method_settings[int(self.export_method_enum)]
self.active_export_method_index = int(self.export_method_enum)
self.export_dir_path = method.value
def export_method_items(self, context) -> List[Tuple[str, str, str]]:
return [(str(i), method.name, "") for i, method in enumerate(self.export_method_settings)]
export_method_enum: bpy.props.EnumProperty(
name="Export Method",
description="Select Export Method",
items=export_method_items,
update=update_export_dir_path
)
cache_dir_path: bpy.props.StringProperty(
name="Cache Directory Path",
description="Any folder that can be used to store textures and other temporary files.",
subtype="DIR_PATH"
)
project_dir_path: bpy.props.StringProperty(
name="Project Directory Path",
description="A directory Blender saves WoW files to and treats it as top-priority patch.",
subtype="DIR_PATH"
)
merge_vertices: bpy.props.BoolProperty(
name="Merge Vertices Algorythm when quick saving M2's",
description="Use the merge vertices algorythm when quick saving m2's with topbar button",
default=True
)
verbosity_level: bpy.props.EnumProperty(
name="Log Verbosity",
description="Controls how detailed output messages should be",
items=[
('1', "ERROR", "Only show critical errors"),
('2', "WARNING", "Show warnings and errors"),
('3', "INFO", "Show general info, warnings, and errors"),
('4', "DEBUG", "Show all messages including debug output"),
],
default='3',
options=set()
)
scene_type: bpy.props.EnumProperty(
name="Default Scene Type",
description="Controls which scene type should be the default",
items=[
('M2', "M2", "Default to M2"),
('WMO', "WMO", "Default to WMO"),
],
default='WMO',
options=set()
)
class WBS_OT_ProjectListActions(bpy.types.Operator):
"""
Moves items up and down the list of projects, adds or removes.
"""
bl_idname = "wbs.project_list_action"
bl_label = "List Actions"
bl_description = "Move items up and down, add and remove"
bl_options = {'REGISTER', 'INTERNAL'}
action: bpy.props.EnumProperty(
items=(
('UP', "Up", ""),
('DOWN', "Down", ""),
('REMOVE', "Remove", ""),
('ADD', "Add", ""),
('DUPLICATE', "Duplicate", "")))
@classmethod
def description(cls, context, properties):
match properties.action:
case 'UP':
return "Move project up the list"
case 'DOWN':
return "Move project down the list"
case 'ADD':
return "Add new project"
case 'DUPLICATE':
return "Duplicate current project"
case 'REMOVE':
return "Remove project from the list"
case _:
raise NotImplementedError()
def duplicate_project(self, source, target):
target.name = f"{source.name}_copy"
target.verbosity_level = source.verbosity_level
target.scene_type = source.scene_type
target.wow_path = source.wow_path
target.wmv_path = source.wmv_path
target.wow_export_path = source.wow_export_path
target.noggit_red_path = source.noggit_red_path
target.time_import_method = source.time_import_method
target.import_method = source.import_method
target.cache_dir_path = source.cache_dir_path
target.project_dir_path = source.project_dir_path
target.export_dir_path = source.export_dir_path
target.merge_vertices = source.merge_vertices
for setting in source.export_method_settings:
new_setting = target.export_method_settings.add()
new_setting.name = setting.name
new_setting.value = setting.value
def invoke(self, context, event):
addon_prefs = get_addon_preferences()
idx = addon_prefs.active_project_index
match self.action:
case 'DOWN':
if idx < len(addon_prefs.projects) - 1:
addon_prefs.projects.move(idx, idx + 1)
addon_prefs.active_project_index += 1
case 'UP':
if idx >= 1:
addon_prefs.projects.move(idx, idx - 1)
addon_prefs.active_project_index -= 1
case 'REMOVE':
if len(addon_prefs.projects):
addon_prefs.active_project_index -= 1
addon_prefs.projects.remove(idx)
case 'ADD':
item = addon_prefs.projects.add()
item.name = 'New project'
addon_prefs.active_project_index = len(addon_prefs.projects) - 1
case 'DUPLICATE':
if len(addon_prefs.projects):
source = addon_prefs.projects[idx]
target = addon_prefs.projects.add()
self.duplicate_project(source, target)
addon_prefs.active_project_index = len(addon_prefs.projects) - 1
return {"FINISHED"}
class WBS_UL_Export(bpy.types.UIList):
""" UI List displaying currently saved export methods. """
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
split = layout.split(factor=0.3)
split.prop(item, "name", text="", emboss=False, translate=False
, icon='RADIOBUT_ON' if index == data.active_export_method_index else 'RADIOBUT_OFF')
def invoke(self, context, event):
...
class WBS_OT_ExportMethodActions(bpy.types.Operator):
"""
Adds or removes export method settings.
"""
bl_idname = "wbs.export_method_action"
bl_label = "Export Method Actions"
bl_description = "Add or remove export method settings"
bl_options = {'REGISTER', 'INTERNAL'}
action: bpy.props.EnumProperty(
items=(
('UP', "Up", ""),
('DOWN', "Down", ""),
('ADD', "Add", ""),
('REMOVE', "Remove", "")))
@classmethod
def description(cls, context, properties):
match properties.action:
case 'UP':
return "Move project up the list"
case 'DOWN':
return "Move project down the list"
case 'ADD':
return "Add new export method setting"
case 'REMOVE':
return "Remove selected export method setting"
case _:
raise NotImplementedError()
def invoke(self, context, event):
proj_prefs = get_project_preferences()
idx = proj_prefs.active_export_method_index
match self.action:
case 'DOWN':
if idx < len(proj_prefs.export_method_settings) - 1:
proj_prefs.export_method_settings.move(idx, idx + 1)
proj_prefs.active_export_method_index += 1
case 'UP':
if idx >= 1:
proj_prefs.export_method_settings.move(idx, idx - 1)
proj_prefs.active_export_method_index -= 1
case 'ADD':
item = proj_prefs.export_method_settings.add()
item.name = 'New setting'
item.value = ''
proj_prefs.active_export_method_index = len(proj_prefs.export_method_settings) - 1
case 'REMOVE':
if len(proj_prefs.export_method_settings):
proj_prefs.export_method_settings.remove(idx)
proj_prefs.active_export_method_index -= 1
return {"FINISHED"}
class WBS_AP_Preferences(bpy.types.AddonPreferences):
"""
Stores global preferences for the addon.
"""
bl_idname = 'io_scene_wmo'
projects: bpy.props.CollectionProperty(
type=WBS_PG_ProjectPreferences,
name='Projects',
description='Project presets.',
options=set()
)
active_project_index: bpy.props.IntProperty(default=0)
def draw(self, context: bpy.types.Context):
layout = self.layout
row = layout.row()
row.template_list('WBS_UL_Projects', '', self, 'projects', self, 'active_project_index', rows=2)
col = row.column(align=True)
col.operator("wbs.project_list_action", text="Create project").action = 'ADD'
col.operator("wbs.project_list_action", text="Duplicate project").action = 'DUPLICATE'
col.operator("wbs.project_list_action", text="Remove project").action = 'REMOVE'
col.separator()
col.operator("wbs.project_list_action", icon='TRIA_UP', text="").action = 'UP'
col.operator("wbs.project_list_action", icon='TRIA_DOWN', text="").action = 'DOWN'
col.separator()
if proj_prefs := get_project_preferences():
col = layout.column(align=True)
col.label(text='Project settings:', icon='SETTINGS')
box = col.box()
box.prop(proj_prefs, 'wow_path')
box.prop(proj_prefs, 'time_import_method')
box.prop(proj_prefs, 'import_method')
if proj_prefs.import_method == 'WMV':
box.prop(proj_prefs, 'wmv_path')
elif proj_prefs.import_method == 'WowExport':
box.prop(proj_prefs, 'wow_export_path')
elif proj_prefs.import_method == 'NoggitRed':
box.prop(proj_prefs, 'noggit_red_path')
box.prop(proj_prefs, 'cache_dir_path')
box.prop(proj_prefs, 'project_dir_path')
box.prop(proj_prefs, 'export_method_enum', text='Export Directory Path')
box.prop(proj_prefs, "verbosity_level")
box.prop(proj_prefs, "scene_type")
col = layout.column(align=True)
col.label(text='Export Folder Settings:', icon='SETTINGS')
row = col.row()
row.template_list('WBS_UL_Export', '', proj_prefs, 'export_method_settings', proj_prefs, 'active_export_method_index')
sub_col = row.column(align=True)
sub_col.operator("wbs.export_method_action", text="", icon='ADD').action = 'ADD'
sub_col.operator("wbs.export_method_action", text="", icon='REMOVE').action = 'REMOVE'
sub_col.operator("wbs.export_method_action", icon='TRIA_UP', text="").action = 'UP'
sub_col.operator("wbs.export_method_action", icon='TRIA_DOWN', text="").action = 'DOWN'
if proj_prefs.export_method_settings:
setting = proj_prefs.export_method_settings[proj_prefs.active_export_method_index]
box = col.box()
box.prop(setting, 'name')
box.prop(setting, 'value')
col.separator()
col = layout.column(align=True)
col.label(text='M2 Quick Save Settings:', icon='SETTINGS')
box = col.box()
box.prop(proj_prefs, 'merge_vertices', text='Merge Vertices')
@@ -0,0 +1,24 @@
import bpy
CLOSE_TO_ZERO = 5e-324
def skybox_follow_viewport_camera():
viewport = bpy.app.driver_namespace.get('wow_viewport')
if not viewport:
return CLOSE_TO_ZERO
if bpy.context.view_layer.objects.active:
bpy.context.view_layer.objects.active.location = viewport.view_matrix.inverted().translation
return 0.0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
#def register():
# bpy.app.timers.register(skybox_follow_viewport_camera, persistent=True)
#def unregister():
# bpy.app.timers.unregister(skybox_follow_viewport_camera)
@@ -0,0 +1,336 @@
/*
this is a default UI stylesheet.
right now, this is a test stylesheet.
*/
* {
color: white;
}
button {
display: block;
width: 100%;
margin: 0px 1px;
/*padding: 0px;*/
background: rgba(64, 64, 64, 0.25);
border: 1px rgba(0,0,0,0.1);
}
button:focus {
background: rgba(64, 64, 64, 1.00);
}
button:active {
color: black;
/*background: rgb(192,128,128);*/
background: hsla(200, 100%, 62.5%, 1.0); /* rgba(128,224,255,0.5); */
}
button:hover {
background: rgba(64, 64, 64, 1.00); /* hsla(200, 100%, 62.5%, 1.0); /* rgb(64,192,255); */
}
button:active:hover {
color: black;
background: hsla(200, 100%, 62.5%, 1.0); /*rgb(128,224,255);*/
}
button:disabled {
background-color: rgb(128,128,128);
color: rgb(192,192,192);
/*border-width: 1px;*/
border-color: rgb(96,96,96);
cursor: default;
}
button.dialog-close {
margin: 2px 4px 2px 0px;
padding: 2px;
border: 1px rgba(0,0,0,0.4);
display: inline;
width: 20px;
height: 20px;
background: rgba(0,0,0,0.25) url('close.png');
}
button.dialog-close:hover {
background-color: rgba(255,255,255,0.25);
}
dialog.framed > div.inside {
margin: 0px;
padding: 0px 0px 2px 0px;
}
div#tools {
border: 1px black;
margin: 1px 1px 2px 1px;
}
input.tool {
margin: 0px;
border-width: 0px;
width: 100%;
display: block;
padding: 0px;
}
input.tool > img {
display: none;
}
input.tool:hover {
background: rgba(64, 64, 64, 1.00); /* hsla(200, 0%, 75%, 1.0); */
}
input.tool[checked] {
background: hsla(200, 100%, 62.5%, 1.0);
}
input.tool:active {
background: hsla(200, 100%, 62.5%, 0.5);
}
input.tool:active:hover {
background: hsla(200, 100%, 62.5%, 0.75);
}
input.tool > label {
margin: 0px;
border: 0px;
border-radius: 0px;
padding: 1px;
width: 100%;
/*height:20px;*/
color: white;
}
input.tool[checked] label {
color: black;
}
input.tool:active label {
color: black;
}
input.tool > label > img {
display: inline;
border-radius: 0px;
border:0px white;
padding:0px;
margin:0px;
width:20px;
height:20px;
}
input.tool > label > label {
border: 0px;
padding:2px 0px; margin:0px; margin-left: 4px;
}
input.symmetry-enable {
width: 33.33%;
overflow-x: hidden;
white-space: nowrap;
}
input.symmetry-viz {
margin: 0px;
border: 0px black;
width: 33.333%;
/*width: 100%;*/
/*display: block;*/
padding: 4px 0px 4px 6px;
}
input.symmetry-viz > img {
display: none;
}
input.symmetry-viz:hover {
background: rgba(64, 64, 64, 1.00); /* hsla(200, 0%, 75%, 1.0); */
}
input.symmetry-viz[checked] {
background: hsla(200, 100%, 62.5%, 1.0);
}
input.symmetry-viz:active {
background: hsla(200, 100%, 62.5%, 0.5);
}
input.symmetry-viz:active:hover {
background: hsla(200, 100%, 62.5%, 0.75);
}
input.symmetry-viz > label {
margin: 0px;
border: 0px;
border-radius: 0px;
padding: 0px;
width: 100%;
/*height:20px;*/
color: white;
}
input.symmetry-viz[checked] label {
color: black;
}
input.symmetry-viz:active label {
color: black;
}
input[type="text"]:hover {
color: black;
}
input[type="text"]:focus {
color: black;
}
*.inputtext-cursor {
color: black; /* hsla(200, 100%, 12.5%, 1.0); /* l=62.5% */
}
input[type="text"]:disabled {
border-color: rgb(64,64,64);
background-color: rgb(64, 64, 64);
color: rgb(32,32,32);
cursor: default;
}
div.collection {
margin: 1px;
border: 1px rgba(32,32,32,1.0);
padding: 0px;
}
div.collection > div.header {
background-color: rgba(32,32,32,1.0);
display: block;
width: 100%;
padding: 2px;
margin: 0px;
border: 1px rgba(32,32,32,1.0);
border-bottom-color: rgba(48, 48, 48, 1.0);
border-radius: 0px;
}
div.collapsible {
margin: 0px;
border: 1px rgba(64,64,64,0.125);
padding: 1px;
}
div.collapsible > input.header {
display: block;
width: 100%;
padding: 0px;
margin: 0px;
border: 0px;
}
div.collapsible > input.header:hover {
background: rgba(32, 32, 32, 1.00);
}
div.collapsible > input.header > img {
margin: 2px 2px 0px 0px;
padding: 0px;
border: 0px;
width: 18px;
height: 18px;
}
div.collapsible > div.inside {
border: 1px rgba(64,64,64,0.5);
padding: 2px 2px 2px 8px;
margin: 1px 0px 0px 0px;
}
*#merge-by-distance button {
display: inline;
width: 50%;
}
*#snap-verts button {
display: inline;
width: 50%;
}
dialog#maindialog {
width: 175px;
min-width: 50px;
max-width: 500px;
padding: 0px;
}
dialog#maindialog * {
overflow-x: hidden;
}
dialog#tinydialog {
width: 100px;
min-width: 50px;
max-width: 500px;
padding: 0px;
}
dialog#tinydialog * {
overflow-x: hidden;
}
dialog#tinydialog > div#ttools > * {
display: inline;
/*background: white;*/
}
dialog#tinydialog input[type="radio"] > img {
display: none;
}
dialog#optionsdialog {
width: 250px;
min-width: 150px;
max-width: 500px;
}
dialog#helpsystem {
width: 60%;
left: 20%;
background: rgba(64,64,64,0.9);
}
dialog#helpsystem div#helpsystem-buttons {
margin: 1px;
width: 100%;
}
dialog#helpsystem div#helpsystem-buttons button {
display: inline;
width: 50%;
}
dialog.alertdialog {
width: 60%;
left: 20%;
}
dialog.alertdialog.note {
background-color: rgba(50, 50, 76, 0.95);
}
dialog.alertdialog.warning {
background-color: rgba(90, 64, 38, 0.95);
}
dialog.alertdialog.error {
background-color: rgba(76, 38, 38, 0.95);
}
dialog.alertdialog.assert, dialog.alertdialog.exception {
background-color: rgba(96, 0, 0, 0.95);
}
dialog.alertdialog div#alertdialog-buttons {
margin: 1px;
width: 100%;
}
dialog.alertdialog div#alertdialog-buttons button {
display: inline;
width: 50%;
}
dialog.alertdialog div#crashdetails pre {
white-space: pre;
}
div.issue-checker button.action {
display: inline;
margin: 0px;
width: 25%;
white-space: pre;
}