Skip to content

Commit f54f92f

Browse files
committed
Add MMD Tools installation feature
1 parent 06463ef commit f54f92f

File tree

6 files changed

+141
-28
lines changed

6 files changed

+141
-28
lines changed

mmd_tools_append/asset_search/panels.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from .. import PACKAGE_PATH
1515
from ..m17n import _, iface_
16-
from ..utilities import get_preferences, label_multiline, to_human_friendly_text, to_int32
16+
from ..utilities import get_preferences, is_mmd_tools_installed, label_multiline, to_human_friendly_text, to_int32
1717
from .actions import ImportActionExecutor, MessageException
1818
from .assets import ASSETS, AssetDescription, AssetType
1919
from .cache import CONTENT_CACHE, Content, Task
@@ -204,7 +204,7 @@ class AssetImport(bpy.types.Operator):
204204

205205
@classmethod
206206
def poll(cls, context):
207-
return bpy.context.mode == "OBJECT"
207+
return bpy.context.mode == "OBJECT" and is_mmd_tools_installed()
208208

209209
def execute(self, context):
210210
print(f"do: {self.bl_idname}")

mmd_tools_append/converters/armatures/panels.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import bpy
66

7+
from ...utilities import is_mmd_tools_installed
8+
79
from ...m17n import _
810
from .autorig import AutoRigArmatureObject
911
from .mmd_bind import ControlType
@@ -20,6 +22,9 @@ class MMDRigifyPanel(bpy.types.Panel):
2022

2123
@classmethod
2224
def poll(cls, context):
25+
if not is_mmd_tools_installed():
26+
return False
27+
2328
active_object = context.active_object
2429
if not MMDRigifyArmatureObject.is_rigify_armature_object(active_object):
2530
return False

mmd_tools_append/converters/physics/cloth.py

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,70 @@
99
from ...editors.meshes import MeshEditor
1010
from ...m17n import _
1111
from ...tuners import TunerABC, TunerRegistry
12-
from ...utilities import MessageException, import_mmd_tools
12+
from ...utilities import MessageException, MMD_TOOLS_IMPORT_HOOKS, import_mmd_tools
1313
from .rigid_body_to_cloth import (
1414
PhysicsMode,
1515
RigidBodyToClothConverter,
1616
)
1717

18-
mmd_tools = import_mmd_tools()
19-
if not hasattr(mmd_tools.core.model.Model, "clothGroupObject"):
18+
19+
def on_import_mmd_tools_ensure_cloth_methods(mmd_tools):
20+
"""
21+
Monkey-patch helper that augments mmd_tools.core.model.Model with cloth-related
22+
convenience methods if they are not already present.
23+
24+
Behavior:
25+
1. Idempotency: Returns immediately if Model already has the attribute
26+
'clothGroupObject', preventing duplicate patching.
27+
2. Adds Model.clothGroupObject(self):
28+
- Lazily resolves (and caches on the instance as _cloth_grp) a dedicated
29+
container object named "cloths" parented to the model's root object.
30+
- Searches existing children of rootObject() for an object named "cloths".
31+
- If absent, creates a new empty object via mmd_tools.bpyutils.FnContext.new_and_link_object.
32+
- Configures the new object to be:
33+
* Hidden and unselectable (hide, hide_select = True)
34+
* Transform-locked on location, rotation, and scale (all axes True)
35+
- Returns the resolved/created container object.
36+
3. Adds Model.cloths(self):
37+
- Iterates over all objects returned by Model.allObjects(clothGroupObject()).
38+
- Yields only MESH type objects that have an existing cloth modifier, as
39+
determined by MeshEditor(obj).find_cloth_modifier().
40+
41+
Side Effects:
42+
- Mutates the mmd_tools.core.model.Model class by attaching two methods:
43+
'clothGroupObject' and 'cloths'.
44+
- May create and link a new helper object named "cloths" into the current
45+
Blender context's scene hierarchy.
46+
47+
Caching Details:
48+
- The resolved group object is stored per-model instance in the private
49+
attribute _cloth_grp to avoid repeated scene searches or object creation.
50+
51+
Prerequisites:
52+
- A functioning import_mmd_tools() utility returning the mmd_tools module.
53+
- Availability of bpy (Blender Python API) and MeshEditor in the execution environment.
54+
55+
Returns:
56+
None
57+
58+
Raises:
59+
None (all operations are performed defensively; absence of expected API
60+
components would surface as AttributeError at runtime).
61+
62+
Usage Pattern:
63+
ensure_cloth_methods()
64+
model = mmd_tools.core.model.Model(...)
65+
grp = model.clothGroupObject()
66+
for cloth_obj in model.cloths():
67+
...
68+
69+
Rationale:
70+
- Centralizes the patch so add-on code can safely call ensure_cloth_methods()
71+
before relying on the extended API, without risking duplicate definitions.
72+
"""
73+
74+
if hasattr(mmd_tools.core.model.Model, "clothGroupObject"):
75+
return
2076

