feat: Add VRM Blender addon with complete import/export functionality

- Add core VRM addon infrastructure with manifest and registration
- Add common utilities module with file system, logging, and conversion helpers
- Add human bone mapper with support for multiple rigging standards (Mixamo, MMD, Unreal, Rigify, etc.)
- Add VRM 0.x and 1.x format support with property groups and handlers
- Add editor UI panels for VRM metadata, spring bones, and MToon materials
- Add exporter with glTF2 extension support for VRM format serialization
- Add importer with scene reconstruction and armature generation
- Add MToon shader support with auto-setup and material migration
- Add spring bone physics simulation with constraint handling
- Add node constraint editor for advanced rigging control
- Add comprehensive validation and error handling with user dialogs
- Add scene watcher for real-time property synchronization
- Add workspace management and preference system
- Include Python cache files and Blender manifest configuration
- This is the initial commit establishing the complete VRM addon ecosystem for Blender
This commit is contained in:
2026-01-01 14:21:56 +08:00
commit 091ad6a49a
243 changed files with 60636 additions and 0 deletions

242
common/error_dialog.py Normal file
View File

@@ -0,0 +1,242 @@
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import contextlib
import datetime
import getpass
import platform
import sys
import sysconfig
from collections.abc import Sequence
from collections.abc import Set as AbstractSet
from pathlib import Path
from typing import TYPE_CHECKING, Union
import bpy
from bpy.props import CollectionProperty, IntProperty, StringProperty
from bpy.types import (
Context,
Event,
Operator,
PropertyGroup,
UILayout,
UIList,
)
from bpy_extras.io_utils import ExportHelper
from ..common import ops
from ..common.logger import get_logger
from ..common.version import get_addon_version
from ..editor.ops import VRM_OT_open_url_in_web_browser, layout_operator
from ..editor.property_group import CollectionPropertyProtocol
logger = get_logger(__name__)
def show_error_dialog(
title: str,
lines: Union[str, Sequence[str]],
*,
append_environment: bool = True,
) -> set[str]:
if isinstance(lines, str):
lines = list(map(str.rstrip, lines.splitlines()))
while lines and not lines[0].strip():
del lines[0]
while lines and not lines[-1].strip():
del lines[-1]
else:
lines = list(lines)
if not lines:
return {"CANCELLED"}
if append_environment:
if lines:
lines[0] = f"Python: {lines[0]}"
os_name = None
platform_system, _, _ = platform.system_alias(
platform.system(), platform.release(), platform.version()
)
if sys.version_info >= (3, 10) and platform_system not in ["Windows", "Darwin"]:
# https://github.com/python/cpython/blob/v3.10.18/Doc/library/platform.rst?plain=1#L271-L272
with contextlib.suppress(OSError):
os_name = platform.freedesktop_os_release().get("PRETTY_NAME")
if not os_name:
os_name = platform_system
python_version = (
platform.python_implementation()
+ " "
+ ".".join(map(str, sys.version_info[:3]))
)
runtime_platform = os_name + " " + platform.machine()
build_platform = sysconfig.get_platform()
lines.insert(
0,
"Environment: Blender {} / VRM Add-on {} / {} / {} ({})".format(
bpy.app.version_string,
".".join(map(str, get_addon_version())),
python_version,
runtime_platform,
build_platform,
),
)
return ops.wm.vrm_error_dialog(
"INVOKE_DEFAULT",
title=title,
lines=[
{
"name": f"error_line_{i}",
"line": mask_private_string(line),
}
for i, line in enumerate(lines)
],
)
def mask_private_string(message: str) -> str:
script_path = Path(__file__).parent.parent.parent.parent.parent
if len(script_path.parts) > 3:
message = message.replace(str(script_path), "<script path>")
message = message.replace(str(Path.home()), "<home path>")
message = message.replace(getpass.getuser(), "<user name>")
return message
class VrmErrorDialogMessageLine(PropertyGroup):
line: StringProperty() # type: ignore[valid-type]
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
line: str # type: ignore[no-redef]
class VRM_UL_vrm_error_dialog_message(UIList):
bl_idname = "VRM_UL_vrm_error_dialog_message"
def draw_item(
self,
_context: Context,
layout: UILayout,
_data: object,
line: object,
_icon: int,
_active_data: object,
_active_prop_name: str,
_index: int,
_flt_flag: int,
) -> None:
if not isinstance(line, VrmErrorDialogMessageLine):
return
column = layout.column(align=True)
column.label(text=line.line, translate=False)
class VRM_OT_save_error_dialog_message(Operator, ExportHelper):
bl_idname = "vrm.save_error_dialog_message"
bl_label = "Save Error Message"
bl_description = "Save Error Message"
bl_options: AbstractSet[str] = {"REGISTER"}
filename_ext = ".txt"
filter_glob: StringProperty( # type: ignore[valid-type]
default="*.txt",
options={"HIDDEN"},
)
title: StringProperty(options={"HIDDEN"}) # type: ignore[valid-type]
lines: CollectionProperty( # type: ignore[valid-type]
type=VrmErrorDialogMessageLine, options={"HIDDEN"}
)
def restore_error_dialog(self) -> set[str]:
if bpy.app.version >= (4, 1):
return {"FINISHED"}
return show_error_dialog(
self.title,
[output.line for output in self.lines],
append_environment=False,
)
def cancel(self, _context: Context) -> None:
self.restore_error_dialog()
def execute(self, _context: Context) -> set[str]:
message = "\n".join(output.line for output in self.lines)
Path(self.filepath).write_bytes(message.encode())
return self.restore_error_dialog()
def invoke(self, context: Context, event: Event) -> set[str]:
if not self.filepath:
local_now = datetime.datetime.now(tz=None) # Get local time # noqa: DTZ005
formatted_time = local_now.strftime("%Y%m%d_%H%M%S")
self.filepath = f"vrm_error_message_{formatted_time}.txt"
return ExportHelper.invoke(self, context, event)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
filter_glob: str # type: ignore[no-redef]
title: str # type: ignore[no-redef]
lines: CollectionPropertyProtocol[ # type: ignore[no-redef]
VrmErrorDialogMessageLine
]
class WM_OT_vrm_error_dialog(Operator):
bl_label = "Error"
bl_idname = "wm.vrm_error_dialog"
bl_options: AbstractSet[str] = {"REGISTER"}
title: StringProperty(options={"HIDDEN"}) # type: ignore[valid-type]
lines: CollectionProperty( # type: ignore[valid-type]
type=VrmErrorDialogMessageLine, options={"HIDDEN"}
)
active_line_index: IntProperty(options={"HIDDEN"}) # type: ignore[valid-type]
def execute(self, _context: Context) -> set[str]:
return {"FINISHED"}
def invoke(self, context: Context, _event: Event) -> set[str]:
self.active_line_index = len(self.lines) - 1
return context.window_manager.invoke_props_dialog(self, width=700)
def draw(self, _context: Context) -> None:
layout = self.layout.column()
layout.emboss = "NONE"
box = layout.box()
box.emboss = "NORMAL"
row = box.split(factor=0.5, align=True)
row.label(text=self.title, icon="ERROR")
save_op = layout_operator(row, VRM_OT_save_error_dialog_message, icon="FILE")
save_op.title = self.title
for process_output_line in self.lines:
output = save_op.lines.add()
output.line = process_output_line.line
open_url_op = layout_operator(
row, VRM_OT_open_url_in_web_browser, icon="URL", text="Open Support Site"
)
open_url_op.url = "https://github.com/saturday06/VRM-Addon-for-Blender/issues"
box.template_list(
VRM_UL_vrm_error_dialog_message.bl_idname,
"",
self,
"lines",
self,
"active_line_index",
rows=20,
sort_lock=True,
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
title: str # type: ignore[no-redef]
lines: CollectionPropertyProtocol[ # type: ignore[no-redef]
VrmErrorDialogMessageLine
]
active_line_index: int # type: ignore[no-redef]