diff --git a/.gitignore b/.gitignore index 5d381cc..0d6f624 100644 --- a/.gitignore +++ b/.gitignore @@ -1,162 +1,2 @@ -# ---> Python -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - +*.un~ +*.pyc diff --git a/README.md b/README.md index 2db875f..41c8276 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ -# ModelCheckerForMaya +# Model Checker (WIP) +![img](./images/ui.png) + +## Requirements + +[CheckTools plugins](https://github.com/minoue/CheckTools) + +## Usage + +python +``` +from ModelCheckerForMaya import modelSanityChecker +modelSanityChecker.main() +``` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/checker.py b/checker.py new file mode 100644 index 0000000..dcf724d --- /dev/null +++ b/checker.py @@ -0,0 +1,1295 @@ +""" +Checker classes +""" + +import re + +from abc import ABCMeta, abstractmethod +from maya import cmds +from maya.api import OpenMaya +from PySide2 import QtWidgets + + +if not cmds.pluginInfo("meshChecker", q=True, loaded=True): + try: + cmds.loadPlugin("meshChecker") + except RuntimeError: + cmds.warning("Failed to load meshChecker plugin") + +if not cmds.pluginInfo("uvChecker", q=True, loaded=True): + try: + cmds.loadPlugin("uvChecker") + except RuntimeError: + cmds.warning("Failed to load uvChecker plugin") + +if not cmds.pluginInfo("findUvOverlaps", q=True, loaded=True): + try: + cmds.loadPlugin("findUvOverlaps") + except RuntimeError: + cmds.warning("Failed to load uvOverlap checker plugin") + + +class Error(QtWidgets.QListWidgetItem): + """ Custom error object """ + + def __init__(self, fullPath, errors=None, parent=None): + # type: (str, list) -> (None) + super(Error, self).__init__(parent) + self.components = errors + self.longName = fullPath + self.shortName = fullPath.split("|")[-1] + + self.setText(self.shortName) + + +class BaseChecker: + """ Base abstract class for each checker """ + + __metaclass__ = ABCMeta + __category__ = "" + __name__ = "" + isWarning = False + isEnabled = True + isFixable = False + + def __init__(self): + + self.errors = [] + + def __eq__(self, other): + return self.name == self.name + + def __ne__(self, other): + return not (self == other) + + def __lt__(self, other): + return (self.category < other.category) + + @abstractmethod + def checkIt(self, objs, settings=None): + """ Check method """ + + pass + + @abstractmethod + def fixIt(self): + """ Fix method """ + + pass + + @property + def name(self): + """ Label property """ + + return self.__name__ + + @property + def category(self): + return self.__category__ + + +class TriangleChecker(BaseChecker): + """ Triangle checker class """ + + __name__ = "Triangles" + __category__ = "Topology" + isWarning = True + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + errorsDict = {} + self.errors = [] + + errs = cmds.checkMesh(obj, c=0) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class NgonChecker(BaseChecker): + + __name__ = "N-gons" + __category__ = "Topology" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + errorsDict = {} + self.errors = [] + + errs = cmds.checkMesh(obj, c=1) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class NonmanifoldEdgeChecker(BaseChecker): + + __name__ = "Nonmanifold Edges" + __category__ = "Topology" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + errorsDict = {} + self.errors = [] + + errs = cmds.checkMesh(obj, c=2) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class NonmanifoldVertexChecker(BaseChecker): + + __name__ = "Nonmanifold Vertices" + __category__ = "Topology" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + self.errors = [] + + children = cmds.listRelatives(obj, fullPath=True, ad=True, type="mesh") + + for obj in children: + try: + errs = cmds.polyInfo(obj, nmv=True) + if errs: + errorObj = Error(obj, errs) + self.errors.append(errorObj) + except RuntimeError: + pass + + return self.errors + + def fixIt(self): + pass + + +class LaminaFaceChecker(BaseChecker): + + __name__ = "Lamina Faces" + __category__ = "Topology" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + errorsDict = {} + self.errors = [] + + errs = cmds.checkMesh(obj, c=3) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class BiValentFaceChecker(BaseChecker): + + __name__ = "Bi-valent Faces" + __category__ = "Topology" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + errorsDict = {} + self.errors = [] + + errs = cmds.checkMesh(obj, c=4) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class ZeroAreaFaceChecker(BaseChecker): + + __name__ = "Zero Area Faces" + __category__ = "Topology" + + def checkIt(self, obj, settings): + # type: (list) -> (list) + + mfa = settings.getSettings()['maxFaceArea'] + + errorsDict = {} + self.errors = [] + + errs = cmds.checkMesh(obj, c=5, maxFaceArea=mfa) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class MeshBorderEdgeChecker(BaseChecker): + + __name__ = "Mesh Border Edges" + isWarning = True + __category__ = "Topology" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + errorsDict = {} + self.errors = [] + + errs = cmds.checkMesh(obj, c=6) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class CreaseEdgeChecker(BaseChecker): + + __name__ = "Crease Edges" + __category__ = "Topology" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + errorsDict = {} + self.errors = [] + + errs = cmds.checkMesh(obj, c=7) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class ZeroLengthEdgeChecker(BaseChecker): + + __name__ = "Zero-length Edges" + __category__ = "Topology" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + errorsDict = {} + self.errors = [] + + errs = cmds.checkMesh(obj, c=8) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class VertexPntsChecker(BaseChecker): + + __name__ = "Vertex Pnts Attribute" + __category__ = "Attribute" + isFixable = True + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + self.errors = [] + + errs = cmds.checkMesh(obj, c=9) + + for e in errs: + errObj = Error(e) + self.errors.append(errObj) + + return self.errors + + def fixIt(self): + mSel = OpenMaya.MSelectionList() + for n, e in enumerate(self.errors): + if cmds.objExists(e.longName): + obj = e.longName + mSel.add(obj) + try: + cmds.polyMoveVertex( + obj, lt=(0, 0, 0), nodeState=1, ch=False) + except RuntimeError: + pass + +class EmptyGeometryChecker(BaseChecker): + + __name__ = "Empty Geometry" + __category__ = "Topology" + isFixable = False + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + self.errors = [] + + errs = cmds.checkMesh(obj, c=10) + + for e in errs: + errObj = Error(e) + self.errors.append(errObj) + + return self.errors + + def fixIt(self): + pass + + +class NameChecker(BaseChecker): + + __name__ = "Name" + __category__ = "Name" + isEnabled = False + + def checkIt(self, objs, settings=None): + # type: (list) -> (list) + + self.errors = [] + + for obj in objs: + try: + pass + except RuntimeError: + pass + + return self.errors + + def fixIt(self): + pass + + +class ShapeNameChecker(BaseChecker): + + __name__ = "ShapeName" + __category__ = "Name" + isFixable = True + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + self.errors = [] + + objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] + objs.insert(0, root) + + for obj in objs: + shapes = cmds.listRelatives( + obj, children=True, fullPath=True, shapes=True) or [] + if shapes: + for shape in shapes: + isIntermediate = cmds.getAttr( + shape + ".intermediateObject") + if isIntermediate: + continue + shortName = obj.split("|")[-1] + shapeShortName = shape.split("|")[-1] + + if shortName + "Shape" != shapeShortName: + err = Error(shape) + self.errors.append(err) + + return self.errors + + def fixIt(self): + for e in self.errors: + shape = e.longName + parent = cmds.listRelatives(shape, parent=True, fullPath=False)[0] + newShapeName = parent + "Shape" + cmds.rename(shape, newShapeName) + + +class HistoryChecker(BaseChecker): + + __name__ = "History" + __category__ = "Node" + isEnabled = True + isFixable = True + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + self.errors = [] + + objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] + objs.insert(0, root) + + for obj in objs: + mesh = cmds.listRelatives(obj, children=True, type="mesh") + if mesh is not None: + for m in mesh: + inMesh = cmds.listConnections(m + ".inMesh", source=True) + if inMesh is not None: + err = Error(obj) + self.errors.append(err) + + return self.errors + + def fixIt(self): + + for e in self.errors: + cmds.delete(e.longName, ch=True) + + +class TransformChecker(BaseChecker): + + __name__ = "Transform" + __category__ = "Attribute" + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + ignore = [] + + self.errors = [] + + identity = OpenMaya.MMatrix.kIdentity + mSel = OpenMaya.MSelectionList() + + objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] + objs.insert(0, root) + + for n, i in enumerate(objs): + mSel.add(i) + dagPath = mSel.getDagPath(n) + groupName = dagPath.fullPathName().split("|")[-1] + if groupName in ignore: + continue + dagNode = OpenMaya.MFnDagNode(dagPath) + transform = dagNode.transformationMatrix() + if not transform == identity: + errorObj = Error(i) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class LockedTransformChecker(BaseChecker): + + __name__ = "Locked Transform" + __category__ = "Attribute" + isFixable = True + + def __init__(self): + super(LockedTransformChecker, self).__init__() + self.attrs = ["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz"] + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + self.errors = [] + + objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] + objs.insert(0, root) + + for obj in objs: + try: + for at in self.attrs: + isLocked = cmds.getAttr(obj + ".{}".format(at), lock=True) + if isLocked: + err = Error(obj) + self.errors.append(err) + break + except RuntimeError: + pass + + return self.errors + + def fixIt(self): + for e in self.errors: + for at in self.attrs: + cmds.setAttr(e.longName + ".{}".format(at), lock=False) + + +class SmoothPreviewChecker(BaseChecker): + + __name__ = "Smooth Preview" + __category__ = "Attribute" + isFixable = True + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + self.errors = [] + + meshes = cmds.listRelatives(root, ad=True, fullPath=True, type="mesh") or [] + + for mesh in meshes: + isSmooth = cmds.getAttr(mesh + ".displaySmoothMesh") + + if isSmooth: + err = Error(mesh) + self.errors.append(err) + + return self.errors + + def fixIt(self): + + for e in self.errors: + cmds.setAttr(e.longName + ".displaySmoothMesh", 0) + + +class KeyframeChecker(BaseChecker): + + __name__ = "Keyframe" + __category__ = "Attribute" + isFixable = True + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + self.errors = [] + + keyNodes = ["animCurveTU", "animCurveTA", "animCurveTL"] + + objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] + objs.insert(0, root) + + for i in objs: + conns = cmds.listConnections(i, source=True) + keys = [] + + if conns is None: + continue + + for c in conns: + if cmds.objectType(c) in keyNodes: + keys.append(c) + if keys: + err = Error(i, keys) + self.errors.append(err) + + return self.errors + + def fixIt(self): + + for e in self.errors: + cmds.delete(e.components) + + +class UnusedVertexChecker(BaseChecker): + """ Unused vertex checker class """ + + __name__ = "Unused Vertices" + __category__ = "Topology" + + def __init__(self): + super(UnusedVertexChecker, self).__init__() + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + errorsDict = {} + self.errors = [] + + errs = cmds.checkMesh(obj, c=11) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + """ Unused vertices ARE fixable """ + + pass + + +class IntermediateObjectChecker(BaseChecker): + + __name__ = "Intermediate Object" + __category__ = "Node" + isFixable = True + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + self.errors = [] + + meshes = cmds.listRelatives(root, ad=True, fullPath=True, type="mesh") + + for mesh in meshes: + isIntermediate = cmds.getAttr(mesh + ".intermediateObject") + if isIntermediate: + err = Error(mesh) + self.errors.append(err) + + return self.errors + + def fixIt(self): + for e in self.errors: + shape = e.longName + + if cmds.objExists(shape): + parents = cmds.listRelatives( + shape, fullPath=True, parent=True) or [] + for i in parents: + # Delete history for parents + cmds.delete(i, ch=True) + try: + cmds.delete(shape) + except ValueError: + pass + + +class InstanceShapeChecker(BaseChecker): + + __name__ = "Instance Shape" + __category__ = "Node" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + self.errors = [] + + errs = cmds.checkMesh(obj, c=12) + + for e in errs: + errObj = Error(e) + self.errors.append(errObj) + + return self.errors + + def fixIt(self): + pass + + +class ConnectionChecker(BaseChecker): + + __name__ = "Connections" + __category__ = "Node" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + self.errors = [] + + errs = cmds.checkMesh(obj, c=13) + + for e in errs: + errObj = Error(e) + self.errors.append(errObj) + + return self.errors + + def fixIt(self): + pass + + +class DisplayLayerCheck(BaseChecker): + + __name__ = "Display layers" + __category__ = "other" + isFixable = True + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + self.errors = [] + + objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] + objs.insert(0, root) + + for obj in objs: + layers = cmds.listConnections(obj + ".drawOverride") or [] + if layers: + err = Error(obj, layers) + self.errors.append(err) + + return self.errors + + def fixIt(self): + + for e in self.errors: + layers = e.components + node = e.longName + for layer in layers: + cmds.disconnectAttr( + layer + ".drawInfo", node + ".drawOverride") + + +class UnusedLayerChecker(BaseChecker): + + __name__ = "Unused layers" + __category__ = "other" + isFixable = True + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + self.errors = [] + + layers = cmds.ls(type="displayLayer") + layers.remove("defaultLayer") + for layer in layers: + contents = cmds.editDisplayLayerMembers( + layer, q=True, fullNames=True) + if contents is None: + err = Error(layer, [layer]) + self.errors.append(err) + + return self.errors + + def fixIt(self): + for e in self.errors: + try: + cmds.delete(e.longName) + except RuntimeError: + pass + + +class Map1Checker(BaseChecker): + + __name__ = "UVSet to map1" + __category__ = "UV" + isFixable = True + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + self.errors = [] + + meshes = cmds.listRelatives(root, ad=True, fullPath=True, type="mesh") or [] + + for mesh in meshes: + curUVSet = cmds.polyUVSet(mesh, q=True, currentUVSet=True)[0] + if curUVSet != "map1": + err = Error(mesh) + self.errors.append(err) + + return self.errors + + def fixIt(self): + + for e in self.errors: + cmds.polyUVSet(e.longName, uvSet="map1", currentUVSet=True) + + +class NegativeUvChecker(BaseChecker): + + __name__ = "UVs in negative space" + __category__ = "UV" + + def checkIt(self, obj, settings=None): + + self.errors = [] + errorsDict = {} + + errs = cmds.checkUV(obj, c=4) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class UdimIntersectionChecker(BaseChecker): + + __name__ = "UDIM intersection" + __category__ = "UV" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + self.errors = [] + + errorsDict = {} + + errs = cmds.checkUV(obj, c=0) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class UnassignedUvChecker(BaseChecker): + + __name__ = "Unassigned UVs" + __category__ = "UV" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + self.errors = [] + + errorsDict = {} + + errs = cmds.checkUV(obj, c=3) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class UnmappedPolygonFaceChecker(BaseChecker): + + __name__ = "Unmapped polygon faces" + __category__ = "UV" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + self.errors = [] + + errorsDict = {} + + errs = cmds.checkUV(obj, c=1) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class ZeroAreaUVFaceChecker(BaseChecker): + + __name__ = "Zero area UV Faces" + __category__ = "UV" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + self.errors = [] + errorsDict = {} + + errs = cmds.checkUV(obj, c=2) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class ConcaveUVChecker(BaseChecker): + + __name__ = "Concave UV Faces" + __category__ = "UV" + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + self.errors = [] + errorsDict = {} + + errs = cmds.checkUV(obj, c=5) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class ReversedUVChecker(BaseChecker): + + __name__ = "Reversed UV Faces" + __category__ = "UV" + isWarning = True + + def checkIt(self, obj, settings=None): + # type: (list) -> (list) + + self.errors = [] + errorsDict = {} + + errs = cmds.checkUV(obj, c=6) + + for e in errs: + base, comp = e.split(".") + + if base in errorsDict: + errorsDict[base].append(e) + else: + errorsDict[base] = [e] + + for err_key in errorsDict: + components = errorsDict[err_key] + errorObj = Error(err_key, errorsDict[err_key]) + self.errors.append(errorObj) + + return self.errors + + def fixIt(self): + pass + + +class UvOverlapChecker(BaseChecker): + + __name__ = "UV Overlaps" + __category__ = "UV" + isEnabled = False + + def checkIt(self, root, settings=None): + + self.errors = [] + + objs = cmds.listRelatives(root, ad=True, fullPath=True, type="mesh") or [] + + mSel = OpenMaya.MSelectionList() + + for obj in objs: + mSel.add(obj) + + for i in range(mSel.length()): + dagPath = mSel.getDagPath(i) + try: + dagPath.extendToShape() + + except RuntimeError: + # Not mesh. Do no nothing + pass + + return self.errors + + def fixIt(self): + pass + + +class SelectionSetChecker(BaseChecker): + + __name__ = "Selection Sets" + __category__ = "other" + isFixable = True + + def getSets(self, path, typ): + + if typ == "transform": + conns = cmds.listConnections(path + ".instObjGroups") or [] + return [i for i in conns if cmds.objectType(i) == "objectSet"] + elif typ == "shape": + conns = cmds.listConnections( + path + ".instObjGroups.objectGroups") or [] + return [i for i in conns if cmds.objectType(i) == "objectSet"] + else: + pass + + return [] + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + self.errors = [] + objectSets = [] + ignore = ["modelPanel[0-9]ViewSelectedSet"] + + objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] + objs.insert(0, root) + + for obj in objs: + shapes = cmds.listRelatives( + obj, children=True, fullPath=True, shapes=True) or [] + + for shape in shapes: + objectSets.extend(self.getSets(shape, "shape")) + + objectSets.extend(self.getSets(obj, "transform")) + + objectSets = list(set(objectSets)) + + for objSet in objectSets: + for i in ignore: + if re.match(i, objSet) is None: + err = Error(objSet) + self.errors.append(err) + + return self.errors + + def fixIt(self): + for e in self.errors: + try: + cmds.delete(e.longName) + except Exception: + pass + + +class ColorSetChecker(BaseChecker): + + __name__ = "Color Sets" + __category__ = "other" + isFixable = True + + def checkIt(self, root, settings=None): + # type: (list) -> (list) + + # Reset result + self.errors = [] + + objs = cmds.listRelatives(root, ad=True, fullPath=True, type="mesh") or [] + + for obj in objs: + try: + allColorSets = cmds.polyColorSet( + obj, q=True, allColorSets=True) + if allColorSets is None: + continue + else: + err = Error(obj) + self.errors.append(err) + except RuntimeError: + pass + + return self.errors + + def fixIt(self): + for i in self.errors: + allSets = cmds.polyColorSet( + i.longName, q=True, allColorSets=True) or [] + for s in allSets: + cmds.polyColorSet(i.longName, delete=True, colorSet=s) + + +CHECKERS = [ + NameChecker, + ShapeNameChecker, + HistoryChecker, + TransformChecker, + LockedTransformChecker, + SmoothPreviewChecker, + KeyframeChecker, + TriangleChecker, + NgonChecker, + NonmanifoldEdgeChecker, + NonmanifoldVertexChecker, + LaminaFaceChecker, + BiValentFaceChecker, + ZeroAreaFaceChecker, + MeshBorderEdgeChecker, + CreaseEdgeChecker, + ZeroLengthEdgeChecker, + VertexPntsChecker, + EmptyGeometryChecker, + UnusedVertexChecker, + IntermediateObjectChecker, + InstanceShapeChecker, + ConnectionChecker, + DisplayLayerCheck, + UnusedLayerChecker, + Map1Checker, + NegativeUvChecker, + UdimIntersectionChecker, + UnassignedUvChecker, + UnmappedPolygonFaceChecker, + ZeroAreaUVFaceChecker, + ConcaveUVChecker, + ReversedUVChecker, + UvOverlapChecker, + SelectionSetChecker, + ColorSetChecker] diff --git a/framelayout.py b/framelayout.py new file mode 100644 index 0000000..8a22470 --- /dev/null +++ b/framelayout.py @@ -0,0 +1,114 @@ +""" qt framelayout sample """ + +from PySide2 import QtWidgets, QtCore +from . import icon + + +class TitleLabel(QtWidgets.QLabel): + + clicked = QtCore.Signal() + + def __init__(self, text="", parent=None): + super(TitleLabel, self).__init__(parent) + + self.setText(text) + + def mousePressEvent(self, event): + self.clicked.emit() + + +class FrameLayout(QtWidgets.QWidget): + + def __init__(self, title="", parent=None): + super(FrameLayout, self).__init__(parent) + + self.baseTitle = title + self.rightArrow = u"\u25b6 " + self.downArrow = u"\u25bc " + + titleLayout = QtWidgets.QHBoxLayout() + self.title = self.rightArrow + title + + self.titleLabel = TitleLabel(self.title) + self.titleLabel.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.titleLabel.clicked.connect(self.titleClicked) + self.statusIconLabel = QtWidgets.QLabel() + self.statusIconLabel.setPixmap(icon.neutralIconPixmap) + + self.childrenWidget = QtWidgets.QWidget() + self.childrenLayout = QtWidgets.QVBoxLayout() + self.childrenLayout.setContentsMargins(0, 0, 0, 0) + + self.childrenWidget.setLayout(self.childrenLayout) + + titleLayout.addWidget(self.statusIconLabel) + titleLayout.addWidget(self.titleLabel) + + layout = QtWidgets.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addLayout(titleLayout) + layout.addWidget(self.childrenWidget) + self.setLayout(layout) + + # Close frame by default + self.childrenWidget.hide() + + def titleClicked(self): + """ + title clicked action + + """ + + newTitle = "" + + if self.childrenWidget.isVisible(): + self.childrenWidget.hide() + newTitle = self.rightArrow + self.baseTitle + else: + self.childrenWidget.show() + newTitle = self.downArrow + self.baseTitle + + self.titleLabel.setText(newTitle) + + def collapse(self): + newTitle = "" + self.childrenWidget.hide() + newTitle = self.rightArrow + self.baseTitle + self.titleLabel.setText(newTitle) + + def expand(self): + newTitle = "" + self.childrenWidget.show() + newTitle = self.rightArrow + self.baseTitle + self.titleLabel.setText(newTitle) + + def addWidget(self, widget): + # type: (QtWidgets.QWidget) -> None + """ + Add widgets + + """ + + self.childrenLayout.addWidget(widget) + + def addLayout(self, layout): + self.childrenLayout.addLayout(layout) + + def setStatusIcon(self, status): + # type: (str) -> None + """ + Change status icon + + """ + + if status == "good": + self.statusIconLabel.setPixmap(icon.goodIconPixmap) + elif status == "bad": + self.statusIconLabel.setPixmap(icon.errorIconPixmap) + elif status == "warning": + self.statusIconLabel.setPixmap(icon.warningIconPixmap) + elif status == "neutral": + self.statusIconLabel.setPixmap(icon.neutralIconPixmap) + else: + pass diff --git a/icon.py b/icon.py new file mode 100644 index 0000000..caf90db --- /dev/null +++ b/icon.py @@ -0,0 +1,34 @@ +""" wip + +https://www.iconfinder.com/iconsets/fatcow +https://creativecommons.org/licenses/by/3.0/us/ +"Farm-fresh" by FatCow Web Hosting is licensed under CC BY 3.0 + +""" + +from PySide2 import QtCore, QtGui + + +b64Error = b"""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""" +b64Warning = b"""iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFJUlEQVRYR+2WfUzUdRzHX7/fHXeCgsrx6BOiCAJCefIg8tDIykwNzHQzwwdmudZcubk5jXQq6WyVLtaUcjhlVqu5tHRm5dwE5NFDQEQINfEBBIEhAnnc3a/97sHRxXFHW7m2vv/d/b7f3+f1fX/en8/vI/CEl/CE4/OfBRDPbY5b46GSDsoK9uqFtam7yw4BpuEq+rcUWD8/wHdlUkhrTGamOV5FXh5HChv9ck63tP0bAG4FWbFnpybOSQ70u2+O19zqw7WiCwXJ2eVzgf7hQAxXAeHQ2uik6DDf89rXl0LRTswuituI7uuTVNe3paw5WF0ISK5CDBdAXbw1risifYHaq7cU7uosccZpeeARz5Xjpx4l7CgbDTz6JwDEHzbO2jQ1ZMKu8BdnQkUuiCpLHJMeYtZR92Ml1xpvb1n00cU9rhpyOAp4XMiK7Zm9bgXC1XzobQdRYQUwgocGaXoGJblHmZNdPlIuDldUcBVAcW5LzLGwJG1a4FQ3aDgNSiW4WVNtEMBggGnzab7WT32h7kTqroolgNEZhCsAQv5b0UmRwZrzM1+bB5ePgCSBSiLx4xREAQ4uqyLMvwsEESJXUvnlGWpvtKdk7HduSFcA1EVZsTfC5yUEjvVsgZbLoFDACCMv7UtGIULOK9VM9usGgwn8Z9DZHUDdmeLmxOzyYGeGdAYgnto4a1NwiP+u8IUzoe4EiErM11YbSPs0BYUo8cniKib79lgENxkgPI26k5XcaLy3ZYETQzoD8CjOiuvRrpiL6kEF9LRbZBYxA7ya84wZ4MPFVQT5PASjAJIJRmrQe8WgO3qWhOyyIQ05FIDi500xn4fGhmRO0mrgVgkISsydR5RA3c/yz1IRBYnd6ZeYZAaQySSQDDBxNk26dhrKG/Oe31PxpiNDOgIQ9mZMm54Y6ncldlUSNJeCXm+5vbysABn7U1EIEjvTLjFRYwOQGUygUkFgPOWHCylqaI3YkP/r1cE6pCMAVcF7sbrIF6Iix/o/go6b1uDW7VaA1QeeNadg+6JKJgwEMKtgAu8gOu+pqf2ppjb5g3ItoLcvy8EAxOPvzsoMDfX5Ijw9Eu5cspTdwNHBCrD2wHOIosTWhReZ8DgFthASCAKMf5q647U0NNx/I33fxTz7DjkYgHtxVnyvdrkWFS3Q90DW/M/ggtwHDGTmzjN74P1FFQT5dIHJbp88Hrh7oScA3Vc6ErJLPYC+gS+zBzAbb0pUQOaUlHHQfsMa3G6b/FNhpKl1jOWSY3oQFUYw2b9OVs4EmmCun7/L9ZqWvxhy4Alhb8aM6Ymho6/EroqC7juWxuJoalPKKpgsj/vlViw6mIckUIrgOZ7ywzUUNXRFbMi//NiQAwFUBVlxurCkiZG+oe7QK0vvwKPy32oTKW/7m1OQ+04nYZMNlj4w6JLAw4u2hj7qC2/VJmeXPTak7YTw7fqnFodNGXssalkIPOwYerqTU60ysWxbhKUPrL5J8Pi+IQCsmRzlTc03jdRf71yyNKfqO7ksbQAjSrYldEe/PEnprlFAvwtTlVKi6a6nxQPefYhKo/M5yM2NvnYj1d83GWZvL/YEfrcBeJduS2iPWx8KehOY7MpuMFnlSrCZ3uo1hykzn5f3C6ASKctpIH57sQbosAH4/rI57twolSJSkoO7PNE5+9rbPRfkfiagl6TbKTtKZB+02QC8gCBAprKOOcN8uevb5dLqAH4DumwA8ldGbhJqx9Z3PYKTnbK+ckuWRza5gJ/s+h/gD1M9zzASYFmcAAAAAElFTkSuQmCC""" +b64Good = b"""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""" +b64Neutral = b"""iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAC10lEQVRYR+2V309SYRzGn+MhHZPSBKUU0VQm6yI9rsVFrZoLajMpNu5gMn5sede1f4HX3XkhOpjekSvMrYVzbtUFrUl20XCoKQp1FMwKxyzgtPcIzbaUw2Djhnc7d+97ns/7PN/v96VQ5kWVWR8VgIoDFQcKdYAymUwakUg0SlHU7eMtzHHcYiqVGpmenvYD4IS2dyEAtMUyNCaRnHMwTC+UyjZU19TwOr8ODxEObyIQ+IBE4se4y+UeBpAWAiEUQGSxDD1tkMr0gwP3EVpbx/LyMmKxGK8hk8nQ09MDVWcHZudeYC8ed7pcrkdCIIQAUAaD4aZC0bI4OPgAvvkFsOxX0DQNijo6znEc0uk05PIL0N7px+zsc0Qi0VszMzOv88UhBKDabDYv6HR3r3/eCGNrK8yLV1VV/eNwJpPhIVpblbjUrsT8vO+t2+3uJwmdFoUQgDqr1bqv1z/EK5/vv+I5gRyETquF1/sMk5OT9QC+FwvQaLPZdvquXsNqKASaJjc/iZtEkUGXSoWl9+8wMTHRBGC3WIAmu93OdqsvIx7f+5v7ST8l9SCVNmAl+AlOp1MOYKdoAIfDzl5sVmZvnq/FiTscvkTDGB8vDUCj2Wx+qVC299XWSkByPm2R4jw4SGA7vLE0NTV1rxQR1Gk0Gj3D9LrbO1S8CydBHHUGh431EBlKQ36/31uKIqwG0Gw0Gp/I5XJ9Z1c3QFF875OPLDIP+JnAcVhbXQHLsl6Px/MYQLQUbUhCPQugzWg0jspkjQPNLQrUn5eCFtE8QDqVxv63OKKRbcRiu3Mej2cEwCaAn6UYRESDKNUBaGEY5oZarR4Wi8VXyI2zFiCZTH4MBoNjgUDgDYBI1vq874GQQZSrOQJRC6CBjH8AkiwYbwKABADyOOwBOBDyDvDxCXmxju0h+88AEAMgtZE7T6wgIzcJ4Hc+249rFgpQIG/+7RWAigMVB8ruwB+gWP0h6xe84QAAAABJRU5ErkJggg==""" + +tempPixmap = QtGui.QPixmap() + +errorData = QtCore.QByteArray.fromBase64(b64Error) +warningData = QtCore.QByteArray.fromBase64(b64Warning) +goodData = QtCore.QByteArray.fromBase64(b64Good) +neutralData = QtCore.QByteArray.fromBase64(b64Neutral) + +tempPixmap.loadFromData(errorData) +errorIconPixmap = tempPixmap.scaled(20, 20, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) + +tempPixmap.loadFromData(warningData) +warningIconPixmap = tempPixmap.scaled(20, 20, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) + +tempPixmap.loadFromData(goodData) +goodIconPixmap = tempPixmap.scaled(20, 20, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) + +tempPixmap.loadFromData(neutralData) +neutralIconPixmap = tempPixmap.scaled(20, 20, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) diff --git a/images/ui.png b/images/ui.png new file mode 100644 index 0000000..2bd874d Binary files /dev/null and b/images/ui.png differ diff --git a/modelSanityChecker.py b/modelSanityChecker.py new file mode 100644 index 0000000..bba9032 --- /dev/null +++ b/modelSanityChecker.py @@ -0,0 +1,456 @@ +""" + +module docstring here +""" + +from PySide2 import QtCore, QtWidgets, QtGui +from maya.app.general.mayaMixin import MayaQWidgetDockableMixin +from maya import cmds +from . import checker +from . import framelayout + +try: + from importlib import reload +except ImportError: + # if python2, use build-in reload + pass + +reload(framelayout) +reload(checker) + + +class Separator(QtWidgets.QWidget): + + def __init__(self, category="", checkers=None): + super(Separator, self).__init__() + + self.checkerWidgets = checkers + self.category = category + + self.checkbox = QtWidgets.QCheckBox() + self.checkbox.setChecked(True) + self.checkbox.stateChanged.connect(self.checkboxToggle) + + line = QtWidgets.QFrame() + line.setFrameShape(QtWidgets.QFrame.HLine) + line.setSizePolicy(QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding) + label = QtWidgets.QLabel(" " + category) + font = QtGui.QFont() + font.setItalic(True) + font.setCapitalization(QtGui.QFont.AllUppercase) + font.setBold(True) + label.setFont(font) + + layout = QtWidgets.QHBoxLayout() + layout.addWidget(self.checkbox) + layout.addWidget(line) + layout.addWidget(label) + # layout.setSpacing(0) + layout.setContentsMargins(2, 2, 2, 2) + self.setLayout(layout) + + def checkboxToggle(self, *args): + state = args[0] + for w in self.checkerWidgets: + if self.category == w.checker.category: + if state == 2: + w.setEnabled(True) + else: + w.setEnabled(False) + + +class CheckerWidget(QtWidgets.QWidget): + + def __init__(self, chk, parent, settings=None): + # type: (checker.BaseChecker) + super(CheckerWidget, self).__init__() + + self.checker = chk + self.settings = settings + self.createUI() + + def createUI(self): + layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight) + layout.setContentsMargins(2, 2, 2, 2) + layout.setSpacing(2) + + self.frame = framelayout.FrameLayout(self.checker.name) + + self.checkbox = QtWidgets.QCheckBox() + self.checkbox.stateChanged.connect(self.toggleEnable) + + self.checkButton = QtWidgets.QPushButton("Check") + # self.checkButton.setSizePolicy( + # QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Expanding) + self.checkButton.clicked.connect(self.check) + self.fixButton = QtWidgets.QPushButton("Fix") + self.fixButton.clicked.connect(self.fix) + if self.checker.isFixable is not True: + self.fixButton.setEnabled(False) + self.selectAllButton = QtWidgets.QPushButton('Select') + self.selectAllButton.clicked.connect(self.selectAll) + + buttonLayout = QtWidgets.QHBoxLayout() + buttonLayout.addWidget(self.checkButton) + buttonLayout.addWidget(self.fixButton) + buttonLayout.addWidget(self.selectAllButton) + + self.errorList = QtWidgets.QListWidget() + self.errorList.itemClicked.connect(self.errorSelected) + + self.frame.addWidget(self.errorList) + self.frame.addLayout(buttonLayout) + + layout.addWidget(self.checkbox, alignment=QtCore.Qt.AlignTop) + layout.addWidget(self.frame) + + self.setLayout(layout) + + if self.checker.isEnabled: + self.setEnabled(True) + else: + self.setEnabled(False) + + def setEnabled(self, state): + if state is True: + self.checkbox.setChecked(True) + self.frame.setEnabled(True) + self.checker.isEnabled = True + else: + self.checkbox.setChecked(False) + self.frame.setEnabled(False) + self.frame.collapse() + self.checker.isEnabled = False + + def check(self, path=None, dummy=None): + if not self.checker.isEnabled: + return + + root = self.parent().parent().parent().parent().rootLE.text() + if root != "": + path = root + + if path is None: + sel = cmds.ls(sl=True, fl=True, long=True) + if not sel: + cmds.warning("Nothing is selected") + return + path = sel[0] + + self.doCheck(path) + + def toggleEnable(self, *args): + state = args[0] + + if state == 2: + self.setEnabled(True) + else: + self.setEnabled(False) + + def doCheck(self, obj): + + # Clear list items + self.errorList.clear() + + errs = self.checker.checkIt(obj, self.settings) + + if errs: + for err in errs: + self.errorList.addItem(err) + if self.checker.isWarning: + self.frame.setStatusIcon("warning") + else: + self.frame.setStatusIcon("bad") + else: + self.frame.setStatusIcon("good") + + def fix(self): + if not self.checker.isEnabled: + return + + self.checker.fixIt() + + # Re-check + self.check() + + def errorSelected(self, *args): + """ + Select error components + + """ + + err = args[0] + if err.components is None: + cmds.select(err.longName, r=True) + else: + cmds.select(err.components, r=True) + + def selectAll(self): + errs = [i.longName for i in self.checker.errors] + cmds.select(errs, r=True) + + +class Settings(QtWidgets.QWidget): + + def __init__(self, parent=None): + super(Settings, self).__init__(parent) + + self.createUI() + + def createUI(self): + + self.maxFaceArea = QtWidgets.QLineEdit("0.000001") + + layout = QtWidgets.QGridLayout() + layout.setAlignment(QtCore.Qt.AlignTop) + layout.addWidget(QtWidgets.QLabel("Max face area"), 0, 0) + layout.addWidget(self.maxFaceArea, 0, 1) + self.setLayout(layout) + + def getSettings(self): + + data = { + "maxFaceArea": float(self.maxFaceArea.text()) + } + + return data + + +class ModelSanityChecker(QtWidgets.QWidget): + """ Main sanity checker class """ + + def __init__(self, settings=None, parent=None): + super(ModelSanityChecker, self).__init__(parent) + + checkerObjs = [i() for i in checker.CHECKERS] + checkerObjs.sort() + self.checkerWidgets = [CheckerWidget(i, self, settings) for i in checkerObjs] + self.separators = [] + + self.createUI() + + def createUI(self): + """ + GUI method + + """ + + mainLayout = QtWidgets.QVBoxLayout() + mainLayout.setContentsMargins(0, 0, 0, 0) + + scroll = QtWidgets.QScrollArea() + scroll.setWidgetResizable(1) + + scrollLayout = QtWidgets.QVBoxLayout() + + currentCategory = self.checkerWidgets[0].checker.category + + sep = Separator(currentCategory, self.checkerWidgets) + self.separators.append(sep) + scrollLayout.addWidget(sep) + + for widget in self.checkerWidgets: + if currentCategory != widget.checker.category: + cat = widget.checker.category + currentCategory = cat + sep = Separator(cat, self.checkerWidgets) + self.separators.append(sep) + scrollLayout.addWidget(sep) + scrollLayout.addWidget(widget) + + content = QtWidgets.QWidget() + content.setLayout(scrollLayout) + + scroll.setWidget(content) + + self.rootLE = QtWidgets.QLineEdit() + setButton = QtWidgets.QPushButton("Set Selected") + setButton.clicked.connect(self.setSelected) + + checkboxAll = QtWidgets.QCheckBox("Select All") + checkboxAll.setChecked(True) + checkboxAll.stateChanged.connect(self.selectAllToggle) + + checkAllButton = QtWidgets.QPushButton("Check All") + checkAllButton.clicked.connect(self.checkAll) + + fixAllButton = QtWidgets.QPushButton("Fix All") + fixAllButton.clicked.connect(self.fixAll) + + rootLayout = QtWidgets.QHBoxLayout() + rootLayout.addWidget(self.rootLE) + rootLayout.addWidget(setButton) + + mainLayout.addLayout(rootLayout) + mainLayout.addWidget(checkboxAll) + mainLayout.addWidget(scroll) + mainLayout.addWidget(checkAllButton) + mainLayout.addWidget(fixAllButton) + + self.setLayout(mainLayout) + + def selectAllToggle(self, *args): + state = args[0] + + for s in self.separators: + if state == 2: + s.checkbox.setChecked(True) + else: + s.checkbox.setChecked(False) + + def setSelected(self): + sel = cmds.ls(sl=True, fl=True, long=True) + if sel: + root = sel[0] + self.rootLE.setText(root) + + def checkAll(self): + """ + Check all + + """ + + node = self.rootLE.text() + + progDialog = QtWidgets.QProgressDialog( + "Now Checking...", + "Cancel", + 0, + len(self.checkerWidgets), + self) + progDialog.setWindowTitle("Building library") + # progDialog.setWindowModality(QtCore.Qt.WindowModal) + progDialog.show() + + for num, widget in enumerate(self.checkerWidgets): + checkerName = widget.checker.name + if widget.checker.isEnabled: + print("Running {} checker".format(checkerName)) + if node == "": + widget.check() + else: + widget.check(node) + else: + print("{} checker is disabled. Skipped".format(checkerName)) + + progDialog.setValue(num+1) + progDialog.setLabel(QtWidgets.QLabel( + r'Now checking "{}"'.format(widget.checker.name))) + QtCore.QCoreApplication.processEvents() + + progDialog.close() + + def fixAll(self): + """ + Fix all + + """ + + for widget in self.checkerWidgets: + widget.fix() + + +class CentralWidget(QtWidgets.QWidget): + """ Central widget """ + + def __init__(self, parent=None): + """ Init """ + + super(CentralWidget, self).__init__(parent) + + self.createUI() + self.layoutUI() + + def createUI(self): + """ Crete widgets """ + + settings = Settings(self) + checker = ModelSanityChecker(settings, self) + + self.tabWidget = QtWidgets.QTabWidget() + self.tabWidget.addTab(checker, "SanityChecker") + self.tabWidget.addTab(settings, "Settings") + + def layoutUI(self): + """ Layout widgets """ + + mainLayout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.TopToBottom) + mainLayout.setContentsMargins(5, 5, 5, 5) + mainLayout.addWidget(self.tabWidget) + + self.setLayout(mainLayout) + + +class MainWindow(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): + """ + Main window + + """ + + def __init__(self, parent=None): + """ init """ + + super(MainWindow, self).__init__(parent) + + self.thisObjectName = "sanityCheckerWindow" + self.winTitle = "Sanity Checker" + self.workspaceControlName = self.thisObjectName + "WorkspaceControl" + + self.setObjectName(self.thisObjectName) + self.setWindowTitle(self.winTitle) + + self.setWindowFlags(QtCore.Qt.Window) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + # Create and set central widget + self.cWidget = CentralWidget() + self.setCentralWidget(self.cWidget) + + self.setupMenu() + + def setupMenu(self): + """ Setup menu """ + + menu = self.menuBar() + + # About + aboutAction = QtWidgets.QAction("&About", self) + aboutAction.setStatusTip('About this script') + aboutAction.triggered.connect(self.showAbout) + + menu.addAction("File") + helpMenu = menu.addMenu("&Help") + helpMenu.addAction(aboutAction) + + def showAbout(self): + """ + About message + """ + + QtWidgets.QMessageBox.about(self, 'About ', 'test\n') + + def run(self): + try: + cmds.deleteUI(self.workspaceControlName) + except RuntimeError: + pass + + self.show(dockable=True) + cmds.workspaceControl( + self.workspaceControlName, + edit=True, + initialWidth=270, + minimumWidth=270, + dockToControl=['Outliner', 'right']) + self.raise_() + + +def main(): + + window = MainWindow() + window.run() + + +if __name__ == "__main__": + main()