├── .gitignore ├── README.md ├── __init__.py ├── checker.py ├── framelayout.py ├── icon.py ├── images └── ui.png └── modelSanityChecker.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Model Checker (WIP) 2 | 3 | ![img](./images/ui.png) 4 | 5 | ## Requirements 6 | Install the following plugins and load them first. 7 | 8 | * [CheckTools](https://github.com/minoue/CheckTools) 9 | 10 | ## Usage 11 | 12 | python 13 | 14 | ```python 15 | from ModelCheckerForMaya import modelSanityChecker 16 | modelSanityChecker.main() 17 | 18 | ``` 19 | Select object and press the check buttons 20 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minoue/ModelCheckerForMaya/24f9d068bff0b3dd211a077ca5c96e2b8f8c4f79/__init__.py -------------------------------------------------------------------------------- /checker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Checker classes 3 | """ 4 | 5 | import re 6 | 7 | from abc import ABCMeta, abstractmethod 8 | from maya import cmds 9 | from maya.api import OpenMaya 10 | 11 | try: 12 | from PySide6 import QtWidgets 13 | except ImportError: 14 | from PySide2 import QtWidgets 15 | 16 | 17 | if not cmds.pluginInfo("meshChecker", q=True, loaded=True): 18 | try: 19 | cmds.loadPlugin("meshChecker") 20 | except RuntimeError: 21 | cmds.warning("Failed to load meshChecker plugin") 22 | 23 | if not cmds.pluginInfo("uvChecker", q=True, loaded=True): 24 | try: 25 | cmds.loadPlugin("uvChecker") 26 | except RuntimeError: 27 | cmds.warning("Failed to load uvChecker plugin") 28 | 29 | if not cmds.pluginInfo("findUvOverlaps", q=True, loaded=True): 30 | try: 31 | cmds.loadPlugin("findUvOverlaps") 32 | except RuntimeError: 33 | cmds.warning("Failed to load uvOverlap checker plugin") 34 | 35 | 36 | class Error(QtWidgets.QListWidgetItem): 37 | """ Custom error object """ 38 | 39 | def __init__(self, fullPath, errors=None, parent=None): 40 | # type: (str, list) -> (None) 41 | super(Error, self).__init__(parent) 42 | self.components = errors 43 | self.longName = fullPath 44 | self.shortName = fullPath.split("|")[-1] 45 | 46 | self.setText(self.shortName) 47 | 48 | 49 | class BaseChecker: 50 | """ Base abstract class for each checker """ 51 | 52 | __metaclass__ = ABCMeta 53 | __category__ = "" 54 | __name__ = "" 55 | isWarning = False 56 | isEnabled = True 57 | isFixable = False 58 | 59 | def __init__(self): 60 | 61 | self.errors = [] 62 | 63 | def __eq__(self, other): 64 | return self.name == self.name 65 | 66 | def __ne__(self, other): 67 | return not (self == other) 68 | 69 | def __lt__(self, other): 70 | return (self.category < other.category) 71 | 72 | @abstractmethod 73 | def checkIt(self, objs, settings=None): 74 | """ Check method """ 75 | 76 | pass 77 | 78 | @abstractmethod 79 | def fixIt(self): 80 | """ Fix method """ 81 | 82 | pass 83 | 84 | @property 85 | def name(self): 86 | """ Label property """ 87 | 88 | return self.__name__ 89 | 90 | @property 91 | def category(self): 92 | return self.__category__ 93 | 94 | 95 | class TriangleChecker(BaseChecker): 96 | """ Triangle checker class """ 97 | 98 | __name__ = "Triangles" 99 | __category__ = "Topology" 100 | isWarning = True 101 | 102 | def checkIt(self, obj, settings=None): 103 | # type: (list) -> (list) 104 | 105 | errorsDict = {} 106 | self.errors = [] 107 | 108 | errs = cmds.checkMesh(obj, c=0) 109 | 110 | for e in errs: 111 | base, comp = e.split(".") 112 | 113 | if base in errorsDict: 114 | errorsDict[base].append(e) 115 | else: 116 | errorsDict[base] = [e] 117 | 118 | for err_key in errorsDict: 119 | components = errorsDict[err_key] 120 | errorObj = Error(err_key, errorsDict[err_key]) 121 | self.errors.append(errorObj) 122 | 123 | return self.errors 124 | 125 | def fixIt(self): 126 | pass 127 | 128 | 129 | class NgonChecker(BaseChecker): 130 | 131 | __name__ = "N-gons" 132 | __category__ = "Topology" 133 | 134 | def checkIt(self, obj, settings=None): 135 | # type: (list) -> (list) 136 | 137 | errorsDict = {} 138 | self.errors = [] 139 | 140 | errs = cmds.checkMesh(obj, c=1) 141 | 142 | for e in errs: 143 | base, comp = e.split(".") 144 | 145 | if base in errorsDict: 146 | errorsDict[base].append(e) 147 | else: 148 | errorsDict[base] = [e] 149 | 150 | for err_key in errorsDict: 151 | components = errorsDict[err_key] 152 | errorObj = Error(err_key, errorsDict[err_key]) 153 | self.errors.append(errorObj) 154 | 155 | return self.errors 156 | 157 | def fixIt(self): 158 | pass 159 | 160 | 161 | class NonmanifoldEdgeChecker(BaseChecker): 162 | 163 | __name__ = "Nonmanifold Edges" 164 | __category__ = "Topology" 165 | 166 | def checkIt(self, obj, settings=None): 167 | # type: (list) -> (list) 168 | 169 | errorsDict = {} 170 | self.errors = [] 171 | 172 | errs = cmds.checkMesh(obj, c=2) 173 | 174 | for e in errs: 175 | base, comp = e.split(".") 176 | 177 | if base in errorsDict: 178 | errorsDict[base].append(e) 179 | else: 180 | errorsDict[base] = [e] 181 | 182 | for err_key in errorsDict: 183 | components = errorsDict[err_key] 184 | errorObj = Error(err_key, errorsDict[err_key]) 185 | self.errors.append(errorObj) 186 | 187 | return self.errors 188 | 189 | def fixIt(self): 190 | pass 191 | 192 | 193 | class NonmanifoldVertexChecker(BaseChecker): 194 | 195 | __name__ = "Nonmanifold Vertices" 196 | __category__ = "Topology" 197 | 198 | def checkIt(self, obj, settings=None): 199 | # type: (list) -> (list) 200 | 201 | self.errors = [] 202 | 203 | children = cmds.listRelatives(obj, fullPath=True, ad=True, type="mesh") 204 | 205 | for obj in children: 206 | try: 207 | errs = cmds.polyInfo(obj, nmv=True) 208 | if errs: 209 | errorObj = Error(obj, errs) 210 | self.errors.append(errorObj) 211 | except RuntimeError: 212 | pass 213 | 214 | return self.errors 215 | 216 | def fixIt(self): 217 | pass 218 | 219 | 220 | class LaminaFaceChecker(BaseChecker): 221 | 222 | __name__ = "Lamina Faces" 223 | __category__ = "Topology" 224 | 225 | def checkIt(self, obj, settings=None): 226 | # type: (list) -> (list) 227 | 228 | errorsDict = {} 229 | self.errors = [] 230 | 231 | errs = cmds.checkMesh(obj, c=3) 232 | 233 | for e in errs: 234 | base, comp = e.split(".") 235 | 236 | if base in errorsDict: 237 | errorsDict[base].append(e) 238 | else: 239 | errorsDict[base] = [e] 240 | 241 | for err_key in errorsDict: 242 | components = errorsDict[err_key] 243 | errorObj = Error(err_key, errorsDict[err_key]) 244 | self.errors.append(errorObj) 245 | 246 | return self.errors 247 | 248 | def fixIt(self): 249 | pass 250 | 251 | 252 | class BiValentFaceChecker(BaseChecker): 253 | 254 | __name__ = "Bi-valent Faces" 255 | __category__ = "Topology" 256 | 257 | def checkIt(self, obj, settings=None): 258 | # type: (list) -> (list) 259 | 260 | errorsDict = {} 261 | self.errors = [] 262 | 263 | errs = cmds.checkMesh(obj, c=4) 264 | 265 | for e in errs: 266 | base, comp = e.split(".") 267 | 268 | if base in errorsDict: 269 | errorsDict[base].append(e) 270 | else: 271 | errorsDict[base] = [e] 272 | 273 | for err_key in errorsDict: 274 | components = errorsDict[err_key] 275 | errorObj = Error(err_key, errorsDict[err_key]) 276 | self.errors.append(errorObj) 277 | 278 | return self.errors 279 | 280 | def fixIt(self): 281 | pass 282 | 283 | 284 | class ZeroAreaFaceChecker(BaseChecker): 285 | 286 | __name__ = "Zero Area Faces" 287 | __category__ = "Topology" 288 | 289 | def checkIt(self, obj, settings): 290 | # type: (list) -> (list) 291 | 292 | mfa = settings.getSettings()['maxFaceArea'] 293 | 294 | errorsDict = {} 295 | self.errors = [] 296 | 297 | errs = cmds.checkMesh(obj, c=5, maxFaceArea=mfa) 298 | 299 | for e in errs: 300 | base, comp = e.split(".") 301 | 302 | if base in errorsDict: 303 | errorsDict[base].append(e) 304 | else: 305 | errorsDict[base] = [e] 306 | 307 | for err_key in errorsDict: 308 | components = errorsDict[err_key] 309 | errorObj = Error(err_key, errorsDict[err_key]) 310 | self.errors.append(errorObj) 311 | 312 | return self.errors 313 | 314 | def fixIt(self): 315 | pass 316 | 317 | 318 | class MeshBorderEdgeChecker(BaseChecker): 319 | 320 | __name__ = "Mesh Border Edges" 321 | isWarning = True 322 | __category__ = "Topology" 323 | 324 | def checkIt(self, obj, settings=None): 325 | # type: (list) -> (list) 326 | 327 | errorsDict = {} 328 | self.errors = [] 329 | 330 | errs = cmds.checkMesh(obj, c=6) 331 | 332 | for e in errs: 333 | base, comp = e.split(".") 334 | 335 | if base in errorsDict: 336 | errorsDict[base].append(e) 337 | else: 338 | errorsDict[base] = [e] 339 | 340 | for err_key in errorsDict: 341 | components = errorsDict[err_key] 342 | errorObj = Error(err_key, errorsDict[err_key]) 343 | self.errors.append(errorObj) 344 | 345 | return self.errors 346 | 347 | def fixIt(self): 348 | pass 349 | 350 | 351 | class CreaseEdgeChecker(BaseChecker): 352 | 353 | __name__ = "Crease Edges" 354 | __category__ = "Topology" 355 | 356 | def checkIt(self, obj, settings=None): 357 | # type: (list) -> (list) 358 | 359 | errorsDict = {} 360 | self.errors = [] 361 | 362 | errs = cmds.checkMesh(obj, c=7) 363 | 364 | for e in errs: 365 | base, comp = e.split(".") 366 | 367 | if base in errorsDict: 368 | errorsDict[base].append(e) 369 | else: 370 | errorsDict[base] = [e] 371 | 372 | for err_key in errorsDict: 373 | components = errorsDict[err_key] 374 | errorObj = Error(err_key, errorsDict[err_key]) 375 | self.errors.append(errorObj) 376 | 377 | return self.errors 378 | 379 | def fixIt(self): 380 | pass 381 | 382 | 383 | class ZeroLengthEdgeChecker(BaseChecker): 384 | 385 | __name__ = "Zero-length Edges" 386 | __category__ = "Topology" 387 | 388 | def checkIt(self, obj, settings=None): 389 | # type: (list) -> (list) 390 | 391 | errorsDict = {} 392 | self.errors = [] 393 | 394 | errs = cmds.checkMesh(obj, c=8) 395 | 396 | for e in errs: 397 | base, comp = e.split(".") 398 | 399 | if base in errorsDict: 400 | errorsDict[base].append(e) 401 | else: 402 | errorsDict[base] = [e] 403 | 404 | for err_key in errorsDict: 405 | components = errorsDict[err_key] 406 | errorObj = Error(err_key, errorsDict[err_key]) 407 | self.errors.append(errorObj) 408 | 409 | return self.errors 410 | 411 | def fixIt(self): 412 | pass 413 | 414 | 415 | class VertexPntsChecker(BaseChecker): 416 | 417 | __name__ = "Vertex Pnts Attribute" 418 | __category__ = "Attribute" 419 | isFixable = True 420 | 421 | def checkIt(self, obj, settings=None): 422 | # type: (list) -> (list) 423 | 424 | self.errors = [] 425 | 426 | errs = cmds.checkMesh(obj, c=9) 427 | 428 | for e in errs: 429 | errObj = Error(e) 430 | self.errors.append(errObj) 431 | 432 | return self.errors 433 | 434 | def fixIt(self): 435 | mSel = OpenMaya.MSelectionList() 436 | for n, e in enumerate(self.errors): 437 | if cmds.objExists(e.longName): 438 | obj = e.longName 439 | mSel.add(obj) 440 | try: 441 | cmds.polyMoveVertex( 442 | obj, lt=(0, 0, 0), nodeState=1, ch=False) 443 | except RuntimeError: 444 | pass 445 | 446 | class EmptyGeometryChecker(BaseChecker): 447 | 448 | __name__ = "Empty Geometry" 449 | __category__ = "Topology" 450 | isFixable = False 451 | 452 | def checkIt(self, obj, settings=None): 453 | # type: (list) -> (list) 454 | 455 | self.errors = [] 456 | 457 | errs = cmds.checkMesh(obj, c=10) 458 | 459 | for e in errs: 460 | errObj = Error(e) 461 | self.errors.append(errObj) 462 | 463 | return self.errors 464 | 465 | def fixIt(self): 466 | pass 467 | 468 | 469 | class NameChecker(BaseChecker): 470 | 471 | __name__ = "Name" 472 | __category__ = "Name" 473 | isEnabled = False 474 | 475 | def checkIt(self, objs, settings=None): 476 | # type: (list) -> (list) 477 | 478 | self.errors = [] 479 | 480 | for obj in objs: 481 | try: 482 | pass 483 | except RuntimeError: 484 | pass 485 | 486 | return self.errors 487 | 488 | def fixIt(self): 489 | pass 490 | 491 | 492 | class ShapeNameChecker(BaseChecker): 493 | 494 | __name__ = "ShapeName" 495 | __category__ = "Name" 496 | isFixable = True 497 | 498 | def checkIt(self, root, settings=None): 499 | # type: (list) -> (list) 500 | 501 | self.errors = [] 502 | 503 | objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] 504 | objs.insert(0, root) 505 | 506 | for obj in objs: 507 | shapes = cmds.listRelatives( 508 | obj, children=True, fullPath=True, shapes=True) or [] 509 | if shapes: 510 | for shape in shapes: 511 | isIntermediate = cmds.getAttr( 512 | shape + ".intermediateObject") 513 | if isIntermediate: 514 | continue 515 | shortName = obj.split("|")[-1] 516 | shapeShortName = shape.split("|")[-1] 517 | 518 | if shortName + "Shape" != shapeShortName: 519 | err = Error(shape) 520 | self.errors.append(err) 521 | 522 | return self.errors 523 | 524 | def fixIt(self): 525 | for e in self.errors: 526 | shape = e.longName 527 | parent = cmds.listRelatives(shape, parent=True, fullPath=False)[0] 528 | newShapeName = parent + "Shape" 529 | cmds.rename(shape, newShapeName) 530 | 531 | 532 | class HistoryChecker(BaseChecker): 533 | 534 | __name__ = "History" 535 | __category__ = "Node" 536 | isEnabled = True 537 | isFixable = True 538 | 539 | def checkIt(self, root, settings=None): 540 | # type: (list) -> (list) 541 | 542 | self.errors = [] 543 | 544 | objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] 545 | objs.insert(0, root) 546 | 547 | for obj in objs: 548 | mesh = cmds.listRelatives(obj, children=True, type="mesh") 549 | if mesh is not None: 550 | for m in mesh: 551 | inMesh = cmds.listConnections(m + ".inMesh", source=True) 552 | if inMesh is not None: 553 | err = Error(obj) 554 | self.errors.append(err) 555 | 556 | return self.errors 557 | 558 | def fixIt(self): 559 | 560 | for e in self.errors: 561 | cmds.delete(e.longName, ch=True) 562 | 563 | 564 | class TransformChecker(BaseChecker): 565 | 566 | __name__ = "Transform" 567 | __category__ = "Attribute" 568 | 569 | def checkIt(self, root, settings=None): 570 | # type: (list) -> (list) 571 | 572 | ignore = [] 573 | 574 | self.errors = [] 575 | 576 | identity = OpenMaya.MMatrix.kIdentity 577 | mSel = OpenMaya.MSelectionList() 578 | 579 | objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] 580 | objs.insert(0, root) 581 | 582 | for n, i in enumerate(objs): 583 | mSel.add(i) 584 | dagPath = mSel.getDagPath(n) 585 | groupName = dagPath.fullPathName().split("|")[-1] 586 | if groupName in ignore: 587 | continue 588 | dagNode = OpenMaya.MFnDagNode(dagPath) 589 | transform = dagNode.transformationMatrix() 590 | if not transform == identity: 591 | errorObj = Error(i) 592 | self.errors.append(errorObj) 593 | 594 | return self.errors 595 | 596 | def fixIt(self): 597 | pass 598 | 599 | 600 | class LockedTransformChecker(BaseChecker): 601 | 602 | __name__ = "Locked Transform" 603 | __category__ = "Attribute" 604 | isFixable = True 605 | 606 | def __init__(self): 607 | super(LockedTransformChecker, self).__init__() 608 | self.attrs = ["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz"] 609 | 610 | def checkIt(self, root, settings=None): 611 | # type: (list) -> (list) 612 | 613 | self.errors = [] 614 | 615 | objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] 616 | objs.insert(0, root) 617 | 618 | for obj in objs: 619 | try: 620 | for at in self.attrs: 621 | isLocked = cmds.getAttr(obj + ".{}".format(at), lock=True) 622 | if isLocked: 623 | err = Error(obj) 624 | self.errors.append(err) 625 | break 626 | except RuntimeError: 627 | pass 628 | 629 | return self.errors 630 | 631 | def fixIt(self): 632 | for e in self.errors: 633 | for at in self.attrs: 634 | cmds.setAttr(e.longName + ".{}".format(at), lock=False) 635 | 636 | 637 | class SmoothPreviewChecker(BaseChecker): 638 | 639 | __name__ = "Smooth Preview" 640 | __category__ = "Attribute" 641 | isFixable = True 642 | 643 | def checkIt(self, root, settings=None): 644 | # type: (list) -> (list) 645 | 646 | self.errors = [] 647 | 648 | meshes = cmds.listRelatives(root, ad=True, fullPath=True, type="mesh") or [] 649 | 650 | for mesh in meshes: 651 | isSmooth = cmds.getAttr(mesh + ".displaySmoothMesh") 652 | 653 | if isSmooth: 654 | err = Error(mesh) 655 | self.errors.append(err) 656 | 657 | return self.errors 658 | 659 | def fixIt(self): 660 | 661 | for e in self.errors: 662 | cmds.setAttr(e.longName + ".displaySmoothMesh", 0) 663 | 664 | 665 | class KeyframeChecker(BaseChecker): 666 | 667 | __name__ = "Keyframe" 668 | __category__ = "Attribute" 669 | isFixable = True 670 | 671 | def checkIt(self, root, settings=None): 672 | # type: (list) -> (list) 673 | 674 | self.errors = [] 675 | 676 | keyNodes = ["animCurveTU", "animCurveTA", "animCurveTL"] 677 | 678 | objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] 679 | objs.insert(0, root) 680 | 681 | for i in objs: 682 | conns = cmds.listConnections(i, source=True) 683 | keys = [] 684 | 685 | if conns is None: 686 | continue 687 | 688 | for c in conns: 689 | if cmds.objectType(c) in keyNodes: 690 | keys.append(c) 691 | if keys: 692 | err = Error(i, keys) 693 | self.errors.append(err) 694 | 695 | return self.errors 696 | 697 | def fixIt(self): 698 | 699 | for e in self.errors: 700 | cmds.delete(e.components) 701 | 702 | 703 | class UnusedVertexChecker(BaseChecker): 704 | """ Unused vertex checker class """ 705 | 706 | __name__ = "Unused Vertices" 707 | __category__ = "Topology" 708 | 709 | def __init__(self): 710 | super(UnusedVertexChecker, self).__init__() 711 | 712 | def checkIt(self, obj, settings=None): 713 | # type: (list) -> (list) 714 | 715 | errorsDict = {} 716 | self.errors = [] 717 | 718 | errs = cmds.checkMesh(obj, c=11) 719 | 720 | for e in errs: 721 | base, comp = e.split(".") 722 | 723 | if base in errorsDict: 724 | errorsDict[base].append(e) 725 | else: 726 | errorsDict[base] = [e] 727 | 728 | for err_key in errorsDict: 729 | components = errorsDict[err_key] 730 | errorObj = Error(err_key, errorsDict[err_key]) 731 | self.errors.append(errorObj) 732 | 733 | return self.errors 734 | 735 | def fixIt(self): 736 | """ Unused vertices ARE fixable """ 737 | 738 | pass 739 | 740 | 741 | class IntermediateObjectChecker(BaseChecker): 742 | 743 | __name__ = "Intermediate Object" 744 | __category__ = "Node" 745 | isFixable = True 746 | 747 | def checkIt(self, root, settings=None): 748 | # type: (list) -> (list) 749 | 750 | self.errors = [] 751 | 752 | meshes = cmds.listRelatives(root, ad=True, fullPath=True, type="mesh") 753 | 754 | for mesh in meshes: 755 | isIntermediate = cmds.getAttr(mesh + ".intermediateObject") 756 | if isIntermediate: 757 | err = Error(mesh) 758 | self.errors.append(err) 759 | 760 | return self.errors 761 | 762 | def fixIt(self): 763 | for e in self.errors: 764 | shape = e.longName 765 | 766 | if cmds.objExists(shape): 767 | parents = cmds.listRelatives( 768 | shape, fullPath=True, parent=True) or [] 769 | for i in parents: 770 | # Delete history for parents 771 | cmds.delete(i, ch=True) 772 | try: 773 | cmds.delete(shape) 774 | except ValueError: 775 | pass 776 | 777 | 778 | class InstanceShapeChecker(BaseChecker): 779 | 780 | __name__ = "Instance Shape" 781 | __category__ = "Node" 782 | 783 | def checkIt(self, obj, settings=None): 784 | # type: (list) -> (list) 785 | 786 | self.errors = [] 787 | 788 | errs = cmds.checkMesh(obj, c=12) 789 | 790 | for e in errs: 791 | errObj = Error(e) 792 | self.errors.append(errObj) 793 | 794 | return self.errors 795 | 796 | def fixIt(self): 797 | pass 798 | 799 | 800 | class ConnectionChecker(BaseChecker): 801 | 802 | __name__ = "Connections" 803 | __category__ = "Node" 804 | 805 | def checkIt(self, obj, settings=None): 806 | # type: (list) -> (list) 807 | 808 | self.errors = [] 809 | 810 | errs = cmds.checkMesh(obj, c=13) 811 | 812 | for e in errs: 813 | errObj = Error(e) 814 | self.errors.append(errObj) 815 | 816 | return self.errors 817 | 818 | def fixIt(self): 819 | pass 820 | 821 | 822 | class DisplayLayerCheck(BaseChecker): 823 | 824 | __name__ = "Display layers" 825 | __category__ = "other" 826 | isFixable = True 827 | 828 | def checkIt(self, root, settings=None): 829 | # type: (list) -> (list) 830 | 831 | self.errors = [] 832 | 833 | objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] 834 | objs.insert(0, root) 835 | 836 | for obj in objs: 837 | layers = cmds.listConnections(obj + ".drawOverride") or [] 838 | if layers: 839 | err = Error(obj, layers) 840 | self.errors.append(err) 841 | 842 | return self.errors 843 | 844 | def fixIt(self): 845 | 846 | for e in self.errors: 847 | layers = e.components 848 | node = e.longName 849 | for layer in layers: 850 | cmds.disconnectAttr( 851 | layer + ".drawInfo", node + ".drawOverride") 852 | 853 | 854 | class UnusedLayerChecker(BaseChecker): 855 | 856 | __name__ = "Unused layers" 857 | __category__ = "other" 858 | isFixable = True 859 | 860 | def checkIt(self, root, settings=None): 861 | # type: (list) -> (list) 862 | 863 | self.errors = [] 864 | 865 | layers = cmds.ls(type="displayLayer") 866 | layers.remove("defaultLayer") 867 | for layer in layers: 868 | contents = cmds.editDisplayLayerMembers( 869 | layer, q=True, fullNames=True) 870 | if contents is None: 871 | err = Error(layer, [layer]) 872 | self.errors.append(err) 873 | 874 | return self.errors 875 | 876 | def fixIt(self): 877 | for e in self.errors: 878 | try: 879 | cmds.delete(e.longName) 880 | except RuntimeError: 881 | pass 882 | 883 | 884 | class Map1Checker(BaseChecker): 885 | 886 | __name__ = "UVSet to map1" 887 | __category__ = "UV" 888 | isFixable = True 889 | 890 | def checkIt(self, root, settings=None): 891 | # type: (list) -> (list) 892 | 893 | self.errors = [] 894 | 895 | meshes = cmds.listRelatives(root, ad=True, fullPath=True, type="mesh") or [] 896 | 897 | for mesh in meshes: 898 | curUVSet = cmds.polyUVSet(mesh, q=True, currentUVSet=True)[0] 899 | if curUVSet != "map1": 900 | err = Error(mesh) 901 | self.errors.append(err) 902 | 903 | return self.errors 904 | 905 | def fixIt(self): 906 | 907 | for e in self.errors: 908 | cmds.polyUVSet(e.longName, uvSet="map1", currentUVSet=True) 909 | 910 | 911 | class NegativeUvChecker(BaseChecker): 912 | 913 | __name__ = "UVs in negative space" 914 | __category__ = "UV" 915 | 916 | def checkIt(self, obj, settings=None): 917 | 918 | self.errors = [] 919 | errorsDict = {} 920 | 921 | errs = cmds.checkUV(obj, c=4) 922 | 923 | for e in errs: 924 | base, comp = e.split(".") 925 | 926 | if base in errorsDict: 927 | errorsDict[base].append(e) 928 | else: 929 | errorsDict[base] = [e] 930 | 931 | for err_key in errorsDict: 932 | components = errorsDict[err_key] 933 | errorObj = Error(err_key, errorsDict[err_key]) 934 | self.errors.append(errorObj) 935 | 936 | return self.errors 937 | 938 | def fixIt(self): 939 | pass 940 | 941 | 942 | class UdimIntersectionChecker(BaseChecker): 943 | 944 | __name__ = "UDIM intersection" 945 | __category__ = "UV" 946 | 947 | def checkIt(self, obj, settings=None): 948 | # type: (list) -> (list) 949 | 950 | self.errors = [] 951 | 952 | errorsDict = {} 953 | 954 | errs = cmds.checkUV(obj, c=0) 955 | 956 | for e in errs: 957 | base, comp = e.split(".") 958 | 959 | if base in errorsDict: 960 | errorsDict[base].append(e) 961 | else: 962 | errorsDict[base] = [e] 963 | 964 | for err_key in errorsDict: 965 | components = errorsDict[err_key] 966 | errorObj = Error(err_key, errorsDict[err_key]) 967 | self.errors.append(errorObj) 968 | 969 | return self.errors 970 | 971 | def fixIt(self): 972 | pass 973 | 974 | 975 | class UnassignedUvChecker(BaseChecker): 976 | 977 | __name__ = "Unassigned UVs" 978 | __category__ = "UV" 979 | 980 | def checkIt(self, obj, settings=None): 981 | # type: (list) -> (list) 982 | 983 | self.errors = [] 984 | 985 | errorsDict = {} 986 | 987 | errs = cmds.checkUV(obj, c=3) 988 | 989 | for e in errs: 990 | base, comp = e.split(".") 991 | 992 | if base in errorsDict: 993 | errorsDict[base].append(e) 994 | else: 995 | errorsDict[base] = [e] 996 | 997 | for err_key in errorsDict: 998 | components = errorsDict[err_key] 999 | errorObj = Error(err_key, errorsDict[err_key]) 1000 | self.errors.append(errorObj) 1001 | 1002 | return self.errors 1003 | 1004 | def fixIt(self): 1005 | pass 1006 | 1007 | 1008 | class UnmappedPolygonFaceChecker(BaseChecker): 1009 | 1010 | __name__ = "Unmapped polygon faces" 1011 | __category__ = "UV" 1012 | 1013 | def checkIt(self, obj, settings=None): 1014 | # type: (list) -> (list) 1015 | 1016 | self.errors = [] 1017 | 1018 | errorsDict = {} 1019 | 1020 | errs = cmds.checkUV(obj, c=1) 1021 | 1022 | for e in errs: 1023 | base, comp = e.split(".") 1024 | 1025 | if base in errorsDict: 1026 | errorsDict[base].append(e) 1027 | else: 1028 | errorsDict[base] = [e] 1029 | 1030 | for err_key in errorsDict: 1031 | components = errorsDict[err_key] 1032 | errorObj = Error(err_key, errorsDict[err_key]) 1033 | self.errors.append(errorObj) 1034 | 1035 | return self.errors 1036 | 1037 | def fixIt(self): 1038 | pass 1039 | 1040 | 1041 | class ZeroAreaUVFaceChecker(BaseChecker): 1042 | 1043 | __name__ = "Zero area UV Faces" 1044 | __category__ = "UV" 1045 | 1046 | def checkIt(self, obj, settings=None): 1047 | # type: (list) -> (list) 1048 | 1049 | self.errors = [] 1050 | errorsDict = {} 1051 | 1052 | errs = cmds.checkUV(obj, c=2) 1053 | 1054 | for e in errs: 1055 | base, comp = e.split(".") 1056 | 1057 | if base in errorsDict: 1058 | errorsDict[base].append(e) 1059 | else: 1060 | errorsDict[base] = [e] 1061 | 1062 | for err_key in errorsDict: 1063 | components = errorsDict[err_key] 1064 | errorObj = Error(err_key, errorsDict[err_key]) 1065 | self.errors.append(errorObj) 1066 | 1067 | return self.errors 1068 | 1069 | def fixIt(self): 1070 | pass 1071 | 1072 | 1073 | class ConcaveUVChecker(BaseChecker): 1074 | 1075 | __name__ = "Concave UV Faces" 1076 | __category__ = "UV" 1077 | 1078 | def checkIt(self, obj, settings=None): 1079 | # type: (list) -> (list) 1080 | 1081 | self.errors = [] 1082 | errorsDict = {} 1083 | 1084 | errs = cmds.checkUV(obj, c=5) 1085 | 1086 | for e in errs: 1087 | base, comp = e.split(".") 1088 | 1089 | if base in errorsDict: 1090 | errorsDict[base].append(e) 1091 | else: 1092 | errorsDict[base] = [e] 1093 | 1094 | for err_key in errorsDict: 1095 | components = errorsDict[err_key] 1096 | errorObj = Error(err_key, errorsDict[err_key]) 1097 | self.errors.append(errorObj) 1098 | 1099 | return self.errors 1100 | 1101 | def fixIt(self): 1102 | pass 1103 | 1104 | 1105 | class ReversedUVChecker(BaseChecker): 1106 | 1107 | __name__ = "Reversed UV Faces" 1108 | __category__ = "UV" 1109 | isWarning = True 1110 | 1111 | def checkIt(self, obj, settings=None): 1112 | # type: (list) -> (list) 1113 | 1114 | self.errors = [] 1115 | errorsDict = {} 1116 | 1117 | errs = cmds.checkUV(obj, c=6) 1118 | 1119 | for e in errs: 1120 | base, comp = e.split(".") 1121 | 1122 | if base in errorsDict: 1123 | errorsDict[base].append(e) 1124 | else: 1125 | errorsDict[base] = [e] 1126 | 1127 | for err_key in errorsDict: 1128 | components = errorsDict[err_key] 1129 | errorObj = Error(err_key, errorsDict[err_key]) 1130 | self.errors.append(errorObj) 1131 | 1132 | return self.errors 1133 | 1134 | def fixIt(self): 1135 | pass 1136 | 1137 | 1138 | class UvOverlapChecker(BaseChecker): 1139 | 1140 | __name__ = "UV Overlaps" 1141 | __category__ = "UV" 1142 | isEnabled = False 1143 | 1144 | def checkIt(self, root, settings=None): 1145 | 1146 | self.errors = [] 1147 | 1148 | objs = cmds.listRelatives(root, ad=True, fullPath=True, type="mesh") or [] 1149 | 1150 | mSel = OpenMaya.MSelectionList() 1151 | 1152 | for obj in objs: 1153 | mSel.add(obj) 1154 | 1155 | for i in range(mSel.length()): 1156 | dagPath = mSel.getDagPath(i) 1157 | try: 1158 | dagPath.extendToShape() 1159 | 1160 | except RuntimeError: 1161 | # Not mesh. Do no nothing 1162 | pass 1163 | 1164 | return self.errors 1165 | 1166 | def fixIt(self): 1167 | pass 1168 | 1169 | 1170 | class SelectionSetChecker(BaseChecker): 1171 | 1172 | __name__ = "Selection Sets" 1173 | __category__ = "other" 1174 | isFixable = True 1175 | 1176 | def getSets(self, path, typ): 1177 | 1178 | if typ == "transform": 1179 | conns = cmds.listConnections(path + ".instObjGroups") or [] 1180 | return [i for i in conns if cmds.objectType(i) == "objectSet"] 1181 | elif typ == "shape": 1182 | conns = cmds.listConnections( 1183 | path + ".instObjGroups.objectGroups") or [] 1184 | return [i for i in conns if cmds.objectType(i) == "objectSet"] 1185 | else: 1186 | pass 1187 | 1188 | return [] 1189 | 1190 | def checkIt(self, root, settings=None): 1191 | # type: (list) -> (list) 1192 | 1193 | self.errors = [] 1194 | objectSets = [] 1195 | ignore = ["modelPanel[0-9]ViewSelectedSet"] 1196 | 1197 | objs = cmds.listRelatives(root, ad=True, fullPath=True, type="transform") or [] 1198 | objs.insert(0, root) 1199 | 1200 | for obj in objs: 1201 | shapes = cmds.listRelatives( 1202 | obj, children=True, fullPath=True, shapes=True) or [] 1203 | 1204 | for shape in shapes: 1205 | objectSets.extend(self.getSets(shape, "shape")) 1206 | 1207 | objectSets.extend(self.getSets(obj, "transform")) 1208 | 1209 | objectSets = list(set(objectSets)) 1210 | 1211 | for objSet in objectSets: 1212 | for i in ignore: 1213 | if re.match(i, objSet) is None: 1214 | err = Error(objSet) 1215 | self.errors.append(err) 1216 | 1217 | return self.errors 1218 | 1219 | def fixIt(self): 1220 | for e in self.errors: 1221 | try: 1222 | cmds.delete(e.longName) 1223 | except Exception: 1224 | pass 1225 | 1226 | 1227 | class ColorSetChecker(BaseChecker): 1228 | 1229 | __name__ = "Color Sets" 1230 | __category__ = "other" 1231 | isFixable = True 1232 | 1233 | def checkIt(self, root, settings=None): 1234 | # type: (list) -> (list) 1235 | 1236 | # Reset result 1237 | self.errors = [] 1238 | 1239 | objs = cmds.listRelatives(root, ad=True, fullPath=True, type="mesh") or [] 1240 | 1241 | for obj in objs: 1242 | try: 1243 | allColorSets = cmds.polyColorSet( 1244 | obj, q=True, allColorSets=True) 1245 | if allColorSets is None: 1246 | continue 1247 | else: 1248 | err = Error(obj) 1249 | self.errors.append(err) 1250 | except RuntimeError: 1251 | pass 1252 | 1253 | return self.errors 1254 | 1255 | def fixIt(self): 1256 | for i in self.errors: 1257 | allSets = cmds.polyColorSet( 1258 | i.longName, q=True, allColorSets=True) or [] 1259 | for s in allSets: 1260 | cmds.polyColorSet(i.longName, delete=True, colorSet=s) 1261 | 1262 | 1263 | CHECKERS = [ 1264 | NameChecker, 1265 | ShapeNameChecker, 1266 | HistoryChecker, 1267 | TransformChecker, 1268 | LockedTransformChecker, 1269 | SmoothPreviewChecker, 1270 | KeyframeChecker, 1271 | TriangleChecker, 1272 | NgonChecker, 1273 | NonmanifoldEdgeChecker, 1274 | NonmanifoldVertexChecker, 1275 | LaminaFaceChecker, 1276 | BiValentFaceChecker, 1277 | ZeroAreaFaceChecker, 1278 | MeshBorderEdgeChecker, 1279 | CreaseEdgeChecker, 1280 | ZeroLengthEdgeChecker, 1281 | VertexPntsChecker, 1282 | EmptyGeometryChecker, 1283 | UnusedVertexChecker, 1284 | IntermediateObjectChecker, 1285 | InstanceShapeChecker, 1286 | ConnectionChecker, 1287 | DisplayLayerCheck, 1288 | UnusedLayerChecker, 1289 | Map1Checker, 1290 | NegativeUvChecker, 1291 | UdimIntersectionChecker, 1292 | UnassignedUvChecker, 1293 | UnmappedPolygonFaceChecker, 1294 | ZeroAreaUVFaceChecker, 1295 | ConcaveUVChecker, 1296 | ReversedUVChecker, 1297 | UvOverlapChecker, 1298 | SelectionSetChecker, 1299 | ColorSetChecker] 1300 | -------------------------------------------------------------------------------- /framelayout.py: -------------------------------------------------------------------------------- 1 | """ qt framelayout sample """ 2 | 3 | try: 4 | from PySide6 import QtWidgets, QtCore 5 | except ImportError: 6 | from PySide2 import QtWidgets, QtCore 7 | from . import icon 8 | 9 | 10 | class TitleLabel(QtWidgets.QLabel): 11 | 12 | clicked = QtCore.Signal() 13 | 14 | def __init__(self, text="", parent=None): 15 | super(TitleLabel, self).__init__(parent) 16 | 17 | self.setText(text) 18 | 19 | def mousePressEvent(self, event): 20 | self.clicked.emit() 21 | 22 | 23 | class FrameLayout(QtWidgets.QWidget): 24 | 25 | def __init__(self, title="", parent=None): 26 | super(FrameLayout, self).__init__(parent) 27 | 28 | self.baseTitle = title 29 | self.rightArrow = u"\u25b6 " 30 | self.downArrow = u"\u25bc " 31 | 32 | titleLayout = QtWidgets.QHBoxLayout() 33 | self.title = self.rightArrow + title 34 | 35 | self.titleLabel = TitleLabel(self.title) 36 | self.titleLabel.setSizePolicy( 37 | QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) 38 | self.titleLabel.clicked.connect(self.titleClicked) 39 | self.statusIconLabel = QtWidgets.QLabel() 40 | self.statusIconLabel.setPixmap(icon.neutralIconPixmap) 41 | 42 | self.childrenWidget = QtWidgets.QWidget() 43 | self.childrenLayout = QtWidgets.QVBoxLayout() 44 | self.childrenLayout.setContentsMargins(0, 0, 0, 0) 45 | 46 | self.childrenWidget.setLayout(self.childrenLayout) 47 | 48 | titleLayout.addWidget(self.statusIconLabel) 49 | titleLayout.addWidget(self.titleLabel) 50 | 51 | layout = QtWidgets.QVBoxLayout() 52 | layout.setContentsMargins(0, 0, 0, 0) 53 | layout.addLayout(titleLayout) 54 | layout.addWidget(self.childrenWidget) 55 | self.setLayout(layout) 56 | 57 | # Close frame by default 58 | self.childrenWidget.hide() 59 | 60 | def titleClicked(self): 61 | """ 62 | title clicked action 63 | 64 | """ 65 | 66 | newTitle = "" 67 | 68 | if self.childrenWidget.isVisible(): 69 | self.childrenWidget.hide() 70 | newTitle = self.rightArrow + self.baseTitle 71 | else: 72 | self.childrenWidget.show() 73 | newTitle = self.downArrow + self.baseTitle 74 | 75 | self.titleLabel.setText(newTitle) 76 | 77 | def collapse(self): 78 | newTitle = "" 79 | self.childrenWidget.hide() 80 | newTitle = self.rightArrow + self.baseTitle 81 | self.titleLabel.setText(newTitle) 82 | 83 | def expand(self): 84 | newTitle = "" 85 | self.childrenWidget.show() 86 | newTitle = self.rightArrow + self.baseTitle 87 | self.titleLabel.setText(newTitle) 88 | 89 | def addWidget(self, widget): 90 | # type: (QtWidgets.QWidget) -> None 91 | """ 92 | Add widgets 93 | 94 | """ 95 | 96 | self.childrenLayout.addWidget(widget) 97 | 98 | def addLayout(self, layout): 99 | self.childrenLayout.addLayout(layout) 100 | 101 | def setStatusIcon(self, status): 102 | # type: (str) -> None 103 | """ 104 | Change status icon 105 | 106 | """ 107 | 108 | if status == "good": 109 | self.statusIconLabel.setPixmap(icon.goodIconPixmap) 110 | elif status == "bad": 111 | self.statusIconLabel.setPixmap(icon.errorIconPixmap) 112 | elif status == "warning": 113 | self.statusIconLabel.setPixmap(icon.warningIconPixmap) 114 | elif status == "neutral": 115 | self.statusIconLabel.setPixmap(icon.neutralIconPixmap) 116 | else: 117 | pass 118 | -------------------------------------------------------------------------------- /icon.py: -------------------------------------------------------------------------------- 1 | """ wip 2 | 3 | https://www.iconfinder.com/iconsets/fatcow 4 | https://creativecommons.org/licenses/by/3.0/us/ 5 | "Farm-fresh" by FatCow Web Hosting is licensed under CC BY 3.0 6 | 7 | """ 8 | 9 | try: 10 | from PySide6 import QtCore, QtGui 11 | except ImportError: 12 | from PySide2 import QtCore, QtGui 13 | 14 | b64Error = b"""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""" 15 | 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""" 16 | b64Good = b"""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""" 17 | 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==""" 18 | 19 | tempPixmap = QtGui.QPixmap() 20 | 21 | errorData = QtCore.QByteArray.fromBase64(b64Error) 22 | warningData = QtCore.QByteArray.fromBase64(b64Warning) 23 | goodData = QtCore.QByteArray.fromBase64(b64Good) 24 | neutralData = QtCore.QByteArray.fromBase64(b64Neutral) 25 | 26 | tempPixmap.loadFromData(errorData) 27 | errorIconPixmap = tempPixmap.scaled(20, 20, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) 28 | 29 | tempPixmap.loadFromData(warningData) 30 | warningIconPixmap = tempPixmap.scaled(20, 20, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) 31 | 32 | tempPixmap.loadFromData(goodData) 33 | goodIconPixmap = tempPixmap.scaled(20, 20, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) 34 | 35 | tempPixmap.loadFromData(neutralData) 36 | neutralIconPixmap = tempPixmap.scaled(20, 20, QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation) 37 | -------------------------------------------------------------------------------- /images/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minoue/ModelCheckerForMaya/24f9d068bff0b3dd211a077ca5c96e2b8f8c4f79/images/ui.png -------------------------------------------------------------------------------- /modelSanityChecker.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | module docstring here 4 | """ 5 | 6 | from maya.app.general.mayaMixin import MayaQWidgetDockableMixin 7 | from maya import cmds 8 | from . import checker 9 | from . import framelayout 10 | 11 | MAYA_VERSION = 2025 12 | 13 | try: 14 | from PySide6 import QtCore, QtWidgets, QtGui 15 | except ImportError: 16 | from PySide2 import QtCore, QtWidgets, QtGui 17 | MAYA_VERSION = 2024 18 | 19 | try: 20 | from importlib import reload 21 | except ImportError: 22 | # if python2, use build-in reload 23 | pass 24 | 25 | reload(framelayout) 26 | reload(checker) 27 | 28 | 29 | class Separator(QtWidgets.QWidget): 30 | 31 | def __init__(self, category="", checkers=None): 32 | super(Separator, self).__init__() 33 | 34 | self.checkerWidgets = checkers 35 | self.category = category 36 | 37 | self.checkbox = QtWidgets.QCheckBox() 38 | self.checkbox.setChecked(True) 39 | self.checkbox.stateChanged.connect(self.checkboxToggle) 40 | 41 | line = QtWidgets.QFrame() 42 | line.setFrameShape(QtWidgets.QFrame.HLine) 43 | line.setSizePolicy(QtWidgets.QSizePolicy.Expanding, 44 | QtWidgets.QSizePolicy.Expanding) 45 | label = QtWidgets.QLabel(" " + category) 46 | font = QtGui.QFont() 47 | font.setItalic(True) 48 | font.setCapitalization(QtGui.QFont.AllUppercase) 49 | font.setBold(True) 50 | label.setFont(font) 51 | 52 | layout = QtWidgets.QHBoxLayout() 53 | layout.addWidget(self.checkbox) 54 | layout.addWidget(line) 55 | layout.addWidget(label) 56 | # layout.setSpacing(0) 57 | layout.setContentsMargins(2, 2, 2, 2) 58 | self.setLayout(layout) 59 | 60 | def checkboxToggle(self, *args): 61 | state = args[0] 62 | for w in self.checkerWidgets: 63 | if self.category == w.checker.category: 64 | if state == 2: 65 | w.setEnabled(True) 66 | else: 67 | w.setEnabled(False) 68 | 69 | 70 | class CheckerWidget(QtWidgets.QWidget): 71 | 72 | def __init__(self, chk, parent, settings=None): 73 | # type: (checker.BaseChecker) 74 | super(CheckerWidget, self).__init__() 75 | 76 | self.checker = chk 77 | self.settings = settings 78 | self.createUI() 79 | 80 | def createUI(self): 81 | layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight) 82 | layout.setContentsMargins(2, 2, 2, 2) 83 | layout.setSpacing(2) 84 | 85 | self.frame = framelayout.FrameLayout(self.checker.name) 86 | 87 | self.checkbox = QtWidgets.QCheckBox() 88 | self.checkbox.stateChanged.connect(self.toggleEnable) 89 | 90 | self.checkButton = QtWidgets.QPushButton("Check") 91 | # self.checkButton.setSizePolicy( 92 | # QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Expanding) 93 | self.checkButton.clicked.connect(self.check) 94 | self.fixButton = QtWidgets.QPushButton("Fix") 95 | self.fixButton.clicked.connect(self.fix) 96 | if self.checker.isFixable is not True: 97 | self.fixButton.setEnabled(False) 98 | self.selectAllButton = QtWidgets.QPushButton('Select') 99 | self.selectAllButton.clicked.connect(self.selectAll) 100 | 101 | buttonLayout = QtWidgets.QHBoxLayout() 102 | buttonLayout.addWidget(self.checkButton) 103 | buttonLayout.addWidget(self.fixButton) 104 | buttonLayout.addWidget(self.selectAllButton) 105 | 106 | self.errorList = QtWidgets.QListWidget() 107 | self.errorList.itemClicked.connect(self.errorSelected) 108 | 109 | self.frame.addWidget(self.errorList) 110 | self.frame.addLayout(buttonLayout) 111 | 112 | layout.addWidget(self.checkbox, alignment=QtCore.Qt.AlignTop) 113 | layout.addWidget(self.frame) 114 | 115 | self.setLayout(layout) 116 | 117 | if self.checker.isEnabled: 118 | self.setEnabled(True) 119 | else: 120 | self.setEnabled(False) 121 | 122 | def setEnabled(self, state): 123 | if state is True: 124 | self.checkbox.setChecked(True) 125 | self.frame.setEnabled(True) 126 | self.checker.isEnabled = True 127 | else: 128 | self.checkbox.setChecked(False) 129 | self.frame.setEnabled(False) 130 | self.frame.collapse() 131 | self.checker.isEnabled = False 132 | 133 | def check(self, path=None, dummy=None): 134 | if not self.checker.isEnabled: 135 | return 136 | 137 | root = self.parent().parent().parent().parent().rootLE.text() 138 | if root != "": 139 | path = root 140 | 141 | if path is None: 142 | sel = cmds.ls(sl=True, fl=True, long=True) 143 | if not sel: 144 | cmds.warning("Nothing is selected") 145 | return 146 | path = sel[0] 147 | 148 | self.doCheck(path) 149 | 150 | def toggleEnable(self, *args): 151 | state = args[0] 152 | 153 | if state == 2: 154 | self.setEnabled(True) 155 | else: 156 | self.setEnabled(False) 157 | 158 | def doCheck(self, obj): 159 | 160 | # Clear list items 161 | self.errorList.clear() 162 | 163 | errs = self.checker.checkIt(obj, self.settings) 164 | 165 | if errs: 166 | for err in errs: 167 | self.errorList.addItem(err) 168 | if self.checker.isWarning: 169 | self.frame.setStatusIcon("warning") 170 | else: 171 | self.frame.setStatusIcon("bad") 172 | else: 173 | self.frame.setStatusIcon("good") 174 | 175 | def fix(self): 176 | if not self.checker.isEnabled: 177 | return 178 | 179 | self.checker.fixIt() 180 | 181 | # Re-check 182 | self.check() 183 | 184 | def errorSelected(self, *args): 185 | """ 186 | Select error components 187 | 188 | """ 189 | 190 | err = args[0] 191 | if err.components is None: 192 | cmds.select(err.longName, r=True) 193 | else: 194 | cmds.select(err.components, r=True) 195 | 196 | def selectAll(self): 197 | errs = [i.longName for i in self.checker.errors] 198 | cmds.select(errs, r=True) 199 | 200 | 201 | class Settings(QtWidgets.QWidget): 202 | 203 | def __init__(self, parent=None): 204 | super(Settings, self).__init__(parent) 205 | 206 | self.createUI() 207 | 208 | def createUI(self): 209 | 210 | self.maxFaceArea = QtWidgets.QLineEdit("0.000001") 211 | 212 | layout = QtWidgets.QGridLayout() 213 | layout.setAlignment(QtCore.Qt.AlignTop) 214 | layout.addWidget(QtWidgets.QLabel("Max face area"), 0, 0) 215 | layout.addWidget(self.maxFaceArea, 0, 1) 216 | self.setLayout(layout) 217 | 218 | def getSettings(self): 219 | 220 | data = { 221 | "maxFaceArea": float(self.maxFaceArea.text()) 222 | } 223 | 224 | return data 225 | 226 | 227 | class ModelSanityChecker(QtWidgets.QWidget): 228 | """ Main sanity checker class """ 229 | 230 | def __init__(self, settings=None, parent=None): 231 | super(ModelSanityChecker, self).__init__(parent) 232 | 233 | checkerObjs = [i() for i in checker.CHECKERS] 234 | checkerObjs.sort() 235 | self.checkerWidgets = [CheckerWidget(i, self, settings) for i in checkerObjs] 236 | self.separators = [] 237 | 238 | self.createUI() 239 | 240 | def createUI(self): 241 | """ 242 | GUI method 243 | 244 | """ 245 | 246 | mainLayout = QtWidgets.QVBoxLayout() 247 | mainLayout.setContentsMargins(0, 0, 0, 0) 248 | 249 | scroll = QtWidgets.QScrollArea() 250 | scroll.setWidgetResizable(1) 251 | 252 | scrollLayout = QtWidgets.QVBoxLayout() 253 | 254 | currentCategory = self.checkerWidgets[0].checker.category 255 | 256 | sep = Separator(currentCategory, self.checkerWidgets) 257 | self.separators.append(sep) 258 | scrollLayout.addWidget(sep) 259 | 260 | for widget in self.checkerWidgets: 261 | if currentCategory != widget.checker.category: 262 | cat = widget.checker.category 263 | currentCategory = cat 264 | sep = Separator(cat, self.checkerWidgets) 265 | self.separators.append(sep) 266 | scrollLayout.addWidget(sep) 267 | scrollLayout.addWidget(widget) 268 | 269 | content = QtWidgets.QWidget() 270 | content.setLayout(scrollLayout) 271 | 272 | scroll.setWidget(content) 273 | 274 | self.rootLE = QtWidgets.QLineEdit() 275 | setButton = QtWidgets.QPushButton("Set Selected") 276 | setButton.clicked.connect(self.setSelected) 277 | 278 | checkboxAll = QtWidgets.QCheckBox("Select All") 279 | checkboxAll.setChecked(True) 280 | checkboxAll.stateChanged.connect(self.selectAllToggle) 281 | 282 | checkAllButton = QtWidgets.QPushButton("Check All") 283 | checkAllButton.clicked.connect(self.checkAll) 284 | 285 | fixAllButton = QtWidgets.QPushButton("Fix All") 286 | fixAllButton.clicked.connect(self.fixAll) 287 | 288 | rootLayout = QtWidgets.QHBoxLayout() 289 | rootLayout.addWidget(self.rootLE) 290 | rootLayout.addWidget(setButton) 291 | 292 | mainLayout.addLayout(rootLayout) 293 | mainLayout.addWidget(checkboxAll) 294 | mainLayout.addWidget(scroll) 295 | mainLayout.addWidget(checkAllButton) 296 | mainLayout.addWidget(fixAllButton) 297 | 298 | self.setLayout(mainLayout) 299 | 300 | def selectAllToggle(self, *args): 301 | state = args[0] 302 | 303 | for s in self.separators: 304 | if state == 2: 305 | s.checkbox.setChecked(True) 306 | else: 307 | s.checkbox.setChecked(False) 308 | 309 | def setSelected(self): 310 | sel = cmds.ls(sl=True, fl=True, long=True) 311 | if sel: 312 | root = sel[0] 313 | self.rootLE.setText(root) 314 | 315 | def checkAll(self): 316 | """ 317 | Check all 318 | 319 | """ 320 | 321 | node = self.rootLE.text() 322 | 323 | progDialog = QtWidgets.QProgressDialog( 324 | "Now Checking...", 325 | "Cancel", 326 | 0, 327 | len(self.checkerWidgets), 328 | self) 329 | progDialog.setWindowTitle("Building library") 330 | # progDialog.setWindowModality(QtCore.Qt.WindowModal) 331 | progDialog.show() 332 | 333 | for num, widget in enumerate(self.checkerWidgets): 334 | checkerName = widget.checker.name 335 | if widget.checker.isEnabled: 336 | print("Running {} checker".format(checkerName)) 337 | if node == "": 338 | widget.check() 339 | else: 340 | widget.check(node) 341 | else: 342 | print("{} checker is disabled. Skipped".format(checkerName)) 343 | 344 | progDialog.setValue(num+1) 345 | progDialog.setLabel(QtWidgets.QLabel( 346 | r'Now checking "{}"'.format(widget.checker.name))) 347 | QtCore.QCoreApplication.processEvents() 348 | 349 | progDialog.close() 350 | 351 | def fixAll(self): 352 | """ 353 | Fix all 354 | 355 | """ 356 | 357 | for widget in self.checkerWidgets: 358 | widget.fix() 359 | 360 | 361 | class CentralWidget(QtWidgets.QWidget): 362 | """ Central widget """ 363 | 364 | def __init__(self, parent=None): 365 | """ Init """ 366 | 367 | super(CentralWidget, self).__init__(parent) 368 | 369 | self.createUI() 370 | self.layoutUI() 371 | 372 | def createUI(self): 373 | """ Crete widgets """ 374 | 375 | settings = Settings(self) 376 | checker = ModelSanityChecker(settings, self) 377 | 378 | self.tabWidget = QtWidgets.QTabWidget() 379 | self.tabWidget.addTab(checker, "SanityChecker") 380 | self.tabWidget.addTab(settings, "Settings") 381 | 382 | def layoutUI(self): 383 | """ Layout widgets """ 384 | 385 | mainLayout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.TopToBottom) 386 | mainLayout.setContentsMargins(5, 5, 5, 5) 387 | mainLayout.addWidget(self.tabWidget) 388 | 389 | self.setLayout(mainLayout) 390 | 391 | 392 | class MainWindow(MayaQWidgetDockableMixin, QtWidgets.QMainWindow): 393 | """ 394 | Main window 395 | 396 | """ 397 | 398 | def __init__(self, parent=None): 399 | """ init """ 400 | 401 | super(MainWindow, self).__init__(parent) 402 | 403 | self.thisObjectName = "sanityCheckerWindow" 404 | self.winTitle = "Sanity Checker" 405 | self.workspaceControlName = self.thisObjectName + "WorkspaceControl" 406 | 407 | self.setObjectName(self.thisObjectName) 408 | self.setWindowTitle(self.winTitle) 409 | 410 | self.setWindowFlags(QtCore.Qt.Window) 411 | self.setAttribute(QtCore.Qt.WA_DeleteOnClose) 412 | 413 | # Create and set central widget 414 | self.cWidget = CentralWidget() 415 | self.setCentralWidget(self.cWidget) 416 | 417 | self.setupMenu() 418 | 419 | def setupMenu(self): 420 | """ Setup menu """ 421 | 422 | menu = self.menuBar() 423 | 424 | # About 425 | if MAYA_VERSION != 2025: 426 | aboutAction = QtWidgets.QAction("&About", self) 427 | else: 428 | aboutAction = QtGui.QAction("&About", self) 429 | 430 | aboutAction.setStatusTip('About this script') 431 | aboutAction.triggered.connect(self.showAbout) 432 | 433 | menu.addAction("File") 434 | helpMenu = menu.addMenu("&Help") 435 | helpMenu.addAction(aboutAction) 436 | 437 | def showAbout(self): 438 | """ 439 | About message 440 | """ 441 | 442 | QtWidgets.QMessageBox.about(self, 'About ', 'ModelSanityChecker\n') 443 | 444 | def run(self): 445 | try: 446 | cmds.deleteUI(self.workspaceControlName) 447 | except RuntimeError: 448 | pass 449 | 450 | self.show(dockable=True) 451 | cmds.workspaceControl( 452 | self.workspaceControlName, 453 | edit=True, 454 | initialWidth=270, 455 | minimumWidth=270, 456 | dockToControl=['Outliner', 'right']) 457 | self.raise_() 458 | 459 | 460 | def main(): 461 | 462 | window = MainWindow() 463 | window.run() 464 | 465 | 466 | if __name__ == "__main__": 467 | main() 468 | --------------------------------------------------------------------------------