Skip to content

Commit

Permalink
Added support for getting node by unique handle
Browse files Browse the repository at this point in the history
Nodes are no longer queried by their names which could be problematic in scenes with duplicate names. A unique scene handle (id) is now used instead.
For 3ds max is the nodes handle, for maya it is the nodes full name.

This has involved the implementation of a host interface class IHost.
Only implemented IHost for max so far.
This has required a large refactor of the dcc sub-package and the setup logic for skin_plus_plus itself.
  • Loading branch information
munkybutt committed Mar 29, 2024
1 parent e123e3a commit 580f517
Show file tree
Hide file tree
Showing 21 changed files with 296 additions and 128 deletions.
81 changes: 32 additions & 49 deletions PYProjects/skin_plus_plus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import sys

current_dcc = None
current_host_interface = None

extract_skin_data = None
apply_skin_data = None
Expand All @@ -22,7 +23,7 @@
__py_version__ = f"{sys.version_info.major}{sys.version_info.minor}"


def __get_skin_plus_plus_py(python_version: str, debug: bool = False):
def _activate_skin_plus_plus_py_(python_version: str, debug: bool = False):
global skin_plus_plus_py
global SkinData

Expand All @@ -34,10 +35,9 @@ def __get_skin_plus_plus_py(python_version: str, debug: bool = False):
sub_module_path = current_directory / f"py/{python_version}"

if not sub_module_path.exists():
raise FileNotFoundError(f"Unsupported Python version!")
raise FileNotFoundError(f"Unsupported Python version: {python_version}")

import_path = f"skin_plus_plus.py.{python_version}.skin_plus_plus_py"
print(f"import_path: {import_path}")
if "skin_plus_plus_py" in sys.modules:
del sys.modules["skin_plus_plus_py"]

Expand All @@ -50,31 +50,31 @@ def __get_skin_plus_plus_py(python_version: str, debug: bool = False):
return skin_plus_plus_py


def __get_dcc_backend(dcc: str, version: str, api: str):
current_directory = pathlib.Path(__file__).parent
sub_module_name = f"skin_plus_plus_{api}_{version}"
sub_module_path = current_directory / f"dccs/{dcc}" / sub_module_name
def _activate_host_():
global current_host_interface

if not sub_module_path.exists():
raise FileNotFoundError("Unsupported DCC version!")
global apply_skin_data
global extract_skin_data

import_path = f"{__name__}.dccs.{dcc}.{sub_module_name}.skin_plus_plus_{api}"
backend = importlib.import_module(import_path)
if is_reloading:
importlib.reload(backend)
executable = sys.executable.lower()
if "3ds max" in executable:
from .dccs.max import IHost

global extract_skin_data
global apply_skin_data
# global get_vertex_positions
current_host_interface = IHost()

elif "maya" in executable:
from .dccs.maya import Host

current_host_interface = IHost()

extract_skin_data = backend.extract_skin_data
apply_skin_data = backend.apply_skin_data
# get_vertex_positions = skin_plus_plus_pymxs.get_vertex_positions
else:
raise RuntimeError(f"Unsupported executable: {executable}")

return backend
extract_skin_data = current_host_interface.extract_skin_data
apply_skin_data = current_host_interface.apply_skin_data


def set_debug(value: bool):
def set_debug(value: bool) -> None:
"""
Toggle debug mode on or off.
Expand All @@ -90,33 +90,11 @@ def set_debug(value: bool):
--------
- `None`
"""
__get_skin_plus_plus_py(__py_version__, debug=value)


# DO NOT REMOVE - Required for access to SkinData class:
__get_skin_plus_plus_py(__py_version__)

_activate_skin_plus_plus_py_(__py_version__, debug=value)


executable = sys.executable.lower()
if "3ds max" in executable:
from pymxs import runtime as mxRt

version_info = mxRt.MaxVersion()
version_number = version_info[7]
__get_dcc_backend("max", version_number, "pymxs")
current_dcc = "max"


elif "maya" in executable:
from pymel import versions

version = str(versions.current())[:4]
__get_dcc_backend("maya", version, "pymaya")
current_dcc = "maya"

else:
raise RuntimeError(f"Unsupported executable: {executable}")
_activate_skin_plus_plus_py_(__py_version__)
_activate_host_()


_typing = False
Expand Down Expand Up @@ -164,6 +142,8 @@ def __getattr__(name: str) -> Any:


__all__ = (
"SkinData",

"dccs",
"py",

Expand All @@ -172,20 +152,23 @@ def __getattr__(name: str) -> Any:
"mesh",

"current_dcc",
"current_host_interface",

"extract_skin_data",
"apply_skin_data",
"set_debug",

"export_skin_data",
"import_skin_data",
"FileType",

"save",
"load",

"max_to_maya",
"maya_to_max"
"maya_to_max",

"set_debug",
)


def __dir__():
return __all__
24 changes: 17 additions & 7 deletions PYProjects/skin_plus_plus/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations


from . import _types
from .dccs import core as _dccs_core
from . import skin_plus_plus_py
from .core import FileType as _FileType
from .core import export_skin_data as _export_skin_data
Expand All @@ -18,16 +20,24 @@ load = _load
max_to_maya = _max_to_maya
maya_to_max = _maya_to_max


current_dcc: str = ...
"""
The name of the current DCC.
"""
current_host_interface: _dccs_core.IHost = ...
"""
The interface to the current Host
"""

SkinData = skin_plus_plus_py.SkinData

def extract_skin_data(mesh_name: str) -> skin_plus_plus_py.SkinData:
...


