новая структура проекта
This commit is contained in:
+185
@@ -0,0 +1,185 @@
|
||||
import bpy
|
||||
import re
|
||||
from mathutils import Matrix, Vector, Quaternion
|
||||
from ..util import can_apply_scale,make_fcurve_compound,get_bone_groups
|
||||
|
||||
def convert_m2_bones():
|
||||
def fix_scale(matrix,curves,keyframe_count):
|
||||
if not can_apply_scale(curves,keyframe_count):
|
||||
return (True,'Non-uniform scaling')
|
||||
|
||||
for i in range(keyframe_count):
|
||||
def co(j): return fcurves[j].keyframe_points[i].co
|
||||
# read vector defining the old rotation
|
||||
vec = Vector((co(0)[1], co(1)[1], co(2)[1]))
|
||||
|
||||
# TODO: CHANGE VECTOR USING 'matrix' HERE SOMEHOW
|
||||
|
||||
# write vector back
|
||||
co(0)[1] = vec.x
|
||||
co(1)[1] = vec.y
|
||||
co(2)[1] = vec.z
|
||||
|
||||
return (False,'')
|
||||
|
||||
def fix_rotation(matrix,fcurves,keyframe_count):
|
||||
def quat_dist(q1,q2):
|
||||
# takes polarity into account on purpose.
|
||||
# we just want to do _mostly_ correct rotations,
|
||||
# but there might be a better formula to use here.
|
||||
return (
|
||||
pow(q1.w-q2.w,2) +
|
||||
pow(q1.x-q2.x,2) +
|
||||
pow(q1.y-q2.y,2) +
|
||||
pow(q1.z-q2.z,2))
|
||||
|
||||
last_quat = None
|
||||
for i in range(keyframe_count):
|
||||
def co(j): return fcurves[j].keyframe_points[i].co
|
||||
|
||||
q_in = Quaternion((co(0)[1], co(1)[1], co(2)[1], co(3)[1]))
|
||||
axis,angle = q_in.to_axis_angle()
|
||||
axis.rotate(matrix)
|
||||
|
||||
rot_q = Quaternion(axis,angle)
|
||||
if last_quat is None:
|
||||
last_quat = rot_q
|
||||
else:
|
||||
rot_q_neg = Quaternion(-rot_q)
|
||||
dist = quat_dist(rot_q,last_quat)
|
||||
neg_dist = quat_dist(rot_q_neg,last_quat)
|
||||
last_quat = rot_q if dist <= neg_dist else rot_q_neg
|
||||
|
||||
co(0)[1] = last_quat.w
|
||||
co(2)[1] = last_quat.x
|
||||
co(1)[1] = -last_quat.y
|
||||
co(3)[1] = last_quat.z
|
||||
return (False,'')
|
||||
|
||||
def fix_location(matrix, fcurves,keyframe_count):
|
||||
for i in range(keyframe_count):
|
||||
def co(j): return fcurves[j].keyframe_points[i].co
|
||||
vec = Vector((co(0)[1],co(1)[1], co(2)[1]))
|
||||
vec.rotate(matrix)
|
||||
|
||||
co(1)[1] = vec.x
|
||||
co(0)[1] = -vec.y
|
||||
co(2)[1] = vec.z
|
||||
return (False,'')
|
||||
|
||||
def fix_curves(name, matrix, fcurves, track_count, callback):
|
||||
for i in range(track_count):
|
||||
if not i in fcurves:
|
||||
raise ValueError(f'Track index {i} missing in {name} fcurves')
|
||||
|
||||
keyframe_count = len(fcurves[0].keyframe_points)
|
||||
for i,fcurve in fcurves.items():
|
||||
cur_count = len(fcurve.keyframe_points)
|
||||
if cur_count != keyframe_count:
|
||||
raise ValueError(f'Track index {i} keyframe count ({cur_count}) is different from index 0 {keyframe_count}')
|
||||
|
||||
for i in range(keyframe_count):
|
||||
time = fcurves[0].keyframe_points[i].co[0]
|
||||
for j in range(track_count):
|
||||
cur_time = fcurves[j].keyframe_points[i].co[0]
|
||||
if cur_time != time:
|
||||
raise ValueError(f'Track index {j} frame {j} has a different time value ({cur_time}) from index 0 ({time})')
|
||||
|
||||
return callback(matrix,fcurves,keyframe_count)
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
fixed_vertices = 0
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'MESH' or obj.parent is None or obj.parent.type != 'ARMATURE':
|
||||
continue
|
||||
|
||||
bone_names = [bone.name for bone in obj.parent.data.bones]
|
||||
for vertex in obj.data.vertices:
|
||||
groups = get_bone_groups(obj, vertex, bone_names)
|
||||
for el in groups[4:]:
|
||||
obj.vertex_groups[el.group].remove([vertex.index])
|
||||
if len(groups) > 4:
|
||||
fixed_vertices += 1
|
||||
print(f'Removed overflowing groups for {fixed_vertices} vertices')
|
||||
|
||||
for action in bpy.data.actions:
|
||||
removed_fcurves = []
|
||||
for curve in action.fcurves:
|
||||
if curve.data_path in ["location","rotation_euler","scale"]:
|
||||
removed_fcurves.append(curve)
|
||||
print(f'Removed fcurve "{curve.data_path}[{curve.array_index}]" from action {action.name}')
|
||||
for curve in removed_fcurves:
|
||||
action.fcurves.remove(curve)
|
||||
|
||||
bpy.ops.object.select_all(action='SELECT')
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=False)
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
changed_bones = {}
|
||||
changed_objects = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'ARMATURE': continue
|
||||
|
||||
try:
|
||||
obj.select_set(True)
|
||||
except RuntimeError as e:
|
||||
print(f"Unable to select armature {obj.name}, not converting it")
|
||||
continue
|
||||
|
||||
changed_objects.append(obj)
|
||||
|
||||
#this was enabled in 3.0 and worked better i think
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
for bone in obj.data.edit_bones:
|
||||
if bone.use_connect:
|
||||
bone.use_connect = False
|
||||
|
||||
for bone in obj.data.edit_bones:
|
||||
bone.use_connect = False
|
||||
bone.roll = 0
|
||||
bone.tail = bone.head + Vector((1,0,0))
|
||||
changed_bones[bone.name] = Matrix(obj.data.bones[bone.name].matrix_local)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
for action in bpy.data.actions:
|
||||
fcurve_compounds = make_fcurve_compound(action.fcurves,
|
||||
lambda path: path.startswith('pose.bones')
|
||||
)
|
||||
for key,fcurves in fcurve_compounds.items():
|
||||
bone = re.search('"(.+?)"',key).group(1)
|
||||
if not bone in changed_bones:
|
||||
continue
|
||||
matrix = changed_bones[bone]
|
||||
curve_type = re.search('([a-zA-Z_]+)$',key).group(0)
|
||||
|
||||
remove_reason = None
|
||||
should_remove = False
|
||||
if curve_type == 'scale':
|
||||
(should_remove,remove_reason) = fix_curves(key,matrix,fcurves,3,fix_scale)
|
||||
|
||||
if curve_type == 'location':
|
||||
(should_remove,remove_reason) = fix_curves(key,matrix,fcurves,3,fix_location)
|
||||
|
||||
if curve_type == 'rotation_quaternion':
|
||||
(should_remove,remove_reason) = fix_curves(key,matrix,fcurves,4,fix_rotation)
|
||||
|
||||
if should_remove:
|
||||
print(f"Deleting incompatible fcurves {fcurve.data_path}: {remove_reason}")
|
||||
for fcurve in fcurves.values():
|
||||
action.fcurves.remove(fcurve)
|
||||
for obj in changed_objects:
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
obj.select_set(True)
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
# clear other curves if needed
|
||||
bpy.ops.pose.scale_clear()
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
else:
|
||||
for fcurve in fcurves.values():
|
||||
fcurve.update()
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
|
||||
from ...ui.preferences import get_project_preferences
|
||||
from . import m2_export_validation
|
||||
|
||||
class LogLevel(IntEnum):
|
||||
"""Logging severity levels."""
|
||||
ERROR = 1
|
||||
WARN = 2
|
||||
INFO = 3
|
||||
DEBUG = 4
|
||||
|
||||
# ---------------------------
|
||||
# Storage
|
||||
# ---------------------------
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
infos = []
|
||||
debugs = []
|
||||
master_log = []
|
||||
|
||||
# ---------------------------
|
||||
# Utility
|
||||
# ---------------------------
|
||||
|
||||
def clear():
|
||||
"""Reset all stored logs."""
|
||||
warnings.clear()
|
||||
infos.clear()
|
||||
errors.clear()
|
||||
debugs.clear()
|
||||
master_log.clear()
|
||||
|
||||
|
||||
def get_verbosity_level() -> int:
|
||||
"""
|
||||
Retrieve verbosity setting from addon preferences.
|
||||
Defaults to INFO level (3) if unavailable.
|
||||
"""
|
||||
try:
|
||||
prefs = get_project_preferences()
|
||||
return int(prefs.verbosity_level)
|
||||
except Exception:
|
||||
return 3 # INFO fallback
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Core logging
|
||||
# ---------------------------
|
||||
|
||||
def add(level: LogLevel, msg: str, print_now: bool = False):
|
||||
"""
|
||||
Store a log entry and optionally print immediately.
|
||||
|
||||
Args:
|
||||
level (LogLevel): severity of message
|
||||
msg (str): log message string
|
||||
print_now (bool): force console print immediately
|
||||
"""
|
||||
verbosity = get_verbosity_level()
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
|
||||
# --- Record per-level ---
|
||||
if level == LogLevel.ERROR:
|
||||
errors.append(msg)
|
||||
elif level == LogLevel.WARN:
|
||||
warnings.append(msg)
|
||||
elif level == LogLevel.INFO:
|
||||
infos.append(msg)
|
||||
elif level == LogLevel.DEBUG:
|
||||
debugs.append(msg)
|
||||
|
||||
# --- Append chronological record ---
|
||||
master_log.append((level, msg, timestamp))
|
||||
|
||||
# --- Live print if enabled ---
|
||||
if print_now and verbosity >= level:
|
||||
print(f"[{timestamp}] [{level.name}] {msg}")
|
||||
|
||||
|
||||
def _print_log_summary(title: str, validation_fn=None):
|
||||
"""
|
||||
Print combined summary for import/export logs.
|
||||
|
||||
Args:
|
||||
title (str): label ("Import" or "Export")
|
||||
validation_fn (callable, optional): optional validation hook
|
||||
|
||||
Returns:
|
||||
tuple(bool, bool): (warnings_present, errors_present)
|
||||
"""
|
||||
verbosity = get_verbosity_level()
|
||||
|
||||
print("\n##############################################################")
|
||||
print(f" {title} Log Summary")
|
||||
print("##############################################################")
|
||||
|
||||
# --- Optional validation ---
|
||||
if callable(validation_fn):
|
||||
for name, descriptions, items in validation_fn():
|
||||
print(f"\n== {name} ==")
|
||||
for d in descriptions:
|
||||
print(f" {d}")
|
||||
for item in items:
|
||||
add(LogLevel.WARN, f"[{name}] {item}", print_now=True)
|
||||
|
||||
# --- Errors & Warnings ---
|
||||
if errors:
|
||||
print(f"\n== {title} Errors ==")
|
||||
for msg in errors:
|
||||
print(f" [ERROR] {msg}")
|
||||
|
||||
if warnings:
|
||||
print(f"\n== {title} Warnings ==")
|
||||
for msg in warnings:
|
||||
print(f" [WARN] {msg}")
|
||||
|
||||
# --- Full chronological log ---
|
||||
print(f"\n== {title} Log ==")
|
||||
for level, msg, timestamp in master_log:
|
||||
if verbosity >= level:
|
||||
print(f" [{timestamp}] [{level.name}] {msg}")
|
||||
|
||||
# --- Summary footer ---
|
||||
print("\n##############################################################")
|
||||
print(f" Summary: {len(errors)} errors, {len(warnings)} warnings, "
|
||||
f"{len(infos)} info, {len(debugs)} debug messages")
|
||||
print("##############################################################\n")
|
||||
|
||||
return bool(warnings), bool(errors)
|
||||
|
||||
|
||||
def print_export_log():
|
||||
"""
|
||||
Print export summary and run export validation checks.
|
||||
Returns: (warnings_present, errors_present)
|
||||
"""
|
||||
return _print_log_summary("Export", validation_fn=m2_export_validation.run_validations)
|
||||
|
||||
|
||||
def print_import_log():
|
||||
"""
|
||||
Print import summary only.
|
||||
Returns: (warnings_present, errors_present)
|
||||
"""
|
||||
return _print_log_summary("Import")
|
||||
|
||||
# ---------------------------
|
||||
# Shorthands
|
||||
# ---------------------------
|
||||
|
||||
error = lambda msg, print_now=False, **kw: add(LogLevel.ERROR, msg, print_now=print_now, **kw)
|
||||
warn = lambda msg, print_now=False, **kw: add(LogLevel.WARN, msg, print_now=print_now, **kw)
|
||||
info = lambda msg, print_now=False, **kw: add(LogLevel.INFO, msg, print_now=print_now, **kw)
|
||||
debug = lambda msg, print_now=False, **kw: add(LogLevel.DEBUG, msg, print_now=print_now, **kw)
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
import bpy
|
||||
from ..util import get_bone_groups
|
||||
|
||||
# ---------------------------
|
||||
# Scene validation functions
|
||||
# ---------------------------
|
||||
|
||||
def wrong_scene_type():
|
||||
name = "Wrong Scene Type"
|
||||
description = [
|
||||
"Issue: The scene type is set to WMO instead of M2",
|
||||
"Fix: Change the scene type to 'M2' in the top-right corner of Blender"
|
||||
]
|
||||
items = []
|
||||
|
||||
if not bpy.context.scene:
|
||||
items.append("Wrong scene: There is no scene object, please report this (unexpected).")
|
||||
elif bpy.context.scene.wow_scene.type != 'M2':
|
||||
items.append(f"Wrong scene: Type is {bpy.context.scene.wow_scene.type} but should be M2")
|
||||
|
||||
return (name, description, items)
|
||||
|
||||
def transformed_objects():
|
||||
name = "Transformed Objects"
|
||||
description = [
|
||||
"Issue: Objects in the scene are transformed (moved, rotated, or scaled).",
|
||||
"Fix: Run the 'Convert Bones To WoW' command and fix any issues it causes."
|
||||
]
|
||||
items = []
|
||||
|
||||
def vec_eq(n, q1, q2):
|
||||
for i in range(n):
|
||||
if q1[i] != q2[i]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def vec_str(value, names):
|
||||
return " ".join([f"{names[i]}={value[i]}" for i in range(len(names))])
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type not in ('ARMATURE', 'MESH'):
|
||||
continue
|
||||
|
||||
def compare(name, names, val1, val2):
|
||||
if not vec_eq(len(names), val1, val2):
|
||||
items.append(f"Object {obj.name}'s {name} is {vec_str(val1, names)}, but should be {vec_str(val2, names)}")
|
||||
|
||||
vec_names = ['x', 'y', 'z']
|
||||
quat_names = ['w', 'x', 'y', 'z']
|
||||
|
||||
compare("location", vec_names, obj.location, (0, 0, 0))
|
||||
compare("scale", vec_names, obj.scale, (1, 1, 1))
|
||||
if obj.rotation_mode == 'QUATERNION':
|
||||
compare("quaternion rotation", quat_names, obj.rotation_quaternion, (1, 0, 0, 0))
|
||||
elif obj.rotation_mode == 'AXIS_ANGLE':
|
||||
compare("axis angle rotation", quat_names, obj.rotation_quaternion, (1, 0, 0, 0))
|
||||
else:
|
||||
compare("euler rotation", vec_names, obj.rotation_euler, (0, 0, 0))
|
||||
|
||||
return (name, description, items)
|
||||
|
||||
def empty_textures():
|
||||
name = "Empty Textures"
|
||||
description = [
|
||||
"Issue: An M2 material has no texture set in any of its texture slots.",
|
||||
"Effect: Will usually cause the model to become invisible ingame.",
|
||||
"Note: This is not *always* an error, not all materials have textures."
|
||||
]
|
||||
items = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
if not hasattr(obj, "wow_m2_geoset") or obj.wow_m2_geoset.collision_mesh or not obj.material_slots:
|
||||
continue
|
||||
|
||||
for slot in obj.material_slots:
|
||||
mat = slot.material
|
||||
if mat is None:
|
||||
items.append(f"Object '{obj.name}' has an empty material slot.")
|
||||
continue
|
||||
|
||||
if not hasattr(mat, "wow_m2_material"):
|
||||
items.append(f"Material '{mat.name}' on object '{obj.name}' has no wow_m2_material attribute.")
|
||||
continue
|
||||
|
||||
wow_mat = mat.wow_m2_material
|
||||
if not hasattr(wow_mat, "texture_1"):
|
||||
items.append(f"wow_m2_material on '{mat.name}' has no 'texture_1' attribute.")
|
||||
continue
|
||||
|
||||
if wow_mat.texture_1 is None:
|
||||
items.append(f"Object '{obj.name}', material '{mat.name}' has no M2 textures, may be invisible ingame.")
|
||||
|
||||
return (name, description, items)
|
||||
|
||||
def empty_texture_paths():
|
||||
name = "Empty Texture Path"
|
||||
description = [
|
||||
"Issue: A model has an M2 material with a texture set that has no .blp path.",
|
||||
"Effect: Will usually cause the model to become invisible ingame.",
|
||||
"Fix: Find the material with the texture and fill the Texture Path.",
|
||||
]
|
||||
items = []
|
||||
|
||||
texture_maps = {}
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type == 'MESH' and not obj.wow_m2_geoset.collision_mesh and obj.material_slots:
|
||||
for slot in obj.material_slots:
|
||||
if slot.material is None or not hasattr(slot.material, 'wow_m2_material'):
|
||||
continue
|
||||
mat = slot.material.wow_m2_material
|
||||
for texture in [mat.texture_1, mat.texture_2]:
|
||||
if texture and hasattr(texture, 'wow_m2_texture') and not texture.wow_m2_texture.path:
|
||||
if texture.wow_m2_texture.texture_type == '0':
|
||||
texture_maps.setdefault(texture.name, []).append(obj.name)
|
||||
|
||||
for texture, obj_names in texture_maps.items():
|
||||
obj_str = ", ".join(obj_names)
|
||||
items.append(f"Texture '{texture}' used by ({obj_str}) has no .blp path set.")
|
||||
|
||||
return (name, description, items)
|
||||
|
||||
def no_materials():
|
||||
name = "No Materials"
|
||||
description = [
|
||||
"Issue: A model has no materials set.",
|
||||
"Effect: The model will usually be invisible ingame.",
|
||||
"Fix: Add at least one material to your model.",
|
||||
"Note: Not always an error, some models don't require materials."
|
||||
]
|
||||
items = [
|
||||
f"Object {obj.name} has no M2 materials (invisible ingame)."
|
||||
for obj in bpy.data.objects
|
||||
if obj.type == 'MESH' and not obj.wow_m2_geoset.collision_mesh and len(obj.material_slots) == 0
|
||||
]
|
||||
return (name, description, items)
|
||||
|
||||
def bone_constraints():
|
||||
name = "Bone Constraints"
|
||||
description = [
|
||||
"Issue: A bone has constraints applied.",
|
||||
"Effect: Will almost always break animations (WoW doesn't support bone constraints).",
|
||||
"Fix: Remove bone constraints or bake animations into keyframes."
|
||||
]
|
||||
items = []
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'ARMATURE':
|
||||
continue
|
||||
for bone in obj.pose.bones:
|
||||
for constraint in bone.constraints:
|
||||
items.append(f"Bone {obj.name}.{bone.name} has constraint '{constraint.name}', usually a mistake.")
|
||||
return (name, description, items)
|
||||
|
||||
def no_animation_pairs():
|
||||
name = "No Animation Pairs"
|
||||
description = [
|
||||
"Issue: Animations in the Animation Editor have no object pairs.",
|
||||
"Effect: No animation data will be exported for this sequence.",
|
||||
"Fix: Add at least one object/action pair."
|
||||
]
|
||||
items = [
|
||||
f"Sequence '{seq.name}' has no pairs."
|
||||
for seq in bpy.context.scene.wow_m2_animations
|
||||
if len(seq.anim_pairs) == 0 and "64" not in seq.flags
|
||||
]
|
||||
return (name, description, items)
|
||||
|
||||
def missing_animation_items():
|
||||
name = "Missing Animation Items"
|
||||
description = [
|
||||
"Issue: Animation pairs lack an object or action.",
|
||||
"Effect: No animation data will be written.",
|
||||
"Fix: Assign both an object and an action."
|
||||
]
|
||||
items = []
|
||||
for seq in bpy.context.scene.wow_m2_animations:
|
||||
for pair in seq.anim_pairs:
|
||||
if pair.object is None:
|
||||
items.append(f"Sequence '{seq.name}' pair missing object.")
|
||||
elif pair.action is None:
|
||||
if pair.object.name not in ['CharInfoCam', 'CharInfoCam_Target', 'PortraitCam', 'PortraitCam_Target']:
|
||||
items.append(f"Sequence '{seq.name}' pair '{pair.object.name}' missing action.")
|
||||
return (name, description, items)
|
||||
|
||||
def non_primary_sequences():
|
||||
name = "Non-primary Sequences"
|
||||
description = [
|
||||
"Issue: Non-primary sequences are unsupported.",
|
||||
"Effect: Animation will break or crash the game.",
|
||||
"Fix: Add the 'primary sequence' flag."
|
||||
]
|
||||
items = [
|
||||
f"Sequence '{seq.name}' is missing the primary flag."
|
||||
for seq in bpy.context.scene.wow_m2_animations
|
||||
if not seq.is_global_sequence and "32" not in seq.flags
|
||||
]
|
||||
return (name, description, items)
|
||||
|
||||
def too_many_bone_groups():
|
||||
name = "Too Many Bone Groups"
|
||||
description = [
|
||||
"Issue: Vertices are influenced by more than 4 bones.",
|
||||
"Effect: Excess bones will be dropped, mesh will deform incorrectly.",
|
||||
"Fix: Use 'Limit Bone Groups' or test the effect in-game."
|
||||
]
|
||||
items = []
|
||||
for obj in bpy.data.objects:
|
||||
if obj.type != 'MESH' or not obj.parent or obj.parent.type != 'ARMATURE':
|
||||
continue
|
||||
bone_names = [b.name for b in obj.parent.data.bones]
|
||||
broken = sum(1 for v in obj.data.vertices if len(get_bone_groups(obj, v, bone_names)) > 4)
|
||||
if broken > 0:
|
||||
items.append(f"Object '{obj.name}' has {broken} vertices with too many bone groups.")
|
||||
return (name, description, items)
|
||||
|
||||
def fcurves_transforming_objects():
|
||||
name = "FCurves Transforming Objects"
|
||||
description = [
|
||||
"Issue: FCurves animate Blender objects directly, unsupported in M2.",
|
||||
"Effect: Object transforms incorrectly in-game.",
|
||||
"Fix: Run 'Convert Bones To WoW' and verify results."
|
||||
]
|
||||
items = []
|
||||
for animation in bpy.context.scene.wow_m2_animations:
|
||||
for pair in animation.anim_pairs:
|
||||
if not pair.action:
|
||||
continue
|
||||
for curve in pair.action.fcurves:
|
||||
if curve.data_path in ["location", "rotation_euler", "scale"]:
|
||||
if pair.object and not pair.object.wow_m2_uv_transform.enabled:
|
||||
items.append(f"FCurve '{curve.data_path}[{curve.array_index}]' in '{pair.action.name}' transforms object '{pair.object.name}'.")
|
||||
return (name, description, items)
|
||||
|
||||
def run_validations():
|
||||
"""Run all static validation checks and return structured results."""
|
||||
validation_callbacks = [
|
||||
wrong_scene_type,
|
||||
transformed_objects,
|
||||
empty_textures,
|
||||
empty_texture_paths,
|
||||
no_materials,
|
||||
bone_constraints,
|
||||
no_animation_pairs,
|
||||
missing_animation_items,
|
||||
non_primary_sequences,
|
||||
too_many_bone_groups,
|
||||
fcurves_transforming_objects,
|
||||
]
|
||||
|
||||
results = []
|
||||
for callback in validation_callbacks:
|
||||
name, descriptions, items = callback()
|
||||
if items:
|
||||
results.append((name, descriptions, items))
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user