From 038ba502e52d82eed0eb4dde3ac25f67628920b2 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 9 Apr 2025 14:48:23 -0700 Subject: [PATCH 01/25] Switch to the meson python wheel backend --- meson.build | 19 +++- pyproject.toml | 26 ++---- quick_compile.bat | 9 +- src/maya/meson.build | 6 +- src/python/meson.build | 35 ++++++-- {scripts => src/python}/simplexui/__init__.py | 3 +- src/python/simplexui/_version.py.in | 1 + .../python}/simplexui/channelBox.py | 0 .../python}/simplexui/comboCheckDialog.py | 0 .../python}/simplexui/commands/__init__.py | 0 .../simplexui/commands/alembicCommon.py | 0 .../simplexui/commands/applyCorrectives.py | 0 .../python}/simplexui/commands/buildIceXML.py | 0 .../simplexui/commands/correctiveInterface.py | 0 .../simplexui/commands/expandedExport.py | 0 .../python}/simplexui/commands/hdf5Convert.py | 0 .../commands/mayaCorrectiveInterface.py | 0 .../python}/simplexui/commands/mesh.py | 0 .../commands/reorderSimplexPoints.py | 0 .../python}/simplexui/commands/rigidAlign.py | 0 .../simplexui/commands/setDelta.xsicompound | 0 .../python}/simplexui/commands/smpxBlend.py | 0 .../python}/simplexui/commands/unsubdivide.py | 0 .../python}/simplexui/commands/uvTransfer.py | 0 .../commands/xsiCorrectiveInterface.py | 0 .../python}/simplexui/dragFilter.py | 0 .../python}/simplexui/falloffDialog.py | 0 .../python}/simplexui/img/ChefHead.png | Bin .../python}/simplexui/img/frozen.png | Bin .../python}/simplexui/interface/__init__.py | 0 .../simplexui/interface/dummyInterface.py | 0 .../simplexui/interface/mayaInterface.py | 0 .../simplexui/interface/xsiInterface.py | 0 .../python}/simplexui/interfaceModel.py | 0 .../python}/simplexui/interfaceModelTrees.py | 0 .../python}/simplexui/interfaceModel_Test.py | 0 .../python}/simplexui/items/__init__.py | 0 .../python}/simplexui/items/accessor.py | 0 .../python}/simplexui/items/combo.py | 0 .../python}/simplexui/items/falloff.py | 0 .../python}/simplexui/items/group.py | 0 .../python}/simplexui/items/progression.py | 0 .../python}/simplexui/items/shape.py | 0 .../python}/simplexui/items/simplex.py | 0 .../python}/simplexui/items/slider.py | 0 .../python}/simplexui/items/stack.py | 0 .../python}/simplexui/items/traversal.py | 0 {scripts => src/python}/simplexui/main.pyw | 0 .../python}/simplexui/menu/__init__.py | 0 .../simplexui/menu/dummyPlugins/__init__.py | 0 .../simplexui/menu/genericPlugins/__init__.py | 0 .../menu/genericPlugins/_builtins.py | 0 .../genericPlugins/checkPossibleCombos.py | 0 .../menu/genericPlugins/exportSplit.py | 0 .../menu/genericPlugins/showFalloffs.py | 0 .../menu/genericPlugins/showTraversals.py | 0 .../menu/genericPlugins/simplexUvTransfer.py | 0 .../menu/genericPlugins/unsubdivide.py | 0 .../simplexui/menu/mayaPlugins/__init__.py | 0 .../simplexui/menu/mayaPlugins/exportOther.py | 0 .../menu/mayaPlugins/extractProgressives.py | 0 .../simplexui/menu/mayaPlugins/freezeCombo.py | 0 .../mayaPlugins/generateShapeIncrementals.py | 0 .../simplexui/menu/mayaPlugins/importObjs.py | 0 .../menu/mayaPlugins/linearizeTraversal.py | 0 .../menu/mayaPlugins/makeShelfBtn.py | 0 .../menu/mayaPlugins/relaxToSelection.py | 0 .../menu/mayaPlugins/reloadDefinition.py | 0 .../menu/mayaPlugins/snapToNeutral.py | 0 .../menu/mayaPlugins/softSelectToCluster.py | 0 .../simplexui/menu/mayaPlugins/tweakMix.py | 0 .../menu/mayaPlugins/updateRestShape.py | 0 .../simplexui/menu/xsiPlugins/__init__.py | 0 src/python/simplexui/meson.build | 82 ++++++++++++++++++ .../python}/simplexui/simplexDialog.py | 0 .../python}/simplexui/travCheckDialog.py | 0 .../python}/simplexui/traversalDialog.py | 0 .../python}/simplexui/ui/comboCheckDialog.ui | 0 .../python}/simplexui/ui/falloffDialog.ui | 0 .../python}/simplexui/ui/simplexDialog.ui | 0 .../python}/simplexui/ui/travCheckDialog.ui | 0 .../python}/simplexui/ui/traversalDialog.ui | 0 {scripts => src/python}/simplexui/utils.py | 0 83 files changed, 145 insertions(+), 36 deletions(-) rename {scripts => src/python}/simplexui/__init__.py (97%) create mode 100644 src/python/simplexui/_version.py.in rename {scripts => src/python}/simplexui/channelBox.py (100%) rename {scripts => src/python}/simplexui/comboCheckDialog.py (100%) rename {scripts => src/python}/simplexui/commands/__init__.py (100%) rename {scripts => src/python}/simplexui/commands/alembicCommon.py (100%) rename {scripts => src/python}/simplexui/commands/applyCorrectives.py (100%) rename {scripts => src/python}/simplexui/commands/buildIceXML.py (100%) rename {scripts => src/python}/simplexui/commands/correctiveInterface.py (100%) rename {scripts => src/python}/simplexui/commands/expandedExport.py (100%) rename {scripts => src/python}/simplexui/commands/hdf5Convert.py (100%) rename {scripts => src/python}/simplexui/commands/mayaCorrectiveInterface.py (100%) rename {scripts => src/python}/simplexui/commands/mesh.py (100%) rename {scripts => src/python}/simplexui/commands/reorderSimplexPoints.py (100%) rename {scripts => src/python}/simplexui/commands/rigidAlign.py (100%) rename {scripts => src/python}/simplexui/commands/setDelta.xsicompound (100%) rename {scripts => src/python}/simplexui/commands/smpxBlend.py (100%) rename {scripts => src/python}/simplexui/commands/unsubdivide.py (100%) rename {scripts => src/python}/simplexui/commands/uvTransfer.py (100%) rename {scripts => src/python}/simplexui/commands/xsiCorrectiveInterface.py (100%) rename {scripts => src/python}/simplexui/dragFilter.py (100%) rename {scripts => src/python}/simplexui/falloffDialog.py (100%) rename {scripts => src/python}/simplexui/img/ChefHead.png (100%) rename {scripts => src/python}/simplexui/img/frozen.png (100%) rename {scripts => src/python}/simplexui/interface/__init__.py (100%) rename {scripts => src/python}/simplexui/interface/dummyInterface.py (100%) rename {scripts => src/python}/simplexui/interface/mayaInterface.py (100%) rename {scripts => src/python}/simplexui/interface/xsiInterface.py (100%) rename {scripts => src/python}/simplexui/interfaceModel.py (100%) rename {scripts => src/python}/simplexui/interfaceModelTrees.py (100%) rename {scripts => src/python}/simplexui/interfaceModel_Test.py (100%) rename {scripts => src/python}/simplexui/items/__init__.py (100%) rename {scripts => src/python}/simplexui/items/accessor.py (100%) rename {scripts => src/python}/simplexui/items/combo.py (100%) rename {scripts => src/python}/simplexui/items/falloff.py (100%) rename {scripts => src/python}/simplexui/items/group.py (100%) rename {scripts => src/python}/simplexui/items/progression.py (100%) rename {scripts => src/python}/simplexui/items/shape.py (100%) rename {scripts => src/python}/simplexui/items/simplex.py (100%) rename {scripts => src/python}/simplexui/items/slider.py (100%) rename {scripts => src/python}/simplexui/items/stack.py (100%) rename {scripts => src/python}/simplexui/items/traversal.py (100%) rename {scripts => src/python}/simplexui/main.pyw (100%) rename {scripts => src/python}/simplexui/menu/__init__.py (100%) rename {scripts => src/python}/simplexui/menu/dummyPlugins/__init__.py (100%) rename {scripts => src/python}/simplexui/menu/genericPlugins/__init__.py (100%) rename {scripts => src/python}/simplexui/menu/genericPlugins/_builtins.py (100%) rename {scripts => src/python}/simplexui/menu/genericPlugins/checkPossibleCombos.py (100%) rename {scripts => src/python}/simplexui/menu/genericPlugins/exportSplit.py (100%) rename {scripts => src/python}/simplexui/menu/genericPlugins/showFalloffs.py (100%) rename {scripts => src/python}/simplexui/menu/genericPlugins/showTraversals.py (100%) rename {scripts => src/python}/simplexui/menu/genericPlugins/simplexUvTransfer.py (100%) rename {scripts => src/python}/simplexui/menu/genericPlugins/unsubdivide.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/__init__.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/exportOther.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/extractProgressives.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/freezeCombo.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/generateShapeIncrementals.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/importObjs.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/linearizeTraversal.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/makeShelfBtn.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/relaxToSelection.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/reloadDefinition.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/snapToNeutral.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/softSelectToCluster.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/tweakMix.py (100%) rename {scripts => src/python}/simplexui/menu/mayaPlugins/updateRestShape.py (100%) rename {scripts => src/python}/simplexui/menu/xsiPlugins/__init__.py (100%) create mode 100644 src/python/simplexui/meson.build rename {scripts => src/python}/simplexui/simplexDialog.py (100%) rename {scripts => src/python}/simplexui/travCheckDialog.py (100%) rename {scripts => src/python}/simplexui/traversalDialog.py (100%) rename {scripts => src/python}/simplexui/ui/comboCheckDialog.ui (100%) rename {scripts => src/python}/simplexui/ui/falloffDialog.ui (100%) rename {scripts => src/python}/simplexui/ui/simplexDialog.ui (100%) rename {scripts => src/python}/simplexui/ui/travCheckDialog.ui (100%) rename {scripts => src/python}/simplexui/ui/traversalDialog.ui (100%) rename {scripts => src/python}/simplexui/utils.py (100%) diff --git a/meson.build b/meson.build index 52228c36..58128086 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,17 @@ -project('Simplex', 'cpp', default_options: ['cpp_std=c++20']) - +project( + 'Simplex', + 'cpp', + meson_version : '>=1.6.0', + version : configure_file( + capture : true, + command : [ + find_program('git', native: true, required: true), + 'describe', '--tags', '--always', '--match', 'v[0-9]*', '--abbrev=0' + ], + output : 'version.txt', + ), + default_options: ['cpp_std=c++20'], +) maya_build = get_option('maya_build') python_build = get_option('python_build') @@ -8,6 +20,9 @@ if not maya_build and not python_build error('No builds requested') endif +conf_data = configuration_data() +conf_data.set('VCS_TAG', meson.project_version()) + subdir('src/simplexlib') if maya_build maya_dep = dependency('maya') diff --git a/pyproject.toml b/pyproject.toml index f83b8bf7..8d13c6ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,27 +31,15 @@ dependencies = [ "Project Page" = "https://github.com/blurstudio/simplex" [build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel.sources] -"scripts" = "" -"output_Python" = "" - -[tool.hatch.build.targets.wheel] -only-packages = false -artifacts = [ - "*.so", - "*.pyd", - "!*.lib", -] -only-include = [ - "scripts/simplexui", - "output_Python", +build-backend = "mesonpy" +requires = [ + "meson-python>=0.15.0", + "meson >= 1.6.0", ] -[tool.hatch.version] -path = "scripts/simplexui/__init__.py" +[tool.meson-python.args] +setup = ['-Dmaya_build=false', '-Dpython_build=true'] +install = ['--skip-subprojects'] [tool.ruff] # Exclude a variety of commonly ignored directories. diff --git a/quick_compile.bat b/quick_compile.bat index 0e2b3944..f6c0adab 100644 --- a/quick_compile.bat +++ b/quick_compile.bat @@ -10,12 +10,15 @@ SET BUILDTYPE=release SET BUILDDIR=mayabuild_%BUILDTYPE%_%MAYA_VERSION%_%BACKEND% if not exist %BUILDDIR%\ ( - meson setup %BUILDDIR% -Dmaya:maya_version=%MAYA_VERSION% --buildtype %BUILDTYPE% --vsenv --backend %BACKEND% + REM meson setup %BUILDDIR% -Dmaya:maya_version=%MAYA_VERSION% --buildtype %BUILDTYPE% --vsenv --backend %BACKEND% + meson setup %BUILDDIR% -Dmaya_build=false -Dpython_build=true -Dmaya:maya_version=%MAYA_VERSION% --buildtype %BUILDTYPE% --vsenv --backend %BACKEND% ) + + if exist %BUILDDIR%\ ( - meson compile -C %BUILDDIR% - meson install --skip-subprojects -C %BUILDDIR% + REM meson compile -C %BUILDDIR% + REM meson install --skip-subprojects -C %BUILDDIR% ) pause diff --git a/src/maya/meson.build b/src/maya/meson.build index 54c308f3..36fac10b 100644 --- a/src/maya/meson.build +++ b/src/maya/meson.build @@ -12,12 +12,10 @@ fs = import('fs') if fs.is_file('src/version.h') message('Using existing version.h') else - git = find_program('git', native: true, required: true) - version_h = vcs_tag( - command: [git, 'describe', '--tags', '--always', '--match', 'v[0-9]*', '--dirty=+'], - fallback: 'v0.0.1', + version_h = configure_file( input: 'src/version.h.in', output: 'version.h', + configuration : conf_data, ) simplex_maya_files = simplex_maya_files + version_h endif diff --git a/src/python/meson.build b/src/python/meson.build index 7aff3a2f..6f8ee5a2 100644 --- a/src/python/meson.build +++ b/src/python/meson.build @@ -1,6 +1,6 @@ -py = import('python') -py_inst = py.find_installation('python3') -py_dep = py_inst.dependency() +pymod = import('python') +py = pymod.find_installation('python3', pure : false) +py_dep = py.dependency() pysimplex_files = files(['pysimplex.cpp']) rapidjson_dep = dependency('rapidjson') @@ -10,11 +10,34 @@ if get_option('buildtype') == 'debug' lapi = '' endif -pysimplex = py_inst.extension_module( + +fs = import('fs') +if fs.is_file('simplexui/_version.py') + message('Using existing _version.py') + py.install_sources( + 'simplexui/_version.py', + subdir:'simplexui', + ) +else + version_py = configure_file( + input: 'simplexui/_version.py.in', + output: '_version.py', + install_dir: py.get_install_dir() / 'simplexui', + configuration : conf_data, + ) + py.install_sources( + version_py, + subdir:'simplexui', + ) +endif + +pysimplex = py.extension_module( 'pysimplex', pysimplex_files, - dependencies : simplexlib_dep, + dependencies : [simplexlib_dep, rapidjson_dep], install: true, - install_dir : meson.global_source_root() / 'output_Python', limited_api : lapi, + subdir : 'simplexui', ) + +subdir('simplexui') diff --git a/scripts/simplexui/__init__.py b/src/python/simplexui/__init__.py similarity index 97% rename from scripts/simplexui/__init__.py rename to src/python/simplexui/__init__.py index 4f2222fe..0814d8ec 100644 --- a/scripts/simplexui/__init__.py +++ b/src/python/simplexui/__init__.py @@ -16,11 +16,10 @@ # along with Simplex. If not, see . from __future__ import absolute_import +from _version import __version__ SIMPLEX_UI = None SIMPLEX_UI_ROOT = None -__version__ = "v0.0.1-dev" - def runSimplexUI(): from .interface import DISPATCH, rootWindow diff --git a/src/python/simplexui/_version.py.in b/src/python/simplexui/_version.py.in new file mode 100644 index 00000000..d1117b81 --- /dev/null +++ b/src/python/simplexui/_version.py.in @@ -0,0 +1 @@ +__version__ = "@VCS_TAG@" diff --git a/scripts/simplexui/channelBox.py b/src/python/simplexui/channelBox.py similarity index 100% rename from scripts/simplexui/channelBox.py rename to src/python/simplexui/channelBox.py diff --git a/scripts/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py similarity index 100% rename from scripts/simplexui/comboCheckDialog.py rename to src/python/simplexui/comboCheckDialog.py diff --git a/scripts/simplexui/commands/__init__.py b/src/python/simplexui/commands/__init__.py similarity index 100% rename from scripts/simplexui/commands/__init__.py rename to src/python/simplexui/commands/__init__.py diff --git a/scripts/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py similarity index 100% rename from scripts/simplexui/commands/alembicCommon.py rename to src/python/simplexui/commands/alembicCommon.py diff --git a/scripts/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py similarity index 100% rename from scripts/simplexui/commands/applyCorrectives.py rename to src/python/simplexui/commands/applyCorrectives.py diff --git a/scripts/simplexui/commands/buildIceXML.py b/src/python/simplexui/commands/buildIceXML.py similarity index 100% rename from scripts/simplexui/commands/buildIceXML.py rename to src/python/simplexui/commands/buildIceXML.py diff --git a/scripts/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py similarity index 100% rename from scripts/simplexui/commands/correctiveInterface.py rename to src/python/simplexui/commands/correctiveInterface.py diff --git a/scripts/simplexui/commands/expandedExport.py b/src/python/simplexui/commands/expandedExport.py similarity index 100% rename from scripts/simplexui/commands/expandedExport.py rename to src/python/simplexui/commands/expandedExport.py diff --git a/scripts/simplexui/commands/hdf5Convert.py b/src/python/simplexui/commands/hdf5Convert.py similarity index 100% rename from scripts/simplexui/commands/hdf5Convert.py rename to src/python/simplexui/commands/hdf5Convert.py diff --git a/scripts/simplexui/commands/mayaCorrectiveInterface.py b/src/python/simplexui/commands/mayaCorrectiveInterface.py similarity index 100% rename from scripts/simplexui/commands/mayaCorrectiveInterface.py rename to src/python/simplexui/commands/mayaCorrectiveInterface.py diff --git a/scripts/simplexui/commands/mesh.py b/src/python/simplexui/commands/mesh.py similarity index 100% rename from scripts/simplexui/commands/mesh.py rename to src/python/simplexui/commands/mesh.py diff --git a/scripts/simplexui/commands/reorderSimplexPoints.py b/src/python/simplexui/commands/reorderSimplexPoints.py similarity index 100% rename from scripts/simplexui/commands/reorderSimplexPoints.py rename to src/python/simplexui/commands/reorderSimplexPoints.py diff --git a/scripts/simplexui/commands/rigidAlign.py b/src/python/simplexui/commands/rigidAlign.py similarity index 100% rename from scripts/simplexui/commands/rigidAlign.py rename to src/python/simplexui/commands/rigidAlign.py diff --git a/scripts/simplexui/commands/setDelta.xsicompound b/src/python/simplexui/commands/setDelta.xsicompound similarity index 100% rename from scripts/simplexui/commands/setDelta.xsicompound rename to src/python/simplexui/commands/setDelta.xsicompound diff --git a/scripts/simplexui/commands/smpxBlend.py b/src/python/simplexui/commands/smpxBlend.py similarity index 100% rename from scripts/simplexui/commands/smpxBlend.py rename to src/python/simplexui/commands/smpxBlend.py diff --git a/scripts/simplexui/commands/unsubdivide.py b/src/python/simplexui/commands/unsubdivide.py similarity index 100% rename from scripts/simplexui/commands/unsubdivide.py rename to src/python/simplexui/commands/unsubdivide.py diff --git a/scripts/simplexui/commands/uvTransfer.py b/src/python/simplexui/commands/uvTransfer.py similarity index 100% rename from scripts/simplexui/commands/uvTransfer.py rename to src/python/simplexui/commands/uvTransfer.py diff --git a/scripts/simplexui/commands/xsiCorrectiveInterface.py b/src/python/simplexui/commands/xsiCorrectiveInterface.py similarity index 100% rename from scripts/simplexui/commands/xsiCorrectiveInterface.py rename to src/python/simplexui/commands/xsiCorrectiveInterface.py diff --git a/scripts/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py similarity index 100% rename from scripts/simplexui/dragFilter.py rename to src/python/simplexui/dragFilter.py diff --git a/scripts/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py similarity index 100% rename from scripts/simplexui/falloffDialog.py rename to src/python/simplexui/falloffDialog.py diff --git a/scripts/simplexui/img/ChefHead.png b/src/python/simplexui/img/ChefHead.png similarity index 100% rename from scripts/simplexui/img/ChefHead.png rename to src/python/simplexui/img/ChefHead.png diff --git a/scripts/simplexui/img/frozen.png b/src/python/simplexui/img/frozen.png similarity index 100% rename from scripts/simplexui/img/frozen.png rename to src/python/simplexui/img/frozen.png diff --git a/scripts/simplexui/interface/__init__.py b/src/python/simplexui/interface/__init__.py similarity index 100% rename from scripts/simplexui/interface/__init__.py rename to src/python/simplexui/interface/__init__.py diff --git a/scripts/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py similarity index 100% rename from scripts/simplexui/interface/dummyInterface.py rename to src/python/simplexui/interface/dummyInterface.py diff --git a/scripts/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py similarity index 100% rename from scripts/simplexui/interface/mayaInterface.py rename to src/python/simplexui/interface/mayaInterface.py diff --git a/scripts/simplexui/interface/xsiInterface.py b/src/python/simplexui/interface/xsiInterface.py similarity index 100% rename from scripts/simplexui/interface/xsiInterface.py rename to src/python/simplexui/interface/xsiInterface.py diff --git a/scripts/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py similarity index 100% rename from scripts/simplexui/interfaceModel.py rename to src/python/simplexui/interfaceModel.py diff --git a/scripts/simplexui/interfaceModelTrees.py b/src/python/simplexui/interfaceModelTrees.py similarity index 100% rename from scripts/simplexui/interfaceModelTrees.py rename to src/python/simplexui/interfaceModelTrees.py diff --git a/scripts/simplexui/interfaceModel_Test.py b/src/python/simplexui/interfaceModel_Test.py similarity index 100% rename from scripts/simplexui/interfaceModel_Test.py rename to src/python/simplexui/interfaceModel_Test.py diff --git a/scripts/simplexui/items/__init__.py b/src/python/simplexui/items/__init__.py similarity index 100% rename from scripts/simplexui/items/__init__.py rename to src/python/simplexui/items/__init__.py diff --git a/scripts/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py similarity index 100% rename from scripts/simplexui/items/accessor.py rename to src/python/simplexui/items/accessor.py diff --git a/scripts/simplexui/items/combo.py b/src/python/simplexui/items/combo.py similarity index 100% rename from scripts/simplexui/items/combo.py rename to src/python/simplexui/items/combo.py diff --git a/scripts/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py similarity index 100% rename from scripts/simplexui/items/falloff.py rename to src/python/simplexui/items/falloff.py diff --git a/scripts/simplexui/items/group.py b/src/python/simplexui/items/group.py similarity index 100% rename from scripts/simplexui/items/group.py rename to src/python/simplexui/items/group.py diff --git a/scripts/simplexui/items/progression.py b/src/python/simplexui/items/progression.py similarity index 100% rename from scripts/simplexui/items/progression.py rename to src/python/simplexui/items/progression.py diff --git a/scripts/simplexui/items/shape.py b/src/python/simplexui/items/shape.py similarity index 100% rename from scripts/simplexui/items/shape.py rename to src/python/simplexui/items/shape.py diff --git a/scripts/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py similarity index 100% rename from scripts/simplexui/items/simplex.py rename to src/python/simplexui/items/simplex.py diff --git a/scripts/simplexui/items/slider.py b/src/python/simplexui/items/slider.py similarity index 100% rename from scripts/simplexui/items/slider.py rename to src/python/simplexui/items/slider.py diff --git a/scripts/simplexui/items/stack.py b/src/python/simplexui/items/stack.py similarity index 100% rename from scripts/simplexui/items/stack.py rename to src/python/simplexui/items/stack.py diff --git a/scripts/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py similarity index 100% rename from scripts/simplexui/items/traversal.py rename to src/python/simplexui/items/traversal.py diff --git a/scripts/simplexui/main.pyw b/src/python/simplexui/main.pyw similarity index 100% rename from scripts/simplexui/main.pyw rename to src/python/simplexui/main.pyw diff --git a/scripts/simplexui/menu/__init__.py b/src/python/simplexui/menu/__init__.py similarity index 100% rename from scripts/simplexui/menu/__init__.py rename to src/python/simplexui/menu/__init__.py diff --git a/scripts/simplexui/menu/dummyPlugins/__init__.py b/src/python/simplexui/menu/dummyPlugins/__init__.py similarity index 100% rename from scripts/simplexui/menu/dummyPlugins/__init__.py rename to src/python/simplexui/menu/dummyPlugins/__init__.py diff --git a/scripts/simplexui/menu/genericPlugins/__init__.py b/src/python/simplexui/menu/genericPlugins/__init__.py similarity index 100% rename from scripts/simplexui/menu/genericPlugins/__init__.py rename to src/python/simplexui/menu/genericPlugins/__init__.py diff --git a/scripts/simplexui/menu/genericPlugins/_builtins.py b/src/python/simplexui/menu/genericPlugins/_builtins.py similarity index 100% rename from scripts/simplexui/menu/genericPlugins/_builtins.py rename to src/python/simplexui/menu/genericPlugins/_builtins.py diff --git a/scripts/simplexui/menu/genericPlugins/checkPossibleCombos.py b/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py similarity index 100% rename from scripts/simplexui/menu/genericPlugins/checkPossibleCombos.py rename to src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py diff --git a/scripts/simplexui/menu/genericPlugins/exportSplit.py b/src/python/simplexui/menu/genericPlugins/exportSplit.py similarity index 100% rename from scripts/simplexui/menu/genericPlugins/exportSplit.py rename to src/python/simplexui/menu/genericPlugins/exportSplit.py diff --git a/scripts/simplexui/menu/genericPlugins/showFalloffs.py b/src/python/simplexui/menu/genericPlugins/showFalloffs.py similarity index 100% rename from scripts/simplexui/menu/genericPlugins/showFalloffs.py rename to src/python/simplexui/menu/genericPlugins/showFalloffs.py diff --git a/scripts/simplexui/menu/genericPlugins/showTraversals.py b/src/python/simplexui/menu/genericPlugins/showTraversals.py similarity index 100% rename from scripts/simplexui/menu/genericPlugins/showTraversals.py rename to src/python/simplexui/menu/genericPlugins/showTraversals.py diff --git a/scripts/simplexui/menu/genericPlugins/simplexUvTransfer.py b/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py similarity index 100% rename from scripts/simplexui/menu/genericPlugins/simplexUvTransfer.py rename to src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py diff --git a/scripts/simplexui/menu/genericPlugins/unsubdivide.py b/src/python/simplexui/menu/genericPlugins/unsubdivide.py similarity index 100% rename from scripts/simplexui/menu/genericPlugins/unsubdivide.py rename to src/python/simplexui/menu/genericPlugins/unsubdivide.py diff --git a/scripts/simplexui/menu/mayaPlugins/__init__.py b/src/python/simplexui/menu/mayaPlugins/__init__.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/__init__.py rename to src/python/simplexui/menu/mayaPlugins/__init__.py diff --git a/scripts/simplexui/menu/mayaPlugins/exportOther.py b/src/python/simplexui/menu/mayaPlugins/exportOther.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/exportOther.py rename to src/python/simplexui/menu/mayaPlugins/exportOther.py diff --git a/scripts/simplexui/menu/mayaPlugins/extractProgressives.py b/src/python/simplexui/menu/mayaPlugins/extractProgressives.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/extractProgressives.py rename to src/python/simplexui/menu/mayaPlugins/extractProgressives.py diff --git a/scripts/simplexui/menu/mayaPlugins/freezeCombo.py b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/freezeCombo.py rename to src/python/simplexui/menu/mayaPlugins/freezeCombo.py diff --git a/scripts/simplexui/menu/mayaPlugins/generateShapeIncrementals.py b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/generateShapeIncrementals.py rename to src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py diff --git a/scripts/simplexui/menu/mayaPlugins/importObjs.py b/src/python/simplexui/menu/mayaPlugins/importObjs.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/importObjs.py rename to src/python/simplexui/menu/mayaPlugins/importObjs.py diff --git a/scripts/simplexui/menu/mayaPlugins/linearizeTraversal.py b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/linearizeTraversal.py rename to src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py diff --git a/scripts/simplexui/menu/mayaPlugins/makeShelfBtn.py b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/makeShelfBtn.py rename to src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py diff --git a/scripts/simplexui/menu/mayaPlugins/relaxToSelection.py b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/relaxToSelection.py rename to src/python/simplexui/menu/mayaPlugins/relaxToSelection.py diff --git a/scripts/simplexui/menu/mayaPlugins/reloadDefinition.py b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/reloadDefinition.py rename to src/python/simplexui/menu/mayaPlugins/reloadDefinition.py diff --git a/scripts/simplexui/menu/mayaPlugins/snapToNeutral.py b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/snapToNeutral.py rename to src/python/simplexui/menu/mayaPlugins/snapToNeutral.py diff --git a/scripts/simplexui/menu/mayaPlugins/softSelectToCluster.py b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/softSelectToCluster.py rename to src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py diff --git a/scripts/simplexui/menu/mayaPlugins/tweakMix.py b/src/python/simplexui/menu/mayaPlugins/tweakMix.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/tweakMix.py rename to src/python/simplexui/menu/mayaPlugins/tweakMix.py diff --git a/scripts/simplexui/menu/mayaPlugins/updateRestShape.py b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py similarity index 100% rename from scripts/simplexui/menu/mayaPlugins/updateRestShape.py rename to src/python/simplexui/menu/mayaPlugins/updateRestShape.py diff --git a/scripts/simplexui/menu/xsiPlugins/__init__.py b/src/python/simplexui/menu/xsiPlugins/__init__.py similarity index 100% rename from scripts/simplexui/menu/xsiPlugins/__init__.py rename to src/python/simplexui/menu/xsiPlugins/__init__.py diff --git a/src/python/simplexui/meson.build b/src/python/simplexui/meson.build new file mode 100644 index 00000000..aaeeae39 --- /dev/null +++ b/src/python/simplexui/meson.build @@ -0,0 +1,82 @@ +py.install_sources( + '__init__.py', + 'channelBox.py', + 'comboCheckDialog.py', + 'commands/__init__.py', + 'commands/alembicCommon.py', + 'commands/applyCorrectives.py', + 'commands/buildIceXML.py', + 'commands/correctiveInterface.py', + 'commands/expandedExport.py', + 'commands/hdf5Convert.py', + 'commands/mayaCorrectiveInterface.py', + 'commands/mesh.py', + 'commands/reorderSimplexPoints.py', + 'commands/rigidAlign.py', + 'commands/setDelta.xsicompound', + 'commands/smpxBlend.py', + 'commands/unsubdivide.py', + 'commands/uvTransfer.py', + 'commands/xsiCorrectiveInterface.py', + 'dragFilter.py', + 'falloffDialog.py', + 'img/ChefHead.png', + 'img/frozen.png', + 'interface/__init__.py', + 'interface/dummyInterface.py', + 'interface/mayaInterface.py', + 'interface/xsiInterface.py', + 'interfaceModel.py', + 'interfaceModelTrees.py', + 'interfaceModel_Test.py', + 'items/__init__.py', + 'items/accessor.py', + 'items/combo.py', + 'items/falloff.py', + 'items/group.py', + 'items/progression.py', + 'items/shape.py', + 'items/simplex.py', + 'items/slider.py', + 'items/stack.py', + 'items/traversal.py', + 'main.pyw', + 'menu/__init__.py', + 'menu/dummyPlugins/__init__.py', + 'menu/genericPlugins/__init__.py', + 'menu/genericPlugins/_builtins.py', + 'menu/genericPlugins/checkPossibleCombos.py', + 'menu/genericPlugins/exportSplit.py', + 'menu/genericPlugins/showFalloffs.py', + 'menu/genericPlugins/showTraversals.py', + 'menu/genericPlugins/simplexUvTransfer.py', + 'menu/genericPlugins/unsubdivide.py', + 'menu/mayaPlugins/__init__.py', + 'menu/mayaPlugins/exportOther.py', + 'menu/mayaPlugins/extractProgressives.py', + 'menu/mayaPlugins/freezeCombo.py', + 'menu/mayaPlugins/generateShapeIncrementals.py', + 'menu/mayaPlugins/importObjs.py', + 'menu/mayaPlugins/linearizeTraversal.py', + 'menu/mayaPlugins/makeShelfBtn.py', + 'menu/mayaPlugins/relaxToSelection.py', + 'menu/mayaPlugins/reloadDefinition.py', + 'menu/mayaPlugins/snapToNeutral.py', + 'menu/mayaPlugins/softSelectToCluster.py', + 'menu/mayaPlugins/tweakMix.py', + 'menu/mayaPlugins/updateRestShape.py', + 'menu/xsiPlugins/__init__.py', + 'simplexDialog.py', + 'travCheckDialog.py', + 'traversalDialog.py', + 'ui/comboCheckDialog.ui', + 'ui/falloffDialog.ui', + 'ui/simplexDialog.ui', + 'ui/travCheckDialog.ui', + 'ui/traversalDialog.ui', + 'utils.py', + subdir : 'simplexui', + preserve_path: true, +) + + diff --git a/scripts/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py similarity index 100% rename from scripts/simplexui/simplexDialog.py rename to src/python/simplexui/simplexDialog.py diff --git a/scripts/simplexui/travCheckDialog.py b/src/python/simplexui/travCheckDialog.py similarity index 100% rename from scripts/simplexui/travCheckDialog.py rename to src/python/simplexui/travCheckDialog.py diff --git a/scripts/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py similarity index 100% rename from scripts/simplexui/traversalDialog.py rename to src/python/simplexui/traversalDialog.py diff --git a/scripts/simplexui/ui/comboCheckDialog.ui b/src/python/simplexui/ui/comboCheckDialog.ui similarity index 100% rename from scripts/simplexui/ui/comboCheckDialog.ui rename to src/python/simplexui/ui/comboCheckDialog.ui diff --git a/scripts/simplexui/ui/falloffDialog.ui b/src/python/simplexui/ui/falloffDialog.ui similarity index 100% rename from scripts/simplexui/ui/falloffDialog.ui rename to src/python/simplexui/ui/falloffDialog.ui diff --git a/scripts/simplexui/ui/simplexDialog.ui b/src/python/simplexui/ui/simplexDialog.ui similarity index 100% rename from scripts/simplexui/ui/simplexDialog.ui rename to src/python/simplexui/ui/simplexDialog.ui diff --git a/scripts/simplexui/ui/travCheckDialog.ui b/src/python/simplexui/ui/travCheckDialog.ui similarity index 100% rename from scripts/simplexui/ui/travCheckDialog.ui rename to src/python/simplexui/ui/travCheckDialog.ui diff --git a/scripts/simplexui/ui/traversalDialog.ui b/src/python/simplexui/ui/traversalDialog.ui similarity index 100% rename from scripts/simplexui/ui/traversalDialog.ui rename to src/python/simplexui/ui/traversalDialog.ui diff --git a/scripts/simplexui/utils.py b/src/python/simplexui/utils.py similarity index 100% rename from scripts/simplexui/utils.py rename to src/python/simplexui/utils.py From 05a2ce13c426c302d431c49c0a7578538e716390 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 9 Apr 2025 14:56:33 -0700 Subject: [PATCH 02/25] Clean up the lint errors in the installer script --- simplex_maya_installer.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/simplex_maya_installer.py b/simplex_maya_installer.py index 782250ca..69107a46 100644 --- a/simplex_maya_installer.py +++ b/simplex_maya_installer.py @@ -1,4 +1,5 @@ import logging +import importlib.util import os import sys import zipfile @@ -124,10 +125,10 @@ def get_latest_git_release(user, repo, asset_regex, out_path): out_path = Path(out_path) outFolder = out_path.parent outFolder.mkdir(exist_ok=True) - logger.info(f"Downloading latest simplex") + logger.info("Downloading latest simplex") logger.info(f"from: {download_url}") logger.info(f"to: {out_path}") - path, headers = urllib.request.urlretrieve(download_url, filename=out_path) + path, _headers = urllib.request.urlretrieve(download_url, filename=out_path) return Path(path) @@ -171,14 +172,14 @@ def get_numpy_simplex_target(mod_folder): return nppath -def onMayaDroppedPythonFile(obj): +def onMayaDroppedPythonFile(_obj): """This function will get run when you drag/drop this python script onto maya""" try: # Ensure that people will report a full error cmds.optionVar(intValue=("stackTraceIsOn", 1)) mel.eval('synchronizeScriptEditorOption(1, "StackTraceMenuItem")') - mod_folder = Path(cmds.internalVar(uad=True)) / "modules" + mod_folder = Path(cmds.internalVar(userAppDir=True)) / "modules" modfile = mod_folder / "simplex.mod" simplex_zip = mod_folder / "simplex.zip" moddir = mod_folder / "simplex" @@ -217,16 +218,12 @@ def onMayaDroppedPythonFile(obj): mayapy = get_mayapy_path() target = get_numpy_simplex_target(mod_folder) - try: - import numpy - except ModuleNotFoundError: + if importlib.util.find_spec('numpy') is None: install_numpy(mayapy, target) else: logger.info("Numpy is already installed for this version of maya") - try: - import Qt - except ModuleNotFoundError: + if importlib.util.find_spec('Qt') is None: install_qtpy(mayapy, target) else: logger.info("Qt.py is already installed for this version of maya") From 881982fd78e9c42feed4b4241441a883bebf100e Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 9 Apr 2025 15:05:33 -0700 Subject: [PATCH 03/25] Fix a couple formatting issues --- pyproject.toml | 4 ++-- src/python/simplexui/interface/mayaInterface.py | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8d13c6ef..12cebece 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ exclude = [ "venv", ] -line-length = 100 +line-length = 88 indent-width = 4 target-version = "py37" @@ -114,7 +114,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [tool.ruff.format] # Like Black, use double quotes for strings. -quote-style = "double" +quote-style = "preserve" # Like Black, indent with spaces, rather than tabs. indent-style = "space" diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 010da719..bbc770d3 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -274,7 +274,6 @@ def _checkAllShapeValidity(self, shapeNames): missingNames.append(shapeName) return missingNames, len(attrs) - @classmethod def _removeExtraShapeNodes(cls, tfm): shapeNodes = cmds.listRelatives(tfm, shapes=True, noIntermediate=True) @@ -290,7 +289,6 @@ def _removeExtraShapeNodes(cls, tfm): if keeper is not None: cmds.delete(todel) - def preLoad(self, simp, simpDict, create=True, pBar=None): """ @@ -326,7 +324,7 @@ def preLoad(self, simp, simpDict, create=True, pBar=None): toMake, nextIndex = self._checkAllShapeValidity(shapeNames) if not toMake: - return + return if not create: if pBar is not None: @@ -379,7 +377,6 @@ def preLoad(self, simp, simpDict, create=True, pBar=None): cmds.undoInfo(state=True) raise - def postLoad(self, simp, preRet): """ @@ -396,7 +393,7 @@ def postLoad(self, simp, preRet): cmds.undoInfo(state=True) def checkForErrors(self, window): - """ Check for any DCC specific errors + """Check for any DCC specific errors Parameters ---------- @@ -408,7 +405,7 @@ def checkForErrors(self, window): msg = ( "The current mesh has multiple shape nodes.", "The UI will still mostly work, but extracting/connecting shapes" - "may fail in unexpected ways." + "may fail in unexpected ways.", ) QMessageBox.warning(window, "Multiple Shape Nodes", '\n'.join(msg)) @@ -2315,7 +2312,6 @@ def extractTraversalShape(self, trav, shape, live=True, offset=10.0): cmds.setAttr(a, 0.0) with disconnected(floatShapes): # tShapes - sliDict = {} for pair in trav.startPoint.pairs: sliDict[pair.slider] = [pair.value] From abfbf6fc2d5ad73d0718a6c77bd23e171714c979 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 25 Apr 2025 14:12:47 -0700 Subject: [PATCH 04/25] Add the pure python imathnumpy --- src/python/simplexui/commands/numpytoimath.py | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 src/python/simplexui/commands/numpytoimath.py diff --git a/src/python/simplexui/commands/numpytoimath.py b/src/python/simplexui/commands/numpytoimath.py new file mode 100644 index 00000000..d6df6e96 --- /dev/null +++ b/src/python/simplexui/commands/numpytoimath.py @@ -0,0 +1,319 @@ +import imath +import ctypes +import numpy as np +from typing import TypeVar, Type + + +NTYPEDICT: dict[type, type] = { + ctypes.c_bool: bool, + ctypes.c_byte: np.int8, + ctypes.c_double: np.float64, + ctypes.c_float: np.float32, + ctypes.c_long: np.int32, + ctypes.c_short: np.int16, + ctypes.c_ubyte: np.uint8, + ctypes.c_ulong: np.uint32, + ctypes.c_ushort: np.uint16, +} + +# fmt: off +TYPEDICT: dict[type, tuple[list[int], type, str]] = { + imath.BoolArray: ([], ctypes.c_bool, 'array'), + imath.DoubleArray: ([], ctypes.c_double, 'array'), + imath.FloatArray: ([], ctypes.c_float, 'array'), + imath.IntArray: ([], ctypes.c_long, 'array'), + imath.ShortArray: ([], ctypes.c_short, 'array'), + imath.SignedCharArray: ([], ctypes.c_byte, 'array'), + imath.UnsignedCharArray: ([], ctypes.c_ubyte, 'array'), + imath.UnsignedIntArray: ([], ctypes.c_ulong, 'array'), + imath.UnsignedShortArray: ([], ctypes.c_ushort, 'array'), + + imath.Box2dArray: ([2, 2], ctypes.c_double, 'array'), + imath.Box2fArray: ([2, 2], ctypes.c_float, 'array'), + imath.Box2iArray: ([2, 2], ctypes.c_long, 'array'), + imath.Box2sArray: ([2, 2], ctypes.c_short, 'array'), + imath.Box3dArray: ([2, 3], ctypes.c_double, 'array'), + imath.Box3fArray: ([2, 3], ctypes.c_float, 'array'), + imath.Box3iArray: ([2, 3], ctypes.c_long, 'array'), + imath.Box3sArray: ([2, 3], ctypes.c_short, 'array'), + imath.C3cArray: ([3], ctypes.c_byte, 'array'), + imath.C3fArray: ([3], ctypes.c_float, 'array'), + imath.C4cArray: ([4], ctypes.c_byte, 'array'), + imath.C4fArray: ([4], ctypes.c_float, 'array'), + imath.M22dArray: ([2, 2], ctypes.c_double, 'array'), + imath.M22fArray: ([2, 2], ctypes.c_float, 'array'), + imath.M33dArray: ([3, 3], ctypes.c_double, 'array'), + imath.M33fArray: ([3, 3], ctypes.c_float, 'array'), + imath.M44dArray: ([4, 4], ctypes.c_double, 'array'), + imath.M44fArray: ([4, 4], ctypes.c_float, 'array'), + imath.QuatdArray: ([4], ctypes.c_double, 'array'), + imath.QuatfArray: ([4], ctypes.c_float, 'array'), + imath.V2dArray: ([2], ctypes.c_double, 'array'), + imath.V2fArray: ([2], ctypes.c_float, 'array'), + imath.V2iArray: ([2], ctypes.c_long, 'array'), + imath.V2sArray: ([2], ctypes.c_short, 'array'), + imath.V3dArray: ([3], ctypes.c_double, 'array'), + imath.V3fArray: ([3], ctypes.c_float, 'array'), + imath.V3iArray: ([3], ctypes.c_long, 'array'), + imath.V3sArray: ([3], ctypes.c_short, 'array'), + imath.V4dArray: ([4], ctypes.c_double, 'array'), + imath.V4fArray: ([4], ctypes.c_float, 'array'), + imath.V4iArray: ([4], ctypes.c_long, 'array'), + imath.V4sArray: ([4], ctypes.c_short, 'array'), + + imath.Color4cArray2D: ([4], ctypes.c_byte, 'array2d'), + imath.Color4fArray2D: ([4], ctypes.c_float, 'array2d'), + imath.DoubleArray2D: ([], ctypes.c_double, 'array2d'), + imath.FloatArray2D: ([], ctypes.c_float, 'array2d'), + imath.IntArray2D: ([], ctypes.c_long, 'array2d'), + + imath.DoubleMatrix: ([], ctypes.c_double, 'matrix'), + imath.FloatMatrix: ([], ctypes.c_float, 'matrix'), + imath.IntMatrix: ([], ctypes.c_long, 'matrix'), + + imath.Box2d: ([2, 2], ctypes.c_double, 'box'), + imath.Box2f: ([2, 2], ctypes.c_float, 'box'), + imath.Box2i: ([2, 2], ctypes.c_long, 'box'), + imath.Box2s: ([2, 2], ctypes.c_short, 'box'), + imath.Box3d: ([2, 3], ctypes.c_double, 'box'), + imath.Box3f: ([2, 3], ctypes.c_float, 'box'), + imath.Box3i: ([2, 3], ctypes.c_long, 'box'), + imath.Box3s: ([2, 3], ctypes.c_short, 'box'), + + imath.Line3d: ([2, 3], ctypes.c_double, 'line'), + imath.Line3f: ([2, 3], ctypes.c_float, 'line'), + + imath.Color3c: ([3], ctypes.c_byte, ''), + imath.Color3f: ([3], ctypes.c_float, ''), + imath.Color4c: ([4], ctypes.c_byte, ''), + imath.Color4f: ([4], ctypes.c_float, ''), + imath.M22d: ([2, 2], ctypes.c_double, ''), + imath.M22dRow: ([2], ctypes.c_double, 'row'), + imath.M22f: ([2, 2], ctypes.c_float, ''), + imath.M22fRow: ([2], ctypes.c_float, 'row'), + imath.M33d: ([3, 3], ctypes.c_double, ''), + imath.M33dRow: ([3], ctypes.c_double, 'row'), + imath.M33f: ([3, 3], ctypes.c_float, ''), + imath.M33fRow: ([3], ctypes.c_float, 'row'), + imath.M44d: ([4, 4], ctypes.c_double, ''), + imath.M44dRow: ([4], ctypes.c_double, 'row'), + imath.M44f: ([4, 4], ctypes.c_float, ''), + imath.M44fRow: ([4], ctypes.c_float, 'row'), + imath.Quatd: ([4], ctypes.c_double, ''), + imath.Quatf: ([4], ctypes.c_float, ''), + imath.Shear6d: ([6], ctypes.c_double, ''), + imath.Shear6f: ([6], ctypes.c_float, ''), + imath.V2d: ([2], ctypes.c_double, ''), + imath.V2f: ([2], ctypes.c_float, ''), + imath.V2i: ([2], ctypes.c_long, ''), + imath.V2s: ([2], ctypes.c_short, ''), + imath.V3c: ([3], ctypes.c_byte, ''), + imath.V3d: ([3], ctypes.c_double, ''), + imath.V3f: ([3], ctypes.c_float, ''), + imath.V3i: ([3], ctypes.c_long, ''), + imath.V3s: ([3], ctypes.c_short, ''), + imath.V4c: ([4], ctypes.c_byte, ''), + imath.V4d: ([4], ctypes.c_double, ''), + imath.V4f: ([4], ctypes.c_float, ''), + imath.V4i: ([4], ctypes.c_long, ''), + imath.V4s: ([4], ctypes.c_short, ''), +} +# fmt: on + +# Define the in-memory structures of the python objects +# I'm *GUESSING* on most of the types here, so they could be refined +# in the future if required + +# Here's hoping these structs don't change with different versions +# of python or imath + + +class PyImoObj(ctypes.Structure): + _fields_ = [ + ("refcount", ctypes.c_ssize_t), # from the PyObject c struct + ("typeptr", ctypes.c_void_p), # from the PyObject c struct + ("unknown1", ctypes.c_ssize_t), # Seems always -48 for some reason + ("unknown2", ctypes.c_ssize_t), # Seems always 0 + ("unknown3", ctypes.c_ssize_t), # Seems always 0 + ("dataptr", ctypes.c_void_p), # pointer to the PyImoDataObj + ] + + +class PyImoDataObj(ctypes.Structure): + _fields_ = [ + ("magic", ctypes.c_ssize_t), # Some kind of type ID? + ("unknown1", ctypes.c_ssize_t), # Seems always 0 + ("dataptr", ctypes.c_void_p), # Pointer to the allocated memory + # There's MORE data after this, like the row/column count + # and some other pointers. But I don't need them + # Plus, there are some edge cases with the different types + # like rows or bounding boxes + ] + + +PyImoObjPtr = ctypes.POINTER(PyImoObj) +PyImoDataObjPtr = ctypes.POINTER(PyImoDataObj) + + +def _getImoPointer(imo, extra: str) -> int: + """Get the memory address to the actual imath data + + Args: + imo (imath object): The imath object to inspect + extra (str): The metadata of this current type + + Returns: + int: The memory address to the actual data + """ + # This is a scary function + # I found this stuff out by trial and error + pyStruct = ctypes.cast(id(imo), PyImoObjPtr).contents + if extra == "box": + # I'm guessing that since the bounding box data is a known + # size, they just put it directly into the structure + # And the data is stored where the dataptr would be. + # So I can just add the pyStruct.dataptr and the memory offset + # of the PyImoDataObj.dataptr to get memory address of the box + return pyStruct.dataptr + PyImoDataObj.dataptr.offset + + pyImoStruct = ctypes.cast(pyStruct.dataptr, PyImoDataObjPtr).contents + return pyImoStruct.dataptr + + +def _link(imo) -> tuple[np.ndarray, int]: + """ Build a numpy object that's referencing the same memory + as the given imath object + + Args: + imo (imath object): The imath object to build a link to + + Returns: + np.ndarray: The numpy array + int: The pointer to the memory address where the data lives + """ + size, cdata, extra = TYPEDICT[type(imo)] + if extra == "array": + shape = [len(imo)] + size + elif extra == "array2d": + shape = list(imo.size()) + size + elif extra == "matrix": + shape = [imo.rows(), imo.columns()] + size + elif extra in ("box", "line"): + shape = size + else: + shape = [len(imo)] + + for s in shape[::-1]: + cdata = cdata * s # type: ignore + + ptr = _getImoPointer(imo, extra) + ctypearray = cdata.from_address(ptr) + nparray = np.ctypeslib.as_array(ctypearray) + return nparray, ptr + + +def imathToNumpy(imo) -> np.ndarray: + """Copy an imath object into a numpy array + + Args: + imo (imath object): The imath object to convert + + Returns: + np.ndarray: A copy of the imath object as a numpy array + """ + gcarray, _ptr = _link(imo) + return np.copy(gcarray) + + +T = TypeVar("T") + + +def numpyToImath(npo: np.ndarray, imtype: Type[T]) -> T: + """Convert a numpy array to the given imath type + + Args: + npo (array like): An object that can be cast to a numpy array + imtype (type): The imath type to convert to + + Returns: + imathObj: An instantiated imtype object with the numpy data loaded into it + """ + size, cdata, extra = TYPEDICT[imtype] + npo = np.asarray(npo, dtype=NTYPEDICT[cdata]) + if extra == "array": + assert npo.ndim == len(size) + 1 + imo = imtype(len(npo)) # type: ignore + elif extra in ("matrix", "array2d"): + assert npo.ndim == 2 + len(size) + imo = imtype(*npo.shape[:2]) + else: + imo = imtype() + + tret, _ptr = _link(imo) + np.copyto(tret, npo) + return imo + + +def _test(): + """A quick test for all this crazy stuff""" + + # By default the bounding box objects are set to + # The min/max for their types + box3dnp = np.empty((2, 3), dtype=np.float64) + box3dnp[0] = imath.DBL_MAX + box3dnp[1] = imath.DBL_MIN + + box3fnp = np.empty((2, 3), dtype=np.float32) + box3fnp[0] = imath.FLT_MAX + box3fnp[1] = imath.FLT_MIN + + box3fanp = np.empty((5, 2, 3), dtype=np.float32) + box3fanp[:] = box3fnp + + # M44fArray + eyes = np.zeros((13, 4, 4), dtype=np.float32) + eyes[:] = np.eye(4, dtype=np.float32) + + # Line3f + line = np.zeros((2, 3), dtype=np.float32) + line[1, 0] = 1.0 + + # M33fRow + im = imath.M33f() + nm = np.eye(3, dtype=np.float32) + + # equivalent to an np.empty(3, 5) + # so I have to set the values manually + dubm = imath.DoubleMatrix(3, 5) + tt = 0.0 + for i in range(3): + row = dubm[i] + for j in range(5): + row[j] = tt + tt += 1.0 + + # fmt: off + eqpairs = [ + (imath.V3dArray(11), np.zeros((11, 3))), + (imath.M44fArray(13), eyes), + (imath.V3d(), np.zeros(3)), + (imath.Color4cArray2D(5, 7), np.zeros((5, 7, 4), dtype=np.int8)), + (imath.Box3fArray(5), box3fanp), + (imath.Box3d(), box3dnp), + (imath.Box3f(), box3fnp), + (imath.FloatArray(13), np.zeros((13), dtype=np.float32)), + (imath.Line3f(), line), + (im[1], nm[1]), + (dubm, np.arange(15, dtype=float).reshape((3, 5))), + ] + # fmt: on + + for imo, chk in eqpairs: + nv = imathToNumpy(imo) + assert nv.dtype == chk.dtype + assert np.all(nv == chk) + + imtype = type(imo) + _size, _cdata, extra = TYPEDICT[imtype] + if extra != "row": # Can't directly build row objects + _iv = numpyToImath(chk, imtype) From e2edf4473e03dd2291e52fa91b2272956bb2dc29 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 25 Apr 2025 14:45:20 -0700 Subject: [PATCH 05/25] Use the numpytoimath library, and Get rid of python2 compat stuff --- .../simplexui/commands/alembicCommon.py | 94 +---- src/python/simplexui/commands/mayatonumpy.py | 375 ++++++++++++++++++ src/python/simplexui/items/simplex.py | 9 +- .../menu/genericPlugins/unsubdivide.py | 9 +- 4 files changed, 404 insertions(+), 83 deletions(-) create mode 100644 src/python/simplexui/commands/mayatonumpy.py diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 427da6d2..48e25076 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -15,9 +15,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -""" Alembic files can be difficult to work with, and can be *very* slow in Python +"""Alembic files can be difficult to work with, and can be *very* slow in Python This is a library of convenience functions with the numpy speed optimizations """ + from __future__ import absolute_import, print_function import os @@ -39,14 +40,11 @@ try: import numpy as np + from .numpytoimath import imathToNumpy, numpyToImath except ImportError: np = None - arrayToNumpy = None -else: - try: - from imathnumpy import arrayToNumpy # pylint:disable=no-name-in-module - except ImportError: - arrayToNumpy = None + numpyToImath = None + imathToNumpy = None def pbPrint(pBar, message=None, val=None, maxVal=None, _pbPrintLastComma=[]): @@ -111,22 +109,12 @@ def mkArray(aType, iList): if isinstance(iList, aType): return iList - if np is None: + if np is None or numpyToImath is None: array = aType(len(iList)) for i in range(len(iList)): array[i] = tuple(iList[i]) return array - elif arrayToNumpy is None: - array = aType(len(iList)) - for i in range(len(iList)): - array[i] = tuple(iList[i].tolist()) - return array - else: - iList = np.array(iList) - array = aType(len(iList)) - memView = arrayToNumpy(array) - np.copyto(memView, iList) - return array + return numpyToImath(iList, aType) def mk1dArray(aType, iList): @@ -146,19 +134,14 @@ def mk1dArray(aType, iList): """ if isinstance(iList, aType): return iList - if np is None or arrayToNumpy is None or aType is UnsignedIntArray: + if np is None or numpyToImath is None: array = aType(len(iList)) for i in range(len(iList)): # Gotta cast to int because an "int" from numpy has # the type np.int32, which makes this conversion angry array[i] = int(iList[i]) return array - else: - iList = np.array(iList) - array = aType(len(iList)) - memView = arrayToNumpy(array) - np.copyto(memView, iList) - return array + return numpyToImath(iList, aType) def mkSampleVertexPoints(pts): @@ -307,18 +290,11 @@ def getSampleArray(imesh, pBar=None): meshSchema = imesh.getSchema() posProp = meshSchema.getPositionsProperty() numShapes = len(posProp.samples) - if arrayToNumpy is not None: + if imathToNumpy is not None and np is not None: shapes = np.empty((len(posProp.samples), len(posProp.samples[0]), 3)) for i, s in enumerate(posProp.samples): pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) - shapes[i] = arrayToNumpy(s) - elif np is not None: - shapes = [] - for i, s in enumerate(posProp.samples): - pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) - shapes.append((list(s.x), list(s.y), list(s.z))) - shapes = np.array(shapes) - shapes = shapes.transpose((0, 2, 1)) + shapes[i] = imathToNumpy(s) else: shapes = [] for i, s in enumerate(posProp.samples): @@ -365,13 +341,11 @@ def getStaticMeshArrays(imesh): The number of vertices per face as np.array if possible """ faces, counts = getStaticMeshData(imesh) - if arrayToNumpy is not None: - faces = arrayToNumpy(faces).copy() - counts = arrayToNumpy(counts).copy() - elif np is not None: - faces, counts = np.array(faces), np.array(counts) - else: + if np is None or imathToNumpy is None: faces, counts = list(faces), list(counts) + else: + faces = imathToNumpy(faces) + counts = imathToNumpy(counts) return faces, counts @@ -456,20 +430,17 @@ def getFlatUvFaces(imesh): if iuvs.isIndexed(): indexed = True idxs = iuvs.getIndexProperty().getValue() - # if arrayToNumpy is not None: - # idxs = arrayToNumpy(idxs).copy() - # elif np is not None: - if np is not None: - idxs = np.array(idxs) - else: + if imathToNumpy is None or np is None: idxs = list(idxs) + else: + idxs = imathToNumpy(idxs) else: indexed = False rawCount = sum(list(sch.getFaceCountsProperty().samples[0])) - if np is not None: - idxs = np.arange(rawCount) - else: + if np is None: idxs = list(range(rawCount)) + else: + idxs = np.arange(rawCount) return idxs, indexed @@ -709,29 +680,6 @@ def flattenFaces(faces): return faceCounts, faceIdxs -def unflattenFaces(faces, counts): - """Take a flat face/count representation of faces - and turn it into a nested list representation - - Parameters - ---------- - faces : np.array - The flat list of face connectivity - counts : np.array - The flat list of vertices per face - - Returns - ------- - : [[int, ...], ...] - The nested list representation - """ - out, ptr = [], 0 - for c in counts: - out.append(faces[ptr : ptr + c].tolist()) - ptr += c - return out - - def buildAbc( outPath, points, diff --git a/src/python/simplexui/commands/mayatonumpy.py b/src/python/simplexui/commands/mayatonumpy.py new file mode 100644 index 00000000..88d75f1d --- /dev/null +++ b/src/python/simplexui/commands/mayatonumpy.py @@ -0,0 +1,375 @@ +from maya import OpenMaya as om +import numpy as np +from ctypes import c_float, c_double, c_int, c_uint + +# fmt: off +_CONVERT_DICT = { + om.MPointArray: (float, 4, c_double, om.MScriptUtil.asDouble4Ptr), + om.MFloatPointArray: (float, 4, c_float , om.MScriptUtil.asFloat4Ptr), + om.MVectorArray: (float, 3, c_double, om.MScriptUtil.asDouble3Ptr), + om.MFloatVectorArray: (float, 3, c_float , om.MScriptUtil.asFloat3Ptr), + om.MDoubleArray: (float, 1, c_double, om.MScriptUtil.asDoublePtr), + om.MFloatArray: (float, 1, c_float , om.MScriptUtil.asFloatPtr), + om.MIntArray: (int , 1, c_int , om.MScriptUtil.asIntPtr), + om.MUintArray: (int , 1, c_uint , om.MScriptUtil.asUintPtr), +} +# fmt: on + + +def _swigConnect(mArray, count, util): + ''' + Use an MScriptUtil to build SWIG array that we can read from and write to. + Make sure to get the MScriptUtil from outside this function, otherwise + it may be garbage collected + The _CONVERT_DICT holds {mayaType: (pyType, numComps, cType, ptrType)} where + pyType: The type that is used to fill the MScriptUtil array. + numComps: The number of components. So a double4Ptr would be 4 + cType: The ctypes type used to read the data + ptrType: An unbound method on MScriptUtil to cast the pointer to the correct type + I can still call that unbound method by manually passing the usually-implicit + self argument (which will be an instance of MScriptUtil) + ''' + pyTyp, comps, ctp, ptrTyp = _CONVERT_DICT[type(mArray)] + cc = count * comps + util.createFromList([pyTyp()] * cc, cc) + + # passing util as 'self' to call the unbound method + ptr = ptrTyp(util) + mArray.get(ptr) + + if comps == 1: + cdata = ctp * count + else: + # Multiplication follows some strange rules here + # I would expect (ctype*3)*N to be an Nx3 array (ctype*3 repeated N times) + # However, it gets converted to a 3xN array + cdata = (ctp * comps) * count + + # int(ptr) gives the memory address + cta = cdata.from_address(int(ptr)) + + # This makes numpy look at the same memory as the ctypes array + # so we can both read from and write to that data through numpy + npArray = np.ctypeslib.as_array(cta) + return npArray, ptr + + +def _swigConnectMatrix(mat, ctp): + # With a matrix, you can just get the double[4][4] without an MScriptUtil + ptr = mat.matrix + cdata = ctp * 4 * 4 + + # int(ptr) gives the memory address + cta = cdata.from_address(int(ptr)) + + # This makes numpy look at the same memory as the ctypes array + # so we can both read from and write to that data through numpy + npArray = np.ctypeslib.as_array(cta) + return npArray, ptr + + +def mayaToNumpy(mArray): + '''Convert a maya array to a numpy array + + Parameters + ---------- + ary : MArray + The maya array to convert to a numpy array + + Returns + ------- + : np.array : + A numpy array that contains the data from mArray + + ''' + if isinstance(mArray, om.MMatrix): + npArray, _ = _swigConnectMatrix(mArray, c_double) + elif isinstance(mArray, om.MFloatMatrix): + npArray, _ = _swigConnectMatrix(mArray, c_float) + else: + util = om.MScriptUtil() + count = mArray.length() + npArray, _ = _swigConnect(mArray, count, util) + return np.copy(npArray) + + +def numpyToMaya(ary, mType): + '''Convert a numpy array to a specific maya type array + + Parameters + ---------- + ary : np.array + The numpy array to convert to a maya array + mType : type + The maya type to convert to out of: MPointArray, MFloatPointArray, MVectorArray, + MFloatVectorArray, MDoubleArray, MFloatArray, MIntArray, MUintArray + + Returns + ------- + : mType : + An array of the provided type that contains the data from ary + + ''' + # Handle matrices separately + if mType in (om.MMatrix, om.MFloatMatrix): + ctp = c_double if mType == om.MMatrix else c_float + if ary.shape != (4, 4): + msg = "Numpy array must have the proper shape. For matrix types that shape must be (4, 4). Got {0}" + raise ValueError(msg.format(ary.shape)) + tmpMat = mType() + npArray, ptr = _swigConnectMatrix(tmpMat, ctp) + np.copyto(npArray, ary) + return mType(ptr) + + # Add a little shape checking + comps = _CONVERT_DICT[mType][1] + if comps == 1: + if len(ary.shape) != 1: + raise ValueError("Numpy array must be 1D to convert to the given maya type") + else: + if len(ary.shape) != 2: + raise ValueError("Numpy array must be 2D to convert to the given maya type") + if ary.shape[1] != comps: + msg = "Numpy array must have the proper shape. Dimension 2 has size {0}, but needs size {1}" + raise ValueError(msg.format(ary.shape[1], comps)) + count = ary.shape[0] + tmpAry = mType(count) + util = om.MScriptUtil() + npArray, ptr = _swigConnect(tmpAry, count, util) + np.copyto(npArray, ary) + return mType(ptr, count) + + +# fmt: off +_NTYPE_DICT = { + om.MFnNumericData.kInvalid: (om.MDataHandle.asDouble, om.MDataHandle.setDouble), + om.MFnNumericData.kFloat: (om.MDataHandle.asDouble, om.MDataHandle.setDouble), + om.MFnNumericData.kDouble: (om.MDataHandle.asDouble, om.MDataHandle.setDouble), + om.MFnNumericData.kByte: (om.MDataHandle.asInt, om.MDataHandle.setInt), + om.MFnNumericData.kChar: (om.MDataHandle.asChar, om.MDataHandle.setChar), + om.MFnNumericData.kShort: (om.MDataHandle.asShort, om.MDataHandle.setShort), + om.MFnNumericData.kInt: (om.MDataHandle.asInt, om.MDataHandle.setInt), + #om.MFnNumericData.kInt64: (om.MDataHandle.asInt, om.MDataHandle.setInt64), + om.MFnNumericData.kAddr: (om.MDataHandle.asInt, om.MDataHandle.setInt), + om.MFnNumericData.kLong: (om.MDataHandle.asInt, om.MDataHandle.setInt), + om.MFnNumericData.kBoolean: (om.MDataHandle.asBool, om.MDataHandle.setBool), + + om.MFnNumericData.k2Short: (om.MDataHandle.asShort2, om.MDataHandle.set2Short), + om.MFnNumericData.k2Long: (om.MDataHandle.asInt2, om.MDataHandle.set2Int), + om.MFnNumericData.k2Int: (om.MDataHandle.asInt2, om.MDataHandle.set2Int), + om.MFnNumericData.k3Short: (om.MDataHandle.asShort3, om.MDataHandle.set3Short), + om.MFnNumericData.k3Long: (om.MDataHandle.asInt3, om.MDataHandle.set3Int), + om.MFnNumericData.k3Int: (om.MDataHandle.asInt3, om.MDataHandle.set3Int), + om.MFnNumericData.k2Float: (om.MDataHandle.asFloat2, om.MDataHandle.set2Float), + om.MFnNumericData.k2Double: (om.MDataHandle.asDouble2, om.MDataHandle.set2Double), + om.MFnNumericData.k3Float: (om.MDataHandle.asFloat3, om.MDataHandle.set3Float), + om.MFnNumericData.k3Double: (om.MDataHandle.asDouble3, om.MDataHandle.set3Double), +} + +_DTYPE_DICT = { + om.MFn.kPointArrayData: (om.MFnPointArrayData, om.MPointArray), + om.MFn.kDoubleArrayData: (om.MFnDoubleArrayData, om.MDoubleArray), + om.MFn.kFloatArrayData: (om.MFnFloatArrayData, om.MFloatArray), + om.MFn.kIntArrayData: (om.MFnIntArrayData, om.MIntArray), + om.MFn.kUInt64ArrayData: (om.MFnUInt64ArrayData, om.MPointArray), + om.MFn.kVectorArrayData: (om.MFnVectorArrayData, om.MVectorArray), +} +# fmt: on + + +def getNumpyAttr(attrName): + '''Read attribute data directly from the plugs into numpy + + This function will read most numeric data types directly into numpy arrays + However, some simple data types (floats, vectors, etc...) have api accessors + that return python tuples. These will not be turned into numpy arrays. + And really, if you're getting simple data like that, just use cmds.getAttr + + Parameters + ---------- + attrName : str or om.MPlug + The name of the attribute to get (For instance "pSphere2.translate", or "group1.pim[0]") + Or the MPlug itself + + Returns + ------- + : object : + The numerical data from the provided plug. A np.array, float, int, or tuple + + ''' + if isinstance(attrName, str): + sl = om.MSelectionList() + sl.add(attrName) + plug = om.MPlug() + sl.getPlug(0, plug) + elif isinstance(attrName, om.MPlug): + plug = attrName + else: + raise ValueError("AttrName is not a name or a plug") + + # First just check if the data is numeric + mdh = plug.asMDataHandle() + if mdh.isNumeric(): + # So, at this point, you should really just use getattr + ntype = mdh.numericType() + if ntype in _NTYPE_DICT: + return _NTYPE_DICT[ntype][0](mdh) + elif ntype == om.MFnNumericData.k4Double: + NotImplementedError("Haven't implemented double4 access yet") + else: + raise RuntimeError( + "I don't know how to access data from the given attribute" + ) + else: + # The data is more complex than a simple number. + try: + pmo = plug.asMObject() + except RuntimeError as e: + # raise a more descriptive error. And make sure to actually print the plug name + raise RuntimeError( + "I don't know how to access data from the given attribute" + ) from e + apiType = pmo.apiType() + + # A list of types that I can just pass to mayaToNumpy + if apiType in _DTYPE_DICT: + fn, dtype = _DTYPE_DICT[apiType] + fnPmo = fn(pmo) + ary = fnPmo.array() + return mayaToNumpy(ary) + + elif apiType == om.MFn.kComponentListData: + fnPmo = om.MFnComponentListData(pmo) + mirs = [] + mir = om.MIntArray() + for attrIndex in range(fnPmo.length()): + fnEL = om.MFnSingleIndexedComponent(fnPmo[attrIndex]) + fnEL.getElements(mir) + mirs.append(mayaToNumpy(mir)) + if not mirs: + return np.array([], dtype=int) + return np.concatenate(mirs) + + elif apiType == om.MFn.kMatrixData: + fnPmo = om.MFnMatrixData(pmo) + mat = fnPmo.matrix() + return mayaToNumpy(mat) + else: + apiTypeStr = pmo.apiTypeStr() + raise NotImplementedError( + "I don't know how to handle {0} yet".format(apiTypeStr) + ) + raise NotImplementedError("Fell all the way through") + + +def setNumpyAttr(attrName, value): + '''Write a numpy array directly into a maya plug + + This function will handle most numeric plug types. + But for single float, individual point, etc.. types, consider using cmds.setAttr + + THIS DOES NOT SUPPORT UNDO + + Parameters + ---------- + attrName : str or om.MPlug + The name of the attribute to get (For instance "pSphere2.translate", or "group1.pim[0]") + Or the MPlug itself + value : int, float, tuple, np.array + The correctly typed value to set on the attribute + ''' + if isinstance(attrName, str): + sl = om.MSelectionList() + sl.add(attrName) + plug = om.MPlug() + sl.getPlug(0, plug) + elif isinstance(attrName, om.MPlug): + plug = attrName + else: + raise ValueError("Data must be string or MPlug. Got {0}".format(type(attrName))) + + # First just check if the data is numeric + mdh = plug.asMDataHandle() + if mdh.isNumeric(): + # So, at this point, you should really just use setattr + ntype = mdh.numericType() + if ntype in _NTYPE_DICT: + _NTYPE_DICT[ntype][1](mdh, *value) + plug.setMObject(mdh.data()) + elif ntype == om.MFnNumericData.k4Double: + NotImplementedError("Haven't implemented double4 access yet") + else: + raise RuntimeError("I don't know how to set data on the given attribute") + else: + # The data is more complex than a simple number. + try: + pmo = plug.asMObject() + except RuntimeError as e: + # raise a more descriptive error. And make sure to actually print the plug name + raise RuntimeError( + "I don't know how to access data from the given attribute" + ) from e + apiType = pmo.apiType() + + if apiType in _DTYPE_DICT: + # build the pointArrayData + fnType, mType = _DTYPE_DICT[apiType] + fn = fnType() + mPts = numpyToMaya(value, mType) + dataObj = fn.create(mPts) + plug.setMObject(dataObj) + return + + elif apiType == om.MFn.kComponentListData: + fnCompList = om.MFnComponentListData() + compList = fnCompList.create() + fnIdx = om.MFnSingleIndexedComponent() + idxObj = fnIdx.create(om.MFn.kMeshVertComponent) + mIdxs = numpyToMaya(value, om.MIntArray) + fnIdx.addElements(mIdxs) + fnCompList.add(idxObj) + plug.setMObject(compList) + return + else: + apiTypeStr = pmo.apiTypeStr() + raise NotImplementedError( + "I don't know how to handle {0} yet".format(apiTypeStr) + ) + + raise NotImplementedError("WTF? How did you get here??") + + +################################################################################ + + +def test(): + import time + from maya import cmds + + meshName = 'pSphere1' + bsName = 'blendShape1' + meshIdx = 0 + bsIdx = 0 + + # A quick test showing how to build a numpy array + # containing the deltas for a shape on a blendshape node + numVerts = cmds.polyEvaluate(meshName, vertex=True) + baseAttr = '{0}.it[{1}].itg[{2}].iti[6000]'.format(bsName, meshIdx, bsIdx) + inPtAttr = baseAttr + '.inputPointsTarget' + inCompAttr = baseAttr + '.inputComponentsTarget' + + start = time.time() + points = getNumpyAttr(inPtAttr) + idxs = getNumpyAttr(inCompAttr) + ret = np.zeros((numVerts, 4)) + ret[idxs] = points + end = time.time() + + print("IDXS", idxs.shape) + print("OUT", points.shape) + print("RET", ret.shape) + print("TOOK", end - start) + + +if __name__ == "__main__": + test() diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 635f7700..3aaa8112 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -22,9 +22,6 @@ import itertools import json -import six -from six.moves import map, zip - try: import numpy as np except ImportError: @@ -121,7 +118,7 @@ def __deepcopy__(self, memo): cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result - for k, v in six.iteritems(self.__dict__): + for k, v in self.__dict__.items(): if k == "models": # do not make a copy of the connected models # a deepcopied simplex won't be connected to a UI @@ -1583,7 +1580,7 @@ def buildSplitterList(self, foList): # Dict of {item : falloff} splitBy = {} - for item, foSet in six.iteritems(splitBySet): + for item, foSet in splitBySet.items(): # Because I can only split an item once on an axis # Assume that the foList is in priority order # So get the min-indexed falloff in the set @@ -1673,7 +1670,7 @@ def split(self, pBar=None): for fo in splitSmpx.falloffs: foByAxis.setdefault(fo.axis.lower(), []).append(fo) - for axis, foList in six.iteritems(foByAxis): + for axis, foList in foByAxis.items(): if pBar is not None: pBar.setLabelText("Splitting On {0} axis".format(axis)) QApplication.processEvents() diff --git a/src/python/simplexui/menu/genericPlugins/unsubdivide.py b/src/python/simplexui/menu/genericPlugins/unsubdivide.py index 369a1491..c6a827a6 100644 --- a/src/python/simplexui/menu/genericPlugins/unsubdivide.py +++ b/src/python/simplexui/menu/genericPlugins/unsubdivide.py @@ -24,21 +24,22 @@ from Qt import QtCompat from Qt.QtWidgets import QAction, QMessageBox, QProgressDialog + try: - import imathnumpy + from ..commands.numpytoimath import numpyToImath except ImportError: - imathnumpy = None + numpyToImath = None def registerTool(window, menu): - if imathnumpy is not None: + if numpyToImath is not None: exportUnsubACT = QAction("Un Subdivide Smpx ...", window) menu.addAction(exportUnsubACT) exportUnsubACT.triggered.connect(partial(exportUnsubInterface, window)) def exportUnsubInterface(window): - if imathnumpy is None: + if numpyToImath is None: QMessageBox.warning( window, "No ImathToNumpy", From b3533fe31afe0c7bdde4b08cce520b1d66a86af1 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 25 Apr 2025 15:13:27 -0700 Subject: [PATCH 06/25] Remove the python 2 compat stuff --- src/python/simplexui/__init__.py | 1 - src/python/simplexui/channelBox.py | 2 -- src/python/simplexui/comboCheckDialog.py | 4 --- .../simplexui/commands/alembicCommon.py | 6 +--- .../simplexui/commands/applyCorrectives.py | 3 -- .../simplexui/commands/correctiveInterface.py | 9 ++--- .../simplexui/commands/expandedExport.py | 13 +++---- src/python/simplexui/commands/hdf5Convert.py | 2 -- .../commands/mayaCorrectiveInterface.py | 3 -- src/python/simplexui/commands/mesh.py | 15 +++----- .../commands/reorderSimplexPoints.py | 2 -- src/python/simplexui/commands/rigidAlign.py | 4 --- src/python/simplexui/commands/smpxBlend.py | 17 ++++----- src/python/simplexui/commands/unsubdivide.py | 15 +++----- src/python/simplexui/commands/uvTransfer.py | 19 +++------- .../commands/xsiCorrectiveInterface.py | 5 +-- src/python/simplexui/falloffDialog.py | 7 +--- src/python/simplexui/interface/__init__.py | 2 -- .../simplexui/interface/dummyInterface.py | 4 --- .../simplexui/interface/mayaInterface.py | 36 +++++++++---------- .../simplexui/interface/xsiInterface.py | 10 ++---- src/python/simplexui/interfaceModel.py | 4 --- src/python/simplexui/interfaceModelTrees.py | 2 -- src/python/simplexui/interfaceModel_Test.py | 4 --- src/python/simplexui/items/accessor.py | 6 +--- src/python/simplexui/items/combo.py | 2 -- src/python/simplexui/items/falloff.py | 2 -- src/python/simplexui/items/progression.py | 2 -- src/python/simplexui/items/shape.py | 2 -- src/python/simplexui/items/simplex.py | 2 -- src/python/simplexui/items/slider.py | 2 -- src/python/simplexui/items/stack.py | 2 -- src/python/simplexui/items/traversal.py | 9 ++--- src/python/simplexui/menu/__init__.py | 2 -- .../menu/genericPlugins/_builtins.py | 2 -- .../genericPlugins/checkPossibleCombos.py | 2 -- .../menu/genericPlugins/exportSplit.py | 2 -- .../menu/genericPlugins/simplexUvTransfer.py | 2 -- .../menu/genericPlugins/unsubdivide.py | 2 -- .../simplexui/menu/mayaPlugins/exportOther.py | 2 -- .../menu/mayaPlugins/extractProgressives.py | 2 -- .../simplexui/menu/mayaPlugins/freezeCombo.py | 7 +--- .../mayaPlugins/generateShapeIncrementals.py | 3 -- .../simplexui/menu/mayaPlugins/importObjs.py | 11 +++--- .../menu/mayaPlugins/linearizeTraversal.py | 1 - .../menu/mayaPlugins/makeShelfBtn.py | 2 -- .../menu/mayaPlugins/relaxToSelection.py | 2 -- .../menu/mayaPlugins/reloadDefinition.py | 1 - .../menu/mayaPlugins/snapToNeutral.py | 2 -- .../menu/mayaPlugins/softSelectToCluster.py | 3 -- .../simplexui/menu/mayaPlugins/tweakMix.py | 8 ++--- .../menu/mayaPlugins/updateRestShape.py | 2 -- src/python/simplexui/simplexDialog.py | 9 +---- src/python/simplexui/travCheckDialog.py | 7 +--- src/python/simplexui/traversalDialog.py | 8 +---- src/python/simplexui/utils.py | 2 -- 56 files changed, 64 insertions(+), 238 deletions(-) diff --git a/src/python/simplexui/__init__.py b/src/python/simplexui/__init__.py index 0814d8ec..e104cecd 100644 --- a/src/python/simplexui/__init__.py +++ b/src/python/simplexui/__init__.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import from _version import __version__ SIMPLEX_UI = None diff --git a/src/python/simplexui/channelBox.py b/src/python/simplexui/channelBox.py index 3d826b81..5711a86d 100644 --- a/src/python/simplexui/channelBox.py +++ b/src/python/simplexui/channelBox.py @@ -21,8 +21,6 @@ """ # pylint:disable=unused-import,relative-import,missing-docstring,unused-argument,no-self-use -from __future__ import absolute_import, print_function - import os import sys diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index 5f8cdd23..29086d68 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -15,12 +15,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - from itertools import combinations, product -from six.moves import range, zip - from .dragFilter import DragFilter from .items import Combo, Slider from Qt import QtCompat diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 48e25076..70046a49 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -19,11 +19,8 @@ This is a library of convenience functions with the numpy speed optimizations """ -from __future__ import absolute_import, print_function - import os -import six from alembic.Abc import IArchive, OArchive, OStringProperty from alembic.AbcGeom import ( GeometryScope, @@ -36,7 +33,6 @@ OXform, ) from imath import IntArray, UnsignedIntArray, V2f, V2fArray, V3fArray -from six.moves import range, zip try: import numpy as np @@ -760,7 +756,7 @@ def buildAbc( opar = OXform(parent, str(name + transformSuffix)) if propDict: props = opar.getSchema().getUserProperties() - for k, v in six.iteritems(propDict): + for k, v in propDict.items(): writeStringProperty(props, str(k), str(v), ogawa=ogawa) omesh = OPolyMesh(opar, str(name + shapeSuffix)) diff --git a/src/python/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py index 1e630e64..4831737f 100644 --- a/src/python/simplexui/commands/applyCorrectives.py +++ b/src/python/simplexui/commands/applyCorrectives.py @@ -16,13 +16,10 @@ # along with Simplex. If not, see . # pylint:disable=unused-variable -from __future__ import absolute_import, print_function - import itertools import os from pysimplex import PySimplex -from six.moves import map, zip from ..items import Combo, Simplex, Slider from Qt.QtWidgets import QApplication diff --git a/src/python/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py index e9245078..b935d96b 100644 --- a/src/python/simplexui/commands/correctiveInterface.py +++ b/src/python/simplexui/commands/correctiveInterface.py @@ -15,11 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - -import six -from six.moves import zip - from Qt.QtWidgets import QApplication try: @@ -197,14 +192,14 @@ def buildCorrectiveReferences(mesh, simplex, poses, sliders, pBar=None): pBar.setLabelText("Building Combo References") pBar.setValue(0) mv = 0 - for combo in six.iterkeys(sliderValuesByCombo): + for combo in sliderValuesByCombo: for p in combo.prog.pairs: if not p.shape.isRest: mv += 1 pBar.setMaximum(mv) QApplication.processEvents() - for combo, sliderVals in six.iteritems(sliderValuesByCombo): + for combo, sliderVals in sliderValuesByCombo.items(): # components = frozenset(sliderVals) poses = [poseBySlider[s] for s, _ in sliderVals] for p in combo.prog.pairs: diff --git a/src/python/simplexui/commands/expandedExport.py b/src/python/simplexui/commands/expandedExport.py index 551d9a18..5ba74e93 100644 --- a/src/python/simplexui/commands/expandedExport.py +++ b/src/python/simplexui/commands/expandedExport.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - -import six from pysimplex import PySimplex from ..interface.mayaInterface import DCC, disconnected @@ -67,7 +64,7 @@ def setSliderGroup(ctrls, val): for ctrl in ctrls: _setSliders(ctrl, val, svs) - for smpx, (slis, vals) in six.iteritems(svs): + for smpx, (slis, vals) in svs.items(): smpx.setSlidersWeights(slis, vals) @@ -306,8 +303,8 @@ def parseExpandedData(smpx, restShape, sliderShapes, comboShapes, travShapes): travIdxs = sorted(set([indexByShape[s] for s in travShapeSet])) # Sliders are simple, just set their shapes directly - for ppDict in six.itervalues(sliderShapes): - for pp, shp in six.iteritems(ppDict): + for ppDict in sliderShapes.values(): + for pp, shp in ppDict.items(): shapeArray[indexByShape[pp.shape]] = shp - restShape # First sort the combos by depth @@ -317,7 +314,7 @@ def parseExpandedData(smpx, restShape, sliderShapes, comboShapes, travShapes): for depth in sorted(comboByDepth.keys()): for combo in comboByDepth[depth]: - for pp, shp in six.iteritems(comboShapes[combo]): + for pp, shp in comboShapes[combo].items(): inVec = _buildSolverInputs(smpx, combo, pp.value, indexBySlider) outVec = np.array(solver.solve(inVec)) outVec[np.where(np.isclose(outVec, 0.0))] = 0.0 @@ -342,7 +339,7 @@ def parseExpandedData(smpx, restShape, sliderShapes, comboShapes, travShapes): for depth in sorted(travByDepth.keys()): for trav in travByDepth[depth]: - for pp, shp in six.iteritems(travShapes[trav]): + for pp, shp in travShapes[trav].items(): inVec = _buildSolverInputs(smpx, trav, pp.value, indexBySlider) outVec = np.array(solver.solve(inVec)) outVec[np.where(np.isclose(outVec, 0.0))] = 0.0 diff --git a/src/python/simplexui/commands/hdf5Convert.py b/src/python/simplexui/commands/hdf5Convert.py index 32ac5ab4..17194d0f 100644 --- a/src/python/simplexui/commands/hdf5Convert.py +++ b/src/python/simplexui/commands/hdf5Convert.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - import json from .alembicCommon import buildSmpx, readSmpx diff --git a/src/python/simplexui/commands/mayaCorrectiveInterface.py b/src/python/simplexui/commands/mayaCorrectiveInterface.py index bf34e019..438160c9 100644 --- a/src/python/simplexui/commands/mayaCorrectiveInterface.py +++ b/src/python/simplexui/commands/mayaCorrectiveInterface.py @@ -17,13 +17,10 @@ """ Get the corrective deltas from a rig in Maya """ -from __future__ import absolute_import - from ctypes import c_float from maya import OpenMaya as om from maya import cmds -from six.moves import range try: import numpy as np diff --git a/src/python/simplexui/commands/mesh.py b/src/python/simplexui/commands/mesh.py index 4907f693..b6a8e23a 100644 --- a/src/python/simplexui/commands/mesh.py +++ b/src/python/simplexui/commands/mesh.py @@ -28,11 +28,6 @@ VertSet and FaceSet classes are just sets that also contain references back to the mesh """ -from __future__ import absolute_import - -import six -from six.moves import range, zip - class Mesh(object): """ @@ -349,7 +344,7 @@ def _linkPairs(pairs): while fwPairs: linked = [] # pick a random start from whatever's left - nxt = next(six.iterkeys(fwPairs)) + nxt = next(fwPairs.keys()) while nxt is not None: # Follow the pairs around until I cant find more nnxt = fwPairs.pop(nxt, None) @@ -359,7 +354,7 @@ def _linkPairs(pairs): if fwPairs and linked[0][0] != linked[-1][0]: # if there's still some left and we didn't find a cycle # then search backwards - bkPairs = {j: i for i, j in six.iteritems(fwPairs)} + bkPairs = {j: i for i, j in fwPairs.items()} inv = [] nxt = linked[0][0] while nxt is not None: @@ -370,7 +365,7 @@ def _linkPairs(pairs): # reverse and remove the extra (idx, None) pair linked = inv[-2::-1] + linked # Rebuild what's left into a new dict for the next group - fwPairs = {j: i for i, j in six.iteritems(bkPairs)} + fwPairs = {j: i for i, j in bkPairs.items()} # Parse the final output fin = [i for i, _ in linked] @@ -588,7 +583,7 @@ def getBorderVerts(self): VertSet of border vertices """ out = VertSet(self) - for edge, adj in six.iteritems(self.faceEdgeAdjacency): + for edge, adj in self.faceEdgeAdjacency.items(): if None in adj: out.update(edge) return out @@ -913,7 +908,7 @@ def inner(self, *args): return super(MeshSetMeta, mcs).__new__(mcs, clsName, bases, dct) -class MeshSet(six.with_metaclass(MeshSetMeta, set)): +class MeshSet(set, metaclass=MeshSetMeta): """An set-like object that deals with geometry""" def __init__(self, mesh, indices=None): diff --git a/src/python/simplexui/commands/reorderSimplexPoints.py b/src/python/simplexui/commands/reorderSimplexPoints.py index 0393e39f..0f16af33 100644 --- a/src/python/simplexui/commands/reorderSimplexPoints.py +++ b/src/python/simplexui/commands/reorderSimplexPoints.py @@ -26,8 +26,6 @@ possible to invert the range if you think you've got it backwards """ # pylint:disable=wrong-import-position -from __future__ import absolute_import, print_function - import json from .alembicCommon import buildSmpx, readSmpx diff --git a/src/python/simplexui/commands/rigidAlign.py b/src/python/simplexui/commands/rigidAlign.py index 43d036a9..e51acb6a 100644 --- a/src/python/simplexui/commands/rigidAlign.py +++ b/src/python/simplexui/commands/rigidAlign.py @@ -15,10 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - -from six.moves import range - try: import numpy as np except ImportError: diff --git a/src/python/simplexui/commands/smpxBlend.py b/src/python/simplexui/commands/smpxBlend.py index 80f9e328..d8c1b772 100644 --- a/src/python/simplexui/commands/smpxBlend.py +++ b/src/python/simplexui/commands/smpxBlend.py @@ -15,11 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import print_function - -import six -from six.moves import zip - from ..items import ( Combo, ComboPair, @@ -72,8 +67,8 @@ def smpxMismatchCheck(simpA, simpB): saSliderNames = {sa.name: sa for sa in simpA.sliders} sbSliderNames = {sb.name: sb for sb in simpB.sliders} - saSliderNameUnique = six.viewkeys(saSliderNames) - six.viewkeys(sbSliderNames) - sbSliderNameUnique = six.viewkeys(sbSliderNames) - six.viewkeys(saSliderNames) + saSliderNameUnique = saSliderNames.keys() - sbSliderNames.keys() + sbSliderNameUnique = sbSliderNames.keys() - saSliderNames.keys() slnMatch = {} for san in saSliderNameUnique: slnMatch[san] = (san, None) @@ -83,8 +78,8 @@ def smpxMismatchCheck(simpA, simpB): saComboNames = {sa.name: sa for sa in simpA.combos} sbComboNames = {sb.name: sb for sb in simpB.combos} - saComboNameUnique = six.viewkeys(saComboNames) - six.viewkeys(sbComboNames) - sbComboNameUnique = six.viewkeys(sbComboNames) - six.viewkeys(saComboNames) + saComboNameUnique = saComboNames.keys() - sbComboNames.keys() + sbComboNameUnique = sbComboNames.keys() - saComboNames.keys() # TODO search for combos with mis-ordered inputs cnMatch = {} for can in saComboNameUnique: @@ -95,8 +90,8 @@ def smpxMismatchCheck(simpA, simpB): saTravNames = {sa.name: sa for sa in simpA.traversals} sbTravNames = {sb.name: sb for sb in simpB.traversals} - saTravNameUnique = six.viewkeys(saTravNames) - six.viewkeys(sbTravNames) - sbTravNameUnique = six.viewkeys(sbTravNames) - six.viewkeys(saTravNames) + saTravNameUnique = saTravNames.keys() - sbTravNames.keys() + sbTravNameUnique = sbTravNames.keys() - saTravNames.keys() # TODO search for travs with mis-ordered inputs tnMatch = {} for tan in saTravNameUnique: diff --git a/src/python/simplexui/commands/unsubdivide.py b/src/python/simplexui/commands/unsubdivide.py index 06d42453..38fa13dc 100644 --- a/src/python/simplexui/commands/unsubdivide.py +++ b/src/python/simplexui/commands/unsubdivide.py @@ -18,13 +18,8 @@ # pylint: disable=unused-argument, too-many-locals # pylint:disable=E0611,E0401 -from __future__ import absolute_import, print_function - import json -from itertools import chain - -import six -from six.moves import map, range, zip, zip_longest +from itertools import chain, zip_longest from Qt.QtWidgets import QApplication from .alembicCommon import buildSmpx, pbPrint, readSmpx @@ -155,7 +150,7 @@ def buildHint(island, neigh, borders): d.setdefault(len(neigh[v]), []).append(v) dd = {} - for k, v in six.iteritems(d): + for k, v in d.items(): dd.setdefault(len(v), []).append(k) mkey = min(dd.keys()) @@ -406,7 +401,7 @@ def buildNeighborDict(faces): borders = set() out = {} - for k, v in six.iteritems(fanDict): + for k, v in fanDict.items(): fans, cycles = mergeCycles(v) for f, c in zip(fans, cycles): if not c: @@ -462,7 +457,7 @@ def buildLayeredNeighborDicts(faces, uFaces, dWings): borders >= uBorders ), "Somehow the unsubdivided borders contain different vIdxs" - for i, (k, uNeigh) in enumerate(six.iteritems(uNeighDict)): + for i, (k, uNeigh) in enumerate(uNeighDict.items()): neighDict[k] = _align(neighDict[k], uNeigh, dWings) return neighDict, uNeighDict, edgeDict, uEdgeDict, borders @@ -690,7 +685,7 @@ def deleteCenters(meshFaces, uvFaces, centerDel, pBar=None): pBar.setMaximum(len(faceDelDict)) chk = -1 - for idx, rFaces in six.iteritems(faceDelDict): + for idx, rFaces in faceDelDict.items(): chk += 1 if pBar is not None: pBar.setValue(chk) diff --git a/src/python/simplexui/commands/uvTransfer.py b/src/python/simplexui/commands/uvTransfer.py index eb5b807d..ff292448 100644 --- a/src/python/simplexui/commands/uvTransfer.py +++ b/src/python/simplexui/commands/uvTransfer.py @@ -16,16 +16,7 @@ # along with Simplex. If not, see . # pylint: disable=invalid-name -from __future__ import absolute_import, division, print_function - -import six -from six.moves import range, zip - -try: - import numpy as np -except ImportError: - pass - +import numpy as np INF = float("inf") EPS = 1e-7 @@ -213,7 +204,7 @@ def mmvc(rawFaces, points, samples, uvToFace, tol=EPS): barys[idxs] = b ret = {} - for qIdxs, barys in six.itervalues(out): + for qIdxs, barys in out.values(): for qi, b in zip(qIdxs, barys): ret[qi] = (uvToFace[qi], b) @@ -652,7 +643,7 @@ def getUvCorrelation(samples, points, faces, tol=0.0001, handleMissing=True, pBa """ tris, triMap, borderMap = triangulateUVs(faces, points) swept, missing = sweep(samples, points, tris, pBar=pBar) - uvToFace = {uvI: triMap[tI] for uvI, tI in six.iteritems(swept)} + uvToFace = {uvI: triMap[tI] for uvI, tI in swept.items()} # import __main__ # __main__.__dict__.update(locals()) # raise RuntimeError("STOPPIT") @@ -760,7 +751,7 @@ def getVertCorrelation( continue mvcVertDict.setdefault(childUvToVert[uvIdx], []).append(mvcUvDict[uvIdx]) - missing = set(range(cNumVerts)) - six.viewkeys(mvcVertDict) + missing = set(range(cNumVerts)) - mvcVertDict.keys() if missing: import time @@ -799,7 +790,7 @@ def applyTransfer(parVerts, parFaces, correlation, outputSize): parVerts = parVerts[None, ...] rows, cols, vals = [], [], [] - for cVertIdx, corrPoss in six.iteritems(correlation): + for cVertIdx, corrPoss in correlation.items(): if len(corrPoss) == 1: pFaceIdx, bary = corrPoss[0] else: diff --git a/src/python/simplexui/commands/xsiCorrectiveInterface.py b/src/python/simplexui/commands/xsiCorrectiveInterface.py index c760234d..b33148fb 100644 --- a/src/python/simplexui/commands/xsiCorrectiveInterface.py +++ b/src/python/simplexui/commands/xsiCorrectiveInterface.py @@ -17,11 +17,8 @@ """ Get the corrective deltas from a rig in XSI """ -from __future__ import absolute_import - from dcc.xsi import constants, xsi from dcc.xsi.ice import ICETree # pylint:disable=import-error -from six.moves import zip def setPose(pvp, multiplier): @@ -46,7 +43,7 @@ def resetPose(pvp): pvp : [(str, float), ...] A list of property/value pairs """ - for prop, val in pvp: + for prop, _val in pvp: xsi.setValue(prop, 0) diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index 308116a7..7ebe9133 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -17,14 +17,9 @@ # This module imports QT from PyQt4, PySide or PySide2 # Depending on what's available -from __future__ import absolute_import, print_function - import os import re -import six -from six.moves import range - import Qt as QtLib from .interfaceModel import FalloffDataModel from .items import Falloff @@ -366,7 +361,7 @@ def loadSimplex(self): print("Adding Mappings") currentIndex = "currentIndex" - if six.PY3 and (QtLib.IsPySide2 or QtLib.IsPyQt5): + if QtLib.IsPySide2 or QtLib.IsPyQt5: currentIndex = QByteArray(bytes("Test", encoding="utf-8")) self._falloffMapper.addMapping(self.uiFalloffTypeCBOX, 1, currentIndex) diff --git a/src/python/simplexui/interface/__init__.py b/src/python/simplexui/interface/__init__.py index c142e40d..a7efccf7 100644 --- a/src/python/simplexui/interface/__init__.py +++ b/src/python/simplexui/interface/__init__.py @@ -16,8 +16,6 @@ # along with Simplex. If not, see . # This file will serve as the only place where the choice of DCC will be chosen -from __future__ import absolute_import - import os import sys diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index a980eafd..3b36ff04 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -17,14 +17,10 @@ # pylint: disable=invalid-name, unused-argument """ A placeholder interface that takes arguments and does nothing with them """ -from __future__ import absolute_import - import copy from contextlib import contextmanager from functools import wraps -from six.moves import map, zip - from Qt import QtCore from Qt.QtCore import Signal diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index bbc770d3..91d3e092 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -16,8 +16,6 @@ # along with Simplex. If not, see . # pylint: disable=invalid-name -from __future__ import absolute_import - import json import re from contextlib import contextmanager @@ -26,10 +24,8 @@ import maya.cmds as cmds import maya.OpenMaya as om -import six from alembic.AbcGeom import GeometryScope, OPolyMeshSchemaSample, OV2fGeomParamSample from imath import IntArray, UnsignedIntArray, V2fArray, V3fArray -from six.moves import map, range, zip from ..commands.alembicCommon import mkSampleVertexPoints from Qt import QtCore @@ -169,8 +165,8 @@ def doReconnect(cnxs): ------- """ - for tdict in six.itervalues(cnxs): - for s, d in six.iteritems(tdict): + for tdict in cnxs.values(): + for s, d in tdict.items(): if not cmds.isConnected(s, d): cmds.connectAttr(s, d, force=True) @@ -676,7 +672,7 @@ def loadAbc(self, abcMesh, js, pBar=None): } fps = cmds.currentUnit(time=True, query=True) - if isinstance(fps, six.string_types): + if isinstance(fps, str): if fps.endswith("fps"): fps = fps[:-3] if fps in timeUnits: @@ -761,7 +757,7 @@ def getAllShapeVertices(self, shapes, pBar=None): ptCount = meshFn.numVertices() with disconnected(self.shapeNode) as cnx: shapeCnx = cnx[self.shapeNode] - for v in six.itervalues(shapeCnx): + for v in shapeCnx.values(): cmds.setAttr(v, 0.0) if pBar is not None: @@ -811,7 +807,7 @@ def getShapeVertices(self, shape): """ with disconnected(self.shapeNode) as cnx: shapeCnx = cnx[self.shapeNode] - for v in six.itervalues(shapeCnx): + for v in shapeCnx.values(): cmds.setAttr(v, 0.0) cmds.setAttr(shape.thing, 1.0) if np is None: @@ -1116,7 +1112,7 @@ def exportAbc( with disconnected(self.shapeNode) as cnx: shapeCnx = cnx[self.shapeNode] - for v in six.itervalues(shapeCnx): + for v in shapeCnx.values(): cmds.setAttr(v, 0.0) for i, shape in enumerate(shapes): if pBar is not None: @@ -1162,7 +1158,7 @@ def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): with disconnected(self.op) as allSliderCnx: sliderCnx = allSliderCnx[self.op] # zero all slider vals on the op to get the rest shape - for a in six.itervalues(sliderCnx): + for a in sliderCnx.values(): cmds.setAttr(a, 0.0) restVerts = self.getNumpyShape(dccMesh, world=world) @@ -1428,7 +1424,7 @@ def extractWithDeltaShape(self, shape, live=True, offset=10.0): """ with disconnected(self.shapeNode) as cnx: shapeCnx = cnx[self.shapeNode] - for v in six.itervalues(shapeCnx): + for v in shapeCnx.values(): cmds.setAttr(v, 0.0) # store the delta shape @@ -2157,7 +2153,7 @@ def _reparentDeltaShapes(self, par, nodeDict, bsNode, toDelete=None): shapeDict = {} origDict = {} - for name, node in six.iteritems(nodeDict): + for name, node in nodeDict.items(): shape = cmds.listRelatives(node, noIntermediate=1, shapes=1)[0] shape = cmds.ls(shape, absoluteName=1)[0] if shape: @@ -2233,7 +2229,7 @@ def _createTravDelta(self, trav, target, tVal, doReparent=True): sliderCnx = cnx[self.op] # zero all slider vals on the op - for a in six.itervalues(sliderCnx): + for a in sliderCnx.values(): cmds.setAttr(a, 0.0) with disconnected(floatShapes + tShapes): @@ -2246,7 +2242,7 @@ def _createTravDelta(self, trav, target, tVal, doReparent=True): for pair in trav.endPoint.pairs: sliDict[pair.slider].append(pair.value) - for slider, (start, end) in six.iteritems(sliDict): + for slider, (start, end) in sliDict.items(): vv = start + tVal * (end - start) cmds.setAttr(sliderCnx[slider.thing], vv) @@ -2308,7 +2304,7 @@ def extractTraversalShape(self, trav, shape, live=True, offset=10.0): with disconnected(self.op) as cnx: sliderCnx = cnx[self.op] # zero all slider vals on the op - for a in six.itervalues(sliderCnx): + for a in sliderCnx.values(): cmds.setAttr(a, 0.0) with disconnected(floatShapes): # tShapes @@ -2318,7 +2314,7 @@ def extractTraversalShape(self, trav, shape, live=True, offset=10.0): for pair in trav.endPoint.pairs: sliDict[pair.slider].append(pair.value) - for slider, (start, end) in six.iteritems(sliDict): + for slider, (start, end) in sliDict.items(): vv = start + val * (end - start) cmds.setAttr(sliderCnx[slider.thing], vv) @@ -2402,7 +2398,7 @@ def _createComboDelta(self, combo, target, tVal, doReparent=True): sliderCnx = cnx[self.op] # zero all slider vals on the op - for a in six.itervalues(sliderCnx): + for a in sliderCnx.values(): cmds.setAttr(a, 0.0) # pull out the rest shape @@ -2467,7 +2463,7 @@ def extractComboShape(self, combo, shape, live=True, offset=10.0): with disconnected(self.op) as cnx: sliderCnx = cnx[self.op] # zero all slider vals on the op - for a in six.itervalues(sliderCnx): + for a in sliderCnx.values(): cmds.setAttr(a, 0.0) with disconnected(floatShapes): @@ -2925,7 +2921,7 @@ def primeShapes(self, combo): with disconnected(self.shapeNode) as cnx: shapeCnx = cnx[self.shapeNode] - for v in six.itervalues(shapeCnx): + for v in shapeCnx.values(): cmds.setAttr(v, 0.0) for shape in upstreams: diff --git a/src/python/simplexui/interface/xsiInterface.py b/src/python/simplexui/interface/xsiInterface.py index 7d0fae86..329f4454 100644 --- a/src/python/simplexui/interface/xsiInterface.py +++ b/src/python/simplexui/interface/xsiInterface.py @@ -17,8 +17,6 @@ # pylint: disable=invalid-name -from __future__ import absolute_import, print_function - import json import os import tempfile @@ -28,10 +26,8 @@ import dcc.xsi as dcc import numpy as np -import six from alembic.AbcGeom import OPolyMeshSchemaSample from imath import IntArray, V3f, V3fArray -from six.moves import range, zip from ..commands.alembicCommon import mkUvSample from ..commands.buildIceXML import buildIceXML, buildLoaderXML, buildSliderIceXML @@ -974,7 +970,7 @@ def _exportAbcFaces(self, mesh): uvD = {uv: i for i, uv in enumerate(set(uvs))} uvIdxs = [uvD[uv] for uv in uvs] uvs = [None] * len(uvD) - for uv, idx in six.iteritems(uvD): + for uv, idx in uvD.items(): uvs[idx] = uv uvSample = mkUvSample(uvs, uvIdxs) @@ -1035,7 +1031,7 @@ def getMeshTopology(cls, mesh, uvName=None): uvD = {uv: i for i, uv in enumerate(set(uvs))} uvIdxs = [uvD[uv] for uv in uvs] uvs = [None] * len(uvD) - for uv, idx in six.iteritems(uvD): + for uv, idx in uvD.items(): uvs[idx] = uv ptr = 0 @@ -2338,7 +2334,7 @@ def setDisabled(op): """ nc = op.rootNodeContainer pairs = [] - for pName, port in six.iteritems(nc.inputPorts): + for pName, port in nc.inputPorts.items(): outs = list(port.connectedPorts.values()) if outs: pairs.append((port, outs[0])) diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index 526d5fbe..50c36c77 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -16,13 +16,9 @@ # along with Simplex. If not, see . # pylint:disable=missing-docstring,unused-argument,no-self-use,too-many-return-statements -from __future__ import absolute_import - import re from contextlib import contextmanager -from six.moves import range - from .items import ( Combo, ComboPair, diff --git a/src/python/simplexui/interfaceModelTrees.py b/src/python/simplexui/interfaceModelTrees.py index 45093226..cde2ee14 100644 --- a/src/python/simplexui/interfaceModelTrees.py +++ b/src/python/simplexui/interfaceModelTrees.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from six.moves import range - from .dragFilter import DragFilter from .items import Group from Qt.QtCore import QItemSelection, QItemSelectionModel, QModelIndex, QRegExp, Qt diff --git a/src/python/simplexui/interfaceModel_Test.py b/src/python/simplexui/interfaceModel_Test.py index c8f0ad92..0d684cba 100644 --- a/src/python/simplexui/interfaceModel_Test.py +++ b/src/python/simplexui/interfaceModel_Test.py @@ -15,13 +15,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import, print_function - import os import sys -from six.moves import range - # Add the parent folder to the path so I can import SimplexUI # Means I can run this test code from inside the module and # keep everything together diff --git a/src/python/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py index 7f53733f..c9c0d1fb 100644 --- a/src/python/simplexui/items/accessor.py +++ b/src/python/simplexui/items/accessor.py @@ -15,12 +15,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - import copy -import six - class SimplexAccessor(object): """The base object for all Simplex System object types @@ -99,7 +95,7 @@ def __deepcopy__(self, memo): cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result - for k, v in six.iteritems(self.__dict__): + for k, v in self.__dict__.items(): if k == "_thing" and self.DCC.program != "dummy": # DO NOT make a copy of the DCC thing (unless its a dummy dcc) # as it may or may not be a persistent object diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index 59b20807..ee33862b 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from six.moves import zip - from ..interface import undoContext # pylint:disable=missing-docstring,unused-argument,no-self-use diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index 26f4ea59..f92a1d1a 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -16,8 +16,6 @@ # along with Simplex. If not, see . # pylint:disable=missing-docstring,unused-argument,no-self-use -from __future__ import absolute_import - import copy import math diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index a5967fa4..4b60f250 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -16,8 +16,6 @@ # along with Simplex. If not, see . -from six.moves import range, zip - # pylint:disable=missing-docstring,unused-argument,no-self-use from ..utils import getNextName, nested from .accessor import SimplexAccessor diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index bc7e5afc..5515ad3b 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -16,8 +16,6 @@ # along with Simplex. If not, see . -from six.moves import zip - from ..interface import DCC, undoContext # pylint:disable=missing-docstring,unused-argument,no-self-use diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 3aaa8112..92981aac 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -16,8 +16,6 @@ # along with Simplex. If not, see . # pylint:disable=missing-docstring,unused-argument,no-self-use -from __future__ import absolute_import, print_function - import copy import itertools import json diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index 0882ed53..d18a10dd 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -16,8 +16,6 @@ # along with Simplex. If not, see . # pylint:disable=missing-docstring,unused-argument,no-self-use -from __future__ import absolute_import - import itertools from ..interface import undoContext diff --git a/src/python/simplexui/items/stack.py b/src/python/simplexui/items/stack.py index d613d752..75d6d25c 100644 --- a/src/python/simplexui/items/stack.py +++ b/src/python/simplexui/items/stack.py @@ -16,8 +16,6 @@ # along with Simplex. If not, see . # pylint:disable=missing-docstring,unused-argument,no-self-use -from __future__ import absolute_import - import copy from collections import OrderedDict from contextlib import contextmanager diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index 831fbd90..cc7db56e 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -import six -from six.moves import range, zip - # pylint:disable=missing-docstring,unused-argument,no-self-use from Qt.QtGui import QColor from ..utils import nested @@ -573,7 +570,7 @@ def ranges(self): """ startDict = {p.slider: p.value for p in self.startPoint.pairs} endDict = {p.slider: p.value for p in self.endPoint.pairs} - allSliders = six.viewkeys(startDict) | six.viewkeys(endDict) + allSliders = startDict.keys() | endDict.keys() rangeDict = {} for sli in allSliders: @@ -596,7 +593,7 @@ def buildTraversalName(ranges): """ static, dynamic = [], [] - for sli, rng in six.iteritems(ranges): + for sli, rng in ranges.items(): if rng[0] == rng[1]: static.append(sli) else: @@ -772,7 +769,7 @@ def loadV3(cls, simplex, progs, data): startDict = dict(data["start"]) endDict = dict(data["end"]) - sliIdxs = sorted(six.viewkeys(startDict) | six.viewkeys(endDict)) + sliIdxs = sorted(startDict.keys() | endDict.keys()) startPairs, endPairs = [], [] for idx in sliIdxs: startPairs.append(TravPair(simplex.sliders[idx], startDict.get(idx, 0.0))) diff --git a/src/python/simplexui/menu/__init__.py b/src/python/simplexui/menu/__init__.py index c070e361..7838e0e3 100644 --- a/src/python/simplexui/menu/__init__.py +++ b/src/python/simplexui/menu/__init__.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - import importlib import os import pkgutil diff --git a/src/python/simplexui/menu/genericPlugins/_builtins.py b/src/python/simplexui/menu/genericPlugins/_builtins.py index b13e8163..020b6b80 100644 --- a/src/python/simplexui/menu/genericPlugins/_builtins.py +++ b/src/python/simplexui/menu/genericPlugins/_builtins.py @@ -16,8 +16,6 @@ # along with Simplex. If not, see . # pylint:disable=unused-variable -from __future__ import absolute_import - from functools import partial from ...items import Combo, ComboPair, ProgPair, Progression, Slider diff --git a/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py b/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py index 58f0e880..a890f550 100644 --- a/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py +++ b/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - from functools import partial from ...comboCheckDialog import ComboCheckDialog diff --git a/src/python/simplexui/menu/genericPlugins/exportSplit.py b/src/python/simplexui/menu/genericPlugins/exportSplit.py index 0545cd03..bd05a97d 100644 --- a/src/python/simplexui/menu/genericPlugins/exportSplit.py +++ b/src/python/simplexui/menu/genericPlugins/exportSplit.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - from functools import partial from Qt import QtCompat diff --git a/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py b/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py index 6982a8d6..7592d8ea 100644 --- a/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py +++ b/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - import json from ...commands.alembicCommon import buildSmpx, readSmpx diff --git a/src/python/simplexui/menu/genericPlugins/unsubdivide.py b/src/python/simplexui/menu/genericPlugins/unsubdivide.py index c6a827a6..99be7fdd 100644 --- a/src/python/simplexui/menu/genericPlugins/unsubdivide.py +++ b/src/python/simplexui/menu/genericPlugins/unsubdivide.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - import os from functools import partial diff --git a/src/python/simplexui/menu/mayaPlugins/exportOther.py b/src/python/simplexui/menu/mayaPlugins/exportOther.py index 1097ceef..502b98e1 100644 --- a/src/python/simplexui/menu/mayaPlugins/exportOther.py +++ b/src/python/simplexui/menu/mayaPlugins/exportOther.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - from functools import partial import maya.cmds as cmds diff --git a/src/python/simplexui/menu/mayaPlugins/extractProgressives.py b/src/python/simplexui/menu/mayaPlugins/extractProgressives.py index e40b90f8..e320ab89 100644 --- a/src/python/simplexui/menu/mayaPlugins/extractProgressives.py +++ b/src/python/simplexui/menu/mayaPlugins/extractProgressives.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - from functools import partial from ...interfaceModel import coerceIndexToType diff --git a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py index 91dca12f..55b967dd 100644 --- a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py +++ b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py @@ -13,13 +13,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - from functools import partial -import six -from six.moves import zip - from maya import cmds from ...interface.mayaInterface import disconnected @@ -87,7 +82,7 @@ def freezeCombo(combo): freezeShapes.append(freezeShape) # zero all the sliders cnx = sliderCnx[simplex.DCC.op] - for a in six.itervalues(cnx): + for a in cnx.values(): cmds.setAttr(a, 0.0) # set the combo values diff --git a/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py index 2206e108..47bfd7ab 100644 --- a/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py +++ b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py @@ -15,12 +15,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - from functools import partial import maya.cmds as cmds -from six.moves import range from ...interfaceModel import coerceIndexToType from ...items import Combo, Slider diff --git a/src/python/simplexui/menu/mayaPlugins/importObjs.py b/src/python/simplexui/menu/mayaPlugins/importObjs.py index 8c49277d..4600ae19 100644 --- a/src/python/simplexui/menu/mayaPlugins/importObjs.py +++ b/src/python/simplexui/menu/mayaPlugins/importObjs.py @@ -1,10 +1,7 @@ -from __future__ import absolute_import - import os from functools import partial import maya.cmds as cmds -import six from ...items import Combo, Slider, Traversal from Qt.QtWidgets import ( @@ -136,23 +133,23 @@ def importObjList(simplex, paths, pBar, reorder=True): travMasters[shape.name] = master comboDepth = {} - for k, v in six.iteritems(comboMasters): + for k, v in comboMasters.items(): depth = len(v.pairs) comboDepth.setdefault(depth, {})[k] = v importOrder = [] - for shapeName, slider in six.iteritems(sliderMasters): + for shapeName, slider in sliderMasters.items(): importOrder.append( (shapeName, slider, shapeDict[shapeName], inPairs[shapeName]) ) for depth in sorted(comboDepth.keys()): - for shapeName, combo in six.iteritems(comboDepth[depth]): + for shapeName, combo in comboDepth[depth].items(): importOrder.append( (shapeName, combo, shapeDict[shapeName], inPairs[shapeName]) ) - for shapeName, trav in six.iteritems(travMasters): + for shapeName, trav in travMasters.items(): importOrder.append((shapeName, trav, shapeDict[shapeName], inPairs[shapeName])) if pBar is not None: diff --git a/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py index 3605c846..dab8823e 100644 --- a/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py +++ b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import from functools import partial import maya.cmds as cmds from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py index 44f97be8..c22f0236 100644 --- a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py +++ b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - import os from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py index 30b4b5d0..16727300 100644 --- a/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py +++ b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - import maya.cmds as cmds from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py index d8ad944a..5172228b 100644 --- a/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py +++ b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import from functools import partial from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py index a6e8d1f9..5ee142e6 100644 --- a/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py +++ b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - from functools import partial import maya.cmds as cmds diff --git a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py index 54cecdc5..daa2dab5 100644 --- a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py +++ b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py @@ -15,11 +15,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import, print_function - import maya.cmds as cmds import maya.OpenMaya as om -from six.moves import range from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/tweakMix.py b/src/python/simplexui/menu/mayaPlugins/tweakMix.py index fb7b3bec..991a59fa 100644 --- a/src/python/simplexui/menu/mayaPlugins/tweakMix.py +++ b/src/python/simplexui/menu/mayaPlugins/tweakMix.py @@ -15,13 +15,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - from functools import partial import maya.cmds as cmds -import six -from six.moves import range, zip from ...interface.mayaInterface import disconnected from ...interfaceModel import coerceIndexToType @@ -93,7 +89,7 @@ def tweakMix(simplex, combos, live): # disconnect any float shapes with disconnected(floatShapes): cnx = sliderCnx[simplex.DCC.op] - for a in six.itervalues(cnx): + for a in cnx.values(): cmds.setAttr(a, 0.0) # set the combo values @@ -108,7 +104,7 @@ def tweakMix(simplex, combos, live): tweakMeshes = [] with disconnected(simplex.DCC.shapeNode) as shapeCnx: cnx = shapeCnx[simplex.DCC.shapeNode] - for a in six.itervalues(cnx): + for a in cnx.values(): cmds.setAttr(a, 0.0) for tshape, shapeVal in tweakShapes: cmds.setAttr(tshape.thing, shapeVal) diff --git a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py index ea951cd7..87fdb18d 100644 --- a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py +++ b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py @@ -15,8 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - import textwrap from functools import partial diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 2e0b6654..af250909 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -19,8 +19,6 @@ # Ignore a bunch of linter warnings that show up because of my choice of abstraction # pylint: disable=unused-argument,too-many-public-methods,relative-import # pylint: disable=too-many-statements,no-self-use,missing-docstring -from __future__ import absolute_import - import json import os import re @@ -28,9 +26,6 @@ import weakref from contextlib import contextmanager -import six -from six.moves import range, zip - from .comboCheckDialog import ComboCheckDialog from .falloffDialog import FalloffDialog from .interface import DCC @@ -829,9 +824,7 @@ def shapeConnectScene(self): pairDict[pp.shape.name] = pp # get all common names - selKeys = set(six.iterkeys(selDict)) - pairKeys = set(six.iterkeys(pairDict)) - common = selKeys & pairKeys + common = selDict.keys() & pairDict.keys() # get those items pairs = [pairDict[i] for i in common] diff --git a/src/python/simplexui/travCheckDialog.py b/src/python/simplexui/travCheckDialog.py index 055b21ff..801604c6 100644 --- a/src/python/simplexui/travCheckDialog.py +++ b/src/python/simplexui/travCheckDialog.py @@ -15,13 +15,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from __future__ import absolute_import - from itertools import combinations, product -import six -from six.moves import range, zip - from .dragFilter import DragFilter from .items import Slider, Traversal from Qt import QtCompat @@ -111,7 +106,7 @@ def buildPossibleTraversals( sls = trav.allSliders() if all(r in sliders for r in sls): rngs = trav.ranges() - key = frozenset([(k.name, v) for k, v in six.iteritems(rngs)]) + key = frozenset([(k.name, v) for k, v in rngs.items()]) onlys[key] = trav toAdd = [] diff --git a/src/python/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py index e97433bf..369038bd 100644 --- a/src/python/simplexui/traversalDialog.py +++ b/src/python/simplexui/traversalDialog.py @@ -19,12 +19,8 @@ # Ignore a bunch of linter warnings that show up because of my choice of abstraction # pylint: disable=unused-argument,too-many-public-methods,relative-import # pylint: disable=too-many-statements,no-self-use,missing-docstring -from __future__ import absolute_import - import re -import six - from .interface import DCC from .interfaceModel import ( TraversalFilterModel, @@ -251,9 +247,7 @@ def shapeConnectFromSelection(self): pairDict[pp.shape.name] = pp # get all common names - selKeys = set(six.iterkeys(selDict)) - pairKeys = set(six.iterkeys(pairDict)) - common = selKeys & pairKeys + common = selDict.keys() & pairDict.keys() # get those items pairs = [pairDict[i] for i in common] diff --git a/src/python/simplexui/utils.py b/src/python/simplexui/utils.py index e49b3044..0eaa98b9 100755 --- a/src/python/simplexui/utils.py +++ b/src/python/simplexui/utils.py @@ -16,8 +16,6 @@ # along with Simplex. If not, see . """Utility functions.""" -from __future__ import absolute_import - import os import re import sys From 261264437f0fd462c3a11e6bf0c221bacbd2aaec Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 13 Jun 2025 19:11:30 -0700 Subject: [PATCH 07/25] A bunch of python cleanup... I don't remember if its good. I had to stop halfway through and forgot --- pyproject.toml | 1 - quick_compile.bat | 11 +- src/python/simplexui/commands/buildIceXML.py | 443 --- .../simplexui/commands/correctiveInterface.py | 10 +- src/python/simplexui/commands/poseblendlib.py | 166 + .../simplexui/commands/setDelta.xsicompound | 60 - .../commands/xsiCorrectiveInterface.py | 133 - src/python/simplexui/interface/__init__.py | 2 - .../simplexui/interface/dummyInterface.py | 16 + .../simplexui/interface/mayaInterface.py | 468 +-- .../simplexui/interface/xsiInterface.py | 2940 ----------------- src/python/simplexui/items/simplex.py | 23 + src/python/simplexui/menu/__init__.py | 2 - .../simplexui/menu/xsiPlugins/__init__.py | 16 - src/python/simplexui/meson.build | 31 +- 15 files changed, 472 insertions(+), 3850 deletions(-) delete mode 100644 src/python/simplexui/commands/buildIceXML.py create mode 100644 src/python/simplexui/commands/poseblendlib.py delete mode 100644 src/python/simplexui/commands/setDelta.xsicompound delete mode 100644 src/python/simplexui/commands/xsiCorrectiveInterface.py delete mode 100644 src/python/simplexui/interface/xsiInterface.py delete mode 100644 src/python/simplexui/menu/xsiPlugins/__init__.py diff --git a/pyproject.toml b/pyproject.toml index 12cebece..91a4e2d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,6 @@ classifiers = [ ] dependencies = [ - "six", "Qt.py", ] diff --git a/quick_compile.bat b/quick_compile.bat index f6c0adab..46ea3eac 100644 --- a/quick_compile.bat +++ b/quick_compile.bat @@ -10,15 +10,18 @@ SET BUILDTYPE=release SET BUILDDIR=mayabuild_%BUILDTYPE%_%MAYA_VERSION%_%BACKEND% if not exist %BUILDDIR%\ ( - REM meson setup %BUILDDIR% -Dmaya:maya_version=%MAYA_VERSION% --buildtype %BUILDTYPE% --vsenv --backend %BACKEND% - meson setup %BUILDDIR% -Dmaya_build=false -Dpython_build=true -Dmaya:maya_version=%MAYA_VERSION% --buildtype %BUILDTYPE% --vsenv --backend %BACKEND% + meson setup %BUILDDIR% ^ + -Dmaya:maya_version=%MAYA_VERSION% ^ + -Dmaya_build=true ^ + -Dpython_build=false ^ + --buildtype %BUILDTYPE% --vsenv --backend %BACKEND% ) if exist %BUILDDIR%\ ( - REM meson compile -C %BUILDDIR% - REM meson install --skip-subprojects -C %BUILDDIR% + meson compile -C %BUILDDIR% -j 8 + meson install --skip-subprojects -C %BUILDDIR% ) pause diff --git a/src/python/simplexui/commands/buildIceXML.py b/src/python/simplexui/commands/buildIceXML.py deleted file mode 100644 index 9b0f6e51..00000000 --- a/src/python/simplexui/commands/buildIceXML.py +++ /dev/null @@ -1,443 +0,0 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -BASEXML = """ - -\t -\t\t -{0} -\t\t -\t\t -\t\t\t -\t\t\t -\t\t -\t\t -{1} -\t\t -\t\t -\t\t\t -\t\t\t -\t\t -\t -""" - - -SLIDERBASEXML = """ - -\t -\t\t -{0} -\t\t -\t\t -\t\t\t -\t\t -\t\t -{1} -\t\t -\t\t -\t\t\t -\t\t -\t -""" - -GETDATANODE = """ -\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t\t0 -\t\t\t\t\t0 -\t\t\t\t -\t\t\t -""" -MULNODE = """ -\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t\t0 -\t\t\t\t\t0 -\t\t\t\t -\t\t\t -""" - -BUILDARRAYNODE = """ -\t\t\t -{0} -\t\t\t -""" - -ADDNODE = """ -\t\t\t -{0} -\t\t\t -""" - -SELECTINARRAYNODE = """ -\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t\t0 -\t\t\t\t\t0 -\t\t\t\t -\t\t\t -""" - -PASSTHROUGHNODE = """ -\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t\t0 -\t\t\t\t\t0 -\t\t\t\t -\t\t\t -""" - -ADDPORT = '\t\t\t\t' -SCALARPORT = '\t\t\t\t' -CONNECTION = ( - '\t\t\t ' -) -OUTPROPERTY = "self.{0}_outProperty.{1}" -POSPROPERTY = "self.cls.{0}.{1}.positions" -INPROPERTY = "self.{0}_inProperty.{1}" - - -LOADER = """ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -""" - - -BASEDEBUGXML = """ - -\t -\t\t -{0} -\t\t -\t\t -\t\t\t -\t\t\t -\t\t -\t\t -{1} -\t\t -\t\t -\t\t\t -\t\t\t -\t\t -\t -""" - -EXECUTENODE = """ -\t\t\t -{1} -\t\t\t\t -\t\t\t\t\t0 -\t\t\t\t\t0 -\t\t\t\t -\t\t\t -""" -EXECPORT = '\t\t\t\t' - -SETDATANODE = """ -\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t -\t\t\t\t\t0 -\t\t\t\t\t0 -\t\t\t\t -\t\t\t -""" - - -def buildIceDebugXML(shapeList, systemName, namePrefix): - """ - - Parameters - ---------- - shapeList : - - systemName : - - namePrefix : - - - Returns - ------- - - """ - execNodeIdx = 0 - passIdx = 1 - passNode = PASSTHROUGHNODE.format(passIdx) - index = 2 - - execPortIdx = 1 - execPorts = [] - nodes = [] - connections = [] - nodes.append(passNode) - - for i, shape in enumerate(shapeList): - sliIdx = index - setIdx = index + 1 - index += 2 - - outPropName = OUTPROPERTY.format(systemName, namePrefix + shape) - sliNode = SELECTINARRAYNODE.format(sliIdx, i) - setNode = SETDATANODE.format(setIdx, outPropName) - - nodes.append(sliNode) - nodes.append(setNode) - - setCnx = CONNECTION.format(sliIdx, "value", setIdx, "source") - execCnx = CONNECTION.format( - setIdx, "value", execNodeIdx, "port" + str(execPortIdx) - ) - passCnx = CONNECTION.format(passIdx, "out", sliIdx, "array") - - connections.append(setCnx) - connections.append(execCnx) - connections.append(passCnx) - - execPort = EXECPORT.format(execPortIdx) - execPorts.append(execPort) - execPortIdx += 1 - - allExecPorts = "\n".join(execPorts) - execNode = EXECUTENODE.format(execNodeIdx, allExecPorts) - nodes.insert(execNodeIdx, execNode) - allNodes = "\n".join(nodes) - allConnections = "\n".join(connections) - output = BASEDEBUGXML.format(allNodes, allConnections) - return output - - -def buildIceXML(shapeList, systemName, clusterName, namePrefix): - """ - - Parameters - ---------- - shapeList : - - systemName : - - clusterName : - - namePrefix : - - - Returns - ------- - - """ - index = 1 - addIdx = 1 - - addPorts = [] - nodes = [] - connections = [] - - passNode = PASSTHROUGHNODE.format(index) - nodes.append(passNode) - - index += 1 - - selfPos = GETDATANODE.format(index, "Self.PointPosition") - selfCnx = CONNECTION.format(index, "value", 0, "value" + str(addIdx)) - addPort = ADDPORT.format(addIdx, addIdx - 1) - - nodes.append(selfPos) - connections.append(selfCnx) - addPorts.append(addPort) - - addIdx += 1 - index += 1 - - for i, shape in enumerate(shapeList): - sliIdx = index - posIdx = index + 1 - mulIdx = index + 2 - index += 3 - - posPropName = POSPROPERTY.format(clusterName, namePrefix + shape) - - sliNode = SELECTINARRAYNODE.format(sliIdx, i) - posNode = GETDATANODE.format(posIdx, posPropName) - mulNode = MULNODE.format(mulIdx) - - nodes.append(sliNode) - nodes.append(posNode) - nodes.append(mulNode) - - sliCnx = CONNECTION.format(sliIdx, "value", mulIdx, "factor") - connections.append(sliCnx) - - posCnx = CONNECTION.format(posIdx, "value", mulIdx, "value") - connections.append(posCnx) - - passCnx = CONNECTION.format(1, "out", sliIdx, "array") - connections.append(passCnx) - - if i != 0: - # Don't connect the rest shape - addCnx = CONNECTION.format(mulIdx, "result", 0, "value" + str(addIdx)) - connections.append(addCnx) - addPort = ADDPORT.format(addIdx, addIdx - 1) - addPorts.append(addPort) - addIdx += 1 - - allPorts = "\n".join(addPorts) - addNode = ADDNODE.format(allPorts) - allNodes = "\n".join(nodes) - allNodes = addNode + "\n" + allNodes - allConnections = "\n".join(connections) - output = BASEXML.format(allNodes, allConnections) - - return output - - -def buildSliderIceXML(sliderList, systemName): - """ - - Parameters - ---------- - sliderList : - - systemName : - - - Returns - ------- - - """ - index = 1 - addIdx = 1 - - sliderPorts = [] - nodes = [] - connections = [] - - for slider in sliderList: - inIdx = index - - inPropName = INPROPERTY.format(systemName, slider) - - inNode = GETDATANODE.format(inIdx, inPropName) - - nodes.append(inNode) - - sliderCnx = CONNECTION.format(inIdx, "value", 0, "value" + str(addIdx)) - connections.append(sliderCnx) - - sliderPort = SCALARPORT.format(addIdx, addIdx - 1) - sliderPorts.append(sliderPort) - - index += 1 - addIdx += 1 - - allPorts = "\n".join(sliderPorts) - arrayNode = BUILDARRAYNODE.format(allPorts) - allNodes = "\n".join(nodes) - allNodes = arrayNode + "\n" + allNodes - allConnections = "\n".join(connections) - output = SLIDERBASEXML.format(allNodes, allConnections) - - return output - - -def buildLoaderXML(loader, rester): - """ - - Parameters - ---------- - loader : - - rester : - - - Returns - ------- - - """ - return LOADER.format(loader.name, rester.name) diff --git a/src/python/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py index b935d96b..78f4406f 100644 --- a/src/python/simplexui/commands/correctiveInterface.py +++ b/src/python/simplexui/commands/correctiveInterface.py @@ -22,14 +22,8 @@ except ImportError: pass -try: - from .mayaCorrectiveInterface import getShiftValues, resetPose, setPose - - dcc = "maya" -except ImportError: - from .xsiCorrectiveInterface import getShiftValues, resetPose, setPose - - dcc = "xsi" +from .mayaCorrectiveInterface import getShiftValues, resetPose, setPose +dcc = "maya" def getRefForPoses(mesh, poses, multipliers): diff --git a/src/python/simplexui/commands/poseblendlib.py b/src/python/simplexui/commands/poseblendlib.py new file mode 100644 index 00000000..fc7fe5da --- /dev/null +++ b/src/python/simplexui/commands/poseblendlib.py @@ -0,0 +1,166 @@ +import numpy as np + +# I could have used the Scipy rotations library, but it doeosn't deal +# with ndim arrays of quaternions. + + +def positve_scalar(q: np.ndarray) -> np.ndarray: + """Ensure the scalar value of an array of quaternions is positive""" + shape = q.shape + q = q.reshape((-1, 4)) + q[q[:, 3] < 0] *= -1 + return q.reshape(shape) + + +def outer(q: np.ndarray) -> np.ndarray: + """Do the outer product of an array of quaternions with itself""" + return q[..., None] * q[..., None, :] # broadcast to do the outer products + + +def inverse_quats(q: np.ndarray) -> np.ndarray: + """Get the inverses of the given unit quaternions""" + ret = q.copy() + ret[..., :-1] *= -1 + return ret + + +def norm_quats(q: np.ndarray) -> np.ndarray: + """Normalize the given quaternions and flip any whose scalar component + is negative + """ + return positive_scalar(q / np.linalg.norm(q, axis=-1, keepdims=True)) + + +def mul_quats(q1, q2): + """Quaternion multiplication of two arrays + + Follows the convention that the right hand is applied "first" + Like row-major transformation matrices + """ + x1, y1, z1, w1 = q1[..., 0], q1[..., 1], q1[..., 2], q1[..., 3] + x2, y2, z2, w2 = q2[..., 0], q2[..., 1], q2[..., 2], q2[..., 3] + + w = w2 * w1 - x2 * x1 - y2 * y1 - z2 * z1 + x = w2 * x1 + x2 * w1 + y2 * z1 - z2 * y1 + y = w2 * y1 - x2 * z1 + y2 * w1 + z2 * x1 + z = w2 * z1 + x2 * y1 - y2 * x1 + z2 * w1 + + return np.stack((x, y, z, w), axis=-1) + + +def quaternion_pow(q: np.ndarray, n: np.ndarray): + """Exponentiate unit quaternions + Could also be thought of as slerping from the unit quaternion to the + given quaternions + + Args: + q (np.array[..., 4]): An array of quaternions + n (np.array[..., 0]): An array of weights broadcastable to the shape + of the quaternion array + + Returns: + np.array[..., 4]: An array of exponentiated quaternions + """ + w = np.clip(q[..., -1], -1.0, 1.0) + v = q[..., :-1] + + theta = np.arccos(w) + sin_theta = np.sin(theta) + + # Broadcast n to shape of theta + n = np.broadcast_to(n, theta.shape) + + # Scale the rotation angle + new_theta = n * theta + new_w = np.cos(new_theta) + + # Fall back to a linear approximation for small angles + with np.errstate(divide='ignore', invalid='ignore'): + scale = np.where(sin_theta > 1e-8, np.sin(new_theta) / sin_theta, n) + + new_v = v * scale[..., np.newaxis] + + result = np.concatenate([new_v, new_w[..., np.newaxis]], axis=-1) + return result + + +def sum_weighted_quat_poses( + restPose: np.ndarray, # (N, 4) float + targets: np.ndarray, # (T, N, 4) float + weights: np.ndarray, # T float + levels: np.ndarray, # T int +) -> np.ndarray: # (N, 4) float + """Perform a "weighted sum" of target poses of quaternions with multiple + levels. Each level is calculated individually, then multiplied in order + to get the final output. + + This assumes Scalar (W) Last formatted quaternions + + This uses some interesting math that I don't fully understand. But the idea + came from a NASA paper on the topic. + + Apparently if you sum a bunch of the outer products of quaternions with + themselves, and then take the largest eigenvector of the resulting 4x4 + matrix, that eigenvector is (in some sense) the average of those quaternions. + + The naiive idea of an average is the sum divided by the count. So if you + multiply an average value by the count, you should get back the sum. + + So if I take this "average" of quaternions and do a "multiplication" by the + count, I should get back something that looks like a "sum" of the quaternions + + And to do "multiplication by the count", I just extrapolate the quaternion + by slerping it to that count. + + This may not be "correct", but it sure seems to behave like I want it to. + And that's all that matters for this application. + + Args: + restPose (np.array[N, 4]): An array of quaternions + targets (np.array[T, N, 4]): An array of target poses of quaternions + weights (np.array[T]): The weight of each target + levels (np.array[T]): The level of each target + + Returns: + np.array[N, 4]: An array of quaternions + """ + targets = norm_quats(targets) + restPose = norm_quats(restPose) + + # Get the existing levels, and how many poses are at each level + exlev, counts = np.unique(levels, return_counts=True) + s_exlev = np.argsort(exlev) + exlev = exlev[s_exlev] + counts = counts[s_exlev] + + # Get the targets in the space of the rest pose + restInv = inverse_quats(restPose) + relTargets = mul_quats(targets, restInv[None]) + + # Apply the weights to all the targets + slerped = quaternion_pow(relTargets, weights[..., None]) + + # Get the outer product of all the quaternions + # and sum them by level + outs = outer(slerped) + levelEigs = np.zeros((exlev[-1] + 1, len(restPose), 4, 4)) + np.add.at(levelEigs, levels, outs) + levelEigs = levelEigs[exlev] + + # Get largest eigenvalue for each matrix + # This represents the *average* of each level + evals, evecs = np.linalg.eig(levelEigs) + mvals = np.argmax(np.abs(evals), axis=-1)[..., None, None] + averages = np.take_along_axis(evecs, mvals, axis=evecs.ndim - 1).squeeze(axis=-1) + averages = norm_quats(averages) + + # Take each level's pose to the power of its count + sums = quaternion_pow(averages, counts[:, None]) + sums = norm_quats(sums) + + # Finally multiply the poses together on top of the rest pose + qret = restPose.copy() + for x in sums: + qret = mul_quats(x, qret) + + return qret diff --git a/src/python/simplexui/commands/setDelta.xsicompound b/src/python/simplexui/commands/setDelta.xsicompound deleted file mode 100644 index 04469390..00000000 --- a/src/python/simplexui/commands/setDelta.xsicompound +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/python/simplexui/commands/xsiCorrectiveInterface.py b/src/python/simplexui/commands/xsiCorrectiveInterface.py deleted file mode 100644 index b33148fb..00000000 --- a/src/python/simplexui/commands/xsiCorrectiveInterface.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -""" Get the corrective deltas from a rig in XSI """ - -from dcc.xsi import constants, xsi -from dcc.xsi.ice import ICETree # pylint:disable=import-error - - -def setPose(pvp, multiplier): - """Set a percentage of a pose - - Parameters - ---------- - pvp : [(str, float), ...] - A list of property/value pairs - multiplier : float - The percentage multiplier of the pose - """ - for prop, val in pvp: - xsi.setValue(prop, val * multiplier) - - -def resetPose(pvp): - """Reset everything back to rest - - Parameters - ---------- - pvp : [(str, float), ...] - A list of property/value pairs - """ - for prop, _val in pvp: - xsi.setValue(prop, 0) - - -def getMeshVerts(mesh): - """Get the verts of the given mesh object - - Parameters - ---------- - mesh : XSIObject - A native xsi mesh object - - Returns - ------- - : [vert, ...] - A list of vertices - """ - vts = mesh.ActivePrimitive.Geometry.Points.PositionArray - return list(zip(*vts)) - - -def buildTree(mesh): - """Build the ICE tree that allows for this procedure - - Parameters - ---------- - mesh : XSIObject - A native xsi mesh object - - Returns - ------- - : XSIObject - The ICE Tree object - : XSIObject - The vector node in the ICE Tree - """ - iceTree = ICETree(None, mesh, "Test", constants.siConstructionModePrimaryShape) - - getter = iceTree.addGetDataNode("Self.PointPosition") - adder = iceTree.addNode("Add") - vector = iceTree.addNode("ScalarTo3DVector") - setter = iceTree.addSetDataNode("Self.PointPosition") - - getter.value.connect(adder.value1) - vector.vector.connect(adder.value2) - adder.result.connect(setter.Value) - iceTree.connect(setter.Execute, 2) - - return iceTree, vector - - -def getShiftValues(mesh): - """Shift the vertices along each axis *before* the skinning - op in the deformer history - - Parameters - ---------- - mesh : XSIObject - A native xsi mesh object - - Returns - ------- - : [vert, ...] - A list of un-shifted vertices - : [vert, ...] - A list of vertices pre-shifted by 1 along the X axis - : [vert, ...] - A list of vertices pre-shifted by 1 along the Y axis - : [vert, ...] - A list of vertices pre-shifted by 1 along the Z axis - """ - tree, vector = buildTree(mesh) - zero = getMeshVerts(mesh) - - vector.x.value = 1.0 - oneX = getMeshVerts(mesh) - vector.x.value = 0.0 - - vector.y.value = 1.0 - oneY = getMeshVerts(mesh) - vector.y.value = 0.0 - - vector.z.value = 1.0 - oneZ = getMeshVerts(mesh) - vector.z.value = 0.0 - - tree.delete() - return zero, oneX, oneY, oneZ diff --git a/src/python/simplexui/interface/__init__.py b/src/python/simplexui/interface/__init__.py index a7efccf7..ba056844 100644 --- a/src/python/simplexui/interface/__init__.py +++ b/src/python/simplexui/interface/__init__.py @@ -22,7 +22,5 @@ CONTEXT = os.path.basename(sys.executable) if CONTEXT in ("maya.exe", "mayabatch.exe", "maya.bin"): from .mayaInterface import DCC, DISPATCH, rootWindow, undoContext -elif CONTEXT in ("XSI.exe", "xsi.bin"): - from .xsiInterface import DCC, DISPATCH, rootWindow, undoContext else: from .dummyInterface import DCC, DISPATCH, rootWindow, undoContext diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index 3b36ff04..11694a42 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -437,6 +437,22 @@ def vertCount(mesh): """ return len(mesh.verts) + @undoable + def loadAbcPoses(self, abcMesh, js, pBar=None): + """Load the joints/skin from an alembic file onto an already-created system + + Parameters + ---------- + abcMesh : IPolyMesh + The Alembic mesh to load shapes from + js : dict + The simplex definition dictionary + pBar : QProgressDialog, optional + An optional progress dialog (Default value = None) + + """ + pass + @undoable def loadAbc(self, abcMesh, js, pBar=None): """Load the shapes from an alembic file onto an already-created system diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 91d3e092..c5c10694 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -19,7 +19,6 @@ import json import re from contextlib import contextmanager -from ctypes import c_double, c_float from functools import wraps import maya.cmds as cmds @@ -28,6 +27,7 @@ from imath import IntArray, UnsignedIntArray, V2fArray, V3fArray from ..commands.alembicCommon import mkSampleVertexPoints +from ..items.simplex import Simplex from Qt import QtCore from Qt.QtCore import Signal from Qt.QtWidgets import ( @@ -40,14 +40,12 @@ try: import numpy as np + from ..commands.numpytoimath import numpyToImath + from ..commands.mayatonumpy import mayaToNumpy except ImportError: np = None - -try: - import imathnumpy -except ImportError: - imathnumpy = None - + numpyToImath = None + mayaToNumpy = None # UNDO STACK INTEGRATION @contextmanager @@ -113,9 +111,8 @@ def stacker(*args, **kwargs): return stacker -# temporarily disconnect inputs from a list of nodes and plugs def doDisconnect(targets, testCnxType=("double", "float")): - """ + """Temporarily disconnect inputs from a list of nodes and plugs Parameters ---------- @@ -203,15 +200,17 @@ class DCC(object): def __init__(self, simplex, stack=None): if not cmds.pluginInfo("simplex_maya", query=True, loaded=True): cmds.loadPlugin("simplex_maya") - self.undoDepth = 0 - self.name = None # the name of the system - self.mesh = None # the mesh object with the system - self.ctrl = None # the object that has all the controllers on it - self.shapeNode = None # the deformer object - self.op = None # the simplex object - self.simplex = simplex # the abstract representation of the setup - self._live = True - self.sliderMul = self.simplex.sliderMul + self.undoDepth: int = 0 + self.name: str = "" # the name of the system + self.mesh: str = "" # the mesh object with the system + self.ctrl: str = "" # the object that has all the controllers on it + self.shapeNode: str = "" # the deformer object + self.hasPoseNode = False + self.poseNode: str = "" # the pose deformer object + self.op: str = "" # the simplex object + self.simplex: Simplex = simplex # the abstract representation of the setup + self._live: bool = True + self.sliderMul: float = self.simplex.sliderMul # def __deepcopy__(self, memo): # ''' @@ -324,14 +323,8 @@ def preLoad(self, simp, simpDict, create=True, pBar=None): if not create: if pBar is not None: - msg = "\n".join( - [ - "Some shapes are Missing:", - ", ".join(toMake), - "", - "Create them?", - ] - ) + msg = "Some shapes are Missing:\n{}\n\nCreate them?" + msg = msg.format(", ".join(toMake)) btns = QMessageBox.Yes | QMessageBox.Cancel bret = QMessageBox.question(pBar, "Missing Shapes", msg, btns) if not bret & QMessageBox.Yes: @@ -403,52 +396,31 @@ def checkForErrors(self, window): "The UI will still mostly work, but extracting/connecting shapes" "may fail in unexpected ways.", ) - QMessageBox.warning(window, "Multiple Shape Nodes", '\n'.join(msg)) - - # System IO - @undoable - def loadNodes(self, simp, thing, create=True, pBar=None): - """Create a new system based on the simplex tree - Build any DCC objects that are missing if create=True - Raises a runtime error if missing objects are found and - create=False + QMessageBox.warning(window, "Multiple Shape Nodes", "\n".join(msg)) - Parameters - ---------- - simp : - - thing : - - create : - (Default value = True) - pBar : - (Default value = None) - - Returns - ------- - - """ - self.name = simp.name - self.mesh = thing - - # Find all blendshapes in the history - rawShapeNodes = [ - h for h in cmds.listHistory(thing) if cmds.nodeType(h) == "blendShape" - ] + def _getOpNodes(self, thing: str): + hist: list[str] = cmds.listHistory(thing) + rawShapeNodes = cmds.ls(hist, type='blendShape') + rawPoseNodes = cmds.ls(hist, type='blendPose') # Find any simplex ops connected to the history # that have the given name - ops = [] - for sn in rawShapeNodes: - op = cmds.listConnections( - "{0}.{1}".format(sn, "message"), - source=False, - destination=True, - type="simplex_maya", + ops: list[str] = [] + rawOps: list[str] = [] + for sn in rawShapeNodes + rawPoseNodes: + op = ( + cmds.listConnections( + "{0}.{1}".format(sn, "message"), + source=False, + destination=True, + type="simplex_maya", + ) + or [] ) - if not op: - continue - op = op[0] + rawOps.extend(op) + rawOps = list(set(rawOps)) + + for op in rawOps: js = cmds.getAttr(op + ".definition") or "" sn = json.loads(js).get("systemName") if sn == self.name: @@ -458,20 +430,41 @@ def loadNodes(self, simp, thing, create=True, pBar=None): raise RuntimeError( "Found too many Simplex systems with the same name on the same object" ) + return ops - # Back-select the shape nodes connected to those ops + def _getShapeNodes(self, ops): shapeNodes = [] for op in ops: - sn = cmds.listConnections( - "{0}.{1}".format(op, "shapeMsg"), - source=True, - destination=False, - type="blendShape", - ) + try: + sn = cmds.listConnections( + "{0}.{1}".format(op, "shapeMsg"), + source=True, + destination=False, + type="blendShape", + ) + except ValueError: + continue if sn: shapeNodes.append(sn[0]) + return shapeNodes - # Find the msg connected control object + def _getPoseNodes(self, ops): + poseNodes = [] + for op in ops: + try: + sn = cmds.listConnections( + "{0}.{1}".format(op, "poseMsg"), + source=True, + destination=False, + type="blendPose", + ) + except ValueError: + continue + if sn: + poseNodes.append(sn[0]) + return poseNodes + + def _getCtrlNodes(self, ops): ctrlCnx = [] for op in ops: ccnx = cmds.listConnections( @@ -481,76 +474,115 @@ def loadNodes(self, simp, thing, create=True, pBar=None): ) if ccnx: ctrlCnx.append(ccnx[0]) + return ctrlCnx - if not shapeNodes: - if not create: - raise RuntimeError( - "Blendshape operator not found with creation turned off" - ) - # Unlock the normals on the rest head because blendshapes don't work with locked normals - # and you can't really do this after the blendshape has been created - intermediates = [ - shp - for shp in cmds.listRelatives(self.mesh, shapes=True, path=True) - if cmds.getAttr(shp + ".intermediateObject") - ] - meshToFreeze = self.mesh if not intermediates else intermediates[0] - isIntermediate = cmds.getAttr(meshToFreeze + ".intermediateObject") - cmds.polyNormalPerVertex(meshToFreeze, ufn=True) - cmds.polySoftEdge(meshToFreeze, a=180, ch=1) - cmds.setAttr(meshToFreeze + ".intermediateObject", 0) - cmds.delete(meshToFreeze, constructionHistory=True) - cmds.setAttr(meshToFreeze + ".intermediateObject", isIntermediate) - - self.shapeNode = cmds.blendShape( - self.mesh, name="{0}_BS".format(self.name), frontOfChain=True - )[0] - else: - self.shapeNode = shapeNodes[0] + def _createShapeNode(self, name, mesh) -> str: + intermediates = [ + shp + for shp in cmds.listRelatives(mesh, shapes=True, path=True) + if cmds.getAttr(shp + ".intermediateObject") + ] + meshToFreeze = mesh if not intermediates else intermediates[0] + isIntermediate = cmds.getAttr(meshToFreeze + ".intermediateObject") + + # Unlock the normals on the rest head because blendshapes don't work with locked normals + # and you can't really do this after the blendshape has been created + cmds.polyNormalPerVertex(meshToFreeze, unFreezeNormal=True) + cmds.polySoftEdge(meshToFreeze, angle=180, constructionHistory=True) + cmds.setAttr(meshToFreeze + ".intermediateObject", 0) + cmds.delete(meshToFreeze, constructionHistory=True) + cmds.setAttr(meshToFreeze + ".intermediateObject", isIntermediate) + + name = "{}_BS".format(name) + return cmds.blendShape(mesh, name=name, frontOfChain=True)[0] + + def _createPoseNode(self, name) -> str: + name = "{}_BP".format(name) + return cmds.createNode('blendPose', name=name) + + def _createSimplexNode(self, name): + op = cmds.createNode("simplex_maya", name=name) + cmds.addAttr(op, longName="revision", attributeType="long") + cmds.addAttr(op, longName="shapeMsg", attributeType="message") + cmds.addAttr(op, longName="poseMsg", attributeType="message") + cmds.addAttr(op, longName="ctrlMsg", attributeType="message") + return op + + def _createControlNode(self, name, op): + tfmAttrs = [".tx", ".ty", ".tz", ".rx", ".ry", ".rz", ".sx", ".sy", ".sz", ".v"] + ctrl = cmds.group(empty=True, name="{0}_CTRL".format(name)) + for attr in tfmAttrs: + cmds.setAttr(ctrl + attr, keyable=False, channelBox=False) + cmds.addAttr(ctrl, longName="solver", attributeType="message") + cmds.connectAttr( + "{0}.{1}".format(ctrl, "solver"), + "{0}.{1}".format(op, "ctrlMsg"), + ) + return ctrl - # find/build the operator - # GODDAMMIT, why does maya return None instead of an empty list????? - if not ops: - if not create: - raise RuntimeError( - "Simplex operator not found with creation turned off" + # System IO + @undoable + def loadNodes(self, simp, thing, create=True, pBar=None): + """Create a new system based on the simplex tree + Build any DCC objects that are missing if create=True + Raises a runtime error if missing objects are found and + create=False + + Parameters + ---------- + simp : + + thing : + + create : + (Default value = True) + pBar : + (Default value = None) + + Returns + ------- + + """ + self.name = simp.name + self.mesh = thing + + ops = self._getOpNodes(thing) + sns = self._getShapeNodes(ops) + pns = self._getPoseNodes(ops) + cc = self._getCtrlNodes(ops) + + if not create and (not sns or not pns or not ops or not cc): + types = [] + if not sns: + types.append("blendShape") + if not pns and self.hasPoseNode: + types.append("blendPose") + if not ops: + types.append("simplex_maya") + if not cc: + types.append("CTRL") + + raise RuntimeError( + "Creation turned off and some objects are missing: {}".format( + ', '.join(types) ) - self.op = cmds.createNode("simplex_maya", name=self.name) - cmds.addAttr(self.op, longName="revision", attributeType="long") - cmds.addAttr(self.op, longName="shapeMsg", attributeType="message") - cmds.addAttr(self.op, longName="ctrlMsg", attributeType="message") - cmds.connectAttr( - "{0}.{1}".format(self.shapeNode, "message"), - "{0}.{1}".format(self.op, "shapeMsg"), ) - else: - self.op = ops[0] - # find/build the ctrl object - if not ctrlCnx: - if not create: - raise RuntimeError("Control object not found with creation turned off") - self.ctrl = cmds.group(empty=True, name="{0}_CTRL".format(self.name)) - for attr in [ - ".tx", - ".ty", - ".tz", - ".rx", - ".ry", - ".rz", - ".sx", - ".sy", - ".sz", - ".v", - ]: - cmds.setAttr(self.ctrl + attr, keyable=False, channelBox=False) - cmds.addAttr(self.ctrl, longName="solver", attributeType="message") + self.op = ops[0] if ops else self._createSimplexNode(self.name) + self.ctrl = cc[0] if cc else self._createControlNode(self.name, self.op) + + self.shapeNode = sns[0] if sns else self._createShapeNode(self.name, self.mesh) + cmds.connectAttr( + "{0}.{1}".format(self.shapeNode, "message"), + "{0}.{1}".format(self.op, "shapeMsg"), + ) + + if self.hasPoseNode: + self.poseNode = pns[0] if pns else self._createPoseNode(self.name) cmds.connectAttr( - "{0}.{1}".format(self.ctrl, "solver"), - "{0}.{1}".format(self.op, "ctrlMsg"), + "{0}.{1}".format(self.poseNode, "message"), + "{0}.{1}".format(self.op, "poseMsg"), ) - else: - self.ctrl = ctrlCnx[0] def getShapeThing(self, shapeName): """ @@ -586,9 +618,16 @@ def getSliderThing(self, sliderName): return None return things[0] - @staticmethod + @classmethod + def buildDummyMesh(cls, name: str): + importHeadShape = cmds.createNode('mesh', name=name + "Shape") + badPar = cmds.listRelatives(importHeadShape, parent=True)[0] + importHead = cmds.rename(badPar, name) + return importHead, importHeadShape + + @classmethod @undoable - def buildRestAbc(abcMesh, name): + def buildRestAbc(cls, abcMesh, name): """ Parameters @@ -614,22 +653,31 @@ def buildRestAbc(abcMesh, name): cmds.setAttr(abcNode + ".speed", 24) # Is this needed anymore? cmds.setAttr(abcNode + ".time", 0) - importHead = cmds.polySphere( - name="{0}_SIMPLEX".format(name), constructionHistory=False - )[0] - importHeadShape = [i for i in cmds.listRelatives(importHead, shapes=True)][0] + importHead, importHeadShape = cls.buildDummyMesh("{0}_SIMPLEX".format(name)) + + importHead = "{0}_SIMPLEX".format(name) + importHeadShape = cmds.createNode('mesh', name=importHead + "Shape") + badPar = cmds.listRelatives(importHeadShape, parent=True)[0] + importHead = cmds.rename(badPar, importHead) cmds.connectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") cmds.polyEvaluate(importHead, vertex=True) # Force a refresh cmds.disconnectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") - cmds.sets(importHead, e=True, forceElement="initialShadingGroup") + cmds.sets(importHead, edit=True, forceElement="initialShadingGroup") cmds.delete(abcNode) return importHead - @staticmethod - def vertCount(mesh): + @classmethod + def vertCount(cls, mesh): return cmds.polyEvaluate(mesh, vertex=True) + + @undoable + def loadAbcPoses(self, abcMesh, js, pBar=None): + pass + + + @undoable def loadAbc(self, abcMesh, js, pBar=None): """ @@ -687,8 +735,7 @@ def loadAbc(self, abcMesh, js, pBar=None): if js["encodingVersion"] > 1: shapes = [i["name"] for i in shapes] - importHead = cmds.polySphere(name="importHead", constructionHistory=False)[0] - importHeadShape = [i for i in cmds.listRelatives(importHead, shapes=True)][0] + importHead, importHeadShape = self.buildDummyMesh("importHead") cmds.connectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") cmds.polyEvaluate(importHead, vertex=True) # Force a refresh @@ -698,7 +745,7 @@ def loadAbc(self, abcMesh, js, pBar=None): cmds.delete(importRest, constructionHistory=True) self._removeExtraShapeNodes(importRest) - importBS = cmds.blendShape(importRest, importHead)[0] + importBS: str = cmds.blendShape(importRest, importHead)[0] cmds.blendShape(importBS, edit=True, weight=[(0, 1.0)]) # Maybe get shapeNode from self.mesh?? importOrig = [ @@ -775,12 +822,8 @@ def getAllShapeVertices(self, shapes, pBar=None): cmds.setAttr(shape.thing, 1.0) - if np is not None: - rawPts = meshFn.getRawPoints() - cta = (c_float * ptCount * 3).from_address(int(rawPts)) - out = np.ctypeslib.as_array(cta) - out = np.copy(out) - out = out.reshape((-1, 3)) + if np is not None and mayaToNumpy is not None: + out = mayaToNumpy(meshFn.getRawPoints()) else: flatverts = cmds.xform( "{0}.vtx[*]".format(self.mesh), @@ -857,8 +900,8 @@ def pushShapeVertices(self, shape): # Push the vertices for a specific shape back to the DCC pass - @staticmethod - def getMeshTopology(mesh, uvName=None): + @classmethod + def getMeshTopology(cls, mesh, uvName=None): """Get the topology of a mesh Parameters @@ -952,41 +995,14 @@ def getNumpyShape(cls, mesh, world=False): The point positions of the mesh """ - # This method uses some neat tricks to get data out of an MPointArray - # Basically, you get an MScriptUtil pointer to the MPointArray data - # Then you get a ctypes pointer to the MScriptUtil pointer - # Then you get a numpy pointer to the ctypes pointer - # Because these are all pointers, you're looking at the exact same - # data in memory ... So just copy the data out to a numpy array - # See: https://gist.github.com/tbttfox/9ca775bf629c7a1285c27c8d9d961bca - - # Get the MPointArray of the mesh + if np is None or mayaToNumpy is None: + raise RuntimeError("Can't do numpy stuff if its not importable") vts = cls._getMeshVertices(mesh, world=world) + ret = mayaToNumpy(vts) + return ret[..., :3].copy() - # Get the double4 pointer from the mpointarray - count = vts.length() - cc = count * 4 - util = om.MScriptUtil() - util.createFromList([0.0] * cc, cc) - ptr = util.asDouble4Ptr() - vts.get(ptr) - - # Build a ctypes double4 pointer - cdata = (c_double * 4) * count - - # int(ptr) gives the memory address - cta = cdata.from_address(int(ptr)) - - # This makes numpy look at the same memory as the ctypes array - # so we can both read from and write to that data through numpy - npArray = np.ctypeslib.as_array(cta) - - # Copy the data out of the numpy array, which is looking - # into the cdata array, which is looking into the ptr array - return npArray[..., :3].copy() - - @staticmethod - def _getMeshVertices(mesh, world=False): + @classmethod + def _getMeshVertices(cls, mesh, world=False): """ """ # Get the MDagPath from the name of the mesh sl = om.MSelectionList() @@ -1005,7 +1021,7 @@ def _getMeshVertices(mesh, world=False): @classmethod def _exportAbcVertices(cls, mesh, world=False): """ """ - if np is None or imathnumpy is None: + if np is None or numpyToImath is None: vts = cls._getMeshVertices(mesh, world=world) vertices = V3fArray(vts.length()) for i in range(vts.length()): @@ -1015,8 +1031,8 @@ def _exportAbcVertices(cls, mesh, world=False): vertices = mkSampleVertexPoints(vts) return vertices - @staticmethod - def getAbcFaces(mesh): + @classmethod + def getAbcFaces(cls, mesh): """ """ # Get the MDagPath from the name of the mesh sl = om.MSelectionList() @@ -2511,8 +2527,8 @@ def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): if delete: cmds.delete(mesh) - @staticmethod - def setDisabled(op): + @classmethod + def setDisabled(cls, op): """ Parameters @@ -2534,8 +2550,8 @@ def setDisabled(op): helpers.append((prop, val)) return helpers - @staticmethod - def reEnable(helpers): + @classmethod + def reEnable(cls, helpers): """ Parameters @@ -2568,13 +2584,13 @@ def renameCombo(self, combo, name): pass # Data Access - @staticmethod - def getSimplexOperators(): + @classmethod + def getSimplexOperators(cls): """ """ return cmds.ls(type="simplex_maya") - @staticmethod - def getSimplexOperatorsByName(name): + @classmethod + def getSimplexOperatorsByName(cls, name): """ Parameters @@ -2590,8 +2606,8 @@ def getSimplexOperatorsByName(name): """ return cmds.ls(name, type="simplex_maya") - @staticmethod - def getSimplexOperatorsOnObject(thing): + @classmethod + def getSimplexOperatorsOnObject(cls, thing): """ Parameters @@ -2621,8 +2637,8 @@ def getSimplexOperatorsOnObject(thing): out.append(op) return out - @staticmethod - def getSimplexString(op): + @classmethod + def getSimplexString(cls, op): """ Parameters @@ -2638,8 +2654,8 @@ def getSimplexString(op): """ return cmds.getAttr(op + ".definition") - @staticmethod - def getSimplexStringOnThing(thing, systemName): + @classmethod + def getSimplexStringOnThing(cls, thing, systemName): """ Parameters @@ -2663,8 +2679,8 @@ def getSimplexStringOnThing(thing, systemName): return js return None - @staticmethod - def setSimplexString(op, val): + @classmethod + def setSimplexString(cls, op, val): """ Parameters @@ -2682,8 +2698,8 @@ def setSimplexString(op, val): """ return cmds.setAttr(op + ".definition", val, type="string") - @staticmethod - def selectObject(thing): + @classmethod + def selectObject(cls, thing): """Select an object in the DCC Parameters @@ -2702,8 +2718,8 @@ def selectCtrl(self): if self.ctrl: self.selectObject(self.ctrl) - @staticmethod - def getObjectByName(name): + @classmethod + def getObjectByName(cls, name): """ Parameters @@ -2722,8 +2738,8 @@ def getObjectByName(name): return None return objs[0] - @staticmethod - def getObjectName(thing): + @classmethod + def getObjectName(cls, thing): """ Parameters @@ -2739,13 +2755,13 @@ def getObjectName(thing): """ return thing - @staticmethod - def staticUndoOpen(): + @classmethod + def staticUndoOpen(cls): """ """ cmds.undoInfo(chunkName="SimplexOperation", openChunk=True) - @staticmethod - def staticUndoClose(): + @classmethod + def staticUndoClose(cls): """ """ cmds.undoInfo(closeChunk=True) @@ -2851,8 +2867,8 @@ def loadPersistentSlider(cls, thing): """ return cls.getObjectByName(thing) - @staticmethod - def getSelectedObjects(): + @classmethod + def getSelectedObjects(cls): """ """ # For maya, only return transform nodes return cmds.ls(sl=True, transforms=True) @@ -2878,8 +2894,8 @@ def importObj(self, path): imp = new.pop() return imp - @staticmethod - def _getDeformerChain(chkObj): + @classmethod + def _getDeformerChain(cls, chkObj): # Get a deformer chain memo = [] while chkObj and chkObj not in memo: @@ -3160,8 +3176,8 @@ def buildDeleterScriptJob(): ) -def simplexDelCB(node, dgMod, clientData): - xNode, dName = clientData +def simplexDelCB(_node, dgMod, clientData): + _xNode, dName = clientData dNode = getMObject(dName) if dNode and not dNode.isNull(): dgMod.deleteNode(dNode) diff --git a/src/python/simplexui/interface/xsiInterface.py b/src/python/simplexui/interface/xsiInterface.py deleted file mode 100644 index 329f4454..00000000 --- a/src/python/simplexui/interface/xsiInterface.py +++ /dev/null @@ -1,2940 +0,0 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - - -# pylint: disable=invalid-name -import json -import os -import tempfile -from contextlib import contextmanager -from functools import wraps -from itertools import repeat - -import dcc.xsi as dcc -import numpy as np -from alembic.AbcGeom import OPolyMeshSchemaSample -from imath import IntArray, V3f, V3fArray - -from ..commands.alembicCommon import mkUvSample -from ..commands.buildIceXML import buildIceXML, buildLoaderXML, buildSliderIceXML -from Qt import QtCore -from Qt.QtCore import Signal -from Qt.QtWidgets import QApplication, QDialog, QMainWindow, QSplashScreen - - -# UNDO STACK INTEGRATION -@contextmanager -def undoContext(inst=None): - """ - - Parameters - ---------- - inst : - (Default value = None) - - Returns - ------- - - """ - if inst is None: - DCC.staticUndoOpen() - else: - inst.undoOpen() - try: - yield - finally: - if inst is None: - DCC.staticUndoClose() - else: - inst.undoClose() - - -def undoable(f): - """ - - Parameters - ---------- - f : - - - Returns - ------- - - """ - - @wraps(f) - def stacker(*args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - - Returns - ------- - - """ - with undoContext(): - return f(*args, **kwargs) - - return stacker - - -# temporarily disconnect inputs from the slider list -@contextmanager -def disconnected(sliders, prop): - """ - - Parameters - ---------- - sliders : - - prop : - - - Returns - ------- - - """ - params = [slider.thing for slider in sliders] - pairs = [] - toRemove = [] - for param in params: - if param.IsAnimated(dcc.constants.siExpressionSource): - pairs.append((param.Name, param.Source.Definition.Value)) - - toRemove.append(param) - dcc.xsi.RemoveAnimation(toRemove) - dcc.xsi.SetValue(params, 0.0) - - try: - yield pairs - finally: - for paramName, exp in pairs: - param = prop.Parameters(paramName) - if param is not None: - param.AddExpression(exp) - - -class DCC(object): - """ """ - - program = "xsi" - shapeNamePrefix = "" - texName = "Texture_Projection" - - def __init__(self, simplex, stack=None): - self.name = None # the name of the system - self.mesh = None # the mesh object with the system - self.inProp = None # the control property on the object - self.shapeCluster = None # the cluster containing the shapes - self.shapeTree = None # the ICETree driving the shapes - self.shapeNode = None # the shape compound node in the IceTree - self.sliderNode = None # the slider compound node in the IceTree - self.op = None # the simplex node in the IceTree - self.simplex = simplex # the abstract representation of the setup - self.undoDepth = 0 - self._live = True - self.sliderMul = self.simplex.sliderMul - - cls = type(self) - self.shapeNamePrefix = cls.shapeNamePrefix - # Reset the prefix because othewise, someone will forget - cls.shapeNamePrefix = "" - - def getShapeThing(self, shapeName): - """ - - Parameters - ---------- - shapeName : - - - Returns - ------- - - """ - for prop in self.shapeCluster.Properties: - if prop.Name == self.shapeNamePrefix + shapeName: - return [prop, None, None, None] - return None - - def getSliderThing(self, sliderName): - """ - - Parameters - ---------- - sliderName : - - - Returns - ------- - - """ - return self.inProp.Parameters(sliderName) - - def preLoad(self, simp, simpDict, create=True, pBar=None): - """ - - Parameters - ---------- - simp : - - simpDict : - - create : - (Default value = True) - pBar : - (Default value = None) - - Returns - ------- - - """ - # Pre-build all the nodes and parameters quickly - if pBar is not None: - pBar.setLabelText("Loading Connections") - QApplication.processEvents() - ev = simpDict["encodingVersion"] - - shapeNames = simpDict.get("shapes") - if not shapeNames: - return - - if ev > 1: - shapeNames = [i["name"] for i in shapeNames] - - # Gather all the missing shapes to create in one go - if pBar is not None: - pBar.setMaximum(len(shapeNames)) - pBar.setLabelText("Checking for missing shapes") - pBar.setValue(0) - QApplication.processEvents() - - toMake = self._checkAllShapeValidity(shapeNames, pBar=pBar) - - if toMake: - # Make 1 master duplicate and store it as a shapekey - # This ensures that all the shapes are created on the correct cluster - dupName = "__Simplex_Master_Dup" - tempVertArray, tempFaceArray = self.mesh.ActivePrimitive.Geometry.Get2() - dup = self.mesh.parent.AddPolygonMesh(tempVertArray, tempFaceArray, dupName) - dup.Properties("Visibility").viewvis = False - - newShape = dcc.xsi.StoreShapeKey( - self.shapeCluster, - dup.Name, - dcc.constants.siShapeObjectReferenceMode, - 1, - 0, - 0, - dcc.constants.siShapeContentPrimaryShape, - False, - ) - dcc.xsi.FreezeObj(newShape) - dcc.xsi.DeleteObj(dup) - sks = [newShape] - - if len(toMake) > 1: - # If there are more shapes to make, then we make them by - # duplicating the shapes from the mixer. This is easily - # 50x faster than creating shapes via any other method - if pBar is not None: - pBar.setMaximum(len(toMake)) - pBar.setValue(0) - pBar.setLabelText("Creating Empty Shapes") - QApplication.processEvents() - - mixShape = "{0}.{1}".format( - newShape.model.mixer.FullName, newShape.Name - ) - # This may fail on stupid-dense meshes. - # Maybe add a pointCount vs. chunk size heuristic? - chunk = 20 - for i in range(0, len(toMake[1:]), chunk): - if pBar is not None: - pBar.setValue(i) - QApplication.processEvents() - - curSize = min(len(toMake) - 1, i + chunk) - i - # Can't duplicate more than 1 at a time, otherwise we get memory issues - dupShapes = dcc.xsi.Duplicate( - mixShape, - curSize, - dcc.constants.siCurrentHistory, - dcc.constants.siSharedParent, - dcc.constants.siShareGrouping, - dcc.constants.siNoProperties, - dcc.constants.siDuplicateAnimation, - dcc.constants.siShareConstraints, - dcc.constants.siNoSelection, - ) - for dd in dupShapes: - cs = dcc.xsi.GetValue( - "{0}.{1}".format(self.shapeCluster.FullName, dd.Name) - ) - sks.append(cs) - - if pBar is not None: - pBar.setLabelText("Naming Shapes") - pBar.setValue(0) - pBar.setMaximum(len(sks)) - QApplication.processEvents() - - with self.noShapeNode(): - chunk = 20 - pfxToMake = [self.shapeNamePrefix + i for i in toMake] - for idx in range(0, len(sks), chunk): - if pBar is not None: - pBar.setValue(idx) - pns = ",".join( - [p.fullname + ".Name" for p in sks[idx : idx + chunk]] - ) - dcc.xsi.SetValue(pns, pfxToMake[idx : idx + chunk]) - - dcc.xsi.FreezeObj( - [i for i in self.shapeCluster.Properties if len(i.NestedObjects) > 2] - ) - - clsCombiner = dcc.operator.getOperatorFromStack( - self.mesh, "clustershapecombiner" - ) - if clsCombiner: - dcc.xsi.DeleteObj(clsCombiner) - - sliderNames = simpDict.get("sliders") - if not sliderNames: - return - if ev > 1: - sliderNames = [i["name"] for i in sliderNames] - else: - sliderNames = [i[0] for i in sliderNames] - - for sliderName in sliderNames: - sliderParam = self.inProp.Parameters(sliderName) - if not sliderParam: - if not create: - raise RuntimeError( - "Slider {0} not found with creation turned off".format( - sliderName - ) - ) - self.inProp.AddParameter3( - sliderName, dcc.constants.siFloat, 0, -2.0, 2.0 - ) - return None - - def postLoad(self, simp, preRet): - """ - - Parameters - ---------- - simp : - - preRet : - - - Returns - ------- - - """ - self.rebuildSliderNode() - self.resetShapeIndexes() - self.recreateShapeNode() - self.updateSlidersRange(simp.sliders) - - def checkForErrors(self, window): - """ Check for any DCC specific errors - - Parameters - ---------- - window : QMainWindow - The simplex window - """ - pass - - # System IO - @undoable - def loadNodes(self, simp, thing, create=True, pBar=None): - """Create a new system based on the simplex tree - Build any DCC objects that are missing if create=True - Raises a runtime error if missing objects are found and - create=False - - Parameters - ---------- - simp : - - thing : - - create : - (Default value = True) - pBar : - (Default value = None) - - Returns - ------- - - """ - self.name = simp.name - self.mesh = thing - - # find/build the shapeCluster - if pBar is not None: - pBar.setLabelText("Loading Nodes") - QApplication.processEvents() - - shapeCluster = thing.ActivePrimitive.Geometry.Clusters("Shape") - if not shapeCluster: - shapeCluster = thing.ActivePrimitive.Geometry.Clusters( - "%s_Shapes" % self.name - ) - if not shapeCluster: - if not create: - raise RuntimeError("Shape cluster not found with creation turned off") - self.shapeCluster = dcc.xsi.CreateCluster("%s.pnt[*]" % thing.FullName)[0] - self.shapeCluster.Name = "%s_Shapes" % self.name - dcc.xsi.SelectObj(thing) - else: - self.shapeCluster = shapeCluster - - # find/build the Icetree - shapeTree = thing.ActivePrimitive.ICETrees("%s_IceTree" % self.name) - if not shapeTree: - if not create: - raise RuntimeError( - "Simplex shape ICETree not found with creation turned off" - ) - self.shapeTree = dcc.ice.ICETree( - None, - self.mesh, - "%s_IceTree" % self.name, - dcc.constants.siConstructionModePrimaryShape, - ) - else: - self.shapeTree = dcc.ice.ICETree(shapeTree) - - # find/build the Slider compound in the Icetree - sliderComp = None - for node in self.shapeTree.compoundNodes: - if node.name == "SliderArray": - sliderComp = node - break - if not sliderComp: - if not create: - raise RuntimeError( - "Slider array compound not found in ICETree with creation turned off" - ) - sliderArray = self.shapeTree.addNode("BuildArray") - sliderComp = self.shapeTree.createCompound([sliderArray]) - sliderComp.rename("SliderArray") - sliderComp.exposePort(sliderArray.outputPorts["array"]) - self.sliderNode = sliderComp - - # find/build the simplex node in the Icetree - ops = DCC.getSimplexOperatorsOnObjectByName(thing, self.name) - if not ops: - if not create: - raise RuntimeError("Simplex Operator not found with create turned off") - op = dcc.ice.ICENode( - dcc.xsi.AddICENode("SimplexNode", self.shapeTree.fullName) - ) - op.inputPorts["Sliders"].connect(self.sliderNode.outputPorts["Array"]) - setter = self.shapeTree.addSetDataNode("Self._%s_SimplexVector" % self.name) - setter.Value.connect(op.outputPorts["Weights"]) - self.shapeTree.connect(setter.Execute, 1) - self.op = op - else: - self.op = ops - - # find/build the shape compound in the Icetree - shapeNode = None - for node in self.shapeTree.compoundNodes: - if node.name == "ShapeCompound": - shapeNode = node - break - if not shapeNode: - if not create: - raise RuntimeError( - "Shape compound not found in ICETree with creation turned off" - ) - setter = self.shapeTree.addSetDataNode("Self.PointPosition") - self.shapeTree.connect(setter.Execute) - shapeCompound = self.rebuildShapeNode(simp) - getter = self.shapeTree.addGetDataNode("Self._%s_SimplexVector" % self.name) - getter.value.connect(shapeCompound.inputPorts["In"]) - setter.Value.connect(shapeCompound.Result) - self.shapeNode = shapeCompound - else: - self.shapeNode = shapeNode - - # find/build the input and output properties - inProp = thing.Properties("%s_inProperty" % self.name) - if not inProp: - if not create: - raise RuntimeError( - "Control parameters not found with creation turned off" - ) - self.inProp = self.mesh.AddProperty( - "CustomProperty", False, "%s_inProperty" % self.name - ) - else: - self.inProp = inProp - - def _checkAllShapeValidity(self, shapeNames, errorOnMissing=False, pBar=None): - """Check shapes to see if they exist, and either gather the missing files, or - Load the proper data onto the shapes - - Parameters - ---------- - shapeNames : - - errorOnMissing : - (Default value = False) - pBar : - (Default value = None) - - Returns - ------- - - """ - propByName = {i.Name: i for i in list(self.shapeCluster.Properties)} - - if pBar is not None: - pBar.setMaximum(len(shapeNames)) - if errorOnMissing: - pBar.setLabelText("Checking Validity") - else: - pBar.setLabelText("Checking For Missing Shapes") - pBar.setValue(0) - QApplication.processEvents() - - # Keep the set ordered, but make a set for quick checking - missingNameSet = set() - missingNames = [] - seen = set() - - for i, shapeName in enumerate(shapeNames): - if shapeName in seen: - continue - seen.add(shapeName) - - if pBar is not None: - pBar.setValue(i) - QApplication.processEvents() - - if self.shapeNamePrefix + shapeName not in propByName: - if errorOnMissing: - raise RuntimeError("Missing shape: {}".format(shapeName)) - else: - if shapeName not in missingNameSet: - missingNameSet.add(shapeName) - missingNames.append(shapeName) - return missingNames - - def _loadShapeIceNodes(self, simp): - """load the ICE nodes related to the given shapeKey - - Parameters - ---------- - simp : - - - Returns - ------- - - """ - # Ice nodes are created in-order by the buildIceXML function - # so we can take advantage of that to properly connect the shapes - simpNode = self.shapeNode - children = simpNode.nodes - # addNode, passNode, getSelfPos = children[:3] - shapeNodes = children[3:] - propByName = {i.Name: i for i in list(self.shapeCluster.Properties)} - - for i, shape in enumerate(simp.shapes): - selector = shapeNodes[(3 * i) + 0]._nativePointer - getData = shapeNodes[(3 * i) + 1]._nativePointer - multiplier = shapeNodes[(3 * i) + 2]._nativePointer - shape.thing = [ - propByName[self.shapeNamePrefix + shape.name], - getData, - selector, - multiplier, - ] - - @contextmanager - def noShapeNode(self): - """ """ - # Clear the shape node so we don't take the renaming speed hit - if self.shapeNode: - vectorGetter = list( - self.shapeNode.inputPortList[0].connectedNodes.values() - )[0] - pointSetter = list( - self.shapeNode.outputPortList[0].connectedNodes.values() - )[0] - # Also disconnect the pointSetter so we don't get the null deformation - self.shapeTree.disconnect(2) - self.shapeNode.delete() - else: - vectorGetter, pointSetter = None, None - yield - - if vectorGetter is None and pointSetter is None: - self.shapeNode = self.rebuildShapeNode(self.simplex) - else: - # Rebuild the shape and slider nodes - self.shapeTree.connect(pointSetter.Execute, 2) - shapeCompound = self.rebuildShapeNode(self.simplex) - pointSetter.Value.connect(shapeCompound.Result) - vectorGetter.value.connect(shapeCompound.inputPorts["In"]) - self.shapeNode = shapeCompound - self._loadShapeIceNodes(self.simplex) - self.rebuildSliderNode() - - @staticmethod - def _getTextureProp(mesh, texName): - """ - - Parameters - ---------- - mesh : - - texName : - - - Returns - ------- - - """ - import pywintypes - - textureCls = [ - cluster - for cluster in mesh.ActivePrimitive.Geometry.Clusters - if cluster.Type == "sample" - ] - try: - for cluster in textureCls: - prop = cluster.Properties(texName) - if prop: - return prop - except pywintypes.com_error: - pass - return None - - @classmethod - def buildRestAbc(cls, abcMesh, name): - """ - - Parameters - ---------- - abcMesh : - - name : - - - Returns - ------- - - """ - with undoContext(): - meshSchema = abcMesh.getSchema() - rawFaces = meshSchema.getFaceIndicesProperty().samples[0] - rawCounts = meshSchema.getFaceCountsProperty().samples[0] - rawPos = meshSchema.getPositionsProperty().samples[0] - iuvs = meshSchema.getUVsParam() - - uvws = None - if iuvs.valid(): - uvValue = iuvs.getValueProperty().getValue() - uvs = list(zip(uvValue.x, uvValue.y, repeat(0.0))) - if iuvs.isIndexed(): - idxs = list(iuvs.getIndexProperty().getValue()) - uvs = [uvs[i] for i in idxs] - uvws = [] - ptr = 0 - for i in rawCounts: - uvws.extend(reversed(uvs[ptr : ptr + i])) - ptr += i - - faces = [] - ptr = 0 - for i in rawCounts: - faces.append(i) - faces.extend(reversed(rawFaces[ptr : ptr + i])) - ptr += i - - vertexArray = [list(rawPos.x), list(rawPos.y), list(rawPos.z)] - - cName = "{0}_SIMPLEX".format(name) - model = dcc.xsi.ActiveSceneRoot.AddModel( - None, "{0}_SIMPLEXModel".format(name) - ) - mesh = model.AddPolygonMesh(vertexArray, faces, cName) - - if uvws is not None: - dcc.xsi.CreateProjection(mesh, "", "", "", cls.texName, True, "", "") - texProp = cls._getTextureProp(mesh, cls.texName) - - if texProp is not None: - dcc.xsi.FreezeObj(texProp) - if len(uvws) == texProp.Elements.Count: - texProp.Elements.Array = list(zip(*uvws)) - dcc.xsi.FreezeObj(texProp) - - return mesh - - @staticmethod - def vertCount(mesh): - geo = mesh.ActivePrimitive.Geometry - vertArray, faceArray = geo.Get2() - return len(vertArray[0]) - - @undoable - def loadAbc(self, abcMesh, js, pBar=False): - """ - - Parameters - ---------- - abcMesh : - - js : - - pBar : - (Default value = False) - - Returns - ------- - - """ - shapes = js["shapes"] - if js["encodingVersion"] > 1: - shapes = [i["name"] for i in shapes] - - if pBar is not None: - pBar.show() - pBar.setMaximum(len(shapes)) - longName = max(shapes, key=len) - pBar.setValue(1) - pBar.setLabelText("Loading:\n{0}".format("_" * len(longName))) - QApplication.processEvents() - - # Load the alembic via geometry - preAtc = self.mesh.model.Properties("alembic_timecontrol") - loader = dcc.io.abcImport( - abcMesh.getArchive().getName(), parentNode=self.mesh.parent() - ) - postAtc = self.mesh.model.Properties("alembic_timecontrol") - loader = loader[0] - loader.name = "Loader" - loader.Properties("Visibility").viewvis = False - dcc.xsi.FreezeModeling(loader) - dcc.xsi.DeleteObj( - "{0}.polymsh.alembic_polymesh.Expression".format(loader.FullName) - ) - - tVal = "{0}.polymsh.alembic_polymesh.time".format(loader.FullName) - dcc.xsi.SetValue(tVal, 0) - - # rester = dcc.xsi.Duplicate(loader, 1, - # dcc.constants.siCurrentHistory, dcc.constants.siSharedParent, - # dcc.constants.siNoGrouping, dcc.constants.siNoProperties, - # dcc.constants.siNoAnimation, dcc.constants.siNoConstraints, - # dcc.constants.siNoSelection) - - tempVertArray, tempFaceArray = loader.ActivePrimitive.Geometry.Get2() - rester = loader.parent.AddPolygonMesh(tempVertArray, tempFaceArray, "Rester") - rester.Properties("Visibility").viewvis = False - - matcher = self._matchDelta(loader, rester) - - cluster = self.shapeCluster.FullName - # "{0}.polymsh.cls.Face_Shapes".format(self.mesh.FullName) - - with self.noShapeNode(): - - for i, shapeName in enumerate(shapes): - if pBar is not None: - pBar.setValue(i) - pBar.setLabelText("Loading:\n{0}".format(shapeName)) - QApplication.processEvents() - if pBar.wasCanceled(): - return - dcc.xsi.SetValue(tVal, i) - dcc.xsi.ReplaceShapeKey( - "{0}.{1}".format(cluster, self.shapeNamePrefix + shapeName) - ) - - matcher.delete() - dcc.xsi.DeleteObj(loader) - dcc.xsi.DeleteObj(rester) - self.deleteShapeCombiner() - if preAtc is None: - if postAtc is not None: - # Clear the alembic time control - dcc.xsi.DeleteObj(postAtc) - - # Force a rebuild of the Icetree - self.recreateShapeNode() - - if pBar is not None: - pBar.setValue(len(shapes)) - - def _matchDelta(self, loader, rester): - """ - - Parameters - ---------- - loader : - - rester : - - - Returns - ------- - - """ - xmlString = buildLoaderXML(loader, rester) - fHandle, compoundPath = tempfile.mkstemp(".xsicompound", text=True) - f = os.fdopen(fHandle, "w") - f.write(xmlString) - f.close() - tree = dcc.ice.ICETree( - None, - self.mesh, - "SimplexDeltaMatch", - dcc.constants.siConstructionModePrimaryShape, - ) - compound = dcc.ice.ICECompoundNode( - dcc.xsi.AddICECompoundNode(compoundPath, tree._nativePointer) - ) - tree.connect(compound.Value) - os.remove(compoundPath) - return tree - - def getShapeVertices(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ - dcc.xsi.DeactivateAbove("%s.modelingmarker" % self.mesh.ActivePrimitive, True) - restVerts = self.mesh.ActivePrimitive.Geometry.Points.PositionArray - restVerts = np.array(list(zip(*restVerts))) - shapeDeltas = np.array(shape.thing[0].Elements.Array).T - dcc.xsi.DeactivateAbove("%s.modelingmarker" % self.mesh.ActivePrimitive, "") - return restVerts + shapeDeltas - - def getAllShapeVertices(self, shapes, pBar=None): - """ - - Parameters - ---------- - shapes : - - pBar : - (Default value = None) - - Returns - ------- - - """ - for shape in shapes: - shape.verts = self.getShapeVertices(shape) - - def pushAllShapeVertices(self, shapes, pBar=None): - """ - - Parameters - ---------- - shapes : - - pBar : - (Default value = None) - - Returns - ------- - - """ - # take all the verts stored on the shapes - # and push them back to the DCC - for shape in shapes: - self.pushShapeVertices(shape) - - def pushShapeVertices(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ - # Push the vertices for a specific shape back to the DCC - pass - - def _getMeshVertices(self, mesh, world=False): - """ - - Parameters - ---------- - mesh : - - world : - (Default value = False) - - Returns - ------- - - """ - # We're ignoring world in XSI because we don't use it for that - vts = mesh.ActivePrimitive.Geometry.Points.PositionArray - vts = list(zip(*vts)) - return vts - - def _exportAbcVertices(self, mesh, shape, world=False): - """ - - Parameters - ---------- - mesh : - - shape : - - world : - (Default value = False) - - Returns - ------- - - """ - vts = np.array(self._getMeshVertices(mesh, world=world)) - shapeVts = np.array(shape.thing[0].Elements.Array) - outVerts = vts + shapeVts.T - - vertices = V3fArray(len(vts)) - setter = V3f(0, 0, 0) - for i in range(len(outVerts)): - setter.setValue(outVerts[i, 0], outVerts[i, 1], outVerts[i, 2]) - vertices[i] = setter - return vertices - - def _exportAbcFaces(self, mesh): - """ - - Parameters - ---------- - mesh : - - - Returns - ------- - - """ - geo = mesh.ActivePrimitive.Geometry - vertArray, faceArray = geo.Get2() - - texProp = self._getTextureProp(mesh, self.texName) - uvSample = None - if texProp is not None: - uvs = texProp.Elements.Array - uvs = list(zip(*uvs)) - uvD = {uv: i for i, uv in enumerate(set(uvs))} - uvIdxs = [uvD[uv] for uv in uvs] - uvs = [None] * len(uvD) - for uv, idx in uvD.items(): - uvs[idx] = uv - uvSample = mkUvSample(uvs, uvIdxs) - - ptr = 0 - faces = [] - faceCounts = [] - while ptr < len(faceArray): - count = faceArray[ptr] - faceCounts.append(count) - ptr += 1 - indices = reversed(faceArray[ptr : ptr + count]) - ptr += count - faces.extend(indices) - - abcFaceIndices = IntArray(len(faces)) - for i in range(len(faces)): - abcFaceIndices[i] = faces[i] - - abcFaceCounts = IntArray(len(faceCounts)) - for i in range(len(faceCounts)): - abcFaceCounts[i] = faceCounts[i] - - return abcFaceIndices, abcFaceCounts, uvSample - - @classmethod - def getMeshTopology(cls, mesh, uvName=None): - """Get the topology of a mesh - - Parameters - ---------- - mesh : object - The DCC Mesh to read - uvName : str, optional - The name of the uv set to read - - Returns - ------- - np.array : - The vertex array - np.array : - The "faces" array - np.array : - The "counts" array - np.array : - The uv positions - np.array : - The "uvFaces" array - """ - geo = mesh.ActivePrimitive.Geometry - vertArray, faceArray = geo.Get2() - - verts = list(zip(*vertArray)) - texProp = cls._getTextureProp(mesh, uvName) - uvs, uvIdxs = None, None - if texProp is not None: - uvs = texProp.Elements.Array - uvs = list(zip(*uvs)) - uvD = {uv: i for i, uv in enumerate(set(uvs))} - uvIdxs = [uvD[uv] for uv in uvs] - uvs = [None] * len(uvD) - for uv, idx in uvD.items(): - uvs[idx] = uv - - ptr = 0 - faces = [] - faceCounts = [] - while ptr < len(faceArray): - count = faceArray[ptr] - faceCounts.append(count) - ptr += 1 - indices = reversed(faceArray[ptr : ptr + count]) - ptr += count - faces.extend(indices) - - return verts, faces, faceCounts, uvs, uvIdxs - - def loadMeshTopology(self): - """ """ - self._faces, self._counts, self._uvs = self._exportAbcFaces(self.mesh) - - def exportAbc( - self, dccMesh, abcMesh, js, world=False, ensureCorrect=False, pBar=None - ): - """ """ - # dccMesh doesn't work in XSI, so just ignore it - # export the data to alembic - shapeDict = {i.name: i for i in self.simplex.shapes} - if js["encodingVersion"] > 1: - shapeNames = [i["name"] for i in js["shapes"]] - else: - shapeNames = js["shapes"] - shapes = [shapeDict[i] for i in shapeNames] - - faces, counts, uvSample = self._exportAbcFaces(dccMesh) - schema = abcMesh.getSchema() - - if pBar is not None: - pBar.show() - pBar.setMaximum(len(shapes)) - - # deactivate evaluation above modeling to insure no deformations are present - dcc.xsi.DeactivateAbove("%s.modelingmarker" % dccMesh.ActivePrimitive, True) - - for i, shape in enumerate(shapes): - if pBar is not None: - pBar.setLabelText("Exporting:\n{0}".format(shape.name)) - pBar.setValue(i) - QApplication.processEvents() - if pBar.wasCanceled(): - return - verts = self._exportAbcVertices(dccMesh, shape, world) - if uvSample is None: - abcSample = OPolyMeshSchemaSample(verts, faces, counts) - else: - # I Can't just do iUVs=None. Alembic uses something different for its defaults - abcSample = OPolyMeshSchemaSample(verts, faces, counts, iUVs=uvSample) - schema.set(abcSample) - - dcc.xsi.DeactivateAbove("%s.modelingmarker" % dccMesh.ActivePrimitive, "") - - # Revision tracking - def getRevision(self): - """ """ - try: - return self.op.inputPorts["Revision"].parameters["Revision"].value - except AttributeError: - return None # object does not exist - - def incrementRevision(self): - """ """ - value = self.getRevision() - if value is None: - return - - dcc.xsi.SetValue("%s.Revision" % self.op.fullName, value + 1) - d = self.simplex.buildDefinition() - jsString = json.dumps(d) - self.setSimplexString(self.op, jsString) - return value + 1 - - def setRevision(self, val): - """ - - Parameters - ---------- - val : - - - Returns - ------- - - """ - dcc.xsi.SetValue("%s.Revision" % self.op.fullName, val) - - # System level - @undoable - def renameSystem(self, name): - """ - - Parameters - ---------- - name : - - - Returns - ------- - - """ - self.name = name - self.inProp.Name = "%s_inProperty" % name - self.shapeTree._nativePointer.Name = "%s_IceTree" % name - self.shapeCluster.Name = "%s_Shapes" % name - - # rename data in the simplex vector setter and getter - vectorSetter = list(self.op.outputPortList[0].connectedNodes.values())[0] - vectorGetter = list(self.shapeNode.inputPortList[0].connectedNodes.values())[0] - # pointSetter = self.shapeNode.outputPortList[0].connectedNodes.values()[0] - for node in [vectorSetter, vectorGetter]: - dcc.xsi.SetValue( - "%s.Reference" % node.fullName, "self._%s_SimplexVector" % name - ) - - # rename the shape nodes in the IceTree by recreating the shape node - self.recreateShapeNode() - - def recreateShapeNode(self): - """ """ - # vectorSetter = self.op.outputPortList[0].connectedNodes.values()[0] - vectorGetter = list(self.shapeNode.inputPortList[0].connectedNodes.values())[0] - pointSetter = list(self.shapeNode.outputPortList[0].connectedNodes.values())[0] - - self.shapeNode.delete() - shapeCompound = self.rebuildShapeNode(self.simplex) - pointSetter.Value.connect(shapeCompound.Result) - vectorGetter.value.connect(shapeCompound.inputPorts["In"]) - - self.shapeNode = shapeCompound - self._loadShapeIceNodes(self.simplex) - - self.rebuildSliderNode() - - def rebuildSliderNode(self): - """ """ - if len(self.inProp.Parameters) == 0: - print("No input sliders") - return - - if self.sliderNode: - self.sliderNode.delete() - - sliderNames = [slider.name for slider in self.simplex.sliders] - # Possibly None - xmlString = buildSliderIceXML(sliderNames, self.name) - - fHandle, compoundPath = tempfile.mkstemp(".xsicompound", text=True) - f = os.fdopen(fHandle, "w") - f.write(xmlString) - f.close() - compound = dcc.ice.ICECompoundNode( - dcc.xsi.AddICECompoundNode(compoundPath, self.shapeTree._nativePointer) - ) - self.sliderNode = compound - os.remove(compoundPath) - - self.op.inputPorts["Sliders"].connect(compound.outputPorts["Array"]) - - # set the simplex string on simplex node - self.setSimplexString(self.op, self.simplex.dump()) - - def resetShapeIndexes(self): - """ """ - # set the simplex string on simplex node - self.setSimplexString(self.op, self.simplex.dump()) - - # check the shape node for proper indexes - for i, shape in enumerate(self.simplex.shapes): - if shape.thing[2] is None: - continue - port = shape.thing[2].InputPorts("index") - if port: - if port.Value != i: - dcc.xsi.SetValue("%s.index" % shape.thing[2], i) - else: - print("SHAPE HAS STRANGENESS", shape.name) - raise RuntimeError("BAD") - - # Shapes - @undoable - def createShape( - self, - shape, - live=False, - dataReferences=None, - deleteCombiner=True, - rebuild=False, - freeze=True, - ): - """ - - Parameters - ---------- - shape : - - live : - (Default value = False) - dataReferences : - (Default value = None) - deleteCombiner : - (Default value = True) - rebuild : - (Default value = False) - freeze : - (Default value = True) - - Returns - ------- - - """ - - ret, nn = self.createRawShape( - shape.name, - dataReferences=dataReferences, - deleteCombiner=False, - rebuild=rebuild, - freeze=freeze, - ) - - if deleteCombiner: - self.deleteShapeCombiner() - - if live: - self.extractShape(shape, live=True, offset=10.0) - - shape.name = nn - return ret - - def createRawShape( - self, - shapeName, - dataReferences=None, - elementArray=None, - deleteCombiner=True, - rebuild=True, - freeze=True, - ): - """ - - Parameters - ---------- - shapeName : - - dataReferences : - (Default value = None) - elementArray : - (Default value = None) - deleteCombiner : - (Default value = True) - rebuild : - (Default value = True) - freeze : - (Default value = True) - - Returns - ------- - - """ - - newShape = self.shapeCluster.Properties(shapeName) - if newShape is None: - newShape = dcc.xsi.StoreShapeKey( - self.shapeCluster, - shapeName, - dcc.constants.siShapeObjectReferenceMode, - 1, - 0, - 0, - dcc.constants.siShapeContentPrimaryShape, - False, - ) - # determine whether to do a inbetween shape or not based on the progression - if elementArray: - newShape.Elements.Array = elementArray - else: - dcc.shape.resetShapeKey(newShape) - if freeze: - dcc.xsi.FreezeObj(newShape) - - # create the node structure in the ICETree if not there already - shapeNodes = self.getShapeIceNodes(newShape, dataReferences) - - if rebuild: - self.resetShapeIndexes() - - if deleteCombiner: - self.deleteShapeCombiner() - - return [newShape] + shapeNodes, newShape.Name - - def getInbetweenArray(self, shapes): - """ - - Parameters - ---------- - shapes : - - - Returns - ------- - - """ - blendArray = [0] * 3 * self.mesh.ActivePrimitive.Geometry.Points.Count - shapeArray = self.getSimplexEvaluation() - if not shapeArray: - return None - shapeList = [shape.name for shape in self.simplex.shapes] - - for shape in shapes: - if not shape.thing: - continue - - if len(shapeArray[0]) == 1: - continue - idx = shapeList.index(shape.name) - shapeBlend = shapeArray[0][idx] - shapeTuple = shape.thing[0].Elements.Array - - for i in range(len(shapeTuple[0])): - blendArray[i * 3 + 0] += shapeTuple[0][i] * shapeBlend - blendArray[i * 3 + 1] += shapeTuple[1][i] * shapeBlend - blendArray[i * 3 + 2] += shapeTuple[2][i] * shapeBlend - - return blendArray - - @undoable - def getShapeIceNodes(self, shapeKey, dataReferences): - """build/return the ICE nodes related to the given shapeKey - - Parameters - ---------- - shapeKey : - - dataReferences : - - - Returns - ------- - - """ - simpNode = self.shapeNode - if not dataReferences: - dataReferences = DCC.getDataReferences(simpNode) - ports = simpNode.exposedPorts - if ports[0][0].isOutput(): - addNode = ports[0][0].parent - passNode = ports[1][0].parent - else: - addNode = ports[1][0].parent - passNode = ports[0][0].parent - - shapeRef = "%s.positions" % shapeKey.FullName.replace( - "%s.polymsh" % self.mesh.FullName, "self" - ) - if shapeRef in list(dataReferences.keys()): - shapeNode = dataReferences[shapeRef] - else: - shapeNode = simpNode.addGetDataNode(shapeRef) - - multNode = shapeNode.outputPorts["value"].connectedNodes - if "Multiply by Scalar" in list(multNode.keys()): - multNode = multNode["Multiply by Scalar"] - else: - multNode = simpNode.addNode("MultiplyByScalar") - - if multNode.name not in list( - shapeNode.outputPorts["value"].connectedNodes.keys() - ): - shapeNode.outputPorts["value"].connect(multNode.inputPorts["value"]) - - if addNode not in list(multNode.outputPorts["result"].connectedNodes.values()): - addPort = DCC.firstAvailablePort(addNode) - multNode.outputPorts["result"].connect(addPort) - - indexNode = multNode.inputPorts["factor"].connectedNodes - if "Select in Array" in list(indexNode.keys()): - indexNode = indexNode["Select in Array"] - else: - indexNode = simpNode.addNode("SelectInArray") - indexNode.value.connect(multNode.inputPorts["factor"]) - indexNode.inputPorts["array"].connect(passNode.outputPorts["out"]) - - return [ - shapeNode._nativePointer, - indexNode._nativePointer, - multNode._nativePointer, - ] - - def checkShapeValidity(self, shape, dataReferences=None): - """ - - Parameters - ---------- - shape : - - dataReferences : - (Default value = None) - - Returns - ------- - - """ - s = self.shapeCluster.Properties(shape.name) - if not s: - return False - - simpNode = self.shapeNode - if not dataReferences: - dataReferences = DCC.getDataReferences(simpNode) - shapeRef = "%s.positions" % s.FullName.replace( - "%s.polymsh" % self.mesh.FullName, "self" - ) - if shapeRef not in list(dataReferences.keys()): - return False - return True - - def rebuildShapeNode(self, simp, shapeNames=None): - """ - - Parameters - ---------- - simp : - - shapeNames : - (Default value = None) - - Returns - ------- - - """ - if shapeNames is None: - shapeNames = [shape.name for shape in simp.shapes] - xmlString = buildIceXML( - shapeNames, self.name, self.shapeCluster.Name, self.shapeNamePrefix - ) - - fHandle, compoundPath = tempfile.mkstemp(".xsicompound", text=True) - f = os.fdopen(fHandle, "w") - f.write(xmlString) - f.close() - - node = dcc.xsi.AddICECompoundNode(compoundPath, self.shapeTree._nativePointer) - shapeCompound = dcc.ice.ICECompoundNode(node) - os.remove(compoundPath) - - return shapeCompound - - @staticmethod - def firstAvailablePort(node): - """ - - Parameters - ---------- - node : - - - Returns - ------- - - """ - openPorts = [port for port in node.inputPortList if not port.isConnected()] - if not openPorts: - return node.addPortAtEnd(node.portAtEnd().name) - else: - return openPorts[0] - - @undoable - def extractShape(self, shape, live=True, offset=10.0): - """make a mesh representing a shape. Can be live or not - - Parameters - ---------- - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ - shapeKey = shape.thing[0] - print("Extracting %s" % shapeKey.Name) - shapeGeo = self.extractShapeAsGeo(shapeKey) - shapeGeo.posx.Value = offset - if live: - self.connectShape(shape, shapeGeo, live, delete=False) - return shapeGeo - - @undoable - def connectShape( - self, shape, mesh=None, live=False, delete=False, deleteCombiner=True - ): - """Force a shape to match a mesh - The "connect shape" button is: - mesh=None, delete=True - The "match shape" button is: - mesh=someMesh, delete=False - There is a possibility of a "make live" button: - live=True, delete=False - - Parameters - ---------- - shape : - - mesh : - (Default value = None) - live : - (Default value = False) - delete : - (Default value = False) - deleteCombiner : - (Default value = True) - - Returns - ------- - - """ - # print("Connected {0}".format(shape.name)) - - if not mesh: - mesh = DCC.findExtractedShape(shape.name) - if not mesh: - print("No extracted shape found to connect for %s" % shape.name) - return - - # print("Connected {0}".format(shape.name)) - - tempShape = dcc.xsi.SelectShapeKey( - self.shapeCluster, - mesh, - dcc.constants.siShapeObjectReferenceMode, - live, - False, - )[0] - dcc.xsi.DeleteObj(shape.thing[0]) - tempShape.Name = shape.name - shape.thing[0] = tempShape - - # print("Connected {0}".format(tempShape.Name)) - - if not live: - dcc.xsi.FreezeObj(tempShape) - if delete: - dcc.xsi.DeleteObj(mesh) - if deleteCombiner: - self.deleteShapeCombiner() - - # print("Connected {0}".format(tempShape.Name)) - - @staticmethod - def findExtractedShape(shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ - testName = "%s_Extract" % shape - testObj = dcc.xsi.ActiveSceneRoot.FindChild(testName) - - return testObj - - @undoable - def extractPosedShape(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ - pass - - @undoable - def zeroShape(self, shape): - """Set the shape to be completely zeroed - - Parameters - ---------- - shape : - - - Returns - ------- - - """ - shapeKey = shape.thing[0] - dcc.shape.resetShapeKey(shapeKey) - dcc.xsi.FreezeObj(shapeKey) - - print("Shape %s has been reset" % shape.name) - - @undoable - def deleteShape(self, toDelShape): - """Remove a shape from the system - - Parameters - ---------- - toDelShape : - - - Returns - ------- - - """ - dcc.xsi.DeleteObj(toDelShape.thing) - - # rebuild the operator - self.resetShapeIndexes() - - @undoable - def renameShape(self, shape, name): - """Change the name of the shape - - Parameters - ---------- - shape : - - name : - - - Returns - ------- - - """ - shape.thing[0].Name = name - name = shape.thing[0].Name - - shapeNodeList = shape.thing[1].Reference.Value.split(".") - shapeNodeList[-2] = name - shape.thing[1].Reference.Value = ".".join(shapeNodeList) - - self.resetShapeIndexes() - - @undoable - def convertShapeToCorrective(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ - pass - - def deleteShapeCombiner(self): - """ """ - clsCombiner = dcc.operator.getOperatorFromStack( - self.mesh, "clustershapecombiner" - ) - if clsCombiner: - dcc.xsi.DeleteObj(clsCombiner) - - def freezeAllShapes(self): - """ """ - toFreeze = [ - shape.thing[0] - for shape in self.simplex.shapes - if len(shape.thing[0].NestedObjects) > 2 - ] - dcc.xsi.FreezeObj(toFreeze) - - # Falloffs - def createFalloff(self, falloff): - """ - - Parameters - ---------- - falloff : - - - Returns - ------- - - """ - pass # for eventual live splits - - def duplicateFalloff(self, falloff, newFalloff, newName): - """ - - Parameters - ---------- - falloff : - - newFalloff : - - newName : - - - Returns - ------- - - """ - pass # for eventual live splits - - def deleteFalloff(self, falloff): - """ - - Parameters - ---------- - falloff : - - - Returns - ------- - - """ - pass # for eventual live splits - - def setFalloffData( - self, falloff, splitType, axis, minVal, minHandle, maxHandle, maxVal, mapName - ): - """ - - Parameters - ---------- - falloff : - - splitType : - - axis : - - minVal : - - minHandle : - - maxHandle : - - maxVal : - - mapName : - - - Returns - ------- - - """ - pass # for eventual live splits - - def getFalloffThing(self, falloff): - """ - - Parameters - ---------- - falloff : - - - Returns - ------- - - """ - return falloff.name - - # Sliders - @undoable - def createSlider(self, slider, rebuildOp=True): - """Create a new slider with a name - - Parameters - ---------- - slider : - - rebuildOp : - (Default value = True) - - Returns - ------- - - """ - name = slider.name - param = self.createSliderParam(slider, name) - - # connect solver - if rebuildOp: - self.rebuildSliderNode() - self.resetShapeIndexes() - self.deleteShapeCombiner() - return param - - @undoable - def renameSlider(self, slider, name): - """Set the name of a slider - - Parameters - ---------- - slider : - - name : - - - Returns - ------- - - """ - param = self.createSliderParam(slider, name) - if slider.thing.IsAnimated(dcc.constants.siAnySource): - dcc.xsi.CopyAnimation(slider.thing, True, True, False, True, True) - dcc.xsi.PasteAnimation(param, 1) - - dcc.xsi.RemoveCustomParam(slider.thing) - slider.thing = param - - self.rebuildSliderNode() - self.resetShapeIndexes() - - @undoable - def setSliderRange(self, slider): - """ - - Parameters - ---------- - slider : - - - Returns - ------- - - """ - vals = [v.value for v in slider.prog.pairs] - dcc.xsi.EditParameterDefinition( - slider.thing, - "", - "", - -2.0, - 2.0, - min(vals) * self.sliderMul, - max(vals) * self.sliderMul, - ) - - @undoable - def renameCombo(self, combo, name): - """ - - Parameters - ---------- - combo : - - name : - - - Returns - ------- - - """ - pass - - @undoable - def deleteSlider(self, toDelSlider): - """Remove a slider - - Parameters - ---------- - toDelSlider : - - - Returns - ------- - - """ - dcc.xsi.RemoveCustomParam(toDelSlider.thing) - self.rebuildSliderNode() - self.resetShapeIndexes() - - def createSliderParam(self, slider, name): - """ - - Parameters - ---------- - slider : - - name : - - - Returns - ------- - - """ - vals = [v.value for v in slider.prog.pairs] - param = self.inProp.AddParameter3(name, dcc.constants.siFloat, 0, -2.0, 2.0) - dcc.xsi.EditParameterDefinition( - param, - "", - "", - -2.0, - 2.0, - min(vals) * self.sliderMul, - max(vals) * self.sliderMul, - ) - return param - - @undoable - def addProgFalloff(self, prog, falloff): - """ - - Parameters - ---------- - prog : - - falloff : - - - Returns - ------- - - """ - pass # for eventual live splits - - @undoable - def removeProgFalloff(self, prog, falloff): - """ - - Parameters - ---------- - prog : - - falloff : - - - Returns - ------- - - """ - pass # for eventual live splits - - @undoable - def setSlidersWeights(self, sliders, weights): - """Set the weight of a slider. This does not change the definition - - Parameters - ---------- - sliders : - - weights : - - - Returns - ------- - - """ - for slider, weight in zip(sliders, weights): - slider.thing.Value = weight - - @undoable - def setSliderWeight(self, slider, weight): - """ - - Parameters - ---------- - slider : - - weight : - - - Returns - ------- - - """ - slider.thing.Value = weight - - @undoable - def updateSlidersRange(self, sliders): - """ - - Parameters - ---------- - sliders : - - - Returns - ------- - - """ - for slider in sliders: - vals = [v.value for v in slider.prog.pairs] - dcc.xsi.EditParameterDefinition( - slider.thing, - "", - "", - -2.0, - 2.0, - min(vals) * self.sliderMul, - max(vals) * self.sliderMul, - ) - - # Combos - def _createDelta(self, combo, target, tVal): - """Part of the combo extraction process. - Combo shapes are fixit shapes added on top of any sliders. - This means that the actual combo-shape by itself will not look good by itself, - and that's bad for artist interaction. - So we must create a setup to take the final sculpted shape, and subtract - the any direct slider deformations to get the actual "combo shape" as a delta - It is this delta shape that is then plugged into the system - - Parameters - ---------- - combo : - - target : - - tVal : - - - Returns - ------- - - """ - - # check if delta object already exists - deltaName = "%s_Delta" % combo.name - deltaObj = dcc.xsi.ActiveSceneRoot.FindChild(deltaName) - if deltaObj: - return deltaObj - - # create and extract a temporary shape to use as a reference - # of everything but the actual combo shape - tempShape = dcc.xsi.StoreShapeKey( - self.shapeCluster, - "temp", - dcc.constants.siShapeObjectReferenceMode, - 1, - 0, - 0, - dcc.constants.siShapeContentPrimaryShape, - False, - ) - affected = self.getComboAffected(combo) - - with disconnected(affected[0], self.inProp): - # set sliders for the combo - for pair in combo.pairs: - pair.slider.thing.Value = pair.value * tVal - - tempShape.Elements.Array = self.getInbetweenArray(affected[1]) - - # build delta object - deltaObj = self.extractShapeAsGeo(tempShape) - deltaObj.Name = deltaName - deltaObj.Properties("visibility").Parameters("viewvis").Value = False - deltaObj.posx.Value = 10.0 - deltaObj.posz.Value = -10.0 - - # apply shape of target to delta object - dcc.xsi.SelectShapeKey( - deltaObj, target, dcc.constants.siShapeObjectReferenceMode, True, True - )[0] - - # get undeformed shape to apply to get delta, and apply as a shape to delta - tempBase = self.extractShapeAsGeo(self.simplex.restShape.thing[0]) - - dcc.xsi.SelectShapeKey( - deltaObj, tempBase, dcc.constants.siShapeObjectReferenceMode, False, True - )[0] - - for param in deltaObj.Properties("ShapeWeights").Parameters: - dcc.xsi.RemoveAnimation(param) - param.Value = 1 - - dcc.xsi.DeleteObj(tempBase) - dcc.xsi.DeleteObj(tempShape) - - return deltaObj - - def getComboAffected(self, combo): - """ - - Parameters - ---------- - combo : - - - Returns - ------- - type - - - """ - # Get all related sliders to the combo - comboSliders = [i.slider for i in combo.pairs] - comboSlidersSet = set(comboSliders) - - # get list of combo shapes that have an impact on the current combo - affected = [] - for slider in comboSliders: - for pair in slider.prog.pairs: - if pair.shape not in affected: - affected.append(pair.shape) - for c in self.simplex.combos: - if c == combo: - continue - cSliders = set(i.slider for i in c.pairs) - if cSliders <= comboSlidersSet: - for pair in c.prog.pairs: - affected.append(pair.shape) - - return [comboSliders, affected] - - @undoable - def extractTraversalShape(self, trav, shape, live=True, offset=10.0): - """ - - Parameters - ---------- - trav : - - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ - pass - - @undoable - def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False): - """ - - Parameters - ---------- - trav : - - shape : - - mesh : - (Default value = None) - live : - (Default value = True) - delete : - (Default value = False) - - Returns - ------- - - """ - pass - - @undoable - def extractComboShape(self, combo, shape, live=True, offset=10.0): - """Extract a shape from a combo progression - - Parameters - ---------- - combo : - - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ - - # extract the rest shape to use as a base - tempShape = dcc.xsi.StoreShapeKey( - self.shapeCluster, - "temp", - dcc.constants.siShapeObjectReferenceMode, - 1, - 0, - 0, - dcc.constants.siShapeContentPrimaryShape, - False, - ) - - affected = self.getComboAffected(combo) - - with disconnected(affected[0], self.inProp): - # set the relevant sliders for the combo - for pair in combo.pairs: - pair.slider.thing.Value = pair.value - - # also grab the shape that we are affecting - affected[1].append(shape) - - # get the blendarray - tempShape.Elements.Array = self.getInbetweenArray(affected[1]) - - # extracted = dcc.shape.extractShapeKeyAsGeo(tempShape) - extracted = self.extractShapeAsGeo(tempShape) - extracted.Name = "%s_Extract" % shape.name - extracted.posx.Value = offset - - dcc.xsi.DeleteObj(tempShape) - - if live: - self.connectComboShape(combo, shape, extracted, live=live, delete=False) - - return extracted - - @undoable - def connectComboShape(self, combo, shape, mesh=None, live=False, delete=False): - """Connect a shape into a combo progression - - Parameters - ---------- - combo : - - shape : - - mesh : - (Default value = None) - live : - (Default value = False) - delete : - (Default value = False) - - Returns - ------- - - """ - if not mesh: - mesh = DCC.findExtractedShape(shape.name) - if not mesh: - print("No extracted shape found to connect for %s" % shape.name) - return - shapeIdx = combo.prog.getShapeIndex(shape) - tVal = combo.prog.pairs[shapeIdx].value - delta = self._createDelta(combo, mesh, tVal) - self.connectShape(shape, delta, live, delete) - if delete: - dcc.xsi.DeleteObj(mesh) - - @undoable - def extractShapeAsGeo(self, shapeKey): - """ - - Parameters - ---------- - shapeKey : - - - Returns - ------- - - """ - # create new object with same shape as mesh using dirty tricks to set geometry directly - temp = dcc.xsi.ActiveSceneRoot.AddGeometry( - "Cube", "MeshSurface", "%s_Extract" % shapeKey.Name - ) - dcc.xsi.FreezeObj(temp) - dcc.xsi.DeactivateAbove("%s.modelingmarker" % self.mesh.ActivePrimitive, True) - vertArray, faceArray = self.mesh.ActivePrimitive.Geometry.Get2() - temp.ActivePrimitive.Geometry.set(vertArray, faceArray) - dcc.xsi.DeactivateAbove("%s.modelingmarker" % self.mesh.ActivePrimitive, "") - - # copy over the shape - tempShape = dcc.xsi.StoreShapeKey( - temp, "Shape", dcc.constants.siShapeObjectReferenceMode - ) - tempShape.Elements.Array = shapeKey.Elements.Array - dcc.xsi.ApplyShapeKey(tempShape, None, None, 100, None, None, None, 2) - dcc.xsi.FreezeObj(temp) - - return temp - - @staticmethod - def setDisabled(op): - """ - - Parameters - ---------- - op : - - - Returns - ------- - - """ - nc = op.rootNodeContainer - pairs = [] - for pName, port in nc.inputPorts.items(): - outs = list(port.connectedPorts.values()) - if outs: - pairs.append((port, outs[0])) - port.disconnect() - return pairs - - @staticmethod - def reEnable(helpers): - """ - - Parameters - ---------- - helpers : - - - Returns - ------- - - """ - for inPort, outPort in helpers: - inPort.connect(outPort) - - # Data Access - @staticmethod - def getSimplexOperatorsOnObject(thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - type - - - """ - out = [] - for tree in thing.ActivePrimitive.ICETrees: - simplexNodes = [ - dcc.ice.ICENode(node) - for node in tree.Nodes - if node.Type == "SimplexNode" - ] - out.extend(simplexNodes) - return out - - @staticmethod - def getSimplexOperatorsOnObjectByName(thing, name): - """ - - Parameters - ---------- - thing : - - name : - - - Returns - ------- - type - - - """ - ops = DCC.getSimplexOperatorsOnObject(thing) - ops = DCC.filterSimplexByName(ops, name) - - if not ops: - return None - else: - return ops[0] - - @staticmethod - def getSimplexString(op): - """ - - Parameters - ---------- - op : - - - Returns - ------- - type - - - """ - return op.inputPorts["Definition"].parameters["Definition_string"].value - - @staticmethod - def getSimplexStringOnThing(thing, systemName): - """ - - Parameters - ---------- - thing : - - systemName : - - - Returns - ------- - type - - - """ - op = DCC.getSimplexOperatorsOnObjectByName(thing, systemName) - return DCC.getSimplexString(op) - - @staticmethod - def filterSimplexByObject(ops, thing): - """ - - Parameters - ---------- - ops : - - thing : - - - Returns - ------- - - """ - filtered = [] - if not ops: - return filtered - for i in ops: - if not i.Parent3DObject: - # dcc.xsi.DeleteObj(i) - continue - if i.Parent3DObject.IsEqualTo(thing): - filtered.append(i) - return filtered - - @staticmethod - def filterSimplexByName(ops, name): - """ - - Parameters - ---------- - ops : - - name : - - - Returns - ------- - - """ - filtered = [] - if not ops: - return filtered - for i in ops: - js = json.loads(DCC.getSimplexString(i)) - if js["systemName"] == name: - filtered.append(i) - return filtered - - @staticmethod - def setSimplexString(op, val): - """set the definition string from a simplex operator - - Parameters - ---------- - op : - - val : - - - Returns - ------- - - """ - if op: - dcc.xsi.SetValue("%s.Definition_string" % op.fullName, val) - return val - else: - return val - - @staticmethod - def selectObject(thing): - """Select an object in the DCC - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - dcc.xsi.SelectObj(thing) - - def selectCtrl(self): - """Select the system's control object""" - self.selectObject(self.inProp) - - @staticmethod - def getObjectByName(name): - """ - - Parameters - ---------- - name : - - - Returns - ------- - type - - - """ - return dcc.xsi.Dictionary.GetObject(name, False) - - @staticmethod - def getObjectName(thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - type - - - """ - return thing.Name - - @staticmethod - def getSelectedObjects(): - """ """ - # For maya, only return transform nodes - return list(dcc.xsi.Selection) - - @staticmethod - def getDataReferences(node): - """ - - Parameters - ---------- - node : - - - Returns - ------- - - """ - dataDict = {} - for n in node.nodes: - if n.type != "SceneReferenceNode": - continue - dataDict[n.Reference.Value] = n - return dataDict - - @staticmethod - def staticUndoOpen(): - """ """ - dcc.xsi.OpenUndo("SimplexUndo") - - @staticmethod - def staticUndoClose(): - """ """ - dcc.xsi.CloseUndo() - - def undoOpen(self): - """ """ - if self.undoDepth == 0: - self.staticUndoOpen() - self.undoDepth += 1 - - def undoClose(self): - """ """ - self.undoDepth -= 1 - if self.undoDepth == 0: - self.staticUndoClose() - - def getSimplexEvaluation(self): - """ """ - geo = self.mesh.ActivePrimitive.Geometry - evalArray = geo.GetICEAttributeFromName( - "_%s_SimplexVector" % self.name - ).DataArray2D - return evalArray - - @classmethod - def getPersistentFalloff(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - return thing.FullName - - @classmethod - def loadPersistentFalloff(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - return cls.getObjectByName(thing) - - @classmethod - def getPersistentShape(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - # return cls.getObjectName(thing) - if thing is None: - return None - names = [] - for item in thing: - app = None if item is None else item.FullName - names.append(app) - return names - - @classmethod - def loadPersistentShape(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - if thing is None: - return None - items = [] - for name in thing: - items.append(cls.getObjectByName(name)) - return items - - @classmethod - def getPersistentSlider(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - return thing.FullName - - @classmethod - def loadPersistentSlider(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - return cls.getObjectByName(thing) - - def getFreezeThing(self, combo): - return [] - - -class SliderDispatch(QtCore.QObject): - """ """ - - valueChanged = Signal() - - def __init__(self, node, parent=None): - super(SliderDispatch, self).__init__(parent) - - def emitValueChanged(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - - Returns - ------- - - """ - self.valueChanged.emit() - - -class Dispatch(QtCore.QObject): - """ """ - - beforeNew = Signal() - afterNew = Signal() - beforeOpen = Signal() - afterOpen = Signal() - undo = Signal() - redo = Signal() - - def __init__(self, parent=None): - super(Dispatch, self).__init__(parent) - self.callbackIDs = [] - self.connectCallbacks() - - def connectCallbacks(self): - """ """ - if self.callbackIDs: - self.disconnectCallbacks() - - # self.callbackIDs.append(om.MSceneMessage.addCallback(om.MSceneMessage.kBeforeNew, self.emitBeforeNew)) - # self.callbackIDs.append(om.MSceneMessage.addCallback(om.MSceneMessage.kAfterNew, self.emitAfterNew)) - # self.callbackIDs.append(om.MSceneMessage.addCallback(om.MSceneMessage.kBeforeOpen, self.emitBeforeOpen)) - # self.callbackIDs.append(om.MSceneMessage.addCallback(om.MSceneMessage.kAfterOpen, self.emitAfterOpen)) - # self.callbackIDs.append(om.MEventMessage.addEventCallback("Undo", self.emitUndo)) - # self.callbackIDs.append(om.MEventMessage.addEventCallback("Redo", self.emitRedo)) - - def disconnectCallbacks(self): - """ """ - for i in self.callbackIDs: - # om.MMessage.removeCallback(i) - pass - - def emitBeforeNew(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - - Returns - ------- - - """ - self.beforeNew.emit() - - def emitAfterNew(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - - Returns - ------- - - """ - self.afterNew.emit() - - def emitBeforeOpen(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - - Returns - ------- - - """ - self.beforeOpen.emit() - - def emitAfterOpen(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - - Returns - ------- - - """ - self.afterOpen.emit() - - def emitUndo(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - - Returns - ------- - - """ - self.undo.emit() - - def emitRedo(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - - Returns - ------- - - """ - self.redo.emit() - - def __del__(self): - self.disconnectCallbacks() - - -DISPATCH = Dispatch() - - -def rootWindow(): - """Returns the currently active QT main window - Only works for QT UI's like Maya - - Parameters - ---------- - - Returns - ------- - - """ - # for MFC apps there should be no root window - window = None - if QApplication.instance(): - inst = QApplication.instance() - window = inst.activeWindow() - # Ignore QSplashScreen's, they should never be considered the root window. - if isinstance(window, QSplashScreen): - return None - # If the application does not have focus try to find A top level widget - # that doesn't have a parent and is a QMainWindow or QDialog - if window is None: - windows = [] - dialogs = [] - for w in QApplication.instance().topLevelWidgets(): - if w.parent() is None: - if isinstance(w, QMainWindow): - windows.append(w) - elif isinstance(w, QDialog): - dialogs.append(w) - if windows: - window = windows[0] - elif dialogs: - window = dialogs[0] - - # grab the root window - if window: - while True: - parent = window.parent() - if not parent: - break - if isinstance(parent, QSplashScreen): - break - window = parent - - return window diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 92981aac..9fdc614e 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -521,6 +521,29 @@ def loadSmpxShapes(self, smpxPath, pBar=None): finally: del abcMesh, iarch + def loadSmpxPoses(self, smpxPath, pBar=None): + """Load the Poses from a .smpx file onto an already loaded system + This is the "Update the joints and skin" method + + Parameters + ---------- + smpxPath : str + The path to the .smpx file + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + + """ + iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) + js = json.loads(jsString) + + try: + self.DCC.loadAbcPoses(abcMesh, js, pBar=pBar) + finally: + del abcMesh, iarch + def loadSmpxFalloffs(self, abcPath, pBar=None): """Load the relevant data from a simplex alembic diff --git a/src/python/simplexui/menu/__init__.py b/src/python/simplexui/menu/__init__.py index 7838e0e3..7df9552e 100644 --- a/src/python/simplexui/menu/__init__.py +++ b/src/python/simplexui/menu/__init__.py @@ -26,8 +26,6 @@ CONTEXT = os.path.basename(sys.executable) if CONTEXT == "maya.exe": from . import mayaPlugins as plugins -elif CONTEXT == "XSI.exe": - from . import xsiPlugins as plugins else: from . import dummyPlugins as plugins diff --git a/src/python/simplexui/menu/xsiPlugins/__init__.py b/src/python/simplexui/menu/xsiPlugins/__init__.py deleted file mode 100644 index c90769bd..00000000 --- a/src/python/simplexui/menu/xsiPlugins/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . diff --git a/src/python/simplexui/meson.build b/src/python/simplexui/meson.build index aaeeae39..da39dd2f 100644 --- a/src/python/simplexui/meson.build +++ b/src/python/simplexui/meson.build @@ -1,34 +1,40 @@ + py.install_sources( '__init__.py', 'channelBox.py', 'comboCheckDialog.py', + 'dragFilter.py', + 'falloffDialog.py', + 'interfaceModel.py', + 'interfaceModelTrees.py', + 'interfaceModel_Test.py', + 'main.pyw', + 'simplexDialog.py', + 'travCheckDialog.py', + 'traversalDialog.py', + 'utils.py', 'commands/__init__.py', 'commands/alembicCommon.py', + 'commands/alembicSimplex.py', 'commands/applyCorrectives.py', - 'commands/buildIceXML.py', 'commands/correctiveInterface.py', 'commands/expandedExport.py', 'commands/hdf5Convert.py', 'commands/mayaCorrectiveInterface.py', + 'commands/mayatonumpy.py', 'commands/mesh.py', + 'commands/numpytoimath.py', + 'commands/poseblendlib.py', 'commands/reorderSimplexPoints.py', 'commands/rigidAlign.py', - 'commands/setDelta.xsicompound', 'commands/smpxBlend.py', 'commands/unsubdivide.py', 'commands/uvTransfer.py', - 'commands/xsiCorrectiveInterface.py', - 'dragFilter.py', - 'falloffDialog.py', 'img/ChefHead.png', 'img/frozen.png', 'interface/__init__.py', 'interface/dummyInterface.py', 'interface/mayaInterface.py', - 'interface/xsiInterface.py', - 'interfaceModel.py', - 'interfaceModelTrees.py', - 'interfaceModel_Test.py', 'items/__init__.py', 'items/accessor.py', 'items/combo.py', @@ -40,7 +46,6 @@ py.install_sources( 'items/slider.py', 'items/stack.py', 'items/traversal.py', - 'main.pyw', 'menu/__init__.py', 'menu/dummyPlugins/__init__.py', 'menu/genericPlugins/__init__.py', @@ -65,16 +70,12 @@ py.install_sources( 'menu/mayaPlugins/softSelectToCluster.py', 'menu/mayaPlugins/tweakMix.py', 'menu/mayaPlugins/updateRestShape.py', - 'menu/xsiPlugins/__init__.py', - 'simplexDialog.py', - 'travCheckDialog.py', - 'traversalDialog.py', 'ui/comboCheckDialog.ui', 'ui/falloffDialog.ui', 'ui/simplexDialog.ui', 'ui/travCheckDialog.ui', 'ui/traversalDialog.ui', - 'utils.py', + subdir : 'simplexui', preserve_path: true, ) From cf08218d446fc8a0f8610c1710493eea333e3862 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Mon, 16 Jun 2025 18:41:02 -0700 Subject: [PATCH 08/25] A quick readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ca319cb..48112203 100644 --- a/README.md +++ b/README.md @@ -63,4 +63,4 @@ runSimplexUI() ## Compiling -Hopefully you don't need to do this, but if you have to, just take a look at `.github/workflows/main.yml` and you should be able to piece together how to get a compile working using CMake. You aren't required to download the devkit or set its path for CMake if you've got maya installed on your machine. Also note, I use features from CMake 3.16+ so I can target python 2 and 3 separately. +Hopefully you don't need to do this, but if you have to, just take a look at the `quick_compile.bat` file to see how to kick off a compile. And also look at `.github/workflows/main.yml` if you need to piece together the dependencies required. From 2e9f3671e9918a2f23a3b37eaf0a8d7f11b81d5d Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 10 Sep 2025 12:05:58 -0700 Subject: [PATCH 09/25] fix zipfile extraction --- simplex_maya_installer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/simplex_maya_installer.py b/simplex_maya_installer.py index 69107a46..0aa3eb1c 100644 --- a/simplex_maya_installer.py +++ b/simplex_maya_installer.py @@ -212,7 +212,9 @@ def onMayaDroppedPythonFile(_obj): raise RuntimeError("Download of simplex zip failed") with zipfile.ZipFile(simplex_zip, "r") as zip_ref: - zip_ref.extractall(mod_folder) + members = [m for m in zip_ref.namelist() if m.startswith("modules/")] + zip_ref.extractall(mod_folder.parent, members=members) + os.remove(simplex_zip) mayapy = get_mayapy_path() From eb52c138eb69d708b95806f6b75e1eada84d64b6 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 10 Sep 2025 12:08:56 -0700 Subject: [PATCH 10/25] Black all the python code --- simplex_maya_installer.py | 5 ++-- src/python/simplexui/__init__.py | 1 + .../simplexui/commands/correctiveInterface.py | 1 + src/python/simplexui/commands/mayatonumpy.py | 30 +++++++++---------- src/python/simplexui/commands/numpytoimath.py | 2 +- src/python/simplexui/commands/poseblendlib.py | 2 +- .../simplexui/interface/dummyInterface.py | 2 +- .../simplexui/interface/mayaInterface.py | 17 +++++------ src/python/simplexui/interfaceModel.py | 4 ++- .../simplexui/menu/mayaPlugins/freezeCombo.py | 1 - .../menu/mayaPlugins/softSelectToCluster.py | 3 +- src/python/simplexui/simplexDialog.py | 24 +++++++-------- src/python/simplexui/utils.py | 3 +- 13 files changed, 47 insertions(+), 48 deletions(-) diff --git a/simplex_maya_installer.py b/simplex_maya_installer.py index 0aa3eb1c..4b1f3065 100644 --- a/simplex_maya_installer.py +++ b/simplex_maya_installer.py @@ -220,12 +220,12 @@ def onMayaDroppedPythonFile(_obj): mayapy = get_mayapy_path() target = get_numpy_simplex_target(mod_folder) - if importlib.util.find_spec('numpy') is None: + if importlib.util.find_spec("numpy") is None: install_numpy(mayapy, target) else: logger.info("Numpy is already installed for this version of maya") - if importlib.util.find_spec('Qt') is None: + if importlib.util.find_spec("Qt") is None: install_qtpy(mayapy, target) else: logger.info("Qt.py is already installed for this version of maya") @@ -238,4 +238,3 @@ def onMayaDroppedPythonFile(_obj): message="Simplex installation complete", button=["OK"], ) - diff --git a/src/python/simplexui/__init__.py b/src/python/simplexui/__init__.py index e104cecd..eb58a622 100644 --- a/src/python/simplexui/__init__.py +++ b/src/python/simplexui/__init__.py @@ -20,6 +20,7 @@ SIMPLEX_UI = None SIMPLEX_UI_ROOT = None + def runSimplexUI(): from .interface import DISPATCH, rootWindow from .simplexDialog import SimplexDialog diff --git a/src/python/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py index 78f4406f..3c94b8c6 100644 --- a/src/python/simplexui/commands/correctiveInterface.py +++ b/src/python/simplexui/commands/correctiveInterface.py @@ -23,6 +23,7 @@ pass from .mayaCorrectiveInterface import getShiftValues, resetPose, setPose + dcc = "maya" diff --git a/src/python/simplexui/commands/mayatonumpy.py b/src/python/simplexui/commands/mayatonumpy.py index 88d75f1d..95f3f4ed 100644 --- a/src/python/simplexui/commands/mayatonumpy.py +++ b/src/python/simplexui/commands/mayatonumpy.py @@ -17,7 +17,7 @@ def _swigConnect(mArray, count, util): - ''' + """ Use an MScriptUtil to build SWIG array that we can read from and write to. Make sure to get the MScriptUtil from outside this function, otherwise it may be garbage collected @@ -28,7 +28,7 @@ def _swigConnect(mArray, count, util): ptrType: An unbound method on MScriptUtil to cast the pointer to the correct type I can still call that unbound method by manually passing the usually-implicit self argument (which will be an instance of MScriptUtil) - ''' + """ pyTyp, comps, ctp, ptrTyp = _CONVERT_DICT[type(mArray)] cc = count * comps util.createFromList([pyTyp()] * cc, cc) @@ -69,7 +69,7 @@ def _swigConnectMatrix(mat, ctp): def mayaToNumpy(mArray): - '''Convert a maya array to a numpy array + """Convert a maya array to a numpy array Parameters ---------- @@ -81,7 +81,7 @@ def mayaToNumpy(mArray): : np.array : A numpy array that contains the data from mArray - ''' + """ if isinstance(mArray, om.MMatrix): npArray, _ = _swigConnectMatrix(mArray, c_double) elif isinstance(mArray, om.MFloatMatrix): @@ -94,7 +94,7 @@ def mayaToNumpy(mArray): def numpyToMaya(ary, mType): - '''Convert a numpy array to a specific maya type array + """Convert a numpy array to a specific maya type array Parameters ---------- @@ -109,7 +109,7 @@ def numpyToMaya(ary, mType): : mType : An array of the provided type that contains the data from ary - ''' + """ # Handle matrices separately if mType in (om.MMatrix, om.MFloatMatrix): ctp = c_double if mType == om.MMatrix else c_float @@ -178,7 +178,7 @@ def numpyToMaya(ary, mType): def getNumpyAttr(attrName): - '''Read attribute data directly from the plugs into numpy + """Read attribute data directly from the plugs into numpy This function will read most numeric data types directly into numpy arrays However, some simple data types (floats, vectors, etc...) have api accessors @@ -196,7 +196,7 @@ def getNumpyAttr(attrName): : object : The numerical data from the provided plug. A np.array, float, int, or tuple - ''' + """ if isinstance(attrName, str): sl = om.MSelectionList() sl.add(attrName) @@ -263,7 +263,7 @@ def getNumpyAttr(attrName): def setNumpyAttr(attrName, value): - '''Write a numpy array directly into a maya plug + """Write a numpy array directly into a maya plug This function will handle most numeric plug types. But for single float, individual point, etc.. types, consider using cmds.setAttr @@ -277,7 +277,7 @@ def setNumpyAttr(attrName, value): Or the MPlug itself value : int, float, tuple, np.array The correctly typed value to set on the attribute - ''' + """ if isinstance(attrName, str): sl = om.MSelectionList() sl.add(attrName) @@ -346,17 +346,17 @@ def test(): import time from maya import cmds - meshName = 'pSphere1' - bsName = 'blendShape1' + meshName = "pSphere1" + bsName = "blendShape1" meshIdx = 0 bsIdx = 0 # A quick test showing how to build a numpy array # containing the deltas for a shape on a blendshape node numVerts = cmds.polyEvaluate(meshName, vertex=True) - baseAttr = '{0}.it[{1}].itg[{2}].iti[6000]'.format(bsName, meshIdx, bsIdx) - inPtAttr = baseAttr + '.inputPointsTarget' - inCompAttr = baseAttr + '.inputComponentsTarget' + baseAttr = "{0}.it[{1}].itg[{2}].iti[6000]".format(bsName, meshIdx, bsIdx) + inPtAttr = baseAttr + ".inputPointsTarget" + inCompAttr = baseAttr + ".inputComponentsTarget" start = time.time() points = getNumpyAttr(inPtAttr) diff --git a/src/python/simplexui/commands/numpytoimath.py b/src/python/simplexui/commands/numpytoimath.py index d6df6e96..4a16b230 100644 --- a/src/python/simplexui/commands/numpytoimath.py +++ b/src/python/simplexui/commands/numpytoimath.py @@ -181,7 +181,7 @@ def _getImoPointer(imo, extra: str) -> int: def _link(imo) -> tuple[np.ndarray, int]: - """ Build a numpy object that's referencing the same memory + """Build a numpy object that's referencing the same memory as the given imath object Args: diff --git a/src/python/simplexui/commands/poseblendlib.py b/src/python/simplexui/commands/poseblendlib.py index fc7fe5da..3f005fe4 100644 --- a/src/python/simplexui/commands/poseblendlib.py +++ b/src/python/simplexui/commands/poseblendlib.py @@ -75,7 +75,7 @@ def quaternion_pow(q: np.ndarray, n: np.ndarray): new_w = np.cos(new_theta) # Fall back to a linear approximation for small angles - with np.errstate(divide='ignore', invalid='ignore'): + with np.errstate(divide="ignore", invalid="ignore"): scale = np.where(sin_theta > 1e-8, np.sin(new_theta) / sin_theta, n) new_v = v * scale[..., np.newaxis] diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index 11694a42..c8ea4c82 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -272,7 +272,7 @@ def postLoad(self, simp, preRet): pass def checkForErrors(self, window): - """ Check for any DCC specific errors + """Check for any DCC specific errors Parameters ---------- diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index c5c10694..208eaaf1 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -276,7 +276,7 @@ def _removeExtraShapeNodes(cls, tfm): keeper = None todel = [] for sn in shapeNodes: - tfmChk = ''.join(sn.rsplit('Shape', 1)) + tfmChk = "".join(sn.rsplit("Shape", 1)) if tfmChk == tfm: keeper = sn else: @@ -400,8 +400,8 @@ def checkForErrors(self, window): def _getOpNodes(self, thing: str): hist: list[str] = cmds.listHistory(thing) - rawShapeNodes = cmds.ls(hist, type='blendShape') - rawPoseNodes = cmds.ls(hist, type='blendPose') + rawShapeNodes = cmds.ls(hist, type="blendShape") + rawPoseNodes = cmds.ls(hist, type="blendPose") # Find any simplex ops connected to the history # that have the given name @@ -498,7 +498,7 @@ def _createShapeNode(self, name, mesh) -> str: def _createPoseNode(self, name) -> str: name = "{}_BP".format(name) - return cmds.createNode('blendPose', name=name) + return cmds.createNode("blendPose", name=name) def _createSimplexNode(self, name): op = cmds.createNode("simplex_maya", name=name) @@ -564,7 +564,7 @@ def loadNodes(self, simp, thing, create=True, pBar=None): raise RuntimeError( "Creation turned off and some objects are missing: {}".format( - ', '.join(types) + ", ".join(types) ) ) @@ -620,7 +620,7 @@ def getSliderThing(self, sliderName): @classmethod def buildDummyMesh(cls, name: str): - importHeadShape = cmds.createNode('mesh', name=name + "Shape") + importHeadShape = cmds.createNode("mesh", name=name + "Shape") badPar = cmds.listRelatives(importHeadShape, parent=True)[0] importHead = cmds.rename(badPar, name) return importHead, importHeadShape @@ -656,7 +656,7 @@ def buildRestAbc(cls, abcMesh, name): importHead, importHeadShape = cls.buildDummyMesh("{0}_SIMPLEX".format(name)) importHead = "{0}_SIMPLEX".format(name) - importHeadShape = cmds.createNode('mesh', name=importHead + "Shape") + importHeadShape = cmds.createNode("mesh", name=importHead + "Shape") badPar = cmds.listRelatives(importHeadShape, parent=True)[0] importHead = cmds.rename(badPar, importHead) @@ -671,13 +671,10 @@ def buildRestAbc(cls, abcMesh, name): def vertCount(cls, mesh): return cmds.polyEvaluate(mesh, vertex=True) - @undoable def loadAbcPoses(self, abcMesh, js, pBar=None): pass - - @undoable def loadAbc(self, abcMesh, js, pBar=None): """ diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index 50c36c77..6a9a5b0f 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -164,7 +164,9 @@ def coerceIndexToRoots(indexes): The coerced list """ indexes = [i for i in indexes if i.column() == 0] - indexes = sorted(indexes, key=lambda x: x.model().itemFromIndex(x).classDepth, reverse=True) + indexes = sorted( + indexes, key=lambda x: x.model().itemFromIndex(x).classDepth, reverse=True + ) # Check each item to see if any of it's ancestors # are in the selection list. If not, it's a root roots = [] diff --git a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py index 55b967dd..5e2671d3 100644 --- a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py +++ b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py @@ -243,4 +243,3 @@ def checkFrozen(combo): # Can't use list history to get the chain because it's a pseudo-cycle ret.extend(_getDeformerChain(cc)) return ret - diff --git a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py index daa2dab5..4aa7f178 100644 --- a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py +++ b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py @@ -30,7 +30,7 @@ def registerTool(window, menu): def softSelectToClusterInterface(): sel = cmds.ls(sl=True, objectsOnly=True) if sel: - name = sel[0].split('|')[-1] + name = sel[0].split("|")[-1] softSelectToCluster(sel[0], "{0}_Soft".format(name)) @@ -198,4 +198,3 @@ def softSelectToCluster(tfm, name): cmds.xform(clusterHandle, absolute=True, worldSpace=True, pivots=pos) clusterShape = cmds.listRelatives(clusterHandle, children=True, shapes=True) cmds.setAttr(clusterShape[0] + ".origin", *pos) - diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index af250909..5429b2f7 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -521,7 +521,9 @@ def enableComboRequirements(self): depCheck = self.uiComboDependGRP.isChecked() comboModel.filterRequiresAll = self.uiComboDependAllRDO.isChecked() and depCheck comboModel.filterRequiresAny = self.uiComboDependAnyRDO.isChecked() and depCheck - comboModel.filterRequiresOnly = self.uiComboDependOnlyRDO.isChecked() and depCheck + comboModel.filterRequiresOnly = ( + self.uiComboDependOnlyRDO.isChecked() and depCheck + ) comboModel.invalidateFilter() def populateSliderRequirements(self): @@ -547,8 +549,12 @@ def enableSliderRequirements(self): return depCheck = self.uiSliderDependGRP.isChecked() - sliderModel.filterRequiresAny = self.uiSliderDependAnyRDO.isChecked() and depCheck - sliderModel.filterRequiresAll = self.uiSliderDependAllRDO.isChecked() and depCheck + sliderModel.filterRequiresAny = ( + self.uiSliderDependAnyRDO.isChecked() and depCheck + ) + sliderModel.filterRequiresAll = ( + self.uiSliderDependAllRDO.isChecked() and depCheck + ) sliderModel.invalidateFilter() # Bottom Left Corner Buttons @@ -1123,13 +1129,9 @@ def importSystemFromFile(self): pref = Prefs() defaultPath = str( - pref.restoreProperty( - "systemImport", os.path.join(os.path.expanduser("~")) - ) - ) - path = self._fileDialog( - "Import Template", defaultPath, impTypes, save=False + pref.restoreProperty("systemImport", os.path.join(os.path.expanduser("~"))) ) + path = self._fileDialog("Import Template", defaultPath, impTypes, save=False) if not path: return pref.recordProperty("systemImport", os.path.dirname(path)) @@ -1212,9 +1214,7 @@ def exportSystemTemplate(self): pref = Prefs() defaultPath = str( - pref.restoreProperty( - "systemExport", os.path.join(os.path.expanduser("~")) - ) + pref.restoreProperty("systemExport", os.path.join(os.path.expanduser("~"))) ) path = self._fileDialog( "Export Template", defaultPath, ["smpx", "json"], save=True diff --git a/src/python/simplexui/utils.py b/src/python/simplexui/utils.py index 0eaa98b9..6ce9eb07 100755 --- a/src/python/simplexui/utils.py +++ b/src/python/simplexui/utils.py @@ -291,9 +291,11 @@ def getIcon(iconName): class Prefs(object): """A wrapper for reading/writing prefs both internal and external to blur""" + def __init__(self): if AT_BLUR: import blurdev.prefs + self._pref = blurdev.prefs.find("tools/simplex3") else: self._pref = QSettings("Blur", "Simplex3") @@ -315,4 +317,3 @@ def save(self): self._pref.save() else: self._pref.sync() - From 1305fae6a09762546b12e3ae77069d880455f7d4 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 10 Sep 2025 12:25:34 -0700 Subject: [PATCH 11/25] Fix an error when trying to connect a combo/trav shape that's already connected --- .../simplexui/interface/mayaInterface.py | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 208eaaf1..eb0f9216 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -47,6 +47,7 @@ numpyToImath = None mayaToNumpy = None + # UNDO STACK INTEGRATION @contextmanager def undoContext(inst=None): @@ -2135,6 +2136,14 @@ def _rebuildSliderConnections(self): cmds.connectAttr(thing, self.op + ".sliders[{0}]".format(i)) # Combos + def _getIncomingShapeConnection(self, shape): + """Convenience function to get the transform of the node connected into a shape""" + index = self.getShapeIndex(shape) + tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) + inAttr = "{0}.inputTargetItem[6000].inputGeomTarget".format(tgn) + inCnx = cmds.listConnections(inAttr, destination=False) + return inCnx[0] if inCnx else None + def _reparentDeltaShapes(self, par, nodeDict, bsNode, toDelete=None): """Reparent and clean up a single-transform delta system @@ -2365,10 +2374,24 @@ def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False) if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) mesh = "{0}_Extract".format(attrName) + + if not cmds.objExists(mesh): + return + shapeIdx = trav.prog.getShapeIndex(shape) tVal = trav.prog.pairs[shapeIdx].value - delta = self._createTravDelta(trav, mesh, tVal) - self.connectShape(shape, delta, live, delete) + + inCnx = self._getIncomingShapeConnection(shape) + + # I can't really check if an expected object is connected + # because maya allows multiple objects with the same name + # but I can print a warning if something's already there + if inCnx: + print(f"Traversal already has an incoming shape connection from {inCnx}") + else: + delta = self._createTravDelta(trav, mesh, tVal) + self.connectShape(shape, delta, live, delete) + if delete: cmds.delete(mesh) @@ -2517,10 +2540,23 @@ def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) mesh = "{0}_Extract".format(attrName) - shapeIdx = combo.prog.getShapeIndex(shape) - tVal = combo.prog.pairs[shapeIdx].value - delta = self._createComboDelta(combo, mesh, tVal) - self.connectShape(shape, delta, live, delete) + + if not cmds.objExists(mesh): + return + + progShapeIdx = combo.prog.getShapeIndex(shape) + tVal = combo.prog.pairs[progShapeIdx].value + + inCnx = self._getIncomingShapeConnection(shape) + # I can't really check if an expected object is connected + # because maya allows multiple objects with the same name + # but I can print a warning if something's already there + if inCnx: + print(f"Combo already has an incoming shape connection from {inCnx}") + else: + delta = self._createComboDelta(combo, mesh, tVal) + self.connectShape(shape, delta, live, delete) + if delete: cmds.delete(mesh) From 9b6a3f6347c5ac7acf9a45fccf6977f1f9286782 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 10 Sep 2025 14:47:23 -0700 Subject: [PATCH 12/25] Add tox, run the ruff linter, and fix the lint errors --- pyproject.toml | 38 +-- src/python/simplexui/__init__.py | 2 +- src/python/simplexui/channelBox.py | 282 +++++++++--------- .../simplexui/commands/alembicCommon.py | 6 +- .../simplexui/commands/applyCorrectives.py | 3 +- .../simplexui/commands/correctiveInterface.py | 2 +- .../simplexui/commands/expandedExport.py | 14 +- .../commands/mayaCorrectiveInterface.py | 4 +- src/python/simplexui/commands/poseblendlib.py | 2 +- .../commands/reorderSimplexPoints.py | 3 +- src/python/simplexui/commands/unsubdivide.py | 24 +- src/python/simplexui/commands/uvTransfer.py | 2 +- src/python/simplexui/interface/__init__.py | 2 + .../simplexui/interface/dummyInterface.py | 7 +- .../simplexui/interface/mayaInterface.py | 16 +- src/python/simplexui/interfaceModel.py | 9 +- src/python/simplexui/interfaceModel_Test.py | 255 ---------------- src/python/simplexui/items/__init__.py | 16 + src/python/simplexui/items/combo.py | 13 +- src/python/simplexui/items/group.py | 6 +- src/python/simplexui/items/shape.py | 3 +- src/python/simplexui/items/simplex.py | 11 +- src/python/simplexui/items/slider.py | 7 +- src/python/simplexui/items/traversal.py | 12 +- .../menu/genericPlugins/_builtins.py | 4 +- .../simplexui/menu/mayaPlugins/freezeCombo.py | 2 +- .../menu/mayaPlugins/makeShelfBtn.py | 4 +- .../menu/mayaPlugins/softSelectToCluster.py | 2 +- src/python/simplexui/meson.build | 2 - src/python/simplexui/simplexDialog.py | 2 +- src/python/simplexui/utils.py | 1 + tox.ini | 55 ++++ 32 files changed, 311 insertions(+), 500 deletions(-) delete mode 100644 src/python/simplexui/interfaceModel_Test.py create mode 100644 tox.ini diff --git a/pyproject.toml b/pyproject.toml index 91a4e2d8..513494f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,8 @@ [project] name = "simplex-solver" -dynamic = [ - "version" -] +dynamic = ["version"] -authors = [ - { name="Tyler Fox", email="tyler@blur.com" }, -] +authors = [{ name = "Tyler Fox", email = "tyler@blur.com" }] description = "The Blur Studio Simplex Blendshape Combination System" readme = "README.md" requires-python = ">=3.7" @@ -22,19 +18,23 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", ] -dependencies = [ - "Qt.py", -] +dependencies = ["Qt.py"] [project.urls] "Project Page" = "https://github.com/blurstudio/simplex" +[project.optional-dependencies] +dev = [ + "tox", + "covdefaults", + "coverage", + "ruff", +] + + [build-system] build-backend = "mesonpy" -requires = [ - "meson-python>=0.15.0", - "meson >= 1.6.0", -] +requires = ["meson-python>=0.15.0", "meson >= 1.6.0"] [tool.meson-python.args] setup = ['-Dmaya_build=false', '-Dpython_build=true'] @@ -75,6 +75,7 @@ exclude = [ "shared-venv", "site-packages", "venv", + "subprojects", ] line-length = 88 @@ -82,15 +83,7 @@ indent-width = 4 target-version = "py37" [tool.ruff.lint] -select = [ - "B", - "C", - "E", - "F", - "N", - "W", - "B9", -] +select = ["B", "C", "E", "F", "W", "B9"] ignore = [ "B905", "C901", @@ -123,4 +116,3 @@ skip-magic-trailing-comma = false # Like Black, automatically detect the appropriate line ending. line-ending = "auto" - diff --git a/src/python/simplexui/__init__.py b/src/python/simplexui/__init__.py index eb58a622..99e87ce3 100644 --- a/src/python/simplexui/__init__.py +++ b/src/python/simplexui/__init__.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from _version import __version__ +from _version import __version__ # noqa: F401 SIMPLEX_UI = None SIMPLEX_UI_ROOT = None diff --git a/src/python/simplexui/channelBox.py b/src/python/simplexui/channelBox.py index 5711a86d..e17a528a 100644 --- a/src/python/simplexui/channelBox.py +++ b/src/python/simplexui/channelBox.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -""" The ChannelBox +"""The ChannelBox A Super-minimal ui for interacting with a Simplex System Currently VERY WIP. Probably shouldn't have committed it to master, but whatever """ @@ -384,7 +384,10 @@ def setChannels(self, channels): self.channels = channels self.endResetModel() - def index(self, row, column=0, parIndex=QModelIndex()): + def index(self, row, column=0, parIndex=None): + if parIndex is None: + parIndex = QModelIndex() + try: item = self.channels[row] except IndexError: @@ -630,77 +633,71 @@ def testSliderListDisplay(smpxPath): simp = Simplex.buildSystemFromSmpx(smpxPath) channels = [] - redAttrs = set( - [ - "lowerLipDepressor_X", - "stretcher_X", - "platysmaFlex_X", - "cheekRaiser_X", - "jawOpen", - "lidTightener_X", - "outerBrowRaiser_X", - "eyesClosed_X", - "cornerPuller_X", - "noseWrinkler_X", - "lipsBlow_X", - "cornerDepressor_X", - "funneler", - "browLateral_X", - "innerBrowRaiser_X", - "upperLipRaiser_X", - "chinRaiser", - "cheek_SuckBlow_X", - "pucker", - "eyeGaze_DownUp_X", - "eyeGaze_RightLeft_X", - "upperLidTweak_X", - "lowerLidTweak_X", - ] - ) - greenAttrs = set( - [ - "nasolabialDeepener_X", - "neckStretcher_X", - "lipsPressed_T", - "lipsPressed_B", - "throatCompress", - "lipsRolled_InOut_B", - "lipsRolled_InOut_T", - "sharpCornerPuller_X", - "dimpler_X", - "eyeBlink_X", - "scalpSlide_BackFwd", - "browDown_X", - "mouthSwing_RightLeft", - "sternoFlex_X", - "throatOpen", - ] - ) - blueAttrs = set( - [ - "adamsApple", - "noseSwing_RightLeft", - "nostrilCompress_X", - "jawThrust_BackFwd", - "eyesWide_X", - "lipsVerticalT_X", - "lipsVerticalB_X", - "earPull_X", - "lipsTighten_T", - "lipsTighten_B", - "lipsCompress_T", - "lipsCompress_B", - "lipsShift_RightLeft_B", - "lipsShift_RightLeft_T", - "lipsNarrowT_X", - "lipsNarrowB_X", - "jawSwing_RightLeft", - "nostril_SuckFlare_X", - "lipsCorner_DownUp_X", - "jawClench", - ] - ) - greyAttrs = set(["lipsTogether"]) + redAttrs = { + "lowerLipDepressor_X", + "stretcher_X", + "platysmaFlex_X", + "cheekRaiser_X", + "jawOpen", + "lidTightener_X", + "outerBrowRaiser_X", + "eyesClosed_X", + "cornerPuller_X", + "noseWrinkler_X", + "lipsBlow_X", + "cornerDepressor_X", + "funneler", + "browLateral_X", + "innerBrowRaiser_X", + "upperLipRaiser_X", + "chinRaiser", + "cheek_SuckBlow_X", + "pucker", + "eyeGaze_DownUp_X", + "eyeGaze_RightLeft_X", + "upperLidTweak_X", + "lowerLidTweak_X", + } + greenAttrs = { + "nasolabialDeepener_X", + "neckStretcher_X", + "lipsPressed_T", + "lipsPressed_B", + "throatCompress", + "lipsRolled_InOut_B", + "lipsRolled_InOut_T", + "sharpCornerPuller_X", + "dimpler_X", + "eyeBlink_X", + "scalpSlide_BackFwd", + "browDown_X", + "mouthSwing_RightLeft", + "sternoFlex_X", + "throatOpen", + } + blueAttrs = { + "adamsApple", + "noseSwing_RightLeft", + "nostrilCompress_X", + "jawThrust_BackFwd", + "eyesWide_X", + "lipsVerticalT_X", + "lipsVerticalB_X", + "earPull_X", + "lipsTighten_T", + "lipsTighten_B", + "lipsCompress_T", + "lipsCompress_B", + "lipsShift_RightLeft_B", + "lipsShift_RightLeft_T", + "lipsNarrowT_X", + "lipsNarrowB_X", + "jawSwing_RightLeft", + "nostril_SuckFlare_X", + "lipsCorner_DownUp_X", + "jawClench", + } + greyAttrs = {"lipsTogether"} app = QApplication(sys.argv) tv = ChannelList() @@ -750,77 +747,74 @@ def testSliderTreeDisplay(smpxPath): """ simp = Simplex.buildSystemFromSmpx(smpxPath) - redAttrs = set( - [ - "lowerLipDepressor_X", - "stretcher_X", - "platysmaFlex_X", - "cheekRaiser_X", - "jawOpen", - "lidTightener_X", - "outerBrowRaiser_X", - "eyesClosed_X", - "cornerPuller_X", - "noseWrinkler_X", - "lipsBlow_X", - "cornerDepressor_X", - "funneler", - "browLateral_X", - "innerBrowRaiser_X", - "upperLipRaiser_X", - "chinRaiser", - "cheek_SuckBlow_X", - "pucker", - "eyeGaze_DownUp_X", - "eyeGaze_RightLeft_X", - "upperLidTweak_X", - "lowerLidTweak_X", - ] - ) - greenAttrs = set( - [ - "nasolabialDeepener_X", - "neckStretcher_X", - "lipsPressed_T", - "lipsPressed_B", - "throatCompress", - "lipsRolled_InOut_B", - "lipsRolled_InOut_T", - "sharpCornerPuller_X", - "dimpler_X", - "eyeBlink_X", - "scalpSlide_BackFwd", - "browDown_X", - "mouthSwing_RightLeft", - "sternoFlex_X", - "throatOpen", - ] - ) - blueAttrs = set( - [ - "adamsApple", - "noseSwing_RightLeft", - "nostrilCompress_X", - "jawThrust_BackFwd", - "eyesWide_X", - "lipsVerticalT_X", - "lipsVerticalB_X", - "earPull_X", - "lipsTighten_T", - "lipsTighten_B", - "lipsCompress_T", - "lipsCompress_B", - "lipsShift_RightLeft_B", - "lipsShift_RightLeft_T", - "lipsNarrowT_X", - "lipsNarrowB_X", - "jawSwing_RightLeft", - "nostril_SuckFlare_X", - "lipsCorner_DownUp_X", - "jawClench", - ] - ) - greyAttrs = set(["lipsTogether"]) + _redAttrs = { + "lowerLipDepressor_X", + "stretcher_X", + "platysmaFlex_X", + "cheekRaiser_X", + "jawOpen", + "lidTightener_X", + "outerBrowRaiser_X", + "eyesClosed_X", + "cornerPuller_X", + "noseWrinkler_X", + "lipsBlow_X", + "cornerDepressor_X", + "funneler", + "browLateral_X", + "innerBrowRaiser_X", + "upperLipRaiser_X", + "chinRaiser", + "cheek_SuckBlow_X", + "pucker", + "eyeGaze_DownUp_X", + "eyeGaze_RightLeft_X", + "upperLidTweak_X", + "lowerLidTweak_X", + } + + _greenAttrs = { + "nasolabialDeepener_X", + "neckStretcher_X", + "lipsPressed_T", + "lipsPressed_B", + "throatCompress", + "lipsRolled_InOut_B", + "lipsRolled_InOut_T", + "sharpCornerPuller_X", + "dimpler_X", + "eyeBlink_X", + "scalpSlide_BackFwd", + "browDown_X", + "mouthSwing_RightLeft", + "sternoFlex_X", + "throatOpen", + } + + _blueAttrs = { + "adamsApple", + "noseSwing_RightLeft", + "nostrilCompress_X", + "jawThrust_BackFwd", + "eyesWide_X", + "lipsVerticalT_X", + "lipsVerticalB_X", + "earPull_X", + "lipsTighten_T", + "lipsTighten_B", + "lipsCompress_T", + "lipsCompress_B", + "lipsShift_RightLeft_B", + "lipsShift_RightLeft_T", + "lipsNarrowT_X", + "lipsNarrowB_X", + "jawSwing_RightLeft", + "nostril_SuckFlare_X", + "lipsCorner_DownUp_X", + "jawClench", + } + + _greyAttrs = {"lipsTogether"} app = QApplication(sys.argv) # tv = ChannelTree() diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 70046a49..30302e54 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -43,7 +43,7 @@ imathToNumpy = None -def pbPrint(pBar, message=None, val=None, maxVal=None, _pbPrintLastComma=[]): +def pbPrint(pBar, message=None, val=None, maxVal=None, _pbPrintLastComma=None): """A function that handles displaying messages in a QProgressDialog or printing to stdout Don't forget to call QApplication.processEvents() after using this function @@ -61,6 +61,8 @@ def pbPrint(pBar, message=None, val=None, maxVal=None, _pbPrintLastComma=[]): _pbPrintLastComma: object INTERNAL USE ONLY """ + _pbPrintLastComma = [] if _pbPrintLastComma is None else _pbPrintLastComma + if pBar is not None: if val is not None: pBar.setValue(val) @@ -910,7 +912,7 @@ def buildSmpx( name=name, shapeSuffix="", transformSuffix="", - propDict=dict(simplex=jsString), + propDict={"simplex": jsString}, ogawa=ogawa, pBar=pBar, ) diff --git a/src/python/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py index 4831737f..226d408f 100644 --- a/src/python/simplexui/commands/applyCorrectives.py +++ b/src/python/simplexui/commands/applyCorrectives.py @@ -217,7 +217,7 @@ def buildFullShapes(simplex, shapeObjs, shapes, solver, pBar=None): indexBySlider = {s: i for i, s in enumerate(simplex.sliders)} indexByShape = {s: i for i, s in enumerate(simplex.shapes)} floaters = set(simplex.getFloatingShapes()) - floatIdxs = set([indexByShape[s] for s in floaters]) + floatIdxs = {indexByShape[s] for s in floaters} shapeDict = {} for item in itertools.chain(simplex.sliders, simplex.combos): @@ -303,7 +303,6 @@ def collapseFullShapes(simplex, allPts, ptsByShape, vecByShape, pBar=None): QApplication.processEvents() for shpOrderIdx, shape in enumerate(shapeOrder): - if pBar is not None: pBar.setValue(shpOrderIdx) pBar.setLabelText("Building Corrected Deltas\n{}".format(shape.name)) diff --git a/src/python/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py index 3c94b8c6..2470ee2e 100644 --- a/src/python/simplexui/commands/correctiveInterface.py +++ b/src/python/simplexui/commands/correctiveInterface.py @@ -251,7 +251,7 @@ def outputCorrectiveReferences( if pBar is not None: pBar.setLabelText("Writing Names") QApplication.processEvents() - nameWrite = ["{};{}".format(s.name, r) for s, r, in zip(shapes, refIdxs)] + nameWrite = ["{};{}".format(s.name, r) for s, r in zip(shapes, refIdxs)] with open(outNames, "w") as f: f.write("\n".join(nameWrite)) diff --git a/src/python/simplexui/commands/expandedExport.py b/src/python/simplexui/commands/expandedExport.py index 5ba74e93..1f7411e8 100644 --- a/src/python/simplexui/commands/expandedExport.py +++ b/src/python/simplexui/commands/expandedExport.py @@ -18,13 +18,13 @@ from pysimplex import PySimplex from ..interface.mayaInterface import DCC, disconnected -from ..items import Combo, Slider, Traversal +from ..items import Combo, Slider, Traversal, Simplex from .alembicCommon import buildSmpx try: import numpy as np except ImportError: - pass + np = None def _setSliders(ctrl, val, svs): @@ -291,6 +291,9 @@ def parseExpandedData(smpx, restShape, sliderShapes, comboShapes, travShapes): The point positions for a new set of shapes """ + if np is None: + raise RuntimeError("Numpy is not available") + solver = PySimplex(smpx.dump()) shapeArray = np.zeros((len(smpx.shapes), len(restShape), 3)) @@ -298,9 +301,9 @@ def parseExpandedData(smpx, restShape, sliderShapes, comboShapes, travShapes): indexByShape = {s: i for i, s in enumerate(smpx.shapes)} floatShapeSet = set(smpx.getFloatingShapes()) - floatIdxs = sorted(set([indexByShape[s] for s in floatShapeSet])) - travShapeSet = set([pp.shape for t in smpx.traversals for pp in t.prog.pairs]) - travIdxs = sorted(set([indexByShape[s] for s in travShapeSet])) + floatIdxs = sorted({indexByShape[s] for s in floatShapeSet}) + travShapeSet = {pp.shape for t in smpx.traversals for pp in t.prog.pairs} + travIdxs = sorted({indexByShape[s] for s in travShapeSet}) # Sliders are simple, just set their shapes directly for ppDict in sliderShapes.values(): @@ -426,6 +429,7 @@ def expandedExportAbc(path, mesh, master, clients=()): if __name__ == "__main__": # get the smpx from the UI + master = Simplex.buildSystemFromMesh("Face_SIMPLEX", "Face") client = Simplex.buildSystemFromMesh("Face_SIMPLEX2", "Face2") outPath = r"D:\Users\tyler\Desktop\TEST\expanded.smpx" diff --git a/src/python/simplexui/commands/mayaCorrectiveInterface.py b/src/python/simplexui/commands/mayaCorrectiveInterface.py index 438160c9..f4258139 100644 --- a/src/python/simplexui/commands/mayaCorrectiveInterface.py +++ b/src/python/simplexui/commands/mayaCorrectiveInterface.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -""" Get the corrective deltas from a rig in Maya """ +"""Get the corrective deltas from a rig in Maya""" from ctypes import c_float @@ -50,7 +50,7 @@ def resetPose(pvp): pvp : [(str, float), ...] A list of property/value pairs """ - for prop, val in pvp: + for prop, _val in pvp: cmds.setAttr(prop, 0) diff --git a/src/python/simplexui/commands/poseblendlib.py b/src/python/simplexui/commands/poseblendlib.py index 3f005fe4..c23b9263 100644 --- a/src/python/simplexui/commands/poseblendlib.py +++ b/src/python/simplexui/commands/poseblendlib.py @@ -4,7 +4,7 @@ # with ndim arrays of quaternions. -def positve_scalar(q: np.ndarray) -> np.ndarray: +def positive_scalar(q: np.ndarray) -> np.ndarray: """Ensure the scalar value of an array of quaternions is positive""" shape = q.shape q = q.reshape((-1, 4)) diff --git a/src/python/simplexui/commands/reorderSimplexPoints.py b/src/python/simplexui/commands/reorderSimplexPoints.py index 0f16af33..82ca7eef 100644 --- a/src/python/simplexui/commands/reorderSimplexPoints.py +++ b/src/python/simplexui/commands/reorderSimplexPoints.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -""" Transfer shapes between mismatched models +"""Transfer shapes between mismatched models Given a 1:1 point correspondence, transfer the shapes from one geometry to another. Figuring out the correspondence is currently @@ -25,6 +25,7 @@ will be used as a numpy index to get the output values. It's also possible to invert the range if you think you've got it backwards """ + # pylint:disable=wrong-import-position import json diff --git a/src/python/simplexui/commands/unsubdivide.py b/src/python/simplexui/commands/unsubdivide.py index 38fa13dc..26178ae7 100644 --- a/src/python/simplexui/commands/unsubdivide.py +++ b/src/python/simplexui/commands/unsubdivide.py @@ -59,11 +59,11 @@ def mergeCycles(groups): heads = {g[0]: g for g in groups} tails = {g[-1]: g for g in groups} - headGetter = lambda x: heads.get(x[-1]) - headSetter = lambda x, y: x + y[1:] + headGetter = lambda x: heads.get(x[-1]) # noqa: E731 + headSetter = lambda x, y: x + y[1:] # noqa: E731 - tailGetter = lambda x: tails.get(x[0]) - tailSetter = lambda x, y: y + x[1:] + tailGetter = lambda x: tails.get(x[0]) # noqa: E731 + tailSetter = lambda x, y: y + x[1:] # noqa: E731 searches = ((headGetter, headSetter), (tailGetter, tailSetter)) @@ -193,7 +193,7 @@ def partitionIslands(faces, neigh, pBar=None): QApplication.processEvents() while allVerts: - verts = set([allVerts.pop()]) + verts = {allVerts.pop()} exclude = set() while verts: verts, exclude = grow(neigh, verts, exclude) @@ -457,7 +457,7 @@ def buildLayeredNeighborDicts(faces, uFaces, dWings): borders >= uBorders ), "Somehow the unsubdivided borders contain different vIdxs" - for i, (k, uNeigh) in enumerate(uNeighDict.items()): + for k, uNeigh in uNeighDict.items(): neighDict[k] = _align(neighDict[k], uNeigh, dWings) return neighDict, uNeighDict, edgeDict, uEdgeDict, borders @@ -596,13 +596,13 @@ def _findOldPosition3Valence( for x, v in enumerate(fNeigh): # working with neigh, but should only ever contain uNeigh indexes eTest = edgeDict[vIdx] - origFace = set([n for n in neighDict[v][0] if n not in eTest]) + origFace = {n for n in neighDict[v][0] if n not in eTest} - check = (origFace - set(ueNeigh)) - set([vIdx]) + check = (origFace - set(ueNeigh)) - {vIdx} if computed >= check: fCtrIdx = v fnIdx = x - fik = sorted(list(check)) + fik = sorted(check) break if fnIdx is None: @@ -779,7 +779,7 @@ def fixVerts( An array of vertex positions """ uVerts = verts.copy() - uIdxs = sorted(list(set([i for i in chain.from_iterable(uFaces)]))) + uIdxs = sorted(set(chain.from_iterable(uFaces))) v3Idxs = [] # bowtie verts are pinned @@ -933,13 +933,13 @@ def collapse(faces, verts, uvFaces, uvs): : np.array The new N*2 array of uvs """ - vset = sorted(list(set(chain.from_iterable(faces)))) + vset = sorted(set(chain.from_iterable(faces))) nVerts = verts[vset] vDict = {v: i for i, v in enumerate(vset)} nFaces = [[vDict[f] for f in face] for face in faces] if uvFaces is not None: - uvset = sorted(list(set(chain.from_iterable(uvFaces)))) + uvset = sorted(set(chain.from_iterable(uvFaces))) nUVs = uvs[uvset] uvDict = {v: i for i, v in enumerate(uvset)} nUVFaces = [[uvDict[f] for f in face] for face in uvFaces] diff --git a/src/python/simplexui/commands/uvTransfer.py b/src/python/simplexui/commands/uvTransfer.py index ff292448..ff69dfbd 100644 --- a/src/python/simplexui/commands/uvTransfer.py +++ b/src/python/simplexui/commands/uvTransfer.py @@ -549,7 +549,7 @@ def triangulateUVs(faces, uvs): negArea = np.where(signedAreas < 0) # Do we need to retriangulate any of the polys - retri = sorted(set(triMap[i] for i in negArea[0])) + retri = sorted({triMap[i] for i in negArea[0]}) if retri: tris = tris.tolist() tmpFaceMap = {} diff --git a/src/python/simplexui/interface/__init__.py b/src/python/simplexui/interface/__init__.py index ba056844..d577438f 100644 --- a/src/python/simplexui/interface/__init__.py +++ b/src/python/simplexui/interface/__init__.py @@ -24,3 +24,5 @@ from .mayaInterface import DCC, DISPATCH, rootWindow, undoContext else: from .dummyInterface import DCC, DISPATCH, rootWindow, undoContext + +__all__ = ["DCC", "DISPATCH", "rootWindow", "undoContext"] diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index c8ea4c82..86ba4c9c 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -16,7 +16,8 @@ # along with Simplex. If not, see . # pylint: disable=invalid-name, unused-argument -""" A placeholder interface that takes arguments and does nothing with them """ +"""A placeholder interface that takes arguments and does nothing with them""" + import copy from contextlib import contextmanager from functools import wraps @@ -487,7 +488,7 @@ def getAllShapeVertices(self, shapes, pBar=None): An optional progress dialog (Default value = None) """ - for i, shape in enumerate(shapes): + for shape in shapes: verts = self.getShapeVertices(shape) shape.verts = verts @@ -680,7 +681,7 @@ def renameSystem(self, name): """ # TODO - oldName = self.name + # oldName = self.name self.name = name # for dd in (DB.nodes, DB.ops, DB.bss, DB.meshes): # oo = dd.get(oldName) diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index eb0f9216..2109af3c 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -799,7 +799,7 @@ def getAllShapeVertices(self, shapes, pBar=None): thing = om.MDagPath() sl.getDagPath(0, thing) meshFn = om.MFnMesh(thing) - ptCount = meshFn.numVertices() + _ptCount = meshFn.numVertices() with disconnected(self.shapeNode) as cnx: shapeCnx = cnx[self.shapeNode] for v in shapeCnx.values(): @@ -1467,11 +1467,11 @@ def extractWithDeltaShape(self, shape, live=True, offset=10.0): cmds.setAttr("{0}.{1}".format(bs, extracted), 1.0) # Cleanup - nodeDict = dict(Delta=delta, Init=init) + nodeDict = {"Delta": delta, "Init": init} repDict = self._reparentDeltaShapes(extracted, nodeDict, bs) # Shift the extracted shape to the side - cmds.xform(extracted, relative=True, translation=[offset, 0, 0]) + cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) if live: self.connectShape(shape, extracted, live, delete=False) @@ -1536,7 +1536,7 @@ def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0 cmds.aliasAttr(delta, "{0}.{1}".format(bs, deltaPar)) # Cleanup - nodeDict = dict(Init=init) + nodeDict = {"Init": init} self._reparentDeltaShapes(extracted, nodeDict, bs) # Remove the tweak node, otherwise editing the input progressives @@ -1549,7 +1549,7 @@ def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0 cmds.delete(tweak) # Shift the extracted shape to the side - cmds.xform(extracted, relative=True, translation=[offset, 0, 0]) + cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) if live: self.connectShape(shape, extracted, live, delete=False) @@ -1842,7 +1842,7 @@ def getFalloffThing(self, falloff): ------- """ - shape = [i for i in cmds.listRelatives(self.mesh, shapes=True)][0] + shape = cmds.listRelatives(self.mesh, shapes=True)[0] return shape + "." + falloff.name # Sliders @@ -2288,7 +2288,7 @@ def _createTravDelta(self, trav, target, tVal, doReparent=True): # Cleanup if doReparent: - nodeDict = dict(Delta=deltaObj) + nodeDict = {"Delta": deltaObj} repDict = self._reparentDeltaShapes(target, nodeDict, bs, [rest, base]) return repDict["Delta"] return deltaObj @@ -2466,7 +2466,7 @@ def _createComboDelta(self, combo, target, tVal, doReparent=True): # Cleanup if doReparent: - nodeDict = dict(Delta=deltaObj) + nodeDict = {"Delta": deltaObj} repDict = self._reparentDeltaShapes(target, nodeDict, bs, [rest, base]) return repDict["Delta"] return deltaObj diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index 6a9a5b0f..fd4aaaae 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -764,7 +764,8 @@ def getItemRow(self, item): def getItemAppendRow(self, item): return len(self.simplex.sliderGroups) + 1 - def index(self, row, column=0, parIndex=QModelIndex()): + def index(self, row, column=0, parIndex=None): + parIndex = QModelIndex() if parIndex is None else parIndex if row <= 0: return self.createIndex(row, column, None) try: @@ -852,7 +853,8 @@ def getItemAppendRow(self, item): except AttributeError: return 0 - def index(self, row, column=0, parIndex=QModelIndex()): + def index(self, row, column=0, parIndex=None): + parIndex = QModelIndex() if parIndex is None else parIndex if row <= 0: return self.createIndex(row, column, None) try: @@ -939,7 +941,8 @@ def getItemAppendRow(self, item): except AttributeError: return 0 - def index(self, row, column=0, parIndex=QModelIndex()): + def index(self, row, column=0, parIndex=None): + parIndex = QModelIndex() if parIndex is None else parIndex if row < 0: return QModelIndex() try: diff --git a/src/python/simplexui/interfaceModel_Test.py b/src/python/simplexui/interfaceModel_Test.py deleted file mode 100644 index 0d684cba..00000000 --- a/src/python/simplexui/interfaceModel_Test.py +++ /dev/null @@ -1,255 +0,0 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -import os -import sys - -# Add the parent folder to the path so I can import SimplexUI -# Means I can run this test code from inside the module and -# keep everything together -base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, base) - -from .interfaceModel import ( - ComboFilterModel, - ComboModel, - Simplex, - SimplexModel, - Slider, - SliderFilterModel, - SliderModel, - TraversalFilterModel, - TraversalModel, -) -from Qt.QtCore import QModelIndex -from Qt.QtWidgets import QApplication, QPushButton, QTreeView, QVBoxLayout, QWidget - - -# HELPERS -def expandRecursive(view, model, index=QModelIndex(), depth=0, debug=False): - """helper to expand the whole tree""" - view.setExpanded(index, True) - rows = model.rowCount(index) - - if debug: - item = model.itemFromIndex(index) - print( - " " * depth - + "Name {0}, children {1}".format(item.name if item else None, rows) - ) - print( - " " * depth - + "Depth {0}, Row {1}, Col {2}".format(depth, index.row(), index.column()) - ) - - for row in range(rows): - child = model.index(row, 0, index) - if not child.isValid(): - continue - expandRecursive(view, model, child, depth + 1) - - -def showTree(model): - app = QApplication(sys.argv) - tv = QTreeView() - tv.setModel(model) - expandRecursive(tv, model) - tv.resizeColumnToContents(0) - tv.show() - sys.exit(app.exec_()) - - -def buildDummySystem(path, name="Face"): - if path.endswith(".json"): - simp = Simplex.buildSystemFromFile(path) - elif path.endswith(".smpx"): - simp = Simplex.buildSystemFromFile(path) - else: - raise IOError("Filepath is not .smpx or .json") - return simp - - -# DISPLAY TESTS -def testSliderDisplay(smpxPath, applyFilter=True): - simp = Simplex.buildSystemFromFile(smpxPath) - model = SimplexModel(simp, None) - model = SliderModel(model) - if applyFilter: - model = SliderFilterModel(model) - showTree(model) - - -def testComboDisplay(smpxPath, applyFilter=True): - simp = Simplex.buildSystemFromFile(smpxPath) - model = SimplexModel(simp, None) - model = ComboModel(model) - if applyFilter: - model = ComboFilterModel(model) - showTree(model) - - -def testTraversalDisplay(path, applyFilter=True): - simp = buildDummySystem(path) - - model = SimplexModel(simp, None) - model = TraversalModel(model) - if applyFilter: - model = TraversalFilterModel(model) - showTree(model) - - -def testBaseDisplay(path): - simp = buildDummySystem(path) - - model = SimplexModel(simp, None) - showTree(model) - - -def testEmptySimplex(): - simp = Simplex.buildEmptySystem(None, "Face") - model = SimplexModel(simp, None) - model = SliderModel(model) - showTree(model) - - -# RowAdd Tests -def testNewSlider(): - simp = Simplex.buildEmptySystem(None, "Face") - model = SimplexModel(simp, None) - smodel = SliderModel(model) - fmodel = SliderFilterModel(smodel) - fmodel.doFilter = True - - app = QApplication(sys.argv) - - topWid = QWidget() - lay = QVBoxLayout(topWid) - - tv = QTreeView(topWid) - btn = QPushButton("NEW", topWid) - lay.addWidget(tv) - lay.addWidget(btn) - - tv.setModel(fmodel) - expandRecursive(tv, fmodel) - topWid.show() - - def newSlider(): - return Slider.createSlider("NewSlider", simp) - - btn.clicked.connect(newSlider) - - sys.exit(app.exec_()) - - -def testDeleteBase(path): - simp = buildDummySystem(path) - model = SimplexModel(simp, None) - - # model = SliderModel(model) - # model = SliderFilterModel(model) - - model = ComboModel(model) - # model = ComboFilterModel(model) - - app = QApplication(sys.argv) - - topWid = QWidget() - lay = QVBoxLayout(topWid) - - tv = QTreeView(topWid) - - btn = QPushButton("DELETE", topWid) - lay.addWidget(tv) - lay.addWidget(btn) - - tv.setModel(model) - topWid.show() - - expandRecursive(tv, model) - tv.resizeColumnToContents(0) - - def delCallback(): - sel = tv.selectedIndexes() - sel = [i for i in sel if i.column() == 0] - items = [s.model().itemFromIndex(s) for s in sel] - item = items[0] - print("Deleting", type(item), item.name) - item.delete() - tv.model().invalidateFilter() - - btn.clicked.connect(delCallback) - - sys.exit(app.exec_()) - - -def testNewChild(path): - simp = buildDummySystem(path) - model = SimplexModel(simp, None) - model = SliderModel(model) - model = SliderFilterModel(model) - - app = QApplication(sys.argv) - - topWid = QWidget() - lay = QVBoxLayout(topWid) - - tv = QTreeView(topWid) - - btn = QPushButton("NEW", topWid) - lay.addWidget(tv) - lay.addWidget(btn) - - tv.setModel(model) - topWid.show() - - expandRecursive(tv, model) - tv.resizeColumnToContents(0) - - def newCallback(): - sel = tv.selectedIndexes() - sel = [i for i in sel if i.column() == 0] - items = [s.model().itemFromIndex(s) for s in sel] - # item = items[0] - - # TODO - # find the child type of item - # make a new one of those - - # tv.model().invalidateFilter() - - btn.clicked.connect(newCallback) - - sys.exit(app.exec_()) - - -if __name__ == "__main__": - # basePath = r'D:\Users\tyler\Documents\GitHub\Simplex\scripts\SimplexUI\build' - basePath = r"D:\Users\tyler\Documents\GitHub\Simplex\Useful" - # path = os.path.join(basePath, 'male_Simplex_v005_Split.smpx') - # path = os.path.join(basePath, 'sphere_abcd_50.smpx') - # path = os.path.join(basePath, 'male_traversal3.json') - path = os.path.join(basePath, "SquareTest_Floater.json") - - # Only works for one at a time - # testEmptySimplex() - # testBaseDisplay(path) - # testSliderDisplay(path, applyFilter=True) - # testComboDisplay(path, applyFilter=True) - # testTraversalDisplay(path, applyFilter=True) - # testNewSlider() - testDeleteBase(path) diff --git a/src/python/simplexui/items/__init__.py b/src/python/simplexui/items/__init__.py index 35f87532..0284cd84 100644 --- a/src/python/simplexui/items/__init__.py +++ b/src/python/simplexui/items/__init__.py @@ -24,3 +24,19 @@ from .slider import Slider from .stack import Stack, stackable from .traversal import Traversal, TravPair + +__all__ = [ + "Combo", + "ComboPair", + "Falloff", + "Group", + "ProgPair", + "Progression", + "Shape", + "Simplex", + "Slider", + "Stack", + "stackable", + "Traversal", + "TravPair", +] diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index ee33862b..4a040853 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -157,12 +157,12 @@ class Combo(SimplexAccessor): ) _freezeIcon = None - def __init__( - self, name, simplex, pairs, prog, group, solveType, color=QColor(128, 128, 128) - ): + def __init__(self, name, simplex, pairs, prog, group, solveType, color=None): super(Combo, self).__init__(simplex) + color = QColor(128, 128, 128) if color is None else color + with self.stack.store(self): - if group.groupType != type(self): + if group.groupType is not type(self): raise ValueError("Cannot add this slider to a combo group") self._name = name self.pairs = pairs @@ -177,7 +177,6 @@ def __init__( mgrs = [model.insertItemManager(group) for model in self.models] with nested(*mgrs): - self.group = group for p in self.pairs: p.combo = self @@ -242,9 +241,9 @@ def comboAlreadyExists(cls, simplex, sliders, values): The combo that exists with the given values, or None if none exist """ - checker = set([(s.name, v) for s, v in zip(sliders, values)]) + checker = {(s.name, v) for s, v in zip(sliders, values)} for combo in simplex.combos: - tester = set([(p.slider.name, p.value) for p in combo.pairs]) + tester = {(p.slider.name, p.value) for p in combo.pairs} if checker == tester: return combo return None diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index bcef2c23..f6ba556e 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -48,12 +48,14 @@ class Group(SimplexAccessor): classDepth = 1 - def __init__(self, name, simplex, groupType, color=QColor(128, 128, 128)): + def __init__(self, name, simplex, groupType, color=None): super(Group, self).__init__(simplex) from .combo import Combo from .slider import Slider from .traversal import Traversal + color = QColor(128, 128, 128) if color is None else color + with self.stack.store(self): self._name = name self.items = [] @@ -284,7 +286,7 @@ def take(self, things): if self.groupType is None: self.groupType = type(things[0]) - if not all([isinstance(i, self.groupType) for i in things]): + if not all(isinstance(i, self.groupType) for i in things): raise ValueError( "All items in this group must be of type: {}".format(self.groupType) ) diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index 5515ad3b..421f4473 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -52,8 +52,9 @@ class Shape(SimplexAccessor): classDepth = 10 - def __init__(self, name, simplex, create=True, color=QColor(128, 128, 128)): + def __init__(self, name, simplex, create=True, color=None): super(Shape, self).__init__(simplex) + color = QColor(128, 128, 128) if color is None else color with self.stack.store(self): self._thing = None self._verts = None diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 9fdc614e..0b3878db 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -134,7 +134,7 @@ def __deepcopy__(self, memo): # do not connect the deepcopied simplex to the DCC # we will want to change it without affecting the current scene # Requires the name be copied already - setattr(result, "_name", copy.deepcopy(self._name, memo)) + setattr(result, "_name", copy.deepcopy(self._name, memo)) # noqa: B010 if v.program == "dummy": # If it's already a dummy, go ahead and just deepcopy setattr(result, k, copy.deepcopy(v, memo)) @@ -650,9 +650,9 @@ def comboExists(self, sliders, values): The Combo with those sliders and values, or None if none found """ - checkSet = set([(s.name, v) for s, v in zip(sliders, values)]) + checkSet = {(s.name, v) for s, v in zip(sliders, values)} for cmb in self.combos: - cmbSet = set([(p.slider.name, p.value) for p in cmb.pairs]) + cmbSet = {(p.slider.name, p.value) for p in cmb.pairs} if checkSet == cmbSet: return cmb return None @@ -1419,7 +1419,7 @@ def buildInputVectors( continue if depthCutoff is not None and len(combo.pairs) > depthCutoff: continue - if ignoreSliders & set([i.name for i in combo.getSliders()]): + if ignoreSliders & {i.name for i in combo.getSliders()}: # if the combo's sliders are in ignoreSliders continue @@ -1653,7 +1653,6 @@ def split(self, pBar=None): # Meaning that splittable progs only contain splittable shapes. And splittable progs # are only controlled by splittable controllers for fo in self.falloffs: - controllers = self.sliders + self.combos + self.traversals for ctrl in controllers: prog = ctrl.prog @@ -1662,7 +1661,7 @@ def split(self, pBar=None): cSplit = fo.canRename(ctrl) sSplit = [s for s in prog.getShapes() if not s.isRest] sSplit = [fo.canRename(shape) for shape in sSplit] - sSplitSame = all([i == sSplit[0] for i in sSplit]) + sSplitSame = all(i == sSplit[0] for i in sSplit) sSplit = sSplit[0] if not sSplitSame: shapes = [i.name for i in prog.getShapes()] diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index d18a10dd..f49bf626 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -53,13 +53,12 @@ class Slider(SimplexAccessor): classDepth = 7 - def __init__( - self, name, simplex, prog, group, color=QColor(128, 128, 128), create=True - ): - if group.groupType != type(self): + def __init__(self, name, simplex, prog, group, color=None, create=True): + if group.groupType is not type(self): raise ValueError("Cannot add this slider to a combo group") super(Slider, self).__init__(simplex) + color = QColor(128, 128, 128) if color is None else color with self.stack.store(self): self._name = name self._thing = None diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index cc7db56e..40a56069 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -120,7 +120,7 @@ def delete(self): @staticmethod def removeAll(pairs): """ """ - travs = list(set([p.travPoint.traversal for p in pairs])) + travs = list({p.travPoint.traversal for p in pairs}) for trav in travs: trav.removePairs(pairs) @@ -370,12 +370,12 @@ def __init__( endPoint, prog, group, - color=QColor(128, 128, 128), + color=None, ): - super(Traversal, self).__init__(simplex) + color = QColor(128, 128, 128) if color is None else color with self.stack.store(self): - if group.groupType != type(self): + if group.groupType is not type(self): raise ValueError( "Cannot add this Traversal to a group of a different type" ) @@ -732,7 +732,7 @@ def loadV2(cls, simplex, progs, data): for cp in cmb.pairs: rangeDict[cp.slider] = (cp.value, cp.value) - ssli = sorted(list(rangeDict.items()), key=lambda x: x[0].name) + ssli = sorted((rangeDict.items()), key=lambda x: x[0].name) startPairs, endPairs = [], [] for slider, (startVal, endVal) in ssli: startPairs.append(TravPair(slider, startVal)) @@ -894,7 +894,7 @@ def removePairs(self, pairs): pairs = sPairs + ePairs # Get all the pairs that use the selected sliders - sliders = set([p.slider for p in pairs]) + sliders = {p.slider for p in pairs} sPairs = [i for i in self.startPoint.pairs if i.slider in sliders] ePairs = [i for i in self.endPoint.pairs if i.slider in sliders] diff --git a/src/python/simplexui/menu/genericPlugins/_builtins.py b/src/python/simplexui/menu/genericPlugins/_builtins.py index 020b6b80..ac2598f9 100644 --- a/src/python/simplexui/menu/genericPlugins/_builtins.py +++ b/src/python/simplexui/menu/genericPlugins/_builtins.py @@ -66,7 +66,7 @@ def registerSliderTree(window, clickIdx, indexes, menu): gAct.triggered.connect(partial(self.setSelectedSliderGroups, group)) setFalloffMenu = menu.addMenu("Set Falloffs") - sliders = list(set([i for i in items if isinstance(i, Slider)])) + sliders = list({i for i in items if isinstance(i, Slider)}) foChecks = {} interps = set() @@ -207,7 +207,7 @@ def registerComboTree(window, clickIdx, indexes, menu): setSolveMenu = menu.addMenu("Set Solve Type") solves = set() - combos = list(set([i for i in items if isinstance(i, Combo)])) + combos = list({i for i in items if isinstance(i, Combo)}) for combo in combos: solves.add(str(combo.solveType)) diff --git a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py index 5e2671d3..90652d57 100644 --- a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py +++ b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py @@ -73,7 +73,7 @@ def freezeCombo(combo): # disconnect the controller from the operator with disconnected(simplex.DCC.op) as sliderCnx: - for shapeIdx, pp in enumerate(combo.prog.pairs): + for _shapeIdx, pp in enumerate(combo.prog.pairs): tVal = pp.value freezeShape = pp.shape if freezeShape.isRest: diff --git a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py index c22f0236..b682376f 100644 --- a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py +++ b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py @@ -51,9 +51,7 @@ simplexui.runSimplexUI() sys.path.pop(0) -""".format( - dn(dn(dn(dn(__file__)))) -) +""".format(dn(dn(dn(dn(__file__))))) def registerTool(window, menu): diff --git a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py index 4aa7f178..c3ff2d2a 100644 --- a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py +++ b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py @@ -145,7 +145,7 @@ def getSoftSelectionValues(myNode, returnSimpleIndices=True): elif ctyp == om.MFn.kLatticeComponent: div_s = cmds.getAttr(depNode_name + ".sDivisions") div_t = cmds.getAttr(depNode_name + ".tDivisions") - div_u = cmds.getAttr(depNode_name + ".uDivisions") + # div_u = cmds.getAttr(depNode_name + ".uDivisions") tripleFn = om.MFnTripleIndexedComponent(component) for i in range(count): diff --git a/src/python/simplexui/meson.build b/src/python/simplexui/meson.build index da39dd2f..4c7fd071 100644 --- a/src/python/simplexui/meson.build +++ b/src/python/simplexui/meson.build @@ -7,7 +7,6 @@ py.install_sources( 'falloffDialog.py', 'interfaceModel.py', 'interfaceModelTrees.py', - 'interfaceModel_Test.py', 'main.pyw', 'simplexDialog.py', 'travCheckDialog.py', @@ -15,7 +14,6 @@ py.install_sources( 'utils.py', 'commands/__init__.py', 'commands/alembicCommon.py', - 'commands/alembicSimplex.py', 'commands/applyCorrectives.py', 'commands/correctiveInterface.py', 'commands/expandedExport.py', diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 5429b2f7..5ac3594f 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -598,7 +598,7 @@ def autoSetSliders(self): self.simplex.setSlidersWeights(sliders, weights) self.uiSliderTREE.repaint() - def _getAName(self, tpe, default=None, taken=tuple(), uniqueAccept=False): + def _getAName(self, tpe, default=None, taken=(), uniqueAccept=False): """uniqueAccept forces the user to provide and accept a unique name If the user enters a non-unique name, the name is uniquified, and the dialog is re-shown with the unique name as the default suggestion diff --git a/src/python/simplexui/utils.py b/src/python/simplexui/utils.py index 6ce9eb07..8cae2ab8 100755 --- a/src/python/simplexui/utils.py +++ b/src/python/simplexui/utils.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . """Utility functions.""" + import os import re import sys diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..30d85ba4 --- /dev/null +++ b/tox.ini @@ -0,0 +1,55 @@ +[tox] +envlist = + compat-py37 + qtcompat-py37 + format + lint + +skip_missing_interpreters = True +requires = tox>4.0 + +[testenv] +package = editable +usedevelop = True +changedir = {toxinidir} +passenv = USERNAME +deps = + pip-tools + Qt.py==1.4.7 + +[testenv:compat-py37] +deps = +commands = + python -m compileall -f -q -x "\.tox|shared-venv|subprojects" . + +[testenv:.qtcompat] +deps = + numpy==1.21.6 + PySide2 + +[testenv:qtcompat-py37] +deps = {[testenv:.qtcompat]deps} +commands = + python {envsitepackagesdir}/Qt_convert_enum.py . --check + python {envsitepackagesdir}/Qt_convert_enum.py . --partial + +[testenv:qtcompat-py37-fix] +deps = {[testenv:.qtcompat]deps} +commands = + python {envsitepackagesdir}/Qt_convert_enum.py . --check --write + python {envsitepackagesdir}/Qt_convert_enum.py . --partial + +[testenv:setup] +deps = + build +commands = + # Ensure the version.py file is created + python -m build --version + +[testenv:lint] +deps = ruff +commands = ruff check --extend-exclude=tests/ . + +[testenv:format] +deps = ruff +commands = ruff format --check . From 0996b717e6cb76bd9d3417a013f0f0660ae0a8d6 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 10 Sep 2025 14:51:27 -0700 Subject: [PATCH 13/25] Add the Qt enum fixes --- src/python/simplexui/channelBox.py | 36 +++++---- src/python/simplexui/comboCheckDialog.py | 30 +++++--- src/python/simplexui/dragFilter.py | 26 +++---- src/python/simplexui/falloffDialog.py | 30 ++++---- .../simplexui/interface/mayaInterface.py | 7 +- src/python/simplexui/interfaceModel.py | 76 +++++++++++-------- src/python/simplexui/interfaceModelTrees.py | 16 ++-- .../menu/genericPlugins/_builtins.py | 6 +- .../menu/genericPlugins/unsubdivide.py | 4 +- .../menu/mayaPlugins/updateRestShape.py | 4 +- src/python/simplexui/simplexDialog.py | 10 ++- src/python/simplexui/travCheckDialog.py | 37 +++++---- 12 files changed, 165 insertions(+), 117 deletions(-) diff --git a/src/python/simplexui/channelBox.py b/src/python/simplexui/channelBox.py index e17a528a..5633c9ea 100644 --- a/src/python/simplexui/channelBox.py +++ b/src/python/simplexui/channelBox.py @@ -51,11 +51,11 @@ class SlideFilter(QObject): def __init__(self, parent): super(SlideFilter, self).__init__(parent) - self.slideCursor = Qt.SizeHorCursor - self.slideButton = Qt.LeftButton + self.slideCursor = Qt.CursorShape.SizeHorCursor + self.slideButton = Qt.MouseButton.LeftButton - self.fastModifier = Qt.ControlModifier - self.slowModifier = Qt.ShiftModifier + self.fastModifier = Qt.KeyboardModifier.ControlModifier + self.slowModifier = Qt.KeyboardModifier.ShiftModifier self.fastMultiplier = 5.0 self.slowDivisor = 5.0 @@ -95,13 +95,13 @@ def eventFilter(self, obj, event): """ if hasattr(self, "SLIDE_ENABLED"): - if event.type() == QEvent.MouseButtonPress: + if event.type() == QEvent.Type.MouseButtonPress: if event.button() & self.slideButton: self.startSlide(obj, event) self.doSlide(obj, event) self._slideStart = True - elif event.type() == QEvent.MouseMove: + elif event.type() == QEvent.Type.MouseMove: if self._slideStart: try: self.doSlide(obj, event) @@ -111,7 +111,7 @@ def eventFilter(self, obj, event): raise # re-raise the exception return True - elif event.type() == QEvent.MouseButtonRelease: + elif event.type() == QEvent.Type.MouseButtonRelease: if event.button() & self.slideButton: self._pressed = False self._slideStart = False @@ -300,7 +300,7 @@ def paintSlider(self, delegate, slider, painter, rect, palette): """ painter.save() try: - painter.setRenderHint(QPainter.Antialiasing, True) + painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) fgColor = slider.color bgColor = QColor(slider.color) @@ -340,7 +340,7 @@ def paintSlider(self, delegate, slider, painter, rect, palette): fgPath = fgPath.translated(rx, ry) painter.fillPath(fgPath, fgBrush) - opts = QTextOption(Qt.AlignCenter) + opts = QTextOption(Qt.AlignmentFlag.AlignCenter) frect = QRectF(rx, ry, rw, rh) painter.drawText(frect, slider.name, opts) # painter.drawPath(bgPath) @@ -408,17 +408,21 @@ def data(self, index, role): return None item = index.internalPointer() - if role in (Qt.DisplayRole, Qt.EditRole): + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): if isinstance(item, (Group, Slider)): return item.name - elif role == Qt.TextAlignmentRole: - return Qt.AlignCenter + elif role == Qt.ItemDataRole.TextAlignmentRole: + return Qt.AlignmentFlag.AlignCenter return None def flags(self, index): - return Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsEditable + | Qt.ItemFlag.ItemIsSelectable + ) def itemFromIndex(self, index): return index.internalPointer() @@ -543,7 +547,7 @@ def getItemAppendRow(self, item): return self.getItemRowCount(item) def getItemData(self, item, column, role): - if role in (Qt.DisplayRole, Qt.EditRole): + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): if column == 0: if isinstance(item, Group): return item.name @@ -705,7 +709,7 @@ def testSliderListDisplay(smpxPath): delegate = ChannelBoxDelegate() slideFilter = SlideFilter(tv.viewport()) - slideFilter.slideButton = Qt.LeftButton + slideFilter.slideButton = Qt.MouseButton.LeftButton tv.viewport().installEventFilter(slideFilter) slideFilter.slidePressed.connect(tv.slideStart) slideFilter.slideReleased.connect(tv.slideStop) @@ -823,7 +827,7 @@ def testSliderTreeDisplay(smpxPath): # delegate = ChannelBoxDelegate() # slideFilter = SlideFilter(tv.viewport()) - # slideFilter.slideButton = Qt.LeftButton + # slideFilter.slideButton = Qt.MouseButton.LeftButton # tv.viewport().installEventFilter(slideFilter) # slideFilter.slidePressed.connect(tv.slideStart) # slideFilter.slideReleased.connect(tv.slideStop) diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index 29086d68..ce330848 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -174,7 +174,7 @@ def __init__(self, sliders, values=None, mode="create", parent=None): self.valueDict = values or {} self.setSliders(sliders) if self.mode == "create": - self.uiAutoUpdateCHK.setCheckState(Qt.Unchecked) + self.uiAutoUpdateCHK.setCheckState(Qt.CheckState.Unchecked) self.uiAutoUpdateCHK.hide() self.uiManualUpdateBTN.hide() @@ -202,12 +202,12 @@ def dragTick(self, ticks, mul): """ items = self.uiEditTREE.selectedItems() for item in items: - val = item.data(3, Qt.EditRole) + val = item.data(3, Qt.ItemDataRole.EditRole) val += (0.05) * ticks * mul if abs(val) < 1.0e-5: val = 0.0 val = max(min(val, 1.0), -1.0) - item.setData(3, Qt.EditRole, val) + item.setData(3, Qt.ItemDataRole.EditRole, val) self.uiEditTREE.viewport().update() def setSliders(self, val): @@ -224,22 +224,29 @@ def setSliders(self, val): """ self.uiEditTREE.clear() dvs = [None, -1.0, 1.0, 0.5] - roles = [Qt.UserRole, Qt.UserRole, Qt.UserRole, Qt.EditRole] + roles = [ + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.EditRole, + ] val = val or [] for slider in val: item = QTreeWidgetItem(self.uiEditTREE, [slider.name]) - item.setFlags(item.flags() | Qt.ItemIsEditable) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) vvv = self.valueDict.get(slider, [-1.0, 1.0]) mvs = [i for i in vvv if abs(i) != 1.0] mvs = mvs[0] if mvs else 0.5 - item.setData(0, Qt.UserRole, slider) + item.setData(0, Qt.ItemDataRole.UserRole, slider) for col in range(1, 4): val = mvs if col == 3 else dvs[col] item.setData(col, roles[col], val) rng = slider.prog.getRange() if val in rng or col == 3: - chk = Qt.Checked if val in vvv else Qt.Unchecked + chk = ( + Qt.CheckState.Checked if val in vvv else Qt.CheckState.Unchecked + ) item.setCheckState(col, chk) for col in reversed(list(range(4))): @@ -276,10 +283,15 @@ def _populate(self): root = self.uiEditTREE.invisibleRootItem() lockDict = {} sliderList = [] - roles = [Qt.UserRole, Qt.UserRole, Qt.UserRole, Qt.EditRole] + roles = [ + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.EditRole, + ] for row in range(root.childCount()): item = root.child(row) - slider = item.data(0, Qt.UserRole) + slider = item.data(0, Qt.ItemDataRole.UserRole) if slider is not None: sliderList.append(slider) lv = [ diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index 5b208a0c..f53c4fdb 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -51,13 +51,13 @@ class DragFilter(QObject): The cursor that will be displayed while dragging CURSOR_ARROWS will show horizontal/vertical drag CURSOR_BLANK will hid the cursor - dragButton(Qt.MouseButton): default=Qt.MiddleButton + dragButton(Qt.MouseButton): default=Qt.MouseButton.MiddleButton The button that will kick off the drag behavior - fastModifier(Qt.KeyboardModifier): default=Qt.ControlModifier + fastModifier(Qt.KeyboardModifier): default=Qt.KeyboardModifier.ControlModifier The modifier key that will cause the multiplier to emit with the signal fastMultiplier(float): default=5.0 The size of the multiplier - slowModifier(Qt.KeyboardModifier): default=Qt.ShiftModifier + slowModifier(Qt.KeyboardModifier): default=Qt.KeyboardModifier.ShiftModifier The modifier key that will cause the divisor to emit with the signal slowDivisor(float): default=5.0 The size of the divisor @@ -88,15 +88,15 @@ def __init__(self, parent): self.cursorLock = False self.wrapBoundary = 10 # wrap when within boundary of screen edge self.dragCursor = self.CURSOR_ARROWS - self.dragButton = Qt.MiddleButton + self.dragButton = Qt.MouseButton.MiddleButton # The QSpinbox has an option where, if you hold down the mouse button # it will continually increment. This flag enables a workaround # for that problem self.isSpinbox = False - self.fastModifier = Qt.ControlModifier - self.slowModifier = Qt.ShiftModifier + self.fastModifier = Qt.KeyboardModifier.ControlModifier + self.slowModifier = Qt.KeyboardModifier.ShiftModifier self.fastMultiplier = 5.0 self.slowDivisor = 5.0 @@ -116,12 +116,12 @@ def doOverrideCursor(self): if self._overridden: return if self.dragCursor == self.CURSOR_BLANK: - QApplication.setOverrideCursor(Qt.BlankCursor) + QApplication.setOverrideCursor(Qt.CursorShape.BlankCursor) elif self.dragCursor == self.CURSOR_ARROWS: if self._dragType == self.DRAG_VERTICAL: - QApplication.setOverrideCursor(Qt.SizeVerCursor) + QApplication.setOverrideCursor(Qt.CursorShape.SizeVerCursor) elif self._dragType == self.DRAG_HORIZONTAL: - QApplication.setOverrideCursor(Qt.SizeHorCursor) + QApplication.setOverrideCursor(Qt.CursorShape.SizeHorCursor) self._overridden = True @@ -228,7 +228,7 @@ def startDrag(self, o, e): # otherwise the spinbox will keep ticking. @longClickFix # There's gotta be a better way to do this :-/ mouseup = QMouseEvent( - QEvent.MouseButtonRelease, + QEvent.Type.MouseButtonRelease, e.pos(), self.dragButton, e.buttons(), @@ -270,7 +270,7 @@ def eventFilter(self, o, e): The QEvent of the mouse drag """ if hasattr(self, "DRAG_ENABLED"): - if e.type() == QEvent.MouseMove: + if e.type() == QEvent.Type.MouseMove: if self._isDragging: try: if self._dragType != self.DRAG_NONE: @@ -283,14 +283,14 @@ def eventFilter(self, o, e): raise # re-raise the exception return True - elif e.type() == QEvent.MouseButtonRelease: + elif e.type() == QEvent.Type.MouseButtonRelease: self.myendDrag(o, e) if e.button() & self.dragButton: # Catch any dragbutton releases and handle them self._isDragging = False return True - elif e.type() == QEvent.MouseButtonPress: + elif e.type() == QEvent.Type.MouseButtonPress: if e.button() & self.dragButton: # Catch any dragbutton presses and handle them self._isDragging = True diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index 7ebe9133..5a5a53da 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -79,9 +79,9 @@ def __init__(self, parent): self.canvasMargin = 16 self.setMinimumHeight(2 * self.canvasMargin) - self.bgColor = Qt.white - self.lineColor = Qt.black - self.limitColor = Qt.gray + self.bgColor = Qt.GlobalColor.white + self.lineColor = Qt.GlobalColor.black + self.limitColor = Qt.GlobalColor.gray def setTangent(self, leftTan=None, rightTan=None): """Set the falloff tangents, clamped 0 to 1 @@ -150,17 +150,17 @@ def _drawCleanLine(self, painter, p1, p2): def _paintBG(self, painter): painter.save() - painter.setBrush(self.palette().color(QPalette.Background)) + painter.setBrush(self.palette().color(QPalette.ColorRole.Background)) painter.drawRect(0, 0, self.width(), self.height()) painter.restore() def _paintLimits(self, painter): painter.save() # pen = QPen(self.limitColor) - baseColor = self.palette().color(QPalette.Base) + baseColor = self.palette().color(QPalette.ColorRole.Base) pen = QPen(baseColor) pen.setWidth(1) - pen.setStyle(Qt.DashLine) + pen.setStyle(Qt.PenStyle.DashLine) painter.setPen(pen) self._drawCleanLine( painter, self.mapToCanvas(QPoint(0, 0)), self.mapToCanvas(QPoint(1, 0)) @@ -176,16 +176,16 @@ def _paintPath(self, painter, p0, p1, p2, p3): path.moveTo(p0) path.cubicTo(p1, p2, p3) # painter.strokePath(path, QPen(QBrush(self.lineColor), 2)) - foregroundColor = self.palette().color(QPalette.Foreground) + foregroundColor = self.palette().color(QPalette.ColorRole.Foreground) painter.strokePath(path, QPen(QBrush(foregroundColor), 2)) painter.restore() def _paintTangents(self, painter, p0, p1, p2, p3): # draw the tangent lines - foregroundColor = self.palette().color(QPalette.Foreground) + foregroundColor = self.palette().color(QPalette.ColorRole.Foreground) pen = QPen(foregroundColor) pen.setWidth(1) - pen.setStyle(Qt.DashLine) + pen.setStyle(Qt.PenStyle.DashLine) painter.setPen(pen) painter.drawLine(p0, p1) painter.drawLine(p3, p2) @@ -197,7 +197,7 @@ def _paintTangents(self, painter, p0, p1, p2, p3): def paintEvent(self, e): painter = QPainter(self) - painter.setRenderHint(QPainter.Antialiasing) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) self._paintBG(painter) self._paintLimits(painter) @@ -247,7 +247,7 @@ def findControlPoint(self, point, tolerance=10): return None def mousePressEvent(self, e): - if e.button() == Qt.LeftButton: + if e.button() == Qt.MouseButton.LeftButton: self._activeControlPoint = self.findControlPoint(e.pos()) if self._activeControlPoint is not None: self.mouseMoveEvent(e) @@ -255,7 +255,7 @@ def mousePressEvent(self, e): e.accept() def mouseReleaseEvent(self, e): - if e.button() == Qt.LeftButton: + if e.button() == Qt.MouseButton.LeftButton: self._activeControlPoint = None self.mouseDrag = False e.accept() @@ -292,7 +292,7 @@ def __init__(self, parent): self.foModel = QStandardItemModel() self.uiFalloffWID = CurveEditWidget(self) - policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) + policy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding) # policy.setVerticalStretch(1) self.uiFalloffWID.setSizePolicy(policy) self.uiFalloffWID.tangentUpdated.connect(self.updateTangents) @@ -323,8 +323,8 @@ def updateTangents(self, leftTangent, rightTangent): leftTanIdx = self.foModel.index(cbIdx, 5) rightTanIdx = self.foModel.index(cbIdx, 4) - self.foModel.setData(leftTanIdx, leftTangent, role=Qt.EditRole) - self.foModel.setData(rightTanIdx, rightTangent, role=Qt.EditRole) + self.foModel.setData(leftTanIdx, leftTangent, role=Qt.ItemDataRole.EditRole) + self.foModel.setData(rightTanIdx, rightTangent, role=Qt.ItemDataRole.EditRole) def setLeftTangent(self, val): self.uiFalloffWID.setTangent(leftTan=val) diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 2109af3c..79ef3227 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -326,9 +326,12 @@ def preLoad(self, simp, simpDict, create=True, pBar=None): if pBar is not None: msg = "Some shapes are Missing:\n{}\n\nCreate them?" msg = msg.format(", ".join(toMake)) - btns = QMessageBox.Yes | QMessageBox.Cancel + btns = ( + QMessageBox.StandardButton.Yes + | QMessageBox.StandardButton.Cancel + ) bret = QMessageBox.question(pBar, "Missing Shapes", msg, btns) - if not bret & QMessageBox.Yes: + if not bret & QMessageBox.StandardButton.Yes: raise RuntimeError("Missing Shapes: {}".format(toMake)) else: raise RuntimeError("Missing Shapes: {}".format(toMake)) diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index fd4aaaae..e89370e9 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -389,29 +389,33 @@ def data(self, index, role): def flags(self, index): if not index.isValid(): - return Qt.ItemIsEnabled + return Qt.ItemFlag.ItemIsEnabled if index.column() == 0: item = index.internalPointer() if isinstance(item, (Slider, Combo, Traversal)): return ( - Qt.ItemIsEnabled - | Qt.ItemIsSelectable - | Qt.ItemIsEditable - | Qt.ItemIsUserCheckable + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsEditable + | Qt.ItemFlag.ItemIsUserCheckable ) # TODO: make the SHAPES object under a combo or traversal not-editable - return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsEditable + ) - def setData(self, index, value, role=Qt.EditRole): + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): if not index.isValid(): return False - if role == Qt.CheckStateRole: + if role == Qt.ItemDataRole.CheckStateRole: item = index.internalPointer() if index.column() == 0: if isinstance(item, (Slider, Combo, Traversal)): - item.enabled = value == Qt.Checked + item.enabled = value == Qt.CheckState.Checked return True - elif role == Qt.EditRole: + elif role == Qt.ItemDataRole.EditRole: item = index.internalPointer() if index.column() == 0: if isinstance(item, (Group, Slider, Combo, Traversal, ProgPair)): @@ -432,8 +436,8 @@ def setData(self, index, value, role=Qt.EditRole): return False def headerData(self, section, orientation, role): - if orientation == Qt.Horizontal: - if role == Qt.DisplayRole: + if orientation == Qt.Orientation.Horizontal: + if role == Qt.ItemDataRole.DisplayRole: sects = ("Items", "Slide", "Value") return sects[section] return None @@ -471,17 +475,17 @@ def getItemData(self, item, column, role): if item is None: return None - if role in (Qt.DisplayRole, Qt.EditRole): + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): return item.treeData(column) - elif role == Qt.CheckStateRole: + elif role == Qt.ItemDataRole.CheckStateRole: chk = None if column == 0: chk = item.treeChecked() if chk is not None: - chk = Qt.Checked if chk else Qt.Unchecked + chk = Qt.CheckState.Checked if chk else Qt.CheckState.Unchecked return chk - elif role == Qt.DecorationRole: + elif role == Qt.ItemDataRole.DecorationRole: if column == 0: return item.icon() return None @@ -787,12 +791,12 @@ def data(self, index, role): if not index.isValid(): return None group = index.internalPointer() - if group and role in (Qt.DisplayRole, Qt.EditRole): + if group and role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): return group.name return None def flags(self, index): - return Qt.ItemIsEnabled | Qt.ItemIsSelectable + return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable def itemFromIndex(self, index): return index.internalPointer() @@ -828,9 +832,9 @@ def buildLine(self): partials = [] for fo in self.simplex.falloffs: cs = self._getCheckState(fo) - if cs == Qt.Checked: + if cs == Qt.CheckState.Checked: fulls.append(fo.name) - elif cs == Qt.PartiallyChecked: + elif cs == Qt.CheckState.PartiallyChecked: partials.append(fo.name) if partials: title = "{0} <<{1}>>".format(",".join(fulls), ",".join(partials)) @@ -880,10 +884,10 @@ def columnCount(self, parent): def _getCheckState(self, fo): sli = self._checks.get(fo, []) if len(sli) == len(self.sliders): - return Qt.Checked + return Qt.CheckState.Checked elif len(sli) == 0: - return Qt.Unchecked - return Qt.PartiallyChecked + return Qt.CheckState.Unchecked + return Qt.CheckState.PartiallyChecked def data(self, index, role): if not index.isValid(): @@ -892,23 +896,23 @@ def data(self, index, role): if not falloff: return None - if role in (Qt.DisplayRole, Qt.EditRole): + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): return falloff.name - elif role == Qt.CheckStateRole: + elif role == Qt.ItemDataRole.CheckStateRole: return self._getCheckState(falloff) return None def setData(self, index, value, role): - if role == Qt.CheckStateRole: + if role == Qt.ItemDataRole.CheckStateRole: fo = index.internalPointer() if not fo: return - if value == Qt.Checked: + if value == Qt.CheckState.Checked: for s in self.sliders: if fo not in s.prog.falloffs: s.prog.addFalloff(fo) self._checks.setdefault(fo, []).append(s) - elif value == Qt.Unchecked: + elif value == Qt.CheckState.Unchecked: for s in self.sliders: if fo in s.prog.falloffs: s.prog.removeFalloff(fo) @@ -920,7 +924,11 @@ def setData(self, index, value, role): return False def flags(self, index): - return Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsUserCheckable + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsEditable + | Qt.ItemFlag.ItemIsUserCheckable + ) def itemFromIndex(self, index): return index.internalPointer() @@ -972,7 +980,7 @@ def data(self, index, role): if not falloff: return None - if role in (Qt.DisplayRole, Qt.EditRole): + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): if index.column() == 0: return falloff.name elif index.column() == 1: @@ -1001,7 +1009,7 @@ def setData(self, index, value, role): falloff = index.internalPointer() if not falloff: return False - if role == Qt.EditRole: + if role == Qt.ItemDataRole.EditRole: if index.column() == 0: falloff.name = value elif index.column() == 1: @@ -1026,7 +1034,11 @@ def setData(self, index, value, role): return False def flags(self, index): - return Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsEditable + | Qt.ItemFlag.ItemIsSelectable + ) def itemFromIndex(self, index): return index.internalPointer() diff --git a/src/python/simplexui/interfaceModelTrees.py b/src/python/simplexui/interfaceModelTrees.py index cde2ee14..f11d7439 100644 --- a/src/python/simplexui/interfaceModelTrees.py +++ b/src/python/simplexui/interfaceModelTrees.py @@ -42,8 +42,8 @@ class SimplexTree(QTreeView): def __init__(self, parent): super(SimplexTree, self).__init__(parent) - self.expandModifier = Qt.ControlModifier - self.depthModifier = Qt.ShiftModifier + self.expandModifier = Qt.KeyboardModifier.ControlModifier + self.depthModifier = Qt.KeyboardModifier.ShiftModifier self._menu = None self._plugins = [] @@ -79,7 +79,9 @@ def unifySelection(self): And it will clear the selection on this tree if no modifiers are being held """ mods = QApplication.keyboardModifiers() - if not mods & (Qt.ControlModifier | Qt.ShiftModifier): + if not mods & ( + Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier + ): selModel = self.selectionModel() selModel.blockSignals(True) try: @@ -309,7 +311,7 @@ def dragTick(self, ticks, mul): # Menus and Actions def connectMenus(self): """Setup the QT signal/slot connections to the context menus""" - self.setContextMenuPolicy(Qt.CustomContextMenu) + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self.openMenu) def openMenu(self, pos): @@ -406,7 +408,9 @@ def setItemSelection(self, items): toSel = QItemSelection() for idx in idxs: - toSel.merge(QItemSelection(idx, idx), QItemSelectionModel.Select) + toSel.merge( + QItemSelection(idx, idx), QItemSelectionModel.SelectionFlag.Select + ) for idx in idxs: par = idx.parent() @@ -414,7 +418,7 @@ def setItemSelection(self, items): self.scrollToIndex(par) selModel = self.selectionModel() - selModel.select(toSel, QItemSelectionModel.ClearAndSelect) + selModel.select(toSel, QItemSelectionModel.SelectionFlag.ClearAndSelect) # Currently, there's no difference between these diff --git a/src/python/simplexui/menu/genericPlugins/_builtins.py b/src/python/simplexui/menu/genericPlugins/_builtins.py index ac2598f9..3238c6cf 100644 --- a/src/python/simplexui/menu/genericPlugins/_builtins.py +++ b/src/python/simplexui/menu/genericPlugins/_builtins.py @@ -79,11 +79,11 @@ def registerSliderTree(window, clickIdx, indexes, menu): cb = QCheckBox(falloff.name, setFalloffMenu) chk = foChecks.get(falloff) if not chk: - cb.setCheckState(Qt.Unchecked) + cb.setCheckState(Qt.CheckState.Unchecked) elif len(chk) != len(sliders): - cb.setCheckState(Qt.PartiallyChecked) + cb.setCheckState(Qt.CheckState.PartiallyChecked) else: - cb.setCheckState(Qt.Checked) + cb.setCheckState(Qt.CheckState.Checked) cb.stateChanged.connect(partial(self.setSelectedSliderFalloff, falloff)) diff --git a/src/python/simplexui/menu/genericPlugins/unsubdivide.py b/src/python/simplexui/menu/genericPlugins/unsubdivide.py index 99be7fdd..5cb77168 100644 --- a/src/python/simplexui/menu/genericPlugins/unsubdivide.py +++ b/src/python/simplexui/menu/genericPlugins/unsubdivide.py @@ -58,10 +58,10 @@ def exportUnsubInterface(window): return if os.path.isfile(outPath): - btns = QMessageBox.Ok | QMessageBox.Cancel + btns = QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel msg = "Unsub file already exists.\n{0}\nOverwrite?".format(outPath) response = QMessageBox.question(window, "File already exists", msg, btns) - if not response & QMessageBox.Ok: + if not response & QMessageBox.StandardButton.Ok: return pBar = QProgressDialog("Exporting Unsubdivided smpx File", "Cancel", 0, 100, window) diff --git a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py index 87fdb18d..00ba7cd1 100644 --- a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py +++ b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py @@ -62,9 +62,9 @@ def updateRestShapeInterface(window): "Continue anyway?", ] msg = "\n".join(msg) - btns = QMessageBox.Ok | QMessageBox.Cancel + btns = QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel bret = QMessageBox.question(window, "Live Connections", msg, btns) - if not bret & QMessageBox.Ok: + if not bret & QMessageBox.StandardButton.Ok: return updateRestShape(mesh, sel, window=window) diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 5ac3594f..19a20c82 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -469,7 +469,9 @@ def unifySliderSelection(self): an item on the slider tree is selected """ mods = QApplication.keyboardModifiers() - if not mods & (Qt.ControlModifier | Qt.ShiftModifier): + if not mods & ( + Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier + ): comboSelModel = self.uiComboTREE.selectionModel() if not comboSelModel: return @@ -483,7 +485,9 @@ def unifyComboSelection(self): an item on the combo tree is selected """ mods = QApplication.keyboardModifiers() - if not mods & (Qt.ControlModifier | Qt.ShiftModifier): + if not mods & ( + Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier + ): sliderSelModel = self.uiSliderTREE.selectionModel() if not sliderSelModel: return @@ -1267,7 +1271,7 @@ def setSelectedSliderFalloff(self, falloff, state): return sliders = self.uiSliderTREE.getSelectedItems(Slider) for s in sliders: - if state == Qt.Checked: + if state == Qt.CheckState.Checked: s.prog.addFalloff(falloff) else: s.prog.removeFalloff(falloff) diff --git a/src/python/simplexui/travCheckDialog.py b/src/python/simplexui/travCheckDialog.py index 801604c6..8168cb2a 100644 --- a/src/python/simplexui/travCheckDialog.py +++ b/src/python/simplexui/travCheckDialog.py @@ -139,9 +139,9 @@ def __init__(self, pairs, trav, *args, **kwargs): for slider, rng in pairs: item = QTreeWidgetItem(self) - item.setData(0, Qt.EditRole, slider.name) - item.setData(1, Qt.EditRole, rng[0]) - item.setData(2, Qt.EditRole, rng[1]) + item.setData(0, Qt.ItemDataRole.EditRole, slider.name) + item.setData(1, Qt.ItemDataRole.EditRole, rng[0]) + item.setData(2, Qt.ItemDataRole.EditRole, rng[1]) if exists: item.setForeground(0, grayBrush) item.setForeground(1, grayBrush) @@ -193,7 +193,12 @@ def __init__( self.parUI = parent self.gparUI = grandparent self.maxPoss = 100 - self.colCheckRoles = [Qt.UserRole, Qt.UserRole, Qt.UserRole, Qt.EditRole] + self.colCheckRoles = [ + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.EditRole, + ] self.uiCreateSelectedBTN.clicked.connect(self.createMissing) self.uiMinLimitSPIN.valueChanged.connect(self.populateWithoutUpdate) @@ -214,7 +219,7 @@ def __init__( self.dynDict = dynamics or {} self.setSliders(sliders) if self.mode == "create": - self.uiAutoUpdateCHK.setCheckState(Qt.Unchecked) + self.uiAutoUpdateCHK.setCheckState(Qt.CheckState.Unchecked) self.uiAutoUpdateCHK.hide() self.uiManualUpdateBTN.hide() @@ -242,12 +247,12 @@ def dragTick(self, ticks, mul): """ items = self.uiEditTREE.selectedItems() for item in items: - val = item.data(3, Qt.EditRole) + val = item.data(3, Qt.ItemDataRole.EditRole) val += (0.05) * ticks * mul if abs(val) < 1.0e-5: val = 0.0 val = max(min(val, 1.0), -1.0) - item.setData(3, Qt.EditRole, val) + item.setData(3, Qt.ItemDataRole.EditRole, val) self.uiEditTREE.viewport().update() def setSliders(self, val): @@ -267,18 +272,22 @@ def setSliders(self, val): val = val or [] for slider in val: item = QTreeWidgetItem(self.uiEditTREE, [slider.name]) - item.setFlags(item.flags() | Qt.ItemIsEditable) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) rangeVals = self.valueDict.get(slider, [-1.0, 1.0]) - item.setData(0, Qt.UserRole, slider) + item.setData(0, Qt.ItemDataRole.UserRole, slider) for col in range(1, 3): val = dvs[col] item.setData(col, self.colCheckRoles[col], val) rng = slider.prog.getRange() if val in rng: - chk = Qt.Checked if val in rangeVals else Qt.Unchecked + chk = ( + Qt.CheckState.Checked + if val in rangeVals + else Qt.CheckState.Unchecked + ) item.setCheckState(col, chk) - item.setCheckState(3, self.dynDict.get(slider, Qt.Checked)) + item.setCheckState(3, self.dynDict.get(slider, Qt.CheckState.Checked)) for col in reversed(list(range(4))): self.uiEditTREE.resizeColumnToContents(col) @@ -316,15 +325,15 @@ def _populate(self): for row in range(root.childCount()): item = root.child(row) - slider = item.data(0, Qt.UserRole) + slider = item.data(0, Qt.ItemDataRole.UserRole) if slider is not None: sliderList.append(slider) lv = [ item.data(col, self.colCheckRoles[col]) for col in range(1, 3) - if item.checkState(col) == Qt.Checked + if item.checkState(col) == Qt.CheckState.Checked ] - dyn = item.checkState(3) == Qt.Checked + dyn = item.checkState(3) == Qt.CheckState.Checked lockDict[slider] = (lv, dyn) tooMany, toAdd = buildPossibleTraversals( From 88971da207e874990681d9d23dad2d137c372722 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 10 Sep 2025 14:55:17 -0700 Subject: [PATCH 14/25] Add maya 2026 to the workflow, update the maya subproject, and add some ignores to the .gitignore --- .github/workflows/main.yml | 8 +++- .gitignore | 8 ++++ subprojects/maya/meson.build | 77 ++++++++++++++++++++++++++++-------- 3 files changed, 74 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d5bb421f..bc5fe5dc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: compile_plugin: strategy: matrix: - maya: [2022, 2023, 2024, 2025] + maya: [2022, 2023, 2024, 2025, 2026] os: [macos-13, macos-latest, ubuntu-latest, windows-latest] include: # Add the maya update versions here @@ -31,7 +31,9 @@ jobs: - maya: 2024 update: 2 - maya: 2025 - update: 1 + update: 3 + - maya: 2026 + update: 2 # cross-compiling is annoying so just fall back to macos-13 exclude: @@ -43,6 +45,8 @@ jobs: maya: 2024 - os: macos-13 maya: 2025 + - os: macos-13 + maya: 2026 fail-fast: false diff --git a/.gitignore b/.gitignore index dfa65758..aabd2e2b 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,14 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk +# clang LSP +.cache + +# Known subprojects +**/Eigen-*/ +**/rapidjson-*/ +subprojects/packagecache/ + # ========================= # Operating System Files # ========================= diff --git a/subprojects/maya/meson.build b/subprojects/maya/meson.build index 65df36ef..263fb2ad 100644 --- a/subprojects/maya/meson.build +++ b/subprojects/maya/meson.build @@ -16,8 +16,14 @@ maya_link_args = [] if os_name == 'windows' maya_install_base = 'c:/Program Files/Autodesk' maya_plugin_ext = 'mll' - maya_compile_args += ['-DNT_PLUGIN'] - maya_link_args = ['/export:initializePlugin', '/export:uninitializePlugin'] + maya_compile_args += ['-DNT_PLUGIN', '/Zc:__cplusplus'] + maya_link_args = [ + # Export the methods that maya requires + '/export:initializePlugin', + '/export:uninitializePlugin', + # Make sure to look for pdb file in the same directory as the .mll + '/PDBALTPATH:%_PDB%', + ] elif os_name == 'darwin' maya_install_base = '/Applications/Autodesk' maya_plugin_ext = 'bundle' @@ -31,8 +37,7 @@ elif os_name == 'darwin' maya_compile_args += ['-arch', 'x86_64'] maya_link_args += ['-arch', 'x86_64'] if maya_version.version_compare('>=2024') - # clang will build for both x86 and arm - # if both arches are in the command line args + # build both the arm and x86 plugins when compiling for mac maya_compile_args += ['-arch', 'arm64'] maya_link_args += ['-arch', 'arm64'] endif @@ -54,13 +59,16 @@ if maya_devkit_base != '' maya_install_path = maya_devkit_base endif +includes = [] maya_inc_dir = maya_install_path / maya_inc_suffix message('Searching Maya Include directory:', maya_inc_dir) -maya_inc = include_directories(maya_inc_dir) +includes += include_directories(maya_inc_dir) maya_lib_dir = maya_install_path / maya_lib_suffix message('Searching Maya lib directory:', maya_lib_dir) +maya_bin_dir = maya_install_path / 'bin' + # Get all the maya libraries cmplr = meson.get_compiler('cpp') maya_libs = [ @@ -74,31 +82,66 @@ maya_libs = [ ] # Link to maya's qt libs if required -# This doesn't do MOC stuff ... yet +# Below, I expose the bin and include dirs so I can directly invoke the +# moc exe included with maya. Saves from having to make a maya qt module + +qt_moc_path = '' if maya_link_qt fs = import('fs') - if not fs.is_dir(maya_inc_dir / 'QtCore') - error( - 'Could not find Maya QT headers with `maya_link_qt` defined\n', - 'You probably need to unzip `include/qt_*-include.zip`\n', - 'Checking in folder: ', maya_inc_dir, - ) + + if maya_version.version_compare('>=2025') + if not fs.is_dir(maya_install_path / 'Qt' / 'include' / 'QtCore') + error( + 'Could not find Maya QT headers with `maya_link_qt` defined\n', + 'You probably need to unzip `devkitBase/Qt.zip`\n', + 'Checking in folder: ', maya_install_path, + ) + endif + maya_qt_lib_names = [f'Qt6Core', f'Qt6Gui', f'Qt6Widgets'] + + qt_lib_dir = maya_lib_dir + qt_moc_path = maya_bin_dir / 'moc' + if maya_devkit_base != '' + qt_lib_dir = maya_devkit_base / 'Qt' / 'lib' + qt_moc_path = maya_devkit_base / 'Qt' / 'libexec' / 'moc' + includes += include_directories(maya_devkit_base / 'Qt' / 'include') + endif + else + if not fs.is_dir(maya_inc_dir / 'QtCore') + error( + 'Could not find Maya QT headers with `maya_link_qt` defined\n', + 'You probably need to unzip `include/qt_*-include.zip`\n', + 'Checking in folder: ', maya_inc_dir, + ) + endif + qt_ver = 5 + maya_qt_lib_names = [f'Qt5Core', f'Qt5Gui', f'Qt5Widgets'] + qt_lib_dir = maya_lib_dir + qt_moc_path = maya_bin_dir / 'moc' + if maya_devkit_base != '' + qt_moc_path = maya_devkit_base / 'devkit' / 'bin' / 'moc' + endif endif - maya_qt_lib_names = ['Qt5Core', 'Qt5Gui', 'Qt5Widgets'] if maya_qt_extra_includes != '' maya_qt_lib_names += maya_qt_extra_includes.split(';') endif - foreach lib_name : maya_qt_lib_names - maya_libs += cmplr.find_library(lib_name, dirs : maya_lib_dir) + maya_libs += cmplr.find_library(lib_name, dirs : qt_lib_dir) endforeach + endif maya_dep = declare_dependency( dependencies : maya_libs, - include_directories : maya_inc, - variables : {'name_suffix' : maya_plugin_ext, 'maya_version' : maya_version}, + include_directories : includes, + variables : { + 'name_suffix' : maya_plugin_ext, + 'maya_version' : maya_version, + 'maya_bin_dir': maya_bin_dir, + 'maya_inc_dir': maya_inc_dir, + 'qt_moc_path': qt_moc_path, + }, compile_args : maya_compile_args, link_args : maya_link_args, ) From 38a40f5b0c6e506a95b77961cf556f6010ef58e5 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 10 Sep 2025 17:27:07 -0700 Subject: [PATCH 15/25] Separated building the python module vs building the python wheel. --- .github/workflows/main.yml | 24 +------------------ meson.options | 1 + pyproject.toml | 2 +- quick_compile.bat | 7 +++--- src/python/meson.build | 47 +++++++++++++++++++++++--------------- 5 files changed, 34 insertions(+), 47 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bc5fe5dc..37f24677 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -106,7 +106,6 @@ jobs: run: | echo "PY_VER=3.9" >> $GITHUB_ENV echo "PY_VER_FLAT=39" >> $GITHUB_ENV - echo "PY_EXT=so" >> $GITHUB_ENV echo "PLAT_TAG=macosx_12_0_arm64" >> $GITHUB_ENV - name: Get pyver ubuntu-latest @@ -115,7 +114,6 @@ jobs: run: | echo "PY_VER=3.7" >> $GITHUB_ENV echo "PY_VER_FLAT=37" >> $GITHUB_ENV - echo "PY_EXT=so" >> $GITHUB_ENV echo "PLAT_TAG=manylinux_2_17_x86_64" >> $GITHUB_ENV - name: Get pyver windows-latest @@ -124,7 +122,6 @@ jobs: run: | echo "PY_VER=3.7" >> $GITHUB_ENV echo "PY_VER_FLAT=37" >> $GITHUB_ENV - echo "PY_EXT=pyd" >> $GITHUB_ENV echo "PLAT_TAG=win_amd64" >> $GITHUB_ENV - name: Get an older python version @@ -132,35 +129,16 @@ jobs: with: python-version: ${{ env.PY_VER }} - - name: Build Python - uses: blurstudio/mayaModuleActions/mesonBuild@v1 - with: - setup-args: > - -Dmaya_build=false - -Dpython_build=true - --buildtype release - --backend ninja - install-args: --skip-subprojects - - name: Build Wheel shell: bash run: | - python -m pip install -U pip - python -m pip install -U build wheel hatch - python -m hatch version ${{ steps.previoustag.outputs.tag }} + python -m pip install -U build wheel python -m build --wheel for PY_WHEEL in dist/*.whl do python -m wheel tags --remove --python-tag ${{ env.PY_VER_FLAT }} --abi-tag abi3 --platform-tag ${{ env.PLAT_TAG }} ${PY_WHEEL} done - - name: Upload Artifacts - uses: actions/upload-artifact@v4 - with: - name: ${{ runner.os }}-pyModule - path: output_Python/*.${{ env.PY_EXT }} - if-no-files-found: error - - name: Upload Artifacts uses: actions/upload-artifact@v4 with: diff --git a/meson.options b/meson.options index 17700b32..167b94d1 100644 --- a/meson.options +++ b/meson.options @@ -1,2 +1,3 @@ option('maya_build', type : 'boolean', value : true) option('python_build', type : 'boolean', value : true) +option('python_wheel_build', type : 'boolean', value : false) diff --git a/pyproject.toml b/pyproject.toml index 513494f4..f469cf6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ build-backend = "mesonpy" requires = ["meson-python>=0.15.0", "meson >= 1.6.0"] [tool.meson-python.args] -setup = ['-Dmaya_build=false', '-Dpython_build=true'] +setup = ['-Dmaya_build=false', '-Dpython_build=true', '-Dpython_wheel_build=true'] install = ['--skip-subprojects'] [tool.ruff] diff --git a/quick_compile.bat b/quick_compile.bat index 46ea3eac..d479a16c 100644 --- a/quick_compile.bat +++ b/quick_compile.bat @@ -1,6 +1,6 @@ setlocal -SET MAYA_VERSION=2024 +SET MAYA_VERSION=2026 REM "vs" "ninja" REM use VS for the debugger, otherwise use NINJA REM Until I figure out how to debug using nvim @@ -12,13 +12,12 @@ SET BUILDDIR=mayabuild_%BUILDTYPE%_%MAYA_VERSION%_%BACKEND% if not exist %BUILDDIR%\ ( meson setup %BUILDDIR% ^ -Dmaya:maya_version=%MAYA_VERSION% ^ + -Dmaya:maya_devkit_base=D:\Autodesk\MayaDev\Maya2026_2\devkitBase ^ -Dmaya_build=true ^ - -Dpython_build=false ^ + -Dpython_build=true ^ --buildtype %BUILDTYPE% --vsenv --backend %BACKEND% ) - - if exist %BUILDDIR%\ ( meson compile -C %BUILDDIR% -j 8 meson install --skip-subprojects -C %BUILDDIR% diff --git a/src/python/meson.build b/src/python/meson.build index 6f8ee5a2..5955ed9f 100644 --- a/src/python/meson.build +++ b/src/python/meson.build @@ -11,24 +11,34 @@ if get_option('buildtype') == 'debug' endif -fs = import('fs') -if fs.is_file('simplexui/_version.py') - message('Using existing _version.py') - py.install_sources( - 'simplexui/_version.py', - subdir:'simplexui', - ) +py_wheel_build = get_option('python_wheel_build') + + +if py_wheel_build + # Only install the .py files if we're building a wheel + fs = import('fs') + if fs.is_file('simplexui/_version.py') + message('Using existing _version.py') + py.install_sources( + 'simplexui/_version.py', + subdir:'simplexui', + ) + else + version_py = configure_file( + input: 'simplexui/_version.py.in', + output: '_version.py', + install_dir: py.get_install_dir() / 'simplexui', + configuration : conf_data, + ) + py.install_sources( + version_py, + subdir:'simplexui', + ) + endif + subdir('simplexui') + install_kwargs = {'subdir' : 'simplexui'} else - version_py = configure_file( - input: 'simplexui/_version.py.in', - output: '_version.py', - install_dir: py.get_install_dir() / 'simplexui', - configuration : conf_data, - ) - py.install_sources( - version_py, - subdir:'simplexui', - ) + install_kwargs = {'install_dir' : meson.global_source_root() / 'output_Python'} endif pysimplex = py.extension_module( @@ -37,7 +47,6 @@ pysimplex = py.extension_module( dependencies : [simplexlib_dep, rapidjson_dep], install: true, limited_api : lapi, - subdir : 'simplexui', + kwargs : install_kwargs, ) -subdir('simplexui') From bbde86afa34174cacab089059135830b1b706c30 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 12 Sep 2025 12:10:56 -0700 Subject: [PATCH 16/25] Fix how maya finds Qt's moc --- subprojects/maya/meson.build | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/subprojects/maya/meson.build b/subprojects/maya/meson.build index 263fb2ad..1d6dc7df 100644 --- a/subprojects/maya/meson.build +++ b/subprojects/maya/meson.build @@ -103,7 +103,14 @@ if maya_link_qt qt_moc_path = maya_bin_dir / 'moc' if maya_devkit_base != '' qt_lib_dir = maya_devkit_base / 'Qt' / 'lib' - qt_moc_path = maya_devkit_base / 'Qt' / 'libexec' / 'moc' + + mocdirs = [ + maya_devkit_base / 'Qt' / 'bin', + maya_devkit_base / 'Qt' / 'libexec', + ] + moc_prg = find_program('moc', dirs : mocdirs, required : true) + qt_moc_path = moc_prg.full_path() + includes += include_directories(maya_devkit_base / 'Qt' / 'include') endif else From 3ccfe9e2f3702ef782365491adb551196517da3a Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 12 Sep 2025 12:11:43 -0700 Subject: [PATCH 17/25] Ignore the python dist --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index aabd2e2b..18c32a4e 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,9 @@ $RECYCLE.BIN/ **/rapidjson-*/ subprojects/packagecache/ +# python wheel dist folder +dist/ + # ========================= # Operating System Files # ========================= From 225677d6e1cfe1ec733f1756fc542ca291160282 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 12 Sep 2025 18:23:31 -0700 Subject: [PATCH 18/25] Add right-click export and duplicate name detection --- src/python/simplexui/__init__.py | 2 +- .../simplexui/interface/dummyInterface.py | 8 + .../simplexui/interface/mayaInterface.py | 146 +++++++++++------- .../menu/genericPlugins/_builtins.py | 10 ++ src/python/simplexui/simplexDialog.py | 66 ++++++++ 5 files changed, 173 insertions(+), 59 deletions(-) diff --git a/src/python/simplexui/__init__.py b/src/python/simplexui/__init__.py index 99e87ce3..25d066cb 100644 --- a/src/python/simplexui/__init__.py +++ b/src/python/simplexui/__init__.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from _version import __version__ # noqa: F401 +from ._version import __version__ # noqa: F401 SIMPLEX_UI = None SIMPLEX_UI_ROOT = None diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index 86ba4c9c..3546163c 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -647,6 +647,14 @@ def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): self.exportAbc( dccMesh, abcMesh, js, world=world, ensureCorrect=False, pBar=pBar ) + + def deleteObj(self, dccMesh, path): + """Export a mesh to the given path""" + pass + + def exportMesh(self, dccMesh, path): + """Export a mesh to the given path""" + pass # Revision tracking def getRevision(self): diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 79ef3227..54a01916 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -16,18 +16,19 @@ # along with Simplex. If not, see . # pylint: disable=invalid-name +from __future__ import annotations import json import re from contextlib import contextmanager from functools import wraps +from typing import TYPE_CHECKING import maya.cmds as cmds import maya.OpenMaya as om from alembic.AbcGeom import GeometryScope, OPolyMeshSchemaSample, OV2fGeomParamSample from imath import IntArray, UnsignedIntArray, V2fArray, V3fArray -from ..commands.alembicCommon import mkSampleVertexPoints -from ..items.simplex import Simplex +from ..commands.alembicCommon import mkSampleVertexPoints, buildAbc from Qt import QtCore from Qt.QtCore import Signal from Qt.QtWidgets import ( @@ -38,6 +39,9 @@ QSplashScreen, ) +if TYPE_CHECKING: + from ..items.simplex import Simplex + try: import numpy as np from ..commands.numpytoimath import numpyToImath @@ -193,6 +197,14 @@ def disconnected(targets, testCnxType=("double", "float")): doReconnect(cnxs) +def split_trailing_digits(s: str) -> tuple[str, str]: + """Split trailing digits from a string into (prefix, digits).""" + match = re.match(r"^(.*?)(\d+)?$", s) + if match: + return match.group(1), match.group(2) or "" + return s, "" + + class DCC(object): """ """ @@ -552,15 +564,12 @@ def loadNodes(self, simp, thing, create=True, pBar=None): ops = self._getOpNodes(thing) sns = self._getShapeNodes(ops) - pns = self._getPoseNodes(ops) cc = self._getCtrlNodes(ops) - if not create and (not sns or not pns or not ops or not cc): + if not create and (not sns or not ops or not cc): types = [] if not sns: types.append("blendShape") - if not pns and self.hasPoseNode: - types.append("blendPose") if not ops: types.append("simplex_maya") if not cc: @@ -576,17 +585,14 @@ def loadNodes(self, simp, thing, create=True, pBar=None): self.ctrl = cc[0] if cc else self._createControlNode(self.name, self.op) self.shapeNode = sns[0] if sns else self._createShapeNode(self.name, self.mesh) - cmds.connectAttr( - "{0}.{1}".format(self.shapeNode, "message"), - "{0}.{1}".format(self.op, "shapeMsg"), - ) + if not cmds.isConnected(f"{self.shapeNode}.message", f"{self.op}.shapeMsg"): + cmds.connectAttr(f"{self.shapeNode}.message", f"{self.op}.shapeMsg") if self.hasPoseNode: + pns = self._getPoseNodes(ops) self.poseNode = pns[0] if pns else self._createPoseNode(self.name) - cmds.connectAttr( - "{0}.{1}".format(self.poseNode, "message"), - "{0}.{1}".format(self.op, "poseMsg"), - ) + if not cmds.isConnected(f"{self.poseNode}.message", f"{self.op}.poseMsg"): + cmds.connectAttr(f"{self.poseNode}.message", f"{self.op}.poseMsg") def getShapeThing(self, shapeName): """ @@ -1247,6 +1253,17 @@ def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): abcSample = OPolyMeshSchemaSample(shpVerts, faces, counts) schema.set(abcSample) + def deleteObj(self, thing): + """Delete the given object""" + cmds.delete(thing) + + def exportMesh(self, mesh, path): + """Export a mesh to the given path""" + faces, counts, uvs = self.getAbcFaces(mesh) + shape = self.getNumpyShape(mesh) + name = mesh.split('|')[-1] + buildAbc(path, shape, faces, counts, uvs, name=name) + # Revision tracking def getRevision(self): """ """ @@ -1553,9 +1570,7 @@ def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0 # Shift the extracted shape to the side cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) - - if live: - self.connectShape(shape, extracted, live, delete=False) + self.connectShape(shape, extracted, live, delete=False) return extracted @@ -1587,8 +1602,7 @@ def extractShape(self, shape, live=True, offset=10.0): )[0] # Shift the extracted shape to the side - cmds.xform(extracted, relative=True, translation=[offset, 0, 0]) - + cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) if live: self.connectShape(shape, extracted, live, delete=False) return extracted @@ -1622,8 +1636,13 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): attrName = cmds.attributeName(shape.thing, long=True) mesh = "{0}_Extract".format(attrName) - if not cmds.objExists(mesh): + chk = cmds.ls(mesh) + if not chk: return + if len(chk) > 1: + msg = "Multiple objects with the same name found in file:\n" + msg += '\n'.join(chk) + raise ValueError(msg) index = self.getShapeIndex(shape) tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) @@ -1639,6 +1658,7 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): if not live: cmds.disconnectAttr(outAttr, inAttr) + if delete: cmds.delete(mesh) @@ -2103,18 +2123,31 @@ def _clearShapes(self, item, doOrig=False): aname = cmds.ls(item, long=1)[0] shapes = cmds.ls(cmds.listRelatives(item, shapes=1), long=1) baseName = aname.split("|")[-1] + baseName, digits = split_trailing_digits(baseName) - primary = "{0}|{1}Shape".format(aname, baseName) - orig = "{0}|{1}ShapeOrig".format(aname, baseName) + primary = "{0}|{1}Shape{2}".format(aname, baseName, digits) + origs = [] + others = [] for shape in shapes: + base, digits = split_trailing_digits(shape) + if base.endswith('Orig'): + origs.append(shape) + else: + others.append(shape) + + for shape in others: if shape == primary: continue - elif shape == orig: - if doOrig: - cmds.delete(shape) - else: - cmds.delete(shape) + cmds.delete(shape) + + if doOrig: + cmds.delete(origs) + else: + # Don't delete the first orig + if len(origs) > 1: + origs = sorted(origs) + cmds.delete(origs[1:]) @undoable def forceRebuildSliderConnections(self): @@ -2139,14 +2172,6 @@ def _rebuildSliderConnections(self): cmds.connectAttr(thing, self.op + ".sliders[{0}]".format(i)) # Combos - def _getIncomingShapeConnection(self, shape): - """Convenience function to get the transform of the node connected into a shape""" - index = self.getShapeIndex(shape) - tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) - inAttr = "{0}.inputTargetItem[6000].inputGeomTarget".format(tgn) - inCnx = cmds.listConnections(inAttr, destination=False) - return inCnx[0] if inCnx else None - def _reparentDeltaShapes(self, par, nodeDict, bsNode, toDelete=None): """Reparent and clean up a single-transform delta system @@ -2348,8 +2373,9 @@ def extractTraversalShape(self, trav, shape, live=True, offset=10.0): ) extracted = extracted[0] self._clearShapes(extracted) - cmds.xform(extracted, relative=True, translation=[offset, 0, 0]) - self.connectTraversalShape(trav, shape, extracted, live=live, delete=False) + cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) + if live: + self.connectTraversalShape(trav, shape, extracted, live=live, delete=False) cmds.select(extracted) return extracted @@ -2378,21 +2404,19 @@ def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False) attrName = cmds.attributeName(shape.thing, long=True) mesh = "{0}_Extract".format(attrName) - if not cmds.objExists(mesh): + chk = cmds.ls(mesh) + if not chk: return + if len(chk) > 1: + msg = "Multiple objects with the same name found in file:\n" + msg += '\n'.join(chk) + raise ValueError(msg) shapeIdx = trav.prog.getShapeIndex(shape) tVal = trav.prog.pairs[shapeIdx].value + delta = self._createTravDelta(trav, mesh, tVal) - inCnx = self._getIncomingShapeConnection(shape) - - # I can't really check if an expected object is connected - # because maya allows multiple objects with the same name - # but I can print a warning if something's already there - if inCnx: - print(f"Traversal already has an incoming shape connection from {inCnx}") - else: - delta = self._createTravDelta(trav, mesh, tVal) + if live: self.connectShape(shape, delta, live, delete) if delete: @@ -2513,9 +2537,13 @@ def extractComboShape(self, combo, shape, live=True, offset=10.0): extracted = cmds.duplicate( self.mesh, name="{0}_Extract".format(shape.name) )[0] + self._clearShapes(extracted) - cmds.xform(extracted, relative=True, translation=[offset, 0, 0]) - self.connectComboShape(combo, shape, extracted, live=live, delete=False) + cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) + + if live: + self.connectComboShape(combo, shape, extracted, live=live, delete=False) + cmds.select(extracted) return extracted @@ -2544,20 +2572,20 @@ def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): attrName = cmds.attributeName(shape.thing, long=True) mesh = "{0}_Extract".format(attrName) - if not cmds.objExists(mesh): + chk = cmds.ls(mesh) + if not chk: return + if len(chk) > 1: + msg = "Multiple objects with the same name found in file:\n" + msg += '\n'.join(chk) + raise ValueError(msg) + mesh = chk[0] progShapeIdx = combo.prog.getShapeIndex(shape) tVal = combo.prog.pairs[progShapeIdx].value - inCnx = self._getIncomingShapeConnection(shape) - # I can't really check if an expected object is connected - # because maya allows multiple objects with the same name - # but I can print a warning if something's already there - if inCnx: - print(f"Combo already has an incoming shape connection from {inCnx}") - else: - delta = self._createComboDelta(combo, mesh, tVal) + delta = self._createComboDelta(combo, mesh, tVal) + if live: self.connectShape(shape, delta, live, delete) if delete: @@ -2772,6 +2800,8 @@ def getObjectByName(cls, name): objs = cmds.ls(name) if not objs: return None + if len(objs) > 1: + raise ValueError("Multiple objects with the same name found") return objs[0] @classmethod diff --git a/src/python/simplexui/menu/genericPlugins/_builtins.py b/src/python/simplexui/menu/genericPlugins/_builtins.py index 3238c6cf..8584943c 100644 --- a/src/python/simplexui/menu/genericPlugins/_builtins.py +++ b/src/python/simplexui/menu/genericPlugins/_builtins.py @@ -142,6 +142,11 @@ def registerSliderTree(window, clickIdx, indexes, menu): # on shape/slider extractShapeACT = menu.addAction("Extract") extractShapeACT.triggered.connect(self.shapeExtract) + + # on shape/slider + exportShapeACT = menu.addAction("Export Selected ...") + exportShapeACT.triggered.connect(self.shapeExport) + # on shape/slider connectShapeACT = menu.addAction("Connect By Name") connectShapeACT.triggered.connect(self.shapeConnect) @@ -228,6 +233,11 @@ def registerComboTree(window, clickIdx, indexes, menu): # combo or below extractShapeACT = menu.addAction("Extract") extractShapeACT.triggered.connect(self.shapeExtract) + + # combo or below + exportShapeACT = menu.addAction("Export Selected ...") + exportShapeACT.triggered.connect(self.shapeExport) + # combo or below connectShapeACT = menu.addAction("Connect By Name") connectShapeACT.triggered.connect(self.shapeConnect) diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 19a20c82..0ea7c44e 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -873,6 +873,72 @@ def shapeExtract(self): comboIdxs = self.uiComboTREE.getSelectedIndexes() return self.shapeIndexExtract(sliderIdxs + comboIdxs) + def shapeExport(self): + """Export meshes""" + sliderIdxs = self.uiSliderTREE.getSelectedIndexes() + comboIdxs = self.uiComboTREE.getSelectedIndexes() + return self.shapeIndexExport(sliderIdxs + comboIdxs) + + def shapeIndexExport(self, indexes): + """Export meshes + + Parameters + ---------- + indexes : [QModelIndex, ...] + A list of indexes to extract + """ + # Importing this here to keep people from getting confused + # You should use self._fileDialog which does compatibility + # stuff for file saving. Since I'm looking for a folder here + # I don't need to do that + from Qt.QtWidgets import QFileDialog + + if self.simplex is None: + return + + pref = Prefs() + defaultPath = str( + pref.restoreProperty("systemExportFolder", os.path.join(os.path.expanduser("~"))) + ) + outfld = QFileDialog.getExistingDirectory( + self, "Pick Export Folder", defaultPath + ) + if not outfld: + return + + pref.recordProperty("systemExportFolder", outfld) + pref.save() + + pairs = coerceIndexToChildType(indexes, ProgPair) + pairs = [i.model().itemFromIndex(i) for i in pairs] + pairs = makeUnique([i for i in pairs if not i.shape.isRest]) + pairs.sort(key=lambda x: naturalSortKey(x.shape.name)) + + # Set up the progress bar + pBar = QProgressDialog("Exporting Shapes", "Cancel", 0, 100, self) + pBar.setMaximum(len(pairs)) + + extension = '.abc' + # Do the extractions + for pair in pairs: + c = pair.prog.controller + extracted = c.extractShape(pair.shape, live=False) + path = os.path.join(outfld, c.name + extension) + if os.path.exists(path): + os.remove(path) + + self.simplex.DCC.exportMesh(extracted, path) + self.simplex.DCC.deleteObj(extracted) + + # ProgressBar + pBar.setValue(pBar.value() + 1) + pBar.setLabelText("Extracting:\n{0}".format(pair.shape.name)) + QApplication.processEvents() + if pBar.wasCanceled(): + return + + pBar.close() + def shapeIndexExtract(self, indexes, live=None): """Create meshes that are possibly live-connected to the shapes From 18fe3992edf1a61003dd4db71018c3d7d3f4267d Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 16 Sep 2025 12:12:34 -0700 Subject: [PATCH 19/25] Update required meson version to latest --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 37f24677..22dec37d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -65,6 +65,7 @@ jobs: - name: Build Maya uses: blurstudio/mayaModuleActions/mesonBuild@v1 with: + meson-version: 1.9.0 setup-args: > -Dmaya:maya_version=${{ matrix.maya }} -Dmaya:maya_devkit_base=${{ steps.get-devkit.outputs.devkit-path }} From a129a5417b00dcf4a35b5444f0ee16901cdf60d5 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 16 Sep 2025 12:33:45 -0700 Subject: [PATCH 20/25] Make versioning work with the meson python backend --- .github/workflows/main.yml | 36 +++++++++++++++++++++++++++--------- .gitignore | 7 +++++++ get_version.py | 38 ++++++++++++++++++++++++++++++++++++++ meson.build | 8 +++++--- 4 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 get_version.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 22dec37d..a0a993e2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,8 +52,11 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - run: git fetch --force --tags origin + - name: 'Checkout repo' + uses: actions/checkout@v5 + + - name: 'ACTUALLY get tags' + run: git fetch --force --tag - name: Get Maya Devkit id: get-devkit @@ -93,14 +96,21 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - run: git fetch --force --tags origin + - name: 'Checkout repo' + uses: actions/checkout@v5 + + - name: 'ACTUALLY get tags' + run: git fetch --force --tags origin + - name: 'Get Previous tag' id: previoustag uses: "WyriHaximus/github-action-get-previous-tag@v1" with: fallback: v0.0.1 + - name: 'Write version.txt' + run: echo "${{ steps.previoustag.outputs.tag }}" > version.txt + - name: Get pyver macos-latest if: ${{ matrix.os == 'macos-latest' }} shell: bash @@ -113,8 +123,8 @@ jobs: if: ${{ matrix.os == 'ubuntu-latest' }} shell: bash run: | - echo "PY_VER=3.7" >> $GITHUB_ENV - echo "PY_VER_FLAT=37" >> $GITHUB_ENV + echo "PY_VER=3.9" >> $GITHUB_ENV + echo "PY_VER_FLAT=39" >> $GITHUB_ENV echo "PLAT_TAG=manylinux_2_17_x86_64" >> $GITHUB_ENV - name: Get pyver windows-latest @@ -130,10 +140,14 @@ jobs: with: python-version: ${{ env.PY_VER }} - - name: Build Wheel + - name: Install pip packages shell: bash run: | python -m pip install -U build wheel + + - name: Build Wheel + shell: bash + run: | python -m build --wheel for PY_WHEEL in dist/*.whl do @@ -152,8 +166,12 @@ jobs: needs: [compile_plugin, compile_python] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - run: git fetch --force --tags origin + - name: 'Checkout repo' + uses: actions/checkout@v5 + + - name: 'ACTUALLY get tags' + run: git fetch --force --tags origin + - name: 'Get Previous tag' id: previoustag uses: "WyriHaximus/github-action-get-previous-tag@v1" diff --git a/.gitignore b/.gitignore index 18c32a4e..125cd6dc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,13 @@ ehthumbs.db # Release zipfile Simplex.zip +# Auto-generated version files +version.txt +_version.py + +# clang compile commands +compile_commands.json + # Build folders Maya20*/ **/build/ diff --git a/get_version.py b/get_version.py new file mode 100644 index 00000000..b19a9045 --- /dev/null +++ b/get_version.py @@ -0,0 +1,38 @@ +import os +import subprocess +import sys + +VERSION_FILE = "version.txt" + + +def main(srcroot, gitpath): + """ + When building a python wheel, the build system copies the entire source tree into a new folder + but it doesn't seem to include git tags, or it doesn't check out the git tags, or *something* + So I'm writing out a version.txt file that this function will read and pass along to the + meson project() function call + """ + # If version.txt already exists, use it + src_file = os.path.join(srcroot, VERSION_FILE) + if os.path.exists(src_file): + with open(src_file) as f: + v = f.read().strip() + else: + # Otherwise, try git describe + try: + # fmt: off + v = subprocess.check_output( + [gitpath, "describe", "--tags", "--always", "--match", "v[0-9]*", "--abbrev=0"], + text=True, + ).strip() + # fmt: on + except Exception: + v = "0.0.1" # fallback if not in a git repo + + print(v) + + +if __name__ == "__main__": + srcroot = sys.argv[1] + gitpath = sys.argv[2] + sys.exit(main(srcroot, gitpath)) diff --git a/meson.build b/meson.build index 58128086..767f3972 100644 --- a/meson.build +++ b/meson.build @@ -1,12 +1,14 @@ project( 'Simplex', 'cpp', - meson_version : '>=1.6.0', + meson_version : '>=1.7.0', version : configure_file( capture : true, command : [ - find_program('git', native: true, required: true), - 'describe', '--tags', '--always', '--match', 'v[0-9]*', '--abbrev=0' + find_program('python', required: true), + meson.project_source_root() / 'get_version.py', + meson.project_source_root(), + find_program('git', native: true, required: true).full_path(), ], output : 'version.txt', ), From 6916e6ce5b732021445da5db682bf9a11b37629a Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 16 Sep 2025 16:56:35 -0700 Subject: [PATCH 21/25] Add a python script build target for building the scripts folder for mod files --- .github/workflows/main.yml | 21 +++++---- meson.options | 1 + quick_compile.bat | 3 +- src/python/meson.build | 58 ++++++++++++----------- src/python/simplexui/meson.build | 81 -------------------------------- 5 files changed, 46 insertions(+), 118 deletions(-) delete mode 100644 src/python/simplexui/meson.build diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a0a993e2..ea28bf05 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,13 +9,6 @@ on: pull_request: branches: [ master ] -# matrix: -# maya: [2024] -# os: [macos-latest, ubuntu-latest, windows-latest] -# include: -# - maya: 2024 -# update: 2 - jobs: compile_plugin: strategy: @@ -166,18 +159,28 @@ jobs: needs: [compile_plugin, compile_python] runs-on: ubuntu-latest steps: - - name: 'Checkout repo' + - name: Checkout repo uses: actions/checkout@v5 - name: 'ACTUALLY get tags' run: git fetch --force --tags origin - - name: 'Get Previous tag' + - name: Get Previous tag id: previoustag uses: "WyriHaximus/github-action-get-previous-tag@v1" with: fallback: v0.0.1 + - name: Build the module scripts folder + uses: blurstudio/mayaModuleActions/mesonBuild@v1 + with: + meson-version: 1.9.0 + setup-args: > + -Dmaya_build=false + -Dpython_build=true + -Dpython_script_build=true + install-args: --skip-subprojects + - name: Package uses: blurstudio/mayaModuleActions/packageMayaModule@v1 with: diff --git a/meson.options b/meson.options index 167b94d1..36793e6c 100644 --- a/meson.options +++ b/meson.options @@ -1,3 +1,4 @@ option('maya_build', type : 'boolean', value : true) option('python_build', type : 'boolean', value : true) option('python_wheel_build', type : 'boolean', value : false) +option('python_script_build', type : 'boolean', value : false) diff --git a/quick_compile.bat b/quick_compile.bat index d479a16c..cf56b98f 100644 --- a/quick_compile.bat +++ b/quick_compile.bat @@ -13,8 +13,9 @@ if not exist %BUILDDIR%\ ( meson setup %BUILDDIR% ^ -Dmaya:maya_version=%MAYA_VERSION% ^ -Dmaya:maya_devkit_base=D:\Autodesk\MayaDev\Maya2026_2\devkitBase ^ - -Dmaya_build=true ^ + -Dmaya_build=false ^ -Dpython_build=true ^ + -Dpython_script_build=true ^ --buildtype %BUILDTYPE% --vsenv --backend %BACKEND% ) diff --git a/src/python/meson.build b/src/python/meson.build index 5955ed9f..de66d04d 100644 --- a/src/python/meson.build +++ b/src/python/meson.build @@ -10,43 +10,47 @@ if get_option('buildtype') == 'debug' lapi = '' endif - py_wheel_build = get_option('python_wheel_build') +py_script_build = get_option('python_script_build') +if py_script_build + pyinstall_dir = meson.project_source_root() / 'scripts' +elif py_wheel_build + pyinstall_dir = py.get_install_dir() +else + pyinstall_dir = meson.project_source_root() / 'output_Python' +endif -if py_wheel_build - # Only install the .py files if we're building a wheel +if py_script_build or py_wheel_build + install_subdir( + 'simplexui', + install_dir: meson.project_source_root() / 'scripts', + exclude_files: ['_version.py.in', 'meson.build'], + ) fs = import('fs') - if fs.is_file('simplexui/_version.py') - message('Using existing _version.py') - py.install_sources( - 'simplexui/_version.py', - subdir:'simplexui', - ) - else + if not fs.is_file('simplexui/_version.py') version_py = configure_file( input: 'simplexui/_version.py.in', output: '_version.py', - install_dir: py.get_install_dir() / 'simplexui', + install_dir: meson.project_source_root() / 'scripts' / 'simplexui', configuration : conf_data, ) - py.install_sources( - version_py, - subdir:'simplexui', - ) endif - subdir('simplexui') - install_kwargs = {'subdir' : 'simplexui'} -else - install_kwargs = {'install_dir' : meson.global_source_root() / 'output_Python'} endif -pysimplex = py.extension_module( - 'pysimplex', - pysimplex_files, - dependencies : [simplexlib_dep, rapidjson_dep], - install: true, - limited_api : lapi, - kwargs : install_kwargs, -) +if not py_script_build + if py_wheel_build + install_kwargs = {'subdir' : 'simplexui'} + else + install_kwargs = {'install_dir' : pyinstall_dir} + endif + pysimplex = py.extension_module( + 'pysimplex', + pysimplex_files, + dependencies : [simplexlib_dep, rapidjson_dep], + install: true, + limited_api : lapi, + kwargs : install_kwargs, + ) +endif diff --git a/src/python/simplexui/meson.build b/src/python/simplexui/meson.build deleted file mode 100644 index 4c7fd071..00000000 --- a/src/python/simplexui/meson.build +++ /dev/null @@ -1,81 +0,0 @@ - -py.install_sources( - '__init__.py', - 'channelBox.py', - 'comboCheckDialog.py', - 'dragFilter.py', - 'falloffDialog.py', - 'interfaceModel.py', - 'interfaceModelTrees.py', - 'main.pyw', - 'simplexDialog.py', - 'travCheckDialog.py', - 'traversalDialog.py', - 'utils.py', - 'commands/__init__.py', - 'commands/alembicCommon.py', - 'commands/applyCorrectives.py', - 'commands/correctiveInterface.py', - 'commands/expandedExport.py', - 'commands/hdf5Convert.py', - 'commands/mayaCorrectiveInterface.py', - 'commands/mayatonumpy.py', - 'commands/mesh.py', - 'commands/numpytoimath.py', - 'commands/poseblendlib.py', - 'commands/reorderSimplexPoints.py', - 'commands/rigidAlign.py', - 'commands/smpxBlend.py', - 'commands/unsubdivide.py', - 'commands/uvTransfer.py', - 'img/ChefHead.png', - 'img/frozen.png', - 'interface/__init__.py', - 'interface/dummyInterface.py', - 'interface/mayaInterface.py', - 'items/__init__.py', - 'items/accessor.py', - 'items/combo.py', - 'items/falloff.py', - 'items/group.py', - 'items/progression.py', - 'items/shape.py', - 'items/simplex.py', - 'items/slider.py', - 'items/stack.py', - 'items/traversal.py', - 'menu/__init__.py', - 'menu/dummyPlugins/__init__.py', - 'menu/genericPlugins/__init__.py', - 'menu/genericPlugins/_builtins.py', - 'menu/genericPlugins/checkPossibleCombos.py', - 'menu/genericPlugins/exportSplit.py', - 'menu/genericPlugins/showFalloffs.py', - 'menu/genericPlugins/showTraversals.py', - 'menu/genericPlugins/simplexUvTransfer.py', - 'menu/genericPlugins/unsubdivide.py', - 'menu/mayaPlugins/__init__.py', - 'menu/mayaPlugins/exportOther.py', - 'menu/mayaPlugins/extractProgressives.py', - 'menu/mayaPlugins/freezeCombo.py', - 'menu/mayaPlugins/generateShapeIncrementals.py', - 'menu/mayaPlugins/importObjs.py', - 'menu/mayaPlugins/linearizeTraversal.py', - 'menu/mayaPlugins/makeShelfBtn.py', - 'menu/mayaPlugins/relaxToSelection.py', - 'menu/mayaPlugins/reloadDefinition.py', - 'menu/mayaPlugins/snapToNeutral.py', - 'menu/mayaPlugins/softSelectToCluster.py', - 'menu/mayaPlugins/tweakMix.py', - 'menu/mayaPlugins/updateRestShape.py', - 'ui/comboCheckDialog.ui', - 'ui/falloffDialog.ui', - 'ui/simplexDialog.ui', - 'ui/travCheckDialog.ui', - 'ui/traversalDialog.ui', - - subdir : 'simplexui', - preserve_path: true, -) - - From 170b615f4a4d59313cf141dd76b62d15608748f1 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 16 Sep 2025 17:06:50 -0700 Subject: [PATCH 22/25] Test with fewer maya versions --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ea28bf05..021d91dc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,7 +13,7 @@ jobs: compile_plugin: strategy: matrix: - maya: [2022, 2023, 2024, 2025, 2026] + maya: [2025, 2026] os: [macos-13, macos-latest, ubuntu-latest, windows-latest] include: # Add the maya update versions here From 443b7d4e9b38ed2cb6a960a6d426eee3705411a4 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 16 Sep 2025 17:11:11 -0700 Subject: [PATCH 23/25] Set the actual install dirs --- .github/workflows/main.yml | 3 +-- src/python/meson.build | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 021d91dc..eb6d5be8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,6 @@ name: build on: push: - branches: [ master ] tags: - v* pull_request: @@ -13,7 +12,7 @@ jobs: compile_plugin: strategy: matrix: - maya: [2025, 2026] + maya: [2022, 2023, 2024, 2025, 2026] os: [macos-13, macos-latest, ubuntu-latest, windows-latest] include: # Add the maya update versions here diff --git a/src/python/meson.build b/src/python/meson.build index de66d04d..243c0736 100644 --- a/src/python/meson.build +++ b/src/python/meson.build @@ -24,7 +24,7 @@ endif if py_script_build or py_wheel_build install_subdir( 'simplexui', - install_dir: meson.project_source_root() / 'scripts', + install_dir: pyinstall_dir, exclude_files: ['_version.py.in', 'meson.build'], ) fs = import('fs') @@ -32,7 +32,7 @@ if py_script_build or py_wheel_build version_py = configure_file( input: 'simplexui/_version.py.in', output: '_version.py', - install_dir: meson.project_source_root() / 'scripts' / 'simplexui', + install_dir: pyinstall_dir / 'simplexui', configuration : conf_data, ) endif From 8d6187ce328d3128740654ed76220168b09dd7be Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 16 Sep 2025 17:43:12 -0700 Subject: [PATCH 24/25] Windows python on 3.9 --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index eb6d5be8..a445f873 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -123,8 +123,8 @@ jobs: if: ${{ matrix.os == 'windows-latest' }} shell: bash run: | - echo "PY_VER=3.7" >> $GITHUB_ENV - echo "PY_VER_FLAT=37" >> $GITHUB_ENV + echo "PY_VER=3.9" >> $GITHUB_ENV + echo "PY_VER_FLAT=39" >> $GITHUB_ENV echo "PLAT_TAG=win_amd64" >> $GITHUB_ENV - name: Get an older python version From a8c789db54269d2340477491ed27a7003166501d Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 16 Sep 2025 18:09:53 -0700 Subject: [PATCH 25/25] Update to latest rapidjson because python compile hits a bug --- quick_compile.bat | 9 ++++----- subprojects/packagefiles/rapidjson/meson.build | 4 ++++ subprojects/rapidjson.wrap | 15 +++++---------- 3 files changed, 13 insertions(+), 15 deletions(-) create mode 100644 subprojects/packagefiles/rapidjson/meson.build diff --git a/quick_compile.bat b/quick_compile.bat index cf56b98f..9bade032 100644 --- a/quick_compile.bat +++ b/quick_compile.bat @@ -1,6 +1,6 @@ setlocal -SET MAYA_VERSION=2026 +SET MAYA_VERSION=2024 REM "vs" "ninja" REM use VS for the debugger, otherwise use NINJA REM Until I figure out how to debug using nvim @@ -12,10 +12,9 @@ SET BUILDDIR=mayabuild_%BUILDTYPE%_%MAYA_VERSION%_%BACKEND% if not exist %BUILDDIR%\ ( meson setup %BUILDDIR% ^ -Dmaya:maya_version=%MAYA_VERSION% ^ - -Dmaya:maya_devkit_base=D:\Autodesk\MayaDev\Maya2026_2\devkitBase ^ - -Dmaya_build=false ^ - -Dpython_build=true ^ - -Dpython_script_build=true ^ + -Dmaya_build=true ^ + -Dpython_build=false ^ + -Dpython_script_build=false ^ --buildtype %BUILDTYPE% --vsenv --backend %BACKEND% ) diff --git a/subprojects/packagefiles/rapidjson/meson.build b/subprojects/packagefiles/rapidjson/meson.build new file mode 100644 index 00000000..5a58fcdb --- /dev/null +++ b/subprojects/packagefiles/rapidjson/meson.build @@ -0,0 +1,4 @@ +project('rapidjson', 'cpp', version: '1.1.0', license: 'MIT') + +rapidjson_inc = include_directories('include') +rapidjson_dep = declare_dependency(include_directories: rapidjson_inc) diff --git a/subprojects/rapidjson.wrap b/subprojects/rapidjson.wrap index 50b83466..a6072631 100644 --- a/subprojects/rapidjson.wrap +++ b/subprojects/rapidjson.wrap @@ -1,13 +1,8 @@ -[wrap-file] -directory = rapidjson-1.1.0 -source_url = https://github.com/Tencent/rapidjson/archive/v1.1.0.tar.gz -source_filename = rapidjson-1.1.0.tar.gz -source_hash = bf7ced29704a1e696fbccf2a2b4ea068e7774fa37f6d7dd4039d0787f8bed98e -patch_filename = rapidjson_1.1.0-2_patch.zip -patch_url = https://wrapdb.mesonbuild.com/v2/rapidjson_1.1.0-2/get_patch -patch_hash = c1480d0ecef09dbaa4b4d85d86090205386fb2c7e87f4f158b20dbbda14c9afc -source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/rapidjson_1.1.0-2/rapidjson-1.1.0.tar.gz -wrapdb_version = 1.1.0-2 +[wrap-git] +url = https://github.com/Tencent/rapidjson.git +revision = 24b5e7a8b27f42fa16b96fc70aade9106cf7102f + +patch_directory = rapidjson [provide] rapidjson = rapidjson_dep