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:
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user