Holy shit this was a pain
- Truly fixes PMX Import lol, i messed up completely - Updated MMD Tools to use Cats One
This commit is contained in:
@@ -3,4 +3,4 @@
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# 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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+143
-99
@@ -1,22 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# 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.
|
||||
# This file is part of MMD Tools.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, StringProperty, FloatProperty
|
||||
from bpy.types import Operator, Context, Object, Material
|
||||
|
||||
from typing import Set, Dict, Any, List, Tuple, Optional, Union, cast
|
||||
from bpy.props import BoolProperty, StringProperty
|
||||
from bpy.types import Operator
|
||||
|
||||
from .. import cycles_converter
|
||||
from ..core.exceptions import MaterialNotFoundError
|
||||
from ..core.material import FnMaterial
|
||||
from ..core.shader import _NodeGroupUtils
|
||||
from ....core.logging_setup import logger
|
||||
import traceback
|
||||
|
||||
|
||||
class ConvertMaterialsForCycles(Operator):
|
||||
@@ -25,14 +19,14 @@ class ConvertMaterialsForCycles(Operator):
|
||||
bl_description = "Convert materials of selected objects for Cycles."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_principled: BoolProperty(
|
||||
use_principled: bpy.props.BoolProperty(
|
||||
name="Convert to Principled BSDF",
|
||||
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
|
||||
default=False,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
clean_nodes: BoolProperty(
|
||||
clean_nodes: bpy.props.BoolProperty(
|
||||
name="Clean Nodes",
|
||||
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
|
||||
default=False,
|
||||
@@ -40,27 +34,22 @@ class ConvertMaterialsForCycles(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
|
||||
def poll(cls, context):
|
||||
return any(x.type == "MESH" for x in context.selected_objects)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(self, "use_principled")
|
||||
layout.prop(self, "clean_nodes")
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
try:
|
||||
context.scene.render.engine = "CYCLES"
|
||||
except Exception:
|
||||
logger.error(f"Failed to change to Cycles render engine: {traceback.format_exc()}")
|
||||
self.report({"ERROR"}, " * Failed to change to Cycles render engine.")
|
||||
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"):
|
||||
logger.debug(f"Converting materials for object: {obj.name}")
|
||||
cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -70,21 +59,21 @@ class ConvertMaterials(Operator):
|
||||
bl_description = "Convert materials of selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
use_principled: BoolProperty(
|
||||
use_principled: bpy.props.BoolProperty(
|
||||
name="Convert to Principled BSDF",
|
||||
description="Convert MMD shader nodes to Principled BSDF as well if enabled",
|
||||
default=True,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
clean_nodes: BoolProperty(
|
||||
clean_nodes: bpy.props.BoolProperty(
|
||||
name="Clean Nodes",
|
||||
description="Remove redundant nodes as well if enabled. Disable it to keep node data.",
|
||||
default=True,
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
subsurface: FloatProperty(
|
||||
subsurface: bpy.props.FloatProperty(
|
||||
name="Subsurface",
|
||||
default=0.001,
|
||||
soft_min=0.000,
|
||||
@@ -94,41 +83,130 @@ class ConvertMaterials(Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return next((x for x in context.selected_objects if x.type == "MESH"), None) is not None
|
||||
def poll(cls, context):
|
||||
return any(x.type == "MESH" for x in context.selected_objects)
|
||||
|
||||
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}")
|
||||
def execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != "MESH":
|
||||
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)
|
||||
return {"FINISHED"}
|
||||
|
||||
class ConvertBSDFMaterials(Operator):
|
||||
bl_idname = 'mmd_tools.convert_bsdf_materials'
|
||||
bl_label = 'Convert Blender Materials'
|
||||
bl_description = 'Convert materials of selected objects.'
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
class MergeMaterials(Operator):
|
||||
bl_idname = "mmd_tools.merge_materials"
|
||||
bl_label = "Merge Materials"
|
||||
bl_description = "Merge materials with the same texture in selected objects. Only merges materials with exactly one texture node. Materials with no texture or with multiple textures are not merged. Please convert to Blender materials first."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return next((x for x in context.selected_objects if x.type == 'MESH'), None) is not None
|
||||
def poll(cls, context):
|
||||
return any(x.type == "MESH" for x in context.selected_objects)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
logger.info("Converting BSDF materials to MMD shader")
|
||||
def execute(self, context):
|
||||
# Process all selected mesh objects
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != 'MESH':
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
|
||||
self.merge_materials_for_object(context, obj)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def merge_materials_for_object(self, context, obj):
|
||||
"""Merge materials with same texture for a single object"""
|
||||
if not obj.data.materials:
|
||||
self.report({"INFO"}, f"Object '{obj.name}' has no materials")
|
||||
return
|
||||
|
||||
# Map texture paths to material indices and names
|
||||
texture_to_materials = defaultdict(list)
|
||||
|
||||
# Check each material
|
||||
for i, material in enumerate(obj.data.materials):
|
||||
# use_nodes is deprecated in 5.0 but always returns True, so check is safe
|
||||
if not material or not material.use_nodes:
|
||||
continue
|
||||
|
||||
# 1. Check texture node count (must be exactly 1)
|
||||
texture_nodes = [node for node in material.node_tree.nodes if node.type == "TEX_IMAGE"]
|
||||
if len(texture_nodes) != 1:
|
||||
continue
|
||||
|
||||
# 2. Record texture path and material info
|
||||
texture_node = texture_nodes[0]
|
||||
if texture_node.image:
|
||||
texture_path = bpy.path.abspath(texture_node.image.filepath)
|
||||
texture_to_materials[texture_path].append({"index": i, "name": material.name})
|
||||
|
||||
# Find material groups that need merging
|
||||
materials_to_merge = {path: materials for path, materials in texture_to_materials.items() if len(materials) > 1}
|
||||
|
||||
if not materials_to_merge:
|
||||
self.report({"INFO"}, f"No materials to merge in object '{obj.name}'")
|
||||
return
|
||||
|
||||
# Process each texture group
|
||||
context.view_layer.objects.active = obj
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
merge_details = []
|
||||
for texture_path, materials in materials_to_merge.items():
|
||||
# Use first material as target
|
||||
target_material = materials[0]
|
||||
target_index = target_material["index"]
|
||||
target_name = target_material["name"]
|
||||
|
||||
source_materials = []
|
||||
|
||||
# Reassign faces from other materials to target material
|
||||
for source_material in materials[1:]:
|
||||
source_index = source_material["index"]
|
||||
source_name = source_material["name"]
|
||||
source_materials.append(source_name)
|
||||
|
||||
bpy.ops.mesh.select_all(action="DESELECT")
|
||||
obj.active_material_index = source_index
|
||||
bpy.ops.object.material_slot_select()
|
||||
obj.active_material_index = target_index
|
||||
bpy.ops.object.material_slot_assign()
|
||||
|
||||
# Record merge details
|
||||
texture_name = bpy.path.basename(texture_path)
|
||||
merge_details.append({"texture": texture_name, "target": target_name, "sources": source_materials})
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
bpy.ops.object.material_slot_remove_unused()
|
||||
|
||||
merged_count = sum(len(details["sources"]) for details in merge_details)
|
||||
self.report({"INFO"}, f"Object '{obj.name}': Merged {merged_count} materials")
|
||||
|
||||
for details in merge_details:
|
||||
sources_text = ", ".join(details["sources"])
|
||||
self.report({"INFO"}, f"Same Texture '{details['texture']}': Merged materials [{sources_text}] into '{details['target']}'")
|
||||
|
||||
|
||||
class ConvertBSDFMaterials(Operator):
|
||||
bl_idname = "mmd_tools.convert_bsdf_materials"
|
||||
bl_label = "Convert Blender Materials"
|
||||
bl_description = "Convert materials of selected objects."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return any(x.type == "MESH" for x in context.selected_objects)
|
||||
|
||||
def execute(self, context):
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
logger.debug(f"Converting BSDF materials for object: {obj.name}")
|
||||
cycles_converter.convertToMMDShader(obj)
|
||||
return {'FINISHED'}
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class _OpenTextureBase:
|
||||
"""Create a texture for mmd model material."""
|
||||
|
||||
bl_options: Set[str] = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
filepath: StringProperty(
|
||||
name="File Path",
|
||||
@@ -142,7 +220,7 @@ class _OpenTextureBase:
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
@@ -152,13 +230,8 @@ class OpenTexture(Operator, _OpenTextureBase):
|
||||
bl_label = "Open Texture"
|
||||
bl_description = "Create main texture of active material"
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
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.create_texture(self.filepath)
|
||||
return {"FINISHED"}
|
||||
@@ -172,13 +245,8 @@ class RemoveTexture(Operator):
|
||||
bl_description = "Remove main texture of active material"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
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.remove_texture()
|
||||
return {"FINISHED"}
|
||||
@@ -191,13 +259,8 @@ class OpenSphereTextureSlot(Operator, _OpenTextureBase):
|
||||
bl_label = "Open Sphere Texture"
|
||||
bl_description = "Create sphere texture of active material"
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
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.create_sphere_texture(self.filepath, context.active_object)
|
||||
return {"FINISHED"}
|
||||
@@ -211,13 +274,8 @@ class RemoveSphereTexture(Operator):
|
||||
bl_description = "Remove sphere texture of active material"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
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.remove_sphere_texture()
|
||||
return {"FINISHED"}
|
||||
@@ -230,21 +288,17 @@ class MoveMaterialUp(Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
|
||||
return bool(valid_mesh and obj.active_material_index > 0)
|
||||
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" and obj.active_material_index > 0
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
current_idx = obj.active_material_index
|
||||
prev_index = current_idx - 1
|
||||
|
||||
logger.debug(f"Moving material {current_idx} up to position {prev_index} for object {obj.name}")
|
||||
try:
|
||||
FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True)
|
||||
except MaterialNotFoundError:
|
||||
logger.error(f"Materials not found for indices {current_idx} and {prev_index}")
|
||||
self.report({"ERROR"}, "Materials not found")
|
||||
return {"CANCELLED"}
|
||||
obj.active_material_index = prev_index
|
||||
@@ -259,21 +313,17 @@ class MoveMaterialDown(Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE"
|
||||
return bool(valid_mesh and obj.active_material_index < len(obj.material_slots) - 1)
|
||||
return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" and obj.active_material_index < len(obj.material_slots) - 1
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
current_idx = obj.active_material_index
|
||||
next_index = current_idx + 1
|
||||
|
||||
logger.debug(f"Moving material {current_idx} down to position {next_index} for object {obj.name}")
|
||||
try:
|
||||
FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True)
|
||||
except MaterialNotFoundError:
|
||||
logger.error(f"Materials not found for indices {current_idx} and {next_index}")
|
||||
self.report({"ERROR"}, "Materials not found")
|
||||
return {"CANCELLED"}
|
||||
obj.active_material_index = next_index
|
||||
@@ -296,31 +346,26 @@ class EdgePreviewSetup(Operator):
|
||||
default="CREATE",
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
from ..core.model import FnModel
|
||||
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
logger.error("No MMD model root found")
|
||||
self.report({"ERROR"}, "Select a MMD model")
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.action == "CLEAN":
|
||||
logger.info(f"Cleaning toon edge for model: {root.name}")
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
self.__clean_toon_edge(obj)
|
||||
else:
|
||||
from ..bpyutils import Props
|
||||
|
||||
logger.info(f"Creating toon edge for model: {root.name}")
|
||||
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))
|
||||
logger.info(f"Created {counts} toon edge(s)")
|
||||
self.report({"INFO"}, "Created %d toon edge(s)" % counts)
|
||||
return {"FINISHED"}
|
||||
|
||||
def __clean_toon_edge(self, obj: Object) -> None:
|
||||
logger.debug(f"Cleaning toon edge for object: {obj.name}")
|
||||
def __clean_toon_edge(self, obj):
|
||||
if "mmd_edge_preview" in obj.modifiers:
|
||||
obj.modifiers.remove(obj.modifiers["mmd_edge_preview"])
|
||||
|
||||
@@ -329,8 +374,7 @@ class EdgePreviewSetup(Operator):
|
||||
|
||||
FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge."))
|
||||
|
||||
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}")
|
||||
def __create_toon_edge(self, obj, scale=1.0):
|
||||
self.__clean_toon_edge(obj)
|
||||
materials = obj.data.materials
|
||||
material_offset = len(materials)
|
||||
@@ -355,10 +399,10 @@ class EdgePreviewSetup(Operator):
|
||||
mod.vertex_group = "mmd_edge_preview"
|
||||
return len(materials) - material_offset
|
||||
|
||||
def __create_edge_preview_group(self, obj: Object) -> None:
|
||||
def __create_edge_preview_group(self, obj):
|
||||
vertices, materials = obj.data.vertices, obj.data.materials
|
||||
weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m}
|
||||
scale_map: Dict[int, float] = {}
|
||||
scale_map = {}
|
||||
vg_scale_index = obj.vertex_groups.find("mmd_edge_scale")
|
||||
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}
|
||||
@@ -367,7 +411,7 @@ class EdgePreviewSetup(Operator):
|
||||
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")
|
||||
|
||||
def __get_edge_material(self, mat_name: str, edge_color: Tuple[float, float, float, float], materials: List[Material]) -> Material:
|
||||
def __get_edge_material(self, mat_name, edge_color, materials):
|
||||
if mat_name in materials:
|
||||
return materials[mat_name]
|
||||
mat = bpy.data.materials.get(mat_name, None)
|
||||
@@ -385,8 +429,8 @@ class EdgePreviewSetup(Operator):
|
||||
self.__make_shader(mat)
|
||||
return mat
|
||||
|
||||
def __make_shader(self, m: Material) -> None:
|
||||
# Note: material.use_nodes is deprecated in Blender 5.0 - materials always use nodes
|
||||
def __make_shader(self, m):
|
||||
m.use_nodes = True
|
||||
nodes, links = m.node_tree.nodes, m.node_tree.links
|
||||
|
||||
node_shader = nodes.get("mmd_edge_preview", None)
|
||||
@@ -406,7 +450,7 @@ class EdgePreviewSetup(Operator):
|
||||
node_shader.inputs["Color"].default_value = m.mmd_material.edge_color
|
||||
node_shader.inputs["Alpha"].default_value = m.mmd_material.edge_color[3]
|
||||
|
||||
def __get_edge_preview_shader(self) -> bpy.types.NodeTree:
|
||||
def __get_edge_preview_shader(self):
|
||||
group_name = "MMDEdgePreview"
|
||||
shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree")
|
||||
if len(shader.nodes):
|
||||
@@ -414,8 +458,8 @@ class EdgePreviewSetup(Operator):
|
||||
|
||||
ng = _NodeGroupUtils(shader)
|
||||
|
||||
node_input = ng.new_node("NodeGroupInput", (-5, 0))
|
||||
node_output = ng.new_node("NodeGroupOutput", (3, 0))
|
||||
ng.new_node("NodeGroupInput", (-5, 0))
|
||||
ng.new_node("NodeGroupOutput", (3, 0))
|
||||
|
||||
############################################################################
|
||||
node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5))
|
||||
|
||||
+47
-68
@@ -1,22 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# 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.
|
||||
# This file is part of MMD Tools.
|
||||
|
||||
import re
|
||||
from typing import List, Dict, Any, Set, Optional, Tuple, Union, Type
|
||||
|
||||
import bpy
|
||||
from bpy.types import Context, Object, Operator, ShapeKey
|
||||
|
||||
from .. import utils
|
||||
from ..bpyutils import FnContext, FnObject
|
||||
from ..core.bone import FnBone
|
||||
from ..core.model import FnModel, Model
|
||||
from ..core.morph import FnMorph
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
class SelectObject(bpy.types.Operator):
|
||||
@@ -32,8 +25,7 @@ class SelectObject(bpy.types.Operator):
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
logger.debug(f"Selecting object: {self.name}")
|
||||
def execute(self, context):
|
||||
utils.selectAObject(context.scene.objects[self.name])
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -47,43 +39,41 @@ class MoveObject(bpy.types.Operator, utils.ItemMoveOp):
|
||||
__PREFIX_REGEXP = re.compile(r"(?P<prefix>[0-9A-Z]{3}_)(?P<name>.*)")
|
||||
|
||||
@classmethod
|
||||
def set_index(cls, obj: Object, index: int) -> None:
|
||||
def set_index(cls, obj, index):
|
||||
m = cls.__PREFIX_REGEXP.match(obj.name)
|
||||
name = m.group("name") if m else obj.name
|
||||
obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name)
|
||||
obj.name = f"{utils.int2base(index, 36, 3)}_{name}"
|
||||
|
||||
@classmethod
|
||||
def get_name(cls, obj: Object, prefix: Optional[str] = None) -> str:
|
||||
def get_name(cls, obj, prefix=None):
|
||||
m = cls.__PREFIX_REGEXP.match(obj.name)
|
||||
name = m.group("name") if m else obj.name
|
||||
return name[len(prefix) :] if prefix and name.startswith(prefix) else name
|
||||
|
||||
@classmethod
|
||||
def normalize_indices(cls, objects: List[Object]) -> None:
|
||||
def normalize_indices(cls, objects):
|
||||
for i, x in enumerate(objects):
|
||||
cls.set_index(x, i)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
def poll(cls, context):
|
||||
return context.active_object is not None
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
objects = self.__get_objects(obj)
|
||||
if obj not in objects:
|
||||
logger.error(f'Cannot move object "{obj.name}"')
|
||||
self.report({"ERROR"}, f'Can not move object "{obj.name}"')
|
||||
return {"CANCELLED"}
|
||||
|
||||
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.normalize_indices(objects)
|
||||
return {"FINISHED"}
|
||||
|
||||
def __get_objects(self, obj: Object) -> Any:
|
||||
def __get_objects(self, obj):
|
||||
class __MovableList(list):
|
||||
def move(self, index_old: int, index_new: int) -> None:
|
||||
def move(self, index_old, index_new):
|
||||
item = self[index_old]
|
||||
self.remove(item)
|
||||
self.insert(index_new, item)
|
||||
@@ -108,43 +98,40 @@ class CleanShapeKeys(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
def poll(cls, context):
|
||||
return any(o.type == "MESH" for o in context.selected_objects)
|
||||
|
||||
@staticmethod
|
||||
def __can_remove(key_block: ShapeKey) -> bool:
|
||||
def __can_remove(key_block):
|
||||
if key_block.relative_key == key_block:
|
||||
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, strict=False):
|
||||
if v0.co != v1.co:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __shape_key_clean(self, obj: Object, key_blocks: List[ShapeKey]) -> None:
|
||||
def __shape_key_clean(self, obj, key_blocks):
|
||||
for kb in key_blocks:
|
||||
if self.__can_remove(kb):
|
||||
logger.debug(f"Removing unused shape key: {kb.name} from {obj.name}")
|
||||
FnObject.mesh_remove_shape_key(obj, kb)
|
||||
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])
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
logger.info("Cleaning shape keys for selected objects")
|
||||
obj: Object
|
||||
def execute(self, context):
|
||||
obj: bpy.types.Object
|
||||
for obj in context.selected_objects:
|
||||
if obj.type != "MESH" or obj.data.shape_keys is None:
|
||||
continue
|
||||
if not obj.data.shape_keys.use_relative:
|
||||
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)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SeparateByMaterials(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.separate_by_materials"
|
||||
bl_label = "Separate By Materials"
|
||||
bl_label = "Sep by Mat(High Risk)"
|
||||
bl_description = "Separate by Materials (High Risk)\nSeparate the mesh into multiple objects based on materials.\nHIGH RISK & BUGGY: This operation is not reversible and may cause various issues. It splits adjacent geometry by material, and merging later will not reconnect shared edges.\nKnown issues include potential mesh corruption, UV mapping problems, and other unpredictable behaviors. Use with extreme caution and backup your work first."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
clean_shape_keys: bpy.props.BoolProperty(
|
||||
@@ -153,26 +140,32 @@ class SeparateByMaterials(bpy.types.Operator):
|
||||
default=True,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "MESH"
|
||||
keep_normals: bpy.props.BoolProperty(
|
||||
name="Keep Normals",
|
||||
default=True,
|
||||
)
|
||||
|
||||
def __separate_by_materials(self, obj: Object) -> None:
|
||||
logger.info(f"Separating {obj.name} by materials")
|
||||
utils.separateByMaterials(obj)
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return obj is not None and obj.type == "MESH"
|
||||
|
||||
def __separate_by_materials(self, obj):
|
||||
utils.separateByMaterials(obj, self.keep_normals)
|
||||
if self.clean_shape_keys:
|
||||
logger.debug("Cleaning shape keys after separation")
|
||||
bpy.ops.mmd_tools.clean_shape_keys()
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
|
||||
# Sep by Mat crashes Blender if used after morph assembly
|
||||
rig = Model(root)
|
||||
rig.morph_slider.unbind()
|
||||
|
||||
if root is None:
|
||||
logger.debug("No root object found, separating single object")
|
||||
self.__separate_by_materials(obj)
|
||||
else:
|
||||
logger.debug(f"Root object found: {root.name}, preparing for separation")
|
||||
bpy.ops.mmd_tools.clear_temp_materials()
|
||||
bpy.ops.mmd_tools.clear_uv_morph_view()
|
||||
|
||||
@@ -185,11 +178,9 @@ class SeparateByMaterials(bpy.types.Operator):
|
||||
if len(mesh.data.materials) > 0:
|
||||
mat = mesh.data.materials[0]
|
||||
idx = mat_names.index(getattr(mat, "name", None))
|
||||
logger.debug(f"Setting index {idx} for mesh {mesh.name}")
|
||||
MoveObject.set_index(mesh, idx)
|
||||
|
||||
for morph in root.mmd_root.material_morphs:
|
||||
logger.debug(f"Updating material morph: {morph.name}")
|
||||
FnMorph(morph, rig).update_mat_related_mesh()
|
||||
utils.clearUnusedMeshes()
|
||||
return {"FINISHED"}
|
||||
@@ -207,15 +198,13 @@ class JoinMeshes(bpy.types.Operator):
|
||||
default=True,
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
if root is None:
|
||||
logger.error("No MMD model found")
|
||||
self.report({"ERROR"}, "Select a MMD model")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Joining meshes for model: {root.name}")
|
||||
bpy.ops.mmd_tools.clear_temp_materials()
|
||||
bpy.ops.mmd_tools.clear_uv_morph_view()
|
||||
|
||||
@@ -223,11 +212,9 @@ class JoinMeshes(bpy.types.Operator):
|
||||
rig = Model(root)
|
||||
meshes_list = sorted(rig.meshes(), key=lambda x: x.name)
|
||||
if not meshes_list:
|
||||
logger.error("No meshes found in the model")
|
||||
self.report({"ERROR"}, "The model does not have any meshes")
|
||||
return {"CANCELLED"}
|
||||
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.set_active_object(context, active_mesh)
|
||||
@@ -236,19 +223,15 @@ class JoinMeshes(bpy.types.Operator):
|
||||
for m in meshes_list[1:]:
|
||||
for mat in m.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)
|
||||
|
||||
# Join selected meshes
|
||||
logger.debug("Joining meshes")
|
||||
bpy.ops.object.join()
|
||||
|
||||
if self.sort_shape_keys:
|
||||
logger.debug("Sorting shape keys")
|
||||
FnMorph.fixShapeKeyOrder(active_mesh, root.mmd_root.vertex_morphs.keys())
|
||||
active_mesh.active_shape_key_index = 0
|
||||
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)
|
||||
utils.clearUnusedMeshes()
|
||||
return {"FINISHED"}
|
||||
@@ -262,20 +245,17 @@ class AttachMeshesToMMD(bpy.types.Operator):
|
||||
|
||||
add_armature_modifier: bpy.props.BoolProperty(default=True)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context: bpy.types.Context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
logger.error("No MMD model found")
|
||||
self.report({"ERROR"}, "Select a MMD model")
|
||||
return {"CANCELLED"}
|
||||
|
||||
armObj = FnModel.find_armature_object(root)
|
||||
if armObj is None:
|
||||
logger.error("Model armature not found")
|
||||
self.report({"ERROR"}, "Model Armature not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
logger.info(f"Attaching meshes to model: {root.name}")
|
||||
FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -295,18 +275,18 @@ class ChangeMMDIKLoopFactor(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return FnModel.find_root_object(context.active_object) is not None
|
||||
def poll(cls, context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
return root is not None
|
||||
|
||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||
def invoke(self, context, event):
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
self.mmd_ik_loop_factor = root_object.mmd_root.ik_loop_factor
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
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)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -318,22 +298,21 @@ class RecalculateBoneRoll(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return obj and obj.type == "ARMATURE"
|
||||
return obj is not None and obj.type == "ARMATURE"
|
||||
|
||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
c = layout.column()
|
||||
c.label(text="This operation will break existing f-curve/action.", icon="QUESTION")
|
||||
c.label(text="Click [OK] to run the operation.")
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
arm = context.active_object
|
||||
logger.info(f"Recalculating bone roll for armature: {arm.name}")
|
||||
FnBone.apply_auto_bone_roll(arm)
|
||||
return {"FINISHED"}
|
||||
|
||||
+240
-139
@@ -1,32 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# 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.
|
||||
# Copyright 2022 MMD Tools authors
|
||||
# This file is part of MMD Tools.
|
||||
|
||||
import itertools
|
||||
from operator import itemgetter
|
||||
from typing import Dict, List, Optional, Set, Tuple, Any
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from bpy.types import Context, Object, Operator, EditBone, Mesh, Armature
|
||||
from mathutils import Matrix
|
||||
|
||||
from ..bpyutils import FnContext
|
||||
from ..bpyutils import FnContext, select_object
|
||||
from ..core.model import FnModel, Model
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
class MessageException(Exception):
|
||||
"""Class for error with message."""
|
||||
class NoModelSelectedError(Exception):
|
||||
"""Raised when no MMD model is selected."""
|
||||
|
||||
|
||||
class ModelJoinByBonesOperator(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.model_join_by_bones"
|
||||
bl_label = "Model Join by Bones"
|
||||
bl_description = "Join multiple MMD models into one.\n\nWARNING: To align models before joining, only adjust the root (cross under the model) transformation. Do not move armatures, meshes, rigid bodies, or joints directly as they will not move together.\n\nIMPORTANT: Don't use any of the 'Assembly' functions before using this function. This function requires the models to be in a clean state."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
join_type: bpy.props.EnumProperty(
|
||||
@@ -39,8 +34,8 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
active_object: Optional[Object] = context.active_object
|
||||
def poll(cls, context: bpy.types.Context):
|
||||
active_object: Optional[bpy.types.Object] = context.active_object
|
||||
|
||||
if context.mode != "POSE":
|
||||
return False
|
||||
@@ -56,22 +51,19 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
||||
|
||||
return len(context.selected_pose_bones) > 0
|
||||
|
||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context: bpy.types.Context):
|
||||
try:
|
||||
logger.info("Starting model join by bones operation")
|
||||
self.join(context)
|
||||
logger.info("Model join by bones completed successfully")
|
||||
except MessageException as ex:
|
||||
logger.error(f"Model join by bones failed: {str(ex)}")
|
||||
except NoModelSelectedError as ex:
|
||||
self.report(type={"ERROR"}, message=str(ex))
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def join(self, context: Context) -> None:
|
||||
def join(self, context: bpy.types.Context):
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
parent_root_object = FnModel.find_root_object(context.active_object)
|
||||
@@ -79,23 +71,35 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
||||
child_root_objects.remove(parent_root_object)
|
||||
|
||||
if parent_root_object is None or len(child_root_objects) == 0:
|
||||
raise MessageException("No MMD Models selected")
|
||||
raise NoModelSelectedError("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):
|
||||
FnModel.join_models(parent_root_object, child_root_objects)
|
||||
# Save original active_layer_collection
|
||||
orig_active_layer_collection = context.view_layer.active_layer_collection
|
||||
|
||||
# Find layer collection containing parent_root_object and set it as active
|
||||
layer_collection = FnContext.find_user_layer_collection_by_object(context, parent_root_object)
|
||||
if layer_collection:
|
||||
context.view_layer.active_layer_collection = layer_collection
|
||||
|
||||
# Execute the join operation
|
||||
FnModel.join_models(parent_root_object, child_root_objects)
|
||||
|
||||
# Restore original active_layer_collection
|
||||
context.view_layer.active_layer_collection = orig_active_layer_collection
|
||||
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
parent_armature_object = FnModel.find_armature_object(parent_root_object)
|
||||
FnContext.set_active_and_select_single_object(context, parent_armature_object)
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.armature.parent_set(type="OFFSET")
|
||||
|
||||
# Connect child bones
|
||||
if self.join_type == "CONNECTED":
|
||||
parent_edit_bone: EditBone = context.active_bone
|
||||
child_edit_bones: Set[EditBone] = set(context.selected_bones)
|
||||
parent_edit_bone: bpy.types.EditBone = context.active_bone
|
||||
child_edit_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
|
||||
child_edit_bones.remove(parent_edit_bone)
|
||||
|
||||
logger.debug(f"Connecting {len(child_edit_bones)} child bones to parent bone: {parent_edit_bone.name}")
|
||||
child_edit_bone: EditBone
|
||||
child_edit_bone: bpy.types.EditBone
|
||||
for child_edit_bone in child_edit_bones:
|
||||
child_edit_bone.use_connect = True
|
||||
|
||||
@@ -105,6 +109,7 @@ class ModelJoinByBonesOperator(bpy.types.Operator):
|
||||
class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.model_separate_by_bones"
|
||||
bl_label = "Model Separate by Bones"
|
||||
bl_description = "Separate MMD model into multiple models based on selected bones.\n\nWARNING: This operation will split meshes, armatures, rigid bodies and joints. To move models before separating, only adjust the root (cross under the model) transformation. Do not move armatures, meshes, rigid bodies, or joints directly before separating as they will not move together.\n\nIMPORTANT: Don't use any of the 'Assembly' functions before using this function. This function requires the model to be in a clean state."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
separate_armature: bpy.props.BoolProperty(name="Separate Armature", default=True)
|
||||
@@ -120,8 +125,8 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
active_object: Optional[Object] = context.active_object
|
||||
def poll(cls, context: bpy.types.Context):
|
||||
active_object: Optional[bpy.types.Object] = context.active_object
|
||||
|
||||
if context.mode != "POSE":
|
||||
return False
|
||||
@@ -137,155 +142,183 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
|
||||
return len(context.selected_pose_bones) > 0
|
||||
|
||||
def invoke(self, context: Context, event: Any) -> Set[str]:
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context: bpy.types.Context):
|
||||
try:
|
||||
logger.info("Starting model separate by bones operation")
|
||||
self.separate(context)
|
||||
logger.info("Model separate by bones completed successfully")
|
||||
except MessageException as ex:
|
||||
logger.error(f"Model separate by bones failed: {str(ex)}")
|
||||
except NoModelSelectedError as ex:
|
||||
self.report(type={"ERROR"}, message=str(ex))
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def separate(self, context: Context) -> None:
|
||||
def separate(self, context: bpy.types.Context):
|
||||
weight_threshold: float = self.weight_threshold
|
||||
mmd_scale = 0.08
|
||||
|
||||
target_armature_object: Object = context.active_object
|
||||
logger.debug(f"Target armature: {target_armature_object.name}")
|
||||
target_armature_object: bpy.types.Object = context.active_object
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
root_bones: Set[EditBone] = set(context.selected_bones)
|
||||
logger.debug(f"Selected root bones: {len(root_bones)}")
|
||||
|
||||
root_bones: Set[bpy.types.EditBone] = set(context.selected_bones)
|
||||
if self.include_descendant_bones:
|
||||
logger.debug("Including descendant bones")
|
||||
original_active_bone = context.active_bone
|
||||
for edit_bone in root_bones:
|
||||
with context.temp_override(active_bone=edit_bone):
|
||||
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
|
||||
context.active_object.data.edit_bones.active = edit_bone
|
||||
bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1)
|
||||
self._select_related_ik_bones(target_armature_object)
|
||||
if original_active_bone:
|
||||
context.active_object.data.edit_bones.active = original_active_bone
|
||||
|
||||
separate_bones: Dict[str, EditBone] = {b.name: b for b in context.selected_bones}
|
||||
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: Object = FnModel.find_root_object(context.active_object)
|
||||
separate_bones: Dict[str, bpy.types.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}
|
||||
mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object)
|
||||
mmd_model = Model(mmd_root_object)
|
||||
mmd_model_mesh_objects: List[Object] = list(mmd_model.meshes())
|
||||
logger.debug(f"Found {len(mmd_model_mesh_objects)} mesh objects in model")
|
||||
|
||||
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_object: Optional[Object]
|
||||
if self.separate_armature:
|
||||
logger.debug("Separating armature")
|
||||
target_armature_object.select_set(True)
|
||||
bpy.ops.armature.separate()
|
||||
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}")
|
||||
mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes())
|
||||
mmd_model_mesh_objects = list(self._select_weighted_vertices(mmd_model_mesh_objects, separate_bones, deform_bones, weight_threshold).keys())
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
# collect separate rigid bodies
|
||||
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")
|
||||
# Store original transform matrix for root object
|
||||
original_matrix_world = mmd_root_object.matrix_world.copy()
|
||||
mmd_root_object.matrix_world = Matrix.Identity(4)
|
||||
|
||||
# Reset object visibility
|
||||
FnContext.set_active_and_select_single_object(context, mmd_root_object)
|
||||
bpy.ops.mmd_tools.reset_object_visibility()
|
||||
|
||||
# Clean additional transform
|
||||
FnContext.set_active_and_select_single_object(context, mmd_root_object)
|
||||
bpy.ops.mmd_tools.clean_additional_transform()
|
||||
|
||||
# Create new separate model first
|
||||
separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, obj_name=mmd_root_object.name, add_root_bone=False)
|
||||
separate_model.initialDisplayFrames()
|
||||
separate_root_object = separate_model.rootObject()
|
||||
separate_root_object.matrix_world = mmd_root_object.matrix_world
|
||||
separate_model_armature_object = separate_model.armature()
|
||||
|
||||
# Now separate armature bones from original model
|
||||
separate_armature_object: Optional[bpy.types.Object] = None
|
||||
if self.separate_armature:
|
||||
FnContext.set_active_and_select_single_object(context, target_armature_object)
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
|
||||
# Re-select the bones that should be separated (they might have been deselected)
|
||||
for bone_name in separate_bones.keys():
|
||||
if bone_name in target_armature_object.data.edit_bones:
|
||||
target_armature_object.data.edit_bones[bone_name].select = True
|
||||
|
||||
bpy.ops.armature.separate()
|
||||
separate_armature_object = next(iter([a for a in context.selected_objects if a != target_armature_object and a.type == "ARMATURE"]), None)
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
# 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}
|
||||
|
||||
boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all
|
||||
|
||||
# collect separate joints
|
||||
separate_joints: Set[Object] = {
|
||||
# Collect separate joints
|
||||
separate_joints: Set[bpy.types.Object] = {
|
||||
joint_object
|
||||
for joint_object in mmd_model.joints()
|
||||
if boundary_joint_owner_condition(
|
||||
[
|
||||
joint_object.rigid_body_constraint.object1 in separate_rigid_bodies,
|
||||
joint_object.rigid_body_constraint.object2 in separate_rigid_bodies,
|
||||
]
|
||||
],
|
||||
)
|
||||
}
|
||||
logger.debug(f"Found {len(separate_joints)} joints to separate")
|
||||
|
||||
separate_mesh_objects: Set[Object]
|
||||
model2separate_mesh_objects: Dict[Object, Object]
|
||||
if len(mmd_model_mesh_objects) == 0:
|
||||
logger.debug("No mesh objects to separate")
|
||||
separate_mesh_objects = set()
|
||||
model2separate_mesh_objects = dict()
|
||||
else:
|
||||
# select meshes
|
||||
logger.debug("Selecting meshes for separation")
|
||||
obj: Object
|
||||
separate_mesh_objects: List[bpy.types.Object] = []
|
||||
model2separate_mesh_objects: Dict[bpy.types.Object, bpy.types.Object] = {}
|
||||
if len(mmd_model_mesh_objects) > 0:
|
||||
# Find a single unique attribute name that doesn't conflict with any existing attributes.
|
||||
all_attribute_names = {attr.name for obj in mmd_model_mesh_objects for attr in obj.data.attributes}
|
||||
temp_normal_name = "mmd_temp_normal"
|
||||
i = 0
|
||||
while temp_normal_name in all_attribute_names:
|
||||
temp_normal_name = f"mmd_temp_normal.{i:03d}"
|
||||
i += 1
|
||||
|
||||
# Backup custom normals to the unique temporary attribute.
|
||||
for mesh_obj in mmd_model_mesh_objects:
|
||||
mesh_data = mesh_obj.data
|
||||
existing_custom_normal = mesh_data.attributes.get("custom_normal")
|
||||
if not existing_custom_normal:
|
||||
continue
|
||||
|
||||
if existing_custom_normal.data_type == "INT16_2D":
|
||||
normals_data = np.empty(len(mesh_data.loops) * 2, dtype=np.int16)
|
||||
existing_custom_normal.data.foreach_get("value", normals_data)
|
||||
temp_normal_attr = mesh_data.attributes.new(temp_normal_name, "INT16_2D", "CORNER")
|
||||
temp_normal_attr.data.foreach_set("value", normals_data)
|
||||
else:
|
||||
raise TypeError(f"Unsupported custom_normal data type: '{existing_custom_normal.data_type}'. Supported types: 'INT16_2D'")
|
||||
|
||||
# Select meshes
|
||||
obj: bpy.types.Object
|
||||
for obj in context.view_layer.objects:
|
||||
obj.select_set(obj in mmd_model_mesh_objects)
|
||||
context.view_layer.objects.active = mmd_model_mesh_objects[0]
|
||||
|
||||
# separate mesh by selected vertices
|
||||
logger.debug("Separating meshes by selected vertices")
|
||||
# Separate mesh by selected vertices
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.mesh.separate(type="SELECTED")
|
||||
separate_mesh_objects: List[Object] = [m for m in context.selected_objects if m.type == "MESH" and m not in mmd_model_mesh_objects]
|
||||
separate_mesh_objects = [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")
|
||||
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, strict=False))
|
||||
|
||||
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)
|
||||
# Restore normal data for all meshes (original and separated)
|
||||
all_mesh_objects = list(mmd_model_mesh_objects) + list(separate_mesh_objects)
|
||||
for mesh_obj in all_mesh_objects:
|
||||
mesh_data = mesh_obj.data
|
||||
temp_normal_attr = mesh_data.attributes.get(temp_normal_name)
|
||||
if not temp_normal_attr:
|
||||
continue
|
||||
|
||||
separate_model.initialDisplayFrames()
|
||||
separate_root_object = separate_model.rootObject()
|
||||
separate_root_object.matrix_world = mmd_root_object.matrix_world
|
||||
separate_model_armature_object = separate_model.armature()
|
||||
logger.debug(f"Created separate model with root: {separate_root_object.name}")
|
||||
try:
|
||||
if temp_normal_attr.data_type == "INT16_2D":
|
||||
normals_data = np.empty(len(mesh_data.loops) * 2, dtype=np.int16)
|
||||
temp_normal_attr.data.foreach_get("value", normals_data)
|
||||
custom_normal_attr = mesh_data.attributes.get("custom_normal")
|
||||
if not custom_normal_attr:
|
||||
custom_normal_attr = mesh_data.attributes.new("custom_normal", "INT16_2D", "CORNER")
|
||||
custom_normal_attr.data.foreach_set("value", normals_data)
|
||||
else:
|
||||
raise TypeError(f"Unsupported custom_normal data type: '{temp_normal_attr.data_type}'. Supported types: 'INT16_2D'")
|
||||
finally:
|
||||
mesh_data.attributes.remove(temp_normal_attr)
|
||||
|
||||
if self.separate_armature:
|
||||
logger.debug("Joining separate armature to new model")
|
||||
with context.temp_override(
|
||||
active_object=separate_model_armature_object,
|
||||
selected_editable_objects=[separate_model_armature_object, separate_armature_object],
|
||||
):
|
||||
if self.separate_armature and separate_armature_object:
|
||||
separate_armature_data = separate_armature_object.data
|
||||
with select_object(separate_model_armature_object, objects=[separate_model_armature_object, separate_armature_object]):
|
||||
bpy.ops.object.join()
|
||||
if separate_armature_data.users == 0:
|
||||
bpy.data.armatures.remove(separate_armature_data)
|
||||
|
||||
# add mesh
|
||||
logger.debug("Parenting separate mesh objects to new model")
|
||||
with context.temp_override(
|
||||
object=separate_model_armature_object,
|
||||
selected_editable_objects=[separate_model_armature_object, *separate_mesh_objects],
|
||||
):
|
||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||
if separate_mesh_objects:
|
||||
with select_object(separate_model_armature_object, objects=[separate_model_armature_object] + separate_mesh_objects):
|
||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||
|
||||
# replace mesh armature modifier.object
|
||||
logger.debug("Updating armature modifiers on separate meshes")
|
||||
# Replace mesh armature modifier.object
|
||||
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)
|
||||
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_armature", "ARMATURE")
|
||||
|
||||
armature_modifier.object = separate_model_armature_object
|
||||
|
||||
logger.debug("Parenting rigid bodies to new model")
|
||||
with context.temp_override(
|
||||
object=separate_model.rigidGroupObject(),
|
||||
selected_editable_objects=[separate_model.rigidGroupObject(), *separate_rigid_bodies],
|
||||
):
|
||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||
if separate_rigid_bodies:
|
||||
with select_object(separate_model.rigidGroupObject(), objects=[separate_model.rigidGroupObject()] + list(separate_rigid_bodies)):
|
||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||
|
||||
logger.debug("Parenting joints to new model")
|
||||
with context.temp_override(
|
||||
object=separate_model.jointGroupObject(),
|
||||
selected_editable_objects=[separate_model.jointGroupObject(), *separate_joints],
|
||||
):
|
||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||
if separate_joints:
|
||||
with select_object(separate_model.jointGroupObject(), objects=[separate_model.jointGroupObject()] + list(separate_joints)):
|
||||
bpy.ops.object.parent_set(type="OBJECT", keep_transform=True)
|
||||
|
||||
# move separate objects to new collection
|
||||
# Move separate objects to new collection
|
||||
mmd_layer_collection = FnContext.find_user_layer_collection_by_object(context, mmd_root_object)
|
||||
assert mmd_layer_collection is not None
|
||||
|
||||
@@ -293,31 +326,42 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
assert separate_layer_collection is not None
|
||||
|
||||
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):
|
||||
separate_layer_collection.collection.objects.link(separate_object)
|
||||
mmd_layer_collection.collection.objects.unlink(separate_object)
|
||||
if separate_object.name not in separate_layer_collection.collection.objects:
|
||||
separate_layer_collection.collection.objects.link(separate_object)
|
||||
if separate_object.name in mmd_layer_collection.collection.objects:
|
||||
mmd_layer_collection.collection.objects.unlink(separate_object)
|
||||
|
||||
logger.debug("Copying MMD root properties")
|
||||
FnModel.copy_mmd_root(
|
||||
separate_root_object,
|
||||
mmd_root_object,
|
||||
overwrite=True,
|
||||
replace_name2values={
|
||||
# replace related_mesh property values
|
||||
"related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()}
|
||||
# Replace related_mesh property values
|
||||
"related_mesh": {m.data.name: s.data.name for m, s in model2separate_mesh_objects.items()},
|
||||
},
|
||||
)
|
||||
|
||||
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]:
|
||||
"""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()
|
||||
# Apply additional transform
|
||||
FnContext.set_active_and_select_single_object(context, mmd_root_object)
|
||||
bpy.ops.mmd_tools.apply_additional_transform()
|
||||
FnContext.set_active_and_select_single_object(context, separate_root_object)
|
||||
bpy.ops.mmd_tools.apply_additional_transform()
|
||||
|
||||
# Restore original transform matrix for root object
|
||||
mmd_root_object.matrix_world = original_matrix_world
|
||||
separate_root_object.matrix_world = original_matrix_world
|
||||
|
||||
# End state
|
||||
FnContext.set_active_and_select_single_object(context, separate_root_object)
|
||||
|
||||
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]:
|
||||
mesh2selected_vertex_count: Dict[bpy.types.Object, int] = {}
|
||||
target_bmesh: bmesh.types.BMesh = bmesh.new()
|
||||
for mesh_object in mmd_model_mesh_objects:
|
||||
vertex_groups: bpy.types.VertexGroups = mesh_object.vertex_groups
|
||||
|
||||
mesh: Mesh = mesh_object.data
|
||||
mesh: bpy.types.Mesh = mesh_object.data
|
||||
target_bmesh.from_mesh(mesh, face_normals=False)
|
||||
target_bmesh.select_mode |= {"VERT"}
|
||||
deform_layer = target_bmesh.verts.layers.deform.verify()
|
||||
@@ -344,7 +388,6 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
vert.select_set(True)
|
||||
|
||||
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
|
||||
target_bmesh.select_flush_mode()
|
||||
target_bmesh.to_mesh(mesh)
|
||||
@@ -352,3 +395,61 @@ class ModelSeparateByBonesOperator(bpy.types.Operator):
|
||||
target_bmesh.clear()
|
||||
|
||||
return mesh2selected_vertex_count
|
||||
|
||||
def _select_related_ik_bones(self, armature_object: bpy.types.Object) -> None:
|
||||
"""
|
||||
Expand the current selection to include any full IK systems that are
|
||||
partially selected. An IK system includes the chain bones, the IK
|
||||
target bone, and the pole target bone.
|
||||
|
||||
NOTE: This method operates entirely in EDIT mode and avoids mode switching
|
||||
to prevent segmentation faults.
|
||||
"""
|
||||
edit_bones = armature_object.data.edit_bones
|
||||
initial_selection_names = {b.name for b in edit_bones if b.select}
|
||||
|
||||
# Access pose bones constraints directly without mode switching
|
||||
pose_bones = armature_object.pose.bones
|
||||
|
||||
# Find all complete IK systems
|
||||
ik_systems = []
|
||||
|
||||
for pose_bone in pose_bones:
|
||||
for constraint in pose_bone.constraints:
|
||||
if constraint.type == "IK":
|
||||
# Build the set of bones in this IK system
|
||||
system_bones = {pose_bone.name}
|
||||
|
||||
# Add the main IK Target bone
|
||||
if constraint.target and constraint.subtarget:
|
||||
system_bones.add(constraint.subtarget)
|
||||
|
||||
# Add the Pole Target bone
|
||||
if constraint.pole_target and constraint.pole_subtarget:
|
||||
system_bones.add(constraint.pole_subtarget)
|
||||
|
||||
# Add all other bones in the IK chain
|
||||
current_bone_name = pose_bone.name
|
||||
chain_count = constraint.chain_count
|
||||
|
||||
# Walk up the parent chain
|
||||
for _ in range(chain_count - 1):
|
||||
if current_bone_name not in edit_bones:
|
||||
break
|
||||
current_bone = edit_bones[current_bone_name]
|
||||
if not current_bone.parent:
|
||||
break
|
||||
current_bone_name = current_bone.parent.name
|
||||
system_bones.add(current_bone_name)
|
||||
|
||||
ik_systems.append(system_bones)
|
||||
|
||||
# Expand selection to include any related, full IK systems
|
||||
final_selection_names = set(initial_selection_names)
|
||||
for system in ik_systems:
|
||||
if not system.isdisjoint(initial_selection_names):
|
||||
final_selection_names.update(system)
|
||||
|
||||
# Apply the final selection
|
||||
for bone in edit_bones:
|
||||
bone.select = bone.name in final_selection_names
|
||||
|
||||
+402
-112
@@ -1,30 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# 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.
|
||||
# Copyright 2015 MMD Tools authors
|
||||
# This file is part of MMD Tools.
|
||||
|
||||
from typing import Optional, cast, List, Dict, Any, Set, Tuple, Union
|
||||
from collections import namedtuple
|
||||
from typing import Optional, cast
|
||||
|
||||
import bpy
|
||||
from mathutils import Quaternion, Vector
|
||||
|
||||
from ..core.model import FnModel
|
||||
from .. import bpyutils, utils
|
||||
from ..core.exceptions import MaterialNotFoundError
|
||||
from ..core.material import FnMaterial
|
||||
from ..core.model import FnModel
|
||||
from ..core.morph import FnMorph
|
||||
from ..utils import ItemMoveOp, ItemOp
|
||||
from ....logging_setup import logger
|
||||
|
||||
|
||||
# Util functions
|
||||
def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
|
||||
def divide_vector_components(vec1, vec2):
|
||||
if len(vec1) != len(vec2):
|
||||
raise ValueError("Vectors should have the same number of components")
|
||||
result = []
|
||||
for v1, v2 in zip(vec1, vec2):
|
||||
for v1, v2 in zip(vec1, vec2, strict=False):
|
||||
if v2 == 0:
|
||||
if v1 == 0:
|
||||
v2 = 1 # If we have a 0/0 case we change the divisor to 1
|
||||
@@ -34,17 +30,17 @@ def divide_vector_components(vec1: List[float], vec2: List[float]) -> List[float
|
||||
return result
|
||||
|
||||
|
||||
def multiply_vector_components(vec1: List[float], vec2: List[float]) -> List[float]:
|
||||
def multiply_vector_components(vec1, vec2):
|
||||
if len(vec1) != len(vec2):
|
||||
raise ValueError("Vectors should have the same number of components")
|
||||
result = []
|
||||
for v1, v2 in zip(vec1, vec2):
|
||||
for v1, v2 in zip(vec1, vec2, strict=False):
|
||||
result.append(v1 * v2)
|
||||
return result
|
||||
|
||||
|
||||
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"""
|
||||
def special_division(n1, n2):
|
||||
"""Return 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised"""
|
||||
if n2 == 0:
|
||||
if n1 == 0:
|
||||
n2 = 1
|
||||
@@ -59,7 +55,7 @@ class AddMorph(bpy.types.Operator):
|
||||
bl_description = "Add a morph item to active morph list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -69,7 +65,6 @@ class AddMorph(bpy.types.Operator):
|
||||
morph.name = "New Morph"
|
||||
if morph_type.startswith("uv"):
|
||||
morph.data_type = "VERTEX_GROUP"
|
||||
logger.debug(f"Added new morph of type {morph_type}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -86,7 +81,7 @@ class RemoveMorph(bpy.types.Operator):
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -101,21 +96,19 @@ class RemoveMorph(bpy.types.Operator):
|
||||
if self.all:
|
||||
morphs.clear()
|
||||
mmd_root.active_morph = 0
|
||||
logger.debug(f"Removed all morphs of type {morph_type}")
|
||||
else:
|
||||
morphs.remove(mmd_root.active_morph)
|
||||
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"}
|
||||
|
||||
|
||||
class MoveMorph(bpy.types.Operator, ItemMoveOp):
|
||||
bl_idname = "mmd_tools.morph_move"
|
||||
bl_label = "Move Morph"
|
||||
bl_description = "Move active morph item up/down in the list"
|
||||
bl_description = "Move active morph item up/down in the list. This will not affect the morph order in exported PMX files (use Display Panel order instead)."
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -124,7 +117,6 @@ class MoveMorph(bpy.types.Operator, ItemMoveOp):
|
||||
mmd_root.active_morph,
|
||||
self.type,
|
||||
)
|
||||
logger.debug(f"Moved morph to index {mmd_root.active_morph}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -134,7 +126,7 @@ class CopyMorph(bpy.types.Operator):
|
||||
bl_description = "Make a copy of active morph in the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -146,7 +138,7 @@ class CopyMorph(bpy.types.Operator):
|
||||
if morph is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
name_orig, name_tmp = morph.name, "_tmp%s" % str(morph.as_pointer())
|
||||
name_orig, name_tmp = morph.name, f"_tmp{str(morph.as_pointer())}"
|
||||
|
||||
if morph_type.startswith("vertex"):
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
@@ -161,7 +153,6 @@ class CopyMorph(bpy.types.Operator):
|
||||
for k, v in morph.items():
|
||||
morph_new[k] = v if k != "name" else name_tmp
|
||||
morph_new.name = name_orig + "_copy" # trigger name check
|
||||
logger.debug(f"Copied morph {name_orig} to {morph_new.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -171,17 +162,14 @@ class OverwriteBoneMorphsFromActionPose(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
def poll(cls, context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
return False
|
||||
return root is not None and root.mmd_root.active_morph_type == "bone_morphs"
|
||||
|
||||
return root.mmd_root.active_morph_type == "bone_morphs"
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
FnMorph.overwrite_bone_morphs_from_action_pose(FnModel.find_armature_object(root))
|
||||
logger.info("Overwrote bone morphs from active action pose")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -191,7 +179,7 @@ class AddMorphOffset(bpy.types.Operator):
|
||||
bl_description = "Add a morph offset item to the list"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -216,7 +204,6 @@ class AddMorphOffset(bpy.types.Operator):
|
||||
item.location = pose_bone.location
|
||||
item.rotation = pose_bone.rotation_quaternion
|
||||
|
||||
logger.debug(f"Added morph offset to {morph_type}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -233,7 +220,7 @@ class RemoveMorphOffset(bpy.types.Operator):
|
||||
options={"SKIP_SAVE"},
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -250,21 +237,17 @@ class RemoveMorphOffset(bpy.types.Operator):
|
||||
if morph_type.startswith("vertex"):
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
FnMorph.remove_shape_key(obj, morph.name)
|
||||
logger.debug(f"Removed all vertex morph offsets for {morph.name}")
|
||||
return {"FINISHED"}
|
||||
elif morph_type.startswith("uv"):
|
||||
if morph_type.startswith("uv"):
|
||||
if morph.data_type == "VERTEX_GROUP":
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
FnMorph.store_uv_morph_data(obj, morph)
|
||||
logger.debug(f"Removed all UV morph offsets for {morph.name}")
|
||||
return {"FINISHED"}
|
||||
morph.data.clear()
|
||||
morph.active_data = 0
|
||||
logger.debug(f"Cleared all morph offsets for {morph.name}")
|
||||
else:
|
||||
morph.data.remove(morph.active_data)
|
||||
morph.active_data = max(0, morph.active_data - 1)
|
||||
logger.debug(f"Removed morph offset at index {morph.active_data}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -280,7 +263,7 @@ class InitMaterialOffset(bpy.types.Operator):
|
||||
default=0,
|
||||
)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -292,7 +275,6 @@ class InitMaterialOffset(bpy.types.Operator):
|
||||
mat_data.specular_color = mat_data.ambient_color = (val,) * 3
|
||||
mat_data.shininess = mat_data.edge_weight = val
|
||||
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"}
|
||||
|
||||
|
||||
@@ -302,7 +284,7 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
||||
bl_description = "Calculates the offsets and apply them, then the temporary material is removed"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -340,7 +322,6 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
||||
|
||||
except ZeroDivisionError:
|
||||
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:
|
||||
self.report({"ERROR"}, "An unexpected error happened")
|
||||
# We should stop on our tracks and re-raise the exception
|
||||
@@ -358,7 +339,6 @@ class ApplyMaterialOffset(bpy.types.Operator):
|
||||
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)
|
||||
logger.info(f"Applied material offset for {mat_data.material}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -368,7 +348,7 @@ class CreateWorkMaterial(bpy.types.Operator):
|
||||
bl_description = "Creates a temporary material to edit this offset"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -385,12 +365,12 @@ class CreateWorkMaterial(bpy.types.Operator):
|
||||
|
||||
base_mat = meshObj.data.materials.get(mat_data.material, None)
|
||||
if base_mat is None:
|
||||
self.report({"ERROR"}, 'Material "%s" not found' % mat_data.material)
|
||||
self.report({"ERROR"}, f'Material "{mat_data.material}" not found')
|
||||
return {"CANCELLED"}
|
||||
|
||||
work_mat_name = base_mat.name + "_temp"
|
||||
if work_mat_name in bpy.data.materials:
|
||||
self.report({"ERROR"}, 'Temporary material "%s" is in use' % work_mat_name)
|
||||
self.report({"ERROR"}, f'Temporary material "{work_mat_name}" is in use')
|
||||
return {"CANCELLED"}
|
||||
|
||||
work_mat = base_mat.copy()
|
||||
@@ -427,7 +407,6 @@ class CreateWorkMaterial(bpy.types.Operator):
|
||||
work_mmd_mat.edge_color = list(edge_offset)
|
||||
work_mmd_mat.edge_weight += mat_data.edge_weight
|
||||
|
||||
logger.info(f"Created work material {work_mat_name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -437,24 +416,23 @@ class ClearTempMaterials(bpy.types.Operator):
|
||||
bl_description = "Clears all the temporary materials"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
for meshObj in FnModel.iterate_mesh_objects(root):
|
||||
|
||||
def __pre_remove(m: Optional[bpy.types.Material]) -> bool:
|
||||
def __pre_remove(m, meshObj=meshObj):
|
||||
if m and "_temp" in m.name:
|
||||
base_mat_name = m.name.split("_temp")[0]
|
||||
try:
|
||||
FnMaterial.swap_materials(meshObj, m.name, base_mat_name)
|
||||
return True
|
||||
except MaterialNotFoundError:
|
||||
self.report({"WARNING"}, "Base material for %s was not found" % m.name)
|
||||
self.report({"WARNING"}, f"Base material for {m.name} was not found")
|
||||
return False
|
||||
|
||||
FnMaterial.clean_materials(meshObj, can_remove=__pre_remove)
|
||||
logger.info("Cleared all temporary materials")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -464,7 +442,7 @@ class ViewBoneMorph(bpy.types.Operator):
|
||||
bl_description = "View the result of active bone morph"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -475,12 +453,10 @@ class ViewBoneMorph(bpy.types.Operator):
|
||||
for morph_data in morph.data:
|
||||
p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None)
|
||||
if p_bone:
|
||||
# Blender 5.0: use pose bone select property directly
|
||||
p_bone.select = True
|
||||
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
|
||||
p_bone.matrix_basis = mtx
|
||||
logger.info(f"Viewing bone morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -490,14 +466,13 @@ class ClearBoneMorphView(bpy.types.Operator):
|
||||
bl_description = "Reset transforms of all bones to their default values"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
armature = FnModel.find_armature_object(root)
|
||||
for p_bone in armature.pose.bones:
|
||||
p_bone.matrix_basis.identity()
|
||||
logger.info("Cleared bone morph view")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -507,7 +482,7 @@ class ApplyBoneMorph(bpy.types.Operator):
|
||||
bl_description = "Apply current pose to active bone morph"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -522,11 +497,9 @@ class ApplyBoneMorph(bpy.types.Operator):
|
||||
item.bone = p_bone.name
|
||||
item.location = p_bone.location
|
||||
item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion()
|
||||
# Blender 5.0: use pose bone select property directly
|
||||
p_bone.select = True
|
||||
else:
|
||||
p_bone.select = False
|
||||
logger.info(f"Applied current pose to bone morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -536,7 +509,7 @@ class SelectRelatedBone(bpy.types.Operator):
|
||||
bl_description = "Select the bone assigned to this offset in the armature"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -545,7 +518,6 @@ class SelectRelatedBone(bpy.types.Operator):
|
||||
morph = mmd_root.bone_morphs[mmd_root.active_morph]
|
||||
morph_data = morph.data[morph.active_data]
|
||||
utils.selectSingleBone(context, armature, morph_data.bone)
|
||||
logger.debug(f"Selected bone: {morph_data.bone}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -555,7 +527,7 @@ class EditBoneOffset(bpy.types.Operator):
|
||||
bl_description = "Applies the location and rotation of this offset to the bone"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -568,7 +540,6 @@ class EditBoneOffset(bpy.types.Operator):
|
||||
mtx.translation = morph_data.location
|
||||
p_bone.matrix_basis = mtx
|
||||
utils.selectSingleBone(context, armature, p_bone.name)
|
||||
logger.debug(f"Edited bone offset for {p_bone.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -578,7 +549,7 @@ class ApplyBoneOffset(bpy.types.Operator):
|
||||
bl_description = "Stores the current bone location and rotation into this offset"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -590,7 +561,6 @@ class ApplyBoneOffset(bpy.types.Operator):
|
||||
p_bone = armature.pose.bones[morph_data.bone]
|
||||
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()
|
||||
logger.debug(f"Applied bone offset for {p_bone.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -600,7 +570,7 @@ class ViewUVMorph(bpy.types.Operator):
|
||||
bl_description = "View the result of active UV morph on current mesh object"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
@@ -618,11 +588,11 @@ class ViewUVMorph(bpy.types.Operator):
|
||||
|
||||
selected = meshObj.select_get()
|
||||
with bpyutils.select_object(meshObj):
|
||||
mesh = cast(bpy.types.Mesh, meshObj.data)
|
||||
mesh = cast("bpy.types.Mesh", meshObj.data)
|
||||
morph = mmd_root.uv_morphs[mmd_root.active_morph]
|
||||
uv_textures = mesh.uv_layers
|
||||
|
||||
base_uv_layers = [l for l in mesh.uv_layers if not l.name.startswith("_")]
|
||||
base_uv_layers = [layer for layer in mesh.uv_layers if not layer.name.startswith("_")]
|
||||
if morph.uv_index >= len(base_uv_layers):
|
||||
self.report({"ERROR"}, "Invalid uv index: %d" % morph.uv_index)
|
||||
return {"CANCELLED"}
|
||||
@@ -632,7 +602,7 @@ class ViewUVMorph(bpy.types.Operator):
|
||||
uv_textures.active = uv_textures[uv_layer_name]
|
||||
|
||||
uv_layer_name = uv_textures.active.name
|
||||
uv_tex = uv_textures.new(name="__uv.%s" % uv_layer_name)
|
||||
uv_tex = uv_textures.new(name=f"__uv.{uv_layer_name}")
|
||||
if uv_tex is None:
|
||||
self.report({"ERROR"}, "Failed to create a temporary uv layer")
|
||||
return {"CANCELLED"}
|
||||
@@ -642,17 +612,15 @@ class ViewUVMorph(bpy.types.Operator):
|
||||
if len(offsets) > 0:
|
||||
base_uv_data = mesh.uv_layers.active.data
|
||||
temp_uv_data = mesh.uv_layers[uv_tex.name].data
|
||||
for i, l in enumerate(mesh.loops):
|
||||
# Blender 5.0+: UV selection is now stored in face-corner attributes
|
||||
# Skipping UV selection assignment as it's not critical for morph preview
|
||||
select = l.vertex_index in offsets
|
||||
for i, loop in enumerate(mesh.loops):
|
||||
select = temp_uv_data[i].select = loop.vertex_index in offsets
|
||||
if select:
|
||||
temp_uv_data[i].uv = base_uv_data[i].uv + offsets[l.vertex_index]
|
||||
temp_uv_data[i].uv = base_uv_data[i].uv + offsets[loop.vertex_index]
|
||||
|
||||
uv_textures.active = uv_tex
|
||||
uv_tex.active_render = True
|
||||
meshObj.hide_set(False)
|
||||
meshObj.select_set(selected)
|
||||
logger.info(f"Viewing UV morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -662,24 +630,24 @@ class ClearUVMorphView(bpy.types.Operator):
|
||||
bl_description = "Clear all temporary data of UV morphs"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
assert root is not None
|
||||
for m in FnModel.iterate_mesh_objects(root):
|
||||
mesh = m.data
|
||||
uv_layers = mesh.uv_layers
|
||||
for t in list(uv_layers): # Create a copy to iterate safely
|
||||
uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers)
|
||||
for t in reversed(uv_textures):
|
||||
if t.name.startswith("__uv."):
|
||||
uv_layers.remove(t)
|
||||
if len(uv_layers) > 0:
|
||||
# Only set active_index
|
||||
uv_layers.active_index = 0
|
||||
uv_textures.remove(t)
|
||||
if len(uv_textures) > 0:
|
||||
uv_textures[0].active_render = True
|
||||
uv_textures.active_index = 0
|
||||
|
||||
animation_data = mesh.animation_data
|
||||
if animation_data:
|
||||
nla_tracks = animation_data.nla_tracks
|
||||
for t in nla_tracks:
|
||||
for t in reversed(nla_tracks):
|
||||
if t.name.startswith("__uv."):
|
||||
nla_tracks.remove(t)
|
||||
if animation_data.action and animation_data.action.name.startswith("__uv."):
|
||||
@@ -687,10 +655,9 @@ class ClearUVMorphView(bpy.types.Operator):
|
||||
if animation_data.action is None and len(nla_tracks) == 0:
|
||||
mesh.animation_data_clear()
|
||||
|
||||
for act in bpy.data.actions:
|
||||
for act in reversed(bpy.data.actions):
|
||||
if act.name.startswith("__uv.") and act.users < 1:
|
||||
bpy.data.actions.remove(act)
|
||||
logger.info("Cleared UV morph view")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -701,20 +668,20 @@ class EditUVMorph(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
if obj.type != "MESH":
|
||||
if obj is None or obj.type != "MESH":
|
||||
return False
|
||||
active_uv_layer = obj.data.uv_layers.active
|
||||
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
||||
return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.")
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
meshObj = obj
|
||||
|
||||
selected = meshObj.select_get()
|
||||
with bpyutils.select_object(meshObj):
|
||||
mesh = cast(bpy.types.Mesh, meshObj.data)
|
||||
mesh = cast("bpy.types.Mesh", meshObj.data)
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
bpy.ops.mesh.select_mode(type="VERT", action="ENABLE")
|
||||
bpy.ops.mesh.reveal() # unhide all vertices
|
||||
@@ -722,16 +689,15 @@ class EditUVMorph(bpy.types.Operator):
|
||||
bpy.ops.object.mode_set(mode="OBJECT")
|
||||
|
||||
vertices = mesh.vertices
|
||||
for l, d in zip(mesh.loops, mesh.uv_layers.active.data):
|
||||
for loop, d in zip(mesh.loops, mesh.uv_layers.active.data, strict=False):
|
||||
if d.select:
|
||||
vertices[l.vertex_index].select = True
|
||||
vertices[loop.vertex_index].select = True
|
||||
|
||||
polygons = mesh.polygons
|
||||
polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), "index", polygons.active)
|
||||
|
||||
bpy.ops.object.mode_set(mode="EDIT")
|
||||
meshObj.select_set(selected)
|
||||
logger.info("Editing UV morph")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -742,14 +708,14 @@ class ApplyUVMorph(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
if obj.type != "MESH":
|
||||
if obj is None or obj.type != "MESH":
|
||||
return False
|
||||
active_uv_layer = obj.data.uv_layers.active
|
||||
return active_uv_layer and active_uv_layer.name.startswith("__uv.")
|
||||
return active_uv_layer is not None and active_uv_layer.name.startswith("__uv.")
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
@@ -757,34 +723,31 @@ class ApplyUVMorph(bpy.types.Operator):
|
||||
|
||||
selected = meshObj.select_get()
|
||||
with bpyutils.select_object(meshObj):
|
||||
mesh = cast(bpy.types.Mesh, meshObj.data)
|
||||
mesh = cast("bpy.types.Mesh", meshObj.data)
|
||||
morph = mmd_root.uv_morphs[mmd_root.active_morph]
|
||||
|
||||
base_uv_name = mesh.uv_layers.active.name[5:]
|
||||
if base_uv_name not in mesh.uv_layers:
|
||||
self.report({"ERROR"}, ' * UV map "%s" not found' % base_uv_name)
|
||||
self.report({"ERROR"}, f' * UV map "{base_uv_name}" not found')
|
||||
return {"CANCELLED"}
|
||||
|
||||
base_uv_data = mesh.uv_layers[base_uv_name].data
|
||||
temp_uv_data = mesh.uv_layers.active.data
|
||||
axis_type = "ZW" if base_uv_name.startswith("_") else "XY"
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
__OffsetData = namedtuple("OffsetData", "index, offset")
|
||||
offsets = {}
|
||||
vertices = mesh.vertices
|
||||
for l, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data):
|
||||
if vertices[l.vertex_index].select and l.vertex_index not in offsets:
|
||||
for loop, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data, strict=False):
|
||||
if vertices[loop.vertex_index].select and loop.vertex_index not in offsets:
|
||||
dx, dy = i1.uv - i0.uv
|
||||
if abs(dx) > 0.0001 or abs(dy) > 0.0001:
|
||||
offsets[l.vertex_index] = __OffsetData(l.vertex_index, (dx, dy, dx, dy))
|
||||
offsets[loop.vertex_index] = __OffsetData(loop.vertex_index, (dx, dy, dx, dy))
|
||||
|
||||
FnMorph.store_uv_morph_data(meshObj, morph, offsets.values(), axis_type)
|
||||
morph.data_type = "VERTEX_GROUP"
|
||||
|
||||
meshObj.select_set(selected)
|
||||
logger.info(f"Applied UV morph: {morph.name}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -795,12 +758,339 @@ class CleanDuplicatedMaterialMorphs(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return FnModel.find_root_object(context.active_object) is not None
|
||||
def poll(cls, context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
return root is not None
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context: bpy.types.Context):
|
||||
mmd_root_object = FnModel.find_root_object(context.active_object)
|
||||
FnMorph.clean_duplicated_material_morphs(mmd_root_object)
|
||||
logger.info("Cleaned duplicated material morphs")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ConvertBoneMorphToVertexMorph(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.convert_bone_morph_to_vertex_morph"
|
||||
bl_label = "Convert To Vertex Morph"
|
||||
bl_description = "Convert a bone morph into a single vertex morph by applying the bone transformations.\nIf a corresponding vertex morph already exists, it will be updated."
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
return False
|
||||
mmd_root = root.mmd_root
|
||||
if mmd_root.active_morph_type != "bone_morphs":
|
||||
return False
|
||||
morph = ItemOp.get_by_index(mmd_root.bone_morphs, mmd_root.active_morph)
|
||||
return morph is not None and len(morph.data) > 0
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
|
||||
# Get the active bone morph
|
||||
bone_morph = ItemOp.get_by_index(mmd_root.bone_morphs, mmd_root.active_morph)
|
||||
if bone_morph is None:
|
||||
self.report({"ERROR"}, "No active bone morph")
|
||||
return {"CANCELLED"}
|
||||
|
||||
original_name = bone_morph.name
|
||||
target_name = original_name
|
||||
|
||||
# Add 'B' suffix if necessary
|
||||
if not original_name.endswith("B"):
|
||||
bone_morph.name = original_name + "B"
|
||||
target_name = original_name
|
||||
else:
|
||||
# If already has B suffix, use name without B
|
||||
target_name = original_name[:-1]
|
||||
|
||||
try:
|
||||
# Step 1: import
|
||||
from ..core.model import Model
|
||||
|
||||
rig = Model(root)
|
||||
|
||||
# Ensure morph slider is bound
|
||||
bpy.ops.mmd_tools.morph_slider_setup(type="BIND")
|
||||
|
||||
# Re-obtain placeholder object
|
||||
placeholder_obj = rig.morph_slider.placeholder()
|
||||
if placeholder_obj is None or placeholder_obj.data.shape_keys is None:
|
||||
self.report({"ERROR"}, "Failed to create morph slider system")
|
||||
return {"CANCELLED"}
|
||||
|
||||
shape_keys = placeholder_obj.data.shape_keys
|
||||
key_blocks = shape_keys.key_blocks
|
||||
|
||||
# Step 2: Check if target bone morph exists
|
||||
current_morph_name = bone_morph.name
|
||||
if current_morph_name not in key_blocks:
|
||||
self.report({"ERROR"}, f"Bone morph '{current_morph_name}' not found in morph sliders")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Step 3: Save all current morph values
|
||||
original_values = {}
|
||||
for key_block in key_blocks:
|
||||
if key_block.name != "--- morph sliders ---":
|
||||
original_values[key_block.name] = key_block.value
|
||||
|
||||
# Step 4: Set all morphs to 0
|
||||
for key_block in key_blocks:
|
||||
if key_block.name != "--- morph sliders ---":
|
||||
key_block.value = 0
|
||||
|
||||
# Step 5: Set target bone morph to 1.0
|
||||
key_blocks[current_morph_name].value = 1.0
|
||||
|
||||
# Step 6: Use Armature Modifier's "Apply as Shape Key" functionality
|
||||
created_shape_keys = []
|
||||
for mesh_obj in FnModel.iterate_mesh_objects(root):
|
||||
# Switch to this mesh object
|
||||
context.view_layer.objects.active = mesh_obj
|
||||
|
||||
# Ensure mesh object has shape keys
|
||||
if mesh_obj.data.shape_keys is None:
|
||||
mesh_obj.shape_key_add(name="Basis", from_mix=False)
|
||||
|
||||
# Delete existing shape key with same name
|
||||
if target_name in mesh_obj.data.shape_keys.key_blocks:
|
||||
idx = mesh_obj.data.shape_keys.key_blocks.find(target_name)
|
||||
if idx >= 0:
|
||||
mesh_obj.active_shape_key_index = idx
|
||||
bpy.ops.object.shape_key_remove()
|
||||
|
||||
# Find armature modifier
|
||||
armature_modifier = None
|
||||
for modifier in mesh_obj.modifiers:
|
||||
if modifier.type == "ARMATURE":
|
||||
armature_modifier = modifier
|
||||
break
|
||||
|
||||
if armature_modifier is None:
|
||||
self.report({"WARNING"}, f"No armature modifier found on mesh '{mesh_obj.name}'")
|
||||
continue
|
||||
|
||||
# Use Apply as Shape Key functionality, keeping the modifier
|
||||
bpy.ops.object.modifier_apply_as_shapekey(modifier=armature_modifier.name, keep_modifier=True)
|
||||
|
||||
# Rename the newly created shape key to target name
|
||||
shape_key_blocks = mesh_obj.data.shape_keys.key_blocks
|
||||
new_shape_key = shape_key_blocks[-1] # Latest created shape key
|
||||
new_shape_key.name = target_name
|
||||
new_shape_key.value = 0.0 # Set to 0 to avoid double effect
|
||||
|
||||
created_shape_keys.append((mesh_obj.name, target_name))
|
||||
self.report({"INFO"}, f"Created shape key '{target_name}' on mesh '{mesh_obj.name}'")
|
||||
|
||||
# Step 7: Restore all original morph values
|
||||
for key_name, original_value in original_values.items():
|
||||
if key_name in key_blocks:
|
||||
key_blocks[key_name].value = original_value
|
||||
|
||||
# Step 8: Create or update vertex morph entry
|
||||
vertex_morph_exists = False
|
||||
for i, morph in enumerate(mmd_root.vertex_morphs):
|
||||
if morph.name == target_name:
|
||||
vertex_morph_exists = True
|
||||
mmd_root.active_morph_type = "vertex_morphs"
|
||||
mmd_root.active_morph = i
|
||||
break
|
||||
|
||||
if not vertex_morph_exists:
|
||||
mmd_root.active_morph_type = "vertex_morphs"
|
||||
morph, mmd_root.active_morph = ItemOp.add_after(mmd_root.vertex_morphs, mmd_root.active_morph)
|
||||
morph.name = target_name
|
||||
|
||||
# Step 9: Add to facial expression display frame
|
||||
facial_frame = None
|
||||
for frame in mmd_root.display_item_frames:
|
||||
if frame.name == "表情":
|
||||
facial_frame = frame
|
||||
break
|
||||
|
||||
if facial_frame:
|
||||
morph_exists_in_frame = False
|
||||
for item in facial_frame.data:
|
||||
if item.type == "MORPH" and item.name == target_name and item.morph_type == "vertex_morphs":
|
||||
morph_exists_in_frame = True
|
||||
break
|
||||
|
||||
if not morph_exists_in_frame:
|
||||
new_item = facial_frame.data.add()
|
||||
new_item.type = "MORPH"
|
||||
new_item.morph_type = "vertex_morphs"
|
||||
new_item.name = target_name
|
||||
|
||||
facial_frame.active_item = len(facial_frame.data) - 1
|
||||
|
||||
for i, frame in enumerate(mmd_root.display_item_frames):
|
||||
if frame.name == "表情":
|
||||
mmd_root.active_display_item_frame = i
|
||||
break
|
||||
|
||||
# UNBIND
|
||||
bpy.ops.mmd_tools.morph_slider_setup(type="UNBIND")
|
||||
|
||||
# Success message
|
||||
shape_key_info = ", ".join([f"{mesh}:{key}" for mesh, key in created_shape_keys])
|
||||
self.report({"INFO"}, f"Successfully converted bone morph '{original_name}' to vertex morph '{target_name}'. Created shape keys: {shape_key_info}")
|
||||
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Error during conversion: {str(e)}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ConvertGroupMorphToVertexMorph(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.convert_group_morph_to_vertex_morph"
|
||||
bl_label = "Convert To Vertex Morph"
|
||||
bl_description = "Convert a group morph into a single vertex morph by merging only the vertex morphs within the group.\nIf a corresponding vertex morph already exists, it will be updated."
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
return False
|
||||
mmd_root = root.mmd_root
|
||||
if mmd_root.active_morph_type != "group_morphs":
|
||||
return False
|
||||
morph = ItemOp.get_by_index(mmd_root.group_morphs, mmd_root.active_morph)
|
||||
return morph is not None and len(morph.data) > 0
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.mmd_tools.morph_slider_setup(type="UNBIND")
|
||||
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
mmd_root = root.mmd_root
|
||||
|
||||
# Get the active group morph
|
||||
group_morph = ItemOp.get_by_index(mmd_root.group_morphs, mmd_root.active_morph)
|
||||
if group_morph is None:
|
||||
self.report({"ERROR"}, "No active group morph")
|
||||
return {"CANCELLED"}
|
||||
|
||||
# Check if the group morph contains any vertex morphs to convert
|
||||
has_vertex_morphs = False
|
||||
for offset in group_morph.data:
|
||||
if offset.morph_type == "vertex_morphs":
|
||||
has_vertex_morphs = True
|
||||
break
|
||||
|
||||
if not has_vertex_morphs:
|
||||
self.report({"ERROR"}, "The group morph does not contain any vertex morphs to convert")
|
||||
return {"CANCELLED"}
|
||||
|
||||
original_name = group_morph.name
|
||||
target_name = original_name
|
||||
|
||||
# Add 'G' suffix if necessary
|
||||
if not original_name.endswith("G"):
|
||||
group_morph.name = original_name + "G"
|
||||
target_name = original_name
|
||||
else:
|
||||
# If already has G suffix, use name without G
|
||||
target_name = original_name[:-1]
|
||||
|
||||
# First, reset all shape keys to zero
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
if obj.data.shape_keys:
|
||||
for kb in obj.data.shape_keys.key_blocks:
|
||||
kb.value = 0
|
||||
|
||||
# Apply only the vertex morphs from the group morph
|
||||
for offset in group_morph.data:
|
||||
if offset.morph_type == "vertex_morphs":
|
||||
# Find the vertex morph by name
|
||||
vertex_morph = getattr(root.mmd_root, offset.morph_type).get(offset.name)
|
||||
if vertex_morph:
|
||||
# Apply this morph at the specified factor
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
if obj.data.shape_keys:
|
||||
kb = obj.data.shape_keys.key_blocks.get(offset.name)
|
||||
if kb:
|
||||
kb.value = offset.factor
|
||||
|
||||
# Now add a new shape key from mix for each mesh
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
if obj.data.shape_keys:
|
||||
# Make this the active object
|
||||
context.view_layer.objects.active = obj
|
||||
|
||||
# Remove existing shape key if it exists
|
||||
if target_name in obj.data.shape_keys.key_blocks:
|
||||
idx = obj.data.shape_keys.key_blocks.find(target_name)
|
||||
if idx >= 0:
|
||||
obj.active_shape_key_index = idx
|
||||
bpy.ops.object.shape_key_remove()
|
||||
|
||||
# Add shape key from mix
|
||||
bpy.ops.object.shape_key_add(from_mix=True)
|
||||
|
||||
# Rename the newly created shape key
|
||||
new_key = obj.data.shape_keys.key_blocks[-1]
|
||||
new_key.name = target_name
|
||||
|
||||
# Check if a vertex morph with the target name already exists
|
||||
vertex_morph_exists = False
|
||||
for i, morph in enumerate(mmd_root.vertex_morphs):
|
||||
if morph.name == target_name:
|
||||
vertex_morph_exists = True
|
||||
mmd_root.active_morph_type = "vertex_morphs"
|
||||
mmd_root.active_morph = i
|
||||
break
|
||||
|
||||
# If not, create a new vertex morph
|
||||
if not vertex_morph_exists:
|
||||
# Switch to vertex morphs panel
|
||||
mmd_root.active_morph_type = "vertex_morphs"
|
||||
|
||||
# Add new vertex morph
|
||||
morph, mmd_root.active_morph = ItemOp.add_after(mmd_root.vertex_morphs, mmd_root.active_morph)
|
||||
morph.name = target_name
|
||||
|
||||
# Add the new vertex morph to the facial display frame
|
||||
facial_frame = None
|
||||
for frame in mmd_root.display_item_frames:
|
||||
if frame.name == "表情": # This is the facial display frame
|
||||
facial_frame = frame
|
||||
break
|
||||
|
||||
if facial_frame:
|
||||
# Check if this morph is already in the facial frame
|
||||
morph_exists_in_frame = False
|
||||
for item in facial_frame.data:
|
||||
if item.type == "MORPH" and item.name == target_name and item.morph_type == "vertex_morphs":
|
||||
morph_exists_in_frame = True
|
||||
break
|
||||
|
||||
# If not, add it
|
||||
if not morph_exists_in_frame:
|
||||
new_item = facial_frame.data.add()
|
||||
new_item.type = "MORPH"
|
||||
new_item.morph_type = "vertex_morphs"
|
||||
new_item.name = target_name
|
||||
|
||||
# Make this the active item in the facial frame
|
||||
facial_frame.active_item = len(facial_frame.data) - 1
|
||||
|
||||
# Set the facial frame as active
|
||||
for i, frame in enumerate(mmd_root.display_item_frames):
|
||||
if frame.name == "表情":
|
||||
mmd_root.active_display_item_frame = i
|
||||
break
|
||||
|
||||
# Reset all shape keys
|
||||
for obj in FnModel.iterate_mesh_objects(root):
|
||||
if obj.data.shape_keys:
|
||||
for kb in obj.data.shape_keys.key_blocks:
|
||||
kb.value = 0
|
||||
|
||||
self.report({"INFO"}, f"Successfully converted vertex morphs in group to vertex morph '{target_name}' and added to facial display frame")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# 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.
|
||||
# Copyright 2015 MMD Tools authors
|
||||
# This file is part of MMD Tools.
|
||||
|
||||
import math
|
||||
from typing import Dict, Optional, Tuple, cast, Set, List, Any, Union, Generator
|
||||
from typing import Dict, Optional, Tuple, cast
|
||||
|
||||
import bpy
|
||||
from mathutils import Euler, Vector
|
||||
@@ -16,7 +12,6 @@ from ..bpyutils import FnContext, Props
|
||||
from ..core import rigid_body
|
||||
from ..core.model import FnModel, Model
|
||||
from ..core.rigid_body import FnRigidBody
|
||||
from ...logging_setup import logger
|
||||
|
||||
|
||||
class SelectRigidBody(bpy.types.Operator):
|
||||
@@ -44,15 +39,15 @@ class SelectRigidBody(bpy.types.Operator):
|
||||
default=False,
|
||||
)
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
def poll(cls, context):
|
||||
return FnModel.is_rigid_body_object(context.active_object)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
if root is None:
|
||||
@@ -174,7 +169,7 @@ class AddRigidBody(bpy.types.Operator):
|
||||
default=0.1,
|
||||
)
|
||||
|
||||
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None) -> bpy.types.Object:
|
||||
def __add_rigid_body(self, context: bpy.types.Context, root_object: bpy.types.Object, pose_bone: Optional[bpy.types.PoseBone] = None):
|
||||
name_j: str = self.name_j
|
||||
name_e: str = self.name_e
|
||||
size = self.size.copy()
|
||||
@@ -227,7 +222,7 @@ class AddRigidBody(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
def poll(cls, context):
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
if root_object is None:
|
||||
return False
|
||||
@@ -238,11 +233,11 @@ class AddRigidBody(bpy.types.Operator):
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
active_object = context.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))
|
||||
root_object = cast("bpy.types.Object", FnModel.find_root_object(active_object))
|
||||
armature_object = cast("bpy.types.Object", FnModel.find_armature_object(root_object))
|
||||
|
||||
if active_object != armature_object:
|
||||
FnContext.select_single_object(context, root_object).select_set(False)
|
||||
@@ -255,17 +250,15 @@ class AddRigidBody(bpy.types.Operator):
|
||||
|
||||
armature_object.select_set(False)
|
||||
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:
|
||||
rigid = self.__add_rigid_body(context, root_object, pose_bone)
|
||||
rigid.select_set(True)
|
||||
else:
|
||||
logger.info("Adding a single rigid body without bone attachment")
|
||||
rigid = self.__add_rigid_body(context, root_object)
|
||||
rigid.select_set(True)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
|
||||
def invoke(self, context, event):
|
||||
no_bone = True
|
||||
if context.selected_bones and len(context.selected_bones) > 0:
|
||||
no_bone = False
|
||||
@@ -291,13 +284,12 @@ class RemoveRigidBody(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
def poll(cls, context):
|
||||
return FnModel.is_rigid_body_object(context.active_object)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
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
|
||||
bpy.ops.object.delete(use_global=True)
|
||||
if root:
|
||||
@@ -310,8 +302,7 @@ class RigidBodyBake(bpy.types.Operator):
|
||||
bl_label = "Bake"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
logger.info("Baking rigid body simulation")
|
||||
def execute(self, context: bpy.types.Context):
|
||||
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
|
||||
bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True)
|
||||
|
||||
@@ -323,8 +314,7 @@ class RigidBodyDeleteBake(bpy.types.Operator):
|
||||
bl_label = "Delete Bake"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
logger.info("Deleting rigid body simulation bake")
|
||||
def execute(self, context: bpy.types.Context):
|
||||
with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache):
|
||||
bpy.ops.ptcache.free_bake("INVOKE_DEFAULT")
|
||||
|
||||
@@ -387,7 +377,7 @@ class AddJoint(bpy.types.Operator):
|
||||
min=0,
|
||||
)
|
||||
|
||||
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]:
|
||||
def __enumerate_rigid_pair(self, bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]]):
|
||||
obj_seq = tuple(bone_map.keys())
|
||||
for rigid_a, bone_a in bone_map.items():
|
||||
for rigid_b, bone_b in bone_map.items():
|
||||
@@ -400,7 +390,7 @@ class AddJoint(bpy.types.Operator):
|
||||
else:
|
||||
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: Dict[bpy.types.Object, Optional[bpy.types.Bone]]) -> bpy.types.Object:
|
||||
def __add_joint(self, context: bpy.types.Context, root_object: bpy.types.Object, rigid_pair: Tuple[bpy.types.Object, bpy.types.Object], bone_map):
|
||||
loc: Optional[Vector] = None
|
||||
rot = Euler((0.0, 0.0, 0.0))
|
||||
rigid_a, rigid_b = rigid_pair
|
||||
@@ -438,7 +428,7 @@ class AddJoint(bpy.types.Operator):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
def poll(cls, context):
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
if root_object is None:
|
||||
return False
|
||||
@@ -449,11 +439,11 @@ class AddJoint(bpy.types.Operator):
|
||||
|
||||
return True
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
active_object = context.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))
|
||||
bones = cast(bpy.types.Armature, armature_object.data).bones
|
||||
root_object = cast("bpy.types.Object", FnModel.find_root_object(active_object))
|
||||
armature_object = cast("bpy.types.Object", FnModel.find_armature_object(root_object))
|
||||
bones = cast("bpy.types.Armature", armature_object.data).bones
|
||||
bone_map: Dict[bpy.types.Object, Optional[bpy.types.Bone]] = {r: bones.get(r.mmd_rigid.bone, None) for r in FnModel.iterate_rigid_body_objects(root_object) if r.select_get()}
|
||||
|
||||
if len(bone_map) < 2:
|
||||
@@ -462,19 +452,15 @@ class AddJoint(bpy.types.Operator):
|
||||
|
||||
FnContext.select_single_object(context, root_object).select_set(False)
|
||||
if context.scene.rigidbody_world is None:
|
||||
logger.info("Creating rigid body world")
|
||||
bpy.ops.rigidbody.world_add()
|
||||
|
||||
joint_count = 0
|
||||
for pair in self.__enumerate_rigid_pair(bone_map):
|
||||
joint = self.__add_joint(context, root_object, pair, bone_map)
|
||||
joint.select_set(True)
|
||||
joint_count += 1
|
||||
|
||||
logger.info(f"Added {joint_count} joints between rigid bodies")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: Any) -> Set[str]:
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
@@ -486,13 +472,12 @@ class RemoveJoint(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
def poll(cls, context):
|
||||
return FnModel.is_joint_object(context.active_object)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
root = FnModel.find_root_object(obj)
|
||||
logger.info(f"Removing joint: {obj.name}")
|
||||
utils.selectAObject(obj) # ensure this is the only one object select
|
||||
bpy.ops.object.delete(use_global=True)
|
||||
if root:
|
||||
@@ -507,7 +492,7 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@staticmethod
|
||||
def __get_rigid_body_world_objects() -> Tuple[bpy.types.Collection, bpy.types.Collection]:
|
||||
def __get_rigid_body_world_objects():
|
||||
rigid_body.setRigidBodyWorldEnabled(True)
|
||||
rbw = bpy.context.scene.rigidbody_world
|
||||
if not rbw.collection:
|
||||
@@ -522,21 +507,21 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
|
||||
return rbw.collection.objects, rbw.constraints.objects
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
scene = context.scene
|
||||
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)
|
||||
|
||||
def _update_group(obj: bpy.types.Object, group: bpy.types.Collection) -> bool:
|
||||
def _update_group(obj, group):
|
||||
if obj in scene_objs:
|
||||
if obj not in group.values():
|
||||
group.link(obj)
|
||||
return True
|
||||
elif obj in group.values():
|
||||
if obj in group.values():
|
||||
group.unlink(obj)
|
||||
return False
|
||||
|
||||
def _references(obj: bpy.types.Object) -> Generator[bpy.types.Object, None, None]:
|
||||
def _references(obj):
|
||||
yield obj
|
||||
if getattr(obj, "proxy", None):
|
||||
yield from _references(obj.proxy)
|
||||
@@ -553,7 +538,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
# Object.rigid_body are removed,
|
||||
# but Object.rigid_body_constraint are retained.
|
||||
# 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"):
|
||||
if not _update_group(i, rb_objs):
|
||||
continue
|
||||
@@ -568,7 +552,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
# TODO Modify mmd_rigid to allow recovery of the remaining rigidbody parameters.
|
||||
# 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):
|
||||
if not _update_group(i, rbc_objs):
|
||||
continue
|
||||
@@ -579,7 +562,6 @@ class UpdateRigidBodyWorld(bpy.types.Operator):
|
||||
rbc.object2 = rb_map.get(rbc.object2, rbc.object2)
|
||||
|
||||
if need_rebuild_physics:
|
||||
logger.info("Rebuilding physics for models")
|
||||
for root_object in scene.objects:
|
||||
if root_object.mmd_type != "ROOT":
|
||||
continue
|
||||
|
||||
+13
-23
@@ -1,23 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# 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.
|
||||
# Copyright 2018 MMD Tools authors
|
||||
# This file is part of MMD Tools.
|
||||
|
||||
from typing import Set, Tuple
|
||||
from typing import Set
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator, Context, Object
|
||||
from bpy.types import Operator
|
||||
|
||||
from ..core.model import FnModel
|
||||
from ..core.sdef import FnSDEF
|
||||
from ....core.logging_setup import logger
|
||||
|
||||
|
||||
def _get_target_objects(context: Context) -> Tuple[Set[Object], Set[Object]]:
|
||||
root_objects: Set[Object] = set()
|
||||
selected_objects: Set[Object] = set()
|
||||
def _get_target_objects(context):
|
||||
root_objects: Set[bpy.types.Object] = set()
|
||||
selected_objects: Set[bpy.types.Object] = set()
|
||||
for i in context.selected_objects:
|
||||
if i.type == "MESH":
|
||||
selected_objects.add(i)
|
||||
@@ -41,13 +36,11 @@ class ResetSDEFCache(Operator):
|
||||
bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
def execute(self, context):
|
||||
target_meshes, _ = _get_target_objects(context)
|
||||
logger.info(f"Resetting SDEF cache for {len(target_meshes)} objects")
|
||||
for i in target_meshes:
|
||||
FnSDEF.clear_cache(i)
|
||||
FnSDEF.clear_cache(unused_only=True)
|
||||
logger.debug("SDEF cache reset completed")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@@ -78,20 +71,19 @@ class BindSDEF(Operator):
|
||||
default=False,
|
||||
)
|
||||
|
||||
def invoke(self, context: Context, event: bpy.types.Event) -> Set[str]:
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
return vm.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
# TODO: Utility Functionalize
|
||||
def execute(self, 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:
|
||||
r.mmd_root.use_sdef = True
|
||||
|
||||
param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale)
|
||||
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)")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -102,15 +94,13 @@ class UnbindSDEF(Operator):
|
||||
bl_description = "Unbind MMD SDEF data of selected objects"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
# TODO: Utility Functionalize
|
||||
def execute(self, context):
|
||||
target_meshes, root_objects = _get_target_objects(context)
|
||||
logger.info(f"Unbinding SDEF for {len(target_meshes)} objects")
|
||||
|
||||
for i in target_meshes:
|
||||
FnSDEF.unbind(i)
|
||||
|
||||
for r in root_objects:
|
||||
r.mmd_root.use_sdef = False
|
||||
|
||||
logger.debug("SDEF unbinding completed")
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# 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.
|
||||
# Copyright 2021 MMD Tools authors
|
||||
# This file is part of MMD Tools.
|
||||
|
||||
import csv
|
||||
import os
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import bpy
|
||||
@@ -14,7 +12,11 @@ from ..core.translations import MMD_DATA_TYPE_TO_HANDLERS, FnTranslations
|
||||
from ..translations import DictionaryEnum
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..properties.translations import MMDTranslation, MMDTranslationElement, MMDTranslationElementIndex
|
||||
from ..properties.translations import (
|
||||
MMDTranslation,
|
||||
MMDTranslationElement,
|
||||
MMDTranslationElementIndex,
|
||||
)
|
||||
|
||||
|
||||
class TranslateMMDModel(bpy.types.Operator):
|
||||
@@ -77,7 +79,8 @@ class TranslateMMDModel(bpy.types.Operator):
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return obj in context.selected_objects and FnModel.find_root_object(obj)
|
||||
root = FnModel.find_root_object(obj)
|
||||
return obj is not None and obj in context.selected_objects and root is not None
|
||||
|
||||
def invoke(self, context, event):
|
||||
vm = context.window_manager
|
||||
@@ -87,7 +90,7 @@ class TranslateMMDModel(bpy.types.Operator):
|
||||
try:
|
||||
self.__translator = DictionaryEnum.get_translator(self.dictionary)
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, "Failed to load dictionary: %s" % e)
|
||||
self.report({"ERROR"}, f"Failed to load dictionary: {e}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
obj = context.active_object
|
||||
@@ -96,7 +99,7 @@ class TranslateMMDModel(bpy.types.Operator):
|
||||
|
||||
if "MMD" in self.modes:
|
||||
for i in self.types:
|
||||
getattr(self, "translate_%s" % i.lower())(rig)
|
||||
getattr(self, f"translate_{i.lower()}")(rig)
|
||||
|
||||
if "BLENDER" in self.modes:
|
||||
self.translate_blender_names(rig)
|
||||
@@ -104,7 +107,11 @@ class TranslateMMDModel(bpy.types.Operator):
|
||||
translator = self.__translator
|
||||
txt = translator.save_fails()
|
||||
if translator.fails:
|
||||
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(translator.fails), txt.name))
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
"Failed to translate %d names, see '%s' in text editor"
|
||||
% (len(translator.fails), txt.name),
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
def translate(self, name_j, name_e):
|
||||
@@ -130,7 +137,7 @@ class TranslateMMDModel(bpy.types.Operator):
|
||||
|
||||
if "DISPLAY" in self.types:
|
||||
g: bpy.types.BoneCollection
|
||||
for g in cast(bpy.types.Armature, rig.armature().data).collections:
|
||||
for g in cast("bpy.types.Armature", rig.armature().data).collections:
|
||||
g.name = self.translate(g.name, g.name)
|
||||
|
||||
if "PHYSICS" in self.types:
|
||||
@@ -153,7 +160,9 @@ class TranslateMMDModel(bpy.types.Operator):
|
||||
comment_text = bpy.data.texts.get(mmd_root.comment_text, None)
|
||||
comment_e_text = bpy.data.texts.get(mmd_root.comment_e_text, None)
|
||||
if comment_text and comment_e_text:
|
||||
comment_e = self.translate(comment_text.as_string(), comment_e_text.as_string())
|
||||
comment_e = self.translate(
|
||||
comment_text.as_string(), comment_e_text.as_string(),
|
||||
)
|
||||
comment_e_text.from_string(comment_e)
|
||||
|
||||
def translate_bone(self, rig):
|
||||
@@ -167,7 +176,7 @@ class TranslateMMDModel(bpy.types.Operator):
|
||||
mmd_root = rig.rootObject().mmd_root
|
||||
attr_list = ("group", "vertex", "bone", "uv", "material")
|
||||
prefix_list = ("G_", "", "B_", "UV_", "M_")
|
||||
for attr, prefix in zip(attr_list, prefix_list):
|
||||
for attr, prefix in zip(attr_list, prefix_list, strict=False):
|
||||
for m in getattr(mmd_root, attr + "_morphs", []):
|
||||
m.name_e = self.translate(m.name, m.name_e)
|
||||
if not prefix:
|
||||
@@ -182,7 +191,9 @@ class TranslateMMDModel(bpy.types.Operator):
|
||||
for m in rig.materials():
|
||||
if m is None:
|
||||
continue
|
||||
m.mmd_material.name_e = self.translate(m.mmd_material.name_j, m.mmd_material.name_e)
|
||||
m.mmd_material.name_e = self.translate(
|
||||
m.mmd_material.name_j, m.mmd_material.name_e,
|
||||
)
|
||||
|
||||
def translate_display(self, rig):
|
||||
mmd_root = rig.rootObject().mmd_root
|
||||
@@ -200,10 +211,24 @@ class TranslateMMDModel(bpy.types.Operator):
|
||||
DEFAULT_SHOW_ROW_COUNT = 20
|
||||
|
||||
|
||||
class MMD_TOOLS_UL_MMDTranslationElementIndex(bpy.types.UIList):
|
||||
def draw_item(self, context, layout: bpy.types.UILayout, data, mmd_translation_element_index: "MMDTranslationElementIndex", icon, active_data, active_propname, index: int):
|
||||
mmd_translation_element: "MMDTranslationElement" = data.translation_elements[mmd_translation_element_index.value]
|
||||
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].draw_item(layout, mmd_translation_element, index)
|
||||
class MMD_TOOLS_LOCAL_UL_MMDTranslationElementIndex(bpy.types.UIList):
|
||||
def draw_item(
|
||||
self,
|
||||
context,
|
||||
layout: bpy.types.UILayout,
|
||||
data,
|
||||
mmd_translation_element_index: "MMDTranslationElementIndex",
|
||||
icon,
|
||||
active_data,
|
||||
active_propname,
|
||||
index: int,
|
||||
):
|
||||
mmd_translation_element: MMDTranslationElement = data.translation_elements[
|
||||
mmd_translation_element_index.value
|
||||
]
|
||||
MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].draw_item(
|
||||
layout, mmd_translation_element, index,
|
||||
)
|
||||
|
||||
|
||||
class RestoreMMDDataReferenceOperator(bpy.types.Operator):
|
||||
@@ -216,9 +241,15 @@ class RestoreMMDDataReferenceOperator(bpy.types.Operator):
|
||||
restore_value: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
root_object = FnModel.find_root_object(context.object)
|
||||
mmd_translation_element_index = root_object.mmd_root.translation.filtered_translation_element_indices[self.index].value
|
||||
mmd_translation_element = root_object.mmd_root.translation.translation_elements[mmd_translation_element_index]
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
mmd_translation_element_index = (
|
||||
root_object.mmd_root.translation.filtered_translation_element_indices[
|
||||
self.index
|
||||
].value
|
||||
)
|
||||
mmd_translation_element = root_object.mmd_root.translation.translation_elements[
|
||||
mmd_translation_element_index
|
||||
]
|
||||
setattr(mmd_translation_element, self.prop_name, self.restore_value)
|
||||
|
||||
return {"FINISHED"}
|
||||
@@ -231,7 +262,8 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return FnModel.find_root_object(context.object) is not None
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
return root is not None
|
||||
|
||||
def draw(self, _context):
|
||||
layout = self.layout
|
||||
@@ -244,13 +276,33 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
||||
|
||||
group = row.row(align=True, heading="is Blank:")
|
||||
group.alignment = "RIGHT"
|
||||
group.prop(mmd_translation, "filter_japanese_blank", toggle=True, text="Japanese")
|
||||
group.prop(
|
||||
mmd_translation, "filter_japanese_blank", toggle=True, text="Japanese",
|
||||
)
|
||||
group.prop(mmd_translation, "filter_english_blank", toggle=True, text="English")
|
||||
|
||||
group = row.row(align=True)
|
||||
group.prop(mmd_translation, "filter_restorable", toggle=True, icon="FILE_REFRESH", icon_only=True)
|
||||
group.prop(mmd_translation, "filter_selected", toggle=True, icon="RESTRICT_SELECT_OFF", icon_only=True)
|
||||
group.prop(mmd_translation, "filter_visible", toggle=True, icon="HIDE_OFF", icon_only=True)
|
||||
group.prop(
|
||||
mmd_translation,
|
||||
"filter_restorable",
|
||||
toggle=True,
|
||||
icon="FILE_REFRESH",
|
||||
icon_only=True,
|
||||
)
|
||||
group.prop(
|
||||
mmd_translation,
|
||||
"filter_selected",
|
||||
toggle=True,
|
||||
icon="RESTRICT_SELECT_OFF",
|
||||
icon_only=True,
|
||||
)
|
||||
group.prop(
|
||||
mmd_translation,
|
||||
"filter_visible",
|
||||
toggle=True,
|
||||
icon="HIDE_OFF",
|
||||
icon_only=True,
|
||||
)
|
||||
|
||||
col = layout.column(align=True)
|
||||
box = col.box().column(align=True)
|
||||
@@ -262,11 +314,14 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
||||
row.label(text="", icon="RESTRICT_SELECT_OFF")
|
||||
row.label(text="", icon="HIDE_OFF")
|
||||
|
||||
if len(mmd_translation.filtered_translation_element_indices) > DEFAULT_SHOW_ROW_COUNT:
|
||||
if (
|
||||
len(mmd_translation.filtered_translation_element_indices)
|
||||
> DEFAULT_SHOW_ROW_COUNT
|
||||
):
|
||||
row.label(text="", icon="BLANK1")
|
||||
|
||||
col.template_list(
|
||||
"MMD_TOOLS_UL_MMDTranslationElementIndex",
|
||||
"mmd_tools_UL_MMDTranslationElementIndex",
|
||||
"",
|
||||
mmd_translation,
|
||||
"filtered_translation_element_indices",
|
||||
@@ -281,7 +336,12 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
||||
|
||||
box.separator()
|
||||
row = box.row()
|
||||
row.prop(mmd_translation, "batch_operation_script_preset", text="Preset", icon="CON_TRANSFORM_CACHE")
|
||||
row.prop(
|
||||
mmd_translation,
|
||||
"batch_operation_script_preset",
|
||||
text="Preset",
|
||||
icon="CON_TRANSFORM_CACHE",
|
||||
)
|
||||
row.operator(ExecuteTranslationBatchOperator.bl_idname, text="Execute")
|
||||
|
||||
box.separator()
|
||||
@@ -289,18 +349,25 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
||||
translation_box.label(text="Dictionaries:", icon="HELP")
|
||||
row = translation_box.row()
|
||||
row.prop(mmd_translation, "dictionary", text="to_english")
|
||||
# row.operator(ExecuteTranslationScriptOperator.bl_idname, text='Write to .csv')
|
||||
|
||||
translation_box.separator()
|
||||
row = translation_box.row()
|
||||
row.prop(mmd_translation, "dictionary", text="replace")
|
||||
|
||||
# CSV import/export
|
||||
box.separator()
|
||||
translation_box = box.box().column(align=True)
|
||||
translation_box.label(text="CSV:", icon="FILE_TEXT")
|
||||
row = translation_box.row()
|
||||
row.operator(ImportTranslationCSVOperator.bl_idname, text="Import CSV")
|
||||
row.operator(ExportTranslationCSVOperator.bl_idname, text="Export CSV")
|
||||
|
||||
def invoke(self, context: bpy.types.Context, _event):
|
||||
root_object = FnModel.find_root_object(context.object)
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
if root_object is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
mmd_translation: "MMDTranslation" = root_object.mmd_root.translation
|
||||
mmd_translation: MMDTranslation = root_object.mmd_root.translation
|
||||
self._mmd_translation = mmd_translation
|
||||
FnTranslations.clear_data(mmd_translation)
|
||||
FnTranslations.collect_data(mmd_translation)
|
||||
@@ -309,7 +376,7 @@ class GlobalTranslationPopup(bpy.types.Operator):
|
||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
||||
|
||||
def execute(self, context):
|
||||
root_object = FnModel.find_root_object(context.object)
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
if root_object is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
@@ -325,12 +392,175 @@ class ExecuteTranslationBatchOperator(bpy.types.Operator):
|
||||
bl_options = {"INTERNAL"}
|
||||
|
||||
def execute(self, context: bpy.types.Context):
|
||||
root = FnModel.find_root_object(context.object)
|
||||
root = FnModel.find_root_object(context.active_object)
|
||||
if root is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
fails, text = FnTranslations.execute_translation_batch(root)
|
||||
if fails:
|
||||
self.report({"WARNING"}, "Failed to translate %d names, see '%s' in text editor" % (len(fails), text.name))
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
"Failed to translate %d names, see '%s' in text editor"
|
||||
% (len(fails), text.name),
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ExportTranslationCSVOperator(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.export_translation_csv"
|
||||
bl_description = "Export CSV for external translation."
|
||||
bl_label = "Export Translation CSV"
|
||||
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||
filename_ext = ".csv"
|
||||
filepath: bpy.props.StringProperty(
|
||||
name="File Path",
|
||||
description="Path to save the translation CSV",
|
||||
subtype="FILE_PATH",
|
||||
default="mmd_translation.csv",
|
||||
)
|
||||
|
||||
def _ensure_csv_extension(self):
|
||||
"""Ensure the file path ends with a .csv extension (case-insensitive)."""
|
||||
if not self.filepath.lower().endswith(".csv"):
|
||||
self.filepath = bpy.path.ensure_ext(self.filepath, ".csv")
|
||||
|
||||
def invoke(self, context, event):
|
||||
self._ensure_csv_extension()
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
self._ensure_csv_extension()
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
if root_object is None:
|
||||
self.report({"ERROR"}, "Root object not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
mmd_translation = root_object.mmd_root.translation
|
||||
|
||||
try:
|
||||
with open(self.filepath, "w", newline="", encoding="utf-8") as csvfile:
|
||||
writer = csv.writer(csvfile)
|
||||
writer.writerow(["type", "blender", "japanese", "english"])
|
||||
for idx in mmd_translation.filtered_translation_element_indices:
|
||||
element = mmd_translation.translation_elements[idx.value]
|
||||
writer.writerow(
|
||||
[element.type, element.name, element.name_j, element.name_e],
|
||||
)
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to write CSV: {e}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
self.report({"INFO"}, f"Exported to {os.path.basename(self.filepath)}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ImportTranslationCSVOperator(bpy.types.Operator):
|
||||
bl_idname = "mmd_tools.import_translation_csv"
|
||||
bl_description = "Import translated CSV."
|
||||
bl_label = "Import Translation CSV"
|
||||
|
||||
only_update_english_name: bpy.props.BoolProperty(
|
||||
name="Only Update English Name",
|
||||
description="(Enabled by default) Only update English name (name_e). otherwise, update all names when different",
|
||||
default=True,
|
||||
)
|
||||
|
||||
filter_glob: bpy.props.StringProperty(default="*.csv", options={"HIDDEN"})
|
||||
filepath: bpy.props.StringProperty(
|
||||
name="File Path",
|
||||
description="Path to import the translation CSV",
|
||||
subtype="FILE_PATH",
|
||||
default="*.csv",
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
context.window_manager.fileselect_add(self)
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def execute(self, context):
|
||||
root_object = FnModel.find_root_object(context.active_object)
|
||||
if root_object is None:
|
||||
self.report({"ERROR"}, "Root object not found")
|
||||
return {"CANCELLED"}
|
||||
|
||||
mmd_translation = root_object.mmd_root.translation
|
||||
updated_count = 0
|
||||
warnings = []
|
||||
|
||||
try:
|
||||
with open(self.filepath, encoding="utf-8") as csvfile:
|
||||
reader = csv.DictReader(csvfile)
|
||||
required_headers = {"blender", "japanese", "english"}
|
||||
if not required_headers.issubset(set(reader.fieldnames or [])):
|
||||
missing = required_headers - set(reader.fieldnames or [])
|
||||
self.report(
|
||||
{"ERROR"},
|
||||
f"Missing required headers in CSV: {', '.join(missing)}",
|
||||
)
|
||||
return {"CANCELLED"}
|
||||
|
||||
visible_indices = [
|
||||
i.value
|
||||
for i in mmd_translation.filtered_translation_element_indices
|
||||
]
|
||||
translation_elements_list = list(mmd_translation.translation_elements)
|
||||
row_count = 0
|
||||
|
||||
for row in reader:
|
||||
if row_count >= len(visible_indices):
|
||||
row_count += 1
|
||||
continue
|
||||
|
||||
element = translation_elements_list[visible_indices[row_count]]
|
||||
|
||||
b_name = row.get("blender", "").strip()
|
||||
j_name = row.get("japanese", "").strip()
|
||||
e_name = row.get("english", "").strip()
|
||||
|
||||
updated = False
|
||||
if self.only_update_english_name:
|
||||
if element.name_e != e_name:
|
||||
element.name_e = e_name
|
||||
updated = True
|
||||
else:
|
||||
if element.name != b_name:
|
||||
element.name = b_name
|
||||
updated = True
|
||||
if element.name_j != j_name:
|
||||
element.name_j = j_name
|
||||
updated = True
|
||||
if element.name_e != e_name:
|
||||
element.name_e = e_name
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
updated_count += 1
|
||||
|
||||
row_count += 1
|
||||
|
||||
# Output warnings
|
||||
if row_count > len(visible_indices):
|
||||
warnings.append(
|
||||
f"{row_count - len(visible_indices)} extra lines in CSV! (ignored)",
|
||||
)
|
||||
elif row_count < len(visible_indices):
|
||||
warnings.append(
|
||||
f"{len(visible_indices) - row_count} missing lines in CSV! (aborted translation)",
|
||||
)
|
||||
except Exception as e:
|
||||
self.report({"ERROR"}, f"Failed to read CSV: {e}")
|
||||
return {"CANCELLED"}
|
||||
|
||||
FnTranslations.update_query(mmd_translation)
|
||||
|
||||
msg = f"Imported {updated_count} entries from CSV"
|
||||
if warnings:
|
||||
for w in warnings:
|
||||
self.report({"WARNING"}, w)
|
||||
msg += " with warnings"
|
||||
|
||||
self.report({"INFO"}, msg)
|
||||
return {"FINISHED"}
|
||||
|
||||
+44
-53
@@ -1,47 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 MMD Tools authors
|
||||
# This file was originally part of the MMD Tools add-on for Blender
|
||||
# You can find MMD Tools here: https://github.com/MMD-Blender/blender_mmd_tools
|
||||
# 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.
|
||||
# This file is part of MMD Tools.
|
||||
|
||||
import re
|
||||
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable, Union, Type, Iterator
|
||||
|
||||
from bpy.types import Operator, Context
|
||||
from mathutils import Matrix, Vector, Quaternion
|
||||
|
||||
from ...logging_setup import logger
|
||||
from bpy.types import Operator
|
||||
from mathutils import Matrix, Quaternion
|
||||
|
||||
|
||||
class _SetShadingBase:
|
||||
bl_options: Set[str] = {"REGISTER", "UNDO"}
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@staticmethod
|
||||
def _get_view3d_spaces(context: Context) -> Iterator[Any]:
|
||||
def _get_view3d_spaces(context):
|
||||
if getattr(context.area, "type", None) == "VIEW_3D":
|
||||
return (context.area.spaces[0],)
|
||||
return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D")
|
||||
|
||||
@staticmethod
|
||||
def _reset_color_management(context: Context, use_display_device: bool = True) -> None:
|
||||
def _reset_color_management(context, use_display_device=True):
|
||||
try:
|
||||
context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device]
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def _reset_material_shading(context: Context, use_shadeless: bool = False) -> None:
|
||||
# Note: material.use_nodes and material.use_shadeless are deprecated in Blender 5.0
|
||||
# Materials always use nodes now, and shadeless is handled differently
|
||||
# This method is kept for compatibility but no longer modifies materials
|
||||
pass
|
||||
def _reset_material_shading(context, use_shadeless=False):
|
||||
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:
|
||||
if s.material is None:
|
||||
continue
|
||||
# use_nodes is deprecated in 5.0 but harmless to set
|
||||
s.material.use_nodes = False
|
||||
s.material.use_shadeless = use_shadeless
|
||||
|
||||
def execute(self, context: Context) -> Dict[str, str]:
|
||||
def execute(self, context):
|
||||
# Changed from BLENDER_EEVEE_NEXT to BLENDER_EEVEE for Blender 5.0
|
||||
context.scene.render.engine = "BLENDER_EEVEE"
|
||||
logger.debug(f"Setting render engine to BLENDER_EEVEE")
|
||||
|
||||
shading_mode: Optional[str] = getattr(self, "_shading_mode", None)
|
||||
shading_mode = getattr(self, "_shading_mode", None)
|
||||
for space in self._get_view3d_spaces(context):
|
||||
shading = space.shading
|
||||
shading.type = "SOLID"
|
||||
@@ -49,40 +45,39 @@ class _SetShadingBase:
|
||||
shading.color_type = "TEXTURE" if shading_mode else "MATERIAL"
|
||||
shading.show_object_outline = False
|
||||
shading.show_backface_culling = False
|
||||
logger.debug(f"Applied shading mode: {shading_mode or 'DEFAULT'}")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetGLSLShading(Operator, _SetShadingBase):
|
||||
bl_idname: str = "mmd_tools.set_glsl_shading"
|
||||
bl_label: str = "GLSL View"
|
||||
bl_description: str = "Use GLSL shading with additional lighting"
|
||||
bl_idname = "mmd_tools.set_glsl_shading"
|
||||
bl_label = "GLSL View"
|
||||
bl_description = "Use GLSL shading with additional lighting"
|
||||
|
||||
_shading_mode: str = "GLSL"
|
||||
_shading_mode = "GLSL"
|
||||
|
||||
|
||||
class SetShadelessGLSLShading(Operator, _SetShadingBase):
|
||||
bl_idname: str = "mmd_tools.set_shadeless_glsl_shading"
|
||||
bl_label: str = "Shadeless GLSL View"
|
||||
bl_description: str = "Use only toon shading"
|
||||
bl_idname = "mmd_tools.set_shadeless_glsl_shading"
|
||||
bl_label = "Shadeless GLSL View"
|
||||
bl_description = "Use only toon shading"
|
||||
|
||||
_shading_mode: str = "SHADELESS"
|
||||
_shading_mode = "SHADELESS"
|
||||
|
||||
|
||||
class ResetShading(Operator, _SetShadingBase):
|
||||
bl_idname: str = "mmd_tools.reset_shading"
|
||||
bl_label: str = "Reset View"
|
||||
bl_description: str = "Reset to default Blender shading"
|
||||
bl_idname = "mmd_tools.reset_shading"
|
||||
bl_label = "Reset View"
|
||||
bl_description = "Reset to default Blender shading"
|
||||
|
||||
|
||||
class FlipPose(Operator):
|
||||
bl_idname: str = "mmd_tools.flip_pose"
|
||||
bl_label: str = "Flip Pose"
|
||||
bl_description: str = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
|
||||
bl_options: Set[str] = {"REGISTER", "UNDO"}
|
||||
bl_idname = "mmd_tools.flip_pose"
|
||||
bl_label = "Flip Pose"
|
||||
bl_description = "Apply the current pose of selected bones to matching bone on opposite side of X-Axis."
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
# https://docs.blender.org/manual/en/dev/rigging/armatures/bones/editing/naming.html
|
||||
__LR_REGEX: List[Dict[str, Any]] = [
|
||||
__LR_REGEX = [
|
||||
{"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"^(LEFT|RIGHT)(.+)$", re.IGNORECASE), "lr": 0},
|
||||
@@ -90,7 +85,7 @@ class FlipPose(Operator):
|
||||
{"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1},
|
||||
{"re": re.compile(r"^(左|右)(.+)$"), "lr": 0},
|
||||
]
|
||||
__LR_MAP: Dict[str, str] = {
|
||||
__LR_MAP = {
|
||||
"RIGHT": "LEFT",
|
||||
"Right": "Left",
|
||||
"right": "left",
|
||||
@@ -106,7 +101,7 @@ class FlipPose(Operator):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def flip_name(cls, name: str) -> str:
|
||||
def flip_name(cls, name):
|
||||
for regex in cls.__LR_REGEX:
|
||||
match = regex["re"].match(name)
|
||||
if match:
|
||||
@@ -124,15 +119,15 @@ class FlipPose(Operator):
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
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)])
|
||||
def __cmul(vec1, vec2):
|
||||
return type(vec1)([x * y for x, y in zip(vec1, vec2, strict=False)])
|
||||
|
||||
@staticmethod
|
||||
def __matrix_compose(loc: Vector, rot: Quaternion, scale: Vector) -> Matrix:
|
||||
def __matrix_compose(loc, rot, scale):
|
||||
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
|
||||
def __flip_pose(cls, matrix_basis: Matrix, bone_src: Any, bone_dest: Any) -> None:
|
||||
def __flip_pose(cls, matrix_basis, bone_src, bone_dest):
|
||||
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()
|
||||
loc, rot, scale = matrix_basis.decompose()
|
||||
@@ -141,16 +136,12 @@ class FlipPose(Operator):
|
||||
bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE"
|
||||
def poll(cls, context):
|
||||
obj = context.active_object
|
||||
return obj is not None and obj.type == "ARMATURE" and obj.mode == "POSE"
|
||||
|
||||
def execute(self, context: Context) -> Dict[str, str]:
|
||||
logger.info("Executing flip pose operation")
|
||||
def execute(self, context):
|
||||
pose_bones = context.active_object.pose.bones
|
||||
for b, mat in [(x, x.matrix_basis.copy()) for x in context.selected_pose_bones]:
|
||||
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")
|
||||
self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b))
|
||||
return {"FINISHED"}
|
||||
|
||||
Reference in New Issue
Block a user