def apply_skin_data(mesh_name: str, skin_data: skin_plus_plus_py.SkinData) -> bool:
def extract_skin_data(node: _types.T_Node) -> SkinData:
"""
Extract skin data from the given DCC node.
"""
...
def apply_skin_data(node: _types.T_Node, skin_data: SkinData) -> bool:
"""
Apply the given skin data to the given DCC node.
"""
...
12 changes: 12 additions & 0 deletions PYProjects/skin_plus_plus/_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pymel.core import nodetypes as pm_ntypes
from pymxs import runtime as mxrt
from typing import Callable
from typing import TypeVar
from typing import Union

from . import SkinData

T_Node = TypeVar("T_Node", mxrt.Node, pm_ntypes.DagNode)
T_Handle = Union[int, str]
T_CExSD = Callable[[T_Handle], SkinData]
T_CApSD = Callable[[T_Handle, SkinData], None]
96 changes: 96 additions & 0 deletions PYProjects/skin_plus_plus/dccs/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from __future__ import annotations

import abc
import importlib
import pathlib


_typing = False
if _typing:

from .. import _types
from .. import SkinData

del _typing


class IHost(metaclass=abc.ABCMeta):
_extract_skin_data: _types.T_CExSD
_apply_skin_data: _types.T_CApSD
_get_vertex_positions: _types.Callable

def __init__(self) -> None:
self._get_dcc_backend()

@property
@abc.abstractmethod
def name(self) -> str:
"""
The name of the host
"""

@property
@abc.abstractmethod
def api_name(self) -> str:
"""
The api name of the compiled backend for the host.
i.e. pymxs or pymaya
"""

def _get_dcc_backend(self):
version = self._get_version_number()
current_directory = pathlib.Path(__file__).parent
sub_module_path = current_directory / self.name / str(version)

if not sub_module_path.exists():
raise FileNotFoundError(f"Unsupported DCC version: {version}")

import_path = f"{__name__.rstrip('core')}{self.name}.{version}.skin_plus_plus_{self.api_name}"
backend = importlib.import_module(import_path)
# if is_reloading:
# importlib.reload(backend)

self._extract_skin_data: _types.T_CExSD = backend.extract_skin_data
self._apply_skin_data: _types.T_CApSD = backend.apply_skin_data
self._get_vertex_positions: _types.Callable = backend.get_vertex_positions

return backend

@abc.abstractmethod
def _get_version_number(self) -> int | str:
"""
Get the version number of the host
"""

@abc.abstractmethod
def get_current_file_path(self) -> pathlib.Path:
"""
Get the file path of the current host scene
"""

@abc.abstractmethod
def get_selection(self) -> tuple[_types.T_Node, ...]:
"""
Get the selection of the current host scene
"""

@abc.abstractmethod
def get_node_name(self, node: _types.T_Node) -> str:
"""
Get the name of the given node
"""

@abc.abstractmethod
def get_node_handle(self, node: _types.T_Node) -> _types.T_Handle:
"""
Get the unique handle of the given node
"""


def extract_skin_data(self, node: _types.T_Node) -> SkinData:
handle = self.get_node_handle(node)
return self._extract_skin_data(handle)

def apply_skin_data(self, node: _types.T_Node, skin_data: SkinData):
handle = self.get_node_handle(node)
return self._apply_skin_data(handle, skin_data)
Binary file not shown.
4 changes: 4 additions & 0 deletions PYProjects/skin_plus_plus/dccs/max/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .core import IHost


__all__ = ("IHost",)
39 changes: 39 additions & 0 deletions PYProjects/skin_plus_plus/dccs/max/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from __future__ import annotations

import pathlib

from pymxs import runtime as mxrt

from .. import core


class IHost(core.IHost):

@property
def name(self) -> str:
return "max"

@property
def api_name(self) -> str:
return "pymxs"

def _get_version_number(self):
version_info = mxrt.MaxVersion()
version_number = version_info[7]
return version_number

def get_current_file_path(self) -> pathlib.Path:
max_file_path = mxrt.MaxFilePath
if not max_file_path:
raise RuntimeError("File is not saved!")

return pathlib.Path(max_file_path, mxrt.MaxFileName)

def get_selection(self) -> tuple[mxrt.Node, ...]:
return tuple(mxrt.Selection)

def get_node_name(self, node: mxrt.Node) -> str:
return node.Name

def get_node_handle(self, node: mxrt.Node) -> int:
return node.Handle
Binary file not shown.
Binary file modified PYProjects/skin_plus_plus/py/310/skin_plus_plus_py.pyd
Binary file not shown.
10 changes: 9 additions & 1 deletion PYProjects/skin_plus_plus/skin_plus_plus_py.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import numpy as np
import numpy.typing as np_typing
import typing


class SkinData:
"""
Class containing data for a given skin object.
Expand All @@ -25,9 +24,18 @@ class SkinData:
"""

bone_names: list[str]
"""The names of the bones in the SkinData"""
bone_ids: np_typing.NDArray[np.int64]
"""The bone ids for each influence on each vertex"""
weights: np_typing.NDArray[np.float32]
"""The weights for each influence on each vertex"""
positions: np_typing.NDArray[np.float32]
"""The position of each vertex in the SkinData's mesh"""
vertex_ids: np_typing.NDArray[np.int64] | None = None
"""
The specific vertex ids that make up the SkinData.
If `None` then all vertices are used to make up the SkinData.
"""

@typing.overload
def __init__(self):
Expand Down
Loading

0 comments on commit 580f517

Please sign in to comment.