новая структура проекта
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user