From 6d56b8c93fcd8be4918f97d69f5ba1b00f1770fb Mon Sep 17 00:00:00 2001 From: Michael Harmon Date: Sat, 16 May 2026 17:32:11 -0600 Subject: [PATCH 1/4] add msgspec as dependency for faster json serialization --- pyproject.toml | 1 + uv.lock | 54 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8082aee..1683f6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.11" dependencies = [ + "msgspec>=0.21.1", "nxt-core>=0.18.1", ] diff --git a/uv.lock b/uv.lock index cd7a319..63edf7a 100644 --- a/uv.lock +++ b/uv.lock @@ -36,6 +36,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/39/df15b423c029fe7dcdf43814f4776fb689c86e6fc48ab36af3cae63b2732/maya_stubs-0.4.2-py3-none-any.whl", hash = "sha256:2dd28457af1877293f5b7544eea5a14384ad89b5a59796847c6c68c1a44f0db9", size = 258974, upload-time = "2025-10-30T21:43:28.101Z" }, ] +[[package]] +name = "msgspec" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/7f/bbc4e74cd33d316b75541149e4d35b163b63bce066530ae185a2ec3b5bfc/msgspec-0.21.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b504b6e7f7a22a24b27232b73034421692147865162daaec9f3bf62439007c87", size = 193131, upload-time = "2026-04-12T21:43:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/c1/60/504886af1aaf854112663b842d5eea9a15d9588f9bf7d0d2df736424b84d/msgspec-0.21.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4692b7c1609155708c4418f88e92f63c13fdf08aa095c84bae82bad75b53389b", size = 186597, upload-time = "2026-04-12T21:43:57.242Z" }, + { url = "https://files.pythonhosted.org/packages/fa/54/d24ddeaa65b5278c9e67f48ce3c17a9831e8f3722f3c8322ee120aca22ef/msgspec-0.21.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3124010b3815451494c85ff345e693cb9fe5889cfcbbef39ed8622e0e72319c", size = 215158, upload-time = "2026-04-12T21:43:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/9f/75/bb79c8b89a93ae23cd33c0d802373f16feaf9633f05d8af77091350dda0a/msgspec-0.21.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6badc03b9725352219cca017bfe71c61f2fbd0fb5982b410ac17c97c213deb30", size = 219856, upload-time = "2026-04-12T21:44:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/c5ca26b46f0ebbd3a6683695ef89396712cb9e4199fd1f0bc1dd968216b1/msgspec-0.21.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5d2d4116ebe3035a78d9ec76e99a9d64e5fa6d44fe61a9c5de7fd1acf54bcc69", size = 220314, upload-time = "2026-04-12T21:44:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/645a351c4285dce40ed6755c3dcc0aa648e26dacb20a98018fe2cce5e87b/msgspec-0.21.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0d1009f6715f5bff3b54d4ff5c7428ad96197e0534e1645b8e9b955890c84664", size = 223215, upload-time = "2026-04-12T21:44:02.884Z" }, + { url = "https://files.pythonhosted.org/packages/09/af/8bf15736a6dd3cb4f90c5467f6dc39197d2daaf10754490cdc0aa17b7312/msgspec-0.21.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6faffe5bb644ec884052679af4dfd776d4b5ca90e4a7ec7e7e319e4e6b93a6e", size = 188554, upload-time = "2026-04-12T21:44:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/ef/29/cc7db3a165b62d16e64a83f82eccb79655055cb5bc1f60459a6f9d7c82f2/msgspec-0.21.1-cp311-cp311-win_arm64.whl", hash = "sha256:ee9e3f11fa94603f7d673bf795cfa31b549c4a2c723bc39b45beb1e7f5a3fb99", size = 174517, upload-time = "2026-04-12T21:44:05.66Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cf/317224852c00248c620a9bcf4b26e2e4ab8afd752f18d2a6ef73ebd423b6/msgspec-0.21.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4248cf0b6129b7d230eacd493c17cc2d4f3989f3bb7f633a928a85b7dcfa251", size = 196188, upload-time = "2026-04-12T21:44:07.181Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/074612945c0666078f7366f40000013de9f6ba687491d450df699bceebc9/msgspec-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5102c7e9b3acff82178449b85006d96310e690291bb1ea0142f1b24bcb8aabcb", size = 188473, upload-time = "2026-04-12T21:44:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/655101799590bcc5fddb2bd3fe0e6194e816c2d1da7c361725f5eb89a910/msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920", size = 218871, upload-time = "2026-04-12T21:44:09.917Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/d4cd9fe89c7d400d7a18f86ccc94daa3f0927f53558846fcb60791dce5d6/msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff", size = 225025, upload-time = "2026-04-12T21:44:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/e20549e602b9edccadeeff98760345a416f9cce846a657e8b18e3396b212/msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee", size = 222672, upload-time = "2026-04-12T21:44:12.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/68/04d7a8f0f786545cf9b8c280c57aa6befb5977af6e884b8b54191cbe44b3/msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521", size = 227303, upload-time = "2026-04-12T21:44:13.709Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4d/619866af2840875be408047bf9e70ceafbae6ab50660de7134ed1b25eb86/msgspec-0.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4ab834a054c6f0cbeef6df9e7e1b33d5f1bc7b86dea1d2fd7cad003873e783d", size = 190017, upload-time = "2026-04-12T21:44:14.977Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2e/a8f9eca8fd00e097d7a9e99ba8a4685db994494448e3d4f0b7f6e9a3c0f7/msgspec-0.21.1-cp312-cp312-win_arm64.whl", hash = "sha256:628aaa35c74950a8c59da330d7e98917e1c7188f983745782027748ee4ca573e", size = 175345, upload-time = "2026-04-12T21:44:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/7e/74/f11ede02839b19ff459f88e3145df5d711626ca84da4e23520cebf819367/msgspec-0.21.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:764173717a01743f007e9f74520ed281f24672c604514f7d76c1c3a10e8edb66", size = 196176, upload-time = "2026-04-12T21:44:17.613Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/4476c1bd341418a046c4955aff632ec769315d1e3cb94e6acf86d461f9ed/msgspec-0.21.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:344c7cd0eaed1fb81d7959f99100ef71ec9b536881a376f11b9a6c4803365697", size = 188524, upload-time = "2026-04-12T21:44:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d9/9e9d7d7e5061b47540d03d640fab9b3965ba7ae49c1b2154861c8f007518/msgspec-0.21.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48943e278b3854c2f89f955ddc6f9f430d3f0784b16e47d10604ee0463cd21f5", size = 218880, upload-time = "2026-04-12T21:44:20.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/2bb344f34abb4b57e60c7c9c761994e0417b9718ec1460bf00c296f2a7ea/msgspec-0.21.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9aa659ebb0101b1cbc31461212b87e341d961f0ab0772aaf068a99e001ec4aa", size = 225050, upload-time = "2026-04-12T21:44:21.577Z" }, + { url = "https://files.pythonhosted.org/packages/1a/84/7c1e412f76092277bf760cef12b7979d03314d259ab5b5cafde5d0c1722d/msgspec-0.21.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7b27d1a8ead2b6f5b0c4f2d07b8be1ccfcc041c8a0e704781edebe3ae13c484", size = 222713, upload-time = "2026-04-12T21:44:22.83Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/0bba04b2b4ef05f3d068429410bc71d2cea925f1596a8f41152cccd5edb8/msgspec-0.21.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38fe93e86b61328fe544cb7fd871fad5a27c8734bfda90f65e5dbe288ae50f61", size = 227259, upload-time = "2026-04-12T21:44:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2d/09574b0eea02fed2c2c1383dbaae2c7f79dc16dcd6487a886000afb5d7c4/msgspec-0.21.1-cp313-cp313-win_amd64.whl", hash = "sha256:8bc666331c35fcce05a7cd2d6221adbe0f6058f8e750711413d22793c080ac6a", size = 189857, upload-time = "2026-04-12T21:44:25.359Z" }, + { url = "https://files.pythonhosted.org/packages/46/34/105b1576ad182879914f0c821f17ee1d13abb165cb060448f96fe2aff078/msgspec-0.21.1-cp313-cp313-win_arm64.whl", hash = "sha256:42bb1241e0750c1a4346f2aa84db26c5ffd99a4eb3a954927d9f149ff2f42898", size = 175403, upload-time = "2026-04-12T21:44:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ad/86954e987d1d6a5c579e2c2e7832b65e0fff194179fdac4f581536086024/msgspec-0.21.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fab48eb45fdbfbdb2c0edfec00ffc53b6b6085beefc6b50b61e01659f9f8757f", size = 196261, upload-time = "2026-04-12T21:44:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a1/c5e46c3e42b866199365e35d11dddfd1fbd8bba4fdb3c52f965b1607ce94/msgspec-0.21.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3cb779ea0c35bc807ff941d415875c1f69ca0be91a2e907ab99a171811d86a9a", size = 188729, upload-time = "2026-04-12T21:44:28.99Z" }, + { url = "https://files.pythonhosted.org/packages/85/7d/1e29a319d678d6cb962ae5bdf32a6858ebdf38f73bc654c0e9c742a0c2c8/msgspec-0.21.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68604db36b3b4dd9bf160e436e12798a4738848144cea1aca1cb984011eb160f", size = 219866, upload-time = "2026-04-12T21:44:31.104Z" }, + { url = "https://files.pythonhosted.org/packages/25/1f/cca084ca2572810fff12ea9dbdcbe39eac048f40daf4a9077b49fcbe8cee/msgspec-0.21.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d6b9dc50948eaf65df54d2fd0ff66e6d8c32f116037209ee861810eb9b676cb", size = 224993, upload-time = "2026-04-12T21:44:32.649Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/d2120fc9d419a89a3a7c13e5b7078798c4b392a96a02a6e2b3ce43a8766c/msgspec-0.21.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:52c5e21930942302394429c5a582ce7e6b62c7f983b3760834c2ce107e0dd6df", size = 223535, upload-time = "2026-04-12T21:44:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/75/17/42418b66a3ad972a89bab73dd78b79cc6282bb488a25e73c853cee7443b9/msgspec-0.21.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:abbb39d65681fa24ed394e01af3d59d869068324f900c61d06062b7fb9980f2f", size = 227222, upload-time = "2026-04-12T21:44:35.093Z" }, + { url = "https://files.pythonhosted.org/packages/c4/33/265c894268cca88ff67b144ca2b4c522fc8b9a6f1966a3640c70516e78e1/msgspec-0.21.1-cp314-cp314-win_amd64.whl", hash = "sha256:5666b1b560b97b6ec2eb3fca8a502298ebac56e13bbca1f88523538ce83d01ea", size = 193810, upload-time = "2026-04-12T21:44:36.612Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8f/a6d35f25bf1fc63c492fdd88fdce01ba0875ead48c2b91f90f33653b4131/msgspec-0.21.1-cp314-cp314-win_arm64.whl", hash = "sha256:d8b8578e4c83b14ceea4cef0d0b747e31d9330fe4b03b2b2ad4063866a178f93", size = 179125, upload-time = "2026-04-12T21:44:38.198Z" }, + { url = "https://files.pythonhosted.org/packages/c6/39/74839641e64b99d87da55af0fc472854d42b46e2183b9e2a67fe1bb2a512/msgspec-0.21.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15f523d51c00ebad412213bfe9f06f0a50ec2b93e0c19e824a2d267cabb48ea2", size = 200171, upload-time = "2026-04-12T21:44:39.414Z" }, + { url = "https://files.pythonhosted.org/packages/70/9b/ce0cca6d2d87fcd4b6ff97600790494e64f26a2c55d61507cd2755c16193/msgspec-0.21.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e47390360583ba3d5c6cb44cf0a9f61b0a06a899d3c2c00627cedebb2e2884b", size = 192879, upload-time = "2026-04-12T21:44:40.882Z" }, + { url = "https://files.pythonhosted.org/packages/a7/08/673a7bb05e5702dc787ddd3011195b509f9867927970da59052211929987/msgspec-0.21.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60800e6299b798142dc40b0644da77ceac5ea0568be58228417eae14135c847", size = 226281, upload-time = "2026-04-12T21:44:42.181Z" }, + { url = "https://files.pythonhosted.org/packages/7d/45/86508cf57283e9070b3c447e3ab25b792a7a0855a3ea4e0c6d111ac34c97/msgspec-0.21.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f8e9dfcd98419cf7568808470c4317a3fb30bef0e3715b568730a2b272a20d7", size = 229863, upload-time = "2026-04-12T21:44:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/2c/62/e7c9367cd08d590559faacd711edbae36840342843e669440363f33c7d36/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92d89dfad13bd1ea640dc3e37e724ed380da1030b272bdf5ecafb983c3ad7c75", size = 230445, upload-time = "2026-04-12T21:44:44.806Z" }, + { url = "https://files.pythonhosted.org/packages/42/b4/c0f54632103846b658a10930025f4de41c8724b5e4805a5f3b395586cb7e/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d03867786e5d7ba25d666df4b11320c27170f4aeafcb8e3a8b0a50a4fb742ca", size = 231822, upload-time = "2026-04-12T21:44:46.343Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1d/0d85cc79d0ccf5508e9c846cc66552a6a16bf92abd1dbd8362617f7b35cd/msgspec-0.21.1-cp314-cp314t-win_amd64.whl", hash = "sha256:740fbf1c9d59992ca3537d6fbe9ebbf9eaf726a65fbf31448e0ecbc710697a63", size = 206650, upload-time = "2026-04-12T21:44:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/90/91/56c5d560f20e6c20e9e4f55bd0e458f7f162aa689ee350346c04c48eac0b/msgspec-0.21.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0d2cc73df6058d811a126ac3a8ad63a4dfa210c82f9cf5a004802eaf4712de90", size = 183149, upload-time = "2026-04-12T21:44:48.833Z" }, +] + [[package]] name = "numpy" version = "2.4.4" @@ -196,6 +244,7 @@ name = "y-rig" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "msgspec" }, { name = "nxt-core" }, ] @@ -223,7 +272,10 @@ maya = [ ] [package.metadata] -requires-dist = [{ name = "nxt-core", specifier = ">=0.18.1" }] +requires-dist = [ + { name = "msgspec", specifier = ">=0.21.1" }, + { name = "nxt-core", specifier = ">=0.18.1" }, +] [package.metadata.requires-dev] debug = [{ name = "debugpy", specifier = ">=1.8.19" }] From 9568753d80f8b624096debf869af58d4a19aa3a0 Mon Sep 17 00:00:00 2001 From: Michael Harmon Date: Sat, 16 May 2026 18:43:05 -0600 Subject: [PATCH 2/4] working file combine --- src/yrig/skin/apply.py | 5 +- src/yrig/skin/ng.py | 135 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 128 insertions(+), 12 deletions(-) diff --git a/src/yrig/skin/apply.py b/src/yrig/skin/apply.py index 27e668b..a9f2a5c 100644 --- a/src/yrig/skin/apply.py +++ b/src/yrig/skin/apply.py @@ -86,9 +86,12 @@ def skin_and_apply_weights_from_directory( for i, geo in enumerate(geometry): if skip_skinned_geometry and get_skin_clusters(geo): continue + ng_skin_manifest_filepath: Path = directory / f"{geo}/manifest.json" ng_skin_filepath: Path = directory / f"{geo}.json" yskin_filepath: Path = directory / f"{geo}.yskin" - if ng_skin_filepath.exists(): + if ng_skin_manifest_filepath.exists(): + skin_and_apply_ng_weights(ng_skin_manifest_filepath, geo) + elif ng_skin_filepath.exists(): skin_and_apply_ng_weights(ng_skin_filepath, geo) elif yskin_filepath.exists(): skin_and_apply_weights(yskin_filepath, geo) diff --git a/src/yrig/skin/ng.py b/src/yrig/skin/ng.py index 5140281..d481fd8 100644 --- a/src/yrig/skin/ng.py +++ b/src/yrig/skin/ng.py @@ -1,10 +1,13 @@ -import json import logging +import tempfile from functools import wraps from pathlib import Path from typing import TYPE_CHECKING, Callable, ParamSpec, TypeVar import maya.cmds as cmds +import msgspec + +from yrig.name import get_short_name, normalize_name log = logging.getLogger(__name__) @@ -118,7 +121,7 @@ def apply_ng_skin_weights(weights_file: Path, geometry: str) -> None: transfer mode, so the topology of the target mesh must match the file. Args: - weights_file: The JSON weights file to read. + weights_file: The JSON weights file to read (either a single file, or the manifest for multi-file). geometry: The transform, shape, or skinCluster Node to apply to. """ config = ng.influenceMapping.InfluenceMappingConfig() @@ -128,13 +131,24 @@ def apply_ng_skin_weights(weights_file: Path, geometry: str) -> None: if not weights_file.exists(): raise RuntimeError(f"{weights_file} doesn't exist, unable to load weights.") - # Run the import - ng.import_json( - target=geometry, - file=str(weights_file), - vertex_transfer_mode=ng.transfer.VertexTransferMode.vertexId, - influences_mapping_config=config, - ) + if weights_file.name == "manifest.json": + with tempfile.NamedTemporaryFile() as file: + filepath = Path(file.name) + combine_ng_skin_file_by_layers(weights_file, filepath) + print(file.read(4096)) + ng.import_json( + target=geometry, + file=str(filepath), + vertex_transfer_mode=ng.transfer.VertexTransferMode.vertexId, + influences_mapping_config=config, + ) + else: + ng.import_json( + target=geometry, + file=str(weights_file), + vertex_transfer_mode=ng.transfer.VertexTransferMode.vertexId, + influences_mapping_config=config, + ) @require_ng_skin @@ -181,8 +195,11 @@ def get_influences_from_ng_skin_weights( """ if not filepath.exists(): raise RuntimeError(f"{filepath} doesn't exist, unable to load weights.") - with open(filepath) as file: - data: dict = json.loads(file.read()) + if filepath.name == "manifest.json": + data = _combine_ng_skin_file_layers_data(filepath) + else: + with open(filepath, mode="rb") as file: + data: dict = msgspec.json.decode(file.read()) return [influence["path"] for influence in data["influences"]] @@ -202,3 +219,99 @@ def cleanup_ng_data_nodes() -> None: log.info( f"Removed {len(ng_data_nodes)} ngst2SkinLayerData node(s) from the scene: {ng_data_nodes}" ) + + +def split_ng_skin_file_by_layers(skin_file: Path, output_path: Path) -> None: + with open(skin_file, mode="rb") as file: + data: dict = msgspec.json.decode(file.read()) + mesh: dict[str, list] = data["mesh"] + influences: list[dict] = data["influences"] + layers: list[dict] = data["layers"] + + manifest_data: dict = {"manifest": True, "layers": []} + layer_file_map: dict[Path, dict] = {} + layer_id_map: dict[int, dict] = {} + for layer in layers: + layer_id: int = layer["id"] + layer_id_map[layer_id] = layer + layer_name: str = layer["name"] + + if layer["parentId"] is None: + layer_filepath = Path(f"{normalize_name(layer_name)}.nglayer") + layer_file_map[layer_filepath] = layer + manifest_data["layers"].append( + {"id": layer_id, "name": layer_name, "path": layer_filepath.as_posix()} + ) + + output_path.mkdir(parents=True, exist_ok=True) + with open(output_path / "manifest.json", mode="wb") as file: + encoded = msgspec.json.encode(manifest_data) + formatted = msgspec.json.format(encoded) + file.write(formatted) + + output_path.mkdir(parents=True, exist_ok=True) + with open(output_path / "mesh_data.json", mode="wb") as file: + encoded = msgspec.json.encode({"mesh": mesh}) + file.write(encoded) + + for filepath, layer in layer_file_map.items(): + file_layers = [layer] + layer_children: list[int] = layer["children"] + file_layers.extend(layer_id_map[layer_id] for layer_id in layer_children) + with open(output_path / filepath, mode="wb") as file: + encoded = msgspec.json.encode({"influences": influences, "layers": file_layers}) + file.write(encoded) + + +def _combine_ng_skin_file_layers_data(manifest: Path) -> dict: + with open(manifest, mode="rb") as file: + manifest_data: dict = msgspec.json.decode(file.read()) + + with open(manifest.parent / "mesh_data.json", mode="rb") as file: + mesh_data: dict = msgspec.json.decode(file.read()) + + merged_influences: list[dict] = [] + merged_layers: list[dict] = [] + + influence_name_to_index: dict[str, int] = {} # name -> new canonical ID + next_influence_id: int = 0 + + for layer_entry in manifest_data["layers"]: + layer_filepath = Path(layer_entry["path"]) + with open(manifest.parent / layer_filepath, mode="rb") as file: + layer_data: dict = msgspec.json.decode(file.read()) + + local_influences: list[dict] = layer_data["influences"] + local_layers: list[dict] = layer_data["layers"] + + # Build a remap from this file's influence index -> merged indices + local_index_to_merged: dict[int, int] = {} + for influence in local_influences: + name: str = get_short_name(influence["path"]) + local_index: int = influence["index"] + if name not in influence_name_to_index: + influence_name_to_index[name] = next_influence_id + merged_influences.append({**influence, "index": next_influence_id}) + next_influence_id += 1 + local_index_to_merged[local_index] = influence_name_to_index[name] + + for layer in local_layers: + layer["influences"] = { + str(local_index_to_merged[int(k)]): v for k, v in layer["influences"].items() + } + merged_layers.append(layer) + + combined: dict = { + "mesh": mesh_data["mesh"], + "influences": merged_influences, + "layers": merged_layers, + } + + return combined + + +def combine_ng_skin_file_by_layers(manifest: Path, output_file: Path) -> None: + combined = _combine_ng_skin_file_layers_data(manifest) + with open(output_file, mode="wb") as file: + encoded = msgspec.json.encode(combined) + file.write(encoded) From a0605beb052bff196a653c3350bf61cd5b00ce25 Mon Sep 17 00:00:00 2001 From: Michael Harmon Date: Sat, 16 May 2026 19:00:56 -0600 Subject: [PATCH 3/4] cleanup of multi file skin data functions --- src/yrig/skin/apply.py | 19 ++++---- src/yrig/skin/ng.py | 99 +++++++++++++++++++----------------------- 2 files changed, 56 insertions(+), 62 deletions(-) diff --git a/src/yrig/skin/apply.py b/src/yrig/skin/apply.py index a9f2a5c..aae3062 100644 --- a/src/yrig/skin/apply.py +++ b/src/yrig/skin/apply.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Callable, Sequence +from typing import Any, Callable, Iterable, Sequence from maya import cmds @@ -13,6 +13,13 @@ log = logging.getLogger(__name__) +def _resolve_valid_influences(influences: Iterable[str]) -> tuple[list[str], set[str]]: + influence_names = [get_short_name(influence) for influence in influences] + valid_influences = [influence for influence in influence_names if cmds.objExists(influence)] + missing_influences = set(influence_names) - set(valid_influences) + return valid_influences, missing_influences + + def skin_and_apply_weights(filepath: Path, geometry: str) -> str: """ Skin geometry (any type) using influences from a ``.yskin`` file and apply weights. @@ -21,8 +28,7 @@ def skin_and_apply_weights(filepath: Path, geometry: str) -> str: """ skin_weight_data = skin_weight_data_from_file(filepath) influence_names = skin_weight_data.influences - valid_influences = [j for j in influence_names if cmds.objExists(j)] - missing_influences = set(influence_names) - set(valid_influences) + valid_influences, missing_influences = _resolve_valid_influences(influence_names) if missing_influences: log.warning( f"[{geometry}] Missing {len(missing_influences)} influence(s) that were defined in its skin file : {sorted(missing_influences)}" @@ -46,10 +52,7 @@ def skin_and_apply_ng_weights(filepath: Path, mesh: str) -> str: if not filepath.exists(): raise FileNotFoundError(f"{filepath} doesn't exist") influence_paths = get_influences_from_ng_skin_weights(filepath) - influence_names = [get_short_name(path) for path in influence_paths] - # Filter to joints that actually exist in scene - valid_influences = [j for j in influence_names if cmds.objExists(j)] - missing_influences = set(influence_names) - set(valid_influences) + valid_influences, missing_influences = _resolve_valid_influences(influence_paths) if missing_influences: log.warning( f"[{mesh}] Missing {len(missing_influences)} influence(s) that were defined in its skin file : {sorted(missing_influences)}" @@ -98,4 +101,4 @@ def skin_and_apply_weights_from_directory( else: if fallback_skinning is not None: fallback_skinning(geo) - progress.update_progress(i / total) + progress.update_progress((i + 1) / total) diff --git a/src/yrig/skin/ng.py b/src/yrig/skin/ng.py index d481fd8..c9b1f5c 100644 --- a/src/yrig/skin/ng.py +++ b/src/yrig/skin/ng.py @@ -8,6 +8,7 @@ import msgspec from yrig.name import get_short_name, normalize_name +from yrig.util import confirm_overwrite log = logging.getLogger(__name__) @@ -113,6 +114,46 @@ def get_or_create_ng_layer(skin_cluster: str, layer_name: str) -> ng.Layer: return new_layer +def _load_ng_skin_data(filepath: Path) -> dict: + if filepath.name == "manifest.json": + return _combine_ng_skin_file_layers_data(filepath) + with open(filepath, "rb") as f: + return msgspec.json.decode(f.read()) + + +def get_influences_from_ng_skin_weights( + filepath: Path, +) -> list[str]: + """Return influence paths from an ngSkinTools2 JSON weights file. + + Args: + filepath: Path to the weights file. + """ + if not filepath.exists(): + raise RuntimeError(f"{filepath} doesn't exist, unable to load data.") + data = _load_ng_skin_data(filepath) + return [influence["path"] for influence in data["influences"]] + + +def _run_ng_import(filepath: Path, geometry: str) -> None: + config = ng.influenceMapping.InfluenceMappingConfig() + config.use_distance_matching = False + config.use_name_matching = True + ng.import_json( + target=geometry, + file=str(filepath), + vertex_transfer_mode=ng.transfer.VertexTransferMode.vertexId, + influences_mapping_config=config, + ) + + +def _apply_ng_skin_from_manifest(manifest: Path, geometry: str) -> None: + with tempfile.NamedTemporaryFile(suffix=".json") as file: + temp_path = Path(file.name) + combine_ng_skin_file_by_layers(manifest, temp_path) + _run_ng_import(temp_path, geometry) + + @require_ng_skin def apply_ng_skin_weights(weights_file: Path, geometry: str) -> None: """Apply an ngSkinTools2 JSON weights file to the specified geometry. @@ -132,23 +173,9 @@ def apply_ng_skin_weights(weights_file: Path, geometry: str) -> None: raise RuntimeError(f"{weights_file} doesn't exist, unable to load weights.") if weights_file.name == "manifest.json": - with tempfile.NamedTemporaryFile() as file: - filepath = Path(file.name) - combine_ng_skin_file_by_layers(weights_file, filepath) - print(file.read(4096)) - ng.import_json( - target=geometry, - file=str(filepath), - vertex_transfer_mode=ng.transfer.VertexTransferMode.vertexId, - influences_mapping_config=config, - ) + _apply_ng_skin_from_manifest(weights_file, geometry) else: - ng.import_json( - target=geometry, - file=str(weights_file), - vertex_transfer_mode=ng.transfer.VertexTransferMode.vertexId, - influences_mapping_config=config, - ) + _run_ng_import(weights_file, geometry) @require_ng_skin @@ -162,45 +189,9 @@ def write_ng_skin_weights(filepath: Path, geometry: str, force: bool = False) -> force: If True, will automatically overwrite any existing file at the filepath specified. """ - - # If the file exists, only write it if force = True, or after asking for confirmation. - if filepath.exists(): - if force: - pass - else: - confirm: str = cmds.confirmDialog( - title="File Overwrite", - message=f"{filepath} already exists and will be overwritten, are you sure you want to write the file?", - button=["Yes", "No"], - defaultButton="Yes", - cancelButton="No", - dismissString="No", - ) - if confirm == "Yes": - pass - else: - return - + if not confirm_overwrite(filepath): + return ng.export_json(target=geometry, file=str(filepath)) - return - - -def get_influences_from_ng_skin_weights( - filepath: Path, -) -> list[str]: - """Return influence paths from an ngSkinTools2 JSON weights file. - - Args: - filepath: Path to the weights file. - """ - if not filepath.exists(): - raise RuntimeError(f"{filepath} doesn't exist, unable to load weights.") - if filepath.name == "manifest.json": - data = _combine_ng_skin_file_layers_data(filepath) - else: - with open(filepath, mode="rb") as file: - data: dict = msgspec.json.decode(file.read()) - return [influence["path"] for influence in data["influences"]] @require_ng_skin From 17d0451be1866a60264c00b322b5707ae94d31bf Mon Sep 17 00:00:00 2001 From: Michael Harmon Date: Sat, 16 May 2026 19:16:46 -0600 Subject: [PATCH 4/4] add ability to only save certain layers --- src/yrig/skin/ng.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/yrig/skin/ng.py b/src/yrig/skin/ng.py index c9b1f5c..795ca84 100644 --- a/src/yrig/skin/ng.py +++ b/src/yrig/skin/ng.py @@ -212,7 +212,9 @@ def cleanup_ng_data_nodes() -> None: ) -def split_ng_skin_file_by_layers(skin_file: Path, output_path: Path) -> None: +def split_ng_skin_file_by_layers( + skin_file: Path, output_path: Path, layers_to_write: set[str] | None = None +) -> None: with open(skin_file, mode="rb") as file: data: dict = msgspec.json.decode(file.read()) mesh: dict[str, list] = data["mesh"] @@ -229,10 +231,13 @@ def split_ng_skin_file_by_layers(skin_file: Path, output_path: Path) -> None: if layer["parentId"] is None: layer_filepath = Path(f"{normalize_name(layer_name)}.nglayer") - layer_file_map[layer_filepath] = layer manifest_data["layers"].append( {"id": layer_id, "name": layer_name, "path": layer_filepath.as_posix()} ) + if layers_to_write is None: + layer_file_map[layer_filepath] = layer + elif layer["name"] in layers_to_write: + layer_file_map[layer_filepath] = layer output_path.mkdir(parents=True, exist_ok=True) with open(output_path / "manifest.json", mode="wb") as file: @@ -282,7 +287,9 @@ def _combine_ng_skin_file_layers_data(manifest: Path) -> dict: local_index: int = influence["index"] if name not in influence_name_to_index: influence_name_to_index[name] = next_influence_id - merged_influences.append({**influence, "index": next_influence_id}) + new_influence = influence.copy() + new_influence["index"] = next_influence_id + merged_influences.append(new_influence) next_influence_id += 1 local_index_to_merged[local_index] = influence_name_to_index[name]