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:
Yusarina
2025-11-19 06:35:06 +00:00
parent f0bda259d3
commit a929f68ad4
38 changed files with 4479 additions and 2709 deletions
+266 -36
View File
@@ -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"}