diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d5bb421f..a445f873 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,24 +3,16 @@ name: build on: push: - branches: [ master ] tags: - v* pull_request: branches: [ master ] -# matrix: -# maya: [2024] -# os: [macos-latest, ubuntu-latest, windows-latest] -# include: -# - maya: 2024 -# update: 2 - 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 +23,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,13 +37,18 @@ jobs: maya: 2024 - os: macos-13 maya: 2025 + - os: macos-13 + maya: 2026 fail-fast: false 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 @@ -61,6 +60,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 }} @@ -88,39 +88,43 @@ 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 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 if: ${{ matrix.os == 'ubuntu-latest' }} shell: bash run: | - echo "PY_VER=3.7" >> $GITHUB_ENV - echo "PY_VER_FLAT=37" >> $GITHUB_ENV - echo "PY_EXT=so" >> $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 if: ${{ matrix.os == 'windows-latest' }} shell: bash run: | - echo "PY_VER=3.7" >> $GITHUB_ENV - echo "PY_VER_FLAT=37" >> $GITHUB_ENV - echo "PY_EXT=pyd" >> $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 @@ -128,35 +132,20 @@ 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: Install pip packages + shell: bash + run: | + python -m pip install -U build wheel - 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 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: @@ -169,14 +158,28 @@ jobs: needs: [compile_plugin, compile_python] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - run: git fetch --force --tags origin - - name: 'Get Previous tag' + - 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: 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/.gitignore b/.gitignore index dfa65758..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/ @@ -38,6 +45,17 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk +# clang LSP +.cache + +# Known subprojects +**/Eigen-*/ +**/rapidjson-*/ +subprojects/packagecache/ + +# python wheel dist folder +dist/ + # ========================= # Operating System Files # ========================= 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. 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 52228c36..767f3972 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,19 @@ -project('Simplex', 'cpp', default_options: ['cpp_std=c++20']) - +project( + 'Simplex', + 'cpp', + meson_version : '>=1.7.0', + version : configure_file( + capture : true, + command : [ + 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', + ), + default_options: ['cpp_std=c++20'], +) maya_build = get_option('maya_build') python_build = get_option('python_build') @@ -8,6 +22,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/meson.options b/meson.options index 17700b32..36793e6c 100644 --- a/meson.options +++ b/meson.options @@ -1,2 +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/pyproject.toml b/pyproject.toml index f83b8bf7..f469cf6c 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,36 +18,27 @@ classifiers = [ "Operating System :: MacOS :: MacOS X", ] -dependencies = [ - "six", - "Qt.py", -] +dependencies = ["Qt.py"] [project.urls] "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", +[project.optional-dependencies] +dev = [ + "tox", + "covdefaults", + "coverage", + "ruff", ] -[tool.hatch.version] -path = "scripts/simplexui/__init__.py" + +[build-system] +build-backend = "mesonpy" +requires = ["meson-python>=0.15.0", "meson >= 1.6.0"] + +[tool.meson-python.args] +setup = ['-Dmaya_build=false', '-Dpython_build=true', '-Dpython_wheel_build=true'] +install = ['--skip-subprojects'] [tool.ruff] # Exclude a variety of commonly ignored directories. @@ -88,22 +75,15 @@ exclude = [ "shared-venv", "site-packages", "venv", + "subprojects", ] -line-length = 100 +line-length = 88 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", @@ -126,7 +106,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" @@ -136,4 +116,3 @@ skip-magic-trailing-comma = false # Like Black, automatically detect the appropriate line ending. line-ending = "auto" - diff --git a/quick_compile.bat b/quick_compile.bat index 0e2b3944..9bade032 100644 --- a/quick_compile.bat +++ b/quick_compile.bat @@ -10,11 +10,16 @@ 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% + meson setup %BUILDDIR% ^ + -Dmaya:maya_version=%MAYA_VERSION% ^ + -Dmaya_build=true ^ + -Dpython_build=false ^ + -Dpython_script_build=false ^ + --buildtype %BUILDTYPE% --vsenv --backend %BACKEND% ) if exist %BUILDDIR%\ ( - meson compile -C %BUILDDIR% + meson compile -C %BUILDDIR% -j 8 meson install --skip-subprojects -C %BUILDDIR% ) diff --git a/scripts/simplexui/commands/buildIceXML.py b/scripts/simplexui/commands/buildIceXML.py deleted file mode 100644 index 9b0f6e51..00000000 --- a/scripts/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/scripts/simplexui/commands/setDelta.xsicompound b/scripts/simplexui/commands/setDelta.xsicompound deleted file mode 100644 index 04469390..00000000 --- a/scripts/simplexui/commands/setDelta.xsicompound +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/scripts/simplexui/commands/xsiCorrectiveInterface.py b/scripts/simplexui/commands/xsiCorrectiveInterface.py deleted file mode 100644 index c760234d..00000000 --- a/scripts/simplexui/commands/xsiCorrectiveInterface.py +++ /dev/null @@ -1,136 +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 __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): - """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/scripts/simplexui/interface/xsiInterface.py b/scripts/simplexui/interface/xsiInterface.py deleted file mode 100644 index 7d0fae86..00000000 --- a/scripts/simplexui/interface/xsiInterface.py +++ /dev/null @@ -1,2944 +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 -from __future__ import absolute_import, print_function - -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 -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 -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 six.iteritems(uvD): - 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 six.iteritems(uvD): - 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 six.iteritems(nc.inputPorts): - 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/scripts/simplexui/interfaceModel_Test.py b/scripts/simplexui/interfaceModel_Test.py deleted file mode 100644 index c8f0ad92..00000000 --- a/scripts/simplexui/interfaceModel_Test.py +++ /dev/null @@ -1,259 +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 . - -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 -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/scripts/simplexui/menu/xsiPlugins/__init__.py b/scripts/simplexui/menu/xsiPlugins/__init__.py deleted file mode 100644 index c90769bd..00000000 --- a/scripts/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/simplex_maya_installer.py b/simplex_maya_installer.py index 782250ca..4b1f3065 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" @@ -211,22 +212,20 @@ 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() 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") @@ -239,4 +238,3 @@ def onMayaDroppedPythonFile(obj): message="Simplex installation complete", button=["OK"], ) - 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..243c0736 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,47 @@ if get_option('buildtype') == 'debug' lapi = '' endif -pysimplex = py_inst.extension_module( - 'pysimplex', - pysimplex_files, - dependencies : simplexlib_dep, - install: true, - install_dir : meson.global_source_root() / 'output_Python', - limited_api : lapi, -) +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_script_build or py_wheel_build + install_subdir( + 'simplexui', + install_dir: pyinstall_dir, + exclude_files: ['_version.py.in', 'meson.build'], + ) + fs = import('fs') + if not fs.is_file('simplexui/_version.py') + version_py = configure_file( + input: 'simplexui/_version.py.in', + output: '_version.py', + install_dir: pyinstall_dir / 'simplexui', + configuration : conf_data, + ) + endif +endif + +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/scripts/simplexui/__init__.py b/src/python/simplexui/__init__.py similarity index 95% rename from scripts/simplexui/__init__.py rename to src/python/simplexui/__init__.py index 4f2222fe..25d066cb 100644 --- a/scripts/simplexui/__init__.py +++ b/src/python/simplexui/__init__.py @@ -15,11 +15,10 @@ # 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__ # noqa: F401 SIMPLEX_UI = None SIMPLEX_UI_ROOT = None -__version__ = "v0.0.1-dev" def runSimplexUI(): 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 77% rename from scripts/simplexui/channelBox.py rename to src/python/simplexui/channelBox.py index 3d826b81..5633c9ea 100644 --- a/scripts/simplexui/channelBox.py +++ b/src/python/simplexui/channelBox.py @@ -15,14 +15,12 @@ # 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 """ # pylint:disable=unused-import,relative-import,missing-docstring,unused-argument,no-self-use -from __future__ import absolute_import, print_function - import os import sys @@ -53,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 @@ -97,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) @@ -113,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 @@ -302,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) @@ -342,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) @@ -386,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: @@ -407,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() @@ -542,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 @@ -632,77 +637,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() @@ -710,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) @@ -752,77 +751,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() @@ -831,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/scripts/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py similarity index 89% rename from scripts/simplexui/comboCheckDialog.py rename to src/python/simplexui/comboCheckDialog.py index 5f8cdd23..ce330848 100644 --- a/scripts/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 @@ -178,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() @@ -206,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): @@ -228,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))): @@ -280,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/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 88% rename from scripts/simplexui/commands/alembicCommon.py rename to src/python/simplexui/commands/alembicCommon.py index 427da6d2..30302e54 100644 --- a/scripts/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -15,14 +15,12 @@ # 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 -import six from alembic.Abc import IArchive, OArchive, OStringProperty from alembic.AbcGeom import ( GeometryScope, @@ -35,21 +33,17 @@ OXform, ) from imath import IntArray, UnsignedIntArray, V2f, V2fArray, V3fArray -from six.moves import range, zip 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=[]): +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 @@ -67,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) @@ -111,22 +107,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 +132,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 +288,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 +339,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 +428,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 +678,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, @@ -812,7 +758,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)) @@ -966,7 +912,7 @@ def buildSmpx( name=name, shapeSuffix="", transformSuffix="", - propDict=dict(simplex=jsString), + propDict={"simplex": jsString}, ogawa=ogawa, pBar=pBar, ) diff --git a/scripts/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py similarity index 95% rename from scripts/simplexui/commands/applyCorrectives.py rename to src/python/simplexui/commands/applyCorrectives.py index 1e630e64..226d408f 100644 --- a/scripts/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 @@ -220,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): @@ -306,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/scripts/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py similarity index 90% rename from scripts/simplexui/commands/correctiveInterface.py rename to src/python/simplexui/commands/correctiveInterface.py index e9245078..2470ee2e 100644 --- a/scripts/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: @@ -27,14 +22,9 @@ except ImportError: pass -try: - from .mayaCorrectiveInterface import getShiftValues, resetPose, setPose - - dcc = "maya" -except ImportError: - from .xsiCorrectiveInterface import getShiftValues, resetPose, setPose +from .mayaCorrectiveInterface import getShiftValues, resetPose, setPose - dcc = "xsi" +dcc = "maya" def getRefForPoses(mesh, poses, multipliers): @@ -197,14 +187,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: @@ -261,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/scripts/simplexui/commands/expandedExport.py b/src/python/simplexui/commands/expandedExport.py similarity index 92% rename from scripts/simplexui/commands/expandedExport.py rename to src/python/simplexui/commands/expandedExport.py index 551d9a18..1f7411e8 100644 --- a/scripts/simplexui/commands/expandedExport.py +++ b/src/python/simplexui/commands/expandedExport.py @@ -15,19 +15,16 @@ # 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 -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): @@ -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) @@ -294,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)) @@ -301,13 +301,13 @@ 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 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 +317,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 +342,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 @@ -429,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/scripts/simplexui/commands/hdf5Convert.py b/src/python/simplexui/commands/hdf5Convert.py similarity index 94% rename from scripts/simplexui/commands/hdf5Convert.py rename to src/python/simplexui/commands/hdf5Convert.py index 32ac5ab4..17194d0f 100644 --- a/scripts/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/scripts/simplexui/commands/mayaCorrectiveInterface.py b/src/python/simplexui/commands/mayaCorrectiveInterface.py similarity index 93% rename from scripts/simplexui/commands/mayaCorrectiveInterface.py rename to src/python/simplexui/commands/mayaCorrectiveInterface.py index bf34e019..f4258139 100644 --- a/scripts/simplexui/commands/mayaCorrectiveInterface.py +++ b/src/python/simplexui/commands/mayaCorrectiveInterface.py @@ -15,15 +15,12 @@ # 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 """ - -from __future__ import absolute_import +"""Get the corrective deltas from a rig in Maya""" 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 @@ -53,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/mayatonumpy.py b/src/python/simplexui/commands/mayatonumpy.py new file mode 100644 index 00000000..95f3f4ed --- /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/scripts/simplexui/commands/mesh.py b/src/python/simplexui/commands/mesh.py similarity index 98% rename from scripts/simplexui/commands/mesh.py rename to src/python/simplexui/commands/mesh.py index 4907f693..b6a8e23a 100644 --- a/scripts/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/numpytoimath.py b/src/python/simplexui/commands/numpytoimath.py new file mode 100644 index 00000000..4a16b230 --- /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) diff --git a/src/python/simplexui/commands/poseblendlib.py b/src/python/simplexui/commands/poseblendlib.py new file mode 100644 index 00000000..c23b9263 --- /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 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)) + 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/scripts/simplexui/commands/reorderSimplexPoints.py b/src/python/simplexui/commands/reorderSimplexPoints.py similarity index 93% rename from scripts/simplexui/commands/reorderSimplexPoints.py rename to src/python/simplexui/commands/reorderSimplexPoints.py index 0393e39f..82ca7eef 100644 --- a/scripts/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,9 +25,8 @@ 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 -from __future__ import absolute_import, print_function +# pylint:disable=wrong-import-position import json from .alembicCommon import buildSmpx, readSmpx diff --git a/scripts/simplexui/commands/rigidAlign.py b/src/python/simplexui/commands/rigidAlign.py similarity index 94% rename from scripts/simplexui/commands/rigidAlign.py rename to src/python/simplexui/commands/rigidAlign.py index 43d036a9..e51acb6a 100644 --- a/scripts/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/scripts/simplexui/commands/smpxBlend.py b/src/python/simplexui/commands/smpxBlend.py similarity index 94% rename from scripts/simplexui/commands/smpxBlend.py rename to src/python/simplexui/commands/smpxBlend.py index 80f9e328..d8c1b772 100644 --- a/scripts/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/scripts/simplexui/commands/unsubdivide.py b/src/python/simplexui/commands/unsubdivide.py similarity index 94% rename from scripts/simplexui/commands/unsubdivide.py rename to src/python/simplexui/commands/unsubdivide.py index 06d42453..26178ae7 100644 --- a/scripts/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 @@ -64,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)) @@ -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()) @@ -198,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) @@ -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 k, uNeigh in uNeighDict.items(): neighDict[k] = _align(neighDict[k], uNeigh, dWings) return neighDict, uNeighDict, edgeDict, uEdgeDict, borders @@ -601,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: @@ -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) @@ -784,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 @@ -938,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/scripts/simplexui/commands/uvTransfer.py b/src/python/simplexui/commands/uvTransfer.py similarity index 95% rename from scripts/simplexui/commands/uvTransfer.py rename to src/python/simplexui/commands/uvTransfer.py index eb5b807d..ff69dfbd 100644 --- a/scripts/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) @@ -558,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 = {} @@ -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/scripts/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py similarity index 89% rename from scripts/simplexui/dragFilter.py rename to src/python/simplexui/dragFilter.py index 5b208a0c..f53c4fdb 100644 --- a/scripts/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/scripts/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py similarity index 91% rename from scripts/simplexui/falloffDialog.py rename to src/python/simplexui/falloffDialog.py index 308116a7..5a5a53da 100644 --- a/scripts/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 @@ -84,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 @@ -155,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)) @@ -181,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) @@ -202,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) @@ -252,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) @@ -260,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() @@ -297,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) @@ -328,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) @@ -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/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 87% rename from scripts/simplexui/interface/__init__.py rename to src/python/simplexui/interface/__init__.py index c142e40d..d577438f 100644 --- a/scripts/simplexui/interface/__init__.py +++ b/src/python/simplexui/interface/__init__.py @@ -16,15 +16,13 @@ # 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 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 + +__all__ = ["DCC", "DISPATCH", "rootWindow", "undoContext"] diff --git a/scripts/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py similarity index 93% rename from scripts/simplexui/interface/dummyInterface.py rename to src/python/simplexui/interface/dummyInterface.py index a980eafd..3546163c 100644 --- a/scripts/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -16,15 +16,12 @@ # along with Simplex. If not, see . # pylint: disable=invalid-name, unused-argument -""" A placeholder interface that takes arguments and does nothing with them """ -from __future__ import absolute_import +"""A placeholder interface that takes arguments and does nothing with them""" 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 @@ -276,7 +273,7 @@ def postLoad(self, simp, preRet): pass def checkForErrors(self, window): - """ Check for any DCC specific errors + """Check for any DCC specific errors Parameters ---------- @@ -441,6 +438,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 @@ -475,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 @@ -634,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): @@ -668,7 +689,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/scripts/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py similarity index 83% rename from scripts/simplexui/interface/mayaInterface.py rename to src/python/simplexui/interface/mayaInterface.py index 010da719..54a01916 100644 --- a/scripts/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -16,22 +16,19 @@ # along with Simplex. If not, see . # pylint: disable=invalid-name -from __future__ import absolute_import - +from __future__ import annotations import json import re from contextlib import contextmanager -from ctypes import c_double, c_float from functools import wraps +from typing import TYPE_CHECKING 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 ..commands.alembicCommon import mkSampleVertexPoints, buildAbc from Qt import QtCore from Qt.QtCore import Signal from Qt.QtWidgets import ( @@ -42,15 +39,17 @@ QSplashScreen, ) +if TYPE_CHECKING: + from ..items.simplex import Simplex + 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 @@ -117,9 +116,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 ---------- @@ -169,8 +167,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) @@ -199,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): """ """ @@ -207,15 +213,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): # ''' @@ -274,7 +282,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) @@ -282,7 +289,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: @@ -290,7 +297,6 @@ def _removeExtraShapeNodes(cls, tfm): if keeper is not None: cmds.delete(todel) - def preLoad(self, simp, simpDict, create=True, pBar=None): """ @@ -326,21 +332,18 @@ 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: - 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.StandardButton.Yes + | QMessageBox.StandardButton.Cancel ) - btns = QMessageBox.Yes | QMessageBox.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)) @@ -379,7 +382,6 @@ def preLoad(self, simp, simpDict, create=True, pBar=None): cmds.undoInfo(state=True) raise - def postLoad(self, simp, preRet): """ @@ -396,7 +398,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,54 +410,33 @@ 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)) + 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 - - 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: @@ -465,20 +446,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 + + 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 - # Find the msg connected control object + def _getCtrlNodes(self, ops): ctrlCnx = [] for op in ops: ccnx = cmds.listConnections( @@ -488,76 +490,109 @@ 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) + cc = self._getCtrlNodes(ops) + + if not create and (not sns or not ops or not cc): + types = [] + if not sns: + types.append("blendShape") + 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") - cmds.connectAttr( - "{0}.{1}".format(self.ctrl, "solver"), - "{0}.{1}".format(self.op, "ctrlMsg"), - ) - else: - self.ctrl = ctrlCnx[0] + 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) + 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) + 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): """ @@ -593,9 +628,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 @@ -621,22 +663,28 @@ 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): """ @@ -679,7 +727,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: @@ -694,8 +742,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 @@ -705,7 +752,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 = [ @@ -761,10 +808,10 @@ 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 six.itervalues(shapeCnx): + for v in shapeCnx.values(): cmds.setAttr(v, 0.0) if pBar is not None: @@ -782,12 +829,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), @@ -814,7 +857,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: @@ -864,8 +907,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 @@ -959,41 +1002,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() @@ -1012,7 +1028,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()): @@ -1022,8 +1038,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() @@ -1119,7 +1135,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: @@ -1165,7 +1181,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) @@ -1237,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): """ """ @@ -1431,7 +1458,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 @@ -1460,11 +1487,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) @@ -1529,7 +1556,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 @@ -1542,10 +1569,8 @@ 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]) - - if live: - self.connectShape(shape, extracted, live, delete=False) + cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) + self.connectShape(shape, extracted, live, delete=False) return extracted @@ -1577,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 @@ -1612,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) @@ -1629,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) @@ -1835,7 +1865,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 @@ -2093,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): @@ -2160,7 +2203,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: @@ -2236,7 +2279,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): @@ -2249,7 +2292,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) @@ -2273,7 +2316,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 @@ -2311,18 +2354,17 @@ 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 - sliDict = {} for pair in trav.startPoint.pairs: sliDict[pair.slider] = [pair.value] 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) @@ -2331,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 @@ -2360,10 +2403,22 @@ 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) + + 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) - self.connectShape(shape, delta, live, delete) + + if live: + self.connectShape(shape, delta, live, delete) + if delete: cmds.delete(mesh) @@ -2406,7 +2461,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 @@ -2438,7 +2493,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 @@ -2471,7 +2526,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): @@ -2482,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 @@ -2512,15 +2571,28 @@ 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 + + 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 + delta = self._createComboDelta(combo, mesh, tVal) - self.connectShape(shape, delta, live, delete) + if live: + self.connectShape(shape, delta, live, delete) + if delete: cmds.delete(mesh) - @staticmethod - def setDisabled(op): + @classmethod + def setDisabled(cls, op): """ Parameters @@ -2542,8 +2614,8 @@ def setDisabled(op): helpers.append((prop, val)) return helpers - @staticmethod - def reEnable(helpers): + @classmethod + def reEnable(cls, helpers): """ Parameters @@ -2576,13 +2648,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 @@ -2598,8 +2670,8 @@ def getSimplexOperatorsByName(name): """ return cmds.ls(name, type="simplex_maya") - @staticmethod - def getSimplexOperatorsOnObject(thing): + @classmethod + def getSimplexOperatorsOnObject(cls, thing): """ Parameters @@ -2629,8 +2701,8 @@ def getSimplexOperatorsOnObject(thing): out.append(op) return out - @staticmethod - def getSimplexString(op): + @classmethod + def getSimplexString(cls, op): """ Parameters @@ -2646,8 +2718,8 @@ def getSimplexString(op): """ return cmds.getAttr(op + ".definition") - @staticmethod - def getSimplexStringOnThing(thing, systemName): + @classmethod + def getSimplexStringOnThing(cls, thing, systemName): """ Parameters @@ -2671,8 +2743,8 @@ def getSimplexStringOnThing(thing, systemName): return js return None - @staticmethod - def setSimplexString(op, val): + @classmethod + def setSimplexString(cls, op, val): """ Parameters @@ -2690,8 +2762,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 @@ -2710,8 +2782,8 @@ def selectCtrl(self): if self.ctrl: self.selectObject(self.ctrl) - @staticmethod - def getObjectByName(name): + @classmethod + def getObjectByName(cls, name): """ Parameters @@ -2728,10 +2800,12 @@ def getObjectByName(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] - @staticmethod - def getObjectName(thing): + @classmethod + def getObjectName(cls, thing): """ Parameters @@ -2747,13 +2821,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) @@ -2859,8 +2933,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) @@ -2886,8 +2960,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: @@ -2929,7 +3003,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: @@ -3168,8 +3242,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/scripts/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py similarity index 89% rename from scripts/simplexui/interfaceModel.py rename to src/python/simplexui/interfaceModel.py index 526d5fbe..e89370e9 100644 --- a/scripts/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, @@ -168,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 = [] @@ -391,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)): @@ -434,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 @@ -473,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 @@ -766,7 +768,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: @@ -788,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() @@ -829,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)) @@ -854,7 +857,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: @@ -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() @@ -941,7 +949,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: @@ -971,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: @@ -1000,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: @@ -1025,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/scripts/simplexui/interfaceModelTrees.py b/src/python/simplexui/interfaceModelTrees.py similarity index 93% rename from scripts/simplexui/interfaceModelTrees.py rename to src/python/simplexui/interfaceModelTrees.py index 45093226..f11d7439 100644 --- a/scripts/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 @@ -44,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 = [] @@ -81,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: @@ -311,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): @@ -408,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() @@ -416,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/scripts/simplexui/items/__init__.py b/src/python/simplexui/items/__init__.py similarity index 82% rename from scripts/simplexui/items/__init__.py rename to src/python/simplexui/items/__init__.py index 35f87532..0284cd84 100644 --- a/scripts/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/scripts/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py similarity index 94% rename from scripts/simplexui/items/accessor.py rename to src/python/simplexui/items/accessor.py index 7f53733f..c9c0d1fb 100644 --- a/scripts/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/scripts/simplexui/items/combo.py b/src/python/simplexui/items/combo.py similarity index 95% rename from scripts/simplexui/items/combo.py rename to src/python/simplexui/items/combo.py index 59b20807..4a040853 100644 --- a/scripts/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 @@ -159,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 @@ -179,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 @@ -244,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/scripts/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py similarity index 96% rename from scripts/simplexui/items/falloff.py rename to src/python/simplexui/items/falloff.py index 26f4ea59..f92a1d1a 100644 --- a/scripts/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/scripts/simplexui/items/group.py b/src/python/simplexui/items/group.py similarity index 94% rename from scripts/simplexui/items/group.py rename to src/python/simplexui/items/group.py index bcef2c23..f6ba556e 100644 --- a/scripts/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/scripts/simplexui/items/progression.py b/src/python/simplexui/items/progression.py similarity index 96% rename from scripts/simplexui/items/progression.py rename to src/python/simplexui/items/progression.py index a5967fa4..4b60f250 100644 --- a/scripts/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/scripts/simplexui/items/shape.py b/src/python/simplexui/items/shape.py similarity index 95% rename from scripts/simplexui/items/shape.py rename to src/python/simplexui/items/shape.py index bc7e5afc..421f4473 100644 --- a/scripts/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 @@ -54,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/scripts/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py similarity index 95% rename from scripts/simplexui/items/simplex.py rename to src/python/simplexui/items/simplex.py index 635f7700..0b3878db 100644 --- a/scripts/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -16,15 +16,10 @@ # 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 -import six -from six.moves import map, zip - try: import numpy as np except ImportError: @@ -121,7 +116,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 @@ -139,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)) @@ -526,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 @@ -632,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 @@ -1401,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 @@ -1583,7 +1601,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 @@ -1635,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 @@ -1644,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()] @@ -1673,7 +1690,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/scripts/simplexui/items/slider.py b/src/python/simplexui/items/slider.py similarity index 95% rename from scripts/simplexui/items/slider.py rename to src/python/simplexui/items/slider.py index 0882ed53..f49bf626 100644 --- a/scripts/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 @@ -55,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/scripts/simplexui/items/stack.py b/src/python/simplexui/items/stack.py similarity index 95% rename from scripts/simplexui/items/stack.py rename to src/python/simplexui/items/stack.py index d613d752..75d6d25c 100644 --- a/scripts/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/scripts/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py similarity index 94% rename from scripts/simplexui/items/traversal.py rename to src/python/simplexui/items/traversal.py index 831fbd90..40a56069 100644 --- a/scripts/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 @@ -123,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) @@ -373,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" ) @@ -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: @@ -735,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)) @@ -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))) @@ -897,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/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 95% rename from scripts/simplexui/menu/__init__.py rename to src/python/simplexui/menu/__init__.py index c070e361..7df9552e 100644 --- a/scripts/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 @@ -28,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/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 93% rename from scripts/simplexui/menu/genericPlugins/_builtins.py rename to src/python/simplexui/menu/genericPlugins/_builtins.py index b13e8163..8584943c 100644 --- a/scripts/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 @@ -68,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() @@ -81,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)) @@ -144,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) @@ -209,7 +212,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)) @@ -230,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/scripts/simplexui/menu/genericPlugins/checkPossibleCombos.py b/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py similarity index 97% rename from scripts/simplexui/menu/genericPlugins/checkPossibleCombos.py rename to src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py index 58f0e880..a890f550 100644 --- a/scripts/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/scripts/simplexui/menu/genericPlugins/exportSplit.py b/src/python/simplexui/menu/genericPlugins/exportSplit.py similarity index 97% rename from scripts/simplexui/menu/genericPlugins/exportSplit.py rename to src/python/simplexui/menu/genericPlugins/exportSplit.py index 0545cd03..bd05a97d 100644 --- a/scripts/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/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 98% rename from scripts/simplexui/menu/genericPlugins/simplexUvTransfer.py rename to src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py index 6982a8d6..7592d8ea 100644 --- a/scripts/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/scripts/simplexui/menu/genericPlugins/unsubdivide.py b/src/python/simplexui/menu/genericPlugins/unsubdivide.py similarity index 88% rename from scripts/simplexui/menu/genericPlugins/unsubdivide.py rename to src/python/simplexui/menu/genericPlugins/unsubdivide.py index 369a1491..5cb77168 100644 --- a/scripts/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 @@ -24,21 +22,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", @@ -59,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/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 97% rename from scripts/simplexui/menu/mayaPlugins/exportOther.py rename to src/python/simplexui/menu/mayaPlugins/exportOther.py index 1097ceef..502b98e1 100644 --- a/scripts/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/scripts/simplexui/menu/mayaPlugins/extractProgressives.py b/src/python/simplexui/menu/mayaPlugins/extractProgressives.py similarity index 98% rename from scripts/simplexui/menu/mayaPlugins/extractProgressives.py rename to src/python/simplexui/menu/mayaPlugins/extractProgressives.py index e40b90f8..e320ab89 100644 --- a/scripts/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/scripts/simplexui/menu/mayaPlugins/freezeCombo.py b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py similarity index 95% rename from scripts/simplexui/menu/mayaPlugins/freezeCombo.py rename to src/python/simplexui/menu/mayaPlugins/freezeCombo.py index 91dca12f..90652d57 100644 --- a/scripts/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 @@ -78,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: @@ -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 @@ -248,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/scripts/simplexui/menu/mayaPlugins/generateShapeIncrementals.py b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py similarity index 97% rename from scripts/simplexui/menu/mayaPlugins/generateShapeIncrementals.py rename to src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py index 2206e108..47bfd7ab 100644 --- a/scripts/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/scripts/simplexui/menu/mayaPlugins/importObjs.py b/src/python/simplexui/menu/mayaPlugins/importObjs.py similarity index 95% rename from scripts/simplexui/menu/mayaPlugins/importObjs.py rename to src/python/simplexui/menu/mayaPlugins/importObjs.py index 8c49277d..4600ae19 100644 --- a/scripts/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/scripts/simplexui/menu/mayaPlugins/linearizeTraversal.py b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py similarity index 97% rename from scripts/simplexui/menu/mayaPlugins/linearizeTraversal.py rename to src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py index 3605c846..dab8823e 100644 --- a/scripts/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/scripts/simplexui/menu/mayaPlugins/makeShelfBtn.py b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py similarity index 95% rename from scripts/simplexui/menu/mayaPlugins/makeShelfBtn.py rename to src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py index 44f97be8..b682376f 100644 --- a/scripts/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 @@ -53,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/scripts/simplexui/menu/mayaPlugins/relaxToSelection.py b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py similarity index 98% rename from scripts/simplexui/menu/mayaPlugins/relaxToSelection.py rename to src/python/simplexui/menu/mayaPlugins/relaxToSelection.py index 30b4b5d0..16727300 100644 --- a/scripts/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/scripts/simplexui/menu/mayaPlugins/reloadDefinition.py b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py similarity index 96% rename from scripts/simplexui/menu/mayaPlugins/reloadDefinition.py rename to src/python/simplexui/menu/mayaPlugins/reloadDefinition.py index d8ad944a..5172228b 100644 --- a/scripts/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/scripts/simplexui/menu/mayaPlugins/snapToNeutral.py b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py similarity index 98% rename from scripts/simplexui/menu/mayaPlugins/snapToNeutral.py rename to src/python/simplexui/menu/mayaPlugins/snapToNeutral.py index a6e8d1f9..5ee142e6 100644 --- a/scripts/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/scripts/simplexui/menu/mayaPlugins/softSelectToCluster.py b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py similarity index 97% rename from scripts/simplexui/menu/mayaPlugins/softSelectToCluster.py rename to src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py index 54cecdc5..c3ff2d2a 100644 --- a/scripts/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 @@ -33,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)) @@ -148,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): @@ -201,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/scripts/simplexui/menu/mayaPlugins/tweakMix.py b/src/python/simplexui/menu/mayaPlugins/tweakMix.py similarity index 96% rename from scripts/simplexui/menu/mayaPlugins/tweakMix.py rename to src/python/simplexui/menu/mayaPlugins/tweakMix.py index fb7b3bec..991a59fa 100644 --- a/scripts/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/scripts/simplexui/menu/mayaPlugins/updateRestShape.py b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py similarity index 96% rename from scripts/simplexui/menu/mayaPlugins/updateRestShape.py rename to src/python/simplexui/menu/mayaPlugins/updateRestShape.py index ea951cd7..00ba7cd1 100644 --- a/scripts/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 @@ -64,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/scripts/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py similarity index 91% rename from scripts/simplexui/simplexDialog.py rename to src/python/simplexui/simplexDialog.py index 2e0b6654..0ea7c44e 100644 --- a/scripts/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 @@ -474,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 @@ -488,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 @@ -526,7 +525,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): @@ -552,8 +553,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 @@ -597,7 +602,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 @@ -829,9 +834,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] @@ -870,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 @@ -1130,13 +1199,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)) @@ -1219,9 +1284,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 @@ -1274,7 +1337,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/scripts/simplexui/travCheckDialog.py b/src/python/simplexui/travCheckDialog.py similarity index 88% rename from scripts/simplexui/travCheckDialog.py rename to src/python/simplexui/travCheckDialog.py index 055b21ff..8168cb2a 100644 --- a/scripts/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 = [] @@ -144,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) @@ -198,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) @@ -219,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() @@ -247,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): @@ -272,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) @@ -321,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( diff --git a/scripts/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py similarity index 95% rename from scripts/simplexui/traversalDialog.py rename to src/python/simplexui/traversalDialog.py index e97433bf..369038bd 100644 --- a/scripts/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/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 95% rename from scripts/simplexui/utils.py rename to src/python/simplexui/utils.py index e49b3044..8cae2ab8 100755 --- a/scripts/simplexui/utils.py +++ b/src/python/simplexui/utils.py @@ -16,7 +16,6 @@ # along with Simplex. If not, see . """Utility functions.""" -from __future__ import absolute_import import os import re @@ -293,9 +292,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") @@ -317,4 +318,3 @@ def save(self): self._pref.save() else: self._pref.sync() - diff --git a/subprojects/maya/meson.build b/subprojects/maya/meson.build index 65df36ef..1d6dc7df 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,73 @@ 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' + + 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 + 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, ) 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 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 .