diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ad3cde7..d017dc2b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,39 +13,25 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Copy LICENSE file run: cp -p LICENSE mmd_tools/ + - name: Remove typings for development + run: rm -rf mmd_tools/typings + - name: Create a zip - run: zip -r -9 artifact.zip mmd_tools/ + run: (cd mmd_tools && find . -type f ! -path "*/.*" ! -path "*/__pycache__/*" -print | zip -9r "../mmd_tools-${GITHUB_REF_NAME}.zip" -@) - name: Create a Release id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v2 with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} + name: Release ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + files: | + mmd_tools-${{ github.ref_name }}.zip draft: true + generate_release_notes: true prerelease: false - - - name: Branch name - id: branch_name - run: | - echo ::set-output name=name::${GITHUB_REF#refs/*/} - echo ::set-output name=branch::${GITHUB_REF#refs/heads/} - echo ::set-output name=tag::${GITHUB_REF#refs/tags/} - - - name: Upload Release Asset - id: upload-release-asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./artifact.zip - asset_name: mmd_tools-${{ steps.branch_name.outputs.tag }}.zip - asset_content_type: application/zip diff --git a/mmd_tools/__init__.py b/mmd_tools/__init__.py index 221b9b3f..026658ea 100644 --- a/mmd_tools/__init__.py +++ b/mmd_tools/__init__.py @@ -1,127 +1,63 @@ # -*- coding: utf-8 -*- +# Copyright 2012 MMD Tools authors +# This file is part of MMD Tools. + +# MMD Tools is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# MMD Tools is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +PACKAGE_NAME = __package__ +PACKAGE_PATH = os.path.dirname(__file__) + +with open(os.path.join(PACKAGE_PATH, "blender_manifest.toml"), "rb") as f: + import tomllib + + manifest = tomllib.load(f) + MMD_TOOLS_VERSION = manifest["version"] + + +from . import auto_load + +auto_load.init(PACKAGE_NAME) -bl_info = { - "name": "mmd_tools", - "author": "sugiany", - "version": (2, 10, 2), - "blender": (2, 93, 0), - "location": "View3D > Sidebar > MMD Tools Panel", - "description": "Utility tools for MMD model editing. (UuuNyaa's forked version)", - "warning": "", - "doc_url": "https://mmd-blender.fandom.com/wiki/MMD_Tools", - "wiki_url": "https://mmd-blender.fandom.com/wiki/MMD_Tools", - "tracker_url": "https://github.com/UuuNyaa/blender_mmd_tools/issues", - "category": "Object", -} - -MMD_TOOLS_VERSION = '.'.join(map(str,bl_info['version'])) - -import bpy - -from mmd_tools import auto_load -auto_load.init() - -from mmd_tools import operators -from mmd_tools import properties - -def menu_func_import(self, _context): - self.layout.operator(operators.fileio.ImportPmx.bl_idname, text='MikuMikuDance Model (.pmd, .pmx)', icon='OUTLINER_OB_ARMATURE') - self.layout.operator(operators.fileio.ImportVmd.bl_idname, text='MikuMikuDance Motion (.vmd)', icon='ANIM') - self.layout.operator(operators.fileio.ImportVpd.bl_idname, text='Vocaloid Pose Data (.vpd)', icon='POSE_HLT') - -def menu_func_export(self, _context): - self.layout.operator(operators.fileio.ExportPmx.bl_idname, text='MikuMikuDance Model (.pmx)', icon='OUTLINER_OB_ARMATURE') - self.layout.operator(operators.fileio.ExportVmd.bl_idname, text='MikuMikuDance Motion (.vmd)', icon='ANIM') - self.layout.operator(operators.fileio.ExportVpd.bl_idname, text='Vocaloid Pose Data (.vpd)', icon='POSE_HLT') - -def menu_func_armature(self, _context): - self.layout.operator(operators.model.CreateMMDModelRoot.bl_idname, text='Create MMD Model', icon='OUTLINER_OB_ARMATURE') - -def menu_view3d_object(self, _context): - self.layout.separator() - self.layout.operator('mmd_tools.clean_shape_keys') - -def menu_view3d_select_object(self, _context): - self.layout.separator() - self.layout.operator_context = 'EXEC_DEFAULT' - operator = self.layout.operator('mmd_tools.rigid_body_select', text='Select MMD Rigid Body') - operator.properties = set(['collision_group_number', 'shape']) - -def menu_view3d_pose_context_menu(self, _context): - self.layout.operator('mmd_tools.flip_pose', text='MMD Flip Pose', icon='ARROW_LEFTRIGHT') - -def panel_view3d_shading(self, context): - if context.space_data.shading.type != 'SOLID': - return - - col = self.layout.column(align=True) - col.label(text='MMD Shading Presets') - row = col.row(align=True) - row.operator('mmd_tools.set_glsl_shading', text='GLSL') - row.operator('mmd_tools.set_shadeless_glsl_shading', text='Shadeless') - row = col.row(align=True) - row.operator('mmd_tools.reset_shading', text='Reset') - -@bpy.app.handlers.persistent -def load_handler(_dummy): - from mmd_tools.core.sdef import FnSDEF - FnSDEF.clear_cache() - FnSDEF.register_driver_function() - - from mmd_tools.core.material import MigrationFnMaterial - MigrationFnMaterial.update_mmd_shader() - - from mmd_tools.core.morph import MigrationFnMorph - MigrationFnMorph.update_mmd_morph() - - from mmd_tools.core.camera import MigrationFnCamera - MigrationFnCamera.update_mmd_camera() - - from mmd_tools.core.model import MigrationFnModel - MigrationFnModel.update_mmd_ik_loop_factor() - MigrationFnModel.update_mmd_tools_version() - -@bpy.app.handlers.persistent -def save_pre_handler(_dummy): - from mmd_tools.core.morph import MigrationFnMorph - MigrationFnMorph.compatible_with_old_version_mmd_tools() def register(): + import bpy + + from . import handlers + auto_load.register() - properties.register() - bpy.app.handlers.load_post.append(load_handler) - bpy.app.handlers.save_pre.append(save_pre_handler) - bpy.types.VIEW3D_MT_object.append(menu_view3d_object) - bpy.types.VIEW3D_MT_select_object.append(menu_view3d_select_object) - bpy.types.VIEW3D_MT_pose.append(menu_view3d_pose_context_menu) - bpy.types.VIEW3D_MT_pose_context_menu.append(menu_view3d_pose_context_menu) - bpy.types.VIEW3D_PT_shading.append(panel_view3d_shading) - bpy.types.TOPBAR_MT_file_import.append(menu_func_import) - bpy.types.TOPBAR_MT_file_export.append(menu_func_export) - bpy.types.VIEW3D_MT_armature_add.append(menu_func_armature) - - from mmd_tools.m17n import translation_dict - bpy.app.translations.register(bl_info['name'], translation_dict) - - operators.addon_updater.register_updater(bl_info, __file__) + + # pylint: disable=import-outside-toplevel + from .m17n import translation_dict + + bpy.app.translations.register(PACKAGE_NAME, translation_dict) + + handlers.MMDHanders.register() + def unregister(): - operators.addon_updater.unregister_updater() - - bpy.app.translations.unregister(bl_info['name']) - - bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) - bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) - bpy.types.VIEW3D_MT_armature_add.remove(menu_func_armature) - bpy.types.VIEW3D_PT_shading.remove(panel_view3d_shading) - bpy.types.VIEW3D_MT_pose_context_menu.remove(menu_view3d_pose_context_menu) - bpy.types.VIEW3D_MT_pose.remove(menu_view3d_pose_context_menu) - bpy.types.VIEW3D_MT_select_object.remove(menu_view3d_select_object) - bpy.types.VIEW3D_MT_object.remove(menu_view3d_object) - bpy.app.handlers.load_post.remove(load_handler) - bpy.app.handlers.save_pre.remove(save_pre_handler) - properties.unregister() + import bpy + + from . import handlers + + handlers.MMDHanders.unregister() + + bpy.app.translations.unregister(PACKAGE_NAME) + auto_load.unregister() + if __name__ == "__main__": register() diff --git a/mmd_tools/auto_load.py b/mmd_tools/auto_load.py index 72254c06..88e04deb 100644 --- a/mmd_tools/auto_load.py +++ b/mmd_tools/auto_load.py @@ -18,11 +18,11 @@ modules = None ordered_classes = None -def init(): +def init(package_name): global modules global ordered_classes - modules = get_all_submodules(Path(__file__).parent) + modules = get_all_submodules(Path(__file__).parent, package_name) ordered_classes = get_ordered_classes_to_register(modules) def register(): @@ -49,8 +49,8 @@ def unregister(): # Import modules ################################################# -def get_all_submodules(directory): - return list(iter_submodules(directory, directory.name)) +def get_all_submodules(directory, package_name): + return list(iter_submodules(directory, package_name)) def iter_submodules(path, package_name): for name in sorted(iter_submodule_names(path)): diff --git a/mmd_tools/auto_scene_setup.py b/mmd_tools/auto_scene_setup.py index 03daf234..629aeef8 100644 --- a/mmd_tools/auto_scene_setup.py +++ b/mmd_tools/auto_scene_setup.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- +# Copyright 2013 MMD Tools authors +# This file is part of MMD Tools. import bpy + def setupFrameRanges(): s, e = 1, 1 for i in bpy.data.actions: @@ -14,11 +17,13 @@ def setupFrameRanges(): bpy.context.scene.rigidbody_world.point_cache.frame_start = int(s) bpy.context.scene.rigidbody_world.point_cache.frame_end = int(e) + def setupLighting(): bpy.context.scene.world.light_settings.use_ambient_occlusion = True bpy.context.scene.world.light_settings.use_environment_light = True bpy.context.scene.world.light_settings.use_indirect_light = True + def setupFps(): bpy.context.scene.render.fps = 30 bpy.context.scene.render.fps_base = 1 diff --git a/mmd_tools/blender_manifest.toml b/mmd_tools/blender_manifest.toml new file mode 100644 index 00000000..68e03f8e --- /dev/null +++ b/mmd_tools/blender_manifest.toml @@ -0,0 +1,49 @@ +schema_version = "1.0.0" + +id = "mmd_tools" +version = "4.2.2" +name = "MMD Tools" +tagline = "Utility tools for MMD model editing" +maintainer = "UuuNyaa " +type = "add-on" + +website = "https://mmd-blender.fandom.com/wiki/MMD_Tools" + +# Optional list defined by Blender and server, see: +# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html +tags = [ + "3D View", + "Camera", + "Import-Export", + "Material", + "Mesh", + "Object", + "Physics", +] + +blender_version_min = "4.2.0" +blender_version_max = "5.0.0" + +license = ["SPDX:GPL-3.0-or-later"] +copyright = ["2021 UuuNyaa", "2014-2022 powroupi", "2012-2015 sugiany"] + +## Optional: add-ons can list which resources they will require: +## * files (for access of any filesystem operations) +## * network (for internet access) +## * clipboard (to read and/or write the system clipboard) +## * camera (to capture photos and videos) +## * microphone (to capture audio) +## +## If using network, remember to also check `bpy.app.online_access` +## https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access +## +## For each permission it is important to also specify the reason why it is required. +## Keep this a single short sentence without a period (.) at the end. +## For longer explanations use the documentation or detail page. +[permissions] +files = "Import/export PMD/PMX/VMD/VPD from/to disk" + +# Optional: build settings. +# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build +[build] +paths_exclude_pattern = ["__pycache__/", ".vscode/", ".venv/"] diff --git a/mmd_tools/bpyutils.py b/mmd_tools/bpyutils.py index f227a3a8..fb7768b0 100644 --- a/mmd_tools/bpyutils.py +++ b/mmd_tools/bpyutils.py @@ -1,23 +1,19 @@ # -*- coding: utf-8 -*- +# Copyright 2013 MMD Tools authors +# This file is part of MMD Tools. + +import contextlib +from typing import Generator, List, Optional, TypeVar -from typing import Optional, Union import bpy -matmul = (lambda a, b: a*b) if bpy.app.version < (2, 80, 0) else (lambda a, b: a.__matmul__(b)) -class Props: # For API changes of only name changed properties - if bpy.app.version < (2, 80, 0): - show_in_front = 'show_x_ray' - display_type = 'draw_type' - display_size = 'draw_size' - empty_display_type = 'empty_draw_type' - empty_display_size = 'empty_draw_size' - else: - show_in_front = 'show_in_front' - display_type = 'display_type' - display_size = 'display_size' - empty_display_type = 'empty_display_type' - empty_display_size = 'empty_display_size' +class Props: # For API changes of only name changed properties + show_in_front = "show_in_front" + display_type = "display_type" + display_size = "display_size" + empty_display_type = "empty_display_type" + empty_display_size = "empty_display_size" class __EditMode: @@ -26,316 +22,195 @@ def __init__(self, obj): raise ValueError self.__prevMode = obj.mode self.__obj = obj - self.__obj_select = obj.select + self.__obj_select = obj.select_get() with select_object(obj): - if obj.mode != 'EDIT': - bpy.ops.object.mode_set(mode='EDIT') + if obj.mode != "EDIT": + bpy.ops.object.mode_set(mode="EDIT") def __enter__(self): return self.__obj.data def __exit__(self, type, value, traceback): - if self.__prevMode == 'EDIT': - bpy.ops.object.mode_set(mode='OBJECT') # update edited data + if self.__prevMode == "EDIT": + bpy.ops.object.mode_set(mode="OBJECT") # update edited data bpy.ops.object.mode_set(mode=self.__prevMode) - self.__obj.select = self.__obj_select + self.__obj.select_set(self.__obj_select) + class __SelectObjects: - def __init__(self, active_object, selected_objects=[]): + def __init__(self, active_object: bpy.types.Object, selected_objects: Optional[List[bpy.types.Object]] = None): if not isinstance(active_object, bpy.types.Object): raise ValueError try: - bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode="OBJECT") except Exception: pass - for i in bpy.context.selected_objects: - i.select = False + contenxt = FnContext.ensure_context() + + for i in contenxt.selected_objects: + i.select_set(False) self.__active_object = active_object - self.__selected_objects = tuple(set(selected_objects)|set([active_object])) + self.__selected_objects = tuple(set(selected_objects) | set([active_object])) if selected_objects else (active_object,) - self.__hides = [] - scene = SceneOp(bpy.context) + self.__hides: List[bool] = [] for i in self.__selected_objects: - self.__hides.append(i.hide) - scene.select_object(i) - scene.active_object = active_object + self.__hides.append(i.hide_get()) + FnContext.select_object(contenxt, i) + FnContext.set_active_object(contenxt, active_object) - def __enter__(self): + def __enter__(self) -> bpy.types.Object: return self.__active_object def __exit__(self, type, value, traceback): for i, j in zip(self.__selected_objects, self.__hides): - i.hide = j - -def find_user_layer_collection(target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]: - context: bpy.types.Context = bpy.context - scene_layer_collection: bpy.types.LayerCollection = context.view_layer.layer_collection - - def find_layer_collection_by_name(layer_collection: bpy.types.LayerCollection, name: str) -> Optional[bpy.types.LayerCollection]: - if layer_collection.name == name: - return layer_collection - - child_layer_collection: bpy.types.LayerCollection - for child_layer_collection in layer_collection.children: - found = find_layer_collection_by_name(child_layer_collection, name) - if found is not None: - return found - - return None - - user_collection: bpy.types.Collection - for user_collection in target_object.users_collection: - found = find_layer_collection_by_name(scene_layer_collection, user_collection.name) - if found is not None: - return found - - return None - -class __ActivateLayerCollection: - def __init__(self, target_layer_collection: Optional[bpy.types.LayerCollection]): - self.__original_layer_collection = bpy.context.view_layer.active_layer_collection - self.__target_layer_collection = target_layer_collection if target_layer_collection else self.__original_layer_collection - - def __enter__(self): - if bpy.context.view_layer.active_layer_collection.name != self.__target_layer_collection.name: - bpy.context.view_layer.active_layer_collection = self.__target_layer_collection - return self.__target_layer_collection - - def __exit__(self, _type, _value, _traceback): - if bpy.context.view_layer.active_layer_collection.name != self.__original_layer_collection.name: - bpy.context.view_layer.active_layer_collection = self.__original_layer_collection + i.hide_set(j) -def addon_preferences(attrname, default=None): - if hasattr(bpy.context, 'preferences'): - addon = bpy.context.preferences.addons.get(__package__, None) - else: - addon = bpy.context.user_preferences.addons.get(__package__, None) - return getattr(addon.preferences, attrname, default) if addon else default def setParent(obj, parent): with select_object(parent, objects=[parent, obj]): - bpy.ops.object.parent_set(type='OBJECT', xmirror=False, keep_transform=False) + bpy.ops.object.parent_set(type="OBJECT", xmirror=False, keep_transform=False) + def setParentToBone(obj, parent, bone_name): with select_object(parent, objects=[parent, obj]): - bpy.ops.object.mode_set(mode='POSE') + bpy.ops.object.mode_set(mode="POSE") parent.data.bones.active = parent.data.bones[bone_name] - bpy.ops.object.parent_set(type='BONE', xmirror=False, keep_transform=False) - bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.parent_set(type="BONE", xmirror=False, keep_transform=False) + bpy.ops.object.mode_set(mode="OBJECT") + def edit_object(obj): - """ Set the object interaction mode to 'EDIT' + """Set the object interaction mode to 'EDIT' - It is recommended to use 'edit_object' with 'with' statement like the following code. + It is recommended to use 'edit_object' with 'with' statement like the following code. - with edit_object: - some functions... + with edit_object: + some functions... """ return __EditMode(obj) -def select_object(obj, objects=[]): - """ Select objects. - It is recommended to use 'select_object' with 'with' statement like the following code. - This function can select "hidden" objects safely. +def select_object(obj: bpy.types.Object, objects: Optional[List[bpy.types.Object]] = None): + """Select objects. - with select_object(obj): - some functions... + It is recommended to use 'select_object' with 'with' statement like the following code. + This function can select "hidden" objects safely. + + with select_object(obj): + some functions... """ + # TODO: reimplement with bpy.context.temp_override return __SelectObjects(obj, objects) -def activate_layer_collection(target: Union[bpy.types.Object, bpy.types.LayerCollection, None]): - if isinstance(target, bpy.types.Object): - layer_collection = find_user_layer_collection(target) - elif isinstance(target, bpy.types.LayerCollection): - layer_collection = target - else: - layer_collection = None - - return __ActivateLayerCollection(layer_collection) def duplicateObject(obj, total_len): - for i in bpy.context.selected_objects: - i.select = False - obj.select = True - assert(len(bpy.context.selected_objects) == 1) - assert(bpy.context.selected_objects[0] == obj) - last_selected = objs = [obj] - while len(objs) < total_len: - bpy.ops.object.duplicate() - objs.extend(bpy.context.selected_objects) - remain = total_len - len(objs) - len(bpy.context.selected_objects) - if remain < 0: - last_selected = bpy.context.selected_objects - for i in range(-remain): - last_selected[i].select = False - else: - for i in range(min(remain, len(last_selected))): - last_selected[i].select = True - last_selected = bpy.context.selected_objects - assert(len(objs) == total_len) - return objs - -def makeCapsuleBak(segment=16, ring_count=8, radius=1.0, height=1.0, target_scene=None): - import math - target_scene = SceneOp(target_scene) - mesh = bpy.data.meshes.new(name='Capsule') - meshObj = bpy.data.objects.new(name='Capsule', object_data=mesh) - vertices = [] - top = (0, 0, height/2+radius) - vertices.append(top) - - f = lambda i: radius*i/ring_count - for i in range(ring_count, 0, -1): - z = f(i-1) - t = math.sqrt(radius**2 - z**2) - for j in range(segment): - theta = 2*math.pi/segment*j - x = t * math.sin(-theta) - y = t * math.cos(-theta) - vertices.append((x,y,z+height/2)) + return FnContext.duplicate_object(FnContext.ensure_context(), obj, total_len) - for i in range(ring_count): - z = -f(i) - t = math.sqrt(radius**2 - z**2) - for j in range(segment): - theta = 2*math.pi/segment*j - x = t * math.sin(-theta) - y = t * math.cos(-theta) - vertices.append((x,y,z-height/2)) - - bottom = (0, 0, -(height/2+radius)) - vertices.append(bottom) - - faces = [] - for i in range(1, segment): - faces.append([0, i, i+1]) - faces.append([0, segment, 1]) - offset = segment + 1 - for i in range(ring_count*2-1): - for j in range(segment-1): - t = offset + j - faces.append([t-segment, t, t+1, t-segment+1]) - faces.append([offset-1, offset+segment-1, offset, offset-segment]) - offset += segment - for i in range(segment-1): - t = offset + i - faces.append([t-segment, offset, t-segment+1]) - faces.append([offset-1, offset, offset-segment]) - mesh.from_pydata(vertices, [], faces) - target_scene.link_object(meshObj) - return meshObj +def createObject(name="Object", object_data=None, target_scene=None): + context = FnContext.ensure_context(target_scene) + return FnContext.set_active_object(context, FnContext.new_and_link_object(context, name, object_data)) -def createObject(name='Object', object_data=None, target_scene=None): - target_scene = SceneOp(target_scene) - obj = bpy.data.objects.new(name=name, object_data=object_data) - target_scene.link_object(obj) - target_scene.active_object = obj - return obj def makeSphere(segment=8, ring_count=5, radius=1.0, target_object=None): import bmesh + if target_object is None: - target_object = createObject(name='Sphere') + target_object = createObject(name="Sphere") mesh = target_object.data bm = bmesh.new() - if bpy.app.version >= (3, 0, 0): - bmesh.ops.create_uvsphere( - bm, - u_segments=segment, - v_segments=ring_count, - radius=radius, - ) - else: - # SUPPORT_UNTIL: 3.3 LTS - bmesh.ops.create_uvsphere( - bm, - u_segments=segment, - v_segments=ring_count, - diameter=radius, - ) + bmesh.ops.create_uvsphere( + bm, + u_segments=segment, + v_segments=ring_count, + radius=radius, + ) for f in bm.faces: f.smooth = True bm.to_mesh(mesh) bm.free() return target_object -def makeBox(size=(1,1,1), target_object=None): + +def makeBox(size=(1, 1, 1), target_object=None): import bmesh from mathutils import Matrix + if target_object is None: - target_object = createObject(name='Box') + target_object = createObject(name="Box") mesh = target_object.data bm = bmesh.new() bmesh.ops.create_cube( bm, size=2, - matrix=Matrix([[size[0],0,0,0], [0,size[1],0,0], [0,0,size[2],0], [0,0,0,1]]), - ) + matrix=Matrix([[size[0], 0, 0, 0], [0, size[1], 0, 0], [0, 0, size[2], 0], [0, 0, 0, 1]]), + ) for f in bm.faces: f.smooth = True bm.to_mesh(mesh) bm.free() return target_object + def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=None): - import bmesh import math + + import bmesh + if target_object is None: - target_object = createObject(name='Capsule') + target_object = createObject(name="Capsule") height = max(height, 1e-3) mesh = target_object.data bm = bmesh.new() verts = bm.verts - top = (0, 0, height/2+radius) + top = (0, 0, height / 2 + radius) verts.new(top) - #f = lambda i: radius*i/ring_count - f = lambda i: radius*math.sin(0.5*math.pi*i/ring_count) + # f = lambda i: radius*i/ring_count + f = lambda i: radius * math.sin(0.5 * math.pi * i / ring_count) for i in range(ring_count, 0, -1): - z = f(i-1) + z = f(i - 1) t = math.sqrt(radius**2 - z**2) for j in range(segment): - theta = 2*math.pi/segment*j + theta = 2 * math.pi / segment * j x = t * math.sin(-theta) y = t * math.cos(-theta) - verts.new((x,y,z+height/2)) + verts.new((x, y, z + height / 2)) for i in range(ring_count): z = -f(i) t = math.sqrt(radius**2 - z**2) for j in range(segment): - theta = 2*math.pi/segment*j + theta = 2 * math.pi / segment * j x = t * math.sin(-theta) y = t * math.cos(-theta) - verts.new((x,y,z-height/2)) + verts.new((x, y, z - height / 2)) - bottom = (0, 0, -(height/2+radius)) + bottom = (0, 0, -(height / 2 + radius)) verts.new(bottom) - if hasattr(verts, 'ensure_lookup_table'): + if hasattr(verts, "ensure_lookup_table"): verts.ensure_lookup_table() faces = bm.faces for i in range(1, segment): - faces.new([verts[x] for x in (0, i, i+1)]) + faces.new([verts[x] for x in (0, i, i + 1)]) faces.new([verts[x] for x in (0, segment, 1)]) offset = segment + 1 - for i in range(ring_count*2-1): - for j in range(segment-1): + for i in range(ring_count * 2 - 1): + for j in range(segment - 1): t = offset + j - faces.new([verts[x] for x in (t-segment, t, t+1, t-segment+1)]) - faces.new([verts[x] for x in (offset-1, offset+segment-1, offset, offset-segment)]) + faces.new([verts[x] for x in (t - segment, t, t + 1, t - segment + 1)]) + faces.new([verts[x] for x in (offset - 1, offset + segment - 1, offset, offset - segment)]) offset += segment - for i in range(segment-1): + for i in range(segment - 1): t = offset + i - faces.new([verts[x] for x in (t-segment, offset, t-segment+1)]) - faces.new([verts[x] for x in (offset-1, offset, offset-segment)]) + faces.new([verts[x] for x in (t - segment, offset, t - segment + 1)]) + faces.new([verts[x] for x in (offset - 1, offset, offset - segment)]) for f in bm.faces: f.smooth = True @@ -345,186 +220,299 @@ def makeCapsule(segment=8, ring_count=2, radius=1.0, height=1.0, target_object=N return target_object -class ObjectOp: - - def __init__(self, obj): - self.__obj = obj - - def __clean_drivers(self, key): - for d in getattr(key.id_data.animation_data, 'drivers', ()): - if d.data_path.startswith(key.path_from_id()): - key.id_data.driver_remove(d.data_path, -1) - - if bpy.app.version < (2, 75, 0): - def shape_key_remove(self, key): - obj = self.__obj - assert(key.id_data == obj.data.shape_keys) - key_blocks = key.id_data.key_blocks - relative_key_map = {k.name:getattr(k.relative_key, 'name', '') for k in key_blocks} - last_index, obj.active_shape_key_index = obj.active_shape_key_index, key_blocks.find(key.name) - if last_index >= obj.active_shape_key_index: - last_index = max(0, last_index-1) - bpy.context.scene.objects.active, last = obj, bpy.context.scene.objects.active - self.__clean_drivers(key) - bpy.ops.object.shape_key_remove() - bpy.context.scene.objects.active = last - for k in key_blocks: - k.relative_key = key_blocks.get(relative_key_map[k.name], key_blocks[0]) - obj.active_shape_key_index = min(last_index, len(key_blocks)-1) - else: - def shape_key_remove(self, key): - obj = self.__obj - assert(key.id_data == obj.data.shape_keys) - key_blocks = key.id_data.key_blocks - last_index = obj.active_shape_key_index - if last_index >= key_blocks.find(key.name): - last_index = max(0, last_index-1) - self.__clean_drivers(key) - obj.shape_key_remove(key) - obj.active_shape_key_index = min(last_index, len(key_blocks)-1) - class TransformConstraintOp: - - __MIN_MAX_MAP = {} if bpy.app.version < (2, 71, 0) else {'ROTATION':'_rot', 'SCALE':'_scale'} + __MIN_MAX_MAP = {"ROTATION": "_rot", "SCALE": "_scale"} @staticmethod def create(constraints, name, map_type): c = constraints.get(name, None) - if c and c.type != 'TRANSFORM': + if c and c.type != "TRANSFORM": constraints.remove(c) c = None if c is None: - c = constraints.new('TRANSFORM') + c = constraints.new("TRANSFORM") c.name = name c.use_motion_extrapolate = True - c.target_space = c.owner_space = 'LOCAL' + c.target_space = c.owner_space = "LOCAL" c.map_from = c.map_to = map_type - c.map_to_x_from = 'X' - c.map_to_y_from = 'Y' - c.map_to_z_from = 'Z' + c.map_to_x_from = "X" + c.map_to_y_from = "Y" + c.map_to_z_from = "Z" c.influence = 1 return c @classmethod - def min_max_attributes(cls, map_type, name_id=''): + def min_max_attributes(cls, map_type, name_id=""): key = (map_type, name_id) ret = cls.__MIN_MAX_MAP.get(key, None) if ret is None: - defaults = (i+j+k for i in ('from_', 'to_') for j in ('min_', 'max_') for k in 'xyz') - extension = cls.__MIN_MAX_MAP.get(map_type, '') - ret = cls.__MIN_MAX_MAP[key] = tuple(n+extension for n in defaults if name_id in n) + defaults = (i + j + k for i in ("from_", "to_") for j in ("min_", "max_") for k in "xyz") + extension = cls.__MIN_MAX_MAP.get(map_type, "") + ret = cls.__MIN_MAX_MAP[key] = tuple(n + extension for n in defaults if name_id in n) return ret @classmethod def update_min_max(cls, constraint, value, influence=1): c = constraint - if not c or c.type != 'TRANSFORM': + if not c or c.type != "TRANSFORM": return - for attr in cls.min_max_attributes(c.map_from, 'from_min'): + for attr in cls.min_max_attributes(c.map_from, "from_min"): setattr(c, attr, -value) - for attr in cls.min_max_attributes(c.map_from, 'from_max'): + for attr in cls.min_max_attributes(c.map_from, "from_max"): setattr(c, attr, value) if influence is None: return - for attr in cls.min_max_attributes(c.map_to, 'to_min'): - setattr(c, attr, -value*influence) - for attr in cls.min_max_attributes(c.map_to, 'to_max'): - setattr(c, attr, value*influence) - -if bpy.app.version < (2, 80, 0): - class SceneOp: - def __init__(self, context=None): - self.__context = context or bpy.context - self.__scene = self.__context.scene - - def __ensure_selectable(self, obj): - obj.hide = obj.hide_select = False - if obj not in self.__context.selectable_objects: - selected_objects = self.__context.selected_objects - self.__scene.layers[next(i for i, enabled in enumerate(obj.layers) if enabled)] = True - if len(self.__context.selected_objects) != len(selected_objects): - for i in self.__context.selected_objects: - if i not in selected_objects: - i.select = False - - def select_object(self, obj): - self.__ensure_selectable(obj) - obj.select = True - - def link_object(self, obj): - self.__scene.objects.link(obj) - - @property - def active_object(self): - return self.__scene.objects.active - - @active_object.setter - def active_object(self, obj): - self.select_object(obj) - self.__scene.objects.active = obj - - @property - def id_scene(self): - return self.__scene - - @property - def id_objects(self): - return self.__scene.objects -else: - class SceneOp: - def __init__(self, context=None): - self.__context = context or bpy.context - self.__scene = self.__context.scene - self.__collection = self.__context.collection - self.__view_layer = self.__context.view_layer - - def __ensure_selectable(self, obj): - obj.hide_viewport = obj.hide_select = False - obj.hide_set(False) - if obj not in self.__context.selectable_objects: - def __unhide(lc): - lc.hide_viewport = lc.collection.hide_viewport = lc.collection.hide_select = False - return True - def __layer_check(layer_collection): - for lc in layer_collection.children: - if __layer_check(lc): - return __unhide(lc) - if obj in layer_collection.collection.objects.values(): - if layer_collection.exclude: - layer_collection.exclude = False + for attr in cls.min_max_attributes(c.map_to, "to_min"): + setattr(c, attr, -value * influence) + for attr in cls.min_max_attributes(c.map_to, "to_max"): + setattr(c, attr, value * influence) + + +class FnObject: + def __init__(self): + raise NotImplementedError("This class is not expected to be instantiated.") + + @staticmethod + def mesh_remove_shape_key(mesh_object: bpy.types.Object, shape_key: bpy.types.ShapeKey): + assert isinstance(mesh_object.data, bpy.types.Mesh) + + key: bpy.types.Key = shape_key.id_data + assert key == mesh_object.data.shape_keys + + if mesh_object.animation_data is not None: + fc_curve: bpy.types.FCurve + for fc_curve in mesh_object.animation_data.drivers: + if not fc_curve.data_path.startswith(shape_key.path_from_id()): + continue + mesh_object.driver_remove(fc_curve.data_path) + + key_blocks = key.key_blocks + + last_index = mesh_object.active_shape_key_index or 0 + if last_index >= key_blocks.find(shape_key.name): + last_index = max(0, last_index - 1) + + mesh_object.shape_key_remove(shape_key) + mesh_object.active_shape_key_index = min(last_index, len(key_blocks) - 1) + + +ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = TypeVar("ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE") + + +class FnContext: + def __init__(self): + raise NotImplementedError("This class is not expected to be instantiated.") + + @staticmethod + def ensure_context(context: Optional[bpy.types.Context] = None) -> bpy.types.Context: + return context or bpy.context + + @staticmethod + def get_active_object(context: bpy.types.Context) -> Optional[bpy.types.Object]: + return context.active_object + + @staticmethod + def set_active_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + context.view_layer.objects.active = obj + return obj + + @staticmethod + def set_active_and_select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + return FnContext.set_active_object(context, FnContext.select_single_object(context, obj)) + + @staticmethod + def get_scene_objects(context: bpy.types.Context) -> bpy.types.SceneObjects: + return context.scene.objects + + @staticmethod + def ensure_selectable(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + obj.hide_viewport = False + obj.hide_select = False + obj.hide_set(False) + + if obj not in context.selectable_objects: + + def __layer_check(layer_collection: bpy.types.LayerCollection) -> bool: + for lc in layer_collection.children: + if __layer_check(lc): + lc.hide_viewport = False + lc.collection.hide_viewport = False + lc.collection.hide_select = False return True - return False - selected_objects = self.__context.selected_objects - __layer_check(self.__view_layer.layer_collection) - if len(self.__context.selected_objects) != len(selected_objects): - for i in self.__context.selected_objects: - if i not in selected_objects: - i.select_set(False) - - def select_object(self, obj): - self.__ensure_selectable(obj) - obj.select_set(True) - - def link_object(self, obj): - self.__collection.objects.link(obj) - - @property - def active_object(self): - return self.__view_layer.objects.active - - @active_object.setter - def active_object(self, obj): - self.select_object(obj) - self.__view_layer.objects.active = obj - - @property - def id_scene(self): - return self.__scene - - @property - def id_objects(self): - return self.__scene.objects + if obj in layer_collection.collection.objects.values(): + if layer_collection.exclude: + layer_collection.exclude = False + return True + return False + selected_objects = set(context.selected_objects) + __layer_check(context.view_layer.layer_collection) + if len(context.selected_objects) != len(selected_objects): + for i in context.selected_objects: + if i not in selected_objects: + i.select_set(False) + return obj + + @staticmethod + def select_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + FnContext.ensure_selectable(context, obj).select_set(True) + return obj + + @staticmethod + def select_objects(context: bpy.types.Context, *objects: bpy.types.Object) -> List[bpy.types.Object]: + return [FnContext.select_object(context, obj) for obj in objects] + + @staticmethod + def select_single_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + for i in context.selected_objects: + if i != obj: + i.select_set(False) + return FnContext.select_object(context, obj) + + @staticmethod + def link_object(context: bpy.types.Context, obj: bpy.types.Object) -> bpy.types.Object: + context.collection.objects.link(obj) + return obj + + @staticmethod + def new_and_link_object(context: bpy.types.Context, name: str, object_data: Optional[bpy.types.ID]) -> bpy.types.Object: + return FnContext.link_object(context, bpy.data.objects.new(name=name, object_data=object_data)) + + @staticmethod + def duplicate_object(context: bpy.types.Context, object_to_duplicate: bpy.types.Object, target_count: int) -> List[bpy.types.Object]: + """ + Duplicate object. + + This function duplicates the given object and returns a list of duplicated objects. + + Args: + context (bpy.types.Context): The context in which the duplication is performed. + object_to_duplicate (bpy.types.Object): The object to be duplicated. + target_count (int): The desired count of duplicated objects. + + Returns: + List[bpy.types.Object]: A list of duplicated objects. + + Raises: + AssertionError: If the number of selected objects in the context is not equal to 1 or if the selected object is not the same as the object to be duplicated. + """ + for o in context.selected_objects: + o.select_set(False) + object_to_duplicate.select_set(True) + assert len(context.selected_objects) == 1 + assert context.selected_objects[0] == object_to_duplicate + last_selected_objects = result_objects = [object_to_duplicate] + while len(result_objects) < target_count: + bpy.ops.object.duplicate() + result_objects.extend(context.selected_objects) + remain = target_count - len(result_objects) - len(context.selected_objects) + if remain < 0: + last_selected_objects = context.selected_objects + for i in range(-remain): + last_selected_objects[i].select_set(False) + else: + for i in range(min(remain, len(last_selected_objects))): + last_selected_objects[i].select_set(True) + last_selected_objects = context.selected_objects + assert len(result_objects) == target_count + return result_objects + + @staticmethod + def find_user_layer_collection_by_object(context: bpy.types.Context, target_object: bpy.types.Object) -> Optional[bpy.types.LayerCollection]: + """ + Finds the layer collection that contains the given target_object in the user's collections. + + Args: + context (bpy.types.Context): The Blender context. + target_object (bpy.types.Object): The target object to find the layer collection for. + + Returns: + Optional[bpy.types.LayerCollection]: The layer collection that contains the target_object, or None if not found. + """ + scene_layer_collection: bpy.types.LayerCollection = context.view_layer.layer_collection + + def find_layer_collection_by_name(layer_collection: bpy.types.LayerCollection, name: str) -> Optional[bpy.types.LayerCollection]: + if layer_collection.name == name: + return layer_collection + + child_layer_collection: bpy.types.LayerCollection + for child_layer_collection in layer_collection.children: + found = find_layer_collection_by_name(child_layer_collection, name) + if found is not None: + return found + + return None + + user_collection: bpy.types.Collection + for user_collection in target_object.users_collection: + found = find_layer_collection_by_name(scene_layer_collection, user_collection.name) + if found is not None: + return found + + return None + + @staticmethod + @contextlib.contextmanager + def temp_override_active_layer_collection(context: bpy.types.Context, target_object: bpy.types.Object) -> Generator[bpy.types.Context, None, None]: + """ + Context manager to temporarily override the active_layer_collection that contains the target object. + + This context manager allows you to temporarily change the active_layer_collection in the given context to the one that contains the target object. + It ensures that the original active_layer_collection is restored after the context is exited. + + Args: + context (bpy.types.Context): The context in which the active_layer_collection will be overridden. + target_object (bpy.types.Object): The target object whose layer collection will be set as the active_layer_collection. + + Yields: + bpy.types.Context: The modified context with the active_layer_collection overridden. + + Example: + with FnContext.temp_override_active_layer_collection(context, target_object): + # Perform operations with the modified context + bpy.ops.object.select_all(action='DESELECT') + target_object.select_set(True) + bpy.ops.object.delete() + + """ + original_layer_collection = context.view_layer.active_layer_collection + target_layer_collection = FnContext.find_user_layer_collection_by_object(context, target_object) + if target_layer_collection is not None: + context.view_layer.active_layer_collection = target_layer_collection + try: + yield context + finally: + if context.view_layer.active_layer_collection.name != original_layer_collection.name: + context.view_layer.active_layer_collection = original_layer_collection + + @staticmethod + def __get_addon_preferences(context: bpy.types.Context) -> Optional[bpy.types.AddonPreferences]: + addon: bpy.types.Addon = context.preferences.addons.get(__package__, None) + return addon.preferences if addon else None + + @staticmethod + def get_addon_preferences_attribute(context: bpy.types.Context, attribute_name: str, default_value: ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE = None) -> ADDON_PREFERENCE_ATTRIBUTE_VALUE_TYPE: + return getattr(FnContext.__get_addon_preferences(context), attribute_name, default_value) + + @staticmethod + def temp_override_objects( + context: bpy.types.Context, + window: Optional[bpy.types.Window] = None, + area: Optional[bpy.types.Area] = None, + region: Optional[bpy.types.Region] = None, + active_object: Optional[bpy.types.Object] = None, + selected_objects: Optional[List[bpy.types.Object]] = None, + **keywords, + ) -> Generator[bpy.types.Context, None, None]: + if active_object is not None: + keywords["active_object"] = active_object + keywords["object"] = active_object + + if selected_objects is not None: + keywords["selected_objects"] = selected_objects + keywords["selected_editable_objects"] = selected_objects + + return context.temp_override(window=window, area=area, region=region, **keywords) diff --git a/mmd_tools/core/__init__.py b/mmd_tools/core/__init__.py index 40a96afc..3cd94a1b 100644 --- a/mmd_tools/core/__init__.py +++ b/mmd_tools/core/__init__.py @@ -1 +1,3 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. diff --git a/mmd_tools/core/bone.py b/mmd_tools/core/bone.py index 1253613c..3d9be0c9 100644 --- a/mmd_tools/core/bone.py +++ b/mmd_tools/core/bone.py @@ -1,13 +1,20 @@ # -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. + +import math +from typing import TYPE_CHECKING, Iterable, Optional, Set import bpy -from bpy.types import PoseBone +from mathutils import Vector -from math import pi -import math -from mathutils import Vector, Quaternion, Matrix -from mmd_tools import bpyutils -from mmd_tools.bpyutils import TransformConstraintOp +from .. import bpyutils +from ..bpyutils import TransformConstraintOp +from ..utils import ItemOp + +if TYPE_CHECKING: + from ..properties.root import MMDRoot, MMDDisplayItemFrame + from ..properties.pose_bone import MMDBone def remove_constraint(constraints, name): @@ -17,6 +24,7 @@ def remove_constraint(constraints, name): return True return False + def remove_edit_bones(edit_bones, bone_names): for name in bone_names: b = edit_bones.get(name, None) @@ -24,57 +32,54 @@ def remove_edit_bones(edit_bones, bone_names): edit_bones.remove(b) -class FnBone(object): - AUTO_LOCAL_AXIS_ARMS = ('左肩', '左腕', '左ひじ', '左手首', '右腕', '右肩', '右ひじ', '右手首') - AUTO_LOCAL_AXIS_FINGERS = ('親指','人指', '中指', '薬指','小指') - AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS = ('左腕捩', '左手捩', '左肩P', '左ダミー', '右腕捩', '右手捩', '右肩P', '右ダミー') +BONE_COLLECTION_CUSTOM_PROPERTY_NAME = "mmd_tools" +BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL = "special collection" +BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL = "normal collection" +BONE_COLLECTION_NAME_SHADOW = "mmd_shadow" +BONE_COLLECTION_NAME_DUMMY = "mmd_dummy" - def __init__(self, pose_bone=None): - if pose_bone is not None and not isinstance(pose_bone, PoseBone): - raise ValueError - self.__bone = pose_bone +SPECIAL_BONE_COLLECTION_NAMES = [BONE_COLLECTION_NAME_SHADOW, BONE_COLLECTION_NAME_DUMMY] - @classmethod - def from_bone_id(cls, armature, bone_id): - for bone in armature.pose.bones: - if bone.mmd_bone.bone_id == bone_id: - return cls(bone) - return None - @property - def bone_id(self): - mmd_bone = self.__bone.mmd_bone - if mmd_bone.bone_id < 0: - max_id = -1 - for bone in self.__bone.id_data.pose.bones: - max_id = max(max_id, bone.mmd_bone.bone_id) - mmd_bone.bone_id = max_id + 1 - return mmd_bone.bone_id +class FnBone: + AUTO_LOCAL_AXIS_ARMS = ("左肩", "左腕", "左ひじ", "左手首", "右腕", "右肩", "右ひじ", "右手首") + AUTO_LOCAL_AXIS_FINGERS = ("親指", "人指", "中指", "薬指", "小指") + AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS = ("左腕捩", "左手捩", "左肩P", "左ダミー", "右腕捩", "右手捩", "右肩P", "右ダミー") - def __get_pose_bone(self): - return self.__bone + def __init__(self): + raise NotImplementedError("This class cannot be instantiated.") - def __set_pose_bone(self, pose_bone): - if not isinstance(pose_bone, bpy.types.PoseBone): - raise ValueError - self.__bone = pose_bone + @staticmethod + def find_pose_bone_by_bone_id(armature_object: bpy.types.Object, bone_id: int) -> Optional[bpy.types.PoseBone]: + for bone in armature_object.pose.bones: + if bone.mmd_bone.bone_id != bone_id: + continue + return bone + return None - pose_bone = property(__get_pose_bone, __set_pose_bone) + @staticmethod + def __new_bone_id(armature_object: bpy.types.Object) -> int: + return max(b.mmd_bone.bone_id for b in armature_object.pose.bones) + 1 + @staticmethod + def get_or_assign_bone_id(pose_bone: bpy.types.PoseBone) -> int: + if pose_bone.mmd_bone.bone_id < 0: + pose_bone.mmd_bone.bone_id = FnBone.__new_bone_id(pose_bone.id_data) + return pose_bone.mmd_bone.bone_id @staticmethod - def get_selected_pose_bones(armature): - if armature.mode == 'EDIT': - with bpyutils.select_object(armature): # update selected bones - bpy.ops.object.mode_set(mode='EDIT') # back to edit mode + def __get_selected_pose_bones(armature_object: bpy.types.Object) -> Iterable[bpy.types.PoseBone]: + if armature_object.mode == "EDIT": + bpy.ops.object.mode_set(mode="OBJECT") # update selected bones + bpy.ops.object.mode_set(mode="EDIT") # back to edit mode context_selected_bones = bpy.context.selected_pose_bones or bpy.context.selected_bones or [] - bones = armature.pose.bones + bones = armature_object.pose.bones return (bones[b.name] for b in context_selected_bones if not bones[b.name].is_mmd_shadow_bone) - @classmethod - def load_bone_fixed_axis(cls, armature, enable=True): - for b in cls.get_selected_pose_bones(armature): - mmd_bone = b.mmd_bone + @staticmethod + def load_bone_fixed_axis(armature_object: bpy.types.Object, enable=True): + for b in FnBone.__get_selected_pose_bones(armature_object): + mmd_bone: MMDBone = b.mmd_bone mmd_bone.enabled_fixed_axis = enable lock_rotation = b.lock_rotation[:] if enable: @@ -82,25 +87,165 @@ def load_bone_fixed_axis(cls, armature, enable=True): if lock_rotation.count(False) == 1: mmd_bone.fixed_axis = axes[lock_rotation.index(False)].xzy else: - mmd_bone.fixed_axis = axes[1].xzy # Y-axis - elif all(b.lock_location) and lock_rotation.count(True) > 1 and \ - lock_rotation == (b.lock_ik_x, b.lock_ik_y, b.lock_ik_z): + mmd_bone.fixed_axis = axes[1].xzy # Y-axis + elif all(b.lock_location) and lock_rotation.count(True) > 1 and lock_rotation == (b.lock_ik_x, b.lock_ik_y, b.lock_ik_z): # unlock transform locks if fixed axis was applied b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = (False, False, False) b.lock_location = b.lock_scale = (False, False, False) - @classmethod - def apply_bone_fixed_axis(cls, armature): + @staticmethod + def setup_special_bone_collections(armature_object: bpy.types.Object) -> bpy.types.Object: + armature: bpy.types.Armature = armature_object.data + bone_collections = armature.collections + for bone_collection_name in SPECIAL_BONE_COLLECTION_NAMES: + if bone_collection_name in bone_collections: + continue + bone_collection = bone_collections.new(bone_collection_name) + FnBone.__set_bone_collection_to_special(bone_collection, is_visible=False) + return armature_object + + @staticmethod + def __is_mmd_tools_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool: + return BONE_COLLECTION_CUSTOM_PROPERTY_NAME in bone_collection + + @staticmethod + def __is_special_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool: + return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) + + @staticmethod + def __set_bone_collection_to_special(bone_collection: bpy.types.BoneCollection, is_visible: bool): + bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_SPECIAL + bone_collection.is_visible = is_visible + + @staticmethod + def __is_normal_bone_collection(bone_collection: bpy.types.BoneCollection) -> bool: + return BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL == bone_collection.get(BONE_COLLECTION_CUSTOM_PROPERTY_NAME) + + @staticmethod + def __set_bone_collection_to_normal(bone_collection: bpy.types.BoneCollection): + bone_collection[BONE_COLLECTION_CUSTOM_PROPERTY_NAME] = BONE_COLLECTION_CUSTOM_PROPERTY_VALUE_NORMAL + + @staticmethod + def __set_edit_bone_to_special(edit_bone: bpy.types.EditBone, bone_collection_name: str) -> bpy.types.EditBone: + edit_bone.id_data.collections[bone_collection_name].assign(edit_bone) + edit_bone.use_deform = False + return edit_bone + + @staticmethod + def set_edit_bone_to_dummy(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone: + return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_DUMMY) + + @staticmethod + def set_edit_bone_to_shadow(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone: + return FnBone.__set_edit_bone_to_special(edit_bone, BONE_COLLECTION_NAME_SHADOW) + + @staticmethod + def __unassign_mmd_tools_bone_collections(edit_bone: bpy.types.EditBone) -> bpy.types.EditBone: + for bone_collection in edit_bone.collections: + if not FnBone.__is_mmd_tools_bone_collection(bone_collection): + continue + bone_collection.unassign(edit_bone) + return edit_bone + + @staticmethod + def sync_bone_collections_from_display_item_frames(armature_object: bpy.types.Object): + armature: bpy.types.Armature = armature_object.data + bone_collections = armature.collections + + from .model import FnModel + + root_object: bpy.types.Object = FnModel.find_root_object(armature_object) + mmd_root: MMDRoot = root_object.mmd_root + + bones = armature.bones + used_groups = set() + unassigned_bone_names = {b.name for b in bones} + + for frame in mmd_root.display_item_frames: + for item in frame.data: + if item.type == "BONE" and item.name in unassigned_bone_names: + unassigned_bone_names.remove(item.name) + group_name = frame.name + used_groups.add(group_name) + bone_collection = bone_collections.get(group_name) + if bone_collection is None: + bone_collection = bone_collections.new(name=group_name) + FnBone.__set_bone_collection_to_normal(bone_collection) + bone_collection.assign(bones[item.name]) + + for name in unassigned_bone_names: + for bc in bones[name].collections: + if not FnBone.__is_mmd_tools_bone_collection(bc): + continue + if not FnBone.__is_normal_bone_collection(bc): + continue + bc.unassign(bones[name]) + + # remove unused bone groups + for bone_collection in bone_collections.values(): + if bone_collection.name in used_groups: + continue + if not FnBone.__is_mmd_tools_bone_collection(bone_collection): + continue + if not FnBone.__is_normal_bone_collection(bone_collection): + continue + bone_collections.remove(bone_collection) + + @staticmethod + def sync_display_item_frames_from_bone_collections(armature_object: bpy.types.Object): + armature: bpy.types.Armature = armature_object.data + bone_collections: bpy.types.BoneCollections = armature.collections + + from .model import FnModel + + root_object: bpy.types.Object = FnModel.find_root_object(armature_object) + mmd_root: MMDRoot = root_object.mmd_root + display_item_frames = mmd_root.display_item_frames + + used_frame_index: Set[int] = set() + + bone_collection: bpy.types.BoneCollection + for bone_collection in bone_collections: + if len(bone_collection.bones) == 0 or FnBone.__is_special_bone_collection(bone_collection): + continue + + bone_collection_name = bone_collection.name + display_item_frame: Optional[MMDDisplayItemFrame] = display_item_frames.get(bone_collection_name) + if display_item_frame is None: + display_item_frame = display_item_frames.add() + display_item_frame.name = bone_collection_name + display_item_frame.name_e = bone_collection_name + used_frame_index.add(display_item_frames.find(bone_collection_name)) + + ItemOp.resize(display_item_frame.data, len(bone_collection.bones)) + for display_item, bone in zip(display_item_frame.data, bone_collection.bones): + display_item.type = "BONE" + display_item.name = bone.name + + for i in reversed(range(len(display_item_frames))): + if i in used_frame_index: + continue + display_item_frame = display_item_frames[i] + if display_item_frame.is_special: + if display_item_frame.name != "表情": + display_item_frame.data.clear() + else: + display_item_frames.remove(i) + mmd_root.active_display_item_frame = 0 + + @staticmethod + def apply_bone_fixed_axis(armature_object: bpy.types.Object): bone_map = {} - for b in armature.pose.bones: + for b in armature_object.pose.bones: if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_fixed_axis: continue - mmd_bone = b.mmd_bone + mmd_bone: MMDBone = b.mmd_bone parent_tip = b.parent and not b.parent.is_mmd_shadow_bone and b.parent.mmd_bone.is_tip bone_map[b.name] = (mmd_bone.fixed_axis.normalized(), mmd_bone.is_tip, parent_tip) force_align = True - with bpyutils.edit_object(armature) as data: + with bpyutils.edit_object(armature_object) as data: + bone: bpy.types.EditBone for bone in data.edit_bones: if bone.name not in bone_map: bone.select = False @@ -110,7 +255,7 @@ def apply_bone_fixed_axis(cls, armature): axes = [bone.x_axis, bone.y_axis, bone.z_axis] direction = fixed_axis.normalized().xzy idx, val = max([(i, direction.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1])) - idx_1, idx_2 = (idx+1)%3, (idx+2)%3 + idx_1, idx_2 = (idx + 1) % 3, (idx + 2) % 3 axes[idx] = -direction if val < 0 else direction axes[idx_2] = axes[idx].cross(axes[idx_1]) axes[idx_1] = axes[idx_2].cross(axes[idx]) @@ -118,7 +263,7 @@ def apply_bone_fixed_axis(cls, armature): bone.use_connect = False bone.head = bone.parent.head if force_align: - tail = bone.head + axes[1].normalized()*bone.length + tail = bone.head + axes[1].normalized() * bone.length if is_tip or (tail - bone.tail).length > 1e-4: for c in bone.children: if c.use_connect: @@ -127,76 +272,75 @@ def apply_bone_fixed_axis(cls, armature): c.head = bone.head bone.tail = tail bone.align_roll(axes[2]) - bone_map[bone.name] = tuple(i!=idx for i in range(3)) + bone_map[bone.name] = tuple(i != idx for i in range(3)) else: bone_map[bone.name] = (True, True, True) bone.select = True for bone_name, locks in bone_map.items(): - b = armature.pose.bones[bone_name] + b = armature_object.pose.bones[bone_name] b.lock_location = (True, True, True) b.lock_ik_x, b.lock_ik_y, b.lock_ik_z = b.lock_rotation = locks - @classmethod - def load_bone_local_axes(cls, armature, enable=True): - for b in cls.get_selected_pose_bones(armature): - mmd_bone = b.mmd_bone + @staticmethod + def load_bone_local_axes(armature_object: bpy.types.Object, enable=True): + for b in FnBone.__get_selected_pose_bones(armature_object): + mmd_bone: MMDBone = b.mmd_bone mmd_bone.enabled_local_axes = enable if enable: axes = b.bone.matrix_local.to_3x3().transposed() mmd_bone.local_axis_x = axes[0].xzy mmd_bone.local_axis_z = axes[2].xzy - @classmethod - def apply_bone_local_axes(cls, armature): + @staticmethod + def apply_bone_local_axes(armature_object: bpy.types.Object): bone_map = {} - for b in armature.pose.bones: + for b in armature_object.pose.bones: if b.is_mmd_shadow_bone or not b.mmd_bone.enabled_local_axes: continue - mmd_bone = b.mmd_bone + mmd_bone: MMDBone = b.mmd_bone bone_map[b.name] = (mmd_bone.local_axis_x, mmd_bone.local_axis_z) - with bpyutils.edit_object(armature) as data: + with bpyutils.edit_object(armature_object) as data: + bone: bpy.types.EditBone for bone in data.edit_bones: if bone.name not in bone_map: bone.select = False continue local_axis_x, local_axis_z = bone_map[bone.name] - cls.update_bone_roll(bone, local_axis_x, local_axis_z) + FnBone.update_bone_roll(bone, local_axis_x, local_axis_z) bone.select = True - @classmethod - def update_bone_roll(cls, edit_bone, mmd_local_axis_x, mmd_local_axis_z): - axes = cls.get_axes(mmd_local_axis_x, mmd_local_axis_z) + @staticmethod + def update_bone_roll(edit_bone: bpy.types.EditBone, mmd_local_axis_x, mmd_local_axis_z): + axes = FnBone.get_axes(mmd_local_axis_x, mmd_local_axis_z) idx, val = max([(i, edit_bone.vector.dot(v)) for i, v in enumerate(axes)], key=lambda x: abs(x[1])) - edit_bone.align_roll(axes[(idx-1)%3 if val < 0 else (idx+1)%3]) + edit_bone.align_roll(axes[(idx - 1) % 3 if val < 0 else (idx + 1) % 3]) @staticmethod def get_axes(mmd_local_axis_x, mmd_local_axis_z): x_axis = Vector(mmd_local_axis_x).normalized().xzy z_axis = Vector(mmd_local_axis_z).normalized().xzy y_axis = z_axis.cross(x_axis).normalized() - z_axis = x_axis.cross(y_axis).normalized() # correction + z_axis = x_axis.cross(y_axis).normalized() # correction return (x_axis, y_axis, z_axis) - @classmethod - def apply_auto_bone_roll(cls, armature): + @staticmethod + def apply_auto_bone_roll(armature): bone_names = [] for b in armature.pose.bones: - if (not b.is_mmd_shadow_bone and - not b.mmd_bone.enabled_local_axes and - cls.has_auto_local_axis(b.mmd_bone.name_j)): + if not b.is_mmd_shadow_bone and not b.mmd_bone.enabled_local_axes and FnBone.has_auto_local_axis(b.mmd_bone.name_j): bone_names.append(b.name) with bpyutils.edit_object(armature) as data: + bone: bpy.types.EditBone for bone in data.edit_bones: if bone.name not in bone_names: - select = False continue - cls.update_auto_bone_roll(bone) + FnBone.update_auto_bone_roll(bone) bone.select = True - @classmethod - def update_auto_bone_roll(cls, edit_bone): + @staticmethod + def update_auto_bone_roll(edit_bone): # make a triangle face (p1,p2,p3) p1 = edit_bone.head.copy() p2 = edit_bone.tail.copy() @@ -212,56 +356,49 @@ def update_auto_bone_roll(cls, edit_bone): # calculate the normal vector of the face y = (p2 - p1).normalized() z_tmp = (p3 - p1).normalized() - x = y.cross(z_tmp) # normal vector + x = y.cross(z_tmp) # normal vector # z = x.cross(y) - cls.update_bone_roll(edit_bone, y.xzy, x.xzy) + FnBone.update_bone_roll(edit_bone, y.xzy, x.xzy) - @classmethod - def has_auto_local_axis(cls, name_j): + @staticmethod + def has_auto_local_axis(name_j): if name_j: - if (name_j in cls.AUTO_LOCAL_AXIS_ARMS or - name_j in cls.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS): + if name_j in FnBone.AUTO_LOCAL_AXIS_ARMS or name_j in FnBone.AUTO_LOCAL_AXIS_SEMI_STANDARD_ARMS: return True - for finger_name in cls.AUTO_LOCAL_AXIS_FINGERS: + for finger_name in FnBone.AUTO_LOCAL_AXIS_FINGERS: if finger_name in name_j: return True return False @staticmethod - def patch_rna_idprop(pose_bones): - if bpy.app.version < (2, 81, 0): # workaround for Rigify conflicts (fixed in Blender 2.81) - from rna_prop_ui import rna_idprop_ui_get - for b in pose_bones: - rna_idprop_ui_get(b, create=True) - - @classmethod - def clean_additional_transformation(cls, armature): + def clean_additional_transformation(armature_object: bpy.types.Object): # clean constraints - for p_bone in armature.pose.bones: + p_bone: bpy.types.PoseBone + for p_bone in armature_object.pose.bones: p_bone.mmd_bone.is_additional_transform_dirty = True constraints = p_bone.constraints - remove_constraint(constraints, 'mmd_additional_rotation') - remove_constraint(constraints, 'mmd_additional_location') - if remove_constraint(constraints, 'mmd_additional_parent'): + remove_constraint(constraints, "mmd_additional_rotation") + remove_constraint(constraints, "mmd_additional_location") + if remove_constraint(constraints, "mmd_additional_parent"): p_bone.bone.use_inherit_rotation = True # clean shadow bones shadow_bone_types = { - 'DUMMY', - 'SHADOW', - 'ADDITIONAL_TRANSFORM', - 'ADDITIONAL_TRANSFORM_INVERT', + "DUMMY", + "SHADOW", + "ADDITIONAL_TRANSFORM", + "ADDITIONAL_TRANSFORM_INVERT", } + def __is_at_shadow_bone(b): return b.is_mmd_shadow_bone and b.mmd_shadow_bone_type in shadow_bone_types - shadow_bone_names = [b.name for b in armature.pose.bones if __is_at_shadow_bone(b)] + + shadow_bone_names = [b.name for b in armature_object.pose.bones if __is_at_shadow_bone(b)] if len(shadow_bone_names) > 0: - with bpyutils.edit_object(armature) as data: + with bpyutils.edit_object(armature_object) as data: remove_edit_bones(data.edit_bones, shadow_bone_names) - cls.patch_rna_idprop(armature.pose.bones) - - @classmethod - def apply_additional_transformation(cls, armature): + @staticmethod + def apply_additional_transformation(armature_object: bpy.types.Object): def __is_dirty_bone(b): if b.is_mmd_shadow_bone: return False @@ -269,43 +406,43 @@ def __is_dirty_bone(b): if mmd_bone.has_additional_rotation or mmd_bone.has_additional_location: return True return mmd_bone.is_additional_transform_dirty - dirty_bones = [b for b in armature.pose.bones if __is_dirty_bone(b)] + + dirty_bones = [b for b in armature_object.pose.bones if __is_dirty_bone(b)] # setup constraints shadow_bone_pool = [] for p_bone in dirty_bones: - sb = cls.__setup_constraints(p_bone) + sb = FnBone.__setup_constraints(p_bone) if sb: shadow_bone_pool.append(sb) # setup shadow bones - with bpyutils.edit_object(armature) as data: + with bpyutils.edit_object(armature_object) as data: edit_bones = data.edit_bones for sb in shadow_bone_pool: sb.update_edit_bones(edit_bones) - pose_bones = armature.pose.bones + pose_bones = armature_object.pose.bones for sb in shadow_bone_pool: sb.update_pose_bones(pose_bones) # finish for p_bone in dirty_bones: p_bone.mmd_bone.is_additional_transform_dirty = False - cls.patch_rna_idprop(armature.pose.bones) - @classmethod - def __setup_constraints(cls, p_bone): + @staticmethod + def __setup_constraints(p_bone): bone_name = p_bone.name mmd_bone = p_bone.mmd_bone influence = mmd_bone.additional_transform_influence target_bone = mmd_bone.additional_transform_bone - mute_rotation = not mmd_bone.has_additional_rotation #or p_bone.is_in_ik_chain + mute_rotation = not mmd_bone.has_additional_rotation # or p_bone.is_in_ik_chain mute_location = not mmd_bone.has_additional_location constraints = p_bone.constraints if not target_bone or (mute_rotation and mute_location) or influence == 0: - rot = remove_constraint(constraints, 'mmd_additional_rotation') - loc = remove_constraint(constraints, 'mmd_additional_location') + rot = remove_constraint(constraints, "mmd_additional_rotation") + loc = remove_constraint(constraints, "mmd_additional_location") if rot or loc: return _AT_ShadowBoneRemove(bone_name) return None @@ -321,35 +458,37 @@ def __config(name, mute, map_type, value): shadow_bone.add_constraint(c) TransformConstraintOp.update_min_max(c, value, influence) - __config('mmd_additional_rotation', mute_rotation, 'ROTATION', pi) - __config('mmd_additional_location', mute_location, 'LOCATION', 100) + __config("mmd_additional_rotation", mute_rotation, "ROTATION", math.pi) + __config("mmd_additional_location", mute_location, "LOCATION", 100) return shadow_bone - def update_additional_transform_influence(self): - p_bone = self.__bone - influence = p_bone.mmd_bone.additional_transform_influence - constraints = p_bone.constraints - c = constraints.get('mmd_additional_rotation', None) - TransformConstraintOp.update_min_max(c, pi, influence) - c = constraints.get('mmd_additional_location', None) + @staticmethod + def update_additional_transform_influence(pose_bone: bpy.types.PoseBone): + influence = pose_bone.mmd_bone.additional_transform_influence + constraints = pose_bone.constraints + c = constraints.get("mmd_additional_rotation", None) + TransformConstraintOp.update_min_max(c, math.pi, influence) + c = constraints.get("mmd_additional_location", None) TransformConstraintOp.update_min_max(c, 100, influence) -class MigrationFnBone(object): + +class MigrationFnBone: """Migration Functions for old MMD models broken by bugs or issues""" - @classmethod - def fix_mmd_ik_limit_override(cls, armature_object: bpy.types.Object): + @staticmethod + def fix_mmd_ik_limit_override(armature_object: bpy.types.Object): pose_bone: bpy.types.PoseBone for pose_bone in armature_object.pose.bones: constraint: bpy.types.Constraint for constraint in pose_bone.constraints: - if constraint.type == 'LIMIT_ROTATION' and 'mmd_ik_limit_override' in constraint.name: - constraint.owner_space = 'LOCAL' + if constraint.type == "LIMIT_ROTATION" and "mmd_ik_limit_override" in constraint.name: + constraint.owner_space = "LOCAL" + class _AT_ShadowBoneRemove: def __init__(self, bone_name): - self.__shadow_bone_names = ('_dummy_' + bone_name, '_shadow_' + bone_name) + self.__shadow_bone_names = ("_dummy_" + bone_name, "_shadow_" + bone_name) def update_edit_bones(self, edit_bones): remove_edit_bones(edit_bones, self.__shadow_bone_names) @@ -357,10 +496,11 @@ def update_edit_bones(self, edit_bones): def update_pose_bones(self, pose_bones): pass + class _AT_ShadowBoneCreate: def __init__(self, bone_name, target_bone_name): - self.__dummy_bone_name = '_dummy_' + bone_name - self.__shadow_bone_name = '_shadow_' + bone_name + self.__dummy_bone_name = "_dummy_" + bone_name + self.__shadow_bone_name = "_shadow_" + bone_name self.__bone_name = bone_name self.__target_bone_name = target_bone_name self.__constraint_pool = [] @@ -384,22 +524,14 @@ def update_edit_bones(self, edit_bones): return dummy_bone_name = self.__dummy_bone_name - dummy = edit_bones.get(dummy_bone_name, None) - if dummy is None: - dummy = edit_bones.new(name=dummy_bone_name) - dummy.layers = [x == 9 for x in range(len(dummy.layers))] - dummy.use_deform = False + dummy = edit_bones.get(dummy_bone_name, None) or FnBone.set_edit_bone_to_dummy(edit_bones.new(name=dummy_bone_name)) dummy.parent = target_bone dummy.head = target_bone.head dummy.tail = dummy.head + bone.tail - bone.head dummy.roll = bone.roll shadow_bone_name = self.__shadow_bone_name - shadow = edit_bones.get(shadow_bone_name, None) - if shadow is None: - shadow = edit_bones.new(name=shadow_bone_name) - shadow.layers = [x == 8 for x in range(len(shadow.layers))] - shadow.use_deform = False + shadow = edit_bones.get(shadow_bone_name, None) or FnBone.set_edit_bone_to_shadow(edit_bones.new(name=shadow_bone_name)) shadow.parent = target_bone.parent shadow.head = dummy.head shadow.tail = dummy.tail @@ -412,19 +544,18 @@ def update_pose_bones(self, pose_bones): dummy_p_bone = pose_bones[self.__dummy_bone_name] dummy_p_bone.is_mmd_shadow_bone = True - dummy_p_bone.mmd_shadow_bone_type = 'DUMMY' + dummy_p_bone.mmd_shadow_bone_type = "DUMMY" shadow_p_bone = pose_bones[self.__shadow_bone_name] shadow_p_bone.is_mmd_shadow_bone = True - shadow_p_bone.mmd_shadow_bone_type = 'SHADOW' + shadow_p_bone.mmd_shadow_bone_type = "SHADOW" - if 'mmd_tools_at_dummy' not in shadow_p_bone.constraints: - c = shadow_p_bone.constraints.new('COPY_TRANSFORMS') - c.name = 'mmd_tools_at_dummy' + if "mmd_tools_at_dummy" not in shadow_p_bone.constraints: + c = shadow_p_bone.constraints.new("COPY_TRANSFORMS") + c.name = "mmd_tools_at_dummy" c.target = dummy_p_bone.id_data c.subtarget = dummy_p_bone.name - c.target_space = 'POSE' - c.owner_space = 'POSE' + c.target_space = "POSE" + c.owner_space = "POSE" self.__update_constraints() - diff --git a/mmd_tools/core/camera.py b/mmd_tools/core/camera.py index 3cdc734e..15608428 100644 --- a/mmd_tools/core/camera.py +++ b/mmd_tools/core/camera.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import math from typing import Optional import bpy -from mmd_tools.bpyutils import Props, SceneOp + +from ..bpyutils import FnContext, Props + class FnCamera: @staticmethod @@ -19,69 +23,70 @@ def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]: @staticmethod def is_mmd_camera(obj: bpy.types.Object) -> bool: - return obj.type == 'CAMERA' and FnCamera.find_root(obj.parent) is not None + return obj.type == "CAMERA" and FnCamera.find_root(obj.parent) is not None @staticmethod def is_mmd_camera_root(obj: bpy.types.Object) -> bool: - return obj.type == 'EMPTY' and obj.mmd_type == 'CAMERA' + return obj.type == "EMPTY" and obj.mmd_type == "CAMERA" @staticmethod def add_drivers(camera_object: bpy.types.Object): - def __add_driver(id_data: bpy.types.ID, data_path: str, expression: str, index:int = -1): + def __add_driver(id_data: bpy.types.ID, data_path: str, expression: str, index: int = -1): d = id_data.driver_add(data_path, index).driver - d.type = 'SCRIPTED' - if '$empty_distance' in expression: + d.type = "SCRIPTED" + if "$empty_distance" in expression: v = d.variables.new() - v.name = 'empty_distance' - v.type = 'TRANSFORMS' + v.name = "empty_distance" + v.type = "TRANSFORMS" v.targets[0].id = camera_object - v.targets[0].transform_type = 'LOC_Y' - v.targets[0].transform_space = 'LOCAL_SPACE' - expression = expression.replace('$empty_distance', v.name) - if '$is_perspective' in expression: + v.targets[0].transform_type = "LOC_Y" + v.targets[0].transform_space = "LOCAL_SPACE" + expression = expression.replace("$empty_distance", v.name) + if "$is_perspective" in expression: v = d.variables.new() - v.name = 'is_perspective' - v.type = 'SINGLE_PROP' - v.targets[0].id_type = 'OBJECT' + v.name = "is_perspective" + v.type = "SINGLE_PROP" + v.targets[0].id_type = "OBJECT" v.targets[0].id = camera_object.parent - v.targets[0].data_path = 'mmd_camera.is_perspective' - expression = expression.replace('$is_perspective', v.name) - if '$angle' in expression: + v.targets[0].data_path = "mmd_camera.is_perspective" + expression = expression.replace("$is_perspective", v.name) + if "$angle" in expression: v = d.variables.new() - v.name = 'angle' - v.type = 'SINGLE_PROP' - v.targets[0].id_type = 'OBJECT' + v.name = "angle" + v.type = "SINGLE_PROP" + v.targets[0].id_type = "OBJECT" v.targets[0].id = camera_object.parent - v.targets[0].data_path = 'mmd_camera.angle' - expression = expression.replace('$angle', v.name) - if '$sensor_height' in expression: + v.targets[0].data_path = "mmd_camera.angle" + expression = expression.replace("$angle", v.name) + if "$sensor_height" in expression: v = d.variables.new() - v.name = 'sensor_height' - v.type = 'SINGLE_PROP' - v.targets[0].id_type = 'CAMERA' + v.name = "sensor_height" + v.type = "SINGLE_PROP" + v.targets[0].id_type = "CAMERA" v.targets[0].id = camera_object.data - v.targets[0].data_path = 'sensor_height' - expression = expression.replace('$sensor_height', v.name) + v.targets[0].data_path = "sensor_height" + expression = expression.replace("$sensor_height", v.name) d.expression = expression - __add_driver(camera_object.data, 'ortho_scale', '25*abs($empty_distance)/45') - __add_driver(camera_object, 'rotation_euler', 'pi if $is_perspective == False and $empty_distance > 1e-5 else 0', index=1) - __add_driver(camera_object.data, 'type', 'not $is_perspective') - __add_driver(camera_object.data, 'lens', '$sensor_height/tan($angle/2)/2') + __add_driver(camera_object.data, "ortho_scale", "25*abs($empty_distance)/45") + __add_driver(camera_object, "rotation_euler", "pi if $is_perspective == False and $empty_distance > 1e-5 else 0", index=1) + __add_driver(camera_object.data, "type", "not $is_perspective") + __add_driver(camera_object.data, "lens", "$sensor_height/tan($angle/2)/2") @staticmethod def remove_drivers(camera_object: bpy.types.Object): - camera_object.data.driver_remove('ortho_scale') - camera_object.driver_remove('rotation_euler') - camera_object.data.driver_remove('ortho_scale') - camera_object.data.driver_remove('lens') + camera_object.data.driver_remove("ortho_scale") + camera_object.driver_remove("rotation_euler") + camera_object.data.driver_remove("ortho_scale") + camera_object.data.driver_remove("lens") + class MigrationFnCamera: @staticmethod def update_mmd_camera(): for camera_object in bpy.data.objects: - if camera_object.type != 'CAMERA': + if camera_object.type != "CAMERA": continue root_object = FnCamera.find_root(camera_object) @@ -92,13 +97,14 @@ def update_mmd_camera(): FnCamera.remove_drivers(camera_object) FnCamera.add_drivers(camera_object) + class MMDCamera: def __init__(self, obj): root_object = FnCamera.find_root(obj) if root_object is None: - raise ValueError('%s is not MMDCamera'%str(obj)) + raise ValueError("%s is not MMDCamera" % str(obj)) - self.__emptyObj = getattr(root_object, 'original', obj) + self.__emptyObj = getattr(root_object, "original", obj) @staticmethod def isMMDCamera(obj: bpy.types.Object) -> bool: @@ -110,7 +116,7 @@ def addDrivers(cameraObj: bpy.types.Object): @staticmethod def removeDrivers(cameraObj: bpy.types.Object): - if cameraObj.type != 'CAMERA': + if cameraObj.type != "CAMERA": return FnCamera.remove_drivers(cameraObj) @@ -119,17 +125,17 @@ def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0): if FnCamera.is_mmd_camera(cameraObj): return MMDCamera(cameraObj) - empty = bpy.data.objects.new(name='MMD_Camera', object_data=None) - SceneOp(bpy.context).link_object(empty) + empty = bpy.data.objects.new(name="MMD_Camera", object_data=None) + FnContext.link_object(FnContext.ensure_context(), empty) cameraObj.parent = empty - cameraObj.data.sensor_fit = 'VERTICAL' - cameraObj.data.lens_unit = 'MILLIMETERS' # MILLIMETERS, FOV - cameraObj.data.ortho_scale = 25*scale - cameraObj.data.clip_end = 500*scale - setattr(cameraObj.data, Props.display_size, 5*scale) - cameraObj.location = (0, -45*scale, 0) - cameraObj.rotation_mode = 'XYZ' + cameraObj.data.sensor_fit = "VERTICAL" + cameraObj.data.lens_unit = "MILLIMETERS" # MILLIMETERS, FOV + cameraObj.data.ortho_scale = 25 * scale + cameraObj.data.clip_end = 500 * scale + setattr(cameraObj.data, Props.display_size, 5 * scale) + cameraObj.location = (0, -45 * scale, 0) + cameraObj.rotation_mode = "XYZ" cameraObj.rotation_euler = (math.radians(90), 0, 0) cameraObj.lock_location = (True, False, True) cameraObj.lock_rotation = (True, True, True) @@ -137,11 +143,11 @@ def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0): cameraObj.data.dof.focus_object = empty FnCamera.add_drivers(cameraObj) - empty.location = (0, 0, 10*scale) - empty.rotation_mode = 'YXZ' - setattr(empty, Props.empty_display_size, 5*scale) + empty.location = (0, 0, 10 * scale) + empty.rotation_mode = "YXZ" + setattr(empty, Props.empty_display_size, 5 * scale) empty.lock_scale = (True, True, True) - empty.mmd_type = 'CAMERA' + empty.mmd_type = "CAMERA" empty.mmd_camera.angle = math.radians(30) empty.mmd_camera.persp = True return MMDCamera(empty) @@ -149,8 +155,8 @@ def convertToMMDCamera(cameraObj: bpy.types.Object, scale=1.0): @staticmethod def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance=0.1): scene = bpy.context.scene - mmd_cam = bpy.data.objects.new(name='Camera', object_data=bpy.data.cameras.new('Camera')) - SceneOp(bpy.context).link_object(mmd_cam) + mmd_cam = bpy.data.objects.new(name="Camera", object_data=bpy.data.cameras.new("Camera")) + FnContext.link_object(FnContext.ensure_context(), mmd_cam) MMDCamera.convertToMMDCamera(mmd_cam, scale=scale) mmd_cam_root = mmd_cam.parent @@ -167,30 +173,29 @@ def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance= action_name = mmd_cam_root.name parent_action = bpy.data.actions.new(name=action_name) - distance_action = bpy.data.actions.new(name=action_name+'_dis') + distance_action = bpy.data.actions.new(name=action_name + "_dis") FnCamera.remove_drivers(mmd_cam) from math import atan from mathutils import Matrix, Vector - from mmd_tools.bpyutils import matmul render = scene.render - factor = (render.resolution_y*render.pixel_aspect_y)/(render.resolution_x*render.pixel_aspect_x) - matrix_rotation = Matrix(([1,0,0,0], [0,0,1,0], [0,-1,0,0], [0,0,0,1])) - neg_z_vector = Vector((0,0,-1)) - frame_start, frame_end, frame_current = scene.frame_start, scene.frame_end+1, scene.frame_current + factor = (render.resolution_y * render.pixel_aspect_y) / (render.resolution_x * render.pixel_aspect_x) + matrix_rotation = Matrix(([1, 0, 0, 0], [0, 0, 1, 0], [0, -1, 0, 0], [0, 0, 0, 1])) + neg_z_vector = Vector((0, 0, -1)) + frame_start, frame_end, frame_current = scene.frame_start, scene.frame_end + 1, scene.frame_current frame_count = frame_end - frame_start frames = range(frame_start, frame_end) fcurves = [] for i in range(3): - fcurves.append(parent_action.fcurves.new(data_path='location', index=i)) # x, y, z + fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z for i in range(3): - fcurves.append(parent_action.fcurves.new(data_path='rotation_euler', index=i)) # rx, ry, rz - fcurves.append(parent_action.fcurves.new(data_path='mmd_camera.angle')) # fov - fcurves.append(parent_action.fcurves.new(data_path='mmd_camera.is_perspective')) # persp - fcurves.append(distance_action.fcurves.new(data_path='location', index=1)) # dis + fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz + fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov + fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp + fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis for c in fcurves: c.keyframe_points.add(frame_count) @@ -202,36 +207,36 @@ def newMMDCameraAnimation(cameraObj, cameraTarget=None, scale=1.0, min_distance= cameraTarget = _target_override_func(cameraObj) cam_matrix_world = cameraObj.matrix_world cam_target_loc = cameraTarget.matrix_world.translation - cam_rotation = matmul(cam_matrix_world, matrix_rotation).to_euler(mmd_cam_root.rotation_mode) - cam_vec = matmul(cam_matrix_world.to_3x3(), neg_z_vector) - if cameraObj.data.type == 'ORTHO': - cam_dis = -(9/5) * cameraObj.data.ortho_scale - if cameraObj.data.sensor_fit != 'VERTICAL': - if cameraObj.data.sensor_fit == 'HORIZONTAL': + cam_rotation = (cam_matrix_world @ matrix_rotation).to_euler(mmd_cam_root.rotation_mode) + cam_vec = cam_matrix_world.to_3x3() @ neg_z_vector + if cameraObj.data.type == "ORTHO": + cam_dis = -(9 / 5) * cameraObj.data.ortho_scale + if cameraObj.data.sensor_fit != "VERTICAL": + if cameraObj.data.sensor_fit == "HORIZONTAL": cam_dis *= factor else: cam_dis *= min(1, factor) else: target_vec = cam_target_loc - cam_matrix_world.translation cam_dis = -max(target_vec.length * cam_vec.dot(target_vec.normalized()), min_distance) - cam_target_loc = cam_matrix_world.translation - cam_vec*cam_dis + cam_target_loc = cam_matrix_world.translation - cam_vec * cam_dis - tan_val = cameraObj.data.sensor_height/cameraObj.data.lens/2 - if cameraObj.data.sensor_fit != 'VERTICAL': - ratio = cameraObj.data.sensor_width/cameraObj.data.sensor_height - if cameraObj.data.sensor_fit == 'HORIZONTAL': - tan_val *= factor*ratio - else: # cameraObj.data.sensor_fit == 'AUTO' - tan_val *= min(ratio, factor*ratio) + tan_val = cameraObj.data.sensor_height / cameraObj.data.lens / 2 + if cameraObj.data.sensor_fit != "VERTICAL": + ratio = cameraObj.data.sensor_width / cameraObj.data.sensor_height + if cameraObj.data.sensor_fit == "HORIZONTAL": + tan_val *= factor * ratio + else: # cameraObj.data.sensor_fit == 'AUTO' + tan_val *= min(ratio, factor * ratio) x.co, y.co, z.co = ((f, i) for i in cam_target_loc) rx.co, ry.co, rz.co = ((f, i) for i in cam_rotation) dis.co = (f, cam_dis) - fov.co = (f, 2*atan(tan_val)) - persp.co = (f, cameraObj.data.type != 'ORTHO') - persp.interpolation = 'CONSTANT' + fov.co = (f, 2 * atan(tan_val)) + persp.co = (f, cameraObj.data.type != "ORTHO") + persp.interpolation = "CONSTANT" for kp in (x, y, z, rx, ry, rz, fov, dis): - kp.interpolation = 'LINEAR' + kp.interpolation = "LINEAR" FnCamera.add_drivers(mmd_cam) mmd_cam_root.animation_data_create().action = parent_action @@ -244,6 +249,6 @@ def object(self): def camera(self): for i in self.__emptyObj.children: - if i.type == 'CAMERA': + if i.type == "CAMERA": return i - raise Exception + raise KeyError diff --git a/mmd_tools/core/exceptions.py b/mmd_tools/core/exceptions.py index e715f1c1..5c764471 100644 --- a/mmd_tools/core/exceptions.py +++ b/mmd_tools/core/exceptions.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- +# Copyright 2016 MMD Tools authors +# This file is part of MMD Tools. # Module for custom exceptions -class MaterialNotFoundError(Exception): - pass -class DivisionError(Exception): - pass +class MaterialNotFoundError(KeyError): + """Exception raised when a material is not found in the scene""" + + def __init__(self, *args: object) -> None: + """Constructor for MaterialNotFoundError""" + super().__init__(*args) diff --git a/mmd_tools/core/lamp.py b/mmd_tools/core/lamp.py index 18610a42..10593d31 100644 --- a/mmd_tools/core/lamp.py +++ b/mmd_tools/core/lamp.py @@ -1,56 +1,58 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import bpy -from mmd_tools.bpyutils import Props, SceneOp +from ..bpyutils import FnContext, Props + class MMDLamp: def __init__(self, obj): if MMDLamp.isLamp(obj): obj = obj.parent - if obj and obj.type == 'EMPTY' and obj.mmd_type == 'LIGHT': + if obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT": self.__emptyObj = obj else: - raise ValueError('%s is not MMDLamp'%str(obj)) - + raise ValueError("%s is not MMDLamp" % str(obj)) @staticmethod def isLamp(obj): - return obj and obj.type in {'LIGHT', 'LAMP'} + return obj and obj.type in {"LIGHT", "LAMP"} @staticmethod def isMMDLamp(obj): if MMDLamp.isLamp(obj): obj = obj.parent - return obj and obj.type == 'EMPTY' and obj.mmd_type == 'LIGHT' + return obj and obj.type == "EMPTY" and obj.mmd_type == "LIGHT" @staticmethod def convertToMMDLamp(lampObj, scale=1.0): if MMDLamp.isMMDLamp(lampObj): return MMDLamp(lampObj) - empty = bpy.data.objects.new(name='MMD_Light', object_data=None) - SceneOp(bpy.context).link_object(empty) + empty = bpy.data.objects.new(name="MMD_Light", object_data=None) + FnContext.link_object(FnContext.ensure_context(), empty) - empty.rotation_mode = 'XYZ' + empty.rotation_mode = "XYZ" empty.lock_rotation = (True, True, True) setattr(empty, Props.empty_display_size, 0.4) - empty.scale = [10*scale] * 3 - empty.mmd_type = 'LIGHT' - empty.location = (0, 0, 11*scale) + empty.scale = [10 * scale] * 3 + empty.mmd_type = "LIGHT" + empty.location = (0, 0, 11 * scale) lampObj.parent = empty lampObj.data.color = (0.602, 0.602, 0.602) lampObj.location = (0.5, -0.5, 1.0) - lampObj.rotation_mode = 'XYZ' + lampObj.rotation_mode = "XYZ" lampObj.rotation_euler = (0, 0, 0) lampObj.lock_rotation = (True, True, True) - constraint = lampObj.constraints.new(type='TRACK_TO') - constraint.name = 'mmd_lamp_track' + constraint = lampObj.constraints.new(type="TRACK_TO") + constraint.name = "mmd_lamp_track" constraint.target = empty - constraint.track_axis = 'TRACK_NEGATIVE_Z' - constraint.up_axis = 'UP_Y' + constraint.track_axis = "TRACK_NEGATIVE_Z" + constraint.up_axis = "UP_Y" return MMDLamp(empty) @@ -61,5 +63,4 @@ def lamp(self): for i in self.__emptyObj.children: if MMDLamp.isLamp(i): return i - raise Exception - + raise KeyError diff --git a/mmd_tools/core/material.py b/mmd_tools/core/material.py index ff2c2d91..2eb75ed6 100644 --- a/mmd_tools/core/material.py +++ b/mmd_tools/core/material.py @@ -1,101 +1,130 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import logging import os -from typing import Optional +from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, cast import bpy -from mmd_tools.bpyutils import addon_preferences -from mmd_tools.core.exceptions import MaterialNotFoundError -from mmd_tools.core.shader import _NodeGroupUtils +from mathutils import Vector -SPHERE_MODE_OFF = 0 -SPHERE_MODE_MULT = 1 -SPHERE_MODE_ADD = 2 +from ..bpyutils import FnContext +from .exceptions import MaterialNotFoundError +from .shader import _NodeGroupUtils + +if TYPE_CHECKING: + from ..properties.material import MMDMaterial + +# TODO: use enum instead of constants +SPHERE_MODE_OFF = 0 +SPHERE_MODE_MULT = 1 +SPHERE_MODE_ADD = 2 SPHERE_MODE_SUBTEX = 3 -# SUPPORT_UNTIL: 3.3LTS -class _FnMaterialBI: - __BASE_TEX_SLOT = 0 - __TOON_TEX_SLOT = 1 - __SPHERE_TEX_SLOT = 2 - __SPHERE_ALPHA_SLOT = 5 - __NODES_ARE_READONLY = False +class _DummyTexture: + def __init__(self, image): + self.type = "IMAGE" + self.image = image + self.use_mipmap = True + + +class _DummyTextureSlot: + def __init__(self, image): + self.diffuse_color_factor = 1 + self.uv_layer = "" + self.texture = _DummyTexture(image) + + +class FnMaterial: + __NODES_ARE_READONLY: bool = False - def __init__(self, material=None): + def __init__(self, material: bpy.types.Material): self.__material = material - self._nodes_are_readonly = _FnMaterialBI.__NODES_ARE_READONLY + self._nodes_are_readonly = FnMaterial.__NODES_ARE_READONLY - # SUPPORT_UNTIL: 3.3LTS - @classmethod - def set_nodes_are_readonly(cls, nodes_are_readonly): - _FnMaterialBI.__NODES_ARE_READONLY = nodes_are_readonly + @staticmethod + def set_nodes_are_readonly(nodes_are_readonly: bool): + FnMaterial.__NODES_ARE_READONLY = nodes_are_readonly @classmethod - def from_material_id(cls, material_id): + def from_material_id(cls, material_id: str): for material in bpy.data.materials: if material.mmd_material.material_id == material_id: return cls(material) return None - @classmethod - def clean_materials(cls, obj, can_remove): + @staticmethod + def clean_materials(obj, can_remove: Callable[[bpy.types.Material], bool]): materials = obj.data.materials - materials_pop = (lambda index: materials.pop(index=index, update_data=True)) if bpy.app.version < (2, 81, 0) else materials.pop + materials_pop = materials.pop for i in sorted((x for x, m in enumerate(materials) if can_remove(m)), reverse=True): m = materials_pop(index=i) if m.users < 1: bpy.data.materials.remove(m) - @classmethod - def swap_materials(cls, meshObj, mat1_ref, mat2_ref, reverse=False, - swap_slots=False): + @staticmethod + def swap_materials(mesh_object: bpy.types.Object, mat1_ref: str | int, mat2_ref: str | int, reverse=False, swap_slots=False) -> Tuple[bpy.types.Material, bpy.types.Material]: """ This method will assign the polygons of mat1 to mat2. If reverse is True it will also swap the polygons assigned to mat2 to mat1. The reference to materials can be indexes or names Finally it will also swap the material slots if the option is given. + + Args: + mesh_object (bpy.types.Object): The mesh object + mat1_ref (str | int): The reference to the first material + mat2_ref (str | int): The reference to the second material + reverse (bool, optional): If true it will also swap the polygons assigned to mat2 to mat1. Defaults to False. + swap_slots (bool, optional): If true it will also swap the material slots. Defaults to False. + + Retruns: + Tuple[bpy.types.Material, bpy.types.Material]: The swapped materials + + Raises: + MaterialNotFoundError: If one of the materials is not found """ + mesh = cast(bpy.types.Mesh, mesh_object.data) try: # Try to find the materials - mat1 = meshObj.data.materials[mat1_ref] - mat2 = meshObj.data.materials[mat2_ref] + mat1 = mesh.materials[mat1_ref] + mat2 = mesh.materials[mat2_ref] if None in (mat1, mat2): raise MaterialNotFoundError() - except (KeyError, IndexError): + except (KeyError, IndexError) as exc: # Wrap exceptions within our custom ones - raise MaterialNotFoundError() - mat1_idx = meshObj.data.materials.find(mat1.name) - mat2_idx = meshObj.data.materials.find(mat2.name) - if 1: #with select_object(meshObj): - # Swap polygons - for poly in meshObj.data.polygons: - if poly.material_index == mat1_idx: - poly.material_index = mat2_idx - elif reverse and poly.material_index == mat2_idx: - poly.material_index = mat1_idx - # Swap slots if specified - if swap_slots: - meshObj.material_slots[mat1_idx].material = mat2 - meshObj.material_slots[mat2_idx].material = mat1 + raise MaterialNotFoundError() from exc + mat1_idx = mesh.materials.find(mat1.name) + mat2_idx = mesh.materials.find(mat2.name) + # Swap polygons + for poly in mesh.polygons: + if poly.material_index == mat1_idx: + poly.material_index = mat2_idx + elif reverse and poly.material_index == mat2_idx: + poly.material_index = mat1_idx + # Swap slots if specified + if swap_slots: + mesh_object.material_slots[mat1_idx].material = mat2 + mesh_object.material_slots[mat2_idx].material = mat1 return mat1, mat2 - @classmethod - def fixMaterialOrder(cls, meshObj, material_names): + @staticmethod + def fixMaterialOrder(meshObj: bpy.types.Object, material_names: Iterable[str]): """ This method will fix the material order. Which is lost after joining meshes. """ + materials = cast(bpy.types.Mesh, meshObj.data).materials for new_idx, mat in enumerate(material_names): # Get the material that is currently on this index - other_mat = meshObj.data.materials[new_idx] + other_mat = materials[new_idx] if other_mat.name == mat: continue # This is already in place - cls.swap_materials(meshObj, mat, new_idx, reverse=True, swap_slots=True) + FnMaterial.swap_materials(meshObj, mat, new_idx, reverse=True, swap_slots=True) @property def material_id(self): - mmd_mat = self.__material.mmd_material + mmd_mat: MMDMaterial = self.__material.mmd_material if mmd_mat.material_id < 0: max_id = -1 for mat in bpy.data.materials: @@ -107,12 +136,13 @@ def material_id(self): def material(self): return self.__material - def __same_image_file(self, image, filepath): - if image and image.source == 'FILE': - img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user() + if image and image.source == "FILE": + # pylint: disable=assignment-from-no-return + img_filepath = bpy.path.abspath(image.filepath) # image.filepath_from_user() if img_filepath == filepath: return True + # pylint: disable=bare-except try: return os.path.samefile(img_filepath, filepath) except: @@ -122,278 +152,42 @@ def __same_image_file(self, image, filepath): def _load_image(self, filepath): img = next((i for i in bpy.data.images if self.__same_image_file(i, filepath)), None) if img is None: + # pylint: disable=bare-except try: img = bpy.data.images.load(filepath) except: - logging.warning('Cannot create a texture for %s. No such file.', filepath) + logging.warning("Cannot create a texture for %s. No such file.", filepath) img = bpy.data.images.new(os.path.basename(filepath), 1, 1) - img.source = 'FILE' + img.source = "FILE" img.filepath = filepath - use_alpha = (img.depth == 32 and img.file_format != 'BMP') - if hasattr(img, 'use_alpha'): + use_alpha = img.depth == 32 and img.file_format != "BMP" + if hasattr(img, "use_alpha"): img.use_alpha = use_alpha elif not use_alpha: - img.alpha_mode = 'NONE' + img.alpha_mode = "NONE" return img - def __load_texture(self, filepath): - tex = next((t for t in bpy.data.textures if t.type == 'IMAGE' and self.__same_image_file(t.image, filepath)), None) - if tex is None: - tex = bpy.data.textures.new(name=bpy.path.display_name_from_filepath(filepath), type='IMAGE') - tex.image = self._load_image(filepath) - tex.use_alpha = tex.image.use_alpha - return tex - - def __has_alpha_channel(self, texture): - return texture.type == 'IMAGE' and getattr(texture.image, 'use_alpha', False) - - - def get_texture(self): - return self.__get_texture(self.__BASE_TEX_SLOT) - - def __get_texture(self, index): - texture_slot = self.__material.texture_slots[index] - return texture_slot.texture if texture_slot else None - - def __use_texture(self, index, use_tex): - texture_slot = self.__material.texture_slots[index] - if texture_slot: - texture_slot.use = use_tex - - def create_texture(self, filepath): - """ create a texture slot for textures of MMD models. - - Args: - material: the material object to add a texture_slot - filepath: the file path to texture. - - Returns: - bpy.types.MaterialTextureSlot object - """ - texture_slot = self.__material.texture_slots.create(self.__BASE_TEX_SLOT) - texture_slot.texture_coords = 'UV' - texture_slot.blend_type = 'MULTIPLY' - texture_slot.texture = self.__load_texture(filepath) - texture_slot.use_map_alpha = self.__has_alpha_channel(texture_slot.texture) - return texture_slot - - def remove_texture(self): - if self._nodes_are_readonly: - return - self.__remove_texture(self.__BASE_TEX_SLOT) - - def __remove_texture(self, index): - texture_slot = self.__material.texture_slots[index] - if texture_slot: - tex = texture_slot.texture - self.__material.texture_slots.clear(index) - #logging.debug('clear texture: %s users: %d', tex.name, tex.users) - if tex and tex.users < 1 and tex.type == 'IMAGE': - #logging.debug(' - remove texture: %s', tex.name) - img = tex.image - tex.image = None - bpy.data.textures.remove(tex) - if img and img.users < 1: - #logging.debug(' - remove image: %s', img.name) - bpy.data.images.remove(img) - - - def get_sphere_texture(self): - return self.__get_texture(self.__SPHERE_TEX_SLOT) - - def use_sphere_texture(self, use_sphere, obj=None): - if self._nodes_are_readonly: - return - if use_sphere: - self.update_sphere_texture_type(obj) - else: - self.__use_texture(self.__SPHERE_TEX_SLOT, use_sphere) - self.__use_texture(self.__SPHERE_ALPHA_SLOT, use_sphere) - - def create_sphere_texture(self, filepath, obj=None): - """ create a texture slot for environment mapping textures of MMD models. - - Args: - material: the material object to add a texture_slot - filepath: the file path to environment mapping texture. - - Returns: - bpy.types.MaterialTextureSlot object - """ - texture_slot = self.__material.texture_slots.create(self.__SPHERE_TEX_SLOT) - texture_slot.texture_coords = 'NORMAL' - texture_slot.texture = self.__load_texture(filepath) - self.update_sphere_texture_type(obj) - return texture_slot - - def update_sphere_texture_type(self, obj=None): - if self._nodes_are_readonly: - return - - texture_slot = self.__material.texture_slots[self.__SPHERE_TEX_SLOT] - if not texture_slot: - self.__remove_texture(self.__SPHERE_ALPHA_SLOT) - return - - sphere_texture_type = int(self.__material.mmd_material.sphere_texture_type) - if sphere_texture_type not in (1, 2, 3): - texture_slot.use = False - else: - texture_slot.use = True - texture_slot.blend_type = ('MULTIPLY', 'ADD', 'MULTIPLY')[sphere_texture_type-1] - if sphere_texture_type == 3: - texture_slot.texture_coords = 'UV' - if obj and obj.type == 'MESH' and self.__material in tuple(obj.data.materials): - uv_layers = (l for l in obj.data.uv_layers if not l.name.startswith('_')) - next(uv_layers, None) # skip base UV - texture_slot.uv_layer = getattr(next(uv_layers, None), 'name', '') - else: - texture_slot.texture_coords = 'NORMAL' - - if not texture_slot.use or not self.__has_alpha_channel(texture_slot.texture): - self.__remove_texture(self.__SPHERE_ALPHA_SLOT) - return - - alpha_slot = self.__material.texture_slots[self.__SPHERE_ALPHA_SLOT] - if not alpha_slot: - alpha_slot = self.__material.texture_slots.create(self.__SPHERE_ALPHA_SLOT) - alpha_slot.use_map_color_diffuse = False - alpha_slot.use_map_alpha = True - alpha_slot.blend_type = 'MULTIPLY' - alpha_slot.texture = texture_slot.texture - alpha_slot.alpha_factor = texture_slot.diffuse_color_factor - alpha_slot.use = texture_slot.use - alpha_slot.texture_coords = texture_slot.texture_coords - alpha_slot.uv_layer = texture_slot.uv_layer - - def remove_sphere_texture(self): - if self._nodes_are_readonly: - return - self.__remove_texture(self.__SPHERE_TEX_SLOT) - self.__remove_texture(self.__SPHERE_ALPHA_SLOT) - - - def get_toon_texture(self): - return self.__get_texture(self.__TOON_TEX_SLOT) - - def use_toon_texture(self, use_toon): - if self._nodes_are_readonly: - return - self.__use_texture(self.__TOON_TEX_SLOT, use_toon) - - def create_toon_texture(self, filepath): - """ create a texture slot for toon textures of MMD models. - - Args: - material: the material object to add a texture_slot - filepath: the file path to toon texture. - - Returns: - bpy.types.MaterialTextureSlot object - """ - texture_slot = self.__material.texture_slots.create(self.__TOON_TEX_SLOT) - texture_slot.texture_coords = 'NORMAL' - texture_slot.blend_type = 'MULTIPLY' - texture_slot.texture = self.__load_texture(filepath) - texture_slot.use_map_alpha = self.__has_alpha_channel(texture_slot.texture) - return texture_slot - def update_toon_texture(self): if self._nodes_are_readonly: return - mmd_mat = self.__material.mmd_material + mmd_mat: MMDMaterial = self.__material.mmd_material if mmd_mat.is_shared_toon_texture: - shared_toon_folder = addon_preferences('shared_toon_folder', '') - toon_path = os.path.join(shared_toon_folder, 'toon%02d.bmp'%(mmd_mat.shared_toon_texture+1)) + shared_toon_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "shared_toon_folder", "") + toon_path = os.path.join(shared_toon_folder, "toon%02d.bmp" % (mmd_mat.shared_toon_texture + 1)) self.create_toon_texture(bpy.path.resolve_ncase(path=toon_path)) - elif mmd_mat.toon_texture != '': + elif mmd_mat.toon_texture != "": self.create_toon_texture(mmd_mat.toon_texture) else: self.remove_toon_texture() - def remove_toon_texture(self): - if self._nodes_are_readonly: - return - self.__remove_texture(self.__TOON_TEX_SLOT) - - - def _mixDiffuseAndAmbient(self, mmd_mat): + def _mix_diffuse_and_ambient(self, mmd_mat): r, g, b = mmd_mat.diffuse_color ar, ag, ab = mmd_mat.ambient_color - return [min(1.0,0.5*r+ar), min(1.0,0.5*g+ag), min(1.0,0.5*b+ab)] - - def update_ambient_color(self): - if self._nodes_are_readonly: - return - self.update_diffuse_color() - - def update_diffuse_color(self): - if self._nodes_are_readonly: - return - mat = self.__material - mmd_mat = mat.mmd_material - mat.diffuse_color[:3] = self._mixDiffuseAndAmbient(mmd_mat) - mat.diffuse_intensity = 0.8 - - def update_alpha(self): - if self._nodes_are_readonly: - return - mat = self.__material - mmd_mat = mat.mmd_material - mat.alpha = mmd_mat.alpha - mat.specular_alpha = mmd_mat.alpha - mat.use_transparency = True - mat.transparency_method = 'Z_TRANSPARENCY' - mat.game_settings.alpha_blend = 'ALPHA' - self.update_self_shadow_map() - - def update_specular_color(self): - if self._nodes_are_readonly: - return - mat = self.__material - mmd_mat = mat.mmd_material - mat.specular_color = mmd_mat.specular_color - mat.specular_shader = 'PHONG' - mat.specular_intensity = 0.8 - - def update_shininess(self): - if self._nodes_are_readonly: - return - mat = self.__material - mmd_mat = mat.mmd_material - shininess = mmd_mat.shininess - mat.specular_hardness = shininess - - def update_is_double_sided(self): - if self._nodes_are_readonly: - return - mat = self.__material - mmd_mat = mat.mmd_material - mat.game_settings.use_backface_culling = not mmd_mat.is_double_sided + return [min(1.0, 0.5 * r + ar), min(1.0, 0.5 * g + ag), min(1.0, 0.5 * b + ab)] def update_drop_shadow(self): pass - def update_self_shadow_map(self): - if self._nodes_are_readonly: - return - mat = self.__material - mmd_mat = mat.mmd_material - cast_shadows = mmd_mat.enabled_self_shadow_map if mat.alpha > 1e-3 else False - mat.use_cast_buffer_shadows = cast_shadows # only buffer shadows - if hasattr(mat, 'use_cast_shadows'): - # "use_cast_shadows" is not supported in older Blender (< 2.71), - # so we still use "use_cast_buffer_shadows". - mat.use_cast_shadows = cast_shadows - - def update_self_shadow(self): - if self._nodes_are_readonly: - return - mat = self.__material - mmd_mat = mat.mmd_material - mat.use_shadows = mmd_mat.enabled_self_shadow - mat.use_transparent_shadows = mmd_mat.enabled_self_shadow - def update_enabled_toon_edge(self): if self._nodes_are_readonly: return @@ -403,76 +197,41 @@ def update_edge_color(self): if self._nodes_are_readonly: return mat = self.__material - mmd_mat = mat.mmd_material + mmd_mat: MMDMaterial = mat.mmd_material color, alpha = mmd_mat.edge_color[:3], mmd_mat.edge_color[3] line_color = color + (min(alpha, int(mmd_mat.enabled_toon_edge)),) - if hasattr(mat, 'line_color'): # freestyle line color + if hasattr(mat, "line_color"): # freestyle line color mat.line_color = line_color - mat_edge = bpy.data.materials.get('mmd_edge.'+mat.name, None) + mat_edge: bpy.types.Material = bpy.data.materials.get("mmd_edge." + mat.name, None) if mat_edge: mat_edge.mmd_material.edge_color = line_color - if mat.name.startswith('mmd_edge.') and mat.node_tree: + if mat.name.startswith("mmd_edge.") and mat.node_tree: mmd_mat.ambient_color, mmd_mat.alpha = color, alpha - node_shader = mat.node_tree.nodes.get('mmd_edge_preview', None) - if node_shader and 'Color' in node_shader.inputs: - node_shader.inputs['Color'].default_value = mmd_mat.edge_color - if node_shader and 'Alpha' in node_shader.inputs: - node_shader.inputs['Alpha'].default_value = alpha + node_shader = mat.node_tree.nodes.get("mmd_edge_preview", None) + if node_shader and "Color" in node_shader.inputs: + node_shader.inputs["Color"].default_value = mmd_mat.edge_color + if node_shader and "Alpha" in node_shader.inputs: + node_shader.inputs["Alpha"].default_value = alpha def update_edge_weight(self): pass - @staticmethod - def convert_to_mmd_material(material, context=bpy.context): - m, mmd_material = material, material.mmd_material - - map_diffuse = next((s.blend_type for s in m.texture_slots if s and s.use_map_color_diffuse), None) - use_diffuse = map_diffuse in {None, 'MULTIPLY'} - diffuse = m.diffuse_color*min(1.0, m.diffuse_intensity/0.8) if use_diffuse else (1.0, 1.0, 1.0) - mmd_material.diffuse_color = diffuse - - cast_shadows = getattr(m, 'cast_shadows', m.use_cast_buffer_shadows) - map_alpha = next((s.blend_type for s in m.texture_slots if s and s.use_map_alpha), None) - if m.use_transparency and map_alpha in {None, 'MULTIPLY'}: - mmd_material.alpha = m.alpha - - mmd_material.specular_color = m.specular_color*min(1.0, m.specular_intensity/0.8) - mmd_material.shininess = m.specular_hardness - mmd_material.is_double_sided = m.game_settings.use_backface_culling - mmd_material.enabled_self_shadow_map = cast_shadows and m.alpha > 1e-3 - mmd_material.enabled_self_shadow = m.use_shadows - - -class _DummyTexture: - def __init__(self, image): - self.type = 'IMAGE' - self.image = image - self.use_mipmap = True - -class _DummyTextureSlot: - def __init__(self, image): - self.diffuse_color_factor = 1 - self.uv_layer = '' - self.texture = _DummyTexture(image) - -class _FnMaterialCycles(_FnMaterialBI): def get_texture(self): - return self.__get_texture_node('mmd_base_tex', use_dummy=True) + return self.__get_texture_node("mmd_base_tex", use_dummy=True) def create_texture(self, filepath): - texture = self.__create_texture_node('mmd_base_tex', filepath, (-4, -1)) + texture = self.__create_texture_node("mmd_base_tex", filepath, (-4, -1)) return _DummyTextureSlot(texture.image) def remove_texture(self): if self._nodes_are_readonly: return - self.__remove_texture_node('mmd_base_tex') - + self.__remove_texture_node("mmd_base_tex") def get_sphere_texture(self): - return self.__get_texture_node('mmd_sphere_tex', use_dummy=True) + return self.__get_texture_node("mmd_sphere_tex", use_dummy=True) def use_sphere_texture(self, use_sphere, obj=None): if self._nodes_are_readonly: @@ -480,10 +239,10 @@ def use_sphere_texture(self, use_sphere, obj=None): if use_sphere: self.update_sphere_texture_type(obj) else: - self.__update_shader_input('Sphere Tex Fac', 0) + self.__update_shader_input("Sphere Tex Fac", 0) def create_sphere_texture(self, filepath, obj=None): - texture = self.__create_texture_node('mmd_sphere_tex', filepath, (-2, -2)) + texture = self.__create_texture_node("mmd_sphere_tex", filepath, (-2, -2)) self.update_sphere_texture_type(obj) return _DummyTextureSlot(texture.image) @@ -491,69 +250,67 @@ def update_sphere_texture_type(self, obj=None): if self._nodes_are_readonly: return sphere_texture_type = int(self.material.mmd_material.sphere_texture_type) - is_sph_add = (sphere_texture_type == 2) + is_sph_add = sphere_texture_type == 2 if sphere_texture_type not in (1, 2, 3): - self.__update_shader_input('Sphere Tex Fac', 0) + self.__update_shader_input("Sphere Tex Fac", 0) else: - self.__update_shader_input('Sphere Tex Fac', 1) - self.__update_shader_input('Sphere Mul/Add', is_sph_add) - self.__update_shader_input('Sphere Tex', (0, 0, 0, 1) if is_sph_add else (1, 1, 1, 1)) + self.__update_shader_input("Sphere Tex Fac", 1) + self.__update_shader_input("Sphere Mul/Add", is_sph_add) + self.__update_shader_input("Sphere Tex", (0, 0, 0, 1) if is_sph_add else (1, 1, 1, 1)) - texture = self.__get_texture_node('mmd_sphere_tex') - if texture and (not texture.inputs['Vector'].is_linked or texture.inputs['Vector'].links[0].from_node.name == 'mmd_tex_uv'): - if hasattr(texture, 'color_space'): - texture.color_space = 'NONE' if is_sph_add else 'COLOR' - elif hasattr(texture.image, 'colorspace_settings'): - texture.image.colorspace_settings.name = 'Linear' if is_sph_add else 'sRGB' + texture = self.__get_texture_node("mmd_sphere_tex") + if texture and (not texture.inputs["Vector"].is_linked or texture.inputs["Vector"].links[0].from_node.name == "mmd_tex_uv"): + if hasattr(texture, "color_space"): + texture.color_space = "NONE" if is_sph_add else "COLOR" + elif hasattr(texture.image, "colorspace_settings"): + texture.image.colorspace_settings.name = "Linear Rec.709" if is_sph_add else "sRGB" mat = self.material nodes, links = mat.node_tree.nodes, mat.node_tree.links if sphere_texture_type == 3: - if obj and obj.type == 'MESH' and mat in tuple(obj.data.materials): - uv_layers = (l for l in obj.data.uv_layers if not l.name.startswith('_')) - next(uv_layers, None) # skip base UV - subtex_uv = getattr(next(uv_layers, None), 'name', '') - if subtex_uv != 'UV1': + if obj and obj.type == "MESH" and mat in tuple(obj.data.materials): + uv_layers = (l for l in obj.data.uv_layers if not l.name.startswith("_")) + next(uv_layers, None) # skip base UV + subtex_uv = getattr(next(uv_layers, None), "name", "") + if subtex_uv != "UV1": logging.info(' * material(%s): object "%s" use UV "%s" for SubTex', mat.name, obj.name, subtex_uv) - links.new(nodes['mmd_tex_uv'].outputs['SubTex UV'], texture.inputs['Vector']) + links.new(nodes["mmd_tex_uv"].outputs["SubTex UV"], texture.inputs["Vector"]) else: - links.new(nodes['mmd_tex_uv'].outputs['Sphere UV'], texture.inputs['Vector']) + links.new(nodes["mmd_tex_uv"].outputs["Sphere UV"], texture.inputs["Vector"]) def remove_sphere_texture(self): if self._nodes_are_readonly: return - self.__remove_texture_node('mmd_sphere_tex') - + self.__remove_texture_node("mmd_sphere_tex") def get_toon_texture(self): - return self.__get_texture_node('mmd_toon_tex', use_dummy=True) + return self.__get_texture_node("mmd_toon_tex", use_dummy=True) def use_toon_texture(self, use_toon): if self._nodes_are_readonly: return - self.__update_shader_input('Toon Tex Fac', use_toon) + self.__update_shader_input("Toon Tex Fac", use_toon) def create_toon_texture(self, filepath): - texture = self.__create_texture_node('mmd_toon_tex', filepath, (-3, -1.5)) + texture = self.__create_texture_node("mmd_toon_tex", filepath, (-3, -1.5)) return _DummyTextureSlot(texture.image) def remove_toon_texture(self): if self._nodes_are_readonly: return - self.__remove_texture_node('mmd_toon_tex') - + self.__remove_texture_node("mmd_toon_tex") def __get_texture_node(self, node_name, use_dummy=False): mat = self.material - texture = getattr(mat.node_tree, 'nodes', {}).get(node_name, None) + texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None) if isinstance(texture, bpy.types.ShaderNodeTexImage): return _DummyTexture(texture.image) if use_dummy else texture return None def __remove_texture_node(self, node_name): mat = self.material - texture = getattr(mat.node_tree, 'nodes', {}).get(node_name, None) + texture = getattr(mat.node_tree, "nodes", {}).get(node_name, None) if isinstance(texture, bpy.types.ShaderNodeTexImage): mat.node_tree.nodes.remove(texture) mat.update_tag() @@ -562,50 +319,51 @@ def __create_texture_node(self, node_name, filepath, pos): texture = self.__get_texture_node(node_name) if texture is None: from mathutils import Vector + self.__update_shader_nodes() nodes = self.material.node_tree.nodes - texture = nodes.new('ShaderNodeTexImage') + texture = nodes.new("ShaderNodeTexImage") + # pylint: disable=assignment-from-no-return texture.label = bpy.path.display_name(node_name) texture.name = node_name - texture.location = nodes['mmd_shader'].location + Vector((pos[0]*210, pos[1]*220)) + texture.location = nodes["mmd_shader"].location + Vector((pos[0] * 210, pos[1] * 220)) texture.image = self._load_image(filepath) self.__update_shader_nodes() return texture - def update_ambient_color(self): if self._nodes_are_readonly: return mat = self.material mmd_mat = mat.mmd_material - mat.diffuse_color[:3] = self._mixDiffuseAndAmbient(mmd_mat) - self.__update_shader_input('Ambient Color', mmd_mat.ambient_color[:]+(1,)) + mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat) + self.__update_shader_input("Ambient Color", mmd_mat.ambient_color[:] + (1,)) def update_diffuse_color(self): if self._nodes_are_readonly: return mat = self.material mmd_mat = mat.mmd_material - mat.diffuse_color[:3] = self._mixDiffuseAndAmbient(mmd_mat) - self.__update_shader_input('Diffuse Color', mmd_mat.diffuse_color[:]+(1,)) + mat.diffuse_color[:3] = self._mix_diffuse_and_ambient(mmd_mat) + self.__update_shader_input("Diffuse Color", mmd_mat.diffuse_color[:] + (1,)) def update_alpha(self): if self._nodes_are_readonly: return mat = self.material mmd_mat = mat.mmd_material - if hasattr(mat, 'blend_method'): - mat.blend_method = 'HASHED' # 'BLEND' - #mat.show_transparent_back = False - elif hasattr(mat, 'transparency_method'): + if hasattr(mat, "blend_method"): + mat.blend_method = "HASHED" # 'BLEND' + # mat.show_transparent_back = False + elif hasattr(mat, "transparency_method"): mat.use_transparency = True - mat.transparency_method = 'Z_TRANSPARENCY' - mat.game_settings.alpha_blend = 'ALPHA' - if hasattr(mat, 'alpha'): + mat.transparency_method = "Z_TRANSPARENCY" + mat.game_settings.alpha_blend = "ALPHA" + if hasattr(mat, "alpha"): mat.alpha = mmd_mat.alpha elif len(mat.diffuse_color) > 3: mat.diffuse_color[3] = mmd_mat.alpha - self.__update_shader_input('Alpha', mmd_mat.alpha) + self.__update_shader_input("Alpha", mmd_mat.alpha) self.update_self_shadow_map() def update_specular_color(self): @@ -614,30 +372,30 @@ def update_specular_color(self): mat = self.material mmd_mat = mat.mmd_material mat.specular_color = mmd_mat.specular_color - self.__update_shader_input('Specular Color', mmd_mat.specular_color[:]+(1,)) + self.__update_shader_input("Specular Color", mmd_mat.specular_color[:] + (1,)) def update_shininess(self): if self._nodes_are_readonly: return mat = self.material mmd_mat = mat.mmd_material - mat.roughness = 1/pow(max(mmd_mat.shininess, 1), 0.37) - if hasattr(mat, 'metallic'): + mat.roughness = 1 / pow(max(mmd_mat.shininess, 1), 0.37) + if hasattr(mat, "metallic"): mat.metallic = pow(1 - mat.roughness, 2.7) - if hasattr(mat, 'specular_hardness'): + if hasattr(mat, "specular_hardness"): mat.specular_hardness = mmd_mat.shininess - self.__update_shader_input('Reflect', mmd_mat.shininess) + self.__update_shader_input("Reflect", mmd_mat.shininess) def update_is_double_sided(self): if self._nodes_are_readonly: return mat = self.material mmd_mat = mat.mmd_material - if hasattr(mat, 'game_settings'): + if hasattr(mat, "game_settings"): mat.game_settings.use_backface_culling = not mmd_mat.is_double_sided - elif hasattr(mat, 'use_backface_culling'): + elif hasattr(mat, "use_backface_culling"): mat.use_backface_culling = not mmd_mat.is_double_sided - self.__update_shader_input('Double Sided', mmd_mat.is_double_sided) + self.__update_shader_input("Double Sided", mmd_mat.is_double_sided) def update_self_shadow_map(self): if self._nodes_are_readonly: @@ -645,40 +403,41 @@ def update_self_shadow_map(self): mat = self.material mmd_mat = mat.mmd_material cast_shadows = mmd_mat.enabled_self_shadow_map if mmd_mat.alpha > 1e-3 else False - if hasattr(mat, 'shadow_method'): - mat.shadow_method = 'HASHED' if cast_shadows else 'NONE' + if hasattr(mat, "shadow_method"): + mat.shadow_method = "HASHED" if cast_shadows else "NONE" def update_self_shadow(self): if self._nodes_are_readonly: return mat = self.material mmd_mat = mat.mmd_material - self.__update_shader_input('Self Shadow', mmd_mat.enabled_self_shadow) + self.__update_shader_input("Self Shadow", mmd_mat.enabled_self_shadow) @staticmethod def convert_to_mmd_material(material, context=bpy.context): m, mmd_material = material, material.mmd_material - if m.use_nodes and next((n for n in m.node_tree.nodes if n.name.startswith('mmd_')), None) is None: + if m.use_nodes and next((n for n in m.node_tree.nodes if n.name.startswith("mmd_")), None) is None: + def search_tex_image_node(node: bpy.types.ShaderNode): - if node.type == 'TEX_IMAGE': + if node.type == "TEX_IMAGE": return node - for input in node.inputs: - if not input.is_linked: + for node_input in node.inputs: + if not node_input.is_linked: continue - child = search_tex_image_node(input.links[0].from_node) + child = search_tex_image_node(node_input.links[0].from_node) if child is not None: return child return None active_render_engine = context.engine preferred_output_node_target = { - 'CYCLES': 'CYCLES', - 'BLENDER_EEVEE': 'EEVEE', - }.get(active_render_engine, 'ALL') + "CYCLES": "CYCLES", + "BLENDER_EEVEE_NEXT": "EEVEE", + }.get(active_render_engine, "ALL") tex_node = None - for target in [preferred_output_node_target, 'ALL']: + for target in [preferred_output_node_target, "ALL"]: output_node = m.node_tree.get_output_node(target) if output_node is None: continue @@ -690,45 +449,43 @@ def search_tex_image_node(node: bpy.types.ShaderNode): break if tex_node is None: - tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == 'ShaderNodeTexImage'), None) + tex_node = next((n for n in m.node_tree.nodes if n.bl_idname == "ShaderNodeTexImage"), None) if tex_node: - tex_node.name = 'mmd_base_tex' + tex_node.name = "mmd_base_tex" - shadow_method = getattr(m, 'shadow_method', None) + shadow_method = getattr(m, "shadow_method", None) mmd_material.diffuse_color = m.diffuse_color[:3] - if hasattr(m, 'alpha'): + if hasattr(m, "alpha"): mmd_material.alpha = m.alpha elif len(m.diffuse_color) > 3: mmd_material.alpha = m.diffuse_color[3] mmd_material.specular_color = m.specular_color - if hasattr(m, 'specular_hardness'): + if hasattr(m, "specular_hardness"): mmd_material.shininess = m.specular_hardness else: - mmd_material.shininess = pow(1/max(m.roughness, 0.099), 1/.37) + mmd_material.shininess = pow(1 / max(m.roughness, 0.099), 1 / 0.37) - if hasattr(m, 'game_settings'): + if hasattr(m, "game_settings"): mmd_material.is_double_sided = not m.game_settings.use_backface_culling - elif hasattr(m, 'use_backface_culling'): + elif hasattr(m, "use_backface_culling"): mmd_material.is_double_sided = not m.use_backface_culling if shadow_method: - mmd_material.enabled_self_shadow_map = (shadow_method != 'NONE') and mmd_material.alpha > 1e-3 - mmd_material.enabled_self_shadow = (shadow_method != 'NONE') - + mmd_material.enabled_self_shadow_map = (shadow_method != "NONE") and mmd_material.alpha > 1e-3 + mmd_material.enabled_self_shadow = shadow_method != "NONE" def __update_shader_input(self, name, val): mat = self.material - if mat.name.startswith('mmd_'): # skip mmd_edge.* + if mat.name.startswith("mmd_"): # skip mmd_edge.* return self.__update_shader_nodes() - shader = mat.node_tree.nodes.get('mmd_shader', None) + shader = mat.node_tree.nodes.get("mmd_shader", None) if shader and name in shader.inputs: - if hasattr(shader, 'node_tree'): - input_socket = shader.node_tree.inputs[name] - if hasattr(input_socket, 'min_value'): - val = min(max(val, input_socket.min_value), input_socket.max_value) + interface_socket = shader.node_tree.interface.items_tree[name] + if hasattr(interface_socket, "min_value"): + val = min(max(val, interface_socket.min_value), interface_socket.max_value) shader.inputs[name].default_value = val def __update_shader_nodes(self): @@ -737,210 +494,200 @@ def __update_shader_nodes(self): mat.use_nodes = True mat.node_tree.nodes.clear() - from mathutils import Vector nodes, links = mat.node_tree.nodes, mat.node_tree.links - class _Dummy: default_value, is_linked = None, True - node_shader = nodes.get('mmd_shader', None) + class _Dummy: + default_value, is_linked = None, True + + node_shader = nodes.get("mmd_shader", None) if node_shader is None: - node_shader = nodes.new('ShaderNodeGroup') - node_shader.name = 'mmd_shader' + node_shader: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup") + node_shader.name = "mmd_shader" node_shader.location = (0, 1500) node_shader.width = 200 node_shader.node_tree = self.__get_shader() - mmd_mat = mat.mmd_material - node_shader.inputs.get('Ambient Color', _Dummy).default_value = mmd_mat.ambient_color[:] + (1,) - node_shader.inputs.get('Diffuse Color', _Dummy).default_value = mmd_mat.diffuse_color[:] + (1,) - node_shader.inputs.get('Specular Color', _Dummy).default_value = mmd_mat.specular_color[:] + (1,) - node_shader.inputs.get('Reflect', _Dummy).default_value = mmd_mat.shininess - node_shader.inputs.get('Alpha', _Dummy).default_value = mmd_mat.alpha - node_shader.inputs.get('Double Sided', _Dummy).default_value = mmd_mat.is_double_sided - node_shader.inputs.get('Self Shadow', _Dummy).default_value = mmd_mat.enabled_self_shadow + mmd_mat: MMDMaterial = mat.mmd_material + node_shader.inputs.get("Ambient Color", _Dummy).default_value = mmd_mat.ambient_color[:] + (1,) + node_shader.inputs.get("Diffuse Color", _Dummy).default_value = mmd_mat.diffuse_color[:] + (1,) + node_shader.inputs.get("Specular Color", _Dummy).default_value = mmd_mat.specular_color[:] + (1,) + node_shader.inputs.get("Reflect", _Dummy).default_value = mmd_mat.shininess + node_shader.inputs.get("Alpha", _Dummy).default_value = mmd_mat.alpha + node_shader.inputs.get("Double Sided", _Dummy).default_value = mmd_mat.is_double_sided + node_shader.inputs.get("Self Shadow", _Dummy).default_value = mmd_mat.enabled_self_shadow self.update_sphere_texture_type() - node_uv = nodes.get('mmd_tex_uv', None) + node_uv = nodes.get("mmd_tex_uv", None) if node_uv is None: - node_uv = nodes.new('ShaderNodeGroup') - node_uv.name = 'mmd_tex_uv' - node_uv.location = node_shader.location + Vector((-5*210, -2.5*220)) + node_uv: bpy.types.ShaderNodeGroup = nodes.new("ShaderNodeGroup") + node_uv.name = "mmd_tex_uv" + node_uv.location = node_shader.location + Vector((-5 * 210, -2.5 * 220)) node_uv.node_tree = self.__get_shader_uv() - if not (node_shader.outputs['Shader'].is_linked or node_shader.outputs['Color'].is_linked or node_shader.outputs['Alpha'].is_linked): + if not (node_shader.outputs["Shader"].is_linked or node_shader.outputs["Color"].is_linked or node_shader.outputs["Alpha"].is_linked): node_output = next((n for n in nodes if isinstance(n, bpy.types.ShaderNodeOutputMaterial) and n.is_active_output), None) if node_output is None: - node_output = nodes.new('ShaderNodeOutputMaterial') + node_output: bpy.types.ShaderNodeOutputMaterial = nodes.new("ShaderNodeOutputMaterial") node_output.is_active_output = True node_output.location = node_shader.location + Vector((400, 0)) - links.new(node_shader.outputs['Shader'], node_output.inputs['Surface']) + links.new(node_shader.outputs["Shader"], node_output.inputs["Surface"]) - for name_id in ('Base', 'Toon', 'Sphere'): - texture = self.__get_texture_node('mmd_%s_tex'%name_id.lower()) + for name_id in ("Base", "Toon", "Sphere"): + texture = self.__get_texture_node("mmd_%s_tex" % name_id.lower()) if texture: - name_tex_in, name_alpha_in, name_uv_out = (name_id+x for x in (' Tex', ' Alpha', ' UV')) + name_tex_in, name_alpha_in, name_uv_out = (name_id + x for x in (" Tex", " Alpha", " UV")) if not node_shader.inputs.get(name_tex_in, _Dummy).is_linked: - links.new(texture.outputs['Color'], node_shader.inputs[name_tex_in]) + links.new(texture.outputs["Color"], node_shader.inputs[name_tex_in]) if not node_shader.inputs.get(name_alpha_in, _Dummy).is_linked: - links.new(texture.outputs['Alpha'], node_shader.inputs[name_alpha_in]) - if not texture.inputs['Vector'].is_linked: - links.new(node_uv.outputs[name_uv_out], texture.inputs['Vector']) + links.new(texture.outputs["Alpha"], node_shader.inputs[name_alpha_in]) + if not texture.inputs["Vector"].is_linked: + links.new(node_uv.outputs[name_uv_out], texture.inputs["Vector"]) def __get_shader_uv(self): - group_name = 'MMDTexUV' - shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type='ShaderNodeTree') + group_name = "MMDTexUV" + shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") if len(shader.nodes): return shader ng = _NodeGroupUtils(shader) ############################################################################ - node_output = ng.new_node('NodeGroupOutput', (6, 0)) + _node_output: bpy.types.NodeGroupOutput = ng.new_node("NodeGroupOutput", (6, 0)) - tex_coord = ng.new_node('ShaderNodeTexCoord', (0, 0)) + tex_coord: bpy.types.ShaderNodeTexCoord = ng.new_node("ShaderNodeTexCoord", (0, 0)) - if hasattr(bpy.types, 'ShaderNodeUVMap'): - tex_coord1 = ng.new_node('ShaderNodeUVMap', (4, -2)) - tex_coord1.uv_map, socketUV1 = 'UV1', 'UV' - else: - tex_coord1 = ng.new_node('ShaderNodeAttribute', (4, -2)) - tex_coord1.attribute_name, socketUV1 = 'UV1', 'Vector' - - vec_trans = ng.new_node('ShaderNodeVectorTransform', (1, -1)) - vec_trans.vector_type = 'NORMAL' - vec_trans.convert_from = 'OBJECT' - vec_trans.convert_to = 'CAMERA' - - node_vector = ng.new_node('ShaderNodeMapping', (2, -1)) - node_vector.vector_type = 'POINT' - if bpy.app.version < (2, 81, 0): - node_vector.translation = (0.5, 0.5, 0.0) - node_vector.scale = (0.5, 0.5, 1.0) - else: - node_vector.inputs['Location'].default_value = (0.5, 0.5, 0.0) - node_vector.inputs['Scale'].default_value = (0.5, 0.5, 1.0) + tex_coord1: bpy.types.ShaderNodeUVMap = ng.new_node("ShaderNodeUVMap", (4, -2)) + tex_coord1.uv_map = "UV1" + + vec_trans: bpy.types.ShaderNodeVectorTransform = ng.new_node("ShaderNodeVectorTransform", (1, -1)) + vec_trans.vector_type = "NORMAL" + vec_trans.convert_from = "OBJECT" + vec_trans.convert_to = "CAMERA" + + node_vector: bpy.types.ShaderNodeMapping = ng.new_node("ShaderNodeMapping", (2, -1)) + node_vector.vector_type = "POINT" + node_vector.inputs["Location"].default_value = (0.5, 0.5, 0.0) + node_vector.inputs["Scale"].default_value = (0.5, 0.5, 1.0) links = ng.links - links.new(tex_coord.outputs['Normal'], vec_trans.inputs['Vector']) - links.new(vec_trans.outputs['Vector'], node_vector.inputs['Vector']) + links.new(tex_coord.outputs["Normal"], vec_trans.inputs["Vector"]) + links.new(vec_trans.outputs["Vector"], node_vector.inputs["Vector"]) - ng.new_output_socket('Base UV', tex_coord.outputs['UV']) - ng.new_output_socket('Toon UV', node_vector.outputs['Vector']) - ng.new_output_socket('Sphere UV', node_vector.outputs['Vector']) - ng.new_output_socket('SubTex UV', tex_coord1.outputs[socketUV1]) + ng.new_output_socket("Base UV", tex_coord.outputs["UV"]) + ng.new_output_socket("Toon UV", node_vector.outputs["Vector"]) + ng.new_output_socket("Sphere UV", node_vector.outputs["Vector"]) + ng.new_output_socket("SubTex UV", tex_coord1.outputs["UV"]) return shader def __get_shader(self): - group_name = 'MMDShaderDev' - shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type='ShaderNodeTree') + group_name = "MMDShaderDev" + shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") if len(shader.nodes): return shader ng = _NodeGroupUtils(shader) ############################################################################ - node_input = ng.new_node('NodeGroupInput', (-5, -1)) - node_output = ng.new_node('NodeGroupOutput', (11, 1)) + node_input: bpy.types.NodeGroupInput = ng.new_node("NodeGroupInput", (-5, -1)) + _node_output: bpy.types.NodeGroupOutput = ng.new_node("NodeGroupOutput", (11, 1)) - node_diffuse = ng.new_mix_node('ADD', (-3, 4), fac=0.6) + node_diffuse: bpy.types.ShaderNodeMath = ng.new_mix_node("ADD", (-3, 4), fac=0.6) node_diffuse.use_clamp = True - node_tex = ng.new_mix_node('MULTIPLY', (-2, 3.5)) - node_toon = ng.new_mix_node('MULTIPLY', (-1, 3)) - node_sph = ng.new_mix_node('MULTIPLY', (0, 2.5)) - node_spa = ng.new_mix_node('ADD', (0, 1.5)) - node_sphere = ng.new_mix_node('MIX', (1, 1)) + node_tex: bpy.types.ShaderNodeMath = ng.new_mix_node("MULTIPLY", (-2, 3.5)) + node_toon: bpy.types.ShaderNodeMath = ng.new_mix_node("MULTIPLY", (-1, 3)) + node_sph: bpy.types.ShaderNodeMath = ng.new_mix_node("MULTIPLY", (0, 2.5)) + node_spa: bpy.types.ShaderNodeMath = ng.new_mix_node("ADD", (0, 1.5)) + node_sphere: bpy.types.ShaderNodeMath = ng.new_mix_node("MIX", (1, 1)) - node_geo = ng.new_node('ShaderNodeNewGeometry', (6, 3.5)) - node_invert = ng.new_math_node('LESS_THAN', (7, 3)) - node_cull = ng.new_math_node('MAXIMUM', (8, 2.5)) - node_alpha = ng.new_math_node('MINIMUM', (9, 2)) + node_geo: bpy.types.ShaderNodeNewGeometry = ng.new_node("ShaderNodeNewGeometry", (6, 3.5)) + node_invert: bpy.types.ShaderNodeMath = ng.new_math_node("LESS_THAN", (7, 3)) + node_cull: bpy.types.ShaderNodeMath = ng.new_math_node("MAXIMUM", (8, 2.5)) + node_alpha: bpy.types.ShaderNodeMath = ng.new_math_node("MINIMUM", (9, 2)) node_alpha.use_clamp = True - node_alpha_tex = ng.new_math_node('MULTIPLY', (-1, -2)) - node_alpha_toon = ng.new_math_node('MULTIPLY', (0, -2.5)) - node_alpha_sph = ng.new_math_node('MULTIPLY', (1, -3)) + node_alpha_tex: bpy.types.ShaderNodeMath = ng.new_math_node("MULTIPLY", (-1, -2)) + node_alpha_toon: bpy.types.ShaderNodeMath = ng.new_math_node("MULTIPLY", (0, -2.5)) + node_alpha_sph: bpy.types.ShaderNodeMath = ng.new_math_node("MULTIPLY", (1, -3)) - node_reflect = ng.new_math_node('DIVIDE', (7, -1.5), value1=1) + node_reflect: bpy.types.ShaderNodeMath = ng.new_math_node("DIVIDE", (7, -1.5), value1=1) node_reflect.use_clamp = True - shader_diffuse = ng.new_node('ShaderNodeBsdfDiffuse', (8, 0)) - shader_glossy = ng.new_node('ShaderNodeBsdfGlossy', (8, -1)) - shader_base_mix = ng.new_node('ShaderNodeMixShader', (9, 0)) - shader_base_mix.inputs['Fac'].default_value = 0.02 - shader_trans = ng.new_node('ShaderNodeBsdfTransparent', (9, 1)) - shader_alpha_mix = ng.new_node('ShaderNodeMixShader', (10, 1)) + shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = ng.new_node("ShaderNodeBsdfDiffuse", (8, 0)) + shader_glossy: bpy.types.ShaderNodeBsdfAnisotropic = ng.new_node("ShaderNodeBsdfAnisotropic", (8, -1)) + shader_base_mix: bpy.types.ShaderNodeMixShader = ng.new_node("ShaderNodeMixShader", (9, 0)) + shader_base_mix.inputs["Fac"].default_value = 0.02 + shader_trans: bpy.types.ShaderNodeBsdfTransparent = ng.new_node("ShaderNodeBsdfTransparent", (9, 1)) + shader_alpha_mix: bpy.types.ShaderNodeMixShader = ng.new_node("ShaderNodeMixShader", (10, 1)) links = ng.links - links.new(node_reflect.outputs['Value'], shader_glossy.inputs['Roughness']) - links.new(shader_diffuse.outputs['BSDF'], shader_base_mix.inputs[1]) - links.new(shader_glossy.outputs['BSDF'], shader_base_mix.inputs[2]) - - links.new(node_diffuse.outputs['Color'], node_tex.inputs['Color1']) - links.new(node_tex.outputs['Color'], node_toon.inputs['Color1']) - links.new(node_toon.outputs['Color'], node_sph.inputs['Color1']) - links.new(node_toon.outputs['Color'], node_spa.inputs['Color1']) - links.new(node_sph.outputs['Color'], node_sphere.inputs['Color1']) - links.new(node_spa.outputs['Color'], node_sphere.inputs['Color2']) - links.new(node_sphere.outputs['Color'], shader_diffuse.inputs['Color']) - - links.new(node_geo.outputs['Backfacing'], node_invert.inputs[0]) - links.new(node_invert.outputs['Value'], node_cull.inputs[0]) - links.new(node_cull.outputs['Value'], node_alpha.inputs[0]) - links.new(node_alpha_tex.outputs['Value'], node_alpha_toon.inputs[0]) - links.new(node_alpha_toon.outputs['Value'], node_alpha_sph.inputs[0]) - links.new(node_alpha_sph.outputs['Value'], node_alpha.inputs[1]) - - links.new(node_alpha.outputs['Value'], shader_alpha_mix.inputs['Fac']) - links.new(shader_trans.outputs['BSDF'], shader_alpha_mix.inputs[1]) - links.new(shader_base_mix.outputs['Shader'], shader_alpha_mix.inputs[2]) + links.new(node_reflect.outputs["Value"], shader_glossy.inputs["Roughness"]) + links.new(shader_diffuse.outputs["BSDF"], shader_base_mix.inputs[1]) + links.new(shader_glossy.outputs["BSDF"], shader_base_mix.inputs[2]) + + links.new(node_diffuse.outputs["Color"], node_tex.inputs["Color1"]) + links.new(node_tex.outputs["Color"], node_toon.inputs["Color1"]) + links.new(node_toon.outputs["Color"], node_sph.inputs["Color1"]) + links.new(node_toon.outputs["Color"], node_spa.inputs["Color1"]) + links.new(node_sph.outputs["Color"], node_sphere.inputs["Color1"]) + links.new(node_spa.outputs["Color"], node_sphere.inputs["Color2"]) + links.new(node_sphere.outputs["Color"], shader_diffuse.inputs["Color"]) + + links.new(node_geo.outputs["Backfacing"], node_invert.inputs[0]) + links.new(node_invert.outputs["Value"], node_cull.inputs[0]) + links.new(node_cull.outputs["Value"], node_alpha.inputs[0]) + links.new(node_alpha_tex.outputs["Value"], node_alpha_toon.inputs[0]) + links.new(node_alpha_toon.outputs["Value"], node_alpha_sph.inputs[0]) + links.new(node_alpha_sph.outputs["Value"], node_alpha.inputs[1]) + + links.new(node_alpha.outputs["Value"], shader_alpha_mix.inputs["Fac"]) + links.new(shader_trans.outputs["BSDF"], shader_alpha_mix.inputs[1]) + links.new(shader_base_mix.outputs["Shader"], shader_alpha_mix.inputs[2]) ############################################################################ - ng.new_input_socket('Ambient Color', node_diffuse.inputs['Color1'], (0.4, 0.4, 0.4, 1)) - ng.new_input_socket('Diffuse Color', node_diffuse.inputs['Color2'], (0.8, 0.8, 0.8, 1)) - ng.new_input_socket('Specular Color', shader_glossy.inputs['Color'], (0.8, 0.8, 0.8, 1)) - ng.new_input_socket('Reflect', node_reflect.inputs[1], 50, min_max=(1, 512)) - ng.new_input_socket('Base Tex Fac', node_tex.inputs['Fac'], 1) - ng.new_input_socket('Base Tex', node_tex.inputs['Color2'], (1, 1, 1, 1)) - ng.new_input_socket('Toon Tex Fac', node_toon.inputs['Fac'], 1) - ng.new_input_socket('Toon Tex', node_toon.inputs['Color2'], (1, 1, 1, 1)) - ng.new_input_socket('Sphere Tex Fac', node_sph.inputs['Fac'], 1) - ng.new_input_socket('Sphere Tex', node_sph.inputs['Color2'], (1, 1, 1, 1)) - ng.new_input_socket('Sphere Mul/Add', node_sphere.inputs['Fac'], 0) - ng.new_input_socket('Double Sided', node_cull.inputs[1], 0, min_max=(0, 1)) - ng.new_input_socket('Alpha', node_alpha_tex.inputs[0], 1, min_max=(0, 1)) - ng.new_input_socket('Base Alpha', node_alpha_tex.inputs[1], 1, min_max=(0, 1)) - ng.new_input_socket('Toon Alpha', node_alpha_toon.inputs[1], 1, min_max=(0, 1)) - ng.new_input_socket('Sphere Alpha', node_alpha_sph.inputs[1], 1, min_max=(0, 1)) - - links.new(node_input.outputs['Sphere Tex Fac'], node_spa.inputs['Fac']) - links.new(node_input.outputs['Sphere Tex'], node_spa.inputs['Color2']) - - ng.new_output_socket('Shader', shader_alpha_mix.outputs['Shader']) - ng.new_output_socket('Color', node_sphere.outputs['Color']) - ng.new_output_socket('Alpha', node_alpha.outputs['Value']) + ng.new_input_socket("Ambient Color", node_diffuse.inputs["Color1"], (0.4, 0.4, 0.4, 1)) + ng.new_input_socket("Diffuse Color", node_diffuse.inputs["Color2"], (0.8, 0.8, 0.8, 1)) + ng.new_input_socket("Specular Color", shader_glossy.inputs["Color"], (0.8, 0.8, 0.8, 1)) + ng.new_input_socket("Reflect", node_reflect.inputs[1], 50, min_max=(1, 512)) + ng.new_input_socket("Base Tex Fac", node_tex.inputs["Fac"], 1) + ng.new_input_socket("Base Tex", node_tex.inputs["Color2"], (1, 1, 1, 1)) + ng.new_input_socket("Toon Tex Fac", node_toon.inputs["Fac"], 1) + ng.new_input_socket("Toon Tex", node_toon.inputs["Color2"], (1, 1, 1, 1)) + ng.new_input_socket("Sphere Tex Fac", node_sph.inputs["Fac"], 1) + ng.new_input_socket("Sphere Tex", node_sph.inputs["Color2"], (1, 1, 1, 1)) + ng.new_input_socket("Sphere Mul/Add", node_sphere.inputs["Fac"], 0) + ng.new_input_socket("Double Sided", node_cull.inputs[1], 0, min_max=(0, 1)) + ng.new_input_socket("Alpha", node_alpha_tex.inputs[0], 1, min_max=(0, 1)) + ng.new_input_socket("Base Alpha", node_alpha_tex.inputs[1], 1, min_max=(0, 1)) + ng.new_input_socket("Toon Alpha", node_alpha_toon.inputs[1], 1, min_max=(0, 1)) + ng.new_input_socket("Sphere Alpha", node_alpha_sph.inputs[1], 1, min_max=(0, 1)) + + links.new(node_input.outputs["Sphere Tex Fac"], node_spa.inputs["Fac"]) + links.new(node_input.outputs["Sphere Tex"], node_spa.inputs["Color2"]) + + ng.new_output_socket("Shader", shader_alpha_mix.outputs["Shader"]) + ng.new_output_socket("Color", node_sphere.outputs["Color"]) + ng.new_output_socket("Alpha", node_alpha.outputs["Value"]) return shader -FnMaterial = _FnMaterialCycles -if bpy.app.version < (2, 80, 0): - FnMaterial = _FnMaterialBI class MigrationFnMaterial: @staticmethod def update_mmd_shader(): - mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get('MMDShaderDev') + mmd_shader_node_tree: Optional[bpy.types.NodeTree] = bpy.data.node_groups.get("MMDShaderDev") if mmd_shader_node_tree is None: return - if 'Color' in mmd_shader_node_tree.outputs: + ng = _NodeGroupUtils(mmd_shader_node_tree) + if "Color" in ng.node_output.inputs: return - ng = _NodeGroupUtils(mmd_shader_node_tree) - shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = [n for n in mmd_shader_node_tree.nodes if n.type == 'BSDF_DIFFUSE'][0] - node_sphere: bpy.types.ShaderNodeMixRGB = shader_diffuse.inputs['Color'].links[0].from_node + shader_diffuse: bpy.types.ShaderNodeBsdfDiffuse = [n for n in mmd_shader_node_tree.nodes if n.type == "BSDF_DIFFUSE"][0] + node_sphere: bpy.types.ShaderNodeMixRGB = shader_diffuse.inputs["Color"].links[0].from_node node_output: bpy.types.NodeGroupOutput = ng.node_output - shader_alpha_mix: bpy.types.ShaderNodeMixShader = node_output.inputs['Shader'].links[0].from_node - node_alpha: bpy.types.ShaderNodeMath = shader_alpha_mix.inputs['Fac'].links[0].from_node + shader_alpha_mix: bpy.types.ShaderNodeMixShader = node_output.inputs["Shader"].links[0].from_node + node_alpha: bpy.types.ShaderNodeMath = shader_alpha_mix.inputs["Fac"].links[0].from_node - ng.new_output_socket('Color', node_sphere.outputs['Color']) - ng.new_output_socket('Alpha', node_alpha.outputs['Value']) + ng.new_output_socket("Color", node_sphere.outputs["Color"]) + ng.new_output_socket("Alpha", node_alpha.outputs["Value"]) diff --git a/mmd_tools/core/model.py b/mmd_tools/core/model.py index 584ae9f3..5cf7278b 100644 --- a/mmd_tools/core/model.py +++ b/mmd_tools/core/model.py @@ -1,257 +1,234 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import itertools import logging import time -from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, Set, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, Optional, Set, TypeGuard, Union, cast import bpy import idprop -import mathutils import rna_prop_ui -from mmd_tools import MMD_TOOLS_VERSION, bpyutils -from mmd_tools.bpyutils import Props, SceneOp, matmul -from mmd_tools.core import rigid_body -from mmd_tools.core.bone import FnBone, MigrationFnBone -from mmd_tools.core.morph import FnMorph -from mmd_tools.core.rigid_body import MODE_DYNAMIC, MODE_DYNAMIC_BONE, MODE_STATIC +from mathutils import Vector -if TYPE_CHECKING: - from properties.rigid_body import MMDRigidBody - from properties.morph import MaterialMorphData - - -class InvalidRigidSettingException(ValueError): - pass - - -def _copy_property_group(destination: bpy.types.PropertyGroup, source: bpy.types.PropertyGroup, overwrite: bool = True, replace_name2values: Dict[str,Dict[Any,Any]] = dict()): - destination_rna_properties = destination.bl_rna.properties - for name in source.keys(): - is_attr = hasattr(source, name) - value = getattr(source, name) if is_attr else source[name] - if isinstance(value, bpy.types.PropertyGroup): - _copy_property_group(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) - elif isinstance(value, bpy.types.bpy_prop_collection): - _copy_collection_property(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) - elif isinstance(value, idprop.types.IDPropertyArray): - pass - # _copy_collection_property(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) - else: - value2values = replace_name2values.get(name) - if value2values is not None: - replace_value = value2values.get(value) - if replace_value is not None: - value = replace_value - - if overwrite or destination_rna_properties[name].default == getattr(destination, name) if is_attr else destination[name]: - if is_attr: - setattr(destination, name, value) - else: - destination[name] = value - - -def _copy_collection_property(destination: bpy.types.bpy_prop_collection, source: bpy.types.bpy_prop_collection, overwrite: bool = True, replace_name2values: Dict[str,Dict[Any,Any]] = dict()): - if overwrite: - destination.clear() - - len_source = len(source) - if len_source == 0: - return - - source_names: Set[str] = set(source.keys()) - if len(source_names) == len_source and source[0].name != '': - # names work - destination_names: Set[str] = set(destination.keys()) - - missing_names = source_names - destination_names +from .. import MMD_TOOLS_VERSION, bpyutils +from ..bpyutils import FnContext, Props +from . import rigid_body +from .morph import FnMorph +from .rigid_body import MODE_DYNAMIC, MODE_DYNAMIC_BONE, MODE_STATIC - destination_index = 0 - for name, value in source.items(): - if name in missing_names: - new_element = destination.add() - new_element['name'] = name - - _copy_property(destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) - destination.move(destination.find(name), destination_index) - destination_index += 1 - else: - # names not work - while len_source > len(destination): - destination.add() - - for index, name in enumerate(source.keys()): - _copy_property(destination[index], source[index], overwrite=True, replace_name2values=replace_name2values) - - -def _copy_property(destination: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], source: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], overwrite: bool = True, replace_name2values: Dict[str,Dict[Any,Any]] = dict()): - if isinstance(destination, bpy.types.PropertyGroup): - _copy_property_group(destination, source, overwrite=overwrite, replace_name2values=replace_name2values) - elif isinstance(destination, bpy.types.bpy_prop_collection): - _copy_collection_property(destination, source, overwrite=overwrite, replace_name2values=replace_name2values) - else: - raise ValueError(f'Unsupported destination: {destination}') +if TYPE_CHECKING: + from ..properties.morph import MaterialMorphData + from ..properties.rigid_body import MMDRigidBody class FnModel: @staticmethod - def copy_mmd_root(destination_root_object: bpy.types.Object, source_root_object: bpy.types.Object, overwrite: bool = True, replace_name2values: Dict[str,Dict[Any,Any]] = dict()): - _copy_property(destination_root_object.mmd_root, source_root_object.mmd_root, overwrite=overwrite, replace_name2values=replace_name2values) + def copy_mmd_root(destination_root_object: bpy.types.Object, source_root_object: bpy.types.Object, overwrite: bool = True, replace_name2values: Dict[str, Dict[Any, Any]] = None): + FnModel.__copy_property(destination_root_object.mmd_root, source_root_object.mmd_root, overwrite=overwrite, replace_name2values=replace_name2values or {}) @staticmethod - def find_root(obj: bpy.types.Object) -> Optional[bpy.types.Object]: - if not obj: - return None - if obj.mmd_type == 'ROOT': - return obj - return FnModel.find_root(obj.parent) + def find_root_object(obj: Optional[bpy.types.Object]) -> Optional[bpy.types.Object]: + """Find the root object of the model. + Args: + obj (bpy.types.Object): The object to start searching from. + Returns: + Optional[bpy.types.Object]: The root object of the model. If the object is not a part of a model, None is returned. + Generally, the root object is a object with type == "EMPTY" and mmd_type == "ROOT". + """ + while obj is not None and obj.mmd_type != "ROOT": + obj = obj.parent + return obj @staticmethod - def find_armature(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + def find_armature_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + """Find the armature object of the model. + Args: + root_object (bpy.types.Object): The root object of the model. + Returns: + Optional[bpy.types.Object]: The armature object of the model. If the model does not have an armature, None is returned. + """ for o in root_object.children: - if o.type == 'ARMATURE': + if o.type == "ARMATURE": return o return None @staticmethod - def find_rigid_group(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + def find_rigid_group_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: for o in root_object.children: - if o.type == 'EMPTY' and o.mmd_type == 'RIGID_GRP_OBJ': + if o.type == "EMPTY" and o.mmd_type == "RIGID_GRP_OBJ": return o return None @staticmethod - def find_joint_group(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + def __new_group_object(context: bpy.types.Context, name: str, mmd_type: str, parent: bpy.types.Object) -> bpy.types.Object: + group_object = FnContext.new_and_link_object(context, name=name, object_data=None) + group_object.mmd_type = mmd_type + group_object.parent = parent + group_object.hide_set(True) + group_object.hide_select = True + group_object.lock_rotation = group_object.lock_location = group_object.lock_scale = [True, True, True] + return group_object + + @staticmethod + def ensure_rigid_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object: + rigid_group_object = FnModel.find_rigid_group_object(root_object) + if rigid_group_object is not None: + return rigid_group_object + return FnModel.__new_group_object(context, name="rigidbodies", mmd_type="RIGID_GRP_OBJ", parent=root_object) + + @staticmethod + def find_joint_group_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: for o in root_object.children: - if o.type == 'EMPTY' and o.mmd_type == 'JOINT_GRP_OBJ': + if o.type == "EMPTY" and o.mmd_type == "JOINT_GRP_OBJ": return o return None @staticmethod - def find_temporary_group(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: + def ensure_joint_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object: + joint_group_object = FnModel.find_joint_group_object(root_object) + if joint_group_object is not None: + return joint_group_object + return FnModel.__new_group_object(context, name="joints", mmd_type="JOINT_GRP_OBJ", parent=root_object) + + @staticmethod + def find_temporary_group_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: for o in root_object.children: - if o.type == 'EMPTY' and o.mmd_type == 'TEMPORARY_GRP_OBJ': + if o.type == "EMPTY" and o.mmd_type == "TEMPORARY_GRP_OBJ": return o return None + @staticmethod + def ensure_temporary_group_object(context: bpy.types.Context, root_object: bpy.types.Object) -> bpy.types.Object: + temporary_group_object = FnModel.find_temporary_group_object(root_object) + if temporary_group_object is not None: + return temporary_group_object + return FnModel.__new_group_object(context, name="temporary", mmd_type="TEMPORARY_GRP_OBJ", parent=root_object) + @staticmethod def find_bone_order_mesh_object(root_object: bpy.types.Object) -> Optional[bpy.types.Object]: - armature_object = FnModel.find_armature(root_object) + armature_object = FnModel.find_armature_object(root_object) if armature_object is None: return None # TODO consistency issue - return next(filter(lambda o: o.type == 'MESH' and 'mmd_bone_order_override' in o.modifiers, armature_object.children), None) + return next(filter(lambda o: o.type == "MESH" and "mmd_bone_order_override" in o.modifiers, armature_object.children), None) + + @staticmethod + def find_mesh_object_by_name(root_object: bpy.types.Object, name: str) -> Optional[bpy.types.Object]: + for o in FnModel.iterate_mesh_objects(root_object): + # TODO: consider o.data.name + if o.name != name: + continue + return o + return None @staticmethod - def all_children(obj: bpy.types.Object) -> Iterator[bpy.types.Object]: - child: bpy.types.Object - obj.children_recursive + def iterate_child_objects(obj: bpy.types.Object) -> Iterator[bpy.types.Object]: for child in obj.children: yield child - yield from FnModel.all_children(child) + yield from FnModel.iterate_child_objects(child) @staticmethod - def filtered_children(condition_function: Callable[[bpy.types.Object], bool], obj: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]: + def iterate_filtered_child_objects(condition_function: Callable[[bpy.types.Object], bool], obj: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]: if obj is None: - return - child: bpy.types.Object + return iter(()) + return FnModel.__iterate_filtered_child_objects_internal(condition_function, obj) + + @staticmethod + def __iterate_filtered_child_objects_internal(condition_function: Callable[[bpy.types.Object], bool], obj: bpy.types.Object) -> Iterator[bpy.types.Object]: for child in obj.children: if condition_function(child): yield child - else: - yield from FnModel.filtered_children(condition_function, child) + yield from FnModel.__iterate_filtered_child_objects_internal(condition_function, child) + + @staticmethod + def __iterate_child_mesh_objects(obj: Optional[bpy.types.Object]) -> Iterator[bpy.types.Object]: + return FnModel.iterate_filtered_child_objects(FnModel.is_mesh_object, obj) @staticmethod - def child_meshes(obj: bpy.types.Object) -> Iterator[bpy.types.Object]: - return FnModel.filtered_children(lambda x: x.type == 'MESH' and x.mmd_type == 'NONE', obj) + def iterate_mesh_objects(root_object: bpy.types.Object) -> Iterator[bpy.types.Object]: + return FnModel.__iterate_child_mesh_objects(FnModel.find_armature_object(root_object)) @staticmethod def iterate_rigid_body_objects(root_object: bpy.types.Object) -> Iterator[bpy.types.Object]: if root_object.mmd_root.is_built: return itertools.chain( - FnModel.filtered_children(FnModel.is_rigid_body_object, FnModel.find_armature(root_object)), - FnModel.filtered_children(FnModel.is_rigid_body_object, FnModel.find_rigid_group(root_object)), + FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_armature_object(root_object)), + FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_rigid_group_object(root_object)), ) - return FnModel.filtered_children(FnModel.is_rigid_body_object, FnModel.find_rigid_group(root_object)) + return FnModel.iterate_filtered_child_objects(FnModel.is_rigid_body_object, FnModel.find_rigid_group_object(root_object)) @staticmethod def iterate_joint_objects(root_object: bpy.types.Object) -> Iterator[bpy.types.Object]: - return FnModel.filtered_children(FnModel.is_joint_object, FnModel.find_joint_group(root_object)) + return FnModel.iterate_filtered_child_objects(FnModel.is_joint_object, FnModel.find_joint_group_object(root_object)) @staticmethod def iterate_temporary_objects(root_object: bpy.types.Object, rigid_track_only: bool = False) -> Iterator[bpy.types.Object]: - rigid_body_objects = FnModel.filtered_children(FnModel.is_temporary_object, FnModel.find_rigid_group(root_object)) + rigid_body_objects = FnModel.iterate_filtered_child_objects(FnModel.is_temporary_object, FnModel.find_rigid_group_object(root_object)) if rigid_track_only: return rigid_body_objects - temporary_group_object = FnModel.find_temporary_group(root_object) + temporary_group_object = FnModel.find_temporary_group_object(root_object) if temporary_group_object is None: return rigid_body_objects - return itertools.chain(rigid_body_objects, FnModel.filtered_children(FnModel.is_temporary_object, temporary_group_object)) + return itertools.chain(rigid_body_objects, FnModel.__iterate_filtered_child_objects_internal(FnModel.is_temporary_object, temporary_group_object)) @staticmethod - def is_root_object(obj: bpy.types.Object): - return obj and obj.mmd_type == 'ROOT' + def iterate_materials(root_object: bpy.types.Object) -> Iterator[bpy.types.Material]: + return (material for mesh_object in FnModel.iterate_mesh_objects(root_object) for material in cast(bpy.types.Mesh, mesh_object.data).materials if material is not None) @staticmethod - def is_rigid_body_object(obj: bpy.types.Object): - return obj and obj.mmd_type == 'RIGID_BODY' + def iterate_unique_materials(root_object: bpy.types.Object) -> Iterator[bpy.types.Material]: + materials: Dict[bpy.types.Material, None] = {} # use dict because set does not guarantee the order + materials.update((material, None) for material in FnModel.iterate_materials(root_object)) + return iter(materials.keys()) @staticmethod - def is_joint_object(obj: bpy.types.Object): - return obj and obj.mmd_type == 'JOINT' + def is_root_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: + return obj is not None and obj.mmd_type == "ROOT" @staticmethod - def is_temporary_object(obj: bpy.types.Object): - return obj and obj.mmd_type in {'TRACK_TARGET', 'NON_COLLISION_CONSTRAINT', 'SPRING_CONSTRAINT', 'SPRING_GOAL'} + def is_rigid_body_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: + return obj is not None and obj.mmd_type == "RIGID_BODY" @staticmethod - def get_rigid_body_size(obj: bpy.types.Object): - assert(obj.mmd_type == 'RIGID_BODY') - - x0, y0, z0 = obj.bound_box[0] - x1, y1, z1 = obj.bound_box[6] - assert(x1 >= x0 and y1 >= y0 and z1 >= z0) - - shape = obj.mmd_rigid.shape - if shape == 'SPHERE': - radius = (z1 - z0)/2 - return (radius, 0.0, 0.0) - elif shape == 'BOX': - x, y, z = (x1 - x0)/2, (y1 - y0)/2, (z1 - z0)/2 - return (x, y, z) - elif shape == 'CAPSULE': - diameter = (x1 - x0) - radius = diameter/2 - height = abs((z1 - z0) - diameter) - return (radius, height, 0.0) - else: - raise Exception('Invalid shape type.') + def is_joint_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: + return obj is not None and obj.mmd_type == "JOINT" @staticmethod - def join_models(parent_root_object: bpy.types.Object, child_root_objects: List[bpy.types.Object]): - parent_armature_object = FnModel.find_armature(parent_root_object) - bpy.ops.object.transform_apply({ - 'active_object': parent_armature_object, - 'selected_editable_objects': [parent_armature_object], - }, location=True, rotation=True, scale=True) + def is_temporary_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: + return obj is not None and obj.mmd_type in {"TRACK_TARGET", "NON_COLLISION_CONSTRAINT", "SPRING_CONSTRAINT", "SPRING_GOAL"} + + @staticmethod + def is_mesh_object(obj: Optional[bpy.types.Object]) -> TypeGuard[bpy.types.Object]: + return obj is not None and obj.type == "MESH" and obj.mmd_type == "NONE" + + @staticmethod + def join_models(parent_root_object: bpy.types.Object, child_root_objects: Iterable[bpy.types.Object]): + parent_armature_object = FnModel.find_armature_object(parent_root_object) + with bpy.context.temp_override( + active_object=parent_armature_object, + selected_editable_objects=[parent_armature_object], + ): + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pose_bones): """This function will also update the references of bone morphs and rotate+/move+.""" bone_id = bone.mmd_bone.bone_id - + # Change Bone ID bone.mmd_bone.bone_id = new_bone_id - + # Update Relative Bone Morph # Update the reference of bone morph # 更新骨骼表情 for bone_morph in bone_morphs: for data in bone_morph.data: if data.bone_id != bone_id: continue data.bone_id = new_bone_id - + # Update Relative Additional Transform # Update the reference of rotate+/move+ # 更新付与親 for pose_bone in pose_bones: if pose_bone.is_mmd_shadow_bone: @@ -260,13 +237,23 @@ def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pos if mmd_bone.additional_transform_bone_id != bone_id: continue mmd_bone.additional_transform_bone_id = new_bone_id - - # Change the Bone ID if necessary to make sure that each ID is still unique after joining models. - max_bone_id = max((b.mmd_bone.bone_id for b in parent_armature_object.pose.bones if not b.is_mmd_shadow_bone), default=-1) - + + max_bone_id = max( + ( + b.mmd_bone.bone_id + for o in itertools.chain( + child_root_objects, + [parent_root_object], + ) + for b in FnModel.find_armature_object(o).pose.bones + if not b.is_mmd_shadow_bone + ), + default=-1, + ) + child_root_object: bpy.types.Object for child_root_object in child_root_objects: - child_armature_object = FnModel.find_armature(child_root_object) + child_armature_object = FnModel.find_armature_object(child_root_object) child_pose_bones = child_armature_object.pose.bones child_bone_morphs = child_root_object.mmd_root.bone_morphs @@ -279,10 +266,11 @@ def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pos child_armature_matrix = child_armature_object.matrix_parent_inverse.copy() - bpy.ops.object.transform_apply({ - 'active_object': child_armature_object, - 'selected_editable_objects': [child_armature_object], - }, location=True, rotation=True, scale=True) + with bpy.context.temp_override( + active_object=child_armature_object, + selected_editable_objects=[child_armature_object], + ): + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) # Disconnect mesh dependencies because transform_apply fails when mesh data are multiple used. related_meshes: Dict[MaterialMorphData, bpy.types.Mesh] = {} @@ -294,11 +282,12 @@ def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pos try: # replace mesh armature modifier.object mesh: bpy.types.Object - for mesh in FnModel.child_meshes(child_armature_object): - bpy.ops.object.transform_apply({ - 'active_object': mesh, - 'selected_editable_objects': [mesh], - }, location=True, rotation=True, scale=True) + for mesh in FnModel.__iterate_child_mesh_objects(child_armature_object): + with bpy.context.temp_override( + active_object=mesh, + selected_editable_objects=[mesh], + ): + bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) finally: # Restore mesh dependencies for material_morph in child_root_object.mmd_root.material_morphs: @@ -306,47 +295,49 @@ def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pos material_morph_data.related_mesh_data = related_meshes.get(material_morph_data, None) # join armatures - bpy.ops.object.join({ - 'active_object': parent_armature_object, - 'selected_editable_objects': [parent_armature_object, child_armature_object], - }) - - for mesh in FnModel.child_meshes(parent_armature_object): - armature_modifier: bpy.types.ArmatureModifier = ( - mesh.modifiers['mmd_bone_order_override'] if 'mmd_bone_order_override' in mesh.modifiers else - mesh.modifiers.new('mmd_bone_order_override', 'ARMATURE') - ) + with bpy.context.temp_override( + active_object=parent_armature_object, + selected_editable_objects=[parent_armature_object, child_armature_object], + ): + bpy.ops.object.join() + + for mesh in FnModel.__iterate_child_mesh_objects(parent_armature_object): + armature_modifier: bpy.types.ArmatureModifier = mesh.modifiers["mmd_bone_order_override"] if "mmd_bone_order_override" in mesh.modifiers else mesh.modifiers.new("mmd_bone_order_override", "ARMATURE") if armature_modifier.object is None: armature_modifier.object = parent_armature_object mesh.matrix_parent_inverse = child_armature_matrix - child_rigid_group_object = FnModel.find_rigid_group(child_root_object) + child_rigid_group_object = FnModel.find_rigid_group_object(child_root_object) if child_rigid_group_object is not None: - parent_rigid_group_object = FnModel.find_rigid_group(parent_root_object) - bpy.ops.object.parent_set({ - 'object': parent_rigid_group_object, - 'selected_editable_objects': [parent_rigid_group_object, *FnModel.iterate_rigid_body_objects(child_root_object)], - }, type='OBJECT', keep_transform=True) + parent_rigid_group_object = FnModel.find_rigid_group_object(parent_root_object) + + with bpy.context.temp_override( + object=parent_rigid_group_object, + selected_editable_objects=[parent_rigid_group_object, *FnModel.iterate_rigid_body_objects(child_root_object)], + ): + bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.data.objects.remove(child_rigid_group_object) - child_joint_group_object = FnModel.find_joint_group(child_root_object) + child_joint_group_object = FnModel.find_joint_group_object(child_root_object) if child_joint_group_object is not None: - parent_joint_group_object = FnModel.find_joint_group(parent_root_object) - bpy.ops.object.parent_set({ - 'object': parent_joint_group_object, - 'selected_editable_objects': [parent_joint_group_object, *FnModel.iterate_joint_objects(child_root_object)], - }, type='OBJECT', keep_transform=True) + parent_joint_group_object = FnModel.find_joint_group_object(parent_root_object) + with bpy.context.temp_override( + object=parent_joint_group_object, + selected_editable_objects=[parent_joint_group_object, *FnModel.iterate_joint_objects(child_root_object)], + ): + bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) bpy.data.objects.remove(child_joint_group_object) - child_temporary_group_object = FnModel.find_temporary_group(child_root_object) + child_temporary_group_object = FnModel.find_temporary_group_object(child_root_object) if child_temporary_group_object is not None: - parent_temporary_group_object = FnModel.find_temporary_group(parent_root_object) - bpy.ops.object.parent_set({ - 'object': parent_temporary_group_object, - 'selected_editable_objects': [parent_temporary_group_object, *FnModel.iterate_temporary_objects(child_root_object)], - }, type='OBJECT', keep_transform=True) - - for obj in list(FnModel.all_children(child_temporary_group_object)): + parent_temporary_group_object = FnModel.find_temporary_group_object(parent_root_object) + with bpy.context.temp_override( + object=parent_temporary_group_object, + selected_editable_objects=[parent_temporary_group_object, *FnModel.iterate_temporary_objects(child_root_object)], + ): + bpy.ops.object.parent_set(type="OBJECT", keep_transform=True) + + for obj in list(FnModel.iterate_child_objects(child_temporary_group_object)): bpy.data.objects.remove(obj) bpy.data.objects.remove(child_temporary_group_object) @@ -358,20 +349,24 @@ def _change_bone_id(bone: bpy.types.PoseBone, new_bone_id: int, bone_morphs, pos @staticmethod def _add_armature_modifier(mesh_object: bpy.types.Object, armature_object: bpy.types.Object) -> bpy.types.ArmatureModifier: - if any(m.type == 'ARMATURE' for m in mesh_object.modifiers): + for m in mesh_object.modifiers: + if m.type != "ARMATURE": + continue # already has armature modifier. - return + return cast(bpy.types.ArmatureModifier, m) - modifier: bpy.types.ArmatureModifier = mesh_object.modifiers.new(name='Armature', type='ARMATURE') + modifier = cast(bpy.types.ArmatureModifier, mesh_object.modifiers.new(name="Armature", type="ARMATURE")) modifier.object = armature_object modifier.use_vertex_groups = True - modifier.name = 'mmd_bone_order_override' + modifier.name = "mmd_bone_order_override" return modifier @staticmethod - def attach_meshes(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool): - armature_object: bpy.types.Object = FnModel.find_armature(parent_root_object) + def attach_mesh_objects(parent_root_object: bpy.types.Object, mesh_objects: Iterable[bpy.types.Object], add_armature_modifier: bool): + armature_object = FnModel.find_armature_object(parent_root_object) + if armature_object is None: + raise ValueError(f"Armature object not found in {parent_root_object}") def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object: if obj.parent is None: @@ -379,16 +374,15 @@ def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object: return __get_root_object(obj.parent) for mesh_object in mesh_objects: - if mesh_object.type != 'MESH': - continue - if mesh_object.mmd_type != 'NONE': + if not FnModel.is_mesh_object(mesh_object): continue - if FnModel.find_root(mesh_object) is not None: + + if FnModel.find_root_object(mesh_object) is not None: continue mesh_root_object = __get_root_object(mesh_object) original_matrix_world = mesh_root_object.matrix_world - mesh_root_object.parent_type = 'OBJECT' + mesh_root_object.parent_type = "OBJECT" mesh_root_object.parent = armature_object mesh_root_object.matrix_world = original_matrix_world @@ -397,15 +391,17 @@ def __get_root_object(obj: bpy.types.Object) -> bpy.types.Object: @staticmethod def add_missing_vertex_groups_from_bones(root_object: bpy.types.Object, mesh_object: bpy.types.Object, search_in_all_meshes: bool): + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + raise ValueError(f"Armature object not found in {root_object}") + vertex_group_names: Set[str] = set() - search_meshes = FnModel.child_meshes(root_object) if search_in_all_meshes else [mesh_object] + search_meshes = FnModel.iterate_mesh_objects(root_object) if search_in_all_meshes else [mesh_object] for search_mesh in search_meshes: vertex_group_names.update(search_mesh.vertex_groups.keys()) - armature_object = FnModel.find_armature(root_object) - pose_bone: bpy.types.PoseBone for pose_bone in armature_object.pose.bones: pose_bone_name = pose_bone.name @@ -413,7 +409,7 @@ def add_missing_vertex_groups_from_bones(root_object: bpy.types.Object, mesh_obj if pose_bone_name in vertex_group_names: continue - if pose_bone_name.startswith('_'): + if pose_bone_name.startswith("_"): continue mesh_object.vertex_groups.new(name=pose_bone_name) @@ -426,147 +422,207 @@ def change_mmd_ik_loop_factor(root_object: bpy.types.Object, new_ik_loop_factor: if new_ik_loop_factor == old_ik_loop_factor: return - armature_object = FnModel.find_armature(root_object) + armature_object = FnModel.find_armature_object(root_object) for pose_bone in armature_object.pose.bones: - constraint: bpy.types.KinematicConstraint - for constraint in (c for c in pose_bone.constraints if c.type == 'IK'): + for constraint in (cast(bpy.types.KinematicConstraint, c) for c in pose_bone.constraints if c.type == "IK"): iterations = int(constraint.iterations * new_ik_loop_factor / old_ik_loop_factor) - logging.info('Update %s of %s: %d -> %d', constraint.name, pose_bone.name, constraint.iterations, iterations) + logging.info("Update %s of %s: %d -> %d", constraint.name, pose_bone.name, constraint.iterations, iterations) constraint.iterations = iterations mmd_root.ik_loop_factor = new_ik_loop_factor return + @staticmethod + def __copy_property_group(destination: bpy.types.PropertyGroup, source: bpy.types.PropertyGroup, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]): + destination_rna_properties = destination.bl_rna.properties + for name in source.keys(): + is_attr = hasattr(source, name) + value = getattr(source, name) if is_attr else source[name] + if isinstance(value, bpy.types.PropertyGroup): + FnModel.__copy_property_group(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) + elif isinstance(value, bpy.types.bpy_prop_collection): + FnModel.__copy_collection_property(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) + elif isinstance(value, idprop.types.IDPropertyArray): + pass + # _copy_collection_property(getattr(destination, name) if is_attr else destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) + else: + value2values = replace_name2values.get(name) + if value2values is not None: + replace_value = value2values.get(value) + if replace_value is not None: + value = replace_value + + if overwrite or destination_rna_properties[name].default == getattr(destination, name) if is_attr else destination[name]: + if is_attr: + setattr(destination, name, value) + else: + destination[name] = value + + @staticmethod + def __copy_collection_property(destination: bpy.types.bpy_prop_collection, source: bpy.types.bpy_prop_collection, overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]): + if overwrite: + destination.clear() + + len_source = len(source) + if len_source == 0: + return + + source_names: Set[str] = set(source.keys()) + if len(source_names) == len_source and source[0].name != "": + # names work + destination_names: Set[str] = set(destination.keys()) + + missing_names = source_names - destination_names + + destination_index = 0 + for name, value in source.items(): + if name in missing_names: + new_element = destination.add() + new_element["name"] = name + + FnModel.__copy_property(destination[name], value, overwrite=overwrite, replace_name2values=replace_name2values) + destination.move(destination.find(name), destination_index) + destination_index += 1 + else: + # names not work + while len_source > len(destination): + destination.add() + + for index, name in enumerate(source.keys()): + FnModel.__copy_property(destination[index], source[index], overwrite=True, replace_name2values=replace_name2values) + + @staticmethod + def __copy_property(destination: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], source: Union[bpy.types.PropertyGroup, bpy.types.bpy_prop_collection], overwrite: bool, replace_name2values: Dict[str, Dict[Any, Any]]): + if isinstance(destination, bpy.types.PropertyGroup): + FnModel.__copy_property_group(destination, source, overwrite=overwrite, replace_name2values=replace_name2values) + elif isinstance(destination, bpy.types.bpy_prop_collection): + FnModel.__copy_collection_property(destination, source, overwrite=overwrite, replace_name2values=replace_name2values) + else: + raise ValueError(f"Unsupported destination: {destination}") + + @staticmethod + def initalize_display_item_frames(root_object: bpy.types.Object, reset: bool = True): + frames = root_object.mmd_root.display_item_frames + if reset and len(frames) > 0: + root_object.mmd_root.active_display_item_frame = 0 + frames.clear() + + frame_names = {"Root": "Root", "表情": "Facial"} + + for frame_name, frame_name_e in frame_names.items(): + frame = frames.get(frame_name, None) or frames.add() + frame.name = frame_name + frame.name_e = frame_name_e + frame.is_special = True + + arm = FnModel.find_armature_object(root_object) + if arm is not None and len(arm.data.bones) > 0 and len(frames[0].data) < 1: + item = frames[0].data.add() + item.type = "BONE" + item.name = arm.data.bones[0].name + + if not reset: + frames.move(frames.find("Root"), 0) + frames.move(frames.find("表情"), 1) + + @staticmethod + def get_empty_display_size(root_object: bpy.types.Object) -> float: + return getattr(root_object, Props.empty_display_size) + + class MigrationFnModel: """Migration Functions for old MMD models broken by bugs or issues""" @classmethod def update_mmd_ik_loop_factor(cls): for armature_object in bpy.data.objects: - if armature_object.type != 'ARMATURE': + if armature_object.type != "ARMATURE": continue - if 'mmd_ik_loop_factor' not in armature_object: + if "mmd_ik_loop_factor" not in armature_object: return - FnModel.find_root(armature_object).mmd_root.ik_loop_factor = max(armature_object['mmd_ik_loop_factor'], 1) - del armature_object['mmd_ik_loop_factor'] + FnModel.find_root_object(armature_object).mmd_root.ik_loop_factor = max(armature_object["mmd_ik_loop_factor"], 1) + del armature_object["mmd_ik_loop_factor"] @staticmethod def update_mmd_tools_version(): for root_object in bpy.data.objects: - if root_object.type != 'EMPTY': + if root_object.type != "EMPTY": continue if not FnModel.is_root_object(root_object): continue - if 'mmd_tools_version' in root_object: + if "mmd_tools_version" in root_object: continue - root_object['mmd_tools_version'] = '2.8.0' - - -# SUPPORT_UNTIL: 4.3 LTS -def isRigidBodyObject(obj): - return FnModel.is_rigid_body_object(obj) - -# SUPPORT_UNTIL: 4.3 LTS -def isJointObject(obj): - return FnModel.is_joint_object(obj) - -# SUPPORT_UNTIL: 4.3 LTS -def isTemporaryObject(obj): - return FnModel.is_temporary_object(obj) - -# SUPPORT_UNTIL: 4.3 LTS -def getRigidBodySize(obj): - return FnModel.get_rigid_body_size(obj) + root_object["mmd_tools_version"] = "2.8.0" class Model: def __init__(self, root_obj): - if root_obj.mmd_type != 'ROOT': - raise ValueError('must be MMD ROOT type object') - self.__root: bpy.types.Object = getattr(root_obj, 'original', root_obj) + if root_obj.mmd_type != "ROOT": + raise ValueError("must be MMD ROOT type object") + self.__root: bpy.types.Object = getattr(root_obj, "original", root_obj) self.__arm: Optional[bpy.types.Object] = None self.__rigid_grp: Optional[bpy.types.Object] = None self.__joint_grp: Optional[bpy.types.Object] = None self.__temporary_grp: Optional[bpy.types.Object] = None @staticmethod - def create(name, name_e='', scale=1, obj_name=None, armature=None, add_root_bone=False): - scene = SceneOp(bpy.context) + def create(name: str, name_e: str = "", scale: float = 1, obj_name: Optional[str] = None, armature_object: Optional[bpy.types.Object] = None, add_root_bone: bool = False): if obj_name is None: obj_name = name - root = bpy.data.objects.new(name=obj_name, object_data=None) - root.mmd_type = 'ROOT' + context = FnContext.ensure_context() + + root: bpy.types.Object = bpy.data.objects.new(name=obj_name, object_data=None) + root.mmd_type = "ROOT" root.mmd_root.name = name root.mmd_root.name_e = name_e - root['mmd_tools_version'] = MMD_TOOLS_VERSION - setattr(root, Props.empty_display_size, scale/0.2) - scene.link_object(root) - - armObj = armature - if armObj: - m = armObj.matrix_world - armObj.parent_type = 'OBJECT' - armObj.parent = root - #armObj.matrix_world = m + root["mmd_tools_version"] = MMD_TOOLS_VERSION + setattr(root, Props.empty_display_size, scale / 0.2) + FnContext.link_object(context, root) + + if armature_object: + m = armature_object.matrix_world + armature_object.parent_type = "OBJECT" + armature_object.parent = root + # armature_object.matrix_world = m root.matrix_world = m - armObj.matrix_local.identity() + armature_object.matrix_local.identity() else: - arm = bpy.data.armatures.new(name=obj_name) - armObj = bpy.data.objects.new(name=obj_name+'_arm', object_data=arm) - armObj.parent = root - scene.link_object(armObj) - armObj.lock_rotation = armObj.lock_location = armObj.lock_scale = [True, True, True] - setattr(armObj, Props.show_in_front, True) - setattr(armObj, Props.display_type, 'WIRE') + armature_object = bpy.data.objects.new(name=obj_name + "_arm", object_data=bpy.data.armatures.new(name=obj_name)) + armature_object.parent = root + FnContext.link_object(context, armature_object) + armature_object.lock_rotation = armature_object.lock_location = armature_object.lock_scale = [True, True, True] + setattr(armature_object, Props.show_in_front, True) + setattr(armature_object, Props.display_type, "WIRE") + + from .bone import FnBone + + FnBone.setup_special_bone_collections(armature_object) if add_root_bone: - bone_name = u'全ての親' - with bpyutils.edit_object(armObj) as data: + bone_name = "全ての親" + with bpyutils.edit_object(armature_object) as data: bone = data.edit_bones.new(name=bone_name) bone.head = [0.0, 0.0, 0.0] bone.tail = [0.0, 0.0, getattr(root, Props.empty_display_size)] - armObj.pose.bones[bone_name].mmd_bone.name_j = bone_name - armObj.pose.bones[bone_name].mmd_bone.name_e = 'Root' + armature_object.pose.bones[bone_name].mmd_bone.name_j = bone_name + armature_object.pose.bones[bone_name].mmd_bone.name_e = "Root" - bpyutils.select_object(root) + FnContext.set_active_and_select_single_object(context, root) return Model(root) - @classmethod - def findRoot(cls, obj): - return FnModel.find_root(obj) + @staticmethod + def findRoot(obj: bpy.types.Object) -> Optional[bpy.types.Object]: + return FnModel.find_root_object(obj) def initialDisplayFrames(self, reset=True): - frames = self.__root.mmd_root.display_item_frames - if reset and len(frames): - self.__root.mmd_root.active_display_item_frame = 0 - frames.clear() - - frame_root = frames.get('Root', None) or frames.add() - frame_root.name = 'Root' - frame_root.name_e = 'Root' - frame_root.is_special = True - - frame_facial = frames.get(u'表情', None) or frames.add() - frame_facial.name = u'表情' - frame_facial.name_e = 'Facial' - frame_facial.is_special = True - - arm = self.armature() - if arm and len(arm.data.bones) and len(frame_root.data) < 1: - item = frame_root.data.add() - item.type = 'BONE' - item.name = arm.data.bones[0].name - - if not reset: - frames.move(frames.find('Root'), 0) - frames.move(frames.find(u'表情'), 1) + FnModel.initalize_display_item_frames(self.__root, reset=reset) @property def morph_slider(self): @@ -575,218 +631,20 @@ def morph_slider(self): def loadMorphs(self): FnMorph.load_morphs(self) - def createRigidBodyPool(self, counts: int) -> List[bpy.types.Object]: - if counts < 1: - return [] - obj = bpyutils.createObject(name='Rigidbody', object_data=bpy.data.meshes.new(name='Rigidbody')) - obj.parent = self.rigidGroupObject() - obj.mmd_type = 'RIGID_BODY' - obj.rotation_mode = 'YXZ' - setattr(obj, Props.display_type, 'SOLID') - obj.show_transparent = True - obj.hide_render = True - if hasattr(obj, 'display'): - obj.display.show_shadows = False - if hasattr(obj, 'cycles_visibility'): - for attr_name in ('camera', 'diffuse', 'glossy', 'scatter', 'shadow', 'transmission'): - if hasattr(obj.cycles_visibility, attr_name): - setattr(obj.cycles_visibility, attr_name, False) - - if bpy.app.version < (2, 71, 0): - obj.mmd_rigid.shape = 'BOX' - obj.mmd_rigid.size = (1, 1, 1) - bpy.ops.rigidbody.object_add(type='ACTIVE') - if counts == 1: - return [obj] - return bpyutils.duplicateObject(obj, counts) - - def createRigidBody(self, **kwargs): - ''' Create a object for MMD rigid body dynamics. - ### Parameters ### - @param shape_type the shape type. - @param location location of the rigid body object. - @param rotation rotation of the rigid body object. - @param size - @param dynamics_type the type of dynamics mode. (STATIC / DYNAMIC / DYNAMIC2) - @param collision_group_number - @param collision_group_mask list of boolean values. (length:16) - @param name Object name (Optional) - @param name_e English object name (Optional) - @param bone - ''' - - shape_type = kwargs['shape_type'] - location = kwargs['location'] - rotation = kwargs['rotation'] - size = kwargs['size'] - dynamics_type = kwargs['dynamics_type'] - collision_group_number = kwargs.get('collision_group_number') - collision_group_mask = kwargs.get('collision_group_mask') - name = kwargs.get('name') - name_e = kwargs.get('name_e') - bone = kwargs.get('bone') - - friction = kwargs.get('friction') - mass = kwargs.get('mass') - angular_damping = kwargs.get('angular_damping') - linear_damping = kwargs.get('linear_damping') - bounce = kwargs.get('bounce') - - obj: Optional[bpy.types.Object] = kwargs.get('obj', None) - if obj is None: - obj, = self.createRigidBodyPool(1) - - obj.location = location - obj.rotation_euler = rotation - - obj.mmd_rigid.shape = rigid_body.collisionShape(shape_type) - obj.mmd_rigid.size = size - obj.mmd_rigid.type = str(dynamics_type) if dynamics_type in range(3) else '1' - - if collision_group_number is not None: - obj.mmd_rigid.collision_group_number = collision_group_number - if collision_group_mask is not None: - obj.mmd_rigid.collision_group_mask = collision_group_mask - if name is not None: - obj.name = name - obj.mmd_rigid.name_j = name - if name_e is not None: - obj.mmd_rigid.name_e = name_e - - obj.mmd_rigid.bone = bone if bone else '' - - rb = obj.rigid_body - if friction is not None: - rb.friction = friction - if mass is not None: - rb.mass = mass - if angular_damping is not None: - rb.angular_damping = angular_damping - if linear_damping is not None: - rb.linear_damping = linear_damping - if bounce: - rb.restitution = bounce - - obj.select = False - return obj - - def createJointPool(self, counts): - if counts < 1: - return [] - obj = bpyutils.createObject(name='Joint', object_data=None) - obj.parent = self.jointGroupObject() - obj.mmd_type = 'JOINT' - obj.rotation_mode = 'YXZ' - setattr(obj, Props.empty_display_type, 'ARROWS') - setattr(obj, Props.empty_display_size, 0.1*getattr(self.__root, Props.empty_display_size)) - obj.hide_render = True - - if bpy.ops.rigidbody.world_add.poll(): - bpy.ops.rigidbody.world_add() - bpy.ops.rigidbody.constraint_add(type='GENERIC_SPRING') - rbc = obj.rigid_body_constraint - rbc.disable_collisions = False - rbc.use_limit_ang_x = True - rbc.use_limit_ang_y = True - rbc.use_limit_ang_z = True - rbc.use_limit_lin_x = True - rbc.use_limit_lin_y = True - rbc.use_limit_lin_z = True - rbc.use_spring_x = True - rbc.use_spring_y = True - rbc.use_spring_z = True - if hasattr(rbc, 'use_spring_ang_x'): - rbc.use_spring_ang_x = True - rbc.use_spring_ang_y = True - rbc.use_spring_ang_z = True - if counts == 1: - return [obj] - return bpyutils.duplicateObject(obj, counts) - - def createJoint(self, **kwargs): - ''' Create a joint object for MMD rigid body dynamics. - ### Parameters ### - @param shape_type the shape type. - @param location location of the rigid body object. - @param rotation rotation of the rigid body object. - @param size - @param dynamics_type the type of dynamics mode. (STATIC / DYNAMIC / DYNAMIC2) - @param collision_group_number - @param collision_group_mask list of boolean values. (length:16) - @param name Object name - @param name_e English object name (Optional) - @param bone - ''' - - location = kwargs['location'] - rotation = kwargs['rotation'] - - rigid_a = kwargs['rigid_a'] - rigid_b = kwargs['rigid_b'] - - max_loc = kwargs['maximum_location'] - min_loc = kwargs['minimum_location'] - max_rot = kwargs['maximum_rotation'] - min_rot = kwargs['minimum_rotation'] - spring_angular = kwargs['spring_angular'] - spring_linear = kwargs['spring_linear'] - - name = kwargs['name'] - name_e = kwargs.get('name_e') - - obj = kwargs.get('obj', None) - if obj is None: - obj, = self.createJointPool(1) - - obj.name = 'J.' + name - obj.mmd_joint.name_j = name - if name_e is not None: - obj.mmd_joint.name_e = name_e - - obj.location = location - obj.rotation_euler = rotation - - rbc = obj.rigid_body_constraint - - rbc.object1 = rigid_a - rbc.object2 = rigid_b - - rbc.limit_lin_x_upper = max_loc[0] - rbc.limit_lin_y_upper = max_loc[1] - rbc.limit_lin_z_upper = max_loc[2] - - rbc.limit_lin_x_lower = min_loc[0] - rbc.limit_lin_y_lower = min_loc[1] - rbc.limit_lin_z_lower = min_loc[2] - - rbc.limit_ang_x_upper = max_rot[0] - rbc.limit_ang_y_upper = max_rot[1] - rbc.limit_ang_z_upper = max_rot[2] - - rbc.limit_ang_x_lower = min_rot[0] - rbc.limit_ang_y_lower = min_rot[1] - rbc.limit_ang_z_lower = min_rot[2] - - obj.mmd_joint.spring_linear = spring_linear - obj.mmd_joint.spring_angular = spring_angular - - obj.select = False - return obj - def create_ik_constraint(self, bone, ik_target): - """ create IK constraint + """create IK constraint - Args: - bone: A pose bone to add a IK constraint - id_target: A pose bone for IK target + Args: + bone: A pose bone to add a IK constraint + id_target: A pose bone for IK target - Returns: - The bpy.types.KinematicConstraint object created. It is set target - and subtarget options. + Returns: + The bpy.types.KinematicConstraint object created. It is set target + and subtarget options. """ ik_target_name = ik_target.name - ik_const = bone.constraints.new('IK') + ik_const = bone.constraints.new("IK") ik_const.target = self.__arm ik_const.subtarget = ik_target_name return ik_const @@ -795,90 +653,91 @@ def allObjects(self, obj: Optional[bpy.types.Object] = None) -> Iterator[bpy.typ if obj is None: obj: bpy.types.Object = self.__root yield obj - yield from FnModel.all_children(obj) + yield from FnModel.iterate_child_objects(obj) - def rootObject(self): + def rootObject(self) -> bpy.types.Object: return self.__root - def armature(self): + def armature(self) -> bpy.types.Object: if self.__arm is None: - self.__arm = FnModel.find_armature(self.__root) + self.__arm = FnModel.find_armature_object(self.__root) + assert self.__arm is not None return self.__arm - def hasRigidGroupObject(self): - return FnModel.find_rigid_group(self.__root) is not None + def hasRigidGroupObject(self) -> bool: + return FnModel.find_rigid_group_object(self.__root) is not None - def rigidGroupObject(self): + def rigidGroupObject(self) -> bpy.types.Object: if self.__rigid_grp is None: - self.__rigid_grp = FnModel.find_rigid_group(self.__root) + self.__rigid_grp = FnModel.find_rigid_group_object(self.__root) if self.__rigid_grp is None: - rigids = bpy.data.objects.new(name='rigidbodies', object_data=None) - SceneOp(bpy.context).link_object(rigids) - rigids.mmd_type = 'RIGID_GRP_OBJ' + rigids = bpy.data.objects.new(name="rigidbodies", object_data=None) + FnContext.link_object(FnContext.ensure_context(), rigids) + rigids.mmd_type = "RIGID_GRP_OBJ" rigids.parent = self.__root - rigids.hide = rigids.hide_select = True + rigids.hide_set(True) + rigids.hide_select = True rigids.lock_rotation = rigids.lock_location = rigids.lock_scale = [True, True, True] self.__rigid_grp = rigids return self.__rigid_grp - def hasJointGroupObject(self): - return FnModel.find_joint_group(self.__root) is not None + def hasJointGroupObject(self) -> bool: + return FnModel.find_joint_group_object(self.__root) is not None - def jointGroupObject(self): + def jointGroupObject(self) -> bpy.types.Object: if self.__joint_grp is None: - self.__joint_grp = FnModel.find_joint_group(self.__root) + self.__joint_grp = FnModel.find_joint_group_object(self.__root) if self.__joint_grp is None: - joints = bpy.data.objects.new(name='joints', object_data=None) - SceneOp(bpy.context).link_object(joints) - joints.mmd_type = 'JOINT_GRP_OBJ' + joints = bpy.data.objects.new(name="joints", object_data=None) + FnContext.link_object(FnContext.ensure_context(), joints) + joints.mmd_type = "JOINT_GRP_OBJ" joints.parent = self.__root - joints.hide = joints.hide_select = True + joints.hide_set(True) + joints.hide_select = True joints.lock_rotation = joints.lock_location = joints.lock_scale = [True, True, True] self.__joint_grp = joints return self.__joint_grp - def hasTemporaryGroupObject(self): - return FnModel.find_temporary_group(self.__root) is not None + def hasTemporaryGroupObject(self) -> bool: + return FnModel.find_temporary_group_object(self.__root) is not None - def temporaryGroupObject(self): + def temporaryGroupObject(self) -> bpy.types.Object: if self.__temporary_grp is None: - self.__temporary_grp = FnModel.find_temporary_group(self.__root) + self.__temporary_grp = FnModel.find_temporary_group_object(self.__root) if self.__temporary_grp is None: - temporarys = bpy.data.objects.new(name='temporary', object_data=None) - SceneOp(bpy.context).link_object(temporarys) - temporarys.mmd_type = 'TEMPORARY_GRP_OBJ' + temporarys = bpy.data.objects.new(name="temporary", object_data=None) + FnContext.link_object(FnContext.ensure_context(), temporarys) + temporarys.mmd_type = "TEMPORARY_GRP_OBJ" temporarys.parent = self.__root - temporarys.hide = temporarys.hide_select = True + temporarys.hide_set(True) + temporarys.hide_select = True temporarys.lock_rotation = temporarys.lock_location = temporarys.lock_scale = [True, True, True] self.__temporary_grp = temporarys return self.__temporary_grp - def meshes(self): - arm = self.armature() - if arm is None: - return [] - return FnModel.child_meshes(arm) + def meshes(self) -> Iterator[bpy.types.Object]: + return FnModel.iterate_mesh_objects(self.__root) def attachMeshes(self, meshes: Iterator[bpy.types.Object], add_armature_modifier: bool = True): - FnModel.attach_meshes(self.rootObject(), meshes, add_armature_modifier) + FnModel.attach_mesh_objects(self.rootObject(), meshes, add_armature_modifier) - def firstMesh(self): + def firstMesh(self) -> Optional[bpy.types.Object]: for i in self.meshes(): return i return None - def findMesh(self, mesh_name): + def findMesh(self, mesh_name) -> Optional[bpy.types.Object]: """ Helper method to find a mesh by name """ - if mesh_name == '': + if mesh_name == "": return None for mesh in self.meshes(): if mesh.name == mesh_name or mesh.data.name == mesh_name: return mesh return None - def findMeshByIndex(self, index): + def findMeshByIndex(self, index: int) -> Optional[bpy.types.Object]: """ Helper method to find the mesh by index """ @@ -889,34 +748,34 @@ def findMeshByIndex(self, index): return mesh return None - def getMeshIndex(self, mesh_name): + def getMeshIndex(self, mesh_name: str) -> int: """ Helper method to get the index of a mesh. Returns -1 if not found """ - if mesh_name == '': + if mesh_name == "": return -1 for i, mesh in enumerate(self.meshes()): if mesh.name == mesh_name or mesh.data.name == mesh_name: return i return -1 - def rigidBodies(self): + def rigidBodies(self) -> Iterator[bpy.types.Object]: return FnModel.iterate_rigid_body_objects(self.__root) - def joints(self): + def joints(self) -> Iterator[bpy.types.Object]: return FnModel.iterate_joint_objects(self.__root) - def temporaryObjects(self, rigid_track_only=False): + def temporaryObjects(self, rigid_track_only=False) -> Iterator[bpy.types.Object]: return FnModel.iterate_temporary_objects(self.__root, rigid_track_only) - def materials(self): + def materials(self) -> Iterator[bpy.types.Material]: """ Helper method to list all materials in all meshes """ - materials = {} # Use dict instead of set to guarantee preserve order + materials = {} # Use dict instead of set to guarantee preserve order for mesh in self.meshes(): - materials.update((slot.material,0) for slot in mesh.material_slots if slot.material is not None) - return list(materials.keys()) + materials.update((slot.material, 0) for slot in mesh.material_slots if slot.material is not None) + return iter(materials.keys()) def renameBone(self, old_bone_name, new_bone_name): if old_bone_name == new_bone_name: @@ -929,34 +788,34 @@ def renameBone(self, old_bone_name, new_bone_name): mmd_root = self.rootObject().mmd_root for frame in mmd_root.display_item_frames: for item in frame.data: - if item.type == 'BONE' and item.name == old_bone_name: + if item.type == "BONE" and item.name == old_bone_name: item.name = new_bone_name for mesh in self.meshes(): if old_bone_name in mesh.vertex_groups: mesh.vertex_groups[old_bone_name].name = new_bone_name - def build(self, non_collision_distance_scale = 1.5, collision_margin = 1e-06): + def build(self, non_collision_distance_scale=1.5, collision_margin=1e-06): rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False) if self.__root.mmd_root.is_built: self.clean() self.__root.mmd_root.is_built = True - logging.info('****************************************') - logging.info(' Build rig') - logging.info('****************************************') + logging.info("****************************************") + logging.info(" Build rig") + logging.info("****************************************") start_time = time.time() self.__preBuild() self.disconnectPhysicsBones() self.buildRigids(non_collision_distance_scale, collision_margin) self.buildJoints() self.__postBuild() - logging.info(' Finished building in %f seconds.', time.time() - start_time) + logging.info(" Finished building in %f seconds.", time.time() - start_time) rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled) def clean(self): rigidbody_world_enabled = rigid_body.setRigidBodyWorldEnabled(False) - logging.info('****************************************') - logging.info(' Clean rig') - logging.info('****************************************') + logging.info("****************************************") + logging.info(" Clean rig") + logging.info("****************************************") start_time = time.time() pose_bones = [] @@ -964,28 +823,28 @@ def clean(self): if arm is not None: pose_bones = arm.pose.bones for i in pose_bones: - if 'mmd_tools_rigid_track' in i.constraints: - const = i.constraints['mmd_tools_rigid_track'] + if "mmd_tools_rigid_track" in i.constraints: + const = i.constraints["mmd_tools_rigid_track"] i.constraints.remove(const) rigid_track_counts = 0 for i in self.rigidBodies(): rigid_type = int(i.mmd_rigid.type) - if 'mmd_tools_rigid_parent' not in i.constraints: + if "mmd_tools_rigid_parent" not in i.constraints: rigid_track_counts += 1 logging.info('%3d# Create a "CHILD_OF" constraint for %s', rigid_track_counts, i.name) i.mmd_rigid.bone = i.mmd_rigid.bone - relation = i.constraints['mmd_tools_rigid_parent'] + relation = i.constraints["mmd_tools_rigid_parent"] relation.mute = True if rigid_type == rigid_body.MODE_STATIC: - i.parent_type = 'OBJECT' + i.parent_type = "OBJECT" i.parent = self.rigidGroupObject() elif rigid_type in [rigid_body.MODE_DYNAMIC, rigid_body.MODE_DYNAMIC_BONE]: arm = relation.target bone_name = relation.subtarget - if arm is not None and bone_name != '': + if arm is not None and bone_name != "": for c in arm.pose.bones[bone_name].constraints: - if c.type == 'IK': + if c.type == "IK": c.mute = False self.__restoreTransforms(i) @@ -1003,65 +862,25 @@ def clean(self): mmd_root = self.rootObject().mmd_root if mmd_root.show_temporary_objects: mmd_root.show_temporary_objects = False - logging.info(' Finished cleaning in %f seconds.', time.time() - start_time) + logging.info(" Finished cleaning in %f seconds.", time.time() - start_time) mmd_root.is_built = False rigid_body.setRigidBodyWorldEnabled(rigidbody_world_enabled) def __removeTemporaryObjects(self): - if bpy.app.version < (2, 78, 0): - self.__removeChildrenOfTemporaryGroupObject() # for speeding up only - for i in self.temporaryObjects(): - bpy.context.scene.objects.unlink(i) - bpy.data.objects.remove(i) - elif bpy.app.version < (2, 80, 0): - for i in self.temporaryObjects(): - bpy.data.objects.remove(i, do_unlink=True) - elif bpy.app.version < (2, 81, 0): - tmp_objs = tuple(self.temporaryObjects()) - for i in tmp_objs: - for c in i.users_collection: - c.objects.unlink(i) - bpy.ops.object.delete({'selected_objects': tmp_objs, 'active_object': self.rootObject()}) - for i in tmp_objs: - if i.users < 1: - bpy.data.objects.remove(i) - else: - bpy.ops.object.delete({'selected_objects': tuple(self.temporaryObjects()), 'active_object': self.rootObject()}) - - def __removeChildrenOfTemporaryGroupObject(self): - tmp_grp_obj = self.temporaryGroupObject() - tmp_cnt = len(tmp_grp_obj.children) - if tmp_cnt == 0: - return - logging.debug(' Removing %d children of temporary group object', tmp_cnt) - start_time = time.time() - total_cnt = len(bpy.data.objects) - layer_index = bpy.context.scene.active_layer - try: - bpy.ops.object.mode_set(mode='OBJECT') - except Exception: - pass - for i in bpy.context.selected_objects: - i.select = False - for i in tmp_grp_obj.children: - i.hide_select = i.hide = False - i.select = i.layers[layer_index] = True - assert(len(bpy.context.selected_objects) == tmp_cnt) - bpy.ops.object.delete() - assert(len(bpy.data.objects) == total_cnt - tmp_cnt) - logging.debug(' - Done in %f seconds.', time.time() - start_time) + with bpy.context.temp_override(selected_objects=tuple(self.temporaryObjects()), active_object=self.rootObject()): + bpy.ops.object.delete() def __restoreTransforms(self, obj): - for attr in ('location', 'rotation_euler'): - attr_name = '__backup_%s__' % attr + for attr in ("location", "rotation_euler"): + attr_name = "__backup_%s__" % attr val = obj.get(attr_name, None) if val is not None: setattr(obj, attr, val) del obj[attr_name] def __backupTransforms(self, obj): - for attr in ('location', 'rotation_euler'): - attr_name = '__backup_%s__' % attr + for attr in ("location", "rotation_euler"): + attr_name = "__backup_%s__" % attr if attr_name in obj: # should not happen in normal build/clean cycle continue obj[attr_name] = getattr(obj, attr, None) @@ -1075,15 +894,15 @@ def __preBuild(self): for i in self.rigidBodies(): self.__backupTransforms(i) # mute relation - relation = i.constraints['mmd_tools_rigid_parent'] + relation = i.constraints["mmd_tools_rigid_parent"] relation.mute = True # mute IK if int(i.mmd_rigid.type) in [rigid_body.MODE_DYNAMIC, rigid_body.MODE_DYNAMIC_BONE]: arm = relation.target bone_name = relation.subtarget - if arm is not None and bone_name != '': + if arm is not None and bone_name != "": for c in arm.pose.bones[bone_name].constraints: - if c.type == 'IK': + if c.type == "IK": c.mute = True c.influence = c.influence # trigger update else: @@ -1107,7 +926,7 @@ def __preBuild(self): self.__fake_parent_map.setdefault(obj2, []).append(obj1) parented.append(obj1) - #assert(len(no_parents) == len(parented)) + # assert(len(no_parents) == len(parented)) def __postBuild(self): self.__fake_parent_map = None @@ -1126,26 +945,26 @@ def __postBuild(self): arm = self.armature() if arm: for p_bone in arm.pose.bones: - c = p_bone.constraints.get('mmd_tools_rigid_track', None) + c = p_bone.constraints.get("mmd_tools_rigid_track", None) if c: c.mute = False def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float): - assert(rigid_obj.mmd_type == 'RIGID_BODY') + assert rigid_obj.mmd_type == "RIGID_BODY" rb = rigid_obj.rigid_body if rb is None: return rigid = rigid_obj.mmd_rigid rigid_type = int(rigid.type) - relation = rigid_obj.constraints['mmd_tools_rigid_parent'] + relation = rigid_obj.constraints["mmd_tools_rigid_parent"] if relation.target is None: relation.target = self.armature() arm = relation.target if relation.subtarget not in arm.pose.bones: - bone_name = '' + bone_name = "" else: bone_name = relation.subtarget @@ -1160,52 +979,50 @@ def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float): rb.use_margin = True rb.collision_margin = collision_margin - if arm is not None and bone_name != '': + if arm is not None and bone_name != "": target_bone = arm.pose.bones[bone_name] if rigid_type == rigid_body.MODE_STATIC: - m = matmul(target_bone.matrix, target_bone.bone.matrix_local.inverted()) + m = target_bone.matrix @ target_bone.bone.matrix_local.inverted() self.__rigid_body_matrix_map[rigid_obj] = m orig_scale = rigid_obj.scale.copy() - to_matrix_world = matmul(rigid_obj.matrix_world, rigid_obj.matrix_local.inverted()) - matrix_world = matmul(to_matrix_world, matmul(m, rigid_obj.matrix_local)) + to_matrix_world = rigid_obj.matrix_world @ rigid_obj.matrix_local.inverted() + matrix_world = to_matrix_world @ (m @ rigid_obj.matrix_local) rigid_obj.parent = arm - rigid_obj.parent_type = 'BONE' + rigid_obj.parent_type = "BONE" rigid_obj.parent_bone = bone_name rigid_obj.matrix_world = matrix_world rigid_obj.scale = orig_scale - #relation.mute = False - #relation.inverse_matrix = matmul(arm.matrix_world, target_bone.bone.matrix_local).inverted() fake_children = self.__fake_parent_map.get(rigid_obj, None) if fake_children: for fake_child in fake_children: - logging.debug(' - fake_child: %s', fake_child.name) - t, r, s = matmul(m, fake_child.matrix_local).decompose() + logging.debug(" - fake_child: %s", fake_child.name) + t, r, s = (m @ fake_child.matrix_local).decompose() fake_child.location = t fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode) elif rigid_type in [rigid_body.MODE_DYNAMIC, rigid_body.MODE_DYNAMIC_BONE]: - m = matmul(target_bone.matrix, target_bone.bone.matrix_local.inverted()) + m = target_bone.matrix @ target_bone.bone.matrix_local.inverted() self.__rigid_body_matrix_map[rigid_obj] = m - t, r, s = matmul(m, rigid_obj.matrix_local).decompose() + t, r, s = (m @ rigid_obj.matrix_local).decompose() rigid_obj.location = t rigid_obj.rotation_euler = r.to_euler(rigid_obj.rotation_mode) fake_children = self.__fake_parent_map.get(rigid_obj, None) if fake_children: for fake_child in fake_children: - logging.debug(' - fake_child: %s', fake_child.name) - t, r, s = matmul(m, fake_child.matrix_local).decompose() + logging.debug(" - fake_child: %s", fake_child.name) + t, r, s = (m @ fake_child.matrix_local).decompose() fake_child.location = t fake_child.rotation_euler = r.to_euler(fake_child.rotation_mode) - if 'mmd_tools_rigid_track' not in target_bone.constraints: - empty = bpy.data.objects.new(name='mmd_bonetrack', object_data=None) - SceneOp(bpy.context).link_object(empty) + if "mmd_tools_rigid_track" not in target_bone.constraints: + empty = bpy.data.objects.new(name="mmd_bonetrack", object_data=None) + FnContext.link_object(FnContext.ensure_context(), empty) empty.matrix_world = target_bone.matrix - setattr(empty, Props.empty_display_type, 'ARROWS') - setattr(empty, Props.empty_display_size, 0.1*getattr(self.__root, Props.empty_display_size)) - empty.mmd_type = 'TRACK_TARGET' - empty.hide = True + setattr(empty, Props.empty_display_type, "ARROWS") + setattr(empty, Props.empty_display_size, 0.1 * getattr(self.__root, Props.empty_display_size)) + empty.mmd_type = "TRACK_TARGET" + empty.hide_set(True) empty.parent = self.temporaryGroupObject() rigid_obj.mmd_rigid.bone = bone_name @@ -1213,18 +1030,17 @@ def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float): self.__empty_parent_map[empty] = rigid_obj - const_type = ('COPY_TRANSFORMS', 'COPY_ROTATION')[rigid_type-1] + const_type = ("COPY_TRANSFORMS", "COPY_ROTATION")[rigid_type - 1] const = target_bone.constraints.new(const_type) const.mute = True - const.name = 'mmd_tools_rigid_track' + const.name = "mmd_tools_rigid_track" const.target = empty else: - empty = target_bone.constraints['mmd_tools_rigid_track'].target + empty = target_bone.constraints["mmd_tools_rigid_track"].target ori_rigid_obj = self.__empty_parent_map[empty] ori_rb = ori_rigid_obj.rigid_body if ori_rb and rb.mass > ori_rb.mass: - logging.debug(' * Bone (%s): change target from [%s] to [%s]', - target_bone.name, ori_rigid_obj.name, rigid_obj.name) + logging.debug(" * Bone (%s): change target from [%s] to [%s]", target_bone.name, ori_rigid_obj.name, rigid_obj.name) # re-parenting rigid_obj.mmd_rigid.bone = bone_name rigid_obj.constraints.remove(relation) @@ -1232,13 +1048,12 @@ def updateRigid(self, rigid_obj: bpy.types.Object, collision_margin: float): # revert change ori_rigid_obj.mmd_rigid.bone = bone_name else: - logging.debug(' * Bone (%s): track target [%s]', - target_bone.name, ori_rigid_obj.name) + logging.debug(" * Bone (%s): track target [%s]", target_bone.name, ori_rigid_obj.name) rb.collision_shape = rigid.shape def __getRigidRange(self, obj): - return (mathutils.Vector(obj.bound_box[0]) - mathutils.Vector(obj.bound_box[6])).length + return (Vector(obj.bound_box[0]) - Vector(obj.bound_box[6])).length def __createNonCollisionConstraint(self, nonCollisionJointTable): total_len = len(nonCollisionJointTable) @@ -1246,35 +1061,36 @@ def __createNonCollisionConstraint(self, nonCollisionJointTable): return start_time = time.time() - logging.debug('-'*60) - logging.debug(' creating ncc, counts: %d', total_len) + logging.debug("-" * 60) + logging.debug(" creating ncc, counts: %d", total_len) - ncc_obj = bpyutils.createObject(name='ncc', object_data=None) + ncc_obj = bpyutils.createObject(name="ncc", object_data=None) ncc_obj.location = [0, 0, 0] - setattr(ncc_obj, Props.empty_display_type, 'ARROWS') - setattr(ncc_obj, Props.empty_display_size, 0.5*getattr(self.__root, Props.empty_display_size)) - ncc_obj.mmd_type = 'NON_COLLISION_CONSTRAINT' + setattr(ncc_obj, Props.empty_display_type, "ARROWS") + setattr(ncc_obj, Props.empty_display_size, 0.5 * getattr(self.__root, Props.empty_display_size)) + ncc_obj.mmd_type = "NON_COLLISION_CONSTRAINT" ncc_obj.hide_render = True ncc_obj.parent = self.temporaryGroupObject() - bpy.ops.rigidbody.constraint_add(type='GENERIC') + bpy.ops.rigidbody.constraint_add(type="GENERIC") rb = ncc_obj.rigid_body_constraint rb.disable_collisions = True ncc_objs = bpyutils.duplicateObject(ncc_obj, total_len) - logging.debug(' created %d ncc.', len(ncc_objs)) + logging.debug(" created %d ncc.", len(ncc_objs)) for ncc_obj, pair in zip(ncc_objs, nonCollisionJointTable): rbc = ncc_obj.rigid_body_constraint rbc.object1, rbc.object2 = pair - ncc_obj.hide = ncc_obj.hide_select = True - logging.debug(' finish in %f seconds.', time.time() - start_time) - logging.debug('-'*60) + ncc_obj.hide_set(True) + ncc_obj.hide_select = True + logging.debug(" finish in %f seconds.", time.time() - start_time) + logging.debug("-" * 60) def buildRigids(self, non_collision_distance_scale, collision_margin): - logging.debug('--------------------------------') - logging.debug(' Build riggings of rigid bodies') - logging.debug('--------------------------------') + logging.debug("--------------------------------") + logging.debug(" Build riggings of rigid bodies") + logging.debug("--------------------------------") rigid_objects = list(self.rigidBodies()) rigid_object_groups = [[] for i in range(16)] for i in rigid_objects: @@ -1288,7 +1104,7 @@ def buildRigids(self, non_collision_distance_scale, collision_margin): rbc.disable_collisions = False jointMap[frozenset((rbc.object1, rbc.object2))] = joint - logging.info('Creating non collision constraints') + logging.info("Creating non collision constraints") # create non collision constraints nonCollisionJointTable = [] non_collision_pairs = set() @@ -1312,7 +1128,7 @@ def buildRigids(self, non_collision_distance_scale, collision_margin): nonCollisionJointTable.append((obj_a, obj_b)) non_collision_pairs.add(pair) for cnt, i in enumerate(rigid_objects): - logging.info('%3d/%3d: Updating rigid body %s', cnt+1, rigid_object_cnt, i.name) + logging.info("%3d/%3d: Updating rigid body %s", cnt + 1, rigid_object_cnt, i.name) self.updateRigid(i, collision_margin) self.__createNonCollisionConstraint(nonCollisionJointTable) return rigid_objects @@ -1327,22 +1143,10 @@ def buildJoints(self): m = self.__rigid_body_matrix_map.get(rbc.object2, None) if m is None: continue - t, r, s = matmul(m, i.matrix_local).decompose() + t, r, s = (m @ i.matrix_local).decompose() i.location = t i.rotation_euler = r.to_euler(i.rotation_mode) - def cleanAdditionalTransformConstraints(self): - arm = self.armature() - if arm: - FnBone.clean_additional_transformation(arm) - - def applyAdditionalTransformConstraints(self): - arm = self.armature() - if not arm: - return - MigrationFnBone.fix_mmd_ik_limit_override(arm) - FnBone.apply_additional_transformation(arm) - def __editPhysicsBones(self, editor: Callable[[bpy.types.EditBone], None], target_modes: Set[str]): armature_object = self.armature() @@ -1364,19 +1168,19 @@ def __editPhysicsBones(self, editor: Callable[[bpy.types.EditBone], None], targe def disconnectPhysicsBones(self): def editor(edit_bone: bpy.types.EditBone): - rna_prop_ui.rna_idprop_ui_create(edit_bone, 'mmd_bone_use_connect', default=edit_bone.use_connect) + rna_prop_ui.rna_idprop_ui_create(edit_bone, "mmd_bone_use_connect", default=edit_bone.use_connect) edit_bone.use_connect = False self.__editPhysicsBones(editor, {str(MODE_DYNAMIC)}) def connectPhysicsBones(self): def editor(edit_bone: bpy.types.EditBone): - mmd_bone_use_connect_str: Optional[str] = edit_bone.get('mmd_bone_use_connect') + mmd_bone_use_connect_str: Optional[str] = edit_bone.get("mmd_bone_use_connect") if mmd_bone_use_connect_str is None: return - if not edit_bone.use_connect: # wasn't it overwritten? + if not edit_bone.use_connect: # wasn't it overwritten? edit_bone.use_connect = bool(mmd_bone_use_connect_str) - del edit_bone['mmd_bone_use_connect'] + del edit_bone["mmd_bone_use_connect"] self.__editPhysicsBones(editor, {str(MODE_STATIC), str(MODE_DYNAMIC), str(MODE_DYNAMIC_BONE)}) diff --git a/mmd_tools/core/morph.py b/mmd_tools/core/morph.py index 44e11b83..f5ed3509 100644 --- a/mmd_tools/core/morph.py +++ b/mmd_tools/core/morph.py @@ -1,15 +1,22 @@ # -*- coding: utf-8 -*- +# Copyright 2016 MMD Tools authors +# This file is part of MMD Tools. + import logging import re +from typing import TYPE_CHECKING, Tuple, cast import bpy -from mmd_tools import bpyutils -from mmd_tools.bpyutils import ObjectOp, SceneOp, TransformConstraintOp +from .. import bpyutils +from ..bpyutils import FnContext, FnObject, TransformConstraintOp + +if TYPE_CHECKING: + from .model import Model -class FnMorph(object): - def __init__(self, morph, model): +class FnMorph: + def __init__(self, morph, model: "Model"): self.__morph = morph self.__rig = model @@ -17,19 +24,13 @@ def __init__(self, morph, model): def storeShapeKeyOrder(cls, obj, shape_key_names): if len(shape_key_names) < 1: return - assert(SceneOp(bpy.context).active_object == obj) + assert FnContext.get_active_object(FnContext.ensure_context()) == obj if obj.data.shape_keys is None: bpy.ops.object.shape_key_add() - if bpy.app.version < (2, 73, 0): - def __move_to_bottom(key_blocks, name): - obj.active_shape_key_index = key_blocks.find(name) - for move in range(len(key_blocks)-1-obj.active_shape_key_index): - bpy.ops.object.shape_key_move(type='DOWN') - else: - def __move_to_bottom(key_blocks, name): - obj.active_shape_key_index = key_blocks.find(name) - bpy.ops.object.shape_key_move(type='BOTTOM') + def __move_to_bottom(key_blocks, name): + obj.active_shape_key_index = key_blocks.find(name) + bpy.ops.object.shape_key_move(type="BOTTOM") key_blocks = obj.data.shape_keys.key_blocks for name in shape_key_names: @@ -42,25 +43,16 @@ def __move_to_bottom(key_blocks, name): def fixShapeKeyOrder(cls, obj, shape_key_names): if len(shape_key_names) < 1: return - assert(SceneOp(bpy.context).active_object == obj) - key_blocks = getattr(obj.data.shape_keys, 'key_blocks', None) + assert FnContext.get_active_object(FnContext.ensure_context()) == obj + key_blocks = getattr(obj.data.shape_keys, "key_blocks", None) if key_blocks is None: return - if bpy.app.version < (2, 73, 0): - len_key_blocks = len(key_blocks) - for ii, name in enumerate(x for x in reversed(shape_key_names) if x in key_blocks): - obj.active_shape_key_index = idx = key_blocks.find(name) - offset = (len_key_blocks - 1 - idx) - ii - move_type = 'UP' if offset < 0 else 'DOWN' - for move in range(abs(offset)): - bpy.ops.object.shape_key_move(type=move_type) - else: - for name in shape_key_names: - idx = key_blocks.find(name) - if idx < 0: - continue - obj.active_shape_key_index = idx - bpy.ops.object.shape_key_move(type='BOTTOM') + for name in shape_key_names: + idx = key_blocks.find(name) + if idx < 0: + continue + obj.active_shape_key_index = idx + bpy.ops.object.shape_key_move(type="BOTTOM") @staticmethod def get_morph_slider(rig): @@ -69,13 +61,13 @@ def get_morph_slider(rig): @staticmethod def category_guess(morph): name_lower = morph.name.lower() - if 'mouth' in name_lower: - morph.category = 'MOUTH' - elif 'eye' in name_lower: - if 'brow' in name_lower: - morph.category = 'EYEBROW' + if "mouth" in name_lower: + morph.category = "MOUTH" + elif "eye" in name_lower: + if "brow" in name_lower: + morph.category = "EYEBROW" else: - morph.category = 'EYE' + morph.category = "EYE" @classmethod def load_morphs(cls, rig): @@ -83,8 +75,8 @@ def load_morphs(cls, rig): vertex_morphs = mmd_root.vertex_morphs uv_morphs = mmd_root.uv_morphs for obj in rig.meshes(): - for kb in getattr(obj.data.shape_keys, 'key_blocks', ())[1:]: - if not kb.name.startswith('mmd_') and kb.name not in vertex_morphs: + for kb in getattr(obj.data.shape_keys, "key_blocks", ())[1:]: + if not kb.name.startswith("mmd_") and kb.name not in vertex_morphs: item = vertex_morphs.add() item.name = kb.name item.name_e = kb.name @@ -93,31 +85,46 @@ def load_morphs(cls, rig): if name not in uv_morphs: item = uv_morphs.add() item.name = item.name_e = name - item.data_type = 'VERTEX_GROUP' + item.data_type = "VERTEX_GROUP" cls.category_guess(item) - @staticmethod - def remove_shape_key(obj, key_name): - key_blocks = getattr(obj.data.shape_keys, 'key_blocks', None) - if key_blocks and key_name in key_blocks: - ObjectOp(obj).shape_key_remove(key_blocks[key_name]) + def remove_shape_key(mesh_object: bpy.types.Object, shape_key_name: str): + assert isinstance(mesh_object.data, bpy.types.Mesh) + + shape_keys = mesh_object.data.shape_keys + if shape_keys is None: + return + + key_blocks = shape_keys.key_blocks + if key_blocks and shape_key_name in key_blocks: + FnObject.mesh_remove_shape_key(mesh_object, key_blocks[shape_key_name]) @staticmethod - def copy_shape_key(obj, src_name, dest_name): - key_blocks = getattr(obj.data.shape_keys, 'key_blocks', None) - if key_blocks and src_name in key_blocks: - if dest_name in key_blocks: - ObjectOp(obj).shape_key_remove(key_blocks[dest_name]) - obj.active_shape_key_index = key_blocks.find(src_name) - obj.show_only_shape_key, last = True, obj.show_only_shape_key - obj.shape_key_add(name=dest_name, from_mix=True) - obj.show_only_shape_key = last - obj.active_shape_key_index = key_blocks.find(dest_name) + def copy_shape_key(mesh_object: bpy.types.Object, src_name: str, dest_name: str): + assert isinstance(mesh_object.data, bpy.types.Mesh) + + shape_keys = mesh_object.data.shape_keys + if shape_keys is None: + return + + key_blocks = shape_keys.key_blocks + + if src_name not in key_blocks: + return + + if dest_name in key_blocks: + FnObject.mesh_remove_shape_key(mesh_object, key_blocks[dest_name]) + + mesh_object.active_shape_key_index = key_blocks.find(src_name) + mesh_object.show_only_shape_key, last = True, mesh_object.show_only_shape_key + mesh_object.shape_key_add(name=dest_name, from_mix=True) + mesh_object.show_only_shape_key = last + mesh_object.active_shape_key_index = key_blocks.find(dest_name) @staticmethod - def get_uv_morph_vertex_groups(obj, morph_name=None, offset_axes='XYZW'): - pattern = 'UV_%s[+-][%s]$'%(morph_name or '.{1,}', offset_axes or 'XYZW') + def get_uv_morph_vertex_groups(obj, morph_name=None, offset_axes="XYZW"): + pattern = "UV_%s[+-][%s]$" % (morph_name or ".{1,}", offset_axes or "XYZW") # yield (vertex_group, morph_name, axis),... return ((g, g.name[3:-2], g.name[-2:]) for g in obj.vertex_groups if re.match(pattern, g.name)) @@ -128,8 +135,8 @@ def copy_uv_morph_vertex_groups(obj, src_name, dest_name): for vg_name in tuple(i[0].name for i in FnMorph.get_uv_morph_vertex_groups(obj, src_name)): obj.vertex_groups.active = obj.vertex_groups[vg_name] - override = {'object':obj, 'window':bpy.context.window, 'region':bpy.context.region} - bpy.ops.object.vertex_group_copy(override) + with bpy.context.temp_override(object=obj, window=bpy.context.window, region=bpy.context.region): + bpy.ops.object.vertex_group_copy() obj.vertex_groups.active.name = vg_name.replace(src_name, dest_name) @staticmethod @@ -145,7 +152,7 @@ def overwrite_bone_morphs_from_pose_library(armature_object): bone_morphs = mmd_root.bone_morphs original_mode = bpy.context.object.mode - bpy.ops.object.mode_set(mode='POSE') + bpy.ops.object.mode_set(mode="POSE") try: for index, pose_maker in enumerate(pose_library.pose_markers): bone_morph = next(iter([m for m in bone_morphs if m.name == pose_maker.name]), None) @@ -153,7 +160,7 @@ def overwrite_bone_morphs_from_pose_library(armature_object): bone_morph = bone_morphs.add() bone_morph.name = pose_maker.name - bpy.ops.pose.select_all(action='SELECT') + bpy.ops.pose.select_all(action="SELECT") bpy.ops.pose.transforms_clear() bpy.ops.poselib.apply_pose(pose_index=index) @@ -176,44 +183,44 @@ def clean_uv_morph_vertex_groups(obj): vg_indices.remove(x.group) for i in sorted(vg_indices, reverse=True): vg = vertex_groups[i] - m = obj.modifiers.get('mmd_bind%s'%hash(vg.name), None) + m = obj.modifiers.get("mmd_bind%s" % hash(vg.name), None) if m: obj.modifiers.remove(m) vertex_groups.remove(vg) @staticmethod def get_uv_morph_offset_map(obj, morph): - offset_map = {} # offset_map[vertex_index] = offset_xyzw - if morph.data_type == 'VERTEX_GROUP': + offset_map = {} # offset_map[vertex_index] = offset_xyzw + if morph.data_type == "VERTEX_GROUP": scale = morph.vertex_group_scale - axis_map = {g.index:x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)} + axis_map = {g.index: x for g, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph.name)} for v in obj.data.vertices: i = v.index for x in v.groups: if x.group in axis_map and x.weight > 0: axis, weight = axis_map[x.group], x.weight d = offset_map.setdefault(i, [0, 0, 0, 0]) - d['XYZW'.index(axis[1])] += -weight*scale if axis[0] == '-' else weight*scale + d["XYZW".index(axis[1])] += -weight * scale if axis[0] == "-" else weight * scale else: for val in morph.data: i = val.index if i in offset_map: - offset_map[i] = [a+b for a, b in zip(offset_map[i], val.offset)] + offset_map[i] = [a + b for a, b in zip(offset_map[i], val.offset)] else: offset_map[i] = val.offset return offset_map @staticmethod - def store_uv_morph_data(obj, morph, offsets=None, offset_axes='XYZW'): + def store_uv_morph_data(obj, morph, offsets=None, offset_axes="XYZW"): vertex_groups = obj.vertex_groups - morph_name = getattr(morph, 'name', None) + morph_name = getattr(morph, "name", None) if offset_axes: for vg, n, x in FnMorph.get_uv_morph_vertex_groups(obj, morph_name, offset_axes): vertex_groups.remove(vg) if not morph_name or not offsets: return - axis_indices = tuple('XYZW'.index(x) for x in offset_axes) or tuple(range(4)) + axis_indices = tuple("XYZW".index(x) for x in offset_axes) or tuple(range(4)) offset_map = FnMorph.get_uv_morph_offset_map(obj, morph) if offset_axes else {} for data in offsets: idx, offset = data.index, data.offset @@ -223,20 +230,20 @@ def store_uv_morph_data(obj, morph, offsets=None, offset_axes='XYZW'): max_value = max(max(abs(x) for x in v) for v in offset_map.values() or ([0],)) scale = morph.vertex_group_scale = max(abs(morph.vertex_group_scale), max_value) for idx, offset in offset_map.items(): - for val, axis in zip(offset, 'XYZW'): + for val, axis in zip(offset, "XYZW"): if abs(val) > 1e-4: - vg_name = 'UV_{0}{1}{2}'.format(morph_name, '-' if val < 0 else '+', axis) + vg_name = "UV_{0}{1}{2}".format(morph_name, "-" if val < 0 else "+", axis) vg = vertex_groups.get(vg_name, None) or vertex_groups.new(name=vg_name) - vg.add(index=[idx], weight=abs(val)/scale, type='REPLACE') + vg.add(index=[idx], weight=abs(val) / scale, type="REPLACE") def update_mat_related_mesh(self, new_mesh=None): for offset in self.__morph.data: - # Use the new_mesh if provided - meshObj = new_mesh + # Use the new_mesh if provided + meshObj = new_mesh if new_mesh is None: # Try to find the mesh by material name meshObj = self.__rig.findMesh(offset.material) - + if meshObj is None: # Given this point we need to loop through all the meshes for mesh in self.__rig.meshes(): @@ -250,12 +257,12 @@ def update_mat_related_mesh(self, new_mesh=None): @staticmethod def clean_duplicated_material_morphs(mmd_root_object: bpy.types.Object): - """Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[] - """ + """Clean duplicated material_morphs and data from mmd_root_object.mmd_root.material_morphs[].data[]""" mmd_root = mmd_root_object.mmd_root def morph_data_equals(l, r) -> bool: - return (l.related_mesh_data == r.related_mesh_data + return ( + l.related_mesh_data == r.related_mesh_data and l.offset_type == r.offset_type and l.material == r.material and all(a == b for a, b in zip(l.diffuse_color, r.diffuse_color)) @@ -299,22 +306,22 @@ def morph_equals(l, r) -> bool: for material_morph_name in remove_material_morph_names: mmd_root.material_morphs.remove(mmd_root.material_morphs.find(material_morph_name)) -class _MorphSlider: - def __init__(self, model): +class _MorphSlider: + def __init__(self, model: "Model"): self.__rig = model def placeholder(self, create=False, binded=False): rig = self.__rig root = rig.rootObject() - obj = next((x for x in root.children if x.mmd_type == 'PLACEHOLDER' and x.type == 'MESH'), None) + obj = next((x for x in root.children if x.mmd_type == "PLACEHOLDER" and x.type == "MESH"), None) if create and obj is None: - obj = bpy.data.objects.new(name='.placeholder', object_data=bpy.data.meshes.new('.placeholder')) - obj.mmd_type = 'PLACEHOLDER' + obj = bpy.data.objects.new(name=".placeholder", object_data=bpy.data.meshes.new(".placeholder")) + obj.mmd_type = "PLACEHOLDER" obj.parent = root - SceneOp(bpy.context).link_object(obj) + FnContext.link_object(FnContext.ensure_context(), obj) if obj and obj.data.shape_keys is None: - key = obj.shape_key_add(name='--- morph sliders ---') + key = obj.shape_key_add(name="--- morph sliders ---") key.mute = True obj.active_shape_key_index = 0 if binded and obj and obj.data.shape_keys.key_blocks[0].mute: @@ -327,14 +334,17 @@ def dummy_armature(self): return self.__dummy_armature(obj) if obj else None def __dummy_armature(self, obj, create=False): - arm = next((x for x in obj.children if x.mmd_type == 'PLACEHOLDER' and x.type == 'ARMATURE'), None) + arm = next((x for x in obj.children if x.mmd_type == "PLACEHOLDER" and x.type == "ARMATURE"), None) if create and arm is None: - arm = bpy.data.objects.new(name='.dummy_armature', object_data=bpy.data.armatures.new(name='.dummy_armature')) - arm.mmd_type = 'PLACEHOLDER' + arm = bpy.data.objects.new(name=".dummy_armature", object_data=bpy.data.armatures.new(name=".dummy_armature")) + arm.mmd_type = "PLACEHOLDER" arm.parent = obj - SceneOp(bpy.context).link_object(arm) - return arm + FnContext.link_object(FnContext.ensure_context(), arm) + from .bone import FnBone + + FnBone.setup_special_bone_collections(arm) + return arm def get(self, morph_name): obj = self.placeholder() @@ -352,16 +362,15 @@ def create(self): return obj def __load(self, obj, mmd_root): - attr_list = ('group', 'vertex', 'bone', 'uv', 'material') + attr_list = ("group", "vertex", "bone", "uv", "material") morph_sliders = obj.data.shape_keys.key_blocks - for m in (x for attr in attr_list for x in getattr(mmd_root, attr+'_morphs', ())): + for m in (x for attr in attr_list for x in getattr(mmd_root, attr + "_morphs", ())): name = m.name - #if name[-1] == '\\': # fix driver's bug??? + # if name[-1] == '\\': # fix driver's bug??? # m.name = name = name + ' ' if name and name not in morph_sliders: obj.shape_key_add(name=name, from_mix=False) - @staticmethod def __driver_variables(id_data, path, index=-1): d = id_data.driver_add(path, index) @@ -373,10 +382,10 @@ def __driver_variables(id_data, path, index=-1): @staticmethod def __add_single_prop(variables, id_obj, data_path, prefix): var = variables.new() - var.name = f'{prefix}{len(variables)}' - var.type = 'SINGLE_PROP' + var.name = f"{prefix}{len(variables)}" + var.type = "SINGLE_PROP" target = var.targets[0] - target.id_type = 'OBJECT' + target.id_type = "OBJECT" target.id = id_obj target.data_path = data_path return var @@ -385,53 +394,59 @@ def __add_single_prop(variables, id_obj, data_path, prefix): def __shape_key_driver_check(key_block, resolve_path=False): if resolve_path: try: - kb = key_block.id_data.path_resolve(key_block.path_from_id()) - except ValueError as e: + key_block.id_data.path_resolve(key_block.path_from_id()) + except ValueError: return False if not key_block.id_data.animation_data: return True - d = key_block.id_data.animation_data.drivers.find(key_block.path_from_id('value')) - if isinstance(d, int): # for Blender 2.76 or older - data_path = key_block.path_from_id('value') + d = key_block.id_data.animation_data.drivers.find(key_block.path_from_id("value")) + if isinstance(d, int): # for Blender 2.76 or older + data_path = key_block.path_from_id("value") d = next((i for i in key_block.id_data.animation_data.drivers if i.data_path == data_path), None) - return (not d or d.driver.expression == ''.join(('*w','+g','v')[-1 if i < 1 else i%2]+str(i+1) for i in range(len(d.driver.variables)))) + return not d or d.driver.expression == "".join(("*w", "+g", "v")[-1 if i < 1 else i % 2] + str(i + 1) for i in range(len(d.driver.variables))) def __cleanup(self, names_in_use=None): from math import ceil, floor + names_in_use = names_in_use or {} rig = self.__rig morph_sliders = self.placeholder() morph_sliders = morph_sliders.data.shape_keys.key_blocks if morph_sliders else {} - for mesh in rig.meshes(): - for kb in getattr(mesh.data.shape_keys, 'key_blocks', ()): - if kb.name not in names_in_use: - if kb.name.startswith('mmd_bind'): - kb.driver_remove('value') - ms = morph_sliders[kb.relative_key.name] - kb.relative_key.slider_min, kb.relative_key.slider_max = min(ms.slider_min, floor(ms.value)), max(ms.slider_max, ceil(ms.value)) - kb.relative_key.value = ms.value - kb.relative_key.mute = False - ObjectOp(mesh).shape_key_remove(kb) - elif kb.name in morph_sliders and self.__shape_key_driver_check(kb): - ms = morph_sliders[kb.name] - kb.driver_remove('value') - kb.slider_min, kb.slider_max = min(ms.slider_min, floor(kb.value)), max(ms.slider_max, ceil(kb.value)) - for m in mesh.modifiers: # uv morph - if m.name.startswith('mmd_bind') and m.name not in names_in_use: - mesh.modifiers.remove(m) - - from mmd_tools.core.shader import _MaterialMorph + for mesh_object in rig.meshes(): + for kb in getattr(mesh_object.data.shape_keys, "key_blocks", cast(Tuple[bpy.types.ShapeKey], ())): + if kb.name in names_in_use: + continue + + if kb.name.startswith("mmd_bind"): + kb.driver_remove("value") + ms = morph_sliders[kb.relative_key.name] + kb.relative_key.slider_min, kb.relative_key.slider_max = min(ms.slider_min, floor(ms.value)), max(ms.slider_max, ceil(ms.value)) + kb.relative_key.value = ms.value + kb.relative_key.mute = False + FnObject.mesh_remove_shape_key(mesh_object, kb) + + elif kb.name in morph_sliders and self.__shape_key_driver_check(kb): + ms = morph_sliders[kb.name] + kb.driver_remove("value") + kb.slider_min, kb.slider_max = min(ms.slider_min, floor(kb.value)), max(ms.slider_max, ceil(kb.value)) + + for m in mesh_object.modifiers: # uv morph + if m.name.startswith("mmd_bind") and m.name not in names_in_use: + mesh_object.modifiers.remove(m) + + from .shader import _MaterialMorph + for m in rig.materials(): if m and m.node_tree: - for n in sorted((x for x in m.node_tree.nodes if x.name.startswith('mmd_bind')), key=lambda x: -x.location[0]): + for n in sorted((x for x in m.node_tree.nodes if x.name.startswith("mmd_bind")), key=lambda x: -x.location[0]): _MaterialMorph.reset_morph_links(n) m.node_tree.nodes.remove(n) - attributes = set(TransformConstraintOp.min_max_attributes('LOCATION', 'to')) - attributes |= set(TransformConstraintOp.min_max_attributes('ROTATION', 'to')) + attributes = set(TransformConstraintOp.min_max_attributes("LOCATION", "to")) + attributes |= set(TransformConstraintOp.min_max_attributes("ROTATION", "to")) for b in rig.armature().pose.bones: for c in b.constraints: - if c.name.startswith('mmd_bind') and c.name[:-4] not in names_in_use: + if c.name.startswith("mmd_bind") and c.name[:-4] not in names_in_use: for attr in attributes: c.driver_remove(attr) b.constraints.remove(c) @@ -444,19 +459,19 @@ def unbind(self): for m in mmd_root.bone_morphs: for d in m.data: - d.name = '' + d.name = "" for m in mmd_root.material_morphs: for d in m.data: - d.name = '' + d.name = "" obj = self.placeholder() if obj: obj.data.shape_keys.key_blocks[0].mute = True arm = self.__dummy_armature(obj) if arm: for b in arm.pose.bones: - if b.name.startswith('mmd_bind'): - b.driver_remove('location') - b.driver_remove('rotation_quaternion') + if b.name.startswith("mmd_bind"): + b.driver_remove("location") + b.driver_remove("rotation_quaternion") self.__cleanup() def bind(self): @@ -477,9 +492,9 @@ def bind(self): shape_key_map = {} uv_morph_map = {} - for mesh in rig.meshes(): - mesh.show_only_shape_key = False - key_blocks = getattr(mesh.data.shape_keys, 'key_blocks', ()) + for mesh_object in rig.meshes(): + mesh_object.show_only_shape_key = False + key_blocks = getattr(mesh_object.data.shape_keys, "key_blocks", ()) for kb in key_blocks: kb_name = kb.name if kb_name not in morph_sliders: @@ -488,50 +503,52 @@ def bind(self): if self.__shape_key_driver_check(kb, resolve_path=True): name_bind, kb_bind = kb_name, kb else: - name_bind = 'mmd_bind%s'%hash(morph_sliders[kb_name]) + name_bind = "mmd_bind%s" % hash(morph_sliders[kb_name]) if name_bind not in key_blocks: - mesh.shape_key_add(name=name_bind, from_mix=False) + mesh_object.shape_key_add(name=name_bind, from_mix=False) kb_bind = key_blocks[name_bind] kb_bind.relative_key = kb kb_bind.slider_min = -10 kb_bind.slider_max = 10 - data_path = 'data.shape_keys.key_blocks["%s"].value'%kb_name.replace('"', '\\"') + data_path = 'data.shape_keys.key_blocks["%s"].value' % kb_name.replace('"', '\\"') groups = [] shape_key_map.setdefault(name_bind, []).append((kb_bind, data_path, groups)) - group_map.setdefault(('vertex_morphs', kb_name), []).append(groups) + group_map.setdefault(("vertex_morphs", kb_name), []).append(groups) - uv_layers = [l.name for l in mesh.data.uv_layers if not l.name.startswith('_')] - uv_layers += ['']*(5-len(uv_layers)) - for vg, morph_name, axis in FnMorph.get_uv_morph_vertex_groups(mesh): + uv_layers = [l.name for l in mesh_object.data.uv_layers if not l.name.startswith("_")] + uv_layers += [""] * (5 - len(uv_layers)) + for vg, morph_name, axis in FnMorph.get_uv_morph_vertex_groups(mesh_object): morph = mmd_root.uv_morphs.get(morph_name, None) - if morph is None or morph.data_type != 'VERTEX_GROUP': + if morph is None or morph.data_type != "VERTEX_GROUP": continue - uv_layer = '_'+uv_layers[morph.uv_index] if axis[1] in 'ZW' else uv_layers[morph.uv_index] - if uv_layer not in mesh.data.uv_layers: + uv_layer = "_" + uv_layers[morph.uv_index] if axis[1] in "ZW" else uv_layers[morph.uv_index] + if uv_layer not in mesh_object.data.uv_layers: continue - name_bind = 'mmd_bind%s'%hash(vg.name) + name_bind = "mmd_bind%s" % hash(vg.name) uv_morph_map.setdefault(name_bind, ()) - mod = mesh.modifiers.get(name_bind, None) or mesh.modifiers.new(name=name_bind, type='UV_WARP') + mod = mesh_object.modifiers.get(name_bind, None) or mesh_object.modifiers.new(name=name_bind, type="UV_WARP") mod.show_expanded = False mod.vertex_group = vg.name - mod.axis_u, mod.axis_v = ('Y', 'X') if axis[1] in 'YW' else ('X', 'Y') + mod.axis_u, mod.axis_v = ("Y", "X") if axis[1] in "YW" else ("X", "Y") mod.uv_layer = uv_layer - name_bind = 'mmd_bind%s'%hash(morph_name) + name_bind = "mmd_bind%s" % hash(morph_name) mod.object_from = mod.object_to = arm - if axis[0] == '-': - mod.bone_from, mod.bone_to = 'mmd_bind_ctrl_base', name_bind + if axis[0] == "-": + mod.bone_from, mod.bone_to = "mmd_bind_ctrl_base", name_bind else: - mod.bone_from, mod.bone_to = name_bind, 'mmd_bind_ctrl_base' + mod.bone_from, mod.bone_to = name_bind, "mmd_bind_ctrl_base" bone_offset_map = {} with bpyutils.edit_object(arm) as data: + from .bone import FnBone + edit_bones = data.edit_bones - def __get_bone(name, layer, parent): + + def __get_bone(name, parent): b = edit_bones.get(name, None) or edit_bones.new(name=name) - b.layers = [x == layer for x in range(len(b.layers))] b.head = (0, 0, 0) b.tail = (0, 0, 1) b.use_deform = False @@ -539,81 +556,82 @@ def __get_bone(name, layer, parent): return b for m in mmd_root.bone_morphs: - data_path = 'data.shape_keys.key_blocks["%s"].value'%m.name.replace('"', '\\"') + morph_name = m.name.replace('"', '\\"') + data_path = f'data.shape_keys.key_blocks["{morph_name}"].value' for d in m.data: if not d.bone: - d.name = '' + d.name = "" continue - d.name = name_bind = 'mmd_bind%s'%hash(d) - b = __get_bone(name_bind, 10, None) + d.name = name_bind = f"mmd_bind{hash(d)}" + b = FnBone.set_edit_bone_to_shadow(__get_bone(name_bind, None)) groups = [] bone_offset_map[name_bind] = (m.name, d, b.name, data_path, groups) - group_map.setdefault(('bone_morphs', m.name), []).append(groups) + group_map.setdefault(("bone_morphs", m.name), []).append(groups) - ctrl_base = __get_bone('mmd_bind_ctrl_base', 11, None) + ctrl_base = FnBone.set_edit_bone_to_dummy(__get_bone("mmd_bind_ctrl_base", None)) for m in mmd_root.uv_morphs: morph_name = m.name.replace('"', '\\"') - data_path = 'data.shape_keys.key_blocks["%s"].value'%morph_name - scale_path = 'mmd_root.uv_morphs["%s"].vertex_group_scale'%morph_name - name_bind = 'mmd_bind%s'%hash(m.name) - b = __get_bone(name_bind, 11, ctrl_base) + data_path = f'data.shape_keys.key_blocks["{morph_name}"].value' + scale_path = f'mmd_root.uv_morphs["{morph_name}"].vertex_group_scale' + name_bind = f"mmd_bind{hash(m.name)}" + b = FnBone.set_edit_bone_to_dummy(__get_bone(name_bind, ctrl_base)) groups = [] uv_morph_map.setdefault(name_bind, []).append((b.name, data_path, scale_path, groups)) - group_map.setdefault(('uv_morphs', m.name), []).append(groups) + group_map.setdefault(("uv_morphs", m.name), []).append(groups) - used_bone_names = bone_offset_map.keys()|uv_morph_map.keys() + used_bone_names = bone_offset_map.keys() | uv_morph_map.keys() used_bone_names.add(ctrl_base.name) - for b in edit_bones: # cleanup - if b.name.startswith('mmd_bind') and b.name not in used_bone_names: + for b in edit_bones: # cleanup + if b.name.startswith("mmd_bind") and b.name not in used_bone_names: edit_bones.remove(b) material_offset_map = {} for m in mmd_root.material_morphs: morph_name = m.name.replace('"', '\\"') - data_path = 'data.shape_keys.key_blocks["%s"].value'%morph_name + data_path = f'data.shape_keys.key_blocks["{morph_name}"].value' groups = [] - group_map.setdefault(('material_morphs', m.name), []).append(groups) - material_offset_map.setdefault('group_dict', {})[m.name] = (data_path, groups) + group_map.setdefault(("material_morphs", m.name), []).append(groups) + material_offset_map.setdefault("group_dict", {})[m.name] = (data_path, groups) for d in m.data: - d.name = name_bind = 'mmd_bind%s'%hash(d) + d.name = name_bind = f"mmd_bind{hash(d)}" # add '#' before material name to avoid conflict with group_dict - table = material_offset_map.setdefault('#'+d.material, ([], [])) - table[1 if d.offset_type == 'ADD' else 0].append((m.name, d, name_bind)) + table = material_offset_map.setdefault("#" + d.material, ([], [])) + table[1 if d.offset_type == "ADD" else 0].append((m.name, d, name_bind)) for m in mmd_root.group_morphs: if len(m.data) != len(set(m.data.keys())): logging.warning(' * Found duplicated morph data in Group Morph "%s"', m.name) morph_name = m.name.replace('"', '\\"') - morph_path = 'data.shape_keys.key_blocks["%s"].value'%morph_name + morph_path = f'data.shape_keys.key_blocks["{morph_name}"].value' for d in m.data: - param = (morph_name, d.name.replace('"', '\\"')) - factor_path = 'mmd_root.group_morphs["%s"].data["%s"].factor'%param + data_name = d.name.replace('"', '\\"') + factor_path = f'mmd_root.group_morphs["{morph_name}"].data["{data_name}"].factor' for groups in group_map.get((d.morph_type, d.name), ()): groups.append((m.name, morph_path, factor_path)) - self.__cleanup(shape_key_map.keys()|bone_offset_map.keys()|uv_morph_map.keys()) + self.__cleanup(shape_key_map.keys() | bone_offset_map.keys() | uv_morph_map.keys()) def __config_groups(variables, expression, groups): for g_name, morph_path, factor_path in groups: - var = self.__add_single_prop(variables, obj, morph_path, 'g') - fvar = self.__add_single_prop(variables, root, factor_path, 'w') - expression = '%s+%s*%s'%(expression, var.name, fvar.name) + var = self.__add_single_prop(variables, obj, morph_path, "g") + fvar = self.__add_single_prop(variables, root, factor_path, "w") + expression = f"{expression}+{var.name}*{fvar.name}" return expression # vertex morphs for kb_bind, morph_data_path, groups in (i for l in shape_key_map.values() for i in l): - driver, variables = self.__driver_variables(kb_bind, 'value') - var = self.__add_single_prop(variables, obj, morph_data_path, 'v') - if kb_bind.name.startswith('mmd_bind'): - driver.expression = '-(%s)'%__config_groups(variables, var.name, groups) + driver, variables = self.__driver_variables(kb_bind, "value") + var = self.__add_single_prop(variables, obj, morph_data_path, "v") + if kb_bind.name.startswith("mmd_bind"): + driver.expression = f"-({__config_groups(variables, var.name, groups)})" kb_bind.relative_key.mute = True else: - driver.expression = '%s'%__config_groups(variables, var.name, groups) + driver.expression = __config_groups(variables, var.name, groups) kb_bind.mute = False # bone morphs def __config_bone_morph(constraints, map_type, attributes, val, val_str): - c_name = 'mmd_bind%s.%s'%(hash(data), map_type[:3]) + c_name = f"mmd_bind{hash(data)}.{map_type[:3]}" c = TransformConstraintOp.create(constraints, c_name, map_type) TransformConstraintOp.update_min_max(c, val, None) c.show_expanded = False @@ -621,63 +639,65 @@ def __config_bone_morph(constraints, map_type, attributes, val, val_str): c.subtarget = bname for attr in attributes: driver, variables = self.__driver_variables(armObj, c.path_from_id(attr)) - var = self.__add_single_prop(variables, obj, morph_data_path, 'b') + var = self.__add_single_prop(variables, obj, morph_data_path, "b") expression = __config_groups(variables, var.name, groups) - sign = '-' if attr.startswith('to_min') else '' - driver.expression = '%s%s*(%s)'%(sign, val_str, expression) + sign = "-" if attr.startswith("to_min") else "" + driver.expression = f"{sign}{val_str}*({expression})" from math import pi - attributes_rot = TransformConstraintOp.min_max_attributes('ROTATION', 'to') - attributes_loc = TransformConstraintOp.min_max_attributes('LOCATION', 'to') + + attributes_rot = TransformConstraintOp.min_max_attributes("ROTATION", "to") + attributes_loc = TransformConstraintOp.min_max_attributes("LOCATION", "to") for morph_name, data, bname, morph_data_path, groups in bone_offset_map.values(): b = arm.pose.bones[bname] b.location = data.location - b.rotation_quaternion = data.rotation.__class__(*data.rotation.to_axis_angle()) # Fix for consistency + b.rotation_quaternion = data.rotation.__class__(*data.rotation.to_axis_angle()) # Fix for consistency b.is_mmd_shadow_bone = True - b.mmd_shadow_bone_type = 'BIND' + b.mmd_shadow_bone_type = "BIND" pb = armObj.pose.bones[data.bone] - __config_bone_morph(pb.constraints, 'ROTATION', attributes_rot, pi, 'pi') - __config_bone_morph(pb.constraints, 'LOCATION', attributes_loc, 100, '100') + __config_bone_morph(pb.constraints, "ROTATION", attributes_rot, pi, "pi") + __config_bone_morph(pb.constraints, "LOCATION", attributes_loc, 100, "100") # uv morphs - if bpy.app.version >= (2, 80, 0): # workaround for Blender 2.80+, data_path can't be properly detected (Save & Reopen file also works) - root.parent, root.parent, root.matrix_parent_inverse = arm, root.parent, root.matrix_parent_inverse.copy() - b = arm.pose.bones['mmd_bind_ctrl_base'] + # HACK: workaround for Blender 2.80+, data_path can't be properly detected (Save & Reopen file also works) + root.parent, root.parent, root.matrix_parent_inverse = arm, root.parent, root.matrix_parent_inverse.copy() + b = arm.pose.bones["mmd_bind_ctrl_base"] b.is_mmd_shadow_bone = True - b.mmd_shadow_bone_type = 'BIND' + b.mmd_shadow_bone_type = "BIND" for bname, data_path, scale_path, groups in (i for l in uv_morph_map.values() for i in l): b = arm.pose.bones[bname] b.is_mmd_shadow_bone = True - b.mmd_shadow_bone_type = 'BIND' - driver, variables = self.__driver_variables(b, 'location', index=0) - var = self.__add_single_prop(variables, obj, data_path, 'u') - fvar = self.__add_single_prop(variables, root, scale_path, 's') - driver.expression = '(%s)*%s'%(__config_groups(variables, var.name, groups), fvar.name) + b.mmd_shadow_bone_type = "BIND" + driver, variables = self.__driver_variables(b, "location", index=0) + var = self.__add_single_prop(variables, obj, data_path, "u") + fvar = self.__add_single_prop(variables, root, scale_path, "s") + driver.expression = f"({__config_groups(variables, var.name, groups)})*{fvar.name}" # material morphs - from mmd_tools.core.shader import _MaterialMorph - group_dict = material_offset_map.get('group_dict', {}) + from .shader import _MaterialMorph + + group_dict = material_offset_map.get("group_dict", {}) def __config_material_morph(mat, morph_list): nodes = _MaterialMorph.setup_morph_nodes(mat, tuple(x[1] for x in morph_list)) for (morph_name, data, name_bind), node in zip(morph_list, nodes): node.label, node.name = morph_name, name_bind data_path, groups = group_dict[morph_name] - driver, variables = self.__driver_variables(mat.node_tree, node.inputs[0].path_from_id('default_value')) - var = self.__add_single_prop(variables, obj, data_path, 'm') - driver.expression = '%s'%__config_groups(variables, var.name, groups) - - for mat in (m for m in rig.materials() if m and m.use_nodes and not m.name.startswith('mmd_')): - mul_all, add_all = material_offset_map.get('#', ([], [])) - if mat.name == '': - logging.warning('Oh no. The material name should never empty.') + driver, variables = self.__driver_variables(mat.node_tree, node.inputs[0].path_from_id("default_value")) + var = self.__add_single_prop(variables, obj, data_path, "m") + driver.expression = "%s" % __config_groups(variables, var.name, groups) + + for mat in (m for m in rig.materials() if m and m.use_nodes and not m.name.startswith("mmd_")): + mul_all, add_all = material_offset_map.get("#", ([], [])) + if mat.name == "": + logging.warning("Oh no. The material name should never empty.") mul_list, add_list = [], [] else: - mat_name = '#'+mat.name + mat_name = "#" + mat.name mul_list, add_list = material_offset_map.get(mat_name, ([], [])) - morph_list = tuple(mul_all+mul_list+add_all+add_list) + morph_list = tuple(mul_all + mul_list + add_all + add_list) __config_material_morph(mat, morph_list) - mat_edge = bpy.data.materials.get('mmd_edge.'+mat.name, None) + mat_edge = bpy.data.materials.get("mmd_edge." + mat.name, None) if mat_edge: __config_material_morph(mat_edge, morph_list) @@ -687,48 +707,44 @@ def __config_material_morph(mat, morph_list): class MigrationFnMorph: @staticmethod def update_mmd_morph(): - from mmd_tools.core.material import FnMaterial + from .material import FnMaterial for root in bpy.data.objects: - if root.mmd_type != 'ROOT': + if root.mmd_type != "ROOT": continue for mat_morph in root.mmd_root.material_morphs: for morph_data in mat_morph.data: - if morph_data.material_data is not None: - + # SUPPORT_UNTIL: 5 LTS # The material_id is also no longer used, but for compatibility with older version mmd_tools, keep it. - if 'material_id' not in morph_data.material_data.mmd_material or\ - 'material_id' not in morph_data or\ - morph_data.material_data.mmd_material['material_id'] == morph_data['material_id']: - + if "material_id" not in morph_data.material_data.mmd_material or "material_id" not in morph_data or morph_data.material_data.mmd_material["material_id"] == morph_data["material_id"]: # In the new version, the related_mesh property is no longer used. # Explicitly remove this property to avoid misuse. - if 'related_mesh' in morph_data: - del morph_data['related_mesh'] + if "related_mesh" in morph_data: + del morph_data["related_mesh"] continue else: # Compat case. The new version mmd_tools saved. And old version mmd_tools edit. Then new version mmd_tools load again. # Go update path. pass - + morph_data.material_data = None - if 'material_id' in morph_data: - mat_id = morph_data['material_id'] + if "material_id" in morph_data: + mat_id = morph_data["material_id"] if mat_id != -1: fnMat = FnMaterial.from_material_id(mat_id) if fnMat: morph_data.material_data = fnMat.material else: - morph_data['material_id'] = -1 - + morph_data["material_id"] = -1 + morph_data.related_mesh_data = None - if 'related_mesh' in morph_data: - related_mesh = morph_data['related_mesh'] - del morph_data['related_mesh'] - if related_mesh != '' and related_mesh in bpy.data.meshes: + if "related_mesh" in morph_data: + related_mesh = morph_data["related_mesh"] + del morph_data["related_mesh"] + if related_mesh != "" and related_mesh in bpy.data.meshes: morph_data.related_mesh_data = bpy.data.meshes[related_mesh] @staticmethod @@ -755,13 +771,12 @@ def compatible_with_old_version_mmd_tools(): MigrationFnMorph.ensure_material_id_not_conflict() for root in bpy.data.objects: - if root.mmd_type != 'ROOT': + if root.mmd_type != "ROOT": continue for mat_morph in root.mmd_root.material_morphs: for morph_data in mat_morph.data: - - morph_data['related_mesh'] = morph_data.related_mesh + morph_data["related_mesh"] = morph_data.related_mesh if morph_data.material_data is None: morph_data.material_id = -1 diff --git a/mmd_tools/core/pmd/__init__.py b/mmd_tools/core/pmd/__init__.py index 8ed1c829..1a00bbb4 100644 --- a/mmd_tools/core/pmd/__init__.py +++ b/mmd_tools/core/pmd/__init__.py @@ -1,15 +1,22 @@ # -*- coding: utf-8 -*- -import struct +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. + +import collections +import logging import os import re -import logging -import collections +import struct + class InvalidFileError(Exception): pass + + class UnsupportedVersionError(Exception): pass + class FileStream: def __init__(self, path, file_obj): self.__path = path @@ -40,83 +47,83 @@ def close(self): self.__file_obj = None -class FileReadStream(FileStream): +class FileReadStream(FileStream): def __init__(self, path, pmx_header=None): - self.__fin = open(path, 'rb') + self.__fin = open(path, "rb") FileStream.__init__(self, path, self.__fin) - # READ / WRITE methods for general types def readInt(self): - v, = struct.unpack(' 0: self.sphere_path = t.pop(0) - if 'aA'.find(self.sphere_path[-1]) != -1: + if "aA".find(self.sphere_path[-1]) != -1: self.sphere_mode = 2 + class Bone: def __init__(self): - self.name = '' - self.name_e = '' - self.parent = 0xffff - self.tail_bone = 0xffff + self.name = "" + self.name_e = "" + self.parent = 0xFFFF + self.tail_bone = 0xFFFF self.type = 1 self.ik_bone = 0 self.position = [] @@ -171,10 +180,10 @@ def __init__(self): def load(self, fs): self.name = fs.readStr(20) self.parent = fs.readUnsignedShort() - if self.parent == 0xffff: + if self.parent == 0xFFFF: self.parent = -1 self.tail_bone = fs.readUnsignedShort() - if self.tail_bone == 0xffff: + if self.tail_bone == 0xFFFF: self.tail_bone = -1 self.type = fs.readByte() if self.type == 9: @@ -183,6 +192,7 @@ def load(self, fs): self.ik_bone = fs.readUnsignedShort() self.position = fs.readVector(3) + class IK: def __init__(self): self.bone = 0 @@ -193,13 +203,7 @@ def __init__(self): self.ik_child_bones = [] def __str__(self): - return ' %s', b.name, t.name) + logging.info("Duplicate the bone: %s -> %s", b.name, t.name) pmx_bone = pmx_model.bones[ik.bone] - logging.debug('Add IK settings to the bone %s', pmx_bone.name) + logging.debug("Add IK settings to the bone %s", pmx_bone.name) pmx_bone.isIK = True pmx_bone.target = ik.target_bone pmx_bone.loopCount = ik.iterations - pmx_bone.rotationConstraint = ik.control_weight*4 + pmx_bone.rotationConstraint = ik.control_weight * 4 for i in ik.ik_child_bones: ik_link = pmx.IKLink() ik_link.target = i if i in knee_bones: ik_link.maximumAngle = [radians(-0.5), 0.0, 0.0] ik_link.minimumAngle = [radians(-180.0), 0.0, 0.0] - logging.info(' Add knee constraints to %s', i) - logging.debug(' IKLink: %s(index: %d)', pmx_model.bones[i].name, i) + logging.info(" Add knee constraints to %s", i) + logging.debug(" IKLink: %s(index: %d)", pmx_model.bones[i].name, i) pmx_bone.ik_links.append(ik_link) applied_ik_bones.append(ik.bone) - logging.info('----- Converted %d bones', len(pmd_model.iks)) + logging.info("----- Converted %d bones", len(pmd_model.iks)) texture_map = {} toon_texture_map = {} - logging.info('') - logging.info('------------------------------') - logging.info(' Convert Materials') - logging.info('------------------------------') + logging.info("") + logging.info("------------------------------") + logging.info(" Convert Materials") + logging.info("------------------------------") for i, mat in enumerate(pmd_model.materials): pmx_mat = pmx.Material() - pmx_mat.name = '材質%d'%(i+1) - pmx_mat.name_e = 'Material%d'%(i+1) + pmx_mat.name = "材質%d" % (i + 1) + pmx_mat.name_e = "Material%d" % (i + 1) pmx_mat.diffuse = mat.diffuse pmx_mat.specular = mat.specular pmx_mat.shininess = mat.shininess pmx_mat.ambient = mat.ambient - pmx_mat.is_double_sided = (mat.diffuse[3] < 1.0) - pmx_mat.enabled_self_shadow_map = abs(mat.diffuse[3] - 0.98) > 1e-7 # consider precision error - pmx_mat.enabled_toon_edge = (mat.edge_flag != 0) - pmx_mat.enabled_self_shadow = pmx_mat.enabled_self_shadow_map # True (in MMD) - pmx_mat.enabled_drop_shadow = pmx_mat.enabled_toon_edge # True (in MMD) + pmx_mat.is_double_sided = mat.diffuse[3] < 1.0 + pmx_mat.enabled_self_shadow_map = abs(mat.diffuse[3] - 0.98) > 1e-7 # consider precision error + pmx_mat.enabled_toon_edge = mat.edge_flag != 0 + pmx_mat.enabled_self_shadow = pmx_mat.enabled_self_shadow_map # True (in MMD) + pmx_mat.enabled_drop_shadow = pmx_mat.enabled_toon_edge # True (in MMD) pmx_mat.edge_color = (0, 0, 0, 1) pmx_mat.vertex_count = mat.vertex_count if len(mat.texture_path) > 0: tex_path = mat.texture_path if tex_path not in texture_map: - logging.info(' Create pmx.Texture -------- %s', tex_path) + logging.info(" Create pmx.Texture -------- %s", tex_path) tex = pmx.Texture() tex.path = os.path.normpath(os.path.join(os.path.dirname(target_path), tex_path)) pmx_model.textures.append(tex) @@ -213,7 +213,7 @@ def import_pmd_to_pmx(filepath): if len(mat.sphere_path) > 0: tex_path = mat.sphere_path if tex_path not in texture_map: - logging.info(' Create pmx.Texture -Sphere- %s', tex_path) + logging.info(" Create pmx.Texture -Sphere- %s", tex_path) tex = pmx.Texture() tex.path = os.path.normpath(os.path.join(os.path.dirname(target_path), tex_path)) pmx_model.textures.append(tex) @@ -225,9 +225,9 @@ def import_pmd_to_pmx(filepath): if mat.toon_index in range(len(pmd_model.toon_textures)): tex_path = pmd_model.toon_textures[mat.toon_index] if tex_path not in toon_texture_map: - logging.info(' Create pmx.Texture --Toon-- %s', tex_path) - if re.match(r'toon(0[1-9]|10)\.bmp$', tex_path): - toon_texture_map[tex_path] = (True, int(tex_path[-6:-4])-1) + logging.info(" Create pmx.Texture --Toon-- %s", tex_path) + if re.match(r"toon(0[1-9]|10)\.bmp$", tex_path): + toon_texture_map[tex_path] = (True, int(tex_path[-6:-4]) - 1) elif tex_path in texture_map: toon_texture_map[tex_path] = (False, texture_map[tex_path]) else: @@ -235,29 +235,29 @@ def import_pmd_to_pmx(filepath): tex.path = os.path.normpath(os.path.join(os.path.dirname(target_path), tex_path)) pmx_model.textures.append(tex) texture_map[tex_path] = len(pmx_model.textures) - 1 - toon_texture_map[tex_path] = (False, len(pmx_model.textures)-1) + toon_texture_map[tex_path] = (False, len(pmx_model.textures) - 1) pmx_mat.is_shared_toon_texture, pmx_mat.toon_texture = toon_texture_map[tex_path] pmx_model.materials.append(pmx_mat) - logging.info('----- Converted %d materials', len(pmx_model.materials)) + logging.info("----- Converted %d materials", len(pmx_model.materials)) - logging.info('') - logging.info('------------------------------') - logging.info(' Convert Morphs') - logging.info('------------------------------') + logging.info("") + logging.info("------------------------------") + logging.info(" Convert Morphs") + logging.info("------------------------------") morph_index_map = [] t = list(filter(lambda x: x.type == 0, pmd_model.morphs)) if len(t) == 0: - logging.error('Not found the base morph') - logging.error('Skip converting vertex morphs.') + logging.error("Not found the base morph") + logging.error("Skip converting vertex morphs.") else: if len(t) > 1: - logging.warning('Found two or more base morphs.') + logging.warning("Found two or more base morphs.") vertex_map = [] for i in t[0].data: vertex_map.append(i.index) for morph in pmd_model.morphs: - logging.debug('Vertex Morph: %s', morph.name) + logging.debug("Vertex Morph: %s", morph.name) if morph.type == 0: morph_index_map.append(-1) continue @@ -269,12 +269,12 @@ def import_pmd_to_pmx(filepath): pmx_morph.offsets.append(mo) morph_index_map.append(len(pmx_model.morphs)) pmx_model.morphs.append(pmx_morph) - logging.info('----- Converted %d morphs', len(pmx_model.morphs)) + logging.info("----- Converted %d morphs", len(pmx_model.morphs)) - logging.info('') - logging.info('------------------------------') - logging.info(' Convert Display Items') - logging.info('------------------------------') + logging.info("") + logging.info("------------------------------") + logging.info(" Convert Display Items") + logging.info("------------------------------") if len(pmd_model.bones) > 0: pmx_model.display[0].data.append((0, 0)) if len(morph_index_map) > 0: @@ -293,12 +293,12 @@ def import_pmd_to_pmx(filepath): for bone_index in bone_disp_list: d.data.append((0, bone_index)) pmx_model.display.append(d) - logging.info('----- Converted %d display items', len(pmx_model.display)) + logging.info("----- Converted %d display items", len(pmx_model.display)) - logging.info('') - logging.info('------------------------------') - logging.info(' Convert Rigid bodies') - logging.info('------------------------------') + logging.info("") + logging.info("------------------------------") + logging.info(" Convert Rigid bodies") + logging.info("------------------------------") for rigid in pmd_model.rigid_bodies: pmx_rigid = pmx.Rigid() @@ -327,12 +327,12 @@ def import_pmd_to_pmx(filepath): pmx_rigid.mode = rigid.mode pmx_model.rigids.append(pmx_rigid) - logging.info('----- Converted %d rigid bodies', len(pmx_model.rigids)) + logging.info("----- Converted %d rigid bodies", len(pmx_model.rigids)) - logging.info('') - logging.info('------------------------------') - logging.info(' Convert Joints') - logging.info('------------------------------') + logging.info("") + logging.info("------------------------------") + logging.info(" Convert Joints") + logging.info("------------------------------") for joint in pmd_model.joints: pmx_joint = pmx.Joint() @@ -352,11 +352,11 @@ def import_pmd_to_pmx(filepath): pmx_joint.spring_rotation_constant = joint.spring_rotation_constant pmx_model.joints.append(pmx_joint) - logging.info('----- Converted %d joints', len(pmx_model.joints)) + logging.info("----- Converted %d joints", len(pmx_model.joints)) - logging.info(' Finish converting pmd into pmx.') - logging.info('----------------------------------------') - logging.info(' mmd_tools.import_pmd module') - logging.info('****************************************') + logging.info(" Finish converting pmd into pmx.") + logging.info("----------------------------------------") + logging.info(" mmd_tools.import_pmd module") + logging.info("****************************************") return pmx_model diff --git a/mmd_tools/core/pmx/__init__.py b/mmd_tools/core/pmx/__init__.py index 16349d98..2c4cba8d 100644 --- a/mmd_tools/core/pmx/__init__.py +++ b/mmd_tools/core/pmx/__init__.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- -import struct -import os +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. + import logging +import os +import struct + class InvalidFileError(Exception): pass diff --git a/mmd_tools/core/pmx/exporter.py b/mmd_tools/core/pmx/exporter.py index 3cface43..27895514 100644 --- a/mmd_tools/core/pmx/exporter.py +++ b/mmd_tools/core/pmx/exporter.py @@ -1,79 +1,81 @@ # -*- coding: utf-8 -*- -import os +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. + import copy import logging import math +import os import shutil import time +from collections import OrderedDict -import mathutils -import bpy import bmesh +import bpy +import mathutils -from collections import OrderedDict -from mmd_tools.core import pmx -from mmd_tools.core.bone import FnBone -from mmd_tools.core.material import FnMaterial -from mmd_tools.core.morph import FnMorph -from mmd_tools.core.translations import FnTranslations -from mmd_tools.core.sdef import FnSDEF -from mmd_tools.core.vmd.importer import BoneConverter, BoneConverterPoseMode -from mmd_tools import bpyutils -from mmd_tools.utils import saferelpath -from mmd_tools.bpyutils import matmul -from mmd_tools.operators.misc import MoveObject +from ...bpyutils import FnContext +from .. import pmx +from ..material import FnMaterial +from ..morph import FnMorph +from ..sdef import FnSDEF +from ..translations import FnTranslations +from ..vmd.importer import BoneConverter, BoneConverterPoseMode +from ...operators.misc import MoveObject +from ...utils import saferelpath class _Vertex: def __init__(self, co, groups, offsets, edge_scale, vertex_order, uv_offsets): self.co = co - self.groups = groups # [(group_number, weight), ...] + self.groups = groups # [(group_number, weight), ...] self.offsets = offsets self.edge_scale = edge_scale - self.vertex_order = vertex_order # used for controlling vertex order + self.vertex_order = vertex_order # used for controlling vertex order self.uv_offsets = uv_offsets self.index = None self.uv = None self.normal = None - self.sdef_data = [] # (C, R0, R1) - self.add_uvs = [None]*4 # UV1~UV4 + self.sdef_data = [] # (C, R0, R1) + self.add_uvs = [None] * 4 # UV1~UV4 + class _Face: def __init__(self, vertices, index=-1): - ''' Temporary Face Class - ''' + """Temporary Face Class""" self.vertices = vertices self.index = index + class _Mesh: def __init__(self, material_faces, shape_key_names, material_names): - self.material_faces = material_faces # dict of {material_index => [face1, face2, ....]} + self.material_faces = material_faces # dict of {material_index => [face1, face2, ....]} self.shape_key_names = shape_key_names self.material_names = material_names class _DefaultMaterial: def __init__(self): - mat = bpy.data.materials.new('') - #mat.mmd_material.diffuse_color = (0, 0, 0) - #mat.mmd_material.specular_color = (0, 0, 0) - #mat.mmd_material.ambient_color = (0, 0, 0) + mat = bpy.data.materials.new("") + # mat.mmd_material.diffuse_color = (0, 0, 0) + # mat.mmd_material.specular_color = (0, 0, 0) + # mat.mmd_material.ambient_color = (0, 0, 0) self.material = mat - logging.debug('create default material: %s', str(self.material)) + logging.debug("create default material: %s", str(self.material)) def __del__(self): if self.material: - logging.debug('remove default material: %s', str(self.material)) + logging.debug("remove default material: %s", str(self.material)) bpy.data.materials.remove(self.material) class __PmxExporter: CATEGORIES = { - 'SYSTEM': pmx.Morph.CATEGORY_SYSTEM, - 'EYEBROW': pmx.Morph.CATEGORY_EYEBROW, - 'EYE': pmx.Morph.CATEGORY_EYE, - 'MOUTH': pmx.Morph.CATEGORY_MOUTH, - } + "SYSTEM": pmx.Morph.CATEGORY_SYSTEM, + "EYEBROW": pmx.Morph.CATEGORY_EYEBROW, + "EYE": pmx.Morph.CATEGORY_EYE, + "MOUTH": pmx.Morph.CATEGORY_MOUTH, + } def __init__(self): self.__model = None @@ -81,7 +83,7 @@ def __init__(self): self.__material_name_table = [] self.__exported_vertices = [] self.__default_material = None - self.__vertex_order_map = None # used for controlling vertex order + self.__vertex_order_map = None # used for controlling vertex order self.__overwrite_bone_morphs_from_pose_library = False self.__translate_in_presets = False self.__disable_specular = False @@ -90,7 +92,7 @@ def __init__(self): @staticmethod def flipUV_V(uv): u, v = uv - return u, 1.0-v + return u, 1.0 - v def __getDefaultMaterial(self): if self.__default_material is None: @@ -98,19 +100,19 @@ def __getDefaultMaterial(self): return self.__default_material.material def __sortVertices(self): - logging.info(' - Sorting vertices ...') + logging.info(" - Sorting vertices ...") weight_items = self.__vertex_order_map.items() sorted_indices = [i[0] for i in sorted(weight_items, key=lambda x: x[1].vertex_order)] vertices = self.__model.vertices self.__model.vertices = [vertices[i] for i in sorted_indices] # update indices - index_map = {x:i for i, x in enumerate(sorted_indices)} + index_map = {x: i for i, x in enumerate(sorted_indices)} for v in self.__vertex_order_map.values(): v.index = index_map[v.index] for f in self.__model.faces: f[:] = [index_map[i] for i in f] - logging.debug(' - Done (count:%d)', len(self.__vertex_order_map)) + logging.debug(" - Done (count:%d)", len(self.__vertex_order_map)) def __exportMeshes(self, meshes, bone_map): mat_map = OrderedDict() @@ -148,7 +150,7 @@ def __exportMeshes(self, meshes, bone_map): pv.edge_scale = v.edge_scale for _uvzw in v.add_uvs: if _uvzw: - pv.additional_uvs.append(self.flipUV_V(_uvzw[0])+self.flipUV_V(_uvzw[1])) + pv.additional_uvs.append(self.flipUV_V(_uvzw[0]) + self.flipUV_V(_uvzw[1])) t = len(v.groups) if t == 0: @@ -167,7 +169,7 @@ def __exportMeshes(self, meshes, bone_map): weight.type = pmx.BoneWeight.BDEF2 weight.bones = [vg1[0], vg2[0]] w1, w2 = vg1[1], vg2[1] - weight.weights = [w1/(w1+w2)] + weight.weights = [w1 / (w1 + w2)] if v.sdef_data: weight.type = pmx.BoneWeight.SDEF sdef_weights = pmx.BoneWeightSDEF() @@ -206,7 +208,7 @@ def __exportMeshes(self, meshes, bone_map): self.__sortVertices() def __exportTexture(self, filepath): - if filepath.strip() == '': + if filepath.strip() == "": return -1 # Use bpy.path to resolve '//' in .blend relative filepaths filepath = bpy.path.abspath(filepath) @@ -218,39 +220,39 @@ def __exportTexture(self, filepath): t.path = filepath self.__model.textures.append(t) if not os.path.isfile(t.path): - logging.warning(' The texture file does not exist: %s', t.path) + logging.warning(" The texture file does not exist: %s", t.path) return len(self.__model.textures) - 1 - def __copy_textures(self, output_dir, base_folder=''): - tex_dir_fallback = os.path.join(output_dir, 'textures') - tex_dir_preference = bpyutils.addon_preferences('base_texture_folder', '') + def __copy_textures(self, output_dir, base_folder=""): + tex_dir_fallback = os.path.join(output_dir, "textures") + tex_dir_preference = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "base_texture_folder", "") - path_set = set() # to prevent overwriting + path_set = set() # to prevent overwriting tex_copy_list = [] for texture in self.__model.textures: path = texture.path tex_dir = output_dir # restart to the default directory at each loop if not os.path.isfile(path): - logging.warning('*** skipping texture file which does not exist: %s', path) + logging.warning("*** skipping texture file which does not exist: %s", path) path_set.add(os.path.normcase(path)) continue dst_name = os.path.basename(path) if base_folder: - dst_name = saferelpath(path, base_folder, strategy='outside') - if dst_name.startswith('..'): + dst_name = saferelpath(path, base_folder, strategy="outside") + if dst_name.startswith(".."): # Check if the texture comes from the preferred folder if tex_dir_preference: - dst_name = saferelpath(path, tex_dir_preference, strategy='outside') - if dst_name.startswith('..'): + dst_name = saferelpath(path, tex_dir_preference, strategy="outside") + if dst_name.startswith(".."): # If the code reaches here the texture is somewhere else - logging.warning('The texture %s is not inside the base texture folder', path) + logging.warning("The texture %s is not inside the base texture folder", path) # Fall back to basename and textures folder dst_name = os.path.basename(path) tex_dir = tex_dir_fallback else: tex_dir = tex_dir_fallback dest_path = os.path.join(tex_dir, dst_name) - if os.path.normcase(path) != os.path.normcase(dest_path): # Only copy if the paths are different + if os.path.normcase(path) != os.path.normcase(dest_path): # Only copy if the paths are different tex_copy_list.append((texture, path, dest_path)) else: path_set.add(os.path.normcase(path)) @@ -259,12 +261,12 @@ def __copy_textures(self, output_dir, base_folder=''): counter = 1 base, ext = os.path.splitext(dest_path) while os.path.normcase(dest_path) in path_set: - dest_path = '%s_%d%s'%(base, counter, ext) + dest_path = "%s_%d%s" % (base, counter, ext) counter += 1 path_set.add(os.path.normcase(dest_path)) os.makedirs(os.path.dirname(dest_path), exist_ok=True) shutil.copyfile(path, dest_path) - logging.info('Copy file %s --> %s', path, dest_path) + logging.info("Copy file %s --> %s", path, dest_path) texture.path = dest_path def __exportMaterial(self, material, num_faces): @@ -292,11 +294,11 @@ def __exportMaterial(self, material, num_faces): p_mat.vertex_count = num_faces * 3 fnMat = FnMaterial(material) tex = fnMat.get_texture() - if tex and tex.type == 'IMAGE' and tex.image: # Ensure the texture is an image + if tex and tex.type == "IMAGE" and tex.image: # Ensure the texture is an image index = self.__exportTexture(tex.image.filepath) p_mat.texture = index tex = fnMat.get_sphere_texture() - if tex and tex.type == 'IMAGE' and tex.image: # Ensure the texture is an image + if tex and tex.type == "IMAGE" and tex.image: # Ensure the texture is an image index = self.__exportTexture(tex.image.filepath) p_mat.sphere_texture = index @@ -304,7 +306,7 @@ def __exportMaterial(self, material, num_faces): p_mat.toon_texture = mmd_mat.shared_toon_texture p_mat.is_shared_toon_texture = True else: - p_mat.toon_texture = self.__exportTexture(mmd_mat.toon_texture) + p_mat.toon_texture = self.__exportTexture(mmd_mat.toon_texture) p_mat.is_shared_toon_texture = False self.__material_name_table.append(material.name) @@ -318,12 +320,12 @@ def __countBoneDepth(cls, bone): return cls.__countBoneDepth(bone.parent) + 1 def __exportBones(self, root, meshes): - """ Export bones. + """Export bones. Returns: A dictionary to map Blender bone names to bone indices of the pmx.model instance. """ arm = self.__armature - if hasattr(arm, 'evaluated_get'): + if hasattr(arm, "evaluated_get"): bpy.context.view_layer.update() arm = arm.evaluated_get(bpy.context.evaluated_depsgraph_get()) boneMap = {} @@ -335,27 +337,30 @@ def __exportBones(self, root, meshes): # determine the bone order vtx_grps = {} for mesh in meshes: - if mesh.modifiers.get('mmd_bone_order_override', None): + if mesh.modifiers.get("mmd_bone_order_override", None): vtx_grps = mesh.vertex_groups break class _Dummy: - index = float('inf') + index = float("inf") + sorted_bones = sorted(pose_bones, key=lambda x: vtx_grps.get(x.name, _Dummy).index) - #sorted_bones = sorted(pose_bones, key=self.__countBoneDepth) + # sorted_bones = sorted(pose_bones, key=self.__countBoneDepth) Vector = mathutils.Vector pmx_matrix = world_mat * self.__scale pmx_matrix[1], pmx_matrix[2] = pmx_matrix[2].copy(), pmx_matrix[1].copy() + def __to_pmx_location(loc): - return matmul(pmx_matrix, Vector(loc)) + return pmx_matrix @ Vector(loc) pmx_matrix_rot = pmx_matrix.to_3x3() + def __to_pmx_axis(axis, pose_bone): - m = matmul(pose_bone.matrix, pose_bone.bone.matrix_local.inverted()).to_3x3() - return matmul(matmul(pmx_matrix_rot, m), Vector(axis).xzy).normalized() + m = (pose_bone.matrix @ pose_bone.bone.matrix_local.inverted()).to_3x3() + return ((pmx_matrix_rot @ m) @ Vector(axis).xzy).normalized() - if True: # no need to enter edit mode + if True: # no need to enter edit mode for p_bone in sorted_bones: if p_bone.is_mmd_shadow_bone: continue @@ -371,7 +376,7 @@ def __to_pmx_axis(axis, pose_bone): pmx_bone.location = __to_pmx_location(p_bone.head) pmx_bone.parent = bone.parent - pmx_bone.visible = not bone.hide and any((all(x) for x in zip(bone.layers, arm.data.layers))) + pmx_bone.visible = not bone.hide and any(c.is_visible for c in bone.collections) pmx_bone.isControllable = mmd_bone.is_controllable pmx_bone.isMovable = not all(p_bone.lock_location) pmx_bone.isRotatable = not all(p_bone.lock_rotation) @@ -382,6 +387,7 @@ def __to_pmx_axis(axis, pose_bone): boneMap[bone] = pmx_bone r[bone.name] = len(pmx_bones) - 1 + # fmt: off if ( pmx_bone.parent is not None and ( @@ -409,6 +415,8 @@ def __to_pmx_axis(axis, pose_bone): ): pmx_bone.displayConnection = child break + # fmt: on + if not pmx_bone.displayConnection: if mmd_bone.is_tip: pmx_bone.displayConnection = -1 @@ -420,14 +428,12 @@ def __to_pmx_axis(axis, pose_bone): pmx_bone.axis = __to_pmx_axis(mmd_bone.fixed_axis, p_bone) if mmd_bone.enabled_local_axes: - pmx_bone.localCoordinate = pmx.Coordinate( - __to_pmx_axis(mmd_bone.local_axis_x, p_bone), - __to_pmx_axis(mmd_bone.local_axis_z, p_bone)) + pmx_bone.localCoordinate = pmx.Coordinate(__to_pmx_axis(mmd_bone.local_axis_x, p_bone), __to_pmx_axis(mmd_bone.local_axis_z, p_bone)) for idx, i in enumerate(pmx_bones): if i.parent is not None: i.parent = pmx_bones.index(boneMap[i.parent]) - logging.debug('the parent of %s:%s: %s', idx, i.name, i.parent) + logging.debug("the parent of %s:%s: %s", idx, i.name, i.parent) if isinstance(i.displayConnection, pmx.Bone): i.displayConnection = pmx_bones.index(i.displayConnection) elif isinstance(i.displayConnection, bpy.types.Bone): @@ -437,10 +443,10 @@ def __to_pmx_axis(axis, pose_bone): if len(pmx_bones) == 0: # avoid crashing MMD pmx_bone = pmx.Bone() - pmx_bone.name = u'全ての親' - pmx_bone.name_e = 'Root' - pmx_bone.location = __to_pmx_location([0,0,0]) - tail_loc = __to_pmx_location([0,0,1]) + pmx_bone.name = "全ての親" + pmx_bone.name_e = "Root" + pmx_bone.location = __to_pmx_location([0, 0, 0]) + tail_loc = __to_pmx_location([0, 0, 1]) pmx_bone.displayConnection = tail_loc - pmx_bone.location pmx_bones.append(pmx_bone) @@ -452,67 +458,68 @@ def __exportIKLinks(self, pose_bone, count, bone_map, ik_links, custom_bone): if count <= 0 or pose_bone is None or pose_bone.name not in bone_map: return ik_links - logging.debug(' Create IK Link for %s', pose_bone.name) + logging.debug(" Create IK Link for %s", pose_bone.name) ik_link = pmx.IKLink() ik_link.target = bone_map[pose_bone.name] from math import pi - minimum, maximum = [-pi]*3, [pi]*3 + + minimum, maximum = [-pi] * 3, [pi] * 3 unused_counts = 0 - ik_limit_custom = next((c for c in custom_bone.constraints if c.type == 'LIMIT_ROTATION' and c.name == 'mmd_ik_limit_custom%d'%len(ik_links)), None) - ik_limit_override = next((c for c in pose_bone.constraints if c.type == 'LIMIT_ROTATION' and not c.mute), None) - for i, axis in enumerate('xyz'): - if ik_limit_custom: # custom ik limits for MMD only - if getattr(ik_limit_custom, 'use_limit_'+axis): - minimum[i] = getattr(ik_limit_custom, 'min_'+axis) - maximum[i] = getattr(ik_limit_custom, 'max_'+axis) + ik_limit_custom = next((c for c in custom_bone.constraints if c.type == "LIMIT_ROTATION" and c.name == "mmd_ik_limit_custom%d" % len(ik_links)), None) + ik_limit_override = next((c for c in pose_bone.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None) + for i, axis in enumerate("xyz"): + if ik_limit_custom: # custom ik limits for MMD only + if getattr(ik_limit_custom, "use_limit_" + axis): + minimum[i] = getattr(ik_limit_custom, "min_" + axis) + maximum[i] = getattr(ik_limit_custom, "max_" + axis) else: unused_counts += 1 continue - if getattr(pose_bone, 'lock_ik_'+axis): + if getattr(pose_bone, "lock_ik_" + axis): minimum[i] = maximum[i] = 0 - elif ik_limit_override is not None and getattr(ik_limit_override, 'use_limit_'+axis): - minimum[i] = getattr(ik_limit_override, 'min_'+axis) - maximum[i] = getattr(ik_limit_override, 'max_'+axis) - elif getattr(pose_bone, 'use_ik_limit_'+axis): - minimum[i] = getattr(pose_bone, 'ik_min_'+axis) - maximum[i] = getattr(pose_bone, 'ik_max_'+axis) + elif ik_limit_override is not None and getattr(ik_limit_override, "use_limit_" + axis): + minimum[i] = getattr(ik_limit_override, "min_" + axis) + maximum[i] = getattr(ik_limit_override, "max_" + axis) + elif getattr(pose_bone, "use_ik_limit_" + axis): + minimum[i] = getattr(pose_bone, "ik_min_" + axis) + maximum[i] = getattr(pose_bone, "ik_max_" + axis) else: unused_counts += 1 if unused_counts < 3: convertIKLimitAngles = pmx.importer.PMXImporter.convertIKLimitAngles - bone_matrix = matmul(pose_bone.id_data.matrix_world, pose_bone.matrix) + bone_matrix = pose_bone.id_data.matrix_world @ pose_bone.matrix minimum, maximum = convertIKLimitAngles(minimum, maximum, bone_matrix, invert=True) ik_link.minimumAngle = list(minimum) ik_link.maximumAngle = list(maximum) return self.__exportIKLinks(pose_bone.parent, count - 1, bone_map, ik_links + [ik_link], custom_bone) - def __exportIK(self, root, bone_map): - """ Export IK constraints - @param bone_map the dictionary to map Blender bone names to bone indices of the pmx.model instance. + """Export IK constraints + @param bone_map the dictionary to map Blender bone names to bone indices of the pmx.model instance. """ pmx_bones = self.__model.bones arm = self.__armature ik_loop_factor = root.mmd_root.ik_loop_factor pose_bones = arm.pose.bones - ik_target_custom_map = {getattr(b.constraints.get('mmd_ik_target_custom', None), 'subtarget', None):b for b in pose_bones if not b.is_mmd_shadow_bone} + ik_target_custom_map = {getattr(b.constraints.get("mmd_ik_target_custom", None), "subtarget", None): b for b in pose_bones if not b.is_mmd_shadow_bone} + def __ik_target_bone_get(ik_constraint_bone, ik_bone): if ik_bone.name in ik_target_custom_map: logging.debug(' (use "mmd_ik_target_custom")') - return ik_target_custom_map[ik_bone.name] # for supporting the ik target which is not a child of ik_constraint_bone - return self.__get_ik_target_bone(ik_constraint_bone) # this only search the children of ik_constraint_bone + return ik_target_custom_map[ik_bone.name] # for supporting the ik target which is not a child of ik_constraint_bone + return self.__get_ik_target_bone(ik_constraint_bone) # this only search the children of ik_constraint_bone for bone in pose_bones: if bone.is_mmd_shadow_bone: continue for c in bone.constraints: - if c.type == 'IK' and not c.mute: - logging.debug(' Found IK constraint on %s', bone.name) + if c.type == "IK" and not c.mute: + logging.debug(" Found IK constraint on %s", bone.name) ik_pose_bone = self.__get_ik_control_bone(c) if ik_pose_bone is None: logging.warning(' * Invalid IK constraint "%s" on bone %s', c.name, bone.name) @@ -531,11 +538,11 @@ def __ik_target_bone_get(ik_constraint_bone, ik_bone): ik_chain0 = bone if c.use_tail else bone.parent ik_target_bone = __ik_target_bone_get(bone, ik_pose_bone) if c.use_tail else bone if ik_target_bone is None: - logging.warning(' * IK bone: %s, IK Target not found !!!', ik_pose_bone.name) + logging.warning(" * IK bone: %s, IK Target not found !!!", ik_pose_bone.name) continue - logging.debug(' - IK bone: %s, IK Target: %s', ik_pose_bone.name, ik_target_bone.name) + logging.debug(" - IK bone: %s, IK Target: %s", ik_pose_bone.name, ik_target_bone.name) pmx_ik_bone.isIK = True - pmx_ik_bone.loopCount = max(int(c.iterations/ik_loop_factor), 1) + pmx_ik_bone.loopCount = max(int(c.iterations / ik_loop_factor), 1) if ik_pose_bone.name in ik_target_custom_map: pmx_ik_bone.rotationConstraint = ik_pose_bone.mmd_bone.ik_rotation_constraint else: @@ -550,26 +557,26 @@ def __get_ik_control_bone(self, ik_constraint): bone = arm.pose.bones.get(ik_constraint.subtarget, None) if bone is None: return None - if bone.mmd_shadow_bone_type == 'IK_TARGET': - logging.debug(' Found IK proxy bone: %s -> %s', bone.name, getattr(bone.parent, 'name', None)) + if bone.mmd_shadow_bone_type == "IK_TARGET": + logging.debug(" Found IK proxy bone: %s -> %s", bone.name, getattr(bone.parent, "name", None)) return bone.parent return bone def __get_ik_target_bone(self, target_bone): - """ Get mmd ik target bone. + """Get mmd ik target bone. - Args: - target_bone: A blender PoseBone + Args: + target_bone: A blender PoseBone - Returns: - A bpy.types.PoseBone object which is the closest bone from the tail position of target_bone. - Return None if target_bone has no child bones. + Returns: + A bpy.types.PoseBone object which is the closest bone from the tail position of target_bone. + Return None if target_bone has no child bones. """ valid_children = [c for c in target_bone.children if not c.is_mmd_shadow_bone] # search 'mmd_ik_target_override' first for c in valid_children: - ik_target_override = c.constraints.get('mmd_ik_target_override', None) + ik_target_override = c.constraints.get("mmd_ik_target_override", None) if ik_target_override and ik_target_override.subtarget == target_bone.name: logging.debug(' (use "mmd_ik_target_override")') return c @@ -602,11 +609,7 @@ def __exportVertexMorphs(self, meshes, root): shape_key_names.sort(key=lambda x: root.mmd_root.vertex_morphs.find(x)) for i in shape_key_names: - morph = pmx.VertexMorph( - name=i, - name_e=morph_english_names.get(i, ''), - category=morph_categories.get(i, pmx.Morph.CATEGORY_OHTER) - ) + morph = pmx.VertexMorph(name=i, name_e=morph_english_names.get(i, ""), category=morph_categories.get(i, pmx.Morph.CATEGORY_OHTER)) self.__model.morphs.append(morph) append_table = dict(zip(shape_key_names, [m.offsets.append for m in self.__model.morphs])) @@ -621,22 +624,18 @@ def __export_material_morphs(self, root): mmd_root = root.mmd_root categories = self.CATEGORIES for morph in mmd_root.material_morphs: - mat_morph = pmx.MaterialMorph( - name=morph.name, - name_e=morph.name_e, - category=categories.get(morph.category, pmx.Morph.CATEGORY_OHTER) - ) + mat_morph = pmx.MaterialMorph(name=morph.name, name_e=morph.name_e, category=categories.get(morph.category, pmx.Morph.CATEGORY_OHTER)) for data in morph.data: morph_data = pmx.MaterialMorphOffset() try: - if data.material != '': + if data.material != "": morph_data.index = self.__material_name_table.index(data.material) else: morph_data.index = -1 except ValueError: logging.warning('Material Morph (%s): Material "%s" was not found.', morph.name, data.material) continue - morph_data.offset_type = ['MULT', 'ADD'].index(data.offset_type) + morph_data.offset_type = ["MULT", "ADD"].index(data.offset_type) morph_data.diffuse_offset = data.diffuse_color morph_data.specular_offset = data.specular_color morph_data.shininess_offset = data.shininess @@ -650,13 +649,13 @@ def __export_material_morphs(self, root): self.__model.morphs.append(mat_morph) def __sortMaterials(self): - """ sort materials for alpha blending + """sort materials for alpha blending - モデル内全頂点の平均座標をモデルの中心と考えて、 - モデル中心座標とマテリアルがアサインされている全ての面の構成頂点との平均距離を算出。 - この値が小さい順にソートしてみる。 - モデル中心座標から離れている位置で使用されているマテリアルほどリストの後ろ側にくるように。 - かなりいいかげんな実装 + モデル内全頂点の平均座標をモデルの中心と考えて、 + モデル中心座標とマテリアルがアサインされている全ての面の構成頂点との平均距離を算出。 + この値が小さい順にソートしてみる。 + モデル中心座標から離れている位置で使用されているマテリアルほどリストの後ろ側にくるように。 + かなりいいかげんな実装 """ center = mathutils.Vector([0, 0, 0]) vertices = self.__model.vertices @@ -675,13 +674,13 @@ def __sortMaterials(self): d += (mathutils.Vector(vertices[face[0]].co) - center).length d += (mathutils.Vector(vertices[face[1]].co) - center).length d += (mathutils.Vector(vertices[face[2]].co) - center).length - distances.append((d/mat.vertex_count, mat, offset, face_num, bl_mat_name)) + distances.append((d / mat.vertex_count, mat, offset, face_num, bl_mat_name)) offset += face_num sorted_faces = [] sorted_mat = [] self.__material_name_table.clear() for d, mat, offset, vert_count, bl_mat_name in sorted(distances, key=lambda x: x[0]): - sorted_faces.extend(faces[offset:offset+vert_count]) + sorted_faces.extend(faces[offset : offset + vert_count]) sorted_mat.append(mat) self.__material_name_table.append(bl_mat_name) self.__model.materials = sorted_mat @@ -697,31 +696,28 @@ def __export_bone_morphs(self, root): categories = self.CATEGORIES pose_bones = self.__armature.pose.bones matrix_world = self.__armature.matrix_world - bone_util_cls = BoneConverterPoseMode if self.__armature.data.pose_position != 'REST' else BoneConverter + bone_util_cls = BoneConverterPoseMode if self.__armature.data.pose_position != "REST" else BoneConverter class _RestBone: def __init__(self, b): - self.matrix_local = matmul(matrix_world, b.bone.matrix_local) + self.matrix_local = matrix_world @ b.bone.matrix_local - class _PoseBone: # world space + class _PoseBone: # world space def __init__(self, b): self.bone = _RestBone(b) - self.matrix = matmul(matrix_world, b.matrix) + self.matrix = matrix_world @ b.matrix self.matrix_basis = b.matrix_basis self.location = b.location converter_cache = {} + def _get_converter(b): if b not in converter_cache: converter_cache[b] = bone_util_cls(_PoseBone(blender_bone), self.__scale, invert=True) return converter_cache[b] for morph in mmd_root.bone_morphs: - bone_morph = pmx.BoneMorph( - name=morph.name, - name_e=morph.name_e, - category=categories.get(morph.category, pmx.Morph.CATEGORY_OHTER) - ) + bone_morph = pmx.BoneMorph(name=morph.name, name_e=morph.name_e, category=categories.get(morph.category, pmx.Morph.CATEGORY_OHTER)) for data in morph.data: morph_data = pmx.BoneMorphOffset() try: @@ -747,14 +743,10 @@ def __export_uv_morphs(self, root): categories = self.CATEGORIES append_table_vg = {} for morph in mmd_root.uv_morphs: - uv_morph = pmx.UVMorph( - name=morph.name, - name_e=morph.name_e, - category=categories.get(morph.category, pmx.Morph.CATEGORY_OHTER) - ) + uv_morph = pmx.UVMorph(name=morph.name, name_e=morph.name_e, category=categories.get(morph.category, pmx.Morph.CATEGORY_OHTER)) uv_morph.uv_index = morph.uv_index self.__model.morphs.append(uv_morph) - if morph.data_type == 'VERTEX_GROUP': + if morph.data_type == "VERTEX_GROUP": append_table_vg[morph.name] = uv_morph.offsets.append continue logging.warning(' * Deprecated UV morph "%s", please convert it to vertex groups', morph.name) @@ -770,11 +762,11 @@ def __export_uv_morphs(self, root): scale = uv_morphs[name].vertex_group_scale morph_data = pmx.UVMorphOffset() morph_data.index = v.index - morph_data.offset = (offset[0]*scale, -offset[1]*scale, offset[2]*scale, -offset[3]*scale) + morph_data.offset = (offset[0] * scale, -offset[1] * scale, offset[2] * scale, -offset[3] * scale) append_table_vg[name](morph_data) if incompleted: - logging.warning(' * Incompleted UV morphs %s with vertex groups', incompleted) + logging.warning(" * Incompleted UV morphs %s with vertex groups", incompleted) def __export_group_morphs(self, root): mmd_root = root.mmd_root @@ -783,11 +775,7 @@ def __export_group_morphs(self, root): categories = self.CATEGORIES start_index = len(self.__model.morphs) for morph in mmd_root.group_morphs: - group_morph = pmx.GroupMorph( - name=morph.name, - name_e=morph.name_e, - category=categories.get(morph.category, pmx.Morph.CATEGORY_OHTER) - ) + group_morph = pmx.GroupMorph(name=morph.name, name_e=morph.name_e, category=categories.get(morph.category, pmx.Morph.CATEGORY_OHTER)) self.__model.morphs.append(group_morph) morph_map = self.__get_pmx_morph_map() @@ -812,37 +800,36 @@ def __exportDisplayItems(self, root, bone_map): d.isSpecial = i.is_special items = [] for j in i.data: - if j.type == 'BONE' and j.name in bone_map: + if j.type == "BONE" and j.name in bone_map: items.append((0, bone_map[j.name])) - elif j.type == 'MORPH' and (j.morph_type, j.name) in morph_map: + elif j.type == "MORPH" and (j.morph_type, j.name) in morph_map: items.append((1, morph_map[(j.morph_type, j.name)])) else: - logging.warning('Display item (%s, %s) was not found.', j.type, j.name) + logging.warning("Display item (%s, %s) was not found.", j.type, j.name) d.data = items res.append(d) self.__model.display = res def __get_pmx_morph_map(self): morph_types = { - pmx.GroupMorph : 'group_morphs', - pmx.VertexMorph : 'vertex_morphs', - pmx.BoneMorph : 'bone_morphs', - pmx.UVMorph : 'uv_morphs', - pmx.MaterialMorph : 'material_morphs', - } + pmx.GroupMorph: "group_morphs", + pmx.VertexMorph: "vertex_morphs", + pmx.BoneMorph: "bone_morphs", + pmx.UVMorph: "uv_morphs", + pmx.MaterialMorph: "material_morphs", + } morph_map = {} for i, m in enumerate(self.__model.morphs): morph_map[(morph_types[type(m)], m.name)] = i return morph_map - def __exportRigidBodies(self, rigid_bodies, bone_map): rigid_map = {} rigid_cnt = 0 Vector = mathutils.Vector for obj in rigid_bodies: t, r, s = obj.matrix_world.decompose() - r = r.to_euler('YXZ') + r = r.to_euler("YXZ") rb = obj.rigid_body if rb is None: logging.warning(' * Settings of rigid body "%s" not found, skipped!', obj.name) @@ -857,24 +844,24 @@ def __exportRigidBodies(self, rigid_bodies, bone_map): rigid_shape = mmd_rigid.shape shape_size = Vector(mmd_rigid.size) * (sum(s) / 3) - if rigid_shape == 'SPHERE': + if rigid_shape == "SPHERE": p_rigid.type = 0 p_rigid.size = shape_size * self.__scale - elif rigid_shape == 'BOX': + elif rigid_shape == "BOX": p_rigid.type = 1 p_rigid.size = shape_size.xzy * self.__scale - elif rigid_shape == 'CAPSULE': + elif rigid_shape == "CAPSULE": p_rigid.type = 2 p_rigid.size = shape_size * self.__scale else: - raise Exception('Invalid rigid body type: %s %s', obj.name, rigid_shape) + raise Exception("Invalid rigid body type: %s %s", obj.name, rigid_shape) p_rigid.bone = bone_map.get(mmd_rigid.bone, -1) p_rigid.collision_group_number = mmd_rigid.collision_group_number mask = 0 for i, v in enumerate(mmd_rigid.collision_group_mask): if not v: - mask += (1< 0: name, axis = uv_morph_names[x.group] d = uv_offsets.setdefault(name, [0, 0, 0, 0]) - d['XYZW'.index(axis[1])] += -x.weight if axis[0] == '-' else x.weight + d["XYZW".index(axis[1])] += -x.weight if axis[0] == "-" else x.weight return uv_offsets base_vertices = {} for v in base_mesh.vertices: - base_vertices[v.index] = [_Vertex( - v.co.copy(), - [(vg_to_bone[x.group], x.weight) for x in v.groups if x.weight > 0 and x.group in vg_to_bone], - {}, - get_edge_scale(v), - get_vertex_order(v), - get_uv_offsets(v), - )] + base_vertices[v.index] = [ + _Vertex( + v.co.copy(), + [(vg_to_bone[x.group], x.weight) for x in v.groups if x.weight > 0 and x.group in vg_to_bone], + {}, + get_edge_scale(v), + get_vertex_order(v), + get_uv_offsets(v), + ) + ] # load face data class _DummyUV: uv1 = uv2 = uv3 = mathutils.Vector((0, 1)) + def __init__(self, uvs): - self.uv1, self.uv2, self.uv3 = (v.uv.copy() for v in uvs) + self.uv1, self.uv2, self.uv3 = (v.uv.copy() for v in uvs) - _UVWrapper = lambda x: (_DummyUV(x[i:i+3]) for i in range(0, len(x), 3)) + _UVWrapper = lambda x: (_DummyUV(x[i : i + 3]) for i in range(0, len(x), 3)) material_faces = {} uv_data = base_mesh.uv_layers.active @@ -1107,7 +1072,7 @@ def __init__(self, uvs): if len(face.vertices) != 3: raise Exception idx = face.index * 3 - n1, n2, n3 = loop_normals[idx:idx+3] + n1, n2, n3 = loop_normals[idx : idx + 3] v1 = self.__convertFaceUVToVertexUV(face.vertices[0], uv.uv1, n1, base_vertices) v2 = self.__convertFaceUVToVertexUV(face.vertices[1], uv.uv2, n2, base_vertices) v3 = self.__convertFaceUVToVertexUV(face.vertices[2], uv.uv3, n3, base_vertices) @@ -1122,19 +1087,19 @@ def __init__(self, uvs): for f in material_faces.values(): f.sort(key=lambda x: x.index) _mat_name = lambda x: x.name if x else self.__getDefaultMaterial().name - material_names = {i:_mat_name(m) for i, m in enumerate(base_mesh.materials)} - material_names = {i:material_names.get(i, None) or _mat_name(None) for i in material_faces.keys()} + material_names = {i: _mat_name(m) for i, m in enumerate(base_mesh.materials)} + material_names = {i: material_names.get(i, None) or _mat_name(None) for i in material_faces.keys()} # export add UV - bl_add_uvs = [i for i in base_mesh.uv_layers[1:] if not i.name.startswith('_')] + bl_add_uvs = [i for i in base_mesh.uv_layers[1:] if not i.name.startswith("_")] self.__add_uv_count = max(self.__add_uv_count, len(bl_add_uvs)) for uv_n, uv_tex in enumerate(bl_add_uvs): if uv_n > 3: - logging.warning(' * extra addUV%d+ are not supported', uv_n+1) + logging.warning(" * extra addUV%d+ are not supported", uv_n + 1) break uv_data = _UVWrapper(uv_tex.data) - zw_data = base_mesh.uv_layers.get('_'+uv_tex.name, None) - logging.info(' # exporting addUV%d: %s [zw: %s]', uv_n+1, uv_tex.name, zw_data) + zw_data = base_mesh.uv_layers.get("_" + uv_tex.name, None) + logging.info(" # exporting addUV%d: %s [zw: %s]", uv_n + 1, uv_tex.name, zw_data) if zw_data: zw_data = _UVWrapper(zw_data.data) else: @@ -1153,11 +1118,11 @@ def __init__(self, uvs): shape_key_list = [] if meshObj.data.shape_keys: for i, kb in enumerate(meshObj.data.shape_keys.key_blocks): - if i == 0: # Basis + if i == 0: # Basis continue - if kb.name.startswith('mmd_bind') or kb.name == FnSDEF.SHAPEKEY_NAME: + if kb.name.startswith("mmd_bind") or kb.name == FnSDEF.SHAPEKEY_NAME: continue - if kb.name == 'mmd_sdef_c': # make sure 'mmd_sdef_c' is at first + if kb.name == "mmd_sdef_c": # make sure 'mmd_sdef_c' is at first shape_key_list = [(i, kb)] + shape_key_list else: shape_key_list.append((i, kb)) @@ -1166,7 +1131,7 @@ def __init__(self, uvs): sdef_counts = 0 for i, kb in shape_key_list: shape_key_name = kb.name - logging.info(' - processing shape key: %s', shape_key_name) + logging.info(" - processing shape key: %s", shape_key_name) kb_mute, kb.mute = kb.mute, False kb_value, kb.value = kb.value, 1.0 meshObj.active_shape_key_index = i @@ -1175,10 +1140,10 @@ def __init__(self, uvs): kb.mute = kb_mute kb.value = kb_value if len(mesh.vertices) != len(base_vertices): - logging.warning(' * Error! vertex count mismatch!') + logging.warning(" * Error! vertex count mismatch!") continue - if shape_key_name in {'mmd_sdef_c', 'mmd_sdef_r0', 'mmd_sdef_r1'}: - if shape_key_name == 'mmd_sdef_c': + if shape_key_name in {"mmd_sdef_c", "mmd_sdef_r0", "mmd_sdef_r1"}: + if shape_key_name == "mmd_sdef_c": for v in mesh.vertices: base = base_vertices[v.index][0] if len(base.groups) != 2: @@ -1189,14 +1154,14 @@ def __init__(self, uvs): continue base.sdef_data[:] = tuple(c_co), base_co, base_co sdef_counts += 1 - logging.info(' - Restored %d SDEF vertices', sdef_counts) + logging.info(" - Restored %d SDEF vertices", sdef_counts) elif sdef_counts > 0: - ri = 1 if shape_key_name == 'mmd_sdef_r0' else 2 + ri = 1 if shape_key_name == "mmd_sdef_r0" else 2 for v in mesh.vertices: sdef_data = base_vertices[v.index][0].sdef_data if sdef_data: sdef_data[ri] = tuple(v.co) - logging.info(' - Updated SDEF data') + logging.info(" - Updated SDEF data") else: shape_key_names.append(shape_key_name) for v in mesh.vertices: @@ -1207,33 +1172,30 @@ def __init__(self, uvs): base.offsets[shape_key_name] = offset _to_mesh_clear(meshObj, mesh) - if not pmx_matrix.is_negative: # pmx.load/pmx.save reverse face vertices by default + if not pmx_matrix.is_negative: # pmx.load/pmx.save reverse face vertices by default for f in face_seq: f.vertices.reverse() - return _Mesh( - material_faces, - shape_key_names, - material_names) + return _Mesh(material_faces, shape_key_names, material_names) def __loadMeshData(self, meshObj, bone_map): show_only_shape_key = meshObj.show_only_shape_key active_shape_key_index = meshObj.active_shape_key_index meshObj.active_shape_key_index = 0 - uv_textures = getattr(meshObj.data, 'uv_textures', meshObj.data.uv_layers) + uv_textures = getattr(meshObj.data, "uv_textures", meshObj.data.uv_layers) active_uv_texture_index = uv_textures.active_index uv_textures.active_index = 0 muted_modifiers = [] for m in meshObj.modifiers: - if m.type != 'ARMATURE' or m.object is None: + if m.type != "ARMATURE" or m.object is None: continue - if m.object.data.pose_position == 'REST': + if m.object.data.pose_position == "REST": muted_modifiers.append((m, m.show_viewport)) m.show_viewport = False try: - logging.info('Loading mesh: %s', meshObj.name) + logging.info("Loading mesh: %s", meshObj.name) meshObj.show_only_shape_key = bool(muted_modifiers) return self.__doLoadMeshData(meshObj, bone_map) finally: @@ -1252,36 +1214,36 @@ def __translate_armature(self, root_object: bpy.types.Object): FnTranslations.clear_data(root_object.mmd_root.translation) def execute(self, filepath, **args): - root = args.get('root', None) + root = args.get("root", None) self.__model = pmx.Model() - self.__model.name = 'test' - self.__model.name_e = 'test eng' - self.__model.comment = 'exported by mmd_tools' - self.__model.comment_e = 'exported by mmd_tools' + self.__model.name = "test" + self.__model.name_e = "test eng" + self.__model.comment = "exported by mmd_tools" + self.__model.comment_e = "exported by mmd_tools" if root is not None: self.__model.name = root.mmd_root.name or root.name self.__model.name_e = root.mmd_root.name_e txt = bpy.data.texts.get(root.mmd_root.comment_text, None) if txt: - self.__model.comment = txt.as_string().replace('\n', '\r\n') + self.__model.comment = txt.as_string().replace("\n", "\r\n") txt = bpy.data.texts.get(root.mmd_root.comment_e_text, None) if txt: - self.__model.comment_e = txt.as_string().replace('\n', '\r\n') + self.__model.comment_e = txt.as_string().replace("\n", "\r\n") - self.__armature = args.get('armature', None) - meshes = sorted(args.get('meshes', []), key=lambda x: x.name) - rigids = sorted(args.get('rigid_bodies', []), key=lambda x: x.name) - joints = sorted(args.get('joints', []), key=lambda x: x.name) + self.__armature = args.get("armature", None) + meshes = sorted(args.get("meshes", []), key=lambda x: x.name) + rigids = sorted(args.get("rigid_bodies", []), key=lambda x: x.name) + joints = sorted(args.get("joints", []), key=lambda x: x.name) - self.__scale = args.get('scale', 1.0) - self.__disable_specular = args.get('disable_specular', False) - sort_vertices = args.get('sort_vertices', 'NONE') - if sort_vertices != 'NONE': - self.__vertex_order_map = {'method':sort_vertices} + self.__scale = args.get("scale", 1.0) + self.__disable_specular = args.get("disable_specular", False) + sort_vertices = args.get("sort_vertices", "NONE") + if sort_vertices != "NONE": + self.__vertex_order_map = {"method": sort_vertices} - self.__overwrite_bone_morphs_from_pose_library = args.get('overwrite_bone_morphs_from_pose_library', False) - self.__translate_in_presets = args.get('translate_in_presets', False) + self.__overwrite_bone_morphs_from_pose_library = args.get("overwrite_bone_morphs_from_pose_library", False) + self.__translate_in_presets = args.get("translate_in_presets", False) if self.__translate_in_presets: self.__translate_armature(root) @@ -1290,7 +1252,7 @@ def execute(self, filepath, **args): mesh_data = [self.__loadMeshData(i, nameMap) for i in meshes] self.__exportMeshes(mesh_data, nameMap) - if args.get('sort_materials', False): + if args.get("sort_materials", False): self.__sortMaterials() self.__exportVertexMorphs(mesh_data, root) @@ -1304,22 +1266,23 @@ def execute(self, filepath, **args): rigid_map = self.__exportRigidBodies(rigids, nameMap) self.__exportJoints(joints, rigid_map) - if args.get('copy_textures', False): + if args.get("copy_textures", False): output_dir = os.path.dirname(filepath) - import_folder = root.get('import_folder', '') if root else '' - base_folder = bpyutils.addon_preferences('base_texture_folder', '') + import_folder = root.get("import_folder", "") if root else "" + base_folder = FnContext.get_addon_preferences_attribute(FnContext.ensure_context(), "base_texture_folder", "") self.__copy_textures(output_dir, import_folder or base_folder) pmx.save(filepath, self.__model, add_uv_count=self.__add_uv_count) + def export(filepath, **kwargs): - logging.info('****************************************') - logging.info(' %s module'%__name__) - logging.info('----------------------------------------') + logging.info("****************************************") + logging.info(" %s module" % __name__) + logging.info("----------------------------------------") start_time = time.time() exporter = __PmxExporter() exporter.execute(filepath, **kwargs) - logging.info(' Finished exporting the model in %f seconds.', time.time() - start_time) - logging.info('----------------------------------------') - logging.info(' %s module'%__name__) - logging.info('****************************************') + logging.info(" Finished exporting the model in %f seconds.", time.time() - start_time) + logging.info("----------------------------------------") + logging.info(" %s module" % __name__) + logging.info("****************************************") diff --git a/mmd_tools/core/pmx/importer.py b/mmd_tools/core/pmx/importer.py index 9904c47e..d431e9c1 100644 --- a/mmd_tools/core/pmx/importer.py +++ b/mmd_tools/core/pmx/importer.py @@ -1,50 +1,60 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. + import collections import logging import os import time +from typing import TYPE_CHECKING, List, Optional import bpy from mathutils import Matrix, Vector -from mmd_tools import bpyutils, utils -from mmd_tools.core import pmx -from mmd_tools.core.bone import FnBone -from mmd_tools.core.material import FnMaterial -from mmd_tools.core.model import FnModel, Model -from mmd_tools.core.morph import FnMorph -from mmd_tools.core.vmd.importer import BoneConverter -from mmd_tools.operators.display_item import DisplayItemQuickSetup -from mmd_tools.operators.misc import MoveObject + +from ... import bpyutils, utils +from ...bpyutils import FnContext +from .. import pmx +from ..bone import FnBone +from ..material import FnMaterial +from ..model import FnModel, Model +from ..morph import FnMorph +from ..rigid_body import FnRigidBody +from ..vmd.importer import BoneConverter +from ...operators.misc import MoveObject + +if TYPE_CHECKING: + from ...properties.pose_bone import MMDBone + from ...properties.root import MMDRoot class PMXImporter: CATEGORIES = { - 0: 'SYSTEM', - 1: 'EYEBROW', - 2: 'EYE', - 3: 'MOUTH', - } + 0: "SYSTEM", + 1: "EYEBROW", + 2: "EYE", + 3: "MOUTH", + } MORPH_TYPES = { - 0: 'group_morphs', - 1: 'vertex_morphs', - 2: 'bone_morphs', - 3: 'uv_morphs', - 4: 'uv_morphs', - 5: 'uv_morphs', - 6: 'uv_morphs', - 7: 'uv_morphs', - 8: 'material_morphs', - } + 0: "group_morphs", + 1: "vertex_morphs", + 2: "bone_morphs", + 3: "uv_morphs", + 4: "uv_morphs", + 5: "uv_morphs", + 6: "uv_morphs", + 7: "uv_morphs", + 8: "material_morphs", + } def __init__(self): self.__model = None - self.__targetScene = bpyutils.SceneOp(bpy.context) + self.__targetContext = FnContext.ensure_context() self.__scale = None - self.__root = None - self.__armObj = None - self.__meshObj = None + self.__root: Optional[bpy.types.Object] = None + self.__armObj: Optional[bpy.types.Object] = None + self.__meshObj: Optional[bpy.types.Object] = None self.__vertexGroupTable = None self.__textureTable = None @@ -54,7 +64,7 @@ def __init__(self): self.__materialTable = [] self.__imageTable = {} - self.__sdefVertices = {} # pmx vertices + self.__sdefVertices = {} # pmx vertices self.__blender_ik_links = set() self.__vertex_map = None @@ -62,50 +72,49 @@ def __init__(self): @staticmethod def __safe_name(name, max_length=59): - return str(bytes(name, 'utf8')[:max_length], 'utf8', errors='replace') + return str(bytes(name, "utf8")[:max_length], "utf8", errors="replace") @staticmethod def flipUV_V(uv): u, v = uv - return u, 1.0-v + return u, 1.0 - v def __createObjects(self): - """ Create main objects and link them to scene. - """ + """Create main objects and link them to scene.""" pmxModel = self.__model obj_name = self.__safe_name(bpy.path.display_name(pmxModel.filepath), max_length=54) self.__rig = Model.create(pmxModel.name, pmxModel.name_e, self.__scale, obj_name) root = self.__rig.rootObject() - mmd_root = root.mmd_root + mmd_root: MMDRoot = root.mmd_root self.__root = root self.__armObj = self.__rig.armature() - root['import_folder'] = os.path.dirname(pmxModel.filepath) + root["import_folder"] = os.path.dirname(pmxModel.filepath) txt = bpy.data.texts.new(obj_name) - txt.from_string(pmxModel.comment.replace('\r', '')) + txt.from_string(pmxModel.comment.replace("\r", "")) mmd_root.comment_text = txt.name - txt = bpy.data.texts.new(obj_name+'_e') - txt.from_string(pmxModel.comment_e.replace('\r', '')) + txt = bpy.data.texts.new(obj_name + "_e") + txt.from_string(pmxModel.comment_e.replace("\r", "")) mmd_root.comment_e_text = txt.name def __createMeshObject(self): model_name = self.__root.name - self.__meshObj = bpy.data.objects.new(name=model_name+'_mesh', object_data=bpy.data.meshes.new(name=model_name)) + self.__meshObj = bpy.data.objects.new(name=model_name + "_mesh", object_data=bpy.data.meshes.new(name=model_name)) self.__meshObj.parent = self.__armObj - self.__targetScene.link_object(self.__meshObj) + FnContext.link_object(self.__targetContext, self.__meshObj) def __createBasisShapeKey(self): if self.__meshObj.data.shape_keys: - assert(len(self.__meshObj.data.vertices) > 0) - assert(len(self.__meshObj.data.shape_keys.key_blocks) > 1) + assert len(self.__meshObj.data.vertices) > 0 + assert len(self.__meshObj.data.shape_keys.key_blocks) > 1 return - self.__targetScene.active_object = self.__meshObj + FnContext.set_active_object(self.__targetContext, self.__meshObj) bpy.ops.object.shape_key_add() def __importVertexGroup(self): vgroups = self.__meshObj.vertex_groups - self.__vertexGroupTable = [vgroups.new(name=i.name) for i in self.__model.bones] or [vgroups.new(name='NO BONES')] + self.__vertexGroupTable = [vgroups.new(name=i.name) for i in self.__model.bones] or [vgroups.new(name="NO BONES")] def __importVertices(self): self.__importVertexGroup() @@ -121,39 +130,39 @@ def __importVertices(self): if vertex_count < 1: return - mesh = self.__meshObj.data + mesh: bpy.types.Mesh = self.__meshObj.data mesh.vertices.add(count=vertex_count) - mesh.vertices.foreach_set('co', tuple(i for pv in pmx_vertices for i in (Vector(pv.co).xzy * self.__scale))) + mesh.vertices.foreach_set("co", tuple(i for pv in pmx_vertices for i in (Vector(pv.co).xzy * self.__scale))) vertex_group_table = self.__vertexGroupTable - vg_edge_scale = self.__meshObj.vertex_groups.new(name='mmd_edge_scale') - vg_vertex_order = self.__meshObj.vertex_groups.new(name='mmd_vertex_order') + vg_edge_scale = self.__meshObj.vertex_groups.new(name="mmd_edge_scale") + vg_vertex_order = self.__meshObj.vertex_groups.new(name="mmd_vertex_order") for i, pv in enumerate(pmx_vertices): pv_bones, pv_weights, idx = pv.weight.bones, pv.weight.weights, (i,) - vg_edge_scale.add(index=idx, weight=pv.edge_scale, type='REPLACE') - vg_vertex_order.add(index=idx, weight=i/vertex_count, type='REPLACE') + vg_edge_scale.add(index=idx, weight=pv.edge_scale, type="REPLACE") + vg_vertex_order.add(index=idx, weight=i / vertex_count, type="REPLACE") if isinstance(pv_weights, pmx.BoneWeightSDEF): if pv_bones[0] > pv_bones[1]: pv_bones.reverse() pv_weights.weight = 1.0 - pv_weights.weight pv_weights.r0, pv_weights.r1 = pv_weights.r1, pv_weights.r0 - vertex_group_table[pv_bones[0]].add(index=idx, weight=pv_weights.weight, type='ADD') - vertex_group_table[pv_bones[1]].add(index=idx, weight=1.0-pv_weights.weight, type='ADD') + vertex_group_table[pv_bones[0]].add(index=idx, weight=pv_weights.weight, type="ADD") + vertex_group_table[pv_bones[1]].add(index=idx, weight=1.0 - pv_weights.weight, type="ADD") self.__sdefVertices[i] = pv elif len(pv_bones) == 1: bone_index = pv_bones[0] if bone_index >= 0: - vertex_group_table[bone_index].add(index=idx, weight=1.0, type='ADD') + vertex_group_table[bone_index].add(index=idx, weight=1.0, type="ADD") elif len(pv_bones) == 2: - vertex_group_table[pv_bones[0]].add(index=idx, weight=pv_weights[0], type='ADD') - vertex_group_table[pv_bones[1]].add(index=idx, weight=1.0-pv_weights[0], type='ADD') + vertex_group_table[pv_bones[0]].add(index=idx, weight=pv_weights[0], type="ADD") + vertex_group_table[pv_bones[1]].add(index=idx, weight=1.0 - pv_weights[0], type="ADD") elif len(pv_bones) == 4: for bone, weight in zip(pv_bones, pv_weights): - vertex_group_table[bone].add(index=idx, weight=weight, type='ADD') + vertex_group_table[bone].add(index=idx, weight=weight, type="ADD") else: - raise Exception('unkown bone weight type.') + raise Exception("unkown bone weight type.") vg_edge_scale.lock_weight = True vg_vertex_order.lock_weight = True @@ -163,15 +172,15 @@ def __storeVerticesSDEF(self): return self.__createBasisShapeKey() - sdefC = self.__meshObj.shape_key_add(name='mmd_sdef_c') - sdefR0 = self.__meshObj.shape_key_add(name='mmd_sdef_r0') - sdefR1 = self.__meshObj.shape_key_add(name='mmd_sdef_r1') + sdefC = self.__meshObj.shape_key_add(name="mmd_sdef_c") + sdefR0 = self.__meshObj.shape_key_add(name="mmd_sdef_r0") + sdefR1 = self.__meshObj.shape_key_add(name="mmd_sdef_r1") for i, pv in self.__sdefVertices.items(): w = pv.weight.weights sdefC.data[i].co = Vector(w.c).xzy * self.__scale sdefR0.data[i].co = Vector(w.r0).xzy * self.__scale sdefR1.data[i].co = Vector(w.r1).xzy * self.__scale - logging.info('Stored %d SDEF vertices', len(self.__sdefVertices)) + logging.info("Stored %d SDEF vertices", len(self.__sdefVertices)) def __importTextures(self): pmxModel = self.__model @@ -181,14 +190,14 @@ def __importTextures(self): self.__textureTable.append(bpy.path.resolve_ncase(path=i.path)) def __createEditBones(self, obj, pmx_bones): - """ create EditBones from pmx file data. + """create EditBones from pmx file data. @return the list of bone names which can be accessed by the bone index of pmx data. """ editBoneTable = [] nameTable = [] specialTipBones = [] dependency_cycle_ik_bones = [] - #for i, p_bone in enumerate(pmx_bones): + # for i, p_bone in enumerate(pmx_bones): # if p_bone.isIK: # if p_bone.target != -1: # t = pmx_bones[p_bone.target] @@ -196,8 +205,9 @@ def __createEditBones(self, obj, pmx_bones): # dependency_cycle_ik_bones.append(i) from math import isfinite + def _VectorXZY(v): - return Vector(v).xzy if all(isfinite(n) for n in v) else Vector((0,0,0)) + return Vector(v).xzy if all(isfinite(n) for n in v) else Vector((0, 0, 0)) with bpyutils.edit_object(obj) as data: for i in pmx_bones: @@ -226,19 +236,19 @@ def _VectorXZY(v): for b_bone, m_bone in zip(editBoneTable, pmx_bones): if m_bone.isIK and m_bone.target != -1: - logging.debug(' - checking IK links of %s', b_bone.name) + logging.debug(" - checking IK links of %s", b_bone.name) b_target = editBoneTable[m_bone.target] for i in range(len(m_bone.ik_links)): b_bone_link = editBoneTable[m_bone.ik_links[i].target] if self.__fix_IK_links or b_bone_link.length < 0.001: - b_bone_tail = b_target if i == 0 else editBoneTable[m_bone.ik_links[i-1].target] + b_bone_tail = b_target if i == 0 else editBoneTable[m_bone.ik_links[i - 1].target] loc = b_bone_tail.head - b_bone_link.head if loc.length < 0.001: - logging.warning(' ** unsolved IK link %s **', b_bone_link.name) + logging.warning(" ** unsolved IK link %s **", b_bone_link.name) elif b_bone_tail.parent != b_bone_link: - logging.warning(' ** skipped IK link %s **', b_bone_link.name) + logging.warning(" ** skipped IK link %s **", b_bone_link.name) elif (b_bone_link.tail - b_bone_tail.head).length > 1e-4: - logging.debug(' * fix IK link %s', b_bone_link.name) + logging.debug(" * fix IK link %s", b_bone_link.name) b_bone_link.tail = b_bone_link.head + loc for b_bone, m_bone in zip(editBoneTable, pmx_bones): @@ -253,7 +263,7 @@ def _VectorXZY(v): else: b_bone.tail = b_bone.head + Vector((0, 0, 1)) * self.__scale if m_bone.displayConnection != -1 and m_bone.displayConnection != [0.0, 0.0, 0.0]: - logging.debug(' * special tip bone %s, display %s', b_bone.name, str(m_bone.displayConnection)) + logging.debug(" * special tip bone %s, display %s", b_bone.name, str(m_bone.displayConnection)) specialTipBones.append(b_bone.name) for b_bone, m_bone in zip(editBoneTable, pmx_bones): @@ -273,13 +283,13 @@ def _VectorXZY(v): continue if not m_bone.isMovable: continue - logging.warning(' * connected: %s (%d)-> %s', b_bone.name, len(b_bone.children), t.name) + logging.warning(" * connected: %s (%d)-> %s", b_bone.name, len(b_bone.children), t.name) t.use_connect = True return nameTable, specialTipBones - def __sortPoseBonesByBoneIndex(self, pose_bones, bone_names): - r = [] + def __sortPoseBonesByBoneIndex(self, pose_bones: List[bpy.types.PoseBone], bone_names): + r: List[bpy.types.PoseBone] = [] for i in bone_names: r.append(pose_bones[i]) return r @@ -293,7 +303,7 @@ def convertIKLimitAngles(min_angle, max_angle, bone_matrix, invert=False): mat.invert() # align matrix to global axes - m = Matrix([[0,0,0], [0,0,0], [0,0,0]]) + m = Matrix([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) i_set, j_set = [0, 1, 2], [0, 1, 2] for _ in range(3): ii, jj = i_set[0], j_set[0] @@ -305,19 +315,19 @@ def convertIKLimitAngles(min_angle, max_angle, bone_matrix, invert=False): j_set.remove(jj) m[ii][jj] = -1 if mat[ii][jj] < 0 else 1 - new_min_angle = bpyutils.matmul(m, Vector(min_angle)) - new_max_angle = bpyutils.matmul(m, Vector(max_angle)) + new_min_angle = m @ Vector(min_angle) + new_max_angle = m @ Vector(max_angle) for i in range(3): if new_min_angle[i] > new_max_angle[i]: new_min_angle[i], new_max_angle[i] = new_max_angle[i], new_min_angle[i] return new_min_angle, new_max_angle def __applyIk(self, index, pmx_bone, pose_bones): - """ create a IK bone constraint - If the IK bone and the target bone is separated, a dummy IK target bone is created as a child of the IK bone. - @param index the bone index - @param pmx_bone pmx.Bone - @param pose_bones the list of PoseBones sorted by the bone index + """create a IK bone constraint + If the IK bone and the target bone is separated, a dummy IK target bone is created as a child of the IK bone. + @param index the bone index + @param pmx_bone pmx.Bone + @param pose_bones the list of PoseBones sorted by the bone index """ # for tracking mmd ik target, simple explaination: @@ -341,28 +351,27 @@ def __applyIk(self, index, pmx_bone, pose_bones): if len(pmx_bone.ik_links) > 1: ik_constraint_bone_real = pose_bones[pmx_bone.ik_links[1].target] del pmx_bone.ik_links[0] - logging.warning(' * fix IK settings of IK bone (%s)', ik_bone.name) - is_valid_ik = (ik_constraint_bone == ik_constraint_bone_real) + logging.warning(" * fix IK settings of IK bone (%s)", ik_bone.name) + is_valid_ik = ik_constraint_bone == ik_constraint_bone_real if not is_valid_ik: ik_constraint_bone = ik_constraint_bone_real - logging.warning(' * IK bone (%s) warning: IK target (%s) is not a child of IK link 0 (%s)', - ik_bone.name, ik_target.name, ik_constraint_bone.name) + logging.warning(" * IK bone (%s) warning: IK target (%s) is not a child of IK link 0 (%s)", ik_bone.name, ik_target.name, ik_constraint_bone.name) elif any(pose_bones[i.target].parent != pose_bones[j.target] for i, j in zip(pmx_bone.ik_links, pmx_bone.ik_links[1:])): - logging.warning(' * Invalid IK bone (%s): IK chain does not follow parent-child relationship', ik_bone.name) + logging.warning(" * Invalid IK bone (%s): IK chain does not follow parent-child relationship", ik_bone.name) return if ik_constraint_bone is None or len(pmx_bone.ik_links) < 1: - logging.warning(' * Invalid IK bone (%s)', ik_bone.name) + logging.warning(" * Invalid IK bone (%s)", ik_bone.name) return - c = ik_target.constraints.new(type='DAMPED_TRACK') - c.name = 'mmd_ik_target_override' + c = ik_target.constraints.new(type="DAMPED_TRACK") + c.name = "mmd_ik_target_override" c.mute = True c.influence = 0 c.target = self.__armObj c.subtarget = ik_constraint_bone.name - if not is_valid_ik or next((c for c in ik_constraint_bone.constraints if c.type == 'IK' and c.is_valid), None): - c.name = 'mmd_ik_target_custom' - c.subtarget = ik_bone.name # point to IK control bone + if not is_valid_ik or next((c for c in ik_constraint_bone.constraints if c.type == "IK" and c.is_valid), None): + c.name = "mmd_ik_target_custom" + c.subtarget = ik_bone.name # point to IK control bone ik_bone.mmd_bone.ik_rotation_constraint = pmx_bone.rotationConstraint use_custom_ik = True else: @@ -373,14 +382,14 @@ def __applyIk(self, index, pmx_bone, pose_bones): ikConst.iterations = pmx_bone.loopCount ikConst.chain_count = len(pmx_bone.ik_links) if not is_valid_ik: - ikConst.pole_target = self.__armObj # make it an incomplete/invalid setting + ikConst.pole_target = self.__armObj # make it an incomplete/invalid setting for idx, i in enumerate(pmx_bone.ik_links): if use_custom_ik or i.target in self.__blender_ik_links: - c = ik_bone.constraints.new(type='LIMIT_ROTATION') + c = ik_bone.constraints.new(type="LIMIT_ROTATION") c.mute = True c.influence = 0 - c.name = 'mmd_ik_limit_custom%d'%idx - use_limits = c.use_limit_x = c.use_limit_y = c.use_limit_z = (i.maximumAngle is not None) + c.name = "mmd_ik_limit_custom%d" % idx + use_limits = c.use_limit_x = c.use_limit_y = c.use_limit_z = i.maximumAngle is not None if use_limits: minimum, maximum = self.convertIKLimitAngles(i.minimumAngle, i.maximumAngle, pose_bones[i.target].bone.matrix_local) c.max_x, c.max_y, c.max_z = maximum @@ -397,10 +406,10 @@ def __applyIk(self, index, pmx_bone, pose_bones): bone.ik_max_x, bone.ik_max_y, bone.ik_max_z = maximum bone.ik_min_x, bone.ik_min_y, bone.ik_min_z = minimum - c = bone.constraints.new(type='LIMIT_ROTATION') + c = bone.constraints.new(type="LIMIT_ROTATION") c.mute = not is_valid_ik - c.name = 'mmd_ik_limit_override' - c.owner_space = 'LOCAL' + c.name = "mmd_ik_limit_override" + c.owner_space = "LOCAL" c.max_x, c.max_y, c.max_z = maximum c.min_x, c.min_y, c.min_z = minimum c.use_limit_x = bone.ik_max_x != c.max_x or bone.ik_min_x != c.min_x @@ -415,8 +424,8 @@ def __importBones(self): self.__boneTable = pose_bones for i, pmx_bone in sorted(enumerate(pmxModel.bones), key=lambda x: x[1].transform_order): b_bone = pose_bones[i] - mmd_bone = b_bone.mmd_bone - mmd_bone.name_j = b_bone.name #pmx_bone.name + mmd_bone: MMDBone = b_bone.mmd_bone + mmd_bone.name_j = b_bone.name # pmx_bone.name mmd_bone.name_e = pmx_bone.name_e mmd_bone.is_controllable = pmx_bone.isControllable mmd_bone.transform_order = pmx_bone.transform_order @@ -427,7 +436,7 @@ def __importBones(self): elif b_bone.name in specialTipBones: mmd_bone.is_tip = True - b_bone.bone.hide = not pmx_bone.visible #or mmd_bone.is_tip + b_bone.bone.hide = not pmx_bone.visible # or mmd_bone.is_tip if not pmx_bone.isRotatable: b_bone.lock_rotation = [True, True, True] @@ -464,63 +473,64 @@ def __importBones(self): def __importRigids(self): start_time = time.time() self.__rigidTable = {} - rigid_pool = self.__rig.createRigidBodyPool(len(self.__model.rigids)) + context = FnContext.ensure_context() + rigid_pool = FnRigidBody.new_rigid_body_objects(context, FnModel.ensure_rigid_group_object(context, self.__rig.rootObject()), len(self.__model.rigids)) for i, (rigid, rigid_obj) in enumerate(zip(self.__model.rigids, rigid_pool)): loc = Vector(rigid.location).xzy * self.__scale rot = Vector(rigid.rotation).xzy * -1 size = Vector(rigid.size).xzy if rigid.type == pmx.Rigid.TYPE_BOX else Vector(rigid.size) - obj = self.__rig.createRigidBody( - obj = rigid_obj, - name = rigid.name, - name_e = rigid.name_e, - shape_type = rigid.type, - dynamics_type = rigid.mode, - location = loc, - rotation = rot, - size = size * self.__scale, - collision_group_number = rigid.collision_group_number, - collision_group_mask = [rigid.collision_group_mask & (1<= (2, 80, 0): - self.__fixOverlappingFaceMaterials(mesh.materials, mesh.vertices, loop_indices, material_indices) + self.__fixOverlappingFaceMaterials(mesh.materials, mesh.vertices, loop_indices, material_indices) def __fixOverlappingFaceMaterials(self, materials, vertices, loop_indices, material_indices): - # This is not the best way to setup blend_method, might just work for some common cases. And FnMaterial.update_alpha() is still using 'HASHED'. + # FIXME: This is not the best way to setup blend_method, might just work for some common cases. And FnMaterial.update_alpha() is still using 'HASHED'. # For EEVEE, basically users should know which blend_method is best for each material of their models. # For Cycles, users have to offset or delete those z-fighting faces to fix it manually. check = {} mi_skip = -1 _vi_cache = {} + def _rounded_co_vi(vi): if vi not in _vi_cache: vco = vertices[vi].co _vi_cache[vi] = (round(vco[0], 6), round(vco[1], 6), round(vco[2], 6)) return _vi_cache[vi] - assert(len(loop_indices) == len(material_indices)*3) + assert len(loop_indices) == len(material_indices) * 3 for i, mi in enumerate(material_indices): if mi <= mi_skip: continue - si = 3*i - verts = tuple(sorted((_rounded_co_vi(loop_indices[si]), _rounded_co_vi(loop_indices[si+1]), _rounded_co_vi(loop_indices[si+2])))) + si = 3 * i + verts = tuple(sorted((_rounded_co_vi(loop_indices[si]), _rounded_co_vi(loop_indices[si + 1]), _rounded_co_vi(loop_indices[si + 2])))) if verts not in check: check[verts] = mi elif check[verts] < mi: - logging.debug(' >> fix blend method of material: %s', materials[mi].name) - materials[mi].blend_method = 'BLEND' + logging.debug(" >> fix blend method of material: %s", materials[mi].name) + materials[mi].blend_method = "BLEND" materials[mi].show_transparent_back = False mi_skip = mi @@ -664,7 +674,7 @@ def __importVertexMorphs(self): vtx_morph = mmd_root.vertex_morphs.add() vtx_morph.name = morph.name vtx_morph.name_e = morph.name_e - vtx_morph.category = categories.get(morph.category, 'OTHER') + vtx_morph.category = categories.get(morph.category, "OTHER") for md in morph.offsets: shapeKeyPoint = shapeKey.data[md.index] shapeKeyPoint.co += Vector(md.offset).xzy * self.__scale @@ -676,13 +686,13 @@ def __importMaterialMorphs(self): mat_morph = mmd_root.material_morphs.add() mat_morph.name = morph.name mat_morph.name_e = morph.name_e - mat_morph.category = categories.get(morph.category, 'OTHER') + mat_morph.category = categories.get(morph.category, "OTHER") for morph_data in morph.offsets: data = mat_morph.data.add() data.related_mesh = self.__meshObj.data.name if 0 <= morph_data.index < len(self.__materialTable): data.material = self.__materialTable[morph_data.index].name - data.offset_type = ['MULT', 'ADD'][morph_data.offset_type] + data.offset_type = ["MULT", "ADD"][morph_data.offset_type] data.diffuse_color = morph_data.diffuse_offset data.specular_color = morph_data.specular_offset data.shininess = morph_data.shininess_offset @@ -700,7 +710,7 @@ def __importBoneMorphs(self): bone_morph = mmd_root.bone_morphs.add() bone_morph.name = morph.name bone_morph.name_e = morph.name_e - bone_morph.category = categories.get(morph.category, 'OTHER') + bone_morph.category = categories.get(morph.category, "OTHER") for morph_data in morph.offsets: if not (0 <= morph_data.index < len(self.__boneTable)): continue @@ -714,18 +724,18 @@ def __importBoneMorphs(self): def __importUVMorphs(self): mmd_root = self.__root.mmd_root categories = self.CATEGORIES - __OffsetData = collections.namedtuple('OffsetData', 'index, offset') + __OffsetData = collections.namedtuple("OffsetData", "index, offset") __convert_offset = lambda x: (x[0], -x[1], x[2], -x[3]) for morph in (x for x in self.__model.morphs if isinstance(x, pmx.UVMorph)): uv_morph = mmd_root.uv_morphs.add() uv_morph.name = morph.name uv_morph.name_e = morph.name_e - uv_morph.category = categories.get(morph.category, 'OTHER') + uv_morph.category = categories.get(morph.category, "OTHER") uv_morph.uv_index = morph.uv_index offsets = (__OffsetData(d.index, __convert_offset(d.offset)) for d in morph.offsets) - FnMorph.store_uv_morph_data(self.__meshObj, uv_morph, offsets, '') - uv_morph.data_type = 'VERTEX_GROUP' + FnMorph.store_uv_morph_data(self.__meshObj, uv_morph, offsets, "") + uv_morph.data_type = "VERTEX_GROUP" def __importGroupMorphs(self): mmd_root = self.__root.mmd_root @@ -736,7 +746,7 @@ def __importGroupMorphs(self): group_morph = mmd_root.group_morphs.add() group_morph.name = morph.name group_morph.name_e = morph.name_e - group_morph.category = categories.get(morph.category, 'OTHER') + group_morph.category = categories.get(morph.category, "OTHER") for morph_data in morph.offsets: if not (0 <= morph_data.morph < len(pmx_morphs)): continue @@ -759,32 +769,29 @@ def __importDisplayFrames(self): for disp_type, index in i.data: item = frame.data.add() if disp_type == 0: - item.type = 'BONE' + item.type = "BONE" item.name = self.__boneTable[index].name elif disp_type == 1: - item.type = 'MORPH' + item.type = "MORPH" morph = pmxModel.morphs[index] item.name = morph.name item.morph_type = morph_types[morph.type_index()] else: - raise Exception('Unknown display item type.') + raise Exception("Unknown display item type.") - DisplayItemQuickSetup.apply_bone_groups(root.mmd_root, self.__armObj) + FnBone.sync_bone_collections_from_display_item_frames(self.__armObj) def __addArmatureModifier(self, meshObj, armObj): # TODO: move to model.py - armModifier = meshObj.modifiers.new(name='Armature', type='ARMATURE') + armModifier = meshObj.modifiers.new(name="Armature", type="ARMATURE") armModifier.object = armObj armModifier.use_vertex_groups = True - armModifier.name = 'mmd_bone_order_override' - armModifier.show_render = armModifier.show_viewport = (len(meshObj.data.vertices) > 0) + armModifier.name = "mmd_bone_order_override" + armModifier.show_render = armModifier.show_viewport = len(meshObj.data.vertices) > 0 def __assignCustomNormals(self): - mesh = self.__meshObj.data - if not hasattr(mesh, 'has_custom_normals'): - logging.info(' * No support for custom normals!!') - return - logging.info('Setting custom normals...') + mesh: bpy.types.Mesh = self.__meshObj.data + logging.info("Setting custom normals...") if self.__vertex_map: verts, faces = self.__model.vertices, self.__model.faces custom_normals = [(Vector(verts[i].normal).xzy).normalized() for f in faces for i in f] @@ -792,8 +799,7 @@ def __assignCustomNormals(self): else: custom_normals = [(Vector(v.normal).xzy).normalized() for v in self.__model.vertices] mesh.normals_split_custom_set_from_vertices(custom_normals) - mesh.use_auto_smooth = True - logging.info(' - Done!!') + logging.info(" - Done!!") def __renameLRBones(self, use_underscore): pose_bones = self.__armObj.pose.bones @@ -809,43 +815,43 @@ def __translateBoneNames(self): def __fixRepeatedMorphName(self): used_names = set() for m in self.__model.morphs: - m.name = utils.uniqueName(m.name or 'Morph', used_names) + m.name = utils.unique_name(m.name or "Morph", used_names) used_names.add(m.name) def execute(self, **args): - if 'pmx' in args: - self.__model = args['pmx'] + if "pmx" in args: + self.__model = args["pmx"] else: - self.__model = pmx.load(args['filepath']) + self.__model = pmx.load(args["filepath"]) self.__fixRepeatedMorphName() - types = args.get('types', set()) - clean_model = args.get('clean_model', False) - remove_doubles = args.get('remove_doubles', False) - self.__scale = args.get('scale', 1.0) - self.__use_mipmap = args.get('use_mipmap', True) - self.__sph_blend_factor = args.get('sph_blend_factor', 1.0) - self.__spa_blend_factor = args.get('spa_blend_factor', 1.0) - self.__fix_IK_links = args.get('fix_IK_links', False) - self.__apply_bone_fixed_axis = args.get('apply_bone_fixed_axis', False) - self.__translator = args.get('translator', None) - - logging.info('****************************************') - logging.info(' mmd_tools.import_pmx module') - logging.info('----------------------------------------') - logging.info(' Start to load model data form a pmx file') - logging.info(' by the mmd_tools.pmx modlue.') - logging.info('') + types = args.get("types", set()) + clean_model = args.get("clean_model", False) + remove_doubles = args.get("remove_doubles", False) + self.__scale = args.get("scale", 1.0) + self.__use_mipmap = args.get("use_mipmap", True) + self.__sph_blend_factor = args.get("sph_blend_factor", 1.0) + self.__spa_blend_factor = args.get("spa_blend_factor", 1.0) + self.__fix_IK_links = args.get("fix_IK_links", False) + self.__apply_bone_fixed_axis = args.get("apply_bone_fixed_axis", False) + self.__translator = args.get("translator", None) + + logging.info("****************************************") + logging.info(" mmd_tools.import_pmx module") + logging.info("----------------------------------------") + logging.info(" Start to load model data form a pmx file") + logging.info(" by the mmd_tools.pmx modlue.") + logging.info("") start_time = time.time() self.__createObjects() - if 'MESH' in types: + if "MESH" in types: if clean_model: - _PMXCleaner.clean(self.__model, 'MORPHS' not in types) + _PMXCleaner.clean(self.__model, "MORPHS" not in types) if remove_doubles: - self.__vertex_map = _PMXCleaner.remove_doubles(self.__model, 'MORPHS' not in types) + self.__vertex_map = _PMXCleaner.remove_doubles(self.__model, "MORPHS" not in types) self.__createMeshObject() self.__importVertices() self.__importMaterials() @@ -854,14 +860,14 @@ def execute(self, **args): self.__assignCustomNormals() self.__storeVerticesSDEF() - if 'ARMATURE' in types: + if "ARMATURE" in types: # for tracking bone order - if 'MESH' not in types: + if "MESH" not in types: self.__createMeshObject() self.__importVertexGroup() self.__importBones() - if args.get('rename_LR_bones', False): - use_underscore = args.get('use_underscore', False) + if args.get("rename_LR_bones", False): + use_underscore = args.get("use_underscore", False) self.__renameLRBones(use_underscore) if self.__translator: self.__translateBoneNames() @@ -869,16 +875,16 @@ def execute(self, **args): FnBone.apply_bone_fixed_axis(self.__armObj) FnBone.apply_additional_transformation(self.__armObj) - if 'PHYSICS' in types: + if "PHYSICS" in types: self.__importRigids() self.__importJoints() - if 'DISPLAY' in types: + if "DISPLAY" in types: self.__importDisplayFrames() else: self.__rig.initialDisplayFrames() - if 'MORPHS' in types: + if "MORPHS" in types: self.__importGroupMorphs() self.__importVertexMorphs() self.__importBoneMorphs() @@ -888,30 +894,30 @@ def execute(self, **args): if self.__meshObj: self.__addArmatureModifier(self.__meshObj, self.__armObj) - FnModel.change_mmd_ik_loop_factor(self.__root, args.get('ik_loop_factor', 1)) - #bpy.context.scene.gravity[2] = -9.81 * 10 * self.__scale - self.__targetScene.active_object = self.__root + FnModel.change_mmd_ik_loop_factor(self.__root, args.get("ik_loop_factor", 1)) + # bpy.context.scene.gravity[2] = -9.81 * 10 * self.__scale + FnContext.set_active_object(self.__targetContext, self.__root) - logging.info(' Finished importing the model in %f seconds.', time.time() - start_time) - logging.info('----------------------------------------') - logging.info(' mmd_tools.import_pmx module') - logging.info('****************************************') + logging.info(" Finished importing the model in %f seconds.", time.time() - start_time) + logging.info("----------------------------------------") + logging.info(" mmd_tools.import_pmx module") + logging.info("****************************************") class _PMXCleaner: @classmethod def clean(cls, pmx_model, mesh_only): - logging.info('Cleaning PMX data...') + logging.info("Cleaning PMX data...") pmx_faces = pmx_model.faces pmx_vertices = pmx_model.vertices # clean face/vertex cls.__clean_pmx_faces(pmx_faces, pmx_model.materials, lambda f: frozenset(f)) - index_map = {v:v for f in pmx_faces for v in f} + index_map = {v: v for f in pmx_faces for v in f} is_index_clean = len(index_map) == len(pmx_vertices) if is_index_clean: - logging.info(' (vertices is clean)') + logging.info(" (vertices is clean)") else: new_vertex_count = 0 for v in sorted(index_map): @@ -919,7 +925,7 @@ def clean(cls, pmx_model, mesh_only): pmx_vertices[new_vertex_count] = pmx_vertices[v] index_map[v] = new_vertex_count new_vertex_count += 1 - logging.warning(' - removed %d vertices', len(pmx_vertices)-new_vertex_count) + logging.warning(" - removed %d vertices", len(pmx_vertices) - new_vertex_count) del pmx_vertices[new_vertex_count:] # update vertex indices of faces @@ -927,7 +933,7 @@ def clean(cls, pmx_model, mesh_only): f[:] = [index_map[v] for v in f] if mesh_only: - logging.info(' - Done (mesh only)!!') + logging.info(" - Done (mesh only)!!") return if not is_index_clean: @@ -935,12 +941,13 @@ def clean(cls, pmx_model, mesh_only): def __update_index(x): x.index = index_map.get(x.index, None) return x.index is not None + cls.__clean_pmx_morphs(pmx_model.morphs, __update_index) - logging.info(' - Done!!') + logging.info(" - Done!!") @classmethod def remove_doubles(cls, pmx_model, mesh_only): - logging.info('Removing doubles...') + logging.info("Removing doubles...") pmx_vertices = pmx_model.vertices vertex_map = [None] * len(pmx_vertices) @@ -952,41 +959,41 @@ def remove_doubles(cls, pmx_model, mesh_only): if not isinstance(m, pmx.VertexMorph) and not isinstance(m, pmx.UVMorph): continue for x in m.offsets: - vertex_map[x.index].append((i,)+tuple(x.offset)) + vertex_map[x.index].append((i,) + tuple(x.offset)) # generate vertex merging table keys = {} for i, v in enumerate(vertex_map): k = tuple(v) if k in keys: - vertex_map[i] = keys[k] # merge pmx_vertices[i] to pmx_vertices[keys[k][0]] + vertex_map[i] = keys[k] # merge pmx_vertices[i] to pmx_vertices[keys[k][0]] else: - vertex_map[i] = keys[k] = (i, len(keys)) # (pmx index, blender index) + vertex_map[i] = keys[k] = (i, len(keys)) # (pmx index, blender index) counts = len(vertex_map) - len(keys) keys.clear() if counts: - logging.warning(' - %d vertices will be removed', counts) + logging.warning(" - %d vertices will be removed", counts) else: - logging.info(' - Done (no changes)!!') + logging.info(" - Done (no changes)!!") return None # clean face - #face_key_func = lambda f: frozenset(vertex_map[x][0] for x in f) - face_key_func = lambda f: frozenset({vertex_map[x][0]:tuple(pmx_vertices[x].uv) for x in f}.items()) + # face_key_func = lambda f: frozenset(vertex_map[x][0] for x in f) + face_key_func = lambda f: frozenset({vertex_map[x][0]: tuple(pmx_vertices[x].uv) for x in f}.items()) cls.__clean_pmx_faces(pmx_model.faces, pmx_model.materials, face_key_func) if mesh_only: - logging.info(' - Done (mesh only)!!') + logging.info(" - Done (mesh only)!!") else: # clean vertex/uv morphs def __update_index(x): indices = vertex_map[x.index] x.index = indices[1] if x.index == indices[0] else None return x.index is not None + cls.__clean_pmx_morphs(pmx_model.morphs, __update_index) - logging.info(' - Done!!') + logging.info(" - Done!!") return vertex_map - @staticmethod def __clean_pmx_faces(pmx_faces, pmx_materials, face_key_func): new_face_count = 0 @@ -994,7 +1001,7 @@ def __clean_pmx_faces(pmx_faces, pmx_materials, face_key_func): for mat in pmx_materials: used_faces = set() new_vertex_count = 0 - for i in range(int(mat.vertex_count/3)): + for i in range(int(mat.vertex_count / 3)): f = next(face_iter) f_key = face_key_func(f) @@ -1008,9 +1015,9 @@ def __clean_pmx_faces(pmx_faces, pmx_materials, face_key_func): mat.vertex_count = new_vertex_count face_iter = None if new_face_count == len(pmx_faces): - logging.info(' (faces is clean)') + logging.info(" (faces is clean)") else: - logging.warning(' - removed %d faces', len(pmx_faces)-new_face_count) + logging.warning(" - removed %d faces", len(pmx_faces) - new_face_count) del pmx_faces[new_face_count:] @staticmethod @@ -1023,4 +1030,3 @@ def __clean_pmx_morphs(pmx_morphs, index_update_func): counts = old_len - len(m.offsets) if counts: logging.warning(' - removed %d (of %d) offsets of "%s"', counts, old_len, m.name) - diff --git a/mmd_tools/core/rigid_body.py b/mmd_tools/core/rigid_body.py index 144c643b..c3f87c73 100644 --- a/mmd_tools/core/rigid_body.py +++ b/mmd_tools/core/rigid_body.py @@ -1,22 +1,29 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. + +from typing import List, Optional import bpy +from mathutils import Euler, Vector +from ..bpyutils import FnContext, Props -SHAPE_SPHERE = 0 -SHAPE_BOX = 1 +SHAPE_SPHERE = 0 +SHAPE_BOX = 1 SHAPE_CAPSULE = 2 -MODE_STATIC = 0 -MODE_DYNAMIC = 1 +MODE_STATIC = 0 +MODE_DYNAMIC = 1 MODE_DYNAMIC_BONE = 2 def shapeType(collision_shape): - return ('SPHERE', 'BOX', 'CAPSULE').index(collision_shape) + return ("SPHERE", "BOX", "CAPSULE").index(collision_shape) + def collisionShape(shape_type): - return ('SPHERE', 'BOX', 'CAPSULE')[shape_type] + return ("SPHERE", "BOX", "CAPSULE")[shape_type] def setRigidBodyWorldEnabled(enable): @@ -30,53 +37,250 @@ def setRigidBodyWorldEnabled(enable): class RigidBodyMaterial: COLORS = [ - 0x7fddd4, - 0xf0e68c, - 0xee82ee, - 0xffe4e1, - 0x8feeee, - 0xadff2f, - 0xfa8072, - 0x9370db, - - 0x40e0d0, - 0x96514d, - 0x5a964e, - 0xe6bfab, - 0xd3381c, - 0x165e83, + 0x7FDDD4, + 0xF0E68C, + 0xEE82EE, + 0xFFE4E1, + 0x8FEEEE, + 0xADFF2F, + 0xFA8072, + 0x9370DB, + 0x40E0D0, + 0x96514D, + 0x5A964E, + 0xE6BFAB, + 0xD3381C, + 0x165E83, 0x701682, 0x828216, - ] + ] + @classmethod def getMaterial(cls, number): number = int(number) - material_name = 'mmd_tools_rigid_%d'%(number) + material_name = "mmd_tools_rigid_%d" % (number) if material_name not in bpy.data.materials: mat = bpy.data.materials.new(material_name) color = cls.COLORS[number] - mat.diffuse_color[:3] = [((0xff0000 & color) >> 16) / float(255), ((0x00ff00 & color) >> 8) / float(255), (0x0000ff & color) / float(255)] + mat.diffuse_color[:3] = [((0xFF0000 & color) >> 16) / float(255), ((0x00FF00 & color) >> 8) / float(255), (0x0000FF & color) / float(255)] mat.specular_intensity = 0 - if bpy.app.version < (2, 80, 0): - mat.diffuse_intensity = 1 - mat.alpha = 0.5 - mat.use_transparency = True - mat.use_shadeless = True - else: - if len(mat.diffuse_color) > 3: - mat.diffuse_color[3] = 0.5 - mat.blend_method = 'BLEND' - mat.shadow_method = 'NONE' - mat.use_backface_culling = True - mat.show_transparent_back = False - mat.use_nodes = True - nodes, links = mat.node_tree.nodes, mat.node_tree.links - nodes.clear() - node_color = nodes.new('ShaderNodeBackground') - node_color.inputs['Color'].default_value = mat.diffuse_color - node_output = nodes.new('ShaderNodeOutputMaterial') - links.new(node_color.outputs[0], node_output.inputs['Surface']) + if len(mat.diffuse_color) > 3: + mat.diffuse_color[3] = 0.5 + mat.blend_method = "BLEND" + mat.shadow_method = "NONE" + mat.use_backface_culling = True + mat.show_transparent_back = False + mat.use_nodes = True + nodes, links = mat.node_tree.nodes, mat.node_tree.links + nodes.clear() + node_color = nodes.new("ShaderNodeBackground") + node_color.inputs["Color"].default_value = mat.diffuse_color + node_output = nodes.new("ShaderNodeOutputMaterial") + links.new(node_color.outputs[0], node_output.inputs["Surface"]) else: mat = bpy.data.materials[material_name] return mat + +class FnRigidBody: + @staticmethod + def new_rigid_body_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int) -> List[bpy.types.Object]: + if count < 1: + return [] + + obj = FnRigidBody.new_rigid_body_object(context, parent_object) + + if count == 1: + return [obj] + + return FnContext.duplicate_object(context, obj, count) + + @staticmethod + def new_rigid_body_object(context: bpy.types.Context, parent_object: bpy.types.Object) -> bpy.types.Object: + obj = FnContext.new_and_link_object(context, name="Rigidbody", object_data=bpy.data.meshes.new(name="Rigidbody")) + obj.parent = parent_object + obj.mmd_type = "RIGID_BODY" + obj.rotation_mode = "YXZ" + setattr(obj, Props.display_type, "SOLID") + obj.show_transparent = True + obj.hide_render = True + obj.display.show_shadows = False + + with context.temp_override(object=obj): + bpy.ops.rigidbody.object_add(type="ACTIVE") + + return obj + + @staticmethod + def setup_rigid_body_object( + obj: bpy.types.Object, + shape_type: str, + location: Vector, + rotation: Euler, + size: Vector, + dynamics_type: str, + collision_group_number: Optional[int] = None, + collision_group_mask: Optional[List[bool]] = None, + name: Optional[str] = None, + name_e: Optional[str] = None, + bone: Optional[str] = None, + friction: Optional[float] = None, + mass: Optional[float] = None, + angular_damping: Optional[float] = None, + linear_damping: Optional[float] = None, + bounce: Optional[float] = None, + ) -> bpy.types.Object: + obj.location = location + obj.rotation_euler = rotation + + obj.mmd_rigid.shape = collisionShape(shape_type) + obj.mmd_rigid.size = size + obj.mmd_rigid.type = str(dynamics_type) if dynamics_type in range(3) else "1" + + if collision_group_number is not None: + obj.mmd_rigid.collision_group_number = collision_group_number + + if collision_group_mask is not None: + obj.mmd_rigid.collision_group_mask = collision_group_mask + + if name is not None: + obj.name = name + obj.mmd_rigid.name_j = name + obj.data.name = name + + if name_e is not None: + obj.mmd_rigid.name_e = name_e + + if bone is not None: + obj.mmd_rigid.bone = bone + else: + obj.mmd_rigid.bone = "" + + rb = obj.rigid_body + if friction is not None: + rb.friction = friction + if mass is not None: + rb.mass = mass + if angular_damping is not None: + rb.angular_damping = angular_damping + if linear_damping is not None: + rb.linear_damping = linear_damping + if bounce is not None: + rb.restitution = bounce + + return obj + + @staticmethod + def get_rigid_body_size(obj: bpy.types.Object): + assert obj.mmd_type == "RIGID_BODY" + + x0, y0, z0 = obj.bound_box[0] + x1, y1, z1 = obj.bound_box[6] + assert x1 >= x0 and y1 >= y0 and z1 >= z0 + + shape = obj.mmd_rigid.shape + if shape == "SPHERE": + radius = (z1 - z0) / 2 + return (radius, 0.0, 0.0) + elif shape == "BOX": + x, y, z = (x1 - x0) / 2, (y1 - y0) / 2, (z1 - z0) / 2 + return (x, y, z) + elif shape == "CAPSULE": + diameter = x1 - x0 + radius = diameter / 2 + height = abs((z1 - z0) - diameter) + return (radius, height, 0.0) + else: + raise ValueError(f"Invalid shape type: {shape}") + + @staticmethod + def new_joint_object(context: bpy.types.Context, parent_object: bpy.types.Object, empty_display_size: float) -> bpy.types.Object: + obj = FnContext.new_and_link_object(context, name="Joint", object_data=None) + obj.parent = parent_object + obj.mmd_type = "JOINT" + obj.rotation_mode = "YXZ" + setattr(obj, Props.empty_display_type, "ARROWS") + setattr(obj, Props.empty_display_size, 0.1 * empty_display_size) + obj.hide_render = True + + with context.temp_override(): + context.view_layer.objects.active = obj + bpy.ops.rigidbody.constraint_add(type="GENERIC_SPRING") + + rigid_body_constraint = obj.rigid_body_constraint + rigid_body_constraint.disable_collisions = False + rigid_body_constraint.use_limit_ang_x = True + rigid_body_constraint.use_limit_ang_y = True + rigid_body_constraint.use_limit_ang_z = True + rigid_body_constraint.use_limit_lin_x = True + rigid_body_constraint.use_limit_lin_y = True + rigid_body_constraint.use_limit_lin_z = True + rigid_body_constraint.use_spring_x = True + rigid_body_constraint.use_spring_y = True + rigid_body_constraint.use_spring_z = True + rigid_body_constraint.use_spring_ang_x = True + rigid_body_constraint.use_spring_ang_y = True + rigid_body_constraint.use_spring_ang_z = True + + return obj + + @staticmethod + def new_joint_objects(context: bpy.types.Context, parent_object: bpy.types.Object, count: int, empty_display_size: float) -> List[bpy.types.Object]: + if count < 1: + return [] + + obj = FnRigidBody.new_joint_object(context, parent_object, empty_display_size) + + if count == 1: + return [obj] + + return FnContext.duplicate_object(context, obj, count) + + @staticmethod + def setup_joint_object( + obj: bpy.types.Object, + location: Vector, + rotation: Euler, + rigid_a: bpy.types.Object, + rigid_b: bpy.types.Object, + maximum_location: Vector, + minimum_location: Vector, + maximum_rotation: Euler, + minimum_rotation: Euler, + spring_angular: Vector, + spring_linear: Vector, + name: str, + name_e: Optional[str] = None, + ) -> bpy.types.Object: + obj.name = f"J.{name}" + + obj.location = location + obj.rotation_euler = rotation + + rigid_body_constraint = obj.rigid_body_constraint + rigid_body_constraint.object1 = rigid_a + rigid_body_constraint.object2 = rigid_b + rigid_body_constraint.limit_lin_x_upper = maximum_location.x + rigid_body_constraint.limit_lin_y_upper = maximum_location.y + rigid_body_constraint.limit_lin_z_upper = maximum_location.z + + rigid_body_constraint.limit_lin_x_lower = minimum_location.x + rigid_body_constraint.limit_lin_y_lower = minimum_location.y + rigid_body_constraint.limit_lin_z_lower = minimum_location.z + + rigid_body_constraint.limit_ang_x_upper = maximum_rotation.x + rigid_body_constraint.limit_ang_y_upper = maximum_rotation.y + rigid_body_constraint.limit_ang_z_upper = maximum_rotation.z + + rigid_body_constraint.limit_ang_x_lower = minimum_rotation.x + rigid_body_constraint.limit_ang_y_lower = minimum_rotation.y + rigid_body_constraint.limit_ang_z_lower = minimum_rotation.z + + obj.mmd_joint.name_j = name + if name_e is not None: + obj.mmd_joint.name_e = name_e + + obj.mmd_joint.spring_linear = spring_linear + obj.mmd_joint.spring_angular = spring_angular + + return obj diff --git a/mmd_tools/core/sdef.py b/mmd_tools/core/sdef.py index f941a073..3c398268 100644 --- a/mmd_tools/core/sdef.py +++ b/mmd_tools/core/sdef.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- +# Copyright 2018 MMD Tools authors +# This file is part of MMD Tools. + import logging import time import bpy -from mathutils import Matrix, Quaternion, Vector -from mmd_tools.bpyutils import matmul +from mathutils import Matrix, Vector + +from ..bpyutils import FnObject def _hash(v): @@ -13,26 +17,26 @@ def _hash(v): elif isinstance(v, bpy.types.Pose): return hash(type(v).__name__ + v.id_data.name) else: - raise NotImplementedError('hash') + raise NotImplementedError("hash") -class FnSDEF(): - g_verts = {} # global cache +class FnSDEF: + g_verts = {} # global cache g_shapekey_data = {} g_bone_check = {} __g_armature_check = {} - SHAPEKEY_NAME = 'mmd_sdef_skinning' - MASK_NAME = 'mmd_sdef_mask' + SHAPEKEY_NAME = "mmd_sdef_skinning" + MASK_NAME = "mmd_sdef_mask" def __init__(self): - raise NotImplementedError('not allowed') + raise NotImplementedError("not allowed") @classmethod def __init_cache(cls, obj, shapekey): key = _hash(obj) - obj = getattr(obj, 'original', obj) - mod = obj.modifiers.get('mmd_bone_order_override') - key_armature = _hash(mod.object.pose) if mod and mod.type == 'ARMATURE' and mod.object else None + obj = getattr(obj, "original", obj) + mod = obj.modifiers.get("mmd_bone_order_override") + key_armature = _hash(mod.object.pose) if mod and mod.type == "ARMATURE" and mod.object else None if key not in cls.g_verts or cls.__g_armature_check.get(key) != key_armature: cls.g_verts[key] = cls.__find_vertices(obj) cls.g_bone_check[key] = {} @@ -52,7 +56,7 @@ def __check_bone_update(cls, obj, bone0, bone1): @classmethod def mute_sdef_set(cls, obj, mute): - key_blocks = getattr(obj.data.shape_keys, 'key_blocks', ()) + key_blocks = getattr(obj.data.shape_keys, "key_blocks", ()) if cls.SHAPEKEY_NAME in key_blocks: shapekey = key_blocks[cls.SHAPEKEY_NAME] shapekey.mute = mute @@ -63,24 +67,24 @@ def mute_sdef_set(cls, obj, mute): @classmethod def __sdef_muted(cls, obj, shapekey): mute = shapekey.mute - if mute != cls.g_bone_check[_hash(obj)].get('sdef_mute'): - mod = obj.modifiers.get('mmd_bone_order_override') - if mod and mod.type == 'ARMATURE': - if not mute and cls.MASK_NAME not in obj.vertex_groups and obj.mode != 'EDIT': + if mute != cls.g_bone_check[_hash(obj)].get("sdef_mute"): + mod = obj.modifiers.get("mmd_bone_order_override") + if mod and mod.type == "ARMATURE": + if not mute and cls.MASK_NAME not in obj.vertex_groups and obj.mode != "EDIT": mask = tuple(i for v in cls.g_verts[_hash(obj)].values() for i in v[3]) - obj.vertex_groups.new(name=cls.MASK_NAME).add(mask, 1, 'REPLACE') - mod.vertex_group = '' if mute else cls.MASK_NAME + obj.vertex_groups.new(name=cls.MASK_NAME).add(mask, 1, "REPLACE") + mod.vertex_group = "" if mute else cls.MASK_NAME mod.invert_vertex_group = True shapekey.vertex_group = cls.MASK_NAME - cls.g_bone_check[_hash(obj)]['sdef_mute'] = mute + cls.g_bone_check[_hash(obj)]["sdef_mute"] = mute return mute @staticmethod def has_sdef_data(obj): - mod = obj.modifiers.get('mmd_bone_order_override') - if mod and mod.type == 'ARMATURE' and mod.object: - kb = getattr(obj.data.shape_keys, 'key_blocks', None) - return kb and 'mmd_sdef_c' in kb and 'mmd_sdef_r0' in kb and 'mmd_sdef_r1' in kb + mod = obj.modifiers.get("mmd_bone_order_override") + if mod and mod.type == "ARMATURE" and mod.object: + kb = getattr(obj.data.shape_keys, "key_blocks", None) + return kb and "mmd_sdef_c" in kb and "mmd_sdef_r0" in kb and "mmd_sdef_r1" in kb return False @classmethod @@ -89,16 +93,16 @@ def __find_vertices(cls, obj): return {} vertices = {} - pose_bones = obj.modifiers.get('mmd_bone_order_override').object.pose.bones - bone_map = {g.index:pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones} - sdef_c = obj.data.shape_keys.key_blocks['mmd_sdef_c'].data - sdef_r0 = obj.data.shape_keys.key_blocks['mmd_sdef_r0'].data - sdef_r1 = obj.data.shape_keys.key_blocks['mmd_sdef_r1'].data + pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones + bone_map = {g.index: pose_bones[g.name] for g in obj.vertex_groups if g.name in pose_bones} + sdef_c = obj.data.shape_keys.key_blocks["mmd_sdef_c"].data + sdef_r0 = obj.data.shape_keys.key_blocks["mmd_sdef_r0"].data + sdef_r1 = obj.data.shape_keys.key_blocks["mmd_sdef_r1"].data vd = obj.data.vertices for i in range(len(sdef_c)): if vd[i].co != sdef_c[i].co: - bgs = [g for g in vd[i].groups if g.group in bone_map and g.weight] # bone groups + bgs = [g for g in vd[i].groups if g.group in bone_map and g.weight] # bone groups if len(bgs) >= 2: bgs.sort(key=lambda x: x.group) # preprocessing @@ -114,9 +118,9 @@ def __find_vertices(cls, obj): key = (bgs[0].group, bgs[1].group) if key not in vertices: - #TODO basically we can not cache any bone reference + # TODO basically we can not cache any bone reference vertices[key] = (bone_map[bgs[0].group], bone_map[bgs[1].group], [], []) - vertices[key][2].append((i, w0, w1, vd[i].co-c, (c+r0)/2, (c+r1)/2)) + vertices[key][2].append((i, w0, w1, vd[i].co - c, (c + r0) / 2, (c + r1) / 2)) vertices[key][3].append(i) return vertices @@ -129,16 +133,16 @@ def driver_function_wrap(cls, obj_name, bulk_update, use_skip, use_scale): @classmethod def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale): obj = bpy.data.objects[obj_name] - if getattr(shapekey.id_data, 'is_evaluated', False): + if getattr(shapekey.id_data, "is_evaluated", False): # For Blender 2.8x, we should use evaluated object, and the only reference is the "obj" variable of SDEF driver - #cls.driver_function(shapekey.id_data.original.key_blocks[shapekey.name], obj_name, bulk_update, use_skip, use_scale) # update original data - data_path = shapekey.path_from_id('value') - obj = next(i for i in shapekey.id_data.animation_data.drivers if i.data_path == data_path).driver.variables['obj'].targets[0].id + # cls.driver_function(shapekey.id_data.original.key_blocks[shapekey.name], obj_name, bulk_update, use_skip, use_scale) # update original data + data_path = shapekey.path_from_id("value") + obj = next(i for i in shapekey.id_data.animation_data.drivers if i.data_path == data_path).driver.variables["obj"].targets[0].id cls.__init_cache(obj, shapekey) if cls.__sdef_muted(obj, shapekey): return 0.0 - pose_bones = obj.modifiers.get('mmd_bone_order_override').object.pose.bones + pose_bones = obj.modifiers.get("mmd_bone_order_override").object.pose.bones if not bulk_update: shapekey_data = shapekey.data if use_scale: @@ -146,91 +150,94 @@ def driver_function(cls, shapekey, obj_name, bulk_update, use_skip, use_scale): key_blocks = tuple(k for k in shapekey.id_data.key_blocks[1:] if not k.mute and k.value and k.name != cls.SHAPEKEY_NAME) for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] - #if use_skip and not cls.__check_bone_update(obj, bone0, bone1): + # if use_skip and not cls.__check_bone_update(obj, bone0, bone1): # continue - mat0 = matmul(bone0.matrix, bone0.bone.matrix_local.inverted()) - mat1 = matmul(bone1.matrix, bone1.bone.matrix_local.inverted()) - rot0 = mat0.to_euler('YXZ').to_quaternion() - rot1 = mat1.to_euler('YXZ').to_quaternion() + mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted() + mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted() + rot0 = mat0.to_euler("YXZ").to_quaternion() + rot1 = mat1.to_euler("YXZ").to_quaternion() if rot1.dot(rot0) < 0: rot1 = -rot1 s0, s1 = mat0.to_scale(), mat1.to_scale() for vid, w0, w1, pos_c, cr0, cr1 in sdef_data: - s = s0*w0 + s1*w1 - mat_rot = matmul((rot0*w0 + rot1*w1).normalized().to_matrix(), Matrix([(s[0],0,0), (0,s[1],0), (0,0,s[2])])) - #shapekey_data[vid].co = matmul(mat_rot, pos_c) + matmul(mat0, cr0)*w0 + matmul(mat1, cr1)*w1 - delta = sum(((key.data[vid].co - key.relative_key.data[vid].co)*key.value for key in key_blocks), Vector()) # assuming key.vertex_group = '' - shapekey_data[vid].co = matmul(mat_rot, pos_c+delta) - delta + matmul(mat0, cr0)*w0 + matmul(mat1, cr1)*w1 + s = s0 * w0 + s1 * w1 + mat_rot = (rot0 * w0 + rot1 * w1).normalized().to_matrix() @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])]) + delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = '' + shapekey_data[vid].co = (mat_rot @ (pos_c + delta)) - delta + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 else: # default for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] if use_skip and not cls.__check_bone_update(obj, bone0, bone1): continue - mat0 = matmul(bone0.matrix, bone0.bone.matrix_local.inverted()) - mat1 = matmul(bone1.matrix, bone1.bone.matrix_local.inverted()) + mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted() + mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted() # workaround some weird result of matrix.to_quaternion() using to_euler(), but still minor issues - rot0 = mat0.to_euler('YXZ').to_quaternion() - rot1 = mat1.to_euler('YXZ').to_quaternion() + rot0 = mat0.to_euler("YXZ").to_quaternion() + rot1 = mat1.to_euler("YXZ").to_quaternion() if rot1.dot(rot0) < 0: rot1 = -rot1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data: - mat_rot = (rot0*w0 + rot1*w1).normalized().to_matrix() - shapekey_data[vid].co = matmul(mat_rot, pos_c) + matmul(mat0, cr0)*w0 + matmul(mat1, cr1)*w1 - else: # bulk update + mat_rot = (rot0 * w0 + rot1 * w1).normalized().to_matrix() + shapekey_data[vid].co = (mat_rot @ pos_c) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 + else: # bulk update shapekey_data = cls.g_shapekey_data[_hash(obj)] if shapekey_data is None: import numpy as np - shapekey_data = np.zeros(len(shapekey.data)*3, dtype=np.float32) - shapekey.data.foreach_get('co', shapekey_data) + + shapekey_data = np.zeros(len(shapekey.data) * 3, dtype=np.float32) + shapekey.data.foreach_get("co", shapekey_data) shapekey_data = cls.g_shapekey_data[_hash(obj)] = shapekey_data.reshape(len(shapekey.data), 3) if use_scale: # scale & bulk update key_blocks = tuple(k for k in shapekey.id_data.key_blocks[1:] if not k.mute and k.value and k.name != cls.SHAPEKEY_NAME) for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] - #if use_skip and not cls.__check_bone_update(obj, bone0, bone1): + # if use_skip and not cls.__check_bone_update(obj, bone0, bone1): # continue - mat0 = matmul(bone0.matrix, bone0.bone.matrix_local.inverted()) - mat1 = matmul(bone1.matrix, bone1.bone.matrix_local.inverted()) - rot0 = mat0.to_euler('YXZ').to_quaternion() - rot1 = mat1.to_euler('YXZ').to_quaternion() + mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted() + mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted() + rot0 = mat0.to_euler("YXZ").to_quaternion() + rot1 = mat1.to_euler("YXZ").to_quaternion() if rot1.dot(rot0) < 0: rot1 = -rot1 s0, s1 = mat0.to_scale(), mat1.to_scale() + def scale(mat_rot, w0, w1): - s = s0*w0 + s1*w1 - return matmul(mat_rot, Matrix([(s[0],0,0), (0,s[1],0), (0,0,s[2])])) - #shapekey_data[vids] = [matmul(scale((rot0*w0 + rot1*w1).normalized().to_matrix(), w0, w1), pos_c) + matmul(mat0, cr0)*w0 + matmul(mat1, cr1)*w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data] + s = s0 * w0 + s1 * w1 + return mat_rot @ Matrix([(s[0], 0, 0), (0, s[1], 0), (0, 0, s[2])]) + def offset(mat_rot, pos_c, vid): - delta = sum(((key.data[vid].co - key.relative_key.data[vid].co)*key.value for key in key_blocks), Vector()) # assuming key.vertex_group = '' - return matmul(mat_rot, pos_c+delta) - delta - shapekey_data[vids] = [offset(scale((rot0*w0 + rot1*w1).normalized().to_matrix(), w0, w1), pos_c, vid) + matmul(mat0, cr0)*w0 + matmul(mat1, cr1)*w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data] + delta = sum(((key.data[vid].co - key.relative_key.data[vid].co) * key.value for key in key_blocks), Vector()) # assuming key.vertex_group = '' + return (mat_rot @ (pos_c + delta)) - delta + + shapekey_data[vids] = [offset(scale((rot0 * w0 + rot1 * w1).normalized().to_matrix(), w0, w1), pos_c, vid) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data] else: # bulk update for bone0, bone1, sdef_data, vids in cls.g_verts[_hash(obj)].values(): bone0, bone1 = pose_bones[bone0.name], pose_bones[bone1.name] if use_skip and not cls.__check_bone_update(obj, bone0, bone1): continue - mat0 = matmul(bone0.matrix, bone0.bone.matrix_local.inverted()) - mat1 = matmul(bone1.matrix, bone1.bone.matrix_local.inverted()) - rot0 = mat0.to_euler('YXZ').to_quaternion() - rot1 = mat1.to_euler('YXZ').to_quaternion() + mat0 = bone0.matrix @ bone0.bone.matrix_local.inverted() + mat1 = bone1.matrix @ bone1.bone.matrix_local.inverted() + rot0 = mat0.to_euler("YXZ").to_quaternion() + rot1 = mat1.to_euler("YXZ").to_quaternion() if rot1.dot(rot0) < 0: rot1 = -rot1 - shapekey_data[vids] = [matmul((rot0*w0 + rot1*w1).normalized().to_matrix(), pos_c) + matmul(mat0, cr0)*w0 + matmul(mat1, cr1)*w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data] - shapekey.data.foreach_set('co', shapekey_data.reshape(3 * len(shapekey.data))) + shapekey_data[vids] = [((rot0 * w0 + rot1 * w1).normalized().to_matrix() @ pos_c) + (mat0 @ cr0) * w0 + (mat1 @ cr1) * w1 for vid, w0, w1, pos_c, cr0, cr1 in sdef_data] + shapekey.data.foreach_set("co", shapekey_data.reshape(3 * len(shapekey.data))) - return 1.0 # shapkey value + return 1.0 # shapkey value @classmethod def register_driver_function(cls): - if 'mmd_sdef_driver' not in bpy.app.driver_namespace: - bpy.app.driver_namespace['mmd_sdef_driver'] = cls.driver_function - if 'mmd_sdef_driver_wrap' not in bpy.app.driver_namespace: - bpy.app.driver_namespace['mmd_sdef_driver_wrap'] = cls.driver_function_wrap + if "mmd_sdef_driver" not in bpy.app.driver_namespace: + bpy.app.driver_namespace["mmd_sdef_driver"] = cls.driver_function + if "mmd_sdef_driver_wrap" not in bpy.app.driver_namespace: + bpy.app.driver_namespace["mmd_sdef_driver_wrap"] = cls.driver_function_wrap + + BENCH_LOOP = 10 - BENCH_LOOP=10 @classmethod def __get_benchmark_result(cls, obj, shapkey, use_scale, use_skip): # warmed up @@ -246,7 +253,7 @@ def __get_benchmark_result(cls, obj, shapkey, use_scale, use_skip): cls.driver_function(shapkey, obj.name, bulk_update=True, use_skip=False, use_scale=use_scale) bulk_time = time.time() - t result = default_time > bulk_time - logging.info('FnSDEF:benchmark: default %.4f vs bulk_update %.4f => bulk_update=%s', default_time, bulk_time, result) + logging.info("FnSDEF:benchmark: default %.4f vs bulk_update %.4f => bulk_update=%s", default_time, bulk_time, result) return result @classmethod @@ -263,43 +270,37 @@ def bind(cls, obj, bulk_update=None, use_skip=True, use_scale=False): if bulk_update is None: bulk_update = cls.__get_benchmark_result(obj, shapekey, use_scale, use_skip) # Add the driver to the shapekey - f = obj.data.shape_keys.driver_add('key_blocks["'+cls.SHAPEKEY_NAME+'"].value', -1) - if hasattr(f.driver, 'show_debug_info'): + f = obj.data.shape_keys.driver_add('key_blocks["' + cls.SHAPEKEY_NAME + '"].value', -1) + if hasattr(f.driver, "show_debug_info"): f.driver.show_debug_info = False - f.driver.type = 'SCRIPTED' + f.driver.type = "SCRIPTED" ov = f.driver.variables.new() - ov.name = 'obj' - ov.type = 'SINGLE_PROP' + ov.name = "obj" + ov.type = "SINGLE_PROP" ov.targets[0].id = obj - ov.targets[0].data_path = 'name' - if bpy.app.version >= (2, 80, 0): - if not bulk_update and use_skip: #FIXME: force disable use_skip=True for bulk_update=False on 2.8 - use_skip = False - mod = obj.modifiers.get('mmd_bone_order_override') - variables = f.driver.variables - for name in set(data[i].name for data in cls.g_verts[_hash(obj)].values() for i in range(2)): # add required bones for dependency graph - var = variables.new() - var.type = 'TRANSFORMS' - var.targets[0].id = mod.object - var.targets[0].bone_target = name - if hasattr(f.driver, 'use_self'): # Blender 2.78+ - f.driver.use_self = True - param = (bulk_update, use_skip, use_scale) - f.driver.expression = 'mmd_sdef_driver(self, obj, bulk_update={}, use_skip={}, use_scale={})'.format(*param) - else: - param = (obj.name, bulk_update, use_skip, use_scale) - f.driver.expression = 'mmd_sdef_driver_wrap("{}", bulk_update={}, use_skip={}, use_scale={})'.format(*param) + ov.targets[0].data_path = "name" + if not bulk_update and use_skip: # FIXME: force disable use_skip=True for bulk_update=False on 2.8 + use_skip = False + mod = obj.modifiers.get("mmd_bone_order_override") + variables = f.driver.variables + for name in set(data[i].name for data in cls.g_verts[_hash(obj)].values() for i in range(2)): # add required bones for dependency graph + var = variables.new() + var.type = "TRANSFORMS" + var.targets[0].id = mod.object + var.targets[0].bone_target = name + f.driver.use_self = True + param = (bulk_update, use_skip, use_scale) + f.driver.expression = "mmd_sdef_driver(self, obj, bulk_update={}, use_skip={}, use_scale={})".format(*param) return True @classmethod def unbind(cls, obj): - from mmd_tools.bpyutils import ObjectOp if obj.data.shape_keys: if cls.SHAPEKEY_NAME in obj.data.shape_keys.key_blocks: - ObjectOp(obj).shape_key_remove(obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME]) + FnObject.mesh_remove_shape_key(obj, obj.data.shape_keys.key_blocks[cls.SHAPEKEY_NAME]) for mod in obj.modifiers: - if mod.type == 'ARMATURE' and mod.vertex_group == cls.MASK_NAME: - mod.vertex_group = '' + if mod.type == "ARMATURE" and mod.vertex_group == cls.MASK_NAME: + mod.vertex_group = "" mod.invert_vertex_group = False break if cls.MASK_NAME in obj.vertex_groups: @@ -309,12 +310,12 @@ def unbind(cls, obj): @classmethod def clear_cache(cls, obj=None, unused_only=False): if unused_only: - valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == 'MESH' and i != obj) - for key in (cls.g_verts.keys()-valid_keys): + valid_keys = set(_hash(i) for i in bpy.data.objects if i.type == "MESH" and i != obj) + for key in cls.g_verts.keys() - valid_keys: del cls.g_verts[key] - for key in (cls.g_shapekey_data.keys()-cls.g_verts.keys()): + for key in cls.g_shapekey_data.keys() - cls.g_verts.keys(): del cls.g_shapekey_data[key] - for key in (cls.g_bone_check.keys()-cls.g_verts.keys()): + for key in cls.g_bone_check.keys() - cls.g_verts.keys(): del cls.g_bone_check[key] elif obj: key = _hash(obj) diff --git a/mmd_tools/core/shader.py b/mmd_tools/core/shader.py index b41c30b8..4455ed63 100644 --- a/mmd_tools/core/shader.py +++ b/mmd_tools/core/shader.py @@ -1,23 +1,27 @@ # -*- coding: utf-8 -*- +# Copyright 2019 MMD Tools authors +# This file is part of MMD Tools. +from typing import Optional, Tuple, cast import bpy class _NodeTreeUtils: + def __init__(self, shader: bpy.types.ShaderNodeTree): + self.shader = shader + self.nodes: bpy.types.bpy_prop_collection[bpy.types.ShaderNode] = shader.nodes # type: ignore + self.links = shader.links - def __init__(self, shader): - self.shader, self.nodes, self.links = shader, shader.nodes, shader.links - - def _find_node(self, node_type): + def _find_node(self, node_type: str) -> Optional[bpy.types.ShaderNode]: return next((n for n in self.nodes if n.bl_idname == node_type), None) - def new_node(self, idname, pos): - node = self.nodes.new(idname) - node.location = (pos[0]*210, pos[1]*220) + def new_node(self, idname: str, pos: Tuple[int, int]) -> bpy.types.ShaderNode: + node: bpy.types.ShaderNode = self.nodes.new(idname) + node.location = (pos[0] * 210, pos[1] * 220) return node def new_math_node(self, operation, pos, value1=None, value2=None): - node = self.new_node('ShaderNodeMath', pos) + node = self.new_node("ShaderNodeMath", pos) node.operation = operation if value1 is not None: node.inputs[0].default_value = value1 @@ -26,7 +30,7 @@ def new_math_node(self, operation, pos, value1=None, value2=None): return node def new_vector_math_node(self, operation, pos, vector1=None, vector2=None): - node = self.new_node('ShaderNodeVectorMath', pos) + node = self.new_node("ShaderNodeVectorMath", pos) node.operation = operation if vector1 is not None: node.inputs[0].default_value = vector1 @@ -35,75 +39,82 @@ def new_vector_math_node(self, operation, pos, vector1=None, vector2=None): return node def new_mix_node(self, blend_type, pos, fac=None, color1=None, color2=None): - node = self.new_node('ShaderNodeMixRGB', pos) + node = self.new_node("ShaderNodeMixRGB", pos) node.blend_type = blend_type if fac is not None: - node.inputs['Fac'].default_value = fac + node.inputs["Fac"].default_value = fac if color1 is not None: - node.inputs['Color1'].default_value = color1 + node.inputs["Color1"].default_value = color1 if color2 is not None: - node.inputs['Color2'].default_value = color2 + node.inputs["Color2"].default_value = color2 return node -class _NodeGroupUtils(_NodeTreeUtils): +SOCKET_TYPE_MAPPING = {"NodeSocketFloatFactor": "NodeSocketFloat"} + +SOCKET_SUBTYPE_MAPPING = {"NodeSocketFloatFactor": "FACTOR"} - def __init__(self, shader): + +class _NodeGroupUtils(_NodeTreeUtils): + def __init__(self, shader: bpy.types.ShaderNodeTree): super().__init__(shader) - self.__node_input = self.__node_output = None + self.__node_input: Optional[bpy.types.NodeGroupInput] = None + self.__node_output: Optional[bpy.types.NodeGroupOutput] = None @property - def node_input(self): + def node_input(self) -> bpy.types.NodeGroupInput: if not self.__node_input: - self.__node_input = self._find_node('NodeGroupInput') or self.new_node('NodeGroupInput', (-2, 0)) + self.__node_input = cast(bpy.types.NodeGroupInput, self._find_node("NodeGroupInput") or self.new_node("NodeGroupInput", (-2, 0))) return self.__node_input @property - def node_output(self): + def node_output(self) -> bpy.types.NodeGroupOutput: if not self.__node_output: - self.__node_output = self._find_node('NodeGroupOutput') or self.new_node('NodeGroupOutput', (2, 0)) + self.__node_output = cast(bpy.types.NodeGroupOutput, self._find_node("NodeGroupOutput") or self.new_node("NodeGroupOutput", (2, 0))) return self.__node_output def hide_nodes(self, hide_sockets=True): skip_nodes = {self.__node_input, self.__node_output} for n in (x for x in self.nodes if x not in skip_nodes): n.hide = True - if hide_sockets: - for s in n.inputs: - s.hide = not s.is_linked - for s in n.outputs: - s.hide = not s.is_linked + if not hide_sockets: + continue + for s in n.inputs: + s.hide = not s.is_linked + for s in n.outputs: + s.hide = not s.is_linked def new_input_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None): - self.__new_io(self.shader.inputs, self.node_input.outputs, io_name, socket, default_val, min_max, socket_type) + self.__new_io("INPUT", self.node_input.outputs, io_name, socket, default_val, min_max, socket_type) def new_output_socket(self, io_name, socket, default_val=None, min_max=None, socket_type=None): - self.__new_io(self.shader.outputs, self.node_output.inputs, io_name, socket, default_val, min_max, socket_type) + self.__new_io("OUTPUT", self.node_output.inputs, io_name, socket, default_val, min_max, socket_type) - def __new_io(self, shader_io, io_sockets, io_name, socket, default_val=None, min_max=None, socket_type=None): + def __new_io(self, in_out, io_sockets, io_name, socket, default_val=None, min_max=None, socket_type=None): if io_name not in io_sockets: idname = socket_type or socket.bl_idname - shader_io.new(type=idname, name=io_name) + interface_socket = self.shader.interface.new_socket(name=io_name, in_out=in_out, socket_type=SOCKET_TYPE_MAPPING.get(idname, idname)) + if idname in SOCKET_SUBTYPE_MAPPING: + interface_socket.subtype = SOCKET_SUBTYPE_MAPPING.get(idname, "") if not min_max: - if idname.endswith('Factor') or io_name.endswith('Alpha'): - shader_io[io_name].min_value, shader_io[io_name].max_value = 0, 1 - elif idname.endswith('Float') or idname.endswith('Vector'): - shader_io[io_name].min_value, shader_io[io_name].max_value = -10, 10 + if idname.endswith("Factor") or io_name.endswith("Alpha"): + interface_socket.min_value, interface_socket.max_value = 0, 1 + elif idname.endswith("Float") or idname.endswith("Vector"): + interface_socket.min_value, interface_socket.max_value = -10, 10 if socket is not None: self.links.new(io_sockets[io_name], socket) if default_val is not None: - shader_io[io_name].default_value = default_val + interface_socket.default_value = default_val if min_max is not None: - shader_io[io_name].min_value, shader_io[io_name].max_value = min_max + interface_socket.min_value, interface_socket.max_value = min_max class _MaterialMorph: - @classmethod def update_morph_inputs(cls, material, morph): if material and material.node_tree and morph.name in material.node_tree.nodes: cls.__update_node_inputs(material.node_tree.nodes[morph.name], morph) - cls.update_morph_inputs(bpy.data.materials.get('mmd_edge.'+material.name, None), morph) + cls.update_morph_inputs(bpy.data.materials.get("mmd_edge." + material.name, None), morph) @classmethod def setup_morph_nodes(cls, material, morphs): @@ -117,7 +128,7 @@ def setup_morph_nodes(cls, material, morphs): n.location += node.location if n.node_tree.name != node.node_tree.name: n.location.x -= 100 - if node.name.startswith('mmd_'): + if node.name.startswith("mmd_"): n.location.y += 1500 node = n return nodes @@ -130,197 +141,203 @@ def reset_morph_links(cls, node): def __update_morph_links(cls, node, reset=False): nodes, links = node.id_data.nodes, node.id_data.links if reset: - if any(l.from_node.name.startswith('mmd_bind') for i in node.inputs for l in i.links): + if any(l.from_node.name.startswith("mmd_bind") for i in node.inputs for l in i.links): return + def __init_link(socket_morph, socket_shader): if socket_shader and socket_morph.is_linked: links.new(socket_morph.links[0].from_socket, socket_shader) + else: + def __init_link(socket_morph, socket_shader): if socket_shader: if socket_shader.is_linked: links.new(socket_shader.links[0].from_socket, socket_morph) - if socket_morph.type == 'VALUE': + if socket_morph.type == "VALUE": socket_morph.default_value = socket_shader.default_value else: socket_morph.default_value[:3] = socket_shader.default_value[:3] - shader = nodes.get('mmd_shader', None) + + shader = nodes.get("mmd_shader", None) if shader: - __init_link(node.inputs['Ambient1'], shader.inputs.get('Ambient Color')) - __init_link(node.inputs['Diffuse1'], shader.inputs.get('Diffuse Color')) - __init_link(node.inputs['Specular1'], shader.inputs.get('Specular Color')) - __init_link(node.inputs['Reflect1'], shader.inputs.get('Reflect')) - __init_link(node.inputs['Alpha1'], shader.inputs.get('Alpha')) - __init_link(node.inputs['Base1 RGB'], shader.inputs.get('Base Tex')) - __init_link(node.inputs['Toon1 RGB'], shader.inputs.get('Toon Tex')) #FIXME toon only affect shadow color - __init_link(node.inputs['Sphere1 RGB'], shader.inputs.get('Sphere Tex')) - elif 'mmd_edge_preview' in nodes: - shader = nodes['mmd_edge_preview'] - __init_link(node.inputs['Edge1 RGB'], shader.inputs['Color']) - __init_link(node.inputs['Edge1 A'], shader.inputs['Alpha']) + __init_link(node.inputs["Ambient1"], shader.inputs.get("Ambient Color")) + __init_link(node.inputs["Diffuse1"], shader.inputs.get("Diffuse Color")) + __init_link(node.inputs["Specular1"], shader.inputs.get("Specular Color")) + __init_link(node.inputs["Reflect1"], shader.inputs.get("Reflect")) + __init_link(node.inputs["Alpha1"], shader.inputs.get("Alpha")) + __init_link(node.inputs["Base1 RGB"], shader.inputs.get("Base Tex")) + __init_link(node.inputs["Toon1 RGB"], shader.inputs.get("Toon Tex")) # FIXME toon only affect shadow color + __init_link(node.inputs["Sphere1 RGB"], shader.inputs.get("Sphere Tex")) + elif "mmd_edge_preview" in nodes: + shader = nodes["mmd_edge_preview"] + __init_link(node.inputs["Edge1 RGB"], shader.inputs["Color"]) + __init_link(node.inputs["Edge1 A"], shader.inputs["Alpha"]) @classmethod def __update_node_inputs(cls, node, morph): - node.inputs['Ambient2'].default_value[:3] = morph.ambient_color[:3] - node.inputs['Diffuse2'].default_value[:3] = morph.diffuse_color[:3] - node.inputs['Specular2'].default_value[:3] = morph.specular_color[:3] - node.inputs['Reflect2'].default_value = morph.shininess - node.inputs['Alpha2'].default_value = morph.diffuse_color[3] - - node.inputs['Edge2 RGB'].default_value[:3] = morph.edge_color[:3] - node.inputs['Edge2 A'].default_value = morph.edge_color[3] - - node.inputs['Base2 RGB'].default_value[:3] = morph.texture_factor[:3] - node.inputs['Base2 A'].default_value = morph.texture_factor[3] - node.inputs['Toon2 RGB'].default_value[:3] = morph.toon_texture_factor[:3] - node.inputs['Toon2 A'].default_value = morph.toon_texture_factor[3] - node.inputs['Sphere2 RGB'].default_value[:3] = morph.sphere_texture_factor[:3] - node.inputs['Sphere2 A'].default_value = morph.sphere_texture_factor[3] + node.inputs["Ambient2"].default_value[:3] = morph.ambient_color[:3] + node.inputs["Diffuse2"].default_value[:3] = morph.diffuse_color[:3] + node.inputs["Specular2"].default_value[:3] = morph.specular_color[:3] + node.inputs["Reflect2"].default_value = morph.shininess + node.inputs["Alpha2"].default_value = morph.diffuse_color[3] + + node.inputs["Edge2 RGB"].default_value[:3] = morph.edge_color[:3] + node.inputs["Edge2 A"].default_value = morph.edge_color[3] + + node.inputs["Base2 RGB"].default_value[:3] = morph.texture_factor[:3] + node.inputs["Base2 A"].default_value = morph.texture_factor[3] + node.inputs["Toon2 RGB"].default_value[:3] = morph.toon_texture_factor[:3] + node.inputs["Toon2 A"].default_value = morph.toon_texture_factor[3] + node.inputs["Sphere2 RGB"].default_value[:3] = morph.sphere_texture_factor[:3] + node.inputs["Sphere2 A"].default_value = morph.sphere_texture_factor[3] @classmethod def __morph_node_add(cls, material, morph, prev_node): nodes, links = material.node_tree.nodes, material.node_tree.links - shader = nodes.get('mmd_shader', None) + shader = nodes.get("mmd_shader", None) if morph: - node = nodes.new('ShaderNodeGroup') - node.parent = getattr(shader, 'parent', None) + node = nodes.new("ShaderNodeGroup") + node.parent = getattr(shader, "parent", None) node.location = (-250, 0) - node.node_tree = cls.__get_shader('Add' if morph.offset_type == 'ADD' else 'Mul') + node.node_tree = cls.__get_shader("Add" if morph.offset_type == "ADD" else "Mul") cls.__update_node_inputs(node, morph) if prev_node: - for id_name in ('Ambient', 'Diffuse', 'Specular' , 'Reflect','Alpha'): - links.new(prev_node.outputs[id_name], node.inputs[id_name+'1']) - for id_name in ('Edge', 'Base', 'Toon' , 'Sphere'): - links.new(prev_node.outputs[id_name+' RGB'], node.inputs[id_name+'1 RGB']) - links.new(prev_node.outputs[id_name+' A'], node.inputs[id_name+'1 A']) - else: # initial first node - if node.node_tree.name.endswith('Add'): - node.inputs['Base1 A'].default_value = 1 - node.inputs['Toon1 A'].default_value = 1 - node.inputs['Sphere1 A'].default_value = 1 + for id_name in ("Ambient", "Diffuse", "Specular", "Reflect", "Alpha"): + links.new(prev_node.outputs[id_name], node.inputs[id_name + "1"]) + for id_name in ("Edge", "Base", "Toon", "Sphere"): + links.new(prev_node.outputs[id_name + " RGB"], node.inputs[id_name + "1 RGB"]) + links.new(prev_node.outputs[id_name + " A"], node.inputs[id_name + "1 A"]) + else: # initial first node + if node.node_tree.name.endswith("Add"): + node.inputs["Base1 A"].default_value = 1 + node.inputs["Toon1 A"].default_value = 1 + node.inputs["Sphere1 A"].default_value = 1 cls.__update_morph_links(node) return node # connect last node to shader if shader: + def __soft_link(socket_out, socket_in): if socket_out and socket_in: links.new(socket_out, socket_in) - __soft_link(prev_node.outputs['Ambient'], shader.inputs.get('Ambient Color')) - __soft_link(prev_node.outputs['Diffuse'], shader.inputs.get('Diffuse Color')) - __soft_link(prev_node.outputs['Specular'], shader.inputs.get('Specular Color')) - __soft_link(prev_node.outputs['Reflect'], shader.inputs.get('Reflect')) - __soft_link(prev_node.outputs['Alpha'], shader.inputs.get('Alpha')) - __soft_link(prev_node.outputs['Base Tex'], shader.inputs.get('Base Tex')) - __soft_link(prev_node.outputs['Toon Tex'], shader.inputs.get('Toon Tex')) - if int(material.mmd_material.sphere_texture_type) != 2: # shader.inputs['Sphere Mul/Add'].default_value < 0.5 - __soft_link(prev_node.outputs['Sphere Tex'], shader.inputs.get('Sphere Tex')) + + __soft_link(prev_node.outputs["Ambient"], shader.inputs.get("Ambient Color")) + __soft_link(prev_node.outputs["Diffuse"], shader.inputs.get("Diffuse Color")) + __soft_link(prev_node.outputs["Specular"], shader.inputs.get("Specular Color")) + __soft_link(prev_node.outputs["Reflect"], shader.inputs.get("Reflect")) + __soft_link(prev_node.outputs["Alpha"], shader.inputs.get("Alpha")) + __soft_link(prev_node.outputs["Base Tex"], shader.inputs.get("Base Tex")) + __soft_link(prev_node.outputs["Toon Tex"], shader.inputs.get("Toon Tex")) + if int(material.mmd_material.sphere_texture_type) != 2: # shader.inputs['Sphere Mul/Add'].default_value < 0.5 + __soft_link(prev_node.outputs["Sphere Tex"], shader.inputs.get("Sphere Tex")) else: - __soft_link(prev_node.outputs['Sphere Tex Add'], shader.inputs.get('Sphere Tex')) - elif 'mmd_edge_preview' in nodes: - shader = nodes['mmd_edge_preview'] - links.new(prev_node.outputs['Edge RGB'], shader.inputs['Color']) - links.new(prev_node.outputs['Edge A'], shader.inputs['Alpha']) + __soft_link(prev_node.outputs["Sphere Tex Add"], shader.inputs.get("Sphere Tex")) + elif "mmd_edge_preview" in nodes: + shader = nodes["mmd_edge_preview"] + links.new(prev_node.outputs["Edge RGB"], shader.inputs["Color"]) + links.new(prev_node.outputs["Edge A"], shader.inputs["Alpha"]) return shader @classmethod def __get_shader(cls, morph_type): - group_name = 'MMDMorph' + morph_type - shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type='ShaderNodeTree') + group_name = "MMDMorph" + morph_type + shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type="ShaderNodeTree") if len(shader.nodes): return shader ng = _NodeGroupUtils(shader) links = ng.links - use_mul = (morph_type == 'Mul') + use_mul = morph_type == "Mul" ############################################################################ - node_input = ng.new_node('NodeGroupInput', (-3, 0)) - ng.new_input_socket('Fac', None, 0, socket_type='NodeSocketFloat') - ng.new_node('NodeGroupOutput', (3, 0)) + node_input = ng.new_node("NodeGroupInput", (-3, 0)) + ng.new_input_socket("Fac", None, 0, socket_type="NodeSocketFloat") + ng.new_node("NodeGroupOutput", (3, 0)) - def __blend_color_add(id_name, pos, tag=''): + def __blend_color_add(id_name, pos, tag=""): # MA_RAMP_MULT: ColorMul = Color1 * (Fac * Color2 + (1 - Fac)) # MA_RAMP_ADD: ColorAdd = Color1 + Fac * Color2 # https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/blenkernel/intern/material.c#L1400 - node_mix = ng.new_mix_node('MULTIPLY' if use_mul else 'ADD', (pos[0]+1, pos[1])) - links.new(node_input.outputs['Fac'], node_mix.inputs['Fac']) - ng.new_input_socket('%s1'%id_name+tag, node_mix.inputs['Color1']) - ng.new_input_socket('%s2'%id_name+tag, node_mix.inputs['Color2'], socket_type='NodeSocketVector') - ng.new_output_socket(id_name+tag, node_mix.outputs['Color']) + node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos[0] + 1, pos[1])) + links.new(node_input.outputs["Fac"], node_mix.inputs["Fac"]) + ng.new_input_socket("%s1" % id_name + tag, node_mix.inputs["Color1"]) + ng.new_input_socket("%s2" % id_name + tag, node_mix.inputs["Color2"], socket_type="NodeSocketVector") + ng.new_output_socket(id_name + tag, node_mix.outputs["Color"]) return node_mix def __blend_tex_color(id_name, pos, node_tex_rgb, node_tex_a_output): # Tex Color = tex_rgb * tex_a + (1 - tex_a) # : tex_rgb = TexRGB * ColorMul + ColorAdd # : tex_a = TexA * ValueMul + ValueAdd - if id_name != 'Sphere': - node_mix = ng.new_mix_node('MULTIPLY', pos, color1=(1,1,1,1)) + if id_name != "Sphere": + node_mix = ng.new_mix_node("MULTIPLY", pos, color1=(1, 1, 1, 1)) links.new(node_tex_a_output, node_mix.inputs[0]) - links.new(node_tex_rgb.outputs['Color'], node_mix.inputs[2]) - ng.new_output_socket(id_name+' Tex', node_mix.outputs[0]) + links.new(node_tex_rgb.outputs["Color"], node_mix.inputs[2]) + ng.new_output_socket(id_name + " Tex", node_mix.outputs[0]) else: - node_inv = ng.new_math_node('SUBTRACT', (pos[0], pos[1]-0.25), value1=1.0) - node_scale = ng.new_vector_math_node('SCALE', (pos[0], pos[1])) - node_add = ng.new_vector_math_node('ADD', (pos[0]+1, pos[1])) + node_inv = ng.new_math_node("SUBTRACT", (pos[0], pos[1] - 0.25), value1=1.0) + node_scale = ng.new_vector_math_node("SCALE", (pos[0], pos[1])) + node_add = ng.new_vector_math_node("ADD", (pos[0] + 1, pos[1])) links.new(node_tex_a_output, node_inv.inputs[1]) - links.new(node_tex_rgb.outputs['Color'], node_scale.inputs[0]) - links.new(node_tex_a_output, node_scale.inputs['Scale']) + links.new(node_tex_rgb.outputs["Color"], node_scale.inputs[0]) + links.new(node_tex_a_output, node_scale.inputs["Scale"]) links.new(node_scale.outputs[0], node_add.inputs[0]) links.new(node_inv.outputs[0], node_add.inputs[1]) - ng.new_output_socket(id_name+' Tex', node_add.outputs[0], socket_type='NodeSocketColor') - ng.new_output_socket(id_name+' Tex Add', node_scale.outputs[0], socket_type='NodeSocketColor') + ng.new_output_socket(id_name + " Tex", node_add.outputs[0], socket_type="NodeSocketColor") + ng.new_output_socket(id_name + " Tex Add", node_scale.outputs[0], socket_type="NodeSocketColor") - def __add_sockets(id_name, input1, input2, output, tag=''): - ng.new_input_socket(f'{id_name}1{tag}', input1, use_mul) - ng.new_input_socket(f'{id_name}2{tag}', input2, use_mul) - ng.new_output_socket(f'{id_name}{tag}', output) + def __add_sockets(id_name, input1, input2, output, tag=""): + ng.new_input_socket(f"{id_name}1{tag}", input1, use_mul) + ng.new_input_socket(f"{id_name}2{tag}", input2, use_mul) + ng.new_output_socket(f"{id_name}{tag}", output) pos_x = -2 - __blend_color_add('Ambient', (pos_x, +0.5)) - __blend_color_add('Diffuse', (pos_x, +0.0)) - __blend_color_add('Specular', (pos_x, -0.5)) + __blend_color_add("Ambient", (pos_x, +0.5)) + __blend_color_add("Diffuse", (pos_x, +0.0)) + __blend_color_add("Specular", (pos_x, -0.5)) - combine_reflect1_alpha1_edge1 = ng.new_node('ShaderNodeCombineRGB', (-2, -1.5)) - combine_reflect2_alpha2_edge2 = ng.new_node('ShaderNodeCombineRGB', (-2, -1.75)) - separate_reflect_alpha_edge = ng.new_node('ShaderNodeSeparateRGB', (pos_x+2, -1.5)) + combine_reflect1_alpha1_edge1 = ng.new_node("ShaderNodeCombineRGB", (-2, -1.5)) + combine_reflect2_alpha2_edge2 = ng.new_node("ShaderNodeCombineRGB", (-2, -1.75)) + separate_reflect_alpha_edge = ng.new_node("ShaderNodeSeparateRGB", (pos_x + 2, -1.5)) - __add_sockets('Reflect', combine_reflect1_alpha1_edge1.inputs[0], combine_reflect2_alpha2_edge2.inputs[0], separate_reflect_alpha_edge.outputs[0]) - __add_sockets('Alpha', combine_reflect1_alpha1_edge1.inputs[1], combine_reflect2_alpha2_edge2.inputs[1], separate_reflect_alpha_edge.outputs[1]) + __add_sockets("Reflect", combine_reflect1_alpha1_edge1.inputs[0], combine_reflect2_alpha2_edge2.inputs[0], separate_reflect_alpha_edge.outputs[0]) + __add_sockets("Alpha", combine_reflect1_alpha1_edge1.inputs[1], combine_reflect2_alpha2_edge2.inputs[1], separate_reflect_alpha_edge.outputs[1]) - __blend_color_add('Edge', (pos_x, -1.0), ' RGB') - __add_sockets('Edge', combine_reflect1_alpha1_edge1.inputs[2], combine_reflect2_alpha2_edge2.inputs[2], separate_reflect_alpha_edge.outputs[2], tag=' A') + __blend_color_add("Edge", (pos_x, -1.0), " RGB") + __add_sockets("Edge", combine_reflect1_alpha1_edge1.inputs[2], combine_reflect2_alpha2_edge2.inputs[2], separate_reflect_alpha_edge.outputs[2], tag=" A") - node_mix = ng.new_mix_node('MULTIPLY' if use_mul else 'ADD', (pos_x+1, -1.5)) - links.new(node_input.outputs['Fac'], node_mix.inputs[0]) + node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos_x + 1, -1.5)) + links.new(node_input.outputs["Fac"], node_mix.inputs[0]) links.new(combine_reflect1_alpha1_edge1.outputs[0], node_mix.inputs[1]) links.new(combine_reflect2_alpha2_edge2.outputs[0], node_mix.inputs[2]) links.new(node_mix.outputs[0], separate_reflect_alpha_edge.inputs[0]) - combine_base1a_toon1a_sphere1a = ng.new_node('ShaderNodeCombineRGB', (-2, -2.0)) - combine_base2a_toon2a_sphere2a = ng.new_node('ShaderNodeCombineRGB', (-2, -2.25)) - separate_basea_toona_spherea = ng.new_node('ShaderNodeSeparateRGB', (pos_x+2, -2.0)) + combine_base1a_toon1a_sphere1a = ng.new_node("ShaderNodeCombineRGB", (-2, -2.0)) + combine_base2a_toon2a_sphere2a = ng.new_node("ShaderNodeCombineRGB", (-2, -2.25)) + separate_basea_toona_spherea = ng.new_node("ShaderNodeSeparateRGB", (pos_x + 2, -2.0)) - node_mix = ng.new_mix_node('MULTIPLY' if use_mul else 'ADD', (pos_x+1, -2.0)) - links.new(node_input.outputs['Fac'], node_mix.inputs[0]) + node_mix = ng.new_mix_node("MULTIPLY" if use_mul else "ADD", (pos_x + 1, -2.0)) + links.new(node_input.outputs["Fac"], node_mix.inputs[0]) links.new(combine_base1a_toon1a_sphere1a.outputs[0], node_mix.inputs[1]) links.new(combine_base2a_toon2a_sphere2a.outputs[0], node_mix.inputs[2]) links.new(node_mix.outputs[0], separate_basea_toona_spherea.inputs[0]) - base_rgb = __blend_color_add('Base', (pos_x, -2.5), ' RGB') - __add_sockets('Base', combine_base1a_toon1a_sphere1a.inputs[0], combine_base2a_toon2a_sphere2a.inputs[0], separate_basea_toona_spherea.outputs[0], tag=' A') - __blend_tex_color('Base', (pos_x+3, -2.5), base_rgb, separate_basea_toona_spherea.outputs[0]) + base_rgb = __blend_color_add("Base", (pos_x, -2.5), " RGB") + __add_sockets("Base", combine_base1a_toon1a_sphere1a.inputs[0], combine_base2a_toon2a_sphere2a.inputs[0], separate_basea_toona_spherea.outputs[0], tag=" A") + __blend_tex_color("Base", (pos_x + 3, -2.5), base_rgb, separate_basea_toona_spherea.outputs[0]) - toon_rgb = __blend_color_add('Toon', (pos_x, -3.0), ' RGB') - __add_sockets('Toon', combine_base1a_toon1a_sphere1a.inputs[1], combine_base2a_toon2a_sphere2a.inputs[1], separate_basea_toona_spherea.outputs[1], tag=' A') - __blend_tex_color('Toon', (pos_x+3, -3.0), toon_rgb, separate_basea_toona_spherea.outputs[1]) + toon_rgb = __blend_color_add("Toon", (pos_x, -3.0), " RGB") + __add_sockets("Toon", combine_base1a_toon1a_sphere1a.inputs[1], combine_base2a_toon2a_sphere2a.inputs[1], separate_basea_toona_spherea.outputs[1], tag=" A") + __blend_tex_color("Toon", (pos_x + 3, -3.0), toon_rgb, separate_basea_toona_spherea.outputs[1]) - sphere_rgb = __blend_color_add('Sphere', (pos_x, -3.5), ' RGB') - __add_sockets('Sphere', combine_base1a_toon1a_sphere1a.inputs[2], combine_base2a_toon2a_sphere2a.inputs[2], separate_basea_toona_spherea.outputs[2], tag=' A') - __blend_tex_color('Sphere', (pos_x+3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2]) + sphere_rgb = __blend_color_add("Sphere", (pos_x, -3.5), " RGB") + __add_sockets("Sphere", combine_base1a_toon1a_sphere1a.inputs[2], combine_base2a_toon2a_sphere2a.inputs[2], separate_basea_toona_spherea.outputs[2], tag=" A") + __blend_tex_color("Sphere", (pos_x + 3, -3.5), sphere_rgb, separate_basea_toona_spherea.outputs[2]) ng.hide_nodes() return ng.shader diff --git a/mmd_tools/core/translations.py b/mmd_tools/core/translations.py index c7f19499..0999917e 100644 --- a/mmd_tools/core/translations.py +++ b/mmd_tools/core/translations.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# Copyright 2021 MMD Tools authors +# This file is part of MMD Tools. import itertools import re @@ -7,25 +9,24 @@ from typing import TYPE_CHECKING, Callable, Dict, Optional, Set, Tuple import bpy -from mmd_tools.core.model import FnModel, Model -from mmd_tools.translations import DictionaryEnum -from mmd_tools.utils import convertLRToName, convertNameToLR + +from ..translations import DictionaryEnum +from ..utils import convertLRToName, convertNameToLR +from .model import FnModel, Model if TYPE_CHECKING: - from mmd_tools.properties.morph import _MorphBase - from mmd_tools.properties.root import MMDRoot - from mmd_tools.properties.translations import (MMDTranslation, - MMDTranslationElement, - MMDTranslationElementIndex) + from ..properties.morph import _MorphBase + from ..properties.root import MMDRoot + from ..properties.translations import MMDTranslation, MMDTranslationElement, MMDTranslationElementIndex class MMDTranslationElementType(Enum): - BONE = 'Bones' - MORPH = 'Morphs' - MATERIAL = 'Materials' - DISPLAY = 'Display' - PHYSICS = 'Physics' - INFO = 'Information' + BONE = "Bones" + MORPH = "Morphs" + MATERIAL = "Materials" + DISPLAY = "Display" + PHYSICS = "Physics" + INFO = "Information" class MMDDataHandlerABC(ABC): @@ -37,66 +38,62 @@ def type_name(cls) -> str: @classmethod @abstractmethod - def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: 'MMDTranslationElement', index: int): + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): pass @classmethod @abstractmethod - def collect_data(cls, mmd_translation: 'MMDTranslation'): + def collect_data(cls, mmd_translation: "MMDTranslation"): pass @classmethod @abstractmethod - def update_index(cls, mmd_translation_element: 'MMDTranslationElement'): + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): pass @classmethod @abstractmethod - def update_query(cls, mmd_translation: 'MMDTranslation', filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): pass @classmethod @abstractmethod - def set_names(cls, mmd_translation_element: 'MMDTranslationElement', name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): pass @classmethod @abstractmethod - def get_names(cls, mmd_translation_element: 'MMDTranslationElement') -> Tuple[str, str, str]: + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: """Returns (name, name_j, name_e)""" @classmethod - def is_restorable(cls, mmd_translation_element: 'MMDTranslationElement') -> bool: + def is_restorable(cls, mmd_translation_element: "MMDTranslationElement") -> bool: return (mmd_translation_element.name, mmd_translation_element.name_j, mmd_translation_element.name_e) != cls.get_names(mmd_translation_element) @classmethod def check_data_visible(cls, filter_selected: bool, filter_visible: bool, select: bool, hide: bool) -> bool: - return ( - filter_selected and not select - or - filter_visible and hide - ) + return filter_selected and not select or filter_visible and hide @classmethod - def prop_restorable(cls, layout: bpy.types.UILayout, mmd_translation_element: 'MMDTranslationElement', prop_name: str, original_value: str, index: int): + def prop_restorable(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", prop_name: str, original_value: str, index: int): row = layout.row(align=True) - row.prop(mmd_translation_element, prop_name, text='') + row.prop(mmd_translation_element, prop_name, text="") if getattr(mmd_translation_element, prop_name) == original_value: - row.label(text='', icon='BLANK1') + row.label(text="", icon="BLANK1") return - op = row.operator('mmd_tools.restore_mmd_translation_element_name', text='', icon='FILE_REFRESH') + op = row.operator("mmd_tools.restore_mmd_translation_element_name", text="", icon="FILE_REFRESH") op.index = index op.prop_name = prop_name op.restore_value = original_value @classmethod - def prop_disabled(cls, layout: bpy.types.UILayout, mmd_translation_element: 'MMDTranslationElement', prop_name: str): + def prop_disabled(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", prop_name: str): row = layout.row(align=True) row.enabled = False - row.prop(mmd_translation_element, prop_name, text='') - row.label(text='', icon='BLANK1') + row.prop(mmd_translation_element, prop_name, text="") + row.label(text="", icon="BLANK1") class MMDBoneHandler(MMDDataHandlerABC): @@ -106,44 +103,41 @@ def type_name(cls) -> str: return MMDTranslationElementType.BONE.name @classmethod - def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: 'MMDTranslationElement', index: int): + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) row = layout.row(align=True) - row.label(text='', icon='BONE_DATA') + row.label(text="", icon="BONE_DATA") prop_row = row.row() - cls.prop_restorable(prop_row, mmd_translation_element, 'name', pose_bone.name, index) - cls.prop_restorable(prop_row, mmd_translation_element, 'name_j', pose_bone.mmd_bone.name_j, index) - cls.prop_restorable(prop_row, mmd_translation_element, 'name_e', pose_bone.mmd_bone.name_e, index) - row.prop(pose_bone.bone, 'select', text='', emboss=False, icon_only=True, icon='RESTRICT_SELECT_OFF' if pose_bone.bone.select else 'RESTRICT_SELECT_ON') - row.prop(pose_bone.bone, 'hide', text='', emboss=False, icon_only=True, icon='HIDE_ON' if pose_bone.bone.hide else 'HIDE_OFF') + cls.prop_restorable(prop_row, mmd_translation_element, "name", pose_bone.name, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_j", pose_bone.mmd_bone.name_j, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_e", pose_bone.mmd_bone.name_e, index) + row.prop(pose_bone.bone, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if pose_bone.bone.select else "RESTRICT_SELECT_ON") + row.prop(pose_bone.bone, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if pose_bone.bone.hide else "HIDE_OFF") @classmethod - def collect_data(cls, mmd_translation: 'MMDTranslation'): - armature_object: bpy.types.Object = FnModel.find_armature(mmd_translation.id_data) - armature: bpy.types.Armature = armature_object.data - visible_layer_indices = {i for i, visible in enumerate(armature.layers) if visible} + def collect_data(cls, mmd_translation: "MMDTranslation"): + armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data) pose_bone: bpy.types.PoseBone for index, pose_bone in enumerate(armature_object.pose.bones): - layers = pose_bone.bone.layers - if not any(layers[i] for i in visible_layer_indices): + if not any(c.is_visible for c in pose_bone.bone.collections): continue - mmd_translation_element: 'MMDTranslationElement' = mmd_translation.translation_elements.add() + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element.type = MMDTranslationElementType.BONE.name mmd_translation_element.object = armature_object - mmd_translation_element.data_path = f'pose.bones[{index}]' + mmd_translation_element.data_path = f"pose.bones[{index}]" mmd_translation_element.name = pose_bone.name mmd_translation_element.name_j = pose_bone.mmd_bone.name_j mmd_translation_element.name_e = pose_bone.mmd_bone.name_e @classmethod - def update_index(cls, mmd_translation_element: 'MMDTranslationElement'): + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): bpy.context.view_layer.objects.active = mmd_translation_element.object mmd_translation_element.object.id_data.data.bones.active = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path).bone @classmethod - def update_query(cls, mmd_translation: 'MMDTranslation', filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): - mmd_translation_element: 'MMDTranslationElement' + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): if mmd_translation_element.type != MMDTranslationElementType.BONE.name: continue @@ -159,11 +153,11 @@ def update_query(cls, mmd_translation: 'MMDTranslation', filter_selected: bool, if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): continue - mmd_translation_element_index: 'MMDTranslationElementIndex' = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index.value = index @classmethod - def set_names(cls, mmd_translation_element: 'MMDTranslationElement', name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) if name is not None: pose_bone.name = name @@ -173,7 +167,7 @@ def set_names(cls, mmd_translation_element: 'MMDTranslationElement', name: Optio pose_bone.mmd_bone.name_e = name_e @classmethod - def get_names(cls, mmd_translation_element: 'MMDTranslationElement') -> Tuple[str, str, str]: + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: pose_bone: bpy.types.PoseBone = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) return (pose_bone.name, pose_bone.mmd_bone.name_j, pose_bone.mmd_bone.name_e) @@ -185,79 +179,79 @@ def type_name(cls) -> str: return MMDTranslationElementType.MORPH.name @classmethod - def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: 'MMDTranslationElement', index: int): - morph: '_MorphBase' = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): + morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) row = layout.row(align=True) - row.label(text='', icon='SHAPEKEY_DATA') + row.label(text="", icon="SHAPEKEY_DATA") prop_row = row.row() - cls.prop_disabled(prop_row, mmd_translation_element, 'name') - cls.prop_restorable(prop_row, mmd_translation_element, 'name', morph.name, index) - cls.prop_restorable(prop_row, mmd_translation_element, 'name_e', morph.name_e, index) - row.label(text='', icon='BLANK1') - row.label(text='', icon='BLANK1') + cls.prop_disabled(prop_row, mmd_translation_element, "name") + cls.prop_restorable(prop_row, mmd_translation_element, "name", morph.name, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_e", morph.name_e, index) + row.label(text="", icon="BLANK1") + row.label(text="", icon="BLANK1") MORPH_DATA_PATH_EXTRACT = re.compile(r"mmd_root\.(?P[^\[]*)\[(?P\d*)\]") @classmethod - def collect_data(cls, mmd_translation: 'MMDTranslation'): + def collect_data(cls, mmd_translation: "MMDTranslation"): root_object: bpy.types.Object = mmd_translation.id_data - mmd_root: 'MMDRoot' = root_object.mmd_root + mmd_root: "MMDRoot" = root_object.mmd_root for morphs_name, morphs in { - 'material_morphs': mmd_root.material_morphs, - 'uv_morphs': mmd_root.uv_morphs, - 'bone_morphs': mmd_root.bone_morphs, - 'vertex_morphs': mmd_root.vertex_morphs, - 'group_morphs': mmd_root.group_morphs, + "material_morphs": mmd_root.material_morphs, + "uv_morphs": mmd_root.uv_morphs, + "bone_morphs": mmd_root.bone_morphs, + "vertex_morphs": mmd_root.vertex_morphs, + "group_morphs": mmd_root.group_morphs, }.items(): - morph: '_MorphBase' + morph: "_MorphBase" for index, morph in enumerate(morphs): - mmd_translation_element: 'MMDTranslationElement' = mmd_translation.translation_elements.add() + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element.type = MMDTranslationElementType.MORPH.name mmd_translation_element.object = root_object - mmd_translation_element.data_path = f'mmd_root.{morphs_name}[{index}]' + mmd_translation_element.data_path = f"mmd_root.{morphs_name}[{index}]" mmd_translation_element.name = morph.name # mmd_translation_element.name_j = None mmd_translation_element.name_e = morph.name_e @classmethod - def update_index(cls, mmd_translation_element: 'MMDTranslationElement'): + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): match = cls.MORPH_DATA_PATH_EXTRACT.match(mmd_translation_element.data_path) if not match: return - mmd_translation_element.object.mmd_root.active_morph_type = match['morphs_name'] - mmd_translation_element.object.mmd_root.active_morph = int(match['index']) + mmd_translation_element.object.mmd_root.active_morph_type = match["morphs_name"] + mmd_translation_element.object.mmd_root.active_morph = int(match["index"]) @classmethod - def update_query(cls, mmd_translation: 'MMDTranslation', filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): - mmd_translation_element: 'MMDTranslationElement' + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): if mmd_translation_element.type != MMDTranslationElementType.MORPH.name: continue - morph: '_MorphBase' = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) if check_blank_name(morph.name, morph.name_e): continue if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): continue - mmd_translation_element_index: 'MMDTranslationElementIndex' = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index.value = index @classmethod - def set_names(cls, mmd_translation_element: 'MMDTranslationElement', name: Optional[str], name_j: Optional[str], name_e: Optional[str]): - morph: '_MorphBase' = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) if name is not None: morph.name = name if name_e is not None: morph.name_e = name_e @classmethod - def get_names(cls, mmd_translation_element: 'MMDTranslationElement') -> Tuple[str, str, str]: - morph: '_MorphBase' = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) - return (morph.name, '', morph.name_e) + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: + morph: "_MorphBase" = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + return (morph.name, "", morph.name_e) class MMDMaterialHandler(MMDDataHandlerABC): @@ -267,25 +261,25 @@ def type_name(cls) -> str: return MMDTranslationElementType.MATERIAL.name @classmethod - def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: 'MMDTranslationElement', index: int): + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): mesh_object: bpy.types.Object = mmd_translation_element.object material: bpy.types.Material = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) row = layout.row(align=True) - row.label(text='', icon='MATERIAL_DATA') + row.label(text="", icon="MATERIAL_DATA") prop_row = row.row() - cls.prop_restorable(prop_row, mmd_translation_element, 'name', material.name, index) - cls.prop_restorable(prop_row, mmd_translation_element, 'name_j', material.mmd_material.name_j, index) - cls.prop_restorable(prop_row, mmd_translation_element, 'name_e', material.mmd_material.name_e, index) - row.prop(mesh_object, 'select', text='', emboss=False, icon_only=True, icon='RESTRICT_SELECT_OFF' if mesh_object.select else 'RESTRICT_SELECT_ON') - row.prop(mesh_object, 'hide', text='', emboss=False, icon_only=True, icon='HIDE_ON' if mesh_object.hide else 'HIDE_OFF') + cls.prop_restorable(prop_row, mmd_translation_element, "name", material.name, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_j", material.mmd_material.name_j, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_e", material.mmd_material.name_e, index) + row.prop(mesh_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mesh_object.select_get() else "RESTRICT_SELECT_ON") + row.prop(mesh_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mesh_object.hide_get() else "HIDE_OFF") MATERIAL_DATA_PATH_EXTRACT = re.compile(r"data\.materials\[(?P\d*)\]") @classmethod - def collect_data(cls, mmd_translation: 'MMDTranslation'): + def collect_data(cls, mmd_translation: "MMDTranslation"): checked_materials: Set[bpy.types.Material] = set() mesh_object: bpy.types.Object - for mesh_object in FnModel.child_meshes(FnModel.find_armature(mmd_translation.id_data)): + for mesh_object in FnModel.iterate_mesh_objects(mmd_translation.id_data): material: bpy.types.Material for index, material in enumerate(mesh_object.data.materials): if material in checked_materials: @@ -293,19 +287,19 @@ def collect_data(cls, mmd_translation: 'MMDTranslation'): checked_materials.add(material) - if not hasattr(material, 'mmd_material'): + if not hasattr(material, "mmd_material"): continue - mmd_translation_element: 'MMDTranslationElement' = mmd_translation.translation_elements.add() + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element.type = MMDTranslationElementType.MATERIAL.name mmd_translation_element.object = mesh_object - mmd_translation_element.data_path = f'data.materials[{index}]' + mmd_translation_element.data_path = f"data.materials[{index}]" mmd_translation_element.name = material.name mmd_translation_element.name_j = material.mmd_material.name_j mmd_translation_element.name_e = material.mmd_material.name_e @classmethod - def update_index(cls, mmd_translation_element: 'MMDTranslationElement'): + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): id_data: bpy.types.Object = mmd_translation_element.object bpy.context.view_layer.objects.active = id_data @@ -313,17 +307,17 @@ def update_index(cls, mmd_translation_element: 'MMDTranslationElement'): if not match: return - id_data.active_material_index = int(match['index']) + id_data.active_material_index = int(match["index"]) @classmethod - def update_query(cls, mmd_translation: 'MMDTranslation', filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): - mmd_translation_element: 'MMDTranslationElement' + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): if mmd_translation_element.type != MMDTranslationElementType.MATERIAL.name: continue mesh_object: bpy.types.Object = mmd_translation_element.object - if cls.check_data_visible(filter_selected, filter_visible, mesh_object.select, mesh_object.hide): + if cls.check_data_visible(filter_selected, filter_visible, mesh_object.select_get(), mesh_object.hide_get()): continue material: bpy.types.Material = mesh_object.path_resolve(mmd_translation_element.data_path) @@ -333,11 +327,11 @@ def update_query(cls, mmd_translation: 'MMDTranslation', filter_selected: bool, if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): continue - mmd_translation_element_index: 'MMDTranslationElementIndex' = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index.value = index @classmethod - def set_names(cls, mmd_translation_element: 'MMDTranslationElement', name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): material: bpy.types.Material = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) if name is not None: material.name = name @@ -347,7 +341,7 @@ def set_names(cls, mmd_translation_element: 'MMDTranslationElement', name: Optio material.mmd_material.name_e = name_e @classmethod - def get_names(cls, mmd_translation_element: 'MMDTranslationElement') -> Tuple[str, str, str]: + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: material: bpy.types.Material = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) return (material.name, material.mmd_material.name_j, material.mmd_material.name_e) @@ -359,35 +353,35 @@ def type_name(cls) -> str: return MMDTranslationElementType.DISPLAY.name @classmethod - def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: 'MMDTranslationElement', index: int): - bone_group: bpy.types.BoneGroup = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): + bone_collection: bpy.types.BoneCollection = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) row = layout.row(align=True) - row.label(text='', icon='GROUP_BONE') + row.label(text="", icon="GROUP_BONE") prop_row = row.row() - cls.prop_restorable(prop_row, mmd_translation_element, 'name', bone_group.name, index) - cls.prop_disabled(prop_row, mmd_translation_element, 'name') - cls.prop_disabled(prop_row, mmd_translation_element, 'name_e') - row.prop(mmd_translation_element.object, 'select', text='', emboss=False, icon_only=True, icon='RESTRICT_SELECT_OFF' if mmd_translation_element.object.select else 'RESTRICT_SELECT_ON') - row.prop(mmd_translation_element.object, 'hide', text='', emboss=False, icon_only=True, icon='HIDE_ON' if mmd_translation_element.object.hide else 'HIDE_OFF') + cls.prop_restorable(prop_row, mmd_translation_element, "name", bone_collection.name, index) + cls.prop_disabled(prop_row, mmd_translation_element, "name") + cls.prop_disabled(prop_row, mmd_translation_element, "name_e") + row.prop(mmd_translation_element.object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if mmd_translation_element.object.select_get() else "RESTRICT_SELECT_ON") + row.prop(mmd_translation_element.object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if mmd_translation_element.object.hide_get() else "HIDE_OFF") - DISPLAY_DATA_PATH_EXTRACT = re.compile(r"pose\.bone_groups\[(?P\d*)\]") + DISPLAY_DATA_PATH_EXTRACT = re.compile(r"data\.collections\[(?P\d*)\]") @classmethod - def collect_data(cls, mmd_translation: 'MMDTranslation'): - armature_object: bpy.types.Object = FnModel.find_armature(mmd_translation.id_data) - bone_group: bpy.types.BoneGroup - for index, bone_group in enumerate(armature_object.pose.bone_groups): - mmd_translation_element: 'MMDTranslationElement' = mmd_translation.translation_elements.add() + def collect_data(cls, mmd_translation: "MMDTranslation"): + armature_object: bpy.types.Object = FnModel.find_armature_object(mmd_translation.id_data) + bone_collection: bpy.types.BoneCollection + for index, bone_collection in enumerate(armature_object.data.collections): + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element.type = MMDTranslationElementType.DISPLAY.name mmd_translation_element.object = armature_object - mmd_translation_element.data_path = f'pose.bone_groups[{index}]' - mmd_translation_element.name = bone_group.name + mmd_translation_element.data_path = f"data.collections[{index}]" + mmd_translation_element.name = bone_collection.name # mmd_translation_element.name_j = None # mmd_translation_element.name_e = None @classmethod - def update_index(cls, mmd_translation_element: 'MMDTranslationElement'): + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): id_data: bpy.types.Object = mmd_translation_element.object bpy.context.view_layer.objects.active = id_data @@ -395,11 +389,11 @@ def update_index(cls, mmd_translation_element: 'MMDTranslationElement'): if not match: return - id_data.pose.bone_groups.active_index = int(match['index']) + id_data.data.collections.active_index = int(match["index"]) @classmethod - def update_query(cls, mmd_translation: 'MMDTranslation', filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): - mmd_translation_element: 'MMDTranslationElement' + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): if mmd_translation_element.type != MMDTranslationElementType.DISPLAY.name: continue @@ -408,26 +402,26 @@ def update_query(cls, mmd_translation: 'MMDTranslation', filter_selected: bool, if cls.check_data_visible(filter_selected, filter_visible, obj.select_get(), obj.hide_get()): continue - bone_group: bpy.types.BoneGroup = obj.path_resolve(mmd_translation_element.data_path) - if check_blank_name(bone_group.name, ''): + bone_collection: bpy.types.BoneCollection = obj.path_resolve(mmd_translation_element.data_path) + if check_blank_name(bone_collection.name, ""): continue if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): continue - mmd_translation_element_index: 'MMDTranslationElementIndex' = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index.value = index @classmethod - def set_names(cls, mmd_translation_element: 'MMDTranslationElement', name: Optional[str], name_j: Optional[str], name_e: Optional[str]): - bone_group: bpy.types.BoneGroup = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + bone_collection: bpy.types.BoneCollection = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) if name is not None: - bone_group.name = name + bone_collection.name = name @classmethod - def get_names(cls, mmd_translation_element: 'MMDTranslationElement') -> Tuple[str, str, str]: - bone_group: bpy.types.BoneGroup = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) - return (bone_group.name, '', '') + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: + bone_collection: bpy.types.BoneCollection = mmd_translation_element.object.path_resolve(mmd_translation_element.data_path) + return (bone_collection.name, "", "") class MMDPhysicsHandler(MMDDataHandlerABC): @@ -437,57 +431,57 @@ def type_name(cls) -> str: return MMDTranslationElementType.PHYSICS.name @classmethod - def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: 'MMDTranslationElement', index: int): + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): obj: bpy.types.Object = mmd_translation_element.object if FnModel.is_rigid_body_object(obj): - icon = 'MESH_ICOSPHERE' + icon = "MESH_ICOSPHERE" mmd_object = obj.mmd_rigid elif FnModel.is_joint_object(obj): - icon = 'CONSTRAINT' + icon = "CONSTRAINT" mmd_object = obj.mmd_joint row = layout.row(align=True) - row.label(text='', icon=icon) + row.label(text="", icon=icon) prop_row = row.row() - cls.prop_restorable(prop_row, mmd_translation_element, 'name', obj.name, index) - cls.prop_restorable(prop_row, mmd_translation_element, 'name_j', mmd_object.name_j, index) - cls.prop_restorable(prop_row, mmd_translation_element, 'name_e', mmd_object.name_e, index) - row.prop(obj, 'select', text='', emboss=False, icon_only=True, icon='RESTRICT_SELECT_OFF' if obj.select else 'RESTRICT_SELECT_ON') - row.prop(obj, 'hide', text='', emboss=False, icon_only=True, icon='HIDE_ON' if obj.hide else 'HIDE_OFF') + cls.prop_restorable(prop_row, mmd_translation_element, "name", obj.name, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_j", mmd_object.name_j, index) + cls.prop_restorable(prop_row, mmd_translation_element, "name_e", mmd_object.name_e, index) + row.prop(obj, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if obj.select_get() else "RESTRICT_SELECT_ON") + row.prop(obj, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if obj.hide_get() else "HIDE_OFF") @classmethod - def collect_data(cls, mmd_translation: 'MMDTranslation'): + def collect_data(cls, mmd_translation: "MMDTranslation"): root_object: bpy.types.Object = mmd_translation.id_data model = Model(root_object) obj: bpy.types.Object for obj in model.rigidBodies(): - mmd_translation_element: 'MMDTranslationElement' = mmd_translation.translation_elements.add() + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name mmd_translation_element.object = obj - mmd_translation_element.data_path = 'mmd_rigid' + mmd_translation_element.data_path = "mmd_rigid" mmd_translation_element.name = obj.name mmd_translation_element.name_j = obj.mmd_rigid.name_j mmd_translation_element.name_e = obj.mmd_rigid.name_e obj: bpy.types.Object for obj in model.joints(): - mmd_translation_element: 'MMDTranslationElement' = mmd_translation.translation_elements.add() + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element.type = MMDTranslationElementType.PHYSICS.name mmd_translation_element.object = obj - mmd_translation_element.data_path = 'mmd_joint' + mmd_translation_element.data_path = "mmd_joint" mmd_translation_element.name = obj.name mmd_translation_element.name_j = obj.mmd_joint.name_j mmd_translation_element.name_e = obj.mmd_joint.name_e @classmethod - def update_index(cls, mmd_translation_element: 'MMDTranslationElement'): + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): bpy.context.view_layer.objects.active = mmd_translation_element.object @classmethod - def update_query(cls, mmd_translation: 'MMDTranslation', filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): - mmd_translation_element: 'MMDTranslationElement' + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): if mmd_translation_element.type != MMDTranslationElementType.PHYSICS.name: continue @@ -507,11 +501,11 @@ def update_query(cls, mmd_translation: 'MMDTranslation', filter_selected: bool, if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): continue - mmd_translation_element_index: 'MMDTranslationElementIndex' = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index.value = index @classmethod - def set_names(cls, mmd_translation_element: 'MMDTranslationElement', name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): obj: bpy.types.Object = mmd_translation_element.object if FnModel.is_rigid_body_object(obj): @@ -527,7 +521,7 @@ def set_names(cls, mmd_translation_element: 'MMDTranslationElement', name: Optio mmd_object.name_e = name_e @classmethod - def get_names(cls, mmd_translation_element: 'MMDTranslationElement') -> Tuple[str, str, str]: + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: obj: bpy.types.Object = mmd_translation_element.object if FnModel.is_rigid_body_object(obj): @@ -545,72 +539,74 @@ def type_name(cls) -> str: return MMDTranslationElementType.INFO.name TYPE_TO_ICONS = { - 'EMPTY': 'EMPTY_DATA', - 'ARMATURE': 'ARMATURE_DATA', - 'MESH': 'MESH_DATA', + "EMPTY": "EMPTY_DATA", + "ARMATURE": "ARMATURE_DATA", + "MESH": "MESH_DATA", } @classmethod - def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: 'MMDTranslationElement', index: int): + def draw_item(cls, layout: bpy.types.UILayout, mmd_translation_element: "MMDTranslationElement", index: int): info_object: bpy.types.Object = mmd_translation_element.object row = layout.row(align=True) - row.label(text='', icon=MMDInfoHandler.TYPE_TO_ICONS.get(info_object.type, 'OBJECT_DATA')) + row.label(text="", icon=MMDInfoHandler.TYPE_TO_ICONS.get(info_object.type, "OBJECT_DATA")) prop_row = row.row() - cls.prop_restorable(prop_row, mmd_translation_element, 'name', info_object.name, index) - cls.prop_disabled(prop_row, mmd_translation_element, 'name') - cls.prop_disabled(prop_row, mmd_translation_element, 'name_e') - row.prop(info_object, 'select', text='', emboss=False, icon_only=True, icon='RESTRICT_SELECT_OFF' if info_object.select else 'RESTRICT_SELECT_ON') - row.prop(info_object, 'hide', text='', emboss=False, icon_only=True, icon='HIDE_ON' if info_object.hide else 'HIDE_OFF') + cls.prop_restorable(prop_row, mmd_translation_element, "name", info_object.name, index) + cls.prop_disabled(prop_row, mmd_translation_element, "name") + cls.prop_disabled(prop_row, mmd_translation_element, "name_e") + row.prop(info_object, "select", text="", emboss=False, icon_only=True, icon="RESTRICT_SELECT_OFF" if info_object.select_get() else "RESTRICT_SELECT_ON") + row.prop(info_object, "hide", text="", emboss=False, icon_only=True, icon="HIDE_ON" if info_object.hide_get() else "HIDE_OFF") @classmethod - def collect_data(cls, mmd_translation: 'MMDTranslation'): + def collect_data(cls, mmd_translation: "MMDTranslation"): root_object: bpy.types.Object = mmd_translation.id_data - armature_object: bpy.types.Object = FnModel.find_armature(root_object) + info_objects = [root_object] + armature_object = FnModel.find_armature_object(root_object) + if armature_object is not None: + info_objects.append(armature_object) - info_object: bpy.types.Object - for info_object in itertools.chain([root_object, armature_object], FnModel.child_meshes(armature_object)): - mmd_translation_element: 'MMDTranslationElement' = mmd_translation.translation_elements.add() + for info_object in itertools.chain(info_objects, FnModel.iterate_mesh_objects(root_object)): + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements.add() mmd_translation_element.type = MMDTranslationElementType.INFO.name mmd_translation_element.object = info_object - mmd_translation_element.data_path = '' + mmd_translation_element.data_path = "" mmd_translation_element.name = info_object.name # mmd_translation_element.name_j = None # mmd_translation_element.name_e = None @classmethod - def update_index(cls, mmd_translation_element: 'MMDTranslationElement'): + def update_index(cls, mmd_translation_element: "MMDTranslationElement"): bpy.context.view_layer.objects.active = mmd_translation_element.object @classmethod - def update_query(cls, mmd_translation: 'MMDTranslation', filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): - mmd_translation_element: 'MMDTranslationElement' + def update_query(cls, mmd_translation: "MMDTranslation", filter_selected: bool, filter_visible: bool, check_blank_name: Callable[[str, str], bool]): + mmd_translation_element: "MMDTranslationElement" for index, mmd_translation_element in enumerate(mmd_translation.translation_elements): if mmd_translation_element.type != MMDTranslationElementType.INFO.name: continue info_object: bpy.types.Object = mmd_translation_element.object - if cls.check_data_visible(filter_selected, filter_visible, info_object.select, info_object.hide): + if cls.check_data_visible(filter_selected, filter_visible, info_object.select_get(), info_object.hide_get()): continue - if check_blank_name(info_object.name, ''): + if check_blank_name(info_object.name, ""): continue if mmd_translation.filter_restorable and not cls.is_restorable(mmd_translation_element): continue - mmd_translation_element_index: 'MMDTranslationElementIndex' = mmd_translation.filtered_translation_element_indices.add() + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices.add() mmd_translation_element_index.value = index @classmethod - def set_names(cls, mmd_translation_element: 'MMDTranslationElement', name: Optional[str], name_j: Optional[str], name_e: Optional[str]): + def set_names(cls, mmd_translation_element: "MMDTranslationElement", name: Optional[str], name_j: Optional[str], name_e: Optional[str]): info_object: bpy.types.Object = mmd_translation_element.object if name is not None: info_object.name = name @classmethod - def get_names(cls, mmd_translation_element: 'MMDTranslationElement') -> Tuple[str, str, str]: + def get_names(cls, mmd_translation_element: "MMDTranslationElement") -> Tuple[str, str, str]: info_object: bpy.types.Object = mmd_translation_element.object - return (info_object.name, '', '') + return (info_object.name, "", "") MMD_DATA_HANDLERS: Set[MMDDataHandlerABC] = { @@ -628,10 +624,10 @@ def get_names(cls, mmd_translation_element: 'MMDTranslationElement') -> Tuple[st class FnTranslations: @staticmethod def apply_translations(root_object: bpy.types.Object): - mmd_translation: 'MMDTranslation' = root_object.mmd_root.translation - mmd_translation_element_index: 'MMDTranslationElementIndex' + mmd_translation: "MMDTranslation" = root_object.mmd_root.translation + mmd_translation_element_index: "MMDTranslationElementIndex" for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices: - mmd_translation_element: 'MMDTranslationElement' = mmd_translation.translation_elements[mmd_translation_element_index.value] + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value] handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type] name, name_j, name_e = handler.get_names(mmd_translation_element) handler.set_names( @@ -643,7 +639,7 @@ def apply_translations(root_object: bpy.types.Object): @staticmethod def execute_translation_batch(root_object: bpy.types.Object) -> Tuple[Dict[str, str], Optional[bpy.types.Text]]: - mmd_translation: 'MMDTranslation' = root_object.mmd_root.translation + mmd_translation: "MMDTranslation" = root_object.mmd_root.translation batch_operation_script = mmd_translation.batch_operation_script if not batch_operation_script: return ({}, None) @@ -655,12 +651,12 @@ def translate(name: str) -> str: return translator.translate(name, name) return name - batch_operation_script_ast = compile(mmd_translation.batch_operation_script, '', 'eval') + batch_operation_script_ast = compile(mmd_translation.batch_operation_script, "", "eval") batch_operation_target: str = mmd_translation.batch_operation_target - mmd_translation_element_index: 'MMDTranslationElementIndex' + mmd_translation_element_index: "MMDTranslationElementIndex" for mmd_translation_element_index in mmd_translation.filtered_translation_element_indices: - mmd_translation_element: 'MMDTranslationElement' = mmd_translation.translation_elements[mmd_translation_element_index.value] + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value] handler: MMDDataHandlerABC = MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type] @@ -670,49 +666,51 @@ def translate(name: str) -> str: org_name, org_name_j, org_name_e = handler.get_names(mmd_translation_element) # pylint: disable=eval-used - result_name = str(eval( - batch_operation_script_ast, - {'__builtins__': {}}, - { - 'to_english': translate, - 'to_mmd_lr': convertLRToName, - 'to_blender_lr': convertNameToLR, - 'name': name, - 'name_j': name_j if name_j != '' else name, - 'name_e': name_e if name_e != '' else name, - 'org_name': org_name, - 'org_name_j': org_name_j, - 'org_name_e': org_name_e, - } - )) - - if batch_operation_target == 'BLENDER': + result_name = str( + eval( + batch_operation_script_ast, + {"__builtins__": {}}, + { + "to_english": translate, + "to_mmd_lr": convertLRToName, + "to_blender_lr": convertNameToLR, + "name": name, + "name_j": name_j if name_j != "" else name, + "name_e": name_e if name_e != "" else name, + "org_name": org_name, + "org_name_j": org_name_j, + "org_name_e": org_name_e, + }, + ) + ) + + if batch_operation_target == "BLENDER": mmd_translation_element.name = result_name - elif batch_operation_target == 'JAPANESE': + elif batch_operation_target == "JAPANESE": mmd_translation_element.name_j = result_name - elif batch_operation_target == 'ENGLISH': + elif batch_operation_target == "ENGLISH": mmd_translation_element.name_e = result_name return (translator.fails, translator.save_fails()) @staticmethod - def update_index(mmd_translation: 'MMDTranslation'): + def update_index(mmd_translation: "MMDTranslation"): if mmd_translation.filtered_translation_element_indices_active_index < 0: return - mmd_translation_element_index: 'MMDTranslationElementIndex' = mmd_translation.filtered_translation_element_indices[mmd_translation.filtered_translation_element_indices_active_index] - mmd_translation_element: 'MMDTranslationElement' = mmd_translation.translation_elements[mmd_translation_element_index.value] + mmd_translation_element_index: "MMDTranslationElementIndex" = mmd_translation.filtered_translation_element_indices[mmd_translation.filtered_translation_element_indices_active_index] + mmd_translation_element: "MMDTranslationElement" = mmd_translation.translation_elements[mmd_translation_element_index.value] MMD_DATA_TYPE_TO_HANDLERS[mmd_translation_element.type].update_index(mmd_translation_element) @staticmethod - def collect_data(mmd_translation: 'MMDTranslation'): + def collect_data(mmd_translation: "MMDTranslation"): mmd_translation.translation_elements.clear() for handler in MMD_DATA_HANDLERS: handler.collect_data(mmd_translation) @staticmethod - def update_query(mmd_translation: 'MMDTranslation'): + def update_query(mmd_translation: "MMDTranslation"): mmd_translation.filtered_translation_element_indices.clear() mmd_translation.filtered_translation_element_indices_active_index = -1 @@ -723,18 +721,14 @@ def update_query(mmd_translation: 'MMDTranslation'): filter_visible: bool = mmd_translation.filter_visible def check_blank_name(name_j: str, name_e: str) -> bool: - return ( - filter_japanese_blank and name_j - or - filter_english_blank and name_e - ) + return filter_japanese_blank and name_j or filter_english_blank and name_e for handler in MMD_DATA_HANDLERS: if handler.type_name in mmd_translation.filter_types: handler.update_query(mmd_translation, filter_selected, filter_visible, check_blank_name) @staticmethod - def clear_data(mmd_translation: 'MMDTranslation'): + def clear_data(mmd_translation: "MMDTranslation"): mmd_translation.translation_elements.clear() mmd_translation.filtered_translation_element_indices.clear() mmd_translation.filtered_translation_element_indices_active_index = -1 diff --git a/mmd_tools/core/vmd/__init__.py b/mmd_tools/core/vmd/__init__.py index 869aba0f..5c823809 100644 --- a/mmd_tools/core/vmd/__init__.py +++ b/mmd_tools/core/vmd/__init__.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. + import collections import logging import struct @@ -7,32 +10,35 @@ class InvalidFileError(Exception): pass + ## vmd仕様の文字列をstringに変換 def _toShiftJisString(byteString): - return byteString.split(b'\x00')[0].decode('shift_jis', errors='replace') + return byteString.split(b"\x00")[0].decode("shift_jis", errors="replace") + def _toShiftJisBytes(string): - return string.encode('shift_jis', errors='replace') + return string.encode("shift_jis", errors="replace") class Header: - VMD_SIGN = b'Vocaloid Motion Data 0002' + VMD_SIGN = b"Vocaloid Motion Data 0002" + def __init__(self): self.signature = None - self.model_name = '' + self.model_name = "" def load(self, fin): - self.signature, = struct.unpack('<30s', fin.read(30)) - if self.signature[:len(self.VMD_SIGN)] != self.VMD_SIGN: - raise InvalidFileError('File signature "%s" is invalid.'%self.signature) - self.model_name = _toShiftJisString(struct.unpack('<20s', fin.read(20))[0]) + (self.signature,) = struct.unpack("<30s", fin.read(30)) + if self.signature[: len(self.VMD_SIGN)] != self.VMD_SIGN: + raise InvalidFileError('File signature "%s" is invalid.' % self.signature) + self.model_name = _toShiftJisString(struct.unpack("<20s", fin.read(20))[0]) def save(self, fin): - fin.write(struct.pack('<30s', self.VMD_SIGN)) - fin.write(struct.pack('<20s', _toShiftJisBytes(self.model_name))) + fin.write(struct.pack("<30s", self.VMD_SIGN)) + fin.write(struct.pack("<20s", _toShiftJisBytes(self.model_name))) def __repr__(self): - return '
'%(self.model_name) + return "
" % (self.model_name) class BoneFrameKey: @@ -43,25 +49,25 @@ def __init__(self): self.interp = [] def load(self, fin): - self.frame_number, = struct.unpack(''%( + return "" % ( str(self.frame_number), str(self.location), str(self.rotation), - ) + ) class ShapeKeyFrameKey: @@ -70,18 +76,18 @@ def __init__(self): self.weight = 0.0 def load(self, fin): - self.frame_number, = struct.unpack(''%( + return "" % ( str(self.frame_number), str(self.weight), - ) + ) class CameraKeyFrameKey: @@ -95,33 +101,33 @@ def __init__(self): self.persp = True def load(self, fin): - self.frame_number, = struct.unpack(''%( + return "" % ( str(self.frame_number), str(self.distance), str(self.location), str(self.rotation), str(self.angle), str(self.persp), - ) + ) class LampKeyFrameKey: @@ -131,82 +137,82 @@ def __init__(self): self.direction = [] def load(self, fin): - self.frame_number, = struct.unpack(''%( + return "" % ( str(self.frame_number), str(self.color), str(self.direction), - ) + ) class SelfShadowFrameKey: def __init__(self): self.frame_number = 0 - self.mode = 0 # 0: none, 1: mode1, 2: mode2 + self.mode = 0 # 0: none, 1: mode1, 2: mode2 self.distance = 0.0 def load(self, fin): - self.frame_number, = struct.unpack(''%( + return "" % ( str(self.frame_number), str(self.mode), str(self.distance), - ) + ) class PropertyFrameKey: def __init__(self): self.frame_number = 0 self.visible = True - self.ik_states = [] # list of (ik_name, enable/disable) + self.ik_states = [] # list of (ik_name, enable/disable) def load(self, fin): - self.frame_number, = struct.unpack(''%( + return "" % ( str(self.frame_number), str(self.visible), str(self.ik_states), - ) + ) class _AnimationBase(collections.defaultdict): @@ -218,10 +224,10 @@ def frameClass(): raise NotImplementedError def load(self, fin): - count, = struct.unpack(' 2.5: - if kp0.interpolation == 'CONSTANT': - result.add(int(kp1.co[0]-0.5)) - elif kp0.interpolation == 'BEZIER': + result.add(int(kp1.co[0] + 0.5)) + if kp0.interpolation != "LINEAR" and kp1.co.x - kp0.co.x > 2.5: + if kp0.interpolation == "CONSTANT": + result.add(int(kp1.co[0] - 0.5)) + elif kp0.interpolation == "BEZIER": bz = _FnBezier.from_fcurve(kp0, kp1) for t in bz.find_critical(): - result.add(int(bz.evaluate(t).x+0.5)) + result.add(int(bz.evaluate(t).x + 0.5)) kp0 = kp1 return result @staticmethod def getVMDControlPoints(kp0, kp1): - if kp0.interpolation == 'BEZIER': + if kp0.interpolation == "BEZIER": return _FCurve.__toVMDControlPoints(_FnBezier.from_fcurve(kp0, kp1)) return ((20, 20), (107, 107)) @@ -70,16 +73,16 @@ def __toVMDControlPoints(bezier): x1, y1 = p1 - p0 x2, y2 = p2 - p0 - x1 = max(0, min(127, int(0.5 + x1*127.0/dx))) - x2 = max(0, min(127, int(0.5 + x2*127.0/dx))) - y1 = max(0, min(127, int(0.5 + y1*127.0/dy))) - y2 = max(0, min(127, int(0.5 + y2*127.0/dy))) + x1 = max(0, min(127, int(0.5 + x1 * 127.0 / dx))) + x2 = max(0, min(127, int(0.5 + x2 * 127.0 / dx))) + y1 = max(0, min(127, int(0.5 + y1 * 127.0 / dy))) + y2 = max(0, min(127, int(0.5 + y2 * 127.0 / dy))) return ((x1, y1), (x2, y2)) def sampleFrames(self, frame_numbers: List[int]): # assume set(frame_numbers) & set(self.frameNumbers()) == set(self.frameNumbers()) fcurve = self.__fcurve - if fcurve is None or len(fcurve.keyframe_points) == 0: # no key frames + if fcurve is None or len(fcurve.keyframe_points) == 0: # no key frames return [[self.__default_value, ((20, 20), (107, 107))] for _ in frame_numbers] result = list() @@ -90,7 +93,7 @@ def sampleFrames(self, frame_numbers: List[int]): prev_i = None kp: bpy.types.Keyframe for kp in self.__sorted_keyframe_points: - i = int(kp.co[0]+0.5) + i = int(kp.co[0] + 0.5) if i == prev_i: prev_kp = kp continue @@ -101,13 +104,13 @@ def sampleFrames(self, frame_numbers: List[int]): frames.append(frame) if frame >= i: break - assert(len(frames) >= 1 and frames[-1] == i) + assert len(frames) >= 1 and frames[-1] == i if prev_kp is None: - for f in frames: # starting key frames + for f in frames: # starting key frames result.append([kp.co[1], ((20, 20), (107, 107))]) elif len(frames) == 1: result.append([kp.co[1], self.getVMDControlPoints(prev_kp, kp)]) - elif prev_kp.interpolation == 'BEZIER': + elif prev_kp.interpolation == "BEZIER": bz = _FnBezier.from_fcurve(prev_kp, kp) for f in frames[:-1]: b1, bz, pt = bz.split_by_x(f) @@ -120,16 +123,15 @@ def sampleFrames(self, frame_numbers: List[int]): prev_kp_co_1 = prev_kp.co[1] result.extend([[prev_kp_co_1, ((20, 20), (107, 107))] for _ in frame_iter]) - + return result class VMDExporter: - def __init__(self): self.__scale = 1 self.__frame_start = 1 - self.__frame_end = float('inf') + self.__frame_end = float("inf") self.__bone_converter_cls = vmd.importer.BoneConverter self.__ik_fcurves = {} @@ -153,7 +155,7 @@ def __allFrameKeys(self, curves: List[_FCurve]): all_frames = sorted(all_frames) all_keys = [i.sampleFrames(all_frames) for i in curves] - #return zip(all_frames, *all_keys) + # return zip(all_frames, *all_keys) for data in zip(all_frames, *all_keys): frame_number = data[0] if frame_number < frame_start: @@ -164,10 +166,10 @@ def __allFrameKeys(self, curves: List[_FCurve]): @staticmethod def __minRotationDiff(prev_q, curr_q): - t1 = (prev_q.w - curr_q.w)**2 + (prev_q.x - curr_q.x)**2 + (prev_q.y - curr_q.y)**2 + (prev_q.z - curr_q.z)**2 - t2 = (prev_q.w + curr_q.w)**2 + (prev_q.x + curr_q.x)**2 + (prev_q.y + curr_q.y)**2 + (prev_q.z + curr_q.z)**2 - #t1 = prev_q.rotation_difference(curr_q).angle - #t2 = prev_q.rotation_difference(-curr_q).angle + t1 = (prev_q.w - curr_q.w) ** 2 + (prev_q.x - curr_q.x) ** 2 + (prev_q.y - curr_q.y) ** 2 + (prev_q.z - curr_q.z) ** 2 + t2 = (prev_q.w + curr_q.w) ** 2 + (prev_q.x + curr_q.x) ** 2 + (prev_q.y + curr_q.y) ** 2 + (prev_q.z + curr_q.z) ** 2 + # t1 = prev_q.rotation_difference(curr_q).angle + # t2 = prev_q.rotation_difference(-curr_q).angle return -curr_q if t2 < t1 else curr_q @staticmethod @@ -180,7 +182,8 @@ def __getVMDBoneInterpolation(x_axis, y_axis, z_axis, rotation): z_x2, z_y2 = z_axis[1] r_x1, r_y1 = rotation[0] r_x2, r_y2 = rotation[1] - #return [ # minimum acceptable data + # fmt: off + # return [ # minimum acceptable data # x_x1, 0, 0, 0, x_y1, 0, 0, 0, x_x2, 0, 0, 0, x_y2, 0, 0, 0, # y_x1, 0, 0, 0, y_y1, 0, 0, 0, y_x2, 0, 0, 0, y_y2, 0, 0, 0, # z_x1, 0, 0, 0, z_y1, 0, 0, 0, z_x2, 0, 0, 0, z_y2, 0, 0, 0, @@ -191,7 +194,8 @@ def __getVMDBoneInterpolation(x_axis, y_axis, z_axis, rotation): y_x1, z_x1, r_x1, x_y1, y_y1, z_y1, r_y1, x_x2, y_x2, z_x2, r_x2, x_y2, y_y2, z_y2, r_y2, 0, z_x1, r_x1, x_y1, y_y1, z_y1, r_y1, x_x2, y_x2, z_x2, r_x2, x_y2, y_y2, z_y2, r_y2, 0, 0, r_x1, x_y1, y_y1, z_y1, r_y1, x_x2, y_x2, z_x2, r_x2, x_y2, y_y2, z_y2, r_y2, 0, 0, 0, - ] + ] + # fmt: on @staticmethod def __pickRotationInterpolation(rotation_interps): @@ -202,20 +206,22 @@ def __pickRotationInterpolation(rotation_interps): @staticmethod def __xyzw_from_rotation_mode(mode): - if mode == 'QUATERNION': + if mode == "QUATERNION": return lambda xyzw: xyzw - if mode == 'AXIS_ANGLE': + if mode == "AXIS_ANGLE": + def __xyzw_from_axis_angle(xyzw): q = mathutils.Quaternion(xyzw[:3], xyzw[3]) return [q.x, q.y, q.z, q.w] + return __xyzw_from_axis_angle def __xyzw_from_euler(xyzw): q = mathutils.Euler(xyzw[:3], xyzw[3]).to_quaternion() return [q.x, q.y, q.z, q.w] - return __xyzw_from_euler + return __xyzw_from_euler def __exportBoneAnimation(self, armObj): if armObj is None: @@ -229,47 +235,47 @@ def __exportBoneAnimation(self, armObj): anim_bones = {} rePath = re.compile(r'^pose\.bones\["(.+)"\]\.([a-z_]+)$') - prop_rotation_map = {'QUATERNION':'rotation_quaternion', 'AXIS_ANGLE':'rotation_axis_angle'} + prop_rotation_map = {"QUATERNION": "rotation_quaternion", "AXIS_ANGLE": "rotation_axis_angle"} for fcurve in animation_data.action.fcurves: m = rePath.match(fcurve.data_path) if m is None: continue bone = armObj.pose.bones.get(m.group(1), None) if bone is None: - logging.warning(' * Bone not found: %s', m.group(1)) + logging.warning(" * Bone not found: %s", m.group(1)) continue if bone.is_mmd_shadow_bone: continue prop_name = m.group(2) - if prop_name == 'mmd_ik_toggle': + if prop_name == "mmd_ik_toggle": self.__ik_fcurves[bone] = fcurve continue - if prop_name not in {'location', prop_rotation_map.get(bone.rotation_mode, 'rotation_euler')}: + if prop_name not in {"location", prop_rotation_map.get(bone.rotation_mode, "rotation_euler")}: continue if bone not in anim_bones: data = list(bone.location) - if bone.rotation_mode == 'QUATERNION': + if bone.rotation_mode == "QUATERNION": data += list(bone.rotation_quaternion) - elif bone.rotation_mode == 'AXIS_ANGLE': + elif bone.rotation_mode == "AXIS_ANGLE": data += list(bone.rotation_axis_angle) else: - data += ([bone.rotation_mode] + list(bone.rotation_euler)) - anim_bones[bone] = [_FCurve(i) for i in data] # x, y, z, rw, rx, ry, rz + data += [bone.rotation_mode] + list(bone.rotation_euler) + anim_bones[bone] = [_FCurve(i) for i in data] # x, y, z, rw, rx, ry, rz bone_curves = anim_bones[bone] - if prop_name == 'location': # x, y, z + if prop_name == "location": # x, y, z bone_curves[fcurve.array_index].setFCurve(fcurve) - elif prop_name == 'rotation_quaternion': # rw, rx, ry, rz - bone_curves[3+fcurve.array_index].setFCurve(fcurve) - elif prop_name == 'rotation_axis_angle': # rw, rx, ry, rz - bone_curves[3+fcurve.array_index].setFCurve(fcurve) - elif prop_name == 'rotation_euler': # mode, rx, ry, rz - bone_curves[3+fcurve.array_index+1].setFCurve(fcurve) + elif prop_name == "rotation_quaternion": # rw, rx, ry, rz + bone_curves[3 + fcurve.array_index].setFCurve(fcurve) + elif prop_name == "rotation_axis_angle": # rw, rx, ry, rz + bone_curves[3 + fcurve.array_index].setFCurve(fcurve) + elif prop_name == "rotation_euler": # mode, rx, ry, rz + bone_curves[3 + fcurve.array_index + 1].setFCurve(fcurve) for bone, bone_curves in anim_bones.items(): key_name = bone.mmd_bone.name_j or bone.name if key_name in vmd_bone_anim: - raise ValueError(f'VMD bone name {key_name} collision') + raise ValueError(f"VMD bone name {key_name} collision") frame_keys = vmd_bone_anim[key_name] @@ -284,17 +290,16 @@ def __exportBoneAnimation(self, armObj): if prev_rot is not None: curr_rot = self.__minRotationDiff(prev_rot, curr_rot) prev_rot = curr_rot - key.rotation = curr_rot[1:] + curr_rot[0:1] # (w, x, y, z) to (x, y, z, w) - #FIXME we can only choose one interpolation from (rw, rx, ry, rz) for bone's rotation + key.rotation = curr_rot[1:] + curr_rot[0:1] # (w, x, y, z) to (x, y, z, w) + # FIXME we can only choose one interpolation from (rw, rx, ry, rz) for bone's rotation ir = self.__pickRotationInterpolation([rw[1], rx[1], ry[1], rz[1]]) ix, iy, iz = converter.convert_interpolation([x[1], y[1], z[1]]) key.interp = self.__getVMDBoneInterpolation(ix, iy, iz, ir) frame_keys.append(key) - logging.info('(bone) frames:%5d name: %s', len(frame_keys), key_name) - logging.info('---- bone animations:%5d source: %s', len(vmd_bone_anim), armObj.name) + logging.info("(bone) frames:%5d name: %s", len(frame_keys), key_name) + logging.info("---- bone animations:%5d source: %s", len(vmd_bone_anim), armObj.name) return vmd_bone_anim - def __exportMorphAnimation(self, meshObj): if meshObj is None: return None @@ -309,6 +314,7 @@ def __exportMorphAnimation(self, meshObj): vmd_morph_anim = vmd.ShapeKeyAnimation() key_blocks = meshObj.data.shape_keys.key_blocks + def __get_key_block(key): if key.isdigit(): try: @@ -317,7 +323,7 @@ def __get_key_block(key): return None return key_blocks.get(eval(key), None) - rePath = re.compile(r'^key_blocks\[(.+)\]\.value$') + rePath = re.compile(r"^key_blocks\[(.+)\]\.value$") for fcurve in animation_data.action.fcurves: m = rePath.match(fcurve.data_path) if m is None: @@ -326,11 +332,11 @@ def __get_key_block(key): key_name = m.group(1) kb = __get_key_block(key_name) if kb is None: - logging.warning(' * Shape key not found: %s', key_name) + logging.warning(" * Shape key not found: %s", key_name) continue key_name = kb.name - assert(key_name not in vmd_morph_anim) + assert key_name not in vmd_morph_anim anim = vmd_morph_anim[key_name] curve = _FCurve(kb.value) @@ -341,25 +347,24 @@ def __get_key_block(key): key.frame_number = frame_number - self.__frame_start key.weight = weight[0] anim.append(key) - logging.info('(mesh) frames:%5d name: %s', len(anim), key_name) - logging.info('---- morph animations:%5d source: %s', len(vmd_morph_anim), meshObj.name) + logging.info("(mesh) frames:%5d name: %s", len(anim), key_name) + logging.info("---- morph animations:%5d source: %s", len(vmd_morph_anim), meshObj.name) return vmd_morph_anim - def __exportPropertyAnimation(self, armObj): if armObj is None: return None vmd_prop_anim = vmd.PropertyAnimation() - prop_curves = [_FCurve(True)] # visible, IKn + prop_curves = [_FCurve(True)] # visible, IKn root = armObj.parent - if getattr(root, 'mmd_type', None) == 'ROOT': + if getattr(root, "mmd_type", None) == "ROOT": animation_data = root.animation_data if animation_data and animation_data.action: for fcurve in animation_data.action.fcurves: - if fcurve.data_path == 'mmd_root.show_meshes': + if fcurve.data_path == "mmd_root.show_meshes": prop_curves[0].setFCurve(fcurve) break @@ -374,12 +379,11 @@ def __exportPropertyAnimation(self, armObj): key = vmd.PropertyFrameKey() key.frame_number = data[0] - self.__frame_start key.visible = int(0.5 + data[1][0]) - key.ik_states = [(ik_name, int(0.5+on_off[0])) for ik_name, on_off in zip(ik_name_list, data[2:])] + key.ik_states = [(ik_name, int(0.5 + on_off[0])) for ik_name, on_off in zip(ik_name_list, data[2:])] vmd_prop_anim.append(key) - logging.info('(property) frames:%5d name: %s', len(vmd_prop_anim), root.name if root else armObj.name) + logging.info("(property) frames:%5d name: %s", len(vmd_prop_anim), root.name if root else armObj.name) return vmd_prop_anim - def __exportCameraAnimation(self, cameraObj): if cameraObj is None: return None @@ -397,38 +401,39 @@ def __exportCameraAnimation(self, cameraObj): data.append(mmd_cam.mmd_camera.angle) data.append(mmd_cam.mmd_camera.is_perspective) data.append(camera.location.y) - cam_curves = [_FCurve(i) for i in data] # x, y, z, rx, ry, rz, fov, persp, distance + cam_curves = [_FCurve(i) for i in data] # x, y, z, rx, ry, rz, fov, persp, distance animation_data = mmd_cam.animation_data if animation_data and animation_data.action: for fcurve in animation_data.action.fcurves: - if fcurve.data_path == 'location': # x, y, z + if fcurve.data_path == "location": # x, y, z cam_curves[fcurve.array_index].setFCurve(fcurve) - elif fcurve.data_path == 'rotation_euler': # rx, ry, rz - cam_curves[3+fcurve.array_index].setFCurve(fcurve) - elif fcurve.data_path == 'mmd_camera.angle': # fov + elif fcurve.data_path == "rotation_euler": # rx, ry, rz + cam_curves[3 + fcurve.array_index].setFCurve(fcurve) + elif fcurve.data_path == "mmd_camera.angle": # fov cam_curves[6].setFCurve(fcurve) - elif fcurve.data_path == 'mmd_camera.is_perspective': # persp + elif fcurve.data_path == "mmd_camera.is_perspective": # persp cam_curves[7].setFCurve(fcurve) animation_data = camera.animation_data if animation_data and animation_data.action: for fcurve in animation_data.action.fcurves: - if fcurve.data_path == 'location' and fcurve.array_index == 1: # distance + if fcurve.data_path == "location" and fcurve.array_index == 1: # distance cam_curves[8].setFCurve(fcurve) for frame_number, x, y, z, rx, ry, rz, fov, persp, distance in self.__allFrameKeys(cam_curves): key = vmd.CameraKeyFrameKey() key.frame_number = frame_number - self.__frame_start - key.location = [x[0]*self.__scale, z[0]*self.__scale, y[0]*self.__scale] - key.rotation = [rx[0], rz[0], ry[0]] # euler + key.location = [x[0] * self.__scale, z[0] * self.__scale, y[0] * self.__scale] + key.rotation = [rx[0], rz[0], ry[0]] # euler key.angle = int(0.5 + math.degrees(fov[0])) key.distance = distance[0] * self.__scale key.persp = True if persp[0] else False - #FIXME we can only choose one interpolation from (rx, ry, rz) for camera's rotation + # FIXME we can only choose one interpolation from (rx, ry, rz) for camera's rotation ir = self.__pickRotationInterpolation([rx[1], ry[1], rz[1]]) ix, iy, iz, iD, iF = x[1], z[1], y[1], distance[1], fov[1] + # fmt: off key.interp = [ ix[0][0], ix[1][0], ix[0][1], ix[1][1], iy[0][0], iy[1][0], iy[0][1], iy[1][1], @@ -436,13 +441,13 @@ def __exportCameraAnimation(self, cameraObj): ir[0][0], ir[1][0], ir[0][1], ir[1][1], iD[0][0], iD[1][0], iD[0][1], iD[1][1], iF[0][0], iF[1][0], iF[0][1], iF[1][1], - ] + ] + # fmt: on vmd_cam_anim.append(key) - logging.info('(camera) frames:%5d name: %s', len(vmd_cam_anim), mmd_cam.name) + logging.info("(camera) frames:%5d name: %s", len(vmd_cam_anim), mmd_cam.name) return vmd_cam_anim - def __exportLampAnimation(self, lampObj): if lampObj is None: return None @@ -457,19 +462,19 @@ def __exportLampAnimation(self, lampObj): vmd_lamp_anim = vmd.LampAnimation() data = list(lamp.data.color) + list(lamp.location) - lamp_curves = [_FCurve(i) for i in data] # r, g, b, x, y, z + lamp_curves = [_FCurve(i) for i in data] # r, g, b, x, y, z animation_data = lamp.data.animation_data if animation_data and animation_data.action: for fcurve in animation_data.action.fcurves: - if fcurve.data_path == 'color': # r, g, b + if fcurve.data_path == "color": # r, g, b lamp_curves[fcurve.array_index].setFCurve(fcurve) animation_data = lamp.animation_data if animation_data and animation_data.action: for fcurve in animation_data.action.fcurves: - if fcurve.data_path == 'location': # x, y, z - lamp_curves[3+fcurve.array_index].setFCurve(fcurve) + if fcurve.data_path == "location": # x, y, z + lamp_curves[3 + fcurve.array_index].setFCurve(fcurve) for frame_number, r, g, b, x, y, z in self.__allFrameKeys(lamp_curves): key = vmd.LampKeyFrameKey() @@ -477,30 +482,29 @@ def __exportLampAnimation(self, lampObj): key.color = [r[0], g[0], b[0]] key.direction = [-x[0], -z[0], -y[0]] vmd_lamp_anim.append(key) - logging.info('(lamp) frames:%5d name: %s', len(vmd_lamp_anim), mmd_lamp.name) + logging.info("(lamp) frames:%5d name: %s", len(vmd_lamp_anim), mmd_lamp.name) return vmd_lamp_anim - def export(self, **args): - armature = args.get('armature', None) - mesh = args.get('mesh', None) - camera = args.get('camera', None) - lamp = args.get('lamp', None) - filepath = args.get('filepath', '') + armature = args.get("armature", None) + mesh = args.get("mesh", None) + camera = args.get("camera", None) + lamp = args.get("lamp", None) + filepath = args.get("filepath", "") - self.__scale = args.get('scale', 1.0) + self.__scale = args.get("scale", 1.0) - if args.get('use_frame_range', False): + if args.get("use_frame_range", False): self.__frame_start = bpy.context.scene.frame_start self.__frame_end = bpy.context.scene.frame_end - if args.get('use_pose_mode', False): + if args.get("use_pose_mode", False): self.__bone_converter_cls = vmd.importer.BoneConverterPoseMode if armature or mesh: vmdFile = vmd.File() vmdFile.header = vmd.Header() - vmdFile.header.model_name = args.get('model_name', '') + vmdFile.header.model_name = args.get("model_name", "") vmdFile.boneAnimation = self.__exportBoneAnimation(armature) vmdFile.shapeKeyAnimation = self.__exportMorphAnimation(mesh) vmdFile.propertyAnimation = self.__exportPropertyAnimation(armature) @@ -509,8 +513,7 @@ def export(self, **args): elif camera or lamp: vmdFile = vmd.File() vmdFile.header = vmd.Header() - vmdFile.header.model_name = u'カメラ・照明' + vmdFile.header.model_name = "カメラ・照明" vmdFile.cameraAnimation = self.__exportCameraAnimation(camera) vmdFile.lampAnimation = self.__exportLampAnimation(lamp) vmdFile.save(filepath=filepath) - diff --git a/mmd_tools/core/vmd/importer.py b/mmd_tools/core/vmd/importer.py index 824caaaf..5f589d87 100644 --- a/mmd_tools/core/vmd/importer.py +++ b/mmd_tools/core/vmd/importer.py @@ -1,23 +1,25 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import logging +import math import os from typing import Union import bpy -import math -from mathutils import Vector, Quaternion +from mathutils import Quaternion, Vector -from mmd_tools import utils -from mmd_tools.bpyutils import matmul -from mmd_tools.core import vmd -from mmd_tools.core.camera import MMDCamera -from mmd_tools.core.lamp import MMDLamp +from ... import utils +from .. import vmd +from ..camera import MMDCamera +from ..lamp import MMDLamp class _MirrorMapper: def __init__(self, data_map=None): - from mmd_tools.operators.view import FlipPose + from ...operators.view import FlipPose + self.__data_map = data_map self.__flip_name = FlipPose.flip_name @@ -72,6 +74,7 @@ def __init__(self, mat): def convert(self, interpolation_xyz): return (interpolation_xyz[i] for i in self.__indices) + class BoneConverter: def __init__(self, pose_bone, scale, invert=False): mat = pose_bone.bone.matrix_local.to_3x3() @@ -83,12 +86,13 @@ def __init__(self, pose_bone, scale, invert=False): self.convert_interpolation = _InterpolationHelper(self.__mat).convert def convert_location(self, location): - return matmul(self.__mat, Vector(location)) * self.__scale + return (self.__mat @ Vector(location)) * self.__scale def convert_rotation(self, rotation_xyzw): rot = Quaternion() rot.x, rot.y, rot.z, rot.w = rotation_xyzw - return Quaternion(matmul(self.__mat, rot.axis) * -1, rot.angle).normalized() + return Quaternion((self.__mat @ rot.axis) * -1, rot.angle).normalized() + class BoneConverterPoseMode: def __init__(self, pose_bone, scale, invert=False): @@ -97,7 +101,7 @@ def __init__(self, pose_bone, scale, invert=False): self.__mat = mat.transposed() self.__scale = scale self.__mat_rot = pose_bone.matrix_basis.to_3x3() - self.__mat_loc = matmul(self.__mat_rot, self.__mat) + self.__mat_loc = self.__mat_rot @ self.__mat self.__offset = pose_bone.location.copy() self.convert_location = self._convert_location self.convert_rotation = self._convert_rotation @@ -110,46 +114,38 @@ def __init__(self, pose_bone, scale, invert=False): self.convert_interpolation = _InterpolationHelper(self.__mat_loc).convert def _convert_location(self, location): - return self.__offset + matmul(self.__mat_loc, Vector(location)) * self.__scale + return self.__offset + (self.__mat_loc @ Vector(location)) * self.__scale def _convert_rotation(self, rotation_xyzw): rot = Quaternion() rot.x, rot.y, rot.z, rot.w = rotation_xyzw - rot = Quaternion(matmul(self.__mat, rot.axis) * -1, rot.angle) - return matmul(self.__mat_rot, rot.to_matrix()).to_quaternion() + rot = Quaternion((self.__mat @ rot.axis) * -1, rot.angle) + return (self.__mat_rot @ rot.to_matrix()).to_quaternion() def _convert_location_inverted(self, location): - return matmul(self.__mat_loc, Vector(location) - self.__offset) * self.__scale + return (self.__mat_loc @ (Vector(location) - self.__offset)) * self.__scale def _convert_rotation_inverted(self, rotation_xyzw): rot = Quaternion() rot.x, rot.y, rot.z, rot.w = rotation_xyzw - rot = matmul(self.__mat_rot, rot.to_matrix()).to_quaternion() - return Quaternion(matmul(self.__mat, rot.axis) * -1, rot.angle).normalized() + rot = (self.__mat_rot @ rot.to_matrix()).to_quaternion() + return Quaternion((self.__mat @ rot.axis) * -1, rot.angle).normalized() class _FnBezier: - - __BLENDER_2_91_OR_NEWER = not (bpy.app.version < (2, 91, 0)) - @classmethod def from_fcurve(cls, kp0, kp1): p0, p1, p2, p3 = kp0.co, kp0.handle_right, kp1.handle_left, kp1.co - if cls.__BLENDER_2_91_OR_NEWER: # the F-Curve can become near-vertical - if p1.x > p3.x: - t = (p3.x - p0.x) / (p1.x - p0.x) - p1 = (1-t)*p0 + p1*t - if p0.x > p2.x: - t = (p3.x - p0.x) / (p3.x - p2.x) - p2 = (1-t)*p3 + p2*t - elif p1.x > p2.x: # legacy F-Curve correction - t = (p3.x - p0.x) / (p1.x - p0.x + p3.x - p2.x) - p1 = (1-t)*p0 + p1*t - p2 = (1-t)*p3 + p2*t + if p1.x > p3.x: + t = (p3.x - p0.x) / (p1.x - p0.x) + p1 = (1 - t) * p0 + p1 * t + if p0.x > p2.x: + t = (p3.x - p0.x) / (p3.x - p2.x) + p2 = (1 - t) * p3 + p2 * t return cls(p0, p1, p2, p3) - def __init__(self, p0, p1, p2, p3): # assuming VMD's bezier or F-Curve's bezier - #assert(p0.x <= p1.x <= p3.x and p0.x <= p2.x <= p3.x) + def __init__(self, p0, p1, p2, p3): # assuming VMD's bezier or F-Curve's bezier + # assert(p0.x <= p1.x <= p3.x and p0.x <= p2.x <= p3.x) self._p0, self._p1, self._p2, self._p3 = p0, p1, p2, p3 @property @@ -158,22 +154,22 @@ def points(self): def split(self, t): p0, p1, p2, p3 = self._p0, self._p1, self._p2, self._p3 - p01t = (1-t)*p0 + t*p1 - p12t = (1-t)*p1 + t*p2 - p23t = (1-t)*p2 + t*p3 - p012t = (1-t)*p01t + t*p12t - p123t = (1-t)*p12t + t*p23t - pt = (1-t)*p012t + t*p123t + p01t = (1 - t) * p0 + t * p1 + p12t = (1 - t) * p1 + t * p2 + p23t = (1 - t) * p2 + t * p3 + p012t = (1 - t) * p01t + t * p12t + p123t = (1 - t) * p12t + t * p23t + pt = (1 - t) * p012t + t * p123t return _FnBezier(p0, p01t, p012t, pt), _FnBezier(pt, p123t, p23t, p3), pt def evaluate(self, t): p0, p1, p2, p3 = self._p0, self._p1, self._p2, self._p3 - p01t = (1-t)*p0 + t*p1 - p12t = (1-t)*p1 + t*p2 - p23t = (1-t)*p2 + t*p3 - p012t = (1-t)*p01t + t*p12t - p123t = (1-t)*p12t + t*p23t - return (1-t)*p012t + t*p123t + p01t = (1 - t) * p0 + t * p1 + p12t = (1 - t) * p1 + t * p2 + p23t = (1 - t) * p2 + t * p3 + p012t = (1 - t) * p01t + t * p12t + p123t = (1 - t) * p12t + t * p23t + return (1 - t) * p012t + t * p123t def split_by_x(self, x): return self.split(self.axis_to_t(x)) @@ -184,7 +180,7 @@ def evaluate_by_x(self, x): def axis_to_t(self, val, axis=0): p0, p1, p2, p3 = self._p0[axis], self._p1[axis], self._p2[axis], self._p3[axis] a = p3 - p0 + 3 * (p1 - p2) - b = 3 * (p0 - 2*p1 + p2) + b = 3 * (p0 - 2 * p1 + p2) c = 3 * (p1 - p0) d = p0 - val return next(self.__find_roots(a, b, c, d)) @@ -194,70 +190,71 @@ def find_critical(self): p_min, p_max = (p0, p3) if p0 < p3 else (p3, p0) if p1 > p_max or p1 < p_min or p2 > p_max or p2 < p_min: a = 3 * (p3 - p0 + 3 * (p1 - p2)) - b = 6 * (p0 - 2*p1 + p2) + b = 6 * (p0 - 2 * p1 + p2) c = 3 * (p1 - p0) yield from self.__find_roots(0, a, b, c) @staticmethod - def __find_roots(a, b, c, d): # a*t*t*t + b*t*t + c*t + d = 0 - #TODO fix precision errors (ex: t=0 and t=1) and improve performance + def __find_roots(a, b, c, d): # a*t*t*t + b*t*t + c*t + d = 0 + # TODO fix precision errors (ex: t=0 and t=1) and improve performance if a == 0: if b == 0: - t = -d/c + t = -d / c if 0 <= t <= 1: yield t else: - D = c*c - 4*b*d + D = c * c - 4 * b * d if D < 0: return D = D**0.5 - b2 = 2*b - t = (-c + D)/b2 + b2 = 2 * b + t = (-c + D) / b2 if 0 <= t <= 1: yield t - t = (-c - D)/b2 + t = (-c - D) / b2 if 0 <= t <= 1: yield t return def _sqrt3(v): - return -((-v)**(1/3)) if v < 0 else v**(1/3) + return -((-v) ** (1 / 3)) if v < 0 else v ** (1 / 3) - A = b*c/(6*a*a) - b*b*b/(27*a*a*a) - d/(2*a) - B = c/(3*a) - b*b/(9*a*a) - b_3a = -b/(3*a) - D = A*A + B*B*B + A = b * c / (6 * a * a) - b * b * b / (27 * a * a * a) - d / (2 * a) + B = c / (3 * a) - b * b / (9 * a * a) + b_3a = -b / (3 * a) + D = A * A + B * B * B if D > 0: D = D**0.5 - t = b_3a + _sqrt3(A+D) + _sqrt3(A-D) + t = b_3a + _sqrt3(A + D) + _sqrt3(A - D) if 0 <= t <= 1: yield t elif D == 0: - t = b_3a + _sqrt3(A)*2 + t = b_3a + _sqrt3(A) * 2 if 0 <= t <= 1: yield t t = b_3a - _sqrt3(A) if 0 <= t <= 1: yield t else: - R = A / (-B*B*B)**0.5 - t = b_3a + 2*(-B)**0.5 * math.cos(math.acos(R) / 3) + R = A / (-B * B * B) ** 0.5 + t = b_3a + 2 * (-B) ** 0.5 * math.cos(math.acos(R) / 3) if 0 <= t <= 1: yield t - t = b_3a + 2*(-B)**0.5 * math.cos((math.acos(R) + 2*math.pi) / 3) + t = b_3a + 2 * (-B) ** 0.5 * math.cos((math.acos(R) + 2 * math.pi) / 3) if 0 <= t <= 1: yield t - t = b_3a + 2*(-B)**0.5 * math.cos((math.acos(R) - 2*math.pi) / 3) + t = b_3a + 2 * (-B) ** 0.5 * math.cos((math.acos(R) - 2 * math.pi) / 3) if 0 <= t <= 1: yield t + class HasAnimationData: animation_data: bpy.types.AnimData + class VMDImporter: - def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False, - convert_mmd_camera=True, convert_mmd_lamp=True, frame_margin=5, use_mirror=False, use_NLA=False): + def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False, convert_mmd_camera=True, convert_mmd_lamp=True, frame_margin=5, use_mirror=False, use_NLA=False): self.__vmdFile = vmd.File() self.__vmdFile.load(filepath=filepath) logging.debug(str(self.__vmdFile.header)) @@ -270,23 +267,22 @@ def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False, self.__mirror = use_mirror self.__use_NLA = use_NLA - @staticmethod def __minRotationDiff(prev_q, curr_q): - t1 = (prev_q.w - curr_q.w)**2 + (prev_q.x - curr_q.x)**2 + (prev_q.y - curr_q.y)**2 + (prev_q.z - curr_q.z)**2 - t2 = (prev_q.w + curr_q.w)**2 + (prev_q.x + curr_q.x)**2 + (prev_q.y + curr_q.y)**2 + (prev_q.z + curr_q.z)**2 - #t1 = prev_q.rotation_difference(curr_q).angle - #t2 = prev_q.rotation_difference(-curr_q).angle + t1 = (prev_q.w - curr_q.w) ** 2 + (prev_q.x - curr_q.x) ** 2 + (prev_q.y - curr_q.y) ** 2 + (prev_q.z - curr_q.z) ** 2 + t2 = (prev_q.w + curr_q.w) ** 2 + (prev_q.x + curr_q.x) ** 2 + (prev_q.y + curr_q.y) ** 2 + (prev_q.z + curr_q.z) ** 2 + # t1 = prev_q.rotation_difference(curr_q).angle + # t2 = prev_q.rotation_difference(-curr_q).angle return -curr_q if t2 < t1 else curr_q @staticmethod def __setInterpolation(bezier, kp0, kp1): if bezier[0] == bezier[1] and bezier[2] == bezier[3]: - kp0.interpolation = 'LINEAR' + kp0.interpolation = "LINEAR" else: - kp0.interpolation = 'BEZIER' - kp0.handle_right_type = 'FREE' - kp1.handle_left_type = 'FREE' + kp0.interpolation = "BEZIER" + kp0.handle_right_type = "FREE" + kp1.handle_left_type = "FREE" d = (kp1.co - kp0.co) / 127.0 kp0.handle_right = kp0.co + Vector((d.x * bezier[0], d.y * bezier[1])) kp1.handle_left = kp0.co + Vector((d.x * bezier[2], d.y * bezier[3])) @@ -294,10 +290,10 @@ def __setInterpolation(bezier, kp0, kp1): @staticmethod def __fixFcurveHandles(fcurve): kp0 = fcurve.keyframe_points[0] - kp0.handle_left_type = 'FREE' + kp0.handle_left_type = "FREE" kp0.handle_left = kp0.co + Vector((-1, 0)) kp = fcurve.keyframe_points[-1] - kp.handle_right_type = 'FREE' + kp.handle_right_type = "FREE" kp.handle_right = kp.co + Vector((1, 0)) @staticmethod @@ -305,7 +301,7 @@ def __keyframe_insert_inner(fcurves: bpy.types.ActionFCurves, path: str, index: fcurve = fcurves.find(path, index=index) if fcurve is None: fcurve = fcurves.new(path, index=index) - fcurve.keyframe_points.insert(frame, value, options={'FAST'}) + fcurve.keyframe_points.insert(frame, value, options={"FAST"}) @staticmethod def __keyframe_insert(fcurves: bpy.types.ActionFCurves, path: str, frame: float, value: Union[int, float, Vector]): @@ -318,37 +314,42 @@ def __keyframe_insert(fcurves: bpy.types.ActionFCurves, path: str, frame: float, VMDImporter.__keyframe_insert_inner(fcurves, path, 2, frame, value[2]) else: - raise TypeError('Unsupported type: {0}'.format(type(value))) + raise TypeError("Unsupported type: {0}".format(type(value))) def __getBoneConverter(self, bone): converter = self.__bone_util_cls(bone, self.__scale) mode = bone.rotation_mode compatible_quaternion = self.__minRotationDiff + class _ConverterWrap: convert_location = converter.convert_location convert_interpolation = converter.convert_interpolation - if mode == 'QUATERNION': + if mode == "QUATERNION": convert_rotation = converter.convert_rotation compatible_rotation = compatible_quaternion - elif mode == 'AXIS_ANGLE': + elif mode == "AXIS_ANGLE": + @staticmethod def convert_rotation(rot): (x, y, z), angle = converter.convert_rotation(rot).to_axis_angle() return (angle, x, y, z) + @staticmethod def compatible_rotation(prev, curr): angle, x, y, z = curr - if prev[1]*x + prev[2]*y + prev[3]*z < 0: + if prev[1] * x + prev[2] * y + prev[3] * z < 0: angle, x, y, z = -angle, -x, -y, -z angle_diff = prev[0] - angle if abs(angle_diff) > math.pi: pi_2 = math.pi * 2 bias = -0.5 if angle_diff < 0 else 0.5 - angle += int(bias + angle_diff/pi_2) * pi_2 + angle += int(bias + angle_diff / pi_2) * pi_2 return (angle, x, y, z) + else: convert_rotation = lambda rot: converter.convert_rotation(rot).to_euler(mode) compatible_rotation = lambda prev, curr: curr.make_compatible(prev) or curr + return _ConverterWrap def __assign_action(self, target: Union[bpy.types.ID, HasAnimationData], action: bpy.types.Action): @@ -362,11 +363,11 @@ def __assign_action(self, target: Union[bpy.types.ID, HasAnimationData], action: target_track: bpy.types.NlaTrack = target.animation_data.nla_tracks.new() target_track.name = action.name target_strip = target_track.strips.new(action.name, frame_current, action) - target_strip.blend_type = 'COMBINE' + target_strip.blend_type = "COMBINE" def __assignToArmature(self, armObj, action_name=None): boneAnim = self.__vmdFile.boneAnimation - logging.info('---- bone animations:%5d target: %s', len(boneAnim), armObj.name) + logging.info("---- bone animations:%5d target: %s", len(boneAnim), armObj.name) if len(boneAnim) < 1: return @@ -384,9 +385,11 @@ def __assignToArmature(self, armObj, action_name=None): pose_bones = _MirrorMapper(pose_bones) _loc, _rot = _MirrorMapper.get_location, _MirrorMapper.get_rotation - class _Dummy: pass + class _Dummy: + pass + dummy_keyframe_points = iter(lambda: _Dummy, None) - prop_rot_map = {'QUATERNION':'rotation_quaternion', 'AXIS_ANGLE':'rotation_axis_angle'} + prop_rot_map = {"QUATERNION": "rotation_quaternion", "AXIS_ANGLE": "rotation_axis_angle"} bone_name_table = {} for name, keyFrames in boneAnim.items(): @@ -395,44 +398,44 @@ class _Dummy: pass continue bone = pose_bones.get(name, None) if bone is None: - logging.warning('WARNING: not found bone %s (%d frames)', name, len(keyFrames)) + logging.warning("WARNING: not found bone %s (%d frames)", name, len(keyFrames)) continue - logging.info('(bone) frames:%5d name: %s', len(keyFrames), name) - assert(bone_name_table.get(bone.name, name) == name) + logging.info("(bone) frames:%5d name: %s", len(keyFrames), name) + assert bone_name_table.get(bone.name, name) == name bone_name_table[bone.name] = name - fcurves = [dummy_keyframe_points]*7 # x, y, z, r0, r1, r2, (r3) - data_path_rot = prop_rot_map.get(bone.rotation_mode, 'rotation_euler') + fcurves = [dummy_keyframe_points] * 7 # x, y, z, r0, r1, r2, (r3) + data_path_rot = prop_rot_map.get(bone.rotation_mode, "rotation_euler") bone_rotation = getattr(bone, data_path_rot) default_values = list(bone.location) + list(bone_rotation) - data_path = 'pose.bones["%s"].location'%bone.name + data_path = 'pose.bones["%s"].location' % bone.name for axis_i in range(3): fcurves[axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name) - data_path = 'pose.bones["%s"].%s'%(bone.name, data_path_rot) + data_path = 'pose.bones["%s"].%s' % (bone.name, data_path_rot) for axis_i in range(len(bone_rotation)): - fcurves[3+axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name) + fcurves[3 + axis_i] = action.fcurves.new(data_path=data_path, index=axis_i, action_group=bone.name) for i in range(len(default_values)): c = fcurves[i] - c.keyframe_points.add(extra_frame+num_frame) + c.keyframe_points.add(extra_frame + num_frame) kp_iter = iter(c.keyframe_points) if extra_frame: kp = next(kp_iter) kp.co = (1, default_values[i]) - kp.interpolation = 'LINEAR' + kp.interpolation = "LINEAR" fcurves[i] = kp_iter converter = self.__getBoneConverter(bone) prev_rot = bone_rotation if extra_frame else None - prev_kps, indices = None, tuple(converter.convert_interpolation((0, 16, 32)))+(48,)*len(bone_rotation) - keyFrames.sort(key=lambda x:x.frame_number) + prev_kps, indices = None, tuple(converter.convert_interpolation((0, 16, 32))) + (48,) * len(bone_rotation) + keyFrames.sort(key=lambda x: x.frame_number) for k, x, y, z, r0, r1, r2, r3 in zip(keyFrames, *fcurves): frame = k.frame_number + self.__frame_margin loc = converter.convert_location(_loc(k.location)) curr_rot = converter.convert_rotation(_rot(k.rotation)) if prev_rot is not None: curr_rot = converter.compatible_rotation(prev_rot, curr_rot) - #FIXME the rotation interpolation has slightly different result + # FIXME the rotation interpolation has slightly different result # Blender: rot(x) = prev_rot*(1 - bezier(t)) + curr_rot*bezier(t) # MMD: rot(x) = prev_rot.slerp(curr_rot, factor=bezier(t)) prev_rot = curr_rot @@ -449,7 +452,7 @@ class _Dummy: pass if prev_kps is not None: interp = k.interp for idx, prev_kp, kp in zip(indices, prev_kps, curr_kps): - self.__setInterpolation(interp[idx:idx+16:4], prev_kp, kp) + self.__setInterpolation(interp[idx : idx + 16 : 4], prev_kp, kp) prev_kps = curr_kps for c in action.fcurves: @@ -463,9 +466,9 @@ class _Dummy: pass # property animation propertyAnim = self.__vmdFile.propertyAnimation if len(propertyAnim) > 0: - logging.info('---- IK animations:%5d target: %s', len(propertyAnim), armObj.name) + logging.info("---- IK animations:%5d target: %s", len(propertyAnim), armObj.name) for keyFrame in propertyAnim: - logging.debug('(IK) frame:%5d list: %s', keyFrame.frame_number, keyFrame.ik_states) + logging.debug("(IK) frame:%5d list: %s", keyFrame.frame_number, keyFrame.ik_states) frame = keyFrame.frame_number + self.__frame_margin for ikName, enable in keyFrame.ik_states: bone = pose_bones.get(ikName, None) @@ -478,7 +481,7 @@ class _Dummy: pass def __assignToMesh(self, meshObj, action_name=None): shapeKeyAnim = self.__vmdFile.shapeKeyAnimation - logging.info('---- morph animations:%5d target: %s', len(shapeKeyAnim), meshObj.name) + logging.info("---- morph animations:%5d target: %s", len(shapeKeyAnim), meshObj.name) if len(shapeKeyAnim) < 1: return @@ -486,21 +489,22 @@ def __assignToMesh(self, meshObj, action_name=None): action = bpy.data.actions.new(name=action_name) mirror_map = _MirrorMapper(meshObj.data.shape_keys.key_blocks) if self.__mirror else {} - shapeKeyDict = {k:mirror_map.get(k, v) for k, v in meshObj.data.shape_keys.key_blocks.items()} + shapeKeyDict = {k: mirror_map.get(k, v) for k, v in meshObj.data.shape_keys.key_blocks.items()} + + from math import ceil, floor - from math import floor, ceil for name, keyFrames in shapeKeyAnim.items(): if name not in shapeKeyDict: - logging.warning('WARNING: not found shape key %s (%d frames)', name, len(keyFrames)) + logging.warning("WARNING: not found shape key %s (%d frames)", name, len(keyFrames)) continue - logging.info('(mesh) frames:%5d name: %s', len(keyFrames), name) + logging.info("(mesh) frames:%5d name: %s", len(keyFrames), name) shapeKey = shapeKeyDict[name] - fcurve = action.fcurves.new(data_path='key_blocks["%s"].value'%shapeKey.name) + fcurve = action.fcurves.new(data_path='key_blocks["%s"].value' % shapeKey.name) fcurve.keyframe_points.add(len(keyFrames)) - keyFrames.sort(key=lambda x:x.frame_number) + keyFrames.sort(key=lambda x: x.frame_number) for k, v in zip(keyFrames, fcurve.keyframe_points): - v.co = (k.frame_number+self.__frame_margin, k.weight) - v.interpolation = 'LINEAR' + v.co = (k.frame_number + self.__frame_margin, k.weight) + v.interpolation = "LINEAR" weights = tuple(i.weight for i in keyFrames) shapeKey.slider_min = min(shapeKey.slider_min, floor(min(weights))) shapeKey.slider_max = max(shapeKey.slider_max, ceil(max(weights))) @@ -509,30 +513,29 @@ def __assignToMesh(self, meshObj, action_name=None): def __assignToRoot(self, rootObj, action_name=None): propertyAnim = self.__vmdFile.propertyAnimation - logging.info('---- display animations:%5d target: %s', len(propertyAnim), rootObj.name) + logging.info("---- display animations:%5d target: %s", len(propertyAnim), rootObj.name) if len(propertyAnim) < 1: return action_name = action_name or rootObj.name action = bpy.data.actions.new(name=action_name) - logging.debug('(Display) list(frame, show): %s', [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim]) + logging.debug("(Display) list(frame, show): %s", [(keyFrame.frame_number, bool(keyFrame.visible)) for keyFrame in propertyAnim]) for keyFrame in propertyAnim: - self.__keyframe_insert(action.fcurves, 'mmd_root.show_meshes', keyFrame.frame_number+self.__frame_margin, float(keyFrame.visible)) + self.__keyframe_insert(action.fcurves, "mmd_root.show_meshes", keyFrame.frame_number + self.__frame_margin, float(keyFrame.visible)) self.__assign_action(rootObj, action) - @staticmethod def detectCameraChange(fcurve, threshold=10.0): frames = list(fcurve.keyframe_points) frameCount = len(frames) - frames.sort(key=lambda x:x.co[0]) + frames.sort(key=lambda x: x.co[0]) for i, f in enumerate(frames): - if i+1 < frameCount: - n = frames[i+1] + if i + 1 < frameCount: + n = frames[i + 1] if n.co[0] - f.co[0] <= 1.0 and abs(f.co[1] - n.co[1]) > threshold: - f.interpolation = 'CONSTANT' + f.interpolation = "CONSTANT" def __assignToCamera(self, cameraObj, action_name=None): mmdCameraInstance = MMDCamera.convertToMMDCamera(cameraObj, self.__scale) @@ -540,13 +543,13 @@ def __assignToCamera(self, cameraObj, action_name=None): cameraObj = mmdCameraInstance.camera() cameraAnim = self.__vmdFile.cameraAnimation - logging.info('(camera) frames:%5d name: %s', len(cameraAnim), mmdCamera.name) + logging.info("(camera) frames:%5d name: %s", len(cameraAnim), mmdCamera.name) if len(cameraAnim) < 1: return action_name = action_name or mmdCamera.name parent_action = bpy.data.actions.new(name=action_name) - distance_action = bpy.data.actions.new(name=action_name+'_dis') + distance_action = bpy.data.actions.new(name=action_name + "_dis") _loc = _rot = lambda i: i if self.__mirror: @@ -554,36 +557,36 @@ def __assignToCamera(self, cameraObj, action_name=None): fcurves = [] for i in range(3): - fcurves.append(parent_action.fcurves.new(data_path='location', index=i)) # x, y, z + fcurves.append(parent_action.fcurves.new(data_path="location", index=i)) # x, y, z for i in range(3): - fcurves.append(parent_action.fcurves.new(data_path='rotation_euler', index=i)) # rx, ry, rz - fcurves.append(parent_action.fcurves.new(data_path='mmd_camera.angle')) # fov - fcurves.append(parent_action.fcurves.new(data_path='mmd_camera.is_perspective')) # persp - fcurves.append(distance_action.fcurves.new(data_path='location', index=1)) # dis + fcurves.append(parent_action.fcurves.new(data_path="rotation_euler", index=i)) # rx, ry, rz + fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.angle")) # fov + fcurves.append(parent_action.fcurves.new(data_path="mmd_camera.is_perspective")) # persp + fcurves.append(distance_action.fcurves.new(data_path="location", index=1)) # dis for c in fcurves: c.keyframe_points.add(len(cameraAnim)) - prev_kps, indices = None, (0, 8, 4, 12, 12, 12, 16, 20) # x, z, y, rx, ry, rz, dis, fov - cameraAnim.sort(key=lambda x:x.frame_number) + prev_kps, indices = None, (0, 8, 4, 12, 12, 12, 16, 20) # x, z, y, rx, ry, rz, dis, fov + cameraAnim.sort(key=lambda x: x.frame_number) for k, x, y, z, rx, ry, rz, fov, persp, dis in zip(cameraAnim, *(c.keyframe_points for c in fcurves)): frame = k.frame_number + self.__frame_margin - x.co, z.co, y.co = ((frame, val*self.__scale) for val in _loc(k.location)) + x.co, z.co, y.co = ((frame, val * self.__scale) for val in _loc(k.location)) rx.co, rz.co, ry.co = ((frame, val) for val in _rot(k.rotation)) fov.co = (frame, math.radians(k.angle)) - dis.co = (frame, k.distance*self.__scale) + dis.co = (frame, k.distance * self.__scale) persp.co = (frame, k.persp) - persp.interpolation = 'CONSTANT' + persp.interpolation = "CONSTANT" curr_kps = (x, y, z, rx, ry, rz, dis, fov) if prev_kps is not None: interp = k.interp for idx, prev_kp, kp in zip(indices, prev_kps, curr_kps): - self.__setInterpolation(interp[idx:idx+4:2]+interp[idx+1:idx+4:2], prev_kp, kp) + self.__setInterpolation(interp[idx : idx + 4 : 2] + interp[idx + 1 : idx + 4 : 2], prev_kp, kp) prev_kps = curr_kps for fcurve in fcurves: self.__fixFcurveHandles(fcurve) - if fcurve.data_path == 'rotation_euler': + if fcurve.data_path == "rotation_euler": self.detectCameraChange(fcurve) self.__assign_action(mmdCamera, parent_action) @@ -593,13 +596,13 @@ def __assignToCamera(self, cameraObj, action_name=None): def detectLampChange(fcurve, threshold=0.1): frames = list(fcurve.keyframe_points) frameCount = len(frames) - frames.sort(key=lambda x:x.co[0]) + frames.sort(key=lambda x: x.co[0]) for i, f in enumerate(frames): - f.interpolation = 'LINEAR' - if i+1 < frameCount: - n = frames[i+1] + f.interpolation = "LINEAR" + if i + 1 < frameCount: + n = frames[i + 1] if n.co[0] - f.co[0] <= 1.0 and abs(f.co[1] - n.co[1]) > threshold: - f.interpolation = 'CONSTANT' + f.interpolation = "CONSTANT" def __assignToLamp(self, lampObj, action_name=None): mmdLampInstance = MMDLamp.convertToMMDLamp(lampObj, self.__scale) @@ -607,19 +610,19 @@ def __assignToLamp(self, lampObj, action_name=None): lampObj = mmdLampInstance.lamp() lampAnim = self.__vmdFile.lampAnimation - logging.info('(lamp) frames:%5d name: %s', len(lampAnim), mmdLamp.name) + logging.info("(lamp) frames:%5d name: %s", len(lampAnim), mmdLamp.name) if len(lampAnim) < 1: return action_name = action_name or mmdLamp.name - color_action = bpy.data.actions.new(name=action_name+'_color') - location_action = bpy.data.actions.new(name=action_name+'_loc') + color_action = bpy.data.actions.new(name=action_name + "_color") + location_action = bpy.data.actions.new(name=action_name + "_loc") _loc = _MirrorMapper.get_location if self.__mirror else lambda i: i for keyFrame in lampAnim: frame = keyFrame.frame_number + self.__frame_margin - self.__keyframe_insert(color_action.fcurves, 'color', frame, Vector(keyFrame.color)) - self.__keyframe_insert(location_action.fcurves, 'location', frame, Vector(_loc(keyFrame.direction)).xzy * -1) + self.__keyframe_insert(color_action.fcurves, "color", frame, Vector(keyFrame.color)) + self.__keyframe_insert(location_action.fcurves, "location", frame, Vector(_loc(keyFrame.direction)).xzy * -1) for fcurve in location_action.fcurves: self.detectLampChange(fcurve) @@ -627,7 +630,6 @@ def __assignToLamp(self, lampObj, action_name=None): self.__assign_action(lampObj.data, color_action) self.__assign_action(lampObj, location_action) - def assign(self, obj, action_name=None): if obj is None: return @@ -635,19 +637,18 @@ def assign(self, obj, action_name=None): action_name = os.path.splitext(os.path.basename(self.__vmdFile.filepath))[0] if MMDCamera.isMMDCamera(obj): - self.__assignToCamera(obj, action_name+'_camera') + self.__assignToCamera(obj, action_name + "_camera") elif MMDLamp.isMMDLamp(obj): - self.__assignToLamp(obj, action_name+'_lamp') - elif getattr(obj.data, 'shape_keys', None): - self.__assignToMesh(obj, action_name+'_facial') - elif obj.type == 'ARMATURE': - self.__assignToArmature(obj, action_name+'_bone') - elif obj.type == 'CAMERA' and self.__convert_mmd_camera: - self.__assignToCamera(obj, action_name+'_camera') - elif obj.type == 'LAMP' and self.__convert_mmd_lamp: - self.__assignToLamp(obj, action_name+'_lamp') - elif obj.mmd_type == 'ROOT': - self.__assignToRoot(obj, action_name+'_display') + self.__assignToLamp(obj, action_name + "_lamp") + elif getattr(obj.data, "shape_keys", None): + self.__assignToMesh(obj, action_name + "_facial") + elif obj.type == "ARMATURE": + self.__assignToArmature(obj, action_name + "_bone") + elif obj.type == "CAMERA" and self.__convert_mmd_camera: + self.__assignToCamera(obj, action_name + "_camera") + elif obj.type == "LAMP" and self.__convert_mmd_lamp: + self.__assignToLamp(obj, action_name + "_lamp") + elif obj.mmd_type == "ROOT": + self.__assignToRoot(obj, action_name + "_display") else: pass - diff --git a/mmd_tools/core/vpd/__init__.py b/mmd_tools/core/vpd/__init__.py index 08e71456..3df2518f 100644 --- a/mmd_tools/core/vpd/__init__.py +++ b/mmd_tools/core/vpd/__init__.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- +# Copyright 2017 MMD Tools authors +# This file is part of MMD Tools. + class InvalidFileError(Exception): pass + class VpdBone: def __init__(self, bone_name, location, rotation): self.bone_name = bone_name @@ -10,11 +14,12 @@ def __init__(self, bone_name, location, rotation): self.rotation = rotation if any(rotation) else [0, 0, 0, 1] def __repr__(self): - return ''%( + return "" % ( self.bone_name, str(self.location), str(self.rotation), - ) + ) + class VpdMorph: def __init__(self, morph_name, weight): @@ -22,62 +27,63 @@ def __init__(self, morph_name, weight): self.weight = weight def __repr__(self): - return ''%( + return "" % ( self.morph_name, self.weight, - ) + ) + class File: def __init__(self): - self.filepath = '' + self.filepath = "" self.osm_name = None self.bones = [] - self.morphs = [] # MikuMikuMoving + self.morphs = [] # MikuMikuMoving def __repr__(self): - return ''%( + return "" % ( self.filepath, self.osm_name, len(self.bones), len(self.morphs), - ) + ) def load(self, **args): - path = args['filepath'] + path = args["filepath"] - encoding = 'shift_jis' - with open(path, 'rt', encoding=encoding, errors='replace') as fin: + encoding = "shift_jis" + with open(path, "rt", encoding=encoding, errors="replace") as fin: self.filepath = path - if not fin.readline().startswith('Vocaloid Pose Data file'): + if not fin.readline().startswith("Vocaloid Pose Data file"): raise InvalidFileError fin.readline() - self.osm_name = fin.readline().split(';')[0].strip() - bone_counts = int(fin.readline().split(';')[0].strip()) + self.osm_name = fin.readline().split(";")[0].strip() + bone_counts = int(fin.readline().split(";")[0].strip()) fin.readline() for line in fin: - if line.startswith('Bone'): - bone_name = line.split('{')[-1].strip() + if line.startswith("Bone"): + bone_name = line.split("{")[-1].strip() - location = [float(x) for x in fin.readline().split(';')[0].strip().split(',')] + location = [float(x) for x in fin.readline().split(";")[0].strip().split(",")] if len(location) != 3: raise InvalidFileError - rotation = [float(x) for x in fin.readline().split(';')[0].strip().split(',')] + rotation = [float(x) for x in fin.readline().split(";")[0].strip().split(",")] if len(rotation) != 4: raise InvalidFileError - if not fin.readline().startswith('}'): + if not fin.readline().startswith("}"): raise InvalidFileError self.bones.append(VpdBone(bone_name, location, rotation)) - elif line.startswith('Morph'): - morph_name = line.split('{')[-1].strip() - weight = float(fin.readline().split(';')[0].strip()) + elif line.startswith("Morph"): + morph_name = line.split("{")[-1].strip() + weight = float(fin.readline().split(";")[0].strip()) - if not fin.readline().startswith('}'): + if not fin.readline().startswith("}"): raise InvalidFileError self.morphs.append(VpdMorph(morph_name, weight)) @@ -86,28 +92,27 @@ def load(self, **args): raise InvalidFileError def save(self, **args): - path = args.get('filepath', self.filepath) + path = args.get("filepath", self.filepath) - encoding = 'shift_jis' - with open(path, 'wt', encoding=encoding, errors='replace', newline='') as fout: + encoding = "shift_jis" + with open(path, "wt", encoding=encoding, errors="replace", newline="") as fout: self.filepath = path - fout.write('Vocaloid Pose Data file\r\n') + fout.write("Vocaloid Pose Data file\r\n") - fout.write('\r\n') - fout.write(u'%s;\t\t// 親ファイル名\r\n'%self.osm_name) - fout.write(u'%d;\t\t\t\t// 総ポーズボーン数\r\n'%len(self.bones)) - fout.write('\r\n') + fout.write("\r\n") + fout.write("%s;\t\t// 親ファイル名\r\n" % self.osm_name) + fout.write("%d;\t\t\t\t// 総ポーズボーン数\r\n" % len(self.bones)) + fout.write("\r\n") for i, b in enumerate(self.bones): - fout.write('Bone%d{%s\r\n'%(i, b.bone_name)) - fout.write(' %f,%f,%f;\t\t\t\t// trans x,y,z\r\n'%tuple(b.location)) - fout.write(' %f,%f,%f,%f;\t\t// Quaternion x,y,z,w\r\n'%tuple(b.rotation)) - fout.write('}\r\n') - fout.write('\r\n') + fout.write("Bone%d{%s\r\n" % (i, b.bone_name)) + fout.write(" %f,%f,%f;\t\t\t\t// trans x,y,z\r\n" % tuple(b.location)) + fout.write(" %f,%f,%f,%f;\t\t// Quaternion x,y,z,w\r\n" % tuple(b.rotation)) + fout.write("}\r\n") + fout.write("\r\n") for i, m in enumerate(self.morphs): - fout.write('Morph%d{%s\r\n'%(i, m.morph_name)) - fout.write(' %f;\t\t\t\t// weight\r\n'%m.weight) - fout.write('}\r\n') - fout.write('\r\n') - + fout.write("Morph%d{%s\r\n" % (i, m.morph_name)) + fout.write(" %f;\t\t\t\t// weight\r\n" % m.weight) + fout.write("}\r\n") + fout.write("\r\n") diff --git a/mmd_tools/core/vpd/exporter.py b/mmd_tools/core/vpd/exporter.py index b8b1bd32..ff093c9a 100644 --- a/mmd_tools/core/vpd/exporter.py +++ b/mmd_tools/core/vpd/exporter.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# Copyright 2017 MMD Tools authors +# This file is part of MMD Tools. import logging import os @@ -6,17 +8,16 @@ import bpy from mathutils import Matrix -from mmd_tools import bpyutils -from mmd_tools.core import vmd -from mmd_tools.core import vpd +from ..vmd import importer +from .. import vpd +from ...bpyutils import FnContext class VPDExporter: def __init__(self): self.__osm_name = None self.__scale = 1 - self.__bone_util_cls = vmd.importer.BoneConverter - + self.__bone_util_cls = importer.BoneConverter def __exportVPDFile(self, filepath, bones=None, morphs=None): vpd_file = vpd.File() @@ -26,12 +27,10 @@ def __exportVPDFile(self, filepath, bones=None, morphs=None): if morphs: vpd_file.morphs = morphs vpd_file.save(filepath=filepath) - logging.info('Exported %s', vpd_file) - + logging.info("Exported %s", vpd_file) def __getConverters(self, pose_bones): - return {b:self.__bone_util_cls(b, self.__scale, invert=True) for b in pose_bones} - + return {b: self.__bone_util_cls(b, self.__scale, invert=True) for b in pose_bones} def __exportBones(self, armObj, converters=None, matrix_basis_map=None): if armObj is None: @@ -59,23 +58,23 @@ def __exportBones(self, armObj, converters=None, matrix_basis_map=None): vpd_bones.append(vpd.VpdBone(bone_name, location, [x, y, z, w])) return vpd_bones - - def __exportPoseLib(self, armObj, pose_type, filepath, use_pose_mode=False): + def __exportPoseLib(self, armObj: bpy.types.Object, pose_type, filepath, use_pose_mode=False): if armObj is None: return None + # FIXME: armObj.pose_library is deprecated, use armObj.animation_data instead if armObj.pose_library is None: return None pose_bones = armObj.pose.bones converters = self.__getConverters(pose_bones) - backup = {b:(b.matrix_basis.copy(), b.bone.select) for b in pose_bones} + backup = {b: (b.matrix_basis.copy(), b.bone.select) for b in pose_bones} for b in pose_bones: b.bone.select = False matrix_basis_map = {} if use_pose_mode: - matrix_basis_map = {b:bak[0] for b, bak in backup.items()} + matrix_basis_map = {b: bak[0] for b, bak in backup.items()} def __export_index(index, filepath): for b in pose_bones: @@ -86,20 +85,19 @@ def __export_index(index, filepath): try: pose_markers = armObj.pose_library.pose_markers - with bpyutils.select_object(armObj): - bpy.ops.object.mode_set(mode='POSE') - if pose_type == 'ACTIVE': + with FnContext.temp_override_objects(FnContext.ensure_context(), active_object=armObj, selected_objects=[armObj]): + bpy.ops.object.mode_set(mode="POSE") + if pose_type == "ACTIVE": if 0 <= pose_markers.active_index < len(pose_markers): __export_index(pose_markers.active_index, filepath) else: folder = os.path.dirname(filepath) for i, m in enumerate(pose_markers): - __export_index(i, os.path.join(folder, m.name+'.vpd')) + __export_index(i, os.path.join(folder, m.name + ".vpd")) finally: for b, bak in backup.items(): b.matrix_basis, b.bone.select = bak - def __exportMorphs(self, meshObj): if meshObj is None: return None @@ -114,24 +112,22 @@ def __exportMorphs(self, meshObj): vpd_morphs.append(vpd.VpdMorph(i.name, i.value)) return vpd_morphs - def export(self, **args): - armature = args.get('armature', None) - mesh = args.get('mesh', None) - filepath = args.get('filepath', '') - self.__scale = args.get('scale', 1.0) - self.__osm_name = '%s.osm'%args.get('model_name', None) - - pose_type = args.get('pose_type', 'CURRENT') - if pose_type == 'CURRENT': + armature = args.get("armature", None) + mesh = args.get("mesh", None) + filepath = args.get("filepath", "") + self.__scale = args.get("scale", 1.0) + self.__osm_name = "%s.osm" % args.get("model_name", None) + + pose_type = args.get("pose_type", "CURRENT") + if pose_type == "CURRENT": vpd_bones = self.__exportBones(armature) vpd_morphs = self.__exportMorphs(mesh) self.__exportVPDFile(filepath, vpd_bones, vpd_morphs) - elif pose_type in {'ACTIVE', 'ALL'}: - use_pose_mode = args.get('use_pose_mode', False) + elif pose_type in {"ACTIVE", "ALL"}: + use_pose_mode = args.get("use_pose_mode", False) if use_pose_mode: - self.__bone_util_cls = vmd.importer.BoneConverterPoseMode + self.__bone_util_cls = importer.BoneConverterPoseMode self.__exportPoseLib(armature, pose_type, filepath, use_pose_mode) else: - raise Exception('Unknown pose type "%s"', pose_type) - + raise ValueError('Unknown pose type "{pose_type}"') diff --git a/mmd_tools/core/vpd/importer.py b/mmd_tools/core/vpd/importer.py index 1e9be644..ff048bf7 100644 --- a/mmd_tools/core/vpd/importer.py +++ b/mmd_tools/core/vpd/importer.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- +# Copyright 2017 MMD Tools authors +# This file is part of MMD Tools. import logging import bpy from mathutils import Matrix -from mmd_tools import bpyutils -from mmd_tools.core import vmd -from mmd_tools.core import vpd +from ..vmd import importer +from .. import vpd +from ...bpyutils import FnContext class VPDImporter: @@ -18,42 +20,39 @@ def __init__(self, filepath, scale=1.0, bone_mapper=None, use_pose_mode=False): self.__scale = scale self.__bone_mapper = bone_mapper if use_pose_mode: - self.__bone_util_cls = vmd.importer.BoneConverterPoseMode + self.__bone_util_cls = importer.BoneConverterPoseMode self.__assignToArmature = self.__assignToArmaturePoseMode else: - self.__bone_util_cls = vmd.importer.BoneConverter + self.__bone_util_cls = importer.BoneConverter self.__assignToArmature = self.__assignToArmatureSimple - logging.info('Loaded %s', self.__vpd_file) - + logging.info("Loaded %s", self.__vpd_file) def __assignToArmaturePoseMode(self, armObj): - pose_orig = {b:b.matrix_basis.copy() for b in armObj.pose.bones} + pose_orig = {b: b.matrix_basis.copy() for b in armObj.pose.bones} try: self.__assignToArmatureSimple(armObj, reset_transform=False) finally: for bone, matrix_basis in pose_orig.items(): bone.matrix_basis = matrix_basis - - def __assignToArmatureSimple(self, armObj, reset_transform=True): + def __assignToArmatureSimple(self, armObj: bpy.types.Object, reset_transform=True): logging.info(' - assigning to armature "%s"', armObj.name) pose_bones = armObj.pose.bones if self.__bone_mapper: pose_bones = self.__bone_mapper(armObj) - matmul = bpyutils.matmul pose_data = {} for b in self.__vpd_file.bones: bone = pose_bones.get(b.bone_name, None) if bone is None: - logging.warning(' * Bone not found: %s', b.bone_name) + logging.warning(" * Bone not found: %s", b.bone_name) continue converter = self.__bone_util_cls(bone, self.__scale) loc = converter.convert_location(b.location) rot = converter.convert_rotation(b.rotation) - assert(bone not in pose_data) - pose_data[bone] = matmul(Matrix.Translation(loc), rot.to_matrix().to_4x4()) + assert bone not in pose_data + pose_data[bone] = Matrix.Translation(loc) @ rot.to_matrix().to_4x4() for bone in armObj.pose.bones: vpd_pose = pose_data.get(bone, None) @@ -63,13 +62,14 @@ def __assignToArmatureSimple(self, armObj, reset_transform=True): elif reset_transform: bone.matrix_basis.identity() + # FIXME: armObj.pose_library is None when the armature is not in pose mode if armObj.pose_library is None: - armObj.pose_library = bpy.data.actions.new(name='PoseLib') + armObj.pose_library = bpy.data.actions.new(name="PoseLib") frames = [m.frame for m in armObj.pose_library.pose_markers] frame_max = max(frames) if len(frames) else 0 - bpy.ops.poselib.pose_add(frame=frame_max+1, name=self.__pose_name) - + # FIXME: poselib.pose_add is deprecated, use animation_data instead + bpy.ops.poselib.pose_add(frame=frame_max + 1, name=self.__pose_name) def __assignToMesh(self, meshObj): if meshObj.data.shape_keys is None: @@ -84,20 +84,22 @@ def __assignToMesh(self, meshObj): for m in self.__vpd_file.morphs: shape_key = key_blocks.get(m.morph_name, None) if shape_key is None: - logging.warning(' * Shape key not found: %s', m.morph_name) + logging.warning(" * Shape key not found: %s", m.morph_name) continue shape_key.value = m.weight - def assign(self, obj): if obj is None: return - if obj.type == 'ARMATURE': - with bpyutils.select_object(obj): - bpy.ops.object.mode_set(mode='POSE') + if obj.type == "ARMATURE": + with FnContext.temp_override_objects( + FnContext.ensure_context(), + active_object=obj, + selected_objects=[obj], + ): + bpy.ops.object.mode_set(mode="POSE") self.__assignToArmature(obj) - elif obj.type == 'MESH': + elif obj.type == "MESH": self.__assignToMesh(obj) else: pass - diff --git a/mmd_tools/cycles_converter.py b/mmd_tools/cycles_converter.py index 92834257..ff2d891b 100644 --- a/mmd_tools/cycles_converter.py +++ b/mmd_tools/cycles_converter.py @@ -1,105 +1,116 @@ # -*- coding: utf-8 -*- +# Copyright 2012 MMD Tools authors +# This file is part of MMD Tools. + +from typing import Iterable, Optional + import bpy -import logging -from mmd_tools.core.shader import _NodeGroupUtils + +from .core.shader import _NodeGroupUtils + def __switchToCyclesRenderEngine(): - if bpy.context.scene.render.engine != 'CYCLES': - bpy.context.scene.render.engine = 'CYCLES' + if bpy.context.scene.render.engine != "CYCLES": + bpy.context.scene.render.engine = "CYCLES" + def __exposeNodeTreeInput(in_socket, name, default_value, node_input, shader): _NodeGroupUtils(shader).new_input_socket(name, in_socket, default_value) + def __exposeNodeTreeOutput(out_socket, name, node_output, shader): _NodeGroupUtils(shader).new_output_socket(name, out_socket) + def __getMaterialOutput(nodes, bl_idname): o = next((n for n in nodes if n.bl_idname == bl_idname and n.is_active_output), None) or nodes.new(bl_idname) o.is_active_output = True return o + def create_MMDAlphaShader(): __switchToCyclesRenderEngine() - if 'MMDAlphaShader' in bpy.data.node_groups: - return bpy.data.node_groups['MMDAlphaShader'] + if "MMDAlphaShader" in bpy.data.node_groups: + return bpy.data.node_groups["MMDAlphaShader"] - shader = bpy.data.node_groups.new(name='MMDAlphaShader', type='ShaderNodeTree') + shader = bpy.data.node_groups.new(name="MMDAlphaShader", type="ShaderNodeTree") - node_input = shader.nodes.new('NodeGroupInput') - node_output = shader.nodes.new('NodeGroupOutput') + node_input = shader.nodes.new("NodeGroupInput") + node_output = shader.nodes.new("NodeGroupOutput") node_output.location.x += 250 node_input.location.x -= 500 - trans = shader.nodes.new('ShaderNodeBsdfTransparent') + trans = shader.nodes.new("ShaderNodeBsdfTransparent") trans.location.x -= 250 trans.location.y += 150 - mix = shader.nodes.new('ShaderNodeMixShader') + mix = shader.nodes.new("ShaderNodeMixShader") - shader.links.new(mix.inputs[1], trans.outputs['BSDF']) + shader.links.new(mix.inputs[1], trans.outputs["BSDF"]) - __exposeNodeTreeInput(mix.inputs[2], 'Shader', None, node_input, shader) - __exposeNodeTreeInput(mix.inputs['Fac'], 'Alpha', 1.0, node_input, shader) - __exposeNodeTreeOutput(mix.outputs['Shader'], 'Shader', node_output, shader) + __exposeNodeTreeInput(mix.inputs[2], "Shader", None, node_input, shader) + __exposeNodeTreeInput(mix.inputs["Fac"], "Alpha", 1.0, node_input, shader) + __exposeNodeTreeOutput(mix.outputs["Shader"], "Shader", node_output, shader) return shader + def create_MMDBasicShader(): __switchToCyclesRenderEngine() - if 'MMDBasicShader' in bpy.data.node_groups: - return bpy.data.node_groups['MMDBasicShader'] + if "MMDBasicShader" in bpy.data.node_groups: + return bpy.data.node_groups["MMDBasicShader"] - shader = bpy.data.node_groups.new(name='MMDBasicShader', type='ShaderNodeTree') + shader: bpy.types.ShaderNodeTree = bpy.data.node_groups.new(name="MMDBasicShader", type="ShaderNodeTree") - node_input = shader.nodes.new('NodeGroupInput') - node_output = shader.nodes.new('NodeGroupOutput') + node_input: bpy.types.NodeGroupInput = shader.nodes.new("NodeGroupInput") + node_output: bpy.types.NodeGroupOutput = shader.nodes.new("NodeGroupOutput") node_output.location.x += 250 node_input.location.x -= 500 - dif = shader.nodes.new('ShaderNodeBsdfDiffuse') + dif: bpy.types.ShaderNodeBsdfDiffuse = shader.nodes.new("ShaderNodeBsdfDiffuse") dif.location.x -= 250 dif.location.y += 150 - glo = shader.nodes.new('ShaderNodeBsdfGlossy') + glo: bpy.types.ShaderNodeBsdfAnisotropic = shader.nodes.new("ShaderNodeBsdfAnisotropic") glo.location.x -= 250 glo.location.y -= 150 - mix = shader.nodes.new('ShaderNodeMixShader') - shader.links.new(mix.inputs[1], dif.outputs['BSDF']) - shader.links.new(mix.inputs[2], glo.outputs['BSDF']) + mix: bpy.types.ShaderNodeMixShader = shader.nodes.new("ShaderNodeMixShader") + shader.links.new(mix.inputs[1], dif.outputs["BSDF"]) + shader.links.new(mix.inputs[2], glo.outputs["BSDF"]) - __exposeNodeTreeInput(dif.inputs['Color'], 'diffuse', [1.0, 1.0, 1.0, 1.0], node_input, shader) - __exposeNodeTreeInput(glo.inputs['Color'], 'glossy', [1.0, 1.0, 1.0, 1.0], node_input, shader) - __exposeNodeTreeInput(glo.inputs['Roughness'], 'glossy_rough', 0.0, node_input, shader) - __exposeNodeTreeInput(mix.inputs['Fac'], 'reflection', 0.02, node_input, shader) - __exposeNodeTreeOutput(mix.outputs['Shader'], 'shader', node_output, shader) + __exposeNodeTreeInput(dif.inputs["Color"], "diffuse", [1.0, 1.0, 1.0, 1.0], node_input, shader) + __exposeNodeTreeInput(glo.inputs["Color"], "glossy", [1.0, 1.0, 1.0, 1.0], node_input, shader) + __exposeNodeTreeInput(glo.inputs["Roughness"], "glossy_rough", 0.0, node_input, shader) + __exposeNodeTreeInput(mix.inputs["Fac"], "reflection", 0.02, node_input, shader) + __exposeNodeTreeOutput(mix.outputs["Shader"], "shader", node_output, shader) return shader -def __enum_linked_nodes(node): + +def __enum_linked_nodes(node: bpy.types.Node) -> Iterable[bpy.types.Node]: yield node if node.parent: yield node.parent for n in set(l.from_node for i in node.inputs for l in i.links): yield from __enum_linked_nodes(n) -def __cleanNodeTree(material): - nodes = getattr(material.node_tree, 'nodes', ()) + +def __cleanNodeTree(material: bpy.types.Material): + nodes = material.node_tree.nodes node_names = set(n.name for n in nodes) - for o in (n for n in nodes if n.bl_idname in {'ShaderNodeOutput', 'ShaderNodeOutputMaterial'}): + for o in (n for n in nodes if n.bl_idname in {"ShaderNodeOutput", "ShaderNodeOutputMaterial"}): if any(i.is_linked for i in o.inputs): node_names -= set(linked.name for linked in __enum_linked_nodes(o)) for name in node_names: nodes.remove(nodes[name]) -def is_principled_bsdf_supported(): - return hasattr(bpy.types, 'ShaderNodeBsdfPrincipled') -def convertToCyclesShader(obj, use_principled=False, clean_nodes=False, subsurface=0.001): +def convertToCyclesShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001): __switchToCyclesRenderEngine() convertToBlenderShader(obj, use_principled, clean_nodes, subsurface) -def convertToBlenderShader(obj, use_principled=False, clean_nodes=False, subsurface=0.001): - use_principled = (use_principled and is_principled_bsdf_supported()) + +def convertToBlenderShader(obj: bpy.types.Object, use_principled=False, clean_nodes=False, subsurface=0.001): for i in obj.material_slots: if not i.material: continue @@ -111,202 +122,86 @@ def convertToBlenderShader(obj, use_principled=False, clean_nodes=False, subsurf if clean_nodes: __cleanNodeTree(i.material) -def __convertToMMDBasicShader(material): + +def __convertToMMDBasicShader(material: bpy.types.Material): + # TODO: test me mmd_basic_shader_grp = create_MMDBasicShader() mmd_alpha_shader_grp = create_MMDAlphaShader() - if not any(filter(lambda x: isinstance(x, bpy.types.ShaderNodeGroup) and x.node_tree.name in {'MMDBasicShader', 'MMDAlphaShader'}, material.node_tree.nodes)): + if not any(filter(lambda x: isinstance(x, bpy.types.ShaderNodeGroup) and x.node_tree.name in {"MMDBasicShader", "MMDAlphaShader"}, material.node_tree.nodes)): # Add nodes for Cycles Render - shader = material.node_tree.nodes.new('ShaderNodeGroup') + shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") shader.node_tree = mmd_basic_shader_grp shader.inputs[0].default_value[:3] = material.diffuse_color[:3] shader.inputs[1].default_value[:3] = material.specular_color[:3] - shader.inputs['glossy_rough'].default_value = 1.0/getattr(material, 'specular_hardness', 50) + shader.inputs["glossy_rough"].default_value = 1.0 / getattr(material, "specular_hardness", 50) outplug = shader.outputs[0] - node_tex, node_alpha = None, None location = shader.location.copy() location.x -= 1000 - reuse_nodes = {} - for j in getattr(material, 'texture_slots', ()): - if j and j.use and isinstance(j.texture, bpy.types.ImageTexture) and getattr(j.texture.image, 'depth', 0): - if not (j.use_map_color_diffuse or j.use_map_alpha): - continue - if j.texture_coords not in {'UV', 'NORMAL'}: - continue - - uv_tag = j.uv_layer if j.texture_coords == 'UV' else '' - key_node_tex = j.texture.name + j.texture_coords + uv_tag - tex_img = reuse_nodes.get(key_node_tex) - if tex_img is None: - tex_img = material.node_tree.nodes.new('ShaderNodeTexImage') - tex_img.location = location - tex_img.image = j.texture.image - if j.texture_coords == 'NORMAL' and j.blend_type == 'ADD': - tex_img.color_space = 'NONE' - reuse_nodes[key_node_tex] = tex_img - location.x += 250 - location.y -= 150 - - key_node_vec = j.texture_coords + uv_tag - plug_vector = reuse_nodes.get(key_node_vec) - if plug_vector is None: - plug_vector = 0 - if j.texture_coords == 'UV': - if j.uv_layer and hasattr(bpy.types, 'ShaderNodeUVMap'): - node_vector = material.node_tree.nodes.new('ShaderNodeUVMap') - node_vector.uv_map = j.uv_layer - node_vector.location.x = shader.location.x - 1500 - node_vector.location.y = tex_img.location.y - 50 - plug_vector = node_vector.outputs[0] - elif j.uv_layer: - node_vector = material.node_tree.nodes.new('ShaderNodeAttribute') - node_vector.attribute_name = j.uv_layer - node_vector.location.x = shader.location.x - 1500 - node_vector.location.y = tex_img.location.y - 50 - plug_vector = node_vector.outputs[1] - - elif j.texture_coords == 'NORMAL': - tex_coord = material.node_tree.nodes.new('ShaderNodeTexCoord') - tex_coord.location.x = shader.location.x - 1900 - tex_coord.location.y = shader.location.y - 600 - - vec_trans = material.node_tree.nodes.new('ShaderNodeVectorTransform') - vec_trans.vector_type = 'NORMAL' - vec_trans.convert_from = 'OBJECT' - vec_trans.convert_to = 'CAMERA' - vec_trans.location.x = tex_coord.location.x + 200 - vec_trans.location.y = tex_coord.location.y - material.node_tree.links.new(vec_trans.inputs[0], tex_coord.outputs['Normal']) - - node_vector = material.node_tree.nodes.new('ShaderNodeMapping') - node_vector.vector_type = 'POINT' - node_vector.translation = (0.5, 0.5, 0.0) - node_vector.scale = (0.5, 0.5, 1.0) - node_vector.location.x = vec_trans.location.x + 200 - node_vector.location.y = vec_trans.location.y - material.node_tree.links.new(node_vector.inputs[0], vec_trans.outputs[0]) - plug_vector = node_vector.outputs[0] - - reuse_nodes[key_node_vec] = plug_vector - - if plug_vector: - material.node_tree.links.new(tex_img.inputs[0], plug_vector) - - if j.use_map_color_diffuse: - if node_tex is None and tuple(material.diffuse_color) == (1.0, 1.0, 1.0): - node_tex = tex_img - else: - node_tex_last = node_tex - node_tex = material.node_tree.nodes.new('ShaderNodeMixRGB') - try: - node_tex.blend_type = j.blend_type - except TypeError as ex: - logging.exception(node_tex) - node_tex.inputs[0].default_value = 1.0 - node_tex.inputs[1].default_value = shader.inputs[0].default_value - node_tex.location.x = tex_img.location.x + 250 - node_tex.location.y = tex_img.location.y + 150 - material.node_tree.links.new(node_tex.inputs[2], tex_img.outputs['Color']) - if node_tex_last: - material.node_tree.links.new(node_tex.inputs[1], node_tex_last.outputs[0]) - - if j.use_map_alpha: - if node_alpha is None and material.alpha == 1.0: - node_alpha = tex_img - else: - node_alpha_last = node_alpha - node_alpha = material.node_tree.nodes.new('ShaderNodeMath') - try: - node_alpha.operation = j.blend_type - except TypeError as ex: - logging.exception(node_alpha) - node_alpha.inputs[0].default_value = material.alpha - node_alpha.location.x = tex_img.location.x + 250 - node_alpha.location.y = tex_img.location.y - 500 - material.node_tree.links.new(node_alpha.inputs[1], tex_img.outputs['Alpha']) - if node_alpha_last: - material.node_tree.links.new(node_alpha.inputs[0], node_alpha_last.outputs[len(node_alpha_last.outputs)-1]) - - if node_tex: - material.node_tree.links.new(shader.inputs[0], node_tex.outputs[0]) alpha_value = 1.0 - if hasattr(material, 'alpha'): - alpha_value = material.alpha - elif len(material.diffuse_color) > 3: + if len(material.diffuse_color) > 3: alpha_value = material.diffuse_color[3] - if node_alpha or alpha_value < 1.0: - alpha_shader = material.node_tree.nodes.new('ShaderNodeGroup') + if alpha_value < 1.0: + alpha_shader: bpy.types.ShaderNodeGroup = material.node_tree.nodes.new("ShaderNodeGroup") alpha_shader.location.x = shader.location.x + 250 alpha_shader.location.y = shader.location.y - 150 alpha_shader.node_tree = mmd_alpha_shader_grp alpha_shader.inputs[1].default_value = alpha_value material.node_tree.links.new(alpha_shader.inputs[0], outplug) outplug = alpha_shader.outputs[0] - if node_alpha: - material.node_tree.links.new(alpha_shader.inputs[1], node_alpha.outputs[len(node_alpha.outputs)-1]) - material_output = __getMaterialOutput(material.node_tree.nodes, 'ShaderNodeOutputMaterial') - material.node_tree.links.new(material_output.inputs['Surface'], outplug) + material_output: bpy.types.ShaderNodeOutputMaterial = __getMaterialOutput(material.node_tree.nodes, "ShaderNodeOutputMaterial") + material.node_tree.links.new(material_output.inputs["Surface"], outplug) material_output.location.x = shader.location.x + 500 material_output.location.y = shader.location.y - 150 - if not hasattr(bpy.types, 'ShaderNodeMaterial'): - return - # Add necessary nodes to retain Blender Render functionality - out_node = __getMaterialOutput(material.node_tree.nodes, 'ShaderNodeOutput') - mat_node = material.node_tree.nodes.new('ShaderNodeMaterial') - mat_node.material = material - mat_node.location.x = shader.location.x - 250 - mat_node.location.y = shader.location.y + 500 - out_node.location.x = mat_node.location.x + 750 - out_node.location.y = mat_node.location.y - material.node_tree.links.new(out_node.inputs['Color'], mat_node.outputs['Color']) - material.node_tree.links.new(out_node.inputs['Alpha'], mat_node.outputs['Alpha']) - -def __convertToPrincipledBsdf(material, subsurface): + +def __convertToPrincipledBsdf(material: bpy.types.Material, subsurface: float): node_names = set() - for s in tuple(n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)): - if s.node_tree.name == 'MMDBasicShader': + for s in (n for n in material.node_tree.nodes if isinstance(n, bpy.types.ShaderNodeGroup)): + if s.node_tree.name == "MMDBasicShader": + l: bpy.types.NodeLink for l in s.outputs[0].links: to_node = l.to_node # assuming there is no bpy.types.NodeReroute between MMDBasicShader and MMDAlphaShader - if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == 'MMDAlphaShader': - __switchToPrincipledBsdf(material.node_tree, s, to_node, subsurface=subsurface) + if isinstance(to_node, bpy.types.ShaderNodeGroup) and to_node.node_tree.name == "MMDAlphaShader": + __switchToPrincipledBsdf(material.node_tree, s, subsurface, node_alpha=to_node) node_names.add(to_node.name) else: - __switchToPrincipledBsdf(material.node_tree, s, subsurface=subsurface) + __switchToPrincipledBsdf(material.node_tree, s, subsurface) node_names.add(s.name) - elif s.node_tree.name == 'MMDShaderDev': - __switchToPrincipledBsdf(material.node_tree, s, subsurface=subsurface) + elif s.node_tree.name == "MMDShaderDev": + __switchToPrincipledBsdf(material.node_tree, s, subsurface) node_names.add(s.name) # remove MMD shader nodes nodes = material.node_tree.nodes for name in node_names: nodes.remove(nodes[name]) -def __switchToPrincipledBsdf(node_tree, node_basic, node_alpha=None, subsurface=0): - shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new('ShaderNodeBsdfPrincipled') + +def __switchToPrincipledBsdf(node_tree: bpy.types.NodeTree, node_basic: bpy.types.ShaderNodeGroup, subsurface: float, node_alpha: Optional[bpy.types.ShaderNodeGroup] = None): + shader: bpy.types.ShaderNodeBsdfPrincipled = node_tree.nodes.new("ShaderNodeBsdfPrincipled") shader.parent = node_basic.parent shader.location.x = node_basic.location.x shader.location.y = node_basic.location.y - alpha_socket_name = 'Alpha' - if node_basic.node_tree.name == 'MMDShaderDev': - node_alpha, alpha_socket_name = node_basic, 'Base Alpha' - if 'Base Tex' in node_basic.inputs and node_basic.inputs['Base Tex'].is_linked: - node_tree.links.new(node_basic.inputs['Base Tex'].links[0].from_socket, shader.inputs['Base Color']) - elif 'Diffuse Color' in node_basic.inputs: - shader.inputs['Base Color'].default_value[:3] = node_basic.inputs['Diffuse Color'].default_value[:3] - elif 'diffuse' in node_basic.inputs: - shader.inputs['Base Color'].default_value[:3] = node_basic.inputs['diffuse'].default_value[:3] - if node_basic.inputs['diffuse'].is_linked: - node_tree.links.new(node_basic.inputs['diffuse'].links[0].from_socket, shader.inputs['Base Color']) - - shader.inputs['IOR'].default_value = 1.0 - shader.inputs['Subsurface'].default_value = subsurface + alpha_socket_name = "Alpha" + if node_basic.node_tree.name == "MMDShaderDev": + node_alpha, alpha_socket_name = node_basic, "Base Alpha" + if "Base Tex" in node_basic.inputs and node_basic.inputs["Base Tex"].is_linked: + node_tree.links.new(node_basic.inputs["Base Tex"].links[0].from_socket, shader.inputs["Base Color"]) + elif "Diffuse Color" in node_basic.inputs: + shader.inputs["Base Color"].default_value[:3] = node_basic.inputs["Diffuse Color"].default_value[:3] + elif "diffuse" in node_basic.inputs: + shader.inputs["Base Color"].default_value[:3] = node_basic.inputs["diffuse"].default_value[:3] + if node_basic.inputs["diffuse"].is_linked: + node_tree.links.new(node_basic.inputs["diffuse"].links[0].from_socket, shader.inputs["Base Color"]) + + shader.inputs["IOR"].default_value = 1.0 + shader.inputs["Subsurface Weight"].default_value = subsurface output_links = node_basic.outputs[0].links if node_alpha: @@ -315,22 +210,22 @@ def __switchToPrincipledBsdf(node_tree, node_basic, node_alpha=None, subsurface= shader.location.x = node_alpha.location.x if alpha_socket_name in node_alpha.inputs: - if 'Alpha' in shader.inputs: - shader.inputs['Alpha'].default_value = node_alpha.inputs[alpha_socket_name].default_value + if "Alpha" in shader.inputs: + shader.inputs["Alpha"].default_value = node_alpha.inputs[alpha_socket_name].default_value if node_alpha.inputs[alpha_socket_name].is_linked: - node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, shader.inputs['Alpha']) + node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, shader.inputs["Alpha"]) else: - shader.inputs['Transmission'].default_value = 1 - node_alpha.inputs[alpha_socket_name].default_value + shader.inputs["Transmission"].default_value = 1 - node_alpha.inputs[alpha_socket_name].default_value if node_alpha.inputs[alpha_socket_name].is_linked: - node_invert = node_tree.nodes.new('ShaderNodeMath') + node_invert = node_tree.nodes.new("ShaderNodeMath") node_invert.parent = shader.parent node_invert.location.x = node_alpha.location.x - 250 node_invert.location.y = node_alpha.location.y - 300 - node_invert.operation = 'SUBTRACT' + node_invert.operation = "SUBTRACT" node_invert.use_clamp = True node_invert.inputs[0].default_value = 1 node_tree.links.new(node_alpha.inputs[alpha_socket_name].links[0].from_socket, node_invert.inputs[1]) - node_tree.links.new(node_invert.outputs[0], shader.inputs['Transmission']) + node_tree.links.new(node_invert.outputs[0], shader.inputs["Transmission"]) for l in output_links: node_tree.links.new(shader.outputs[0], l.to_socket) diff --git a/mmd_tools/handlers.py b/mmd_tools/handlers.py new file mode 100644 index 00000000..58250a86 --- /dev/null +++ b/mmd_tools/handlers.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 MMD Tools authors +# This file is part of MMD Tools. + + +import bpy + + +class MMDHanders: + @staticmethod + @bpy.app.handlers.persistent + def load_hander(_): + # pylint: disable=import-outside-toplevel + from .core.sdef import FnSDEF + + FnSDEF.clear_cache() + FnSDEF.register_driver_function() + + from .core.material import MigrationFnMaterial + + MigrationFnMaterial.update_mmd_shader() + + from .core.morph import MigrationFnMorph + + MigrationFnMorph.update_mmd_morph() + + from .core.camera import MigrationFnCamera + + MigrationFnCamera.update_mmd_camera() + + from .core.model import MigrationFnModel + + MigrationFnModel.update_mmd_ik_loop_factor() + MigrationFnModel.update_mmd_tools_version() + + @staticmethod + @bpy.app.handlers.persistent + def save_pre_handler(_): + # pylint: disable=import-outside-toplevel + from .core.morph import MigrationFnMorph + + MigrationFnMorph.compatible_with_old_version_mmd_tools() + + @staticmethod + def register(): + bpy.app.handlers.load_post.append(MMDHanders.load_hander) + bpy.app.handlers.save_pre.append(MMDHanders.save_pre_handler) + + @staticmethod + def unregister(): + bpy.app.handlers.save_pre.remove(MMDHanders.save_pre_handler) + bpy.app.handlers.load_post.remove(MMDHanders.load_hander) diff --git a/mmd_tools/m17n.py b/mmd_tools/m17n.py index ef784125..b71b1bf5 100644 --- a/mmd_tools/m17n.py +++ b/mmd_tools/m17n.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# Copyright 2021 MMD Tools authors +# This file is part of MMD Tools. translation_dict = { "ja_JP": { @@ -402,7 +404,7 @@ ("*", "Sphere Tex"): "スフィアテクスチャ", ("*", "Sphere Tex Add"): "スフィアテクスチャ加算", }, - "zh_CN": { + "zh_HANS": { # Preferences ("*", "View3D > Sidebar > MMD Tools Panel"): "3D视图 > 侧栏 > MMD Tools面板", ("*", "Utility tools for MMD model editing. (UuuNyaa's forked version)"): "用于MMD模型编辑的实用工具。(UuuNyaa的分叉版本)", diff --git a/mmd_tools/menus.py b/mmd_tools/menus.py new file mode 100644 index 00000000..0e16b502 --- /dev/null +++ b/mmd_tools/menus.py @@ -0,0 +1,139 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 MMD Tools authors +# This file is part of MMD Tools. + +import bpy +from .operators import fileio +from .operators import model +from .operators import misc +from .operators import rigid_body +from .operators import view + + +class MMDFileImportMenu(bpy.types.Menu): + bl_idname = "TOPBAR_MT_mmd_file_import" + bl_label = "MMD UuuNyaa" + + def draw(self, _): + pass + + @staticmethod + def draw_menu(this: bpy.types.Menu, _): + this.layout.operator(fileio.ImportPmx.bl_idname, text="MikuMikuDance Model (.pmd, .pmx)", icon="OUTLINER_OB_ARMATURE") + this.layout.operator(fileio.ImportVmd.bl_idname, text="MikuMikuDance Motion (.vmd)", icon="ANIM") + this.layout.operator(fileio.ImportVpd.bl_idname, text="Vocaloid Pose Data (.vpd)", icon="POSE_HLT") + + @staticmethod + def register(): + bpy.types.TOPBAR_MT_file_import.append(MMDFileImportMenu.draw_menu) + + @staticmethod + def unregister(): + bpy.types.TOPBAR_MT_file_import.remove(MMDFileImportMenu.draw_menu) + + +class MMDFileExportMenu(bpy.types.Menu): + bl_idname = "TOPBAR_MT_mmd_file_export" + bl_label = "MMD UuuNyaa" + + def draw(self, _): + pass + + @staticmethod + def draw_menu(this: bpy.types.Menu, _): + this.layout.operator(fileio.ExportPmx.bl_idname, text="MikuMikuDance Model (.pmx)", icon="OUTLINER_OB_ARMATURE") + this.layout.operator(fileio.ExportVmd.bl_idname, text="MikuMikuDance Motion (.vmd)", icon="ANIM") + this.layout.operator(fileio.ExportVpd.bl_idname, text="Vocaloid Pose Data (.vpd)", icon="POSE_HLT") + + @staticmethod + def register(): + bpy.types.TOPBAR_MT_file_export.append(MMDFileExportMenu.draw_menu) + + @staticmethod + def unregister(): + bpy.types.TOPBAR_MT_file_export.remove(MMDFileExportMenu.draw_menu) + + +class MMDArmatureAddMenu(bpy.types.Menu): + bl_idname = "VIEW3D_MT_mmd_armature_add" + bl_label = "MMD UuuNyaa" + + def draw(self, _): + pass + + @staticmethod + def draw_menu(this: bpy.types.Menu, _): + this.layout.operator(model.CreateMMDModelRoot.bl_idname, text="Create MMD Model", icon="OUTLINER_OB_ARMATURE") + + @staticmethod + def register(): + bpy.types.VIEW3D_MT_armature_add.append(MMDArmatureAddMenu.draw_menu) + + @staticmethod + def unregister(): + bpy.types.VIEW3D_MT_armature_add.remove(MMDArmatureAddMenu.draw_menu) + + +class MMDObjectMenu(bpy.types.Menu): + bl_idname = "VIEW3D_MT_mmd_object" + bl_label = "MMD UuuNyaa" + + def draw(self, _): + pass + + @staticmethod + def draw_menu(this: bpy.types.Menu, _): + this.layout.separator() + this.layout.operator(misc.CleanShapeKeys.bl_idname, text="Clean Shape Keys", icon="SHAPEKEY_DATA") + + @staticmethod + def register(): + bpy.types.VIEW3D_MT_object.append(MMDObjectMenu.draw_menu) + + @staticmethod + def unregister(): + bpy.types.VIEW3D_MT_object.remove(MMDObjectMenu.draw_menu) + + +class MMDSelectObjectMenu(bpy.types.Menu): + bl_idname = "VIEW3D_MT_mmd_select_object" + bl_label = "MMD UuuNyaa" + + def draw(self, _): + pass + + @staticmethod + def draw_menu(this: bpy.types.Menu, _): + this.layout.separator() + this.layout.operator_context = "EXEC_DEFAULT" + this.layout.operator(rigid_body.SelectRigidBody.bl_idname, text="Select MMD Rigid Body").properties = {"collision_group_number", "shape"} + + @staticmethod + def register(): + bpy.types.VIEW3D_MT_select_object.append(MMDSelectObjectMenu.draw_menu) + + @staticmethod + def unregister(): + bpy.types.VIEW3D_MT_select_object.remove(MMDSelectObjectMenu.draw_menu) + + +class MMDPoseMenu(bpy.types.Menu): + bl_idname = "VIEW3D_MT_mmd_pose" + bl_label = "MMD UuuNyaa" + + def draw(self, _): + pass + + @staticmethod + def draw_menu(this: bpy.types.Menu, _): + this.layout.operator(view.FlipPose.bl_idname, text="MMD Flip Pose", icon="ARROW_LEFTRIGHT") + + @staticmethod + def register(): + bpy.types.VIEW3D_MT_pose.append(MMDPoseMenu.draw_menu) + bpy.types.VIEW3D_MT_pose_context_menu.append(MMDPoseMenu.draw_menu) + + @staticmethod + def unregister(): + bpy.types.VIEW3D_MT_pose_context_menu.remove(MMDPoseMenu.draw_menu) + bpy.types.VIEW3D_MT_pose.remove(MMDPoseMenu.draw_menu) diff --git a/mmd_tools/operators/__init__.py b/mmd_tools/operators/__init__.py index 40a96afc..3cd94a1b 100644 --- a/mmd_tools/operators/__init__.py +++ b/mmd_tools/operators/__init__.py @@ -1 +1,3 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. diff --git a/mmd_tools/operators/addon_updater.py b/mmd_tools/operators/addon_updater.py deleted file mode 100644 index 397201ce..00000000 --- a/mmd_tools/operators/addon_updater.py +++ /dev/null @@ -1,431 +0,0 @@ -# -*- coding: utf-8 -*- - -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# This file is copied from -# https://github.com/nutti/Screencast-Keys/blob/a16f6c7dd697f6ec7bced5811db4a8144514d320/src/screencast_keys/utils/addon_updater.py -# fmt: off - -import datetime -import json -import os -import shutil -import ssl -import urllib -import urllib.request -import zipfile -from threading import Lock - -import bpy - - -def get_separator(): - if os.name == "nt": - return "\\" - return "/" - - -def _request(url, json_decode=True): - # pylint: disable=W0212 - ssl._create_default_https_context = ssl._create_unverified_context - req = urllib.request.Request(url) - - try: - result = urllib.request.urlopen(req) - except urllib.error.HTTPError as e: - raise RuntimeError("HTTP error ({})".format(str(e.code))) - except urllib.error.URLError as e: - raise RuntimeError("URL error ({})".format(str(e.reason))) - - data = result.read() - result.close() - - if json_decode: - try: - return json.JSONDecoder().decode(data.decode()) - except Exception as e: - raise RuntimeError("API response has invalid JSON format ({})" - .format(str(e))) - - return data.decode() - - -def _download(url, path): - try: - urllib.request.urlretrieve(url, path) - except urllib.error.HTTPError as e: - raise RuntimeError("HTTP error ({})".format(str(e.code))) - except urllib.error.URLError as e: - raise RuntimeError("URL error ({})".format(str(e.reason))) - - -def _make_workspace_path(addon_dir): - return addon_dir + get_separator() + "addon_updater_workspace" - - -def _make_workspace(addon_dir): - dir_path = _make_workspace_path(addon_dir) - os.mkdir(dir_path) - - -def _make_temp_addon_path(addon_dir, url): - filename = url.split("/")[-1] - filepath = _make_workspace_path(addon_dir) + get_separator() + filename - return filepath - - -def _download_addon(addon_dir, url): - filepath = _make_temp_addon_path(addon_dir, url) - _download(url, filepath) - - -def _replace_addon(addon_dir, info, current_addon_path, offset_path=""): - # remove current add-on - if os.path.isfile(current_addon_path): - os.remove(current_addon_path) - elif os.path.isdir(current_addon_path): - shutil.rmtree(current_addon_path) - - # replace to the new add-on - workspace_path = _make_workspace_path(addon_dir) - tmp_addon_path = _make_temp_addon_path(addon_dir, info.url) - _, ext = os.path.splitext(tmp_addon_path) - if ext == ".zip": - with zipfile.ZipFile(tmp_addon_path) as zf: - zf.extractall(workspace_path) - if offset_path != "": - src = workspace_path + get_separator() + offset_path - dst = addon_dir - shutil.move(src, dst) - elif ext == ".py": - shutil.move(tmp_addon_path, addon_dir) - else: - raise RuntimeError("Unsupported file extension. (ext: {})".format(ext)) - - -def _get_all_releases_data(owner, repository): - url = "https://api.github.com/repos/{}/{}/releases".format(owner, repository) - data = _request(url) - - return data - - -def _get_all_branches_data(owner, repository): - url = "https://api.github.com/repos/{}/{}/branches".format(owner, repository) - data = _request(url) - - return data - - -def _parse_release_version(version): - return [int(c) for c in version[1:].split(".")] - - -# ver1 > ver2 : > 0 -# ver1 == ver2 : == 0 -# ver1 < ver2 : < 0 -def _compare_version(ver1, ver2): - if len(ver1) < len(ver2): - ver1.extend([-1 for _ in range(len(ver2) - len(ver1))]) - elif len(ver1) > len(ver2): - ver2.extend([-1 for _ in range(len(ver1) - len(ver2))]) - - def comp(v1, v2, idx): - if len(v1) == idx: - return 0 # v1 == v2 - - if v1[idx] > v2[idx]: - return 1 # v1 > v2 - if v1[idx] < v2[idx]: - return -1 # v1 < v2 - - return comp(v1, v2, idx + 1) - - return comp(ver1, ver2, 0) - - -class AddonUpdaterConfig: - def __init__(self): - # Name of owner - self.owner = "" - - # Name of repository - self.repository = "" - - # Additional branch for update candidate - self.branches = [] - - # Set minimum release version for update candidate. - # e.g. (5, 2) if your release tag name is "v5.2" - # If you specify (-1, -1), ignore versions less than current add-on - # version specified in bl_info. - self.min_release_version = (-1, -1) - self.max_release_version = (+999, +999) - - # Target add-on path - # {"branch/tag": "add-on path"} - self.target_addon_path = {} - - # Default target add-on path. - # Search this path if branch/tag is not found in - # self.target_addon_path. - self.default_target_addon_path = "" - - # Current add-on path - self.current_addon_path = "" - - # Blender add-on directory - self.addon_directory = "" - - -class UpdateCandidateInfo: - def __init__(self): - self.name = "" - self.url = "" - self.group = "" # BRANCH|RELEASE - - -class AddonUpdaterManager: - __inst = None - __lock = Lock() - - __initialized = False - __bl_info = None - __config = None - __update_candidate = [] - __candidate_checked = False - __error = "" - __info = "" - __updated = False - - def __init__(self): - raise NotImplementedError("Not allowed to call constructor") - - @classmethod - def __internal_new(cls): - return super().__new__(cls) - - @classmethod - def get_instance(cls): - if not cls.__inst: - with cls.__lock: - if not cls.__inst: - cls.__inst = cls.__internal_new() - - return cls.__inst - - def init(self, bl_info, config): - self.__bl_info = bl_info - self.__config = config - self.__update_candidate = [] - self.__candidate_checked = False - self.__error = "" - self.__info = "" - self.__updated = False - self.__initialized = True - - def initialized(self): - return self.__initialized - - def candidate_checked(self): - return self.__candidate_checked - - def check_update_candidate(self): - if not self.initialized(): - raise RuntimeError("AddonUpdaterManager must be initialized") - - self.__update_candidate = [] - self.__candidate_checked = False - - try: - # setup branch information - branches = _get_all_branches_data(self.__config.owner, - self.__config.repository) - for b in branches: - if b["name"] in self.__config.branches: - info = UpdateCandidateInfo() - info.name = b["name"] - info.url = "https://github.com/{}/{}/archive/{}.zip".format(self.__config.owner, self.__config.repository, b["name"]) - info.group = 'BRANCH' - self.__update_candidate.append(info) - - # setup release information - releases = _get_all_releases_data(self.__config.owner, - self.__config.repository) - for r in releases: - if _compare_version(_parse_release_version(r["tag_name"]), self.__config.min_release_version) > 0 and _compare_version(_parse_release_version(r["tag_name"]), self.__config.max_release_version) < 0: - info = UpdateCandidateInfo() - info.name = r["tag_name"] - info.url = r["assets"][0]["browser_download_url"] - info.group = 'RELEASE' - self.__update_candidate.append(info) - except RuntimeError as e: - self.__error = bpy.app.translations.pgettext_iface("Failed to check update {}. ({})").format(str(e), datetime.datetime.now()) - - self.__info = bpy.app.translations.pgettext_iface("Checked update. ({})").format(datetime.datetime.now()) - - self.__candidate_checked = True - - def has_error(self): - return self.__error != "" - - def error(self): - return self.__error - - def has_info(self): - return self.__info != "" - - def info(self): - return self.__info - - def update(self, version_name): - if not self.initialized(): - raise RuntimeError("AddonUpdaterManager must be initialized.") - - if not self.candidate_checked(): - raise RuntimeError("Update candidate is not checked.") - - info = None - for info in self.__update_candidate: - if info.name == version_name: - break - else: - raise RuntimeError("{} is not found in update candidate" - .format(version_name)) - - if info is None: - raise RuntimeError("Not found any update candidates") - - try: - # create workspace - _make_workspace(self.__config.addon_directory) - # download add-on - _download_addon(self.__config.addon_directory, info.url) - - # get add-on path - if info.name in self.__config.target_addon_path: - addon_path = self.__config.target_addon_path[info.name] - else: - addon_path = self.__config.default_target_addon_path - - # replace add-on - offset_path = "" - if info.group == 'BRANCH': - offset_path = "{}-{}{}{}".format( - self.__config.repository, info.name, get_separator(), - addon_path) - elif info.group == 'RELEASE': - offset_path = addon_path - _replace_addon(self.__config.addon_directory, - info, self.__config.current_addon_path, - offset_path) - - self.__updated = True - self.__info = bpy.app.translations.pgettext_iface("Updated to {}. ({})").format(info.name, datetime.datetime.now()) - except RuntimeError as e: - self.__error = bpy.app.translations.pgettext_iface("Failed to update {}. ({})").format(str(e), datetime.datetime.now()) - - shutil.rmtree(_make_workspace_path(self.__config.addon_directory)) - - def get_candidate_branch_names(self): - if not self.initialized(): - raise RuntimeError("AddonUpdaterManager must be initialized.") - - if not self.candidate_checked(): - raise RuntimeError("Update candidate is not checked.") - - return [info.name for info in self.__update_candidate] - - def latest_version(self): - release_versions = [info.name - for info in self.__update_candidate - if info.group == 'RELEASE'] - - latest = "" - for version in release_versions: - if latest == "": - latest = version - elif _compare_version(_parse_release_version(version), - _parse_release_version(latest)) > 0: - latest = version - - return latest - - def update_ready(self): - latest_version = self.latest_version() - if latest_version == "": - return False - compare = _compare_version(_parse_release_version(latest_version), self.__bl_info['version']) - return compare > 0 - - def updated(self): - return self.__updated - -# fmt: on - - -class CheckAddonUpdate(bpy.types.Operator): - bl_idname = "mmd_tools.check_addon_update" - bl_label = "Check Update" - bl_description = "Check Add-on Update" - bl_options = {"INTERNAL"} - - def execute(self, context): - updater = AddonUpdaterManager.get_instance() - updater.check_update_candidate() - - return {"FINISHED"} - - -class UpdateAddon(bpy.types.Operator): - bl_idname = "mmd_tools.update_addon" - bl_label = "Update" - bl_description = "Update Add-on" - bl_options = {"INTERNAL"} - - branch_name: bpy.props.StringProperty( - name="Branch Name", - description="Branch name to update", - default="", - ) - - def execute(self, context): - updater = AddonUpdaterManager.get_instance() - updater.update(self.branch_name) - - return {"FINISHED"} - - -def register_updater(bl_info, init_py_file): - config = AddonUpdaterConfig() - config.owner = "UuuNyaa" - config.repository = "blender_mmd_tools" - config.current_addon_path = os.path.dirname(os.path.realpath(init_py_file)) - config.branches = ["main"] - config.addon_directory = os.path.dirname(config.current_addon_path) - config.min_release_version = (1, 0, 0) - config.max_release_version = (4, 0, 0) - config.default_target_addon_path = "mmd_tools" - config.target_addon_path = {} - updater = AddonUpdaterManager.get_instance() - updater.init(bl_info, config) - - -def unregister_updater(): - pass diff --git a/mmd_tools/operators/animation.py b/mmd_tools/operators/animation.py index 97c785e0..5c91b69e 100644 --- a/mmd_tools/operators/animation.py +++ b/mmd_tools/operators/animation.py @@ -1,16 +1,19 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. from bpy.types import Operator -from mmd_tools import auto_scene_setup +from .. import auto_scene_setup + class SetFrameRange(Operator): - bl_idname = 'mmd_tools.set_frame_range' - bl_label = 'Set Frame Range' - bl_description = 'Set the frame range to best values to play the animation from start to finish. And set the frame rate to 30.0.' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.set_frame_range" + bl_label = "Set Frame Range" + bl_description = "Set the frame range to best values to play the animation from start to finish. And set the frame rate to 30.0." + bl_options = {"REGISTER", "UNDO"} def execute(self, context): auto_scene_setup.setupFrameRanges() auto_scene_setup.setupFps() - return {'FINISHED'} + return {"FINISHED"} diff --git a/mmd_tools/operators/camera.py b/mmd_tools/operators/camera.py index e2b8a324..ed47c6f1 100644 --- a/mmd_tools/operators/camera.py +++ b/mmd_tools/operators/camera.py @@ -1,51 +1,53 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. -from bpy.props import FloatProperty -from bpy.props import BoolProperty -from bpy.props import EnumProperty +from bpy.props import BoolProperty, EnumProperty, FloatProperty from bpy.types import Operator -from mmd_tools.core.camera import MMDCamera +from ..bpyutils import FnContext +from ..core.camera import MMDCamera + class ConvertToMMDCamera(Operator): - bl_idname = 'mmd_tools.convert_to_mmd_camera' - bl_label = 'Convert to MMD Camera' - bl_description = 'Create a camera rig for MMD' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.convert_to_mmd_camera" + bl_label = "Convert to MMD Camera" + bl_description = "Create a camera rig for MMD" + bl_options = {"REGISTER", "UNDO"} scale: FloatProperty( - name='Scale', - description='Scaling factor for initializing the camera', + name="Scale", + description="Scaling factor for initializing the camera", default=0.08, - ) + ) bake_animation: BoolProperty( - name='Bake Animation', - description='Bake camera animation to a new MMD camera rig', + name="Bake Animation", + description="Bake camera animation to a new MMD camera rig", default=False, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) camera_source: EnumProperty( - name='Camera Source', - description='Select camera source to bake animation (camera target is the selected or DoF object)', - items = [ - ('CURRENT', 'Current', 'Current active camera object', 0), - ('SCENE', 'Scene', 'Scene camera object', 1), - ], - default='CURRENT', - ) + name="Camera Source", + description="Select camera source to bake animation (camera target is the selected or DoF object)", + items=[ + ("CURRENT", "Current", "Current active camera object", 0), + ("SCENE", "Scene", "Scene camera object", 1), + ], + default="CURRENT", + ) min_distance: FloatProperty( - name='Min Distance', - description='Minimum distance to camera target when baking animation', + name="Min Distance", + description="Minimum distance to camera target when baking animation", default=0.1, - ) + ) @classmethod def poll(cls, context): obj = context.active_object - return obj and obj.type == 'CAMERA' + return obj and obj.type == "CAMERA" def invoke(self, context, event): vm = context.window_manager @@ -53,14 +55,13 @@ def invoke(self, context, event): def execute(self, context): if self.bake_animation: - from mmd_tools.bpyutils import SceneOp obj = context.active_object targets = [x for x in context.selected_objects if x != obj] target = targets[0] if len(targets) == 1 else None - if self.camera_source == 'SCENE': + if self.camera_source == "SCENE": obj = None camera = MMDCamera.newMMDCameraAnimation(obj, target, self.scale, self.min_distance).camera() - SceneOp(context).active_object = camera + FnContext.set_active_object(context, camera) else: MMDCamera.convertToMMDCamera(context.active_object, self.scale) - return {'FINISHED'} + return {"FINISHED"} diff --git a/mmd_tools/operators/display_item.py b/mmd_tools/operators/display_item.py index e00dcff3..5d095a3e 100644 --- a/mmd_tools/operators/display_item.py +++ b/mmd_tools/operators/display_item.py @@ -1,41 +1,42 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import bpy from bpy.types import Operator -from collections import OrderedDict - -from mmd_tools import utils -from mmd_tools.utils import ItemOp, ItemMoveOp -import mmd_tools.core.model as mmd_model +from ..core.model import Model, FnModel +from ..core.bone import FnBone +from ..utils import ItemMoveOp, ItemOp, selectSingleBone class AddDisplayItemFrame(Operator): - bl_idname = 'mmd_tools.display_item_frame_add' - bl_label = 'Add Display Item Frame' - bl_description = 'Add a display item frame to the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.display_item_frame_add" + bl_label = "Add Display Item Frame" + bl_description = "Add a display item frame to the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root frames = mmd_root.display_item_frames item, index = ItemOp.add_after(frames, max(1, mmd_root.active_display_item_frame)) - item.name = 'Display Frame' + item.name = "Display Frame" mmd_root.active_display_item_frame = index - return {'FINISHED'} + return {"FINISHED"} + class RemoveDisplayItemFrame(Operator): - bl_idname = 'mmd_tools.display_item_frame_remove' - bl_label = 'Remove Display Item Frame' - bl_description = 'Remove active display item frame from the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.display_item_frame_remove" + bl_label = "Remove Display Item Frame" + bl_description = "Remove active display item frame from the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root index = mmd_root.active_display_item_frame @@ -46,18 +47,19 @@ def execute(self, context): frame.active_item = 0 else: frames.remove(index) - mmd_root.active_display_item_frame = min(len(frames)-1, max(2, index-1)) - return {'FINISHED'} + mmd_root.active_display_item_frame = min(len(frames) - 1, max(2, index - 1)) + return {"FINISHED"} + class MoveDisplayItemFrame(Operator, ItemMoveOp): - bl_idname = 'mmd_tools.display_item_frame_move' - bl_label = 'Move Display Item Frame' - bl_description = 'Move active display item frame up/down in the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.display_item_frame_move" + bl_label = "Move Display Item Frame" + bl_description = "Move active display item frame up/down in the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root index = mmd_root.active_display_item_frame @@ -67,26 +69,27 @@ def execute(self, context): pass else: mmd_root.active_display_item_frame = self.move(frames, index, self.type, index_min=2) - return {'FINISHED'} + return {"FINISHED"} + class AddDisplayItem(Operator): - bl_idname = 'mmd_tools.display_item_add' - bl_label = 'Add Display Item' - bl_description = 'Add a display item to the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.display_item_add" + bl_label = "Add Display Item" + bl_description = "Add a display item to the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root frame = ItemOp.get_by_index(mmd_root.display_item_frames, mmd_root.active_display_item_frame) if frame is None: - return {'CANCELLED'} + return {"CANCELLED"} - if frame.name == u'表情': + if frame.name == "表情": morph = ItemOp.get_by_index(getattr(mmd_root, mmd_root.active_morph_type), mmd_root.active_morph) - morph_name = morph.name if morph else 'Morph Item' - self._add_item(frame, 'MORPH', morph_name, mmd_root.active_morph_type) + morph_name = morph.name if morph else "Morph Item" + self._add_item(frame, "MORPH", morph_name, mmd_root.active_morph_type) else: if context.active_bone: bone_names = [context.active_bone.name] @@ -98,10 +101,10 @@ def execute(self, context): bone_names += [b.name for b in context.selected_pose_bones] bone_names = sorted(list(set(bone_names))) for bone_name in bone_names: - self._add_item(frame, 'BONE', bone_name) + self._add_item(frame, "BONE", bone_name) else: - self._add_item(frame, 'BONE', 'Bone Item') - return {'FINISHED'} + self._add_item(frame, "BONE", "Bone Item") + return {"FINISHED"} def _add_item(self, frame, item_type, item_name, morph_type=None): items = frame.data @@ -112,89 +115,96 @@ def _add_item(self, frame, item_type, item_name, morph_type=None): item.morph_type = morph_type frame.active_item = index + class RemoveDisplayItem(Operator): - bl_idname = 'mmd_tools.display_item_remove' - bl_label = 'Remove Display Item' - bl_description = 'Remove display item(s) from the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.display_item_remove" + bl_label = "Remove Display Item" + bl_description = "Remove display item(s) from the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} all: bpy.props.BoolProperty( - name='All', - description='Delete all display items', + name="All", + description="Delete all display items", default=False, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root frame = ItemOp.get_by_index(mmd_root.display_item_frames, mmd_root.active_display_item_frame) if frame is None: - return {'CANCELLED'} + return {"CANCELLED"} if self.all: frame.data.clear() frame.active_item = 0 else: frame.data.remove(frame.active_item) - frame.active_item = max(0, frame.active_item-1) - return {'FINISHED'} + frame.active_item = max(0, frame.active_item - 1) + return {"FINISHED"} + class MoveDisplayItem(Operator, ItemMoveOp): - bl_idname = 'mmd_tools.display_item_move' - bl_label = 'Move Display Item' - bl_description = 'Move active display item up/dowm in the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.display_item_move" + bl_label = "Move Display Item" + bl_description = "Move active display item up/dowm in the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root frame = ItemOp.get_by_index(mmd_root.display_item_frames, mmd_root.active_display_item_frame) if frame is None: - return {'CANCELLED'} + return {"CANCELLED"} frame.active_item = self.move(frame.data, frame.active_item, self.type) - return {'FINISHED'} + return {"FINISHED"} + class FindDisplayItem(Operator): - bl_idname = 'mmd_tools.display_item_find' - bl_label = 'Find Display Item' - bl_description = 'Find the display item of active bone or morph' - bl_options = {'INTERNAL'} + bl_idname = "mmd_tools.display_item_find" + bl_label = "Find Display Item" + bl_description = "Find the display item of active bone or morph" + bl_options = {"INTERNAL"} type: bpy.props.EnumProperty( - name='Type', - description='Find type', - items = [ - ('BONE', 'Find Bone Item', 'Find active bone in Display Panel', 0), - ('MORPH', 'Find Morph Item', 'Find active morph of Morph Tools Panel in Display Panel', 1), - ], - default = 'BONE', - ) + name="Type", + description="Find type", + items=[ + ("BONE", "Find Bone Item", "Find active bone in Display Panel", 0), + ("MORPH", "Find Morph Item", "Find active morph of Morph Tools Panel in Display Panel", 1), + ], + default="BONE", + ) def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root - if self.type == 'MORPH': + if self.type == "MORPH": morph_type = mmd_root.active_morph_type morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph) if morph is None: - return {'CANCELLED'} + return {"CANCELLED"} morph_name = morph.name + def __check(item): - return item.type == 'MORPH' and item.name == morph_name and item.morph_type == morph_type + return item.type == "MORPH" and item.name == morph_name and item.morph_type == morph_type + self._find_display_item(mmd_root, __check) else: if context.active_bone is None: - return {'CANCELLED'} + return {"CANCELLED"} bone_name = context.active_bone.name + def __check(item): - return item.type == 'BONE' and item.name == bone_name + return item.type == "BONE" and item.name == bone_name + self._find_display_item(mmd_root, __check) - return {'FINISHED'} + return {"FINISHED"} def _find_display_item(self, mmd_root, check_func=None): for i, frame in enumerate(mmd_root.display_item_frames): @@ -204,78 +214,80 @@ def _find_display_item(self, mmd_root, check_func=None): frame.active_item = j return + class SelectCurrentDisplayItem(Operator): - bl_idname = 'mmd_tools.display_item_select_current' - bl_label = 'Select Current Display Item' - bl_description = 'Select the bone or morph assigned to the display item' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.display_item_select_current" + bl_label = "Select Current Display Item" + bl_description = "Select the bone or morph assigned to the display item" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root frame = ItemOp.get_by_index(mmd_root.display_item_frames, mmd_root.active_display_item_frame) if frame is None: - return {'CANCELLED'} + return {"CANCELLED"} item = ItemOp.get_by_index(frame.data, frame.active_item) if item is None: - return {'CANCELLED'} + return {"CANCELLED"} - if item.type == 'MORPH': + if item.type == "MORPH": morphs = getattr(mmd_root, item.morph_type) index = morphs.find(item.name) if index >= 0: mmd_root.active_morph_type = item.morph_type mmd_root.active_morph = index else: - utils.selectSingleBone(context, mmd_model.FnModel.find_armature(root), item.name) - return {'FINISHED'} + selectSingleBone(context, FnModel.find_armature_object(root), item.name) + return {"FINISHED"} + class DisplayItemQuickSetup(Operator): - bl_idname = 'mmd_tools.display_item_quick_setup' - bl_label = 'Display Item Quick Setup' - bl_description = 'Quick setup display items' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.display_item_quick_setup" + bl_label = "Display Item Quick Setup" + bl_description = "Quick setup display items" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} type: bpy.props.EnumProperty( - name='Type', - description='Select type', - items = [ - ('RESET', 'Reset', 'Clear all items and frames, reset to default', 'X', 0), - ('FACIAL', 'Load Facial Items', 'Load all morphs to faical frame', 'SHAPEKEY_DATA', 1), - ('GROUP_LOAD', 'Load Bone Groups', "Load armature's bone groups to display item frames", 'GROUP_BONE', 2), - ('GROUP_APPLY', 'Apply Bone Groups', "Apply display item frames to armature's bone groups", 'GROUP_BONE', 3), - ], - default='FACIAL', - ) + name="Type", + description="Select type", + items=[ + ("RESET", "Reset", "Clear all items and frames, reset to default", "X", 0), + ("FACIAL", "Load Facial Items", "Load all morphs to faical frame", "SHAPEKEY_DATA", 1), + ("GROUP_LOAD", "Sync from Bone Collections", "Sync armature's bone collections to display item frames", "GROUP_BONE", 2), + ("GROUP_APPLY", "Sync to Bone Collections", "Sync display item frames to armature's bone collections", "GROUP_BONE", 3), + ], + default="FACIAL", + ) def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) - if self.type == 'RESET': + root = FnModel.find_root_object(obj) + rig = Model(root) + if self.type == "RESET": rig.initialDisplayFrames() - elif self.type == 'FACIAL': - rig.initialDisplayFrames(reset=False) # ensure default frames + elif self.type == "FACIAL": + rig.initialDisplayFrames(reset=False) # ensure default frames self.load_facial_items(root.mmd_root) - elif self.type == 'GROUP_LOAD': - self.load_bone_groups(root.mmd_root, rig.armature()) - rig.initialDisplayFrames(reset=False) # ensure default frames - elif self.type == 'GROUP_APPLY': - self.apply_bone_groups(root.mmd_root, rig.armature()) - return {'FINISHED'} + elif self.type == "GROUP_LOAD": + FnBone.sync_display_item_frames_from_bone_collections(rig.armature()) + rig.initialDisplayFrames(reset=False) # ensure default frames + elif self.type == "GROUP_APPLY": + FnBone.sync_bone_collections_from_display_item_frames(rig.armature()) + return {"FINISHED"} @staticmethod def load_facial_items(mmd_root): item_list = [] - item_list.extend(('vertex_morphs', i.name) for i in mmd_root.vertex_morphs) - item_list.extend(('bone_morphs', i.name) for i in mmd_root.bone_morphs) - item_list.extend(('material_morphs', i.name) for i in mmd_root.material_morphs) - item_list.extend(('uv_morphs', i.name) for i in mmd_root.uv_morphs) - item_list.extend(('group_morphs', i.name) for i in mmd_root.group_morphs) + item_list.extend(("vertex_morphs", i.name) for i in mmd_root.vertex_morphs) + item_list.extend(("bone_morphs", i.name) for i in mmd_root.bone_morphs) + item_list.extend(("material_morphs", i.name) for i in mmd_root.material_morphs) + item_list.extend(("uv_morphs", i.name) for i in mmd_root.uv_morphs) + item_list.extend(("group_morphs", i.name) for i in mmd_root.group_morphs) frames = mmd_root.display_item_frames - frame = frames[u'表情'] + frame = frames["表情"] facial_items = frame.data mmd_root.active_display_item_frame = frames.find(frame.name) @@ -285,88 +297,6 @@ def load_facial_items(mmd_root): ItemOp.resize(facial_items, len(item_list)) for item, data in zip(facial_items, item_list): - item.type = 'MORPH' + item.type = "MORPH" item.morph_type, item.name = data frame.active_item = 0 - - @staticmethod - def load_bone_groups(mmd_root, armature): - bone_groups = OrderedDict((i.name, []) for i in armature.pose.bone_groups) - for b in armature.pose.bones: - if b.bone_group: - bone_groups[b.bone_group.name].append(b.name) - - frames = mmd_root.display_item_frames - used_index = set() - for group_name, bone_names in bone_groups.items(): - if len(bone_names) < 1: # skip empty group - continue - - frame = frames.get(group_name) - if frame is None: - frame = frames.add() - frame.name = group_name - frame.name_e = group_name - used_index.add(frames.find(group_name)) - - items = frame.data - ItemOp.resize(items, len(bone_names)) - for item, name in zip(items, bone_names): - item.type = 'BONE' - item.name = name - frame.active_item = 0 - - # remove unused frames - for i in reversed(range(len(frames))): - if i not in used_index: - frame = frames[i] - if frame.is_special: - if frame.name != u'表情': - frame.data.clear() - else: - frames.remove(i) - mmd_root.active_display_item_frame = 0 - - @staticmethod - def apply_bone_groups(mmd_root, armature): - arm_bone_groups = armature.pose.bone_groups - if not hasattr(arm_bone_groups, 'remove'): #bpy.app.version < (2, 72, 0): - from mmd_tools import bpyutils - bpyutils.select_object(armature) - bpy.ops.object.mode_set(mode='POSE') - class arm_bone_groups: - values = armature.pose.bone_groups.values - get = armature.pose.bone_groups.get - @staticmethod - def new(name): - bpy.ops.pose.group_add() - group = armature.pose.bone_groups.active - group.name = name - return group - @staticmethod - def remove(group): - armature.pose.bone_groups.active = group - bpy.ops.pose.group_remove() - - pose_bones = armature.pose.bones - used_groups = set() - unassigned_bones = {b.name for b in pose_bones} - for frame in mmd_root.display_item_frames: - for item in frame.data: - if item.type == 'BONE' and item.name in unassigned_bones: - unassigned_bones.remove(item.name) - group_name = frame.name - used_groups.add(group_name) - group = arm_bone_groups.get(group_name) - if group is None: - group = arm_bone_groups.new(name=group_name) - pose_bones[item.name].bone_group = group - - for name in unassigned_bones: - pose_bones[name].bone_group = None - - # remove unused bone groups - for group in arm_bone_groups.values(): - if group.name not in used_groups: - arm_bone_groups.remove(group) - diff --git a/mmd_tools/operators/fileio.py b/mmd_tools/operators/fileio.py index ecabdfd1..c2b68642 100644 --- a/mmd_tools/operators/fileio.py +++ b/mmd_tools/operators/fileio.py @@ -1,46 +1,45 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import logging -import re -import traceback import os +import re import time +import traceback import bpy -from bpy.types import Operator -from bpy.types import OperatorFileListElement -from bpy_extras.io_utils import ImportHelper, ExportHelper - -from mmd_tools import auto_scene_setup -from mmd_tools.utils import makePmxBoneMap -from mmd_tools.core.camera import MMDCamera -from mmd_tools.core.lamp import MMDLamp -from mmd_tools.translations import DictionaryEnum - -import mmd_tools.core.pmd.importer as pmd_importer -import mmd_tools.core.pmx.importer as pmx_importer -import mmd_tools.core.pmx.exporter as pmx_exporter -import mmd_tools.core.vmd.importer as vmd_importer -import mmd_tools.core.vmd.exporter as vmd_exporter -import mmd_tools.core.vpd.importer as vpd_importer -import mmd_tools.core.vpd.exporter as vpd_exporter -import mmd_tools.core.model as mmd_model - - +from bpy.types import Operator, OperatorFileListElement +from bpy_extras.io_utils import ExportHelper, ImportHelper + +from ..core.model import Model, FnModel +from ..core.pmd import importer as pmd_importer +from ..core.pmx import exporter as pmx_exporter +from ..core.pmx import importer as pmx_importer +from ..core.vmd import exporter as vmd_exporter +from ..core.vmd import importer as vmd_importer +from ..core.vpd import exporter as vpd_exporter +from ..core.vpd import importer as vpd_importer +from .. import auto_scene_setup +from ..core.camera import MMDCamera +from ..core.lamp import MMDLamp +from ..translations import DictionaryEnum +from ..utils import makePmxBoneMap LOG_LEVEL_ITEMS = [ - ('DEBUG', '4. DEBUG', '', 1), - ('INFO', '3. INFO', '', 2), - ('WARNING', '2. WARNING', '', 3), - ('ERROR', '1. ERROR', '', 4), - ] + ("DEBUG", "4. DEBUG", "", 1), + ("INFO", "3. INFO", "", 2), + ("WARNING", "2. WARNING", "", 3), + ("ERROR", "1. ERROR", "", 4), +] + def log_handler(log_level, filepath=None): if filepath is None: handler = logging.StreamHandler() else: - handler = logging.FileHandler(filepath, mode='w', encoding='utf-8') - formatter = logging.Formatter('%(message)s') + handler = logging.FileHandler(filepath, mode="w", encoding="utf-8") + formatter = logging.Formatter("%(message)s") handler.setFormatter(formatter) return handler @@ -48,116 +47,124 @@ def log_handler(log_level, filepath=None): def _update_types(cls, prop): types = cls.types.copy() - if 'PHYSICS' in types: - types.add('ARMATURE') - if 'DISPLAY' in types: - types.add('ARMATURE') - if 'MORPHS' in types: - types.add('ARMATURE') - types.add('MESH') + if "PHYSICS" in types: + types.add("ARMATURE") + if "DISPLAY" in types: + types.add("ARMATURE") + if "MORPHS" in types: + types.add("ARMATURE") + types.add("MESH") if types != cls.types: - cls.types = types # trigger update + cls.types = types # trigger update + class ImportPmx(Operator, ImportHelper): - bl_idname = 'mmd_tools.import_model' - bl_label = 'Import Model File (.pmd, .pmx)' - bl_description = 'Import model file(s) (.pmd, .pmx)' - bl_options = {'REGISTER', 'UNDO', 'PRESET'} + bl_idname = "mmd_tools.import_model" + bl_label = "Import Model File (.pmd, .pmx)" + bl_description = "Import model file(s) (.pmd, .pmx)" + bl_options = {"REGISTER", "UNDO", "PRESET"} - files: bpy.props.CollectionProperty(type=OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) - directory: bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'}) + files: bpy.props.CollectionProperty(type=OperatorFileListElement, options={"HIDDEN", "SKIP_SAVE"}) + directory: bpy.props.StringProperty(maxlen=1024, subtype="FILE_PATH", options={"HIDDEN", "SKIP_SAVE"}) - filename_ext = '.pmx' - filter_glob: bpy.props.StringProperty(default='*.pmx;*.pmd', options={'HIDDEN'}) + filename_ext = ".pmx" + filter_glob: bpy.props.StringProperty(default="*.pmx;*.pmd", options={"HIDDEN"}) types: bpy.props.EnumProperty( - name='Types', - description='Select which parts will be imported', - options={'ENUM_FLAG'}, - items = [ - ('MESH', 'Mesh', 'Mesh', 1), - ('ARMATURE', 'Armature', 'Armature', 2), - ('PHYSICS', 'Physics', 'Rigidbodies and joints (include Armature)', 4), - ('DISPLAY', 'Display', 'Display frames (include Armature)', 8), - ('MORPHS', 'Morphs', 'Morphs (include Armature and Mesh)', 16), - ], - default={'MESH', 'ARMATURE', 'PHYSICS', 'DISPLAY', 'MORPHS',}, + name="Types", + description="Select which parts will be imported", + options={"ENUM_FLAG"}, + items=[ + ("MESH", "Mesh", "Mesh", 1), + ("ARMATURE", "Armature", "Armature", 2), + ("PHYSICS", "Physics", "Rigidbodies and joints (include Armature)", 4), + ("DISPLAY", "Display", "Display frames (include Armature)", 8), + ("MORPHS", "Morphs", "Morphs (include Armature and Mesh)", 16), + ], + default={ + "MESH", + "ARMATURE", + "PHYSICS", + "DISPLAY", + "MORPHS", + }, update=_update_types, - ) + ) scale: bpy.props.FloatProperty( - name='Scale', - description='Scaling factor for importing the model', + name="Scale", + description="Scaling factor for importing the model", default=0.08, - ) + ) clean_model: bpy.props.BoolProperty( - name='Clean Model', - description='Remove unused vertices and duplicated/invalid faces', + name="Clean Model", + description="Remove unused vertices and duplicated/invalid faces", default=True, - ) + ) remove_doubles: bpy.props.BoolProperty( - name='Remove Doubles', - description='Merge duplicated vertices and faces', + name="Remove Doubles", + description="Merge duplicated vertices and faces", default=False, - ) + ) fix_IK_links: bpy.props.BoolProperty( - name='Fix IK Links', - description='Fix IK links to be blender suitable', + name="Fix IK Links", + description="Fix IK links to be blender suitable", default=False, - ) + ) ik_loop_factor: bpy.props.IntProperty( - name='IK Loop Factor', - description='Scaling factor of MMD IK loop', + name="IK Loop Factor", + description="Scaling factor of MMD IK loop", min=1, - soft_max=10, max=100, + soft_max=10, + max=100, default=5, - ) + ) apply_bone_fixed_axis: bpy.props.BoolProperty( - name='Apply Bone Fixed Axis', + name="Apply Bone Fixed Axis", description="Apply bone's fixed axis to be blender suitable", default=False, - ) + ) rename_bones: bpy.props.BoolProperty( - name='Rename Bones - L / R Suffix', - description='Use Blender naming conventions for Left / Right paired bones', + name="Rename Bones - L / R Suffix", + description="Use Blender naming conventions for Left / Right paired bones", default=True, - ) + ) use_underscore: bpy.props.BoolProperty( name="Rename Bones - Use Underscore", - description='Will not use dot, e.g. if renaming bones, will use _R instead of .R', + description="Will not use dot, e.g. if renaming bones, will use _R instead of .R", default=False, - ) + ) dictionary: bpy.props.EnumProperty( - name='Rename Bones To English', + name="Rename Bones To English", items=DictionaryEnum.get_dictionary_items, - description='Translate bone names from Japanese to English using selected dictionary', - ) + description="Translate bone names from Japanese to English using selected dictionary", + ) use_mipmap: bpy.props.BoolProperty( - name='use MIP maps for UV textures', - description='Specify if mipmaps will be generated', + name="use MIP maps for UV textures", + description="Specify if mipmaps will be generated", default=True, - ) + ) sph_blend_factor: bpy.props.FloatProperty( - name='influence of .sph textures', - description='The diffuse color factor of texture slot for .sph textures', + name="influence of .sph textures", + description="The diffuse color factor of texture slot for .sph textures", default=1.0, - ) + ) spa_blend_factor: bpy.props.FloatProperty( - name='influence of .spa textures', - description='The diffuse color factor of texture slot for .spa textures', + name="influence of .spa textures", + description="The diffuse color factor of texture slot for .spa textures", default=1.0, - ) + ) log_level: bpy.props.EnumProperty( - name='Log level', - description='Select log level', + name="Log level", + description="Select log level", items=LOG_LEVEL_ITEMS, - default='INFO', - ) + default="INFO", + ) save_log: bpy.props.BoolProperty( - name='Create a log file', - description='Create a log file', + name="Create a log file", + description="Create a log file", default=False, - ) + ) def execute(self, context): try: @@ -170,18 +177,18 @@ def execute(self, context): self._do_execute(context) except Exception as e: err_msg = traceback.format_exc() - self.report({'ERROR'}, err_msg) - return {'FINISHED'} + self.report({"ERROR"}, err_msg) + return {"FINISHED"} def _do_execute(self, context): logger = logging.getLogger() logger.setLevel(self.log_level) if self.save_log: - handler = log_handler(self.log_level, filepath=self.filepath + '.mmd_tools.import.log') + handler = log_handler(self.log_level, filepath=self.filepath + ".mmd_tools.import.log") logger.addHandler(handler) try: importer_cls = pmx_importer.PMXImporter - if re.search('\.pmd$', self.filepath, flags=re.I): + if re.search("\.pmd$", self.filepath, flags=re.I): importer_cls = pmd_importer.PMDImporter importer_cls().execute( @@ -199,8 +206,8 @@ def _do_execute(self, context): use_mipmap=self.use_mipmap, sph_blend_factor=self.sph_blend_factor, spa_blend_factor=self.spa_blend_factor, - ) - self.report({'INFO'}, 'Imported MMD model from "%s"'%self.filepath) + ) + self.report({"INFO"}, 'Imported MMD model from "%s"' % self.filepath) except Exception as e: err_msg = traceback.format_exc() logging.error(err_msg) @@ -209,78 +216,79 @@ def _do_execute(self, context): if self.save_log: logger.removeHandler(handler) - return {'FINISHED'} + return {"FINISHED"} + class ImportVmd(Operator, ImportHelper): - bl_idname = 'mmd_tools.import_vmd' - bl_label = 'Import VMD File (.vmd)' - bl_description = 'Import a VMD file to selected objects (.vmd)' - bl_options = {'REGISTER', 'UNDO', 'PRESET'} + bl_idname = "mmd_tools.import_vmd" + bl_label = "Import VMD File (.vmd)" + bl_description = "Import a VMD file to selected objects (.vmd)" + bl_options = {"REGISTER", "UNDO", "PRESET"} - filename_ext = '.vmd' - filter_glob: bpy.props.StringProperty(default='*.vmd', options={'HIDDEN'}) + filename_ext = ".vmd" + filter_glob: bpy.props.StringProperty(default="*.vmd", options={"HIDDEN"}) scale: bpy.props.FloatProperty( - name='Scale', - description='Scaling factor for importing the motion', + name="Scale", + description="Scaling factor for importing the motion", default=0.08, - ) + ) margin: bpy.props.IntProperty( - name='Margin', - description='How many frames added before motion starting', + name="Margin", + description="How many frames added before motion starting", min=0, default=5, - ) + ) bone_mapper: bpy.props.EnumProperty( - name='Bone Mapper', - description='Select bone mapper', + name="Bone Mapper", + description="Select bone mapper", items=[ - ('BLENDER', 'Blender', 'Use blender bone name', 0), - ('PMX', 'PMX', 'Use japanese name of MMD bone', 1), - ('RENAMED_BONES', 'Renamed bones', 'Rename the bone of motion data to be blender suitable', 2), - ], - default='PMX', - ) + ("BLENDER", "Blender", "Use blender bone name", 0), + ("PMX", "PMX", "Use japanese name of MMD bone", 1), + ("RENAMED_BONES", "Renamed bones", "Rename the bone of motion data to be blender suitable", 2), + ], + default="PMX", + ) rename_bones: bpy.props.BoolProperty( - name='Rename Bones - L / R Suffix', - description='Use Blender naming conventions for Left / Right paired bones', + name="Rename Bones - L / R Suffix", + description="Use Blender naming conventions for Left / Right paired bones", default=True, - ) + ) use_underscore: bpy.props.BoolProperty( name="Rename Bones - Use Underscore", - description='Will not use dot, e.g. if renaming bones, will use _R instead of .R', + description="Will not use dot, e.g. if renaming bones, will use _R instead of .R", default=False, - ) + ) dictionary: bpy.props.EnumProperty( - name='Rename Bones To English', + name="Rename Bones To English", items=DictionaryEnum.get_dictionary_items, - description='Translate bone names from Japanese to English using selected dictionary', - ) + description="Translate bone names from Japanese to English using selected dictionary", + ) use_pose_mode: bpy.props.BoolProperty( - name='Treat Current Pose as Rest Pose', - description='You can pose the model to fit the original pose of a motion data, such as T-Pose or A-Pose', + name="Treat Current Pose as Rest Pose", + description="You can pose the model to fit the original pose of a motion data, such as T-Pose or A-Pose", default=False, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) use_mirror: bpy.props.BoolProperty( - name='Mirror Motion', - description='Import the motion by using X-Axis mirror', + name="Mirror Motion", + description="Import the motion by using X-Axis mirror", default=False, - ) + ) update_scene_settings: bpy.props.BoolProperty( - name='Update scene settings', - description='Update frame range and frame rate (30 fps)', + name="Update scene settings", + description="Update frame range and frame rate (30 fps)", default=True, - ) + ) use_NLA: bpy.props.BoolProperty( - name='Use NLA', - description='Import the motion as NLA strips', + name="Use NLA", + description="Import the motion as NLA strips", default=False, - ) + ) files: bpy.props.CollectionProperty( type=OperatorFileListElement, - ) - directory: bpy.props.StringProperty(subtype='DIR_PATH') + ) + directory: bpy.props.StringProperty(subtype="DIR_PATH") @classmethod def poll(cls, context): @@ -288,39 +296,39 @@ def poll(cls, context): def draw(self, context): layout = self.layout - layout.prop(self, 'scale') - layout.prop(self, 'margin') - layout.prop(self, 'use_NLA') + layout.prop(self, "scale") + layout.prop(self, "margin") + layout.prop(self, "use_NLA") - layout.prop(self, 'bone_mapper') - if self.bone_mapper == 'RENAMED_BONES': - layout.prop(self, 'rename_bones') - layout.prop(self, 'use_underscore') - layout.prop(self, 'dictionary') - layout.prop(self, 'use_pose_mode') - layout.prop(self, 'use_mirror') + layout.prop(self, "bone_mapper") + if self.bone_mapper == "RENAMED_BONES": + layout.prop(self, "rename_bones") + layout.prop(self, "use_underscore") + layout.prop(self, "dictionary") + layout.prop(self, "use_pose_mode") + layout.prop(self, "use_mirror") - layout.prop(self, 'update_scene_settings') + layout.prop(self, "update_scene_settings") def execute(self, context): selected_objects = set(context.selected_objects) for i in frozenset(selected_objects): - root = mmd_model.Model.findRoot(i) + root = FnModel.find_root_object(i) if root == i: - rig = mmd_model.Model(root) + rig = Model(root) selected_objects.add(rig.armature()) selected_objects.add(rig.morph_slider.placeholder()) selected_objects |= set(rig.meshes()) bone_mapper = None - if self.bone_mapper == 'PMX': + if self.bone_mapper == "PMX": bone_mapper = makePmxBoneMap - elif self.bone_mapper == 'RENAMED_BONES': + elif self.bone_mapper == "RENAMED_BONES": bone_mapper = vmd_importer.RenamedBoneMapper( rename_LR_bones=self.rename_bones, use_underscore=self.use_underscore, translator=DictionaryEnum.get_translator(self.dictionary), - ).init + ).init for file in self.files: start_time = time.time() @@ -332,66 +340,67 @@ def execute(self, context): frame_margin=self.margin, use_mirror=self.use_mirror, use_NLA=self.use_NLA, - ) + ) for i in selected_objects: importer.assign(i) - logging.info(' Finished importing motion in %f seconds.', time.time() - start_time) + logging.info(" Finished importing motion in %f seconds.", time.time() - start_time) if self.update_scene_settings: auto_scene_setup.setupFrameRanges() auto_scene_setup.setupFps() context.scene.frame_set(context.scene.frame_current) - return {'FINISHED'} + return {"FINISHED"} + class ImportVpd(Operator, ImportHelper): - bl_idname = 'mmd_tools.import_vpd' - bl_label = 'Import VPD File (.vpd)' + bl_idname = "mmd_tools.import_vpd" + bl_label = "Import VPD File (.vpd)" bl_description = "Import VPD file(s) to selected rig's pose library (.vpd)" - bl_options = {'REGISTER', 'UNDO', 'PRESET'} + bl_options = {"REGISTER", "UNDO", "PRESET"} - files: bpy.props.CollectionProperty(type=OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) - directory: bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'}) + files: bpy.props.CollectionProperty(type=OperatorFileListElement, options={"HIDDEN", "SKIP_SAVE"}) + directory: bpy.props.StringProperty(maxlen=1024, subtype="FILE_PATH", options={"HIDDEN", "SKIP_SAVE"}) - filename_ext = '.vpd' - filter_glob: bpy.props.StringProperty(default='*.vpd', options={'HIDDEN'}) + filename_ext = ".vpd" + filter_glob: bpy.props.StringProperty(default="*.vpd", options={"HIDDEN"}) scale: bpy.props.FloatProperty( - name='Scale', - description='Scaling factor for importing the pose', + name="Scale", + description="Scaling factor for importing the pose", default=0.08, - ) + ) bone_mapper: bpy.props.EnumProperty( - name='Bone Mapper', - description='Select bone mapper', + name="Bone Mapper", + description="Select bone mapper", items=[ - ('BLENDER', 'Blender', 'Use blender bone name', 0), - ('PMX', 'PMX', 'Use japanese name of MMD bone', 1), - ('RENAMED_BONES', 'Renamed bones', 'Rename the bone of pose data to be blender suitable', 2), - ], - default='PMX', - ) + ("BLENDER", "Blender", "Use blender bone name", 0), + ("PMX", "PMX", "Use japanese name of MMD bone", 1), + ("RENAMED_BONES", "Renamed bones", "Rename the bone of pose data to be blender suitable", 2), + ], + default="PMX", + ) rename_bones: bpy.props.BoolProperty( - name='Rename Bones - L / R Suffix', - description='Use Blender naming conventions for Left / Right paired bones', + name="Rename Bones - L / R Suffix", + description="Use Blender naming conventions for Left / Right paired bones", default=True, - ) + ) use_underscore: bpy.props.BoolProperty( name="Rename Bones - Use Underscore", - description='Will not use dot, e.g. if renaming bones, will use _R instead of .R', + description="Will not use dot, e.g. if renaming bones, will use _R instead of .R", default=False, - ) + ) dictionary: bpy.props.EnumProperty( - name='Rename Bones To English', + name="Rename Bones To English", items=DictionaryEnum.get_dictionary_items, - description='Translate bone names from Japanese to English using selected dictionary', - ) + description="Translate bone names from Japanese to English using selected dictionary", + ) use_pose_mode: bpy.props.BoolProperty( - name='Treat Current Pose as Rest Pose', - description='You can pose the model to fit the original pose of a pose data, such as T-Pose or A-Pose', + name="Treat Current Pose as Rest Pose", + description="You can pose the model to fit the original pose of a pose data, such as T-Pose or A-Pose", default=False, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) @classmethod def poll(cls, context): @@ -399,34 +408,34 @@ def poll(cls, context): def draw(self, context): layout = self.layout - layout.prop(self, 'scale') + layout.prop(self, "scale") - layout.prop(self, 'bone_mapper') - if self.bone_mapper == 'RENAMED_BONES': - layout.prop(self, 'rename_bones') - layout.prop(self, 'use_underscore') - layout.prop(self, 'dictionary') - layout.prop(self, 'use_pose_mode') + layout.prop(self, "bone_mapper") + if self.bone_mapper == "RENAMED_BONES": + layout.prop(self, "rename_bones") + layout.prop(self, "use_underscore") + layout.prop(self, "dictionary") + layout.prop(self, "use_pose_mode") def execute(self, context): selected_objects = set(context.selected_objects) for i in frozenset(selected_objects): - root = mmd_model.Model.findRoot(i) + root = FnModel.find_root_object(i) if root == i: - rig = mmd_model.Model(root) + rig = Model(root) selected_objects.add(rig.armature()) selected_objects.add(rig.morph_slider.placeholder()) selected_objects |= set(rig.meshes()) bone_mapper = None - if self.bone_mapper == 'PMX': + if self.bone_mapper == "PMX": bone_mapper = makePmxBoneMap - elif self.bone_mapper == 'RENAMED_BONES': + elif self.bone_mapper == "RENAMED_BONES": bone_mapper = vmd_importer.RenamedBoneMapper( rename_LR_bones=self.rename_bones, use_underscore=self.use_underscore, translator=DictionaryEnum.get_translator(self.dictionary), - ).init + ).init for f in self.files: importer = vpd_importer.VPDImporter( @@ -434,89 +443,87 @@ def execute(self, context): scale=self.scale, bone_mapper=bone_mapper, use_pose_mode=self.use_pose_mode, - ) + ) for i in selected_objects: importer.assign(i) - return {'FINISHED'} + return {"FINISHED"} + class ExportPmx(Operator, ExportHelper): - bl_idname = 'mmd_tools.export_pmx' - bl_label = 'Export PMX File (.pmx)' - bl_description = 'Export selected MMD model(s) to PMX file(s) (.pmx)' - bl_options = {'PRESET'} + bl_idname = "mmd_tools.export_pmx" + bl_label = "Export PMX File (.pmx)" + bl_description = "Export selected MMD model(s) to PMX file(s) (.pmx)" + bl_options = {"PRESET"} - filename_ext = '.pmx' - filter_glob: bpy.props.StringProperty(default='*.pmx', options={'HIDDEN'}) + filename_ext = ".pmx" + filter_glob: bpy.props.StringProperty(default="*.pmx", options={"HIDDEN"}) scale: bpy.props.FloatProperty( - name='Scale', - description='Scaling factor for exporting the model', + name="Scale", + description="Scaling factor for exporting the model", default=12.5, - ) + ) copy_textures: bpy.props.BoolProperty( - name='Copy textures', - description='Copy textures', + name="Copy textures", + description="Copy textures", default=True, - ) + ) sort_materials: bpy.props.BoolProperty( - name='Sort Materials', - description=('Sort materials for alpha blending. ' - 'WARNING: Will not work if you have ' + - 'transparent meshes inside the model. ' + - 'E.g. blush meshes'), + name="Sort Materials", + description=("Sort materials for alpha blending. " "WARNING: Will not work if you have " + "transparent meshes inside the model. " + "E.g. blush meshes"), default=False, - ) + ) disable_specular: bpy.props.BoolProperty( - name='Disable SPH/SPA', - description='Disables all the Specular Map textures. It is required for some MME Shaders.', + name="Disable SPH/SPA", + description="Disables all the Specular Map textures. It is required for some MME Shaders.", default=False, - ) + ) visible_meshes_only: bpy.props.BoolProperty( - name='Visible Meshes Only', - description='Export visible meshes only', + name="Visible Meshes Only", + description="Export visible meshes only", default=False, - ) + ) overwrite_bone_morphs_from_pose_library: bpy.props.BoolProperty( - name='Overwrite Bone Morphs', - description='Overwrite the bone morphs from active pose library before exporting.', + name="Overwrite Bone Morphs", + description="Overwrite the bone morphs from active pose library before exporting.", default=False, - ) + ) translate_in_presets: bpy.props.BoolProperty( - name='(Experimental) Translate in Presets', - description='Translate in presets before exporting.', + name="(Experimental) Translate in Presets", + description="Translate in presets before exporting.", default=False, - ) + ) sort_vertices: bpy.props.EnumProperty( - name='Sort Vertices', - description='Choose the method to sort vertices', + name="Sort Vertices", + description="Choose the method to sort vertices", items=[ - ('NONE', 'None', 'No sorting', 0), - ('BLENDER', 'Blender', "Use blender's internal vertex order", 1), - ('CUSTOM', 'Custom', 'Use custom vertex weight of vertex group "mmd_vertex_order"', 2), - ], - default='NONE', - ) + ("NONE", "None", "No sorting", 0), + ("BLENDER", "Blender", "Use blender's internal vertex order", 1), + ("CUSTOM", "Custom", 'Use custom vertex weight of vertex group "mmd_vertex_order"', 2), + ], + default="NONE", + ) log_level: bpy.props.EnumProperty( - name='Log level', - description='Select log level', + name="Log level", + description="Select log level", items=LOG_LEVEL_ITEMS, - default='DEBUG', - ) + default="DEBUG", + ) save_log: bpy.props.BoolProperty( - name='Create a log file', - description='Create a log file', + name="Create a log file", + description="Create a log file", default=False, - ) + ) @classmethod def poll(cls, context): obj = context.active_object - return obj in context.selected_objects and mmd_model.Model.findRoot(obj) + return obj in context.selected_objects and FnModel.find_root_object(obj) def execute(self, context): try: folder = os.path.dirname(self.filepath) - models = {mmd_model.Model.findRoot(i) for i in context.selected_objects} + models = {FnModel.find_root_object(i) for i in context.selected_objects} for root in models: if root is None: continue @@ -526,53 +533,52 @@ def execute(self, context): model_name = bpy.path.clean_name(root.name) model_folder = os.path.join(folder, model_name) os.makedirs(model_folder, exist_ok=True) - self.filepath = os.path.join(model_folder, model_name + '.pmx') + self.filepath = os.path.join(model_folder, model_name + ".pmx") self._do_execute(context, root) except Exception as e: err_msg = traceback.format_exc() - self.report({'ERROR'}, err_msg) - return {'FINISHED'} + self.report({"ERROR"}, err_msg) + return {"FINISHED"} def _do_execute(self, context, root): logger = logging.getLogger() logger.setLevel(self.log_level) if self.save_log: - handler = log_handler(self.log_level, filepath=self.filepath + '.mmd_tools.export.log') + handler = log_handler(self.log_level, filepath=self.filepath + ".mmd_tools.export.log") logger.addHandler(handler) - rig = mmd_model.Model(root) - arm = rig.armature() + arm = FnModel.find_armature_object(root) if arm is None: - self.report({'ERROR'}, '[Skipped] The armature object of MMD model "%s" can\'t be found'%root.name) - return {'CANCELLED'} + self.report({"ERROR"}, '[Skipped] The armature object of MMD model "%s" can\'t be found' % root.name) + return {"CANCELLED"} orig_pose_position = None - if not root.mmd_root.is_built: # use 'REST' pose when the model is not built + if not root.mmd_root.is_built: # use 'REST' pose when the model is not built orig_pose_position = arm.data.pose_position - arm.data.pose_position = 'REST' + arm.data.pose_position = "REST" arm.update_tag() context.scene.frame_set(context.scene.frame_current) try: - meshes = rig.meshes() + meshes = FnModel.iterate_mesh_objects(root) if self.visible_meshes_only: meshes = (x for x in meshes if x in context.visible_objects) pmx_exporter.export( filepath=self.filepath, scale=self.scale, - root=rig.rootObject(), - armature=rig.armature(), + root=root, + armature=FnModel.find_armature_object(root), meshes=meshes, - rigid_bodies=rig.rigidBodies(), - joints=rig.joints(), + rigid_bodies=FnModel.iterate_rigid_body_objects(root), + joints=FnModel.iterate_joint_objects(root), copy_textures=self.copy_textures, overwrite_bone_morphs_from_pose_library=self.overwrite_bone_morphs_from_pose_library, translate_in_presets=self.translate_in_presets, sort_materials=self.sort_materials, sort_vertices=self.sort_vertices, disable_specular=self.disable_specular, - ) - self.report({'INFO'}, 'Exported MMD model "%s" to "%s"'%(root.name, self.filepath)) - except Exception as e: + ) + self.report({"INFO"}, 'Exported MMD model "%s" to "%s"' % (root.name, self.filepath)) + except: err_msg = traceback.format_exc() logging.error(err_msg) raise @@ -582,33 +588,34 @@ def _do_execute(self, context, root): if self.save_log: logger.removeHandler(handler) - return {'FINISHED'} + return {"FINISHED"} + class ExportVmd(Operator, ExportHelper): - bl_idname = 'mmd_tools.export_vmd' - bl_label = 'Export VMD File (.vmd)' - bl_description = 'Export motion data of active object to a VMD file (.vmd)' - bl_options = {'PRESET'} + bl_idname = "mmd_tools.export_vmd" + bl_label = "Export VMD File (.vmd)" + bl_description = "Export motion data of active object to a VMD file (.vmd)" + bl_options = {"PRESET"} - filename_ext = '.vmd' - filter_glob: bpy.props.StringProperty(default='*.vmd', options={'HIDDEN'}) + filename_ext = ".vmd" + filter_glob: bpy.props.StringProperty(default="*.vmd", options={"HIDDEN"}) scale: bpy.props.FloatProperty( - name='Scale', - description='Scaling factor for exporting the motion', + name="Scale", + description="Scaling factor for exporting the motion", default=12.5, - ) + ) use_pose_mode: bpy.props.BoolProperty( - name='Treat Current Pose as Rest Pose', - description='You can pose the model to export a motion data to different pose base, such as T-Pose or A-Pose', + name="Treat Current Pose as Rest Pose", + description="You can pose the model to export a motion data to different pose base, such as T-Pose or A-Pose", default=False, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) use_frame_range: bpy.props.BoolProperty( - name='Use Frame Range', - description = 'Export frames only in the frame range of context scene', - default = False, - ) + name="Use Frame Range", + description="Export frames only in the frame range of context scene", + default=False, + ) @classmethod def poll(cls, context): @@ -616,9 +623,9 @@ def poll(cls, context): if obj is None: return False - if obj.mmd_type == 'ROOT': + if obj.mmd_type == "ROOT": return True - if obj.mmd_type == 'NONE' and (obj.type == 'ARMATURE' or getattr(obj.data, 'shape_keys', None)): + if obj.mmd_type == "NONE" and (obj.type == "ARMATURE" or getattr(obj.data, "shape_keys", None)): return True if MMDCamera.isMMDCamera(obj) or MMDLamp.isMMDLamp(obj): return True @@ -627,73 +634,74 @@ def poll(cls, context): def execute(self, context): params = { - 'filepath':self.filepath, - 'scale':self.scale, - 'use_pose_mode':self.use_pose_mode, - 'use_frame_range':self.use_frame_range, - } + "filepath": self.filepath, + "scale": self.scale, + "use_pose_mode": self.use_pose_mode, + "use_frame_range": self.use_frame_range, + } obj = context.active_object - if obj.mmd_type == 'ROOT': - rig = mmd_model.Model(obj) - params['mesh'] = rig.morph_slider.placeholder(binded=True) or rig.firstMesh() - params['armature'] = rig.armature() - params['model_name'] = obj.mmd_root.name or obj.name - elif getattr(obj.data, 'shape_keys', None): - params['mesh'] = obj - params['model_name'] = obj.name - elif obj.type == 'ARMATURE': - params['armature'] = obj - params['model_name'] = obj.name + if obj.mmd_type == "ROOT": + rig = Model(obj) + params["mesh"] = rig.morph_slider.placeholder(binded=True) or rig.firstMesh() + params["armature"] = rig.armature() + params["model_name"] = obj.mmd_root.name or obj.name + elif getattr(obj.data, "shape_keys", None): + params["mesh"] = obj + params["model_name"] = obj.name + elif obj.type == "ARMATURE": + params["armature"] = obj + params["model_name"] = obj.name else: for i in context.selected_objects: if MMDCamera.isMMDCamera(i): - params['camera'] = i + params["camera"] = i elif MMDLamp.isMMDLamp(i): - params['lamp'] = i + params["lamp"] = i try: start_time = time.time() vmd_exporter.VMDExporter().export(**params) - logging.info(' Finished exporting motion in %f seconds.', time.time() - start_time) + logging.info(" Finished exporting motion in %f seconds.", time.time() - start_time) except Exception as e: err_msg = traceback.format_exc() logging.error(err_msg) - self.report({'ERROR'}, err_msg) + self.report({"ERROR"}, err_msg) + + return {"FINISHED"} - return {'FINISHED'} class ExportVpd(Operator, ExportHelper): - bl_idname = 'mmd_tools.export_vpd' - bl_label = 'Export VPD File (.vpd)' - bl_description = 'Export to VPD file(s) (.vpd)' + bl_idname = "mmd_tools.export_vpd" + bl_label = "Export VPD File (.vpd)" + bl_description = "Export to VPD file(s) (.vpd)" bl_description = "Export active rig's pose library to VPD file(s) (.vpd)" - bl_options = {'PRESET'} + bl_options = {"PRESET"} - filename_ext = '.vpd' - filter_glob: bpy.props.StringProperty(default='*.vpd', options={'HIDDEN'}) + filename_ext = ".vpd" + filter_glob: bpy.props.StringProperty(default="*.vpd", options={"HIDDEN"}) scale: bpy.props.FloatProperty( - name='Scale', - description='Scaling factor for exporting the pose', + name="Scale", + description="Scaling factor for exporting the pose", default=12.5, - ) + ) pose_type: bpy.props.EnumProperty( - name='Pose Type', - description='Choose the pose type to export', + name="Pose Type", + description="Choose the pose type to export", items=[ - ('CURRENT', 'Current Pose', 'Current pose of the rig', 0), - ('ACTIVE', 'Active Pose', "Active pose of the rig's pose library", 1), - ('ALL', 'All Poses', "All poses of the rig's pose library (the pose name will be the file name)", 2), - ], - default='CURRENT', - ) + ("CURRENT", "Current Pose", "Current pose of the rig", 0), + ("ACTIVE", "Active Pose", "Active pose of the rig's pose library", 1), + ("ALL", "All Poses", "All poses of the rig's pose library (the pose name will be the file name)", 2), + ], + default="CURRENT", + ) use_pose_mode: bpy.props.BoolProperty( - name='Treat Current Pose as Rest Pose', - description='You can pose the model to export a pose data to different pose base, such as T-Pose or A-Pose', + name="Treat Current Pose as Rest Pose", + description="You can pose the model to export a pose data to different pose base, such as T-Pose or A-Pose", default=False, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) @classmethod def poll(cls, context): @@ -701,46 +709,45 @@ def poll(cls, context): if obj is None: return False - if obj.mmd_type == 'ROOT': + if obj.mmd_type == "ROOT": return True - if obj.mmd_type == 'NONE' and (obj.type == 'ARMATURE' or getattr(obj.data, 'shape_keys', None)): + if obj.mmd_type == "NONE" and (obj.type == "ARMATURE" or getattr(obj.data, "shape_keys", None)): return True return False def draw(self, context): layout = self.layout - layout.prop(self, 'scale') - layout.prop(self, 'pose_type', expand=True) - if self.pose_type != 'CURRENT': - layout.prop(self, 'use_pose_mode') + layout.prop(self, "scale") + layout.prop(self, "pose_type", expand=True) + if self.pose_type != "CURRENT": + layout.prop(self, "use_pose_mode") def execute(self, context): params = { - 'filepath':self.filepath, - 'scale':self.scale, - 'pose_type':self.pose_type, - 'use_pose_mode':self.use_pose_mode, - } + "filepath": self.filepath, + "scale": self.scale, + "pose_type": self.pose_type, + "use_pose_mode": self.use_pose_mode, + } obj = context.active_object - if obj.mmd_type == 'ROOT': - rig = mmd_model.Model(obj) - params['mesh'] = rig.morph_slider.placeholder(binded=True) or rig.firstMesh() - params['armature'] = rig.armature() - params['model_name'] = obj.mmd_root.name or obj.name - elif getattr(obj.data, 'shape_keys', None): - params['mesh'] = obj - params['model_name'] = obj.name - elif obj.type == 'ARMATURE': - params['armature'] = obj - params['model_name'] = obj.name + if obj.mmd_type == "ROOT": + rig = Model(obj) + params["mesh"] = rig.morph_slider.placeholder(binded=True) or rig.firstMesh() + params["armature"] = rig.armature() + params["model_name"] = obj.mmd_root.name or obj.name + elif getattr(obj.data, "shape_keys", None): + params["mesh"] = obj + params["model_name"] = obj.name + elif obj.type == "ARMATURE": + params["armature"] = obj + params["model_name"] = obj.name try: vpd_exporter.VPDExporter().export(**params) except Exception as e: err_msg = traceback.format_exc() logging.error(err_msg) - self.report({'ERROR'}, err_msg) - return {'FINISHED'} - + self.report({"ERROR"}, err_msg) + return {"FINISHED"} diff --git a/mmd_tools/operators/lamp.py b/mmd_tools/operators/lamp.py index 6f43a0dd..7a253140 100644 --- a/mmd_tools/operators/lamp.py +++ b/mmd_tools/operators/lamp.py @@ -1,21 +1,24 @@ # -*- coding: utf-8 -*- +# Copyright 2017 MMD Tools authors +# This file is part of MMD Tools. from bpy.props import FloatProperty from bpy.types import Operator -from mmd_tools.core.lamp import MMDLamp +from ..core.lamp import MMDLamp + class ConvertToMMDLamp(Operator): - bl_idname = 'mmd_tools.convert_to_mmd_lamp' - bl_label = 'Convert to MMD Light' - bl_description = 'Create a light rig for MMD' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.convert_to_mmd_lamp" + bl_label = "Convert to MMD Light" + bl_description = "Create a light rig for MMD" + bl_options = {"REGISTER", "UNDO"} scale: FloatProperty( - name='Scale', - description='Scaling factor for initializing the light', + name="Scale", + description="Scaling factor for initializing the light", default=0.08, - ) + ) @classmethod def poll(cls, context): @@ -27,4 +30,4 @@ def invoke(self, context, event): def execute(self, context): MMDLamp.convertToMMDLamp(context.active_object, self.scale) - return {'FINISHED'} + return {"FINISHED"} diff --git a/mmd_tools/operators/material.py b/mmd_tools/operators/material.py index d0cb96b4..549ce9cf 100644 --- a/mmd_tools/operators/material.py +++ b/mmd_tools/operators/material.py @@ -1,176 +1,186 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import bpy +from bpy.props import BoolProperty, StringProperty from bpy.types import Operator -from bpy.props import StringProperty, BoolProperty -from mmd_tools import cycles_converter -from mmd_tools.core.material import FnMaterial -from mmd_tools.core.exceptions import MaterialNotFoundError -from mmd_tools.core.shader import _NodeGroupUtils +from .. import cycles_converter +from ..core.exceptions import MaterialNotFoundError +from ..core.material import FnMaterial +from ..core.shader import _NodeGroupUtils + class ConvertMaterialsForCycles(Operator): - bl_idname = 'mmd_tools.convert_materials_for_cycles' - bl_label = 'Convert Materials For Cycles' - bl_description = 'Convert materials of selected objects for Cycles.' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.convert_materials_for_cycles" + bl_label = "Convert Materials For Cycles" + bl_description = "Convert materials of selected objects for Cycles." + bl_options = {"REGISTER", "UNDO"} use_principled: bpy.props.BoolProperty( - name='Convert to Principled BSDF', - description='Convert MMD shader nodes to Principled BSDF as well if enabled', + name="Convert to Principled BSDF", + description="Convert MMD shader nodes to Principled BSDF as well if enabled", default=False, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) clean_nodes: bpy.props.BoolProperty( - name='Clean Nodes', - description='Remove redundant nodes as well if enabled. Disable it to keep node data.', + name="Clean Nodes", + description="Remove redundant nodes as well if enabled. Disable it to keep node data.", default=False, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) @classmethod def poll(cls, context): - return next((x for x in context.selected_objects if x.type == 'MESH'), None) + return next((x for x in context.selected_objects if x.type == "MESH"), None) def draw(self, context): layout = self.layout - if cycles_converter.is_principled_bsdf_supported(): - layout.prop(self, 'use_principled') - layout.prop(self, 'clean_nodes') + layout.prop(self, "use_principled") + layout.prop(self, "clean_nodes") def execute(self, context): try: - context.scene.render.engine = 'CYCLES' + context.scene.render.engine = "CYCLES" except: - self.report({'ERROR'}, ' * Failed to change to Cycles render engine.') - return {'CANCELLED'} - for obj in (x for x in context.selected_objects if x.type == 'MESH'): + self.report({"ERROR"}, " * Failed to change to Cycles render engine.") + return {"CANCELLED"} + for obj in (x for x in context.selected_objects if x.type == "MESH"): cycles_converter.convertToCyclesShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes) - return {'FINISHED'} + return {"FINISHED"} + class ConvertMaterials(Operator): - bl_idname = 'mmd_tools.convert_materials' - bl_label = 'Convert Materials' - bl_description = 'Convert materials of selected objects.' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.convert_materials" + bl_label = "Convert Materials" + bl_description = "Convert materials of selected objects." + bl_options = {"REGISTER", "UNDO"} use_principled: bpy.props.BoolProperty( - name='Convert to Principled BSDF', - description='Convert MMD shader nodes to Principled BSDF as well if enabled', + name="Convert to Principled BSDF", + description="Convert MMD shader nodes to Principled BSDF as well if enabled", default=True, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) clean_nodes: bpy.props.BoolProperty( - name='Clean Nodes', - description='Remove redundant nodes as well if enabled. Disable it to keep node data.', + name="Clean Nodes", + description="Remove redundant nodes as well if enabled. Disable it to keep node data.", default=True, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) subsurface: bpy.props.FloatProperty( - name='Subsurface', + name="Subsurface", default=0.001, - soft_min=0.000, soft_max=1.000, + soft_min=0.000, + soft_max=1.000, precision=3, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) @classmethod def poll(cls, context): - return next((x for x in context.selected_objects if x.type == 'MESH'), None) + return next((x for x in context.selected_objects if x.type == "MESH"), None) def execute(self, context): for obj in context.selected_objects: - if obj.type != 'MESH': + if obj.type != "MESH": continue cycles_converter.convertToBlenderShader(obj, use_principled=self.use_principled, clean_nodes=self.clean_nodes, subsurface=self.subsurface) - return {'FINISHED'} + return {"FINISHED"} + -class _OpenTextureBase(object): - """ Create a texture for mmd model material. - """ - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class _OpenTextureBase: + """Create a texture for mmd model material.""" + + bl_options = {"REGISTER", "UNDO", "INTERNAL"} filepath: StringProperty( name="File Path", description="Filepath used for importing the file", maxlen=1024, - subtype='FILE_PATH', - ) + subtype="FILE_PATH", + ) use_filter_image: BoolProperty( default=True, - options={'HIDDEN'}, - ) + options={"HIDDEN"}, + ) def invoke(self, context, event): context.window_manager.fileselect_add(self) - return {'RUNNING_MODAL'} + return {"RUNNING_MODAL"} + class OpenTexture(Operator, _OpenTextureBase): - bl_idname = 'mmd_tools.material_open_texture' - bl_label = 'Open Texture' - bl_description = 'Create main texture of active material' + bl_idname = "mmd_tools.material_open_texture" + bl_label = "Open Texture" + bl_description = "Create main texture of active material" def execute(self, context): mat = context.active_object.active_material fnMat = FnMaterial(mat) fnMat.create_texture(self.filepath) - return {'FINISHED'} + return {"FINISHED"} + class RemoveTexture(Operator): - """ Create a texture for mmd model material. - """ - bl_idname = 'mmd_tools.material_remove_texture' - bl_label = 'Remove Texture' - bl_description = 'Remove main texture of active material' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + """Create a texture for mmd model material.""" + + bl_idname = "mmd_tools.material_remove_texture" + bl_label = "Remove Texture" + bl_description = "Remove main texture of active material" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): mat = context.active_object.active_material fnMat = FnMaterial(mat) fnMat.remove_texture() - return {'FINISHED'} + return {"FINISHED"} + class OpenSphereTextureSlot(Operator, _OpenTextureBase): - """ Create a texture for mmd model material. - """ - bl_idname = 'mmd_tools.material_open_sphere_texture' - bl_label = 'Open Sphere Texture' - bl_description = 'Create sphere texture of active material' + """Create a texture for mmd model material.""" + + bl_idname = "mmd_tools.material_open_sphere_texture" + bl_label = "Open Sphere Texture" + bl_description = "Create sphere texture of active material" def execute(self, context): mat = context.active_object.active_material fnMat = FnMaterial(mat) fnMat.create_sphere_texture(self.filepath, context.active_object) - return {'FINISHED'} + return {"FINISHED"} + class RemoveSphereTexture(Operator): - """ Create a texture for mmd model material. - """ - bl_idname = 'mmd_tools.material_remove_sphere_texture' - bl_label = 'Remove Sphere Texture' - bl_description = 'Remove sphere texture of active material' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + """Create a texture for mmd model material.""" + + bl_idname = "mmd_tools.material_remove_sphere_texture" + bl_label = "Remove Sphere Texture" + bl_description = "Remove sphere texture of active material" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): mat = context.active_object.active_material fnMat = FnMaterial(mat) fnMat.remove_sphere_texture() - return {'FINISHED'} + return {"FINISHED"} + class MoveMaterialUp(Operator): - bl_idname = 'mmd_tools.move_material_up' - bl_label = 'Move Material Up' - bl_description = 'Moves selected material one slot up' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.move_material_up" + bl_label = "Move Material Up" + bl_description = "Moves selected material one slot up" + bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): obj = context.active_object - valid_mesh = obj and obj.type == 'MESH' and obj.mmd_type == 'NONE' + valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" return valid_mesh and obj.active_material_index > 0 def execute(self, context): @@ -178,25 +188,25 @@ def execute(self, context): current_idx = obj.active_material_index prev_index = current_idx - 1 try: - FnMaterial.swap_materials(obj, current_idx, prev_index, - reverse=True, swap_slots=True) + FnMaterial.swap_materials(obj, current_idx, prev_index, reverse=True, swap_slots=True) except MaterialNotFoundError: - self.report({'ERROR'}, 'Materials not found') - return { 'CANCELLED' } + self.report({"ERROR"}, "Materials not found") + return {"CANCELLED"} obj.active_material_index = prev_index - return { 'FINISHED' } + return {"FINISHED"} + class MoveMaterialDown(Operator): - bl_idname = 'mmd_tools.move_material_down' - bl_label = 'Move Material Down' - bl_description = 'Moves the selected material one slot down' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.move_material_down" + bl_label = "Move Material Down" + bl_description = "Moves the selected material one slot down" + bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): obj = context.active_object - valid_mesh = obj and obj.type == 'MESH' and obj.mmd_type == 'NONE' + valid_mesh = obj and obj.type == "MESH" and obj.mmd_type == "NONE" return valid_mesh and obj.active_material_index < len(obj.material_slots) - 1 def execute(self, context): @@ -204,56 +214,57 @@ def execute(self, context): current_idx = obj.active_material_index next_index = current_idx + 1 try: - FnMaterial.swap_materials(obj, current_idx, next_index, - reverse=True, swap_slots=True) + FnMaterial.swap_materials(obj, current_idx, next_index, reverse=True, swap_slots=True) except MaterialNotFoundError: - self.report({'ERROR'}, 'Materials not found') - return { 'CANCELLED' } + self.report({"ERROR"}, "Materials not found") + return {"CANCELLED"} obj.active_material_index = next_index - return { 'FINISHED' } + return {"FINISHED"} + class EdgePreviewSetup(Operator): - bl_idname = 'mmd_tools.edge_preview_setup' - bl_label = 'Edge Preview Setup' + bl_idname = "mmd_tools.edge_preview_setup" + bl_label = "Edge Preview Setup" bl_description = 'Preview toon edge settings of active model using "Solidify" modifier' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_options = {"REGISTER", "UNDO", "INTERNAL"} action: bpy.props.EnumProperty( - name='Action', - description='Select action', + name="Action", + description="Select action", items=[ - ('CREATE', 'Create', 'Create toon edge', 0), - ('CLEAN', 'Clean', 'Clear toon edge', 1), - ], - default='CREATE', - ) + ("CREATE", "Create", "Create toon edge", 0), + ("CLEAN", "Clean", "Clear toon edge", 1), + ], + default="CREATE", + ) def execute(self, context): - from mmd_tools.core.model import Model - root = Model.findRoot(context.active_object) + from ..core.model import FnModel + + root = FnModel.find_root_object(context.active_object) if root is None: - self.report({'ERROR'}, 'Select a MMD model') - return {'CANCELLED'} + self.report({"ERROR"}, "Select a MMD model") + return {"CANCELLED"} - rig = Model(root) - if self.action == 'CLEAN': - for obj in rig.meshes(): + if self.action == "CLEAN": + for obj in FnModel.iterate_mesh_objects(root): self.__clean_toon_edge(obj) else: - from mmd_tools.bpyutils import Props - scale = 0.2*getattr(rig.rootObject(), Props.empty_display_size) - counts = sum(self.__create_toon_edge(obj, scale) for obj in rig.meshes()) - self.report({'INFO'}, 'Created %d toon edge(s)'%counts) - return {'FINISHED'} + from ..bpyutils import Props + + 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)) + self.report({"INFO"}, "Created %d toon edge(s)" % counts) + return {"FINISHED"} def __clean_toon_edge(self, obj): - if 'mmd_edge_preview' in obj.modifiers: - obj.modifiers.remove(obj.modifiers['mmd_edge_preview']) + if "mmd_edge_preview" in obj.modifiers: + obj.modifiers.remove(obj.modifiers["mmd_edge_preview"]) - if 'mmd_edge_preview' in obj.vertex_groups: - obj.vertex_groups.remove(obj.vertex_groups['mmd_edge_preview']) + if "mmd_edge_preview" in obj.vertex_groups: + obj.vertex_groups.remove(obj.vertex_groups["mmd_edge_preview"]) - FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith('mmd_edge.')) + FnMaterial.clean_materials(obj, can_remove=lambda m: m and m.name.startswith("mmd_edge.")) def __create_toon_edge(self, obj, scale=1.0): self.__clean_toon_edge(obj) @@ -261,36 +272,36 @@ def __create_toon_edge(self, obj, scale=1.0): material_offset = len(materials) for m in tuple(materials): if m and m.mmd_material.enabled_toon_edge: - mat_edge = self.__get_edge_material('mmd_edge.'+m.name, m.mmd_material.edge_color, materials) + mat_edge = self.__get_edge_material("mmd_edge." + m.name, m.mmd_material.edge_color, materials) materials.append(mat_edge) elif material_offset > 1: - mat_edge = self.__get_edge_material('mmd_edge.disabled', (0, 0, 0, 0), materials) + mat_edge = self.__get_edge_material("mmd_edge.disabled", (0, 0, 0, 0), materials) materials.append(mat_edge) if len(materials) > material_offset: - mod = obj.modifiers.get('mmd_edge_preview', None) + mod = obj.modifiers.get("mmd_edge_preview", None) if mod is None: - mod = obj.modifiers.new('mmd_edge_preview', 'SOLIDIFY') + mod = obj.modifiers.new("mmd_edge_preview", "SOLIDIFY") mod.material_offset = material_offset - mod.thickness_vertex_group = 1e-3 # avoid overlapped faces + mod.thickness_vertex_group = 1e-3 # avoid overlapped faces mod.use_flip_normals = True mod.use_rim = False mod.offset = 1 self.__create_edge_preview_group(obj) mod.thickness = scale - mod.vertex_group = 'mmd_edge_preview' + mod.vertex_group = "mmd_edge_preview" return len(materials) - material_offset 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} + weight_map = {i: m.mmd_material.edge_weight for i, m in enumerate(materials) if m} scale_map = {} - vg_scale_index = obj.vertex_groups.find('mmd_edge_scale') + 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} - vg_edge_preview = obj.vertex_groups.new(name='mmd_edge_preview') - for i, mi in {v:f.material_index for f in reversed(obj.data.polygons) for v in f.vertices}.items(): + scale_map = {v.index: g.weight for v in vertices for g in v.groups if g.group == vg_scale_index} + vg_edge_preview = obj.vertex_groups.new(name="mmd_edge_preview") + for i, mi in {v: f.material_index for f in reversed(obj.data.polygons) for v in f.vertices}.items(): 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') + vg_edge_preview.add(index=[i], weight=weight, type="REPLACE") def __get_edge_material(self, mat_name, edge_color, materials): if mat_name in materials: @@ -302,7 +313,7 @@ def __get_edge_material(self, mat_name, edge_color, materials): # note: edge affects ground shadow mmd_mat.is_double_sided = mmd_mat.enabled_drop_shadow = False mmd_mat.enabled_self_shadow_map = mmd_mat.enabled_self_shadow = False - #mmd_mat.enabled_self_shadow_map = True # for blender 2.78+ BI viewport only + # mmd_mat.enabled_self_shadow_map = True # for blender 2.78+ BI viewport only mmd_mat.diffuse_color = mmd_mat.specular_color = (0, 0, 0) mmd_mat.ambient_color = edge_color[:3] mmd_mat.alpha = edge_color[3] @@ -314,81 +325,63 @@ 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) + node_shader = nodes.get("mmd_edge_preview", None) if node_shader is None or not any(s.is_linked for s in node_shader.outputs): XPOS, YPOS = 210, 110 nodes.clear() - node_shader = nodes.new('ShaderNodeGroup') - node_shader.name = 'mmd_edge_preview' + node_shader = nodes.new("ShaderNodeGroup") + node_shader.name = "mmd_edge_preview" node_shader.location = (0, 0) node_shader.width = 200 node_shader.node_tree = self.__get_edge_preview_shader() - if bpy.app.version < (2, 80, 0): - node_out = nodes.new('ShaderNodeOutput') - node_out.location = (XPOS*2, YPOS*2) - links.new(node_shader.outputs['Color'], node_out.inputs['Color']) - links.new(node_shader.outputs['Alpha'], node_out.inputs['Alpha']) + node_out = nodes.new("ShaderNodeOutputMaterial") + node_out.location = (XPOS * 2, YPOS * 0) + links.new(node_shader.outputs["Shader"], node_out.inputs["Surface"]) - node_out = nodes.new('ShaderNodeOutputMaterial') - node_out.location = (XPOS*2, YPOS*0) - links.new(node_shader.outputs['Shader'], node_out.inputs['Surface']) - - node_shader.inputs['Color'].default_value = m.mmd_material.edge_color - node_shader.inputs['Alpha'].default_value = m.mmd_material.edge_color[3] + 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): - group_name = 'MMDEdgePreview' - shader = bpy.data.node_groups.get(group_name, None) or bpy.data.node_groups.new(name=group_name, type='ShaderNodeTree') + 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): return shader ng = _NodeGroupUtils(shader) - node_input = ng.new_node('NodeGroupInput', (-5, 0)) - node_output = ng.new_node('NodeGroupOutput', (3, 0)) + node_input = ng.new_node("NodeGroupInput", (-5, 0)) + node_output = ng.new_node("NodeGroupOutput", (3, 0)) ############################################################################ - node_color = ng.new_node('ShaderNodeMixRGB', (-1, -1.5)) + node_color = ng.new_node("ShaderNodeMixRGB", (-1, -1.5)) node_color.mute = True - ng.new_input_socket('Color', node_color.inputs['Color1']) - - if bpy.app.version < (2, 80, 0): - node_geo = ng.new_node('ShaderNodeGeometry', (-2, -2.5)) - node_cull = ng.new_math_node('MULTIPLY', (-1, -2.5)) - - ng.links.new(node_geo.outputs['Front/Back'], node_cull.inputs[1]) - - ng.new_input_socket('Alpha', node_cull.inputs[0]) - ng.new_output_socket('Color', node_color.outputs['Color']) - ng.new_output_socket('Alpha', node_cull.outputs['Value']) + ng.new_input_socket("Color", node_color.inputs["Color1"]) ############################################################################ - node_ray = ng.new_node('ShaderNodeLightPath', (-3, 1.5)) - node_geo = ng.new_node('ShaderNodeNewGeometry', (-3, 0)) - node_max = ng.new_math_node('MAXIMUM', (-2, 1.5)) + node_ray = ng.new_node("ShaderNodeLightPath", (-3, 1.5)) + node_geo = ng.new_node("ShaderNodeNewGeometry", (-3, 0)) + node_max = ng.new_math_node("MAXIMUM", (-2, 1.5)) node_max.mute = True - node_gt = ng.new_math_node('GREATER_THAN', (-1, 1)) - node_alpha = ng.new_math_node('MULTIPLY', (0, 1)) - node_trans = ng.new_node('ShaderNodeBsdfTransparent', (0, 0)) - EDGE_NODE_NAME = 'ShaderNodeEmission' if bpy.app.version < (2, 80, 0) else 'ShaderNodeBackground' - node_rgb = ng.new_node(EDGE_NODE_NAME, (0, -0.5)) # BsdfDiffuse/Background/Emission - node_mix = ng.new_node('ShaderNodeMixShader', (1, 0.5)) + node_gt = ng.new_math_node("GREATER_THAN", (-1, 1)) + node_alpha = ng.new_math_node("MULTIPLY", (0, 1)) + node_trans = ng.new_node("ShaderNodeBsdfTransparent", (0, 0)) + node_rgb = ng.new_node("ShaderNodeBackground", (0, -0.5)) + node_mix = ng.new_node("ShaderNodeMixShader", (1, 0.5)) links = ng.links - links.new(node_ray.outputs['Is Camera Ray'], node_max.inputs[0]) - links.new(node_ray.outputs['Is Glossy Ray'], node_max.inputs[1]) - links.new(node_max.outputs['Value'], node_gt.inputs[0]) - links.new(node_geo.outputs['Backfacing'], node_gt.inputs[1]) - links.new(node_gt.outputs['Value'], node_alpha.inputs[0]) - links.new(node_alpha.outputs['Value'], node_mix.inputs['Fac']) - links.new(node_trans.outputs['BSDF'], node_mix.inputs[1]) + links.new(node_ray.outputs["Is Camera Ray"], node_max.inputs[0]) + links.new(node_ray.outputs["Is Glossy Ray"], node_max.inputs[1]) + links.new(node_max.outputs["Value"], node_gt.inputs[0]) + links.new(node_geo.outputs["Backfacing"], node_gt.inputs[1]) + links.new(node_gt.outputs["Value"], node_alpha.inputs[0]) + links.new(node_alpha.outputs["Value"], node_mix.inputs["Fac"]) + links.new(node_trans.outputs["BSDF"], node_mix.inputs[1]) links.new(node_rgb.outputs[0], node_mix.inputs[2]) - links.new(node_color.outputs['Color'], node_rgb.inputs['Color']) + links.new(node_color.outputs["Color"], node_rgb.inputs["Color"]) - ng.new_input_socket('Alpha', node_alpha.inputs[1]) - ng.new_output_socket('Shader', node_mix.outputs['Shader']) + ng.new_input_socket("Alpha", node_alpha.inputs[1]) + ng.new_output_socket("Shader", node_mix.outputs["Shader"]) return shader - diff --git a/mmd_tools/operators/misc.py b/mmd_tools/operators/misc.py index babf18d3..ef6048f1 100644 --- a/mmd_tools/operators/misc.py +++ b/mmd_tools/operators/misc.py @@ -1,54 +1,55 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import re import bpy -from bpy.types import Operator -from mmd_tools import utils -from mmd_tools.bpyutils import ObjectOp -from mmd_tools.core import model as mmd_model -from mmd_tools.core.morph import FnMorph -from mmd_tools.core.material import FnMaterial -from mmd_tools.core.bone import FnBone +from .. import utils +from ..bpyutils import FnContext, FnObject +from ..core.bone import FnBone +from ..core.model import FnModel, Model +from ..core.morph import FnMorph -class SelectObject(Operator): - bl_idname = 'mmd_tools.object_select' - bl_label = 'Select Object' - bl_description = 'Select the object' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class SelectObject(bpy.types.Operator): + bl_idname = "mmd_tools.object_select" + bl_label = "Select Object" + bl_description = "Select the object" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} name: bpy.props.StringProperty( - name='Name', - description='The object name', - default='', - options={'HIDDEN', 'SKIP_SAVE'}, - ) + name="Name", + description="The object name", + default="", + options={"HIDDEN", "SKIP_SAVE"}, + ) def execute(self, context): utils.selectAObject(context.scene.objects[self.name]) - return {'FINISHED'} + return {"FINISHED"} + -class MoveObject(Operator, utils.ItemMoveOp): - bl_idname = 'mmd_tools.object_move' - bl_label = 'Move Object' - bl_description = 'Move active object up/down in the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class MoveObject(bpy.types.Operator, utils.ItemMoveOp): + bl_idname = "mmd_tools.object_move" + bl_label = "Move Object" + bl_description = "Move active object up/down in the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} - __PREFIX_REGEXP = re.compile(r'(?P[0-9A-Z]{3}_)(?P.*)') + __PREFIX_REGEXP = re.compile(r"(?P[0-9A-Z]{3}_)(?P.*)") @classmethod 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) + name = m.group("name") if m else obj.name + obj.name = "%s_%s" % (utils.int2base(index, 36, 3), name) @classmethod 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 + 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): @@ -63,13 +64,13 @@ def execute(self, context): obj = context.active_object objects = self.__get_objects(obj) if obj not in objects: - self.report({ 'ERROR' }, 'Can not move object "%s"'%obj.name) - return { 'CANCELLED' } + self.report({"ERROR"}, 'Can not move object "%s"' % obj.name) + return {"CANCELLED"} objects.sort(key=lambda x: x.name) self.move(objects, objects.index(obj), self.type) self.normalize_indices(objects) - return { 'FINISHED' } + return {"FINISHED"} def __get_objects(self, obj): class __MovableList(list): @@ -79,34 +80,32 @@ def move(self, index_old, index_new): self.insert(index_new, item) objects = [] - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) if root: - rig = mmd_model.Model(root) - if obj.mmd_type == 'NONE' and obj.type == 'MESH': + rig = Model(root) + if obj.mmd_type == "NONE" and obj.type == "MESH": objects = rig.meshes() - elif obj.mmd_type == 'RIGID_BODY': + elif obj.mmd_type == "RIGID_BODY": objects = rig.rigidBodies() - elif obj.mmd_type == 'JOINT': + elif obj.mmd_type == "JOINT": objects = rig.joints() return __MovableList(objects) -class CleanShapeKeys(Operator): - bl_idname = 'mmd_tools.clean_shape_keys' - bl_label = 'Clean Shape Keys' - bl_description = 'Remove unused shape keys of selected mesh objects' - bl_options = {'REGISTER', 'UNDO'} + +class CleanShapeKeys(bpy.types.Operator): + bl_idname = "mmd_tools.clean_shape_keys" + bl_label = "Clean Shape Keys" + bl_description = "Remove unused shape keys of selected mesh objects" + bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): - for obj in context.selected_objects: - if obj.type == 'MESH': - return True - return False + return any(o.type == "MESH" for o in context.selected_objects) @staticmethod def __can_remove(key_block): if key_block.relative_key == key_block: - return False # Basis + return False # Basis for v0, v1 in zip(key_block.relative_key.data, key_block.data): if v0.co != v1.co: return False @@ -115,34 +114,36 @@ def __can_remove(key_block): def __shape_key_clean(self, obj, key_blocks): for kb in key_blocks: if self.__can_remove(kb): - obj.shape_key_remove(kb) + FnObject.mesh_remove_shape_key(obj, kb) if len(key_blocks) == 1: - obj.shape_key_remove(key_blocks[0]) + FnObject.mesh_remove_shape_key(obj, key_blocks[0]) def execute(self, context): - for ob in context.selected_objects: - if ob.type != 'MESH' or ob.data.shape_keys is None: + obj: bpy.types.Object + for obj in context.selected_objects: + if obj.type != "MESH" or obj.data.shape_keys is None: continue - if not ob.data.shape_keys.use_relative: - continue # not be considered yet - self.__shape_key_clean(ObjectOp(ob), ob.data.shape_keys.key_blocks) - return {'FINISHED'} + if not obj.data.shape_keys.use_relative: + continue # not be considered yet + self.__shape_key_clean(obj, obj.data.shape_keys.key_blocks) + return {"FINISHED"} + -class SeparateByMaterials(Operator): - bl_idname = 'mmd_tools.separate_by_materials' - bl_label = 'Separate By Materials' - bl_options = {'REGISTER', 'UNDO'} +class SeparateByMaterials(bpy.types.Operator): + bl_idname = "mmd_tools.separate_by_materials" + bl_label = "Separate By Materials" + bl_options = {"REGISTER", "UNDO"} clean_shape_keys: bpy.props.BoolProperty( - name='Clean Shape Keys', - description='Remove unused shape keys of separated objects', + name="Clean Shape Keys", + description="Remove unused shape keys of separated objects", default=True, - ) + ) @classmethod def poll(cls, context): obj = context.active_object - return obj and obj.type == 'MESH' + return obj and obj.type == "MESH" def __separate_by_materials(self, obj): utils.separateByMaterials(obj) @@ -151,7 +152,7 @@ def __separate_by_materials(self, obj): def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) if root is None: self.__separate_by_materials(obj) else: @@ -159,53 +160,54 @@ def execute(self, context): bpy.ops.mmd_tools.clear_uv_morph_view() # Store the current material names - rig = mmd_model.Model(root) - mat_names = [getattr(mat, 'name', None) for mat in rig.materials()] + rig = Model(root) + mat_names = [getattr(mat, "name", None) for mat in rig.materials()] self.__separate_by_materials(obj) for mesh in rig.meshes(): FnMorph.clean_uv_morph_vertex_groups(mesh) if len(mesh.data.materials) > 0: mat = mesh.data.materials[0] - idx = mat_names.index(getattr(mat, 'name', None)) + idx = mat_names.index(getattr(mat, "name", None)) MoveObject.set_index(mesh, idx) for morph in root.mmd_root.material_morphs: FnMorph(morph, rig).update_mat_related_mesh() utils.clearUnusedMeshes() - return {'FINISHED'} + return {"FINISHED"} -class JoinMeshes(Operator): - bl_idname = 'mmd_tools.join_meshes' - bl_label = 'Join Meshes' - bl_description = 'Join the Model meshes into a single one' - bl_options = {'REGISTER', 'UNDO'} + +class JoinMeshes(bpy.types.Operator): + bl_idname = "mmd_tools.join_meshes" + bl_label = "Join Meshes" + bl_description = "Join the Model meshes into a single one" + bl_options = {"REGISTER", "UNDO"} sort_shape_keys: bpy.props.BoolProperty( - name='Sort Shape Keys', - description='Sort shape keys in the order of vertex morph', + name="Sort Shape Keys", + description="Sort shape keys in the order of vertex morph", default=True, - ) + ) def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) if root is None: - self.report({ 'ERROR' }, 'Select a MMD model') - return { 'CANCELLED' } + self.report({"ERROR"}, "Select a MMD model") + return {"CANCELLED"} bpy.ops.mmd_tools.clear_temp_materials() bpy.ops.mmd_tools.clear_uv_morph_view() # Find all the meshes in mmd_root - rig = mmd_model.Model(root) + rig = Model(root) meshes_list = sorted(rig.meshes(), key=lambda x: x.name) if not meshes_list: - self.report({ 'ERROR' }, 'The model does not have any meshes') - return { 'CANCELLED' } + self.report({"ERROR"}, "The model does not have any meshes") + return {"CANCELLED"} active_mesh = meshes_list[0] - from mmd_tools import bpyutils - bpyutils.select_object(active_mesh, objects=meshes_list) + FnContext.select_objects(context, *meshes_list) + FnContext.set_active_object(context, active_mesh) # Store the current order of the materials for m in meshes_list[1:]: @@ -222,41 +224,41 @@ def execute(self, context): for morph in root.mmd_root.material_morphs: FnMorph(morph, rig).update_mat_related_mesh(active_mesh) utils.clearUnusedMeshes() - return { 'FINISHED' } + return {"FINISHED"} -class AttachMeshesToMMD(Operator): - bl_idname = 'mmd_tools.attach_meshes' - bl_label = 'Attach Meshes to Model' - bl_description = 'Finds existing meshes and attaches them to the selected MMD model' - bl_options = {'REGISTER', 'UNDO'} - add_armature_modifier: bpy.props.BoolProperty( - default=True - ) +class AttachMeshesToMMD(bpy.types.Operator): + bl_idname = "mmd_tools.attach_meshes" + bl_label = "Attach Meshes to Model" + bl_description = "Finds existing meshes and attaches them to the selected MMD model" + bl_options = {"REGISTER", "UNDO"} + + add_armature_modifier: bpy.props.BoolProperty(default=True) def execute(self, context: bpy.types.Context): - root = mmd_model.FnModel.find_root(context.active_object) + root = FnModel.find_root_object(context.active_object) if root is None: - self.report({ 'ERROR' }, 'Select a MMD model') - return { 'CANCELLED' } + self.report({"ERROR"}, "Select a MMD model") + return {"CANCELLED"} - armObj = mmd_model.FnModel.find_armature(root) + armObj = FnModel.find_armature_object(root) if armObj is None: - self.report({ 'ERROR' }, 'Model Armature not found') - return { 'CANCELLED' } + self.report({"ERROR"}, "Model Armature not found") + return {"CANCELLED"} + + FnModel.attach_mesh_objects(root, context.visible_objects, self.add_armature_modifier) + return {"FINISHED"} - mmd_model.FnModel.attach_meshes(root, context.visible_objects, self.add_armature_modifier) - return { 'FINISHED' } -class ChangeMMDIKLoopFactor(Operator): - bl_idname = 'mmd_tools.change_mmd_ik_loop_factor' - bl_label = 'Change MMD IK Loop Factor' +class ChangeMMDIKLoopFactor(bpy.types.Operator): + bl_idname = "mmd_tools.change_mmd_ik_loop_factor" + bl_label = "Change MMD IK Loop Factor" bl_description = "Multiplier for all bones' IK iterations in Blender" - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} mmd_ik_loop_factor: bpy.props.IntProperty( - name='MMD IK Loop Factor', - description='Scaling factor of MMD IK loop', + name="MMD IK Loop Factor", + description="Scaling factor of MMD IK loop", min=1, soft_max=10, max=100, @@ -264,29 +266,30 @@ class ChangeMMDIKLoopFactor(Operator): @classmethod def poll(cls, context): - return mmd_model.FnModel.find_root(context.active_object) is not None + return FnModel.find_root_object(context.active_object) is not None def invoke(self, context, event): - root_object = mmd_model.FnModel.find_root(context.active_object) + 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): - root_object = mmd_model.FnModel.find_root(context.active_object) - mmd_model.FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor) - return { 'FINISHED' } + root_object = FnModel.find_root_object(context.active_object) + FnModel.change_mmd_ik_loop_factor(root_object, self.mmd_ik_loop_factor) + return {"FINISHED"} + -class RecalculateBoneRoll(Operator): - bl_idname = 'mmd_tools.recalculate_bone_roll' - bl_label = 'Recalculate bone roll' - bl_description = 'Recalculate bone roll for arm related bones' - bl_options = {'REGISTER', 'UNDO'} +class RecalculateBoneRoll(bpy.types.Operator): + bl_idname = "mmd_tools.recalculate_bone_roll" + bl_label = "Recalculate bone roll" + bl_description = "Recalculate bone roll for arm related bones" + bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): obj = context.active_object - return obj and obj.type == 'ARMATURE' + return obj and obj.type == "ARMATURE" def invoke(self, context, event): vm = context.window_manager @@ -295,10 +298,10 @@ def invoke(self, context, event): 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.') + 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): arm = context.active_object FnBone.apply_auto_bone_roll(arm) - return { 'FINISHED' } + return {"FINISHED"} diff --git a/mmd_tools/operators/model.py b/mmd_tools/operators/model.py index 5a0b263f..e521f744 100644 --- a/mmd_tools/operators/model.py +++ b/mmd_tools/operators/model.py @@ -1,317 +1,338 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import bpy -import mmd_tools.core.model as mmd_model -from bpy.types import Operator -from mmd_tools.bpyutils import SceneOp, activate_layer_collection -from mmd_tools.core.bone import FnBone +from ..bpyutils import FnContext +from ..core.bone import FnBone, MigrationFnBone +from ..core.model import FnModel, Model -class MorphSliderSetup(Operator): - bl_idname = 'mmd_tools.morph_slider_setup' - bl_label = 'Morph Slider Setup' - bl_description = 'Translate MMD morphs of selected object into format usable by Blender' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class MorphSliderSetup(bpy.types.Operator): + bl_idname = "mmd_tools.morph_slider_setup" + bl_label = "Morph Slider Setup" + bl_description = "Translate MMD morphs of selected object into format usable by Blender" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} type: bpy.props.EnumProperty( - name='Type', - description='Select type', - items = [ - ('CREATE', 'Create', 'Create placeholder object for morph sliders', 'SHAPEKEY_DATA', 0), - ('BIND', 'Bind', 'Bind morph sliders', 'DRIVER', 1), - ('UNBIND', 'Unbind', 'Unbind morph sliders', 'X', 2), - ], - default='CREATE', - ) + name="Type", + description="Select type", + items=[ + ("CREATE", "Create", "Create placeholder object for morph sliders", "SHAPEKEY_DATA", 0), + ("BIND", "Bind", "Bind morph sliders", "DRIVER", 1), + ("UNBIND", "Unbind", "Unbind morph sliders", "X", 2), + ], + default="CREATE", + ) - def execute(self, context): - obj = context.active_object - root = mmd_model.Model.findRoot(context.active_object) + def execute(self, context: bpy.types.Context): + active_object = context.active_object + root_object = FnModel.find_root_object(active_object) + assert root_object is not None - with activate_layer_collection(root): - rig = mmd_model.Model(root) - if self.type == 'BIND': + with FnContext.temp_override_active_layer_collection(context, root_object): + rig = Model(root_object) + if self.type == "BIND": rig.morph_slider.bind() - elif self.type == 'UNBIND': + elif self.type == "UNBIND": rig.morph_slider.unbind() else: rig.morph_slider.create() - SceneOp(context).active_object = obj + FnContext.set_active_object(context, active_object) - return {'FINISHED'} + return {"FINISHED"} -class CleanRiggingObjects(Operator): - bl_idname = 'mmd_tools.clean_rig' - bl_label = 'Clean Rig' - bl_description = 'Delete temporary physics objects of selected object and revert physics to default MMD state' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class CleanRiggingObjects(bpy.types.Operator): + bl_idname = "mmd_tools.clean_rig" + bl_label = "Clean Rig" + bl_description = "Delete temporary physics objects of selected object and revert physics to default MMD state" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): - root = mmd_model.Model.findRoot(context.active_object) - rig = mmd_model.Model(root) + root_object = FnModel.find_root_object(context.active_object) + assert root_object is not None + + rig = Model(root_object) rig.clean() - SceneOp(context).active_object = root - return {'FINISHED'} + FnContext.set_active_object(context, root_object) + return {"FINISHED"} + -class BuildRig(Operator): - bl_idname = 'mmd_tools.build_rig' - bl_label = 'Build Rig' - bl_description = 'Translate physics of selected object into format usable by Blender' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class BuildRig(bpy.types.Operator): + bl_idname = "mmd_tools.build_rig" + bl_label = "Build Rig" + bl_description = "Translate physics of selected object into format usable by Blender" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} non_collision_distance_scale: bpy.props.FloatProperty( - name='Non-Collision Distance Scale', - description='The distance scale for creating extra non-collision constraints while building physics', - min=0, soft_max=10, + name="Non-Collision Distance Scale", + description="The distance scale for creating extra non-collision constraints while building physics", + min=0, + soft_max=10, default=1.5, ) collision_margin: bpy.props.FloatProperty( - name='Collision Margin', - description='The collision margin between rigid bodies. If 0, the default value for each shape is adopted.', - unit='LENGTH', - min=0, soft_max=10, + name="Collision Margin", + description="The collision margin between rigid bodies. If 0, the default value for each shape is adopted.", + unit="LENGTH", + min=0, + soft_max=10, default=1e-06, ) def execute(self, context): - root = mmd_model.Model.findRoot(context.active_object) + root_object = FnModel.find_root_object(context.active_object) - with activate_layer_collection(root): - rig = mmd_model.Model(root) + with FnContext.temp_override_active_layer_collection(context, root_object): + rig = Model(root_object) rig.build(self.non_collision_distance_scale, self.collision_margin) - SceneOp(context).active_object = root + FnContext.set_active_object(context, root_object) + + return {"FINISHED"} - return {'FINISHED'} -class CleanAdditionalTransformConstraints(Operator): - bl_idname = 'mmd_tools.clean_additional_transform' - bl_label = 'Clean Additional Transform' - bl_description = 'Delete shadow bones of selected object and revert bones to default MMD state' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class CleanAdditionalTransformConstraints(bpy.types.Operator): + bl_idname = "mmd_tools.clean_additional_transform" + bl_label = "Clean Additional Transform" + bl_description = "Delete shadow bones of selected object and revert bones to default MMD state" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): - obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) - rig.cleanAdditionalTransformConstraints() - SceneOp(context).active_object = obj - return {'FINISHED'} - -class ApplyAdditionalTransformConstraints(Operator): - bl_idname = 'mmd_tools.apply_additional_transform' - bl_label = 'Apply Additional Transform' - bl_description = 'Translate appended bones of selected object for Blender' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + active_object = context.active_object + root_object = FnModel.find_root_object(active_object) + assert root_object is not None + FnBone.clean_additional_transformation(FnModel.find_armature_object(root_object)) + FnContext.set_active_object(context, active_object) + return {"FINISHED"} + + +class ApplyAdditionalTransformConstraints(bpy.types.Operator): + bl_idname = "mmd_tools.apply_additional_transform" + bl_label = "Apply Additional Transform" + bl_description = "Translate appended bones of selected object for Blender" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): - obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) - rig.applyAdditionalTransformConstraints() - SceneOp(context).active_object = obj - return {'FINISHED'} - -class SetupBoneFixedAxes(Operator): - bl_idname = 'mmd_tools.bone_fixed_axis_setup' - bl_label = 'Setup Bone Fixed Axis' - bl_description = 'Setup fixed axis of selected bones' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + active_object = context.active_object + root_object = FnModel.find_root_object(active_object) + assert root_object is not None + + armature_object = FnModel.find_armature_object(root_object) + assert armature_object is not None + + MigrationFnBone.fix_mmd_ik_limit_override(armature_object) + FnBone.apply_additional_transformation(armature_object) + FnContext.set_active_object(context, active_object) + return {"FINISHED"} + + +class SetupBoneFixedAxes(bpy.types.Operator): + bl_idname = "mmd_tools.bone_fixed_axis_setup" + bl_label = "Setup Bone Fixed Axis" + bl_description = "Setup fixed axis of selected bones" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} type: bpy.props.EnumProperty( - name='Type', - description='Select type', - items = [ - ('DISABLE', 'Disable', 'Disable MMD fixed axis of selected bones', 0), - ('LOAD', 'Load', 'Load/Enable MMD fixed axis of selected bones from their Y-axis or the only rotatable axis', 1), - ('APPLY', 'Apply', 'Align bone axes to MMD fixed axis of each bone', 2), - ], - default='LOAD', - ) + name="Type", + description="Select type", + items=[ + ("DISABLE", "Disable", "Disable MMD fixed axis of selected bones", 0), + ("LOAD", "Load", "Load/Enable MMD fixed axis of selected bones from their Y-axis or the only rotatable axis", 1), + ("APPLY", "Apply", "Align bone axes to MMD fixed axis of each bone", 2), + ], + default="LOAD", + ) def execute(self, context): - arm = context.active_object - if not arm or arm.type != 'ARMATURE': - self.report({'ERROR'}, 'Active object is not an armature object') - return {'CANCELLED'} - - if self.type == 'APPLY': - FnBone.apply_bone_fixed_axis(arm) - FnBone.apply_additional_transformation(arm) + armature_object = context.active_object + if not armature_object or armature_object.type != "ARMATURE": + self.report({"ERROR"}, "Active object is not an armature object") + return {"CANCELLED"} + + if self.type == "APPLY": + FnBone.apply_bone_fixed_axis(armature_object) + FnBone.apply_additional_transformation(armature_object) else: - FnBone.load_bone_fixed_axis(arm, enable=(self.type=='LOAD')) - return {'FINISHED'} + FnBone.load_bone_fixed_axis(armature_object, enable=(self.type == "LOAD")) + return {"FINISHED"} -class SetupBoneLocalAxes(Operator): - bl_idname = 'mmd_tools.bone_local_axes_setup' - bl_label = 'Setup Bone Local Axes' - bl_description = 'Setup local axes of each bone' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class SetupBoneLocalAxes(bpy.types.Operator): + bl_idname = "mmd_tools.bone_local_axes_setup" + bl_label = "Setup Bone Local Axes" + bl_description = "Setup local axes of each bone" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} type: bpy.props.EnumProperty( - name='Type', - description='Select type', - items = [ - ('DISABLE', 'Disable', 'Disable MMD local axes of selected bones', 0), - ('LOAD', 'Load', 'Load/Enable MMD local axes of selected bones from their bone axes', 1), - ('APPLY', 'Apply', 'Align bone axes to MMD local axes of each bone', 2), - ], - default='LOAD', - ) + name="Type", + description="Select type", + items=[ + ("DISABLE", "Disable", "Disable MMD local axes of selected bones", 0), + ("LOAD", "Load", "Load/Enable MMD local axes of selected bones from their bone axes", 1), + ("APPLY", "Apply", "Align bone axes to MMD local axes of each bone", 2), + ], + default="LOAD", + ) def execute(self, context): - arm = context.active_object - if not arm or arm.type != 'ARMATURE': - self.report({'ERROR'}, 'Active object is not an armature object') - return {'CANCELLED'} - - if self.type == 'APPLY': - FnBone.apply_bone_local_axes(arm) - FnBone.apply_additional_transformation(arm) + armature_object = context.active_object + if not armature_object or armature_object.type != "ARMATURE": + self.report({"ERROR"}, "Active object is not an armature object") + return {"CANCELLED"} + + if self.type == "APPLY": + FnBone.apply_bone_local_axes(armature_object) + FnBone.apply_additional_transformation(armature_object) else: - FnBone.load_bone_local_axes(arm, enable=(self.type=='LOAD')) - return {'FINISHED'} + FnBone.load_bone_local_axes(armature_object, enable=(self.type == "LOAD")) + return {"FINISHED"} + -class AddMissingVertexGroupsFromBones(Operator): - bl_idname = 'mmd_tools.add_missing_vertex_groups_from_bones' - bl_label = 'Add Missing Vertex Groups from Bones' - bl_description = 'Add the missing vertex groups to the selected mesh' - bl_options = {'REGISTER', 'UNDO'} +class AddMissingVertexGroupsFromBones(bpy.types.Operator): + bl_idname = "mmd_tools.add_missing_vertex_groups_from_bones" + bl_label = "Add Missing Vertex Groups from Bones" + bl_description = "Add the missing vertex groups to the selected mesh" + bl_options = {"REGISTER", "UNDO"} search_in_all_meshes: bpy.props.BoolProperty( - name='Search in all meshes', - description='Search for vertex groups in all meshes', + name="Search in all meshes", + description="Search for vertex groups in all meshes", default=False, ) @classmethod def poll(cls, context: bpy.types.Context): - return mmd_model.FnModel.find_root(context.active_object) is not None + return FnModel.find_root_object(context.active_object) is not None def execute(self, context: bpy.types.Context): active_object: bpy.types.Object = context.active_object - root_object = mmd_model.FnModel.find_root(active_object) + root_object = FnModel.find_root_object(active_object) + assert root_object is not None - bone_order_mesh_object = mmd_model.FnModel.find_bone_order_mesh_object(root_object) + bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root_object) if bone_order_mesh_object is None: - return {'CANCELLED'} + return {"CANCELLED"} - mmd_model.FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes) + FnModel.add_missing_vertex_groups_from_bones(root_object, bone_order_mesh_object, self.search_in_all_meshes) - return {'FINISHED'} + return {"FINISHED"} -class CreateMMDModelRoot(Operator): - bl_idname = 'mmd_tools.create_mmd_model_root_object' - bl_label = 'Create a MMD Model Root Object' - bl_description = 'Create a MMD model root object with a basic armature' - bl_options = {'REGISTER', 'UNDO'} + +class CreateMMDModelRoot(bpy.types.Operator): + bl_idname = "mmd_tools.create_mmd_model_root_object" + bl_label = "Create a MMD Model Root Object" + bl_description = "Create a MMD model root object with a basic armature" + bl_options = {"REGISTER", "UNDO"} name_j: bpy.props.StringProperty( - name='Name', - description='The name of the MMD model', - default='New MMD Model', - ) + name="Name", + description="The name of the MMD model", + default="New MMD Model", + ) name_e: bpy.props.StringProperty( - name='Name(Eng)', - description='The english name of the MMD model', - default='New MMD Model', - ) + name="Name(Eng)", + description="The english name of the MMD model", + default="New MMD Model", + ) scale: bpy.props.FloatProperty( - name='Scale', - description='Scale', + name="Scale", + description="Scale", default=0.08, - ) + ) def execute(self, context): - rig = mmd_model.Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True) + rig = Model.create(self.name_j, self.name_e, self.scale, add_root_bone=True) rig.initialDisplayFrames() - return {'FINISHED'} + return {"FINISHED"} def invoke(self, context, event): vm = context.window_manager return vm.invoke_props_dialog(self) -class ConvertToMMDModel(Operator): - bl_idname = 'mmd_tools.convert_to_mmd_model' - bl_label = 'Convert to a MMD Model' - bl_description = 'Convert active armature with its meshes to a MMD model (experimental)' - bl_options = {'REGISTER', 'UNDO'} + +class ConvertToMMDModel(bpy.types.Operator): + bl_idname = "mmd_tools.convert_to_mmd_model" + bl_label = "Convert to a MMD Model" + bl_description = "Convert active armature with its meshes to a MMD model (experimental)" + bl_options = {"REGISTER", "UNDO"} ambient_color_source: bpy.props.EnumProperty( - name='Ambient Color Source', - description='Select ambient color source', - items = [ - ('DIFFUSE', 'Diffuse', 'Diffuse color', 0), - ('MIRROR', 'Mirror', 'Mirror color (if property "mirror_color" is available)', 1), - ], - default='DIFFUSE', - ) + name="Ambient Color Source", + description="Select ambient color source", + items=[ + ("DIFFUSE", "Diffuse", "Diffuse color", 0), + ("MIRROR", "Mirror", 'Mirror color (if property "mirror_color" is available)', 1), + ], + default="DIFFUSE", + ) edge_threshold: bpy.props.FloatProperty( - name='Edge Threshold', - description='MMD toon edge will not be enabled if freestyle line color alpha less than this value', + name="Edge Threshold", + description="MMD toon edge will not be enabled if freestyle line color alpha less than this value", min=0, max=1.001, precision=3, step=0.1, default=0.1, - ) + ) edge_alpha_min: bpy.props.FloatProperty( - name='Minimum Edge Alpha', - description='Minimum alpha of MMD toon edge color', + name="Minimum Edge Alpha", + description="Minimum alpha of MMD toon edge color", min=0, max=1, precision=3, step=0.1, default=0.5, - ) + ) scale: bpy.props.FloatProperty( - name='Scale', - description='Scaling factor for converting the model', + name="Scale", + description="Scaling factor for converting the model", default=0.08, - ) + ) convert_material_nodes: bpy.props.BoolProperty( - name='Convert Material Nodes', + name="Convert Material Nodes", default=True, - ) + ) middle_joint_bones_lock: bpy.props.BoolProperty( - name='Middle Joint Bones Lock', - description='Lock specific bones for backward compatibility.', + name="Middle Joint Bones Lock", + description="Lock specific bones for backward compatibility.", default=False, - ) + ) @classmethod def poll(cls, context): obj = context.active_object - return obj and obj.type == 'ARMATURE' and obj.mode != 'EDIT' + return obj and obj.type == "ARMATURE" and obj.mode != "EDIT" def invoke(self, context, event): vm = context.window_manager return vm.invoke_props_dialog(self) def execute(self, context): - #TODO convert some basic MMD properties - armature = context.active_object + # TODO convert some basic MMD properties + armature_object = context.active_object scale = self.scale - model_name = 'New MMD Model' - - root = mmd_model.Model.findRoot(armature) - if root is None or root != armature.parent: - rig = mmd_model.Model.create(model_name, model_name, scale, armature=armature) + model_name = "New MMD Model" - self.__attach_meshes_to(armature, SceneOp(context).id_objects) - self.__configure_rig(context, mmd_model.Model(armature.parent)) - return {'FINISHED'} + root_object = FnModel.find_root_object(armature_object) + if root_object is None or root_object != armature_object.parent: + Model.create(model_name, model_name, scale, armature_object=armature_object) - def __attach_meshes_to(self, armature, objects): + self.__attach_meshes_to(armature_object, FnContext.get_scene_objects(context)) + self.__configure_rig(context, Model(armature_object.parent)) + return {"FINISHED"} + def __attach_meshes_to(self, armature_object: bpy.types.Object, objects: bpy.types.SceneObjects): def __is_child_of_armature(mesh): if mesh.parent is None: return False - return mesh.parent == armature or __is_child_of_armature(mesh.parent) + return mesh.parent == armature_object or __is_child_of_armature(mesh.parent) def __is_using_armature(mesh): for m in mesh.modifiers: - if m.type =='ARMATURE' and m.object == armature: + if m.type == "ARMATURE" and m.object == armature_object: return True return False @@ -324,77 +345,81 @@ def __get_root(mesh): if __is_using_armature(x) and not __is_child_of_armature(x): x_root = __get_root(x) m = x_root.matrix_world - x_root.parent_type = 'OBJECT' - x_root.parent = armature + x_root.parent_type = "OBJECT" + x_root.parent = armature_object x_root.matrix_world = m - def __configure_rig(self, context, rig): - root = rig.rootObject() - armature = rig.armature() - meshes = tuple(rig.meshes()) + def __configure_rig(self, context: bpy.types.Context, mmd_model: Model): + root_object = mmd_model.rootObject() + armature_object = mmd_model.armature() + mesh_objects = tuple(mmd_model.meshes()) - rig.loadMorphs() + mmd_model.loadMorphs() if self.middle_joint_bones_lock: - vertex_groups = {g.name for mesh in meshes for g in mesh.vertex_groups} - for pose_bone in armature.pose.bones: + vertex_groups = {g.name for mesh in mesh_objects for g in mesh.vertex_groups} + for pose_bone in armature_object.pose.bones: if not pose_bone.parent: continue if not pose_bone.bone.use_connect and pose_bone.name not in vertex_groups: continue pose_bone.lock_location = (True, True, True) - from mmd_tools.core.material import FnMaterial + from ..core.material import FnMaterial + FnMaterial.set_nodes_are_readonly(not self.convert_material_nodes) try: - for m in {x for mesh in meshes for x in mesh.data.materials if x}: + for m in (x for mesh in mesh_objects for x in mesh.data.materials if x): FnMaterial.convert_to_mmd_material(m, context) mmd_material = m.mmd_material - if self.ambient_color_source == 'MIRROR' and hasattr(m, 'mirror_color'): + if self.ambient_color_source == "MIRROR" and hasattr(m, "mirror_color"): mmd_material.ambient_color = m.mirror_color else: - mmd_material.ambient_color = [0.5*c for c in mmd_material.diffuse_color] + mmd_material.ambient_color = [0.5 * c for c in mmd_material.diffuse_color] - if hasattr(m, 'line_color'): # freestyle line color + if hasattr(m, "line_color"): # freestyle line color line_color = list(m.line_color) mmd_material.enabled_toon_edge = line_color[3] >= self.edge_threshold mmd_material.edge_color = line_color[:3] + [max(line_color[3], self.edge_alpha_min)] finally: FnMaterial.set_nodes_are_readonly(False) - from mmd_tools.operators.display_item import DisplayItemQuickSetup - DisplayItemQuickSetup.load_bone_groups(root.mmd_root, armature) - rig.initialDisplayFrames(reset=False) # ensure default frames - DisplayItemQuickSetup.load_facial_items(root.mmd_root) - root.mmd_root.active_display_item_frame = 0 + from .display_item import DisplayItemQuickSetup + + FnBone.sync_display_item_frames_from_bone_collections(armature_object) + mmd_model.initialDisplayFrames(reset=False) # ensure default frames + DisplayItemQuickSetup.load_facial_items(root_object.mmd_root) + root_object.mmd_root.active_display_item_frame = 0 + class ResetObjectVisibility(bpy.types.Operator): - bl_idname = 'mmd_tools.reset_object_visibility' - bl_label = 'Reset Object Visivility' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.reset_object_visibility" + bl_label = "Reset Object Visivility" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod def poll(cls, context: bpy.types.Context): active_object: bpy.types.Object = context.active_object - return mmd_model.Model.findRoot(active_object) is not None + return FnModel.find_root_object(active_object) is not None def execute(self, context: bpy.types.Context): active_object: bpy.types.Object = context.active_object - mmd_root_object = mmd_model.Model.findRoot(active_object) + mmd_root_object = FnModel.find_root_object(active_object) + assert mmd_root_object is not None mmd_root = mmd_root_object.mmd_root - mmd_root_object.hide = False + mmd_root_object.hide_set(False) - rigid_group_object = mmd_model.FnModel.find_rigid_group(mmd_root_object) + rigid_group_object = FnModel.find_rigid_group_object(mmd_root_object) if rigid_group_object: - rigid_group_object.hide = True + rigid_group_object.hide_set(True) - joint_group_object = mmd_model.FnModel.find_joint_group(mmd_root_object) + joint_group_object = FnModel.find_joint_group_object(mmd_root_object) if joint_group_object: - joint_group_object.hide = True + joint_group_object.hide_set(True) - temporary_group_object = mmd_model.FnModel.find_temporary_group(mmd_root_object) + temporary_group_object = FnModel.find_temporary_group_object(mmd_root_object) if temporary_group_object: - temporary_group_object.hide = True + temporary_group_object.hide_set(True) mmd_root.show_meshes = True mmd_root.show_armature = True @@ -404,49 +429,55 @@ def execute(self, context: bpy.types.Context): mmd_root.show_joints = False mmd_root.show_names_of_joints = False - return {'FINISHED'} + return {"FINISHED"} -class AssembleAll(Operator): - bl_idname = 'mmd_tools.assemble_all' - bl_label = 'Assemble All' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class AssembleAll(bpy.types.Operator): + bl_idname = "mmd_tools.assemble_all" + bl_label = "Assemble All" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): active_object = context.active_object - root_object = mmd_model.Model.findRoot(active_object) - - with activate_layer_collection(root_object): - rig = mmd_model.Model(root_object) + root_object = FnModel.find_root_object(active_object) + assert root_object is not None - rig.applyAdditionalTransformConstraints() + with FnContext.temp_override_active_layer_collection(context, root_object) as context: + rig = Model(root_object) + MigrationFnBone.fix_mmd_ik_limit_override(rig.armature()) + FnBone.apply_additional_transformation(rig.armature()) rig.build() rig.morph_slider.bind() - bpy.ops.mmd_tools.sdef_bind({'selected_objects': [active_object]}) + with context.temp_override(selected_objects=[active_object]): + bpy.ops.mmd_tools.sdef_bind() root_object.mmd_root.use_property_driver = True - SceneOp(context).active_object = active_object + FnContext.set_active_object(context, active_object) + + return {"FINISHED"} - return {'FINISHED'} -class DisassembleAll(Operator): - bl_idname = 'mmd_tools.disassemble_all' - bl_label = 'Disassemble All' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class DisassembleAll(bpy.types.Operator): + bl_idname = "mmd_tools.disassemble_all" + bl_label = "Disassemble All" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): active_object = context.active_object - root_object = mmd_model.Model.findRoot(active_object) - - with activate_layer_collection(root_object): - rig = mmd_model.Model(root_object) + root_object = FnModel.find_root_object(active_object) + assert root_object is not None + with FnContext.temp_override_active_layer_collection(context, root_object) as context: root_object.mmd_root.use_property_driver = False - bpy.ops.mmd_tools.sdef_unbind({'selected_objects': [active_object]}) + with context.temp_override(selected_objects=[active_object]): + bpy.ops.mmd_tools.sdef_unbind() + + rig = Model(root_object) rig.morph_slider.unbind() rig.clean() - rig.cleanAdditionalTransformConstraints() + FnBone.clean_additional_transformation(rig.armature()) - SceneOp(context).active_object = active_object + FnContext.set_active_object(context, active_object) - return {'FINISHED'} + return {"FINISHED"} diff --git a/mmd_tools/operators/model_edit.py b/mmd_tools/operators/model_edit.py index a23bfc65..913dffe8 100644 --- a/mmd_tools/operators/model_edit.py +++ b/mmd_tools/operators/model_edit.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# Copyright 2022 MMD Tools authors +# This file is part of MMD Tools. import itertools from operator import itemgetter @@ -6,8 +8,9 @@ import bmesh import bpy -from mmd_tools.bpyutils import activate_layer_collection, find_user_layer_collection -from mmd_tools.core.model import FnModel, Model + +from ..bpyutils import FnContext +from ..core.model import FnModel, Model class MessageException(Exception): @@ -15,33 +18,33 @@ class MessageException(Exception): class ModelJoinByBonesOperator(bpy.types.Operator): - bl_idname = 'mmd_tools.model_join_by_bones' - bl_label = 'Model Join by Bones' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.model_join_by_bones" + bl_label = "Model Join by Bones" + bl_options = {"REGISTER", "UNDO"} join_type: bpy.props.EnumProperty( - name='Join Type', + name="Join Type", items=[ - ('CONNECTED', 'Connected', ''), - ('OFFSET', 'Keep Offset', ''), + ("CONNECTED", "Connected", ""), + ("OFFSET", "Keep Offset", ""), ], - default='OFFSET', + default="OFFSET", ) @classmethod def poll(cls, context: bpy.types.Context): active_object: Optional[bpy.types.Object] = context.active_object - if context.mode != 'POSE': + if context.mode != "POSE": return False if active_object is None: return False - if active_object.type != 'ARMATURE': + if active_object.type != "ARMATURE": return False - if len(list(filter(lambda o: o.type == 'ARMATURE', context.selected_objects))) < 2: + if len(list(filter(lambda o: o.type == "ARMATURE", context.selected_objects))) < 2: return False return len(context.selected_pose_bones) > 0 @@ -53,29 +56,29 @@ def execute(self, context: bpy.types.Context): try: self.join(context) except MessageException as ex: - self.report(type={'ERROR'}, message=str(ex)) - return {'CANCELLED'} + self.report(type={"ERROR"}, message=str(ex)) + return {"CANCELLED"} - return {'FINISHED'} + return {"FINISHED"} def join(self, context: bpy.types.Context): - bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode="OBJECT") - parent_root_object = FnModel.find_root(context.active_object) - child_root_objects = {FnModel.find_root(o) for o in context.selected_objects} + parent_root_object = FnModel.find_root_object(context.active_object) + child_root_objects = {FnModel.find_root_object(o) for o in context.selected_objects} 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") - with activate_layer_collection(parent_root_object): + with FnContext.temp_override_active_layer_collection(context, parent_root_object): FnModel.join_models(parent_root_object, child_root_objects) - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.armature.parent_set(type='OFFSET') + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.armature.parent_set(type="OFFSET") # Connect child bones - if self.join_type == 'CONNECTED': + if self.join_type == "CONNECTED": 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) @@ -84,40 +87,40 @@ def join(self, context: bpy.types.Context): for child_edit_bone in child_edit_bones: child_edit_bone.use_connect = True - bpy.ops.object.mode_set(mode='POSE') + bpy.ops.object.mode_set(mode="POSE") class ModelSeparateByBonesOperator(bpy.types.Operator): - bl_idname = 'mmd_tools.model_separate_by_bones' - bl_label = 'Model Separate by Bones' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.model_separate_by_bones" + bl_label = "Model Separate by Bones" + bl_options = {"REGISTER", "UNDO"} - separate_armature: bpy.props.BoolProperty(name='Separate Armature', default=True) - include_descendant_bones: bpy.props.BoolProperty(name='Include Descendant Bones', default=True) - weight_threshold: bpy.props.FloatProperty(name='Weight Threshold', default=0.001, min=0.0, max=1.0, precision=4, subtype='FACTOR') + separate_armature: bpy.props.BoolProperty(name="Separate Armature", default=True) + include_descendant_bones: bpy.props.BoolProperty(name="Include Descendant Bones", default=True) + weight_threshold: bpy.props.FloatProperty(name="Weight Threshold", default=0.001, min=0.0, max=1.0, precision=4, subtype="FACTOR") boundary_joint_owner: bpy.props.EnumProperty( - name='Boundary Joint Owner', + name="Boundary Joint Owner", items=[ - ('SOURCE', 'Source Model', ''), - ('DESTINATION', 'Destination Model', ''), + ("SOURCE", "Source Model", ""), + ("DESTINATION", "Destination Model", ""), ], - default='DESTINATION', + default="DESTINATION", ) @classmethod def poll(cls, context: bpy.types.Context): active_object: Optional[bpy.types.Object] = context.active_object - if context.mode != 'POSE': + if context.mode != "POSE": return False if active_object is None: return False - if active_object.type != 'ARMATURE': + if active_object.type != "ARMATURE": return False - if FnModel.find_root(active_object) is None: + if FnModel.find_root_object(active_object) is None: return False return len(context.selected_pose_bones) > 0 @@ -129,10 +132,10 @@ def execute(self, context: bpy.types.Context): try: self.separate(context) except MessageException as ex: - self.report(type={'ERROR'}, message=str(ex)) - return {'CANCELLED'} + self.report(type={"ERROR"}, message=str(ex)) + return {"CANCELLED"} - return {'FINISHED'} + return {"FINISHED"} def separate(self, context: bpy.types.Context): weight_threshold: float = self.weight_threshold @@ -140,17 +143,18 @@ def separate(self, context: bpy.types.Context): target_armature_object: bpy.types.Object = context.active_object - bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.object.mode_set(mode="EDIT") root_bones: Set[bpy.types.EditBone] = set(context.selected_bones) if self.include_descendant_bones: for edit_bone in root_bones: - bpy.ops.armature.select_similar({'active_bone': edit_bone}, type='CHILDREN', threshold=0.1) + with context.temp_override(active_bone=edit_bone): + bpy.ops.armature.select_similar(type="CHILDREN", threshold=0.1) 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(context.active_object) + mmd_root_object: bpy.types.Object = FnModel.find_root_object(context.active_object) mmd_model = Model(mmd_root_object) mmd_model_mesh_objects: List[bpy.types.Object] = list(mmd_model.meshes()) @@ -162,25 +166,23 @@ def separate(self, context: bpy.types.Context): 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) - bpy.ops.object.mode_set(mode='OBJECT') + 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 - } + 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 + boundary_joint_owner_condition = any if self.boundary_joint_owner == "DESTINATION" else all # 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, - ]) + 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, + ] + ) } separate_mesh_objects: Set[bpy.types.Object] @@ -196,19 +198,14 @@ def separate(self, context: bpy.types.Context): context.view_layer.objects.active = mmd_model_mesh_objects[0] # separate mesh by selected vertices - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.separate(type='SELECTED') - separate_mesh_objects: List[bpy.types.Object] = [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') + bpy.ops.object.mode_set(mode="EDIT") + bpy.ops.mesh.separate(type="SELECTED") + separate_mesh_objects: List[bpy.types.Object] = [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") model2separate_mesh_objects = dict(zip(mmd_model_mesh_objects, separate_mesh_objects)) - separate_model: Model = Model.create( - mmd_root_object.mmd_root.name, - mmd_root_object.mmd_root.name_e, - mmd_scale, - add_root_bone=False - ) + separate_model: Model = Model.create(mmd_root_object.mmd_root.name, mmd_root_object.mmd_root.name_e, mmd_scale, add_root_bone=False) separate_model.initialDisplayFrames() separate_root_object = separate_model.rootObject() @@ -216,38 +213,46 @@ def separate(self, context: bpy.types.Context): separate_model_armature_object = separate_model.armature() if self.separate_armature: - bpy.ops.object.join({ - 'active_object': separate_model_armature_object, - 'selected_editable_objects': [separate_model_armature_object, separate_armature_object], - }) + with context.temp_override( + active_object=separate_model_armature_object, + selected_editable_objects=[separate_model_armature_object, separate_armature_object], + ): + bpy.ops.object.join() # add mesh - bpy.ops.object.parent_set({ - 'object': separate_model_armature_object, - 'selected_editable_objects': [separate_model_armature_object, *separate_mesh_objects], - }, type='OBJECT', keep_transform=True) + 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) # 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) + 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: - armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new('mmd_bone_order_override', 'ARMATURE') + armature_modifier: bpy.types.ArmatureModifier = separate_mesh.modifiers.new("mmd_bone_order_override", "ARMATURE") armature_modifier.object = separate_model_armature_object - bpy.ops.object.parent_set({ - 'object': separate_model.rigidGroupObject(), - 'selected_editable_objects': [separate_model.rigidGroupObject(), *separate_rigid_bodies], - }, type='OBJECT', keep_transform=True) + 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) - bpy.ops.object.parent_set({ - 'object': separate_model.jointGroupObject(), - 'selected_editable_objects': [separate_model.jointGroupObject(), *separate_joints], - }, type='OBJECT', keep_transform=True) + 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) # move separate objects to new collection - mmd_layer_collection = find_user_layer_collection(mmd_root_object) - separate_layer_collection = find_user_layer_collection(separate_root_object) + mmd_layer_collection = FnContext.find_user_layer_collection_by_object(context, mmd_root_object) + assert mmd_layer_collection is not None + + separate_layer_collection = FnContext.find_user_layer_collection_by_object(context, separate_root_object) + assert separate_layer_collection is not None + if mmd_layer_collection.name != 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) @@ -259,8 +264,8 @@ def separate(self, context: bpy.types.Context): 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()} - } + "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[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]: @@ -271,7 +276,7 @@ def select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object mesh: bpy.types.Mesh = mesh_object.data target_bmesh.from_mesh(mesh, face_normals=False) - target_bmesh.select_mode |= {'VERT'} + target_bmesh.select_mode |= {"VERT"} deform_layer = target_bmesh.verts.layers.deform.verify() selected_vertex_count = 0 @@ -280,11 +285,7 @@ def select_weighted_vertices(self, mmd_model_mesh_objects: List[bpy.types.Object vert.select_set(False) # Find the largest weight vertex group - weights = [ - (group_index, weight) - for group_index, weight in vert[deform_layer].items() - if vertex_groups[group_index].name in deform_bones - ] + weights = [(group_index, weight) for group_index, weight in vert[deform_layer].items() if vertex_groups[group_index].name in deform_bones] weights.sort(key=lambda i: vertex_groups[i[0]].name in separate_bones, reverse=True) weights.sort(key=itemgetter(1), reverse=True) diff --git a/mmd_tools/operators/morph.py b/mmd_tools/operators/morph.py index 932e067c..aa0422aa 100644 --- a/mmd_tools/operators/morph.py +++ b/mmd_tools/operators/morph.py @@ -1,17 +1,21 @@ # -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. + +from typing import Optional, cast import bpy -import mmd_tools.core.model as mmd_model -from bpy.types import Operator from mathutils import Quaternion, Vector -from mmd_tools import bpyutils, utils -from mmd_tools.core.exceptions import DivisionError, MaterialNotFoundError -from mmd_tools.core.material import FnMaterial -from mmd_tools.core.morph import FnMorph -from mmd_tools.utils import ItemMoveOp, ItemOp + +from ..core.model import FnModel +from .. import bpyutils, utils +from ..core.exceptions import MaterialNotFoundError +from ..core.material import FnMaterial +from ..core.morph import FnMorph +from ..utils import ItemMoveOp, ItemOp -#Util functions +# Util functions def divide_vector_components(vec1, vec2): if len(vec1) != len(vec2): raise ValueError("Vectors should have the same number of components") @@ -19,72 +23,73 @@ def divide_vector_components(vec1, vec2): for v1, v2 in zip(vec1, vec2): if v2 == 0: if v1 == 0: - v2 = 1 #If we have a 0/0 case we change the divisor to 1 + v2 = 1 # If we have a 0/0 case we change the divisor to 1 else: - raise DivisionError("Invalid Input: a non-zero value can't be divided by zero") - result.append(v1/v2) + raise ZeroDivisionError("Invalid Input: a non-zero value can't be divided by zero") + result.append(v1 / v2) return result + 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): - result.append(v1*v2) + result.append(v1 * v2) return result + def special_division(n1, n2): - """This function returns 0 in case of 0/0. If non-zero divided by zero case is found, an Exception is raised - """ + """This function returns 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 else: - raise DivisionError("Invalid Input: a non-zero value can't be divided by zero") - return n1/n2 + raise ZeroDivisionError("Invalid Input: a non-zero value can't be divided by zero") + return n1 / n2 -class AddMorph(Operator): - bl_idname = 'mmd_tools.morph_add' - bl_label = 'Add Morph' - bl_description = 'Add a morph item to active morph list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class AddMorph(bpy.types.Operator): + bl_idname = "mmd_tools.morph_add" + bl_label = "Add Morph" + bl_description = "Add a morph item to active morph list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph_type = mmd_root.active_morph_type morphs = getattr(mmd_root, morph_type) morph, mmd_root.active_morph = ItemOp.add_after(morphs, mmd_root.active_morph) - morph.name = 'New Morph' - if morph_type.startswith('uv'): - morph.data_type = 'VERTEX_GROUP' - return {'FINISHED'} + morph.name = "New Morph" + if morph_type.startswith("uv"): + morph.data_type = "VERTEX_GROUP" + return {"FINISHED"} -class RemoveMorph(Operator): - bl_idname = 'mmd_tools.morph_remove' - bl_label = 'Remove Morph' - bl_description = 'Remove morph item(s) from the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class RemoveMorph(bpy.types.Operator): + bl_idname = "mmd_tools.morph_remove" + bl_label = "Remove Morph" + bl_description = "Remove morph item(s) from the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} all: bpy.props.BoolProperty( - name='All', - description='Delete all morph items', + name="All", + description="Delete all morph items", default=False, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph_type = mmd_root.active_morph_type - if morph_type.startswith('material'): + if morph_type.startswith("material"): bpy.ops.mmd_tools.clear_temp_materials() - elif morph_type.startswith('uv'): + elif morph_type.startswith("uv"): bpy.ops.mmd_tools.clear_uv_morph_view() morphs = getattr(mmd_root, morph_type) @@ -93,208 +98,212 @@ def execute(self, context): mmd_root.active_morph = 0 else: morphs.remove(mmd_root.active_morph) - mmd_root.active_morph = max(0, mmd_root.active_morph-1) - return {'FINISHED'} + mmd_root.active_morph = max(0, mmd_root.active_morph - 1) + return {"FINISHED"} + -class MoveMorph(Operator, ItemMoveOp): - bl_idname = 'mmd_tools.morph_move' - bl_label = 'Move Morph' - bl_description = 'Move active morph item up/down in the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +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_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root mmd_root.active_morph = self.move( getattr(mmd_root, mmd_root.active_morph_type), mmd_root.active_morph, self.type, - ) - return {'FINISHED'} + ) + return {"FINISHED"} + -class CopyMorph(Operator): - bl_idname = 'mmd_tools.morph_copy' - bl_label = 'Copy Morph' - bl_description = 'Make a copy of active morph in the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class CopyMorph(bpy.types.Operator): + bl_idname = "mmd_tools.morph_copy" + bl_label = "Copy Morph" + bl_description = "Make a copy of active morph in the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) + assert root is not None mmd_root = root.mmd_root morph_type = mmd_root.active_morph_type morphs = getattr(mmd_root, morph_type) morph = ItemOp.get_by_index(morphs, mmd_root.active_morph) if morph is None: - return {'CANCELLED'} + return {"CANCELLED"} - name_orig, name_tmp = morph.name, '_tmp%s'%str(morph.as_pointer()) + name_orig, name_tmp = morph.name, "_tmp%s" % str(morph.as_pointer()) - if morph_type.startswith('vertex'): - for obj in mmd_model.Model(root).meshes(): + if morph_type.startswith("vertex"): + for obj in FnModel.iterate_mesh_objects(root): FnMorph.copy_shape_key(obj, name_orig, name_tmp) - elif morph_type.startswith('uv'): - if morph.data_type == 'VERTEX_GROUP': - for obj in mmd_model.Model(root).meshes(): + elif morph_type.startswith("uv"): + if morph.data_type == "VERTEX_GROUP": + for obj in FnModel.iterate_mesh_objects(root): FnMorph.copy_uv_morph_vertex_groups(obj, name_orig, name_tmp) morph_new, mmd_root.active_morph = ItemOp.add_after(morphs, mmd_root.active_morph) 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 - return {'FINISHED'} + morph_new[k] = v if k != "name" else name_tmp + morph_new.name = name_orig + "_copy" # trigger name check + return {"FINISHED"} -class OverwriteBoneMorphsFromPoseLibrary(Operator): - bl_idname = 'mmd_tools.morph_overwrite_from_active_pose_library' - bl_label = 'Overwrite Bone Morphs from active Pose Library' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class OverwriteBoneMorphsFromPoseLibrary(bpy.types.Operator): + bl_idname = "mmd_tools.morph_overwrite_from_active_pose_library" + bl_label = "Overwrite Bone Morphs from active Pose Library" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod def poll(cls, context): - root = mmd_model.Model.findRoot(context.active_object) + root = FnModel.find_root_object(context.active_object) if root is None: return False - return root.mmd_root.active_morph_type == 'bone_morphs' + return root.mmd_root.active_morph_type == "bone_morphs" def execute(self, context): - root = mmd_model.Model.findRoot(context.active_object) - FnMorph.overwrite_bone_morphs_from_pose_library(mmd_model.FnModel.find_armature(root)) + root = FnModel.find_root_object(context.active_object) + FnMorph.overwrite_bone_morphs_from_pose_library(FnModel.find_armature_object(root)) - return {'FINISHED'} + return {"FINISHED"} -class AddMorphOffset(Operator): - bl_idname = 'mmd_tools.morph_offset_add' - bl_label = 'Add Morph Offset' - bl_description = 'Add a morph offset item to the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class AddMorphOffset(bpy.types.Operator): + bl_idname = "mmd_tools.morph_offset_add" + bl_label = "Add Morph Offset" + bl_description = "Add a morph offset item to the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph_type = mmd_root.active_morph_type morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph) if morph is None: - return {'CANCELLED'} + return {"CANCELLED"} item, morph.active_data = ItemOp.add_after(morph.data, morph.active_data) - if morph_type.startswith('material'): - if obj.type == 'MESH' and obj.mmd_type == 'NONE': + if morph_type.startswith("material"): + if obj.type == "MESH" and obj.mmd_type == "NONE": item.related_mesh = obj.data.name active_material = obj.active_material - if active_material and '_temp' not in active_material.name: + if active_material and "_temp" not in active_material.name: item.material = active_material.name - elif morph_type.startswith('bone'): + elif morph_type.startswith("bone"): pose_bone = context.active_pose_bone if pose_bone: item.bone = pose_bone.name item.location = pose_bone.location item.rotation = pose_bone.rotation_quaternion - return { 'FINISHED' } + return {"FINISHED"} -class RemoveMorphOffset(Operator): - bl_idname = 'mmd_tools.morph_offset_remove' - bl_label = 'Remove Morph Offset' - bl_description = 'Remove morph offset item(s) from the list' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class RemoveMorphOffset(bpy.types.Operator): + bl_idname = "mmd_tools.morph_offset_remove" + bl_label = "Remove Morph Offset" + bl_description = "Remove morph offset item(s) from the list" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} all: bpy.props.BoolProperty( - name='All', - description='Delete all morph offset items', + name="All", + description="Delete all morph offset items", default=False, - options={'SKIP_SAVE'}, - ) + options={"SKIP_SAVE"}, + ) def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) + assert root is not None mmd_root = root.mmd_root morph_type = mmd_root.active_morph_type morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph) if morph is None: - return {'CANCELLED'} + return {"CANCELLED"} - if morph_type.startswith('material'): + if morph_type.startswith("material"): bpy.ops.mmd_tools.clear_temp_materials() if self.all: - if morph_type.startswith('vertex'): - for obj in mmd_model.Model(root).meshes(): + if morph_type.startswith("vertex"): + for obj in FnModel.iterate_mesh_objects(root): FnMorph.remove_shape_key(obj, morph.name) - return {'FINISHED'} - elif morph_type.startswith('uv'): - if morph.data_type == 'VERTEX_GROUP': - for obj in mmd_model.Model(root).meshes(): + return {"FINISHED"} + elif 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) - return {'FINISHED'} + return {"FINISHED"} morph.data.clear() morph.active_data = 0 else: morph.data.remove(morph.active_data) - morph.active_data = max(0, morph.active_data-1) - return { 'FINISHED' } + morph.active_data = max(0, morph.active_data - 1) + return {"FINISHED"} -class InitMaterialOffset(Operator): - bl_idname = 'mmd_tools.material_morph_offset_init' - bl_label = 'Init Material Offset' - bl_description = 'Set all offset values to target value' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class InitMaterialOffset(bpy.types.Operator): + bl_idname = "mmd_tools.material_morph_offset_init" + bl_label = "Init Material Offset" + bl_description = "Set all offset values to target value" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} target_value: bpy.props.FloatProperty( - name='Target Value', - description='Target value', + name="Target Value", + description="Target value", default=0, - ) + ) def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph = mmd_root.material_morphs[mmd_root.active_morph] mat_data = morph.data[morph.active_data] val = self.target_value - mat_data.diffuse_color = mat_data.edge_color = (val,)*4 - mat_data.specular_color = mat_data.ambient_color = (val,)*3 + mat_data.diffuse_color = mat_data.edge_color = (val,) * 4 + 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 - return {'FINISHED'} + mat_data.texture_factor = mat_data.toon_texture_factor = mat_data.sphere_texture_factor = (val,) * 4 + return {"FINISHED"} -class ApplyMaterialOffset(Operator): - bl_idname = 'mmd_tools.apply_material_morph_offset' - bl_label = 'Apply Material Offset' - bl_description = 'Calculates the offsets and apply them, then the temporary material is removed' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class ApplyMaterialOffset(bpy.types.Operator): + bl_idname = "mmd_tools.apply_material_morph_offset" + bl_label = "Apply Material Offset" + bl_description = "Calculates the offsets and apply them, then the temporary material is removed" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph = mmd_root.material_morphs[mmd_root.active_morph] mat_data = morph.data[morph.active_data] - meshObj = rig.findMesh(mat_data.related_mesh) + meshObj = FnModel.find_mesh_object_by_name(mat_data.related_mesh) if meshObj is None: - self.report({ 'ERROR' }, "The model mesh can't be found") - return { 'CANCELLED' } + self.report({"ERROR"}, "The model mesh can't be found") + return {"CANCELLED"} try: - work_mat_name = mat_data.material + '_temp' - work_mat, base_mat = FnMaterial.swap_materials(meshObj, work_mat_name, - mat_data.material) + work_mat_name = mat_data.material + "_temp" + work_mat, base_mat = FnMaterial.swap_materials(meshObj, work_mat_name, mat_data.material) except MaterialNotFoundError: - self.report({ 'ERROR' }, "Material not found") - return { 'CANCELLED' } + self.report({"ERROR"}, "Material not found") + return {"CANCELLED"} base_mmd_mat = base_mat.mmd_material work_mmd_mat = work_mat.mmd_material @@ -311,10 +320,10 @@ def execute(self, context): mat_data.edge_color = edge_offset mat_data.edge_weight = special_division(work_mmd_mat.edge_weight, base_mmd_mat.edge_weight) - except DivisionError: - mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD + except ZeroDivisionError: + mat_data.offset_type = "ADD" # If there is any 0 division we automatically switch it to type ADD except ValueError: - self.report({ 'ERROR' }, 'An unexpected error happened') + self.report({"ERROR"}, "An unexpected error happened") # We should stop on our tracks and re-raise the exception raise @@ -330,36 +339,36 @@ def execute(self, context): 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) - return { 'FINISHED' } + return {"FINISHED"} + -class CreateWorkMaterial(Operator): - bl_idname = 'mmd_tools.create_work_material' - bl_label = 'Create Work Material' - bl_description = 'Creates a temporary material to edit this offset' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class CreateWorkMaterial(bpy.types.Operator): + bl_idname = "mmd_tools.create_work_material" + bl_label = "Create Work Material" + bl_description = "Creates a temporary material to edit this offset" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root morph = mmd_root.material_morphs[mmd_root.active_morph] mat_data = morph.data[morph.active_data] - meshObj = rig.findMesh(mat_data.related_mesh) + meshObj = FnModel.find_mesh_object_by_name(mat_data.related_mesh) if meshObj is None: - self.report({ 'ERROR' }, "The model mesh can't be found") - return { 'CANCELLED' } + self.report({"ERROR"}, "The model mesh can't be found") + return {"CANCELLED"} 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) - return { 'CANCELLED' } + self.report({"ERROR"}, 'Material "%s" not found' % mat_data.material) + return {"CANCELLED"} - work_mat_name = base_mat.name + '_temp' + 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) - return { 'CANCELLED' } + self.report({"ERROR"}, 'Temporary material "%s" is in use' % work_mat_name) + return {"CANCELLED"} work_mat = base_mat.copy() work_mat.name = work_mat_name @@ -395,81 +404,86 @@ def execute(self, context): work_mmd_mat.edge_color = list(edge_offset) work_mmd_mat.edge_weight += mat_data.edge_weight - return { 'FINISHED' } + return {"FINISHED"} -class ClearTempMaterials(Operator): - bl_idname = 'mmd_tools.clear_temp_materials' - bl_label = 'Clear Temp Materials' - bl_description = 'Clears all the temporary materials' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class ClearTempMaterials(bpy.types.Operator): + bl_idname = "mmd_tools.clear_temp_materials" + bl_label = "Clear Temp Materials" + bl_description = "Clears all the temporary materials" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) - for meshObj in rig.meshes(): + root = FnModel.find_root_object(obj) + assert root is not None + for meshObj in FnModel.iterate_mesh_objects(root): + def __pre_remove(m): - if m and '_temp' in m.name: - base_mat_name = m.name.split('_temp')[0] + 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"}, "Base material for %s was not found" % m.name) return False + FnMaterial.clean_materials(meshObj, can_remove=__pre_remove) - return { 'FINISHED' } + return {"FINISHED"} + -class ViewBoneMorph(Operator): - bl_idname = 'mmd_tools.view_bone_morph' - bl_label = 'View Bone Morph' - bl_description = 'View the result of active bone morph' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class ViewBoneMorph(bpy.types.Operator): + bl_idname = "mmd_tools.view_bone_morph" + bl_label = "View Bone Morph" + bl_description = "View the result of active bone morph" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): - from mmd_tools.bpyutils import matmul obj = context.active_object - root = mmd_model.Model.findRoot(obj) - mmd_root=root.mmd_root - rig = mmd_model.Model(root) - armature = rig.armature() + root = FnModel.find_root_object(obj) + assert root is not None + mmd_root = root.mmd_root + armature = FnModel.find_armature_object(root) utils.selectSingleBone(context, armature, None, True) morph = mmd_root.bone_morphs[mmd_root.active_morph] for morph_data in morph.data: - p_bone = armature.pose.bones.get(morph_data.bone, None) + p_bone: Optional[bpy.types.PoseBone] = armature.pose.bones.get(morph_data.bone, None) if p_bone: p_bone.bone.select = True - mtx = matmul(p_bone.matrix_basis.to_3x3(), Quaternion(*morph_data.rotation.to_axis_angle()).to_matrix()).to_4x4() + 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 - return { 'FINISHED' } + return {"FINISHED"} + -class ClearBoneMorphView(Operator): - bl_idname = 'mmd_tools.clear_bone_morph_view' - bl_label = 'Clear Bone Morph View' - bl_description = 'Reset transforms of all bones to their default values' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class ClearBoneMorphView(bpy.types.Operator): + bl_idname = "mmd_tools.clear_bone_morph_view" + bl_label = "Clear Bone Morph View" + bl_description = "Reset transforms of all bones to their default values" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) - armature = rig.armature() + 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() - return { 'FINISHED' } + return {"FINISHED"} -class ApplyBoneMorph(Operator): - bl_idname = 'mmd_tools.apply_bone_morph' - bl_label = 'Apply Bone Morph' - bl_description = 'Apply current pose to active bone morph' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class ApplyBoneMorph(bpy.types.Operator): + bl_idname = "mmd_tools.apply_bone_morph" + bl_label = "Apply Bone Morph" + bl_description = "Apply current pose to active bone morph" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) - armature = rig.armature() + root = FnModel.find_root_object(obj) + assert root is not None + armature = FnModel.find_armature_object(root) mmd_root = root.mmd_root morph = mmd_root.bone_morphs[mmd_root.active_morph] morph.data.clear() @@ -479,41 +493,43 @@ def execute(self, context): item = morph.data.add() 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() + item.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() p_bone.bone.select = True else: p_bone.bone.select = False - return { 'FINISHED' } + return {"FINISHED"} + -class SelectRelatedBone(Operator): - bl_idname = 'mmd_tools.select_bone_morph_offset_bone' - bl_label = 'Select Related Bone' - bl_description = 'Select the bone assigned to this offset in the armature' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class SelectRelatedBone(bpy.types.Operator): + bl_idname = "mmd_tools.select_bone_morph_offset_bone" + bl_label = "Select Related Bone" + bl_description = "Select the bone assigned to this offset in the armature" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) + assert root is not None mmd_root = root.mmd_root - rig = mmd_model.Model(root) - armature = rig.armature() + armature = FnModel.find_armature_object(root) morph = mmd_root.bone_morphs[mmd_root.active_morph] morph_data = morph.data[morph.active_data] utils.selectSingleBone(context, armature, morph_data.bone) - return { 'FINISHED' } + return {"FINISHED"} -class EditBoneOffset(Operator): - bl_idname = 'mmd_tools.edit_bone_morph_offset' - bl_label = 'Edit Related Bone' - bl_description = 'Applies the location and rotation of this offset to the bone' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class EditBoneOffset(bpy.types.Operator): + bl_idname = "mmd_tools.edit_bone_morph_offset" + bl_label = "Edit Related Bone" + bl_description = "Applies the location and rotation of this offset to the bone" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) + assert root is not None mmd_root = root.mmd_root - rig = mmd_model.Model(root) - armature = rig.armature() + armature = FnModel.find_armature_object(root) morph = mmd_root.bone_morphs[mmd_root.active_morph] morph_data = morph.data[morph.active_data] p_bone = armature.pose.bones[morph_data.bone] @@ -521,101 +537,105 @@ def execute(self, context): mtx.translation = morph_data.location p_bone.matrix_basis = mtx utils.selectSingleBone(context, armature, p_bone.name) - return { 'FINISHED' } + return {"FINISHED"} + -class ApplyBoneOffset(Operator): - bl_idname = 'mmd_tools.apply_bone_morph_offset' - bl_label = 'Apply Bone Morph Offset' - bl_description = 'Stores the current bone location and rotation into this offset' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class ApplyBoneOffset(bpy.types.Operator): + bl_idname = "mmd_tools.apply_bone_morph_offset" + bl_label = "Apply Bone Morph Offset" + bl_description = "Stores the current bone location and rotation into this offset" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) + assert root is not None mmd_root = root.mmd_root - rig = mmd_model.Model(root) - armature = rig.armature() + armature = FnModel.find_armature_object(root) + assert armature is not None morph = mmd_root.bone_morphs[mmd_root.active_morph] morph_data = morph.data[morph.active_data] 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() - return { 'FINISHED' } + morph_data.rotation = p_bone.rotation_quaternion if p_bone.rotation_mode == "QUATERNION" else p_bone.matrix_basis.to_quaternion() + return {"FINISHED"} -class ViewUVMorph(Operator): - bl_idname = 'mmd_tools.view_uv_morph' - bl_label = 'View UV Morph' - bl_description = 'View the result of active UV morph on current mesh object' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class ViewUVMorph(bpy.types.Operator): + bl_idname = "mmd_tools.view_uv_morph" + bl_label = "View UV Morph" + bl_description = "View the result of active UV morph on current mesh object" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) + root = FnModel.find_root_object(obj) + assert root is not None mmd_root = root.mmd_root - meshes = tuple(rig.meshes()) + meshes = tuple(FnModel.iterate_mesh_objects(root)) if len(meshes) == 1: obj = meshes[0] elif obj not in meshes: - self.report({'ERROR'}, 'Please select a mesh object') - return {'CANCELLED'} + self.report({"ERROR"}, "Please select a mesh object") + return {"CANCELLED"} meshObj = obj bpy.ops.mmd_tools.clear_uv_morph_view() - selected = meshObj.select - with bpyutils.select_object(meshObj) as data: + selected = meshObj.select_get() + with bpyutils.select_object(meshObj): + mesh = cast(bpy.types.Mesh, meshObj.data) morph = mmd_root.uv_morphs[mmd_root.active_morph] - mesh = meshObj.data - uv_textures = getattr(mesh, 'uv_textures', mesh.uv_layers) + uv_textures = mesh.uv_layers - base_uv_layers = [l for l in mesh.uv_layers if not l.name.startswith('_')] + base_uv_layers = [l for l in mesh.uv_layers if not l.name.startswith("_")] if morph.uv_index >= len(base_uv_layers): - self.report({ 'ERROR' }, "Invalid uv index: %d"%morph.uv_index) - return { 'CANCELLED' } + self.report({"ERROR"}, "Invalid uv index: %d" % morph.uv_index) + return {"CANCELLED"} uv_layer_name = base_uv_layers[morph.uv_index].name - if morph.uv_index == 0 or uv_textures.active.name not in {uv_layer_name, '_'+uv_layer_name}: + if morph.uv_index == 0 or uv_textures.active.name not in {uv_layer_name, "_" + uv_layer_name}: 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="__uv.%s" % uv_layer_name) if uv_tex is None: - self.report({ 'ERROR' }, "Failed to create a temporary uv layer") - return { 'CANCELLED' } + self.report({"ERROR"}, "Failed to create a temporary uv layer") + return {"CANCELLED"} offsets = FnMorph.get_uv_morph_offset_map(meshObj, morph).items() - offsets = {k:getattr(Vector(v), 'zw' if uv_layer_name.startswith('_') else 'xy') for k, v in offsets} + offsets = {k: getattr(Vector(v), "zw" if uv_layer_name.startswith("_") else "xy") for k, v in offsets} 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): - select = temp_uv_data[i].select = (l.vertex_index in offsets) + select = temp_uv_data[i].select = l.vertex_index in offsets if select: temp_uv_data[i].uv = base_uv_data[i].uv + offsets[l.vertex_index] uv_textures.active = uv_tex uv_tex.active_render = True - meshObj.hide = False - meshObj.select = selected - return { 'FINISHED' } + meshObj.hide_set(False) + meshObj.select_set(selected) + return {"FINISHED"} + -class ClearUVMorphView(Operator): - bl_idname = 'mmd_tools.clear_uv_morph_view' - bl_label = 'Clear UV Morph View' - bl_description = 'Clear all temporary data of UV morphs' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class ClearUVMorphView(bpy.types.Operator): + bl_idname = "mmd_tools.clear_uv_morph_view" + bl_label = "Clear UV Morph View" + bl_description = "Clear all temporary data of UV morphs" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) - for m in rig.meshes(): + root = FnModel.find_root_object(obj) + assert root is not None + for m in FnModel.iterate_mesh_objects(root): mesh = m.data - uv_textures = getattr(mesh, 'uv_textures', mesh.uv_layers) + uv_textures = getattr(mesh, "uv_textures", mesh.uv_layers) for t in uv_textures: - if t.name.startswith('__uv.'): + if t.name.startswith("__uv."): uv_textures.remove(t) if len(uv_textures) > 0: uv_textures[0].active_render = True @@ -625,97 +645,96 @@ def execute(self, context): if animation_data: nla_tracks = animation_data.nla_tracks for t in nla_tracks: - if t.name.startswith('__uv.'): + if t.name.startswith("__uv."): nla_tracks.remove(t) - if animation_data.action and animation_data.action.name.startswith('__uv.'): + if animation_data.action and animation_data.action.name.startswith("__uv."): animation_data.action = None if animation_data.action is None and len(nla_tracks) == 0: mesh.animation_data_clear() for act in bpy.data.actions: - if act.name.startswith('__uv.') and act.users < 1: + if act.name.startswith("__uv.") and act.users < 1: bpy.data.actions.remove(act) - return { 'FINISHED' } + return {"FINISHED"} -class EditUVMorph(Operator): - bl_idname = 'mmd_tools.edit_uv_morph' - bl_label = 'Edit UV Morph' - bl_description = 'Edit UV morph on a temporary UV layer (use UV Editor to edit the result)' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + +class EditUVMorph(bpy.types.Operator): + bl_idname = "mmd_tools.edit_uv_morph" + bl_label = "Edit UV Morph" + bl_description = "Edit UV morph on a temporary UV layer (use UV Editor to edit the result)" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod def poll(cls, context): obj = context.active_object - if obj.type != 'MESH': + if 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 and active_uv_layer.name.startswith("__uv.") def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) - mmd_root = root.mmd_root meshObj = obj - selected = meshObj.select - with bpyutils.select_object(meshObj) as data: - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.mesh.select_mode(type='VERT', action='ENABLE') - bpy.ops.mesh.reveal() # unhide all vertices - bpy.ops.mesh.select_all(action='DESELECT') - bpy.ops.object.mode_set(mode='OBJECT') + selected = meshObj.select_get() + with bpyutils.select_object(meshObj): + 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 + bpy.ops.mesh.select_all(action="DESELECT") + bpy.ops.object.mode_set(mode="OBJECT") - vertices = meshObj.data.vertices - for l, d in zip(meshObj.data.loops, meshObj.data.uv_layers.active.data): + vertices = mesh.vertices + for l, d in zip(mesh.loops, mesh.uv_layers.active.data): if d.select: vertices[l.vertex_index].select = True - if not hasattr(meshObj.data, 'uv_textures'): - polygons = meshObj.data.polygons - polygons.active = getattr(next((p for p in polygons if all(vertices[i].select for i in p.vertices)), None), 'index', polygons.active) + 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) + return {"FINISHED"} - bpy.ops.object.mode_set(mode='EDIT') - meshObj.select = selected - return { 'FINISHED' } -class ApplyUVMorph(Operator): - bl_idname = 'mmd_tools.apply_uv_morph' - bl_label = 'Apply UV Morph' - bl_description = 'Calculate the UV offsets of selected vertices and apply to active UV morph' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} +class ApplyUVMorph(bpy.types.Operator): + bl_idname = "mmd_tools.apply_uv_morph" + bl_label = "Apply UV Morph" + bl_description = "Calculate the UV offsets of selected vertices and apply to active UV morph" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod def poll(cls, context): obj = context.active_object - if obj.type != 'MESH': + if 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 and active_uv_layer.name.startswith("__uv.") def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) + root = FnModel.find_root_object(obj) mmd_root = root.mmd_root meshObj = obj - selected = meshObj.select - with bpyutils.select_object(meshObj) as data: + selected = meshObj.select_get() + with bpyutils.select_object(meshObj): + mesh = cast(bpy.types.Mesh, meshObj.data) morph = mmd_root.uv_morphs[mmd_root.active_morph] - mesh = meshObj.data 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) - return {'CANCELLED'} + self.report({"ERROR"}, ' * UV map "%s" not found' % base_uv_name) + 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' + axis_type = "ZW" if base_uv_name.startswith("_") else "XY" from collections import namedtuple - __OffsetData = namedtuple('OffsetData', 'index, offset') + + __OffsetData = namedtuple("OffsetData", "index, offset") offsets = {} vertices = mesh.vertices for l, i0, i1 in zip(mesh.loops, base_uv_data, temp_uv_data): @@ -725,24 +744,24 @@ def execute(self, context): offsets[l.vertex_index] = __OffsetData(l.vertex_index, (dx, dy, dx, dy)) FnMorph.store_uv_morph_data(meshObj, morph, offsets.values(), axis_type) - morph.data_type = 'VERTEX_GROUP' + morph.data_type = "VERTEX_GROUP" - meshObj.select = selected - return { 'FINISHED' } + meshObj.select_set(selected) + return {"FINISHED"} class CleanDuplicatedMaterialMorphs(bpy.types.Operator): - bl_idname = 'mmd_tools.clean_duplicated_material_morphs' - bl_label = 'Clean Duplicated Material Morphs' - bl_description = 'Clean duplicated material morphs' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.clean_duplicated_material_morphs" + bl_label = "Clean Duplicated Material Morphs" + bl_description = "Clean duplicated material morphs" + bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): - return mmd_model.Model.findRoot(context.active_object) is not None + return FnModel.find_root_object(context.active_object) is not None def execute(self, context: bpy.types.Context): - mmd_root_object = mmd_model.FnModel.find_root(context.active_object) + mmd_root_object = FnModel.find_root_object(context.active_object) FnMorph.clean_duplicated_material_morphs(mmd_root_object) - return { 'FINISHED' } + return {"FINISHED"} diff --git a/mmd_tools/operators/rigid_body.py b/mmd_tools/operators/rigid_body.py index dc97e75f..2729767d 100644 --- a/mmd_tools/operators/rigid_body.py +++ b/mmd_tools/operators/rigid_body.py @@ -1,39 +1,44 @@ # -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. import math -from typing import Dict +from typing import Dict, Optional, Tuple, cast import bpy -import mmd_tools.core.model as mmd_model -from mmd_tools import utils -from mmd_tools.bpyutils import Props, activate_layer_collection -from mmd_tools.core import rigid_body +from mathutils import Euler, Vector + +from .. import utils +from ..bpyutils import FnContext, Props +from ..core import rigid_body +from ..core.model import FnModel, Model +from ..core.rigid_body import FnRigidBody class SelectRigidBody(bpy.types.Operator): - bl_idname = 'mmd_tools.rigid_body_select' - bl_label = 'Select Rigid Body' - bl_description = 'Select similar rigidbody objects which have the same property values with active rigidbody object' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.rigid_body_select" + bl_label = "Select Rigid Body" + bl_description = "Select similar rigidbody objects which have the same property values with active rigidbody object" + bl_options = {"REGISTER", "UNDO"} properties: bpy.props.EnumProperty( - name='Properties', - description='Select the properties to be compared', - options={'ENUM_FLAG'}, - items = [ - ('collision_group_number', 'Collision Group', 'Collision group', 1), - ('collision_group_mask', 'Collision Group Mask', 'Collision group mask', 2), - ('type', 'Rigid Type', 'Rigid type', 4), - ('shape', 'Shape', 'Collision shape', 8), - ('bone', 'Bone', 'Target bone', 16), - ], + name="Properties", + description="Select the properties to be compared", + options={"ENUM_FLAG"}, + items=[ + ("collision_group_number", "Collision Group", "Collision group", 1), + ("collision_group_mask", "Collision Group Mask", "Collision group mask", 2), + ("type", "Rigid Type", "Rigid type", 4), + ("shape", "Shape", "Collision shape", 8), + ("bone", "Bone", "Target bone", 16), + ], default=set(), - ) + ) hide_others: bpy.props.BoolProperty( - name='Hide Others', - description='Hide the rigidbody object which does not have the same property values with active rigidbody object', + name="Hide Others", + description="Hide the rigidbody object which does not have the same property values with active rigidbody object", default=False, - ) + ) def invoke(self, context, event): vm = context.window_manager @@ -41,208 +46,218 @@ def invoke(self, context, event): @classmethod def poll(cls, context): - return mmd_model.isRigidBodyObject(context.active_object) + return FnModel.is_rigid_body_object(context.active_object) def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) + root = FnModel.find_root_object(obj) if root is None: - self.report({ 'ERROR' }, "The model root can't be found") - return { 'CANCELLED' } + self.report({"ERROR"}, "The model root can't be found") + return {"CANCELLED"} - rig = mmd_model.Model(root) - selection = set(rig.rigidBodies()) + selection = set(FnModel.iterate_rigid_body_objects(root)) for prop_name in self.properties: prop_value = getattr(obj.mmd_rigid, prop_name) - if prop_name == 'collision_group_mask': + if prop_name == "collision_group_mask": prop_value = tuple(prop_value) for i in selection.copy(): if tuple(i.mmd_rigid.collision_group_mask) != prop_value: selection.remove(i) if self.hide_others: - i.select = False - i.hide = True + i.select_set(False) + i.hide_set(True) else: for i in selection.copy(): if getattr(i.mmd_rigid, prop_name) != prop_value: selection.remove(i) if self.hide_others: - i.select = False - i.hide = True + i.select_set(False) + i.hide_set(True) for i in selection: - i.hide = False - i.select = True + i.hide_set(False) + i.select_set(True) + + return {"FINISHED"} - return { 'FINISHED' } class AddRigidBody(bpy.types.Operator): - bl_idname = 'mmd_tools.rigid_body_add' - bl_label = 'Add Rigid Body' - bl_description = 'Add Rigid Bodies to selected bones' - bl_options = {'REGISTER', 'UNDO', 'PRESET', 'INTERNAL'} + bl_idname = "mmd_tools.rigid_body_add" + bl_label = "Add Rigid Body" + bl_description = "Add Rigid Bodies to selected bones" + bl_options = {"REGISTER", "UNDO", "PRESET", "INTERNAL"} name_j: bpy.props.StringProperty( - name='Name', - description='The name of rigid body ($name_j means use the japanese name of target bone)', - default='$name_j', - ) + name="Name", + description="The name of rigid body ($name_j means use the japanese name of target bone)", + default="$name_j", + ) name_e: bpy.props.StringProperty( - name='Name(Eng)', - description='The english name of rigid body ($name_e means use the english name of target bone)', - default='$name_e', - ) + name="Name(Eng)", + description="The english name of rigid body ($name_e means use the english name of target bone)", + default="$name_e", + ) collision_group_number: bpy.props.IntProperty( - name='Collision Group', - description='The collision group of the object', + name="Collision Group", + description="The collision group of the object", min=0, max=15, - ) + ) collision_group_mask: bpy.props.BoolVectorProperty( - name='Collision Group Mask', - description='The groups the object can not collide with', + name="Collision Group Mask", + description="The groups the object can not collide with", size=16, - subtype='LAYER', - ) + subtype="LAYER", + ) rigid_type: bpy.props.EnumProperty( - name='Rigid Type', - description='Select rigid type', - items = [ - (str(rigid_body.MODE_STATIC), 'Bone', - "Rigid body's orientation completely determined by attached bone", 1), - (str(rigid_body.MODE_DYNAMIC), 'Physics', - "Attached bone's orientation completely determined by rigid body", 2), - (str(rigid_body.MODE_DYNAMIC_BONE), 'Physics + Bone', - "Bone determined by combination of parent and attached rigid body", 3), - ], - ) + name="Rigid Type", + description="Select rigid type", + items=[ + (str(rigid_body.MODE_STATIC), "Bone", "Rigid body's orientation completely determined by attached bone", 1), + (str(rigid_body.MODE_DYNAMIC), "Physics", "Attached bone's orientation completely determined by rigid body", 2), + (str(rigid_body.MODE_DYNAMIC_BONE), "Physics + Bone", "Bone determined by combination of parent and attached rigid body", 3), + ], + ) rigid_shape: bpy.props.EnumProperty( - name='Shape', - description='Select the collision shape', - items = [ - ('SPHERE', 'Sphere', '', 1), - ('BOX', 'Box', '', 2), - ('CAPSULE', 'Capsule', '', 3), - ], - ) + name="Shape", + description="Select the collision shape", + items=[ + ("SPHERE", "Sphere", "", 1), + ("BOX", "Box", "", 2), + ("CAPSULE", "Capsule", "", 3), + ], + ) size: bpy.props.FloatVectorProperty( - name='Size', - description='Size of the object, the values will multiply the length of target bone', - subtype='XYZ', + name="Size", + description="Size of the object, the values will multiply the length of target bone", + subtype="XYZ", size=3, min=0, default=[0.6, 0.6, 0.6], - ) + ) mass: bpy.props.FloatProperty( - name='Mass', + name="Mass", description="How much the object 'weights' irrespective of gravity", min=0.001, default=1, - ) + ) friction: bpy.props.FloatProperty( - name='Friction', - description='Resistance of object to movement', + name="Friction", + description="Resistance of object to movement", min=0, soft_max=1, default=0.5, - ) + ) bounce: bpy.props.FloatProperty( - name='Restitution', - description='Tendency of object to bounce after colliding with another (0 = stays still, 1 = perfectly elastic)', + name="Restitution", + description="Tendency of object to bounce after colliding with another (0 = stays still, 1 = perfectly elastic)", min=0, soft_max=1, - ) + ) linear_damping: bpy.props.FloatProperty( - name='Linear Damping', - description='Amount of linear velocity that is lost over time', + name="Linear Damping", + description="Amount of linear velocity that is lost over time", min=0, max=1, default=0.04, - ) + ) angular_damping: bpy.props.FloatProperty( - name='Angular Damping', - description='Amount of angular velocity that is lost over time', + name="Angular Damping", + description="Amount of angular velocity that is lost over time", min=0, max=1, default=0.1, - ) + ) - def __add_rigid_body(self, rig, arm_obj=None, pose_bone=None): - name_j = self.name_j - name_e = self.name_e + 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() - loc = (0.0, 0.0, 0.0) - rot = (0.0, 0.0, 0.0) - bone_name = None + loc = Vector((0.0, 0.0, 0.0)) + rot = Euler((0.0, 0.0, 0.0)) + bone_name: Optional[str] = None - if pose_bone: + if pose_bone is None: + size *= getattr(root_object, Props.empty_display_size) + else: bone_name = pose_bone.name mmd_bone = pose_bone.mmd_bone - name_j = name_j.replace('$name_j', mmd_bone.name_j or bone_name) - name_e = name_e.replace('$name_e', mmd_bone.name_e or bone_name) + name_j = name_j.replace("$name_j", mmd_bone.name_j or bone_name) + name_e = name_e.replace("$name_e", mmd_bone.name_e or bone_name) target_bone = pose_bone.bone - loc = (target_bone.head_local + target_bone.tail_local)/2 - rot = target_bone.matrix_local.to_euler('YXZ') - rot.rotate_axis('X', math.pi/2) + loc = (target_bone.head_local + target_bone.tail_local) / 2 + rot = target_bone.matrix_local.to_euler("YXZ") + rot.rotate_axis("X", math.pi / 2) size *= target_bone.length if 1: - pass # bypass resizing - elif self.rigid_shape == 'SPHERE': + pass # bypass resizing + elif self.rigid_shape == "SPHERE": size.x *= 0.8 - elif self.rigid_shape == 'BOX': + elif self.rigid_shape == "BOX": size.x /= 3 size.y /= 3 size.z *= 0.8 - elif self.rigid_shape == 'CAPSULE': + elif self.rigid_shape == "CAPSULE": size.x /= 3 - else: - size *= getattr(rig.rootObject(), Props.empty_display_size) - - return rig.createRigidBody( - name=name_j, - name_e=name_e, - location=loc, - rotation=rot, - size=size, - shape_type=rigid_body.shapeType(self.rigid_shape), - dynamics_type=int(self.rigid_type), - collision_group_number=self.collision_group_number, - collision_group_mask=self.collision_group_mask, - mass=self.mass, - friction=self.friction, - bounce=self.bounce, - linear_damping=self.linear_damping, - angular_damping=self.angular_damping, - bone=bone_name, - ) + + return FnRigidBody.setup_rigid_body_object( + obj=FnRigidBody.new_rigid_body_object(context, FnModel.ensure_rigid_group_object(context, root_object)), + shape_type=rigid_body.shapeType(self.rigid_shape), + location=loc, + rotation=rot, + size=size, + dynamics_type=int(self.rigid_type), + name=name_j, + name_e=name_e, + collision_group_number=self.collision_group_number, + collision_group_mask=self.collision_group_mask, + mass=self.mass, + friction=self.friction, + bounce=self.bounce, + linear_damping=self.linear_damping, + angular_damping=self.angular_damping, + bone=bone_name, + ) + + @classmethod + def poll(cls, context): + root_object = FnModel.find_root_object(context.active_object) + if root_object is None: + return False + + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + return False + + return True def execute(self, context): - obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) - arm = rig.armature() - if obj != arm: - utils.selectAObject(root) - root.select = False - elif arm.mode != 'POSE': - bpy.ops.object.mode_set(mode='POSE') + 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)) + + if active_object != armature_object: + FnContext.select_single_object(context, root_object).select_set(False) + elif armature_object.mode != "POSE": + bpy.ops.object.mode_set(mode="POSE") selected_pose_bones = [] if context.selected_pose_bones: selected_pose_bones = context.selected_pose_bones - arm.select = False + armature_object.select_set(False) if len(selected_pose_bones) > 0: for pose_bone in selected_pose_bones: - rigid = self.__add_rigid_body(rig, arm, pose_bone) - rigid.select = True + rigid = self.__add_rigid_body(context, root_object, pose_bone) + rigid.select_set(True) else: - rigid = self.__add_rigid_body(rig) - rigid.select = True - return { 'FINISHED' } + rigid = self.__add_rigid_body(context, root_object) + rigid.select_set(True) + return {"FINISHED"} def invoke(self, context, event): no_bone = True @@ -252,122 +267,118 @@ def invoke(self, context, event): no_bone = False if no_bone: - self.name_j = 'Rigid' - self.name_e = 'Rigid_e' + self.name_j = "Rigid" + self.name_e = "Rigid_e" else: - if self.name_j == 'Rigid': - self.name_j = '$name_j' - if self.name_e == 'Rigid_e': - self.name_e = '$name_e' + if self.name_j == "Rigid": + self.name_j = "$name_j" + if self.name_e == "Rigid_e": + self.name_e = "$name_e" vm = context.window_manager return vm.invoke_props_dialog(self) + class RemoveRigidBody(bpy.types.Operator): - bl_idname = 'mmd_tools.rigid_body_remove' - bl_label = 'Remove Rigid Body' - bl_description = 'Deletes the currently selected Rigid Body' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.rigid_body_remove" + bl_label = "Remove Rigid Body" + bl_description = "Deletes the currently selected Rigid Body" + bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): - return mmd_model.isRigidBodyObject(context.active_object) + return FnModel.is_rigid_body_object(context.active_object) def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - utils.selectAObject(obj) #ensure this is the only one object select + root = FnModel.find_root_object(obj) + utils.selectAObject(obj) # ensure this is the only one object select bpy.ops.object.delete(use_global=True) if root: utils.selectAObject(root) - return { 'FINISHED' } + return {"FINISHED"} + class RigidBodyBake(bpy.types.Operator): - bl_idname = 'mmd_tools.ptcache_rigid_body_bake' - bl_label = 'Bake' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.ptcache_rigid_body_bake" + bl_label = "Bake" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context: bpy.types.Context): - override: Dict = context.copy() - override.update({ - 'scene': context.scene, - 'point_cache': context.scene.rigidbody_world.point_cache - }) - bpy.ops.ptcache.bake(override, 'INVOKE_DEFAULT', bake=True) + with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache): + bpy.ops.ptcache.bake("INVOKE_DEFAULT", bake=True) + + return {"FINISHED"} - return {'FINISHED'} class RigidBodyDeleteBake(bpy.types.Operator): - bl_idname = 'mmd_tools.ptcache_rigid_body_delete_bake' - bl_label = 'Delete Bake' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.ptcache_rigid_body_delete_bake" + bl_label = "Delete Bake" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context: bpy.types.Context): - override: Dict = context.copy() - override.update({ - 'scene': context.scene, - 'point_cache': context.scene.rigidbody_world.point_cache - }) - bpy.ops.ptcache.free_bake(override, 'INVOKE_DEFAULT') + with context.temp_override(scene=context.scene, point_cache=context.scene.rigidbody_world.point_cache): + bpy.ops.ptcache.free_bake("INVOKE_DEFAULT") + + return {"FINISHED"} - return {'FINISHED'} -class AddJoint(bpy.types.Operator): - bl_idname = 'mmd_tools.joint_add' - bl_label = 'Add Joint' - bl_description = 'Add Joint(s) to selected rigidbody objects' - bl_options = {'REGISTER', 'UNDO', 'PRESET', 'INTERNAL'} +class AddJoint(bpy.types.Operator): + bl_idname = "mmd_tools.joint_add" + bl_label = "Add Joint" + bl_description = "Add Joint(s) to selected rigidbody objects" + bl_options = {"REGISTER", "UNDO", "PRESET", "INTERNAL"} use_bone_rotation: bpy.props.BoolProperty( - name='Use Bone Rotation', - description='Match joint orientation to bone orientation if enabled', + name="Use Bone Rotation", + description="Match joint orientation to bone orientation if enabled", default=True, - ) + ) limit_linear_lower: bpy.props.FloatVectorProperty( - name='Limit Linear Lower', - description='Lower limit of translation', - subtype='XYZ', + name="Limit Linear Lower", + description="Lower limit of translation", + subtype="XYZ", size=3, - ) + ) limit_linear_upper: bpy.props.FloatVectorProperty( - name='Limit Linear Upper', - description='Upper limit of translation', - subtype='XYZ', + name="Limit Linear Upper", + description="Upper limit of translation", + subtype="XYZ", size=3, - ) + ) limit_angular_lower: bpy.props.FloatVectorProperty( - name='Limit Angular Lower', - description='Lower limit of rotation', - subtype='EULER', + name="Limit Angular Lower", + description="Lower limit of rotation", + subtype="EULER", size=3, - min=-math.pi*2, - max=math.pi*2, - default=[-math.pi/4]*3, - ) + min=-math.pi * 2, + max=math.pi * 2, + default=[-math.pi / 4] * 3, + ) limit_angular_upper: bpy.props.FloatVectorProperty( - name='Limit Angular Upper', - description='Upper limit of rotation', - subtype='EULER', + name="Limit Angular Upper", + description="Upper limit of rotation", + subtype="EULER", size=3, - min=-math.pi*2, - max=math.pi*2, - default=[math.pi/4]*3, - ) + min=-math.pi * 2, + max=math.pi * 2, + default=[math.pi / 4] * 3, + ) spring_linear: bpy.props.FloatVectorProperty( - name='Spring(Linear)', - description='Spring constant of movement', - subtype='XYZ', + name="Spring(Linear)", + description="Spring constant of movement", + subtype="XYZ", size=3, min=0, - ) + ) spring_angular: bpy.props.FloatVectorProperty( - name='Spring(Angular)', - description='Spring constant of rotation', - subtype='XYZ', + name="Spring(Angular)", + description="Spring constant of rotation", + subtype="XYZ", size=3, min=0, - ) + ) - def __enumerate_rigid_pair(self, bone_map): + 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(): @@ -380,8 +391,9 @@ def __enumerate_rigid_pair(self, bone_map): else: yield obj_seq - def __add_joint(self, rig, rigid_pair, bone_map): - loc, rot = None, [0, 0, 0] + 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 bone_a = bone_map[rigid_a] bone_b = bone_map[rigid_b] @@ -392,104 +404,114 @@ def __add_joint(self, rig, rigid_pair, bone_map): if bone_b.parent == bone_a: loc = bone_b.head_local if self.use_bone_rotation: - rot = bone_b.matrix_local.to_euler('YXZ') - rot.rotate_axis('X', math.pi/2) + rot = bone_b.matrix_local.to_euler("YXZ") + rot.rotate_axis("X", math.pi / 2) if loc is None: - loc = (rigid_a.location + rigid_b.location)/2 + loc = (rigid_a.location + rigid_b.location) / 2 name_j = rigid_b.mmd_rigid.name_j or rigid_b.name name_e = rigid_b.mmd_rigid.name_e or rigid_b.name - return rig.createJoint( - name=name_j, - name_e=name_e, - location=loc, - rotation=rot, - rigid_a=rigid_a, - rigid_b=rigid_b, - maximum_location=self.limit_linear_upper, - minimum_location=self.limit_linear_lower, - maximum_rotation=self.limit_angular_upper, - minimum_rotation=self.limit_angular_lower, - spring_linear=self.spring_linear, - spring_angular=self.spring_angular, - ) - def execute(self, context): - obj = context.active_object - root = mmd_model.Model.findRoot(obj) - rig = mmd_model.Model(root) + return FnRigidBody.setup_joint_object( + obj=FnRigidBody.new_joint_object(context, FnModel.ensure_joint_group_object(context, root_object), FnModel.get_empty_display_size(root_object)), + name=name_j, + name_e=name_e, + location=loc, + rotation=rot, + rigid_a=rigid_a, + rigid_b=rigid_b, + maximum_location=self.limit_linear_upper, + minimum_location=self.limit_linear_lower, + maximum_rotation=self.limit_angular_upper, + minimum_rotation=self.limit_angular_lower, + spring_linear=self.spring_linear, + spring_angular=self.spring_angular, + ) - arm = rig.armature() - bone_map = {} - for i in context.selected_objects: - if mmd_model.isRigidBodyObject(i): - bone_map[i] = arm.data.bones.get(i.mmd_rigid.bone, None) + @classmethod + def poll(cls, context): + root_object = FnModel.find_root_object(context.active_object) + if root_object is None: + return False + + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + return False + + return True + + 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 + 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: - self.report({ 'ERROR' }, "Please select two or more mmd rigid objects") - return { 'CANCELLED' } + self.report({"ERROR"}, "Please select two or more mmd rigid objects") + return {"CANCELLED"} - utils.selectAObject(root) - root.select = False + FnContext.select_single_object(context, root_object).select_set(False) if context.scene.rigidbody_world is None: bpy.ops.rigidbody.world_add() for pair in self.__enumerate_rigid_pair(bone_map): - joint = self.__add_joint(rig, pair, bone_map) - joint.select = True + joint = self.__add_joint(context, root_object, pair, bone_map) + joint.select_set(True) - return { 'FINISHED' } + return {"FINISHED"} def invoke(self, context, event): vm = context.window_manager return vm.invoke_props_dialog(self) + class RemoveJoint(bpy.types.Operator): - bl_idname = 'mmd_tools.joint_remove' - bl_label = 'Remove Joint' - bl_description = 'Deletes the currently selected Joint' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.joint_remove" + bl_label = "Remove Joint" + bl_description = "Deletes the currently selected Joint" + bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): - return mmd_model.isJointObject(context.active_object) + return FnModel.is_joint_object(context.active_object) def execute(self, context): obj = context.active_object - root = mmd_model.Model.findRoot(obj) - utils.selectAObject(obj) #ensure this is the only one object select + root = FnModel.find_root_object(obj) + utils.selectAObject(obj) # ensure this is the only one object select bpy.ops.object.delete(use_global=True) if root: utils.selectAObject(root) - return { 'FINISHED' } + return {"FINISHED"} + class UpdateRigidBodyWorld(bpy.types.Operator): - bl_idname = 'mmd_tools.rigid_body_world_update' - bl_label = 'Update Rigid Body World' - bl_description = 'Update rigid body world and references of rigid body constraint according to current scene objects (experimental)' - bl_options = {'REGISTER', 'UNDO'} + bl_idname = "mmd_tools.rigid_body_world_update" + bl_label = "Update Rigid Body World" + bl_description = "Update rigid body world and references of rigid body constraint according to current scene objects (experimental)" + bl_options = {"REGISTER", "UNDO"} @staticmethod def __get_rigid_body_world_objects(): rigid_body.setRigidBodyWorldEnabled(True) rbw = bpy.context.scene.rigidbody_world if not rbw.collection: - rbw.collection = bpy.data.collections.new('RigidBodyWorld') + rbw.collection = bpy.data.collections.new("RigidBodyWorld") rbw.collection.use_fake_user = True if not rbw.constraints: - rbw.constraints = bpy.data.collections.new('RigidBodyConstraints') + rbw.constraints = bpy.data.collections.new("RigidBodyConstraints") rbw.constraints.use_fake_user = True - if hasattr(bpy.context.scene.rigidbody_world, 'substeps_per_frame'): - bpy.context.scene.rigidbody_world.substeps_per_frame = 1 - bpy.context.scene.rigidbody_world.solver_iterations = 60 + bpy.context.scene.rigidbody_world.substeps_per_frame = 6 + bpy.context.scene.rigidbody_world.solver_iterations = 10 return rbw.collection.objects, rbw.constraints.objects 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) + 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, group): if obj in scene_objs: @@ -502,12 +524,11 @@ def _update_group(obj, group): def _references(obj): yield obj - if getattr(obj, 'proxy', None): + if getattr(obj, "proxy", None): yield from _references(obj.proxy) - if getattr(obj, 'override_library', None): + if getattr(obj, "override_library", None): yield from _references(obj.override_library.reference) - _find_root = mmd_model.FnModel.find_root need_rebuild_physics = scene.rigidbody_world is None or scene.rigidbody_world.collection is None or scene.rigidbody_world.constraints is None rb_objs, rbc_objs = self.__get_rigid_body_world_objects() objects = bpy.data.objects @@ -518,12 +539,12 @@ def _references(obj): # Object.rigid_body are removed, # but Object.rigid_body_constraint are retained. # Therefore, it must be checked with Object.mmd_type. - for i in (x for x in objects if x.mmd_type == 'RIGID_BODY'): + for i in (x for x in objects if x.mmd_type == "RIGID_BODY"): if not _update_group(i, rb_objs): continue - rb_map = table.setdefault(_find_root(i), {}) - if i in rb_map: # means rb_map[i] will replace i + rb_map = table.setdefault(FnModel.find_root_object(i), {}) + if i in rb_map: # means rb_map[i] will replace i rb_objs.unlink(i) continue for r in _references(i): @@ -536,20 +557,20 @@ def _references(obj): if not _update_group(i, rbc_objs): continue - rbc, root = i.rigid_body_constraint, _find_root(i) - rb_map = table.get(root, {}) + rbc, root_object = i.rigid_body_constraint, FnModel.find_root_object(i) + rb_map = table.get(root_object, {}) rbc.object1 = rb_map.get(rbc.object1, rbc.object1) rbc.object2 = rb_map.get(rbc.object2, rbc.object2) if need_rebuild_physics: - for root in scene.objects: - if root.mmd_type != 'ROOT': + for root_object in scene.objects: + if root_object.mmd_type != "ROOT": continue - if not root.mmd_root.is_built: + if not root_object.mmd_root.is_built: continue - with activate_layer_collection(root): - mmd_model.Model(root).build() + with FnContext.temp_override_active_layer_collection(context, root_object): + Model(root_object).build() # After rebuild. First play. Will be crash! # But saved it before. Reload after crash. The play can be work. - return { 'FINISHED' } + return {"FINISHED"} diff --git a/mmd_tools/operators/sdef.py b/mmd_tools/operators/sdef.py index e0730c52..5b698cb3 100644 --- a/mmd_tools/operators/sdef.py +++ b/mmd_tools/operators/sdef.py @@ -1,77 +1,82 @@ # -*- coding: utf-8 -*- +# Copyright 2018 MMD Tools authors +# This file is part of MMD Tools. from typing import Set + import bpy from bpy.types import Operator -from mmd_tools.core.model import FnModel -from mmd_tools.core.sdef import FnSDEF +from ..core.model import FnModel +from ..core.sdef import FnSDEF + 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': + if i.type == "MESH": selected_objects.add(i) continue - root = FnModel.find_root(i) - if root not in {i, i.parent}: + root_object = FnModel.find_root_object(i) + if root_object is None: continue - - root_objects.add(root) - - arm = FnModel.find_armature(root) - if arm is None: + if root_object in root_objects: continue - selected_objects |= set(FnModel.child_meshes(arm)) + root_objects.add(root_object) + + selected_objects |= set(FnModel.iterate_mesh_objects(root_object)) return selected_objects, root_objects + class ResetSDEFCache(Operator): - bl_idname = 'mmd_tools.sdef_cache_reset' - bl_label = 'Reset MMD SDEF cache' - bl_description = 'Reset MMD SDEF cache of selected objects and clean unused cache' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.sdef_cache_reset" + bl_label = "Reset MMD SDEF cache" + bl_description = "Reset MMD SDEF cache of selected objects and clean unused cache" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} def execute(self, context): target_meshes, _ = _get_target_objects(context) for i in target_meshes: FnSDEF.clear_cache(i) FnSDEF.clear_cache(unused_only=True) - return {'FINISHED'} + return {"FINISHED"} + class BindSDEF(Operator): - bl_idname = 'mmd_tools.sdef_bind' - bl_label = 'Bind SDEF Driver' - bl_description = 'Bind MMD SDEF data of selected objects' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.sdef_bind" + bl_label = "Bind SDEF Driver" + bl_description = "Bind MMD SDEF data of selected objects" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} mode: bpy.props.EnumProperty( - name='Mode', - description='Select mode', - items = [ - ('2', 'Bulk', 'Speed up with numpy (may be slower in some cases)', 2), - ('1', 'Normal', 'Normal mode', 1), - ('0', '- Auto -', 'Select best mode by benchmark result', 0), - ], - default='0', - ) + name="Mode", + description="Select mode", + items=[ + ("2", "Bulk", "Speed up with numpy (may be slower in some cases)", 2), + ("1", "Normal", "Normal mode", 1), + ("0", "- Auto -", "Select best mode by benchmark result", 0), + ], + default="0", + ) use_skip: bpy.props.BoolProperty( - name='Skip', - description='Skip when the bones are not moving', + name="Skip", + description="Skip when the bones are not moving", default=True, - ) + ) use_scale: bpy.props.BoolProperty( - name='Scale', - description='Support bone scaling (slow)', + name="Scale", + description="Support bone scaling (slow)", default=False, - ) + ) def invoke(self, context, event): vm = context.window_manager return vm.invoke_props_dialog(self) + # TODO: Utility Functionalize def execute(self, context): target_meshes, root_objects = _get_target_objects(context) @@ -80,15 +85,17 @@ def execute(self, context): param = ((None, False, True)[int(self.mode)], self.use_skip, self.use_scale) count = sum(FnSDEF.bind(i, *param) for i in target_meshes) - self.report({'INFO'}, 'Binded %d of %d selected mesh(es)'%(count, len(target_meshes))) - return {'FINISHED'} + self.report({"INFO"}, f"Binded {count} of {len(target_meshes)} selected mesh(es)") + return {"FINISHED"} + class UnbindSDEF(Operator): - bl_idname = 'mmd_tools.sdef_unbind' - bl_label = 'Unbind SDEF Driver' - bl_description = 'Unbind MMD SDEF data of selected objects' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.sdef_unbind" + bl_label = "Unbind SDEF Driver" + bl_description = "Unbind MMD SDEF data of selected objects" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} + # TODO: Utility Functionalize def execute(self, context): target_meshes, root_objects = _get_target_objects(context) for i in target_meshes: @@ -97,4 +104,4 @@ def execute(self, context): for r in root_objects: r.mmd_root.use_sdef = False - return {'FINISHED'} + return {"FINISHED"} diff --git a/mmd_tools/operators/translations.py b/mmd_tools/operators/translations.py index 4f705f1c..790d4595 100644 --- a/mmd_tools/operators/translations.py +++ b/mmd_tools/operators/translations.py @@ -1,66 +1,73 @@ # -*- coding: utf-8 -*- -from typing import TYPE_CHECKING +# Copyright 2021 MMD Tools authors +# This file is part of MMD Tools. + +from typing import TYPE_CHECKING, cast import bpy -from mmd_tools.core.model import FnModel, Model -from mmd_tools.core.translations import (MMD_DATA_TYPE_TO_HANDLERS, - FnTranslations) -from mmd_tools.translations import DictionaryEnum + +from ..core.model import FnModel, Model +from ..core.translations import MMD_DATA_TYPE_TO_HANDLERS, FnTranslations +from ..translations import DictionaryEnum if TYPE_CHECKING: - from mmd_tools.properties.translations import (MMDTranslation, - MMDTranslationElement, - MMDTranslationElementIndex) + from ..properties.translations import MMDTranslation, MMDTranslationElement, MMDTranslationElementIndex class TranslateMMDModel(bpy.types.Operator): - bl_idname = 'mmd_tools.translate_mmd_model' - bl_label = 'Translate a MMD Model' - bl_description = 'Translate Japanese names of a MMD model' - bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_idname = "mmd_tools.translate_mmd_model" + bl_label = "Translate a MMD Model" + bl_description = "Translate Japanese names of a MMD model" + bl_options = {"REGISTER", "UNDO", "INTERNAL"} dictionary: bpy.props.EnumProperty( - name='Dictionary', + name="Dictionary", items=DictionaryEnum.get_dictionary_items, - description='Translate names from Japanese to English using selected dictionary', + description="Translate names from Japanese to English using selected dictionary", ) types: bpy.props.EnumProperty( - name='Types', - description='Select which parts will be translated', - options={'ENUM_FLAG'}, + name="Types", + description="Select which parts will be translated", + options={"ENUM_FLAG"}, items=[ - ('BONE', 'Bones', 'Bones', 1), - ('MORPH', 'Morphs', 'Morphs', 2), - ('MATERIAL', 'Materials', 'Materials', 4), - ('DISPLAY', 'Display', 'Display frames', 8), - ('PHYSICS', 'Physics', 'Rigidbodies and joints', 16), - ('INFO', 'Information', 'Model name and comments', 32), + ("BONE", "Bones", "Bones", 1), + ("MORPH", "Morphs", "Morphs", 2), + ("MATERIAL", "Materials", "Materials", 4), + ("DISPLAY", "Display", "Display frames", 8), + ("PHYSICS", "Physics", "Rigidbodies and joints", 16), + ("INFO", "Information", "Model name and comments", 32), ], - default={'BONE', 'MORPH', 'MATERIAL', 'DISPLAY', 'PHYSICS', }, + default={ + "BONE", + "MORPH", + "MATERIAL", + "DISPLAY", + "PHYSICS", + }, ) modes: bpy.props.EnumProperty( - name='Modes', - description='Select translation mode', - options={'ENUM_FLAG'}, + name="Modes", + description="Select translation mode", + options={"ENUM_FLAG"}, items=[ - ('MMD', 'MMD Names', 'Fill MMD English names', 1), - ('BLENDER', 'Blender Names', 'Translate blender names (experimental)', 2), + ("MMD", "MMD Names", "Fill MMD English names", 1), + ("BLENDER", "Blender Names", "Translate blender names (experimental)", 2), ], - default={'MMD'}, + default={"MMD"}, ) use_morph_prefix: bpy.props.BoolProperty( - name='Use Morph Prefix', - description='Add/remove prefix to English name of morph', + name="Use Morph Prefix", + description="Add/remove prefix to English name of morph", default=False, ) overwrite: bpy.props.BoolProperty( - name='Overwrite', - description='Overwrite a translated English name', + name="Overwrite", + description="Overwrite a translated English name", default=False, ) allow_fails: bpy.props.BoolProperty( - name='Allow Fails', - description='Allow incompletely translated names', + name="Allow Fails", + description="Allow incompletely translated names", default=False, ) @@ -72,25 +79,25 @@ def execute(self, context): try: self.__translator = DictionaryEnum.get_translator(self.dictionary) except Exception as e: - self.report({'ERROR'}, 'Failed to load dictionary: %s' % e) - return {'CANCELLED'} + self.report({"ERROR"}, "Failed to load dictionary: %s" % e) + return {"CANCELLED"} obj = context.active_object - root = FnModel.find_root(obj) + root = FnModel.find_root_object(obj) rig = Model(root) - if 'MMD' in self.modes: + if "MMD" in self.modes: for i in self.types: - getattr(self, 'translate_%s' % i.lower())(rig) + getattr(self, "translate_%s" % i.lower())(rig) - if 'BLENDER' in self.modes: + if "BLENDER" in self.modes: self.translate_blender_names(rig) 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)) - return {'FINISHED'} + 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): if not self.overwrite and name_e and self.__translator.is_translated(name_e): @@ -99,32 +106,33 @@ def translate(self, name_j, name_e): name_e = None return self.__translator.translate(name_j, name_e) - def translate_blender_names(self, rig): - if 'BONE' in self.types: + def translate_blender_names(self, rig: Model): + if "BONE" in self.types: for b in rig.armature().pose.bones: rig.renameBone(b.name, self.translate(b.name, b.name)) - if 'MORPH' in self.types: + if "MORPH" in self.types: for i in (x for x in rig.meshes() if x.data.shape_keys): for kb in i.data.shape_keys.key_blocks: kb.name = self.translate(kb.name, kb.name) - if 'MATERIAL' in self.types: + if "MATERIAL" in self.types: for m in (x for x in rig.materials() if x): m.name = self.translate(m.name, m.name) - if 'DISPLAY' in self.types: - for g in rig.armature().pose.bone_groups: + if "DISPLAY" in self.types: + g: bpy.types.BoneCollection + for g in cast(bpy.types.Armature, rig.armature().data).collections: g.name = self.translate(g.name, g.name) - if 'PHYSICS' in self.types: + if "PHYSICS" in self.types: for i in rig.rigidBodies(): i.name = self.translate(i.name, i.name) for i in rig.joints(): i.name = self.translate(i.name, i.name) - if 'INFO' in self.types: + if "INFO" in self.types: objects = [rig.rootObject(), rig.armature()] objects.extend(rig.meshes()) for i in objects: @@ -149,10 +157,10 @@ def translate_bone(self, rig): def translate_morph(self, rig): mmd_root = rig.rootObject().mmd_root - attr_list = ('group', 'vertex', 'bone', 'uv', 'material') - prefix_list = ('G_', '', 'B_', 'UV_', 'M_') + attr_list = ("group", "vertex", "bone", "uv", "material") + prefix_list = ("G_", "", "B_", "UV_", "M_") for attr, prefix in zip(attr_list, prefix_list): - for m in getattr(mmd_root, attr+'_morphs', []): + for m in getattr(mmd_root, attr + "_morphs", []): m.name_e = self.translate(m.name, m.name_e) if not prefix: continue @@ -160,7 +168,7 @@ def translate_morph(self, rig): if not m.name_e.startswith(prefix): m.name_e = prefix + m.name_e elif m.name_e.startswith(prefix): - m.name_e = m.name_e[len(prefix):] + m.name_e = m.name_e[len(prefix) :] def translate_material(self, rig): for m in rig.materials(): @@ -185,103 +193,106 @@ def translate_physics(self, rig): 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] + 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): - bl_idname = 'mmd_tools.restore_mmd_translation_element_name' - bl_label = 'Restore this Name' - bl_options = {'INTERNAL'} + bl_idname = "mmd_tools.restore_mmd_translation_element_name" + bl_label = "Restore this Name" + bl_options = {"INTERNAL"} index: bpy.props.IntProperty() prop_name: bpy.props.StringProperty() restore_value: bpy.props.StringProperty() def execute(self, context: bpy.types.Context): - root_object = FnModel.find_root(context.object) + 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] setattr(mmd_translation_element, self.prop_name, self.restore_value) - return {'FINISHED'} + return {"FINISHED"} class GlobalTranslationPopup(bpy.types.Operator): - bl_idname = 'mmd_tools.global_translation_popup' - bl_label = 'Global Translation Popup' - bl_options = {'INTERNAL', 'UNDO'} + bl_idname = "mmd_tools.global_translation_popup" + bl_label = "Global Translation Popup" + bl_options = {"INTERNAL", "UNDO"} @classmethod def poll(cls, context): - return FnModel.find_root(context.object) is not None + return FnModel.find_root_object(context.object) is not None def draw(self, _context): layout = self.layout mmd_translation = self._mmd_translation col = layout.column(align=True) - col.label(text='Filter', icon='FILTER') + col.label(text="Filter", icon="FILTER") row = col.row() - row.prop(mmd_translation, 'filter_types') + row.prop(mmd_translation, "filter_types") - 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_english_blank', toggle=True, text='English') + 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_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) row = box.row(align=True) - row.label(text='Select the target column for Batch Operations:', icon='TRACKER') + row.label(text="Select the target column for Batch Operations:", icon="TRACKER") row = box.row(align=True) - row.label(text='', icon='BLANK1') - row.prop(mmd_translation, 'batch_operation_target', expand=True) - row.label(text='', icon='RESTRICT_SELECT_OFF') - row.label(text='', icon='HIDE_OFF') + row.label(text="", icon="BLANK1") + row.prop(mmd_translation, "batch_operation_target", expand=True) + 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: - row.label(text='', icon='BLANK1') + row.label(text="", icon="BLANK1") col.template_list( - "MMD_TOOLS_UL_MMDTranslationElementIndex", "", - mmd_translation, 'filtered_translation_element_indices', - mmd_translation, 'filtered_translation_element_indices_active_index', + "MMD_TOOLS_UL_MMDTranslationElementIndex", + "", + mmd_translation, + "filtered_translation_element_indices", + mmd_translation, + "filtered_translation_element_indices_active_index", rows=DEFAULT_SHOW_ROW_COUNT, ) box = layout.box().column(align=True) - box.label(text='Batch Operation:', icon='MODIFIER') - box.prop(mmd_translation, 'batch_operation_script', text='', icon='SCRIPT') + box.label(text="Batch Operation:", icon="MODIFIER") + box.prop(mmd_translation, "batch_operation_script", text="", icon="SCRIPT") box.separator() row = box.row() - row.prop(mmd_translation, 'batch_operation_script_preset', text='Preset', icon='CON_TRANSFORM_CACHE') - row.operator(ExecuteTranslationBatchOperator.bl_idname, text='Execute') + row.prop(mmd_translation, "batch_operation_script_preset", text="Preset", icon="CON_TRANSFORM_CACHE") + row.operator(ExecuteTranslationBatchOperator.bl_idname, text="Execute") box.separator() translation_box = box.box().column(align=True) - translation_box.label(text='Dictionaries:', icon='HELP') + translation_box.label(text="Dictionaries:", icon="HELP") row = translation_box.row() - row.prop(mmd_translation, 'dictionary', text='to_english') + 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') + row.prop(mmd_translation, "dictionary", text="replace") def invoke(self, context: bpy.types.Context, _event): - root_object = FnModel.find_root(context.object) + root_object = FnModel.find_root_object(context.object) if root_object is None: - return {'CANCELLED'} + 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) @@ -290,28 +301,28 @@ def invoke(self, context: bpy.types.Context, _event): return context.window_manager.invoke_props_dialog(self, width=800) def execute(self, context): - root_object = FnModel.find_root(context.object) + root_object = FnModel.find_root_object(context.object) if root_object is None: - return {'CANCELLED'} + return {"CANCELLED"} FnTranslations.apply_translations(root_object) FnTranslations.clear_data(root_object.mmd_root.translation) - return {'FINISHED'} + return {"FINISHED"} class ExecuteTranslationBatchOperator(bpy.types.Operator): - bl_idname = 'mmd_tools.execute_translation_batch' - bl_label = 'Execute Translation Batch' - bl_options = {'INTERNAL'} + bl_idname = "mmd_tools.execute_translation_batch" + bl_label = "Execute Translation Batch" + bl_options = {"INTERNAL"} def execute(self, context: bpy.types.Context): - root = FnModel.find_root(context.object) + root = FnModel.find_root_object(context.object) if root is None: - return {'CANCELLED'} + 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'} + return {"FINISHED"} diff --git a/mmd_tools/operators/view.py b/mmd_tools/operators/view.py index 1a05aa4d..af1e600c 100644 --- a/mmd_tools/operators/view.py +++ b/mmd_tools/operators/view.py @@ -1,87 +1,89 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import re -import bpy + from bpy.types import Operator from mathutils import Matrix -from mmd_tools.bpyutils import matmul - class _SetShadingBase: - bl_options = {'REGISTER', 'UNDO'} + bl_options = {"REGISTER", "UNDO"} @staticmethod def _get_view3d_spaces(context): - if getattr(context.area, 'type', None) == 'VIEW_3D': + 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') + return (area.spaces[0] for area in getattr(context.screen, "areas", ()) if area.type == "VIEW_3D") @staticmethod def _reset_color_management(context, use_display_device=True): try: - context.scene.display_settings.display_device = ('None', 'sRGB')[use_display_device] + context.scene.display_settings.display_device = ("None", "sRGB")[use_display_device] except TypeError: pass @staticmethod 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 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 s.material.use_nodes = False s.material.use_shadeless = use_shadeless - def execute(self, context): #TODO - context.scene.render.engine = 'BLENDER_EEVEE' + def execute(self, context): + context.scene.render.engine = "BLENDER_EEVEE_NEXT" - shading_mode = 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' - shading.light = 'FLAT' if shading_mode == 'SHADELESS' else 'STUDIO' - shading.color_type = 'TEXTURE' if shading_mode else 'MATERIAL' + shading.type = "SOLID" + shading.light = "FLAT" if shading_mode == "SHADELESS" else "STUDIO" + shading.color_type = "TEXTURE" if shading_mode else "MATERIAL" shading.show_object_outline = False shading.show_backface_culling = False - return {'FINISHED'} + return {"FINISHED"} class SetGLSLShading(Operator, _SetShadingBase): - bl_idname = 'mmd_tools.set_glsl_shading' - bl_label = 'GLSL View' - bl_description = '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 = "GLSL" - _shading_mode = 'GLSL' class SetShadelessGLSLShading(Operator, _SetShadingBase): - bl_idname = 'mmd_tools.set_shadeless_glsl_shading' - bl_label = 'Shadeless GLSL View' - bl_description = '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 = "SHADELESS" - _shading_mode = 'SHADELESS' class ResetShading(Operator, _SetShadingBase): - bl_idname = 'mmd_tools.reset_shading' - bl_label = 'Reset View' - bl_description = '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 = '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'} + 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 = [ - {"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}, - {"re": re.compile(r'^(L|R)([\.\- _])(.+)$', re.IGNORECASE), "lr": 0}, - {"re": re.compile(r'^(.+)(左|右)(\.\d+)?$'), "lr": 1}, - {"re": re.compile(r'^(左|右)(.+)$'), "lr": 0}, - ] + {"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}, + {"re": re.compile(r"^(L|R)([\.\- _])(.+)$", re.IGNORECASE), "lr": 0}, + {"re": re.compile(r"^(.+)(左|右)(\.\d+)?$"), "lr": 1}, + {"re": re.compile(r"^(左|右)(.+)$"), "lr": 0}, + ] __LR_MAP = { "RIGHT": "LEFT", "Right": "Left", @@ -95,7 +97,8 @@ class FlipPose(Operator): "r": "l", "左": "右", "右": "左", - } + } + @classmethod def flip_name(cls, name): for regex in cls.__LR_REGEX: @@ -105,14 +108,14 @@ def flip_name(cls, name): lr = groups[regex["lr"]] if lr in cls.__LR_MAP: flip_lr = cls.__LR_MAP[lr] - name = '' + name = "" for i, s in enumerate(groups): if i == regex["lr"]: name += flip_lr elif s: name += s return name - return '' + return "" @staticmethod def __cmul(vec1, vec2): @@ -120,28 +123,25 @@ def __cmul(vec1, vec2): @staticmethod def __matrix_compose(loc, rot, scale): - return matmul(matmul(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)])) + 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, bone_src, bone_dest): from mathutils import Quaternion + 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() - loc = cls.__cmul(matmul(mi, loc), (-1, 1, 1)) - rot = cls.__cmul(Quaternion(matmul(mi, rot.axis), rot.angle).normalized(), (1, 1, -1, -1)) - bone_dest.matrix_basis = cls.__matrix_compose(matmul(m, loc), Quaternion(matmul(m, rot.axis), rot.angle).normalized(), scale) + loc = cls.__cmul(mi @ loc, (-1, 1, 1)) + rot = cls.__cmul(Quaternion(mi @ rot.axis, rot.angle).normalized(), (1, 1, -1, -1)) + bone_dest.matrix_basis = cls.__matrix_compose(m @ loc, Quaternion(m @ rot.axis, rot.angle).normalized(), scale) @classmethod def poll(cls, context): - return (context.active_object and - context.active_object.type == 'ARMATURE' and - context.active_object.mode == 'POSE') + return context.active_object and context.active_object.type == "ARMATURE" and context.active_object.mode == "POSE" 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]: self.__flip_pose(mat, b, pose_bones.get(self.flip_name(b.name), b)) - return {'FINISHED'} - + return {"FINISHED"} diff --git a/mmd_tools/panels/__init__.py b/mmd_tools/panels/__init__.py index 40a96afc..3cd94a1b 100644 --- a/mmd_tools/panels/__init__.py +++ b/mmd_tools/panels/__init__.py @@ -1 +1,3 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. diff --git a/mmd_tools/panels/prop_bone.py b/mmd_tools/panels/prop_bone.py index f2d23917..69b500f3 100644 --- a/mmd_tools/panels/prop_bone.py +++ b/mmd_tools/panels/prop_bone.py @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. -from bpy.types import Panel +import bpy -class MMDBonePanel(Panel): - bl_idname = 'BONE_PT_mmd_tools_bone' - bl_label = 'MMD Bone Tools' - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'bone' +class MMDBonePanel(bpy.types.Panel): + bl_idname = "BONE_PT_mmd_tools_bone" + bl_label = "MMD Bone Tools" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "bone" @classmethod def poll(cls, context): @@ -16,25 +18,24 @@ def poll(cls, context): def __draw_ik_data(self, pose_bone): bones = pose_bone.id_data.pose.bones - ik_bone_names = tuple(c.subtarget for c in pose_bone.constraints if c.type == 'IK' and c.subtarget in bones) + ik_bone_names = tuple(c.subtarget for c in pose_bone.constraints if c.type == "IK" and c.subtarget in bones) if ik_bone_names: - ik_custom_map = {getattr(b.constraints.get('mmd_ik_target_custom', None), 'subtarget', None) for b in bones if not b.is_mmd_shadow_bone} + ik_custom_map = {getattr(b.constraints.get("mmd_ik_target_custom", None), "subtarget", None) for b in bones if not b.is_mmd_shadow_bone} row = self.layout.column(align=True) for name in ik_bone_names: if name in ik_custom_map: - row.prop(bones[name].mmd_bone, 'ik_rotation_constraint', text='IK Angle {%s}'%name) + row.prop(bones[name].mmd_bone, "ik_rotation_constraint", text="IK Angle {%s}" % name) else: - row.prop(pose_bone.mmd_bone, 'ik_rotation_constraint', text='IK Angle (%s)'%name) + row.prop(pose_bone.mmd_bone, "ik_rotation_constraint", text="IK Angle (%s)" % name) def draw(self, context): - pose_bone = context.active_pose_bone or \ - context.active_object.pose.bones.get(context.active_bone.name, None) + pose_bone = context.active_pose_bone or context.active_object.pose.bones.get(context.active_bone.name, None) if pose_bone is None: return layout = self.layout if pose_bone.is_mmd_shadow_bone: - layout.label(text='MMD Shadow Bone!', icon='INFO') + layout.label(text="MMD Shadow Bone!", icon="INFO") return mmd_bone = pose_bone.mmd_bone @@ -42,51 +43,50 @@ def draw(self, context): c = layout.column() row = c.row(align=True) - row.label(text='Information:') + row.label(text="Information:") if not mmd_bone.is_id_unique(): - row.label(icon='ERROR') - row.prop(mmd_bone, 'bone_id', text='ID') + row.label(icon="ERROR") + row.prop(mmd_bone, "bone_id", text="ID") - c.prop(mmd_bone, 'name_j') - c.prop(mmd_bone, 'name_e') + c.prop(mmd_bone, "name_j") + c.prop(mmd_bone, "name_e") c = layout.column(align=True) row = c.row() - row.prop(mmd_bone, 'transform_order') - row.prop(mmd_bone, 'transform_after_dynamics') + row.prop(mmd_bone, "transform_order") + row.prop(mmd_bone, "transform_after_dynamics") row = c.row() - row.prop(mmd_bone, 'is_controllable') - row.prop(mmd_bone, 'is_tip') + row.prop(mmd_bone, "is_controllable") + row.prop(mmd_bone, "is_tip") self.__draw_ik_data(pose_bone) c = layout.column(align=True) row = c.row(align=True) - row.prop(mmd_bone, 'enabled_fixed_axis') - row.operator('mmd_tools.bone_fixed_axis_setup', text='', icon='X').type = 'DISABLE' - row.operator('mmd_tools.bone_fixed_axis_setup', text='Load').type = 'LOAD' - row.operator('mmd_tools.bone_fixed_axis_setup', text='Apply').type = 'APPLY' + row.prop(mmd_bone, "enabled_fixed_axis") + row.operator("mmd_tools.bone_fixed_axis_setup", text="", icon="X").type = "DISABLE" + row.operator("mmd_tools.bone_fixed_axis_setup", text="Load").type = "LOAD" + row.operator("mmd_tools.bone_fixed_axis_setup", text="Apply").type = "APPLY" row = c.row() row.active = mmd_bone.enabled_fixed_axis - row.column(align=True).prop(mmd_bone, 'fixed_axis', text='') + row.column(align=True).prop(mmd_bone, "fixed_axis", text="") c = layout.column(align=True) row = c.row(align=True) - row.prop(mmd_bone, 'enabled_local_axes') - row.operator('mmd_tools.bone_local_axes_setup', text='', icon='X').type = 'DISABLE' - row.operator('mmd_tools.bone_local_axes_setup', text='Load').type = 'LOAD' - row.operator('mmd_tools.bone_local_axes_setup', text='Apply').type = 'APPLY' + row.prop(mmd_bone, "enabled_local_axes") + row.operator("mmd_tools.bone_local_axes_setup", text="", icon="X").type = "DISABLE" + row.operator("mmd_tools.bone_local_axes_setup", text="Load").type = "LOAD" + row.operator("mmd_tools.bone_local_axes_setup", text="Apply").type = "APPLY" row = c.row() row.active = mmd_bone.enabled_local_axes - row.column(align=True).prop(mmd_bone, 'local_axis_x') - row.column(align=True).prop(mmd_bone, 'local_axis_z') + row.column(align=True).prop(mmd_bone, "local_axis_x") + row.column(align=True).prop(mmd_bone, "local_axis_z") c = layout.column(align=True) row = c.row(align=True) - row.prop(mmd_bone, 'has_additional_rotation', text='Rotate +', toggle=True) - row.prop(mmd_bone, 'has_additional_location', text='Move +', toggle=True) + row.prop(mmd_bone, "has_additional_rotation", text="Rotate +", toggle=True) + row.prop(mmd_bone, "has_additional_location", text="Move +", toggle=True) if mmd_bone.is_additional_transform_dirty: - row.label(icon='ERROR') - c.prop_search(mmd_bone, 'additional_transform_bone', pose_bone.id_data.pose, 'bones', icon='BONE_DATA', text='') - c.prop(mmd_bone, 'additional_transform_influence', text='Influence', slider=True) - + row.label(icon="ERROR") + c.prop_search(mmd_bone, "additional_transform_bone", pose_bone.id_data.pose, "bones", icon="BONE_DATA", text="") + c.prop(mmd_bone, "additional_transform_influence", text="Influence", slider=True) diff --git a/mmd_tools/panels/prop_camera.py b/mmd_tools/panels/prop_camera.py index ee9e9a74..d014c11e 100644 --- a/mmd_tools/panels/prop_camera.py +++ b/mmd_tools/panels/prop_camera.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. from bpy.types import Panel -from mmd_tools.core.camera import MMDCamera +from ..core.camera import MMDCamera + class MMDCameraPanel(Panel): - bl_idname = 'OBJECT_PT_mmd_tools_camera' - bl_label = 'MMD Camera Tools' - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'data' + bl_idname = "OBJECT_PT_mmd_tools_camera" + bl_label = "MMD Camera Tools" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "data" @classmethod def poll(cls, context): obj = context.active_object - return obj and (obj.type == 'CAMERA' or MMDCamera.isMMDCamera(obj)) + return obj and (obj.type == "CAMERA" or MMDCamera.isMMDCamera(obj)) def draw(self, context): obj = context.active_object @@ -29,13 +32,13 @@ def draw(self, context): row = layout.row() c = row.column() - c.prop(empty, 'location') - c.prop(camera, 'location', index=1, text='Distance') + c.prop(empty, "location") + c.prop(camera, "location", index=1, text="Distance") c = row.column() - c.prop(empty, 'rotation_euler') + c.prop(empty, "rotation_euler") - layout.prop(empty.mmd_camera, 'angle') - layout.prop(empty.mmd_camera, 'is_perspective') + layout.prop(empty.mmd_camera, "angle") + layout.prop(empty.mmd_camera, "is_perspective") else: - layout.operator('mmd_tools.convert_to_mmd_camera', text='Convert') + layout.operator("mmd_tools.convert_to_mmd_camera", text="Convert") diff --git a/mmd_tools/panels/prop_lamp.py b/mmd_tools/panels/prop_lamp.py index d701230f..50530729 100644 --- a/mmd_tools/panels/prop_lamp.py +++ b/mmd_tools/panels/prop_lamp.py @@ -1,15 +1,18 @@ # -*- coding: utf-8 -*- +# Copyright 2017 MMD Tools authors +# This file is part of MMD Tools. from bpy.types import Panel -from mmd_tools.core.lamp import MMDLamp +from ..core.lamp import MMDLamp + class MMDLampPanel(Panel): - bl_idname = 'OBJECT_PT_mmd_tools_light' - bl_label = 'MMD Light Tools' - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'data' + bl_idname = "OBJECT_PT_mmd_tools_light" + bl_label = "MMD Light Tools" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "data" @classmethod def poll(cls, context): @@ -27,8 +30,7 @@ def draw(self, context): lamp = mmd_lamp.lamp() c = layout.column() - c.prop(lamp.data, 'color') - c.prop(lamp, 'location', text='Light Source') + c.prop(lamp.data, "color") + c.prop(lamp, "location", text="Light Source") else: - layout.operator('mmd_tools.convert_to_mmd_lamp', text='Convert') - + layout.operator("mmd_tools.convert_to_mmd_lamp", text="Convert") diff --git a/mmd_tools/panels/prop_material.py b/mmd_tools/panels/prop_material.py index 00eea547..c5229589 100644 --- a/mmd_tools/panels/prop_material.py +++ b/mmd_tools/panels/prop_material.py @@ -1,27 +1,23 @@ # -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. -import bpy from bpy.types import Panel -from mmd_tools.core.material import FnMaterial - - -ICON_FILE_FOLDER = 'FILE_FOLDER' -if bpy.app.version < (2, 80, 0): - ICON_FILE_FOLDER = 'FILESEL' +from ..core.material import FnMaterial class MMDMaterialPanel(Panel): - bl_idname = 'MATERIAL_PT_mmd_tools_material' - bl_label = 'MMD Material' - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'material' + bl_idname = "MATERIAL_PT_mmd_tools_material" + bl_label = "MMD Material" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" @classmethod def poll(cls, context): obj = context.active_object - return obj.active_material and obj.mmd_type == 'NONE' + return obj.active_material and obj.mmd_type == "NONE" def draw(self, context): material = context.active_object.active_material @@ -32,55 +28,56 @@ def draw(self, context): col = layout.column() row = col.row(align=True) - row.label(text='Information:') + row.label(text="Information:") if not mmd_material.is_id_unique(): - row.label(icon='ERROR') - row.prop(mmd_material, 'material_id', text='ID') + row.label(icon="ERROR") + row.prop(mmd_material, "material_id", text="ID") - col.prop(mmd_material, 'name_j') - col.prop(mmd_material, 'name_e') - col.prop(mmd_material, 'comment') + col.prop(mmd_material, "name_j") + col.prop(mmd_material, "name_e") + col.prop(mmd_material, "comment") col = layout.column() - col.label(text='Color:') + col.label(text="Color:") r = col.row() - r.prop(mmd_material, 'diffuse_color') - r.prop(mmd_material, 'alpha', slider=True) + r.prop(mmd_material, "diffuse_color") + r.prop(mmd_material, "alpha", slider=True) r = col.row() - r.prop(mmd_material, 'specular_color') - r.prop(mmd_material, 'shininess', slider=True) + r.prop(mmd_material, "specular_color") + r.prop(mmd_material, "shininess", slider=True) r = col.row() - r.prop(mmd_material, 'ambient_color') - r.label() # for alignment only + r.prop(mmd_material, "ambient_color") + r.label() # for alignment only col = layout.column() - col.label(text='Shadow:') + col.label(text="Shadow:") r = col.row() - r.prop(mmd_material, 'is_double_sided') - r.prop(mmd_material, 'enabled_drop_shadow') + r.prop(mmd_material, "is_double_sided") + r.prop(mmd_material, "enabled_drop_shadow") r = col.row() - r.prop(mmd_material, 'enabled_self_shadow_map') - r.prop(mmd_material, 'enabled_self_shadow') + r.prop(mmd_material, "enabled_self_shadow_map") + r.prop(mmd_material, "enabled_self_shadow") col = layout.column() r = col.row() - r.prop(mmd_material, 'enabled_toon_edge') + r.prop(mmd_material, "enabled_toon_edge") r = col.row() r.active = mmd_material.enabled_toon_edge - r.prop(mmd_material, 'edge_color') - r.prop(mmd_material, 'edge_weight', slider=True) + r.prop(mmd_material, "edge_color") + r.prop(mmd_material, "edge_weight", slider=True) + class MMDTexturePanel(Panel): - bl_idname = 'MATERIAL_PT_mmd_tools_texture' - bl_label = 'MMD Texture' - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'material' + bl_idname = "MATERIAL_PT_mmd_tools_texture" + bl_label = "MMD Texture" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "material" @classmethod def poll(cls, context): obj = context.active_object - return obj.active_material and obj.mmd_type == 'NONE' + return obj.active_material and obj.mmd_type == "NONE" def draw(self, context): material = context.active_object.active_material @@ -91,41 +88,40 @@ def draw(self, context): fnMat = FnMaterial(material) col = layout.column() - col.label(text='Texture:') + col.label(text="Texture:") r = col.row(align=True) tex = fnMat.get_texture() if tex: - if tex.type == 'IMAGE' and tex.image: - r.prop(tex.image, 'filepath', text='') - r.operator('mmd_tools.material_remove_texture', text='', icon='PANEL_CLOSE') + if tex.type == "IMAGE" and tex.image: + r.prop(tex.image, "filepath", text="") + r.operator("mmd_tools.material_remove_texture", text="", icon="PANEL_CLOSE") else: - r.operator('mmd_tools.material_remove_texture', text='Remove', icon='PANEL_CLOSE') - r.label(icon='ERROR') + r.operator("mmd_tools.material_remove_texture", text="Remove", icon="PANEL_CLOSE") + r.label(icon="ERROR") else: - r.operator('mmd_tools.material_open_texture', text='Add', icon=ICON_FILE_FOLDER) + r.operator("mmd_tools.material_open_texture", text="Add", icon="FILE_FOLDER") col = layout.column() - col.label(text='Sphere Texture:') + col.label(text="Sphere Texture:") r = col.row(align=True) tex = fnMat.get_sphere_texture() if tex: - if tex.type == 'IMAGE' and tex.image: - r.prop(tex.image, 'filepath', text='') - r.operator('mmd_tools.material_remove_sphere_texture', text='', icon='PANEL_CLOSE') + if tex.type == "IMAGE" and tex.image: + r.prop(tex.image, "filepath", text="") + r.operator("mmd_tools.material_remove_sphere_texture", text="", icon="PANEL_CLOSE") else: - r.operator('mmd_tools.material_remove_sphere_texture', text='Remove', icon='PANEL_CLOSE') - r.label(icon='ERROR') + r.operator("mmd_tools.material_remove_sphere_texture", text="Remove", icon="PANEL_CLOSE") + r.label(icon="ERROR") else: - r.operator('mmd_tools.material_open_sphere_texture', text='Add', icon=ICON_FILE_FOLDER) - col.row(align=True).prop(mmd_material, 'sphere_texture_type', expand=True) + r.operator("mmd_tools.material_open_sphere_texture", text="Add", icon="FILE_FOLDER") + col.row(align=True).prop(mmd_material, "sphere_texture_type", expand=True) col = layout.column() row = col.row() - row.prop(mmd_material, 'is_shared_toon_texture') + row.prop(mmd_material, "is_shared_toon_texture") r = row.row() r.active = mmd_material.is_shared_toon_texture - r.prop(mmd_material, 'shared_toon_texture') + r.prop(mmd_material, "shared_toon_texture") r = col.row() r.active = not mmd_material.is_shared_toon_texture - r.prop(mmd_material, 'toon_texture') - + r.prop(mmd_material, "toon_texture") diff --git a/mmd_tools/panels/prop_object.py b/mmd_tools/panels/prop_object.py index 7d43c334..44478d9c 100644 --- a/mmd_tools/panels/prop_object.py +++ b/mmd_tools/panels/prop_object.py @@ -1,35 +1,35 @@ # -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. import bpy -import mmd_tools.core.model as mmd_model +from ..core.model import FnModel -class _PanelBase(object): - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'object' - -class MMDModelObjectPanel(_PanelBase, bpy.types.Panel): - bl_idname = 'OBJECT_PT_mmd_tools_root_object' - bl_label = 'MMD Model Information' +class MMDModelObjectPanel(bpy.types.Panel): + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + bl_idname = "OBJECT_PT_mmd_tools_root_object" + bl_label = "MMD Model Information" @classmethod def poll(cls, context): - return mmd_model.FnModel.find_root(context.active_object) + return FnModel.find_root_object(context.active_object) def draw(self, context): layout = self.layout obj = context.active_object - root = mmd_model.FnModel.find_root(obj) + root = FnModel.find_root_object(obj) c = layout.column() - c.prop(root.mmd_root, 'name') - c.prop(root.mmd_root, 'name_e') + c.prop(root.mmd_root, "name") + c.prop(root.mmd_root, "name_e") c = layout.column() - c.prop_search(root.mmd_root, 'comment_text', search_data=bpy.data, search_property='texts') - c.prop_search(root.mmd_root, 'comment_e_text', search_data=bpy.data, search_property='texts') + c.prop_search(root.mmd_root, "comment_text", search_data=bpy.data, search_property="texts") + c.prop_search(root.mmd_root, "comment_e_text", search_data=bpy.data, search_property="texts") c = layout.column() - c.operator('mmd_tools.change_mmd_ik_loop_factor', text='Change MMD IK Loop Factor') - c.operator('mmd_tools.recalculate_bone_roll', text='Recalculate bone roll') + c.operator("mmd_tools.change_mmd_ik_loop_factor", text="Change MMD IK Loop Factor") + c.operator("mmd_tools.recalculate_bone_roll", text="Recalculate bone roll") diff --git a/mmd_tools/panels/prop_physics.py b/mmd_tools/panels/prop_physics.py index 7b7f5d48..2c73afc0 100644 --- a/mmd_tools/panels/prop_physics.py +++ b/mmd_tools/panels/prop_physics.py @@ -1,98 +1,101 @@ # -*- coding: utf-8 -*- +# Copyright 2021 MMD Tools authors +# This file is part of MMD Tools. import bpy -import mmd_tools.core.model as mmd_model + +from ..core.model import FnModel class MMDRigidPanel(bpy.types.Panel): - bl_idname = 'RIGID_PT_mmd_tools_bone' - bl_label = 'MMD Rigid Body' - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'physics' + bl_idname = "RIGID_PT_mmd_tools_bone" + bl_label = "MMD Rigid Body" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "physics" __RIGID_SIZE_MAP = { - 'SPHERE': ('Radius',), - 'BOX': ('X', 'Y', 'Z'), - 'CAPSULE': ('Radius', 'Height'), + "SPHERE": ("Radius",), + "BOX": ("X", "Y", "Z"), + "CAPSULE": ("Radius", "Height"), } @classmethod def poll(cls, context): obj = context.active_object - return obj is not None and obj.mmd_type == 'RIGID_BODY' + return obj is not None and obj.mmd_type == "RIGID_BODY" def draw(self, context): obj = context.active_object layout = self.layout c = layout.column() - c.prop(obj.mmd_rigid, 'name_j') - c.prop(obj.mmd_rigid, 'name_e') + c.prop(obj.mmd_rigid, "name_j") + c.prop(obj.mmd_rigid, "name_e") c = layout.column(align=True) row = c.row(align=True) - row.prop(obj.mmd_rigid, 'type', expand=True) + row.prop(obj.mmd_rigid, "type", expand=True) - root = mmd_model.FnModel.find_root(obj) + root = FnModel.find_root_object(obj) if root is None: row = c.row(align=True) row.enabled = False - row.prop(obj.mmd_rigid, 'bone', text='', icon='BONE_DATA') + row.prop(obj.mmd_rigid, "bone", text="", icon="BONE_DATA") else: row = c.row(align=True) - armature = mmd_model.FnModel.find_armature(root) - row.prop_search(obj.mmd_rigid, 'bone', text='', search_data=armature.pose, search_property='bones', icon='BONE_DATA') + armature = FnModel.find_armature_object(root) + row.prop_search(obj.mmd_rigid, "bone", text="", search_data=armature.pose, search_property="bones", icon="BONE_DATA") c = layout.column(align=True) - c.enabled = obj.mode == 'OBJECT' - c.row(align=True).prop(obj.mmd_rigid, 'shape', expand=True) - #c.column(align=True).prop(obj.mmd_rigid, 'size', text='') + c.enabled = obj.mode == "OBJECT" + c.row(align=True).prop(obj.mmd_rigid, "shape", expand=True) + # c.column(align=True).prop(obj.mmd_rigid, 'size', text='') col = c.column(align=True) for i, name in enumerate(self.__RIGID_SIZE_MAP[obj.mmd_rigid.shape]): - col.prop(obj.mmd_rigid, 'size', text=name, index=i) + col.prop(obj.mmd_rigid, "size", text=name, index=i) row = layout.row() if obj.rigid_body is None: - row.operator('rigidbody.object_add', icon='MESH_ICOSPHERE') + row.operator("rigidbody.object_add", icon="MESH_ICOSPHERE") return c = row.column() - c.prop(obj.rigid_body, 'mass') - c.prop(obj.mmd_rigid, 'collision_group_number') + c.prop(obj.rigid_body, "mass") + c.prop(obj.mmd_rigid, "collision_group_number") c = row.column() - c.prop(obj.rigid_body, 'restitution') - c.prop(obj.rigid_body, 'friction') + c.prop(obj.rigid_body, "restitution") + c.prop(obj.rigid_body, "friction") c = layout.column() - #c.prop(obj.mmd_rigid, 'collision_group_mask') + # c.prop(obj.mmd_rigid, 'collision_group_mask') col = c.column(align=True) - col.label(text='Collision Group Mask:') + col.label(text="Collision Group Mask:") row = col.row(align=True) for i in range(0, 8): - row.prop(obj.mmd_rigid, 'collision_group_mask', index=i, text=str(i), toggle=True) + row.prop(obj.mmd_rigid, "collision_group_mask", index=i, text=str(i), toggle=True) row = col.row(align=True) for i in range(8, 16): - row.prop(obj.mmd_rigid, 'collision_group_mask', index=i, text=str(i), toggle=True) + row.prop(obj.mmd_rigid, "collision_group_mask", index=i, text=str(i), toggle=True) c = layout.column() - c.label(text='Damping') + c.label(text="Damping") row = c.row() - row.prop(obj.rigid_body, 'linear_damping') - row.prop(obj.rigid_body, 'angular_damping') + row.prop(obj.rigid_body, "linear_damping") + row.prop(obj.rigid_body, "angular_damping") class MMDJointPanel(bpy.types.Panel): - bl_idname = 'JOINT_PT_mmd_tools_bone' - bl_label = 'MMD Joint' - bl_space_type = 'PROPERTIES' - bl_region_type = 'WINDOW' - bl_context = 'physics' + bl_idname = "JOINT_PT_mmd_tools_bone" + bl_label = "MMD Joint" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "physics" @classmethod def poll(cls, context): obj = context.active_object - return obj is not None and obj.mmd_type == 'JOINT' + return obj is not None and obj.mmd_type == "JOINT" def draw(self, context): obj = context.active_object @@ -100,48 +103,48 @@ def draw(self, context): layout = self.layout c = layout.column() - c.prop(obj.mmd_joint, 'name_j') - c.prop(obj.mmd_joint, 'name_e') + c.prop(obj.mmd_joint, "name_j") + c.prop(obj.mmd_joint, "name_e") c = layout.column() if rbc is None: - c.operator('rigidbody.constraint_add', icon='CONSTRAINT').type = 'GENERIC_SPRING' + c.operator("rigidbody.constraint_add", icon="CONSTRAINT").type = "GENERIC_SPRING" else: - c.prop(rbc, 'object1') - c.prop(rbc, 'object2') + c.prop(rbc, "object1") + c.prop(rbc, "object2") row = layout.row(align=True) col = row.column(align=True) - col.label(text='X-Axis:') - col.label(text='Y-Axis:') - col.label(text='Z-Axis:') + col.label(text="X-Axis:") + col.label(text="Y-Axis:") + col.label(text="Z-Axis:") col = row.column(align=True) row = col.row(align=True) - row.prop(rbc, 'limit_lin_x_lower') - row.prop(rbc, 'limit_lin_x_upper') + row.prop(rbc, "limit_lin_x_lower") + row.prop(rbc, "limit_lin_x_upper") row = col.row(align=True) - row.prop(rbc, 'limit_lin_y_lower') - row.prop(rbc, 'limit_lin_y_upper') + row.prop(rbc, "limit_lin_y_lower") + row.prop(rbc, "limit_lin_y_upper") row = col.row(align=True) - row.prop(rbc, 'limit_lin_z_lower') - row.prop(rbc, 'limit_lin_z_upper') + row.prop(rbc, "limit_lin_z_lower") + row.prop(rbc, "limit_lin_z_upper") row = layout.row(align=True) col = row.column(align=True) - col.label(text='X-Axis:') - col.label(text='Y-Axis:') - col.label(text='Z-Axis:') + col.label(text="X-Axis:") + col.label(text="Y-Axis:") + col.label(text="Z-Axis:") col = row.column(align=True) row = col.row(align=True) - row.prop(rbc, 'limit_ang_x_lower') - row.prop(rbc, 'limit_ang_x_upper') + row.prop(rbc, "limit_ang_x_lower") + row.prop(rbc, "limit_ang_x_upper") row = col.row(align=True) - row.prop(rbc, 'limit_ang_y_lower') - row.prop(rbc, 'limit_ang_y_upper') + row.prop(rbc, "limit_ang_y_lower") + row.prop(rbc, "limit_ang_y_upper") row = col.row(align=True) - row.prop(rbc, 'limit_ang_z_lower') - row.prop(rbc, 'limit_ang_z_upper') + row.prop(rbc, "limit_ang_z_lower") + row.prop(rbc, "limit_ang_z_upper") row = layout.row() - row.column(align=True).prop(obj.mmd_joint, 'spring_linear') - row.column(align=True).prop(obj.mmd_joint, 'spring_angular') + row.column(align=True).prop(obj.mmd_joint, "spring_linear") + row.column(align=True).prop(obj.mmd_joint, "spring_angular") diff --git a/mmd_tools/panels/shading.py b/mmd_tools/panels/shading.py new file mode 100644 index 00000000..84ddda6b --- /dev/null +++ b/mmd_tools/panels/shading.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 MMD Tools authors +# This file is part of MMD Tools. + +import bpy + + +class MMDShadingPanel(bpy.types.Panel): + bl_idname = "VIEW3D_PT_mmd_shading" + bl_label = "MMD UuuNyaa" + bl_space_type = "VIEW_3D" + bl_region_type = "HEADER" + + def draw(self, _context): + pass + + @staticmethod + def draw_panel(this: bpy.types.Panel, context): + if context.space_data.shading.type != "SOLID": + return + + col = this.layout.column(align=True) + col.label(text="MMD Shading Presets") + row = col.row(align=True) + row.operator("mmd_tools.set_glsl_shading", text="GLSL") + row.operator("mmd_tools.set_shadeless_glsl_shading", text="Shadeless") + row = col.row(align=True) + row.operator("mmd_tools.reset_shading", text="Reset") + + @staticmethod + def register(): + bpy.types.VIEW3D_PT_shading.append(MMDShadingPanel.draw_panel) + + @staticmethod + def unregister(): + bpy.types.VIEW3D_PT_shading.remove(MMDShadingPanel.draw_panel) diff --git a/mmd_tools/panels/sidebar.py b/mmd_tools/panels/sidebar.py deleted file mode 100644 index a1e2b88c..00000000 --- a/mmd_tools/panels/sidebar.py +++ /dev/null @@ -1,245 +0,0 @@ -# -*- coding: utf-8 -*- - -import time - -import bpy -from mmd_tools.core import model -from mmd_tools.core.sdef import FnSDEF - - -class MMDToolsSceneSetupPanel(bpy.types.Panel): - bl_idname = 'OBJECT_PT_mmd_tools_scene_setup' - bl_label = 'Scene Setup' - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_category = 'MMD' - - __LANGUAGE_MANUAL_URL = { - 'ja_JP': 'https://mmd-blender.fandom.com/ja/wiki/MMD_Tools/%E3%83%9E%E3%83%8B%E3%83%A5%E3%82%A2%E3%83%AB', - } - - def draw(self, context: bpy.types.Context): - self.layout.row(align=True).operator( - 'wm.url_open', text='MMD Tools/Manual', icon='URL' - ).url = self.__LANGUAGE_MANUAL_URL.get(context.preferences.view.language, 'https://mmd-blender.fandom.com/wiki/MMD_Tools/Manual') - - self.draw_io() - self.draw_timeline(context) - self.draw_rigid_body(context) - - def draw_io(self): - row = self.layout.row() - col = row.column(align=True) - col.label(text='Model:', icon='OUTLINER_OB_ARMATURE') - col.operator('mmd_tools.import_model', text='Import') - col.operator('mmd_tools.export_pmx', text='Export') - - col = row.column(align=True) - col.label(text='Motion:', icon='ANIM') - col.operator('mmd_tools.import_vmd', text='Import') - col.operator('mmd_tools.export_vmd', text='Export') - - col = row.column(align=True) - col.label(text='Pose:', icon='POSE_HLT') - col.operator('mmd_tools.import_vpd', text='Import') - col.operator('mmd_tools.export_vpd', text='Export') - - def draw_timeline(self, context): - col = self.layout.column(align=True) - row = col.row(align=False) - row.label(text='Timeline:', icon='TIME') - row.prop(context.scene, 'frame_current') - row = col.row(align=True) - row.prop(context.scene, 'frame_start', text='Start') - row.prop(context.scene, 'frame_end', text='End') - - def draw_rigid_body(self, context): - rigidbody_world = context.scene.rigidbody_world - - layout = self.layout - col = layout.column(align=True) - row = col.row(align=False) - row.label(text='Rigid Body Physics:', icon='PHYSICS') - row.row().operator( - 'mmd_tools.rigid_body_world_update', - text='Update World', - icon='NONE' if getattr(rigidbody_world, 'substeps_per_frame', 0) == 1 else 'ERROR' - ) - - if rigidbody_world: - row = col.row(align=True) - row.prop(rigidbody_world, 'substeps_per_frame', text='Substeps') - row.prop(rigidbody_world, 'solver_iterations', text='Iterations') - - point_cache = rigidbody_world.point_cache - - col = layout.column(align=True) - row = col.row(align=True) - row.enabled = not point_cache.is_baked - row.prop(point_cache, 'frame_start') - row.prop(point_cache, 'frame_end') - - row = col.row(align=True) - if point_cache.is_baked is True: - row.operator("mmd_tools.ptcache_rigid_body_delete_bake", text="Delete Bake") - else: - row.operator("mmd_tools.ptcache_rigid_body_bake", text="Bake") - - -class MMDToolsModelSetupPanel(bpy.types.Panel): - bl_idname = 'OBJECT_PT_mmd_tools_model_setup' - bl_label = 'Model Setup' - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - bl_category = 'MMD' - - def draw(self, context: bpy.types.Context): - active_object: bpy.types.Object = context.active_object - mmd_root_object = model.Model.findRoot(active_object) - - if mmd_root_object is None: - self.layout.label(text='Select a MMD Model') - return - - col = self.layout.column(align=True) - col.label(text=mmd_root_object.mmd_root.name, icon='OUTLINER_OB_ARMATURE') - - self.draw_visibility(context, mmd_root_object) - self.draw_assembly(context, mmd_root_object) - self.draw_ik_toggle(context, mmd_root_object) - self.draw_mesh(context, mmd_root_object) - self.draw_material(context, mmd_root_object) - self.draw_misc(context, mmd_root_object) - - def draw_visibility(self, context, mmd_root_object): - col = self.layout.column(align=True) - row = col.row(align=False) - row.label(text='Visibility:', icon='HIDE_OFF') - row.operator('mmd_tools.reset_object_visibility', text='Reset') - - mmd_root = mmd_root_object.mmd_root - row = col.row(align=False) - cell = row.row(align=True) - cell.prop(mmd_root, 'show_meshes', toggle=True, text='Mesh', icon='MESH_DATA') - cell.prop(mmd_root, 'show_armature', toggle=True, text='Armature', icon='ARMATURE_DATA') - cell.prop(mmd_root, 'show_temporary_objects', toggle=True, text='Temporary Object', icon='EMPTY_AXIS') - cell = row.row(align=True) - cell.prop(mmd_root, 'show_rigid_bodies', toggle=True, text='Rigid Body', icon='RIGID_BODY') - cell.prop(mmd_root, 'show_names_of_rigid_bodies', toggle=True, icon_only=True, icon='SHORTDISPLAY') - cell = row.row(align=True) - cell.prop(mmd_root, 'show_joints', toggle=True, text='Joint', icon='RIGID_BODY_CONSTRAINT') - cell.prop(mmd_root, 'show_names_of_joints', toggle=True, icon_only=True, icon='SHORTDISPLAY') - - def draw_assembly(self, context, mmd_root_object): - col = self.layout.column(align=False) - row = col.row(align=True) - row.label(text='Assembly:', icon='MODIFIER_ON') - - grid = col.grid_flow(row_major=True) - - row = grid.row(align=True) - row.operator('mmd_tools.assemble_all', text='All', icon='SETTINGS') - row.operator('mmd_tools.disassemble_all', text='', icon='TRASH') - - row = grid.row(align=True) - row.operator('mmd_tools.sdef_bind', text='SDEF', icon='MOD_SIMPLEDEFORM') - if len(FnSDEF.g_verts) > 0: - row.operator('mmd_tools.sdef_cache_reset', text='', icon='FILE_REFRESH') - row.operator('mmd_tools.sdef_unbind', text='', icon='TRASH') - - row = grid.row(align=True) - row.operator('mmd_tools.apply_additional_transform', text='Bone', icon='CONSTRAINT_BONE') - row.operator('mmd_tools.clean_additional_transform', text='', icon='TRASH') - - row = grid.row(align=True) - row.operator('mmd_tools.morph_slider_setup', text='Morph', icon='SHAPEKEY_DATA').type = 'BIND' - row.operator('mmd_tools.morph_slider_setup', text='', icon='TRASH').type = 'UNBIND' - - row = grid.row(align=True) - row.active = getattr(context.scene.rigidbody_world, 'enabled', False) - - mmd_root = mmd_root_object.mmd_root - if not mmd_root.is_built: - row.operator('mmd_tools.build_rig', text='Physics', icon='PHYSICS', depress=False) - else: - row.operator('mmd_tools.clean_rig', text='Physics', icon='PHYSICS', depress=True) - - row = grid.row(align=True) - row.prop(mmd_root, 'use_property_driver', text='Property', toggle=True, icon='DRIVER') - - __toggle_items_ttl = 0.0 - __toggle_items_cache = None - - def __get_toggle_items(self, mmd_root_object: bpy.types.Object): - if self.__toggle_items_ttl > time.time(): - return self.__toggle_items_cache - - self.__toggle_items_ttl = time.time() + 10 - self.__toggle_items_cache = [] - armature_object = model.FnModel.find_armature(mmd_root_object) - pose_bones = armature_object.pose.bones - ik_map = { - pose_bones[c.subtarget]: (b.bone, c.chain_count, not c.is_valid) - for b in pose_bones - for c in b.constraints - if c.type == 'IK' and c.subtarget in pose_bones - } - - if not ik_map: - return self.__toggle_items_cache - - base = sum(b.bone.length for b in ik_map.keys())/len(ik_map)*0.8 - - groups = {} - for ik, (b, cnt, err) in ik_map.items(): - if any(all(x) for x in zip(ik.bone.layers, armature_object.data.layers)): - px, py, pz = -ik.bone.head_local/base - bx, by, bz = -b.head_local/base*0.15 - groups.setdefault( - (int(pz), int(bz), int(px**2), -cnt), set() - ).add(((px, -py, bx), ik)) # (px, pz, -py, bx, bz, -by) - - for _, group in sorted(groups.items()): - for _, ik in sorted(group, key=lambda x: x[0]): - ic = 'ERROR' if ik_map[ik][-1] else 'NONE' - self.__toggle_items_cache.append((ik, ic)) - - return self.__toggle_items_cache - - def draw_ik_toggle(self, _context, mmd_root_object): - col = self.layout.column(align=True) - row = col.row(align=False) - row.label(text='IK Toggle:', icon='CON_KINEMATIC') - grid = col.grid_flow(row_major=True, align=True) - - for ik, ic in self.__get_toggle_items(mmd_root_object): - grid.row(align=True).prop(ik, 'mmd_ik_toggle', text=ik.name, toggle=True, icon=ic) - - def draw_mesh(self, context, mmd_root_object): - col = self.layout.column(align=True) - col.label(text='Mesh:', icon='MESH_DATA') - grid = col.grid_flow(row_major=True, align=True) - grid.row(align=True).operator('mmd_tools.separate_by_materials', text='Separate by Materials', icon='MOD_EXPLODE') - grid.row(align=True).operator('mmd_tools.join_meshes', text='Join', icon='MESH_CUBE') - - def draw_material(self, context, mmd_root_object): - col = self.layout.column(align=True) - col.label(text='Material:', icon='MATERIAL') - - grid = col.grid_flow(row_major=True, align=False) - row = grid.row(align=True) - row.prop(mmd_root_object.mmd_root, 'use_toon_texture', text='Toon Texture', toggle=True, icon='SHADING_RENDERED') - row.prop(mmd_root_object.mmd_root, 'use_sphere_texture', text='Sphere Texture', toggle=True, icon='MATSPHERE') - row = grid.row(align=True) - row.operator('mmd_tools.edge_preview_setup', text='Edge Preview', icon='ANTIALIASED').action = 'CREATE' - row.operator('mmd_tools.edge_preview_setup', text='', icon='TRASH').action = 'CLEAN' - row = grid.row(align=True) - row.operator('mmd_tools.convert_materials', text='Convert to Blender', icon='BLENDER') - - def draw_misc(self, context, mmd_root_object): - col = self.layout.column(align=True) - col.label(text='Misc:', icon='TOOL_SETTINGS') - grid = col.grid_flow(row_major=True) - grid.row(align=True).operator('mmd_tools.global_translation_popup', text='(Experimental) Global Translation') - grid.row(align=True).operator('mmd_tools.change_mmd_ik_loop_factor', text='Change MMD IK Loop Factor') - grid.row(align=True).operator('mmd_tools.clean_duplicated_material_morphs') diff --git a/mmd_tools/panels/sidebar/__init__.py b/mmd_tools/panels/sidebar/__init__.py new file mode 100644 index 00000000..661f90b5 --- /dev/null +++ b/mmd_tools/panels/sidebar/__init__.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 MMD Tools authors +# This file is part of MMD Tools. + +from typing import Optional + +import bpy + +from ...bpyutils import FnContext +from ...core.model import FnModel + + +class FnDraw: + @staticmethod + def draw_bone_special(layout: bpy.types.UILayout, armature: bpy.types.Object, bone_name: str, mmd_name: Optional[str] = None): + if armature is None: + return + row = layout.row(align=True) + p_bone: bpy.types.PoseBone = armature.pose.bones.get(bone_name, None) + if p_bone: + bone = p_bone.bone + if mmd_name: + row.prop(p_bone.mmd_bone, mmd_name, text="", emboss=True) + ic = "RESTRICT_VIEW_ON" if bone.hide else "RESTRICT_VIEW_OFF" + row.prop(bone, "hide", text="", emboss=p_bone.mmd_bone.is_tip, icon=ic) + row.active = armature.mode != "EDIT" + else: + row.label() # for alignment only + row.label(icon="ERROR") + + +class PT_PanelBase: + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "MMD" + + +class PT_ProductionPanelBase(PT_PanelBase): + @classmethod + def poll(cls, context): + return FnContext.get_addon_preferences_attribute(context, "enable_mmd_model_production_features", True) + + +class UL_ObjectsMixIn: + model_filter: bpy.props.EnumProperty( + name="Model Filter", + description="Show items of active model or all models", + items=[ + ("ACTIVE", "Active Model", "", 0), + ("ALL", "All Models", "", 1), + ], + default="ACTIVE", + ) + visible_only: bpy.props.BoolProperty( + name="Visible Only", + description="Only show visible items", + default=False, + ) + + def draw_item(self, context, layout, _data, item, _icon, _active_data, _active_propname, _index): + if self.layout_type in {"DEFAULT", "COMPACT"}: + row = layout.split(factor=0.5, align=True) + item_prop = getattr(item, self.prop_name) + row.prop(item_prop, "name_j", text="", emboss=False, icon=self.icon) + row = row.row(align=True) + row.prop(item_prop, "name_e", text="", emboss=True) + self.draw_item_special(context, row, item) + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text="", icon=self.icon) + + def draw_filter(self, context, layout): + row = layout.row(align=True) + row.prop(self, "model_filter", expand=True) + row.prop(self, "visible_only", text="", toggle=True, icon="RESTRICT_VIEW_OFF") + + def filter_items(self, context, data, propname): + objects = getattr(data, propname) + flt_flags = [~self.bitflag_filter_item] * len(objects) + flt_neworder = list(range(len(objects))) + + if self.model_filter == "ACTIVE": + active_root = FnModel.find_root_object(context.active_object) + for i, obj in enumerate(objects): + if obj.mmd_type == self.mmd_type and FnModel.find_root_object(obj) == active_root: + flt_flags[i] = self.bitflag_filter_item + else: + for i, obj in enumerate(objects): + if obj.mmd_type == self.mmd_type: + flt_flags[i] = self.bitflag_filter_item + + if self.visible_only: + for i, obj in enumerate(objects): + if obj.hide_get() and flt_flags[i] == self.bitflag_filter_item: + flt_flags[i] = ~self.bitflag_filter_item + + indices = (i for i, x in enumerate(flt_flags) if x == self.bitflag_filter_item) + for i_new, i_orig in enumerate(sorted(indices, key=lambda k: objects[k].name)): + flt_neworder[i_orig] = i_new + return flt_flags, flt_neworder diff --git a/mmd_tools/panels/sidebar/bone_order.py b/mmd_tools/panels/sidebar/bone_order.py new file mode 100644 index 00000000..179cda4e --- /dev/null +++ b/mmd_tools/panels/sidebar/bone_order.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 MMD Tools authors +# This file is part of MMD Tools. + +import bpy + +from ...core.model import FnModel +from . import PT_ProductionPanelBase + + +class MMDBoneOrder(PT_ProductionPanelBase, bpy.types.Panel): + bl_idname = "OBJECT_PT_mmd_tools_bone_order" + bl_label = "Bone Order" + bl_options = {"DEFAULT_CLOSED"} + bl_order = 5 + + def draw(self, context): + layout = self.layout + active_obj = context.active_object + root = FnModel.find_root_object(active_obj) + if root is None: + layout.label(text="Select a MMD Model") + return + + armature = FnModel.find_armature_object(root) + if armature is None: + layout.label(text="The armature object of active MMD model can't be found", icon="ERROR") + return + + bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root) + bone_count = MMD_TOOLS_UL_ModelBones.update_bone_tables(armature, bone_order_mesh_object) + + col = layout.column(align=True) + row = col.row() + if bone_order_mesh_object is None: + row.template_list("MMD_TOOLS_UL_ModelBones", "", armature.pose, "bones", root.vertex_groups, "active_index") + col.operator("mmd_tools.object_select", text="(%d) %s" % (bone_count, armature.name), icon="OUTLINER_OB_ARMATURE", emboss=False).name = armature.name + col.label(text='No mesh object with "mmd_bone_order_override" modifier', icon="ERROR") + else: + row.template_list("MMD_TOOLS_UL_ModelBones", "", bone_order_mesh_object, "vertex_groups", bone_order_mesh_object.vertex_groups, "active_index") + + tb = row.column() + tb.enabled = bone_order_mesh_object == active_obj + tb1 = tb.column(align=True) + tb1.menu("OBJECT_MT_mmd_tools_bone_order_menu", text="", icon="DOWNARROW_HLT") + tb.separator() + tb1 = tb.column(align=True) + tb1.operator("object.vertex_group_move", text="", icon="TRIA_UP").direction = "UP" + tb1.operator("object.vertex_group_move", text="", icon="TRIA_DOWN").direction = "DOWN" + + row = col.row() + row.operator("mmd_tools.object_select", text="(%d) %s" % (bone_count, armature.name), icon="OUTLINER_OB_ARMATURE", emboss=False).name = armature.name + row.label(icon="BACK") + row.operator("mmd_tools.object_select", text=bone_order_mesh_object.name, icon="OBJECT_DATA", emboss=False).name = bone_order_mesh_object.name + + +class _DummyVertexGroup: + index = None + + def __init__(self, index): + self.index = index + + +class MMD_TOOLS_UL_ModelBones(bpy.types.UIList): + _IK_MAP = {} + _IK_BONES = {} + _DUMMY_VERTEX_GROUPS = {} + + @classmethod + def __wrap_pose_bones(cls, pose_bones): + for i, b in enumerate(pose_bones): + cls._DUMMY_VERTEX_GROUPS[b.name] = _DummyVertexGroup(i) + yield b + + @classmethod + def update_bone_tables(cls, armature, bone_order_object): + cls._IK_MAP.clear() + cls._IK_BONES.clear() + cls._DUMMY_VERTEX_GROUPS.clear() + + ik_target_override = {} + ik_target_custom = {} + ik_target_fin = {} + pose_bones = armature.pose.bones + bone_count = len(pose_bones) + pose_bone_list = pose_bones if bone_order_object else cls.__wrap_pose_bones(pose_bones) + + for b in pose_bone_list: + if b.is_mmd_shadow_bone: + bone_count -= 1 + continue + for c in b.constraints: + if c.type == "IK" and c.subtarget in pose_bones and c.subtarget not in cls._IK_BONES: + if not c.use_tail: + cls._IK_MAP.setdefault(hash(b), []).append(c.subtarget) + cls._IK_BONES[c.subtarget] = ik_target_fin[c.subtarget] = hash(b) + bone_chain = b.parent_recursive + else: + cls._IK_BONES[c.subtarget] = b.name + bone_chain = [b] + b.parent_recursive + for l in bone_chain[: c.chain_count]: + cls._IK_MAP.setdefault(hash(l), []).append(c.subtarget) + if "mmd_ik_target_custom" == c.name: + ik_target_custom[getattr(c, "subtarget", "")] = hash(b) + elif "mmd_ik_target_override" == c.name and b.parent: + if b.parent.name == getattr(c, "subtarget", ""): + for c in b.parent.constraints: + if c.type == "IK" and c.subtarget in pose_bones and c.subtarget not in ik_target_override and c.subtarget not in ik_target_custom: + ik_target_override[c.subtarget] = hash(b) + + for k, v in ik_target_custom.items(): + if k not in ik_target_fin and k in cls._IK_BONES: + cls._IK_BONES[k] = v + cls._IK_MAP.setdefault(v, []).append(k) + if k in ik_target_override: + del ik_target_override[k] + + for k, v in ik_target_override.items(): + if k not in ik_target_fin and k in cls._IK_BONES: + cls._IK_BONES[k] = v + cls._IK_MAP.setdefault(v, []).append(k) + + for k, v in tuple(cls._IK_BONES.items()): + if isinstance(v, str): + b = cls.__get_ik_target_bone(pose_bones[v]) + if b: + cls._IK_BONES[k] = hash(b) + cls._IK_MAP.setdefault(hash(b), []).append(k) + else: + del cls._IK_BONES[k] + return bone_count + + @staticmethod + def __get_ik_target_bone(target_bone): + r = None + min_length = None + for c in (c for c in target_bone.children if not c.is_mmd_shadow_bone): + if c.bone.use_connect: + return c + length = (c.head - target_bone.tail).length + if min_length is None or length < min_length: + min_length = length + r = c + return r + + @classmethod + def _draw_bone_item(cls, layout, bone_name, pose_bones, vertex_groups, index): + bone = pose_bones.get(bone_name, None) + if not bone or bone.is_mmd_shadow_bone: + layout.active = False + layout.label(text=bone_name, translate=False, icon="GROUP_BONE" if bone else "MESH_DATA") + r = layout.row() + r.alignment = "RIGHT" + r.label(text=str(index)) + else: + row = layout.split(factor=0.45, align=False) + r0 = row.row() + r0.label(text=bone_name, translate=False, icon="POSE_HLT" if bone_name in cls._IK_BONES else "BONE_DATA") + r = r0.row() + r.alignment = "RIGHT" + r.label(text=str(index)) + + row_sub = row.split(factor=0.67, align=False) + + mmd_bone = bone.mmd_bone + count = len(pose_bones) + bone_transform_rank = index + mmd_bone.transform_order * count + + r = row_sub.row() + bone_parent = bone.parent + if bone_parent: + bone_parent = bone_parent.name + idx = vertex_groups.get(bone_parent, _DummyVertexGroup).index + if idx is None or bone_transform_rank < (idx + pose_bones[bone_parent].mmd_bone.transform_order * count): + r.label(text=str(idx), icon="ERROR") + else: + r.label(text=str(idx), icon="INFO" if index < idx else "FILE_PARENT") + else: + r.label() + + r = r.row() + if mmd_bone.has_additional_rotation: + append_bone = mmd_bone.additional_transform_bone + idx = vertex_groups.get(append_bone, _DummyVertexGroup).index + if idx is None or bone_transform_rank < (idx + pose_bones[append_bone].mmd_bone.transform_order * count): + if append_bone: + r.label(text=str(idx), icon="ERROR") + else: + r.label(text=str(idx), icon="IPO_QUAD" if mmd_bone.has_additional_location else "IPO_EXPO") + elif mmd_bone.has_additional_location: + append_bone = mmd_bone.additional_transform_bone + idx = vertex_groups.get(append_bone, _DummyVertexGroup).index + if idx is None or bone_transform_rank < (idx + pose_bones[append_bone].mmd_bone.transform_order * count): + if append_bone: + r.label(text=str(idx), icon="ERROR") + else: + r.label(text=str(idx), icon="IPO_LINEAR") + + for idx, b in sorted(((vertex_groups.get(b, _DummyVertexGroup).index, b) for b in cls._IK_MAP.get(hash(bone), ())), key=lambda i: i[0] or 0): + ik_bone = pose_bones[b] + is_ik_chain = hash(bone) != cls._IK_BONES.get(b) + if idx is None or (is_ik_chain and bone_transform_rank > (idx + ik_bone.mmd_bone.transform_order * count)): + r.prop(ik_bone, "mmd_ik_toggle", text=str(idx), toggle=True, icon="ERROR") + elif b not in cls._IK_BONES: + r.prop(ik_bone, "mmd_ik_toggle", text=str(idx), toggle=True, icon="QUESTION") + else: + r.prop(ik_bone, "mmd_ik_toggle", text=str(idx), toggle=True, icon="LINKED" if is_ik_chain else "HOOK") + + row = row_sub.row(align=True) + if mmd_bone.transform_after_dynamics: + row.prop(mmd_bone, "transform_after_dynamics", text="", toggle=True, icon="PHYSICS") + else: + row.prop(mmd_bone, "transform_after_dynamics", text="", toggle=True) + row.prop(mmd_bone, "transform_order", text="", slider=bool(mmd_bone.transform_order)) + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + if self.layout_type in {"DEFAULT"}: + if self._DUMMY_VERTEX_GROUPS: + self._draw_bone_item(layout, item.name, data.bones, self._DUMMY_VERTEX_GROUPS, index) + else: + self._draw_bone_item(layout, item.name, data.parent.pose.bones, data.vertex_groups, index) + elif self.layout_type in {"COMPACT"}: + pass + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text="", icon_value=icon) + + +class MMDBoneOrderMenu(bpy.types.Menu): + bl_idname = "OBJECT_MT_mmd_tools_bone_order_menu" + bl_label = "Bone Order Menu" + + def draw(self, _context): + layout = self.layout + layout.operator("object.vertex_group_sort", text="Sort by Bone Hierarchy", icon="BONE_DATA").sort_type = "BONE_HIERARCHY" + layout.separator() + layout.operator("mmd_tools.add_missing_vertex_groups_from_bones", icon="PRESET_NEW") diff --git a/mmd_tools/panels/sidebar/display_panel.py b/mmd_tools/panels/sidebar/display_panel.py new file mode 100644 index 00000000..0b4be6da --- /dev/null +++ b/mmd_tools/panels/sidebar/display_panel.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. + + +import bpy + +from ...core.model import FnModel +from . import FnDraw, PT_ProductionPanelBase +from ...utils import ItemOp + + +class MMDDisplayItemsPanel(PT_ProductionPanelBase, bpy.types.Panel): + bl_idname = "OBJECT_PT_mmd_tools_display_items" + bl_label = "Display Panel" + bl_options = {"DEFAULT_CLOSED"} + bl_order = 10 + + def draw(self, context): + active_obj = context.active_object + root = FnModel.find_root_object(active_obj) + if root is None: + self.layout.label(text="Select a MMD Model") + return + + mmd_root = root.mmd_root + col = self.layout.column() + row = col.row() + row.template_list( + "MMD_ROOT_UL_display_item_frames", + "", + mmd_root, + "display_item_frames", + mmd_root, + "active_display_item_frame", + ) + tb = row.column() + tb1 = tb.column(align=True) + tb1.operator("mmd_tools.display_item_frame_add", text="", icon="ADD") + tb1.operator("mmd_tools.display_item_frame_remove", text="", icon="REMOVE") + tb1.menu("OBJECT_MT_mmd_tools_display_item_frame_menu", text="", icon="DOWNARROW_HLT") + tb.separator() + tb1 = tb.column(align=True) + tb1.operator("mmd_tools.display_item_frame_move", text="", icon="TRIA_UP").type = "UP" + tb1.operator("mmd_tools.display_item_frame_move", text="", icon="TRIA_DOWN").type = "DOWN" + + frame = ItemOp.get_by_index(mmd_root.display_item_frames, mmd_root.active_display_item_frame) + if frame is None: + return + + c = col.column(align=True) + row = c.row() + row.template_list( + "MMD_ROOT_UL_display_items", + "", + frame, + "data", + frame, + "active_item", + ) + tb = row.column() + tb1 = tb.column(align=True) + tb1.operator("mmd_tools.display_item_add", text="", icon="ADD") + tb1.operator("mmd_tools.display_item_remove", text="", icon="REMOVE") + tb1.menu("OBJECT_MT_mmd_tools_display_item_menu", text="", icon="DOWNARROW_HLT") + tb.separator() + tb1 = tb.column(align=True) + tb1.operator("mmd_tools.display_item_move", text="", icon="TRIA_UP").type = "UP" + tb1.operator("mmd_tools.display_item_move", text="", icon="TRIA_DOWN").type = "DOWN" + + row = col.row() + r = row.row(align=True) + r.operator("mmd_tools.display_item_find", text="Bone", icon="VIEWZOOM").type = "BONE" + r.operator("mmd_tools.display_item_find", text="Morph", icon="VIEWZOOM").type = "MORPH" + row.operator("mmd_tools.display_item_select_current", text="Select") + + +class MMD_ROOT_UL_display_item_frames(bpy.types.UIList): + def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index): + frame = item + if self.layout_type in {"DEFAULT"}: + row = layout.split(factor=0.5, align=True) + if frame.is_special: + row.label(text=frame.name, translate=False) + row = row.row(align=True) + row.label(text=frame.name_e, translate=False) + row.label(text="", icon="LOCKED") + else: + row.prop(frame, "name", text="", emboss=False) + row.prop(frame, "name_e", text="", emboss=True) + elif self.layout_type in {"COMPACT"}: + pass + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text="", icon_value=icon) + + +class MMD_ROOT_UL_display_items(bpy.types.UIList): + morph_filter: bpy.props.EnumProperty( + name="Morph Filter", + description="Only show items matching this category", + options={"ENUM_FLAG"}, + items=[ + ("SYSTEM", "Hidden", "", 1), + ("EYEBROW", "Eye Brow", "", 2), + ("EYE", "Eye", "", 4), + ("MOUTH", "Mouth", "", 8), + ("OTHER", "Other", "", 16), + ], + default={ + "SYSTEM", + "EYEBROW", + "EYE", + "MOUTH", + "OTHER", + }, + ) + mmd_name: bpy.props.EnumProperty( + name="MMD Name", + description="Show JP or EN name of MMD bone", + items=[ + ("name_j", "JP", "", 1), + ("name_e", "EN", "", 2), + ], + default="name_e", + ) + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + if self.layout_type in {"DEFAULT"}: + if item.type == "BONE": + row = layout.split(factor=0.5, align=True) + row.prop(item, "name", text="", emboss=False, icon="BONE_DATA") + FnDraw.draw_bone_special(row, FnModel.find_armature_object(item.id_data), item.name, self.mmd_name) + else: + row = layout.split(factor=0.6, align=True) + row.prop(item, "name", text="", emboss=False, icon="SHAPEKEY_DATA") + row = row.row(align=True) + row.prop(item, "morph_type", text="", emboss=False) + if item.name not in getattr(item.id_data.mmd_root, item.morph_type): + row.label(icon="ERROR") + elif self.layout_type in {"COMPACT"}: + pass + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text="", icon_value=icon) + + def filter_items(self, context, data, propname): + if len(self.morph_filter) == 5 or data.name != "表情": + return [], [] + + objects = getattr(data, propname) + flt_flags = [~self.bitflag_filter_item] * len(objects) + flt_neworder = [] + + for i, item in enumerate(objects): + morph = getattr(item.id_data.mmd_root, item.morph_type).get(item.name, None) + if morph and morph.category in self.morph_filter: + flt_flags[i] = self.bitflag_filter_item + + return flt_flags, flt_neworder + + def draw_filter(self, context, layout): + row = layout.row() + row.prop(self, "morph_filter", expand=True) + row.prop(self, "mmd_name", expand=True) + + +class MMDDisplayItemFrameMenu(bpy.types.Menu): + bl_idname = "OBJECT_MT_mmd_tools_display_item_frame_menu" + bl_label = "Display Item Frame Menu" + + def draw(self, context): + layout = self.layout + layout.operator_enum("mmd_tools.display_item_quick_setup", "type") + layout.separator() + layout.operator("mmd_tools.display_item_frame_move", icon="TRIA_UP_BAR", text="Move To Top").type = "TOP" + layout.operator("mmd_tools.display_item_frame_move", icon="TRIA_DOWN_BAR", text="Move To Bottom").type = "BOTTOM" + + +class MMDDisplayItemMenu(bpy.types.Menu): + bl_idname = "OBJECT_MT_mmd_tools_display_item_menu" + bl_label = "Display Item Menu" + + def draw(self, context): + layout = self.layout + layout.operator("mmd_tools.display_item_remove", text="Delete All", icon="X").all = True + layout.separator() + layout.operator("mmd_tools.display_item_move", icon="TRIA_UP_BAR", text="Move To Top").type = "TOP" + layout.operator("mmd_tools.display_item_move", icon="TRIA_DOWN_BAR", text="Move To Bottom").type = "BOTTOM" diff --git a/mmd_tools/panels/sidebar/joints.py b/mmd_tools/panels/sidebar/joints.py new file mode 100644 index 00000000..69016e61 --- /dev/null +++ b/mmd_tools/panels/sidebar/joints.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. + +import bpy + +from ...core.model import FnModel +from . import PT_ProductionPanelBase, UL_ObjectsMixIn + + +class MMDJointSelectorPanel(PT_ProductionPanelBase, bpy.types.Panel): + bl_idname = "OBJECT_PT_mmd_tools_joint_list" + bl_label = "Joints" + bl_options = {"DEFAULT_CLOSED"} + bl_order = 7 + + def draw(self, context): + active_obj = context.active_object + root = FnModel.find_root_object(active_obj) + if root is None: + self.layout.label(text="Select a MMD Model") + return + + col = self.layout.column() + c = col.column(align=True) + + row = c.row() + row.template_list( + "MMD_TOOLS_UL_joints", + "", + context.scene, + "objects", + root.mmd_root, + "active_joint_index", + ) + tb = row.column() + tb1 = tb.column(align=True) + tb1.operator("mmd_tools.joint_add", text="", icon="ADD") + tb1.operator("mmd_tools.joint_remove", text="", icon="REMOVE") + tb1.menu("OBJECT_MT_mmd_tools_joint_menu", text="", icon="DOWNARROW_HLT") + tb.separator() + tb1 = tb.column(align=True) + tb1.enabled = active_obj.mmd_type == "JOINT" + tb1.operator("mmd_tools.object_move", text="", icon="TRIA_UP").type = "UP" + tb1.operator("mmd_tools.object_move", text="", icon="TRIA_DOWN").type = "DOWN" + + +class MMD_TOOLS_UL_joints(bpy.types.UIList, UL_ObjectsMixIn): + mmd_type = "JOINT" + icon = "CONSTRAINT" + prop_name = "mmd_joint" + + def draw_item_special(self, context, layout, item): + rbc = item.rigid_body_constraint + if rbc is None: + layout.label(icon="ERROR") + elif rbc.object1 is None or rbc.object2 is None: + layout.label(icon="OBJECT_DATA") + elif rbc.object1 == rbc.object2: + layout.label(icon="MESH_CUBE") + + +class MMDJointMenu(bpy.types.Menu): + bl_idname = "OBJECT_MT_mmd_tools_joint_menu" + bl_label = "Joint Menu" + + def draw(self, context): + layout = self.layout + layout.enabled = context.active_object.mmd_type == "JOINT" + layout.operator("mmd_tools.object_move", icon="TRIA_UP_BAR", text="Move To Top").type = "TOP" + layout.operator("mmd_tools.object_move", icon="TRIA_DOWN_BAR", text="Move To Bottom").type = "BOTTOM" diff --git a/mmd_tools/panels/sidebar/material_sorter.py b/mmd_tools/panels/sidebar/material_sorter.py new file mode 100644 index 00000000..061bd7b7 --- /dev/null +++ b/mmd_tools/panels/sidebar/material_sorter.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 MMD Tools authors +# This file is part of MMD Tools. + +import bpy + +from . import PT_ProductionPanelBase + + +class MMDMaterialSorter(PT_ProductionPanelBase, bpy.types.Panel): + bl_idname = "OBJECT_PT_mmd_tools_material_sorter" + bl_label = "Material Sorter" + bl_options = {"DEFAULT_CLOSED"} + bl_order = 9 + + def draw(self, context): + layout = self.layout + active_obj = context.active_object + if active_obj is None or active_obj.type != "MESH" or active_obj.mmd_type != "NONE": + layout.label(text="Select a mesh object") + return + + col = layout.column(align=True) + row = col.row() + row.template_list("MMD_TOOLS_UL_Materials", "", active_obj.data, "materials", active_obj, "active_material_index") + tb = row.column() + tb1 = tb.column(align=True) + tb1.operator("mmd_tools.move_material_up", text="", icon="TRIA_UP") + tb1.operator("mmd_tools.move_material_down", text="", icon="TRIA_DOWN") + + +class MMD_TOOLS_UL_Materials(bpy.types.UIList): + def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index): + if self.layout_type in {"DEFAULT"}: + if item: + row = layout.row(align=True) + item_prop = getattr(item, "mmd_material") + row.prop(item_prop, "name_j", text="", emboss=False, icon="MATERIAL") + row.prop(item_prop, "name_e", text="", emboss=True) + else: + layout.label(text="UNSET", translate=False, icon="ERROR") + elif self.layout_type in {"COMPACT"}: + pass + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text="", icon_value=icon) + + def draw_filter(self, context, layout): + layout.label(text="Use the arrows to sort", icon="INFO") diff --git a/mmd_tools/panels/sidebar/meshes_sorter.py b/mmd_tools/panels/sidebar/meshes_sorter.py new file mode 100644 index 00000000..0742c44c --- /dev/null +++ b/mmd_tools/panels/sidebar/meshes_sorter.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 MMD Tools authors +# This file is part of MMD Tools. + +import bpy + +from ...core.model import FnModel +from . import PT_ProductionPanelBase + + +class MMDMeshSorter(PT_ProductionPanelBase, bpy.types.Panel): + bl_idname = "OBJECT_PT_mmd_tools_meshes_sorter" + bl_label = "Meshes Sorter" + bl_options = {"DEFAULT_CLOSED"} + bl_order = 8 + + def draw(self, context): + layout = self.layout + active_obj = context.active_object + root = FnModel.find_root_object(active_obj) + if root is None: + layout.label(text="Select a MMD Model") + return + + col = layout.column(align=True) + row = col.row() + row.template_list("MMD_TOOLS_UL_ModelMeshes", "", context.scene, "objects", root.mmd_root, "active_mesh_index") + tb = row.column() + tb1 = tb.column(align=True) + tb1.enabled = active_obj.type == "MESH" and active_obj.mmd_type == "NONE" + tb1.operator("mmd_tools.object_move", text="", icon="TRIA_UP_BAR").type = "TOP" + tb1.operator("mmd_tools.object_move", text="", icon="TRIA_UP").type = "UP" + tb1.operator("mmd_tools.object_move", text="", icon="TRIA_DOWN").type = "DOWN" + tb1.operator("mmd_tools.object_move", text="", icon="TRIA_DOWN_BAR").type = "BOTTOM" + + +class MMD_TOOLS_UL_ModelMeshes(bpy.types.UIList): + def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index): + if self.layout_type in {"DEFAULT"}: + layout.label(text=item.name, translate=False, icon="OBJECT_DATA") + elif self.layout_type in {"COMPACT"}: + pass + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text="", icon_value=icon) + + def draw_filter(self, context, layout): + layout.label(text="Use the arrows to sort", icon="INFO") + + def filter_items(self, context, data, propname): + # We will use the filtering to sort the mesh objects to match the rig order + objects = getattr(data, propname) + flt_flags = [~self.bitflag_filter_item] * len(objects) + flt_neworder = list(range(len(objects))) + + armature = FnModel.find_armature_object(FnModel.find_root_object(context.active_object)) + __is_child_of_armature = lambda x: x.parent and (x.parent == armature or __is_child_of_armature(x.parent)) + + name_dict = {} + for i, obj in enumerate(objects): + if obj.type == "MESH" and obj.mmd_type == "NONE" and __is_child_of_armature(obj): + flt_flags[i] = self.bitflag_filter_item + name_dict[obj.name] = i + + for new_index, name in enumerate(sorted(name_dict.keys())): + i = name_dict[name] + flt_neworder[i] = new_index + + return flt_flags, flt_neworder diff --git a/mmd_tools/panels/sidebar/model_production.py b/mmd_tools/panels/sidebar/model_production.py new file mode 100644 index 00000000..e83a3f20 --- /dev/null +++ b/mmd_tools/panels/sidebar/model_production.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. + +import bpy + +from ...core.model import FnModel +from . import PT_ProductionPanelBase + + +class MMDModelProductionPanel(PT_ProductionPanelBase, bpy.types.Panel): + bl_idname = "OBJECT_PT_mmd_tools_model_production" + bl_label = "Model Production" + bl_order = 3 + + def draw(self, context): + active_obj = context.active_object + + layout = self.layout + col = layout.column(align=True) + grid = col.grid_flow(row_major=True) + row = grid.row(align=True) + row.operator("mmd_tools.create_mmd_model_root_object", text="Create Model", icon="OUTLINER_OB_ARMATURE") + row.operator("mmd_tools.convert_to_mmd_model", text="Convert Model", icon="ARMATURE_DATA") + + root = FnModel.find_root_object(active_obj) + row = grid.row(align=True) + row.enabled = root is not None + row.operator("mmd_tools.attach_meshes", text="Attach Meshes", icon="OUTLINER_OB_MESH") + + row = grid.row(align=True) + row.operator("mmd_tools.translate_mmd_model", text="Translate", icon="HELP") + row.operator("mmd_tools.global_translation_popup", text="", icon="WINDOW") + + self.draw_edit(context) + + def draw_edit(self, _context): + col = self.layout.column(align=True) + col.label(text="Model Surgery:", icon="MOD_ARMATURE") + grid = col.grid_flow(row_major=True, align=True) + + separate_row = grid.row(align=True) + row = separate_row.row(align=True) + row.operator_context = "EXEC_DEFAULT" + op = row.operator("mmd_tools.model_separate_by_bones", text="Chop", icon="BONE_DATA") + op.separate_armature = True + op.include_descendant_bones = True + op.boundary_joint_owner = "DESTINATION" + + row = row.row(align=True) + row.operator_context = "INVOKE_DEFAULT" + op = row.operator("mmd_tools.model_separate_by_bones", text="", icon="WINDOW") + + row = separate_row.row(align=True) + row.operator_context = "EXEC_DEFAULT" + op = row.operator("mmd_tools.model_separate_by_bones", text="Peel", icon="MOD_EXPLODE") + op.separate_armature = False + op.include_descendant_bones = False + op.boundary_joint_owner = "DESTINATION" + + row = grid.row(align=True) + row.operator_context = "INVOKE_DEFAULT" + row.operator("mmd_tools.model_join_by_bones", text="Join", icon="GROUP_BONE") diff --git a/mmd_tools/panels/sidebar/model_setup.py b/mmd_tools/panels/sidebar/model_setup.py new file mode 100644 index 00000000..32a673b4 --- /dev/null +++ b/mmd_tools/panels/sidebar/model_setup.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 MMD Tools authors +# This file is part of MMD Tools. + +import time + +import bpy + +from ...core.model import FnModel +from ...core.sdef import FnSDEF +from . import PT_PanelBase + + +class MMDToolsModelSetupPanel(PT_PanelBase, bpy.types.Panel): + bl_idname = "OBJECT_PT_mmd_tools_model_setup" + bl_label = "Model Setup" + bl_order = 2 + + def draw(self, context: bpy.types.Context): + active_object: bpy.types.Object = context.active_object + mmd_root_object = FnModel.find_root_object(active_object) + + if mmd_root_object is None: + self.layout.label(text="Select a MMD Model") + return + + col = self.layout.column(align=True) + col.label(text=mmd_root_object.mmd_root.name, icon="OUTLINER_OB_ARMATURE") + + self.draw_visibility(context, mmd_root_object) + self.draw_assembly(context, mmd_root_object) + self.draw_ik_toggle(context, mmd_root_object) + self.draw_mesh(context, mmd_root_object) + self.draw_material(context, mmd_root_object) + self.draw_misc(context, mmd_root_object) + + def draw_visibility(self, context, mmd_root_object): + col = self.layout.column(align=True) + row = col.row(align=False) + row.label(text="Visibility:", icon="HIDE_OFF") + row.operator("mmd_tools.reset_object_visibility", text="Reset") + + mmd_root = mmd_root_object.mmd_root + row = col.row(align=False) + cell = row.row(align=True) + cell.prop(mmd_root, "show_meshes", toggle=True, text="Mesh", icon="MESH_DATA") + cell.prop(mmd_root, "show_armature", toggle=True, text="Armature", icon="ARMATURE_DATA") + cell.prop(mmd_root, "show_temporary_objects", toggle=True, text="Temporary Object", icon="EMPTY_AXIS") + cell = row.row(align=True) + cell.prop(mmd_root, "show_rigid_bodies", toggle=True, text="Rigid Body", icon="RIGID_BODY") + cell.prop(mmd_root, "show_names_of_rigid_bodies", toggle=True, icon_only=True, icon="SHORTDISPLAY") + cell = row.row(align=True) + cell.prop(mmd_root, "show_joints", toggle=True, text="Joint", icon="RIGID_BODY_CONSTRAINT") + cell.prop(mmd_root, "show_names_of_joints", toggle=True, icon_only=True, icon="SHORTDISPLAY") + + def draw_assembly(self, context, mmd_root_object): + col = self.layout.column(align=False) + row = col.row(align=True) + row.label(text="Assembly:", icon="MODIFIER_ON") + + grid = col.grid_flow(row_major=True) + + row = grid.row(align=True) + row.operator("mmd_tools.assemble_all", text="All", icon="SETTINGS") + row.operator("mmd_tools.disassemble_all", text="", icon="TRASH") + + row = grid.row(align=True) + row.operator("mmd_tools.sdef_bind", text="SDEF", icon="MOD_SIMPLEDEFORM") + if len(FnSDEF.g_verts) > 0: + row.operator("mmd_tools.sdef_cache_reset", text="", icon="FILE_REFRESH") + row.operator("mmd_tools.sdef_unbind", text="", icon="TRASH") + + row = grid.row(align=True) + row.operator("mmd_tools.apply_additional_transform", text="Bone", icon="CONSTRAINT_BONE") + row.operator("mmd_tools.clean_additional_transform", text="", icon="TRASH") + + row = grid.row(align=True) + row.operator("mmd_tools.morph_slider_setup", text="Morph", icon="SHAPEKEY_DATA").type = "BIND" + row.operator("mmd_tools.morph_slider_setup", text="", icon="TRASH").type = "UNBIND" + + row = grid.row(align=True) + row.active = getattr(context.scene.rigidbody_world, "enabled", False) + + mmd_root = mmd_root_object.mmd_root + if not mmd_root.is_built: + row.operator("mmd_tools.build_rig", text="Physics", icon="PHYSICS", depress=False) + else: + row.operator("mmd_tools.clean_rig", text="Physics", icon="PHYSICS", depress=True) + + row = grid.row(align=True) + row.prop(mmd_root, "use_property_driver", text="Property", toggle=True, icon="DRIVER") + + __toggle_items_ttl = 0.0 + __toggle_items_cache = None + + def __get_toggle_items(self, mmd_root_object: bpy.types.Object): + if self.__toggle_items_ttl > time.time(): + return self.__toggle_items_cache + + self.__toggle_items_ttl = time.time() + 10 + self.__toggle_items_cache = [] + armature_object = FnModel.find_armature_object(mmd_root_object) + pose_bones = armature_object.pose.bones + ik_map = {pose_bones[c.subtarget]: (b.bone, c.chain_count, not c.is_valid) for b in pose_bones for c in b.constraints if c.type == "IK" and c.subtarget in pose_bones} + + if not ik_map: + return self.__toggle_items_cache + + base = sum(b.bone.length for b in ik_map.keys()) / len(ik_map) * 0.8 + + groups = {} + for ik, (b, cnt, err) in ik_map.items(): + if any(c.is_visible for c in ik.bone.collections): + px, py, pz = -ik.bone.head_local / base + bx, by, bz = -b.head_local / base * 0.15 + groups.setdefault((int(pz), int(bz), int(px**2), -cnt), set()).add(((px, -py, bx), ik)) # (px, pz, -py, bx, bz, -by) + + for _, group in sorted(groups.items()): + for _, ik in sorted(group, key=lambda x: x[0]): + ic = "ERROR" if ik_map[ik][-1] else "NONE" + self.__toggle_items_cache.append((ik, ic)) + + return self.__toggle_items_cache + + def draw_ik_toggle(self, _context, mmd_root_object): + col = self.layout.column(align=True) + row = col.row(align=False) + row.label(text="IK Toggle:", icon="CON_KINEMATIC") + grid = col.grid_flow(row_major=True, align=True) + + for ik, ic in self.__get_toggle_items(mmd_root_object): + grid.row(align=True).prop(ik, "mmd_ik_toggle", text=ik.name, toggle=True, icon=ic) + + def draw_mesh(self, context, mmd_root_object): + col = self.layout.column(align=True) + col.label(text="Mesh:", icon="MESH_DATA") + grid = col.grid_flow(row_major=True, align=True) + grid.row(align=True).operator("mmd_tools.separate_by_materials", text="Separate by Materials", icon="MOD_EXPLODE") + grid.row(align=True).operator("mmd_tools.join_meshes", text="Join", icon="MESH_CUBE") + + def draw_material(self, context, mmd_root_object): + col = self.layout.column(align=True) + col.label(text="Material:", icon="MATERIAL") + + grid = col.grid_flow(row_major=True, align=False) + row = grid.row(align=True) + row.prop(mmd_root_object.mmd_root, "use_toon_texture", text="Toon Texture", toggle=True, icon="SHADING_RENDERED") + row.prop(mmd_root_object.mmd_root, "use_sphere_texture", text="Sphere Texture", toggle=True, icon="MATSPHERE") + row = grid.row(align=True) + row.operator("mmd_tools.edge_preview_setup", text="Edge Preview", icon="ANTIALIASED").action = "CREATE" + row.operator("mmd_tools.edge_preview_setup", text="", icon="TRASH").action = "CLEAN" + row = grid.row(align=True) + row.operator("mmd_tools.convert_materials", text="Convert to Blender", icon="BLENDER") + + def draw_misc(self, context, mmd_root_object): + col = self.layout.column(align=True) + col.label(text="Misc:", icon="TOOL_SETTINGS") + grid = col.grid_flow(row_major=True) + grid.row(align=True).operator("mmd_tools.global_translation_popup", text="(Experimental) Global Translation") + grid.row(align=True).operator("mmd_tools.change_mmd_ik_loop_factor", text="Change MMD IK Loop Factor") + grid.row(align=True).operator("mmd_tools.clean_duplicated_material_morphs") diff --git a/mmd_tools/panels/sidebar/morph_tools.py b/mmd_tools/panels/sidebar/morph_tools.py new file mode 100644 index 00000000..fa681613 --- /dev/null +++ b/mmd_tools/panels/sidebar/morph_tools.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. + +import bpy + +from ...core.model import FnModel, Model +from . import FnDraw, PT_ProductionPanelBase +from ...properties.morph import MaterialMorph +from ...utils import ItemOp +from ...operators import morph as operators_morph + + +class MMDMorphToolsPanel(PT_ProductionPanelBase, bpy.types.Panel): + bl_idname = "OBJECT_PT_mmd_tools_morph_tools" + bl_label = "Morph Tools" + bl_options = {"DEFAULT_CLOSED"} + bl_order = 4 + + def draw(self, context): + active_obj = context.active_object + root = FnModel.find_root_object(active_obj) + if root is None: + self.layout.label(text="Select a MMD Model") + return + + rig = Model(root) + mmd_root = root.mmd_root + col = self.layout.column() + row = col.row() + row.prop(mmd_root, "active_morph_type", expand=True) + morph_type = mmd_root.active_morph_type + + c = col.column(align=True) + row = c.row() + row.template_list("MMD_TOOLS_UL_Morphs", "", mmd_root, morph_type, mmd_root, "active_morph") + tb = row.column() + tb1 = tb.column(align=True) + tb1.operator("mmd_tools.morph_add", text="", icon="ADD") + tb1.operator("mmd_tools.morph_remove", text="", icon="REMOVE") + tb1.menu("OBJECT_MT_mmd_tools_morph_menu", text="", icon="DOWNARROW_HLT") + tb.separator() + tb1 = tb.column(align=True) + tb1.operator("mmd_tools.morph_move", text="", icon="TRIA_UP").type = "UP" + tb1.operator("mmd_tools.morph_move", text="", icon="TRIA_DOWN").type = "DOWN" + + morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph) + if morph: + slider = rig.morph_slider.get(morph.name) + if slider: + col.row().prop(slider, "value") + + row = col.row(align=True) + row.prop( + mmd_root, + "morph_panel_show_settings", + icon="TRIA_DOWN" if mmd_root.morph_panel_show_settings else "TRIA_RIGHT", + icon_only=True, + emboss=False, + ) + row.label(text="Morph Settings") + if mmd_root.morph_panel_show_settings: + draw_func = getattr(self, "_draw_%s_data" % morph_type[:-7], None) + if draw_func: + draw_func(context, rig, col, morph) + + def _template_morph_offset_list(self, layout, morph, list_type_name): + row = layout.row() + row.template_list( + list_type_name, + "", + morph, + "data", + morph, + "active_data", + ) + tb = row.column() + tb1 = tb.column(align=True) + tb1.operator("mmd_tools.morph_offset_add", text="", icon="ADD") + tb1.operator("mmd_tools.morph_offset_remove", text="", icon="REMOVE") + tb.operator("mmd_tools.morph_offset_remove", text="", icon="X").all = True + return ItemOp.get_by_index(morph.data, morph.active_data) + + def _draw_vertex_data(self, context, rig, col, morph): + r = col.row() + col = r.column(align=True) + for i in rig.meshes(): + shape_keys = i.data.shape_keys + if shape_keys is None: + continue + kb = shape_keys.key_blocks.get(morph.name, None) + if kb: + found = row = col.row(align=True) + row.active = not (i.show_only_shape_key or kb.mute) + row.operator("mmd_tools.object_select", text=i.name, icon="OBJECT_DATA").name = i.name + row.prop(kb, "value", text=kb.name) + if "found" not in locals(): + col.label(text="Not found", icon="INFO") + else: + r.operator("mmd_tools.morph_offset_remove", text="", icon="X").all = True + + def _draw_material_data(self, context, rig, col, morph): + col.label(text=bpy.app.translations.pgettext_iface("Material Offsets (%d)") % len(morph.data)) + data = self._template_morph_offset_list(col, morph, "MMD_TOOLS_UL_MaterialMorphOffsets") + if data is None: + return + + c_mat = col.column(align=True) + c_mat.prop_search(data, "related_mesh", bpy.data, "meshes") + + related_mesh = bpy.data.meshes.get(data.related_mesh, None) + c_mat.prop_search(data, "material", related_mesh or bpy.data, "materials") + + base_mat_name = data.material + if "_temp" in base_mat_name: + col.label(text="This is not a valid base material", icon="ERROR") + return + + work_mat = bpy.data.materials.get(base_mat_name + "_temp", None) + use_work_mat = work_mat and related_mesh and work_mat.name in related_mesh.materials + if not use_work_mat: + c = col.column() + row = c.row(align=True) + if base_mat_name == "": + row.label(text="This offset affects all materials", icon="INFO") + else: + row.operator(operators_morph.CreateWorkMaterial.bl_idname) + row.operator(operators_morph.ClearTempMaterials.bl_idname, text="Clear") + + row = c.row() + row.prop(data, "offset_type", expand=True) + r1 = row.row(align=True) + r1.operator(operators_morph.InitMaterialOffset.bl_idname, text="", icon="TRIA_LEFT").target_value = 0 + r1.operator(operators_morph.InitMaterialOffset.bl_idname, text="", icon="TRIA_RIGHT").target_value = 1 + row = c.row() + row.column(align=True).prop(data, "diffuse_color", expand=True, slider=True) + c1 = row.column(align=True) + c1.prop(data, "specular_color", expand=True, slider=True) + c1.prop(data, "shininess", slider=True) + row.column(align=True).prop(data, "ambient_color", expand=True, slider=True) + row = c.row() + row.column(align=True).prop(data, "edge_color", expand=True, slider=True) + row = c.row() + row.prop(data, "edge_weight", slider=True) + row = c.row() + row.column(align=True).prop(data, "texture_factor", expand=True, slider=True) + row.column(align=True).prop(data, "sphere_texture_factor", expand=True, slider=True) + row.column(align=True).prop(data, "toon_texture_factor", expand=True, slider=True) + else: + c_mat.enabled = False + c = col.column() + row = c.row(align=True) + row.operator(operators_morph.ApplyMaterialOffset.bl_idname, text="Apply") + row.operator(operators_morph.ClearTempMaterials.bl_idname, text="Clear") + + row = c.row() + row.prop(data, "offset_type") + row = c.row() + row.prop(work_mat.mmd_material, "diffuse_color") + row.prop(work_mat.mmd_material, "alpha", slider=True) + row = c.row() + row.prop(work_mat.mmd_material, "specular_color") + row.prop(work_mat.mmd_material, "shininess", slider=True) + row = c.row() + row.prop(work_mat.mmd_material, "ambient_color") + row.label() # for alignment only + row = c.row() + row.prop(work_mat.mmd_material, "edge_color") + row.prop(work_mat.mmd_material, "edge_weight", slider=True) + row = c.row() + row.column(align=True).prop(data, "texture_factor", expand=True, slider=True) + row.column(align=True).prop(data, "sphere_texture_factor", expand=True, slider=True) + row.column(align=True).prop(data, "toon_texture_factor", expand=True, slider=True) + + def _draw_bone_data(self, context, rig, col, morph): + armature = rig.armature() + if armature is None: + col.label(text="Armature not found", icon="ERROR") + return + + row = col.row(align=True) + row.operator(operators_morph.ViewBoneMorph.bl_idname, text="View") + row.operator(operators_morph.ApplyBoneMorph.bl_idname, text="Apply") + row.operator(operators_morph.ClearBoneMorphView.bl_idname, text="Clear") + + col.label(text=bpy.app.translations.pgettext_iface("Bone Offsets (%d)") % len(morph.data)) + data = self._template_morph_offset_list(col, morph, "MMD_TOOLS_UL_BoneMorphOffsets") + if data is None: + return + + row = col.row(align=True) + row.prop_search(data, "bone", armature.pose, "bones") + if data.bone: + row = col.row(align=True) + row.operator(operators_morph.SelectRelatedBone.bl_idname, text="Select") + row.operator(operators_morph.EditBoneOffset.bl_idname, text="Edit") + row.operator(operators_morph.ApplyBoneOffset.bl_idname, text="Update") + + row = col.row() + row.column(align=True).prop(data, "location") + row.column(align=True).prop(data, "rotation") + + def _draw_uv_data(self, context, rig, col, morph): + c = col.column(align=True) + row = c.row(align=True) + row.operator(operators_morph.ViewUVMorph.bl_idname, text="View") + row.operator(operators_morph.ClearUVMorphView.bl_idname, text="Clear") + row = c.row(align=True) + row.operator(operators_morph.EditUVMorph.bl_idname, text="Edit") + row.operator(operators_morph.ApplyUVMorph.bl_idname, text="Apply") + + c = col.column() + if len(morph.data): + row = c.row() + row.prop(morph, "data_type", expand=True) + row = c.row() + if morph.data_type == "VERTEX_GROUP": + row.prop(morph, "vertex_group_scale", text="Scale") + else: + row.label(text=bpy.app.translations.pgettext_iface("UV Offsets (%d)") % len(morph.data)) + # self._template_morph_offset_list(c, morph, 'MMD_TOOLS_UL_UVMorphOffsets') + row.prop(morph, "uv_index") + row.operator("mmd_tools.morph_offset_remove", text="", icon="X").all = True + + def _draw_group_data(self, context, rig, col, morph): + col.label(text=bpy.app.translations.pgettext_iface("Group Offsets (%d)") % len(morph.data)) + item = self._template_morph_offset_list(col, morph, "MMD_TOOLS_UL_GroupMorphOffsets") + if item is None: + return + + c = col.column(align=True) + row = c.split(factor=0.67, align=True) + row.prop_search(item, "name", morph.id_data.mmd_root, item.morph_type, icon="SHAPEKEY_DATA", text="") + row.prop(item, "morph_type", text="") + + +class MMD_TOOLS_UL_Morphs(bpy.types.UIList): + def draw_item(self, _context, layout, data, item, icon, _active_data, _active_propname, _index): + mmd_root = data + if self.layout_type in {"DEFAULT"}: + row = layout.split(factor=0.4, align=True) + row.prop(item, "name", text="", emboss=False, icon="SHAPEKEY_DATA") + row = row.split(factor=0.6, align=True) + row.prop(item, "name_e", text="", emboss=True) + row = row.row(align=True) + row.prop(item, "category", text="", emboss=False) + frame_facial = mmd_root.display_item_frames.get("表情") + morph_item = frame_facial.data.get(item.name) if frame_facial else None + if morph_item is None: + row.label(icon="INFO") + elif morph_item.morph_type != mmd_root.active_morph_type: + row.label(icon="SHAPEKEY_DATA") + else: + row.label(icon="BLANK1") + if isinstance(item, MaterialMorph) and any(not d.material for d in item.data): + row.label(icon="TEMP") + elif self.layout_type in {"COMPACT"}: + pass + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text="", icon_value=icon) + + +class MMD_TOOLS_UL_MaterialMorphOffsets(bpy.types.UIList): + def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index): + if self.layout_type in {"DEFAULT"}: + material = item.material + layout.label(text=material or "All Materials", translate=False, icon="MATERIAL") + elif self.layout_type in {"COMPACT"}: + pass + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text="", icon_value=icon) + + +class MMD_TOOLS_UL_UVMorphOffsets(bpy.types.UIList): + def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index): + if self.layout_type in {"DEFAULT"}: + layout.label(text=str(item.index), translate=False, icon="MESH_DATA") + layout.prop(item, "offset", text="", emboss=False, slider=True) + elif self.layout_type in {"COMPACT"}: + pass + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text="", icon_value=icon) + + +class MMD_TOOLS_UL_BoneMorphOffsets(bpy.types.UIList): + def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index): + if self.layout_type in {"DEFAULT"}: + layout.prop(item, "bone", text="", emboss=False, icon="BONE_DATA") + FnDraw.draw_bone_special(layout, FnModel.find_armature_object(item.id_data), item.bone) + elif self.layout_type in {"COMPACT"}: + pass + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text="", icon_value=icon) + + +class MMD_TOOLS_UL_GroupMorphOffsets(bpy.types.UIList): + def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index): + if self.layout_type in {"DEFAULT"}: + row = layout.split(factor=0.5, align=True) + row.prop(item, "name", text="", emboss=False, icon="SHAPEKEY_DATA") + row = row.row(align=True) + row.prop(item, "morph_type", text="", emboss=False) + if item.name in getattr(item.id_data.mmd_root, item.morph_type): + row.prop(item, "factor", text="", emboss=False, slider=True) + else: + row.label(icon="ERROR") + elif self.layout_type in {"COMPACT"}: + pass + elif self.layout_type in {"GRID"}: + layout.alignment = "CENTER" + layout.label(text="", icon_value=icon) + + +class MMDMorphMenu(bpy.types.Menu): + bl_idname = "OBJECT_MT_mmd_tools_morph_menu" + bl_label = "Morph Menu" + + def draw(self, context): + layout = self.layout + layout.operator("mmd_tools.morph_remove", text="Delete All", icon="X").all = True + layout.separator() + layout.operator("mmd_tools.morph_slider_setup", text="Bind morphs to .placeholder", icon="DRIVER").type = "BIND" + layout.operator("mmd_tools.morph_slider_setup", text="Unbind morphs from .placeholder", icon="UNLINKED").type = "UNBIND" + layout.separator() + layout.operator("mmd_tools.morph_copy", icon="COPY_ID") + layout.operator("mmd_tools.morph_overwrite_from_active_pose_library", icon="PRESET_NEW") + layout.operator("mmd_tools.clean_duplicated_material_morphs", icon="TRASH") + layout.separator() + layout.operator("mmd_tools.morph_move", icon="TRIA_UP_BAR", text="Move To Top").type = "TOP" + layout.operator("mmd_tools.morph_move", icon="TRIA_DOWN_BAR", text="Move To Bottom").type = "BOTTOM" diff --git a/mmd_tools/panels/sidebar/rigid_bodies.py b/mmd_tools/panels/sidebar/rigid_bodies.py new file mode 100644 index 00000000..4f072b52 --- /dev/null +++ b/mmd_tools/panels/sidebar/rigid_bodies.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. + +import bpy + +from ...core.model import FnModel +from . import PT_ProductionPanelBase, UL_ObjectsMixIn + + +class MMDRigidbodySelectorPanel(PT_ProductionPanelBase, bpy.types.Panel): + bl_idname = "OBJECT_PT_mmd_tools_rigidbody_list" + bl_label = "Rigid Bodies" + bl_options = {"DEFAULT_CLOSED"} + bl_order = 6 + + def draw(self, context): + active_obj = context.active_object + root = FnModel.find_root_object(active_obj) + if root is None: + self.layout.label(text="Select a MMD Model") + return + + col = self.layout.column() + c = col.column(align=True) + row = c.row() + row.template_list( + "MMD_TOOLS_UL_rigidbodies", + "", + context.scene, + "objects", + root.mmd_root, + "active_rigidbody_index", + ) + tb = row.column() + tb1 = tb.column(align=True) + tb1.operator("mmd_tools.rigid_body_add", text="", icon="ADD") + tb1.operator("mmd_tools.rigid_body_remove", text="", icon="REMOVE") + tb1.menu("OBJECT_MT_mmd_tools_rigidbody_menu", text="", icon="DOWNARROW_HLT") + tb.separator() + tb1 = tb.column(align=True) + tb1.enabled = active_obj.mmd_type == "RIGID_BODY" + tb1.operator("mmd_tools.object_move", text="", icon="TRIA_UP").type = "UP" + tb1.operator("mmd_tools.object_move", text="", icon="TRIA_DOWN").type = "DOWN" + + +class MMD_TOOLS_UL_rigidbodies(bpy.types.UIList, UL_ObjectsMixIn): + mmd_type = "RIGID_BODY" + icon = "MESH_ICOSPHERE" + prop_name = "mmd_rigid" + + def draw_item_special(self, context, layout, item): + rb = item.rigid_body + if rb is None: + layout.label(icon="ERROR") + elif not item.mmd_rigid.bone: + layout.label(icon="BONE_DATA") + + +class MMDRigidbodySelectMenu(bpy.types.Menu): + bl_idname = "OBJECT_MT_mmd_tools_rigidbody_select_menu" + bl_label = "Rigidbody Select Menu" + + def draw(self, context): + layout = self.layout + layout.operator_context = "INVOKE_DEFAULT" + layout.operator("mmd_tools.rigid_body_select", text="Select Similar...") + layout.separator() + layout.operator_context = "EXEC_DEFAULT" + layout.operator_enum("mmd_tools.rigid_body_select", "properties") + + +class MMDRigidbodyMenu(bpy.types.Menu): + bl_idname = "OBJECT_MT_mmd_tools_rigidbody_menu" + bl_label = "Rigidbody Menu" + + def draw(self, context): + layout = self.layout + layout.enabled = context.active_object.mmd_type == "RIGID_BODY" + layout.menu("OBJECT_MT_mmd_tools_rigidbody_select_menu", text="Select Similar") + layout.separator() + layout.operator("mmd_tools.object_move", icon="TRIA_UP_BAR", text="Move To Top").type = "TOP" + layout.operator("mmd_tools.object_move", icon="TRIA_DOWN_BAR", text="Move To Bottom").type = "BOTTOM" diff --git a/mmd_tools/panels/sidebar/scene_setup.py b/mmd_tools/panels/sidebar/scene_setup.py new file mode 100644 index 00000000..e19fccbc --- /dev/null +++ b/mmd_tools/panels/sidebar/scene_setup.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 MMD Tools authors +# This file is part of MMD Tools. + +import bpy + +from . import PT_PanelBase + + +class MMDToolsSceneSetupPanel(PT_PanelBase, bpy.types.Panel): + bl_idname = "OBJECT_PT_mmd_tools_scene_setup" + bl_label = "Scene Setup" + bl_order = 1 + + __LANGUAGE_MANUAL_URL = { + "ja_JP": "https://mmd-blender.fandom.com/ja/wiki/MMD_Tools/%E3%83%9E%E3%83%8B%E3%83%A5%E3%82%A2%E3%83%AB", + } + + def draw(self, context: bpy.types.Context): + self.layout.row(align=True).operator("wm.url_open", text="MMD Tools/Manual", icon="URL").url = self.__LANGUAGE_MANUAL_URL.get(context.preferences.view.language, "https://mmd-blender.fandom.com/wiki/MMD_Tools/Manual") + + self.draw_io() + self.draw_timeline(context) + self.draw_rigid_body(context) + + def draw_io(self): + row = self.layout.row() + col = row.column(align=True) + col.label(text="Model:", icon="OUTLINER_OB_ARMATURE") + col.operator("mmd_tools.import_model", text="Import") + col.operator("mmd_tools.export_pmx", text="Export") + + col = row.column(align=True) + col.label(text="Motion:", icon="ANIM") + col.operator("mmd_tools.import_vmd", text="Import") + col.operator("mmd_tools.export_vmd", text="Export") + + col = row.column(align=True) + col.label(text="Pose:", icon="POSE_HLT") + col.operator("mmd_tools.import_vpd", text="Import") + col.operator("mmd_tools.export_vpd", text="Export") + + def draw_timeline(self, context): + col = self.layout.column(align=True) + row = col.row(align=False) + row.label(text="Timeline:", icon="TIME") + row.prop(context.scene, "frame_current") + row = col.row(align=True) + row.prop(context.scene, "frame_start", text="Start") + row.prop(context.scene, "frame_end", text="End") + + def draw_rigid_body(self, context): + rigidbody_world = context.scene.rigidbody_world + + layout = self.layout + col = layout.column(align=True) + row = col.row(align=False) + row.label(text="Rigid Body Physics:", icon="PHYSICS") + row.row().operator("mmd_tools.rigid_body_world_update", text="Update World", icon="NONE" if getattr(rigidbody_world, "substeps_per_frame", 0) == 6 else "ERROR") + + if rigidbody_world: + row = col.row(align=True) + row.prop(rigidbody_world, "substeps_per_frame", text="Substeps") + row.prop(rigidbody_world, "solver_iterations", text="Iterations") + + point_cache = rigidbody_world.point_cache + + col = layout.column(align=True) + row = col.row(align=True) + row.enabled = not point_cache.is_baked + row.prop(point_cache, "frame_start") + row.prop(point_cache, "frame_end") + + row = col.row(align=True) + if point_cache.is_baked is True: + row.operator("mmd_tools.ptcache_rigid_body_delete_bake", text="Delete Bake") + else: + row.operator("mmd_tools.ptcache_rigid_body_bake", text="Bake") diff --git a/mmd_tools/panels/tool.py b/mmd_tools/panels/tool.py deleted file mode 100644 index 02ab2411..00000000 --- a/mmd_tools/panels/tool.py +++ /dev/null @@ -1,776 +0,0 @@ -# -*- coding: utf-8 -*- - -import bpy -from bpy.types import Panel, Menu, UIList - -from mmd_tools import bpyutils, operators -from mmd_tools.utils import ItemOp -from mmd_tools.bpyutils import SceneOp -import mmd_tools.core.model as mmd_model - - -TRIA_UP_BAR = 'TRIA_UP_BAR' -TRIA_DOWN_BAR = 'TRIA_DOWN_BAR' -if bpy.app.version < (2, 73, 0): - TRIA_UP_BAR = 'TRIA_UP' - TRIA_DOWN_BAR = 'TRIA_DOWN' - -ICON_ADD, ICON_REMOVE = 'ADD', 'REMOVE' -if bpy.app.version < (2, 80, 0): - ICON_ADD, ICON_REMOVE = 'ZOOMIN', 'ZOOMOUT' - - -if bpy.app.version < (2, 80, 0): - def _layout_split(layout, factor, align): - return layout.split(percentage=factor, align=align) -else: - def _layout_split(layout, factor, align): - return layout.split(factor=factor, align=align) - - -class _PanelBase(object): - bl_space_type = 'VIEW_3D' - bl_region_type = 'TOOLS' if bpy.app.version < (2, 80, 0) else 'UI' - bl_category = 'MMD' - - @classmethod - def poll(cls, _context): - return bpyutils.addon_preferences('enable_mmd_model_production_features', True) - -class MMDModelProductionPanel(_PanelBase, Panel): - bl_idname = 'OBJECT_PT_mmd_tools_model_production' - bl_label = 'Model Production' - bl_context = '' - - def draw(self, context): - active_obj = context.active_object - - layout = self.layout - col = layout.column(align=True) - grid = col.grid_flow(row_major=True) - row = grid.row(align=True) - row.operator('mmd_tools.create_mmd_model_root_object', text='Create Model', icon='OUTLINER_OB_ARMATURE') - row.operator('mmd_tools.convert_to_mmd_model', text='Convert Model', icon='ARMATURE_DATA') - - root = mmd_model.Model.findRoot(active_obj) - row = grid.row(align=True) - row.enabled = root is not None - row.operator('mmd_tools.attach_meshes', text='Attach Meshes', icon='OUTLINER_OB_MESH') - - row = grid.row(align=True) - row.operator('mmd_tools.translate_mmd_model', text='Translate', icon='HELP') - row.operator('mmd_tools.global_translation_popup', text='', icon='WINDOW') - - self.draw_edit(context) - - def draw_edit(self, _context): - col = self.layout.column(align=True) - col.label(text='Model Surgery:', icon='MOD_ARMATURE') - grid = col.grid_flow(row_major=True, align=True) - - separate_row = grid.row(align=True) - row = separate_row.row(align=True) - row.operator_context = 'EXEC_DEFAULT' - op = row.operator('mmd_tools.model_separate_by_bones', text='Chop', icon='BONE_DATA') - op.separate_armature = True - op.include_descendant_bones = True - op.boundary_joint_owner = 'DESTINATION' - - row = row.row(align=True) - row.operator_context = 'INVOKE_DEFAULT' - op = row.operator('mmd_tools.model_separate_by_bones', text='', icon='WINDOW') - - row = separate_row.row(align=True) - row.operator_context = 'EXEC_DEFAULT' - op = row.operator('mmd_tools.model_separate_by_bones', text='Peel', icon='MOD_EXPLODE') - op.separate_armature = False - op.include_descendant_bones = False - op.boundary_joint_owner = 'DESTINATION' - - row = grid.row(align=True) - row.operator_context = 'INVOKE_DEFAULT' - row.operator('mmd_tools.model_join_by_bones', text='Join', icon='GROUP_BONE') - -class MMD_ROOT_UL_display_item_frames(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - frame = item - if self.layout_type in {'DEFAULT'}: - row = _layout_split(layout, factor=0.5, align=True) - if frame.is_special: - row.label(text=frame.name, translate=False) - row = row.row(align=True) - row.label(text=frame.name_e, translate=False) - row.label(text='', icon='LOCKED') - else: - row.prop(frame, 'name', text='', emboss=False) - row.prop(frame, 'name_e', text='', emboss=True) - elif self.layout_type in {'COMPACT'}: - pass - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - -class MMD_ROOT_UL_display_items(UIList): - morph_filter: bpy.props.EnumProperty( - name="Morph Filter", - description='Only show items matching this category', - options={'ENUM_FLAG'}, - items = [ - ('SYSTEM', 'Hidden', '', 1), - ('EYEBROW', 'Eye Brow', '', 2), - ('EYE', 'Eye', '', 4), - ('MOUTH', 'Mouth', '', 8), - ('OTHER', 'Other', '', 16), - ], - default={'SYSTEM', 'EYEBROW', 'EYE', 'MOUTH', 'OTHER',}, - ) - mmd_name: bpy.props.EnumProperty( - name='MMD Name', - description='Show JP or EN name of MMD bone', - items = [ - ('name_j', 'JP', '', 1), - ('name_e', 'EN', '', 2), - ], - default='name_e', - ) - - @staticmethod - def draw_bone_special(layout, armature, bone_name, mmd_name=None): - if armature is None: - return - row = layout.row(align=True) - p_bone = armature.pose.bones.get(bone_name, None) - if p_bone: - bone = p_bone.bone - if mmd_name: - row.prop(p_bone.mmd_bone, mmd_name, text='', emboss=True) - ic = 'RESTRICT_VIEW_ON' if bone.hide else 'RESTRICT_VIEW_OFF' - row.prop(bone, 'hide', text='', emboss=p_bone.mmd_bone.is_tip, icon=ic) - row.active = armature.mode != 'EDIT' - else: - row.label() # for alignment only - row.label(icon='ERROR') - - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - if self.layout_type in {'DEFAULT'}: - if item.type == 'BONE': - row = _layout_split(layout, factor=0.5, align=True) - row.prop(item, 'name', text='', emboss=False, icon='BONE_DATA') - self.draw_bone_special(row, mmd_model.Model(item.id_data).armature(), item.name, self.mmd_name) - else: - row = _layout_split(layout, factor=0.6, align=True) - row.prop(item, 'name', text='', emboss=False, icon='SHAPEKEY_DATA') - row = row.row(align=True) - row.prop(item, 'morph_type', text='', emboss=False) - if item.name not in getattr(item.id_data.mmd_root, item.morph_type): - row.label(icon='ERROR') - elif self.layout_type in {'COMPACT'}: - pass - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - - def filter_items(self, context, data, propname): - if len(self.morph_filter) == 5 or data.name != u'表情': - return [], [] - - objects = getattr(data, propname) - flt_flags = [~self.bitflag_filter_item] * len(objects) - flt_neworder = [] - - for i, item in enumerate(objects): - morph = getattr(item.id_data.mmd_root, item.morph_type).get(item.name, None) - if morph and morph.category in self.morph_filter: - flt_flags[i] = self.bitflag_filter_item - - return flt_flags, flt_neworder - - def draw_filter(self, context, layout): - row = layout.row() - row.prop(self, 'morph_filter', expand=True) - row.prop(self, 'mmd_name', expand=True) - - -class MMDDisplayItemFrameMenu(Menu): - bl_idname = 'OBJECT_MT_mmd_tools_display_item_frame_menu' - bl_label = 'Display Item Frame Menu' - - def draw(self, context): - layout = self.layout - layout.operator_enum('mmd_tools.display_item_quick_setup', 'type') - layout.separator() - layout.operator('mmd_tools.display_item_frame_move', icon=TRIA_UP_BAR, text='Move To Top').type = 'TOP' - layout.operator('mmd_tools.display_item_frame_move', icon=TRIA_DOWN_BAR, text='Move To Bottom').type = 'BOTTOM' - -class MMDDisplayItemMenu(Menu): - bl_idname = 'OBJECT_MT_mmd_tools_display_item_menu' - bl_label = 'Display Item Menu' - - def draw(self, context): - layout = self.layout - layout.operator('mmd_tools.display_item_remove', text='Delete All', icon='X').all = True - layout.separator() - layout.operator('mmd_tools.display_item_move', icon=TRIA_UP_BAR, text='Move To Top').type = 'TOP' - layout.operator('mmd_tools.display_item_move', icon=TRIA_DOWN_BAR, text='Move To Bottom').type = 'BOTTOM' - -class MMDDisplayItemsPanel(_PanelBase, Panel): - bl_idname = 'OBJECT_PT_mmd_tools_display_items' - bl_label = 'Display Panel' - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - active_obj = context.active_object - root = mmd_model.Model.findRoot(active_obj) - if root is None: - self.layout.label(text='Select a MMD Model') - return - - mmd_root = root.mmd_root - col = self.layout.column() - row = col.row() - row.template_list( - "MMD_ROOT_UL_display_item_frames", - "", - mmd_root, "display_item_frames", - mmd_root, "active_display_item_frame", - ) - tb = row.column() - tb1 = tb.column(align=True) - tb1.operator('mmd_tools.display_item_frame_add', text='', icon=ICON_ADD) - tb1.operator('mmd_tools.display_item_frame_remove', text='', icon=ICON_REMOVE) - tb1.menu('OBJECT_MT_mmd_tools_display_item_frame_menu', text='', icon='DOWNARROW_HLT') - tb.separator() - tb1 = tb.column(align=True) - tb1.operator('mmd_tools.display_item_frame_move', text='', icon='TRIA_UP').type = 'UP' - tb1.operator('mmd_tools.display_item_frame_move', text='', icon='TRIA_DOWN').type = 'DOWN' - - frame = ItemOp.get_by_index(mmd_root.display_item_frames, mmd_root.active_display_item_frame) - if frame is None: - return - - c = col.column(align=True) - row = c.row() - row.template_list( - "MMD_ROOT_UL_display_items", - "", - frame, "data", - frame, "active_item", - ) - tb = row.column() - tb1 = tb.column(align=True) - tb1.operator('mmd_tools.display_item_add', text='', icon=ICON_ADD) - tb1.operator('mmd_tools.display_item_remove', text='', icon=ICON_REMOVE) - tb1.menu('OBJECT_MT_mmd_tools_display_item_menu', text='', icon='DOWNARROW_HLT') - tb.separator() - tb1 = tb.column(align=True) - tb1.operator('mmd_tools.display_item_move', text='', icon='TRIA_UP').type = 'UP' - tb1.operator('mmd_tools.display_item_move', text='', icon='TRIA_DOWN').type = 'DOWN' - - row = col.row() - r = row.row(align=True) - r.operator('mmd_tools.display_item_find', text='Bone', icon='VIEWZOOM').type = 'BONE' - r.operator('mmd_tools.display_item_find', text='Morph', icon='VIEWZOOM').type = 'MORPH' - row.operator('mmd_tools.display_item_select_current', text='Select') - -from mmd_tools.properties.morph import MaterialMorph - -class MMD_TOOLS_UL_Morphs(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - mmd_root = data - if self.layout_type in {'DEFAULT'}: - row = _layout_split(layout, factor=0.4, align=True) - row.prop(item, 'name', text='', emboss=False, icon='SHAPEKEY_DATA') - row = _layout_split(row, factor=0.6, align=True) - row.prop(item, 'name_e', text='', emboss=True) - row = row.row(align=True) - row.prop(item, 'category', text='', emboss=False) - frame_facial = mmd_root.display_item_frames.get(u'表情') - morph_item = frame_facial.data.get(item.name) if frame_facial else None - if morph_item is None: - row.label(icon='INFO') - elif morph_item.morph_type != mmd_root.active_morph_type: - row.label(icon='SHAPEKEY_DATA') - else: - row.label(icon='BLANK1') - if isinstance(item, MaterialMorph) and any(not d.material for d in item.data): - row.label(icon='TEMP') - elif self.layout_type in {'COMPACT'}: - pass - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - -class MMD_TOOLS_UL_MaterialMorphOffsets(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - if self.layout_type in {'DEFAULT'}: - material = item.material - layout.label(text=material or 'All Materials', translate=False, icon='MATERIAL') - elif self.layout_type in {'COMPACT'}: - pass - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - -class MMD_TOOLS_UL_UVMorphOffsets(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - if self.layout_type in {'DEFAULT'}: - layout.label(text=str(item.index), translate=False, icon='MESH_DATA') - layout.prop(item, 'offset', text='', emboss=False, slider=True) - elif self.layout_type in {'COMPACT'}: - pass - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - -class MMD_TOOLS_UL_BoneMorphOffsets(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - if self.layout_type in {'DEFAULT'}: - layout.prop(item, 'bone', text='', emboss=False, icon='BONE_DATA') - MMD_ROOT_UL_display_items.draw_bone_special(layout, mmd_model.Model(item.id_data).armature(), item.bone) - elif self.layout_type in {'COMPACT'}: - pass - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - -class MMD_TOOLS_UL_GroupMorphOffsets(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - if self.layout_type in {'DEFAULT'}: - row = _layout_split(layout, factor=0.5, align=True) - row.prop(item, 'name', text='', emboss=False, icon='SHAPEKEY_DATA') - row = row.row(align=True) - row.prop(item, 'morph_type', text='', emboss=False) - if item.name in getattr(item.id_data.mmd_root, item.morph_type): - row.prop(item, 'factor', text='', emboss=False, slider=True) - else: - row.label(icon='ERROR') - elif self.layout_type in {'COMPACT'}: - pass - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - -class MMDMorphMenu(Menu): - bl_idname = 'OBJECT_MT_mmd_tools_morph_menu' - bl_label = 'Morph Menu' - - def draw(self, context): - layout = self.layout - layout.operator('mmd_tools.morph_remove', text='Delete All', icon='X').all = True - layout.separator() - layout.operator('mmd_tools.morph_slider_setup', text='Bind morphs to .placeholder', icon='DRIVER').type = 'BIND' - layout.operator('mmd_tools.morph_slider_setup', text='Unbind morphs from .placeholder', icon='UNLINKED').type = 'UNBIND' - layout.separator() - layout.operator('mmd_tools.morph_copy', icon='COPY_ID') - layout.operator('mmd_tools.morph_overwrite_from_active_pose_library', icon='PRESET_NEW') - layout.operator('mmd_tools.clean_duplicated_material_morphs', icon='TRASH') - layout.separator() - layout.operator('mmd_tools.morph_move', icon=TRIA_UP_BAR, text='Move To Top').type = 'TOP' - layout.operator('mmd_tools.morph_move', icon=TRIA_DOWN_BAR, text='Move To Bottom').type = 'BOTTOM' - -class MMDMorphToolsPanel(_PanelBase, Panel): - bl_idname = 'OBJECT_PT_mmd_tools_morph_tools' - bl_label = 'Morph Tools' - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - active_obj = context.active_object - root = mmd_model.Model.findRoot(active_obj) - if root is None: - self.layout.label(text='Select a MMD Model') - return - - rig = mmd_model.Model(root) - mmd_root = root.mmd_root - col = self.layout.column() - row = col.row() - row.prop(mmd_root, 'active_morph_type', expand=True) - morph_type = mmd_root.active_morph_type - - c = col.column(align=True) - row = c.row() - row.template_list( - "MMD_TOOLS_UL_Morphs", "", - mmd_root, morph_type, - mmd_root, "active_morph" - ) - tb = row.column() - tb1 = tb.column(align=True) - tb1.operator('mmd_tools.morph_add', text='', icon=ICON_ADD) - tb1.operator('mmd_tools.morph_remove', text='', icon=ICON_REMOVE) - tb1.menu('OBJECT_MT_mmd_tools_morph_menu', text='', icon='DOWNARROW_HLT') - tb.separator() - tb1 = tb.column(align=True) - tb1.operator('mmd_tools.morph_move', text='', icon='TRIA_UP').type = 'UP' - tb1.operator('mmd_tools.morph_move', text='', icon='TRIA_DOWN').type = 'DOWN' - - morph = ItemOp.get_by_index(getattr(mmd_root, morph_type), mmd_root.active_morph) - if morph: - slider = rig.morph_slider.get(morph.name) - if slider: - col.row().prop(slider, 'value') - - row = col.row(align=True) - row.prop( - mmd_root, 'morph_panel_show_settings', - icon='TRIA_DOWN' if mmd_root.morph_panel_show_settings else 'TRIA_RIGHT', - icon_only=True, - emboss=False, - ) - row.label(text='Morph Settings') - if mmd_root.morph_panel_show_settings: - draw_func = getattr(self, '_draw_%s_data'%morph_type[:-7], None) - if draw_func: - draw_func(context, rig, col, morph) - - def _template_morph_offset_list(self, layout, morph, list_type_name): - row = layout.row() - row.template_list( - list_type_name, '', - morph, 'data', - morph, 'active_data', - ) - tb = row.column() - tb1 = tb.column(align=True) - tb1.operator('mmd_tools.morph_offset_add', text='', icon=ICON_ADD) - tb1.operator('mmd_tools.morph_offset_remove', text='', icon=ICON_REMOVE) - tb.operator('mmd_tools.morph_offset_remove', text='', icon='X').all = True - return ItemOp.get_by_index(morph.data, morph.active_data) - - def _draw_vertex_data(self, context, rig, col, morph): - r = col.row() - col = r.column(align=True) - for i in rig.meshes(): - shape_keys = i.data.shape_keys - if shape_keys is None: - continue - kb = shape_keys.key_blocks.get(morph.name, None) - if kb: - found = row = col.row(align=True) - row.active = not (i.show_only_shape_key or kb.mute) - row.operator('mmd_tools.object_select', text=i.name, icon='OBJECT_DATA').name = i.name - row.prop(kb, 'value', text=kb.name) - if 'found' not in locals(): - col.label(text='Not found', icon='INFO') - else: - r.operator('mmd_tools.morph_offset_remove', text='', icon='X').all = True - - def _draw_material_data(self, context, rig, col, morph): - col.label(text=bpy.app.translations.pgettext_iface('Material Offsets (%d)')%len(morph.data)) - data = self._template_morph_offset_list(col, morph, 'MMD_TOOLS_UL_MaterialMorphOffsets') - if data is None: - return - - c_mat = col.column(align=True) - c_mat.prop_search(data, 'related_mesh', bpy.data, 'meshes') - - related_mesh = bpy.data.meshes.get(data.related_mesh, None) - c_mat.prop_search(data, 'material', related_mesh or bpy.data, 'materials') - - base_mat_name = data.material - if '_temp' in base_mat_name: - col.label(text='This is not a valid base material', icon='ERROR') - return - - work_mat = bpy.data.materials.get(base_mat_name + '_temp', None) - use_work_mat = work_mat and related_mesh and work_mat.name in related_mesh.materials - if not use_work_mat: - c = col.column() - row = c.row(align=True) - if base_mat_name == '': - row.label(text='This offset affects all materials', icon='INFO') - else: - row.operator(operators.morph.CreateWorkMaterial.bl_idname) - row.operator(operators.morph.ClearTempMaterials.bl_idname, text='Clear') - - row = c.row() - row.prop(data, 'offset_type', expand=True) - r1 = row.row(align=True) - r1.operator(operators.morph.InitMaterialOffset.bl_idname, text='', icon='TRIA_LEFT').target_value = 0 - r1.operator(operators.morph.InitMaterialOffset.bl_idname, text='', icon='TRIA_RIGHT').target_value = 1 - row = c.row() - row.column(align=True).prop(data, 'diffuse_color', expand=True, slider=True) - c1 = row.column(align=True) - c1.prop(data, 'specular_color', expand=True, slider=True) - c1.prop(data, 'shininess', slider=True) - row.column(align=True).prop(data, 'ambient_color', expand=True, slider=True) - row = c.row() - row.column(align=True).prop(data, 'edge_color', expand=True, slider=True) - row = c.row() - row.prop(data, 'edge_weight', slider=True) - row = c.row() - row.column(align=True).prop(data, 'texture_factor', expand=True, slider=True) - row.column(align=True).prop(data, 'sphere_texture_factor', expand=True, slider=True) - row.column(align=True).prop(data, 'toon_texture_factor', expand=True, slider=True) - else: - c_mat.enabled = False - c = col.column() - row = c.row(align=True) - row.operator(operators.morph.ApplyMaterialOffset.bl_idname, text='Apply') - row.operator(operators.morph.ClearTempMaterials.bl_idname, text='Clear') - - row = c.row() - row.prop(data, 'offset_type') - row = c.row() - row.prop(work_mat.mmd_material, 'diffuse_color') - row.prop(work_mat.mmd_material, 'alpha', slider=True) - row = c.row() - row.prop(work_mat.mmd_material, 'specular_color') - row.prop(work_mat.mmd_material, 'shininess', slider=True) - row = c.row() - row.prop(work_mat.mmd_material, 'ambient_color') - row.label() # for alignment only - row = c.row() - row.prop(work_mat.mmd_material, 'edge_color') - row.prop(work_mat.mmd_material, 'edge_weight', slider=True) - row = c.row() - row.column(align=True).prop(data, 'texture_factor', expand=True, slider=True) - row.column(align=True).prop(data, 'sphere_texture_factor', expand=True, slider=True) - row.column(align=True).prop(data, 'toon_texture_factor', expand=True, slider=True) - - def _draw_bone_data(self, context, rig, col, morph): - armature = rig.armature() - if armature is None: - col.label(text='Armature not found', icon='ERROR') - return - - row = col.row(align=True) - row.operator(operators.morph.ViewBoneMorph.bl_idname, text='View') - row.operator(operators.morph.ApplyBoneMorph.bl_idname, text='Apply') - row.operator(operators.morph.ClearBoneMorphView.bl_idname, text='Clear') - - col.label(text=bpy.app.translations.pgettext_iface('Bone Offsets (%d)')%len(morph.data)) - data = self._template_morph_offset_list(col, morph, 'MMD_TOOLS_UL_BoneMorphOffsets') - if data is None: - return - - row = col.row(align=True) - row.prop_search(data, 'bone', armature.pose, 'bones') - if data.bone: - row = col.row(align=True) - row.operator(operators.morph.SelectRelatedBone.bl_idname, text='Select') - row.operator(operators.morph.EditBoneOffset.bl_idname, text='Edit') - row.operator(operators.morph.ApplyBoneOffset.bl_idname, text='Update') - - row = col.row() - row.column(align=True).prop(data, 'location') - row.column(align=True).prop(data, 'rotation') - - def _draw_uv_data(self, context, rig, col, morph): - c = col.column(align=True) - row = c.row(align=True) - row.operator(operators.morph.ViewUVMorph.bl_idname, text='View') - row.operator(operators.morph.ClearUVMorphView.bl_idname, text='Clear') - row = c.row(align=True) - row.operator(operators.morph.EditUVMorph.bl_idname, text='Edit') - row.operator(operators.morph.ApplyUVMorph.bl_idname, text='Apply') - - c = col.column() - if len(morph.data): - row = c.row() - row.prop(morph, 'data_type', expand=True) - row = c.row() - if morph.data_type == 'VERTEX_GROUP': - row.prop(morph, 'vertex_group_scale', text='Scale') - else: - row.label(text=bpy.app.translations.pgettext_iface('UV Offsets (%d)')%len(morph.data)) - #self._template_morph_offset_list(c, morph, 'MMD_TOOLS_UL_UVMorphOffsets') - row.prop(morph, 'uv_index') - row.operator('mmd_tools.morph_offset_remove', text='', icon='X').all = True - - def _draw_group_data(self, context, rig, col, morph): - col.label(text=bpy.app.translations.pgettext_iface('Group Offsets (%d)')%len(morph.data)) - item = self._template_morph_offset_list(col, morph, 'MMD_TOOLS_UL_GroupMorphOffsets') - if item is None: - return - - c = col.column(align=True) - row = _layout_split(c, factor=0.67, align=True) - row.prop_search(item, 'name', morph.id_data.mmd_root, item.morph_type, icon='SHAPEKEY_DATA', text='') - row.prop(item, 'morph_type', text='') - - -class UL_ObjectsMixIn(object): - model_filter: bpy.props.EnumProperty( - name="Model Filter", - description='Show items of active model or all models', - items = [ - ('ACTIVE', 'Active Model', '', 0), - ('ALL', 'All Models', '', 1), - ], - default='ACTIVE', - ) - visible_only: bpy.props.BoolProperty( - name='Visible Only', - description='Only show visible items', - default=False, - ) - - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - if self.layout_type in {'DEFAULT', 'COMPACT'}: - row = _layout_split(layout, factor=0.5, align=True) - item_prop = getattr(item, self.prop_name) - row.prop(item_prop, 'name_j', text='', emboss=False, icon=self.icon) - row = row.row(align=True) - row.prop(item_prop, 'name_e', text='', emboss=True) - self.draw_item_special(context, row, item) - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text='', icon=self.icon) - - def draw_filter(self, context, layout): - row = layout.row(align=True) - row.prop(self, 'model_filter', expand=True) - row.prop(self, 'visible_only', text='', toggle=True, icon='RESTRICT_VIEW_OFF') - - def filter_items(self, context, data, propname): - objects = getattr(data, propname) - flt_flags = [~self.bitflag_filter_item] * len(objects) - flt_neworder = list(range(len(objects))) - - if self.model_filter == 'ACTIVE': - active_root = mmd_model.Model.findRoot(context.active_object) - for i, obj in enumerate(objects): - if obj.mmd_type == self.mmd_type and mmd_model.Model.findRoot(obj) == active_root: - flt_flags[i] = self.bitflag_filter_item - else: - for i, obj in enumerate(objects): - if obj.mmd_type == self.mmd_type: - flt_flags[i] = self.bitflag_filter_item - - if self.visible_only: - for i, obj in enumerate(objects): - if obj.hide and flt_flags[i] == self.bitflag_filter_item: - flt_flags[i] = ~self.bitflag_filter_item - - indices = (i for i, x in enumerate(flt_flags) if x == self.bitflag_filter_item) - for i_new, i_orig in enumerate(sorted(indices, key=lambda k: objects[k].name)): - flt_neworder[i_orig] = i_new - return flt_flags, flt_neworder - -class MMD_TOOLS_UL_rigidbodies(UIList, UL_ObjectsMixIn): - mmd_type = 'RIGID_BODY' - icon = 'MESH_ICOSPHERE' - prop_name = 'mmd_rigid' - - def draw_item_special(self, context, layout, item): - rb = item.rigid_body - if rb is None: - layout.label(icon='ERROR') - elif not item.mmd_rigid.bone: - layout.label(icon='BONE_DATA') - -class MMDRigidbodySelectMenu(Menu): - bl_idname = 'OBJECT_MT_mmd_tools_rigidbody_select_menu' - bl_label = 'Rigidbody Select Menu' - - def draw(self, context): - layout = self.layout - layout.operator_context = 'INVOKE_DEFAULT' - layout.operator('mmd_tools.rigid_body_select', text='Select Similar...') - layout.separator() - layout.operator_context = 'EXEC_DEFAULT' - layout.operator_enum('mmd_tools.rigid_body_select', 'properties') - -class MMDRigidbodyMenu(Menu): - bl_idname = 'OBJECT_MT_mmd_tools_rigidbody_menu' - bl_label = 'Rigidbody Menu' - - def draw(self, context): - layout = self.layout - layout.enabled = context.active_object.mmd_type == 'RIGID_BODY' - layout.menu('OBJECT_MT_mmd_tools_rigidbody_select_menu', text='Select Similar') - layout.separator() - layout.operator('mmd_tools.object_move', icon=TRIA_UP_BAR, text='Move To Top').type = 'TOP' - layout.operator('mmd_tools.object_move', icon=TRIA_DOWN_BAR, text='Move To Bottom').type = 'BOTTOM' - -class MMDRigidbodySelectorPanel(_PanelBase, Panel): - bl_idname = 'OBJECT_PT_mmd_tools_rigidbody_list' - bl_label = 'Rigid Bodies' - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - active_obj = context.active_object - root = mmd_model.Model.findRoot(active_obj) - if root is None: - self.layout.label(text='Select a MMD Model') - return - - col = self.layout.column() - c = col.column(align=True) - row = c.row() - row.template_list( - "MMD_TOOLS_UL_rigidbodies", - "", - SceneOp(context).id_scene, 'objects', - root.mmd_root, 'active_rigidbody_index', - ) - tb = row.column() - tb1 = tb.column(align=True) - tb1.operator('mmd_tools.rigid_body_add', text='', icon=ICON_ADD) - tb1.operator('mmd_tools.rigid_body_remove', text='', icon=ICON_REMOVE) - tb1.menu('OBJECT_MT_mmd_tools_rigidbody_menu', text='', icon='DOWNARROW_HLT') - tb.separator() - tb1 = tb.column(align=True) - tb1.enabled = active_obj.mmd_type == 'RIGID_BODY' - tb1.operator('mmd_tools.object_move', text='', icon='TRIA_UP').type = 'UP' - tb1.operator('mmd_tools.object_move', text='', icon='TRIA_DOWN').type = 'DOWN' - - -class MMD_TOOLS_UL_joints(UIList, UL_ObjectsMixIn): - mmd_type = 'JOINT' - icon = 'CONSTRAINT' - prop_name = 'mmd_joint' - - def draw_item_special(self, context, layout, item): - rbc = item.rigid_body_constraint - if rbc is None: - layout.label(icon='ERROR') - elif rbc.object1 is None or rbc.object2 is None: - layout.label(icon='OBJECT_DATA') - elif rbc.object1 == rbc.object2: - layout.label(icon='MESH_CUBE') - -class MMDJointMenu(Menu): - bl_idname = 'OBJECT_MT_mmd_tools_joint_menu' - bl_label = 'Joint Menu' - - def draw(self, context): - layout = self.layout - layout.enabled = context.active_object.mmd_type == 'JOINT' - layout.operator('mmd_tools.object_move', icon=TRIA_UP_BAR, text='Move To Top').type = 'TOP' - layout.operator('mmd_tools.object_move', icon=TRIA_DOWN_BAR, text='Move To Bottom').type = 'BOTTOM' - -class MMDJointSelectorPanel(_PanelBase, Panel): - bl_idname = 'OBJECT_PT_mmd_tools_joint_list' - bl_label = 'Joints' - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - active_obj = context.active_object - root = mmd_model.Model.findRoot(active_obj) - if root is None: - self.layout.label(text='Select a MMD Model') - return - - col = self.layout.column() - c = col.column(align=True) - - row = c.row() - row.template_list( - "MMD_TOOLS_UL_joints", - "", - SceneOp(context).id_scene, 'objects', - root.mmd_root, 'active_joint_index', - ) - tb = row.column() - tb1 = tb.column(align=True) - tb1.operator('mmd_tools.joint_add', text='', icon=ICON_ADD) - tb1.operator('mmd_tools.joint_remove', text='', icon=ICON_REMOVE) - tb1.menu('OBJECT_MT_mmd_tools_joint_menu', text='', icon='DOWNARROW_HLT') - tb.separator() - tb1 = tb.column(align=True) - tb1.enabled = active_obj.mmd_type == 'JOINT' - tb1.operator('mmd_tools.object_move', text='', icon='TRIA_UP').type = 'UP' - tb1.operator('mmd_tools.object_move', text='', icon='TRIA_DOWN').type = 'DOWN' - diff --git a/mmd_tools/panels/util_tools.py b/mmd_tools/panels/util_tools.py deleted file mode 100644 index 1ff1af39..00000000 --- a/mmd_tools/panels/util_tools.py +++ /dev/null @@ -1,348 +0,0 @@ -# -*- coding: utf-8 -*- - -import bpy -from bpy.types import Panel, UIList - -from mmd_tools.bpyutils import SceneOp -from mmd_tools.core.model import FnModel, Model -from mmd_tools.panels.tool import TRIA_UP_BAR, TRIA_DOWN_BAR -from mmd_tools.panels.tool import _layout_split -from mmd_tools.panels.tool import _PanelBase - - -ICON_APPEND_MOVE, ICON_APPEND_ROT, ICON_APPEND_MOVE_ROT = 'IPO_LINEAR', 'IPO_EXPO', 'IPO_QUAD' -if bpy.app.version < (2, 71, 0): - ICON_APPEND_MOVE, ICON_APPEND_ROT, ICON_APPEND_MOVE_ROT = 'NDOF_TRANS', 'NDOF_TURN', 'FORCE_MAGNETIC' - - -class MMD_TOOLS_UL_Materials(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - if self.layout_type in {'DEFAULT'}: - if item: - row = layout.row(align=True) - item_prop = getattr(item, 'mmd_material') - row.prop(item_prop, 'name_j', text='', emboss=False, icon='MATERIAL') - row.prop(item_prop, 'name_e', text='', emboss=True) - else: - layout.label(text='UNSET', translate=False, icon='ERROR') - elif self.layout_type in {'COMPACT'}: - pass - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - - def draw_filter(self, context, layout): - layout.label(text="Use the arrows to sort", icon='INFO') - -class MMDMaterialSorter(_PanelBase, Panel): - bl_idname = 'OBJECT_PT_mmd_tools_material_sorter' - bl_label = 'Material Sorter' - bl_context = '' - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - layout = self.layout - active_obj = context.active_object - if (active_obj is None or active_obj.type != 'MESH' or - active_obj.mmd_type != 'NONE'): - layout.label(text='Select a mesh object') - return - - col = layout.column(align=True) - row = col.row() - row.template_list("MMD_TOOLS_UL_Materials", "", - active_obj.data, "materials", - active_obj, "active_material_index") - tb = row.column() - tb1 = tb.column(align=True) - tb1.operator('mmd_tools.move_material_up', text='', icon='TRIA_UP') - tb1.operator('mmd_tools.move_material_down', text='', icon='TRIA_DOWN') - -class MMD_TOOLS_UL_ModelMeshes(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - if self.layout_type in {'DEFAULT'}: - layout.label(text=item.name, translate=False, icon='OBJECT_DATA') - elif self.layout_type in {'COMPACT'}: - pass - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - - def draw_filter(self, context, layout): - layout.label(text="Use the arrows to sort", icon='INFO') - - def filter_items(self, context, data, propname): - # We will use the filtering to sort the mesh objects to match the rig order - objects = getattr(data, propname) - flt_flags = [~self.bitflag_filter_item] * len(objects) - flt_neworder = list(range(len(objects))) - - armature = Model(Model.findRoot(context.active_object)).armature() - __is_child_of_armature = lambda x: x.parent and (x.parent == armature or __is_child_of_armature(x.parent)) - - name_dict = {} - for i, obj in enumerate(objects): - if obj.type == 'MESH' and obj.mmd_type == 'NONE' and __is_child_of_armature(obj): - flt_flags[i] = self.bitflag_filter_item - name_dict[obj.name] = i - - for new_index, name in enumerate(sorted(name_dict.keys())): - i = name_dict[name] - flt_neworder[i] = new_index - - return flt_flags, flt_neworder - -class MMDMeshSorter(_PanelBase, Panel): - bl_idname = 'OBJECT_PT_mmd_tools_meshes_sorter' - bl_label = 'Meshes Sorter' - bl_context = '' - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - layout = self.layout - active_obj = context.active_object - root = Model.findRoot(active_obj) - if root is None: - layout.label(text='Select a MMD Model') - return - - col = layout.column(align=True) - row = col.row() - row.template_list("MMD_TOOLS_UL_ModelMeshes", "", - SceneOp(context).id_scene, 'objects', - root.mmd_root, "active_mesh_index") - tb = row.column() - tb1 = tb.column(align=True) - tb1.enabled = active_obj.type == 'MESH' and active_obj.mmd_type == 'NONE' - tb1.operator('mmd_tools.object_move', text='', icon=TRIA_UP_BAR).type = 'TOP' - tb1.operator('mmd_tools.object_move', text='', icon='TRIA_UP').type = 'UP' - tb1.operator('mmd_tools.object_move', text='', icon='TRIA_DOWN').type = 'DOWN' - tb1.operator('mmd_tools.object_move', text='', icon=TRIA_DOWN_BAR).type = 'BOTTOM' - -class _DummyVertexGroup: - index = None - def __init__(self, index): - self.index = index - -class MMD_TOOLS_UL_ModelBones(UIList): - _IK_MAP = {} - _IK_BONES = {} - _DUMMY_VERTEX_GROUPS = {} - - @classmethod - def __wrap_pose_bones(cls, pose_bones): - for i, b in enumerate(pose_bones): - cls._DUMMY_VERTEX_GROUPS[b.name] = _DummyVertexGroup(i) - yield b - - @classmethod - def update_bone_tables(cls, armature, bone_order_object): - cls._IK_MAP.clear() - cls._IK_BONES.clear() - cls._DUMMY_VERTEX_GROUPS.clear() - - ik_target_override = {} - ik_target_custom = {} - ik_target_fin = {} - pose_bones = armature.pose.bones - bone_count = len(pose_bones) - pose_bone_list = pose_bones if bone_order_object else cls.__wrap_pose_bones(pose_bones) - - for b in pose_bone_list: - if b.is_mmd_shadow_bone: - bone_count -= 1 - continue - for c in b.constraints: - if c.type == 'IK' and c.subtarget in pose_bones and c.subtarget not in cls._IK_BONES: - if not c.use_tail: - cls._IK_MAP.setdefault(hash(b), []).append(c.subtarget) - cls._IK_BONES[c.subtarget] = ik_target_fin[c.subtarget] = hash(b) - bone_chain = b.parent_recursive - else: - cls._IK_BONES[c.subtarget] = b.name - bone_chain = [b] + b.parent_recursive - for l in bone_chain[:c.chain_count]: - cls._IK_MAP.setdefault(hash(l), []).append(c.subtarget) - if 'mmd_ik_target_custom' == c.name: - ik_target_custom[getattr(c, 'subtarget', '')] = hash(b) - elif 'mmd_ik_target_override' == c.name and b.parent: - if b.parent.name == getattr(c, 'subtarget', ''): - for c in b.parent.constraints: - if c.type == 'IK' and c.subtarget in pose_bones and c.subtarget not in ik_target_override and c.subtarget not in ik_target_custom: - ik_target_override[c.subtarget] = hash(b) - - for k, v in ik_target_custom.items(): - if k not in ik_target_fin and k in cls._IK_BONES: - cls._IK_BONES[k] = v - cls._IK_MAP.setdefault(v, []).append(k) - if k in ik_target_override: - del ik_target_override[k] - - for k, v in ik_target_override.items(): - if k not in ik_target_fin and k in cls._IK_BONES: - cls._IK_BONES[k] = v - cls._IK_MAP.setdefault(v, []).append(k) - - for k, v in tuple(cls._IK_BONES.items()): - if isinstance(v, str): - b = cls.__get_ik_target_bone(pose_bones[v]) - if b: - cls._IK_BONES[k] = hash(b) - cls._IK_MAP.setdefault(hash(b), []).append(k) - else: - del cls._IK_BONES[k] - return bone_count - - @staticmethod - def __get_ik_target_bone(target_bone): - r = None - min_length = None - for c in (c for c in target_bone.children if not c.is_mmd_shadow_bone): - if c.bone.use_connect: - return c - length = (c.head - target_bone.tail).length - if min_length is None or length < min_length: - min_length = length - r = c - return r - - @classmethod - def _draw_bone_item(cls, layout, bone_name, pose_bones, vertex_groups, index): - bone = pose_bones.get(bone_name, None) - if not bone or bone.is_mmd_shadow_bone: - layout.active = False - layout.label(text=bone_name, translate=False, icon='GROUP_BONE' if bone else 'MESH_DATA') - r = layout.row() - r.alignment = 'RIGHT' - r.label(text=str(index)) - else: - row = _layout_split(layout, factor=0.45, align=False) - r0 = row.row() - r0.label(text=bone_name, translate=False, icon='POSE_HLT' if bone_name in cls._IK_BONES else 'BONE_DATA') - r = r0.row() - r.alignment = 'RIGHT' - r.label(text=str(index)) - - row_sub = _layout_split(row, factor=0.67, align=False) - - mmd_bone = bone.mmd_bone - count = len(pose_bones) - bone_transform_rank = index + mmd_bone.transform_order*count - - r = row_sub.row() - bone_parent = bone.parent - if bone_parent: - bone_parent = bone_parent.name - idx = vertex_groups.get(bone_parent, _DummyVertexGroup).index - if idx is None or bone_transform_rank < (idx + pose_bones[bone_parent].mmd_bone.transform_order*count): - r.label(text=str(idx), icon='ERROR') - else: - r.label(text=str(idx), icon='INFO' if index < idx else 'FILE_PARENT') - else: - r.label() - - r = r.row() - if mmd_bone.has_additional_rotation: - append_bone = mmd_bone.additional_transform_bone - idx = vertex_groups.get(append_bone, _DummyVertexGroup).index - if idx is None or bone_transform_rank < (idx + pose_bones[append_bone].mmd_bone.transform_order*count): - if append_bone: - r.label(text=str(idx), icon='ERROR') - else: - r.label(text=str(idx), icon=ICON_APPEND_MOVE_ROT if mmd_bone.has_additional_location else ICON_APPEND_ROT) - elif mmd_bone.has_additional_location: - append_bone = mmd_bone.additional_transform_bone - idx = vertex_groups.get(append_bone, _DummyVertexGroup).index - if idx is None or bone_transform_rank < (idx + pose_bones[append_bone].mmd_bone.transform_order*count): - if append_bone: - r.label(text=str(idx), icon='ERROR') - else: - r.label(text=str(idx), icon=ICON_APPEND_MOVE) - - for idx, b in sorted(((vertex_groups.get(b, _DummyVertexGroup).index, b) for b in cls._IK_MAP.get(hash(bone), ())), key=lambda i: i[0] or 0): - ik_bone = pose_bones[b] - is_ik_chain = (hash(bone) != cls._IK_BONES.get(b)) - if idx is None or (is_ik_chain and bone_transform_rank > (idx + ik_bone.mmd_bone.transform_order*count)): - r.prop(ik_bone, 'mmd_ik_toggle', text=str(idx), toggle=True, icon='ERROR') - elif b not in cls._IK_BONES: - r.prop(ik_bone, 'mmd_ik_toggle', text=str(idx), toggle=True, icon='QUESTION') - else: - r.prop(ik_bone, 'mmd_ik_toggle', text=str(idx), toggle=True, icon='LINKED' if is_ik_chain else 'HOOK') - - row = row_sub.row(align=True) - if mmd_bone.transform_after_dynamics: - row.prop(mmd_bone, 'transform_after_dynamics', text='', toggle=True, icon='PHYSICS') - else: - row.prop(mmd_bone, 'transform_after_dynamics', text='', toggle=True) - row.prop(mmd_bone, 'transform_order', text='', slider=bool(mmd_bone.transform_order)) - - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): - if self.layout_type in {'DEFAULT'}: - if self._DUMMY_VERTEX_GROUPS: - self._draw_bone_item(layout, item.name, data.bones, self._DUMMY_VERTEX_GROUPS, index) - else: - self._draw_bone_item(layout, item.name, data.parent.pose.bones, data.vertex_groups, index) - elif self.layout_type in {'COMPACT'}: - pass - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - -class MMDBoneOrderMenu(bpy.types.Menu): - bl_idname = 'OBJECT_MT_mmd_tools_bone_order_menu' - bl_label = 'Bone Order Menu' - - def draw(self, _context): - layout = self.layout - layout.operator('object.vertex_group_sort', text='Sort by Bone Hierarchy', icon='BONE_DATA').sort_type='BONE_HIERARCHY' - layout.separator() - layout.operator('mmd_tools.add_missing_vertex_groups_from_bones', icon='PRESET_NEW') - -class MMDBoneOrder(_PanelBase, Panel): - bl_idname = 'OBJECT_PT_mmd_tools_bone_order' - bl_label = 'Bone Order' - bl_context = '' - bl_options = {'DEFAULT_CLOSED'} - - def draw(self, context): - layout = self.layout - active_obj = context.active_object - root = FnModel.find_root(active_obj) - if root is None: - layout.label(text='Select a MMD Model') - return - - armature = FnModel.find_armature(root) - if armature is None: - layout.label(text='The armature object of active MMD model can\'t be found', icon='ERROR') - return - - bone_order_mesh_object = FnModel.find_bone_order_mesh_object(root) - bone_count = MMD_TOOLS_UL_ModelBones.update_bone_tables(armature, bone_order_mesh_object) - - col = layout.column(align=True) - row = col.row() - if bone_order_mesh_object is None: - row.template_list("MMD_TOOLS_UL_ModelBones", "", - armature.pose, 'bones', - root.vertex_groups, 'active_index') - col.operator('mmd_tools.object_select', text='(%d) %s'%(bone_count, armature.name), icon='OUTLINER_OB_ARMATURE', emboss=False).name = armature.name - col.label(text='No mesh object with "mmd_bone_order_override" modifier', icon='ERROR') - else: - row.template_list("MMD_TOOLS_UL_ModelBones", "", - bone_order_mesh_object, 'vertex_groups', - bone_order_mesh_object.vertex_groups, 'active_index') - - tb = row.column() - tb.enabled = bone_order_mesh_object == active_obj - tb1 = tb.column(align=True) - tb1.menu('OBJECT_MT_mmd_tools_bone_order_menu', text='', icon='DOWNARROW_HLT') - tb.separator() - tb1 = tb.column(align=True) - tb1.operator('object.vertex_group_move', text='', icon='TRIA_UP').direction = 'UP' - tb1.operator('object.vertex_group_move', text='', icon='TRIA_DOWN').direction = 'DOWN' - - row = col.row() - row.operator('mmd_tools.object_select', text='(%d) %s'%(bone_count, armature.name), icon='OUTLINER_OB_ARMATURE', emboss=False).name = armature.name - row.label(icon='BACK') - row.operator('mmd_tools.object_select', text=bone_order_mesh_object.name, icon='OBJECT_DATA', emboss=False).name = bone_order_mesh_object.name diff --git a/mmd_tools/panels/view_header.py b/mmd_tools/panels/view_header.py deleted file mode 100644 index 686cf5b0..00000000 --- a/mmd_tools/panels/view_header.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# SUPPORT_UNTIL: 3.3 LTS - -from bpy.types import Header - - -class MMDViewHeader: - bl_idname = 'MMD_TOOLS_HT_view_header' - bl_space_type = 'VIEW_3D' - - @classmethod - def poll(cls, context): - return (context.active_object and - context.active_object.type == 'ARMATURE' and - context.active_object.mode == 'POSE') - - def draw(self, context): - if self.poll(context): - self.layout.operator('mmd_tools.flip_pose', text='', icon='ARROW_LEFTRIGHT') diff --git a/mmd_tools/panels/view_prop.py b/mmd_tools/panels/view_prop.py deleted file mode 100644 index c2bebafe..00000000 --- a/mmd_tools/panels/view_prop.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -# SUPPORT_UNTIL: 3.3 LTS - -from bpy.types import Panel - -from mmd_tools import bpyutils -from mmd_tools.core.model import Model, FnModel -from mmd_tools.core.sdef import FnSDEF - -class _PanelBase(object): - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - - @classmethod - def poll(cls, _context): - return bpyutils.addon_preferences('enable_mmd_model_production_features', True) - -class MMDModelObjectDisplayPanel(_PanelBase): - bl_idname = 'OBJECT_PT_mmd_tools_root_object_display' - bl_label = 'MMD Display' - - @classmethod - def poll(cls, context): - return super().poll(cls) and Model.findRoot(context.active_object) - - def draw(self, context): - layout = self.layout - obj = context.active_object - - root = Model.findRoot(obj) - - row = layout.row(align=True) - c = row.column(align=True) - c.prop(root.mmd_root, 'show_meshes', text='Mesh') - c.prop(root.mmd_root, 'show_armature', text='Armature') - c.prop(root.mmd_root, 'show_rigid_bodies', text='Rigid Body') - c.prop(root.mmd_root, 'show_joints', text='Joint') - c = row.column(align=True) - c.prop(root.mmd_root, 'show_temporary_objects', text='Temporary Object') - c.label() # for alignment only - c.prop(root.mmd_root, 'show_names_of_rigid_bodies', text='Rigid Body Name') - c.prop(root.mmd_root, 'show_names_of_joints', text='Joint Name') - - row = layout.row(align=True) - #row.active = context.scene.render.engine in {'BLENDER_RENDER', 'BLENDER_GAME'} - row.prop(root.mmd_root, 'use_toon_texture', text='Toon Texture') - row.prop(root.mmd_root, 'use_sphere_texture', text='Sphere Texture') - - row = layout.row(align=True) - row.prop(root.mmd_root, 'use_sdef', text='SDEF') - - layout.prop(root.mmd_root, 'use_property_driver', text='Property Drivers', icon='DRIVER') - - self.__draw_IK_toggle(FnModel.find_armature(root) or root) - - def __draw_IK_toggle(self, armature): - bones = getattr(armature.pose, 'bones', ()) - ik_map = {bones[c.subtarget]:(b.bone, c.chain_count, not c.is_valid) for b in bones for c in b.constraints if c.type == 'IK' and c.subtarget in bones} - if ik_map: - base = sum(b.bone.length for b in ik_map.keys())/len(ik_map)*0.8 - groups = {} - for ik, (b, cnt, err) in ik_map.items(): - if any(all(x) for x in zip(ik.bone.layers, armature.data.layers)): - px, py, pz = -ik.bone.head_local/base - bx, by, bz = -b.head_local/base*0.15 - groups.setdefault((int(pz), int(bz), int(px**2), -cnt), set()).add(((px, -py, bx), ik)) # (px, pz, -py, bx, bz, -by) - layout = self.layout.box().column() - for _, group in sorted(groups.items()): - row = layout.row() - for _, ik in sorted(group, key=lambda x: x[0]): - ic = 'ERROR' if ik_map[ik][-1] else 'NONE' - row.prop(ik, 'mmd_ik_toggle', text=ik.name, toggle=True, icon=ic) - -class MMDViewPanel(_PanelBase): - bl_idname = 'OBJECT_PT_mmd_tools_view' - bl_label = 'MMD Shading' - - def draw(self, context): - layout = self.layout - - c = layout.column(align=True) - r = c.row(align=True) - r.operator('mmd_tools.set_glsl_shading', text='GLSL') - r.operator('mmd_tools.set_shadeless_glsl_shading', text='Shadeless') - r = c.row(align=True) - r.operator('mmd_tools.reset_shading', text='Reset') - -class MMDSDEFPanel(_PanelBase): - bl_idname = 'OBJECT_PT_mmd_tools_sdef' - bl_label = 'MMD SDEF Driver' - - def draw(self, context): - c = self.layout.column(align=True) - c.operator('mmd_tools.sdef_bind', text='Bind') - c.operator('mmd_tools.sdef_unbind', text='Unbind') - row = c.row() - row.label(text='Cache Info: %d data'%(len(FnSDEF.g_verts)), icon='INFO') - row.operator('mmd_tools.sdef_cache_reset', text='', icon='X') diff --git a/mmd_tools/preferences.py b/mmd_tools/preferences.py index 418b191d..10b0fdec 100644 --- a/mmd_tools/preferences.py +++ b/mmd_tools/preferences.py @@ -1,19 +1,11 @@ # -*- coding: utf-8 -*- +# Copyright 2022 MMD Tools authors +# This file is part of MMD Tools. import os import bpy -from mmd_tools import operators - - -def _get_update_candidate_branches(_, __): - updater = operators.addon_updater.AddonUpdaterManager.get_instance() - if not updater.candidate_checked(): - return [] - - return [(name, name, "") for name in updater.get_candidate_branch_names()] - class MMDToolsAddonPreferences(bpy.types.AddonPreferences): bl_idname = __package__ @@ -25,96 +17,24 @@ class MMDToolsAddonPreferences(bpy.types.AddonPreferences): shared_toon_folder: bpy.props.StringProperty( name="Shared Toon Texture Folder", description=('Directory path to toon textures. This is normally the "Data" directory within of your MikuMikuDance directory'), - subtype='DIR_PATH', - default=os.path.join(os.path.dirname(__file__), 'externals', 'MikuMikuDance'), + subtype="DIR_PATH", + default=os.path.join(os.path.dirname(__file__), "externals", "MikuMikuDance"), ) base_texture_folder: bpy.props.StringProperty( - name='Base Texture Folder', - description='Path for textures shared between models', - subtype='DIR_PATH', + name="Base Texture Folder", + description="Path for textures shared between models", + subtype="DIR_PATH", ) dictionary_folder: bpy.props.StringProperty( - name='Dictionary Folder', - description='Path for searching csv dictionaries', - subtype='DIR_PATH', + name="Dictionary Folder", + description="Path for searching csv dictionaries", + subtype="DIR_PATH", default=os.path.dirname(__file__), ) - # for add-on updater - updater_branch_to_update: bpy.props.EnumProperty( - name='Branch', - description='Target branch to update add-on', - items=_get_update_candidate_branches - ) - def draw(self, _context): - layout: bpy.types.UILayout = self.layout # pylint: disable=no-member + layout: bpy.types.UILayout = self.layout # pylint: disable=no-member layout.prop(self, "enable_mmd_model_production_features") layout.prop(self, "shared_toon_folder") layout.prop(self, "base_texture_folder") layout.prop(self, "dictionary_folder") - - # add-on updater - update_col = layout.box().column(align=False) - update_col.label(text='Add-on update', icon='RECOVER_LAST') - updater = operators.addon_updater.AddonUpdaterManager.get_instance() - - if updater.updated(): - col = update_col.column() - col.scale_y = 2 - col.alert = True - col.operator( - "wm.quit_blender", - text="Restart Blender to complete update", - icon="ERROR" - ) - return - - if not updater.candidate_checked(): - col = update_col.column() - col.scale_y = 2 - col.operator( - operators.addon_updater.CheckAddonUpdate.bl_idname, - text="Check mmd_tools add-on update", - icon='FILE_REFRESH' - ) - else: - row = update_col.row(align=True) - row.scale_y = 2 - col = row.column() - col.operator( - operators.addon_updater.CheckAddonUpdate.bl_idname, - text="Check mmd_tools add-on update", - icon='FILE_REFRESH' - ) - col = row.column() - if updater.update_ready(): - col.enabled = True - col.operator( - operators.addon_updater.UpdateAddon.bl_idname, - text=bpy.app.translations.pgettext_iface("Update to the latest release version ({})").format(updater.latest_version()), - icon='TRIA_DOWN_BAR' - ).branch_name = updater.latest_version() - else: - col.enabled = False - col.operator( - operators.addon_updater.UpdateAddon.bl_idname, - text="No updates are available" - ) - - update_col.separator() - update_col.label(text="(Danger) Manual Update:") - row = update_col.row(align=True) - row.prop(self, "updater_branch_to_update", text="Target") - row.operator( - operators.addon_updater.UpdateAddon.bl_idname, text="Update", - icon='TRIA_DOWN_BAR' - ).branch_name = self.updater_branch_to_update - - update_col.separator() - if updater.has_error(): - box = update_col.box() - box.label(text=updater.error(), icon='CANCEL') - elif updater.has_info(): - box = update_col.box() - box.label(text=updater.info(), icon='ERROR') diff --git a/mmd_tools/properties/__init__.py b/mmd_tools/properties/__init__.py index a7f91891..e93a1c72 100644 --- a/mmd_tools/properties/__init__.py +++ b/mmd_tools/properties/__init__.py @@ -1,128 +1,31 @@ # -*- coding: utf-8 -*- - -import logging +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import bpy -from mmd_tools.properties.bone import MMDBone, _mmd_ik_toggle_update -from mmd_tools.properties.camera import MMDCamera -from mmd_tools.properties.material import MMDMaterial -from mmd_tools.properties.rigid_body import MMDJoint, MMDRigidBody -from mmd_tools.properties.root import MMDRoot - -__properties = { - bpy.types.Object: { - 'mmd_type': bpy.props.EnumProperty( - name='Type', - description='Internal MMD type of this object (DO NOT CHANGE IT DIRECTLY)', - default='NONE', - items=[ - ('NONE', 'None', '', 1), - ('ROOT', 'Root', '', 2), - ('RIGID_GRP_OBJ', 'Rigid Body Grp Empty', '', 3), - ('JOINT_GRP_OBJ', 'Joint Grp Empty', '', 4), - ('TEMPORARY_GRP_OBJ', 'Temporary Grp Empty', '', 5), - ('PLACEHOLDER', 'Place Holder', '', 6), - - ('CAMERA', 'Camera', '', 21), - ('JOINT', 'Joint', '', 22), - ('RIGID_BODY', 'Rigid body', '', 23), - ('LIGHT', 'Light', '', 24), - - ('TRACK_TARGET', 'Track Target', '', 51), - ('NON_COLLISION_CONSTRAINT', 'Non Collision Constraint', '', 52), - ('SPRING_CONSTRAINT', 'Spring Constraint', '', 53), - ('SPRING_GOAL', 'Spring Goal', '', 54), - ] - ), - 'mmd_root': bpy.props.PointerProperty(type=MMDRoot), - 'mmd_camera': bpy.props.PointerProperty(type=MMDCamera), - 'mmd_rigid': bpy.props.PointerProperty(type=MMDRigidBody), - 'mmd_joint': bpy.props.PointerProperty(type=MMDJoint), - }, - bpy.types.Material: { - 'mmd_material': bpy.props.PointerProperty(type=MMDMaterial), - }, - bpy.types.PoseBone: { - 'mmd_bone': bpy.props.PointerProperty(type=MMDBone), - 'is_mmd_shadow_bone': bpy.props.BoolProperty(name='is_mmd_shadow_bone', default=False), - 'mmd_shadow_bone_type': bpy.props.StringProperty(name='mmd_shadow_bone_type'), - # TODO: Replace to driver for NLA - 'mmd_ik_toggle': bpy.props.BoolProperty( - name='MMD IK Toggle', - description='MMD IK toggle is used to import/export animation of IK on-off', - update=_mmd_ik_toggle_update, - default=True, - ), - } -} -def __set_hide(prop, value): - prop.hide_set(value) - if getattr(prop, 'hide_viewport'): - setattr(prop, 'hide_viewport', False) +def patch_library_overridable(property: "bpy.props._PropertyDeferred") -> "bpy.props._PropertyDeferred": + """Apply recursively for each mmd_tools property class annotations. + Args: + property: The property to be patched. + Returns: + The patched property. + """ + property.keywords.setdefault("override", set()).add("LIBRARY_OVERRIDABLE") -def __patch(properties): # temporary patching, should be removed in the future - def __patch_override(prop: bpy.props._PropertyDeferred): - """Apply recursively for each mmd_tools property class annotations + if property.function.__name__ not in {"PointerProperty", "CollectionProperty"}: + return property - # Hint for bpy.props._PropertyDeferred class - p = bpy.props.IntProperty(name='abc123') - assert isinstance(p, bpy.props._PropertyDeferred) - assert p.function == bpy.props.IntProperty - assert p.keywords == {'name': 'abc123'} - """ - prop.keywords.setdefault('override', set()).add('LIBRARY_OVERRIDABLE') - - if prop.function.__name__ not in {'PointerProperty', 'CollectionProperty'}: - return - - prop_type = prop.keywords['type'] - # The __annotations__ cannot be inherited. Manually search for base classes. - for inherited_type in [prop_type, *prop_type.__bases__]: - if not inherited_type.__module__.startswith('mmd_tools.properties'): + property_type = property.keywords["type"] + # The __annotations__ cannot be inherited. Manually search for base classes. + for inherited_type in (property_type, *property_type.__bases__): + if not inherited_type.__module__.startswith("mmd_tools.properties"): + continue + for annotation in inherited_type.__annotations__.values(): + if not isinstance(annotation, bpy.props._PropertyDeferred): continue - for annotation in inherited_type.__annotations__.values(): - if not isinstance(annotation, bpy.props._PropertyDeferred): - continue - __patch_override(annotation) - - for props in properties.values(): - for prop in props.values(): - __patch_override(prop) - - prop_obj = properties.setdefault(bpy.types.Object, {}) - - prop_obj['select'] = bpy.props.BoolProperty( - get=lambda prop: prop.select_get(), - set=lambda prop, value: prop.select_set(value), - options={'SKIP_SAVE', 'ANIMATABLE', 'LIBRARY_EDITABLE', }, - ) - prop_obj['hide'] = bpy.props.BoolProperty( - get=lambda prop: prop.hide_get(), - set=__set_hide, - options={'SKIP_SAVE', 'ANIMATABLE', 'LIBRARY_EDITABLE', }, - ) - - -if bpy.app.version >= (2, 80, 0): - __patch(__properties) - - -def register(): - for typ, t in __properties.items(): - for attr, prop in t.items(): - if hasattr(typ, attr): - logging.warning(' * warning: overwrite\t%s\t%s', typ, attr) - try: - setattr(typ, attr, prop) - except: # pylint: disable=bare-except - logging.warning(' * warning: register\t%s\t%s', typ, attr) - + patch_library_overridable(annotation) -def unregister(): - for typ, t in __properties.items(): - for attr in t.keys(): - if hasattr(typ, attr): - delattr(typ, attr) + return property diff --git a/mmd_tools/properties/bone.py b/mmd_tools/properties/bone.py deleted file mode 100644 index 50596242..00000000 --- a/mmd_tools/properties/bone.py +++ /dev/null @@ -1,208 +0,0 @@ -# -*- coding: utf-8 -*- -import bpy -from mmd_tools.core.bone import FnBone - - -def _updateMMDBoneAdditionalTransform(prop, context): - prop['is_additional_transform_dirty'] = True - p_bone = context.active_pose_bone - if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer(): - FnBone.apply_additional_transformation(prop.id_data) - - -def _updateAdditionalTransformInfluence(prop, context): - p_bone = context.active_pose_bone - if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer(): - FnBone(p_bone).update_additional_transform_influence() - else: - prop['is_additional_transform_dirty'] = True - - -def _getAdditionalTransformBone(prop): - arm = prop.id_data - bone_id = prop.get('additional_transform_bone_id', -1) - if bone_id < 0: - return '' - fnBone = FnBone.from_bone_id(arm, bone_id) - if not fnBone: - return '' - return fnBone.pose_bone.name - - -def _setAdditionalTransformBone(prop, value): - arm = prop.id_data - prop['is_additional_transform_dirty'] = True - if value not in arm.pose.bones.keys(): - prop['additional_transform_bone_id'] = -1 - return - pose_bone = arm.pose.bones[value] - bone = FnBone(pose_bone) - prop['additional_transform_bone_id'] = bone.bone_id - - -class MMDBone(bpy.types.PropertyGroup): - name_j: bpy.props.StringProperty( - name='Name', - description='Japanese Name', - default='', - ) - - name_e: bpy.props.StringProperty( - name='Name(Eng)', - description='English Name', - default='', - ) - - bone_id: bpy.props.IntProperty( - name='Bone ID', - description='Unique ID for the reference of bone morph and rotate+/move+', - default=-1, - min=-1, - ) - - transform_order: bpy.props.IntProperty( - name='Transform Order', - description='Deformation tier', - min=0, - max=100, - soft_max=7, - ) - - is_controllable: bpy.props.BoolProperty( - name='Controllable', - description='Is controllable', - default=True, - ) - - transform_after_dynamics: bpy.props.BoolProperty( - name='After Dynamics', - description='After physics', - default=False, - ) - - enabled_fixed_axis: bpy.props.BoolProperty( - name='Fixed Axis', - description='Use fixed axis', - default=False, - ) - - fixed_axis: bpy.props.FloatVectorProperty( - name='Fixed Axis', - description='Fixed axis', - subtype='XYZ', - size=3, - precision=3, - step=0.1, # 0.1 / 100 - default=[0, 0, 0], - ) - - enabled_local_axes: bpy.props.BoolProperty( - name='Local Axes', - description='Use local axes', - default=False, - ) - - local_axis_x: bpy.props.FloatVectorProperty( - name='Local X-Axis', - description='Local x-axis', - subtype='XYZ', - size=3, - precision=3, - step=0.1, - default=[1, 0, 0], - ) - - local_axis_z: bpy.props.FloatVectorProperty( - name='Local Z-Axis', - description='Local z-axis', - subtype='XYZ', - size=3, - precision=3, - step=0.1, - default=[0, 0, 1], - ) - - is_tip: bpy.props.BoolProperty( - name='Tip Bone', - description='Is zero length bone', - default=False, - ) - - ik_rotation_constraint: bpy.props.FloatProperty( - name='IK Rotation Constraint', - description='The unit angle of IK', - subtype='ANGLE', - soft_min=0, - soft_max=4, - default=1, - ) - - has_additional_rotation: bpy.props.BoolProperty( - name='Additional Rotation', - description='Additional rotation', - default=False, - update=_updateMMDBoneAdditionalTransform, - ) - - has_additional_location: bpy.props.BoolProperty( - name='Additional Location', - description='Additional location', - default=False, - update=_updateMMDBoneAdditionalTransform, - ) - - additional_transform_bone: bpy.props.StringProperty( - name='Additional Transform Bone', - description='Additional transform bone', - set=_setAdditionalTransformBone, - get=_getAdditionalTransformBone, - update=_updateMMDBoneAdditionalTransform, - ) - - additional_transform_bone_id: bpy.props.IntProperty( - name='Additional Transform Bone ID', - default=-1, - update=_updateMMDBoneAdditionalTransform, - ) - - additional_transform_influence: bpy.props.FloatProperty( - name='Additional Transform Influence', - description='Additional transform influence', - default=1, - soft_min=-1, - soft_max=1, - update=_updateAdditionalTransformInfluence, - ) - - is_additional_transform_dirty: bpy.props.BoolProperty( - name='', - default=True - ) - - def is_id_unique(self): - return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None) - - -def _mmd_ik_toggle_get(prop): - return prop.get('mmd_ik_toggle', True) - - -def _mmd_ik_toggle_set(prop, v): - if v != prop.get('mmd_ik_toggle', None): - prop['mmd_ik_toggle'] = v - _mmd_ik_toggle_update(prop, None) - - -def _mmd_ik_toggle_update(prop, context): - v = prop.mmd_ik_toggle - #logging.debug('_mmd_ik_toggle_update %s %s', v, prop.name) - for b in prop.id_data.pose.bones: - for c in b.constraints: - if c.type == 'IK' and c.subtarget == prop.name: - #logging.debug(' %s %s', b.name, c.name) - c.influence = v - b = b if c.use_tail else b.parent - for b in ([b]+b.parent_recursive)[:c.chain_count]: - c = next((c for c in b.constraints if c.type == 'LIMIT_ROTATION' and not c.mute), None) - if c: - c.influence = v diff --git a/mmd_tools/properties/camera.py b/mmd_tools/properties/camera.py index eba70204..61c2e0b4 100644 --- a/mmd_tools/properties/camera.py +++ b/mmd_tools/properties/camera.py @@ -1,15 +1,19 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import math import bpy +from . import patch_library_overridable + class MMDCamera(bpy.types.PropertyGroup): angle: bpy.props.FloatProperty( - name='Angle', - description='Camera lens field of view', - subtype='ANGLE', + name="Angle", + description="Camera lens field of view", + subtype="ANGLE", min=math.radians(1), max=math.radians(180), soft_max=math.radians(125), @@ -17,7 +21,15 @@ class MMDCamera(bpy.types.PropertyGroup): ) is_perspective: bpy.props.BoolProperty( - name='Perspective', - description='Is perspective', + name="Perspective", + description="Is perspective", default=True, ) + + @staticmethod + def register(): + bpy.types.Object.mmd_camera = patch_library_overridable(bpy.props.PointerProperty(type=MMDCamera)) + + @staticmethod + def unregister(): + del bpy.types.Object.mmd_camera diff --git a/mmd_tools/properties/material.py b/mmd_tools/properties/material.py index ca358cfa..a3264a5e 100644 --- a/mmd_tools/properties/material.py +++ b/mmd_tools/properties/material.py @@ -1,84 +1,87 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. import bpy -from mmd_tools import utils -from mmd_tools.core import material -from mmd_tools.core.material import FnMaterial -from mmd_tools.core.model import Model +from .. import utils +from ..core import material +from ..core.material import FnMaterial +from ..core.model import FnModel +from . import patch_library_overridable -def _updateAmbientColor(prop, context): + +def _mmd_material_update_ambient_color(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_ambient_color() -def _updateDiffuseColor(prop, context): +def _mmd_material_update_diffuse_color(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_diffuse_color() -def _updateAlpha(prop, context): +def _mmd_material_update_alpha(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_alpha() -def _updateSpecularColor(prop, context): +def _mmd_material_update_specular_color(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_specular_color() -def _updateShininess(prop, context): +def _mmd_material_update_shininess(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_shininess() -def _updateIsDoubleSided(prop, context): +def _mmd_material_update_is_double_sided(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_is_double_sided() -def _updateSphereMapType(prop, context): +def _mmd_material_update_sphere_texture_type(prop: "MMDMaterial", context): FnMaterial(prop.id_data).update_sphere_texture_type(context.active_object) -def _updateToonTexture(prop, context): +def _mmd_material_update_toon_texture(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_toon_texture() -def _updateDropShadow(prop, context): +def _mmd_material_update_enabled_drop_shadow(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_drop_shadow() -def _updateSelfShadowMap(prop, context): +def _mmd_material_update_enabled_self_shadow_map(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_self_shadow_map() -def _updateSelfShadow(prop, context): +def _mmd_material_update_enabled_self_shadow(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_self_shadow() -def _updateEnabledToonEdge(prop, context): +def _mmd_material_update_enabled_toon_edge(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_enabled_toon_edge() -def _updateEdgeColor(prop, context): +def _mmd_material_update_edge_color(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_edge_color() -def _updateEdgeWeight(prop, context): +def _mmd_material_update_edge_weight(prop: "MMDMaterial", _context): FnMaterial(prop.id_data).update_edge_weight() -def _getNameJ(prop): - return prop.get('name_j', '') +def _mmd_material_get_name_j(prop: "MMDMaterial"): + return prop.get("name_j", "") -def _setNameJ(prop, value): - old_value = prop.get('name_j') +def _mmd_material_set_name_j(prop: "MMDMaterial", value: str): prop_value = value - if prop_value and prop_value != old_value: - root = Model.findRoot(bpy.context.active_object) - if root: - rig = Model(root) - prop_value = utils.uniqueName(value, [mat.mmd_material.name_j for mat in rig.materials() if mat]) + if prop_value and prop_value != prop.get("name_j"): + root = FnModel.find_root_object(bpy.context.active_object) + if root is None: + prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in bpy.data.materials}) else: - prop_value = utils.uniqueName(value, [mat.mmd_material.name_j for mat in bpy.data.materials]) + prop_value = utils.unique_name(value, {mat.mmd_material.name_j for mat in FnModel.iterate_materials(root)}) + + prop["name_j"] = prop_value - prop['name_j'] = prop_value # =========================================== # Property classes @@ -86,188 +89,196 @@ def _setNameJ(prop, value): class MMDMaterial(bpy.types.PropertyGroup): - """ マテリアル - """ + """マテリアル""" + name_j: bpy.props.StringProperty( - name='Name', - description='Japanese Name', - default='', - set=_setNameJ, - get=_getNameJ, + name="Name", + description="Japanese Name", + default="", + set=_mmd_material_set_name_j, + get=_mmd_material_get_name_j, ) name_e: bpy.props.StringProperty( - name='Name(Eng)', - description='English Name', - default='', + name="Name(Eng)", + description="English Name", + default="", ) material_id: bpy.props.IntProperty( - name='Material ID', - description='Unique ID for the reference of material morph', + name="Material ID", + description="Unique ID for the reference of material morph", default=-1, min=-1, ) ambient_color: bpy.props.FloatVectorProperty( - name='Ambient Color', - description='Ambient color', - subtype='COLOR', + name="Ambient Color", + description="Ambient color", + subtype="COLOR", size=3, min=0, max=1, precision=3, step=0.1, default=[0.4, 0.4, 0.4], - update=_updateAmbientColor, + update=_mmd_material_update_ambient_color, ) diffuse_color: bpy.props.FloatVectorProperty( - name='Diffuse Color', - description='Diffuse color', - subtype='COLOR', + name="Diffuse Color", + description="Diffuse color", + subtype="COLOR", size=3, min=0, max=1, precision=3, step=0.1, default=[0.8, 0.8, 0.8], - update=_updateDiffuseColor, + update=_mmd_material_update_diffuse_color, ) alpha: bpy.props.FloatProperty( - name='Alpha', - description='Alpha transparency', + name="Alpha", + description="Alpha transparency", min=0, max=1, precision=3, step=0.1, default=1.0, - update=_updateAlpha, + update=_mmd_material_update_alpha, ) specular_color: bpy.props.FloatVectorProperty( - name='Specular Color', - description='Specular color', - subtype='COLOR', + name="Specular Color", + description="Specular color", + subtype="COLOR", size=3, min=0, max=1, precision=3, step=0.1, default=[0.625, 0.625, 0.625], - update=_updateSpecularColor, + update=_mmd_material_update_specular_color, ) shininess: bpy.props.FloatProperty( - name='Reflect', - description='Sharpness of reflected highlights', + name="Reflect", + description="Sharpness of reflected highlights", min=0, soft_max=512, step=100.0, default=50.0, - update=_updateShininess, + update=_mmd_material_update_shininess, ) is_double_sided: bpy.props.BoolProperty( - name='Double Sided', - description='Both sides of mesh should be rendered', + name="Double Sided", + description="Both sides of mesh should be rendered", default=False, - update=_updateIsDoubleSided, + update=_mmd_material_update_is_double_sided, ) enabled_drop_shadow: bpy.props.BoolProperty( - name='Ground Shadow', - description='Display ground shadow', + name="Ground Shadow", + description="Display ground shadow", default=True, - update=_updateDropShadow, + update=_mmd_material_update_enabled_drop_shadow, ) enabled_self_shadow_map: bpy.props.BoolProperty( - name='Self Shadow Map', - description='Object can become shadowed by other objects', + name="Self Shadow Map", + description="Object can become shadowed by other objects", default=True, - update=_updateSelfShadowMap, + update=_mmd_material_update_enabled_self_shadow_map, ) enabled_self_shadow: bpy.props.BoolProperty( - name='Self Shadow', - description='Object can cast shadows', + name="Self Shadow", + description="Object can cast shadows", default=True, - update=_updateSelfShadow, + update=_mmd_material_update_enabled_self_shadow, ) enabled_toon_edge: bpy.props.BoolProperty( - name='Toon Edge', - description='Use toon edge', + name="Toon Edge", + description="Use toon edge", default=False, - update=_updateEnabledToonEdge, + update=_mmd_material_update_enabled_toon_edge, ) edge_color: bpy.props.FloatVectorProperty( - name='Edge Color', - description='Toon edge color', - subtype='COLOR', + name="Edge Color", + description="Toon edge color", + subtype="COLOR", size=4, min=0, max=1, precision=3, step=0.1, default=[0, 0, 0, 1], - update=_updateEdgeColor, + update=_mmd_material_update_edge_color, ) edge_weight: bpy.props.FloatProperty( - name='Edge Weight', - description='Toon edge size', + name="Edge Weight", + description="Toon edge size", min=0, max=100, soft_max=2, step=1.0, default=1.0, - update=_updateEdgeWeight, + update=_mmd_material_update_edge_weight, ) sphere_texture_type: bpy.props.EnumProperty( - name='Sphere Map Type', - description='Choose sphere texture blend type', + name="Sphere Map Type", + description="Choose sphere texture blend type", items=[ - (str(material.SPHERE_MODE_OFF), 'Off', '', 1), - (str(material.SPHERE_MODE_MULT), 'Multiply', '', 2), - (str(material.SPHERE_MODE_ADD), 'Add', '', 3), - (str(material.SPHERE_MODE_SUBTEX), 'SubTexture', '', 4), + (str(material.SPHERE_MODE_OFF), "Off", "", 1), + (str(material.SPHERE_MODE_MULT), "Multiply", "", 2), + (str(material.SPHERE_MODE_ADD), "Add", "", 3), + (str(material.SPHERE_MODE_SUBTEX), "SubTexture", "", 4), ], - update=_updateSphereMapType, + update=_mmd_material_update_sphere_texture_type, ) is_shared_toon_texture: bpy.props.BoolProperty( - name='Use Shared Toon Texture', - description='Use shared toon texture or custom toon texture', + name="Use Shared Toon Texture", + description="Use shared toon texture or custom toon texture", default=False, - update=_updateToonTexture, + update=_mmd_material_update_toon_texture, ) toon_texture: bpy.props.StringProperty( - name='Toon Texture', - subtype='FILE_PATH', - description='The file path of custom toon texture', - default='', - update=_updateToonTexture, + name="Toon Texture", + subtype="FILE_PATH", + description="The file path of custom toon texture", + default="", + update=_mmd_material_update_toon_texture, ) shared_toon_texture: bpy.props.IntProperty( - name='Shared Toon Texture', - description='Shared toon texture id (toon01.bmp ~ toon10.bmp)', + name="Shared Toon Texture", + description="Shared toon texture id (toon01.bmp ~ toon10.bmp)", default=0, min=0, max=9, - update=_updateToonTexture, + update=_mmd_material_update_toon_texture, ) comment: bpy.props.StringProperty( - name='Comment', - description='Comment', + name="Comment", + description="Comment", ) def is_id_unique(self): return self.material_id < 0 or not next((m for m in bpy.data.materials if m.mmd_material != self and m.mmd_material.material_id == self.material_id), None) + + @staticmethod + def register(): + bpy.types.Material.mmd_material = patch_library_overridable(bpy.props.PointerProperty(type=MMDMaterial)) + + @staticmethod + def unregister(): + del bpy.types.Material.mmd_material diff --git a/mmd_tools/properties/morph.py b/mmd_tools/properties/morph.py index ca3a14f3..8ceb1e89 100644 --- a/mmd_tools/properties/morph.py +++ b/mmd_tools/properties/morph.py @@ -1,50 +1,52 @@ # -*- coding: utf-8 -*- -import logging +# Copyright 2015 MMD Tools authors +# This file is part of MMD Tools. import bpy -from mmd_tools import utils -from mmd_tools.core.bone import FnBone -from mmd_tools.core.material import FnMaterial -from mmd_tools.core.model import Model as FnModel -from mmd_tools.core.morph import FnMorph +from .. import utils +from ..core.bone import FnBone +from ..core.material import FnMaterial +from ..core.model import FnModel, Model +from ..core.morph import FnMorph -def _get_name(prop): - return prop.get('name', '') +def _morph_base_get_name(prop: "_MorphBase") -> str: + return prop.get("name", "") -def _set_name(prop, value): + +def _morph_base_set_name(prop: "_MorphBase", value: str): mmd_root = prop.id_data.mmd_root - #morph_type = mmd_root.active_morph_type - morph_type = '%s_morphs' % prop.bl_rna.identifier[:-5].lower() + # morph_type = mmd_root.active_morph_type + morph_type = "%s_morphs" % prop.bl_rna.identifier[:-5].lower() # assert(prop.bl_rna.identifier.endswith('Morph')) - #logging.debug('_set_name: %s %s %s', prop, value, morph_type) - prop_name = prop.get('name', None) + # logging.debug('_set_name: %s %s %s', prop, value, morph_type) + prop_name = prop.get("name", None) if prop_name == value: return used_names = {x.name for x in getattr(mmd_root, morph_type) if x != prop} - value = utils.uniqueName(value, used_names) + value = utils.unique_name(value, used_names) if prop_name is not None: - if morph_type == 'vertex_morphs': + if morph_type == "vertex_morphs": kb_list = {} - for mesh in FnModel(prop.id_data).meshes(): - for kb in getattr(mesh.data.shape_keys, 'key_blocks', ()): + for mesh in FnModel.iterate_mesh_objects(prop.id_data): + for kb in getattr(mesh.data.shape_keys, "key_blocks", ()): kb_list.setdefault(kb.name, []).append(kb) if prop_name in kb_list: - value = utils.uniqueName(value, used_names | kb_list.keys()) + value = utils.unique_name(value, used_names | kb_list.keys()) for kb in kb_list[prop_name]: kb.name = value - elif morph_type == 'uv_morphs': + elif morph_type == "uv_morphs": vg_list = {} - for mesh in FnModel(prop.id_data).meshes(): + for mesh in FnModel.iterate_mesh_objects(prop.id_data): for vg, n, x in FnMorph.get_uv_morph_vertex_groups(mesh): vg_list.setdefault(n, []).append(vg) if prop_name in vg_list: - value = utils.uniqueName(value, used_names | vg_list.keys()) + value = utils.unique_name(value, used_names | vg_list.keys()) for vg in vg_list[prop_name]: vg.name = vg.name.replace(prop_name, value) @@ -54,66 +56,64 @@ def _set_name(prop, value): if d.name == prop_name and d.morph_type == morph_type: d.name = value - frame_facial = mmd_root.display_item_frames.get(u'表情') - for item in getattr(frame_facial, 'data', []): + frame_facial = mmd_root.display_item_frames.get("表情") + for item in getattr(frame_facial, "data", []): if item.name == prop_name and item.morph_type == morph_type: item.name = value break - obj = FnModel(prop.id_data).morph_slider.placeholder() + obj = Model(prop.id_data).morph_slider.placeholder() if obj and value not in obj.data.shape_keys.key_blocks: kb = obj.data.shape_keys.key_blocks.get(prop_name, None) if kb: kb.name = value - prop['name'] = value + prop["name"] = value class _MorphBase: name: bpy.props.StringProperty( - name='Name', - description='Japanese Name', - set=_set_name, - get=_get_name, + name="Name", + description="Japanese Name", + set=_morph_base_set_name, + get=_morph_base_get_name, ) name_e: bpy.props.StringProperty( - name='Name(Eng)', - description='English Name', - default='', + name="Name(Eng)", + description="English Name", + default="", ) category: bpy.props.EnumProperty( - name='Category', - description='Select category', + name="Category", + description="Select category", items=[ - ('SYSTEM', 'Hidden', '', 0), - ('EYEBROW', 'Eye Brow', '', 1), - ('EYE', 'Eye', '', 2), - ('MOUTH', 'Mouth', '', 3), - ('OTHER', 'Other', '', 4), + ("SYSTEM", "Hidden", "", 0), + ("EYEBROW", "Eye Brow", "", 1), + ("EYE", "Eye", "", 2), + ("MOUTH", "Mouth", "", 3), + ("OTHER", "Other", "", 4), ], - default='OTHER', + default="OTHER", ) -def _get_bone(prop): - bone_id = prop.get('bone_id', -1) +def _bone_morph_data_get_bone(prop: "BoneMorphData") -> str: + bone_id = prop.get("bone_id", -1) if bone_id < 0: - return '' - root = prop.id_data - fnModel = FnModel(root) - arm = fnModel.armature() - if arm is None: - return '' - fnBone = FnBone.from_bone_id(arm, bone_id) - if not fnBone: - return '' - return fnBone.pose_bone.name - - -def _set_bone(prop, value): + return "" + root_object = prop.id_data + armature_object = FnModel.find_armature_object(root_object) + if armature_object is None: + return "" + pose_bone = FnBone.find_pose_bone_by_bone_id(armature_object, bone_id) + if pose_bone is None: + return "" + return pose_bone.name + + +def _bone_morph_data_set_bone(prop: "BoneMorphData", value: str): root = prop.id_data - fnModel = FnModel(root) - arm = fnModel.armature() + arm = FnModel.find_armature_object(root) # Load the library_override file. This function is triggered when loading, but the arm obj cannot be found. # The arm obj is exist, but the relative relationship has not yet been established. @@ -121,15 +121,14 @@ def _set_bone(prop, value): return if value not in arm.pose.bones.keys(): - prop['bone_id'] = -1 + prop["bone_id"] = -1 return pose_bone = arm.pose.bones[value] - fnBone = FnBone(pose_bone) - prop['bone_id'] = fnBone.bone_id + prop["bone_id"] = FnBone.get_or_assign_bone_id(pose_bone) -def _update_bone_morph_data(prop, context): - if not prop.name.startswith('mmd_bind'): +def _bone_morph_data_update_location_or_rotation(prop: "BoneMorphData", _context): + if not prop.name.startswith("mmd_bind"): return arm = FnModel(prop.id_data).morph_slider.dummy_armature if arm: @@ -140,91 +139,91 @@ def _update_bone_morph_data(prop, context): class BoneMorphData(bpy.types.PropertyGroup): - """ - """ + """ """ + bone: bpy.props.StringProperty( - name='Bone', - description='Target bone', - set=_set_bone, - get=_get_bone, + name="Bone", + description="Target bone", + set=_bone_morph_data_set_bone, + get=_bone_morph_data_get_bone, ) bone_id: bpy.props.IntProperty( - name='Bone ID', + name="Bone ID", ) location: bpy.props.FloatVectorProperty( - name='Location', - description='Location', - subtype='TRANSLATION', + name="Location", + description="Location", + subtype="TRANSLATION", size=3, default=[0, 0, 0], - update=_update_bone_morph_data, + update=_bone_morph_data_update_location_or_rotation, ) rotation: bpy.props.FloatVectorProperty( - name='Rotation', - description='Rotation in quaternions', - subtype='QUATERNION', + name="Rotation", + description="Rotation in quaternions", + subtype="QUATERNION", size=4, default=[1, 0, 0, 0], - update=_update_bone_morph_data, + update=_bone_morph_data_update_location_or_rotation, ) class BoneMorph(_MorphBase, bpy.types.PropertyGroup): - """Bone Morph - """ + """Bone Morph""" + data: bpy.props.CollectionProperty( - name='Morph Data', + name="Morph Data", type=BoneMorphData, ) active_data: bpy.props.IntProperty( - name='Active Bone Data', + name="Active Bone Data", min=0, default=0, ) -def _get_material(prop): - mat_p = prop.get('material_data', None) +def _material_morph_data_get_material(prop: "MaterialMorphData"): + mat_p = prop.get("material_data", None) if mat_p is not None: return mat_p.name - return '' + return "" -def _set_material(prop, value): +def _material_morph_data_set_material(prop: "MaterialMorphData", value: str): if value not in bpy.data.materials: - prop['material_data'] = None - prop['material_id'] = -1 + prop["material_data"] = None + prop["material_id"] = -1 else: mat = bpy.data.materials[value] fnMat = FnMaterial(mat) - prop['material_data'] = mat - prop['material_id'] = fnMat.material_id + prop["material_data"] = mat + prop["material_id"] = fnMat.material_id -def _set_related_mesh(prop, value): - rig = FnModel(prop.id_data) - mesh = rig.findMesh(value) +def _material_morph_data_set_related_mesh(prop: "MaterialMorphData", value: str): + mesh = FnModel.find_mesh_object_by_name(prop.id_data, value) if mesh is not None: - prop['related_mesh_data'] = mesh.data + prop["related_mesh_data"] = mesh.data else: - prop['related_mesh_data'] = None + prop["related_mesh_data"] = None -def _get_related_mesh(prop): - mesh_p = prop.get('related_mesh_data', None) +def _material_morph_data_get_related_mesh(prop): + mesh_p = prop.get("related_mesh_data", None) if mesh_p is not None: return mesh_p.name - return '' + return "" -def _update_material_morph_data(prop, context): - if not prop.name.startswith('mmd_bind'): +def _material_morph_data_update_modifiable_values(prop: "MaterialMorphData", _context): + if not prop.name.startswith("mmd_bind"): return - from mmd_tools.core.shader import _MaterialMorph - mat = prop['material_data'] + from ..core.shader import _MaterialMorph + + mat = prop["material_data"] if mat is not None: _MaterialMorph.update_morph_inputs(mat, prop) else: @@ -233,185 +232,177 @@ def _update_material_morph_data(prop, context): class MaterialMorphData(bpy.types.PropertyGroup): - """ - """ + """ """ + related_mesh: bpy.props.StringProperty( - name='Related Mesh', - description='Stores a reference to the mesh where this morph data belongs to', - set=_set_related_mesh, - get=_get_related_mesh, + name="Related Mesh", + description="Stores a reference to the mesh where this morph data belongs to", + set=_material_morph_data_set_related_mesh, + get=_material_morph_data_get_related_mesh, ) related_mesh_data: bpy.props.PointerProperty( - name='Related Mesh Data', + name="Related Mesh Data", type=bpy.types.Mesh, ) - offset_type: bpy.props.EnumProperty( - name='Offset Type', - description='Select offset type', - items=[ - ('MULT', 'Multiply', '', 0), - ('ADD', 'Add', '', 1) - ], - default='ADD' - ) + offset_type: bpy.props.EnumProperty(name="Offset Type", description="Select offset type", items=[("MULT", "Multiply", "", 0), ("ADD", "Add", "", 1)], default="ADD") material: bpy.props.StringProperty( - name='Material', - description='Target material', - get=_get_material, - set=_set_material, + name="Material", + description="Target material", + get=_material_morph_data_get_material, + set=_material_morph_data_set_material, ) material_id: bpy.props.IntProperty( - name='Material ID', + name="Material ID", default=-1, ) material_data: bpy.props.PointerProperty( - name='Material Data', + name="Material Data", type=bpy.types.Material, ) diffuse_color: bpy.props.FloatVectorProperty( - name='Diffuse Color', - description='Diffuse color', - subtype='COLOR', + name="Diffuse Color", + description="Diffuse color", + subtype="COLOR", size=4, soft_min=0, soft_max=1, precision=3, step=0.1, default=[0, 0, 0, 1], - update=_update_material_morph_data, + update=_material_morph_data_update_modifiable_values, ) specular_color: bpy.props.FloatVectorProperty( - name='Specular Color', - description='Specular color', - subtype='COLOR', + name="Specular Color", + description="Specular color", + subtype="COLOR", size=3, soft_min=0, soft_max=1, precision=3, step=0.1, default=[0, 0, 0], - update=_update_material_morph_data, + update=_material_morph_data_update_modifiable_values, ) shininess: bpy.props.FloatProperty( - name='Reflect', - description='Reflect', + name="Reflect", + description="Reflect", soft_min=0, soft_max=500, step=100.0, default=0.0, - update=_update_material_morph_data, + update=_material_morph_data_update_modifiable_values, ) ambient_color: bpy.props.FloatVectorProperty( - name='Ambient Color', - description='Ambient color', - subtype='COLOR', + name="Ambient Color", + description="Ambient color", + subtype="COLOR", size=3, soft_min=0, soft_max=1, precision=3, step=0.1, default=[0, 0, 0], - update=_update_material_morph_data, + update=_material_morph_data_update_modifiable_values, ) edge_color: bpy.props.FloatVectorProperty( - name='Edge Color', - description='Edge color', - subtype='COLOR', + name="Edge Color", + description="Edge color", + subtype="COLOR", size=4, soft_min=0, soft_max=1, precision=3, step=0.1, default=[0, 0, 0, 1], - update=_update_material_morph_data, + update=_material_morph_data_update_modifiable_values, ) edge_weight: bpy.props.FloatProperty( - name='Edge Weight', - description='Edge weight', + name="Edge Weight", + description="Edge weight", soft_min=0, soft_max=2, step=0.1, default=0, - update=_update_material_morph_data, + update=_material_morph_data_update_modifiable_values, ) texture_factor: bpy.props.FloatVectorProperty( - name='Texture factor', - description='Texture factor', - subtype='COLOR', + name="Texture factor", + description="Texture factor", + subtype="COLOR", size=4, soft_min=0, soft_max=1, precision=3, step=0.1, default=[0, 0, 0, 1], - update=_update_material_morph_data, + update=_material_morph_data_update_modifiable_values, ) sphere_texture_factor: bpy.props.FloatVectorProperty( - name='Sphere Texture factor', - description='Sphere texture factor', - subtype='COLOR', + name="Sphere Texture factor", + description="Sphere texture factor", + subtype="COLOR", size=4, soft_min=0, soft_max=1, precision=3, step=0.1, default=[0, 0, 0, 1], - update=_update_material_morph_data, + update=_material_morph_data_update_modifiable_values, ) toon_texture_factor: bpy.props.FloatVectorProperty( - name='Toon Texture factor', - description='Toon texture factor', - subtype='COLOR', + name="Toon Texture factor", + description="Toon texture factor", + subtype="COLOR", size=4, soft_min=0, soft_max=1, precision=3, step=0.1, default=[0, 0, 0, 1], - update=_update_material_morph_data, + update=_material_morph_data_update_modifiable_values, ) class MaterialMorph(_MorphBase, bpy.types.PropertyGroup): - """ Material Morph - """ + """Material Morph""" + data: bpy.props.CollectionProperty( - name='Morph Data', + name="Morph Data", type=MaterialMorphData, ) active_data: bpy.props.IntProperty( - name='Active Material Data', + name="Active Material Data", min=0, default=0, ) class UVMorphOffset(bpy.types.PropertyGroup): - """UV Morph Offset - """ + """UV Morph Offset""" + index: bpy.props.IntProperty( - name='Vertex Index', - description='Vertex index', + name="Vertex Index", + description="Vertex index", min=0, default=0, ) offset: bpy.props.FloatVectorProperty( - name='UV Offset', - description='UV offset', + name="UV Offset", + description="UV offset", size=4, # min=-1, # max=1, @@ -422,35 +413,35 @@ class UVMorphOffset(bpy.types.PropertyGroup): class UVMorph(_MorphBase, bpy.types.PropertyGroup): - """UV Morph - """ + """UV Morph""" + uv_index: bpy.props.IntProperty( - name='UV Index', - description='UV index (UV, UV1 ~ UV4)', + name="UV Index", + description="UV index (UV, UV1 ~ UV4)", min=0, max=4, default=0, ) data_type: bpy.props.EnumProperty( - name='Data Type', - description='Select data type', + name="Data Type", + description="Select data type", items=[ - ('DATA', 'Data', 'Store offset data in root object (deprecated)', 0), - ('VERTEX_GROUP', 'Vertex Group', 'Store offset data in vertex groups', 1), + ("DATA", "Data", "Store offset data in root object (deprecated)", 0), + ("VERTEX_GROUP", "Vertex Group", "Store offset data in vertex groups", 1), ], - default='DATA', + default="DATA", ) data: bpy.props.CollectionProperty( - name='Morph Data', + name="Morph Data", type=UVMorphOffset, ) active_data: bpy.props.IntProperty( - name='Active UV Data', + name="Active UV Data", min=0, default=0, ) vertex_group_scale: bpy.props.FloatProperty( - name='Vertex Group Scale', + name="Vertex Group Scale", description='The value scale of "Vertex Group" data type', precision=3, step=0.1, @@ -459,45 +450,36 @@ class UVMorph(_MorphBase, bpy.types.PropertyGroup): class GroupMorphOffset(bpy.types.PropertyGroup): - """Group Morph Offset - """ + """Group Morph Offset""" + morph_type: bpy.props.EnumProperty( - name='Morph Type', - description='Select morph type', + name="Morph Type", + description="Select morph type", items=[ - ('material_morphs', 'Material', 'Material Morphs', 0), - ('uv_morphs', 'UV', 'UV Morphs', 1), - ('bone_morphs', 'Bone', 'Bone Morphs', 2), - ('vertex_morphs', 'Vertex', 'Vertex Morphs', 3), - ('group_morphs', 'Group', 'Group Morphs', 4), + ("material_morphs", "Material", "Material Morphs", 0), + ("uv_morphs", "UV", "UV Morphs", 1), + ("bone_morphs", "Bone", "Bone Morphs", 2), + ("vertex_morphs", "Vertex", "Vertex Morphs", 3), + ("group_morphs", "Group", "Group Morphs", 4), ], - default='vertex_morphs', - ) - factor: bpy.props.FloatProperty( - name='Factor', - description='Factor', - soft_min=0, - soft_max=1, - precision=3, - step=0.1, - default=0 + default="vertex_morphs", ) + factor: bpy.props.FloatProperty(name="Factor", description="Factor", soft_min=0, soft_max=1, precision=3, step=0.1, default=0) class GroupMorph(_MorphBase, bpy.types.PropertyGroup): - """Group Morph - """ + """Group Morph""" + data: bpy.props.CollectionProperty( - name='Morph Data', + name="Morph Data", type=GroupMorphOffset, ) active_data: bpy.props.IntProperty( - name='Active Group Data', + name="Active Group Data", min=0, default=0, ) class VertexMorph(_MorphBase, bpy.types.PropertyGroup): - """Vertex Morph - """ + """Vertex Morph""" diff --git a/mmd_tools/properties/pose_bone.py b/mmd_tools/properties/pose_bone.py new file mode 100644 index 00000000..75bc5693 --- /dev/null +++ b/mmd_tools/properties/pose_bone.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. + +from typing import cast +import bpy + +from ..core.bone import FnBone +from . import patch_library_overridable + + +def _mmd_bone_update_additional_transform(prop: "MMDBone", context: bpy.types.Context): + prop["is_additional_transform_dirty"] = True + p_bone = context.active_pose_bone + if p_bone and p_bone.mmd_bone.as_pointer() == prop.as_pointer(): + FnBone.apply_additional_transformation(prop.id_data) + + +def _mmd_bone_update_additional_transform_influence(prop: "MMDBone", context: bpy.types.Context): + pose_bone = context.active_pose_bone + if pose_bone and pose_bone.mmd_bone.as_pointer() == prop.as_pointer(): + FnBone.update_additional_transform_influence(pose_bone) + else: + prop["is_additional_transform_dirty"] = True + + +def _mmd_bone_get_additional_transform_bone(prop: "MMDBone"): + arm = prop.id_data + bone_id = prop.get("additional_transform_bone_id", -1) + if bone_id < 0: + return "" + pose_bone = FnBone.find_pose_bone_by_bone_id(arm, bone_id) + if pose_bone is None: + return "" + return pose_bone.name + + +def _mmd_bone_set_additional_transform_bone(prop: "MMDBone", value: str): + arm = prop.id_data + prop["is_additional_transform_dirty"] = True + if value not in arm.pose.bones.keys(): + prop["additional_transform_bone_id"] = -1 + return + pose_bone = arm.pose.bones[value] + prop["additional_transform_bone_id"] = FnBone.get_or_assign_bone_id(pose_bone) + + +class MMDBone(bpy.types.PropertyGroup): + name_j: bpy.props.StringProperty( + name="Name", + description="Japanese Name", + default="", + ) + + name_e: bpy.props.StringProperty( + name="Name(Eng)", + description="English Name", + default="", + ) + + bone_id: bpy.props.IntProperty( + name="Bone ID", + description="Unique ID for the reference of bone morph and rotate+/move+", + default=-1, + min=-1, + ) + + transform_order: bpy.props.IntProperty( + name="Transform Order", + description="Deformation tier", + min=0, + max=100, + soft_max=7, + ) + + is_controllable: bpy.props.BoolProperty( + name="Controllable", + description="Is controllable", + default=True, + ) + + transform_after_dynamics: bpy.props.BoolProperty( + name="After Dynamics", + description="After physics", + default=False, + ) + + enabled_fixed_axis: bpy.props.BoolProperty( + name="Fixed Axis", + description="Use fixed axis", + default=False, + ) + + fixed_axis: bpy.props.FloatVectorProperty( + name="Fixed Axis", + description="Fixed axis", + subtype="XYZ", + size=3, + precision=3, + step=0.1, # 0.1 / 100 + default=[0, 0, 0], + ) + + enabled_local_axes: bpy.props.BoolProperty( + name="Local Axes", + description="Use local axes", + default=False, + ) + + local_axis_x: bpy.props.FloatVectorProperty( + name="Local X-Axis", + description="Local x-axis", + subtype="XYZ", + size=3, + precision=3, + step=0.1, + default=[1, 0, 0], + ) + + local_axis_z: bpy.props.FloatVectorProperty( + name="Local Z-Axis", + description="Local z-axis", + subtype="XYZ", + size=3, + precision=3, + step=0.1, + default=[0, 0, 1], + ) + + is_tip: bpy.props.BoolProperty( + name="Tip Bone", + description="Is zero length bone", + default=False, + ) + + ik_rotation_constraint: bpy.props.FloatProperty( + name="IK Rotation Constraint", + description="The unit angle of IK", + subtype="ANGLE", + soft_min=0, + soft_max=4, + default=1, + ) + + has_additional_rotation: bpy.props.BoolProperty( + name="Additional Rotation", + description="Additional rotation", + default=False, + update=_mmd_bone_update_additional_transform, + ) + + has_additional_location: bpy.props.BoolProperty( + name="Additional Location", + description="Additional location", + default=False, + update=_mmd_bone_update_additional_transform, + ) + + additional_transform_bone: bpy.props.StringProperty( + name="Additional Transform Bone", + description="Additional transform bone", + set=_mmd_bone_set_additional_transform_bone, + get=_mmd_bone_get_additional_transform_bone, + update=_mmd_bone_update_additional_transform, + ) + + additional_transform_bone_id: bpy.props.IntProperty( + name="Additional Transform Bone ID", + default=-1, + update=_mmd_bone_update_additional_transform, + ) + + additional_transform_influence: bpy.props.FloatProperty( + name="Additional Transform Influence", + description="Additional transform influence", + default=1, + soft_min=-1, + soft_max=1, + update=_mmd_bone_update_additional_transform_influence, + ) + + is_additional_transform_dirty: bpy.props.BoolProperty(name="", default=True) + + def is_id_unique(self): + return self.bone_id < 0 or not next((b for b in self.id_data.pose.bones if b.mmd_bone != self and b.mmd_bone.bone_id == self.bone_id), None) + + @staticmethod + def register(): + bpy.types.PoseBone.mmd_bone = patch_library_overridable(bpy.props.PointerProperty(type=MMDBone)) + bpy.types.PoseBone.is_mmd_shadow_bone = patch_library_overridable(bpy.props.BoolProperty(name="is_mmd_shadow_bone", default=False)) + bpy.types.PoseBone.mmd_shadow_bone_type = patch_library_overridable(bpy.props.StringProperty(name="mmd_shadow_bone_type")) + bpy.types.PoseBone.mmd_ik_toggle = patch_library_overridable( + bpy.props.BoolProperty( + name="MMD IK Toggle", + description="MMD IK toggle is used to import/export animation of IK on-off", + update=_pose_bone_update_mmd_ik_toggle, + default=True, + ) + ) + + @staticmethod + def unregister(): + del bpy.types.PoseBone.mmd_ik_toggle + del bpy.types.PoseBone.mmd_shadow_bone_type + del bpy.types.PoseBone.is_mmd_shadow_bone + del bpy.types.PoseBone.mmd_bone + + +def _pose_bone_update_mmd_ik_toggle(prop: bpy.types.PoseBone, _context): + v = prop.mmd_ik_toggle + armature_object = cast(bpy.types.Object, prop.id_data) + for b in armature_object.pose.bones: + for c in b.constraints: + if c.type == "IK" and c.subtarget == prop.name: + # logging.debug(' %s %s', b.name, c.name) + c.influence = v + b = b if c.use_tail else b.parent + for b in ([b] + b.parent_recursive)[: c.chain_count]: + c = next((c for c in b.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None) + if c: + c.influence = v diff --git a/mmd_tools/properties/rigid_body.py b/mmd_tools/properties/rigid_body.py index c0990588..c8fa1b86 100644 --- a/mmd_tools/properties/rigid_body.py +++ b/mmd_tools/properties/rigid_body.py @@ -1,28 +1,35 @@ # -*- coding: utf-8 -*- +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. + +"""Properties for rigid bodies and joints""" import bpy -from mmd_tools import bpyutils -from mmd_tools.core import rigid_body -from mmd_tools.core.model import FnModel, getRigidBodySize + +from .. import bpyutils +from ..core import rigid_body +from ..core.rigid_body import RigidBodyMaterial, FnRigidBody +from ..core.model import FnModel +from . import patch_library_overridable -def _updateCollisionGroup(prop, context): +def _updateCollisionGroup(prop, _context): obj = prop.id_data materials = obj.data.materials if len(materials) == 0: - materials.append(rigid_body.RigidBodyMaterial.getMaterial(prop.collision_group_number)) + materials.append(RigidBodyMaterial.getMaterial(prop.collision_group_number)) else: - obj.material_slots[0].material = rigid_body.RigidBodyMaterial.getMaterial(prop.collision_group_number) + obj.material_slots[0].material = RigidBodyMaterial.getMaterial(prop.collision_group_number) -def _updateType(prop, context): +def _updateType(prop, _context): obj = prop.id_data rb = obj.rigid_body if rb: - rb.kinematic = (int(prop.type) == rigid_body.MODE_STATIC) + rb.kinematic = int(prop.type) == rigid_body.MODE_STATIC -def _updateShape(prop, context): +def _updateShape(prop, _context): obj = prop.id_data if len(obj.data.vertices) > 0: @@ -36,64 +43,64 @@ def _updateShape(prop, context): def _get_bone(prop): obj = prop.id_data - relation = obj.constraints.get('mmd_tools_rigid_parent', None) + relation = obj.constraints.get("mmd_tools_rigid_parent", None) if relation: arm = relation.target bone_name = relation.subtarget if arm is not None and bone_name in arm.data.bones: return bone_name - return prop.get('bone', '') + return prop.get("bone", "") def _set_bone(prop, value): bone_name = value obj = prop.id_data - relation = obj.constraints.get('mmd_tools_rigid_parent', None) + relation = obj.constraints.get("mmd_tools_rigid_parent", None) if relation is None: - relation = obj.constraints.new('CHILD_OF') - relation.name = 'mmd_tools_rigid_parent' + relation = obj.constraints.new("CHILD_OF") + relation.name = "mmd_tools_rigid_parent" relation.mute = True arm = relation.target if arm is None: - root = FnModel.find_root(obj) + root = FnModel.find_root_object(obj) if root: - arm = relation.target = FnModel.find_armature(root) + arm = relation.target = FnModel.find_armature_object(root) if arm is not None and bone_name in arm.data.bones: relation.subtarget = bone_name else: - relation.subtarget = bone_name = '' + relation.subtarget = bone_name = "" - prop['bone'] = bone_name + prop["bone"] = bone_name def _get_size(prop): - if prop.id_data.mmd_type != 'RIGID_BODY': + if prop.id_data.mmd_type != "RIGID_BODY": return (0, 0, 0) - return getRigidBodySize(prop.id_data) + return FnRigidBody.get_rigid_body_size(prop.id_data) def _set_size(prop, value): obj = prop.id_data - assert(obj.mode == 'OBJECT') # not support other mode yet + assert obj.mode == "OBJECT" # not support other mode yet shape = prop.shape mesh = obj.data rb = obj.rigid_body if len(mesh.vertices) == 0 or rb is None or rb.collision_shape != shape: - if shape == 'SPHERE': + if shape == "SPHERE": bpyutils.makeSphere( radius=value[0], target_object=obj, ) - elif shape == 'BOX': + elif shape == "BOX": bpyutils.makeBox( size=value, target_object=obj, ) - elif shape == 'CAPSULE': + elif shape == "CAPSULE": bpyutils.makeCapsule( radius=value[0], height=value[1], @@ -103,12 +110,12 @@ def _set_size(prop, value): if rb: rb.collision_shape = shape else: - if shape == 'SPHERE': + if shape == "SPHERE": radius = max(value[0], 1e-3) for v in mesh.vertices: vec = v.co.normalized() v.co = vec * radius - elif shape == 'BOX': + elif shape == "BOX": x = max(value[0], 1e-3) y = max(value[1], 1e-3) z = max(value[2], 1e-3) @@ -118,50 +125,50 @@ def _set_size(prop, value): y0 = -y if y0 < 0 else y z0 = -z if z0 < 0 else z v.co = [x0, y0, z0] - elif shape == 'CAPSULE': - r0, h0, xx = getRigidBodySize(prop.id_data) + elif shape == "CAPSULE": + r0, h0, xx = FnRigidBody.get_rigid_body_size(prop.id_data) h0 *= 0.5 radius = max(value[0], 1e-3) - height = max(value[1], 1e-3)*0.5 - scale = radius/max(r0, 1e-3) + height = max(value[1], 1e-3) * 0.5 + scale = radius / max(r0, 1e-3) for v in mesh.vertices: x0, y0, z0 = v.co x0 *= scale y0 *= scale if z0 < 0: - z0 = (z0 + h0)*scale - height + z0 = (z0 + h0) * scale - height else: - z0 = (z0 - h0)*scale + height + z0 = (z0 - h0) * scale + height v.co = [x0, y0, z0] mesh.update() def _get_rigid_name(prop): - return prop.get('name', '') + return prop.get("name", "") def _set_rigid_name(prop, value): - prop['name'] = value + prop["name"] = value class MMDRigidBody(bpy.types.PropertyGroup): name_j: bpy.props.StringProperty( - name='Name', - description='Japanese Name', - default='', + name="Name", + description="Japanese Name", + default="", get=_get_rigid_name, set=_set_rigid_name, ) name_e: bpy.props.StringProperty( - name='Name(Eng)', - description='English Name', - default='', + name="Name(Eng)", + description="English Name", + default="", ) collision_group_number: bpy.props.IntProperty( - name='Collision Group', - description='The collision group of the object', + name="Collision Group", + description="The collision group of the object", min=0, max=15, default=1, @@ -169,49 +176,46 @@ class MMDRigidBody(bpy.types.PropertyGroup): ) collision_group_mask: bpy.props.BoolVectorProperty( - name='Collision Group Mask', - description='The groups the object can not collide with', + name="Collision Group Mask", + description="The groups the object can not collide with", size=16, - subtype='LAYER', + subtype="LAYER", ) type: bpy.props.EnumProperty( - name='Rigid Type', - description='Select rigid type', + name="Rigid Type", + description="Select rigid type", items=[ - (str(rigid_body.MODE_STATIC), 'Bone', - "Rigid body's orientation completely determined by attached bone", 1), - (str(rigid_body.MODE_DYNAMIC), 'Physics', - "Attached bone's orientation completely determined by rigid body", 2), - (str(rigid_body.MODE_DYNAMIC_BONE), 'Physics + Bone', - "Bone determined by combination of parent and attached rigid body", 3), + (str(rigid_body.MODE_STATIC), "Bone", "Rigid body's orientation completely determined by attached bone", 1), + (str(rigid_body.MODE_DYNAMIC), "Physics", "Attached bone's orientation completely determined by rigid body", 2), + (str(rigid_body.MODE_DYNAMIC_BONE), "Physics + Bone", "Bone determined by combination of parent and attached rigid body", 3), ], update=_updateType, ) shape: bpy.props.EnumProperty( - name='Shape', - description='Select the collision shape', + name="Shape", + description="Select the collision shape", items=[ - ('SPHERE', 'Sphere', '', 1), - ('BOX', 'Box', '', 2), - ('CAPSULE', 'Capsule', '', 3), + ("SPHERE", "Sphere", "", 1), + ("BOX", "Box", "", 2), + ("CAPSULE", "Capsule", "", 3), ], update=_updateShape, ) bone: bpy.props.StringProperty( - name='Bone', - description='Target bone', - default='', + name="Bone", + description="Target bone", + default="", get=_get_bone, set=_set_bone, ) size: bpy.props.FloatVectorProperty( - name='Size', - description='Size of the object', - subtype='XYZ', + name="Size", + description="Size of the object", + subtype="XYZ", size=3, min=0, step=0.1, @@ -219,6 +223,14 @@ class MMDRigidBody(bpy.types.PropertyGroup): set=_set_size, ) + @staticmethod + def register(): + bpy.types.Object.mmd_rigid = patch_library_overridable(bpy.props.PointerProperty(type=MMDRigidBody)) + + @staticmethod + def unregister(): + del bpy.types.Object.mmd_rigid + def _updateSpringLinear(prop, context): obj = prop.id_data @@ -232,7 +244,7 @@ def _updateSpringLinear(prop, context): def _updateSpringAngular(prop, context): obj = prop.id_data rbc = obj.rigid_body_constraint - if rbc and hasattr(rbc, 'use_spring_ang_x'): + if rbc and hasattr(rbc, "use_spring_ang_x"): rbc.spring_stiffness_ang_x = prop.spring_angular[0] rbc.spring_stiffness_ang_y = prop.spring_angular[1] rbc.spring_stiffness_ang_z = prop.spring_angular[2] @@ -240,21 +252,21 @@ def _updateSpringAngular(prop, context): class MMDJoint(bpy.types.PropertyGroup): name_j: bpy.props.StringProperty( - name='Name', - description='Japanese Name', - default='', + name="Name", + description="Japanese Name", + default="", ) name_e: bpy.props.StringProperty( - name='Name(Eng)', - description='English Name', - default='', + name="Name(Eng)", + description="English Name", + default="", ) spring_linear: bpy.props.FloatVectorProperty( - name='Spring(Linear)', - description='Spring constant of movement', - subtype='XYZ', + name="Spring(Linear)", + description="Spring constant of movement", + subtype="XYZ", size=3, min=0, step=0.1, @@ -262,11 +274,19 @@ class MMDJoint(bpy.types.PropertyGroup): ) spring_angular: bpy.props.FloatVectorProperty( - name='Spring(Angular)', - description='Spring constant of rotation', - subtype='XYZ', + name="Spring(Angular)", + description="Spring constant of rotation", + subtype="XYZ", size=3, min=0, step=0.1, update=_updateSpringAngular, ) + + @staticmethod + def register(): + bpy.types.Object.mmd_joint = patch_library_overridable(bpy.props.PointerProperty(type=MMDJoint)) + + @staticmethod + def unregister(): + del bpy.types.Object.mmd_joint diff --git a/mmd_tools/properties/root.py b/mmd_tools/properties/root.py index 0021ccd3..6cc074dd 100644 --- a/mmd_tools/properties/root.py +++ b/mmd_tools/properties/root.py @@ -1,19 +1,23 @@ # -*- coding: utf-8 -*- -""" MMDモデルパラメータ用Prop -""" +# Copyright 2014 MMD Tools authors +# This file is part of MMD Tools. + +"""Properties for MMD model root object""" + import bpy -import mmd_tools.core.model as mmd_model -from mmd_tools import utils -from mmd_tools.bpyutils import SceneOp, activate_layer_collection -from mmd_tools.core.material import FnMaterial -from mmd_tools.core.sdef import FnSDEF -from mmd_tools.properties.morph import (BoneMorph, GroupMorph, MaterialMorph, - UVMorph, VertexMorph) -from mmd_tools.properties.translations import MMDTranslation - - -def __driver_variables(id_data, path, index=-1): - d = id_data.driver_add(path, index) + +from .. import utils +from ..bpyutils import FnContext +from ..core.material import FnMaterial +from ..core.model import FnModel +from ..core.sdef import FnSDEF +from . import patch_library_overridable +from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph +from .translations import MMDTranslation + + +def __driver_variables(constraint: bpy.types.Constraint, path: str, index=-1): + d = constraint.driver_add(path, index) variables = d.driver.variables for x in variables: variables.remove(x) @@ -23,208 +27,203 @@ def __driver_variables(id_data, path, index=-1): def __add_single_prop(variables, id_obj, data_path, prefix): var = variables.new() var.name = prefix + str(len(variables)) - var.type = 'SINGLE_PROP' + var.type = "SINGLE_PROP" target = var.targets[0] - target.id_type = 'OBJECT' + target.id_type = "OBJECT" target.id = id_obj target.data_path = data_path return var -def _toggleUsePropertyDriver(self, context): - root = self.id_data - rig = mmd_model.Model(root) - bones = getattr((rig.armature() or root).pose, 'bones', ()) - ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == 'IK' and c.is_valid and c.subtarget in bones} - prop_hide_viewport = 'hide_viewport' if hasattr(root, 'hide_viewport') else 'hide' +def _toggleUsePropertyDriver(self: "MMDRoot", _context): + root_object: bpy.types.Object = self.id_data + armature_object = FnModel.find_armature_object(root_object) + + if armature_object is None: + ik_map = {} + else: + bones = armature_object.pose.bones + ik_map = {bones[c.subtarget]: (b, c) for b in bones for c in b.constraints if c.type == "IK" and c.is_valid and c.subtarget in bones} + if self.use_property_driver: for ik, (b, c) in ik_map.items(): - driver, variables = __driver_variables(c, 'influence') - driver.expression = '%s' % __add_single_prop(variables, ik.id_data, ik.path_from_id('mmd_ik_toggle'), 'use_ik').name + driver, variables = __driver_variables(c, "influence") + driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name b = b if c.use_tail else b.parent - for b in ([b]+b.parent_recursive)[:c.chain_count]: - c = next((c for c in b.constraints if c.type == 'LIMIT_ROTATION' and not c.mute), None) + for b in ([b] + b.parent_recursive)[: c.chain_count]: + c = next((c for c in b.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None) if c: - driver, variables = __driver_variables(c, 'influence') - driver.expression = '%s' % __add_single_prop(variables, ik.id_data, ik.path_from_id('mmd_ik_toggle'), 'use_ik').name - for i in rig.meshes(): - for prop_hide in (prop_hide_viewport, 'hide_render'): + driver, variables = __driver_variables(c, "influence") + driver.expression = "%s" % __add_single_prop(variables, ik.id_data, ik.path_from_id("mmd_ik_toggle"), "use_ik").name + for i in FnModel.iterate_mesh_objects(root_object): + for prop_hide in ("hide_viewport", "hide_render"): driver, variables = __driver_variables(i, prop_hide) - driver.expression = 'not %s' % __add_single_prop(variables, root, 'mmd_root.show_meshes', 'show').name + driver.expression = "not %s" % __add_single_prop(variables, root_object, "mmd_root.show_meshes", "show").name else: for ik, (b, c) in ik_map.items(): - c.driver_remove('influence') + c.driver_remove("influence") b = b if c.use_tail else b.parent - for b in ([b]+b.parent_recursive)[:c.chain_count]: - c = next((c for c in b.constraints if c.type == 'LIMIT_ROTATION' and not c.mute), None) + for b in ([b] + b.parent_recursive)[: c.chain_count]: + c = next((c for c in b.constraints if c.type == "LIMIT_ROTATION" and not c.mute), None) if c: - c.driver_remove('influence') - for i in rig.meshes(): - for prop_hide in (prop_hide_viewport, 'hide_render'): + c.driver_remove("influence") + for i in FnModel.iterate_mesh_objects(root_object): + for prop_hide in ("hide_viewport", "hide_render"): i.driver_remove(prop_hide) + # =========================================== # Callback functions # =========================================== -def _toggleUseToonTexture(self, context): - root = self.id_data +def _toggleUseToonTexture(self: "MMDRoot", _context): use_toon = self.use_toon_texture - for i in mmd_model.Model(root).meshes(): + for i in FnModel.iterate_mesh_objects(self.id_data): for m in i.data.materials: if m: FnMaterial(m).use_toon_texture(use_toon) -def _toggleUseSphereTexture(self, context): - root = self.id_data +def _toggleUseSphereTexture(self: "MMDRoot", _context): use_sphere = self.use_sphere_texture - for i in mmd_model.Model(root).meshes(): + for i in FnModel.iterate_mesh_objects(self.id_data): for m in i.data.materials: if m: FnMaterial(m).use_sphere_texture(use_sphere, i) -def _toggleUseSDEF(self, context): - root = self.id_data +def _toggleUseSDEF(self: "MMDRoot", _context): mute_sdef = not self.use_sdef - for i in mmd_model.Model(root).meshes(): + for i in FnModel.iterate_mesh_objects(self.id_data): FnSDEF.mute_sdef_set(i, mute_sdef) -def _toggleVisibilityOfMeshes(self, context): +def _toggleVisibilityOfMeshes(self: "MMDRoot", context: bpy.types.Context): root = self.id_data hide = not self.show_meshes - for i in mmd_model.Model(root).meshes(): - i.hide = i.hide_render = hide + for i in FnModel.iterate_mesh_objects(self.id_data): + i.hide_set(hide) + i.hide_render = hide if hide and context.active_object is None: - SceneOp(context).active_object = root + FnContext.set_active_object(context, root) -def _show_meshes_get(prop): - return prop.get('show_meshes', True) - - -def _show_meshes_set(prop, v): - if v != prop.get('show_meshes', None): - prop['show_meshes'] = v - _toggleVisibilityOfMeshes(prop, bpy.context) - - -def _toggleVisibilityOfRigidBodies(self, context): +def _toggleVisibilityOfRigidBodies(self: "MMDRoot", context: bpy.types.Context): root = self.id_data hide = not self.show_rigid_bodies - for i in mmd_model.Model(root).rigidBodies(): - i.hide = hide + for i in FnModel.iterate_rigid_body_objects(root): + i.hide_set(hide) if hide and context.active_object is None: - SceneOp(context).active_object = root + FnContext.set_active_object(context, root) -def _toggleVisibilityOfJoints(self, context): - root = self.id_data +def _toggleVisibilityOfJoints(self: "MMDRoot", context): + root_object = self.id_data hide = not self.show_joints - for i in mmd_model.Model(root).joints(): - i.hide = hide + for i in FnModel.iterate_joint_objects(root_object): + i.hide_set(hide) if hide and context.active_object is None: - SceneOp(context).active_object = root + FnContext.set_active_object(context, root_object) -def _toggleVisibilityOfTemporaryObjects(self, context): - root = self.id_data +def _toggleVisibilityOfTemporaryObjects(self: "MMDRoot", context: bpy.types.Context): + root_object: bpy.types.Object = self.id_data hide = not self.show_temporary_objects - with activate_layer_collection(root): - for i in mmd_model.Model(root).temporaryObjects(): - i.hide = hide + with FnContext.temp_override_active_layer_collection(context, root_object): + for i in FnModel.iterate_temporary_objects(root_object): + i.hide_set(hide) if hide and context.active_object is None: - SceneOp(context).active_object = root + FnContext.set_active_object(context, root_object) -def _toggleShowNamesOfRigidBodies(self, context): +def _toggleShowNamesOfRigidBodies(self: "MMDRoot", _context): root = self.id_data - for i in mmd_model.Model(root).rigidBodies(): - i.show_name = root.mmd_root.show_names_of_rigid_bodies + show_names = root.mmd_root.show_names_of_rigid_bodies + for i in FnModel.iterate_rigid_body_objects(root): + i.show_name = show_names -def _toggleShowNamesOfJoints(self, context): +def _toggleShowNamesOfJoints(self: "MMDRoot", _context): root = self.id_data - for i in mmd_model.Model(root).joints(): - i.show_name = root.mmd_root.show_names_of_joints + show_names = root.mmd_root.show_names_of_joints + for i in FnModel.iterate_joint_objects(root): + i.show_name = show_names -def _setVisibilityOfMMDRigArmature(prop, v): +def _setVisibilityOfMMDRigArmature(prop: "MMDRoot", v: bool): root = prop.id_data - arm = mmd_model.FnModel.find_armature(root) - if arm: - if not v and bpy.context.active_object == arm: - SceneOp(bpy.context).active_object = root - arm.hide = not v + arm = FnModel.find_armature_object(root) + if arm is None: + return + if not v and bpy.context.active_object == arm: + FnContext.set_active_object(bpy.context, root) + arm.hide_set(not v) -def _getVisibilityOfMMDRigArmature(prop): - if prop.id_data.mmd_type != 'ROOT': +def _getVisibilityOfMMDRigArmature(prop: "MMDRoot"): + if prop.id_data.mmd_type != "ROOT": return False - arm = mmd_model.FnModel.find_armature(prop.id_data) - r = arm and not arm.hide - # Returning the non-BOOL type will raise a exception in the library override mode. - return bool(r) + arm = FnModel.find_armature_object(prop.id_data) + return arm and not arm.hide_get() -def _setActiveRigidbodyObject(prop, v): - obj = SceneOp(bpy.context).id_objects[v] - if mmd_model.isRigidBodyObject(obj): - utils.selectAObject(obj) - prop['active_rigidbody_object_index'] = v +def _setActiveRigidbodyObject(prop: "MMDRoot", v: int): + obj = FnContext.get_scene_objects(bpy.context)[v] + if FnModel.is_rigid_body_object(obj): + FnContext.set_active_and_select_single_object(bpy.context, obj) + prop["active_rigidbody_object_index"] = v -def _getActiveRigidbodyObject(prop): - scene = SceneOp(bpy.context) - active_obj = scene.active_object - if mmd_model.isRigidBodyObject(active_obj): - prop['active_rigidbody_object_index'] = scene.id_objects.find(active_obj.name) - return prop.get('active_rigidbody_object_index', 0) +def _getActiveRigidbodyObject(prop: "MMDRoot"): + context = bpy.context + active_obj = FnContext.get_active_object(context) + if FnModel.is_rigid_body_object(active_obj): + prop["active_rigidbody_object_index"] = FnContext.get_scene_objects(context).find(active_obj.name) + return prop.get("active_rigidbody_object_index", 0) -def _setActiveJointObject(prop, v): - obj = SceneOp(bpy.context).id_objects[v] - if mmd_model.isJointObject(obj): - utils.selectAObject(obj) - prop['active_joint_object_index'] = v +def _setActiveJointObject(prop: "MMDRoot", v: int): + obj = FnContext.get_scene_objects(bpy.context)[v] + if FnModel.is_joint_object(obj): + FnContext.set_active_and_select_single_object(bpy.context, obj) + prop["active_joint_object_index"] = v -def _getActiveJointObject(prop): - scene = SceneOp(bpy.context) - active_obj = scene.active_object - if mmd_model.isJointObject(active_obj): - prop['active_joint_object_index'] = scene.id_objects.find(active_obj.name) - return prop.get('active_joint_object_index', 0) +def _getActiveJointObject(prop: "MMDRoot"): + context = bpy.context + active_obj = FnContext.get_active_object(context) + if FnModel.is_joint_object(active_obj): + prop["active_joint_object_index"] = FnContext.get_scene_objects(context).find(active_obj.name) + return prop.get("active_joint_object_index", 0) -def _setActiveMorph(prop, v): - if 'active_morph_indices' not in prop: - prop['active_morph_indices'] = [0]*5 - prop['active_morph_indices'][prop.get('active_morph_type', 3)] = v +def _setActiveMorph(prop: "MMDRoot", v: bool): + if "active_morph_indices" not in prop: + prop["active_morph_indices"] = [0] * 5 + prop["active_morph_indices"][prop.get("active_morph_type", 3)] = v -def _getActiveMorph(prop): - if 'active_morph_indices' in prop: - return prop['active_morph_indices'][prop.get('active_morph_type', 3)] +def _getActiveMorph(prop: "MMDRoot"): + if "active_morph_indices" in prop: + return prop["active_morph_indices"][prop.get("active_morph_type", 3)] return 0 -def _setActiveMeshObject(prop, v): - obj = SceneOp(bpy.context).id_objects[v] - if obj.type == 'MESH' and obj.mmd_type == 'NONE': - utils.selectAObject(obj) - prop['active_mesh_index'] = v +def _setActiveMeshObject(prop: "MMDRoot", v: int): + obj = FnContext.get_scene_objects(bpy.context)[v] + if FnModel.is_mesh_object(obj): + FnContext.set_active_and_select_single_object(bpy.context, obj) + prop["active_mesh_index"] = v + +def _getActiveMeshObject(prop: "MMDRoot"): + context = bpy.context + active_obj = FnContext.get_active_object(context) + if FnModel.is_mesh_object(active_obj): + prop["active_mesh_index"] = FnContext.get_scene_objects(context).find(active_obj.name) + return prop.get("active_mesh_index", -1) -def _getActiveMeshObject(prop): - scene = SceneOp(bpy.context) - active_obj = scene.active_object - if active_obj and active_obj.type == 'MESH' and active_obj.mmd_type == 'NONE': - prop['active_mesh_index'] = scene.id_objects.find(active_obj.name) - return prop.get('active_mesh_index', -1) # =========================================== # Property classes @@ -232,96 +231,98 @@ def _getActiveMeshObject(prop): class MMDDisplayItem(bpy.types.PropertyGroup): - """ PMX 表示項目(表示枠内の1項目) - """ + """PMX 表示項目(表示枠内の1項目)""" + type: bpy.props.EnumProperty( - name='Type', - description='Select item type', + name="Type", + description="Select item type", items=[ - ('BONE', 'Bone', '', 1), - ('MORPH', 'Morph', '', 2), + ("BONE", "Bone", "", 1), + ("MORPH", "Morph", "", 2), ], ) morph_type: bpy.props.EnumProperty( - name='Morph Type', - description='Select morph type', + name="Morph Type", + description="Select morph type", items=[ - ('material_morphs', 'Material', 'Material Morphs', 0), - ('uv_morphs', 'UV', 'UV Morphs', 1), - ('bone_morphs', 'Bone', 'Bone Morphs', 2), - ('vertex_morphs', 'Vertex', 'Vertex Morphs', 3), - ('group_morphs', 'Group', 'Group Morphs', 4), + ("material_morphs", "Material", "Material Morphs", 0), + ("uv_morphs", "UV", "UV Morphs", 1), + ("bone_morphs", "Bone", "Bone Morphs", 2), + ("vertex_morphs", "Vertex", "Vertex Morphs", 3), + ("group_morphs", "Group", "Group Morphs", 4), ], - default='vertex_morphs', + default="vertex_morphs", ) class MMDDisplayItemFrame(bpy.types.PropertyGroup): - """ PMX 表示枠 + """PMX 表示枠 - PMXファイル内では表示枠がリストで格納されています。 + PMXファイル内では表示枠がリストで格納されています。 """ + name_e: bpy.props.StringProperty( - name='Name(Eng)', - description='English Name', - default='', + name="Name(Eng)", + description="English Name", + default="", ) # 特殊枠フラグ # 特殊枠はファイル仕様上の固定枠(削除、リネーム不可) is_special: bpy.props.BoolProperty( - name='Special', - description='Is special', + name="Special", + description="Is special", default=False, ) # 表示項目のリスト data: bpy.props.CollectionProperty( - name='Display Items', + name="Display Items", type=MMDDisplayItem, ) # 現在アクティブな項目のインデックス active_item: bpy.props.IntProperty( - name='Active Display Item', + name="Active Display Item", min=0, default=0, ) class MMDRoot(bpy.types.PropertyGroup): - """ MMDモデルデータ + """MMDモデルデータ - モデルルート用に作成されたEmtpyオブジェクトで使用します + モデルルート用に作成されたEmtpyオブジェクトで使用します """ + name: bpy.props.StringProperty( - name='Name', - description='The name of the MMD model', - default='', + name="Name", + description="The name of the MMD model", + default="", ) name_e: bpy.props.StringProperty( - name='Name (English)', - description='The english name of the MMD model', - default='', + name="Name (English)", + description="The english name of the MMD model", + default="", ) comment_text: bpy.props.StringProperty( - name='Comment', - description='The text datablock of the comment', - default='', + name="Comment", + description="The text datablock of the comment", + default="", ) comment_e_text: bpy.props.StringProperty( - name='Comment (English)', - description='The text datablock of the english comment', - default='', + name="Comment (English)", + description="The text datablock of the english comment", + default="", ) ik_loop_factor: bpy.props.IntProperty( - name='MMD IK Loop Factor', - description='Scaling factor of MMD IK loop', + name="MMD IK Loop Factor", + description="Scaling factor of MMD IK loop", min=1, soft_max=10, max=100, @@ -330,8 +331,8 @@ class MMDRoot(bpy.types.PropertyGroup): # TODO: Replace to driver for NLA show_meshes: bpy.props.BoolProperty( - name='Show Meshes', - description='Show all meshes of the MMD model', + name="Show Meshes", + description="Show all meshes of the MMD model", # get=_show_meshes_get, # set=_show_meshes_set, update=_toggleVisibilityOfMeshes, @@ -339,83 +340,83 @@ class MMDRoot(bpy.types.PropertyGroup): ) show_rigid_bodies: bpy.props.BoolProperty( - name='Show Rigid Bodies', - description='Show all rigid bodies of the MMD model', + name="Show Rigid Bodies", + description="Show all rigid bodies of the MMD model", update=_toggleVisibilityOfRigidBodies, ) show_joints: bpy.props.BoolProperty( - name='Show Joints', - description='Show all joints of the MMD model', + name="Show Joints", + description="Show all joints of the MMD model", update=_toggleVisibilityOfJoints, ) show_temporary_objects: bpy.props.BoolProperty( - name='Show Temps', - description='Show all temporary objects of the MMD model', + name="Show Temps", + description="Show all temporary objects of the MMD model", update=_toggleVisibilityOfTemporaryObjects, ) show_armature: bpy.props.BoolProperty( - name='Show Armature', - description='Show the armature object of the MMD model', + name="Show Armature", + description="Show the armature object of the MMD model", get=_getVisibilityOfMMDRigArmature, set=_setVisibilityOfMMDRigArmature, ) show_names_of_rigid_bodies: bpy.props.BoolProperty( - name='Show Rigid Body Names', - description='Show rigid body names', + name="Show Rigid Body Names", + description="Show rigid body names", update=_toggleShowNamesOfRigidBodies, ) show_names_of_joints: bpy.props.BoolProperty( - name='Show Joint Names', - description='Show joint names', + name="Show Joint Names", + description="Show joint names", update=_toggleShowNamesOfJoints, ) use_toon_texture: bpy.props.BoolProperty( - name='Use Toon Texture', - description='Use toon texture', + name="Use Toon Texture", + description="Use toon texture", update=_toggleUseToonTexture, default=True, ) use_sphere_texture: bpy.props.BoolProperty( - name='Use Sphere Texture', - description='Use sphere texture', + name="Use Sphere Texture", + description="Use sphere texture", update=_toggleUseSphereTexture, default=True, ) use_sdef: bpy.props.BoolProperty( - name='Use SDEF', - description='Use SDEF', + name="Use SDEF", + description="Use SDEF", update=_toggleUseSDEF, default=True, ) use_property_driver: bpy.props.BoolProperty( - name='Use Property Driver', - description='Setup drivers for MMD property animation (Visibility and IK toggles)', + name="Use Property Driver", + description="Setup drivers for MMD property animation (Visibility and IK toggles)", update=_toggleUsePropertyDriver, default=False, ) is_built: bpy.props.BoolProperty( - name='Is Built', + name="Is Built", ) active_rigidbody_index: bpy.props.IntProperty( - name='Active Rigidbody Index', + name="Active Rigidbody Index", min=0, get=_getActiveRigidbodyObject, set=_setActiveRigidbodyObject, ) active_joint_index: bpy.props.IntProperty( - name='Active Joint Index', + name="Active Joint Index", min=0, get=_getActiveJointObject, set=_setActiveJointObject, @@ -425,12 +426,12 @@ class MMDRoot(bpy.types.PropertyGroup): # Display Items # ************************* display_item_frames: bpy.props.CollectionProperty( - name='Display Frames', + name="Display Frames", type=MMDDisplayItemFrame, ) active_display_item_frame: bpy.props.IntProperty( - name='Active Display Item Frame', + name="Active Display Item Frame", min=0, default=0, ) @@ -439,50 +440,47 @@ class MMDRoot(bpy.types.PropertyGroup): # Morph # ************************* material_morphs: bpy.props.CollectionProperty( - name='Material Morphs', + name="Material Morphs", type=MaterialMorph, ) uv_morphs: bpy.props.CollectionProperty( - name='UV Morphs', + name="UV Morphs", type=UVMorph, ) bone_morphs: bpy.props.CollectionProperty( - name='Bone Morphs', + name="Bone Morphs", type=BoneMorph, ) - vertex_morphs: bpy.props.CollectionProperty( - name='Vertex Morphs', - type=VertexMorph - ) + vertex_morphs: bpy.props.CollectionProperty(name="Vertex Morphs", type=VertexMorph) group_morphs: bpy.props.CollectionProperty( - name='Group Morphs', + name="Group Morphs", type=GroupMorph, ) active_morph_type: bpy.props.EnumProperty( - name='Active Morph Type', - description='Select current morph type', + name="Active Morph Type", + description="Select current morph type", items=[ - ('material_morphs', 'Material', 'Material Morphs', 0), - ('uv_morphs', 'UV', 'UV Morphs', 1), - ('bone_morphs', 'Bone', 'Bone Morphs', 2), - ('vertex_morphs', 'Vertex', 'Vertex Morphs', 3), - ('group_morphs', 'Group', 'Group Morphs', 4), + ("material_morphs", "Material", "Material Morphs", 0), + ("uv_morphs", "UV", "UV Morphs", 1), + ("bone_morphs", "Bone", "Bone Morphs", 2), + ("vertex_morphs", "Vertex", "Vertex Morphs", 3), + ("group_morphs", "Group", "Group Morphs", 4), ], - default='vertex_morphs', + default="vertex_morphs", ) active_morph: bpy.props.IntProperty( - name='Active Morph', + name="Active Morph", min=0, set=_setActiveMorph, get=_getActiveMorph, ) morph_panel_show_settings: bpy.props.BoolProperty( - name='Morph Panel Show Settings', - description='Show Morph Settings', + name="Morph Panel Show Settings", + description="Show Morph Settings", default=True, ) active_mesh_index: bpy.props.IntProperty( - name='Active Mesh', + name="Active Mesh", min=0, set=_setActiveMeshObject, get=_getActiveMeshObject, @@ -492,6 +490,85 @@ class MMDRoot(bpy.types.PropertyGroup): # Translation # ************************* translation: bpy.props.PointerProperty( - name='Translation', + name="Translation", type=MMDTranslation, ) + + @staticmethod + def __get_select(prop: bpy.types.Object) -> bool: + utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_get() method instead") + return prop.select_get() + + @staticmethod + def __set_select(prop: bpy.types.Object, value: bool) -> None: + utils.warn_deprecation("Object.select", "v4.0.0", "Use Object.select_set() method instead") + prop.select_set(value) + + @staticmethod + def __get_hide(prop: bpy.types.Object) -> bool: + utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_get() method instead") + return prop.hide_get() + + @staticmethod + def __set_hide(prop: bpy.types.Object, value: bool) -> None: + utils.warn_deprecation("Object.hide", "v4.0.0", "Use Object.hide_set() method instead") + prop.hide_set(value) + if prop.hide_viewport != value: + prop.hide_viewport = value + + @staticmethod + def register(): + bpy.types.Object.mmd_type = patch_library_overridable( + bpy.props.EnumProperty( + name="Type", + description="Internal MMD type of this object (DO NOT CHANGE IT DIRECTLY)", + default="NONE", + items=[ + ("NONE", "None", "", 1), + ("ROOT", "Root", "", 2), + ("RIGID_GRP_OBJ", "Rigid Body Grp Empty", "", 3), + ("JOINT_GRP_OBJ", "Joint Grp Empty", "", 4), + ("TEMPORARY_GRP_OBJ", "Temporary Grp Empty", "", 5), + ("PLACEHOLDER", "Place Holder", "", 6), + ("CAMERA", "Camera", "", 21), + ("JOINT", "Joint", "", 22), + ("RIGID_BODY", "Rigid body", "", 23), + ("LIGHT", "Light", "", 24), + ("TRACK_TARGET", "Track Target", "", 51), + ("NON_COLLISION_CONSTRAINT", "Non Collision Constraint", "", 52), + ("SPRING_CONSTRAINT", "Spring Constraint", "", 53), + ("SPRING_GOAL", "Spring Goal", "", 54), + ], + ) + ) + bpy.types.Object.mmd_root = patch_library_overridable(bpy.props.PointerProperty(type=MMDRoot)) + + bpy.types.Object.select = patch_library_overridable( + bpy.props.BoolProperty( + get=MMDRoot.__get_select, + set=MMDRoot.__set_select, + options={ + "SKIP_SAVE", + "ANIMATABLE", + "LIBRARY_EDITABLE", + }, + ) + ) + bpy.types.Object.hide = patch_library_overridable( + bpy.props.BoolProperty( + get=MMDRoot.__get_hide, + set=MMDRoot.__set_hide, + options={ + "SKIP_SAVE", + "ANIMATABLE", + "LIBRARY_EDITABLE", + }, + ) + ) + + @staticmethod + def unregister(): + del bpy.types.Object.hide + del bpy.types.Object.select + del bpy.types.Object.mmd_root + del bpy.types.Object.mmd_type diff --git a/mmd_tools/properties/translations.py b/mmd_tools/properties/translations.py index abddb559..6f751ba4 100644 --- a/mmd_tools/properties/translations.py +++ b/mmd_tools/properties/translations.py @@ -1,19 +1,21 @@ # -*- coding: utf-8 -*- +# Copyright 2021 MMD Tools authors +# This file is part of MMD Tools. -from typing import Dict, List, Tuple +from typing import Dict, List, Optional, Tuple import bpy -from mmd_tools.core.translations import (FnTranslations, - MMDTranslationElementType) -from mmd_tools.translations import DictionaryEnum + +from ..core.translations import FnTranslations, MMDTranslationElementType +from ..translations import DictionaryEnum MMD_TRANSLATION_ELEMENT_TYPE_ENUM_ITEMS = [ - (MMDTranslationElementType.BONE.name, MMDTranslationElementType.BONE.value, 'Bones', 1), - (MMDTranslationElementType.MORPH.name, MMDTranslationElementType.MORPH.value, 'Morphs', 2), - (MMDTranslationElementType.MATERIAL.name, MMDTranslationElementType.MATERIAL.value, 'Materials', 4), - (MMDTranslationElementType.DISPLAY.name, MMDTranslationElementType.DISPLAY.value, 'Display frames', 8), - (MMDTranslationElementType.PHYSICS.name, MMDTranslationElementType.PHYSICS.value, 'Rigidbodies and joints', 16), - (MMDTranslationElementType.INFO.name, MMDTranslationElementType.INFO.value, 'Model name and comments', 32), + (MMDTranslationElementType.BONE.name, MMDTranslationElementType.BONE.value, "Bones", 1), + (MMDTranslationElementType.MORPH.name, MMDTranslationElementType.MORPH.value, "Morphs", 2), + (MMDTranslationElementType.MATERIAL.name, MMDTranslationElementType.MATERIAL.value, "Materials", 4), + (MMDTranslationElementType.DISPLAY.name, MMDTranslationElementType.DISPLAY.value, "Display frames", 8), + (MMDTranslationElementType.PHYSICS.name, MMDTranslationElementType.PHYSICS.value, "Rigidbodies and joints", 16), + (MMDTranslationElementType.INFO.name, MMDTranslationElementType.INFO.value, "Model name and comments", 32), ] @@ -30,41 +32,38 @@ class MMDTranslationElementIndex(bpy.types.PropertyGroup): value: bpy.props.IntProperty() -BATCH_OPERATION_SCRIPT_PRESETS: Dict[str, Tuple[str, str, str, int]] = { - 'NOTHING': ('', '', '', 1), - 'CLEAR': (None, 'Clear', '""', 10), - 'TO_ENGLISH': ('BLENDER', 'Translate to English', 'to_english(name)', 2), - 'TO_MMD_LR': ('JAPANESE', 'Blender L/R to MMD L/R', 'to_mmd_lr(name)', 3), - 'TO_BLENDER_LR': ('BLENDER', 'MMD L/R to Blender L/R', 'to_blender_lr(name_j)', 4), - 'RESTORE_BLENDER': ('BLENDER', 'Restore Blender Names', 'org_name', 5), - 'RESTORE_JAPANESE': ('JAPANESE', 'Restore Japanese MMD Names', 'org_name_j', 6), - 'RESTORE_ENGLISH': ('ENGLISH', 'Restore English MMD Names', 'org_name_e', 7), - 'ENGLISH_IF_EMPTY_JAPANESE': (None, 'Copy English MMD Names, if empty copy Japanese MMD Name', 'name_e if name_e else name_j', 8), - 'JAPANESE_IF_EMPTY_ENGLISH': (None, 'Copy Japanese MMD Names, if empty copy English MMD Name', 'name_j if name_j else name_e', 9), +BATCH_OPERATION_SCRIPT_PRESETS: Dict[str, Tuple[Optional[str], str, str, int]] = { + "NOTHING": ("", "", "", 1), + "CLEAR": (None, "Clear", '""', 10), + "TO_ENGLISH": ("BLENDER", "Translate to English", "to_english(name)", 2), + "TO_MMD_LR": ("JAPANESE", "Blender L/R to MMD L/R", "to_mmd_lr(name)", 3), + "TO_BLENDER_LR": ("BLENDER", "MMD L/R to Blender L/R", "to_blender_lr(name_j)", 4), + "RESTORE_BLENDER": ("BLENDER", "Restore Blender Names", "org_name", 5), + "RESTORE_JAPANESE": ("JAPANESE", "Restore Japanese MMD Names", "org_name_j", 6), + "RESTORE_ENGLISH": ("ENGLISH", "Restore English MMD Names", "org_name_e", 7), + "ENGLISH_IF_EMPTY_JAPANESE": (None, "Copy English MMD Names, if empty copy Japanese MMD Name", "name_e if name_e else name_j", 8), + "JAPANESE_IF_EMPTY_ENGLISH": (None, "Copy Japanese MMD Names, if empty copy English MMD Name", "name_j if name_j else name_e", 9), } -BATCH_OPERATION_SCRIPT_PRESET_ITEMS: List[Tuple[str, str, str, int]] = [ - (k, t[1], t[2], t[3]) - for k, t in BATCH_OPERATION_SCRIPT_PRESETS.items() -] +BATCH_OPERATION_SCRIPT_PRESET_ITEMS: List[Tuple[str, str, str, int]] = [(k, t[1], t[2], t[3]) for k, t in BATCH_OPERATION_SCRIPT_PRESETS.items()] class MMDTranslation(bpy.types.PropertyGroup): @staticmethod - def _update_index(mmd_translation: 'MMDTranslation', _context): + def _update_index(mmd_translation: "MMDTranslation", _context): FnTranslations.update_index(mmd_translation) @staticmethod - def _collect_data(mmd_translation: 'MMDTranslation', _context): + def _collect_data(mmd_translation: "MMDTranslation", _context): FnTranslations.collect_data(mmd_translation) @staticmethod - def _update_query(mmd_translation: 'MMDTranslation', _context): + def _update_query(mmd_translation: "MMDTranslation", _context): FnTranslations.update_query(mmd_translation) @staticmethod - def _update_batch_operation_script_preset(mmd_translation: 'MMDTranslation', _context): - if mmd_translation.batch_operation_script_preset == 'NOTHING': + def _update_batch_operation_script_preset(mmd_translation: "MMDTranslation", _context): + if mmd_translation.batch_operation_script_preset == "NOTHING": return id2scripts: Dict[str, str] = {i[0]: i[2] for i in BATCH_OPERATION_SCRIPT_PRESET_ITEMS} @@ -82,37 +81,43 @@ def _update_batch_operation_script_preset(mmd_translation: 'MMDTranslation', _co filtered_translation_element_indices_active_index: bpy.props.IntProperty(update=_update_index.__func__) filtered_translation_element_indices: bpy.props.CollectionProperty(type=MMDTranslationElementIndex) - filter_japanese_blank: bpy.props.BoolProperty(name='Japanese Blank', default=False, update=_update_query.__func__) - filter_english_blank: bpy.props.BoolProperty(name='English Blank', default=False, update=_update_query.__func__) - filter_restorable: bpy.props.BoolProperty(name='Restorable', default=False, update=_update_query.__func__) - filter_selected: bpy.props.BoolProperty(name='Selected', default=False, update=_update_query.__func__) - filter_visible: bpy.props.BoolProperty(name='Visible', default=False, update=_update_query.__func__) + filter_japanese_blank: bpy.props.BoolProperty(name="Japanese Blank", default=False, update=_update_query.__func__) + filter_english_blank: bpy.props.BoolProperty(name="English Blank", default=False, update=_update_query.__func__) + filter_restorable: bpy.props.BoolProperty(name="Restorable", default=False, update=_update_query.__func__) + filter_selected: bpy.props.BoolProperty(name="Selected", default=False, update=_update_query.__func__) + filter_visible: bpy.props.BoolProperty(name="Visible", default=False, update=_update_query.__func__) filter_types: bpy.props.EnumProperty( items=MMD_TRANSLATION_ELEMENT_TYPE_ENUM_ITEMS, - default={'BONE', 'MORPH', 'MATERIAL', 'DISPLAY', 'PHYSICS', }, - options={'ENUM_FLAG'}, + default={ + "BONE", + "MORPH", + "MATERIAL", + "DISPLAY", + "PHYSICS", + }, + options={"ENUM_FLAG"}, update=_update_query.__func__, ) dictionary: bpy.props.EnumProperty( items=DictionaryEnum.get_dictionary_items, - name='Dictionary', + name="Dictionary", ) batch_operation_target: bpy.props.EnumProperty( items=[ - ('BLENDER', 'Blender Name (name)', '', 1), - ('JAPANESE', 'Japanese MMD Name (name_j)', '', 2), - ('ENGLISH', 'English MMD Name (name_e)', '', 3), + ("BLENDER", "Blender Name (name)", "", 1), + ("JAPANESE", "Japanese MMD Name (name_j)", "", 2), + ("ENGLISH", "English MMD Name (name_e)", "", 3), ], - name='Operation Target', - default='JAPANESE', + name="Operation Target", + default="JAPANESE", ) batch_operation_script_preset: bpy.props.EnumProperty( items=BATCH_OPERATION_SCRIPT_PRESET_ITEMS, - name='Operation Script Preset', - default='NOTHING', + name="Operation Script Preset", + default="NOTHING", update=_update_batch_operation_script_preset.__func__, ) diff --git a/mmd_tools/translations.py b/mmd_tools/translations.py index 07b4c437..c7eebdc3 100644 --- a/mmd_tools/translations.py +++ b/mmd_tools/translations.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# Copyright 2016 MMD Tools authors +# This file is part of MMD Tools. import csv import logging @@ -6,157 +8,287 @@ import bpy +from .bpyutils import FnContext + jp_half_to_full_tuples = ( - ('ヴ', 'ヴ'), ('ガ', 'ガ'), ('ギ', 'ギ'), ('グ', 'グ'), ('ゲ', 'ゲ'), - ('ゴ', 'ゴ'), ('ザ', 'ザ'), ('ジ', 'ジ'), ('ズ', 'ズ'), ('ゼ', 'ゼ'), - ('ゾ', 'ゾ'), ('ダ', 'ダ'), ('ヂ', 'ヂ'), ('ヅ', 'ヅ'), ('デ', 'デ'), - ('ド', 'ド'), ('バ', 'バ'), ('パ', 'パ'), ('ビ', 'ビ'), ('ピ', 'ピ'), - ('ブ', 'ブ'), ('プ', 'プ'), ('ベ', 'ベ'), ('ペ', 'ペ'), ('ボ', 'ボ'), - ('ポ', 'ポ'), ('。', '。'), ('「', '「'), ('」', '」'), ('、', '、'), - ('・', '・'), ('ヲ', 'ヲ'), ('ァ', 'ァ'), ('ィ', 'ィ'), ('ゥ', 'ゥ'), - ('ェ', 'ェ'), ('ォ', 'ォ'), ('ャ', 'ャ'), ('ュ', 'ュ'), ('ョ', 'ョ'), - ('ッ', 'ッ'), ('ー', 'ー'), ('ア', 'ア'), ('イ', 'イ'), ('ウ', 'ウ'), - ('エ', 'エ'), ('オ', 'オ'), ('カ', 'カ'), ('キ', 'キ'), ('ク', 'ク'), - ('ケ', 'ケ'), ('コ', 'コ'), ('サ', 'サ'), ('シ', 'シ'), ('ス', 'ス'), - ('セ', 'セ'), ('ソ', 'ソ'), ('タ', 'タ'), ('チ', 'チ'), ('ツ', 'ツ'), - ('テ', 'テ'), ('ト', 'ト'), ('ナ', 'ナ'), ('ニ', 'ニ'), ('ヌ', 'ヌ'), - ('ネ', 'ネ'), ('ノ', 'ノ'), ('ハ', 'ハ'), ('ヒ', 'ヒ'), ('フ', 'フ'), - ('ヘ', 'ヘ'), ('ホ', 'ホ'), ('マ', 'マ'), ('ミ', 'ミ'), ('ム', 'ム'), - ('メ', 'メ'), ('モ', 'モ'), ('ヤ', 'ヤ'), ('ユ', 'ユ'), ('ヨ', 'ヨ'), - ('ラ', 'ラ'), ('リ', 'リ'), ('ル', 'ル'), ('レ', 'レ'), ('ロ', 'ロ'), - ('ワ', 'ワ'), ('ン', 'ン'), - ) + ("ヴ", "ヴ"), + ("ガ", "ガ"), + ("ギ", "ギ"), + ("グ", "グ"), + ("ゲ", "ゲ"), + ("ゴ", "ゴ"), + ("ザ", "ザ"), + ("ジ", "ジ"), + ("ズ", "ズ"), + ("ゼ", "ゼ"), + ("ゾ", "ゾ"), + ("ダ", "ダ"), + ("ヂ", "ヂ"), + ("ヅ", "ヅ"), + ("デ", "デ"), + ("ド", "ド"), + ("バ", "バ"), + ("パ", "パ"), + ("ビ", "ビ"), + ("ピ", "ピ"), + ("ブ", "ブ"), + ("プ", "プ"), + ("ベ", "ベ"), + ("ペ", "ペ"), + ("ボ", "ボ"), + ("ポ", "ポ"), + ("。", "。"), + ("「", "「"), + ("」", "」"), + ("、", "、"), + ("・", "・"), + ("ヲ", "ヲ"), + ("ァ", "ァ"), + ("ィ", "ィ"), + ("ゥ", "ゥ"), + ("ェ", "ェ"), + ("ォ", "ォ"), + ("ャ", "ャ"), + ("ュ", "ュ"), + ("ョ", "ョ"), + ("ッ", "ッ"), + ("ー", "ー"), + ("ア", "ア"), + ("イ", "イ"), + ("ウ", "ウ"), + ("エ", "エ"), + ("オ", "オ"), + ("カ", "カ"), + ("キ", "キ"), + ("ク", "ク"), + ("ケ", "ケ"), + ("コ", "コ"), + ("サ", "サ"), + ("シ", "シ"), + ("ス", "ス"), + ("セ", "セ"), + ("ソ", "ソ"), + ("タ", "タ"), + ("チ", "チ"), + ("ツ", "ツ"), + ("テ", "テ"), + ("ト", "ト"), + ("ナ", "ナ"), + ("ニ", "ニ"), + ("ヌ", "ヌ"), + ("ネ", "ネ"), + ("ノ", "ノ"), + ("ハ", "ハ"), + ("ヒ", "ヒ"), + ("フ", "フ"), + ("ヘ", "ヘ"), + ("ホ", "ホ"), + ("マ", "マ"), + ("ミ", "ミ"), + ("ム", "ム"), + ("メ", "メ"), + ("モ", "モ"), + ("ヤ", "ヤ"), + ("ユ", "ユ"), + ("ヨ", "ヨ"), + ("ラ", "ラ"), + ("リ", "リ"), + ("ル", "ル"), + ("レ", "レ"), + ("ロ", "ロ"), + ("ワ", "ワ"), + ("ン", "ン"), +) jp_to_en_tuples = [ - ('全ての親', 'ParentNode'), - ('操作中心', 'ControlNode'), - ('センター', 'Center'), - ('センター', 'Center'), - ('グループ', 'Group'), - ('グルーブ', 'Groove'), - ('キャンセル', 'Cancel'), - ('上半身', 'UpperBody'), - ('下半身', 'LowerBody'), - ('手首', 'Wrist'), - ('足首', 'Ankle'), - ('首', 'Neck'), - ('頭', 'Head'), - ('顔', 'Face'), - ('下顎', 'Chin'), - ('下あご', 'Chin'), - ('あご', 'Jaw'), - ('顎', 'Jaw'), - ('両目', 'Eyes'), - ('目', 'Eye'), - ('眉', 'Eyebrow'), - ('舌', 'Tongue'), - ('涙', 'Tears'), - ('泣き', 'Cry'), - ('歯', 'Teeth'), - ('照れ', 'Blush'), - ('青ざめ', 'Pale'), - ('ガーン', 'Gloom'), - ('汗', 'Sweat'), - ('怒', 'Anger'), - ('感情', 'Emotion'), - ('符', 'Marks'), - ('暗い', 'Dark'), - ('腰', 'Waist'), - ('髪', 'Hair'), - ('三つ編み', 'Braid'), - ('胸', 'Breast'), - ('乳', 'Boob'), - ('おっぱい', 'Tits'), - ('筋', 'Muscle'), - ('腹', 'Belly'), - ('鎖骨', 'Clavicle'), - ('肩', 'Shoulder'), - ('腕', 'Arm'), - ('うで', 'Arm'), - ('ひじ', 'Elbow'), - ('肘', 'Elbow'), - ('手', 'Hand'), - ('親指', 'Thumb'), - ('人指', 'IndexFinger'), - ('人差指', 'IndexFinger'), - ('中指', 'MiddleFinger'), - ('薬指', 'RingFinger'), - ('小指', 'LittleFinger'), - ('足', 'Leg'), - ('ひざ', 'Knee'), - ('つま', 'Toe'), - ('袖', 'Sleeve'), - ('新規', 'New'), - ('ボーン', 'Bone'), - ('捩', 'Twist'), - ('回転', 'Rotation'), - ('軸', 'Axis'), - ('ネクタイ', 'Necktie'), - ('ネクタイ', 'Necktie'), - ('ヘッドセット', 'Headset'), - ('飾り', 'Accessory'), - ('リボン', 'Ribbon'), - ('襟', 'Collar'), - ('紐', 'String'), - ('コード', 'Cord'), - ('イヤリング', 'Earring'), - ('メガネ', 'Eyeglasses'), - ('眼鏡', 'Glasses'), - ('帽子', 'Hat'), - ('スカート', 'Skirt'), - ('スカート', 'Skirt'), - ('パンツ', 'Pantsu'), - ('シャツ', 'Shirt'), - ('フリル', 'Frill'), - ('マフラー', 'Muffler'), - ('マフラー', 'Muffler'), - ('服', 'Clothes'), - ('ブーツ', 'Boots'), - ('ねこみみ', 'CatEars'), - ('ジップ', 'Zip'), - ('ジップ', 'Zip'), - ('ダミー', 'Dummy'), - ('ダミー', 'Dummy'), - ('基', 'Category'), - ('あほ毛', 'Antenna'), - ('アホ毛', 'Antenna'), - ('モミアゲ', 'Sideburn'), - ('もみあげ', 'Sideburn'), - ('ツインテ', 'Twintail'), - ('おさげ', 'Pigtail'), - ('ひらひら', 'Flutter'), - ('調整', 'Adjustment'), - ('補助', 'Aux'), - ('右', 'Right'), - ('左', 'Left'), - ('前', 'Front'), - ('後ろ', 'Behind'), - ('後', 'Back'), - ('横', 'Side'), - ('中', 'Middle'), - ('上', 'Upper'), - ('下', 'Lower'), - ('親', 'Parent'), - ('先', 'Tip'), - ('パーツ', 'Part'), - ('光', 'Light'), - ('戻', 'Return'), - ('羽', 'Wing'), - ('根', 'Base'), # ideally 'Root' but to avoid confusion - ('毛', 'Strand'), - ('尾', 'Tail'), - ('尻', 'Butt'), - # full-width unicode forms I think: https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms - ('0', '0'), ('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('6', '6'), ('7', '7'), ('8', '8'), ('9', '9'), - ('a', 'a'), ('b', 'b'), ('c', 'c'), ('d', 'd'), ('e', 'e'), ('f', 'f'), ('g', 'g'), ('h', 'h'), ('i', 'i'), ('j', 'j'), - ('k', 'k'), ('l', 'l'), ('m', 'm'), ('n', 'n'), ('o', 'o'), ('p', 'p'), ('q', 'q'), ('r', 'r'), ('s', 's'), ('t', 't'), - ('u', 'u'), ('v', 'v'), ('w', 'w'), ('x', 'x'), ('y', 'y'), ('z', 'z'), - ('A', 'A'), ('B', 'B'), ('C', 'C'), ('D', 'D'), ('E', 'E'), ('F', 'F'), ('G', 'G'), ('H', 'H'), ('I', 'I'), ('J', 'J'), - ('K', 'K'), ('L', 'L'), ('M', 'M'), ('N', 'N'), ('O', 'O'), ('P', 'P'), ('Q', 'Q'), ('R', 'R'), ('S', 'S'), ('T', 'T'), - ('U', 'U'), ('V', 'V'), ('W', 'W'), ('X', 'X'), ('Y', 'Y'), ('Z', 'Z'), - ('+', '+'), ('-', '-'), ('_', '_'), ('/', '/'), - ('.', '_'), # probably should be combined with the global 'use underscore' option - ] + ("全ての親", "ParentNode"), + ("操作中心", "ControlNode"), + ("センター", "Center"), + ("センター", "Center"), + ("グループ", "Group"), + ("グルーブ", "Groove"), + ("キャンセル", "Cancel"), + ("上半身", "UpperBody"), + ("下半身", "LowerBody"), + ("手首", "Wrist"), + ("足首", "Ankle"), + ("首", "Neck"), + ("頭", "Head"), + ("顔", "Face"), + ("下顎", "Chin"), + ("下あご", "Chin"), + ("あご", "Jaw"), + ("顎", "Jaw"), + ("両目", "Eyes"), + ("目", "Eye"), + ("眉", "Eyebrow"), + ("舌", "Tongue"), + ("涙", "Tears"), + ("泣き", "Cry"), + ("歯", "Teeth"), + ("照れ", "Blush"), + ("青ざめ", "Pale"), + ("ガーン", "Gloom"), + ("汗", "Sweat"), + ("怒", "Anger"), + ("感情", "Emotion"), + ("符", "Marks"), + ("暗い", "Dark"), + ("腰", "Waist"), + ("髪", "Hair"), + ("三つ編み", "Braid"), + ("胸", "Breast"), + ("乳", "Boob"), + ("おっぱい", "Tits"), + ("筋", "Muscle"), + ("腹", "Belly"), + ("鎖骨", "Clavicle"), + ("肩", "Shoulder"), + ("腕", "Arm"), + ("うで", "Arm"), + ("ひじ", "Elbow"), + ("肘", "Elbow"), + ("手", "Hand"), + ("親指", "Thumb"), + ("人指", "IndexFinger"), + ("人差指", "IndexFinger"), + ("中指", "MiddleFinger"), + ("薬指", "RingFinger"), + ("小指", "LittleFinger"), + ("足", "Leg"), + ("ひざ", "Knee"), + ("つま", "Toe"), + ("袖", "Sleeve"), + ("新規", "New"), + ("ボーン", "Bone"), + ("捩", "Twist"), + ("回転", "Rotation"), + ("軸", "Axis"), + ("ネクタイ", "Necktie"), + ("ネクタイ", "Necktie"), + ("ヘッドセット", "Headset"), + ("飾り", "Accessory"), + ("リボン", "Ribbon"), + ("襟", "Collar"), + ("紐", "String"), + ("コード", "Cord"), + ("イヤリング", "Earring"), + ("メガネ", "Eyeglasses"), + ("眼鏡", "Glasses"), + ("帽子", "Hat"), + ("スカート", "Skirt"), + ("スカート", "Skirt"), + ("パンツ", "Pantsu"), + ("シャツ", "Shirt"), + ("フリル", "Frill"), + ("マフラー", "Muffler"), + ("マフラー", "Muffler"), + ("服", "Clothes"), + ("ブーツ", "Boots"), + ("ねこみみ", "CatEars"), + ("ジップ", "Zip"), + ("ジップ", "Zip"), + ("ダミー", "Dummy"), + ("ダミー", "Dummy"), + ("基", "Category"), + ("あほ毛", "Antenna"), + ("アホ毛", "Antenna"), + ("モミアゲ", "Sideburn"), + ("もみあげ", "Sideburn"), + ("ツインテ", "Twintail"), + ("おさげ", "Pigtail"), + ("ひらひら", "Flutter"), + ("調整", "Adjustment"), + ("補助", "Aux"), + ("右", "Right"), + ("左", "Left"), + ("前", "Front"), + ("後ろ", "Behind"), + ("後", "Back"), + ("横", "Side"), + ("中", "Middle"), + ("上", "Upper"), + ("下", "Lower"), + ("親", "Parent"), + ("先", "Tip"), + ("パーツ", "Part"), + ("光", "Light"), + ("戻", "Return"), + ("羽", "Wing"), + ("根", "Base"), # ideally 'Root' but to avoid confusion + ("毛", "Strand"), + ("尾", "Tail"), + ("尻", "Butt"), + # full-width unicode forms I think: https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms + ("0", "0"), + ("1", "1"), + ("2", "2"), + ("3", "3"), + ("4", "4"), + ("5", "5"), + ("6", "6"), + ("7", "7"), + ("8", "8"), + ("9", "9"), + ("a", "a"), + ("b", "b"), + ("c", "c"), + ("d", "d"), + ("e", "e"), + ("f", "f"), + ("g", "g"), + ("h", "h"), + ("i", "i"), + ("j", "j"), + ("k", "k"), + ("l", "l"), + ("m", "m"), + ("n", "n"), + ("o", "o"), + ("p", "p"), + ("q", "q"), + ("r", "r"), + ("s", "s"), + ("t", "t"), + ("u", "u"), + ("v", "v"), + ("w", "w"), + ("x", "x"), + ("y", "y"), + ("z", "z"), + ("A", "A"), + ("B", "B"), + ("C", "C"), + ("D", "D"), + ("E", "E"), + ("F", "F"), + ("G", "G"), + ("H", "H"), + ("I", "I"), + ("J", "J"), + ("K", "K"), + ("L", "L"), + ("M", "M"), + ("N", "N"), + ("O", "O"), + ("P", "P"), + ("Q", "Q"), + ("R", "R"), + ("S", "S"), + ("T", "T"), + ("U", "U"), + ("V", "V"), + ("W", "W"), + ("X", "X"), + ("Y", "Y"), + ("Z", "Z"), + ("+", "+"), + ("-", "-"), + ("_", "_"), + ("/", "/"), + (".", "_"), # probably should be combined with the global 'use underscore' option +] + def translateFromJp(name): for tuple in jp_to_en_tuples: @@ -165,7 +297,7 @@ def translateFromJp(name): return name -def getTranslator(csvfile='', keep_order=False): +def getTranslator(csvfile="", keep_order=False): translator = MMDTranslator() if isinstance(csvfile, bpy.types.Text): translator.load_from_stream(csvfile) @@ -181,15 +313,15 @@ def getTranslator(csvfile='', keep_order=False): translator.update() return translator -class MMDTranslator: +class MMDTranslator: def __init__(self): self.__csv_tuples = [] self.__fails = {} @staticmethod def default_csv_filepath(): - return __file__[:-3]+'.csv' + return __file__[:-3] + ".csv" @staticmethod def get_csv_text(text_name=None): @@ -219,18 +351,19 @@ def sort(self): def update(self): from collections import OrderedDict + count_old = len(self.__csv_tuples) tuples_dict = OrderedDict((row[0], row) for row in self.__csv_tuples if len(row) >= 2 and row[0]) self.__csv_tuples.clear() self.__csv_tuples.extend(tuples_dict.values()) - logging.info(' - removed items:\t%d\t(of %d)', count_old-len(self.__csv_tuples), count_old) + logging.info(" - removed items:\t%d\t(of %d)", count_old - len(self.__csv_tuples), count_old) def half_to_full(self, name): return self.replace_from_tuples(name, jp_half_to_full_tuples) def is_translated(self, name): try: - name.encode('ascii', errors='strict') + name.encode("ascii", errors="strict") except UnicodeEncodeError: return False return True @@ -245,42 +378,42 @@ def translate(self, name, default=None, from_full_width=True): return name_new def save_fails(self, text_name=None): - text_name = text_name or (__name__+'.fails') + text_name = text_name or (__name__ + ".fails") txt = self.get_csv_text(text_name) fmt = '"%s","%s"' items = sorted(self.__fails.items(), key=lambda row: (-len(row[0]), row)) - txt.from_string('\n'.join(fmt%(k, v) for k, v in items)) + txt.from_string("\n".join(fmt % (k, v) for k, v in items)) return txt def load_from_stream(self, csvfile=None): csvfile = csvfile or self.get_csv_text() if isinstance(csvfile, bpy.types.Text): - csvfile = (l.body+'\n' for l in csvfile.lines) - spamreader = csv.reader(csvfile, delimiter=',', skipinitialspace=True) + csvfile = (l.body + "\n" for l in csvfile.lines) + spamreader = csv.reader(csvfile, delimiter=",", skipinitialspace=True) csv_tuples = [tuple(row) for row in spamreader if len(row) >= 2] self.__csv_tuples = csv_tuples - logging.info(' - load items:\t%d', len(self.__csv_tuples)) + logging.info(" - load items:\t%d", len(self.__csv_tuples)) def save_to_stream(self, csvfile=None): csvfile = csvfile or self.get_csv_text() - lineterminator = '\r\n' + lineterminator = "\r\n" if isinstance(csvfile, bpy.types.Text): csvfile.clear() - lineterminator = '\n' - spamwriter = csv.writer(csvfile, delimiter=',', lineterminator=lineterminator, quoting=csv.QUOTE_ALL) + lineterminator = "\n" + spamwriter = csv.writer(csvfile, delimiter=",", lineterminator=lineterminator, quoting=csv.QUOTE_ALL) spamwriter.writerows(self.__csv_tuples) - logging.info(' - save items:\t%d', len(self.__csv_tuples)) + logging.info(" - save items:\t%d", len(self.__csv_tuples)) def load(self, filepath=None): filepath = filepath or self.default_csv_filepath() - logging.info('Loading csv file:\t%s', filepath) - with open(filepath, 'rt', encoding='utf-8', newline='') as csvfile: + logging.info("Loading csv file:\t%s", filepath) + with open(filepath, "rt", encoding="utf-8", newline="") as csvfile: self.load_from_stream(csvfile) def save(self, filepath=None): filepath = filepath or self.default_csv_filepath() - logging.info('Saving csv file:\t%s', filepath) - with open(filepath, 'wt', encoding='utf-8', newline='') as csvfile: + logging.info("Saving csv file:\t%s", filepath) + with open(filepath, "wt", encoding="utf-8", newline="") as csvfile: self.save_to_stream(csvfile) @@ -295,32 +428,31 @@ def get_dictionary_items(prop, context): DictionaryEnum.__items_ttl = time.time() + 5 DictionaryEnum.__items_cache = items = [] - if 'import' in prop.bl_rna.identifier: - items.append(('DISABLED', 'Disabled', '', 0)) + if "import" in prop.bl_rna.identifier: + items.append(("DISABLED", "Disabled", "", 0)) - items.append(('INTERNAL', 'Internal Dictionary', 'The dictionary defined in '+__name__, len(items))) + items.append(("INTERNAL", "Internal Dictionary", "The dictionary defined in " + __name__, len(items))) - for txt_name in sorted(x.name for x in bpy.data.texts if x.name.lower().endswith('.csv')): - items.append((txt_name, txt_name, "bpy.data.texts['%s']"%txt_name, 'TEXT', len(items))) + for txt_name in sorted(x.name for x in bpy.data.texts if x.name.lower().endswith(".csv")): + items.append((txt_name, txt_name, "bpy.data.texts['%s']" % txt_name, "TEXT", len(items))) import os - from mmd_tools.bpyutils import addon_preferences - folder = addon_preferences('dictionary_folder', '') + + folder = FnContext.get_addon_preferences_attribute(context, "dictionary_folder", "") if os.path.isdir(folder): - for filename in sorted(x for x in os.listdir(folder) if x.lower().endswith('.csv')): + for filename in sorted(x for x in os.listdir(folder) if x.lower().endswith(".csv")): filepath = os.path.join(folder, filename) if os.path.isfile(filepath): - items.append((filepath, filename, filepath, 'FILE', len(items))) + items.append((filepath, filename, filepath, "FILE", len(items))) - if 'dictionary' in prop: - prop['dictionary'] = min(prop['dictionary'], len(items)-1) + if "dictionary" in prop: + prop["dictionary"] = min(prop["dictionary"], len(items) - 1) return items @staticmethod def get_translator(dictionary): - if dictionary == 'DISABLED': + if dictionary == "DISABLED": return None - if dictionary == 'INTERNAL': + if dictionary == "INTERNAL": return getTranslator(dict(jp_to_en_tuples)) return getTranslator(dictionary) - diff --git a/mmd_tools/typings/mmd_tools/properties/material.pyi b/mmd_tools/typings/mmd_tools/properties/material.pyi new file mode 100644 index 00000000..6c2bc700 --- /dev/null +++ b/mmd_tools/typings/mmd_tools/properties/material.pyi @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 MMD Tools authors +# This file is part of MMD Tools. + +from typing import List + +class MMDMaterial(bpy.types.PropertyGroup): + name_j: str + name_e: str + + material_id: int + + ambient_color: List[float] + diffuse_color: List[float] + + alpha: float + + specular_color: List[float] + + shininess: float + + is_double_sided: bool + + enabled_drop_shadow: bool + + enabled_self_shadow_map: bool + + enabled_self_shadow: bool + + enabled_toon_edge: bool + + edge_color: List[float] + + edge_weight: float + + sphere_texture_type: str + + is_shared_toon_texture: bool + toon_texture: str + + shared_toon_texture: int + + comment: int + + def is_id_unique(self) -> bool: ... diff --git a/mmd_tools/typings/mmd_tools/properties/morph.pyi b/mmd_tools/typings/mmd_tools/properties/morph.pyi new file mode 100644 index 00000000..1d556c19 --- /dev/null +++ b/mmd_tools/typings/mmd_tools/properties/morph.pyi @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 MMD Tools authors +# This file is part of MMD Tools. + +class MaterialMorph: + pass + +class UVMorph: + pass + +class BoneMorph: + pass + +class GroupMorph: + pass + +class VertexMorph: + pass diff --git a/mmd_tools/typings/mmd_tools/properties/pose_bone.pyi b/mmd_tools/typings/mmd_tools/properties/pose_bone.pyi new file mode 100644 index 00000000..30fdf50b --- /dev/null +++ b/mmd_tools/typings/mmd_tools/properties/pose_bone.pyi @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 MMD Tools authors +# This file is part of MMD Tools. + +from mathutils import Vector + +class MMDBone: + name_j: str + name_e: str + bone_id: int + transform_order: int + is_controllable: bool + transform_after_dynamics: bool + enabled_fixed_axis: bool + fixed_axis: Vector + enabled_local_axes: bool + local_axis_x: Vector + local_axis_z: Vector + is_tip: bool + ik_rotation_constraint: float + has_additional_rotation: bool + has_additional_location: bool + additional_transform_bone: str + additional_transform_bone_id: int + additional_transform_influence: float + is_additional_transform_dirty: bool diff --git a/mmd_tools/typings/mmd_tools/properties/root.pyi b/mmd_tools/typings/mmd_tools/properties/root.pyi new file mode 100644 index 00000000..874b9f57 --- /dev/null +++ b/mmd_tools/typings/mmd_tools/properties/root.pyi @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 MMD Tools authors +# This file is part of MMD Tools. + +from typing import Literal + +import bpy +from .morph import BoneMorph, GroupMorph, MaterialMorph, UVMorph, VertexMorph +from .translations import MMDTranslation + +class MMDDisplayItem: + name: str + type: Literal["BONE"] | Literal["MORPH"] + + morph_type: Literal["material_morphs"] | Literal["uv_morphs"] | Literal["bone_morphs"] | Literal["vertex_morphs"] | Literal["group_morphs"] + +class MMDDisplayItemFrame: + name: str + name_e: str + + is_special: bool + + data: bpy.types.bpy_prop_collection[MMDDisplayItem] + + active_item: int + +class MMDRoot: + id_data: bpy.types.Object + name: str + name_j: str + name_e: str + comment_text: str + comment_e_text: str + ik_lookup_factor: int + show_meshes: bool + show_rigid_bodies: bool + show_joints: bool + show_temporary_objects: bool + show_armature: bool + show_names_of_rigid_bodies: bool + show_names_of_joints: bool + use_toon_texture: bool + use_sphere_texture: bool + use_sdef: bool + use_property_driver: bool + is_built: bool + active_rigidbody_index: int + active_joint_index: int + display_item_frames: bpy.types.bpy_prop_collection[MMDDisplayItemFrame] + active_display_item_frame: int + material_morphs: bpy.types.bpy_prop_collection[MaterialMorph] + uv_morphs: bpy.types.bpy_prop_collection[UVMorph] + bone_morphs: bpy.types.bpy_prop_collection[BoneMorph] + vertex_morphs: bpy.types.bpy_prop_collection[VertexMorph] + group_morphs: bpy.types.bpy_prop_collection[GroupMorph] + + active_morph_type: str # TODO: Replace with StrEnum + active_morph: int + morph_panel_show_settings: bool + active_mesh_index: int + translation: MMDTranslation + + @staticmethod + def register() -> None: ... + @staticmethod + def unregister() -> None: ... diff --git a/mmd_tools/typings/mmd_tools/properties/translations.pyi b/mmd_tools/typings/mmd_tools/properties/translations.pyi new file mode 100644 index 00000000..407fe9f8 --- /dev/null +++ b/mmd_tools/typings/mmd_tools/properties/translations.pyi @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 MMD Tools authors +# This file is part of MMD Tools. + +from typing import List + +import bpy + +class MMDTranslationElement: + type: str + object: bpy.types.Object + data_path: str + name: str + name_j: str + name_e: str + +class MMDTranslationElementIndex(bpy.types.PropertyGroup): + value: int + +class MMDTranslation: + id_data: bpy.types.Object + translation_elements: List[MMDTranslationElement] + filtered_translation_element_indices_active_index: int + filtered_translation_element_indices: List[MMDTranslationElementIndex] + + filter_japanese_blank: bool + filter_english_blank: bool + filter_restorable: bool + filter_selected: bool + filter_visible: bool + filter_types: str + + dictionary: str + + batch_operation_target: str + + batch_operation_script_preset: str + + batch_operation_script: str diff --git a/mmd_tools/utils.py b/mmd_tools/utils.py index dd313f7e..9c6cc97b 100644 --- a/mmd_tools/utils.py +++ b/mmd_tools/utils.py @@ -1,70 +1,85 @@ # -*- coding: utf-8 -*- -import re +# Copyright 2012 MMD Tools authors +# This file is part of MMD Tools. + +import logging import os +import re +from typing import Callable, Optional, Set import bpy -from mmd_tools.bpyutils import SceneOp + +from .bpyutils import FnContext + ## 指定したオブジェクトのみを選択状態かつアクティブにする def selectAObject(obj): try: - bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode="OBJECT") except Exception: pass - bpy.ops.object.select_all(action='DESELECT') - SceneOp(bpy.context).active_object = obj + bpy.ops.object.select_all(action="DESELECT") + FnContext.set_active_object(FnContext.ensure_context(), obj) + ## 現在のモードを指定したオブジェクトのEdit Modeに変更する def enterEditMode(obj): selectAObject(obj) - if obj.mode != 'EDIT': - bpy.ops.object.mode_set(mode='EDIT') + if obj.mode != "EDIT": + bpy.ops.object.mode_set(mode="EDIT") + def setParentToBone(obj, parent, bone_name): selectAObject(obj) - SceneOp(bpy.context).active_object = parent - bpy.ops.object.mode_set(mode='POSE') + FnContext.set_active_object(FnContext.ensure_context(), parent) + bpy.ops.object.mode_set(mode="POSE") parent.data.bones.active = parent.data.bones[bone_name] - bpy.ops.object.parent_set(type='BONE', xmirror=False, keep_transform=False) - bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.parent_set(type="BONE", xmirror=False, keep_transform=False) + bpy.ops.object.mode_set(mode="OBJECT") + def selectSingleBone(context, armature, bone_name, reset_pose=False): try: - bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode="OBJECT") except: pass for i in context.selected_objects: - i.select = False - SceneOp(context).active_object = armature - bpy.ops.object.mode_set(mode='POSE') + i.select_set(False) + FnContext.set_active_object(context, armature) + bpy.ops.object.mode_set(mode="POSE") if reset_pose: for p_bone in armature.pose.bones: p_bone.matrix_basis.identity() - armature_bones = armature.data.bones + armature_bones: bpy.types.ArmatureBones = armature.data.bones + i: bpy.types.Bone for i in armature_bones: - i.select = (i.name == bone_name) + i.select = i.name == bone_name i.select_head = i.select_tail = i.select if i.select: armature_bones.active = i i.hide = False - #armature.data.layers[list(i.layers).index(True)] = True -__CONVERT_NAME_TO_L_REGEXP = re.compile('^(.*)左(.*)$') -__CONVERT_NAME_TO_R_REGEXP = re.compile('^(.*)右(.*)$') +__CONVERT_NAME_TO_L_REGEXP = re.compile("^(.*)左(.*)$") +__CONVERT_NAME_TO_R_REGEXP = re.compile("^(.*)右(.*)$") + + ## 日本語で左右を命名されている名前をblender方式のL(R)に変更する def convertNameToLR(name, use_underscore=False): m = __CONVERT_NAME_TO_L_REGEXP.match(name) - delimiter = '_' if use_underscore else '.' + delimiter = "_" if use_underscore else "." if m: - name = m.group(1) + m.group(2) + delimiter + 'L' + name = m.group(1) + m.group(2) + delimiter + "L" m = __CONVERT_NAME_TO_R_REGEXP.match(name) if m: - name = m.group(1) + m.group(2) + delimiter + 'R' + name = m.group(1) + m.group(2) + delimiter + "R" return name -__CONVERT_L_TO_NAME_REGEXP = re.compile(r'(?P(?P[._])[lL])(?P($|(?P=separator)))') -__CONVERT_R_TO_NAME_REGEXP = re.compile(r'(?P(?P[._])[rR])(?P($|(?P=separator)))') + +__CONVERT_L_TO_NAME_REGEXP = re.compile(r"(?P(?P[._])[lL])(?P($|(?P=separator)))") +__CONVERT_R_TO_NAME_REGEXP = re.compile(r"(?P(?P[._])[rR])(?P($|(?P=separator)))") + + def convertLRToName(name): match = __CONVERT_L_TO_NAME_REGEXP.search(name) if match: @@ -76,6 +91,7 @@ def convertLRToName(name): return name + ## src_vertex_groupのWeightをdest_vertex_groupにaddする def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name): mesh = meshObj.data @@ -86,62 +102,33 @@ def mergeVertexGroup(meshObj, src_vertex_group_name, dest_vertex_group_name): for v in mesh.vertices: try: gi = [i.group for i in v.groups].index(vtxIndex) - dest_vertex_group.add([v.index], v.groups[gi].weight, 'ADD') + dest_vertex_group.add([v.index], v.groups[gi].weight, "ADD") except ValueError: pass -def __getCustomNormalKeeper(mesh): - if hasattr(mesh, 'has_custom_normals') and mesh.use_auto_smooth: - class _CustomNormalKeeper: - def __init__(self, mesh): - mesh.calc_normals_split() - self.__normals = tuple(zip((l.normal.copy() for l in mesh.loops), (p.material_index for p in mesh.polygons for v in p.vertices))) - mesh.free_normals_split() - self.__material_map = {} - materials = mesh.materials - for i, m in enumerate(materials): - if m is None or m.name in self.__material_map: - materials[i] = bpy.data.materials.new('_mmd_tmp_') - self.__material_map[materials[i].name] = (i, getattr(m, 'name', '')) - - def restore_custom_normals(self, mesh): - materials = mesh.materials - for i, m in enumerate(materials): - mat_id, mat_name_orig = self.__material_map[m.name] - if m.name != mat_name_orig: - materials[i] = bpy.data.materials.get(mat_name_orig, None) - m.user_clear() - bpy.data.materials.remove(m) - if len(materials) == 1: - mesh.normals_split_custom_set([n for n, x in self.__normals if x == mat_id]) - mesh.update() - return _CustomNormalKeeper(mesh) # This fixes the issue that "SeparateByMaterials" could break custom normals - return None - -def separateByMaterials(meshObj): + +def separateByMaterials(meshObj: bpy.types.Object): if len(meshObj.data.materials) < 2: selectAObject(meshObj) return - custom_normal_keeper = __getCustomNormalKeeper(meshObj.data) matrix_parent_inverse = meshObj.matrix_parent_inverse.copy() prev_parent = meshObj.parent - dummy_parent = bpy.data.objects.new(name='tmp', object_data=None) + dummy_parent = bpy.data.objects.new(name="tmp", object_data=None) meshObj.parent = dummy_parent meshObj.active_shape_key_index = 0 try: enterEditMode(meshObj) - bpy.ops.mesh.select_all(action='SELECT') - bpy.ops.mesh.separate(type='MATERIAL') + bpy.ops.mesh.select_all(action="SELECT") + bpy.ops.mesh.separate(type="MATERIAL") finally: - bpy.ops.object.mode_set(mode='OBJECT') - for i in dummy_parent.children: - if custom_normal_keeper: - custom_normal_keeper.restore_custom_normals(i.data) - materials = i.data.materials - i.name = getattr(materials[0], 'name', 'None') if len(materials) else 'None' - i.parent = prev_parent - i.matrix_parent_inverse = matrix_parent_inverse - bpy.data.objects.remove(dummy_parent) + bpy.ops.object.mode_set(mode="OBJECT") + for i in dummy_parent.children: + materials = i.data.materials + i.name = getattr(materials[0], "name", "None") if len(materials) else "None" + i.parent = prev_parent + i.matrix_parent_inverse = matrix_parent_inverse + bpy.data.objects.remove(dummy_parent) + def clearUnusedMeshes(): meshes_to_delete = [] @@ -157,40 +144,57 @@ def clearUnusedMeshes(): # それ以外の場合は通常のbone名をキーとしたpose_boneへの辞書を作成 def makePmxBoneMap(armObj): # Maintain backward compatibility with mmd_tools v0.4.x or older. - return {(i.mmd_bone.name_j or i.get('mmd_bone_name_j', i.get('name_j', i.name))):i for i in armObj.pose.bones} + return {(i.mmd_bone.name_j or i.get("mmd_bone_name_j", i.get("name_j", i.name))): i for i in armObj.pose.bones} + -def uniqueName(name, used_names): +__REMOVE_PREFIX_DIGITS_REGEXP = re.compile(r"\.\d{1,}$") + + +def unique_name(name: str, used_names: Set[str]) -> str: + """Helper function for storing unique names. + This function is a limited and simplified version of bpy_extras.io_utils.unique_name. + + Args: + name (str): The name to make unique. + used_names (Set[str]): A set of names that are already used. + + Returns: + str: The unique name, formatted as "{name}.{number:03d}". + """ if name not in used_names: return name count = 1 - new_name = orig_name = re.sub(r'\.\d{1,}$', '', name) + new_name = orig_name = __REMOVE_PREFIX_DIGITS_REGEXP.sub("", name) while new_name in used_names: - new_name = '%s.%03d'%(orig_name, count) + new_name = f"{orig_name}.{count:03d}" count += 1 return new_name + def int2base(x, base, width=0): """ Method to convert an int to a base Source: http://stackoverflow.com/questions/2267362 """ import string + digs = string.digits + string.ascii_uppercase - assert(2 <= base <= len(digs)) - digits, negtive = '', False + assert 2 <= base <= len(digs) + digits, negtive = "", False if x <= 0: if x == 0: - return '0'*max(1, width) - x, negtive, width = -x, True, width-1 + return "0" * max(1, width) + x, negtive, width = -x, True, width - 1 while x: digits = digs[x % base] + digits x //= base - digits = '0'*(width-len(digits)) + digits + digits = "0" * (width - len(digits)) + digits if negtive: - digits = '-' + digits + digits = "-" + digits return digits -def saferelpath(path, start, strategy='inside'): + +def saferelpath(path, start, strategy="inside"): """ On Windows relpath will raise a ValueError when trying to calculate the relative path to a @@ -204,13 +208,13 @@ def saferelpath(path, start, strategy='inside'): See http://bugs.python.org/issue7195 """ result = os.path.basename(path) - if os.name == 'nt': + if os.name == "nt": d1 = os.path.splitdrive(path)[0] d2 = os.path.splitdrive(start)[0] if d1 != d2: - if strategy == 'outside': - result = '..'+os.sep+os.path.basename(path) - elif strategy == 'absolute': + if strategy == "outside": + result = ".." + os.sep + os.path.basename(path) + elif strategy == "absolute": result = os.path.abspath(path) else: result = os.path.relpath(path, start) @@ -227,7 +231,7 @@ def get_by_index(items, index): return None @staticmethod - def resize(items, length): + def resize(items: bpy.types.bpy_prop_collection, length: int): count = length - len(items) if count > 0: for i in range(count): @@ -239,30 +243,31 @@ def resize(items, length): @staticmethod def add_after(items, index): index_end = len(items) - index = max(0, min(index_end, index+1)) + index = max(0, min(index_end, index + 1)) items.add() items.move(index_end, index) return items[index], index + class ItemMoveOp: type: bpy.props.EnumProperty( - name='Type', - description='Move type', - items = [ - ('UP', 'Up', '', 0), - ('DOWN', 'Down', '', 1), - ('TOP', 'Top', '', 2), - ('BOTTOM', 'Bottom', '', 3), - ], - default='UP', - ) + name="Type", + description="Move type", + items=[ + ("UP", "Up", "", 0), + ("DOWN", "Down", "", 1), + ("TOP", "Top", "", 2), + ("BOTTOM", "Bottom", "", 3), + ], + default="UP", + ) @staticmethod def move(items, index, move_type, index_min=0, index_max=None): if index_max is None: - index_max = len(items)-1 + index_max = len(items) - 1 else: - index_max = min(index_max, len(items)-1) + index_max = min(index_max, len(items) - 1) index_min = min(index_min, index_max) if index < index_min: @@ -273,16 +278,55 @@ def move(items, index, move_type, index_min=0, index_max=None): return index_max index_new = index - if move_type == 'UP': - index_new = max(index_min, index-1) - elif move_type == 'DOWN': - index_new = min(index+1, index_max) - elif move_type == 'TOP': + if move_type == "UP": + index_new = max(index_min, index - 1) + elif move_type == "DOWN": + index_new = min(index + 1, index_max) + elif move_type == "TOP": index_new = index_min - elif move_type == 'BOTTOM': + elif move_type == "BOTTOM": index_new = index_max if index_new != index: items.move(index, index_new) return index_new + +def deprecated(deprecated_in: Optional[str] = None, details: Optional[str] = None): + """Decorator to mark a function as deprecated. + Args: + deprecated_in (Optional[str]): Version in which the function was deprecated. + details (Optional[str]): Additional details about the deprecation. + Returns: + Callable: The decorated function. + """ + + def _function_wrapper(function: Callable): + def _inner_wrapper(*args, **kwargs): + warn_deprecation(function.__name__, deprecated_in, details) + return function(*args, **kwargs) + + return _inner_wrapper + + return _function_wrapper + + +def warn_deprecation(function_name: str, deprecated_in: Optional[str] = None, details: Optional[str] = None) -> None: + """Reports a deprecation warning. + Args: + function_name (str): Name of the deprecated function. + deprecated_in (Optional[str]): Version in which the function was deprecated. + details (Optional[str]): Additional details about the deprecation. + """ + logging.warning( + "%s is deprecated%s%s", + function_name, + f" since {deprecated_in}" if deprecated_in else "", + f": {details}" if details else "", + stack_info=True, + stacklevel=4, + ) + + # import warnings # pylint: disable=import-outside-toplevel + + # warnings.warn(f"""{function_name}is deprecated{f" since {deprecated_in}" if deprecated_in else ""}{f": {details}" if details else ""}""", category=DeprecationWarning, stacklevel=2)