Updated Operations and Properties

- Updated Operations and Properties with tpying and logging.

I have not updated translation files, this is because i want to gut MMD Tools system and replace it with our own, however I want to make MMD Tools more simple and ajust it to our needs only. This is going to take a while and my aim for this is Alpha 4, also the MMD Translation system hurt my head....

- Fixes a couple of bugs as well, with quick access and the PMX importer.
This commit is contained in:
Yusarina
2025-04-23 00:43:38 +01:00
parent 61e4269764
commit cfe760e8df
21 changed files with 689 additions and 347 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
schema_version = "1.0.0" schema_version = "1.0.0"
id = "avatar_toolkit" id = "avatar_toolkit"
version = "0.2.1" version = "0.3.0"
name = "Avatar Toolkit" name = "Avatar Toolkit"
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
maintainer = "Team NekoNeo" maintainer = "Team NekoNeo"
+27 -2
View File
@@ -25,12 +25,18 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
non_standard_messages: List[str] = [] non_standard_messages: List[str] = []
scale_messages: List[str] = [] scale_messages: List[str] = []
# Check if this is a PMX model
is_pmx_model = False
if armature and hasattr(armature, 'mmd_type') or (hasattr(armature, 'parent') and armature.parent and hasattr(armature.parent, 'mmd_type')):
is_pmx_model = True
logger.debug("Detected PMX model, using specialized validation")
if validation_mode == 'NONE': if validation_mode == 'NONE':
logger.debug("Validation mode is NONE, skipping validation") logger.debug("Validation mode is NONE, skipping validation")
if detailed_messages: if detailed_messages:
return True, [], False, [], [], [] return True, [t("Validation.mode.none")], False, [], [], []
else: else:
return True, [], False return True, [t("Validation.mode.none")], False
if not armature or armature.type != 'ARMATURE' or not armature.data.bones: if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
logger.warning("Basic armature check failed") logger.warning("Basic armature check failed")
@@ -125,6 +131,21 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
non_standard_messages.append(t("Armature.validation.standardize_note.line2")) non_standard_messages.append(t("Armature.validation.standardize_note.line2"))
non_standard_messages.append(t("Armature.validation.standardize_note.line3")) non_standard_messages.append(t("Armature.validation.standardize_note.line3"))
# Special handling for PMX models
if is_pmx_model:
logger.info("PMX model detected, applying specialized validation")
# For PMX models, we'll be more lenient with validation
# and provide specific guidance for these models
if not messages:
messages = [t("Armature.validation.pmx_model_detected")]
# Add PMX-specific messages
if validation_mode == 'STRICT':
messages.append(t("Armature.validation.pmx_model_strict"))
messages.append(t("Armature.validation.pmx_model_standardize"))
else:
messages.append(t("Armature.validation.pmx_model_basic"))
# Combine messages in correct order # Combine messages in correct order
messages.extend(non_standard_messages) messages.extend(non_standard_messages)
@@ -149,6 +170,10 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio
else: else:
return True, messages, True return True, messages, True
# Ensure messages has at least one element
if not messages:
messages = [t("Armature.validation.unknown_format")]
logger.info(f"Armature validation complete. Valid: {is_valid}") logger.info(f"Armature validation complete. Valid: {is_valid}")
if detailed_messages: if detailed_messages:
return is_valid, messages, False, hierarchy_messages, scale_messages, non_standard_messages return is_valid, messages, False, hierarchy_messages, scale_messages, non_standard_messages
+4 -2
View File
@@ -41,9 +41,11 @@ class FnModel:
Optional[bpy.types.Object]: The root object of the model. If the object is not a part of a model, None is returned. Optional[bpy.types.Object]: The root object of the model. If the object is not a part of a model, None is returned.
Generally, the root object is a object with type == "EMPTY" and mmd_type == "ROOT". Generally, the root object is a object with type == "EMPTY" and mmd_type == "ROOT".
""" """
while obj is not None and obj.mmd_type != "ROOT": while obj is not None:
if hasattr(obj, 'mmd_type') and obj.mmd_type == "ROOT":
return obj
obj = obj.parent obj = obj.parent
return obj return None
@staticmethod @staticmethod
def find_armature_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: def find_armature_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]:
+82 -38
View File
@@ -6,13 +6,16 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from bpy.props import BoolProperty, StringProperty from bpy.props import BoolProperty, StringProperty, FloatProperty
from bpy.types import Operator from bpy.types import Operator, Context, Object, Material
from typing import Set, Dict, Any, List, Tuple, Optional, Union, cast
from .. import cycles_converter from .. import cycles_converter
from ..core.exceptions import MaterialNotFoundError from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.shader import _NodeGroupUtils from ..core.shader import _NodeGroupUtils
from ....core.logging_setup import logger
class ConvertMaterialsForCycles(Operator): class ConvertMaterialsForCycles(Operator):
@@ -21,14 +24,14 @@ class ConvertMaterialsForCycles(Operator):
bl_description = "Convert materials of selected objects for Cycles." bl_description = "Convert materials of selected objects for Cycles."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
use_principled: bpy.props.BoolProperty( use_principled: BoolProperty(
name="Convert to Principled BSDF", name="Convert to Principled BSDF",
description="Convert MMD shader nodes to Principled BSDF as well if enabled", description="Convert MMD shader nodes to Principled BSDF as well if enabled",
default=False, default=False,
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
clean_nodes: bpy.props.BoolProperty( clean_nodes: BoolProperty(
name="Clean Nodes", name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.", description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=False, default=False,
@@ -36,22 +39,27 @@ class ConvertMaterialsForCycles(Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == "MESH"), None) return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
def draw(self, context): def draw(self, context: Context) -> None:
layout = self.layout layout = self.layout
layout.prop(self, "use_principled") layout.prop(self, "use_principled")
layout.prop(self, "clean_nodes") layout.prop(self, "clean_nodes")
def execute(self, context): def execute(self, context: Context) -> Set[str]:
try: try:
context.scene.render.engine = "CYCLES" context.scene.render.engine = "CYCLES"
except: except Exception as e:
logger.error(f"Failed to change to Cycles render engine: {str(e)}")
self.report({"ERROR"}, " * Failed to change to Cycles render engine.") self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
return {"CANCELLED"} return {"CANCELLED"}
logger.info(f"Converting materials for Cycles with principled={self.use_principled}, clean_nodes={self.clean_nodes}")
for obj in (x for x in context.selected_objects if x.type == "MESH"): for obj in (x for x in context.selected_objects if x.type == "MESH"):
logger.debug(f"Converting materials for object: {obj.name}")
cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes) cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
return {"FINISHED"} return {"FINISHED"}
@@ -61,21 +69,21 @@ class ConvertMaterials(Operator):
bl_description = "Convert materials of selected objects." bl_description = "Convert materials of selected objects."
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
use_principled: bpy.props.BoolProperty( use_principled: BoolProperty(
name="Convert to Principled BSDF", name="Convert to Principled BSDF",
description="Convert MMD shader nodes to Principled BSDF as well if enabled", description="Convert MMD shader nodes to Principled BSDF as well if enabled",
default=True, default=True,
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
clean_nodes: bpy.props.BoolProperty( clean_nodes: BoolProperty(
name="Clean Nodes", name="Clean Nodes",
description="Remove redundant nodes as well if enabled. Disable it to keep node data.", description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
default=True, default=True,
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
subsurface: bpy.props.FloatProperty( subsurface: FloatProperty(
name="Subsurface", name="Subsurface",
default=0.001, default=0.001,
soft_min=0.000, soft_min=0.000,
@@ -85,13 +93,15 @@ class ConvertMaterials(Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == "MESH"), None) return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
def execute(self, context): def execute(self, context: Context) -> Set[str]:
logger.info(f"Converting materials with principled={self.use_principled}, clean_nodes={self.clean_nodes}, subsurface={self.subsurface}")
for obj in context.selected_objects: for obj in context.selected_objects:
if obj.type != "MESH": if obj.type != "MESH":
continue continue
logger.debug(f"Converting materials for object: {obj.name}")
cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface) cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface)
return {"FINISHED"} return {"FINISHED"}
@@ -102,20 +112,22 @@ class ConvertBSDFMaterials(Operator):
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return next((x for x in context.selected_objects if x.type == 'MESH'), None) return next((x for x in context.selected_objects if x.type == 'MESH'), None) is not None
def execute(self, context): def execute(self, context: Context) -> Set[str]:
logger.info("Converting BSDF materials to MMD shader")
for obj in context.selected_objects: for obj in context.selected_objects:
if obj.type != 'MESH': if obj.type != 'MESH':
continue continue
logger.debug(f"Converting BSDF materials for object: {obj.name}")
cycles_converter.convertToMMDShader(obj) cycles_converter.convertToMMDShader(obj)
return {'FINISHED'} return {'FINISHED'}
class _OpenTextureBase: class _OpenTextureBase:
"""Create a texture for mmd model material.""" """Create a texture for mmd model material."""
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"}
filepath: StringProperty( filepath: StringProperty(
name="File Path", name="File Path",
@@ -129,7 +141,7 @@ class _OpenTextureBase:
options={"HIDDEN"}, options={"HIDDEN"},
) )
def invoke(self, context, event): def invoke(self, context: Context, event: Any) -> Set[str]:
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
@@ -139,8 +151,13 @@ class OpenTexture(Operator, _OpenTextureBase):
bl_label = "Open Texture" bl_label = "Open Texture"
bl_description = "Create main texture of active material" bl_description = "Create main texture of active material"
def execute(self, context): def execute(self, context: Context) -> Set[str]:
mat = context.active_object.active_material mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Creating texture for material: {mat.name} from {self.filepath}")
fnMat = FnMaterial(mat) fnMat = FnMaterial(mat)
fnMat.create_texture(self.filepath) fnMat.create_texture(self.filepath)
return {"FINISHED"} return {"FINISHED"}
@@ -154,8 +171,13 @@ class RemoveTexture(Operator):
bl_description = "Remove main texture of active material" bl_description = "Remove main texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: Context) -> Set[str]:
mat = context.active_object.active_material mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Removing texture from material: {mat.name}")
fnMat = FnMaterial(mat) fnMat = FnMaterial(mat)
fnMat.remove_texture() fnMat.remove_texture()
return {"FINISHED"} return {"FINISHED"}
@@ -168,8 +190,13 @@ class OpenSphereTextureSlot(Operator, _OpenTextureBase):
bl_label = "Open Sphere Texture" bl_label = "Open Sphere Texture"
bl_description = "Create sphere texture of active material" bl_description = "Create sphere texture of active material"
def execute(self, context): def execute(self, context: Context) -> Set[str]:
mat = context.active_object.active_material mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Creating sphere texture for material: {mat.name} from {self.filepath}")
fnMat = FnMaterial(mat) fnMat = FnMaterial(mat)
fnMat.create_sphere_texture(self.filepath, context.active_object) fnMat.create_sphere_texture(self.filepath, context.active_object)
return {"FINISHED"} return {"FINISHED"}
@@ -183,8 +210,13 @@ class RemoveSphereTexture(Operator):
bl_description = "Remove sphere texture of active material" bl_description = "Remove sphere texture of active material"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: Context) -> Set[str]:
mat = context.active_object.active_material mat = context.active_object.active_material
if not mat:
logger.error("No active material found")
return {"CANCELLED"}
logger.info(f"Removing sphere texture from material: {mat.name}")
fnMat = FnMaterial(mat) fnMat = FnMaterial(mat)
fnMat.remove_sphere_texture() fnMat.remove_sphere_texture()
return {"FINISHED"} return {"FINISHED"}
@@ -197,18 +229,21 @@ class MoveMaterialUp(Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
obj = context.active_object obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
return valid_mesh and obj.active_material_index > 0 return bool(valid_mesh and obj.active_material_index > 0)
def execute(self, context): def execute(self, context: Context) -> Set[str]:
obj = context.active_object obj = context.active_object
current_idx = obj.active_material_index current_idx = obj.active_material_index
prev_index = current_idx - 1 prev_index = current_idx - 1
logger.debug(f"Moving material {current_idx} up to position {prev_index} for object {obj.name}")
try: try:
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True) FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
except MaterialNotFoundError: except MaterialNotFoundError:
logger.error(f"Materials not found for indices {current_idx} and {prev_index}")
self.report({"ERROR"}, "Materials not found") self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"} return {"CANCELLED"}
obj.active_material_index = prev_index obj.active_material_index = prev_index
@@ -223,18 +258,21 @@ class MoveMaterialDown(Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
obj = context.active_object obj = context.active_object
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
return valid_mesh and obj.active_material_index < len(obj.material_slots) - 1 return bool(valid_mesh and obj.active_material_index < len(obj.material_slots) - 1)
def execute(self, context): def execute(self, context: Context) -> Set[str]:
obj = context.active_object obj = context.active_object
current_idx = obj.active_material_index current_idx = obj.active_material_index
next_index = current_idx + 1 next_index = current_idx + 1
logger.debug(f"Moving material {current_idx} down to position {next_index} for object {obj.name}")
try: try:
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True) FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
except MaterialNotFoundError: except MaterialNotFoundError:
logger.error(f"Materials not found for indices {current_idx} and {next_index}")
self.report({"ERROR"}, "Materials not found") self.report({"ERROR"}, "Materials not found")
return {"CANCELLED"} return {"CANCELLED"}
obj.active_material_index = next_index obj.active_material_index = next_index
@@ -257,26 +295,31 @@ class EdgePreviewSetup(Operator):
default="CREATE", default="CREATE",
) )
def execute(self, context): def execute(self, context: Context) -> Set[str]:
from ..core.model import FnModel from ..core.model import FnModel
root = FnModel.find_root_object(context.active_object) root = FnModel.find_root_object(context.active_object)
if root is None: if root is None:
logger.error("No MMD model root found")
self.report({"ERROR"}, "Select a MMD model") self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"} return {"CANCELLED"}
if self.action == "CLEAN": if self.action == "CLEAN":
logger.info(f"Cleaning toon edge for model: {root.name}")
for obj in FnModel.iterate_mesh_objects(root): for obj in FnModel.iterate_mesh_objects(root):
self.__clean_toon_edge(obj) self.__clean_toon_edge(obj)
else: else:
from ..bpyutils import Props from ..bpyutils import Props
logger.info(f"Creating toon edge for model: {root.name}")
scale = 0.2 * getattr(root, Props.empty_display_size) scale = 0.2 * getattr(root, Props.empty_display_size)
counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root)) counts = sum(self.__create_toon_edge(obj, scale) for obj in FnModel.iterate_mesh_objects(root))
logger.info(f"Created {counts} toon edge(s)")
self.report({"INFO"}, "Created %d toon edge(s)" % counts) self.report({"INFO"}, "Created %d toon edge(s)" % counts)
return {"FINISHED"} return {"FINISHED"}
def __clean_toon_edge(self, obj): def __clean_toon_edge(self, obj: Object) -> None:
logger.debug(f"Cleaning toon edge for object: {obj.name}")
if "mmd_edge_preview" in obj.modifiers: if "mmd_edge_preview" in obj.modifiers:
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"]) obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
@@ -285,7 +328,8 @@ class EdgePreviewSetup(Operator):
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge.")) FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
def __create_toon_edge(self, obj, scale=1.0): def __create_toon_edge(self, obj: Object, scale: float = 1.0) -> int:
logger.debug(f"Creating toon edge for object: {obj.name} with scale {scale}")
self.__clean_toon_edge(obj) self.__clean_toon_edge(obj)
materials = obj.data.materials materials = obj.data.materials
material_offset = len(materials) material_offset = len(materials)
@@ -310,10 +354,10 @@ class EdgePreviewSetup(Operator):
mod.vertex_group = "mmd_edge_preview" mod.vertex_group = "mmd_edge_preview"
return len(materials) - material_offset return len(materials) - material_offset
def __create_edge_preview_group(self, obj): def __create_edge_preview_group(self, obj: Object) -> None:
vertices, materials = obj.data.vertices, obj.data.materials vertices, materials = obj.data.vertices, obj.data.materials
weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m} weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m}
scale_map = {} scale_map: Dict[int, float] = {}
vg_scale_index = obj.vertex_groups.find("mmd_edge_scale") vg_scale_index = obj.vertex_groups.find("mmd_edge_scale")
if vg_scale_index >= 0: if vg_scale_index >= 0:
scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index} scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index}
@@ -322,7 +366,7 @@ class EdgePreviewSetup(Operator):
weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02 weight = scale_map.get(i, 1.0) * weight_map.get(mi, 1.0) * 0.02
vg_edge_preview.add(index=[i], weight=weight, type="REPLACE") vg_edge_preview.add(index=[i], weight=weight, type="REPLACE")
def __get_edge_material(self, mat_name, edge_color, materials): def __get_edge_material(self, mat_name: str, edge_color: Tuple[float, float, float, float], materials: List[Material]) -> Material:
if mat_name in materials: if mat_name in materials:
return materials[mat_name] return materials[mat_name]
mat = bpy.data.materials.get(mat_name, None) mat = bpy.data.materials.get(mat_name, None)
@@ -340,7 +384,7 @@ class EdgePreviewSetup(Operator):
self.__make_shader(mat) self.__make_shader(mat)
return mat return mat
def __make_shader(self, m): def __make_shader(self, m: Material) -> None:
m.use_nodes = True m.use_nodes = True
nodes, links = m.node_tree.nodes, m.node_tree.links nodes, links = m.node_tree.nodes, m.node_tree.links
@@ -361,7 +405,7 @@ class EdgePreviewSetup(Operator):
node_shader.inputs["Color"].default_value = m.mmd_material.edge_color node_shader.inputs["Color"].default_value = m.mmd_material.edge_color
node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3] node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3]
def __get_edge_preview_shader(self): def __get_edge_preview_shader(self) -> bpy.types.NodeTree:
group_name = "MMDEdgePreview" group_name = "MMDEdgePreview"
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
if len(shader.nodes): if len(shader.nodes):
+56 -27
View File
@@ -6,14 +6,17 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re import re
from typing import List, Dict, Any, Set, Optional, Tuple, Union, Type
import bpy import bpy
from bpy.types import Context, Object, Operator, ShapeKey
from .. import utils from .. import utils
from ..bpyutils import FnContext, FnObject from ..bpyutils import FnContext, FnObject
from ..core.bone import FnBone from ..core.bone import FnBone
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ..core.morph import FnMorph from ..core.morph import FnMorph
from ....core.logging_setup import logger
class SelectObject(bpy.types.Operator): class SelectObject(bpy.types.Operator):
@@ -29,7 +32,8 @@ class SelectObject(bpy.types.Operator):
options={"HIDDEN", "SKIP_SAVE"}, options={"HIDDEN", "SKIP_SAVE"},
) )
def execute(self, context): def execute(self, context: Context) -> Set[str]:
logger.debug(f"Selecting object: {self.name}")
utils.selectAObject(context.scene.objects[self.name]) utils.selectAObject(context.scene.objects[self.name])
return {"FINISHED"} return {"FINISHED"}
@@ -43,41 +47,43 @@ class MoveObject(bpy.types.Operator, utils.ItemMoveOp):
__PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)") __PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
@classmethod @classmethod
def set_index(cls, obj, index): def set_index(cls, obj: Object, index: int) -> None:
m = cls.__PREFIX_REGEXP.match(obj.name) m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name name = m.group("name") if m else obj.name
obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name) obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name)
@classmethod @classmethod
def get_name(cls, obj, prefix=None): def get_name(cls, obj: Object, prefix: Optional[str] = None) -> str:
m = cls.__PREFIX_REGEXP.match(obj.name) m = cls.__PREFIX_REGEXP.match(obj.name)
name = m.group("name") if m else obj.name name = m.group("name") if m else obj.name
return name[len(prefix) :] if prefix and name.startswith(prefix) else name return name[len(prefix) :] if prefix and name.startswith(prefix) else name
@classmethod @classmethod
def normalize_indices(cls, objects): def normalize_indices(cls, objects: List[Object]) -> None:
for i, x in enumerate(objects): for i, x in enumerate(objects):
cls.set_index(x, i) cls.set_index(x, i)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return context.active_object return context.active_object is not None
def execute(self, context): def execute(self, context: Context) -> Set[str]:
obj = context.active_object obj = context.active_object
objects = self.__get_objects(obj) objects = self.__get_objects(obj)
if obj not in objects: if obj not in objects:
self.report({"ERROR"}, 'Can not move object "%s"' % obj.name) logger.error(f'Cannot move object "{obj.name}"')
self.report({"ERROR"}, f'Can not move object "{obj.name}"')
return {"CANCELLED"} return {"CANCELLED"}
objects.sort(key=lambda x: x.name) objects.sort(key=lambda x: x.name)
logger.debug(f"Moving object {obj.name} {self.type}")
self.move(objects, objects.index(obj), self.type) self.move(objects, objects.index(obj), self.type)
self.normalize_indices(objects) self.normalize_indices(objects)
return {"FINISHED"} return {"FINISHED"}
def __get_objects(self, obj): def __get_objects(self, obj: Object) -> Any:
class __MovableList(list): class __MovableList(list):
def move(self, index_old, index_new): def move(self, index_old: int, index_new: int) -> None:
item = self[index_old] item = self[index_old]
self.remove(item) self.remove(item)
self.insert(index_new, item) self.insert(index_new, item)
@@ -102,11 +108,11 @@ class CleanShapeKeys(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return any(o.type == "MESH" for o in context.selected_objects) return any(o.type == "MESH" for o in context.selected_objects)
@staticmethod @staticmethod
def __can_remove(key_block): def __can_remove(key_block: ShapeKey) -> bool:
if key_block.relative_key == key_block: if key_block.relative_key == key_block:
return False # Basis return False # Basis
for v0, v1 in zip(key_block.relative_key.data, key_block.data): for v0, v1 in zip(key_block.relative_key.data, key_block.data):
@@ -114,20 +120,24 @@ class CleanShapeKeys(bpy.types.Operator):
return False return False
return True return True
def __shape_key_clean(self, obj, key_blocks): def __shape_key_clean(self, obj: Object, key_blocks: List[ShapeKey]) -> None:
for kb in key_blocks: for kb in key_blocks:
if self.__can_remove(kb): if self.__can_remove(kb):
logger.debug(f"Removing unused shape key: {kb.name} from {obj.name}")
FnObject.mesh_remove_shape_key(obj, kb) FnObject.mesh_remove_shape_key(obj, kb)
if len(key_blocks) == 1: if len(key_blocks) == 1:
logger.debug(f"Removing single shape key: {key_blocks[0].name} from {obj.name}")
FnObject.mesh_remove_shape_key(obj, key_blocks[0]) FnObject.mesh_remove_shape_key(obj, key_blocks[0])
def execute(self, context): def execute(self, context: Context) -> Set[str]:
obj: bpy.types.Object logger.info("Cleaning shape keys for selected objects")
obj: Object
for obj in context.selected_objects: for obj in context.selected_objects:
if obj.type != "MESH" or obj.data.shape_keys is None: if obj.type != "MESH" or obj.data.shape_keys is None:
continue continue
if not obj.data.shape_keys.use_relative: if not obj.data.shape_keys.use_relative:
continue # not be considered yet continue # not be considered yet
logger.debug(f"Processing shape keys for {obj.name}")
self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks) self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks)
return {"FINISHED"} return {"FINISHED"}
@@ -144,21 +154,25 @@ class SeparateByMaterials(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
obj = context.active_object obj = context.active_object
return obj and obj.type == "MESH" return obj and obj.type == "MESH"
def __separate_by_materials(self, obj): def __separate_by_materials(self, obj: Object) -> None:
logger.info(f"Separating {obj.name} by materials")
utils.separateByMaterials(obj) utils.separateByMaterials(obj)
if self.clean_shape_keys: if self.clean_shape_keys:
logger.debug("Cleaning shape keys after separation")
bpy.ops.mmd_tools.clean_shape_keys() bpy.ops.mmd_tools.clean_shape_keys()
def execute(self, context): def execute(self, context: Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
if root is None: if root is None:
logger.debug("No root object found, separating single object")
self.__separate_by_materials(obj) self.__separate_by_materials(obj)
else: else:
logger.debug(f"Root object found: {root.name}, preparing for separation")
bpy.ops.mmd_tools.clear_temp_materials() bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view() bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -171,9 +185,11 @@ class SeparateByMaterials(bpy.types.Operator):
if len(mesh.data.materials) > 0: if len(mesh.data.materials) > 0:
mat = mesh.data.materials[0] mat = mesh.data.materials[0]
idx = mat_names.index(getattr(mat, "name", None)) idx = mat_names.index(getattr(mat, "name", None))
logger.debug(f"Setting index {idx} for mesh {mesh.name}")
MoveObject.set_index(mesh, idx) MoveObject.set_index(mesh, idx)
for morph in root.mmd_root.material_morphs: for morph in root.mmd_root.material_morphs:
logger.debug(f"Updating material morph: {morph.name}")
FnMorph(morph, rig).update_mat_related_mesh() FnMorph(morph, rig).update_mat_related_mesh()
utils.clearUnusedMeshes() utils.clearUnusedMeshes()
return {"FINISHED"} return {"FINISHED"}
@@ -191,13 +207,15 @@ class JoinMeshes(bpy.types.Operator):
default=True, default=True,
) )
def execute(self, context): def execute(self, context: Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
if root is None: if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model") self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"} return {"CANCELLED"}
logger.info(f"Joining meshes for model: {root.name}")
bpy.ops.mmd_tools.clear_temp_materials() bpy.ops.mmd_tools.clear_temp_materials()
bpy.ops.mmd_tools.clear_uv_morph_view() bpy.ops.mmd_tools.clear_uv_morph_view()
@@ -205,9 +223,11 @@ class JoinMeshes(bpy.types.Operator):
rig = Model(root) rig = Model(root)
meshes_list = sorted(rig.meshes(), key=lambda x: x.name) meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
if not meshes_list: if not meshes_list:
logger.error("No meshes found in the model")
self.report({"ERROR"}, "The model does not have any meshes") self.report({"ERROR"}, "The model does not have any meshes")
return {"CANCELLED"} return {"CANCELLED"}
active_mesh = meshes_list[0] active_mesh = meshes_list[0]
logger.debug(f"Found {len(meshes_list)} meshes, using {active_mesh.name} as active")
FnContext.select_objects(context, *meshes_list) FnContext.select_objects(context, *meshes_list)
FnContext.set_active_object(context, active_mesh) FnContext.set_active_object(context, active_mesh)
@@ -216,15 +236,19 @@ class JoinMeshes(bpy.types.Operator):
for m in meshes_list[1:]: for m in meshes_list[1:]:
for mat in m.data.materials: for mat in m.data.materials:
if mat not in active_mesh.data.materials[:]: if mat not in active_mesh.data.materials[:]:
logger.debug(f"Adding material {mat.name} to active mesh")
active_mesh.data.materials.append(mat) active_mesh.data.materials.append(mat)
# Join selected meshes # Join selected meshes
logger.debug("Joining meshes")
bpy.ops.object.join() bpy.ops.object.join()
if self.sort_shape_keys: if self.sort_shape_keys:
logger.debug("Sorting shape keys")
FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys()) FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys())
active_mesh.active_shape_key_index = 0 active_mesh.active_shape_key_index = 0
for morph in root.mmd_root.material_morphs: for morph in root.mmd_root.material_morphs:
logger.debug(f"Updating material morph: {morph.name}")
FnMorph(morph, rig).update_mat_related_mesh(active_mesh) FnMorph(morph, rig).update_mat_related_mesh(active_mesh)
utils.clearUnusedMeshes() utils.clearUnusedMeshes()
return {"FINISHED"} return {"FINISHED"}
@@ -238,17 +262,20 @@ class AttachMeshesToMMD(bpy.types.Operator):
add_armature_modifier: bpy.props.BoolProperty(default=True) add_armature_modifier: bpy.props.BoolProperty(default=True)
def execute(self, context: bpy.types.Context): def execute(self, context: Context) -> Set[str]:
root = FnModel.find_root_object(context.active_object) root = FnModel.find_root_object(context.active_object)
if root is None: if root is None:
logger.error("No MMD model found")
self.report({"ERROR"}, "Select a MMD model") self.report({"ERROR"}, "Select a MMD model")
return {"CANCELLED"} return {"CANCELLED"}
armObj = FnModel.find_armature_object(root) armObj = FnModel.find_armature_object(root)
if armObj is None: if armObj is None:
logger.error("Model armature not found")
self.report({"ERROR"}, "Model Armature not found") self.report({"ERROR"}, "Model Armature not found")
return {"CANCELLED"} return {"CANCELLED"}
logger.info(f"Attaching meshes to model: {root.name}")
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier) FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
return {"FINISHED"} return {"FINISHED"}
@@ -268,17 +295,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None return FnModel.find_root_object(context.active_object) is not None
def invoke(self, context, event): def invoke(self, context: Context, event: Any) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
def execute(self, context): def execute(self, context: Context) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
logger.info(f"Changing IK loop factor to {self.mmd_ik_loop_factor} for model: {root_object.name}")
FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor) FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor)
return {"FINISHED"} return {"FINISHED"}
@@ -290,21 +318,22 @@ class RecalculateBoneRoll(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
obj = context.active_object obj = context.active_object
return obj and obj.type == "ARMATURE" return obj and obj.type == "ARMATURE"
def invoke(self, context, event): def invoke(self, context: Context, event: Any) -> Set[str]:
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
def draw(self, context): def draw(self, context: Context) -> None:
layout = self.layout layout = self.layout
c = layout.column() c = layout.column()
c.label(text="This operation will break existing f-curve/action.", icon="QUESTION") c.label(text="This operation will break existing f-curve/action.", icon="QUESTION")
c.label(text="Click [OK] to run the operation.") c.label(text="Click [OK] to run the operation.")
def execute(self, context): def execute(self, context: Context) -> Set[str]:
arm = context.active_object arm = context.active_object
logger.info(f"Recalculating bone roll for armature: {arm.name}")
FnBone.apply_auto_bone_roll(arm) FnBone.apply_auto_bone_roll(arm)
return {"FINISHED"} return {"FINISHED"}
+61 -24
View File
@@ -6,10 +6,12 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from typing import Optional, Set, Dict, Any, List, Tuple, Union
from ..bpyutils import FnContext from ..bpyutils import FnContext
from ..core.bone import FnBone, MigrationFnBone from ..core.bone import FnBone, MigrationFnBone
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MorphSliderSetup(bpy.types.Operator): class MorphSliderSetup(bpy.types.Operator):
@@ -29,18 +31,22 @@ class MorphSliderSetup(bpy.types.Operator):
default="CREATE", default="CREATE",
) )
def execute(self, context: bpy.types.Context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None assert root_object is not None
logger.debug(f"Executing MorphSliderSetup with type: {self.type}")
with FnContext.temp_override_active_layer_collection(context, root_object): with FnContext.temp_override_active_layer_collection(context, root_object):
rig = Model(root_object) rig = Model(root_object)
if self.type == "BIND": if self.type == "BIND":
logger.info(f"Binding morph sliders for {root_object.name}")
rig.morph_slider.bind() rig.morph_slider.bind()
elif self.type == "UNBIND": elif self.type == "UNBIND":
logger.info(f"Unbinding morph sliders for {root_object.name}")
rig.morph_slider.unbind() rig.morph_slider.unbind()
else: else:
logger.info(f"Creating morph sliders for {root_object.name}")
rig.morph_slider.create() rig.morph_slider.create()
FnContext.set_active_object(context, active_object) FnContext.set_active_object(context, active_object)
@@ -53,10 +59,11 @@ class CleanRiggingObjects(bpy.types.Operator):
bl_description = "Delete temporary physics objects of selected object and revert physics to default MMD state" bl_description = "Delete temporary physics objects of selected object and revert physics to default MMD state"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
assert root_object is not None assert root_object is not None
logger.info(f"Cleaning rig for {root_object.name}")
rig = Model(root_object) rig = Model(root_object)
rig.clean() rig.clean()
FnContext.set_active_object(context, root_object) FnContext.set_active_object(context, root_object)
@@ -86,9 +93,10 @@ class BuildRig(bpy.types.Operator):
default=1e-06, default=1e-06,
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
logger.info(f"Building rig for {root_object.name} with non_collision_distance_scale={self.non_collision_distance_scale}, collision_margin={self.collision_margin}")
with FnContext.temp_override_active_layer_collection(context, root_object): with FnContext.temp_override_active_layer_collection(context, root_object):
rig = Model(root_object) rig = Model(root_object)
rig.build(self.non_collision_distance_scale, self.collision_margin) rig.build(self.non_collision_distance_scale, self.collision_margin)
@@ -103,11 +111,14 @@ class CleanAdditionalTransformConstraints(bpy.types.Operator):
bl_description = "Delete shadow bones of selected object and revert bones to default MMD state" bl_description = "Delete shadow bones of selected object and revert bones to default MMD state"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None assert root_object is not None
FnBone.clean_additional_transformation(FnModel.find_armature_object(root_object))
logger.info(f"Cleaning additional transform constraints for {root_object.name}")
armature_object = FnModel.find_armature_object(root_object)
FnBone.clean_additional_transformation(armature_object)
FnContext.set_active_object(context, active_object) FnContext.set_active_object(context, active_object)
return {"FINISHED"} return {"FINISHED"}
@@ -118,11 +129,12 @@ class ApplyAdditionalTransformConstraints(bpy.types.Operator):
bl_description = "Translate appended bones of selected object for Blender" bl_description = "Translate appended bones of selected object for Blender"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None assert root_object is not None
logger.info(f"Applying additional transform constraints for {root_object.name}")
armature_object = FnModel.find_armature_object(root_object) armature_object = FnModel.find_armature_object(root_object)
assert armature_object is not None assert armature_object is not None
@@ -149,12 +161,14 @@ class SetupBoneFixedAxes(bpy.types.Operator):
default="LOAD", default="LOAD",
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
armature_object = context.active_object armature_object = context.active_object
if not armature_object or armature_object.type != "ARMATURE": if not armature_object or armature_object.type != "ARMATURE":
self.report({"ERROR"}, "Active object is not an armature object") self.report({"ERROR"}, "Active object is not an armature object")
logger.error("Setup Bone Fixed Axis failed: Active object is not an armature object")
return {"CANCELLED"} return {"CANCELLED"}
logger.info(f"Setting up bone fixed axes with type: {self.type}")
if self.type == "APPLY": if self.type == "APPLY":
FnBone.apply_bone_fixed_axis(armature_object) FnBone.apply_bone_fixed_axis(armature_object)
FnBone.apply_additional_transformation(armature_object) FnBone.apply_additional_transformation(armature_object)
@@ -180,12 +194,14 @@ class SetupBoneLocalAxes(bpy.types.Operator):
default="LOAD", default="LOAD",
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
armature_object = context.active_object armature_object = context.active_object
if not armature_object or armature_object.type != "ARMATURE": if not armature_object or armature_object.type != "ARMATURE":
self.report({"ERROR"}, "Active object is not an armature object") self.report({"ERROR"}, "Active object is not an armature object")
logger.error("Setup Bone Local Axes failed: Active object is not an armature object")
return {"CANCELLED"} return {"CANCELLED"}
logger.info(f"Setting up bone local axes with type: {self.type}")
if self.type == "APPLY": if self.type == "APPLY":
FnBone.apply_bone_local_axes(armature_object) FnBone.apply_bone_local_axes(armature_object)
FnBone.apply_additional_transformation(armature_object) FnBone.apply_additional_transformation(armature_object)
@@ -207,16 +223,18 @@ class AddMissingVertexGroupsFromBones(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context: bpy.types.Context): def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None return FnModel.find_root_object(context.active_object) is not None
def execute(self, context: bpy.types.Context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object: bpy.types.Object = context.active_object active_object: bpy.types.Object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None assert root_object is not None
logger.info(f"Adding missing vertex groups from bones for {root_object.name}, search_in_all_meshes={self.search_in_all_meshes}")
bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root_object) bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root_object)
if bone_order_mesh_object is None: if bone_order_mesh_object is None:
logger.error("Failed to find bone order mesh object")
return {"CANCELLED"} return {"CANCELLED"}
FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes) FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes)
@@ -246,12 +264,13 @@ class CreateMMDModelRoot(bpy.types.Operator):
default=0.08, default=0.08,
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info(f"Creating MMD model root object with name_j={self.name_j}, name_e={self.name_e}, scale={self.scale}")
rig = Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True) rig = Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True)
rig.initialDisplayFrames() rig.initialDisplayFrames()
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
@@ -305,15 +324,16 @@ class ConvertToMMDModel(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object obj = context.active_object
return obj and obj.type == "ARMATURE" and obj.mode != "EDIT" return obj and obj.type == "ARMATURE" and obj.mode != "EDIT"
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info(f"Converting to MMD model with scale={self.scale}, convert_material_nodes={self.convert_material_nodes}")
# TODO convert some basic MMD properties # TODO convert some basic MMD properties
armature_object = context.active_object armature_object = context.active_object
scale = self.scale scale = self.scale
@@ -321,29 +341,31 @@ class ConvertToMMDModel(bpy.types.Operator):
root_object = FnModel.find_root_object(armature_object) root_object = FnModel.find_root_object(armature_object)
if root_object is None or root_object != armature_object.parent: if root_object is None or root_object != armature_object.parent:
logger.debug("Creating new MMD model")
Model.create(model_name, model_name, scale, armature_object=armature_object) Model.create(model_name, model_name, scale, armature_object=armature_object)
self.__attach_meshes_to(armature_object, FnContext.get_scene_objects(context)) self.__attach_meshes_to(armature_object, FnContext.get_scene_objects(context))
self.__configure_rig(context, Model(armature_object.parent)) self.__configure_rig(context, Model(armature_object.parent))
return {"FINISHED"} return {"FINISHED"}
def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects): def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects) -> None:
def __is_child_of_armature(mesh): def __is_child_of_armature(mesh: bpy.types.Object) -> bool:
if mesh.parent is None: if mesh.parent is None:
return False return False
return mesh.parent == armature_object or __is_child_of_armature(mesh.parent) return mesh.parent == armature_object or __is_child_of_armature(mesh.parent)
def __is_using_armature(mesh): def __is_using_armature(mesh: bpy.types.Object) -> bool:
for m in mesh.modifiers: for m in mesh.modifiers:
if m.type == "ARMATURE" and m.object == armature_object: if m.type == "ARMATURE" and m.object == armature_object:
return True return True
return False return False
def __get_root(mesh): def __get_root(mesh: bpy.types.Object) -> bpy.types.Object:
if mesh.parent is None: if mesh.parent is None:
return mesh return mesh
return __get_root(mesh.parent) return __get_root(mesh.parent)
attached_count = 0
for x in objects: for x in objects:
if __is_using_armature(x) and not __is_child_of_armature(x): if __is_using_armature(x) and not __is_child_of_armature(x):
x_root = __get_root(x) x_root = __get_root(x)
@@ -351,27 +373,35 @@ class ConvertToMMDModel(bpy.types.Operator):
x_root.parent_type = "OBJECT" x_root.parent_type = "OBJECT"
x_root.parent = armature_object x_root.parent = armature_object
x_root.matrix_world = m x_root.matrix_world = m
attached_count += 1
logger.debug(f"Attached {attached_count} meshes to armature")
def __configure_rig(self, context: bpy.types.Context, mmd_model: Model): def __configure_rig(self, context: bpy.types.Context, mmd_model: Model) -> None:
root_object = mmd_model.rootObject() root_object = mmd_model.rootObject()
armature_object = mmd_model.armature() armature_object = mmd_model.armature()
mesh_objects = tuple(mmd_model.meshes()) mesh_objects = tuple(mmd_model.meshes())
logger.info(f"Configuring rig for {root_object.name} with {len(mesh_objects)} meshes")
mmd_model.loadMorphs() mmd_model.loadMorphs()
if self.middle_joint_bones_lock: if self.middle_joint_bones_lock:
vertex_groups = {g.name for mesh in mesh_objects for g in mesh.vertex_groups} vertex_groups = {g.name for mesh in mesh_objects for g in mesh.vertex_groups}
locked_bones = 0
for pose_bone in armature_object.pose.bones: for pose_bone in armature_object.pose.bones:
if not pose_bone.parent: if not pose_bone.parent:
continue continue
if not pose_bone.bone.use_connect and pose_bone.name not in vertex_groups: if not pose_bone.bone.use_connect and pose_bone.name not in vertex_groups:
continue continue
pose_bone.lock_location = (True, True, True) pose_bone.lock_location = (True, True, True)
locked_bones += 1
logger.debug(f"Locked {locked_bones} middle joint bones")
from ..core.material import FnMaterial from ..core.material import FnMaterial
FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes) FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes)
try: try:
converted_materials = 0
for m in (x for mesh in mesh_objects for x in mesh.data.materials if x): for m in (x for mesh in mesh_objects for x in mesh.data.materials if x):
FnMaterial.convert_to_mmd_material(m, context) FnMaterial.convert_to_mmd_material(m, context)
mmd_material = m.mmd_material mmd_material = m.mmd_material
@@ -384,6 +414,8 @@ class ConvertToMMDModel(bpy.types.Operator):
line_color = list(m.line_color) line_color = list(m.line_color)
mmd_material.enabled_toon_edge = line_color[3] >= self.edge_threshold mmd_material.enabled_toon_edge = line_color[3] >= self.edge_threshold
mmd_material.edge_color = line_color[:3] + [max(line_color[3], self.edge_alpha_min)] mmd_material.edge_color = line_color[:3] + [max(line_color[3], self.edge_alpha_min)]
converted_materials += 1
logger.debug(f"Converted {converted_materials} materials")
finally: finally:
FnMaterial.set_nodes_are_readonly(False) FnMaterial.set_nodes_are_readonly(False)
from .display_item import DisplayItemQuickSetup from .display_item import DisplayItemQuickSetup
@@ -400,16 +432,17 @@ class ResetObjectVisibility(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod @classmethod
def poll(cls, context: bpy.types.Context): def poll(cls, context: bpy.types.Context) -> bool:
active_object: bpy.types.Object = context.active_object active_object: bpy.types.Object = context.active_object
return FnModel.find_root_object(active_object) is not None return FnModel.find_root_object(active_object) is not None
def execute(self, context: bpy.types.Context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object: bpy.types.Object = context.active_object active_object: bpy.types.Object = context.active_object
mmd_root_object = FnModel.find_root_object(active_object) mmd_root_object = FnModel.find_root_object(active_object)
assert mmd_root_object is not None assert mmd_root_object is not None
mmd_root = mmd_root_object.mmd_root mmd_root = mmd_root_object.mmd_root
logger.info(f"Resetting object visibility for {mmd_root_object.name}")
mmd_root_object.hide_set(False) mmd_root_object.hide_set(False)
rigid_group_object = FnModel.find_rigid_group_object(mmd_root_object) rigid_group_object = FnModel.find_rigid_group_object(mmd_root_object)
@@ -440,11 +473,12 @@ class AssembleAll(bpy.types.Operator):
bl_label = "Assemble All" bl_label = "Assemble All"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None assert root_object is not None
logger.info(f"Assembling all components for {root_object.name}")
with FnContext.temp_override_active_layer_collection(context, root_object) as context: with FnContext.temp_override_active_layer_collection(context, root_object) as context:
rig = Model(root_object) rig = Model(root_object)
MigrationFnBone.fix_mmd_ik_limit_override(rig.armature()) MigrationFnBone.fix_mmd_ik_limit_override(rig.armature())
@@ -452,6 +486,7 @@ class AssembleAll(bpy.types.Operator):
rig.build() rig.build()
rig.morph_slider.bind() rig.morph_slider.bind()
logger.debug("Binding SDEF weights")
with context.temp_override(selected_objects=[active_object]): with context.temp_override(selected_objects=[active_object]):
bpy.ops.mmd_tools.sdef_bind() bpy.ops.mmd_tools.sdef_bind()
root_object.mmd_root.use_property_driver = True root_object.mmd_root.use_property_driver = True
@@ -466,13 +501,15 @@ class DisassembleAll(bpy.types.Operator):
bl_label = "Disassemble All" bl_label = "Disassemble All"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = FnModel.find_root_object(active_object) root_object = FnModel.find_root_object(active_object)
assert root_object is not None assert root_object is not None
logger.info(f"Disassembling all components for {root_object.name}")
with FnContext.temp_override_active_layer_collection(context, root_object) as context: with FnContext.temp_override_active_layer_collection(context, root_object) as context:
root_object.mmd_root.use_property_driver = False root_object.mmd_root.use_property_driver = False
logger.debug("Unbinding SDEF weights")
with context.temp_override(selected_objects=[active_object]): with context.temp_override(selected_objects=[active_object]):
bpy.ops.mmd_tools.sdef_unbind() bpy.ops.mmd_tools.sdef_unbind()
+72 -31
View File
@@ -7,13 +7,17 @@
import itertools import itertools
from operator import itemgetter from operator import itemgetter
from typing import Dict, List, Optional, Set from typing import Dict, List, Optional, Set, Tuple, Any
import bmesh import bmesh
import bpy import bpy
import numpy as np
import numpy.typing as npt
from bpy.types import Context, Object, Operator, EditBone, Mesh, Armature
from ..bpyutils import FnContext from ..bpyutils import FnContext
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ....core.logging_setup import logger
class MessageException(Exception): class MessageException(Exception):
@@ -35,8 +39,8 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context: bpy.types.Context): def poll(cls, context: Context) -> bool:
active_object: Optional[bpy.types.Object] = context.active_object active_object: Optional[Object] = context.active_object
if context.mode != "POSE": if context.mode != "POSE":
return False return False
@@ -52,19 +56,22 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0 return len(context.selected_pose_bones) > 0
def invoke(self, context, event): def invoke(self, context: Context, event: Any) -> Set[str]:
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context): def execute(self, context: Context) -> Set[str]:
try: try:
logger.info("Starting model join by bones operation")
self.join(context) self.join(context)
logger.info("Model join by bones completed successfully")
except MessageException as ex: except MessageException as ex:
logger.error(f"Model join by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex)) self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"} return {"CANCELLED"}
return {"FINISHED"} return {"FINISHED"}
def join(self, context: bpy.types.Context): def join(self, context: Context) -> None:
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
parent_root_object = FnModel.find_root_object(context.active_object) parent_root_object = FnModel.find_root_object(context.active_object)
@@ -74,6 +81,7 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
if parent_root_object is None or len(child_root_objects) == 0: if parent_root_object is None or len(child_root_objects) == 0:
raise MessageException("No MMD Models selected") raise MessageException("No MMD Models selected")
logger.debug(f"Joining {len(child_root_objects)} models into parent model: {parent_root_object.name}")
with FnContext.temp_override_active_layer_collection(context, parent_root_object): with FnContext.temp_override_active_layer_collection(context, parent_root_object):
FnModel.join_models(parent_root_object, child_root_objects) FnModel.join_models(parent_root_object, child_root_objects)
@@ -82,11 +90,12 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
# Connect child bones # Connect child bones
if self.join_type == "CONNECTED": if self.join_type == "CONNECTED":
parent_edit_bone: bpy.types.EditBone = context.active_bone parent_edit_bone: EditBone = context.active_bone
child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones) child_edit_bones: Set[EditBone] = set(context.selected_bones)
child_edit_bones.remove(parent_edit_bone) child_edit_bones.remove(parent_edit_bone)
child_edit_bone: bpy.types.EditBone logger.debug(f"Connecting {len(child_edit_bones)} child bones to parent bone: {parent_edit_bone.name}")
child_edit_bone: EditBone
for child_edit_bone in child_edit_bones: for child_edit_bone in child_edit_bones:
child_edit_bone.use_connect = True child_edit_bone.use_connect = True
@@ -111,8 +120,8 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context: bpy.types.Context): def poll(cls, context: Context) -> bool:
active_object: Optional[bpy.types.Object] = context.active_object active_object: Optional[Object] = context.active_object
if context.mode != "POSE": if context.mode != "POSE":
return False return False
@@ -128,56 +137,70 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
return len(context.selected_pose_bones) > 0 return len(context.selected_pose_bones) > 0
def invoke(self, context, event): def invoke(self, context: Context, event: Any) -> Set[str]:
return context.window_manager.invoke_props_dialog(self) return context.window_manager.invoke_props_dialog(self)
def execute(self, context: bpy.types.Context): def execute(self, context: Context) -> Set[str]:
try: try:
logger.info("Starting model separate by bones operation")
self.separate(context) self.separate(context)
logger.info("Model separate by bones completed successfully")
except MessageException as ex: except MessageException as ex:
logger.error(f"Model separate by bones failed: {str(ex)}")
self.report(type={"ERROR"}, message=str(ex)) self.report(type={"ERROR"}, message=str(ex))
return {"CANCELLED"} return {"CANCELLED"}
return {"FINISHED"} return {"FINISHED"}
def separate(self, context: bpy.types.Context): def separate(self, context: Context) -> None:
weight_threshold: float = self.weight_threshold weight_threshold: float = self.weight_threshold
mmd_scale = 0.08 mmd_scale = 0.08
target_armature_object: bpy.types.Object = context.active_object target_armature_object: Object = context.active_object
logger.debug(f"Target armature: {target_armature_object.name}")
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
root_bones: Set[bpy.types.EditBone] = set(context.selected_bones) root_bones: Set[EditBone] = set(context.selected_bones)
logger.debug(f"Selected root bones: {len(root_bones)}")
if self.include_descendant_bones: if self.include_descendant_bones:
logger.debug("Including descendant bones")
for edit_bone in root_bones: for edit_bone in root_bones:
with context.temp_override(active_bone=edit_bone): with context.temp_override(active_bone=edit_bone):
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1) bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
separate_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in context.selected_bones} separate_bones: Dict[str, EditBone] = {b.name: b for b in context.selected_bones}
deform_bones: Dict[str, bpy.types.EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform} deform_bones: Dict[str, EditBone] = {b.name: b for b in target_armature_object.data.edit_bones if b.use_deform}
logger.debug(f"Total bones to separate: {len(separate_bones)}")
mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object) mmd_root_object: Object = FnModel.find_root_object(context.active_object)
mmd_model = Model(mmd_root_object) mmd_model = Model(mmd_root_object)
mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes()) mmd_model_mesh_objects: List[Object] = list(mmd_model.meshes())
logger.debug(f"Found {len(mmd_model_mesh_objects)} mesh objects in model")
mmd_model_mesh_objects = list(self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys()) mesh_selection_result = self.select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold)
mmd_model_mesh_objects = list(mesh_selection_result.keys())
logger.debug(f"Selected {len(mmd_model_mesh_objects)} mesh objects with weighted vertices")
# separate armature bones # separate armature bones
separate_armature_object: Optional[bpy.types.Object] separate_armature_object: Optional[Object]
if self.separate_armature: if self.separate_armature:
logger.debug("Separating armature")
target_armature_object.select_set(True) target_armature_object.select_set(True)
bpy.ops.armature.separate() bpy.ops.armature.separate()
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None) separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object]), None)
if separate_armature_object:
logger.debug(f"Created separate armature: {separate_armature_object.name}")
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
# collect separate rigid bodies # collect separate rigid bodies
separate_rigid_bodies: Set[bpy.types.Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones} separate_rigid_bodies: Set[Object] = {rigid_body_object for rigid_body_object in mmd_model.rigidBodies() if rigid_body_object.mmd_rigid.bone in separate_bones}
logger.debug(f"Found {len(separate_rigid_bodies)} rigid bodies to separate")
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
# collect separate joints # collect separate joints
separate_joints: Set[bpy.types.Object] = { separate_joints: Set[Object] = {
joint_object joint_object
for joint_object in mmd_model.joints() for joint_object in mmd_model.joints()
if boundary_joint_owner_condition( if boundary_joint_owner_condition(
@@ -187,35 +210,43 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
] ]
) )
} }
logger.debug(f"Found {len(separate_joints)} joints to separate")
separate_mesh_objects: Set[bpy.types.Object] separate_mesh_objects: Set[Object]
model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object] model2separate_mesh_objects: Dict[Object, Object]
if len(mmd_model_mesh_objects) == 0: if len(mmd_model_mesh_objects) == 0:
logger.debug("No mesh objects to separate")
separate_mesh_objects = set() separate_mesh_objects = set()
model2separate_mesh_objects = dict() model2separate_mesh_objects = dict()
else: else:
# select meshes # select meshes
obj: bpy.types.Object logger.debug("Selecting meshes for separation")
obj: Object
for obj in context.view_layer.objects: for obj in context.view_layer.objects:
obj.select_set(obj in mmd_model_mesh_objects) obj.select_set(obj in mmd_model_mesh_objects)
context.view_layer.objects.active = mmd_model_mesh_objects[0] context.view_layer.objects.active = mmd_model_mesh_objects[0]
# separate mesh by selected vertices # separate mesh by selected vertices
logger.debug("Separating meshes by selected vertices")
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
bpy.ops.mesh.separate(type="SELECTED") bpy.ops.mesh.separate(type="SELECTED")
separate_mesh_objects: List[bpy.types.Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects] separate_mesh_objects: List[Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="OBJECT")
logger.debug(f"Created {len(separate_mesh_objects)} separate mesh objects")
model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects)) model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects))
logger.debug(f"Creating new model with scale {mmd_scale}")
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False) separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False)
separate_model.initialDisplayFrames() separate_model.initialDisplayFrames()
separate_root_object = separate_model.rootObject() separate_root_object = separate_model.rootObject()
separate_root_object.matrix_world = mmd_root_object.matrix_world separate_root_object.matrix_world = mmd_root_object.matrix_world
separate_model_armature_object = separate_model.armature() separate_model_armature_object = separate_model.armature()
logger.debug(f"Created separate model with root: {separate_root_object.name}")
if self.separate_armature: if self.separate_armature:
logger.debug("Joining separate armature to new model")
with context.temp_override( with context.temp_override(
active_object=separate_model_armature_object, active_object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, separate_armature_object], selected_editable_objects=[separate_model_armature_object, separate_armature_object],
@@ -223,6 +254,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
bpy.ops.object.join() bpy.ops.object.join()
# add mesh # add mesh
logger.debug("Parenting separate mesh objects to new model")
with context.temp_override( with context.temp_override(
object=separate_model_armature_object, object=separate_model_armature_object,
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects], selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects],
@@ -230,19 +262,23 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
# replace mesh armature modifier.object # replace mesh armature modifier.object
logger.debug("Updating armature modifiers on separate meshes")
for separate_mesh in separate_mesh_objects: for separate_mesh in separate_mesh_objects:
armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None) armature_modifier: Optional[bpy.types.ArmatureModifier] = next(iter([m for m in separate_mesh.modifiers if m.type == "ARMATURE"]), None)
if armature_modifier is None: if armature_modifier is None:
logger.debug(f"Creating new armature modifier for {separate_mesh.name}")
armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE") armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE")
armature_modifier.object = separate_model_armature_object armature_modifier.object = separate_model_armature_object
logger.debug("Parenting rigid bodies to new model")
with context.temp_override( with context.temp_override(
object=separate_model.rigidGroupObject(), object=separate_model.rigidGroupObject(),
selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies], selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies],
): ):
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
logger.debug("Parenting joints to new model")
with context.temp_override( with context.temp_override(
object=separate_model.jointGroupObject(), object=separate_model.jointGroupObject(),
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints], selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
@@ -257,10 +293,12 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
assert separate_layer_collection is not None assert separate_layer_collection is not None
if mmd_layer_collection.name != separate_layer_collection.name: if mmd_layer_collection.name != separate_layer_collection.name:
logger.debug(f"Moving objects from collection {mmd_layer_collection.name} to {separate_layer_collection.name}")
for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints): for separate_object in itertools.chain(separate_mesh_objects, separate_rigid_bodies, separate_joints):
separate_layer_collection.collection.objects.link(separate_object) separate_layer_collection.collection.objects.link(separate_object)
mmd_layer_collection.collection.objects.unlink(separate_object) mmd_layer_collection.collection.objects.unlink(separate_object)
logger.debug("Copying MMD root properties")
FnModel.copy_mmd_root( FnModel.copy_mmd_root(
separate_root_object, separate_root_object,
mmd_root_object, mmd_root_object,
@@ -271,13 +309,15 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
}, },
) )
def select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object], separate_bones: Dict[str, bpy.types.EditBone], deform_bones: Dict[str, bpy.types.EditBone], weight_threshold: float) -> Dict[bpy.types.Object, int]: def select_weighted_vertices(self, mmd_model_mesh_objects: List[Object], separate_bones: Dict[str, EditBone], deform_bones: Dict[str, EditBone], weight_threshold: float) -> Dict[Object, int]:
mesh2selected_vertex_count: Dict[bpy.types.Object, int] = dict() """Select vertices weighted to the bones to be separated"""
logger.debug(f"Selecting vertices weighted to {len(separate_bones)} bones with threshold {weight_threshold}")
mesh2selected_vertex_count: Dict[Object, int] = dict()
target_bmesh: bmesh.types.BMesh = bmesh.new() target_bmesh: bmesh.types.BMesh = bmesh.new()
for mesh_object in mmd_model_mesh_objects: for mesh_object in mmd_model_mesh_objects:
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups
mesh: bpy.types.Mesh = mesh_object.data mesh: Mesh = mesh_object.data
target_bmesh.from_mesh(mesh, face_normals=False) target_bmesh.from_mesh(mesh, face_normals=False)
target_bmesh.select_mode |= {"VERT"} target_bmesh.select_mode |= {"VERT"}
deform_layer = target_bmesh.verts.layers.deform.verify() deform_layer = target_bmesh.verts.layers.deform.verify()
@@ -304,6 +344,7 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
vert.select_set(True) vert.select_set(True)
if selected_vertex_count > 0: if selected_vertex_count > 0:
logger.debug(f"Selected {selected_vertex_count} vertices in mesh {mesh_object.name}")
mesh2selected_vertex_count[mesh_object] = selected_vertex_count mesh2selected_vertex_count[mesh_object] = selected_vertex_count
target_bmesh.select_flush_mode() target_bmesh.select_flush_mode()
target_bmesh.to_mesh(mesh) target_bmesh.to_mesh(mesh)
+59 -32
View File
@@ -5,7 +5,7 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Optional, cast from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union
import bpy import bpy
from mathutils import Quaternion, Vector from mathutils import Quaternion, Vector
@@ -16,10 +16,11 @@ from ..core.exceptions import MaterialNotFoundError
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.morph import FnMorph from ..core.morph import FnMorph
from ..utils import ItemMoveOp, ItemOp from ..utils import ItemMoveOp, ItemOp
from ....logging_setup import logger
# Util functions # Util functions
def divide_vector_components(vec1, vec2): def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
if len(vec1) != len(vec2): if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components") raise ValueError("Vectors should have the same number of components")
result = [] result = []
@@ -33,7 +34,7 @@ def divide_vector_components(vec1, vec2):
return result return result
def multiply_vector_components(vec1, vec2): def multiply_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
if len(vec1) != len(vec2): if len(vec1) != len(vec2):
raise ValueError("Vectors should have the same number of components") raise ValueError("Vectors should have the same number of components")
result = [] result = []
@@ -42,7 +43,7 @@ def multiply_vector_components(vec1, vec2):
return result return result
def special_division(n1, n2): def special_division(n1: float, n2: float) -> float:
"""This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised""" """This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised"""
if n2 == 0: if n2 == 0:
if n1 == 0: if n1 == 0:
@@ -58,7 +59,7 @@ class AddMorph(bpy.types.Operator):
bl_description = "Add a morph item to active morph list" bl_description = "Add a morph item to active morph list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -68,6 +69,7 @@ class AddMorph(bpy.types.Operator):
morph.name = "New Morph" morph.name = "New Morph"
if morph_type.startswith("uv"): if morph_type.startswith("uv"):
morph.data_type = "VERTEX_GROUP" morph.data_type = "VERTEX_GROUP"
logger.debug(f"Added new morph of type {morph_type}")
return {"FINISHED"} return {"FINISHED"}
@@ -84,7 +86,7 @@ class RemoveMorph(bpy.types.Operator):
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -99,9 +101,11 @@ class RemoveMorph(bpy.types.Operator):
if self.all: if self.all:
morphs.clear() morphs.clear()
mmd_root.active_morph = 0 mmd_root.active_morph = 0
logger.debug(f"Removed all morphs of type {morph_type}")
else: else:
morphs.remove(mmd_root.active_morph) morphs.remove(mmd_root.active_morph)
mmd_root.active_morph = max(0, mmd_root.active_morph - 1) mmd_root.active_morph = max(0, mmd_root.active_morph - 1)
logger.debug(f"Removed morph at index {mmd_root.active_morph} of type {morph_type}")
return {"FINISHED"} return {"FINISHED"}
@@ -111,7 +115,7 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
bl_description = "Move active morph item up/down in the list" bl_description = "Move active morph item up/down in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -120,6 +124,7 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
mmd_root.active_morph, mmd_root.active_morph,
self.type, self.type,
) )
logger.debug(f"Moved morph to index {mmd_root.active_morph}")
return {"FINISHED"} return {"FINISHED"}
@@ -129,7 +134,7 @@ class CopyMorph(bpy.types.Operator):
bl_description = "Make a copy of active morph in the list" bl_description = "Make a copy of active morph in the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -156,6 +161,7 @@ class CopyMorph(bpy.types.Operator):
for k, v in morph.items(): for k, v in morph.items():
morph_new[k] = v if k != "name" else name_tmp morph_new[k] = v if k != "name" else name_tmp
morph_new.name = name_orig + "_copy" # trigger name check morph_new.name = name_orig + "_copy" # trigger name check
logger.debug(f"Copied morph {name_orig} to {morph_new.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -165,17 +171,17 @@ class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
root = FnModel.find_root_object(context.active_object) root = FnModel.find_root_object(context.active_object)
if root is None: if root is None:
return False return False
return root.mmd_root.active_morph_type == "bone_morphs" return root.mmd_root.active_morph_type == "bone_morphs"
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
root = FnModel.find_root_object(context.active_object) root = FnModel.find_root_object(context.active_object)
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root)) FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
logger.info("Overwrote bone morphs from active action pose")
return {"FINISHED"} return {"FINISHED"}
@@ -185,7 +191,7 @@ class AddMorphOffset(bpy.types.Operator):
bl_description = "Add a morph offset item to the list" bl_description = "Add a morph offset item to the list"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -210,6 +216,7 @@ class AddMorphOffset(bpy.types.Operator):
item.location = pose_bone.location item.location = pose_bone.location
item.rotation = pose_bone.rotation_quaternion item.rotation = pose_bone.rotation_quaternion
logger.debug(f"Added morph offset to {morph_type}")
return {"FINISHED"} return {"FINISHED"}
@@ -226,7 +233,7 @@ class RemoveMorphOffset(bpy.types.Operator):
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -243,17 +250,21 @@ class RemoveMorphOffset(bpy.types.Operator):
if morph_type.startswith("vertex"): if morph_type.startswith("vertex"):
for obj in FnModel.iterate_mesh_objects(root): for obj in FnModel.iterate_mesh_objects(root):
FnMorph.remove_shape_key(obj, morph.name) FnMorph.remove_shape_key(obj, morph.name)
logger.debug(f"Removed all vertex morph offsets for {morph.name}")
return {"FINISHED"} return {"FINISHED"}
elif morph_type.startswith("uv"): elif morph_type.startswith("uv"):
if morph.data_type == "VERTEX_GROUP": if morph.data_type == "VERTEX_GROUP":
for obj in FnModel.iterate_mesh_objects(root): for obj in FnModel.iterate_mesh_objects(root):
FnMorph.store_uv_morph_data(obj, morph) FnMorph.store_uv_morph_data(obj, morph)
logger.debug(f"Removed all UV morph offsets for {morph.name}")
return {"FINISHED"} return {"FINISHED"}
morph.data.clear() morph.data.clear()
morph.active_data = 0 morph.active_data = 0
logger.debug(f"Cleared all morph offsets for {morph.name}")
else: else:
morph.data.remove(morph.active_data) morph.data.remove(morph.active_data)
morph.active_data = max(0, morph.active_data - 1) morph.active_data = max(0, morph.active_data - 1)
logger.debug(f"Removed morph offset at index {morph.active_data}")
return {"FINISHED"} return {"FINISHED"}
@@ -269,7 +280,7 @@ class InitMaterialOffset(bpy.types.Operator):
default=0, default=0,
) )
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -281,6 +292,7 @@ class InitMaterialOffset(bpy.types.Operator):
mat_data.specular_color = mat_data.ambient_color = (val,) * 3 mat_data.specular_color = mat_data.ambient_color = (val,) * 3
mat_data.shininess = mat_data.edge_weight = val mat_data.shininess = mat_data.edge_weight = val
mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4 mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4
logger.debug(f"Initialized material offset with value {val}")
return {"FINISHED"} return {"FINISHED"}
@@ -290,7 +302,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
bl_description = "Calculates the offsets and apply them, then the temporary material is removed" bl_description = "Calculates the offsets and apply them, then the temporary material is removed"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -328,6 +340,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
except ZeroDivisionError: except ZeroDivisionError:
mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD
logger.warning("Zero division detected, switching to ADD offset type")
except ValueError: except ValueError:
self.report({"ERROR"}, "An unexpected error happened") self.report({"ERROR"}, "An unexpected error happened")
# We should stop on our tracks and re-raise the exception # We should stop on our tracks and re-raise the exception
@@ -345,6 +358,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight mat_data.edge_weight = work_mmd_mat.edge_weight - base_mmd_mat.edge_weight
FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat) FnMaterial.clean_materials(meshObj, can_remove=lambda m: m == work_mat)
logger.info(f"Applied material offset for {mat_data.material}")
return {"FINISHED"} return {"FINISHED"}
@@ -354,7 +368,7 @@ class CreateWorkMaterial(bpy.types.Operator):
bl_description = "Creates a temporary material to edit this offset" bl_description = "Creates a temporary material to edit this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -413,6 +427,7 @@ class CreateWorkMaterial(bpy.types.Operator):
work_mmd_mat.edge_color = list(edge_offset) work_mmd_mat.edge_color = list(edge_offset)
work_mmd_mat.edge_weight += mat_data.edge_weight work_mmd_mat.edge_weight += mat_data.edge_weight
logger.info(f"Created work material {work_mat_name}")
return {"FINISHED"} return {"FINISHED"}
@@ -422,13 +437,13 @@ class ClearTempMaterials(bpy.types.Operator):
bl_description = "Clears all the temporary materials" bl_description = "Clears all the temporary materials"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
for meshObj in FnModel.iterate_mesh_objects(root): for meshObj in FnModel.iterate_mesh_objects(root):
def __pre_remove(m): def __pre_remove(m: Optional[bpy.types.Material]) -> bool:
if m and "_temp" in m.name: if m and "_temp" in m.name:
base_mat_name = m.name.split("_temp")[0] base_mat_name = m.name.split("_temp")[0]
try: try:
@@ -439,6 +454,7 @@ class ClearTempMaterials(bpy.types.Operator):
return False return False
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove) FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
logger.info("Cleared all temporary materials")
return {"FINISHED"} return {"FINISHED"}
@@ -448,7 +464,7 @@ class ViewBoneMorph(bpy.types.Operator):
bl_description = "View the result of active bone morph" bl_description = "View the result of active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -463,6 +479,7 @@ class ViewBoneMorph(bpy.types.Operator):
mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4() mtx = (p_bone.matrix_basis.to_3x3() @ Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4()
mtx.translation = p_bone.location + morph_data.location mtx.translation = p_bone.location + morph_data.location
p_bone.matrix_basis = mtx p_bone.matrix_basis = mtx
logger.info(f"Viewing bone morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -472,13 +489,14 @@ class ClearBoneMorphView(bpy.types.Operator):
bl_description = "Reset transforms of all bones to their default values" bl_description = "Reset transforms of all bones to their default values"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
armature = FnModel.find_armature_object(root) armature = FnModel.find_armature_object(root)
for p_bone in armature.pose.bones: for p_bone in armature.pose.bones:
p_bone.matrix_basis.identity() p_bone.matrix_basis.identity()
logger.info("Cleared bone morph view")
return {"FINISHED"} return {"FINISHED"}
@@ -488,7 +506,7 @@ class ApplyBoneMorph(bpy.types.Operator):
bl_description = "Apply current pose to active bone morph" bl_description = "Apply current pose to active bone morph"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -506,6 +524,7 @@ class ApplyBoneMorph(bpy.types.Operator):
p_bone.bone.select = True p_bone.bone.select = True
else: else:
p_bone.bone.select = False p_bone.bone.select = False
logger.info(f"Applied current pose to bone morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -515,7 +534,7 @@ class SelectRelatedBone(bpy.types.Operator):
bl_description = "Select the bone assigned to this offset in the armature" bl_description = "Select the bone assigned to this offset in the armature"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -524,6 +543,7 @@ class SelectRelatedBone(bpy.types.Operator):
morph = mmd_root.bone_morphs[mmd_root.active_morph] morph = mmd_root.bone_morphs[mmd_root.active_morph]
morph_data = morph.data[morph.active_data] morph_data = morph.data[morph.active_data]
utils.selectSingleBone(context, armature, morph_data.bone) utils.selectSingleBone(context, armature, morph_data.bone)
logger.debug(f"Selected bone: {morph_data.bone}")
return {"FINISHED"} return {"FINISHED"}
@@ -533,7 +553,7 @@ class EditBoneOffset(bpy.types.Operator):
bl_description = "Applies the location and rotation of this offset to the bone" bl_description = "Applies the location and rotation of this offset to the bone"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -546,6 +566,7 @@ class EditBoneOffset(bpy.types.Operator):
mtx.translation = morph_data.location mtx.translation = morph_data.location
p_bone.matrix_basis = mtx p_bone.matrix_basis = mtx
utils.selectSingleBone(context, armature, p_bone.name) utils.selectSingleBone(context, armature, p_bone.name)
logger.debug(f"Edited bone offset for {p_bone.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -555,7 +576,7 @@ class ApplyBoneOffset(bpy.types.Operator):
bl_description = "Stores the current bone location and rotation into this offset" bl_description = "Stores the current bone location and rotation into this offset"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -567,6 +588,7 @@ class ApplyBoneOffset(bpy.types.Operator):
p_bone = armature.pose.bones[morph_data.bone] p_bone = armature.pose.bones[morph_data.bone]
morph_data.location = p_bone.location morph_data.location = p_bone.location
morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
logger.debug(f"Applied bone offset for {p_bone.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -576,7 +598,7 @@ class ViewUVMorph(bpy.types.Operator):
bl_description = "View the result of active UV morph on current mesh object" bl_description = "View the result of active UV morph on current mesh object"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -627,6 +649,7 @@ class ViewUVMorph(bpy.types.Operator):
uv_tex.active_render = True uv_tex.active_render = True
meshObj.hide_set(False) meshObj.hide_set(False)
meshObj.select_set(selected) meshObj.select_set(selected)
logger.info(f"Viewing UV morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -636,7 +659,7 @@ class ClearUVMorphView(bpy.types.Operator):
bl_description = "Clear all temporary data of UV morphs" bl_description = "Clear all temporary data of UV morphs"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
assert root is not None assert root is not None
@@ -664,6 +687,7 @@ class ClearUVMorphView(bpy.types.Operator):
for act in bpy.data.actions: for act in bpy.data.actions:
if act.name.startswith("__uv.") and act.users < 1: if act.name.startswith("__uv.") and act.users < 1:
bpy.data.actions.remove(act) bpy.data.actions.remove(act)
logger.info("Cleared UV morph view")
return {"FINISHED"} return {"FINISHED"}
@@ -674,14 +698,14 @@ class EditUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object obj = context.active_object
if obj.type != "MESH": if obj.type != "MESH":
return False return False
active_uv_layer = obj.data.uv_layers.active active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.") return active_uv_layer and active_uv_layer.name.startswith("__uv.")
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
meshObj = obj meshObj = obj
@@ -704,6 +728,7 @@ class EditUVMorph(bpy.types.Operator):
bpy.ops.object.mode_set(mode="EDIT") bpy.ops.object.mode_set(mode="EDIT")
meshObj.select_set(selected) meshObj.select_set(selected)
logger.info("Editing UV morph")
return {"FINISHED"} return {"FINISHED"}
@@ -714,14 +739,14 @@ class ApplyUVMorph(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
obj = context.active_object obj = context.active_object
if obj.type != "MESH": if obj.type != "MESH":
return False return False
active_uv_layer = obj.data.uv_layers.active active_uv_layer = obj.data.uv_layers.active
return active_uv_layer and active_uv_layer.name.startswith("__uv.") return active_uv_layer and active_uv_layer.name.startswith("__uv.")
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
mmd_root = root.mmd_root mmd_root = root.mmd_root
@@ -756,6 +781,7 @@ class ApplyUVMorph(bpy.types.Operator):
morph.data_type = "VERTEX_GROUP" morph.data_type = "VERTEX_GROUP"
meshObj.select_set(selected) meshObj.select_set(selected)
logger.info(f"Applied UV morph: {morph.name}")
return {"FINISHED"} return {"FINISHED"}
@@ -766,11 +792,12 @@ class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.find_root_object(context.active_object) is not None return FnModel.find_root_object(context.active_object) is not None
def execute(self, context: bpy.types.Context): def execute(self, context: bpy.types.Context) -> Set[str]:
mmd_root_object = FnModel.find_root_object(context.active_object) mmd_root_object = FnModel.find_root_object(context.active_object)
FnMorph.clean_duplicated_material_morphs(mmd_root_object) FnMorph.clean_duplicated_material_morphs(mmd_root_object)
logger.info("Cleaned duplicated material morphs")
return {"FINISHED"} return {"FINISHED"}
+38 -24
View File
@@ -6,7 +6,7 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import math import math
from typing import Dict, Optional, Tuple, cast from typing import Dict, Optional, Tuple, cast, Set, List, Any, Union, Generator
import bpy import bpy
from mathutils import Euler, Vector from mathutils import Euler, Vector
@@ -16,6 +16,7 @@ from ..bpyutils import FnContext, Props
from ..core import rigid_body from ..core import rigid_body
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ..core.rigid_body import FnRigidBody from ..core.rigid_body import FnRigidBody
from ...logging_setup import logger
class SelectRigidBody(bpy.types.Operator): class SelectRigidBody(bpy.types.Operator):
@@ -43,15 +44,15 @@ class SelectRigidBody(bpy.types.Operator):
default=False, default=False,
) )
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_rigid_body_object(context.active_object) return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
if root is None: if root is None:
@@ -173,7 +174,7 @@ class AddRigidBody(bpy.types.Operator):
default=0.1, default=0.1,
) )
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None): def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None) -> bpy.types.Object:
name_j: str = self.name_j name_j: str = self.name_j
name_e: str = self.name_e name_e: str = self.name_e
size = self.size.copy() size = self.size.copy()
@@ -226,7 +227,7 @@ class AddRigidBody(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
if root_object is None: if root_object is None:
return False return False
@@ -237,7 +238,7 @@ class AddRigidBody(bpy.types.Operator):
return True return True
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object)) root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
@@ -254,15 +255,17 @@ class AddRigidBody(bpy.types.Operator):
armature_object.select_set(False) armature_object.select_set(False)
if len(selected_pose_bones) > 0: if len(selected_pose_bones) > 0:
logger.info(f"Adding rigid bodies to {len(selected_pose_bones)} selected bones")
for pose_bone in selected_pose_bones: for pose_bone in selected_pose_bones:
rigid = self.__add_rigid_body(context, root_object, pose_bone) rigid = self.__add_rigid_body(context, root_object, pose_bone)
rigid.select_set(True) rigid.select_set(True)
else: else:
logger.info("Adding a single rigid body without bone attachment")
rigid = self.__add_rigid_body(context, root_object) rigid = self.__add_rigid_body(context, root_object)
rigid.select_set(True) rigid.select_set(True)
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
no_bone = True no_bone = True
if context.selected_bones and len(context.selected_bones) > 0: if context.selected_bones and len(context.selected_bones) > 0:
no_bone = False no_bone = False
@@ -288,12 +291,13 @@ class RemoveRigidBody(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_rigid_body_object(context.active_object) return FnModel.is_rigid_body_object(context.active_object)
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
logger.info(f"Removing rigid body: {obj.name}")
utils.selectAObject(obj) # ensure this is the only one object select utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True) bpy.ops.object.delete(use_global=True)
if root: if root:
@@ -306,7 +310,8 @@ class RigidBodyBake(bpy.types.Operator):
bl_label = "Bake" bl_label = "Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context): def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info("Baking rigid body simulation")
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache): with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True) bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
@@ -318,7 +323,8 @@ class RigidBodyDeleteBake(bpy.types.Operator):
bl_label = "Delete Bake" bl_label = "Delete Bake"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context: bpy.types.Context): def execute(self, context: bpy.types.Context) -> Set[str]:
logger.info("Deleting rigid body simulation bake")
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache): with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT") bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
@@ -381,7 +387,7 @@ class AddJoint(bpy.types.Operator):
min=0, min=0,
) )
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]): def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> Generator[Tuple[bpy.types.Object, bpy.types.Object], None, None]:
obj_seq = tuple(bone_map.keys()) obj_seq = tuple(bone_map.keys())
for rigid_a, bone_a in bone_map.items(): for rigid_a, bone_a in bone_map.items():
for rigid_b, bone_b in bone_map.items(): for rigid_b, bone_b in bone_map.items():
@@ -394,7 +400,7 @@ class AddJoint(bpy.types.Operator):
else: else:
yield obj_seq yield obj_seq
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map): def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> bpy.types.Object:
loc: Optional[Vector] = None loc: Optional[Vector] = None
rot = Euler((0.0, 0.0, 0.0)) rot = Euler((0.0, 0.0, 0.0))
rigid_a, rigid_b = rigid_pair rigid_a, rigid_b = rigid_pair
@@ -432,7 +438,7 @@ class AddJoint(bpy.types.Operator):
) )
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
root_object = FnModel.find_root_object(context.active_object) root_object = FnModel.find_root_object(context.active_object)
if root_object is None: if root_object is None:
return False return False
@@ -443,7 +449,7 @@ class AddJoint(bpy.types.Operator):
return True return True
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
active_object = context.active_object active_object = context.active_object
root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object)) root_object = cast(bpy.types.Object, FnModel.find_root_object(active_object))
armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object)) armature_object = cast(bpy.types.Object, FnModel.find_armature_object(root_object))
@@ -456,15 +462,19 @@ class AddJoint(bpy.types.Operator):
FnContext.select_single_object(context, root_object).select_set(False) FnContext.select_single_object(context, root_object).select_set(False)
if context.scene.rigidbody_world is None: if context.scene.rigidbody_world is None:
logger.info("Creating rigid body world")
bpy.ops.rigidbody.world_add() bpy.ops.rigidbody.world_add()
joint_count = 0
for pair in self.__enumerate_rigid_pair(bone_map): for pair in self.__enumerate_rigid_pair(bone_map):
joint = self.__add_joint(context, root_object, pair, bone_map) joint = self.__add_joint(context, root_object, pair, bone_map)
joint.select_set(True) joint.select_set(True)
joint_count += 1
logger.info(f"Added {joint_count} joints between rigid bodies")
return {"FINISHED"} return {"FINISHED"}
def invoke(self, context, event): def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
@@ -476,12 +486,13 @@ class RemoveJoint(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: bpy.types.Context) -> bool:
return FnModel.is_joint_object(context.active_object) return FnModel.is_joint_object(context.active_object)
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
obj = context.active_object obj = context.active_object
root = FnModel.find_root_object(obj) root = FnModel.find_root_object(obj)
logger.info(f"Removing joint: {obj.name}")
utils.selectAObject(obj) # ensure this is the only one object select utils.selectAObject(obj) # ensure this is the only one object select
bpy.ops.object.delete(use_global=True) bpy.ops.object.delete(use_global=True)
if root: if root:
@@ -496,7 +507,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
@staticmethod @staticmethod
def __get_rigid_body_world_objects(): def __get_rigid_body_world_objects() -> Tuple[bpy.types.Collection, bpy.types.Collection]:
rigid_body.setRigidBodyWorldEnabled(True) rigid_body.setRigidBodyWorldEnabled(True)
rbw = bpy.context.scene.rigidbody_world rbw = bpy.context.scene.rigidbody_world
if not rbw.collection: if not rbw.collection:
@@ -511,12 +522,12 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
return rbw.collection.objects, rbw.constraints.objects return rbw.collection.objects, rbw.constraints.objects
def execute(self, context): def execute(self, context: bpy.types.Context) -> Set[str]:
scene = context.scene scene = context.scene
scene_objs = set(scene.objects) scene_objs = set(scene.objects)
scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects) scene_objs.union(o for x in scene.objects if x.instance_type == "COLLECTION" and x.instance_collection for o in x.instance_collection.objects)
def _update_group(obj, group): def _update_group(obj: bpy.types.Object, group: bpy.types.Collection) -> bool:
if obj in scene_objs: if obj in scene_objs:
if obj not in group.values(): if obj not in group.values():
group.link(obj) group.link(obj)
@@ -525,7 +536,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
group.unlink(obj) group.unlink(obj)
return False return False
def _references(obj): def _references(obj: bpy.types.Object) -> Generator[bpy.types.Object, None, None]:
yield obj yield obj
if getattr(obj, "proxy", None): if getattr(obj, "proxy", None):
yield from _references(obj.proxy) yield from _references(obj.proxy)
@@ -542,6 +553,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
# Object.rigid_body are removed, # Object.rigid_body are removed,
# but Object.rigid_body_constraint are retained. # but Object.rigid_body_constraint are retained.
# Therefore, it must be checked with Object.mmd_type. # Therefore, it must be checked with Object.mmd_type.
logger.info("Updating rigid body world objects")
for i in (x for x in objects if x.mmd_type == "RIGID_BODY"): for i in (x for x in objects if x.mmd_type == "RIGID_BODY"):
if not _update_group(i, rb_objs): if not _update_group(i, rb_objs):
continue continue
@@ -556,6 +568,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
# TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters. # TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters.
# mass, friction, restitution, linear_dumping, angular_dumping # mass, friction, restitution, linear_dumping, angular_dumping
logger.info("Updating rigid body constraints")
for i in (x for x in objects if x.rigid_body_constraint): for i in (x for x in objects if x.rigid_body_constraint):
if not _update_group(i, rbc_objs): if not _update_group(i, rbc_objs):
continue continue
@@ -566,6 +579,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
rbc.object2 = rb_map.get(rbc.object2, rbc.object2) rbc.object2 = rb_map.get(rbc.object2, rbc.object2)
if need_rebuild_physics: if need_rebuild_physics:
logger.info("Rebuilding physics for models")
for root_object in scene.objects: for root_object in scene.objects:
if root_object.mmd_type != "ROOT": if root_object.mmd_type != "ROOT":
continue continue
+17 -11
View File
@@ -5,18 +5,19 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import Set from typing import Set, Tuple
import bpy import bpy
from bpy.types import Operator from bpy.types import Operator, Context, Object
from ..core.model import FnModel from ..core.model import FnModel
from ..core.sdef import FnSDEF from ..core.sdef import FnSDEF
from ....core.logging_setup import logger
def _get_target_objects(context): def _get_target_objects(context: Context) -> Tuple[Set[Object], Set[Object]]:
root_objects: Set[bpy.types.Object] = set() root_objects: Set[Object] = set()
selected_objects: Set[bpy.types.Object] = set() selected_objects: Set[Object] = set()
for i in context.selected_objects: for i in context.selected_objects:
if i.type == "MESH": if i.type == "MESH":
selected_objects.add(i) selected_objects.add(i)
@@ -40,11 +41,13 @@ class ResetSDEFCache(Operator):
bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache" bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
def execute(self, context): def execute(self, context: Context) -> Set[str]:
target_meshes, _ = _get_target_objects(context) target_meshes, _ = _get_target_objects(context)
logger.info(f"Resetting SDEF cache for {len(target_meshes)} objects")
for i in target_meshes: for i in target_meshes:
FnSDEF.clear_cache(i) FnSDEF.clear_cache(i)
FnSDEF.clear_cache(unused_only=True) FnSDEF.clear_cache(unused_only=True)
logger.debug("SDEF cache reset completed")
return {"FINISHED"} return {"FINISHED"}
@@ -75,19 +78,20 @@ class BindSDEF(Operator):
default=False, default=False,
) )
def invoke(self, context, event): def invoke(self, context: Context, event: bpy.types.Event) -> Set[str]:
vm = context.window_manager vm = context.window_manager
return vm.invoke_props_dialog(self) return vm.invoke_props_dialog(self)
# TODO: Utility Functionalize def execute(self, context: Context) -> Set[str]:
def execute(self, context):
target_meshes, root_objects = _get_target_objects(context) target_meshes, root_objects = _get_target_objects(context)
logger.info(f"Binding SDEF for {len(target_meshes)} objects with mode={self.mode}, skip={self.use_skip}, scale={self.use_scale}")
for r in root_objects: for r in root_objects:
r.mmd_root.use_sdef = True r.mmd_root.use_sdef = True
param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale) param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale)
count = sum(FnSDEF.bind(i, *param) for i in target_meshes) count = sum(FnSDEF.bind(i, *param) for i in target_meshes)
logger.info(f"Successfully bound SDEF for {count} of {len(target_meshes)} meshes")
self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)") self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)")
return {"FINISHED"} return {"FINISHED"}
@@ -98,13 +102,15 @@ class UnbindSDEF(Operator):
bl_description = "Unbind MMD SDEF data of selected objects" bl_description = "Unbind MMD SDEF data of selected objects"
bl_options = {"REGISTER", "UNDO", "INTERNAL"} bl_options = {"REGISTER", "UNDO", "INTERNAL"}
# TODO: Utility Functionalize def execute(self, context: Context) -> Set[str]:
def execute(self, context):
target_meshes, root_objects = _get_target_objects(context) target_meshes, root_objects = _get_target_objects(context)
logger.info(f"Unbinding SDEF for {len(target_meshes)} objects")
for i in target_meshes: for i in target_meshes:
FnSDEF.unbind(i) FnSDEF.unbind(i)
for r in root_objects: for r in root_objects:
r.mmd_root.use_sdef = False r.mmd_root.use_sdef = False
logger.debug("SDEF unbinding completed")
return {"FINISHED"} return {"FINISHED"}
+42 -34
View File
@@ -6,29 +6,32 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import re import re
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type, Iterator
from bpy.types import Operator from bpy.types import Operator, Context
from mathutils import Matrix from mathutils import Matrix, Vector, Quaternion
from ...logging_setup import logger
class _SetShadingBase: class _SetShadingBase:
bl_options = {"REGISTER", "UNDO"} bl_options: Set[str] = {"REGISTER", "UNDO"}
@staticmethod @staticmethod
def _get_view3d_spaces(context): def _get_view3d_spaces(context: Context) -> Iterator[Any]:
if getattr(context.area, "type", None) == "VIEW_3D": if getattr(context.area, "type", None) == "VIEW_3D":
return (context.area.spaces[0],) return (context.area.spaces[0],)
return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D") return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D")
@staticmethod @staticmethod
def _reset_color_management(context, use_display_device=True): def _reset_color_management(context: Context, use_display_device: bool = True) -> None:
try: try:
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device] context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
except TypeError: except TypeError:
pass pass
@staticmethod @staticmethod
def _reset_material_shading(context, use_shadeless=False): def _reset_material_shading(context: Context, use_shadeless: bool = False) -> None:
for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"): for i in (x for x in context.scene.objects if x.type == "MESH" and x.mmd_type == "NONE"):
for s in i.material_slots: for s in i.material_slots:
if s.material is None: if s.material is None:
@@ -36,10 +39,11 @@ class _SetShadingBase:
s.material.use_nodes = False s.material.use_nodes = False
s.material.use_shadeless = use_shadeless s.material.use_shadeless = use_shadeless
def execute(self, context): def execute(self, context: Context) -> Dict[str, str]:
context.scene.render.engine = "BLENDER_EEVEE_NEXT" context.scene.render.engine = "BLENDER_EEVEE_NEXT"
logger.debug(f"Setting render engine to BLENDER_EEVEE_NEXT")
shading_mode = getattr(self, "_shading_mode", None) shading_mode: Optional[str] = getattr(self, "_shading_mode", None)
for space in self._get_view3d_spaces(context): for space in self._get_view3d_spaces(context):
shading = space.shading shading = space.shading
shading.type = "SOLID" shading.type = "SOLID"
@@ -47,39 +51,40 @@ class _SetShadingBase:
shading.color_type = "TEXTURE" if shading_mode else "MATERIAL" shading.color_type = "TEXTURE" if shading_mode else "MATERIAL"
shading.show_object_outline = False shading.show_object_outline = False
shading.show_backface_culling = False shading.show_backface_culling = False
logger.debug(f"Applied shading mode: {shading_mode or 'DEFAULT'}")
return {"FINISHED"} return {"FINISHED"}
class SetGLSLShading(Operator, _SetShadingBase): class SetGLSLShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.set_glsl_shading" bl_idname: str = "mmd_tools.set_glsl_shading"
bl_label = "GLSL View" bl_label: str = "GLSL View"
bl_description = "Use GLSL shading with additional lighting" bl_description: str = "Use GLSL shading with additional lighting"
_shading_mode = "GLSL" _shading_mode: str = "GLSL"
class SetShadelessGLSLShading(Operator, _SetShadingBase): class SetShadelessGLSLShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.set_shadeless_glsl_shading" bl_idname: str = "mmd_tools.set_shadeless_glsl_shading"
bl_label = "Shadeless GLSL View" bl_label: str = "Shadeless GLSL View"
bl_description = "Use only toon shading" bl_description: str = "Use only toon shading"
_shading_mode = "SHADELESS" _shading_mode: str = "SHADELESS"
class ResetShading(Operator, _SetShadingBase): class ResetShading(Operator, _SetShadingBase):
bl_idname = "mmd_tools.reset_shading" bl_idname: str = "mmd_tools.reset_shading"
bl_label = "Reset View" bl_label: str = "Reset View"
bl_description = "Reset to default Blender shading" bl_description: str = "Reset to default Blender shading"
class FlipPose(Operator): class FlipPose(Operator):
bl_idname = "mmd_tools.flip_pose" bl_idname: str = "mmd_tools.flip_pose"
bl_label = "Flip Pose" bl_label: str = "Flip Pose"
bl_description = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis." bl_description: str = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
bl_options = {"REGISTER", "UNDO"} bl_options: Set[str] = {"REGISTER", "UNDO"}
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html # https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html
__LR_REGEX = [ __LR_REGEX: List[Dict[str, Any]] = [
{"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1}, {"re": re.compile(r"^(.+)(RIGHT|LEFT)(\.\d+)?$", re.IGNORECASE), "lr": 1},
{"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2}, {"re": re.compile(r"^(.+)([\.\- _])(L|R)(\.\d+)?$", re.IGNORECASE), "lr": 2},
{"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0}, {"re": re.compile(r"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0},
@@ -87,7 +92,7 @@ class FlipPose(Operator):
{"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1}, {"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0}, {"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
] ]
__LR_MAP = { __LR_MAP: Dict[str, str] = {
"RIGHT": "LEFT", "RIGHT": "LEFT",
"Right": "Left", "Right": "Left",
"right": "left", "right": "left",
@@ -103,7 +108,7 @@ class FlipPose(Operator):
} }
@classmethod @classmethod
def flip_name(cls, name): def flip_name(cls, name: str) -> str:
for regex in cls.__LR_REGEX: for regex in cls.__LR_REGEX:
match = regex["re"].match(name) match = regex["re"].match(name)
if match: if match:
@@ -121,17 +126,15 @@ class FlipPose(Operator):
return "" return ""
@staticmethod @staticmethod
def __cmul(vec1, vec2): def __cmul(vec1: Union[Vector, Quaternion], vec2: Tuple[float, float, float, float]) -> Union[Vector, Quaternion]:
return type(vec1)([x * y for x, y in zip(vec1, vec2)]) return type(vec1)([x * y for x, y in zip(vec1, vec2)])
@staticmethod @staticmethod
def __matrix_compose(loc, rot, scale): def __matrix_compose(loc: Vector, rot: Quaternion, scale: Vector) -> Matrix:
return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)]) return (Matrix.Translation(loc) @ rot.to_matrix().to_4x4()) @ Matrix([(scale[0], 0, 0, 0), (0, scale[1], 0, 0), (0, 0, scale[2], 0), (0, 0, 0, 1)])
@classmethod @classmethod
def __flip_pose(cls, matrix_basis, bone_src, bone_dest): def __flip_pose(cls, matrix_basis: Matrix, bone_src: Any, bone_dest: Any) -> None:
from mathutils import Quaternion
m = bone_dest.bone.matrix_local.to_3x3().transposed() m = bone_dest.bone.matrix_local.to_3x3().transposed()
mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted() mi = bone_src.bone.matrix_local.to_3x3().transposed().inverted() if bone_src != bone_dest else m.inverted()
loc, rot, scale = matrix_basis.decompose() loc, rot, scale = matrix_basis.decompose()
@@ -140,11 +143,16 @@ class FlipPose(Operator):
bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale) bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale)
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE" return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE"
def execute(self, context): def execute(self, context: Context) -> Dict[str, str]:
logger.info("Executing flip pose operation")
pose_bones = context.active_object.pose.bones pose_bones = context.active_object.pose.bones
for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]: for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]:
self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b)) flip_name = self.flip_name(b.name)
target_bone = pose_bones.get(flip_name, b)
logger.debug(f"Flipping pose from {b.name} to {target_bone.name}")
self.__flip_pose(mat, b, target_bone)
logger.info("Flip pose operation completed")
return {"FINISHED"} return {"FINISHED"}
+25 -19
View File
@@ -6,81 +6,85 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from typing import Optional, Set, Dict, Any, List, Tuple, Union, Type
from .. import utils from .. import utils
from ..core import material from ..core import material
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.model import FnModel from ..core.model import FnModel
from . import patch_library_overridable from . import patch_library_overridable
from ....core.logging_setup import logger
def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context): def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_ambient_color() FnMaterial(prop.id_data).update_ambient_color()
def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context): def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_diffuse_color() FnMaterial(prop.id_data).update_diffuse_color()
def _mmd_material_update_alpha(prop: "MMDMaterial", _context): def _mmd_material_update_alpha(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_alpha() FnMaterial(prop.id_data).update_alpha()
def _mmd_material_update_specular_color(prop: "MMDMaterial", _context): def _mmd_material_update_specular_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_specular_color() FnMaterial(prop.id_data).update_specular_color()
def _mmd_material_update_shininess(prop: "MMDMaterial", _context): def _mmd_material_update_shininess(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_shininess() FnMaterial(prop.id_data).update_shininess()
def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context): def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_is_double_sided() FnMaterial(prop.id_data).update_is_double_sided()
def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context): def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object) FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object)
def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context): def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_toon_texture() FnMaterial(prop.id_data).update_toon_texture()
def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context): def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_drop_shadow() FnMaterial(prop.id_data).update_drop_shadow()
def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context): def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_self_shadow_map() FnMaterial(prop.id_data).update_self_shadow_map()
def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context): def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_self_shadow() FnMaterial(prop.id_data).update_self_shadow()
def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context): def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_enabled_toon_edge() FnMaterial(prop.id_data).update_enabled_toon_edge()
def _mmd_material_update_edge_color(prop: "MMDMaterial", _context): def _mmd_material_update_edge_color(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_edge_color() FnMaterial(prop.id_data).update_edge_color()
def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context): def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context: bpy.types.Context) -> None:
FnMaterial(prop.id_data).update_edge_weight() FnMaterial(prop.id_data).update_edge_weight()
def _mmd_material_get_name_j(prop: "MMDMaterial"): def _mmd_material_get_name_j(prop: "MMDMaterial") -> str:
return prop.get("name_j", "") return prop.get("name_j", "")
def _mmd_material_set_name_j(prop: "MMDMaterial", value: str): def _mmd_material_set_name_j(prop: "MMDMaterial", value: str) -> None:
prop_value = value prop_value = value
if prop_value and prop_value != prop.get("name_j"): if prop_value and prop_value != prop.get("name_j"):
root = FnModel.find_root_object(bpy.context.active_object) root = FnModel.find_root_object(bpy.context.active_object)
if root is None: if root is None:
logger.debug(f"No root object found, using unique name for material: {value}")
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials}) prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials})
else: else:
logger.debug(f"Root object found, using unique name for material within model: {value}")
prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)}) prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)})
prop["name_j"] = prop_value prop["name_j"] = prop_value
@@ -275,13 +279,15 @@ class MMDMaterial(bpy.types.PropertyGroup):
description="Comment", description="Comment",
) )
def is_id_unique(self): def is_id_unique(self) -> bool:
return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None) return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None)
@staticmethod @staticmethod
def register(): def register() -> None:
logger.debug("Registering MMD material properties")
bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial)) bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial))
@staticmethod @staticmethod
def unregister(): def unregister() -> None:
logger.debug("Unregistering MMD material properties")
del bpy.types.Material.mmd_material del bpy.types.Material.mmd_material
+28 -22
View File
@@ -6,33 +6,33 @@
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
import bpy import bpy
from typing import Optional, List, Dict, Any, Set, Tuple, Union, TypeVar, Type
from bpy.types import PropertyGroup, Object, ShapeKey
from .. import utils from .. import utils
from ..core.bone import FnBone from ..core.bone import FnBone
from ..core.material import FnMaterial from ..core.material import FnMaterial
from ..core.model import FnModel, Model from ..core.model import FnModel, Model
from ..core.morph import FnMorph from ..core.morph import FnMorph
from ....core.logging_setup import logger
def _morph_base_get_name(prop: "_MorphBase") -> str: def _morph_base_get_name(prop: "_MorphBase") -> str:
return prop.get("name", "") return prop.get("name", "")
def _morph_base_set_name(prop: "_MorphBase", value: str): def _morph_base_set_name(prop: "_MorphBase", value: str) -> None:
mmd_root = prop.id_data.mmd_root mmd_root = prop.id_data.mmd_root
# morph_type = mmd_root.active_morph_type
morph_type = "%s_morphs" % prop.bl_rna.identifier[:-5].lower() morph_type = "%s_morphs" % prop.bl_rna.identifier[:-5].lower()
# assert(prop.bl_rna.identifier.endswith('Morph'))
# logging.debug('_set_name: %s %s %s', prop, value, morph_type)
prop_name = prop.get("name", None) prop_name = prop.get("name", None)
if prop_name == value: if prop_name == value:
return return
used_names = {x.name for x in getattr(mmd_root, morph_type) if x != prop} used_names: Set[str] = {x.name for x in getattr(mmd_root, morph_type) if x != prop}
value = utils.unique_name(value, used_names) value = utils.unique_name(value, used_names)
if prop_name is not None: if prop_name is not None:
if morph_type == "vertex_morphs": if morph_type == "vertex_morphs":
kb_list = {} kb_list: Dict[str, List[ShapeKey]] = {}
for mesh in FnModel.iterate_mesh_objects(prop.id_data): for mesh in FnModel.iterate_mesh_objects(prop.id_data):
for kb in getattr(mesh.data.shape_keys, "key_blocks", ()): for kb in getattr(mesh.data.shape_keys, "key_blocks", ()):
kb_list.setdefault(kb.name, []).append(kb) kb_list.setdefault(kb.name, []).append(kb)
@@ -43,7 +43,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str):
kb.name = value kb.name = value
elif morph_type == "uv_morphs": elif morph_type == "uv_morphs":
vg_list = {} vg_list: Dict[str, List[Any]] = {}
for mesh in FnModel.iterate_mesh_objects(prop.id_data): for mesh in FnModel.iterate_mesh_objects(prop.id_data):
for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh): for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh):
vg_list.setdefault(n, []).append(vg) vg_list.setdefault(n, []).append(vg)
@@ -72,6 +72,7 @@ def _morph_base_set_name(prop: "_MorphBase", value: str):
kb.name = value kb.name = value
prop["name"] = value prop["name"] = value
logger.debug(f"Renamed morph from '{prop_name}' to '{value}'")
class _MorphBase: class _MorphBase:
@@ -101,11 +102,11 @@ class _MorphBase:
def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str: def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
bone_id = prop.get("bone_id", -1) bone_id: int = prop.get("bone_id", -1)
if bone_id < 0: if bone_id < 0:
return "" return ""
root_object = prop.id_data root_object: Object = prop.id_data
armature_object = FnModel.find_armature_object(root_object) armature_object: Optional[Object] = FnModel.find_armature_object(root_object)
if armature_object is None: if armature_object is None:
return "" return ""
pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id) pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id)
@@ -114,9 +115,9 @@ def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str:
return pose_bone.name return pose_bone.name
def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str): def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str) -> None:
root = prop.id_data root: Object = prop.id_data
arm = FnModel.find_armature_object(root) arm: Optional[Object] = FnModel.find_armature_object(root)
# Load the library_override file. This function is triggered when loading, but the arm obj cannot be found. # Load the library_override file. This function is triggered when loading, but the arm obj cannot be found.
# The arm obj is exist, but the relative relationship has not yet been established. # The arm obj is exist, but the relative relationship has not yet been established.
@@ -128,9 +129,10 @@ def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str):
return return
pose_bone = arm.pose.bones[value] pose_bone = arm.pose.bones[value]
prop["bone_id"] = FnBone.get_or_assign_bone_id(pose_bone) prop["bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
logger.debug(f"Set bone morph data bone to '{value}' with ID {prop['bone_id']}")
def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context): def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context: bpy.types.Context) -> None:
if not prop.name.startswith("mmd_bind"): if not prop.name.startswith("mmd_bind"):
return return
arm = FnModel(prop.id_data).morph_slider.dummy_armature arm = FnModel(prop.id_data).morph_slider.dummy_armature
@@ -139,6 +141,7 @@ def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context
if bone: if bone:
bone.location = prop.location bone.location = prop.location
bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency bone.rotation_quaternion = prop.rotation.__class__(*prop.rotation.to_axis_angle()) # Fix for consistency
logger.debug(f"Updated bone morph data location/rotation for '{prop.name}'")
class BoneMorphData(bpy.types.PropertyGroup): class BoneMorphData(bpy.types.PropertyGroup):
@@ -188,40 +191,44 @@ class BoneMorph(_MorphBase, bpy.types.PropertyGroup):
) )
def _material_morph_data_get_material(prop: "MaterialMorphData"): def _material_morph_data_get_material(prop: "MaterialMorphData") -> str:
mat_p = prop.get("material_data", None) mat_p = prop.get("material_data", None)
if mat_p is not None: if mat_p is not None:
return mat_p.name return mat_p.name
return "" return ""
def _material_morph_data_set_material(prop: "MaterialMorphData", value: str): def _material_morph_data_set_material(prop: "MaterialMorphData", value: str) -> None:
if value not in bpy.data.materials: if value not in bpy.data.materials:
prop["material_data"] = None prop["material_data"] = None
prop["material_id"] = -1 prop["material_id"] = -1
logger.debug(f"Material '{value}' not found, setting material_data to None")
else: else:
mat = bpy.data.materials[value] mat = bpy.data.materials[value]
fnMat = FnMaterial(mat) fnMat = FnMaterial(mat)
prop["material_data"] = mat prop["material_data"] = mat
prop["material_id"] = fnMat.material_id prop["material_id"] = fnMat.material_id
logger.debug(f"Set material morph data material to '{value}' with ID {fnMat.material_id}")
def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str): def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str) -> None:
mesh = FnModel.find_mesh_object_by_name(prop.id_data, value) mesh = FnModel.find_mesh_object_by_name(prop.id_data, value)
if mesh is not None: if mesh is not None:
prop["related_mesh_data"] = mesh.data prop["related_mesh_data"] = mesh.data
logger.debug(f"Set material morph data related mesh to '{value}'")
else: else:
prop["related_mesh_data"] = None prop["related_mesh_data"] = None
logger.debug(f"Mesh '{value}' not found, setting related_mesh_data to None")
def _material_morph_data_get_related_mesh(prop): def _material_morph_data_get_related_mesh(prop: "MaterialMorphData") -> str:
mesh_p = prop.get("related_mesh_data", None) mesh_p = prop.get("related_mesh_data", None)
if mesh_p is not None: if mesh_p is not None:
return mesh_p.name return mesh_p.name
return "" return ""
def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context): def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context: bpy.types.Context) -> None:
if not prop.name.startswith("mmd_bind"): if not prop.name.startswith("mmd_bind"):
return return
from ..core.shader import _MaterialMorph from ..core.shader import _MaterialMorph
@@ -229,9 +236,11 @@ def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _co
mat = prop["material_data"] mat = prop["material_data"]
if mat is not None: if mat is not None:
_MaterialMorph.update_morph_inputs(mat, prop) _MaterialMorph.update_morph_inputs(mat, prop)
logger.debug(f"Updated material morph modifiable values for '{prop.name}'")
else: else:
for mat in FnModel(prop.id_data).materials(): for mat in FnModel(prop.id_data).materials():
_MaterialMorph.update_morph_inputs(mat, prop) _MaterialMorph.update_morph_inputs(mat, prop)
logger.debug(f"Updated material morph modifiable values for all materials")
class MaterialMorphData(bpy.types.PropertyGroup): class MaterialMorphData(bpy.types.PropertyGroup):
@@ -407,9 +416,6 @@ class UVMorphOffset(bpy.types.PropertyGroup):
name="UV Offset", name="UV Offset",
description="UV offset", description="UV offset",
size=4, size=4,
# min=-1,
# max=1,
# precision=3,
step=0.1, step=0.1,
default=[0, 0, 0, 0], default=[0, 0, 0, 0],
) )
+18 -12
View File
@@ -5,29 +5,33 @@
# Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements. # Neoneko has modified this file to work with Avatar Toolkit and may of made changes or improvements.
# MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit. # MMD Tools is licensed under the terms of the GNU General Public License version 3 (GPLv3) same as Avatar Toolkit.
from typing import cast from typing import cast, Optional, Any, Union
import bpy import bpy
from bpy.types import Context, PropertyGroup, PoseBone, Object, Armature
from ..core.bone import FnBone from ..core.bone import FnBone
from . import patch_library_overridable from . import patch_library_overridable
from ....core.logging_setup import logger
def _mmd_bone_update_additional_transform(prop: "MMDBone", context: bpy.types.Context): def _mmd_bone_update_additional_transform(prop: "MMDBone", context: Context) -> None:
prop["is_additional_transform_dirty"] = True prop["is_additional_transform_dirty"] = True
p_bone = context.active_pose_bone p_bone = context.active_pose_bone
if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer(): if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer():
logger.debug(f"Applying additional transformation for {p_bone.name}")
FnBone.apply_additional_transformation(prop.id_data) FnBone.apply_additional_transformation(prop.id_data)
def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: bpy.types.Context): def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: Context) -> None:
pose_bone = context.active_pose_bone pose_bone = context.active_pose_bone
if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer(): if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer():
logger.debug(f"Updating additional transform influence for {pose_bone.name}")
FnBone.update_additional_transform_influence(pose_bone) FnBone.update_additional_transform_influence(pose_bone)
else: else:
prop["is_additional_transform_dirty"] = True prop["is_additional_transform_dirty"] = True
def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"): def _mmd_bone_get_additional_transform_bone(prop: "MMDBone") -> str:
arm = prop.id_data arm = prop.id_data
bone_id = prop.get("additional_transform_bone_id", -1) bone_id = prop.get("additional_transform_bone_id", -1)
if bone_id < 0: if bone_id < 0:
@@ -38,7 +42,7 @@ def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"):
return pose_bone.name return pose_bone.name
def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str): def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str) -> None:
arm = prop.id_data arm = prop.id_data
prop["is_additional_transform_dirty"] = True prop["is_additional_transform_dirty"] = True
if value not in arm.pose.bones.keys(): if value not in arm.pose.bones.keys():
@@ -48,7 +52,7 @@ def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str):
prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone) prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone)
class MMDBone(bpy.types.PropertyGroup): class MMDBone(PropertyGroup):
name_j: bpy.props.StringProperty( name_j: bpy.props.StringProperty(
name="Name", name="Name",
description="Japanese Name", description="Japanese Name",
@@ -184,11 +188,12 @@ class MMDBone(bpy.types.PropertyGroup):
is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True) is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True)
def is_id_unique(self): def is_id_unique(self) -> bool:
return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None) return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None)
@staticmethod @staticmethod
def register(): def register() -> None:
logger.debug("Registering MMDBone properties")
bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone)) bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone))
bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False)) bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False))
bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type")) bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type"))
@@ -202,20 +207,21 @@ class MMDBone(bpy.types.PropertyGroup):
) )
@staticmethod @staticmethod
def unregister(): def unregister() -> None:
logger.debug("Unregistering MMDBone properties")
del bpy.types.PoseBone.mmd_ik_toggle del bpy.types.PoseBone.mmd_ik_toggle
del bpy.types.PoseBone.mmd_shadow_bone_type del bpy.types.PoseBone.mmd_shadow_bone_type
del bpy.types.PoseBone.is_mmd_shadow_bone del bpy.types.PoseBone.is_mmd_shadow_bone
del bpy.types.PoseBone.mmd_bone del bpy.types.PoseBone.mmd_bone
def _pose_bone_update_mmd_ik_toggle(prop: bpy.types.PoseBone, _context): def _pose_bone_update_mmd_ik_toggle(prop: PoseBone, _context: Any) -> None:
v = prop.mmd_ik_toggle v = prop.mmd_ik_toggle
armature_object = cast(bpy.types.Object, prop.id_data) armature_object = cast(Object, prop.id_data)
for b in armature_object.pose.bones: for b in armature_object.pose.bones:
for c in b.constraints: for c in b.constraints:
if c.type == "IK" and c.subtarget == prop.name: if c.type == "IK" and c.subtarget == prop.name:
# logging.debug(' %s %s', b.name, c.name) logger.debug(f"Updating IK toggle for {b.name} {c.name}")
c.influence = v c.influence = v
b = b if c.use_tail else b.parent b = b if c.use_tail else b.parent
for b in ([b] + b.parent_recursive)[: c.chain_count]: for b in ([b] + b.parent_recursive)[: c.chain_count]:
+36 -28
View File
@@ -8,32 +8,35 @@
"""Properties for rigid bodies and joints""" """Properties for rigid bodies and joints"""
import bpy import bpy
from typing import Optional, Any, Set, List, Dict, Tuple, Union
from bpy.types import Context, Object, PropertyGroup, Material
from .. import bpyutils from .. import bpyutils
from ..core import rigid_body from ..core import rigid_body
from ..core.rigid_body import RigidBodyMaterial, FnRigidBody from ..core.rigid_body import RigidBodyMaterial, FnRigidBody
from ..core.model import FnModel from ..core.model import FnModel
from . import patch_library_overridable from . import patch_library_overridable
from ....core.logging_setup import logger
def _updateCollisionGroup(prop, _context): def _updateCollisionGroup(prop: PropertyGroup, _context: Context) -> None:
obj = prop.id_data obj: Object = prop.id_data
materials = obj.data.materials materials: List[Material] = obj.data.materials
if len(materials) == 0: if len(materials) == 0:
materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number)) materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number))
else: else:
obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number) obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number)
def _updateType(prop, _context): def _updateType(prop: PropertyGroup, _context: Context) -> None:
obj = prop.id_data obj: Object = prop.id_data
rb = obj.rigid_body rb = obj.rigid_body
if rb: if rb:
rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC
def _updateShape(prop, _context): def _updateShape(prop: PropertyGroup, _context: Context) -> None:
obj = prop.id_data obj: Object = prop.id_data
if len(obj.data.vertices) > 0: if len(obj.data.vertices) > 0:
size = prop.size size = prop.size
@@ -44,8 +47,8 @@ def _updateShape(prop, _context):
rb.collision_shape = prop.shape rb.collision_shape = prop.shape
def _get_bone(prop): def _get_bone(prop: PropertyGroup) -> str:
obj = prop.id_data obj: Object = prop.id_data
relation = obj.constraints.get("mmd_tools_rigid_parent", None) relation = obj.constraints.get("mmd_tools_rigid_parent", None)
if relation: if relation:
arm = relation.target arm = relation.target
@@ -55,9 +58,9 @@ def _get_bone(prop):
return prop.get("bone", "") return prop.get("bone", "")
def _set_bone(prop, value): def _set_bone(prop: PropertyGroup, value: str) -> None:
bone_name = value bone_name: str = value
obj = prop.id_data obj: Object = prop.id_data
relation = obj.constraints.get("mmd_tools_rigid_parent", None) relation = obj.constraints.get("mmd_tools_rigid_parent", None)
if relation is None: if relation is None:
relation = obj.constraints.new("CHILD_OF") relation = obj.constraints.new("CHILD_OF")
@@ -78,16 +81,16 @@ def _set_bone(prop, value):
prop["bone"] = bone_name prop["bone"] = bone_name
def _get_size(prop): def _get_size(prop: PropertyGroup) -> Tuple[float, float, float]:
if prop.id_data.mmd_type != "RIGID_BODY": if prop.id_data.mmd_type != "RIGID_BODY":
return (0, 0, 0) return (0, 0, 0)
return FnRigidBody.get_rigid_body_size(prop.id_data) return FnRigidBody.get_rigid_body_size(prop.id_data)
def _set_size(prop, value): def _set_size(prop: PropertyGroup, value: Tuple[float, float, float]) -> None:
obj = prop.id_data obj: Object = prop.id_data
assert obj.mode == "OBJECT" # not support other mode yet assert obj.mode == "OBJECT" # not support other mode yet
shape = prop.shape shape: str = prop.shape
mesh = obj.data mesh = obj.data
rb = obj.rigid_body rb = obj.rigid_body
@@ -146,15 +149,15 @@ def _set_size(prop, value):
mesh.update() mesh.update()
def _get_rigid_name(prop): def _get_rigid_name(prop: PropertyGroup) -> str:
return prop.get("name", "") return prop.get("name", "")
def _set_rigid_name(prop, value): def _set_rigid_name(prop: PropertyGroup, value: str) -> None:
prop["name"] = value prop["name"] = value
class MMDRigidBody(bpy.types.PropertyGroup): class MMDRigidBody(PropertyGroup):
name_j: bpy.props.StringProperty( name_j: bpy.props.StringProperty(
name="Name", name="Name",
description="Japanese Name", description="Japanese Name",
@@ -227,16 +230,18 @@ class MMDRigidBody(bpy.types.PropertyGroup):
) )
@staticmethod @staticmethod
def register(): def register() -> None:
logger.debug("Registering MMDRigidBody property")
bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody)) bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody))
@staticmethod @staticmethod
def unregister(): def unregister() -> None:
logger.debug("Unregistering MMDRigidBody property")
del bpy.types.Object.mmd_rigid del bpy.types.Object.mmd_rigid
def _updateSpringLinear(prop, context): def _updateSpringLinear(prop: PropertyGroup, context: Context) -> None:
obj = prop.id_data obj: Object = prop.id_data
rbc = obj.rigid_body_constraint rbc = obj.rigid_body_constraint
if rbc: if rbc:
rbc.spring_stiffness_x = prop.spring_linear[0] rbc.spring_stiffness_x = prop.spring_linear[0]
@@ -244,8 +249,8 @@ def _updateSpringLinear(prop, context):
rbc.spring_stiffness_z = prop.spring_linear[2] rbc.spring_stiffness_z = prop.spring_linear[2]
def _updateSpringAngular(prop, context): def _updateSpringAngular(prop: PropertyGroup, context: Context) -> None:
obj = prop.id_data obj: Object = prop.id_data
rbc = obj.rigid_body_constraint rbc = obj.rigid_body_constraint
if rbc and hasattr(rbc, "use_spring_ang_x"): if rbc and hasattr(rbc, "use_spring_ang_x"):
rbc.spring_stiffness_ang_x = prop.spring_angular[0] rbc.spring_stiffness_ang_x = prop.spring_angular[0]
@@ -253,7 +258,7 @@ def _updateSpringAngular(prop, context):
rbc.spring_stiffness_ang_z = prop.spring_angular[2] rbc.spring_stiffness_ang_z = prop.spring_angular[2]
class MMDJoint(bpy.types.PropertyGroup): class MMDJoint(PropertyGroup):
name_j: bpy.props.StringProperty( name_j: bpy.props.StringProperty(
name="Name", name="Name",
description="Japanese Name", description="Japanese Name",
@@ -287,9 +292,12 @@ class MMDJoint(bpy.types.PropertyGroup):
) )
@staticmethod @staticmethod
def register(): def register() -> None:
logger.debug("Registering MMDJoint property")
bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint)) bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint))
@staticmethod @staticmethod
def unregister(): def unregister() -> None:
logger.debug("Unregistering MMDJoint property")
del bpy.types.Object.mmd_joint del bpy.types.Object.mmd_joint
+41 -25
View File
@@ -8,6 +8,7 @@
"""Properties for MMD model root object""" """Properties for MMD model root object"""
import bpy import bpy
from typing import Optional, List, Dict, Any, Set, Tuple, Union, Type, TypeVar, cast
from .. import utils from .. import utils
from ..bpyutils import FnContext from ..bpyutils import FnContext
@@ -17,9 +18,10 @@ from ..core.sdef import FnSDEF
from . import patch_library_overridable from . import patch_library_overridable
from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph
from .translations import MMDTranslation from .translations import MMDTranslation
from ....core.logging_setup import logger
def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1): def __driver_variables(constraint: bpy.types.Constraint, path: str, index: int = -1) -> Tuple[bpy.types.Driver, Any]:
d = constraint.driver_add(path, index) d = constraint.driver_add(path, index)
variables = d.driver.variables variables = d.driver.variables
for x in variables: for x in variables:
@@ -27,7 +29,7 @@ def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1):
return d.driver, variables return d.driver, variables
def __add_single_prop(variables, id_obj, data_path, prefix): def __add_single_prop(variables: Any, id_obj: bpy.types.Object, data_path: str, prefix: str) -> Any:
var = variables.new() var = variables.new()
var.name = prefix + str(len(variables)) var.name = prefix + str(len(variables))
var.type = "SINGLE_PROP" var.type = "SINGLE_PROP"
@@ -38,17 +40,18 @@ def __add_single_prop(variables, id_obj, data_path, prefix):
return var return var
def _toggleUsePropertyDriver(self: "MMDRoot", _context): def _toggleUsePropertyDriver(self: "MMDRoot", _context: bpy.types.Context) -> None:
root_object: bpy.types.Object = self.id_data root_object: bpy.types.Object = self.id_data
armature_object = FnModel.find_armature_object(root_object) armature_object = FnModel.find_armature_object(root_object)
if armature_object is None: if armature_object is None:
ik_map = {} ik_map: Dict[Any, Tuple[Any, Any]] = {}
else: else:
bones = armature_object.pose.bones bones = armature_object.pose.bones
ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones} ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones}
if self.use_property_driver: if self.use_property_driver:
logger.debug("Enabling property drivers for %s", root_object.name)
for ik, (b, c) in ik_map.items(): for ik, (b, c) in ik_map.items():
driver, variables = __driver_variables(c, "influence") driver, variables = __driver_variables(c, "influence")
driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name
@@ -63,6 +66,7 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context):
driver, variables = __driver_variables(i, prop_hide) driver, variables = __driver_variables(i, prop_hide)
driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name
else: else:
logger.debug("Disabling property drivers for %s", root_object.name)
for ik, (b, c) in ik_map.items(): for ik, (b, c) in ik_map.items():
c.driver_remove("influence") c.driver_remove("influence")
b = b if c.use_tail else b.parent b = b if c.use_tail else b.parent
@@ -80,31 +84,35 @@ def _toggleUsePropertyDriver(self: "MMDRoot", _context):
# =========================================== # ===========================================
def _toggleUseToonTexture(self: "MMDRoot", _context): def _toggleUseToonTexture(self: "MMDRoot", _context: bpy.types.Context) -> None:
use_toon = self.use_toon_texture use_toon = self.use_toon_texture
logger.debug("Toggling toon texture to %s for %s", use_toon, self.id_data.name)
for i in FnModel.iterate_mesh_objects(self.id_data): for i in FnModel.iterate_mesh_objects(self.id_data):
for m in i.data.materials: for m in i.data.materials:
if m: if m:
FnMaterial(m).use_toon_texture(use_toon) FnMaterial(m).use_toon_texture(use_toon)
def _toggleUseSphereTexture(self: "MMDRoot", _context): def _toggleUseSphereTexture(self: "MMDRoot", _context: bpy.types.Context) -> None:
use_sphere = self.use_sphere_texture use_sphere = self.use_sphere_texture
logger.debug("Toggling sphere texture to %s for %s", use_sphere, self.id_data.name)
for i in FnModel.iterate_mesh_objects(self.id_data): for i in FnModel.iterate_mesh_objects(self.id_data):
for m in i.data.materials: for m in i.data.materials:
if m: if m:
FnMaterial(m).use_sphere_texture(use_sphere, i) FnMaterial(m).use_sphere_texture(use_sphere, i)
def _toggleUseSDEF(self: "MMDRoot", _context): def _toggleUseSDEF(self: "MMDRoot", _context: bpy.types.Context) -> None:
mute_sdef = not self.use_sdef mute_sdef = not self.use_sdef
logger.debug("Toggling SDEF to %s for %s", not mute_sdef, self.id_data.name)
for i in FnModel.iterate_mesh_objects(self.id_data): for i in FnModel.iterate_mesh_objects(self.id_data):
FnSDEF.mute_sdef_set(i, mute_sdef) FnSDEF.mute_sdef_set(i, mute_sdef)
def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context): def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context) -> None:
root = self.id_data root = self.id_data
hide = not self.show_meshes hide = not self.show_meshes
logger.debug("Toggling mesh visibility to %s for %s", not hide, root.name)
for i in FnModel.iterate_mesh_objects(self.id_data): for i in FnModel.iterate_mesh_objects(self.id_data):
i.hide_set(hide) i.hide_set(hide)
i.hide_render = hide i.hide_render = hide
@@ -112,27 +120,30 @@ def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context):
FnContext.set_active_object(context, root) FnContext.set_active_object(context, root)
def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context): def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context) -> None:
root = self.id_data root = self.id_data
hide = not self.show_rigid_bodies hide = not self.show_rigid_bodies
logger.debug("Toggling rigid body visibility to %s for %s", not hide, root.name)
for i in FnModel.iterate_rigid_body_objects(root): for i in FnModel.iterate_rigid_body_objects(root):
i.hide_set(hide) i.hide_set(hide)
if hide and context.active_object is None: if hide and context.active_object is None:
FnContext.set_active_object(context, root) FnContext.set_active_object(context, root)
def _toggleVisibilityOfJoints(self: "MMDRoot", context): def _toggleVisibilityOfJoints(self: "MMDRoot", context: bpy.types.Context) -> None:
root_object = self.id_data root_object = self.id_data
hide = not self.show_joints hide = not self.show_joints
logger.debug("Toggling joint visibility to %s for %s", not hide, root_object.name)
for i in FnModel.iterate_joint_objects(root_object): for i in FnModel.iterate_joint_objects(root_object):
i.hide_set(hide) i.hide_set(hide)
if hide and context.active_object is None: if hide and context.active_object is None:
FnContext.set_active_object(context, root_object) FnContext.set_active_object(context, root_object)
def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context): def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context) -> None:
root_object: bpy.types.Object = self.id_data root_object: bpy.types.Object = self.id_data
hide = not self.show_temporary_objects hide = not self.show_temporary_objects
logger.debug("Toggling temporary object visibility to %s for %s", not hide, root_object.name)
with FnContext.temp_override_active_layer_collection(context, root_object): with FnContext.temp_override_active_layer_collection(context, root_object):
for i in FnModel.iterate_temporary_objects(root_object): for i in FnModel.iterate_temporary_objects(root_object):
i.hide_set(hide) i.hide_set(hide)
@@ -140,45 +151,48 @@ def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Cont
FnContext.set_active_object(context, root_object) FnContext.set_active_object(context, root_object)
def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context): def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context: bpy.types.Context) -> None:
root = self.id_data root = self.id_data
show_names = root.mmd_root.show_names_of_rigid_bodies show_names = root.mmd_root.show_names_of_rigid_bodies
logger.debug("Toggling rigid body names to %s for %s", show_names, root.name)
for i in FnModel.iterate_rigid_body_objects(root): for i in FnModel.iterate_rigid_body_objects(root):
i.show_name = show_names i.show_name = show_names
def _toggleShowNamesOfJoints(self: "MMDRoot", _context): def _toggleShowNamesOfJoints(self: "MMDRoot", _context: bpy.types.Context) -> None:
root = self.id_data root = self.id_data
show_names = root.mmd_root.show_names_of_joints show_names = root.mmd_root.show_names_of_joints
logger.debug("Toggling joint names to %s for %s", show_names, root.name)
for i in FnModel.iterate_joint_objects(root): for i in FnModel.iterate_joint_objects(root):
i.show_name = show_names i.show_name = show_names
def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool): def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool) -> None:
root = prop.id_data root = prop.id_data
arm = FnModel.find_armature_object(root) arm = FnModel.find_armature_object(root)
if arm is None: if arm is None:
return return
if not v and bpy.context.active_object == arm: if not v and bpy.context.active_object == arm:
FnContext.set_active_object(bpy.context, root) FnContext.set_active_object(bpy.context, root)
logger.debug("Setting armature visibility to %s for %s", v, root.name)
arm.hide_set(not v) arm.hide_set(not v)
def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"): def _getVisibilityOfMMDRigArmature(prop: "MMDRoot") -> bool:
if prop.id_data.mmd_type != "ROOT": if prop.id_data.mmd_type != "ROOT":
return False return False
arm = FnModel.find_armature_object(prop.id_data) arm = FnModel.find_armature_object(prop.id_data)
return arm and not arm.hide_get() return arm and not arm.hide_get()
def _setActiveRigidbodyObject(prop: "MMDRoot", v: int): def _setActiveRigidbodyObject(prop: "MMDRoot", v: int) -> None:
obj = FnContext.get_scene_objects(bpy.context)[v] obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_rigid_body_object(obj): if FnModel.is_rigid_body_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj) FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_rigidbody_object_index"] = v prop["active_rigidbody_object_index"] = v
def _getActiveRigidbodyObject(prop: "MMDRoot"): def _getActiveRigidbodyObject(prop: "MMDRoot") -> int:
context = bpy.context context = bpy.context
active_obj = FnContext.get_active_object(context) active_obj = FnContext.get_active_object(context)
if FnModel.is_rigid_body_object(active_obj): if FnModel.is_rigid_body_object(active_obj):
@@ -186,14 +200,14 @@ def _getActiveRigidbodyObject(prop: "MMDRoot"):
return prop.get("active_rigidbody_object_index", 0) return prop.get("active_rigidbody_object_index", 0)
def _setActiveJointObject(prop: "MMDRoot", v: int): def _setActiveJointObject(prop: "MMDRoot", v: int) -> None:
obj = FnContext.get_scene_objects(bpy.context)[v] obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_joint_object(obj): if FnModel.is_joint_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj) FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_joint_object_index"] = v prop["active_joint_object_index"] = v
def _getActiveJointObject(prop: "MMDRoot"): def _getActiveJointObject(prop: "MMDRoot") -> int:
context = bpy.context context = bpy.context
active_obj = FnContext.get_active_object(context) active_obj = FnContext.get_active_object(context)
if FnModel.is_joint_object(active_obj): if FnModel.is_joint_object(active_obj):
@@ -201,26 +215,26 @@ def _getActiveJointObject(prop: "MMDRoot"):
return prop.get("active_joint_object_index", 0) return prop.get("active_joint_object_index", 0)
def _setActiveMorph(prop: "MMDRoot", v: bool): def _setActiveMorph(prop: "MMDRoot", v: bool) -> None:
if "active_morph_indices" not in prop: if "active_morph_indices" not in prop:
prop["active_morph_indices"] = [0] * 5 prop["active_morph_indices"] = [0] * 5
prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v
def _getActiveMorph(prop: "MMDRoot"): def _getActiveMorph(prop: "MMDRoot") -> int:
if "active_morph_indices" in prop: if "active_morph_indices" in prop:
return prop["active_morph_indices"][prop.get("active_morph_type", 3)] return prop["active_morph_indices"][prop.get("active_morph_type", 3)]
return 0 return 0
def _setActiveMeshObject(prop: "MMDRoot", v: int): def _setActiveMeshObject(prop: "MMDRoot", v: int) -> None:
obj = FnContext.get_scene_objects(bpy.context)[v] obj = FnContext.get_scene_objects(bpy.context)[v]
if FnModel.is_mesh_object(obj): if FnModel.is_mesh_object(obj):
FnContext.set_active_and_select_single_object(bpy.context, obj) FnContext.set_active_and_select_single_object(bpy.context, obj)
prop["active_mesh_index"] = v prop["active_mesh_index"] = v
def _getActiveMeshObject(prop: "MMDRoot"): def _getActiveMeshObject(prop: "MMDRoot") -> int:
context = bpy.context context = bpy.context
active_obj = FnContext.get_active_object(context) active_obj = FnContext.get_active_object(context)
if FnModel.is_mesh_object(active_obj): if FnModel.is_mesh_object(active_obj):
@@ -520,7 +534,8 @@ class MMDRoot(bpy.types.PropertyGroup):
prop.hide_viewport = value prop.hide_viewport = value
@staticmethod @staticmethod
def register(): def register() -> None:
logger.debug("Registering MMDRoot property group")
bpy.types.Object.mmd_type = patch_library_overridable( bpy.types.Object.mmd_type = patch_library_overridable(
bpy.props.EnumProperty( bpy.props.EnumProperty(
name="Type", name="Type",
@@ -570,7 +585,8 @@ class MMDRoot(bpy.types.PropertyGroup):
) )
@staticmethod @staticmethod
def unregister(): def unregister() -> None:
logger.debug("Unregistering MMDRoot property group")
del bpy.types.Object.hide del bpy.types.Object.hide
del bpy.types.Object.select del bpy.types.Object.select
del bpy.types.Object.mmd_root del bpy.types.Object.mmd_root
+1 -1
View File
@@ -20,7 +20,7 @@ GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
# Define which version series this installation can update to # Define which version series this installation can update to
# For example: ["0.1"] means only look for 0.1.x updates # For example: ["0.1"] means only look for 0.1.x updates
# ["0.2", "0.3"] would look for both 0.2.x and 0.3.x updates # ["0.2", "0.3"] would look for both 0.2.x and 0.3.x updates
ALLOWED_VERSION_SERIES = ["0.2"] ALLOWED_VERSION_SERIES = ["0.3"]
is_checking_for_update: bool = False is_checking_for_update: bool = False
update_needed: bool = False update_needed: bool = False
+8 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.2.1)", "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.3.0)",
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there", "AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
"AvatarToolkit.desc2": "will be issues, if you find any issues,", "AvatarToolkit.desc2": "will be issues, if you find any issues,",
"AvatarToolkit.desc3": "please report it on our Github.", "AvatarToolkit.desc3": "please report it on our Github.",
@@ -63,6 +63,13 @@
"PoseMode.basis": "Basis", "PoseMode.basis": "Basis",
"Armature.validation.no_armature": "No armature selected", "Armature.validation.no_armature": "No armature selected",
"Armature.validation.pmx_model_detected": "PMX model detected. Japanese bone names may not match standard naming conventions.",
"Armature.validation.pmx_model_strict": "Consider using the 'Standardize Armature' option to convert Japanese bone names to standard names.",
"Armature.validation.pmx_model_standardize": "This will make the model compatible with standard avatar systems.",
"Armature.validation.pmx_model_basic": "PMX models use Japanese bone names which may not match standard naming conventions.",
"Armature.validation.unknown_format": "Unknown armature format detected.",
"Validation.mode.none": "Validation is disabled in settings.",
"Validation.no_messages": "No validation messages available.",
"Armature.validation.not_armature": "Selected object is not an armature", "Armature.validation.not_armature": "Selected object is not an armature",
"Armature.validation.no_bones": "Armature has no bones", "Armature.validation.no_bones": "Armature has no bones",
"Armature.validation.basic_check_failed": "Basic armature validation failed", "Armature.validation.basic_check_failed": "Basic armature validation failed",
+8 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "アバターツールキット (アルファ 0.2.1)", "AvatarToolkit.label": "アバターツールキット (アルファ 0.3.0)",
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、", "AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、", "AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
"AvatarToolkit.desc3": "GitHubで報告してください。", "AvatarToolkit.desc3": "GitHubで報告してください。",
@@ -63,6 +63,13 @@
"PoseMode.basis": "基本形", "PoseMode.basis": "基本形",
"Armature.validation.no_armature": "アーマチュアが選択されていません", "Armature.validation.no_armature": "アーマチュアが選択されていません",
"Armature.validation.pmx_model_detected": "PMXモデルが検出されました。日本語の骨名が標準の命名規則と一致しない場合があります。",
"Armature.validation.pmx_model_strict": "「アーマチュアの標準化」オプションを使用して、日本語の骨名を標準名に変換することを検討してください。",
"Armature.validation.pmx_model_standardize": "これにより、モデルが標準的なアバターシステムと互換性を持つようになります。",
"Armature.validation.pmx_model_basic": "PMXモデルは日本語の骨名を使用しており、標準の命名規則と一致しない場合があります。",
"Armature.validation.unknown_format": "不明なアーマチュア形式が検出されました。",
"Validation.mode.none": "検証は設定で無効になっています。",
"Validation.no_messages": "検証メッセージはありません。",
"Armature.validation.not_armature": "選択されたオブジェクトはアーマチュアではありません", "Armature.validation.not_armature": "選択されたオブジェクトはアーマチュアではありません",
"Armature.validation.no_bones": "アーマチュアにボーンがありません", "Armature.validation.no_bones": "アーマチュアにボーンがありません",
"Armature.validation.basic_check_failed": "基本的なアーマチュア検証に失敗しました", "Armature.validation.basic_check_failed": "基本的なアーマチュア検証に失敗しました",
+8 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "아바타 툴킷 (알파 0.2.1)", "AvatarToolkit.label": "아바타 툴킷 (알파 0.3.0)",
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로", "AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면", "AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
"AvatarToolkit.desc3": "Github에 보고해 주세요.", "AvatarToolkit.desc3": "Github에 보고해 주세요.",
@@ -63,6 +63,13 @@
"PoseMode.basis": "기본", "PoseMode.basis": "기본",
"Armature.validation.no_armature": "선택된 아마추어 없음", "Armature.validation.no_armature": "선택된 아마추어 없음",
"Armature.validation.pmx_model_detected": "PMX 모델이 감지되었습니다. 일본어 본 이름이 표준 명명 규칙과 일치하지 않을 수 있습니다.",
"Armature.validation.pmx_model_strict": "'아마추어 표준화' 옵션을 사용하여 일본어 본 이름을 표준 이름으로 변환하는 것을 고려하세요.",
"Armature.validation.pmx_model_standardize": "이렇게 하면 모델이 표준 아바타 시스템과 호환됩니다.",
"Armature.validation.pmx_model_basic": "PMX 모델은 일본어 본 이름을 사용하며 표준 명명 규칙과 일치하지 않을 수 있습니다.",
"Armature.validation.unknown_format": "알 수 없는 아마추어 형식이 감지되었습니다.",
"Validation.mode.none": "유효성 검사가 설정에서 비활성화되었습니다.",
"Validation.no_messages": "사용 가능한 유효성 검사 메시지가 없습니다.",
"Armature.validation.not_armature": "선택된 객체가 아마추어가 아님", "Armature.validation.not_armature": "선택된 객체가 아마추어가 아님",
"Armature.validation.no_bones": "아마추어에 본이 없음", "Armature.validation.no_bones": "아마추어에 본이 없음",
"Armature.validation.basic_check_failed": "기본 아마추어 검증 실패", "Armature.validation.basic_check_failed": "기본 아마추어 검증 실패",
+57 -11
View File
@@ -89,16 +89,33 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
if active_armature: if active_armature:
is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = validate_armature(active_armature, detailed_messages=True) is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = validate_armature(active_armature, detailed_messages=True)
# Check if this is a PMX model
is_pmx_model = False
if hasattr(active_armature, 'mmd_type') or (hasattr(active_armature, 'parent') and active_armature.parent and hasattr(active_armature.parent, 'mmd_type')):
is_pmx_model = True
info_box = col.box() info_box = col.box()
# If it's a PMX model, display a prominent notice
if is_pmx_model:
pmx_box = info_box.box()
pmx_box.label(text=t("Armature.validation.pmx_model_detected"), icon='INFO')
validation_mode = context.scene.avatar_toolkit.validation_mode
if validation_mode == 'STRICT':
pmx_box.label(text=t("Armature.validation.pmx_model_strict"))
pmx_box.label(text=t("Armature.validation.pmx_model_standardize"))
else:
pmx_box.label(text=t("Armature.validation.pmx_model_basic"))
if not is_valid: if not is_valid:
# Display non-standard bones and hierarchy issues # Display non-standard bones and hierarchy issues
if len(messages) > 1: if messages and len(messages) > 0:
# Found Bones section # Found Bones section
validation_box = info_box.box() validation_box = info_box.box()
row = validation_box.row() row = validation_box.row()
row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False) row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False)
if props.show_found_bones: if props.show_found_bones and len(messages) > 0:
for line in messages[0].split('\n'): for line in messages[0].split('\n'):
validation_box.label(text=line) validation_box.label(text=line)
@@ -127,15 +144,31 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"), row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"),
icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False) icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False)
if props.show_non_standard: if props.show_non_standard:
if non_standard_messages: if non_standard_messages and len(non_standard_messages) > 0:
for message in non_standard_messages: for message in non_standard_messages:
for line in message.split('\n'): for line in message.split('\n'):
sub_row = validation_box.row() sub_row = validation_box.row()
sub_row.alert = True sub_row.alert = True
sub_row.label(text=line) sub_row.label(text=line)
else: else:
sub_row = validation_box.row() # For PMX models, if no non-standard messages but it's a PMX model,
sub_row.label(text=t("Validation.no_non_standard_issues")) # we should still indicate there might be non-standard bones
if is_pmx_model:
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_basic"))
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_strict"))
sub_row = validation_box.row()
sub_row.alert = True
sub_row.label(text=t("Armature.validation.pmx_model_standardize"))
else:
sub_row = validation_box.row()
sub_row.label(text=t("Validation.no_non_standard_issues"))
# Hierarchy Issues section # Hierarchy Issues section
validation_box = info_box.box() validation_box = info_box.box()
@@ -190,9 +223,14 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
row.label(text=msg.name) row.label(text=msg.name)
else: else:
# If no specific issues, show acceptable message # If no specific issues, show acceptable message
info_box.label(text=messages[0], icon='INFO') if messages and len(messages) > 0:
info_box.label(text=messages[1]) info_box.label(text=messages[0], icon='INFO')
info_box.label(text=messages[2]) if len(messages) > 1:
info_box.label(text=messages[1])
if len(messages) > 2:
info_box.label(text=messages[2])
else:
info_box.label(text=t("Validation.no_messages"), icon='INFO')
elif is_valid and not is_acceptable: elif is_valid and not is_acceptable:
row = info_box.row() row = info_box.row()
split = row.split(factor=0.6) split = row.split(factor=0.6)
@@ -204,9 +242,16 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
elif is_valid and is_acceptable: elif is_valid and is_acceptable:
# Show acceptable standard message # Show acceptable standard message
info_box.label(text=messages[0], icon='INFO') if messages and len(messages) > 0:
info_box.label(text=messages[1]) info_box.label(text=messages[0], icon='INFO')
info_box.label(text=messages[2])
# Only try to access additional messages if they exist
if len(messages) > 1:
info_box.label(text=messages[1])
if len(messages) > 2:
info_box.label(text=messages[2])
else:
info_box.label(text=t("Validation.no_messages"), icon='INFO')
# Add standardize button # Add standardize button
standardize_box = info_box.box() standardize_box = info_box.box()
@@ -252,3 +297,4 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
button_row.scale_y = 1.5 button_row.scale_y = 1.5
button_row.operator(AvatarToolKit_OT_Import.bl_idname, text=t("QuickAccess.import"), icon='IMPORT') button_row.operator(AvatarToolKit_OT_Import.bl_idname, text=t("QuickAccess.import"), icon='IMPORT')
button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT') button_row.operator(AvatarToolKit_OT_ExportMenu.bl_idname, text=t("QuickAccess.export"), icon='EXPORT')