- 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
288 lines
9.1 KiB
Python
288 lines
9.1 KiB
Python
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
|
from collections.abc import Set as AbstractSet
|
|
from typing import Callable, Optional, Protocol, TypeVar, Union, runtime_checkable
|
|
|
|
from bpy.app.translations import pgettext
|
|
from bpy.types import (
|
|
AnyType,
|
|
Armature,
|
|
Context,
|
|
Menu,
|
|
Operator,
|
|
Panel,
|
|
UILayout,
|
|
UIList,
|
|
)
|
|
|
|
from ..common import version
|
|
from ..common.preferences import get_preferences
|
|
from . import make_armature, search, validation
|
|
from .extension import get_armature_extension
|
|
from .ops import layout_operator
|
|
|
|
__AddOperator = TypeVar("__AddOperator", bound=Operator)
|
|
__RemoveOperator = TypeVar("__RemoveOperator", bound=Operator)
|
|
__MoveUpOperator = TypeVar("__MoveUpOperator", bound=Operator)
|
|
__MoveDownOperator = TypeVar("__MoveDownOperator", bound=Operator)
|
|
|
|
|
|
@runtime_checkable
|
|
class TemplateListCollectionProtocol(Protocol):
|
|
def __len__(self) -> int: ...
|
|
def __getitem__(self, index: int) -> object: ...
|
|
|
|
|
|
def draw_template_list(
|
|
layout: UILayout,
|
|
template_list: type[UIList],
|
|
base_object: AnyType,
|
|
collection_attribue_name: str,
|
|
active_index_attribute_name: str,
|
|
add_operator_type: type[__AddOperator],
|
|
remove_operator_type: type[__RemoveOperator],
|
|
move_up_operator_type: type[__MoveUpOperator],
|
|
move_down_operator_type: type[__MoveDownOperator],
|
|
*,
|
|
can_remove: Callable[[int], bool] = lambda _: True,
|
|
can_move: Callable[[int], bool] = lambda _: True,
|
|
compact: bool = False,
|
|
menu: Optional[type[Menu]] = None,
|
|
) -> tuple[
|
|
list[Union[__AddOperator, __RemoveOperator, __MoveUpOperator, __MoveDownOperator]],
|
|
list[Union[__RemoveOperator, __MoveUpOperator, __MoveDownOperator]],
|
|
int,
|
|
object,
|
|
tuple[
|
|
__AddOperator,
|
|
__RemoveOperator,
|
|
Optional[__MoveUpOperator],
|
|
Optional[__MoveDownOperator],
|
|
],
|
|
]:
|
|
collection = getattr(base_object, collection_attribue_name, None)
|
|
if not isinstance(collection, TemplateListCollectionProtocol):
|
|
message = (
|
|
f"{collection}.{collection_attribue_name}"
|
|
+ " is not a Template List Collection Protocol."
|
|
)
|
|
raise TypeError(message)
|
|
|
|
active_index = getattr(base_object, active_index_attribute_name, None)
|
|
if not isinstance(active_index, int):
|
|
message = f"{base_object}.{active_index_attribute_name} is not an int."
|
|
raise TypeError(message)
|
|
|
|
if 0 <= active_index < len(collection):
|
|
active_object = collection[active_index]
|
|
else:
|
|
active_object = None
|
|
|
|
length = len(collection)
|
|
list_row_len = 4 if length > int(compact) else 2
|
|
|
|
list_row = layout.row()
|
|
list_row.template_list(
|
|
template_list.bl_idname,
|
|
"",
|
|
base_object,
|
|
collection_attribue_name,
|
|
base_object,
|
|
active_index_attribute_name,
|
|
rows=list_row_len,
|
|
)
|
|
list_side_column = list_row.column(align=True)
|
|
add_operator = layout_operator(
|
|
list_side_column, add_operator_type, icon="ADD", text="", translate=False
|
|
)
|
|
|
|
if length >= 1 and 0 <= active_index < length and can_remove(active_index):
|
|
remove_operator_parent = list_side_column
|
|
else:
|
|
remove_operator_parent = list_side_column.column(align=True)
|
|
remove_operator_parent.enabled = False
|
|
|
|
remove_operator = layout_operator(
|
|
remove_operator_parent,
|
|
remove_operator_type,
|
|
icon="REMOVE",
|
|
text="",
|
|
translate=False,
|
|
)
|
|
|
|
if menu is not None:
|
|
list_side_column.separator()
|
|
list_side_column.menu(menu=menu.bl_idname, text="", icon="DOWNARROW_HLT")
|
|
|
|
move_operator_parent = list_side_column.column(align=True)
|
|
# Without separator, the button width changes mysteriously when list elements are 0
|
|
move_operator_parent.separator()
|
|
if length <= 1 or not (0 <= active_index < length) or not can_move(active_index):
|
|
move_operator_parent.enabled = False
|
|
|
|
collection_ops: list[
|
|
Union[__AddOperator, __RemoveOperator, __MoveUpOperator, __MoveDownOperator]
|
|
] = [add_operator]
|
|
collection_item_ops: list[
|
|
Union[__RemoveOperator, __MoveUpOperator, __MoveDownOperator]
|
|
] = [remove_operator]
|
|
|
|
if length > int(compact):
|
|
move_up_operator = layout_operator(
|
|
move_operator_parent,
|
|
move_up_operator_type,
|
|
icon="TRIA_UP",
|
|
text="",
|
|
translate=False,
|
|
)
|
|
collection_item_ops.append(move_up_operator)
|
|
move_down_operator = layout_operator(
|
|
move_operator_parent,
|
|
move_down_operator_type,
|
|
icon="TRIA_DOWN",
|
|
text="",
|
|
translate=False,
|
|
)
|
|
collection_item_ops.append(move_down_operator)
|
|
else:
|
|
move_up_operator = None
|
|
move_down_operator = None
|
|
|
|
collection_ops.extend(collection_item_ops)
|
|
|
|
return (
|
|
collection_ops,
|
|
collection_item_ops,
|
|
active_index,
|
|
active_object,
|
|
(add_operator, remove_operator, move_up_operator, move_down_operator),
|
|
)
|
|
|
|
|
|
class VRM_PT_vrm_armature_object_property(Panel):
|
|
bl_idname = "VRM_PT_vrm_armature_object_property"
|
|
bl_label = "VRM"
|
|
bl_space_type = "PROPERTIES"
|
|
bl_region_type = "WINDOW"
|
|
bl_context = "object"
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
active_object = context.active_object
|
|
if not active_object:
|
|
return False
|
|
armature_data = active_object.data
|
|
return isinstance(armature_data, Armature)
|
|
|
|
def draw(self, _context: Context) -> None:
|
|
warning_message = version.panel_warning_message()
|
|
if not warning_message:
|
|
return
|
|
|
|
box = self.layout.box()
|
|
warning_column = box.column()
|
|
for index, warning_line in enumerate(warning_message.splitlines()):
|
|
warning_column.label(
|
|
text=warning_line,
|
|
translate=False,
|
|
icon="NONE" if index else "ERROR",
|
|
)
|
|
|
|
|
|
def add_armature(add_armature_op: Operator, _context: Context) -> None:
|
|
layout_operator(
|
|
add_armature_op.layout,
|
|
make_armature.ICYP_OT_make_armature,
|
|
text="VRM Humanoid",
|
|
icon="OUTLINER_OB_ARMATURE",
|
|
).skip_heavy_armature_setup = True
|
|
|
|
|
|
class VRM_PT_current_selected_armature(Panel):
|
|
bl_idname = "VRM_PT_current_selected_armature"
|
|
bl_label = "Current selected armature"
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "VRM"
|
|
bl_options: AbstractSet[str] = {"HIDE_HEADER"}
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
return search.multiple_armatures_exist(context)
|
|
|
|
def draw(self, context: Context) -> None:
|
|
armature = search.current_armature(context)
|
|
if not armature:
|
|
return
|
|
layout = self.layout
|
|
layout.label(text=armature.name, icon="ARMATURE_DATA", translate=False)
|
|
|
|
|
|
class VRM_PT_controller(Panel):
|
|
bl_idname = "ICYP_PT_ui_controller"
|
|
bl_label = "Operator"
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "VRM"
|
|
|
|
def draw_header(self, _context: Context) -> None:
|
|
self.layout.label(icon="TOOL_SETTINGS")
|
|
|
|
def draw(self, context: Context) -> None:
|
|
layout = self.layout
|
|
preferences = get_preferences(context)
|
|
|
|
# draw_main
|
|
layout_operator(
|
|
layout,
|
|
make_armature.ICYP_OT_make_armature,
|
|
text=pgettext("Create VRM Model"),
|
|
icon="OUTLINER_OB_ARMATURE",
|
|
).skip_heavy_armature_setup = True
|
|
vrm_validator_op = layout_operator(
|
|
layout,
|
|
validation.WM_OT_vrm_validator,
|
|
text=pgettext("Check VRM Model"),
|
|
icon="VIEWZOOM",
|
|
)
|
|
vrm_validator_op.show_successful_message = True
|
|
layout.prop(preferences, "export_invisibles")
|
|
layout.prop(preferences, "export_only_selections")
|
|
|
|
armature = search.current_armature(context)
|
|
if armature:
|
|
vrm_validator_op.armature_object_name = armature.name
|
|
armature_data = armature.data
|
|
if isinstance(armature_data, Armature):
|
|
layout.prop(
|
|
get_armature_extension(armature_data),
|
|
"spec_version",
|
|
text="",
|
|
translate=False,
|
|
)
|
|
|
|
|
|
class VRM_PT_controller_unsupported_blender_version_warning(Panel):
|
|
bl_idname = "VRM_PT_controller_unsupported_blender_version_warning"
|
|
bl_label = "Unsupported Blender Version Warning"
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "VRM"
|
|
bl_options: AbstractSet[str] = {"HIDE_HEADER"}
|
|
|
|
@classmethod
|
|
def poll(cls, _context: Context) -> bool:
|
|
return bool(version.panel_warning_message())
|
|
|
|
def draw(self, _context: Context) -> None:
|
|
warning_message = version.panel_warning_message()
|
|
if warning_message is None:
|
|
return
|
|
box = self.layout.box()
|
|
warning_column = box.column()
|
|
for index, warning_line in enumerate(warning_message.splitlines()):
|
|
warning_column.label(
|
|
text=warning_line,
|
|
translate=False,
|
|
icon="NONE" if index else "ERROR",
|
|
)
|