новая структура проекта
This commit is contained in:
+34
@@ -0,0 +1,34 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class ActiveObjectOverride:
|
||||
"""
|
||||
Overrides context of selected / active object for operators.
|
||||
"""
|
||||
|
||||
__slots__ = ('_obj', '_context_override')
|
||||
_obj: bpy.types.Object
|
||||
|
||||
def __init__(self, obj: bpy.types.Object, context: bpy.types.Context | None = None):
|
||||
"""
|
||||
Initialize context override helper class.
|
||||
:param obj: Object to set as active for this override.
|
||||
:param context: Current context.
|
||||
"""
|
||||
self._obj = obj
|
||||
|
||||
if context is None:
|
||||
context = bpy.context
|
||||
|
||||
override = context.copy()
|
||||
override['active_object'] = obj
|
||||
override['selected_objects'] = [obj]
|
||||
|
||||
self._context_override = context.temp_override(**override)
|
||||
|
||||
def __enter__(self):
|
||||
self._context_override.__enter__()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self._context_override.__exit__(exc_type, exc_val, exc_tb)
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import re
|
||||
|
||||
|
||||
BL_ID_NAME_TEMPLATE = r'(.*)\.\d\d\d'
|
||||
""" Template of ID data block names in Blender. E.g. name.001 and alike. """
|
||||
|
||||
|
||||
def match_id_name(name: str, expected_name: str) -> bool:
|
||||
"""
|
||||
Matches the name on an ID data-block omitting the possible .xxx duplicate prefix.
|
||||
:param name: Name to match.
|
||||
:param expected_name: Expected base of ID data block name.
|
||||
:return: True if matches, else False.
|
||||
"""
|
||||
|
||||
if name == expected_name:
|
||||
return True
|
||||
|
||||
match = re.match(BL_ID_NAME_TEMPLATE, name)
|
||||
|
||||
if match is None or match.group(1) != expected_name:
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,157 @@
|
||||
import bpy
|
||||
from functools import partial
|
||||
from typing import Protocol
|
||||
from time import time
|
||||
|
||||
from ..third_party.boltons.funcutils import wraps
|
||||
from ..utils.misc import show_message_box
|
||||
from ..ui.locks import UIPropUpdateLock
|
||||
|
||||
|
||||
def parametrized(dec):
|
||||
def layer(*args, **kwargs):
|
||||
def repl(f):
|
||||
return dec(f, *args, **kwargs)
|
||||
return repl
|
||||
return layer
|
||||
|
||||
|
||||
@parametrized
|
||||
def delay_execution(func, delay_sec=1.0):
|
||||
|
||||
lock = False
|
||||
def timer(*args, **kwargs):
|
||||
nonlocal lock
|
||||
lock = False
|
||||
|
||||
func(*args, **kwargs)
|
||||
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
nonlocal lock
|
||||
|
||||
if not lock:
|
||||
lock = True
|
||||
bpy.app.timers.register(partial(timer, *args, **kwargs), first_interval=delay_sec)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
@parametrized
|
||||
def on_release(func, delay_sec=1.5):
|
||||
|
||||
exec_time = time()
|
||||
|
||||
def timer(*args, **kwargs):
|
||||
nonlocal exec_time
|
||||
if not abs(exec_time - time()) < delay_sec:
|
||||
func(*args, **kwargs)
|
||||
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
nonlocal exec_time
|
||||
exec_time = time()
|
||||
|
||||
bpy.app.timers.register(partial(timer, *args, **kwargs), first_interval=max(1.0, delay_sec))
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def no_recurse(func):
|
||||
"""
|
||||
Decorator to block the function's ability to indirectly call itself recursively.
|
||||
:param func: Any func.
|
||||
:return: Wrapped func.
|
||||
"""
|
||||
func.__no_recurse_lock = False
|
||||
|
||||
@wraps(func)
|
||||
def wrapped(*args, **kwargs):
|
||||
if func.__no_recurse_lock:
|
||||
return
|
||||
|
||||
func.__no_recurse_lock = True
|
||||
func(*args, **kwargs)
|
||||
func.__no_recurse_lock = False
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
class StringFilterProtocol(Protocol):
|
||||
"""
|
||||
Protocol the filters for string_property_validator must implement.
|
||||
"""
|
||||
def __call__(self, string: str) -> None | str: ...
|
||||
|
||||
|
||||
@no_recurse
|
||||
def string_property_validator(self: bpy.types.PropertyGroup
|
||||
, context: bpy.types.Context
|
||||
, *
|
||||
, name: str
|
||||
, str_filter: StringFilterProtocol
|
||||
, lockable: bool = True):
|
||||
|
||||
if lockable and UIPropUpdateLock().status:
|
||||
return
|
||||
|
||||
cur_value = getattr(self, name)
|
||||
|
||||
filtered = str_filter(cur_value)
|
||||
|
||||
if filtered is None:
|
||||
return
|
||||
|
||||
setattr(self, name, filtered)
|
||||
|
||||
|
||||
def string_filter_internal_dir(string: str) -> None | str:
|
||||
"""
|
||||
Validate and attempt to fix the string that is supposed to represent internal WoW dir.
|
||||
:param string: Any string.
|
||||
:return: Fixed string or None if string was valid.
|
||||
"""
|
||||
|
||||
if not string:
|
||||
return None
|
||||
|
||||
slashes_fixed = False
|
||||
if '/' in string:
|
||||
string = string.replace('/', '\\')
|
||||
slashes_fixed = True
|
||||
|
||||
if any(char == '.' for char in string):
|
||||
show_message_box("Path must not contain '.' characters.", "Error", 'ERROR')
|
||||
return ""
|
||||
|
||||
if any(char == ':' for char in string):
|
||||
print("Path must not contain ':' characters. Path was probably added on import from a full disk filepath.")
|
||||
return ""
|
||||
|
||||
# TODO: some characters like \ / : * ? " < > | are not valid path characters in windows
|
||||
if any(char in ('*', '?', '"', '<', '>', '|') for char in string):
|
||||
show_message_box("""Path must not contain * ? " < > | characters.""", "Error", 'ERROR')
|
||||
return ""
|
||||
|
||||
try:
|
||||
string.encode('ascii')
|
||||
except UnicodeError:
|
||||
show_message_box("Path must only use latin characters.", "Error", 'ERROR')
|
||||
return ""
|
||||
|
||||
backslash_count = 0
|
||||
for char in string:
|
||||
if char == '\\':
|
||||
backslash_count += 1
|
||||
else:
|
||||
backslash_count = 0
|
||||
|
||||
if backslash_count > 1:
|
||||
show_message_box("Invalid path (multiple consecutive path separators)", "Error", 'ERROR')
|
||||
return ""
|
||||
|
||||
if slashes_fixed:
|
||||
return string
|
||||
|
||||
|
||||
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
from .custom_object import CustomObject
|
||||
from .bl_id_types_utils import match_id_name, BL_ID_NAME_TEMPLATE
|
||||
from ..ui.message_stack import MessageStack
|
||||
|
||||
import bpy
|
||||
import re
|
||||
import os
|
||||
|
||||
from typing import Set, Sequence, Type
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_collection(model_collection: bpy.types.Collection
|
||||
, col_name: str) -> bpy.types.Collection | None:
|
||||
"""
|
||||
Get child collection matching provided name excluding Blender's copy index (.xxx).
|
||||
:param model_collection: Collection to search in.
|
||||
:param col_name: Name of child collection.
|
||||
:return: Collection if found, else None.
|
||||
"""
|
||||
|
||||
# check if parent collection exists
|
||||
if not model_collection:
|
||||
raise KeyError("get_collection() called with Null WoW Model Collection.")
|
||||
|
||||
# attempt regular search
|
||||
if (col := model_collection.children.get(col_name)) is not None:
|
||||
return col
|
||||
|
||||
# attempt regex search
|
||||
for col in model_collection.children:
|
||||
match = re.match(BL_ID_NAME_TEMPLATE, col.name)
|
||||
|
||||
if match is None or match.group(1) != col_name:
|
||||
continue
|
||||
|
||||
return col
|
||||
|
||||
|
||||
def get_or_create_collection(model_collection: bpy.types.Collection
|
||||
, col_name: str) -> bpy.types.Collection:
|
||||
"""
|
||||
Get child collection matching provided name excluding Blender's copy index (.xxx) or create it.
|
||||
:param model_collection: Collection to search in. ('wow_wmo', 'wow_m2'...)
|
||||
:param col_name: Name of child collection.
|
||||
:return: Requested collection.
|
||||
"""
|
||||
# check if model_collection exists
|
||||
if not model_collection:
|
||||
# print("Error : WoW Model collection doesn't exist.")
|
||||
raise KeyError("get_or_create_collection() called with Null WoW Model Collection.")
|
||||
# can't create it because we don't have the type (wmo/m2/adt)
|
||||
# This shouldn't fail if it is called with model_collection = get_current_wow_model_collection() if there is an active collection.
|
||||
|
||||
col = get_collection(model_collection, col_name)
|
||||
|
||||
if not col:
|
||||
col = bpy.data.collections.new(col_name)
|
||||
model_collection.children.link(col)
|
||||
|
||||
col.color_tag = 'COLOR_05'
|
||||
|
||||
return col
|
||||
|
||||
|
||||
def obj_swap_collections(obj: bpy.types.Object, col_from: bpy.types.Collection, col_to: bpy.types.Collection):
|
||||
"""
|
||||
Safely unlink an object from one collection and link to another.
|
||||
:param obj: Object to swap collection.
|
||||
:param col_from: Collection from which to unlink.
|
||||
:param col_to: Collection to which to link.
|
||||
"""
|
||||
try:
|
||||
col_from.objects.unlink(obj)
|
||||
except RuntimeError as e:
|
||||
print(f"Error: exception occured while swapping object \"{obj.name}\" from collection \"{col_from.name}\" to"
|
||||
"collection \"{col_to.name}\".\n{e}")
|
||||
try:
|
||||
col_to.objects.link(obj)
|
||||
except RuntimeError as e:
|
||||
print(f"Error: exception occured while swapping object \"{obj.name}\" from collection \"{col_from.name}\" to"
|
||||
"collection \"{col_to.name}\".\n{e}")
|
||||
|
||||
|
||||
def collection_swap_parent_collection(col: bpy.types.Collection
|
||||
, col_from: bpy.types.Collection
|
||||
, col_to: bpy.types.Collection):
|
||||
"""
|
||||
Safely unlink a collection from one collection, and link to another.
|
||||
:param col: Collection to swap.
|
||||
:param col_from: Collection from which to unlink.
|
||||
:param col_to: Collection to which to link.
|
||||
"""
|
||||
|
||||
try:
|
||||
col_from.children.unlink(col)
|
||||
except RuntimeError:
|
||||
print(f"Error: exception occured while swapping parent of collection \"{col.name}\" from collection"
|
||||
"\"{col_from.name}\" to collection \"{col_to.name}\".\n{e}")
|
||||
|
||||
try:
|
||||
col_to.children.link(col)
|
||||
except RuntimeError:
|
||||
print(f"Error: exception occured while swapping parent of collection \"{col.name}\" from collection"
|
||||
"\"{col_from.name}\" to collection \"{col_to.name}\".\n{e}")
|
||||
|
||||
|
||||
def unlink_nested_collections(parent_col: bpy.types.Collection
|
||||
, col: bpy.types.Collection) -> bool:
|
||||
"""
|
||||
Unlinks all nested collections from col to wmo_col.
|
||||
:param parent_col: WMO collection to unlink irrelevant ones into.
|
||||
:param col: Collection to sanitize.
|
||||
:return True if had nested collections, else False.
|
||||
"""
|
||||
|
||||
has_children = bool(len(col.children))
|
||||
|
||||
for child_col in col.children:
|
||||
collection_swap_parent_collection(child_col, col, parent_col)
|
||||
|
||||
return has_children
|
||||
|
||||
|
||||
def create_wmo_model_collection(scene: bpy.types.Scene,
|
||||
filepath: str, wowpath: str)-> bpy.types.Collection:
|
||||
# col = bpy.data.collections.new(os.path.basename(wowpath)) # filename (+ extension?)
|
||||
filename = ''
|
||||
if wowpath:
|
||||
filename = Path(wowpath).stem
|
||||
else:
|
||||
filename = Path(filepath).stem
|
||||
|
||||
col = bpy.data.collections.new(filename) # filename only without extension
|
||||
col.wow_wmo.enabled = True
|
||||
|
||||
def filepath_wmo(filepath):
|
||||
filepath = filepath.lower()
|
||||
filepath = Path(filepath)
|
||||
filepath_parts = filepath.parts
|
||||
|
||||
try:
|
||||
world_index = filepath_parts.index('world')
|
||||
except ValueError:
|
||||
return 'World directory not found in the filepath'
|
||||
|
||||
return str(Path(*filepath_parts[world_index:-1]))
|
||||
|
||||
col.wow_wmo.dir_path = filepath_wmo(filepath)
|
||||
|
||||
scene.collection.children.link(col)
|
||||
# set the collection as active
|
||||
layer_collection = bpy.context.view_layer.layer_collection.children[col.name]
|
||||
bpy.context.view_layer.active_layer_collection = layer_collection
|
||||
|
||||
print("Created new WMO model Collection:" + col.name)
|
||||
return col
|
||||
|
||||
""""
|
||||
def create_wow_model_collection(scene: bpy.types.Scene
|
||||
, id_prop: str) -> bpy.types.Collection | None:
|
||||
Create a WoW model collection.
|
||||
:param scene: Current scene.
|
||||
:param id_prop: Identifier of the collection property for the given model type.
|
||||
:return: Model (M2/WMO/ADT) collection
|
||||
|
||||
col = bpy.data.collections.new(id_prop) # just name it 'wow_wmo' ?
|
||||
getattr(col, id_prop).enabled = True
|
||||
|
||||
# scene.collection = col
|
||||
bpy.context.scene.collection.children.link(col)
|
||||
layer_collection = bpy.context.view_layer.layer_collection.children[col.name]
|
||||
bpy.context.view_layer.active_layer_collection = layer_collection
|
||||
|
||||
print("Created new WoW model Collection:" + col.name)
|
||||
return col
|
||||
"""
|
||||
|
||||
def get_current_wow_model_collection(scene: bpy.types.Scene
|
||||
, id_prop: str) -> bpy.types.Collection | None:
|
||||
"""
|
||||
Gets currently active model collection.
|
||||
:param scene: Current scene.
|
||||
:param id_prop: Identifier of the collection property for the given model type.
|
||||
:return: Model (M2/WMO/ADT) collection if found, else None.
|
||||
"""
|
||||
|
||||
# check if there is an active collection at all
|
||||
# don't print or it will spam when using it in panels
|
||||
if not bpy.context.collection:
|
||||
# print("Error: Couldn't find current WoW model collection: there is no active collection at all.")
|
||||
return None
|
||||
# return create_wow_model_collection(scene, id_prop)
|
||||
|
||||
act_col: bpy.types.Collection = bpy.context.collection
|
||||
# print("active collection : " + act_col.name)
|
||||
|
||||
# check if the collection is a model collection itself
|
||||
if act_col.name in scene.collection.children:
|
||||
if getattr(act_col, id_prop).enabled:
|
||||
return act_col
|
||||
# print("Error: Couldn't find current WoW model collection: Active collection is not enabled to be a WoW "
|
||||
# "Collection.")
|
||||
return None
|
||||
# return create_wow_model_collection(scene, id_prop)
|
||||
|
||||
# check if the collection is a child of some existing collections.
|
||||
for col in scene.collection.children:
|
||||
# double check if this fix is right and I udnerstood it correctly
|
||||
if getattr(col, id_prop).enabled and act_col in col.children_recursive:
|
||||
return col
|
||||
|
||||
# print("Error : Failed to find a WoW Model Collection.")
|
||||
return None
|
||||
# return create_wow_model_collection(scene, id_prop)
|
||||
|
||||
|
||||
class SpecialCollection:
|
||||
""" Defines rules for handling updates to special collections. """
|
||||
|
||||
__wbs_collection_name__: str
|
||||
""" Name of the special collection. """
|
||||
|
||||
__wbs_root_collection_id_prop_name__: str
|
||||
""" Name of the property group of a root collection. E.g. wow_wmo or wow_m2"""
|
||||
|
||||
__wbs_bl_object_types__: Set[str] = set()
|
||||
""" Set of Blender object types allowed to be part of the collection.
|
||||
(Used when no custom object type is defined.
|
||||
"""
|
||||
|
||||
__wbs_custom_object_types__: Set[Type[CustomObject]] | None = None
|
||||
""" Optional set of custom object types restricted to the collection. """
|
||||
|
||||
_required = { '__wbs_collection_name__', '__wbs_root_collection_id_prop_name__'}
|
||||
""" Required fields to override. """
|
||||
|
||||
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 _get_root_collection(cls
|
||||
, scene: bpy.types.Scene
|
||||
, col: bpy.types.Collection) -> bpy.types.Collection | None:
|
||||
"""
|
||||
Gets parent WoW collection (recursive) if existing, or None.
|
||||
:param scene: Current scene.
|
||||
:param col: Collection to search the parent for.
|
||||
:return: WMO parent collection or None if not found.
|
||||
"""
|
||||
|
||||
for scene_col in scene.collection.children:
|
||||
if not getattr(scene_col, cls.__wbs_root_collection_id_prop_name__).enabled:
|
||||
continue
|
||||
|
||||
for child_col in scene_col.children_recursive:
|
||||
if child_col == col:
|
||||
return scene_col
|
||||
|
||||
@classmethod
|
||||
def verify_root_collection_integrity(cls
|
||||
, root_col: bpy.types.Collection
|
||||
, special_collection_ts: Sequence[Type['SpecialCollection']]):
|
||||
"""
|
||||
Verifies presence of all special collections within a model collection.
|
||||
:param root_col: Root model collection.
|
||||
:param special_collection_ts: Special collections to ensure.
|
||||
"""
|
||||
|
||||
n_special_cols = len(special_collection_ts)
|
||||
status = [False] * n_special_cols
|
||||
status_counter = 0
|
||||
|
||||
# search for matching collections
|
||||
for child_col in root_col.children:
|
||||
for i, special_col_t in enumerate(special_collection_ts):
|
||||
if match_id_name(child_col.name, special_col_t.__wbs_collection_name__):
|
||||
status[i] = True
|
||||
status_counter += 1
|
||||
break
|
||||
|
||||
if status_counter == n_special_cols:
|
||||
break
|
||||
|
||||
# check if we found everything
|
||||
if status_counter == n_special_cols:
|
||||
return
|
||||
|
||||
for i, s in enumerate(status):
|
||||
if not s:
|
||||
special_collection_ts[i].create_collection(root_col)
|
||||
|
||||
@classmethod
|
||||
def handle_collection_if_matched(cls
|
||||
, scene: bpy.types.Scene
|
||||
, update: bpy.types.DepsgraphUpdate
|
||||
, special_collection_ts: Sequence[Type['SpecialCollection']]) -> bool:
|
||||
"""
|
||||
Handle update to collection of matches the provided depsgraph update.
|
||||
:param scene: Current scene.
|
||||
:param update: Depsgraph update.
|
||||
:param special_collection_ts: Available collection types.
|
||||
:return: True if handled, else False.
|
||||
"""
|
||||
collection: bpy.types.Collection = update.id.original
|
||||
|
||||
# test if collection matches the criteria of a special collection
|
||||
if not match_id_name(collection.name, cls.__wbs_collection_name__):
|
||||
return False
|
||||
|
||||
# test if collection is within a root collection
|
||||
root_col = cls._get_root_collection(scene, collection)
|
||||
|
||||
if root_col is None:
|
||||
return False
|
||||
|
||||
cls.verify_root_collection_integrity(root_col, special_collection_ts)
|
||||
|
||||
for obj in collection.objects:
|
||||
# handle custom object types
|
||||
if cls.__wbs_custom_object_types__ is not None:
|
||||
if not any(c_obj.match(obj) for c_obj in cls.__wbs_custom_object_types__):
|
||||
obj_swap_collections(obj, collection, root_col)
|
||||
MessageStack().push_message(msg=f'Object "{obj.name}" removed from special collection '
|
||||
f'"{cls.__wbs_collection_name__}" due to not matching '
|
||||
f'with required custom object types.', icon='ERROR')
|
||||
|
||||
# handle normal object types
|
||||
elif obj.type not in cls.__wbs_bl_object_types__:
|
||||
obj_swap_collections(obj, collection, root_col)
|
||||
MessageStack().push_message(msg=f'Object "{obj.name}" removed from special collection '
|
||||
f'"{cls.__wbs_collection_name__}". Objects of type \'{obj.type}\' '
|
||||
f'are not allowed.', icon='ERROR')
|
||||
|
||||
# handle nested collections
|
||||
if unlink_nested_collections(root_col, collection):
|
||||
MessageStack().push_message(msg=f'This collection cannot have nested collections.', icon='ERROR')
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def create_collection(cls
|
||||
, parent_col: bpy.types.Collection):
|
||||
"""
|
||||
Creates a special collection in a parent collection.
|
||||
:param parent_col: Parent collection.
|
||||
"""
|
||||
col = bpy.data.collections.new(name=cls.__wbs_collection_name__)
|
||||
col.color_tag = 'COLOR_02'
|
||||
parent_col.children.link(col)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
def linear_to_srgb(c: float) -> float:
|
||||
a = .055
|
||||
if c <= .0031308:
|
||||
return 12.92 * c
|
||||
else:
|
||||
return (1+a) * c ** (1/2.4) - a
|
||||
|
||||
|
||||
def srgb_to_linear(c: float) -> float:
|
||||
a = .055
|
||||
if c <= .04045:
|
||||
return c / 12.92
|
||||
else:
|
||||
return ((c+a) / (1+a)) ** 2.4
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
from ..utils.bl_context_utils import ActiveObjectOverride
|
||||
from ..ui.message_stack import MessageStack
|
||||
|
||||
import bpy
|
||||
import inspect
|
||||
import importlib
|
||||
import types
|
||||
|
||||
from typing import Set, Iterable, Dict, Callable, Type
|
||||
from types import FunctionType
|
||||
from functools import wraps
|
||||
|
||||
|
||||
class CustomObject:
|
||||
__wbs_bl_object_type__: str
|
||||
""" Type of Blender bpy.types.Object. """
|
||||
|
||||
__wbs_prop_group_id__: str
|
||||
""" String name of property group in the object to control data. """
|
||||
|
||||
__wbs_allowed_modes__: Set[str] = {}
|
||||
""" Blender modes allowed for this object. 'OBJECT' mode is implicitly always allowed. """
|
||||
|
||||
__wbs_allow_scale__: bool = True
|
||||
""" Allow scaling of object. """
|
||||
|
||||
__wbs_allow_non_uniform_scale__: bool = True
|
||||
""" Allow non-uniform scaling of object. """
|
||||
|
||||
__wbs_allow_rotation__: bool = True
|
||||
""" Allow rotating the object. """
|
||||
|
||||
__wbs_allow_modifiers__: bool = True
|
||||
""" Allow modifiers to be used on the object. """
|
||||
|
||||
__wbs_allow_particles__: bool = True
|
||||
""" Allow particle systems to be used on the object. """
|
||||
|
||||
__wbs_allow_physics__: bool = True
|
||||
""" Allow physics to influence the object. """
|
||||
|
||||
__wbs_allow_constraints__: bool = True
|
||||
""" Allow particle constraints to be used on the object. """
|
||||
|
||||
__wbs_allow_mesh_properties__: bool = True
|
||||
""" Allow mesh properties to be available through this object. """
|
||||
|
||||
__wbs_allow_material_properties__: bool = True
|
||||
""" Allow material properties to be available through this object. """
|
||||
|
||||
__wbs_banned_ops__: Iterable[str] = {}
|
||||
""" List of banned operators, that lead to post_banned_op() function fired. By default removes an object."""
|
||||
|
||||
__wbs_on_mode_handlers__: Dict[str, Callable[[bpy.types.DepsgraphUpdate], None]] = {}
|
||||
""" Dict of per-mode handlers run in specific modes. """
|
||||
|
||||
_required = {'__wbs_bl_object_type__', '__wbs_prop_group_id__'}
|
||||
""" 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 match(cls, obj: bpy.types.Object) -> bool:
|
||||
"""
|
||||
Checks if a Blender object satisfies the criteria of a custom object type.
|
||||
:param obj: Blender object.
|
||||
:return: True if matched, else False.
|
||||
"""
|
||||
|
||||
# check Blender object type validity
|
||||
if cls.__wbs_bl_object_type__ != obj.type:
|
||||
return False
|
||||
|
||||
# check if special property group is enabled
|
||||
try:
|
||||
if not getattr(obj, cls.__wbs_prop_group_id__).enabled:
|
||||
return False
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# check if any other special property is enabled
|
||||
for subclass in CustomObject.__subclasses__():
|
||||
if subclass is cls:
|
||||
continue
|
||||
|
||||
try:
|
||||
if getattr(obj, subclass.__wbs_prop_group_id__).enabled:
|
||||
return False
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def handle_object_if_matched(cls
|
||||
, update: bpy.types.DepsgraphUpdate) -> bool:
|
||||
"""
|
||||
Handles the custom object update checks.
|
||||
:param update: Depsgraph update for this object.
|
||||
:return: True if handled, else False.
|
||||
"""
|
||||
obj: bpy.types.Object = update.id.original
|
||||
|
||||
# skip non-matching object
|
||||
if not cls.match(obj):
|
||||
return False
|
||||
|
||||
cls.on_each_update(update)
|
||||
|
||||
# check if any of the banned operators ran
|
||||
if bpy.context.active_operator is not None and bpy.context.active_operator.bl_idname in cls.__wbs_banned_ops__:
|
||||
cls.on_banned_operator_use(update, bpy.context.active_operator.bl_idname)
|
||||
return True
|
||||
|
||||
# make sure we are not in a prohibited mode
|
||||
if obj.mode != 'OBJECT' and obj.mode not in cls.__wbs_allowed_modes__:
|
||||
with ActiveObjectOverride(obj):
|
||||
MessageStack().push_message(msg=f'Object "{obj.name}" of custom type "{cls.__name__}" '
|
||||
f'cannot use mode \'{obj.mode}\'', icon='ERROR')
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
cls.on_mode_change_failure(update, obj.mode)
|
||||
|
||||
# check validity of transform operations
|
||||
if update.is_updated_transform:
|
||||
# handle scale
|
||||
if not cls.__wbs_allow_scale__:
|
||||
obj.scale = (1, 1, 1)
|
||||
elif not cls.__wbs_allow_non_uniform_scale__:
|
||||
scale = (obj.scale[0] + obj.scale[1] + obj.scale[2]) / 3
|
||||
obj.scale = (scale, scale, scale)
|
||||
|
||||
# handle rotation
|
||||
if not cls.__wbs_allow_rotation__:
|
||||
rot_mode = obj.rotation_mode
|
||||
obj.rotation_mode = 'XYZ'
|
||||
update.id.original.rotation_euler = (0, 0, 0)
|
||||
obj.rotation_mode = rot_mode
|
||||
|
||||
# check if modifiers need cleaning
|
||||
if not cls.__wbs_allow_modifiers__ and len(obj.modifiers):
|
||||
obj.modifiers.clear()
|
||||
MessageStack().push_message(msg=f'Object "{obj.name}" of custom type "{cls.__name__}" '
|
||||
f'cannot use modifiers.', icon='ERROR')
|
||||
|
||||
# check if particles need cleaning
|
||||
if not cls.__wbs_allow_particles__ and len(obj.particle_systems):
|
||||
obj.particle_systems.clear()
|
||||
MessageStack().push_message(msg=f'Object "{obj.name}" of custom type "{cls.__name__}" '
|
||||
f'cannot use particle systems.', icon='ERROR')
|
||||
|
||||
# check if constraints need cleaning
|
||||
if not cls.__wbs_allow_constraints__ and len(obj.constraints):
|
||||
obj.constraints.clear()
|
||||
MessageStack().push_message(msg=f'Object "{obj.name}" of custom type "{cls.__name__}" '
|
||||
f'cannot use constraints.', icon='ERROR')
|
||||
|
||||
# execute mode handlers
|
||||
mode_handler = cls.__wbs_on_mode_handlers__.get(update.id.original.mode)
|
||||
|
||||
if mode_handler is not None:
|
||||
mode_handler(update)
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def on_mode_change_failure(cls, update: bpy.types.DepsgraphUpdate, attempted_mode: str, ):
|
||||
"""
|
||||
Called on attempt to enter a prohibited Blender mode. Override this to support handling this.
|
||||
:param update: Current update.
|
||||
:param attempted_mode: Attempted mode identifier.
|
||||
"""
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def on_banned_operator_use(cls, update: bpy.types.DepsgraphUpdate, bl_op_id_name: str):
|
||||
"""
|
||||
Called after the use of a banned operator. Deletes an object by default and displays an error message.
|
||||
Override to support custom behavior.
|
||||
:param update: Current update.
|
||||
:param bl_op_id_name: Name of the executed / invoked operator.
|
||||
"""
|
||||
bpy.data.objects.remove(update.id.original, do_unlink=True)
|
||||
|
||||
op_class = getattr(bpy.types, bl_op_id_name)
|
||||
MessageStack().push_message(msg=f'Object "{update.id.original.name}" of custom type "{cls.__name__}" '
|
||||
f'was removed due to the use of prohibited action "{op_class.bl_lavel}" '
|
||||
f'({op_class.bl_idname}). Use undo to cancel.', icon='ERROR')
|
||||
|
||||
@classmethod
|
||||
def on_each_update(cls, update: bpy.types.DepsgraphUpdate) -> bool:
|
||||
"""
|
||||
Called on each update. Override for custom behavior.
|
||||
:param update: Current update.
|
||||
:return If returns True, handle_object_if_matched() proceeds to other checks, else stops after this step.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
class CustomObjectInterfaceHandler:
|
||||
""" This class handles patching of Blender's bpy.types.Panel subclasses to support custom context-aware polls. """
|
||||
|
||||
_altered_panels: Dict[Type[bpy.types.Panel], Callable[[Type[bpy.types.Panel], bpy.types.Context], None]] = {}
|
||||
""" Panel classes and their original poll methods. """
|
||||
|
||||
@staticmethod
|
||||
def copy_func(f: FunctionType, name=None):
|
||||
"""
|
||||
Copies a function.
|
||||
:param f: Function to copy.
|
||||
:param name: Custom name for copied function.
|
||||
:return: a function with same code, globals, defaults, closure, and
|
||||
name (or provide a new name).
|
||||
"""
|
||||
fn = types.FunctionType(f.__code__, f.__globals__, name or f.__name__,
|
||||
f.__defaults__, f.__closure__)
|
||||
# in case f was given attrs (note this dict is a shallow copy):
|
||||
fn.__dict__.update(f.__dict__)
|
||||
return fn
|
||||
|
||||
@classmethod
|
||||
def register(cls):
|
||||
""" Installs poll() hooks to matching panels. """
|
||||
|
||||
for typename in dir(bpy.types):
|
||||
|
||||
# skip this addon panels
|
||||
if typename.startswith('WMO') or typename.startswith('M2'):
|
||||
continue
|
||||
|
||||
try:
|
||||
bpy_type_desc = getattr(bpy.types, typename)
|
||||
|
||||
if not inspect.isclass(bpy_type_desc) \
|
||||
or not issubclass(bpy_type_desc, bpy.types.Panel) \
|
||||
or not (not hasattr(bpy_type_desc, 'bl_parent_id') or not bpy_type_desc.bl_parent_id) \
|
||||
or bpy_type_desc.bl_context not in {'modifier'
|
||||
, 'physics'
|
||||
, 'particle'
|
||||
, 'constraint'
|
||||
, 'data'
|
||||
, 'material'}:
|
||||
continue
|
||||
|
||||
bpy_module = importlib.import_module(getattr(bpy.types, typename).__module__)
|
||||
bpy_type = getattr(bpy_module, typename)
|
||||
except RuntimeError:
|
||||
continue
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
cls._altered_panels[bpy_type] = cls.copy_func(bpy_type.poll.__func__, name='poll')
|
||||
|
||||
for panel, panel_poll in cls._altered_panels.items():
|
||||
|
||||
@wraps(panel_poll)
|
||||
def poll_override(cls, context):
|
||||
try:
|
||||
if not context.object:
|
||||
return CustomObjectInterfaceHandler._altered_panels.get(cls)(cls, context)
|
||||
|
||||
for custom_obj in CustomObject.__subclasses__():
|
||||
bl_context = cls.bl_context
|
||||
if custom_obj.match(context.object):
|
||||
if (not custom_obj.__wbs_allow_modifiers__ and bl_context == 'modifier') \
|
||||
or (not custom_obj.__wbs_allow_physics__ and bl_context == 'physics') \
|
||||
or (not custom_obj.__wbs_allow_particles__ and bl_context == 'particle') \
|
||||
or (not custom_obj.__wbs_allow_constraints__ and bl_context == 'constraint') \
|
||||
or (not custom_obj.__wbs_allow_mesh_properties__ and bl_context == 'data') \
|
||||
or (not custom_obj.__wbs_allow_material_properties__ and bl_context == 'material'):
|
||||
return False
|
||||
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return CustomObjectInterfaceHandler._altered_panels.get(cls)(cls, context)
|
||||
|
||||
panel.poll = classmethod(poll_override)
|
||||
|
||||
@classmethod
|
||||
def unregister(cls):
|
||||
""" Uninstalls poll() hooks. """
|
||||
|
||||
for panel, orig_panel_poll in cls._altered_panels.items():
|
||||
panel.poll = classmethod(cls.copy_func(orig_panel_poll, name='poll'))
|
||||
|
||||
cls._altered_panels.clear()
|
||||
|
||||
|
||||
|
||||
def register():
|
||||
CustomObjectInterfaceHandler.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
CustomObjectInterfaceHandler.unregister()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
import bpy
|
||||
import os
|
||||
import sys
|
||||
|
||||
from mathutils import Vector
|
||||
from collections import namedtuple
|
||||
|
||||
from ..pywowlib import WoWVersionManager
|
||||
from ..pywowlib.archives.wow_filesystem import WoWFileData
|
||||
from .. import PACKAGE_NAME
|
||||
from ..ui.preferences import get_project_preferences
|
||||
|
||||
|
||||
SequenceRecord = namedtuple('SequenceRecord', ['name', 'value', 'index'])
|
||||
|
||||
|
||||
class Sequence(type):
|
||||
|
||||
def __new__(mcs, name, bases, dct):
|
||||
|
||||
dct['__fields__'] = list(dct.keys())[2:]
|
||||
dct['_iter'] = 0
|
||||
|
||||
return super().__new__(mcs, name, bases, dct)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, self.__fields__[item])
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
|
||||
if self._iter == len(self.__fields__):
|
||||
self._iter = 0
|
||||
raise StopIteration
|
||||
|
||||
item = SequenceRecord(self.__fields__[self._iter], getattr(self, self.__fields__[self._iter]), self._iter)
|
||||
self._iter += 1
|
||||
return item
|
||||
|
||||
def index(self, item):
|
||||
return self.__fields__.index(item)
|
||||
|
||||
def singleton(class_):
|
||||
instances = {}
|
||||
|
||||
def getinstance(*args, **kwargs):
|
||||
if class_ not in instances:
|
||||
instances[class_] = class_(*args, **kwargs)
|
||||
return instances[class_]
|
||||
return getinstance
|
||||
|
||||
|
||||
def find_nearest_object(obj_, objects):
|
||||
"""Get closest object to another object"""
|
||||
|
||||
dist = sys.float_info.max
|
||||
result = None
|
||||
|
||||
for obj in objects:
|
||||
obj_location_relative = obj.matrix_world.inverted() @ obj_.location
|
||||
hit = obj.closest_point_on_mesh(obj_location_relative)
|
||||
hit_dist = (obj_location_relative - hit[1]).length
|
||||
if hit_dist < dist:
|
||||
dist = hit_dist
|
||||
result = obj
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parse_bitfield(bitfield, last_flag=0x1000):
|
||||
|
||||
flags = set()
|
||||
bit = 1
|
||||
while bit <= last_flag:
|
||||
if bitfield & bit:
|
||||
flags.add(str(bit))
|
||||
bit <<= 1
|
||||
|
||||
return flags
|
||||
|
||||
|
||||
def construct_bitfield(flag_set):
|
||||
|
||||
bitfield = 0
|
||||
|
||||
for flag in flag_set:
|
||||
bitfield |= int(flag)
|
||||
|
||||
return bitfield
|
||||
|
||||
|
||||
def get_material_viewport_image(material):
|
||||
""" Get viewport image assigned to a material """
|
||||
for i in range(3):
|
||||
try:
|
||||
img = material.texture_slots[3 - i].texture.image
|
||||
return img
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def load_game_data() -> WoWFileData:
|
||||
|
||||
WoWVersionManager().set_client_version(int(bpy.context.scene.wow_scene.version))
|
||||
|
||||
if not hasattr(bpy, 'wow_game_data'):
|
||||
project_preferences = get_project_preferences()
|
||||
bpy.wow_game_data = WoWFileData(project_preferences.wow_path, project_preferences.project_dir_path)
|
||||
|
||||
if not bpy.wow_game_data.files:
|
||||
raise UserWarning("WoW game data is not loaded. Check settings.")
|
||||
|
||||
return bpy.wow_game_data
|
||||
|
||||
def custom_relpath(path, start):
|
||||
if path.lower().startswith(start.lower()):
|
||||
return path[len(start):].lstrip('\\')
|
||||
else:
|
||||
return resolve_outside_texture_path(path)
|
||||
|
||||
def resolve_texture_path(filepath: str) -> str:
|
||||
filepath = os.path.splitext(bpy.path.abspath(filepath))[0] + ".blp"
|
||||
prefs = get_project_preferences()
|
||||
|
||||
# TODO: project folder
|
||||
rel_path = custom_relpath(filepath, prefs.cache_dir_path)
|
||||
test_path = os.path.join(prefs.cache_dir_path, rel_path)
|
||||
if os.path.exists(test_path) and os.path.isfile(test_path):
|
||||
return rel_path.replace('/', '\\')
|
||||
|
||||
game_data = load_game_data()
|
||||
|
||||
path = (filepath, "")
|
||||
rest_path = ""
|
||||
|
||||
while True:
|
||||
path = os.path.split(path[0])
|
||||
|
||||
if not path[1]:
|
||||
print("\nTexture \"{}\" not found.".format(path))
|
||||
break
|
||||
|
||||
rest_path = os.path.join(path[1], rest_path)
|
||||
rest_path = rest_path[:-1] if rest_path.endswith('\\') else rest_path
|
||||
|
||||
if os.name != 'nt':
|
||||
rest_path_n = rest_path.replace('/', '\\')
|
||||
else:
|
||||
rest_path_n = rest_path
|
||||
|
||||
rest_path_n = rest_path_n[:-1] if rest_path_n.endswith('\\') else rest_path_n
|
||||
|
||||
if game_data.has_file(rest_path_n)[0]:
|
||||
return rest_path_n
|
||||
|
||||
def resolve_outside_texture_path(filepath: str) -> str:
|
||||
keywords = ["world\\", "dungeon\\", "creature\\", "interface\\", "item\\", "models\\", "spells\\", "textures\\", "tileset\\", "xtextures\\"]
|
||||
lowercase_filepath = filepath.lower()
|
||||
|
||||
for keyword in keywords:
|
||||
if keyword in lowercase_filepath:
|
||||
index = lowercase_filepath.rfind(keyword)
|
||||
extracted_path = lowercase_filepath[index:]
|
||||
extracted_path = os.path.splitext(extracted_path)[0] + ".blp"
|
||||
normalized_path = os.path.normpath(extracted_path)
|
||||
return normalized_path
|
||||
|
||||
lowercase_filepath = os.path.splitext(bpy.path.abspath(lowercase_filepath))[0] + ".blp"
|
||||
return lowercase_filepath
|
||||
|
||||
def resolve_outside_model_path(filepath: str) -> str:
|
||||
keywords = ["world\\", "dungeon\\", "creature\\", "interface\\", "item\\", "models\\", "spells\\", "textures\\", "tileset\\", "xtextures\\"]
|
||||
lowercase_filepath = filepath.lower()
|
||||
|
||||
for keyword in keywords:
|
||||
if keyword in lowercase_filepath:
|
||||
index = lowercase_filepath.rfind(keyword)
|
||||
extracted_path = lowercase_filepath[index:]
|
||||
extracted_path = os.path.splitext(extracted_path)[0] + ".m2"
|
||||
normalized_path = os.path.normpath(extracted_path)
|
||||
return normalized_path
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_origin_position():
|
||||
loc = bpy.context.scene.cursor.location
|
||||
|
||||
origin_loc = None
|
||||
for area in bpy.context.screen.areas:
|
||||
if area.type == 'VIEW_3D':
|
||||
ctx = bpy.context.copy()
|
||||
ctx['area'] = area
|
||||
ctx['region'] = area.regions[-1]
|
||||
bpy.ops.view3d.snap_cursor_to_selected(ctx)
|
||||
origin_loc = bpy.context.scene.cursor.location
|
||||
|
||||
bpy.context.scene.cursor.location = loc
|
||||
|
||||
return origin_loc
|
||||
|
||||
|
||||
def get_obj_boundbox_center(obj):
|
||||
return obj.matrix_world @ (0.125 * sum((Vector(b) for b in obj.bound_box), Vector()))
|
||||
|
||||
|
||||
def get_obj_radius(obj, bb_center):
|
||||
mesh = obj.data
|
||||
radius = 0.0
|
||||
for vertex in mesh.vertices:
|
||||
dist = (vertex.co - bb_center).length
|
||||
if dist > radius:
|
||||
radius = dist
|
||||
|
||||
return radius
|
||||
|
||||
|
||||
def get_obj_boundbox_world(obj):
|
||||
return tuple(obj.matrix_world @ Vector(obj.bound_box[0])), tuple(obj.matrix_world @ Vector(obj.bound_box[6]))
|
||||
|
||||
|
||||
def get_objs_boundbox_world(objects):
|
||||
corner1 = [32768, 32768, 32768]
|
||||
corner2 = [-32768, -32768, -32768]
|
||||
|
||||
for obj in objects:
|
||||
obj_bb_corner1, obj_bb_corner2 = get_obj_boundbox_world(obj)
|
||||
|
||||
for i, value in enumerate(obj_bb_corner1):
|
||||
if value < corner1[i]:
|
||||
corner1[i] = value
|
||||
|
||||
for i, value in enumerate(obj_bb_corner2):
|
||||
if value > corner2[i]:
|
||||
corner2[i] = value
|
||||
|
||||
return tuple(corner1), tuple(corner2)
|
||||
|
||||
|
||||
def simplify_numbers(num):
|
||||
num = float('{:.3g}'.format(num))
|
||||
magnitude = 0
|
||||
while abs(num) >= 1000:
|
||||
magnitude += 1
|
||||
num /= 1000.0
|
||||
return '{}{}'.format('{:f}'.format(num).rstrip('0').rstrip('.'), ['', 'K', 'M', 'B', 'T'][magnitude])
|
||||
|
||||
|
||||
def wrap_text(width, text):
|
||||
|
||||
lines = []
|
||||
|
||||
arr = text.split()
|
||||
length_sum = 0
|
||||
|
||||
str_sum = ""
|
||||
|
||||
for var in arr:
|
||||
length_sum += len(var) + 1
|
||||
if length_sum <= width:
|
||||
str_sum += " " + var
|
||||
else:
|
||||
lines.append(str_sum)
|
||||
length_sum = 0
|
||||
str_sum = var
|
||||
|
||||
if length_sum != 0:
|
||||
lines.append(str_sum)
|
||||
|
||||
# lines.append(" " + arr[len(arr) - 1])
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def draw_spoiler(layout, data, toggle_prop_name, name="", data1=None, layout_enabled=None, icon=None, align=True):
|
||||
""" Draw a spoiler-like layout in Blender UI """
|
||||
|
||||
is_expanded = getattr(data, toggle_prop_name)
|
||||
|
||||
body = layout.box()
|
||||
|
||||
header = body.box()
|
||||
header_row = header.row(align=True)
|
||||
header_row.prop(data, toggle_prop_name, emboss=False, text='', icon='TRIA_DOWN' if is_expanded else 'TRIA_RIGHT')
|
||||
|
||||
if data1 and layout_enabled:
|
||||
if icon:
|
||||
header_row.label(text='', icon=icon)
|
||||
header_row.prop(data1, layout_enabled, text=name)
|
||||
else:
|
||||
header_row.label(text=' ' + name if align else name, icon=icon)
|
||||
|
||||
if is_expanded:
|
||||
|
||||
content = body.column()
|
||||
|
||||
if data1 and layout_enabled:
|
||||
content.enabled = getattr(data1, layout_enabled)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def show_message_box(message = "", title = "Message Box", icon = 'INFO'):
|
||||
|
||||
def draw(self, context):
|
||||
self.layout.label(text=message)
|
||||
|
||||
bpy.context.window_manager.popup_menu(draw, title=title, icon=icon)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class NodeTreeBuilder:
|
||||
|
||||
def __init__( self
|
||||
, node_tree : bpy.types.NodeTree
|
||||
, x : float = -200
|
||||
, y : float = -200
|
||||
, delimeter : float = 300
|
||||
):
|
||||
self._x = x
|
||||
self._y = y
|
||||
self.tree = node_tree
|
||||
self.delimeter = delimeter
|
||||
|
||||
self.purge_tree()
|
||||
|
||||
def purge_tree(self):
|
||||
for node in self.tree.nodes:
|
||||
self.tree.nodes.remove(node)
|
||||
|
||||
def add_node( self
|
||||
, node_type : str
|
||||
, node_name : str
|
||||
, column : int
|
||||
, row : int
|
||||
, node_descr : str = ""
|
||||
):
|
||||
|
||||
node = self.tree.nodes.new(node_type)
|
||||
node.name = node_name
|
||||
node.label = node_descr if node_descr else node_name
|
||||
|
||||
node.location = (self.delimeter * column + self._x, -self.delimeter * row + self._y)
|
||||
|
||||
return node
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
Binary file not shown.
+60
@@ -0,0 +1,60 @@
|
||||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://b84ohqhssylna"
|
||||
path="res://.godot/imported/placeholder.blend-6e69f9b134a01fbbcb802f96f00fa827.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://reference/blender-wow-studio-3.4-1.1.0_Experimental/io_scene_wmo/utils/placeholder/placeholder.blend"
|
||||
dest_files=["res://.godot/imported/placeholder.blend-6e69f9b134a01fbbcb802f96f00fa827.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
nodes/root_type=""
|
||||
nodes/root_name=""
|
||||
nodes/root_script=null
|
||||
nodes/apply_root_scale=true
|
||||
nodes/root_scale=1.0
|
||||
nodes/import_as_skeleton_bones=false
|
||||
nodes/use_name_suffixes=true
|
||||
nodes/use_node_type_suffixes=true
|
||||
meshes/ensure_tangents=true
|
||||
meshes/generate_lods=true
|
||||
meshes/create_shadow_meshes=true
|
||||
meshes/light_baking=1
|
||||
meshes/lightmap_texel_size=0.2
|
||||
meshes/force_disable_compression=false
|
||||
skins/use_named_skins=true
|
||||
animation/import=true
|
||||
animation/fps=30
|
||||
animation/trimming=false
|
||||
animation/remove_immutable_tracks=true
|
||||
animation/import_rest_as_RESET=false
|
||||
import_script/path=""
|
||||
materials/extract=0
|
||||
materials/extract_format=0
|
||||
materials/extract_path=""
|
||||
_subresources={}
|
||||
blender/nodes/visible=0
|
||||
blender/nodes/active_collection_only=false
|
||||
blender/nodes/punctual_lights=true
|
||||
blender/nodes/cameras=true
|
||||
blender/nodes/custom_properties=true
|
||||
blender/nodes/modifiers=1
|
||||
blender/meshes/colors=false
|
||||
blender/meshes/uvs=true
|
||||
blender/meshes/normals=true
|
||||
blender/meshes/export_geometry_nodes_instances=false
|
||||
blender/meshes/gpu_instances=false
|
||||
blender/meshes/tangents=true
|
||||
blender/meshes/skins=2
|
||||
blender/meshes/export_bones_deforming_mesh_only=false
|
||||
blender/materials/unpack_enabled=true
|
||||
blender/materials/export_materials=1
|
||||
blender/animation/limit_playback=true
|
||||
blender/animation/always_sample=true
|
||||
blender/animation/group_tracks=true
|
||||
gltf/naming_version=2
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import bpy
|
||||
import blf
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
class WBS_Viewport_Text_Display(bpy.types.Operator):
|
||||
|
||||
bl_idname = "wbs.viewport_text_display"
|
||||
bl_label = "Generic Text Display"
|
||||
_timer = None
|
||||
_draw_handler = None
|
||||
_start_time = None
|
||||
message: bpy.props.StringProperty()
|
||||
font_size: bpy.props.IntProperty(default=24)
|
||||
y_offset: bpy.props.IntProperty(default=67)
|
||||
color:bpy.props.FloatVectorProperty(
|
||||
size=4,
|
||||
default=(1.0, 1.0, 1.0, 1.0)
|
||||
)
|
||||
|
||||
def draw_callback_px(self, context, message, font_size, y_offset, color):
|
||||
|
||||
|
||||
script_paths = bpy.utils.script_paths()
|
||||
addons_path = os.path.join(script_paths[0], "addons")
|
||||
addon_name = 'io_scene_wmo'
|
||||
font_rel_path = os.path.join("fonts", "frizqt_ck.ttf")
|
||||
font_path = os.path.join(addons_path, addon_name, font_rel_path)
|
||||
|
||||
font_id = blf.load(font_path)
|
||||
blf.size(font_id, font_size, 72)
|
||||
width, height = context.area.width, context.area.height
|
||||
text_width, text_height = blf.dimensions(font_id, message)
|
||||
text_x = (width - text_width) / 2
|
||||
text_y = height - text_height - y_offset
|
||||
|
||||
blf.position(font_id, text_x, text_y, 0)
|
||||
if color is None:
|
||||
color = (1, 1, 1, 1)
|
||||
blf.color(font_id, *color)
|
||||
blf.enable(font_id, blf.SHADOW)
|
||||
blf.shadow(font_id, 5, 0, 0, 0, 1)
|
||||
blf.shadow_offset(font_id, 1, -2)
|
||||
blf.draw(font_id, message)
|
||||
blf.disable(font_id, blf.SHADOW)
|
||||
|
||||
def modal(self, context, event):
|
||||
if (time.time() - self._start_time) > 3.5:
|
||||
self.cancel(context)
|
||||
return {'CANCELLED'}
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
self._start_time = time.time()
|
||||
args = (context, self.message, self.font_size, self.y_offset, self.color)
|
||||
self._draw_handler = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback_px, args, 'WINDOW', 'POST_PIXEL')
|
||||
|
||||
self._timer = context.window_manager.event_timer_add(0.1, window=context.window)
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def cancel(self, context):
|
||||
if self._draw_handler is not None:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._draw_handler, 'WINDOW')
|
||||
self._draw_handler = None
|
||||
if self._timer is not None:
|
||||
context.window_manager.event_timer_remove(self._timer)
|
||||
self._timer = None
|
||||
Reference in New Issue
Block a user