2177
def mmd_model_cloth_group_object_method(self):
2278
# pylint: disable=protected-access
@@ -50,17 +106,12 @@ def mmd_model_cloths_method(self):
50106
continue
51107
yield obj
52108

53-
def iterate_joint_objects(
54-
root_object: bpy.types.Object,
55-
) -> Iterator[bpy.types.Object]:
56-
return mmd_tools.core.model.FnModel.iterate_filtered_child_objects(
57-
mmd_tools.core.model.FnModel.is_joint_object,
58-
mmd_tools.core.model.FnModel.find_joint_group_object(root_object),
59-
)
60-
61109
mmd_tools.core.model.Model.cloths = mmd_model_cloths_method
62110

63111

112+
MMD_TOOLS_IMPORT_HOOKS.append(on_import_mmd_tools_ensure_cloth_methods)
113+
114+
64115
class ClothTunerABC(TunerABC, MeshEditor):
65116
pass
66117

@@ -268,6 +319,7 @@ def poll(cls, context: bpy.types.Context):
268319
def filter_only_in_mmd_model(
269320
key_object: bpy.types.Object,
270321
) -> Iterable[bpy.types.Object]:
322+
mmd_tools = import_mmd_tools()
271323
mmd_root = mmd_tools.core.model.FnModel.find_root_object(key_object)
272324
if mmd_root is None:
273325
return
@@ -362,7 +414,7 @@ def poll(cls, context: bpy.types.Context):
362414
selected_mesh_mmd_root = None
363415
selected_rigid_body_mmd_root = None
364416

365-
mmd_find_root = mmd_tools.core.model.FnModel.find_root_object
417+
mmd_find_root = import_mmd_tools().core.model.FnModel.find_root_object
366418
for obj in context.selected_objects:
367419
if obj.type != "MESH":
368420
return False
@@ -382,7 +434,7 @@ def invoke(self, context, event):
382434

383435
def execute(self, context: bpy.types.Context):
384436
try:
385-
mmd_find_root = mmd_tools.core.model.FnModel.find_root_object
437+
mmd_find_root = import_mmd_tools().core.model.FnModel.find_root_object
386438

387439
target_mmd_root_object = None
388440
rigid_body_objects: List[bpy.types.Object] = []

mmd_tools_append/panels.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,23 @@
3333
)
3434
from .generators.physics import AddCenterOfGravityObject
3535
from .m17n import _
36-
from .utilities import import_mmd_tools
36+
from .utilities import import_mmd_tools, is_mmd_tools_installed
3737

38-
mmd_tools = import_mmd_tools()
38+
39+
class InstallMMDTools(bpy.types.Operator):
40+
bl_idname = "mmd_tools_append.install_mmd_tools"
41+
bl_label = _("Install MMD Tools")
42+
bl_options = {"REGISTER"}
43+
44+
@classmethod
45+
def poll(cls, _context):
46+
return not is_mmd_tools_installed()
47+
48+
def execute(self, context):
49+
bpy.ops.extensions.userpref_allow_online()
50+
bpy.ops.extensions.repo_sync(repo_index=0)
51+
bpy.ops.extensions.package_install(repo_index=0, pkg_id="mmd_tools")
52+
return {"FINISHED"}
3953

4054

4155
class OperatorPanel(bpy.types.Panel):
@@ -49,6 +63,11 @@ class OperatorPanel(bpy.types.Panel):
4963
def draw(self, _context):
5064
layout = self.layout
5165

66+
if not is_mmd_tools_installed():
67+
layout.label(text=_("MMD Tools is not installed."), icon="ERROR")
68+
layout.operator(InstallMMDTools.bl_idname, icon="IMPORT")
69+
return
70+
5271
col = layout.column(align=True)
5372
col.label(text=_("Render:"), icon="SCENE_DATA")
5473
grid = col.grid_flow(row_major=True, align=True)
@@ -97,6 +116,10 @@ class MMDAppendPhysicsPanel(bpy.types.Panel):
97116
bl_region_type = "UI"
98117
bl_category = "MMD"
99118

119+
@classmethod
120+
def poll(cls, context: bpy.types.Context):
121+
return is_mmd_tools_installed()
122+
100123
def draw(self, context: bpy.types.Context):
101124
layout = self.layout
102125

@@ -108,7 +131,7 @@ def draw(self, context: bpy.types.Context):
108131
row.operator(SelectCollisionMesh.bl_idname, text=_(""), icon="RESTRICT_SELECT_OFF")
109132
row.operator(RemoveMeshCollision.bl_idname, text=_(""), icon="TRASH")
110133

111-
mmd_root_object = mmd_tools.core.model.FnModel.find_root_object(context.active_object)
134+
mmd_root_object = import_mmd_tools().core.model.FnModel.find_root_object(context.active_object)
112135
if mmd_root_object is None:
113136
col = layout.column(align=True)
114137
col.label(text=_("MMD Model is not selected."), icon="ERROR")
@@ -166,11 +189,13 @@ def draw(self, context: bpy.types.Context):
166189

167190
@staticmethod
168191
def _toggle_visibility_of_cloths(obj, context):
192+
mmd_tools = import_mmd_tools()
193+
169194
mmd_root_object = mmd_tools.core.model.FnModel.find_root_object(obj)
170195
mmd_model = mmd_tools.core.model.Model(mmd_root_object)
171196
hide = not mmd_root_object.mmd_tools_append_show_cloths
172197

173-
with mmd_tools.bpyutils.activate_layer_collection(mmd_root_object):
198+
with mmd_tools.bpyutils.FnContext.temp_override_active_layer_collection(context, mmd_root_object):
174199
cloth_object: bpy.types.Object
175200
for cloth_object in mmd_model.cloths():
176201
cloth_object.hide = hide
@@ -197,7 +222,7 @@ class MMDAppendSegmentationPanel(bpy.types.Panel):
197222

198223
@classmethod
199224
def poll(cls, context: bpy.types.Context):
200-
return context.mode in {"PAINT_VERTEX", "EDIT_MESH"}
225+
return is_mmd_tools_installed() and context.mode in {"PAINT_VERTEX", "EDIT_MESH"}
201226

202227
def draw(self, context: bpy.types.Context):
203228
layout = self.layout

mmd_tools_append/tuners/panels.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import bpy
77

8+
from ..utilities import is_mmd_tools_installed
89
from ..editors.nodes import MaterialEditor
910
from ..m17n import _, iface_
1011
from ..tuners.lighting_tuners import LightingUtilities
@@ -122,7 +123,7 @@ class MaterialPanel(bpy.types.Panel):
122123
@classmethod
123124
def poll(cls, context):
124125
obj = context.active_object
125-
return obj.active_material and obj.mmd_type == "NONE"
126+
return is_mmd_tools_installed() and obj.active_material and obj.mmd_type == "NONE"
126127

127128
def draw(self, context):
128129
material = context.active_object.active_material

mmd_tools_append/utilities.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
# This file is part of MMD Tools Append.
44

55
import hashlib
6-
import importlib
6+
import importlib.util
7+
import logging
78
import math
89
import re
10+
import sys
911

1012
import bpy
1113

@@ -56,24 +58,52 @@ def import_from_file(module_name: str, module_path: str):
5658
raise ImportError(f"Cannot load module '{module_name}' from '{module_path}'")
5759

5860
module = importlib.util.module_from_spec(spec)
59-
# Optional: cache in sys.modules to avoid reloading
60-
import sys
61-
6261
sys.modules[module_name] = module # ensures single instance if repeatedly called
6362
spec.loader.exec_module(module) # type: ignore[attr-defined]
6463
return module
6564

6665

66+
def is_mmd_tools_installed():
67+
candidates = (
68+
"bl_ext.blender_org.mmd_tools",
69+
"bl_ext.vscode_development.mmd_tools",
70+
)
71+
72+
for name in candidates:
73+
if name in sys.modules or importlib.util.find_spec(name) is not None:
74+
return True
75+
76+
return False
77+
78+
79+
_MMD_TOOLS_CACHE = None
80+
81+
MMD_TOOLS_IMPORT_HOOKS = []
82+
83+
6784
def import_mmd_tools():
85+
global _MMD_TOOLS_CACHE
86+
global MMD_TOOLS_IMPORT_HOOKS
87+
88+
if _MMD_TOOLS_CACHE is not None:
89+
return _MMD_TOOLS_CACHE
90+
6891
try:
69-
return importlib.import_module("bl_ext.blender_org.mmd_tools")
92+
_MMD_TOOLS_CACHE = importlib.import_module("bl_ext.blender_org.mmd_tools")
7093
except ImportError as exception:
71-
# for debugging
7294
try:
73-
return importlib.import_module("bl_ext.vscode_development.mmd_tools")
95+
_MMD_TOOLS_CACHE = importlib.import_module("bl_ext.vscode_development.mmd_tools")
7496
except ImportError:
7597
raise RuntimeError(_("MMD Tools is not installed correctly. Please install MMD Tools using the correct steps, as MMD Tools Append depends on MMD Tools.")) from exception
7698

99+
for hook in MMD_TOOLS_IMPORT_HOOKS:
100+
try:
101+
hook(_MMD_TOOLS_CACHE)
102+
except:
103+
logging.exception(f"Warning: An error occurred in import_mmd_tools hook {hook}")
104+
105+
return _MMD_TOOLS_CACHE
106+
77107

78108
def label_multiline(layout, text="", width=0):
79109
if text.strip() == "":

0 commit comments

Comments
 (0)