From 8d6cfcb727ba7f2c62c9182cc09da6957a8205f0 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 6 Aug 2026 16:00:52 -0700 Subject: [PATCH 01/47] Revert "Use the numpytoimath library, and Get rid of python2 compat stuff" This reverts commit dfa1468ec845ad9b87deb72611059fbdfd6b6926. --- .../simplexui/commands/alembicCommon.py | 91 +++++++++++++++---- .../menu/genericPlugins/unsubdivide.py | 9 +- 2 files changed, 76 insertions(+), 24 deletions(-) diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 30302e54..7bb9c566 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -36,11 +36,14 @@ try: import numpy as np - from .numpytoimath import imathToNumpy, numpyToImath except ImportError: np = None - numpyToImath = None - imathToNumpy = None + arrayToNumpy = None +else: + try: + from imathnumpy import arrayToNumpy # pylint:disable=no-name-in-module + except ImportError: + arrayToNumpy = None def pbPrint(pBar, message=None, val=None, maxVal=None, _pbPrintLastComma=None): @@ -107,12 +110,22 @@ def mkArray(aType, iList): if isinstance(iList, aType): return iList - if np is None or numpyToImath is None: + if np is None: array = aType(len(iList)) for i in range(len(iList)): array[i] = tuple(iList[i]) return array - return numpyToImath(iList, aType) + elif arrayToNumpy is None: + array = aType(len(iList)) + for i in range(len(iList)): + array[i] = tuple(iList[i].tolist()) + return array + else: + iList = np.array(iList) + array = aType(len(iList)) + memView = arrayToNumpy(array) + np.copyto(memView, iList) + return array def mk1dArray(aType, iList): @@ -132,14 +145,19 @@ def mk1dArray(aType, iList): """ if isinstance(iList, aType): return iList - if np is None or numpyToImath is None: + if np is None or arrayToNumpy is None or aType is UnsignedIntArray: array = aType(len(iList)) for i in range(len(iList)): # Gotta cast to int because an "int" from numpy has # the type np.int32, which makes this conversion angry array[i] = int(iList[i]) return array - return numpyToImath(iList, aType) + else: + iList = np.array(iList) + array = aType(len(iList)) + memView = arrayToNumpy(array) + np.copyto(memView, iList) + return array def mkSampleVertexPoints(pts): @@ -288,11 +306,18 @@ def getSampleArray(imesh, pBar=None): meshSchema = imesh.getSchema() posProp = meshSchema.getPositionsProperty() numShapes = len(posProp.samples) - if imathToNumpy is not None and np is not None: + if arrayToNumpy is not None: shapes = np.empty((len(posProp.samples), len(posProp.samples[0]), 3)) for i, s in enumerate(posProp.samples): pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) - shapes[i] = imathToNumpy(s) + shapes[i] = arrayToNumpy(s) + elif np is not None: + shapes = [] + for i, s in enumerate(posProp.samples): + pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) + shapes.append((list(s.x), list(s.y), list(s.z))) + shapes = np.array(shapes) + shapes = shapes.transpose((0, 2, 1)) else: shapes = [] for i, s in enumerate(posProp.samples): @@ -339,11 +364,13 @@ def getStaticMeshArrays(imesh): The number of vertices per face as np.array if possible """ faces, counts = getStaticMeshData(imesh) - if np is None or imathToNumpy is None: - faces, counts = list(faces), list(counts) + if arrayToNumpy is not None: + faces = arrayToNumpy(faces).copy() + counts = arrayToNumpy(counts).copy() + elif np is not None: + faces, counts = np.array(faces), np.array(counts) else: - faces = imathToNumpy(faces) - counts = imathToNumpy(counts) + faces, counts = list(faces), list(counts) return faces, counts @@ -428,17 +455,20 @@ def getFlatUvFaces(imesh): if iuvs.isIndexed(): indexed = True idxs = iuvs.getIndexProperty().getValue() - if imathToNumpy is None or np is None: - idxs = list(idxs) + # if arrayToNumpy is not None: + # idxs = arrayToNumpy(idxs).copy() + # elif np is not None: + if np is not None: + idxs = np.array(idxs) else: - idxs = imathToNumpy(idxs) + idxs = list(idxs) else: indexed = False rawCount = sum(list(sch.getFaceCountsProperty().samples[0])) - if np is None: - idxs = list(range(rawCount)) - else: + if np is not None: idxs = np.arange(rawCount) + else: + idxs = list(range(rawCount)) return idxs, indexed @@ -678,6 +708,29 @@ def flattenFaces(faces): return faceCounts, faceIdxs +def unflattenFaces(faces, counts): + """Take a flat face/count representation of faces + and turn it into a nested list representation + + Parameters + ---------- + faces : np.array + The flat list of face connectivity + counts : np.array + The flat list of vertices per face + + Returns + ------- + : [[int, ...], ...] + The nested list representation + """ + out, ptr = [], 0 + for c in counts: + out.append(faces[ptr : ptr + c].tolist()) + ptr += c + return out + + def buildAbc( outPath, points, diff --git a/src/python/simplexui/menu/genericPlugins/unsubdivide.py b/src/python/simplexui/menu/genericPlugins/unsubdivide.py index 5cb77168..1ea0059e 100644 --- a/src/python/simplexui/menu/genericPlugins/unsubdivide.py +++ b/src/python/simplexui/menu/genericPlugins/unsubdivide.py @@ -22,22 +22,21 @@ from Qt import QtCompat from Qt.QtWidgets import QAction, QMessageBox, QProgressDialog - try: - from ..commands.numpytoimath import numpyToImath + import imathnumpy except ImportError: - numpyToImath = None + imathnumpy = None def registerTool(window, menu): - if numpyToImath is not None: + if imathnumpy is not None: exportUnsubACT = QAction("Un Subdivide Smpx ...", window) menu.addAction(exportUnsubACT) exportUnsubACT.triggered.connect(partial(exportUnsubInterface, window)) def exportUnsubInterface(window): - if numpyToImath is None: + if imathnumpy is None: QMessageBox.warning( window, "No ImathToNumpy", From 3878b788e4f24bf80d4d27277ea07b28736b0c8c Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 6 Aug 2026 16:04:07 -0700 Subject: [PATCH 02/47] Add maya 27 to the workflow --- .github/workflows/main.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a445f873..9ecd044b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: compile_plugin: strategy: matrix: - maya: [2022, 2023, 2024, 2025, 2026] + maya: [2022, 2023, 2024, 2025, 2026, 2027] os: [macos-13, macos-latest, ubuntu-latest, windows-latest] include: # Add the maya update versions here @@ -26,6 +26,8 @@ jobs: update: 3 - maya: 2026 update: 2 + - maya: 2027 + update: 2 # cross-compiling is annoying so just fall back to macos-13 exclude: @@ -39,6 +41,8 @@ jobs: maya: 2025 - os: macos-13 maya: 2026 + - os: macos-13 + maya: 2027 fail-fast: false From 8125994d3c6c7dac4f0f1bf6928634f276c5fd7b Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 11 Aug 2026 16:18:00 -0700 Subject: [PATCH 03/47] Fix some Qt6 compatibility --- quick_compile.bat | 6 +++--- src/python/simplexui/falloffDialog.py | 14 ++++---------- src/python/simplexui/interfaceModel.py | 2 +- src/python/simplexui/simplexDialog.py | 8 ++++---- src/python/simplexui/traversalDialog.py | 4 ++-- 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/quick_compile.bat b/quick_compile.bat index 9bade032..0ef31eb9 100644 --- a/quick_compile.bat +++ b/quick_compile.bat @@ -1,6 +1,6 @@ setlocal -SET MAYA_VERSION=2024 +SET MAYA_VERSION=2027 REM "vs" "ninja" REM use VS for the debugger, otherwise use NINJA REM Until I figure out how to debug using nvim @@ -13,8 +13,8 @@ if not exist %BUILDDIR%\ ( meson setup %BUILDDIR% ^ -Dmaya:maya_version=%MAYA_VERSION% ^ -Dmaya_build=true ^ - -Dpython_build=false ^ - -Dpython_script_build=false ^ + -Dpython_build=true ^ + -Dpython_script_build=true ^ --buildtype %BUILDTYPE% --vsenv --backend %BACKEND% ) diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index 5a5a53da..37d3436a 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -150,7 +150,7 @@ def _drawCleanLine(self, painter, p1, p2): def _paintBG(self, painter): painter.save() - painter.setBrush(self.palette().color(QPalette.ColorRole.Background)) + painter.setBrush(self.palette().color(QPalette.ColorRole.Window)) painter.drawRect(0, 0, self.width(), self.height()) painter.restore() @@ -176,13 +176,13 @@ def _paintPath(self, painter, p0, p1, p2, p3): path.moveTo(p0) path.cubicTo(p1, p2, p3) # painter.strokePath(path, QPen(QBrush(self.lineColor), 2)) - foregroundColor = self.palette().color(QPalette.ColorRole.Foreground) + foregroundColor = self.palette().color(QPalette.ColorRole.WindowText) painter.strokePath(path, QPen(QBrush(foregroundColor), 2)) painter.restore() def _paintTangents(self, painter, p0, p1, p2, p3): # draw the tangent lines - foregroundColor = self.palette().color(QPalette.ColorRole.Foreground) + foregroundColor = self.palette().color(QPalette.ColorRole.WindowText) pen = QPen(foregroundColor) pen.setWidth(1) pen.setStyle(Qt.PenStyle.DashLine) @@ -349,20 +349,15 @@ def loadSimplex(self): else: self.uiFalloffSettingsGRP.setEnabled(True) - print("Setting System") self.simplex = system # Populate Settings widgets - print("Populating") self.foModel = FalloffDataModel(self.simplex, self) self.simplex.falloffModels.append(self.foModel) self.uiShapeFalloffCBOX.setModel(self.foModel) self._falloffMapper.setModel(self.foModel) - print("Adding Mappings") - currentIndex = "currentIndex" - if QtLib.IsPySide2 or QtLib.IsPyQt5: - currentIndex = QByteArray(bytes("Test", encoding="utf-8")) + currentIndex = QByteArray("currentIndex".encode()) self._falloffMapper.addMapping(self.uiFalloffTypeCBOX, 1, currentIndex) self._falloffMapper.addMapping(self.uiFalloffAxisCBOX, 2, currentIndex) @@ -371,7 +366,6 @@ def loadSimplex(self): self._falloffMapper.addMapping(self.uiFalloffMaxHandleSPN, 5) self._falloffMapper.addMapping(self.uiFalloffMaxSPN, 6) - print("Setting Index 0") self.uiShapeFalloffCBOX.setCurrentIndex(0) self._falloffMapper.setCurrentIndex(0) diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index e89370e9..a2cfab02 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -531,7 +531,7 @@ def invalidateFilter(self): source.invalidateFilter() super(BaseProxyModel, self).invalidateFilter() - def filterAcceptsRow(self, sourceRow, sourceParent): + def filterAcceptsRow(self, sourceRow, sourceParent) -> bool: return True diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 0ea7c44e..badb9627 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -104,8 +104,8 @@ def __init__(self, parent=None, dispatch=None): # Custom widgets aren't working properly, so I bring them in manually self.uiSliderTREE = SliderTree(self.uiMainShapesGRP) self.uiSliderTREE.setDragEnabled(False) - self.uiSliderTREE.setDragDropMode(SliderTree.NoDragDrop) - self.uiSliderTREE.setSelectionMode(SliderTree.ExtendedSelection) + self.uiSliderTREE.setDragDropMode(SliderTree.DragDropMode.NoDragDrop) + self.uiSliderTREE.setSelectionMode(SliderTree.SelectionMode.ExtendedSelection) self.uiSliderTREE.dragFilter.dragPressed.connect(self.dragStart) self.uiSliderTREE.dragFilter.dragReleased.connect(self.dragStop) @@ -113,8 +113,8 @@ def __init__(self, parent=None, dispatch=None): self.uiComboTREE = ComboTree(self.uiComboShapesGRP) self.uiComboTREE.setDragEnabled(False) - self.uiComboTREE.setDragDropMode(ComboTree.NoDragDrop) - self.uiComboTREE.setSelectionMode(ComboTree.ExtendedSelection) + self.uiComboTREE.setDragDropMode(ComboTree.DragDropMode.NoDragDrop) + self.uiComboTREE.setSelectionMode(ComboTree.SelectionMode.ExtendedSelection) self.uiComboTREE.dragFilter.dragPressed.connect(self.dragStart) self.uiComboTREE.dragFilter.dragReleased.connect(self.dragStop) self.uiComboLAY.addWidget(self.uiComboTREE) diff --git a/src/python/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py index 369038bd..e2406004 100644 --- a/src/python/simplexui/traversalDialog.py +++ b/src/python/simplexui/traversalDialog.py @@ -67,8 +67,8 @@ def __init__(self, parent): # Load the custom tree manually self.uiTraversalTREE = TraversalTree(self) self.uiTraversalTREE.setDragEnabled(False) - self.uiTraversalTREE.setDragDropMode(TraversalTree.NoDragDrop) - self.uiTraversalTREE.setSelectionMode(TraversalTree.ExtendedSelection) + self.uiTraversalTREE.setDragDropMode(TraversalTree.DragDropMode.NoDragDrop) + self.uiTraversalTREE.setSelectionMode(TraversalTree.SelectionMode.ExtendedSelection) self.uiTraversalTREE.dragFilter.dragPressed.connect(self.dragStart) self.uiTraversalTREE.dragFilter.dragReleased.connect(self.dragStop) From 5714b49d3026f46d390eaed763050c8bbdc19f25 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 11 Aug 2026 16:40:23 -0700 Subject: [PATCH 04/47] Add dialog types loaded from the ui, and some simple type checking errors --- src/python/simplexui/comboCheckDialog.py | 29 +++++++- src/python/simplexui/falloffDialog.py | 29 +++++++- src/python/simplexui/simplexDialog.py | 95 +++++++++++++++++++++++- src/python/simplexui/travCheckDialog.py | 31 +++++++- src/python/simplexui/traversalDialog.py | 34 ++++++++- 5 files changed, 207 insertions(+), 11 deletions(-) diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index ce330848..d1e64747 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -22,7 +22,20 @@ from Qt import QtCompat from Qt.QtCore import Qt from Qt.QtGui import QBrush, QColor -from Qt.QtWidgets import QDialog, QListWidgetItem, QTreeWidgetItem +from Qt.QtWidgets import ( + QCheckBox, + QDialog, + QGroupBox, + QLabel, + QListWidget, + QListWidgetItem, + QPushButton, + QSpinBox, + QTreeWidget, + QTreeWidgetItem, + QWidget, +) + from .utils import getUiFile @@ -145,7 +158,21 @@ class ComboCheckDialog(QDialog): """ + uiHeaderWID: QWidget + uiLimitGRP: QGroupBox + uiMinLimitSPIN: QSpinBox + uiMaxLimitSPIN: QSpinBox + uiAutoUpdateCHK: QCheckBox + uiManualUpdateBTN: QPushButton + uiEditTREE: QTreeWidget + uiComboCheckLIST: QListWidget + uiWarningLBL: QLabel + uiCancelBTN: QPushButton + uiCreateSelectedBTN: QPushButton + def __init__(self, sliders, values=None, mode="create", parent=None): + if parent is None: + raise ValueError("Parent must not be None") super(ComboCheckDialog, self).__init__(parent) uiPath = getUiFile(__file__) diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index 37d3436a..699faa2b 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -43,11 +43,17 @@ QStandardItemModel, ) from Qt.QtWidgets import ( + QComboBox, QDataWidgetMapper, QDialog, + QDoubleSpinBox, + QGroupBox, QInputDialog, + QLabel, QMessageBox, + QPushButton, QSizePolicy, + QVBoxLayout, QWidget, ) from .utils import getNextName, getUiFile, Prefs @@ -281,6 +287,21 @@ def mouseMoveEvent(self, e): class FalloffDialog(QDialog): """The ui for interacting with Falloffs""" + uiFalloffSettingsGRP: QGroupBox + uiShapeFalloffLBL: QLabel + uiShapeFalloffCBOX: QComboBox + uiShapeFalloffRenameBTN: QPushButton + uiShapeFalloffNewBTN: QPushButton + uiShapeFalloffDuplicateBTN: QPushButton + uiShapeFalloffDeleteBTN: QPushButton + uiFalloffLAY: QVBoxLayout + uiFalloffTypeCBOX: QComboBox + uiFalloffAxisCBOX: QComboBox + uiFalloffMaxSPN: QDoubleSpinBox + uiFalloffMaxHandleSPN: QDoubleSpinBox + uiFalloffMinHandleSPN: QDoubleSpinBox + uiFalloffMinSPN: QDoubleSpinBox + def __init__(self, parent): super(FalloffDialog, self).__init__(parent) uiPath = getUiFile(__file__) @@ -372,6 +393,8 @@ def loadSimplex(self): # Falloff Settings def newFalloff(self): """Create a new Falloff object""" + if self.simplex is None: + return foNames = [f.name for f in self.simplex.falloffs] tempName = getNextName("NewFalloff", foNames) @@ -391,6 +414,8 @@ def newFalloff(self): def duplicateFalloff(self): """Duplicate the selected falloff""" + if self.simplex is None: + return if not self.simplex.falloffs: self.newFalloff() return @@ -407,7 +432,7 @@ def duplicateFalloff(self): def deleteFalloff(self): """Delete the selected falloff""" - if not self.simplex.falloffs: + if not self.simplex or not self.simplex.falloffs: return idx = self.uiShapeFalloffCBOX.currentIndex() if idx < 0: @@ -418,7 +443,7 @@ def deleteFalloff(self): def renameFalloff(self): """Rename the selected falloff""" - if not self.simplex.falloffs: + if not self.simplex or not self.simplex.falloffs: return idx = self.uiShapeFalloffCBOX.currentIndex() if idx < 0: diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index badb9627..1773a745 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -47,8 +47,25 @@ # Depending on what's available from Qt import QtCompat from Qt.QtCore import Qt, Signal -from Qt.QtGui import QStandardItemModel -from Qt.QtWidgets import QApplication, QInputDialog, QMessageBox, QProgressDialog +from Qt.QtGui import QStandardItemModel, QAction +from Qt.QtWidgets import ( + QApplication, + QInputDialog, + QMessageBox, + QProgressDialog, + QCheckBox, + QComboBox, + QFrame, + QGroupBox, + QLabel, + QLineEdit, + QPushButton, + QRadioButton, + QVBoxLayout, + QWidget, +) + + from .traversalDialog import TraversalDialog from .utils import getNextName, getUiFile, makeUnique, naturalSortKey, Prefs @@ -91,6 +108,73 @@ class SimplexDialog(Window): Qt Signals that the Simplex Ui can understand """ + uiConvertCorrectiveACT: QAction + uiDoubleSliderRangeACT: QAction + uiExportACT: QAction + uiExportObjPSDACT: QAction + uiExtractOnCreateACT: QAction + uiExtractPosedACT: QAction + uiHideRedundantACT: QAction + uiImportACT: QAction + uiImportObjPSDACT: QAction + uiLegacyJsonACT: QAction + uiLiveShapeConnectionACT: QAction + uiLiveUpdateACT: QAction + uiSetWorkingDirectoryACT: QAction + uiSplitShapePSDACT: QAction + + uiComboLAY: QVBoxLayout + uiSliderLAY: QVBoxLayout + uiAutoSetCombosCHK: QCheckBox + uiAutoSetSlidersCHK: QCheckBox + uiComboDependLockCHK: QCheckBox + uiSliderDependLockCHK: QCheckBox + uiCurrentSystemCBOX: QComboBox + uiComboButtonFRM: QFrame + uiSliderButtonFRM: QFrame + uiComboDependGRP: QGroupBox + uiComboShapesGRP: QGroupBox + uiMainShapesGRP: QGroupBox + uiObjectGRP: QGroupBox + uiSliderDependGRP: QGroupBox + uiSystemGRP: QGroupBox + uiCurrentObjectLBL: QLabel + uiCurrentSystemLBL: QLabel + uiComboFilterLINE: QLineEdit + uiCurrentObjectTXT: QLineEdit + uiSliderFilterLINE: QLineEdit + uiClearSelectedObjectBTN: QPushButton + uiComboExitIsolateBTN: QPushButton + uiComboFilterClearBTN: QPushButton + uiDeleteComboBTN: QPushButton + uiDeleteSystemBTN: QPushButton + uiGetSelectedObjectBTN: QPushButton + uiNewComboActiveBTN: QPushButton + uiNewComboGroupBTN: QPushButton + uiNewComboSelectBTN: QPushButton + uiNewComboShapeBTN: QPushButton + uiNewGroupBTN: QPushButton + uiNewShapeBTN: QPushButton + uiNewSliderBTN: QPushButton + uiNewSystemBTN: QPushButton + uiRenameSystemBTN: QPushButton + uiSelectCtrlBTN: QPushButton + uiSelectSlidersBTN: QPushButton + uiSetSliderValsBTN: QPushButton + uiShapeConnectBTN: QPushButton + uiShapeConnectSceneBTN: QPushButton + uiShapeExtractBTN: QPushButton + uiSliderDeleteBTN: QPushButton + uiSliderExitIsolateBTN: QPushButton + uiSliderFilterClearBTN: QPushButton + uiZeroAllBTN: QPushButton + uiZeroSelectedBTN: QPushButton + uiComboDependAllRDO: QRadioButton + uiComboDependAnyRDO: QRadioButton + uiComboDependOnlyRDO: QRadioButton + uiSliderDependAllRDO: QRadioButton + uiSliderDependAnyRDO: QRadioButton + uiConnectionGroupWID: QWidget simplexLoaded = Signal() openedDialogs = [] @@ -235,6 +319,9 @@ def newScene(self): def handleUndo(self): """Call this after an undo/redo action. Usually called from the stack""" + if self.simplex is None: + return + rev = self.simplex.DCC.getRevision() data = self.simplex.stack.getRevision(rev) if data is not None: @@ -898,7 +985,9 @@ def shapeIndexExport(self, indexes): pref = Prefs() defaultPath = str( - pref.restoreProperty("systemExportFolder", os.path.join(os.path.expanduser("~"))) + pref.restoreProperty( + "systemExportFolder", os.path.join(os.path.expanduser("~")) + ) ) outfld = QFileDialog.getExistingDirectory( self, "Pick Export Folder", defaultPath diff --git a/src/python/simplexui/travCheckDialog.py b/src/python/simplexui/travCheckDialog.py index 8168cb2a..fcc1147c 100644 --- a/src/python/simplexui/travCheckDialog.py +++ b/src/python/simplexui/travCheckDialog.py @@ -22,7 +22,18 @@ from Qt import QtCompat from Qt.QtCore import Qt from Qt.QtGui import QBrush, QColor -from Qt.QtWidgets import QDialog, QTreeWidgetItem +from Qt.QtWidgets import ( + QDialog, + QTreeWidgetItem, + QCheckBox, + QGroupBox, + QLabel, + QPushButton, + QSpinBox, + QTreeWidget, +) + + from .utils import getUiFile @@ -173,6 +184,17 @@ class TraversalCheckDialog(QDialog): ------- """ + uiLimitGRP: QGroupBox + uiMinLimitSPIN: QSpinBox + uiMaxLimitSPIN: QSpinBox + uiAutoUpdateCHK: QCheckBox + uiManualUpdateBTN: QPushButton + uiEditTREE: QTreeWidget + uiTravCheckTREE: QTreeWidget + uiWarningLBL: QLabel + uiCancelBTN: QPushButton + uiCreateSelectedBTN: QPushButton + def __init__( self, sliders, @@ -182,14 +204,17 @@ def __init__( parent=None, grandparent=None, ): + # Store the Parent UI rather than relying on Qt's .parent() + # Could cause crashes otherwise + if parent is None or grandparent is None: + raise ValueError("The parent and grandparent must be provided") + super(TraversalCheckDialog, self).__init__(parent) uiPath = getUiFile(__file__) QtCompat.loadUi(uiPath, self) self.mode = mode.lower() - # Store the Parent UI rather than relying on Qt's .parent() - # Could cause crashes otherwise self.parUI = parent self.gparUI = grandparent self.maxPoss = 100 diff --git a/src/python/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py index e2406004..93fd0fc0 100644 --- a/src/python/simplexui/traversalDialog.py +++ b/src/python/simplexui/traversalDialog.py @@ -34,13 +34,16 @@ # This module imports QT from PyQt4, PySide or PySide2 # Depending on what's available from Qt import QtCompat -from Qt.QtGui import QStandardItemModel +from Qt.QtGui import QStandardItemModel, QAction from Qt.QtWidgets import ( QApplication, QDialog, + QFrame, QInputDialog, QMessageBox, QProgressDialog, + QPushButton, + QVBoxLayout, ) from .travCheckDialog import TraversalCheckDialog from .utils import getUiFile, makeUnique @@ -57,6 +60,31 @@ class TraversalDialog(QDialog): The parent simplex dialog """ + uiTraversalLAY: QVBoxLayout + uiTravButtonFRM: QFrame + uiTravNewBTN: QPushButton + uiTravAddSliderBTN: QPushButton + uiTravNewGroupBTN: QPushButton + uiTravNewShapeBTN: QPushButton + uiTravDeleteBTN: QPushButton + uiShapeExtractBTN: QPushButton + uiShapeConnectBTN: QPushButton + + uiConvertCorrectiveACT: QAction + uiDoubleSliderRangeACT: QAction + uiExportACT: QAction + uiExportObjPSDACT: QAction + uiExtractOnCreateACT: QAction + uiExtractPosedACT: QAction + uiHideRedundantACT: QAction + uiImportACT: QAction + uiImportObjPSDACT: QAction + uiLegacyJsonACT: QAction + uiLiveShapeConnectionACT: QAction + uiLiveUpdateACT: QAction + uiSetWorkingDirectoryACT: QAction + uiSplitShapePSDACT: QAction + def __init__(self, parent): super(TraversalDialog, self).__init__(parent) @@ -68,7 +96,9 @@ def __init__(self, parent): self.uiTraversalTREE = TraversalTree(self) self.uiTraversalTREE.setDragEnabled(False) self.uiTraversalTREE.setDragDropMode(TraversalTree.DragDropMode.NoDragDrop) - self.uiTraversalTREE.setSelectionMode(TraversalTree.SelectionMode.ExtendedSelection) + self.uiTraversalTREE.setSelectionMode( + TraversalTree.SelectionMode.ExtendedSelection + ) self.uiTraversalTREE.dragFilter.dragPressed.connect(self.dragStart) self.uiTraversalTREE.dragFilter.dragReleased.connect(self.dragStop) From 2e05af2bd4327afbac58fcc5ade3ee695aac8388 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 11 Aug 2026 23:17:51 -0700 Subject: [PATCH 05/47] First pass typing the alembic stuff. I'm just gonna require numpy next --- .../simplexui/commands/alembicCommon.py | 230 ++++++++++++------ src/python/simplexui/commands/mayatonumpy.py | 1 + src/python/simplexui/items/accessor.py | 2 + typings/alembic/Abc.pyi | 53 ++++ typings/alembic/AbcCoreAbstract.pyi | 1 + typings/alembic/AbcGeom.pyi | 58 +++++ typings/alembic/__init__.pyi | 0 typings/imath.pyi | 27 ++ 8 files changed, 303 insertions(+), 69 deletions(-) create mode 100644 typings/alembic/Abc.pyi create mode 100644 typings/alembic/AbcCoreAbstract.pyi create mode 100644 typings/alembic/AbcGeom.pyi create mode 100644 typings/alembic/__init__.pyi create mode 100644 typings/imath.pyi diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 7bb9c566..eb8dece6 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -21,18 +21,26 @@ import os -from alembic.Abc import IArchive, OArchive, OStringProperty +from alembic.Abc import ( + IArchive, + OArchive, + OStringProperty, + IObject, + OCompoundProperty, + ICompoundProperty, +) from alembic.AbcGeom import ( GeometryScope, IPolyMesh, IXform, ON3fGeomParamSample, OPolyMesh, + OPolyMeshSchema, OPolyMeshSchemaSample, OV2fGeomParamSample, OXform, ) -from imath import IntArray, UnsignedIntArray, V2f, V2fArray, V3fArray +from imath import IntArray, UnsignedIntArray, V2f, V2fArray, V3fArray, Box3d try: import numpy as np @@ -46,7 +54,35 @@ arrayToNumpy = None -def pbPrint(pBar, message=None, val=None, maxVal=None, _pbPrintLastComma=None): +from typing import ( + TYPE_CHECKING, + Optional, + Union, + Sequence, + Type, + TypeVar, + overload, + Any, +) + + +if TYPE_CHECKING: + from Qt.QtWidgets import QProgressDialog + import numpy as np + import numpy.typing as npt + from alembic.AbcGeom import _IBase # A helper typing-only class + + +T = TypeVar("T", bound=Union[IntArray, UnsignedIntArray, V2fArray, V3fArray]) + + +def pbPrint( + pBar: Optional[QProgressDialog], + message: Optional[str] = None, + val: Optional[int] = None, + maxVal: Optional[int] = None, + _pbPrintLastComma=None, +): """A function that handles displaying messages in a QProgressDialog or printing to stdout Don't forget to call QApplication.processEvents() after using this function @@ -92,7 +128,7 @@ def pbPrint(pBar, message=None, val=None, maxVal=None, _pbPrintLastComma=None): print(message) -def mkArray(aType, iList): +def mkArray(aType: Type[T], iList: Sequence) -> T: """Makes the alembic-usable c++ typed 2-d arrays Parameters @@ -110,25 +146,25 @@ def mkArray(aType, iList): if isinstance(iList, aType): return iList + size = len(iList) + array = aType(size) + if np is None: - array = aType(len(iList)) - for i in range(len(iList)): + for i in range(size): array[i] = tuple(iList[i]) return array elif arrayToNumpy is None: - array = aType(len(iList)) - for i in range(len(iList)): + for i in range(size): array[i] = tuple(iList[i].tolist()) return array else: - iList = np.array(iList) - array = aType(len(iList)) + nplist = np.array(iList) memView = arrayToNumpy(array) - np.copyto(memView, iList) + np.copyto(memView, nplist) return array -def mk1dArray(aType, iList): +def mk1dArray(aType: Type[T], iList: Sequence) -> T: """Makes the alembic-usable c++ typed 1-d arrays Parameters @@ -145,22 +181,22 @@ def mk1dArray(aType, iList): """ if isinstance(iList, aType): return iList + + array = aType(len(iList)) if np is None or arrayToNumpy is None or aType is UnsignedIntArray: - array = aType(len(iList)) for i in range(len(iList)): # Gotta cast to int because an "int" from numpy has # the type np.int32, which makes this conversion angry array[i] = int(iList[i]) return array else: - iList = np.array(iList) - array = aType(len(iList)) + nplist = np.array(iList) memView = arrayToNumpy(array) - np.copyto(memView, iList) + np.copyto(memView, nplist) return array -def mkSampleVertexPoints(pts): +def mkSampleVertexPoints(pts: Sequence) -> V3fArray: """Make an imath array of vertices Parameters @@ -176,7 +212,7 @@ def mkSampleVertexPoints(pts): return mkArray(V3fArray, pts) -def mkSampleIntArray(vals): +def mkSampleIntArray(vals: Sequence) -> IntArray: """Make an imath array of integers Parameters @@ -192,7 +228,7 @@ def mkSampleIntArray(vals): return mk1dArray(IntArray, vals) -def mkSampleUIntArray(vals): +def mkSampleUIntArray(vals: Sequence) -> UnsignedIntArray: """Make an imath array of unsigned integers Parameters @@ -208,7 +244,7 @@ def mkSampleUIntArray(vals): return mk1dArray(UnsignedIntArray, vals) -def mkSampleUvArray(uvs): +def mkSampleUvArray(uvs: Sequence) -> V2fArray: """Make an imath array of uvs Parameters @@ -229,7 +265,9 @@ def mkSampleUvArray(uvs): return array -def mkUvSample(uvs, indexes=None): +def mkUvSample( + uvs: Sequence, indexes: Optional[Sequence] = None +) -> OV2fGeomParamSample: """Take an array, and make a poly mesh sample of the uvs Parameters @@ -251,7 +289,9 @@ def mkUvSample(uvs, indexes=None): return OV2fGeomParamSample(ary, idxs, GeometryScope.kFacevaryingScope) -def mkNormalSample(norms, indexes=None): +def mkNormalSample( + norms: Sequence, indexes: Optional[Sequence] = None +) -> ON3fGeomParamSample: """Take an array, and make a poly mesh sample of the normals Parameters @@ -274,7 +314,13 @@ def mkNormalSample(norms, indexes=None): def setAlembicSample( - omeshSch, points, faceCount, faceIndex, bounds=None, uvs=None, normals=None + omeshSch: OPolyMeshSchema, + points: V3fArray, + faceCount: IntArray, + faceIndex: IntArray, + bounds: Optional[Box3d] = None, + uvs: Optional[OV2fGeomParamSample] = None, + normals: Optional[ON3fGeomParamSample] = None, ): """Set an alembic sample to the output mesh with the given properties""" # Do it this way because the defaults for these arguments are some value other than None @@ -290,7 +336,9 @@ def setAlembicSample( omeshSch.set(s) -def getSampleArray(imesh, pBar=None): +def getSampleArray( + imesh: IPolyMesh, pBar: Optional[QProgressDialog] = None +) -> Union[Sequence, npt.NDArray]: """Get the per-frame vertex positions for a mesh Parameters @@ -306,11 +354,12 @@ def getSampleArray(imesh, pBar=None): meshSchema = imesh.getSchema() posProp = meshSchema.getPositionsProperty() numShapes = len(posProp.samples) - if arrayToNumpy is not None: + if arrayToNumpy is not None and np is not None: shapes = np.empty((len(posProp.samples), len(posProp.samples[0]), 3)) for i, s in enumerate(posProp.samples): pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) shapes[i] = arrayToNumpy(s) + return shapes elif np is not None: shapes = [] for i, s in enumerate(posProp.samples): @@ -318,16 +367,16 @@ def getSampleArray(imesh, pBar=None): shapes.append((list(s.x), list(s.y), list(s.z))) shapes = np.array(shapes) shapes = shapes.transpose((0, 2, 1)) + return shapes else: shapes = [] for i, s in enumerate(posProp.samples): pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) shapes.append(s) - pbPrint(pBar, message="Done Reading") - return shapes + return shapes -def getStaticMeshData(imesh): +def getStaticMeshData(imesh: IPolyMesh) -> tuple[IntArray, IntArray]: """Get all the generally non-changing data for a mesh Parameters @@ -348,7 +397,9 @@ def getStaticMeshData(imesh): return faces, counts -def getStaticMeshArrays(imesh): +def getStaticMeshArrays( + imesh: IPolyMesh, +) -> tuple[Union[npt.NDArray, Sequence], Union[npt.NDArray, Sequence]]: """Get all the generally non-changing data for a mesh as numpy arrays Parameters @@ -364,7 +415,7 @@ def getStaticMeshArrays(imesh): The number of vertices per face as np.array if possible """ faces, counts = getStaticMeshData(imesh) - if arrayToNumpy is not None: + if arrayToNumpy is not None and np is not None: faces = arrayToNumpy(faces).copy() counts = arrayToNumpy(counts).copy() elif np is not None: @@ -374,7 +425,7 @@ def getStaticMeshArrays(imesh): return faces, counts -def getUvSample(imesh): +def getUvSample(imesh: IPolyMesh) -> Optional[OV2fGeomParamSample]: """Get the UV's for a mesh Parameters @@ -402,7 +453,9 @@ def getUvSample(imesh): return uv -def getUvArray(imesh): +def getUvArray( + imesh: IPolyMesh, +) -> Optional[Union[npt.NDArray, list[tuple[float, float]]]]: """Get the uv positions for a mesh Parameters @@ -430,7 +483,9 @@ def getUvArray(imesh): return uv -def getFlatUvFaces(imesh): +def getFlatUvFaces( + imesh: IPolyMesh, +) -> tuple[Union[list[int], npt.NDArray], Optional[bool]]: """Get the UV structure for a mesh if it's indexed. If un-indexed, return None This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed and can handle the data appropriately for export without keeping track of index-ness @@ -455,7 +510,7 @@ def getFlatUvFaces(imesh): if iuvs.isIndexed(): indexed = True idxs = iuvs.getIndexProperty().getValue() - # if arrayToNumpy is not None: + # if arrayToNumpy is not None and np is not None: # idxs = arrayToNumpy(idxs).copy() # elif np is not None: if np is not None: @@ -473,7 +528,7 @@ def getFlatUvFaces(imesh): return idxs, indexed -def getUvFaces(imesh): +def getUvFaces(imesh: IPolyMesh) -> Optional[Union[list[list[int]], npt.NDArray]]: """Get the UV structure for a mesh if it's indexed. If un-indexed, return None This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed and can handle the data appropriately for export without keeping track of index-ness @@ -503,7 +558,7 @@ def getUvFaces(imesh): return uvFaces -def getMeshFaces(imesh): +def getMeshFaces(imesh: IPolyMesh) -> list[list[int]]: """Get The vertex indices used per face Parameters @@ -520,12 +575,13 @@ def getMeshFaces(imesh): faces = [] ptr = 0 for count in rawCounts: - faces.append(list(rawFaces[ptr : ptr + count])) + chunk = rawFaces[ptr : ptr + count] + faces.append(list(chunk)) ptr += count return faces -def getPointCount(imesh): +def getPointCount(imesh: IPolyMesh) -> int: """Get the number of vertices in a mesh Parameters @@ -543,7 +599,24 @@ def getPointCount(imesh): return len(posProp.samples[0]) -def findAlembicObject(obj, abcType=None, name=None): +B = TypeVar("B", bound=_IBase) + + +@overload +def findAlembicObject( + obj: IObject, abcType: None = None, name: Optional[str] = None +) -> Optional[IObject]: ... + + +@overload +def findAlembicObject( + obj: IObject, abcType: Type[B], name: Optional[str] = None +) -> Optional[B]: ... + + +def findAlembicObject( + obj: IObject, abcType: Optional[Type[B]] = None, name: Optional[str] = None +) -> Optional[Union[IObject, B]]: """ Finds a single object in an alembic archive by name and/or type If only type is specified, then the first object of that type @@ -563,7 +636,11 @@ def findAlembicObject(obj, abcType=None, name=None): return None -def findAllAlembicObjects(obj, abcType=None, out=None): +def findAllAlembicObjects( + obj: IObject, + abcType: Optional[Type[_IBase]] = None, + out: Optional[list[IObject]] = None, +) -> list[IObject]: """Finds all objects of a type in an alembic archive""" md = obj.getMetaData() out = [] if out is None else out @@ -576,7 +653,7 @@ def findAllAlembicObjects(obj, abcType=None, out=None): return out -def getTypedIObject(obj): +def getTypedIObject(obj: IObject) -> Optional[_IBase]: from alembic.AbcGeom import ( ICamera, ICurves, @@ -604,14 +681,16 @@ def getTypedIObject(obj): return None -def getMesh(infile): +def getMesh(infile: str) -> Optional[IPolyMesh]: """Get the first found mesh object from the alembic filepath""" - iarch = IArchive(infile) + iarch = IArchive(str(infile)) ipolymsh = findAlembicObject(iarch.getTop(), abcType=IPolyMesh) return ipolymsh -def writeStringProperty(props, key, value, ogawa=True): +def writeStringProperty( + props: OCompoundProperty, key: str, value: str, ogawa: bool = True +) -> None: """Write the definition string to an alembic OObject HDF5 (which we must still support) has a character limit @@ -640,7 +719,7 @@ def writeStringProperty(props, key, value, ogawa=True): prop.setValue(str(value)) -def readStringProperty(props, key): +def readStringProperty(props: ICompoundProperty, key: str) -> str: """Read the definition string from an alembic OObject HDF5 (which we must still support) has a character limit @@ -682,7 +761,9 @@ def readStringProperty(props, key): return jsString -def flattenFaces(faces): +def flattenFaces( + faces: list[list[int]], +) -> tuple[Union[list[int], npt.NDArray], Union[list[int], npt.NDArray]]: """Take a nested list representation of faces and turn it into a flat face/count representation @@ -708,7 +789,7 @@ def flattenFaces(faces): return faceCounts, faceIdxs -def unflattenFaces(faces, counts): +def unflattenFaces(faces: npt.NDArray, counts: npt.NDArray) -> list[list[int]]: """Take a flat face/count representation of faces and turn it into a nested list representation @@ -732,20 +813,20 @@ def unflattenFaces(faces, counts): def buildAbc( - outPath, - points, - faces, - faceCounts=None, - uvs=None, - uvFaces=None, - normals=None, - normFaces=None, - name="polymsh", - shapeSuffix="Shape", - transformSuffix="", - propDict=None, - ogawa=True, - pBar=None, + outPath: str, + points: Union[list, npt.NDArray], + faces: Union[list[list[int]], list[int]], + faceCounts: Optional[list[int]] = None, + uvs: Optional[Union[list, npt.NDArray]] = None, + uvFaces: Optional[Union[list[list[int]], list[int]]] = None, + normals: Optional[Union[list, npt.NDArray]] = None, + normFaces: Optional[Union[list[list[int]], list[int]]] = None, + name: str = "polymsh", + shapeSuffix: str = "Shape", + transformSuffix: str = "", + propDict: Optional[dict[str, str]] = None, + ogawa: bool = True, + pBar: Optional[QProgressDialog] = None, ): """ Build a single-mesh alembic file from all of the non-alembic raw data @@ -837,7 +918,7 @@ def buildAbc( # Simplex format specific stuff -def getSmpxArchiveData(abcPath): +def getSmpxArchiveData(abcPath: str) -> tuple[IArchive, IPolyMesh, str]: """Read and return the low level relevant data from a simplex alembic Parameters @@ -879,7 +960,16 @@ def getSmpxArchiveData(abcPath): return iarch, abcMesh, jsString -def readSmpx(path, pBar=None): +def readSmpx( + path: str, pBar: Optional[QProgressDialog] = None +) -> tuple[ + str, + list[int], + npt.NDArray, + npt.NDArray, + Optional[npt.NDArray], + Optional[npt.NDArray], +]: """Read and return the raw alembic vertex/face data in the flat alembic style Parameters @@ -916,7 +1006,7 @@ def readSmpx(path, pBar=None): def buildSmpx( - outPath, + outPath: str, points, faces, jsString, @@ -924,9 +1014,9 @@ def buildSmpx( faceCounts=None, uvs=None, uvFaces=None, - ogawa=True, - pBar=None, -): + ogawa: bool = True, + pBar: Optional[QProgressDialog] = None, +) -> None: """ Build a simplex output from raw data @@ -971,7 +1061,9 @@ def buildSmpx( ) -def buildAlembicArchiveData(path, name, jsString, ogawa): +def buildAlembicArchiveData( + path: str, name: str, jsString: str, ogawa: bool +) -> tuple[Optional[OArchive], Optional[OPolyMesh]]: """Set up an output alembic archive with a mesh ready for writing Parameters @@ -1006,7 +1098,7 @@ def buildAlembicArchiveData(path, name, jsString, ogawa): return arch, abcMesh -def readFalloffData(abcPath): +def readFalloffData(abcPath: str) -> dict[str, npt.NDArray]: """Load the relevant data from a simplex alembic Parameters diff --git a/src/python/simplexui/commands/mayatonumpy.py b/src/python/simplexui/commands/mayatonumpy.py index 95f3f4ed..be9afd3a 100644 --- a/src/python/simplexui/commands/mayatonumpy.py +++ b/src/python/simplexui/commands/mayatonumpy.py @@ -1,3 +1,4 @@ +# pytype: disable=import-error from maya import OpenMaya as om import numpy as np from ctypes import c_float, c_double, c_int, c_uint diff --git a/src/python/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py index c9c0d1fb..e4eef9d9 100644 --- a/src/python/simplexui/items/accessor.py +++ b/src/python/simplexui/items/accessor.py @@ -39,6 +39,8 @@ def __init__(self, simplex): self.dragStep = 0.05 self.maxValue = 1.0 self.minValue = 0.0 + self.tickable = False + self.value = 0.0 def valueTick(self, ticks, mul): """Change the value of the current object by some number of ticks diff --git a/typings/alembic/Abc.pyi b/typings/alembic/Abc.pyi new file mode 100644 index 00000000..67e00301 --- /dev/null +++ b/typings/alembic/Abc.pyi @@ -0,0 +1,53 @@ +from typing import Any, List +from imath import Box3d, V3fArray, IntArray, V2fArray, UnsignedIntArray + +class IObject: + children: List['IObject'] + def getName(self) -> str: ... + def getParent(self) -> 'IObject': ... + def getMetaData(self) -> Any: ... + +class OObject: + pass + +class IArchive: + def __init__(self, path: str) -> None: ... + def getTop(self) -> IObject: ... + +class OArchive: + def __init__(self, path: str, ogawa: bool = ...) -> None: ... + def getTop(self) -> OObject: ... + +class IProperty: + def getValue(self) -> Any: ... + +class ICompoundProperty: + def valid(self) -> bool: ... + def getProperty(self, str) -> IProperty: ... + +class OCompoundProperty: ... + +class OStringProperty: + def __init__(self, props: OCompoundProperty, key: str) -> None: ... + def setValue(self, value: str) -> None: ... + +class OBox3dProperty: + def setValue(self, value: Box3d) -> None: ... + +class IP3fArrayProperty: + samples: list[V3fArray] + +class IInt32ArrayProperty: + samples: list[IntArray] + +class IV2fArrayProperty: + def getValue(self) -> V2fArray: ... + +class IUInt32ArrayProperty: + def getValue(self) -> UnsignedIntArray: ... + +class IV2fGeomParam: + def valid(self) -> bool: ... + def isIndexed(self) -> bool: ... + def getValueProperty(self) -> IV2fArrayProperty: ... + def getIndexProperty(self) -> IUInt32ArrayProperty: ... diff --git a/typings/alembic/AbcCoreAbstract.pyi b/typings/alembic/AbcCoreAbstract.pyi new file mode 100644 index 00000000..33971745 --- /dev/null +++ b/typings/alembic/AbcCoreAbstract.pyi @@ -0,0 +1 @@ +class MetaData: ... diff --git a/typings/alembic/AbcGeom.pyi b/typings/alembic/AbcGeom.pyi new file mode 100644 index 00000000..622c37d2 --- /dev/null +++ b/typings/alembic/AbcGeom.pyi @@ -0,0 +1,58 @@ +from typing import Any, Optional +from alembic.Abc import ( + IObject, + OObject, + OBox3dProperty, + IP3fArrayProperty, + IInt32ArrayProperty, + IV2fGeomParam, +) +from alembic.AbcCoreAbstract import MetaData + +class GeometryScope: + kFacevaryingScope: Any + +class ON3fGeomParamSample: + def __init__(self, array: Any, indexes_or_scope: Any, scope: Any = ...) -> None: ... + +class OV2fGeomParamSample: + def __init__(self, array: Any, indexes_or_scope: Any, scope: Any = ...) -> None: ... + +class OPolyMeshSchema: + def getChildBoundsProperty(self) -> OBox3dProperty: ... + def set(self, sample: OPolyMeshSchemaSample) -> None: ... + +class IPolyMeshSchema: + def getPositionsProperty(self) -> IP3fArrayProperty: ... + def getFaceIndicesProperty(self) -> IInt32ArrayProperty: ... + def getFaceCountsProperty(self) -> IInt32ArrayProperty: ... + def getUVsParam(self) -> IV2fGeomParam: ... + +class IPolyMesh(_IBase): + def getSchema(self) -> IPolyMeshSchema: ... + +class OPolyMeshSchemaSample: + def __init__( + self, points: Any, faceIndex: Any, faceCount: Any, **kwargs + ) -> None: ... + +class _IBase(IObject): + def __init__(self, parent: IObject, name: str) -> None: ... + @classmethod + def matches(cls, md: MetaData) -> bool: ... + def getSchema(self) -> Any: ... + +class ICamera(_IBase): ... +class ICurves(_IBase): ... +class ILight(_IBase): ... +class INuPatch(_IBase): ... +class IPoints(_IBase): ... +class ISubD(_IBase): ... +class IXform(_IBase): ... + +class _OBase(OObject): + def __init__(self, parent: OObject, name: str) -> None: ... + def getSchema(self) -> Any: ... + +class OPolyMesh(_OBase): ... +class OXform(_OBase): ... diff --git a/typings/alembic/__init__.pyi b/typings/alembic/__init__.pyi new file mode 100644 index 00000000..e69de29b diff --git a/typings/imath.pyi b/typings/imath.pyi new file mode 100644 index 00000000..b548b022 --- /dev/null +++ b/typings/imath.pyi @@ -0,0 +1,27 @@ +from typing import Any, Sequence, Tuple, Union + +class V2f: + def __init__(self, x: float, y: float) -> None: ... + def setValue(self, x: float, y: float) -> None: ... + x: float + y: float + +class Box3d: ... + + +class _BaseArray: + def __init__(self, length: int) -> None: ... + def __setitem__(self, index: Union[int, slice], value: Any) -> None: ... + def __getitem__(self, index: Union[int, slice]) -> Any: ... + def __len__(self) -> int: ... + def __iter__(self) -> Any: ... + +class FloatArray(_BaseArray): ... +class IntArray(_BaseArray): ... +class UnsignedIntArray(_BaseArray): ... +class V2fArray(_BaseArray): + x: FloatArray + y: FloatArray +class V3fArray(_BaseArray): ... + + From d840349048fdb8e651d6520123f92b6a5faf5171 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 11 Aug 2026 23:47:10 -0700 Subject: [PATCH 06/47] Make numpy a reqirement for alembicCommon --- .../simplexui/commands/alembicCommon.py | 147 +++++++----------- typings/alembic/AbcGeom.pyi | 2 +- typings/imath.pyi | 10 +- 3 files changed, 60 insertions(+), 99 deletions(-) diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index eb8dece6..904c8abc 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -41,35 +41,27 @@ OXform, ) from imath import IntArray, UnsignedIntArray, V2f, V2fArray, V3fArray, Box3d +import numpy as np +import numpy.typing as npt try: - import numpy as np + from imathnumpy import arrayToNumpy # pylint:disable=no-name-in-module except ImportError: - np = None arrayToNumpy = None -else: - try: - from imathnumpy import arrayToNumpy # pylint:disable=no-name-in-module - except ImportError: - arrayToNumpy = None from typing import ( TYPE_CHECKING, Optional, Union, - Sequence, Type, TypeVar, overload, - Any, ) if TYPE_CHECKING: from Qt.QtWidgets import QProgressDialog - import numpy as np - import numpy.typing as npt from alembic.AbcGeom import _IBase # A helper typing-only class @@ -128,7 +120,7 @@ def pbPrint( print(message) -def mkArray(aType: Type[T], iList: Sequence) -> T: +def mkArray(aType: Type[T], iList: npt.NDArray) -> T: """Makes the alembic-usable c++ typed 2-d arrays Parameters @@ -148,23 +140,21 @@ def mkArray(aType: Type[T], iList: Sequence) -> T: size = len(iList) array = aType(size) - - if np is None: - for i in range(size): - array[i] = tuple(iList[i]) - return array - elif arrayToNumpy is None: - for i in range(size): - array[i] = tuple(iList[i].tolist()) - return array + if arrayToNumpy is None: + if isinstance(iList, np.ndarray): + for i in range(size): + array[i] = tuple(iList[i].tolist()) + else: + for i in range(size): + array[i] = tuple(iList[i]) else: nplist = np.array(iList) memView = arrayToNumpy(array) np.copyto(memView, nplist) - return array + return array -def mk1dArray(aType: Type[T], iList: Sequence) -> T: +def mk1dArray(aType: Type[T], iList: npt.NDArray) -> T: """Makes the alembic-usable c++ typed 1-d arrays Parameters @@ -183,7 +173,7 @@ def mk1dArray(aType: Type[T], iList: Sequence) -> T: return iList array = aType(len(iList)) - if np is None or arrayToNumpy is None or aType is UnsignedIntArray: + if arrayToNumpy is None or aType is UnsignedIntArray: for i in range(len(iList)): # Gotta cast to int because an "int" from numpy has # the type np.int32, which makes this conversion angry @@ -196,7 +186,7 @@ def mk1dArray(aType: Type[T], iList: Sequence) -> T: return array -def mkSampleVertexPoints(pts: Sequence) -> V3fArray: +def mkSampleVertexPoints(pts: npt.NDArray[Union[np.float64, np.float32]]) -> V3fArray: """Make an imath array of vertices Parameters @@ -212,7 +202,7 @@ def mkSampleVertexPoints(pts: Sequence) -> V3fArray: return mkArray(V3fArray, pts) -def mkSampleIntArray(vals: Sequence) -> IntArray: +def mkSampleIntArray(vals: npt.NDArray[Union[np.int64, np.int32]]) -> IntArray: """Make an imath array of integers Parameters @@ -228,7 +218,7 @@ def mkSampleIntArray(vals: Sequence) -> IntArray: return mk1dArray(IntArray, vals) -def mkSampleUIntArray(vals: Sequence) -> UnsignedIntArray: +def mkSampleUIntArray(vals: npt.NDArray[Union[np.int64, np.int32]]) -> UnsignedIntArray: """Make an imath array of unsigned integers Parameters @@ -244,7 +234,7 @@ def mkSampleUIntArray(vals: Sequence) -> UnsignedIntArray: return mk1dArray(UnsignedIntArray, vals) -def mkSampleUvArray(uvs: Sequence) -> V2fArray: +def mkSampleUvArray(uvs: npt.NDArray) -> V2fArray: """Make an imath array of uvs Parameters @@ -266,7 +256,7 @@ def mkSampleUvArray(uvs: Sequence) -> V2fArray: def mkUvSample( - uvs: Sequence, indexes: Optional[Sequence] = None + uvs: npt.NDArray, indexes: Optional[npt.NDArray] = None ) -> OV2fGeomParamSample: """Take an array, and make a poly mesh sample of the uvs @@ -290,7 +280,7 @@ def mkUvSample( def mkNormalSample( - norms: Sequence, indexes: Optional[Sequence] = None + norms: npt.NDArray, indexes: Optional[npt.NDArray] = None ) -> ON3fGeomParamSample: """Take an array, and make a poly mesh sample of the normals @@ -338,7 +328,7 @@ def setAlembicSample( def getSampleArray( imesh: IPolyMesh, pBar: Optional[QProgressDialog] = None -) -> Union[Sequence, npt.NDArray]: +) -> npt.NDArray: """Get the per-frame vertex positions for a mesh Parameters @@ -354,13 +344,13 @@ def getSampleArray( meshSchema = imesh.getSchema() posProp = meshSchema.getPositionsProperty() numShapes = len(posProp.samples) - if arrayToNumpy is not None and np is not None: + if arrayToNumpy is not None: shapes = np.empty((len(posProp.samples), len(posProp.samples[0]), 3)) for i, s in enumerate(posProp.samples): pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) shapes[i] = arrayToNumpy(s) return shapes - elif np is not None: + else: shapes = [] for i, s in enumerate(posProp.samples): pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) @@ -368,12 +358,6 @@ def getSampleArray( shapes = np.array(shapes) shapes = shapes.transpose((0, 2, 1)) return shapes - else: - shapes = [] - for i, s in enumerate(posProp.samples): - pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) - shapes.append(s) - return shapes def getStaticMeshData(imesh: IPolyMesh) -> tuple[IntArray, IntArray]: @@ -397,9 +381,7 @@ def getStaticMeshData(imesh: IPolyMesh) -> tuple[IntArray, IntArray]: return faces, counts -def getStaticMeshArrays( - imesh: IPolyMesh, -) -> tuple[Union[npt.NDArray, Sequence], Union[npt.NDArray, Sequence]]: +def getStaticMeshArrays(imesh: IPolyMesh) -> tuple[npt.NDArray, npt.NDArray]: """Get all the generally non-changing data for a mesh as numpy arrays Parameters @@ -415,13 +397,11 @@ def getStaticMeshArrays( The number of vertices per face as np.array if possible """ faces, counts = getStaticMeshData(imesh) - if arrayToNumpy is not None and np is not None: + if arrayToNumpy is not None: faces = arrayToNumpy(faces).copy() counts = arrayToNumpy(counts).copy() - elif np is not None: - faces, counts = np.array(faces), np.array(counts) else: - faces, counts = list(faces), list(counts) + faces, counts = np.array(faces), np.array(counts) return faces, counts @@ -453,9 +433,7 @@ def getUvSample(imesh: IPolyMesh) -> Optional[OV2fGeomParamSample]: return uv -def getUvArray( - imesh: IPolyMesh, -) -> Optional[Union[npt.NDArray, list[tuple[float, float]]]]: +def getUvArray(imesh: IPolyMesh) -> Optional[npt.NDArray]: """Get the uv positions for a mesh Parameters @@ -476,8 +454,7 @@ def getUvArray( # imathNumpy doesn't work on V2f arrays # so I have to use one of the slow ways uv = list(zip(uvVals.x, uvVals.y)) - if np is not None: - uv = np.array(uv) + uv = np.array(uv) else: uv = None return uv @@ -485,7 +462,7 @@ def getUvArray( def getFlatUvFaces( imesh: IPolyMesh, -) -> tuple[Union[list[int], npt.NDArray], Optional[bool]]: +) -> tuple[npt.NDArray, Optional[bool]]: """Get the UV structure for a mesh if it's indexed. If un-indexed, return None This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed and can handle the data appropriately for export without keeping track of index-ness @@ -510,25 +487,16 @@ def getFlatUvFaces( if iuvs.isIndexed(): indexed = True idxs = iuvs.getIndexProperty().getValue() - # if arrayToNumpy is not None and np is not None: - # idxs = arrayToNumpy(idxs).copy() - # elif np is not None: - if np is not None: - idxs = np.array(idxs) - else: - idxs = list(idxs) + idxs = np.array(idxs) else: indexed = False rawCount = sum(list(sch.getFaceCountsProperty().samples[0])) - if np is not None: - idxs = np.arange(rawCount) - else: - idxs = list(range(rawCount)) + idxs = np.arange(rawCount) return idxs, indexed -def getUvFaces(imesh: IPolyMesh) -> Optional[Union[list[list[int]], npt.NDArray]]: +def getUvFaces(imesh: IPolyMesh) -> Optional[npt.NDArray]: """Get the UV structure for a mesh if it's indexed. If un-indexed, return None This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed and can handle the data appropriately for export without keeping track of index-ness @@ -763,7 +731,7 @@ def readStringProperty(props: ICompoundProperty, key: str) -> str: def flattenFaces( faces: list[list[int]], -) -> tuple[Union[list[int], npt.NDArray], Union[list[int], npt.NDArray]]: +) -> tuple[npt.NDArray, npt.NDArray]: """Take a nested list representation of faces and turn it into a flat face/count representation @@ -784,9 +752,7 @@ def flattenFaces( for f in faces: faceCounts.append(len(f)) faceIdxs.extend(f) - if np is not None: - return np.array(faceCounts), np.array(faceIdxs) - return faceCounts, faceIdxs + return np.array(faceCounts), np.array(faceIdxs) def unflattenFaces(faces: npt.NDArray, counts: npt.NDArray) -> list[list[int]]: @@ -814,13 +780,13 @@ def unflattenFaces(faces: npt.NDArray, counts: npt.NDArray) -> list[list[int]]: def buildAbc( outPath: str, - points: Union[list, npt.NDArray], - faces: Union[list[list[int]], list[int]], - faceCounts: Optional[list[int]] = None, - uvs: Optional[Union[list, npt.NDArray]] = None, - uvFaces: Optional[Union[list[list[int]], list[int]]] = None, - normals: Optional[Union[list, npt.NDArray]] = None, - normFaces: Optional[Union[list[list[int]], list[int]]] = None, + points: npt.NDArray, + faces: npt.NDArray, + faceCounts: Optional[npt.NDArray] = None, + uvs: Optional[npt.NDArray] = None, + uvFaces: Optional[npt.NDArray] = None, + normals: Optional[npt.NDArray] = None, + normFaces: Optional[npt.NDArray] = None, name: str = "polymsh", shapeSuffix: str = "Shape", transformSuffix: str = "", @@ -897,13 +863,9 @@ def buildAbc( omesh = OPolyMesh(opar, str(name + shapeSuffix)) - if np is not None: - points = np.array(points) - if len(points.shape) == 2: - points = points[None, ...] - else: - if not isinstance(points[0][0], (list, tuple)): - points = [points] + points = np.array(points) + if len(points.shape) == 2: + points = points[None, ...] sch = omesh.getSchema() for i, frame in enumerate(points): @@ -964,7 +926,7 @@ def readSmpx( path: str, pBar: Optional[QProgressDialog] = None ) -> tuple[ str, - list[int], + npt.NDArray, npt.NDArray, npt.NDArray, Optional[npt.NDArray], @@ -1007,13 +969,13 @@ def readSmpx( def buildSmpx( outPath: str, - points, - faces, - jsString, - name, - faceCounts=None, - uvs=None, - uvFaces=None, + points: npt.NDArray, + faces: npt.NDArray, + jsString: str, + name: str, + faceCounts: Optional[npt.NDArray] = None, + uvs: Optional[npt.NDArray] = None, + uvFaces: Optional[npt.NDArray] = None, ogawa: bool = True, pBar: Optional[QProgressDialog] = None, ) -> None: @@ -1126,10 +1088,7 @@ def readFalloffData(abcPath: str) -> dict[str, npt.NDArray]: nps = foPropPar.getNumProperties() for i in range(nps): foProp = foPropPar.getProperty(i) - fon = foProp.getName() - fov = foProp.getValue() # imath.FloatArray - fov = list(fov) if np is None else np.array(fov) - foDict[fon] = fov + foDict[foProp.getName()] = np.array(foProp.getValue()) finally: iarch, top, par, systemSchema, foPropPar, foProp = [None] * 6 diff --git a/typings/alembic/AbcGeom.pyi b/typings/alembic/AbcGeom.pyi index 622c37d2..1bb096fb 100644 --- a/typings/alembic/AbcGeom.pyi +++ b/typings/alembic/AbcGeom.pyi @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from alembic.Abc import ( IObject, OObject, diff --git a/typings/imath.pyi b/typings/imath.pyi index b548b022..c1904d1f 100644 --- a/typings/imath.pyi +++ b/typings/imath.pyi @@ -1,4 +1,4 @@ -from typing import Any, Sequence, Tuple, Union +from typing import Any, Union class V2f: def __init__(self, x: float, y: float) -> None: ... @@ -8,7 +8,6 @@ class V2f: class Box3d: ... - class _BaseArray: def __init__(self, length: int) -> None: ... def __setitem__(self, index: Union[int, slice], value: Any) -> None: ... @@ -19,9 +18,12 @@ class _BaseArray: class FloatArray(_BaseArray): ... class IntArray(_BaseArray): ... class UnsignedIntArray(_BaseArray): ... + class V2fArray(_BaseArray): x: FloatArray y: FloatArray -class V3fArray(_BaseArray): ... - +class V3fArray(_BaseArray): + x: FloatArray + y: FloatArray + z: FloatArray From d3bd5614cf00758041c9d209056a35542237dcc5 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 3 Sep 2026 17:30:03 -0700 Subject: [PATCH 07/47] Format pysimplex.cpp --- src/python/pysimplex.cpp | 131 +++++++++++++++------------------------ 1 file changed, 49 insertions(+), 82 deletions(-) diff --git a/src/python/pysimplex.cpp b/src/python/pysimplex.cpp index 3b5cbd7c..31b81776 100644 --- a/src/python/pysimplex.cpp +++ b/src/python/pysimplex.cpp @@ -1,30 +1,29 @@ #include #include -#include "simplex.h" -#include #include -#include #include +#include +#include + +#include "simplex.h" typedef struct { - PyObject_HEAD // No Semicolon for this Macro; - PyObject *definition; - simplex::Simplex *sPointer; + PyObject_HEAD // No Semicolon for this Macro; + PyObject* definition; + simplex::Simplex* sPointer; } PySimplex; -static void -PySimplex_dealloc(PySimplex* self) { +static void PySimplex_dealloc(PySimplex* self) { Py_XDECREF(self->definition); - if (self->sPointer != NULL) - delete self->sPointer; + if (self->sPointer != NULL) { + delete self->sPointer; + } PyObject_Del(self); } -static PyObject * -PySimplex_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { - - PySimplex *self = PyObject_New(PySimplex, type); +static PyObject* PySimplex_new(PyTypeObject* type, PyObject* args, PyObject* kwds) { + PySimplex* self = PyObject_New(PySimplex, type); if (self != NULL) { self->definition = PyUnicode_FromString(""); if (self->definition == NULL) { @@ -34,27 +33,25 @@ PySimplex_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { self->sPointer = new simplex::Simplex(); } - return (PyObject *)self; + return (PyObject*)self; } -static PyObject * -PySimplex_getdefinition(PySimplex* self, void* closure){ +static PyObject* PySimplex_getdefinition(PySimplex* self, void* closure) { Py_INCREF(self->definition); return self->definition; } -static int -PySimplex_setdefinition(PySimplex* self, PyObject* jsValue, void* closure){ - if (jsValue == NULL || jsValue == Py_None){ +static int PySimplex_setdefinition(PySimplex* self, PyObject* jsValue, void* closure) { + if (jsValue == NULL || jsValue == Py_None) { jsValue = PyUnicode_FromString(""); } - if (! PyUnicode_Check(jsValue)) { + if (!PyUnicode_Check(jsValue)) { PyErr_SetString(PyExc_TypeError, "The simplex definition must be a string"); return -1; } - PyObject *tmp = self->definition; + PyObject* tmp = self->definition; Py_INCREF(jsValue); self->definition = jsValue; Py_DECREF(tmp); @@ -72,18 +69,16 @@ PySimplex_setdefinition(PySimplex* self, PyObject* jsValue, void* closure){ return 0; } -static PyObject * -PySimplex_getexactsolve(PySimplex* self, void* closure){ - if (self->sPointer->getExactSolve()){ +static PyObject* PySimplex_getexactsolve(PySimplex* self, void* closure) { + if (self->sPointer->getExactSolve()) { Py_RETURN_TRUE; } Py_RETURN_FALSE; } -static int -PySimplex_setexactsolve(PySimplex* self, PyObject* exact, void* closure){ +static int PySimplex_setexactsolve(PySimplex* self, PyObject* exact, void* closure) { int truthy = PyObject_IsTrue(exact); - if (truthy == -1){ + if (truthy == -1) { PyErr_SetString(PyExc_TypeError, "The value passed cannot be cast to boolean"); return -1; } @@ -92,31 +87,30 @@ PySimplex_setexactsolve(PySimplex* self, PyObject* exact, void* closure){ return 0; } -static int -PySimplex_init(PySimplex *self, PyObject *args, PyObject *kwds) { - PyObject *jsValue=NULL, *tmp=NULL; +static int PySimplex_init(PySimplex* self, PyObject* args, PyObject* kwds) { + PyObject *jsValue = NULL, *tmp = NULL; char jsValueLiteral[] = "jsValue"; - static char *kwlist[] = {jsValueLiteral, NULL}; + static char* kwlist[] = {jsValueLiteral, NULL}; - if (! PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &jsValue)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &jsValue)) { return -1; + } return PySimplex_setdefinition(self, jsValue, NULL); } -static PyObject * -PySimplex_solve(PySimplex* self, PyObject* vec){ - if (! PySequence_Check(vec)){ +static PyObject* PySimplex_solve(PySimplex* self, PyObject* vec) { + if (!PySequence_Check(vec)) { PyErr_SetString(PyExc_TypeError, "Input must be a list or tuple"); return NULL; } - PyObject *item; + PyObject* item; std::vector stdVec, outVec; - for (Py_ssize_t i=0; isPointer->clearValues(); + self->sPointer->clearValues(); outVec = self->sPointer->solve(stdVec); - PyObject *out = PyList_New(outVec.size()); - for (size_t i=0; i Date: Thu, 3 Sep 2026 18:35:51 -0700 Subject: [PATCH 08/47] Add a couple exposed variables --- subprojects/maya/meson.build | 2 ++ 1 file changed, 2 insertions(+) diff --git a/subprojects/maya/meson.build b/subprojects/maya/meson.build index 1d6dc7df..e1f6c145 100644 --- a/subprojects/maya/meson.build +++ b/subprojects/maya/meson.build @@ -148,6 +148,8 @@ maya_dep = declare_dependency( 'maya_bin_dir': maya_bin_dir, 'maya_inc_dir': maya_inc_dir, 'qt_moc_path': qt_moc_path, + 'maya_install_path': maya_install_path, + 'maya_using_devkit': maya_devkit_base != '', }, compile_args : maya_compile_args, link_args : maya_link_args, From ceff0a0962e1ae4d0762b6d9089ccaccad54cd01 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 15 Sep 2026 16:32:06 -0700 Subject: [PATCH 09/47] Move to a new maya meson paradigm --- meson.build | 30 ++- meson.options | 1 + src/maya/meson.build | 9 +- subprojects/maya/meson.build | 395 ++++++++++++++++++++++++--------- subprojects/maya/meson.options | 59 ++++- 5 files changed, 372 insertions(+), 122 deletions(-) diff --git a/meson.build b/meson.build index 767f3972..f741af89 100644 --- a/meson.build +++ b/meson.build @@ -17,22 +17,34 @@ project( maya_build = get_option('maya_build') python_build = get_option('python_build') +imath_bridge_build = get_option('imath_bridge_build') -if not maya_build and not python_build +if not maya_build and not python_build and not imath_bridge_build error('No builds requested') endif conf_data = configuration_data() conf_data.set('VCS_TAG', meson.project_version()) -subdir('src/simplexlib') -if maya_build - maya_dep = dependency('maya') - maya_name_suffix = maya_dep.get_variable('name_suffix') - maya_version = maya_dep.get_variable('maya_version') - subdir('src/maya') + +if maya_build or imath_bridge_build + subproject('maya') +endif + +if imath_bridge_build + subdir('src/imathbridge') endif -if python_build - subdir('src/python') +if maya_build or python_build + subdir('src/simplexlib') + if maya_build + subdir('src/maya') + endif + + if python_build + subdir('src/python') + endif endif + + + diff --git a/meson.options b/meson.options index 36793e6c..0a4986f8 100644 --- a/meson.options +++ b/meson.options @@ -1,4 +1,5 @@ option('maya_build', type : 'boolean', value : true) option('python_build', type : 'boolean', value : true) +option('imath_bridge_build', type : 'boolean', value : false) option('python_wheel_build', type : 'boolean', value : false) option('python_script_build', type : 'boolean', value : false) diff --git a/src/maya/meson.build b/src/maya/meson.build index 36fac10b..46d45606 100644 --- a/src/maya/meson.build +++ b/src/maya/meson.build @@ -1,6 +1,7 @@ -maya_dep = dependency('maya') -maya_name_suffix = maya_dep.get_variable('name_suffix') -maya_version = maya_dep.get_variable('maya_version') +maya_plugin_dep = dependency('maya-plugin') +maya_core_dep = dependency('maya-core') +maya_name_suffix = maya_plugin_dep.get_variable('name_suffix') +maya_version = maya_core_dep.get_variable('maya_version') simplex_maya_files = files([ 'src/basicBlendShape.cpp', @@ -29,7 +30,7 @@ simplex_maya = shared_library( install: true, install_dir : meson.global_source_root() / 'output_Maya' + maya_version, include_directories : simplex_maya_inc, - dependencies : [maya_dep, simplexlib_dep], + dependencies : [maya_core_dep, maya_plugin_dep, simplexlib_dep], name_prefix : '', name_suffix : maya_name_suffix, ) diff --git a/subprojects/maya/meson.build b/subprojects/maya/meson.build index e1f6c145..2703b6bd 100644 --- a/subprojects/maya/meson.build +++ b/subprojects/maya/meson.build @@ -1,12 +1,44 @@ -project('maya', 'cpp') +project('maya', 'cpp', meson_version : '>=1.7.0') + +fs = import('fs') +py = import('python').find_installation(pure: false) +cmplr = meson.get_compiler('cpp') + +# --- Zip Finder Helper Script --- +find_zip_script = ''' +import sys, glob, os +base = sys.argv[1] +pattern = sys.argv[2] + +bases = [b for b in base.split(';') if os.path.exists(b)] +if not bases: + print(f"a zip file matching '{pattern}'") + sys.exit(0) + +for base in bases: + matches = glob.glob(os.path.join(base, "**", pattern), recursive=True) + if matches: + print(matches[0]) + sys.exit(0) + +print(f"a zip file matching '{pattern}'") +''' maya_version = get_option('maya_version') maya_devkit_base = get_option('maya_devkit_base') -maya_link_qt = get_option('maya_link_qt') maya_qt_extra_includes = get_option('maya_qt_extra_includes') +maya_custom_install_path = get_option('maya_custom_install_path') -os_name = build_machine.system() +alembic_opt = get_option('maya_link_alembic') +imath_opt = get_option('maya_link_imath') +ufe_opt = get_option('maya_link_ufe') +qt_opt = get_option('maya_link_qt') +gl_opt = get_option('maya_link_opengl') +numpy_opt = get_option('maya_link_numpy') + +has_devkit = maya_devkit_base != '' +os_name = build_machine.system() maya_inc_suffix = 'include' maya_lib_suffix = 'lib' @@ -14,145 +46,300 @@ maya_compile_args = ['-DREQUIRE_IOSTREAM', '-D_BOOL'] maya_link_args = [] if os_name == 'windows' - maya_install_base = 'c:/Program Files/Autodesk' + maya_default_install_base = 'c:/Program Files/Autodesk' maya_plugin_ext = 'mll' maya_compile_args += ['-DNT_PLUGIN', '/Zc:__cplusplus'] - maya_link_args = [ - # Export the methods that maya requires - '/export:initializePlugin', - '/export:uninitializePlugin', - # Make sure to look for pdb file in the same directory as the .mll - '/PDBALTPATH:%_PDB%', - ] + maya_link_args = ['/export:initializePlugin', '/export:uninitializePlugin', '/PDBALTPATH:%_PDB%'] elif os_name == 'darwin' - maya_install_base = '/Applications/Autodesk' + maya_default_install_base = '/Applications/Autodesk' maya_plugin_ext = 'bundle' - if maya_devkit_base == '' + if not has_devkit maya_lib_suffix = 'Maya.app/Contents/MacOS' maya_bin_suffix = 'Maya.app/Contents/bin' endif - maya_compile_args += ['-DOSMac_'] - if meson.get_compiler('cpp').get_id() == 'clang' - maya_compile_args += ['--stdlib', 'libc++'] - maya_compile_args += ['-arch', 'x86_64'] + maya_compile_args += ['-DOSMac_', '-Wno-inconsistent-missing-override'] + if cmplr.get_id() == 'clang' + maya_compile_args += ['--stdlib', 'libc++', '-arch', 'x86_64'] maya_link_args += ['-arch', 'x86_64'] if maya_version.version_compare('>=2024') - # build both the arm and x86 plugins when compiling for mac maya_compile_args += ['-arch', 'arm64'] maya_link_args += ['-arch', 'arm64'] endif endif - - # ignore this warning that comes from maya's headers - maya_compile_args += ['-Wno-inconsistent-missing-override'] elif os_name == 'linux' - maya_install_base = '/usr/autodesk' + maya_default_install_base = '/usr/autodesk' maya_plugin_ext = 'so' maya_compile_args += ['-DLINUX', '-fPIC'] else error('Incompatible operating system') endif -maya_install_path = maya_install_base / ('Maya' + maya_version) -if maya_devkit_base != '' - message('Using Maya Devkit:', maya_devkit_base) - maya_install_path = maya_devkit_base +if maya_custom_install_path != '' + maya_install_path = maya_custom_install_path +else + maya_install_path = maya_default_install_base / ('Maya' + maya_version) endif -includes = [] -maya_inc_dir = maya_install_path / maya_inc_suffix -message('Searching Maya Include directory:', maya_inc_dir) -includes += include_directories(maya_inc_dir) +search_base = has_devkit ? maya_devkit_base : maya_install_path -maya_lib_dir = maya_install_path / maya_lib_suffix -message('Searching Maya lib directory:', maya_lib_dir) +# --- Define Ordered Base Search Paths --- +search_inc_dirs = [] +search_lib_dirs = [] +maya_bin_dir = '' +maya_py_ver = '' -maya_bin_dir = maya_install_path / 'bin' +has_install = maya_install_path != '' and fs.is_dir(maya_install_path) -# Get all the maya libraries -cmplr = meson.get_compiler('cpp') -maya_libs = [ - cmplr.find_library('Foundation', dirs : maya_lib_dir), - cmplr.find_library('OpenMaya', dirs : maya_lib_dir), - cmplr.find_library('OpenMayaAnim', dirs : maya_lib_dir), - cmplr.find_library('OpenMayaFX', dirs : maya_lib_dir), - cmplr.find_library('OpenMayaRender', dirs : maya_lib_dir), - cmplr.find_library('OpenMayaUI', dirs : maya_lib_dir), - cmplr.find_library('clew', dirs : maya_lib_dir), -] - -# Link to maya's qt libs if required -# Below, I expose the bin and include dirs so I can directly invoke the -# moc exe included with maya. Saves from having to make a maya qt module - -qt_moc_path = '' -if maya_link_qt - fs = import('fs') - - if maya_version.version_compare('>=2025') - if not fs.is_dir(maya_install_path / 'Qt' / 'include' / 'QtCore') - error( - 'Could not find Maya QT headers with `maya_link_qt` defined\n', - 'You probably need to unzip `devkitBase/Qt.zip`\n', - 'Checking in folder: ', maya_install_path, - ) - endif - maya_qt_lib_names = [f'Qt6Core', f'Qt6Gui', f'Qt6Widgets'] +if has_install + search_inc_dirs += maya_install_path / maya_inc_suffix + search_lib_dirs += maya_install_path / maya_lib_suffix + maya_bin_dir = maya_install_path / 'bin' + mayapy_prg = find_program('mayapy', dirs : maya_bin_dir, required : numpy_opt.enabled()) + pyverscript = 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' + maya_py_ver = run_command(mayapy_prg, '-c', pyverscript, check: false).stdout().strip() - qt_lib_dir = maya_lib_dir - qt_moc_path = maya_bin_dir / 'moc' - if maya_devkit_base != '' - qt_lib_dir = maya_devkit_base / 'Qt' / 'lib' + if mayapy_prg.found() + meson.override_find_program('maya:mayapy', mayapy_prg) + endif +endif - mocdirs = [ - maya_devkit_base / 'Qt' / 'bin', - maya_devkit_base / 'Qt' / 'libexec', - ] - moc_prg = find_program('moc', dirs : mocdirs, required : true) - qt_moc_path = moc_prg.full_path() +if has_devkit + search_inc_dirs += maya_devkit_base / maya_inc_suffix + search_lib_dirs += maya_devkit_base / maya_lib_suffix +endif - includes += include_directories(maya_devkit_base / 'Qt' / 'include') - endif - else - if not fs.is_dir(maya_inc_dir / 'QtCore') - error( - 'Could not find Maya QT headers with `maya_link_qt` defined\n', - 'You probably need to unzip `include/qt_*-include.zip`\n', - 'Checking in folder: ', maya_inc_dir, - ) - endif - qt_ver = 5 - maya_qt_lib_names = [f'Qt5Core', f'Qt5Gui', f'Qt5Widgets'] - qt_lib_dir = maya_lib_dir - qt_moc_path = maya_bin_dir / 'moc' - if maya_devkit_base != '' - qt_moc_path = maya_devkit_base / 'devkit' / 'bin' / 'moc' - endif - endif +if search_inc_dirs.length() == 0 + error('Could not find any maya install or devkit paths') +endif + +maya_inc_dir = search_inc_dirs[-1] +maya_lib_dir = search_lib_dirs[-1] - if maya_qt_extra_includes != '' - maya_qt_lib_names += maya_qt_extra_includes.split(';') +core_includes = [] +foreach inc_dir : search_inc_dirs + if fs.is_dir(inc_dir) + core_includes += include_directories(inc_dir) endif - foreach lib_name : maya_qt_lib_names - maya_libs += cmplr.find_library(lib_name, dirs : qt_lib_dir) - endforeach +endforeach -endif +# --- Retrieve Core Libraries --- +core_libs = [] +core_lib_names = ['Foundation', 'OpenMaya', 'OpenMayaAnim', 'OpenMayaFX', 'OpenMayaRender', 'OpenMayaUI', 'clew'] +foreach lib_name : core_lib_names + core_libs += cmplr.find_library(lib_name, dirs : search_lib_dirs) +endforeach -maya_dep = declare_dependency( - dependencies : maya_libs, - include_directories : includes, +# ============================================================================== +# DEPENDENCY 1: Core Maya (For Python Modules / Standalone Apps) +# ============================================================================== +maya_core_dep = declare_dependency( + dependencies : core_libs, + include_directories : core_includes, variables : { - 'name_suffix' : maya_plugin_ext, 'maya_version' : maya_version, 'maya_bin_dir': maya_bin_dir, 'maya_inc_dir': maya_inc_dir, - 'qt_moc_path': qt_moc_path, 'maya_install_path': maya_install_path, - 'maya_using_devkit': maya_devkit_base != '', - }, + 'maya_using_devkit': has_devkit.to_string(), + } +) +meson.override_dependency('maya-core', maya_core_dep) + +# ============================================================================== +# DEPENDENCY 2: Maya Plugin (For MLL/SO/Bundle compilation) +# ============================================================================== +maya_plugin_dep = declare_dependency( + dependencies: [maya_core_dep], compile_args : maya_compile_args, link_args : maya_link_args, + variables : { + 'name_suffix' : maya_plugin_ext, + } ) +meson.override_dependency('maya', maya_plugin_dep) +meson.override_dependency('maya-plugin', maya_plugin_dep) + +# ============================================================================== +# OPTIONAL GRANULAR DEPENDENCIES +# ============================================================================== +build_numpy = false +build_alembic = false +build_imath = false +build_ufe = false +build_qt = false +build_opengl = false + +# --- Numpy --- +if not numpy_opt.disabled() + if not has_install + error('The numpy install comes from the maya install. No install path found') + endif + build_numpy = true + np_include_script = 'import numpy;print(numpy.get_include())' + np_include_path = run_command(mayapy_prg, '-c', np_include_script, check: false).stdout().strip() + + maya_numpy_dep = declare_dependency( + include_directories: np_include_path, + variables : { + 'mayapy_version': maya_py_ver, + } + ) + meson.override_dependency('maya-numpy', maya_numpy_dep) +endif + +# --- Alembic & Imath --- +check_alembic = not alembic_opt.disabled() +check_imath = not imath_opt.disabled() -meson.override_dependency('maya', maya_dep) +if check_alembic or check_imath + if (alembic_opt.enabled() or imath_opt.enabled()) and not has_devkit + error('Alembic and Imath dependencies require maya_devkit_base') + endif + + if has_devkit + alembic_base = maya_devkit_base / 'devkit' / 'Alembic' + + if fs.is_dir(alembic_base / 'include') + alembic_inc = include_directories(alembic_base / 'include') + alembic_lib_dir = alembic_base / 'lib' + + if check_alembic + maya_alembic_dep = declare_dependency( + dependencies: [cmplr.find_library('Alembic', dirs : alembic_lib_dir)], + include_directories: alembic_inc + ) + meson.override_dependency('maya-alembic', maya_alembic_dep) + build_alembic = true + endif + + if check_imath + imath_lib = cmplr.find_library('Imath-3_2-maya', dirs : alembic_lib_dir, required: false) + if not imath_lib.found() + imath_lib = cmplr.find_library('Imath', dirs : alembic_lib_dir) + endif + imath_inc = include_directories(alembic_base / 'include' / 'Imath') + maya_imath_dep = declare_dependency(dependencies: [imath_lib], include_directories: imath_inc) + meson.override_dependency('maya-imath', maya_imath_dep) + build_imath = true + endif + + else + if alembic_opt.enabled() or imath_opt.enabled() + zip_hint = run_command(py, '-c', find_zip_script, search_base, '*Alembic*.zip', check: false).stdout().strip() + error('Could not find Alembic/Imath headers at: ' + alembic_base + '\nYou probably need to unzip: ' + zip_hint) + endif + endif + endif +endif + +# --- UFE --- +if not ufe_opt.disabled() + if ufe_opt.enabled() and not has_devkit + error('maya_link_ufe requires maya_devkit_base') + endif + + if has_devkit + ufe_base = maya_devkit_base / 'devkit' / 'ufe' + if fs.is_dir(ufe_base / 'include') + ufe_lib = cmplr.find_library('ufe_7', dirs : ufe_base / 'lib', required: false) + if not ufe_lib.found() + ufe_lib = cmplr.find_library('ufe', dirs : ufe_base / 'lib') + endif + maya_ufe_dep = declare_dependency( + dependencies: [ufe_lib], + include_directories: include_directories(ufe_base / 'include') + ) + meson.override_dependency('maya-ufe', maya_ufe_dep) + build_ufe = true + elif ufe_opt.enabled() + zip_hint = run_command(py, '-c', find_zip_script, search_base, '*ufe*.zip', check: false).stdout().strip() + error('Could not find UFE headers at: ' + ufe_base + '\nYou probably need to unzip: ' + zip_hint) + endif + endif +endif + +# --- Qt --- +if not qt_opt.disabled() + search_paths = [ + maya_inc_dir, maya_inc_dir / 'Qt', maya_inc_dir / 'Qt' / 'include', + maya_install_path, maya_install_path / 'Qt', maya_install_path / 'Qt' / 'include', + ] + qt_include_dir = '' + foreach search_path: search_paths + if fs.is_dir(search_path / 'QtCore') + qt_include_dir = search_path + break + endif + endforeach + + if qt_include_dir != '' + mocdirs = maya_bin_dir != '' ? [maya_bin_dir] : [] + mocdirs += [maya_install_path / 'Qt' / 'bin', maya_install_path / 'Qt' / 'libexec', maya_install_path / 'devkit' / 'bin'] + moc_prg = find_program('moc', dirs : mocdirs, required : qt_opt.enabled()) + + if moc_prg.found() + meson.override_find_program('maya:moc', moc_prg) + maya_qt_lib_names = maya_version.version_compare('>=2025') ? ['Qt6Core', 'Qt6Gui', 'Qt6Widgets'] : ['Qt5Core', 'Qt5Gui', 'Qt5Widgets'] + if maya_qt_extra_includes != '' + maya_qt_lib_names += maya_qt_extra_includes.split(';') + endif + + qt_libs = [] + foreach lib_name : maya_qt_lib_names + qt_libs += cmplr.find_library(lib_name, dirs : search_lib_dirs) + endforeach + + maya_qt_dep = declare_dependency( + dependencies: qt_libs, + include_directories: include_directories(qt_include_dir), + ) + meson.override_dependency('maya-qt', maya_qt_dep) + build_qt = true + endif + elif qt_opt.enabled() + baselist = ';'.join(search_paths) + zip_hint = run_command(py, '-c', find_zip_script, baselist, 'qt_*include.zip', check: false).stdout().strip() + error('Could not find Maya QT headers. Unzip: ' + zip_hint) + endif +endif + +# --- OpenGL --- +if not gl_opt.disabled() + gl_dep = dependency('gl', method : 'system', required: gl_opt.enabled()) + if gl_dep.found() + glu_dep = os_name == 'windows' ? cmplr.find_library('glu32', required : gl_opt.enabled()) : dependency('glu', required: gl_opt.enabled()) + if glu_dep.found() + maya_opengl_dep = declare_dependency(dependencies: [gl_dep, glu_dep]) + meson.override_dependency('maya-opengl', maya_opengl_dep) + build_opengl = true + endif + endif +endif + +# ============================================================================== +# SUMMARY +# ============================================================================== +summary( + { + 'Maya Version': maya_version, + 'Found Install Path': has_install, + 'Using Devkit': has_devkit, + }, + section: 'Maya Subproject Configuration', + bool_yn: true +) + +summary( + { + 'maya-core': true, + 'maya-plugin': true, + 'maya-qt': build_qt, + 'maya-numpy': build_numpy, + 'maya-opengl': build_opengl, + 'maya-alembic': build_alembic, + 'maya-imath': build_imath, + 'maya-ufe': build_ufe, + }, + section: 'Exported Dependencies', + bool_yn: true +) diff --git a/subprojects/maya/meson.options b/subprojects/maya/meson.options index c28e3585..22b6a942 100644 --- a/subprojects/maya/meson.options +++ b/subprojects/maya/meson.options @@ -9,22 +9,71 @@ option( option( 'maya_devkit_base', type : 'string', + value : '', description : 'Optional path to the maya devkit', yield : true, ) option( - 'maya_link_qt', - type : 'boolean', - description : 'Whether to link to the Qt libraries that maya provides in their devkit/install', - value: false, + 'maya_custom_install_path', + type : 'string', + value : '', + description : 'Path to a non-standard maya install', yield : true, ) option( 'maya_qt_extra_includes', type : 'string', + value : '', description : 'Any qt headers other than QtCore, QtGui, or QtWidgets that you need to include, separated by semicolons', - value: '', + yield : true, +) + +option( + 'maya_link_qt', + type : 'feature', + value : 'auto', + description : 'Provide maya-qt dependency linking to Maya\'s Qt libraries', + yield : true, +) + +option( + 'maya_link_opengl', + type : 'feature', + value : 'auto', + description : 'Provide maya-opengl dependency linking to opengl and glu', + yield : true, +) + +option( + 'maya_link_numpy', + type : 'feature', + value : 'auto', + description : 'Provide maya-numpy dependency (Requires maya_install_path to exist)', + yield : true, +) + +option( + 'maya_link_alembic', + type : 'feature', + value : 'auto', + description : 'Provide maya-alembic dependency (Requires maya_devkit_base)', + yield : true, +) + +option( + 'maya_link_imath', + type : 'feature', + value : 'auto', + description : 'Provide maya-imath dependency (Requires maya_devkit_base)', + yield : true, +) + +option( + 'maya_link_ufe', + type : 'feature', + value : 'auto', + description : 'Provide maya-ufe dependency (Requires maya_devkit_base)', yield : true, ) From 82a8305b66061a9403852aa12769e93ae81a11f3 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 15 Sep 2026 16:42:20 -0700 Subject: [PATCH 10/47] Do the tick accessor and some typing --- .../simplexui/commands/alembicCommon.py | 75 ++++++------- src/python/simplexui/items/accessor.py | 100 +++++++++++------- src/python/simplexui/items/combo.py | 4 +- src/python/simplexui/items/progression.py | 4 +- src/python/simplexui/items/slider.py | 4 +- src/python/simplexui/items/traversal.py | 9 +- 6 files changed, 103 insertions(+), 93 deletions(-) diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 904c8abc..23775d24 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -65,6 +65,9 @@ from alembic.AbcGeom import _IBase # A helper typing-only class +npfloat = npt.NDArray[Union[np.float32, np.float64]] +npint = npt.NDArray[Union[np.int32, np.int64]] + T = TypeVar("T", bound=Union[IntArray, UnsignedIntArray, V2fArray, V3fArray]) @@ -186,7 +189,7 @@ def mk1dArray(aType: Type[T], iList: npt.NDArray) -> T: return array -def mkSampleVertexPoints(pts: npt.NDArray[Union[np.float64, np.float32]]) -> V3fArray: +def mkSampleVertexPoints(pts: npfloat) -> V3fArray: """Make an imath array of vertices Parameters @@ -202,7 +205,7 @@ def mkSampleVertexPoints(pts: npt.NDArray[Union[np.float64, np.float32]]) -> V3f return mkArray(V3fArray, pts) -def mkSampleIntArray(vals: npt.NDArray[Union[np.int64, np.int32]]) -> IntArray: +def mkSampleIntArray(vals: npint) -> IntArray: """Make an imath array of integers Parameters @@ -218,7 +221,7 @@ def mkSampleIntArray(vals: npt.NDArray[Union[np.int64, np.int32]]) -> IntArray: return mk1dArray(IntArray, vals) -def mkSampleUIntArray(vals: npt.NDArray[Union[np.int64, np.int32]]) -> UnsignedIntArray: +def mkSampleUIntArray(vals: npint) -> UnsignedIntArray: """Make an imath array of unsigned integers Parameters @@ -234,7 +237,7 @@ def mkSampleUIntArray(vals: npt.NDArray[Union[np.int64, np.int32]]) -> UnsignedI return mk1dArray(UnsignedIntArray, vals) -def mkSampleUvArray(uvs: npt.NDArray) -> V2fArray: +def mkSampleUvArray(uvs: npfloat) -> V2fArray: """Make an imath array of uvs Parameters @@ -255,9 +258,7 @@ def mkSampleUvArray(uvs: npt.NDArray) -> V2fArray: return array -def mkUvSample( - uvs: npt.NDArray, indexes: Optional[npt.NDArray] = None -) -> OV2fGeomParamSample: +def mkUvSample(uvs: npfloat, indexes: Optional[npint] = None) -> OV2fGeomParamSample: """Take an array, and make a poly mesh sample of the uvs Parameters @@ -280,7 +281,7 @@ def mkUvSample( def mkNormalSample( - norms: npt.NDArray, indexes: Optional[npt.NDArray] = None + norms: npfloat, indexes: Optional[npint] = None ) -> ON3fGeomParamSample: """Take an array, and make a poly mesh sample of the normals @@ -326,9 +327,7 @@ def setAlembicSample( omeshSch.set(s) -def getSampleArray( - imesh: IPolyMesh, pBar: Optional[QProgressDialog] = None -) -> npt.NDArray: +def getSampleArray(imesh: IPolyMesh, pBar: Optional[QProgressDialog] = None) -> npfloat: """Get the per-frame vertex positions for a mesh Parameters @@ -381,7 +380,7 @@ def getStaticMeshData(imesh: IPolyMesh) -> tuple[IntArray, IntArray]: return faces, counts -def getStaticMeshArrays(imesh: IPolyMesh) -> tuple[npt.NDArray, npt.NDArray]: +def getStaticMeshArrays(imesh: IPolyMesh) -> tuple[npint, npint]: """Get all the generally non-changing data for a mesh as numpy arrays Parameters @@ -433,7 +432,7 @@ def getUvSample(imesh: IPolyMesh) -> Optional[OV2fGeomParamSample]: return uv -def getUvArray(imesh: IPolyMesh) -> Optional[npt.NDArray]: +def getUvArray(imesh: IPolyMesh) -> Optional[npfloat]: """Get the uv positions for a mesh Parameters @@ -460,9 +459,7 @@ def getUvArray(imesh: IPolyMesh) -> Optional[npt.NDArray]: return uv -def getFlatUvFaces( - imesh: IPolyMesh, -) -> tuple[npt.NDArray, Optional[bool]]: +def getFlatUvFaces(imesh: IPolyMesh) -> tuple[Optional[npint], Optional[bool]]: """Get the UV structure for a mesh if it's indexed. If un-indexed, return None This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed and can handle the data appropriately for export without keeping track of index-ness @@ -496,7 +493,7 @@ def getFlatUvFaces( return idxs, indexed -def getUvFaces(imesh: IPolyMesh) -> Optional[npt.NDArray]: +def getUvFaces(imesh: IPolyMesh) -> Optional[list[list[int]]]: """Get the UV structure for a mesh if it's indexed. If un-indexed, return None This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed and can handle the data appropriately for export without keeping track of index-ness @@ -585,8 +582,7 @@ def findAlembicObject( def findAlembicObject( obj: IObject, abcType: Optional[Type[B]] = None, name: Optional[str] = None ) -> Optional[Union[IObject, B]]: - """ - Finds a single object in an alembic archive by name and/or type + """Finds a single object in an alembic archive by name and/or type If only type is specified, then the first object of that type encountered will be returned """ @@ -729,9 +725,7 @@ def readStringProperty(props: ICompoundProperty, key: str) -> str: return jsString -def flattenFaces( - faces: list[list[int]], -) -> tuple[npt.NDArray, npt.NDArray]: +def flattenFaces(faces: list[list[int]]) -> tuple[npint, npint]: """Take a nested list representation of faces and turn it into a flat face/count representation @@ -755,7 +749,7 @@ def flattenFaces( return np.array(faceCounts), np.array(faceIdxs) -def unflattenFaces(faces: npt.NDArray, counts: npt.NDArray) -> list[list[int]]: +def unflattenFaces(faces: npint, counts: npint) -> list[list[int]]: """Take a flat face/count representation of faces and turn it into a nested list representation @@ -780,13 +774,13 @@ def unflattenFaces(faces: npt.NDArray, counts: npt.NDArray) -> list[list[int]]: def buildAbc( outPath: str, - points: npt.NDArray, - faces: npt.NDArray, - faceCounts: Optional[npt.NDArray] = None, - uvs: Optional[npt.NDArray] = None, - uvFaces: Optional[npt.NDArray] = None, - normals: Optional[npt.NDArray] = None, - normFaces: Optional[npt.NDArray] = None, + points: npfloat, + faces: npint, + faceCounts: Optional[npint] = None, + uvs: Optional[npfloat] = None, + uvFaces: Optional[npint] = None, + normals: Optional[npfloat] = None, + normFaces: Optional[npint] = None, name: str = "polymsh", shapeSuffix: str = "Shape", transformSuffix: str = "", @@ -924,14 +918,7 @@ def getSmpxArchiveData(abcPath: str) -> tuple[IArchive, IPolyMesh, str]: def readSmpx( path: str, pBar: Optional[QProgressDialog] = None -) -> tuple[ - str, - npt.NDArray, - npt.NDArray, - npt.NDArray, - Optional[npt.NDArray], - Optional[npt.NDArray], -]: +) -> tuple[str, npint, npfloat, npint, Optional[npfloat], Optional[npint]]: """Read and return the raw alembic vertex/face data in the flat alembic style Parameters @@ -969,13 +956,13 @@ def readSmpx( def buildSmpx( outPath: str, - points: npt.NDArray, - faces: npt.NDArray, + points: npfloat, + faces: npint, jsString: str, name: str, - faceCounts: Optional[npt.NDArray] = None, - uvs: Optional[npt.NDArray] = None, - uvFaces: Optional[npt.NDArray] = None, + faceCounts: Optional[npint] = None, + uvs: Optional[npfloat] = None, + uvFaces: Optional[npint] = None, ogawa: bool = True, pBar: Optional[QProgressDialog] = None, ) -> None: @@ -1060,7 +1047,7 @@ def buildAlembicArchiveData( return arch, abcMesh -def readFalloffData(abcPath: str) -> dict[str, npt.NDArray]: +def readFalloffData(abcPath: str) -> dict[str, npfloat]: """Load the relevant data from a simplex alembic Parameters diff --git a/src/python/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py index e4eef9d9..3a0ae2f9 100644 --- a/src/python/simplexui/items/accessor.py +++ b/src/python/simplexui/items/accessor.py @@ -14,10 +14,17 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - +from __future__ import annotations import copy +from typing import TYPE_CHECKING, Optional, Any + +if TYPE_CHECKING: + from .simplex import Simplex + from .stack import Stack + + class SimplexAccessor(object): """The base object for all Simplex System object types This class provides access to the simplex system, draggability, @@ -31,55 +38,28 @@ class SimplexAccessor(object): """ - def __init__(self, simplex): - self.simplex = simplex - self._name = None + def __init__(self, simplex: Simplex): + self.simplex: Simplex = simplex + self._name: str = "" self._splitApplied = set() - self.dragStep = 0.05 - self.maxValue = 1.0 - self.minValue = 0.0 - self.tickable = False - self.value = 0.0 - - def valueTick(self, ticks, mul): - """Change the value of the current object by some number of ticks - with some given multiplier. This is the interface for the MMB drag - - Parameters - ---------- - ticks : int - The number of dragStep ticks to apply - mul : float - An overall multiplier - """ - try: - val = self.value - except AttributeError: - return - val += self.dragStep * ticks * mul - if abs(val) < 1.0e-5: - val = 0.0 - val = max(min(val, self.maxValue), self.minValue) - self.value = val - @property - def name(self): + def name(self) -> str: """ """ return self._name @name.setter - def name(self, val): + def name(self, val: str): """The name of the current object""" self._name = val @property - def models(self): + def models(self) -> list: """ """ return self.simplex.models @property - def falloffModels(self): + def falloffModels(self) -> list: """ """ return self.simplex.falloffModels @@ -89,7 +69,7 @@ def DCC(self): return self.simplex.DCC @property - def stack(self): + def stack(self) -> Stack: """ """ return self.simplex.stack @@ -110,7 +90,12 @@ def __deepcopy__(self, memo): setattr(result, k, copy.deepcopy(v, memo)) return result - def _buildLinkedRename(self, newName, maxDepth, currentLinks): + def _buildLinkedRename( + self, + newName: str, + maxDepth: int, + currentLinks: dict[type, dict[str, tuple[SimplexAccessor, int]]], + ): """Build the proposed set of renames specifically for this object This allows sub-classes to override the linked name behavior @@ -129,7 +114,14 @@ def _buildLinkedRename(self, newName, maxDepth, currentLinks): """ return currentLinks - def buildLinkedRename(self, newName, maxDepth=5, currentLinks=None): + def buildLinkedRename( + self, + newName: str, + maxDepth: int = 5, + currentLinks: Optional[ + dict[type, dict[str, tuple[SimplexAccessor, int]]] + ] = None, + ) -> dict[type, dict[str, tuple[SimplexAccessor, int]]]: """For the Shape, Slider, Combo, and Traversal items, build a linked rename dictionary like {itemType: {newName: (item, maxDepth)}} recursively up to a maximum given depth @@ -171,7 +163,7 @@ def buildLinkedRename(self, newName, maxDepth=5, currentLinks=None): # Error out if a name conflict is found. msg = "Linked rename produced a conflict: Trying to rename {0} {1} and {2} to {3}" msg = msg.format( - type(self), typeLinks[newName].name, self.name, newName + type(self), typeLinks[newName][0].name, self.name, newName ) raise ValueError(msg) elif tlPair[1] >= maxDepth: @@ -197,11 +189,11 @@ def treeParent(self): """ """ return None - def treeChildCount(self): + def treeChildCount(self) -> int: """ """ return 0 - def treeData(self, column): + def treeData(self, column) -> Any: """ """ if column == 0: return self.name @@ -213,3 +205,29 @@ def treeChecked(self): def icon(self): return None + + +class SimplexTickAccessor(SimplexAccessor): + value: float + + def __init__(self, simplex): + super().__init__(simplex) + self.dragStep: float = 0.05 + self.maxValue: float = 1.0 + self.minValue: float = 0.0 + + def valueTick(self, ticks: int, mul: float): + """Change the value of the current object by some number of ticks + with some given multiplier. This is the interface for the MMB drag + + Parameters + ---------- + ticks : int + The number of dragStep ticks to apply + mul : float + An overall multiplier + """ + val = self.value + (self.dragStep * ticks * mul) + val = 0.0 if abs(val) < 1e-5 else val + val = max(min(val, self.maxValue), self.minValue) + self.value = val diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index 4a040853..8343bb86 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -20,12 +20,12 @@ # pylint:disable=missing-docstring,unused-argument,no-self-use from Qt.QtGui import QColor from ..utils import getIcon, nested -from .accessor import SimplexAccessor +from .accessor import SimplexAccessor, SimplexTickAccessor from .stack import stackable # Abstract Items -class ComboPair(SimplexAccessor): +class ComboPair(SimplexTickAccessor): """A Slider/Value pair for use in Combos""" classDepth = 6 diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index 4b60f250..db0447f2 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -18,11 +18,11 @@ # pylint:disable=missing-docstring,unused-argument,no-self-use from ..utils import getNextName, nested -from .accessor import SimplexAccessor +from .accessor import SimplexAccessor, SimplexTickAccessor from .stack import stackable -class ProgPair(SimplexAccessor): +class ProgPair(SimplexTickAccessor): """ """ classDepth = 9 diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index f49bf626..72c7d9f1 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -21,14 +21,14 @@ from ..interface import undoContext from Qt.QtGui import QColor from ..utils import caseSplit, getNextName, makeUnique, nested, singleShot -from .accessor import SimplexAccessor +from .accessor import SimplexTickAccessor from .group import Group from .progression import ProgPair, Progression from .shape import Shape from .stack import stackable -class Slider(SimplexAccessor): +class Slider(SimplexTickAccessor): """A user-input to the simplex system that directly controls a Progression Parameters diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index 40a56069..adf6cc28 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -18,7 +18,7 @@ # pylint:disable=missing-docstring,unused-argument,no-self-use from Qt.QtGui import QColor from ..utils import nested -from .accessor import SimplexAccessor +from .accessor import SimplexAccessor, SimplexTickAccessor from .combo import Combo from .group import Group from .progression import Progression @@ -26,7 +26,7 @@ from .stack import stackable -class TravPair(SimplexAccessor): +class TravPair(SimplexTickAccessor): """ """ classDepth = 4 @@ -235,6 +235,11 @@ def name(self): """ """ return "START" if self.row == 0 else "END" + @name.setter + def name(self, val): + # Do nothing. This is just here to make the linter happy + return + def treeData(self, column): """ From 1f70e7bd965659cb0cb614609390d5632febb614 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 12 Aug 2026 12:07:42 -0700 Subject: [PATCH 11/47] Add the simplex hab distro --- src/python/simplexui/channelBox.py | 1716 ++--- src/python/simplexui/comboCheckDialog.py | 740 +- .../simplexui/commands/alembicCommon.py | 2164 +++--- .../simplexui/commands/applyCorrectives.py | 902 +-- .../simplexui/commands/correctiveInterface.py | 522 +- .../simplexui/commands/expandedExport.py | 872 +-- src/python/simplexui/commands/hdf5Convert.py | 120 +- .../commands/mayaCorrectiveInterface.py | 322 +- .../commands/reorderSimplexPoints.py | 204 +- src/python/simplexui/commands/rigidAlign.py | 198 +- src/python/simplexui/commands/smpxBlend.py | 1258 ++-- src/python/simplexui/commands/unsubdivide.py | 2454 +++---- src/python/simplexui/commands/uvTransfer.py | 1954 ++--- src/python/simplexui/dragFilter.py | 598 +- src/python/simplexui/falloffDialog.py | 980 +-- .../simplexui/interface/dummyInterface.py | 3170 ++++---- .../simplexui/interface/mayaInterface.py | 6542 ++++++++--------- src/python/simplexui/interfaceModel.py | 2106 +++--- src/python/simplexui/interfaceModelTrees.py | 882 +-- src/python/simplexui/items/accessor.py | 466 +- src/python/simplexui/items/combo.py | 1704 ++--- src/python/simplexui/items/falloff.py | 1556 ++-- src/python/simplexui/items/group.py | 594 +- src/python/simplexui/items/progression.py | 1486 ++-- src/python/simplexui/items/shape.py | 886 +-- src/python/simplexui/items/simplex.py | 3528 ++++----- src/python/simplexui/items/slider.py | 1476 ++-- src/python/simplexui/items/stack.py | 308 +- src/python/simplexui/items/traversal.py | 1942 ++--- .../simplexui/menu/mayaPlugins/freezeCombo.py | 490 +- src/python/simplexui/simplexDialog.py | 3086 ++++---- src/python/simplexui/travCheckDialog.py | 844 +-- src/python/simplexui/traversalDialog.py | 602 +- 33 files changed, 23336 insertions(+), 23336 deletions(-) diff --git a/src/python/simplexui/channelBox.py b/src/python/simplexui/channelBox.py index 5633c9ea..38c32790 100644 --- a/src/python/simplexui/channelBox.py +++ b/src/python/simplexui/channelBox.py @@ -1,858 +1,858 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -"""The ChannelBox -A Super-minimal ui for interacting with a Simplex System -Currently VERY WIP. Probably shouldn't have committed it to master, but whatever -""" - -# pylint:disable=unused-import,relative-import,missing-docstring,unused-argument,no-self-use -import os -import sys - -from .interfaceModel import Group, Simplex, SimplexModel, Slider -from Qt.QtCore import ( - QAbstractItemModel, - QEvent, - QModelIndex, - QObject, - QRectF, - Qt, - QTimer, - Signal, -) -from Qt.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QTextOption -from Qt.QtWidgets import QApplication, QListView, QStyledItemDelegate, QTreeView - - -class SlideFilter(QObject): - """A simplified drag filter, specialized for this purpose""" - - SLIDE_ENABLED = 0 - - slideTick = Signal(float, float, float) # AbsValue, OffsetValue, Multiplier - slidePressed = Signal() - slideReleased = Signal() - - def __init__(self, parent): - super(SlideFilter, self).__init__(parent) - - self.slideCursor = Qt.CursorShape.SizeHorCursor - self.slideButton = Qt.MouseButton.LeftButton - - self.fastModifier = Qt.KeyboardModifier.ControlModifier - self.slowModifier = Qt.KeyboardModifier.ShiftModifier - - self.fastMultiplier = 5.0 - self.slowDivisor = 5.0 - - # private vars - self._slideStart = True - self._overridden = False - self._pressed = True - self._prevValue = None - - def doOverrideCursor(self): - """Override the cursor""" - if self._overridden: - return - QApplication.setOverrideCursor(self.slideCursor) - self._overridden = True - - def restoreOverrideCursor(self): - """Restore the overridden cursor""" - if not self._overridden: - return - QApplication.restoreOverrideCursor() - self._overridden = False - - def eventFilter(self, obj, event): - """Event filter override - - Parameters - ---------- - obj : QObject - The object to get events for - event : QEvent - The event being filtered - - Returns - ------- - - """ - if hasattr(self, "SLIDE_ENABLED"): - if event.type() == QEvent.Type.MouseButtonPress: - if event.button() & self.slideButton: - self.startSlide(obj, event) - self.doSlide(obj, event) - self._slideStart = True - - elif event.type() == QEvent.Type.MouseMove: - if self._slideStart: - try: - self.doSlide(obj, event) - except Exception: - # fix the cursor if there's an error during slideging - self.restoreOverrideCursor() - raise # re-raise the exception - return True - - elif event.type() == QEvent.Type.MouseButtonRelease: - if event.button() & self.slideButton: - self._pressed = False - self._slideStart = False - self.myendSlide(obj, event) - return True - - return super(SlideFilter, self).eventFilter(obj, event) - - def startSlide(self, obj, event): - """Start the slide operation - - Parameters - ---------- - obj : QObject - The object to get events for - event : QEvent - The event being filtered - - Returns - ------- - - """ - self.slidePressed.emit() - self.doOverrideCursor() - - def doSlide(self, obj, event): - """Do a slide tick - - Parameters - ---------- - obj : QObject - The object to get events for - event : QEvent - The event being filtered - - Returns - ------- - - """ - width = obj.width() - click = event.pos() - perc = click.x() / float(width) - - mul = 1.0 - if event.modifiers() & self.fastModifier: - mul = self.fastMultiplier - elif event.modifiers() & self.slowModifier: - mul = 1.0 / self.slowDivisor - - if self._prevValue is None: - offset = 0.0 - else: - offset = perc - self._prevValue - self._prevValue = perc - - self.slideTick.emit(perc, offset, mul) - - def myendSlide(self, obj, event): - """End the slide operation - - Parameters - ---------- - obj : QObject - The object to get events for - event : QEvent - The event being filtered - - Returns - ------- - - """ - self.restoreOverrideCursor() - self._slideStart = None - self.slideReleased.emit() - - -class ChannelBoxDelegate(QStyledItemDelegate): - """Delegate to draw the slider items""" - - def __init__(self, parent=None): - super(ChannelBoxDelegate, self).__init__(parent) - self.store = {} - - def paint(self, painter, opt, index): - """Overridden paint function""" - item = index.model().itemFromIndex(index) - if isinstance(item, Slider): - self.paintSlider(self, item, painter, opt.rect, opt.palette) - else: - super(ChannelBoxDelegate, self).paint(painter, opt, index) - - def roundedPath(self, width, height, left=True, right=True): - """Get a path with rounded corners for drawing - - Parameters - ---------- - width : float - The width of the rectangle - height : float - The height of the rectangle - left : bool - Round the left side of the rectangle (Default value = True) - right : bool - Round the right side of the rectangle (Default value = True) - - Returns - ------- - QPainterPath - The requested path - - """ - key = (round(width, 2), round(height, 2), round(left, 2), round(right, 2)) - if key in self.store: - return self.store[key] - - # off = 0.5 - off = 1.0 - ew = height - off # ellipse width - eh = height - 2 * off # ellipse height - ts = 0.0 + off # topside - bs = height - off # bottomside - ls = 0.0 + off # left side - rs = width - off # righSide - lc = height + off # left corner - rc = width - height - off # left corner - - # If we're too narrow then flatten the points - if left and right: - if width < 2 * ew: - lc = width * 0.5 - rc = lc - ew = lc - else: - if left: - if width < ew: - lc = rs - ew = width - elif right: - if width < ew: - rc = ls - ew = width - - bgPath = QPainterPath() - if left: - bgPath.moveTo(lc, ts) - else: - bgPath.moveTo(ls, ts) - - if right: - bgPath.lineTo(rc, ts) - bgPath.arcTo(rc, ts, ew, eh, 90, -180) - else: - bgPath.lineTo(rs, ts) - bgPath.lineTo(rs, bs) - - if left: - bgPath.lineTo(lc, bs) - bgPath.arcTo(ls, ts, ew, eh, -90, -180) - else: - bgPath.lineTo(ls, bs) - bgPath.lineTo(ls, ts) - - bgPath.closeSubpath() - self.store[key] = bgPath - return bgPath - - def paintSlider(self, delegate, slider, painter, rect, palette): - """Paint a slider - - Parameters - ---------- - delegate : QStyledItemDelegate - The paint delegate - slider : Slider - The slider to paint - painter : QPainter - The painter to paint with - rect : QRectF - The rectangle to fill - palette : QPalette - The palette to use - - Returns - ------- - - """ - painter.save() - try: - painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) - - fgColor = slider.color - bgColor = QColor(slider.color) - bgColor.setAlpha(128) - - fgBrush = QBrush(fgColor) - bgBrush = QBrush(bgColor) - painter.setPen(QPen(palette.foreground().color())) - - rx = rect.x() - ry = rect.y() - rw = rect.width() - rh = rect.height() - - bgLeft = slider.minValue != 0.0 - bgPath = self.roundedPath(rw, rh, left=bgLeft) - bgPath = bgPath.translated(rx, ry) - painter.fillPath(bgPath, bgBrush) - if bgLeft: - # Double sided slider - perc = slider.value - right = perc >= 0.0 - fgPath = self.roundedPath( - abs(perc) * rw * 0.5, rh, left=not right, right=right - ) - if right: - fgPath = fgPath.translated(rx + rw * 0.5, ry) - else: - fgPath = fgPath.translated(rx + rw * 0.5 * (1 + perc), ry) - painter.fillPath(fgPath, fgBrush) - - else: - # Positive only slider - perc = slider.value - perc = max(min(perc, 1.0), 0.0) # clamp between 0 and 1 - fgPath = self.roundedPath(rw * perc, rh, left=False) - fgPath = fgPath.translated(rx, ry) - painter.fillPath(fgPath, fgBrush) - - opts = QTextOption(Qt.AlignmentFlag.AlignCenter) - frect = QRectF(rx, ry, rw, rh) - painter.drawText(frect, slider.name, opts) - # painter.drawPath(bgPath) - finally: - painter.restore() - - -class ChannelListModel(QAbstractItemModel): - """A model to handle a list of sliders - Many functions will be un-documented. They're just overrides - for the QAbstractItemModel. Look at the Qt docs if you really - want to know - - Parameters - ---------- - simplex : Simplex - The simplex system - parent : QObject - The parent of this model - """ - - def __init__(self, simplex, parent): - super(ChannelListModel, self).__init__(parent) - self.simplex = simplex - self.simplex.models.append(self) - self.channels = [] - - def setChannels(self, channels): - """Set the channels to display in this model - - Parameters - ---------- - channels : [object, ...] - A list of tree objects to show in Channel Box - - Returns - ------- - - """ - self.beginResetModel() - self.channels = channels - self.endResetModel() - - def index(self, row, column=0, parIndex=None): - if parIndex is None: - parIndex = QModelIndex() - - try: - item = self.channels[row] - except IndexError: - return QModelIndex() - return self.createIndex(row, column, item) - - def parent(self, index): - return QModelIndex() - - def rowCount(self, parent): - return len(self.channels) - - def columnCount(self, parent): - return 1 - - def data(self, index, role): - if not index.isValid(): - return None - item = index.internalPointer() - - if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): - if isinstance(item, (Group, Slider)): - return item.name - - elif role == Qt.ItemDataRole.TextAlignmentRole: - return Qt.AlignmentFlag.AlignCenter - - return None - - def flags(self, index): - return ( - Qt.ItemFlag.ItemIsEnabled - | Qt.ItemFlag.ItemIsEditable - | Qt.ItemFlag.ItemIsSelectable - ) - - def itemFromIndex(self, index): - return index.internalPointer() - - def indexFromItem(self, item): - try: - row = self.channels.index(item) - except ValueError: - return QModelIndex() - return self.index(row) - - def typeHandled(self, item): - if isinstance(item, Group): - return item.groupType == Slider - return isinstance(item, Slider) - - def itemDataChanged(self, item): - if self.typeHandled(item): - idx = self.indexFromItem(item) - if idx.isValid(): - self.dataChanged.emit(idx, idx) - - -class ChannelList(QListView): - """A list to display the chosen channels""" - - def __init__(self, parent=None): - super(ChannelList, self).__init__(parent) - self.slider = None - self._nxt = 0.0 - self.start = False - self.residual = 0.0 - - def slideStart(self): - """Handle user sliding values""" - p = self.mapFromGlobal(QCursor.pos()) - item = self.indexAt(p).internalPointer() - if isinstance(item, Slider): - self.slider = item - self.start = True - - def slideStop(self): - """End the user sliding values""" - self.slider = None - - def slideTick(self, val, offset, mul): - """Handle the ticks from the slider Filter""" - if self.slider is not None: - mx = self.slider.maxValue - mn = self.slider.minValue - - tick = 20.0 / mul - - if self.start or mul == 1.0: - self.start = False - val = (val * (mx - mn)) + mn - rn = round(val * tick) / tick - else: - # When working in relative mode, we keep track of the - # unused residual value and add it to the next tick. - # Because, unless each mouse move refresh is more than - # one full tick from the previous, we get no movement - val = offset * (mx - mn) - val = self.slider.value + (val * mul) - val += self.residual - rn = round(val * tick) / tick - self.residual = val - rn - - rn = min(max(rn, mn), mx) - # Do this to keep the ui snappy - self._nxt = rn - QTimer.singleShot(0, self.setval) - - def setval(self): - """Set the value of a slider""" - if self.slider is not None and self._nxt is not None: - self.slider.value = self._nxt - self._nxt = None - - -class ChannelTreeModel(SimplexModel): - """A model to handle a tree of sliders from a simplex system - Many functions will be un-documented. They're just overrides - for the QAbstractItemModel or the SimplexModel. - """ - - def getChildItem(self, parent, row): - try: - if isinstance(parent, Group): - return parent.items[row] - elif parent is None: - return self.simplex.sliderGroups[row] - except IndexError: - pass - return None - - def getItemRow(self, item): - row = None - if isinstance(item, Group): - row = item.simplex.sliderGroups.index(item) - elif isinstance(item, Slider): - row = item.group.items.index(item) - return row - - def getParentItem(self, item): - par = None - if isinstance(item, Slider): - par = item.group - return par - - def columnCount(self, parent): - return 1 - - def getItemRowCount(self, item): - if isinstance(item, Group): - return len(item.items) - elif item is None: - return len(self.simplex.sliderGroups) - return 0 - - def getItemAppendRow(self, item): - return self.getItemRowCount(item) - - def getItemData(self, item, column, role): - if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): - if column == 0: - if isinstance(item, Group): - return item.name - elif isinstance(item, Slider): - return item.name - # return "{0}: {1:.2f}".format(item.name, item.value) - print("BAD ICR", item, column, role) - return "BAD" - return None - - def typeHandled(self, item): - if isinstance(item, Group): - return item.groupType == Slider - return isinstance(item, Slider) - - -class ChannelTree(QTreeView): - """Display the channels in a Tree form""" - - def __init__(self, parent=None): - super(ChannelTree, self).__init__(parent) - self.slider = None - self._nxt = 0.0 - self.start = False - self.residual = 0.0 - - def slideStart(self): - """Handle starting a slide drag operation""" - p = self.mapFromGlobal(QCursor.pos()) - item = self.indexAt(p).internalPointer() - if isinstance(item, Slider): - self.slider = item - self.start = True - - def slideStop(self): - """Handle ending a slide drag operation""" - self.slider = None - - def slideTick(self, val, offset, mul): - """Handle the ticks from the SliderFilter""" - if self.slider is not None: - mx = self.slider.maxValue - mn = self.slider.minValue - - tick = 20.0 / mul - - if self.start or mul == 1.0: - self.start = False - val = (val * (mx - mn)) + mn - rn = round(val * tick) / tick - else: - # When working in relative mode, we keep track of the - # unused residual value and add it to the next tick. - # Because, unless each mouse move refresh is more than - # one full tick from the previous, we get no movement - val = offset * (mx - mn) - val = self.slider.value + (val * mul) - val += self.residual - rn = round(val * tick) / tick - self.residual = val - rn - - rn = min(max(rn, mn), mx) - # Do this to keep the ui snappy - self._nxt = rn - QTimer.singleShot(0, self.setval) - - def setval(self): - """Set the value of a slider""" - if self.slider is not None and self._nxt is not None: - self.slider.value = self._nxt - self._nxt = None - - -# DISPLAY TESTS -def testSliderListDisplay(smpxPath): - """ - - Parameters - ---------- - smpxPath : - - - Returns - ------- - - """ - simp = Simplex.buildSystemFromSmpx(smpxPath) - channels = [] - - redAttrs = { - "lowerLipDepressor_X", - "stretcher_X", - "platysmaFlex_X", - "cheekRaiser_X", - "jawOpen", - "lidTightener_X", - "outerBrowRaiser_X", - "eyesClosed_X", - "cornerPuller_X", - "noseWrinkler_X", - "lipsBlow_X", - "cornerDepressor_X", - "funneler", - "browLateral_X", - "innerBrowRaiser_X", - "upperLipRaiser_X", - "chinRaiser", - "cheek_SuckBlow_X", - "pucker", - "eyeGaze_DownUp_X", - "eyeGaze_RightLeft_X", - "upperLidTweak_X", - "lowerLidTweak_X", - } - greenAttrs = { - "nasolabialDeepener_X", - "neckStretcher_X", - "lipsPressed_T", - "lipsPressed_B", - "throatCompress", - "lipsRolled_InOut_B", - "lipsRolled_InOut_T", - "sharpCornerPuller_X", - "dimpler_X", - "eyeBlink_X", - "scalpSlide_BackFwd", - "browDown_X", - "mouthSwing_RightLeft", - "sternoFlex_X", - "throatOpen", - } - blueAttrs = { - "adamsApple", - "noseSwing_RightLeft", - "nostrilCompress_X", - "jawThrust_BackFwd", - "eyesWide_X", - "lipsVerticalT_X", - "lipsVerticalB_X", - "earPull_X", - "lipsTighten_T", - "lipsTighten_B", - "lipsCompress_T", - "lipsCompress_B", - "lipsShift_RightLeft_B", - "lipsShift_RightLeft_T", - "lipsNarrowT_X", - "lipsNarrowB_X", - "jawSwing_RightLeft", - "nostril_SuckFlare_X", - "lipsCorner_DownUp_X", - "jawClench", - } - greyAttrs = {"lipsTogether"} - - app = QApplication(sys.argv) - tv = ChannelList() - model = ChannelListModel(simp, None) - delegate = ChannelBoxDelegate() - - slideFilter = SlideFilter(tv.viewport()) - slideFilter.slideButton = Qt.MouseButton.LeftButton - tv.viewport().installEventFilter(slideFilter) - slideFilter.slidePressed.connect(tv.slideStart) - slideFilter.slideReleased.connect(tv.slideStop) - slideFilter.slideTick.connect(tv.slideTick) - - for g in simp.sliderGroups: - channels.append(g) - for item in g.items: - if item.name in redAttrs: - item.color = QColor(178, 103, 103) - elif item.name in greenAttrs: - item.color = QColor(90, 161, 27) - elif item.name in blueAttrs: - item.color = QColor(103, 141, 178) - elif item.name in greyAttrs: - item.color = QColor(130, 130, 130) - channels.append(item) - - model.setChannels(channels) - - tv.setModel(model) - tv.setItemDelegate(delegate) - - tv.show() - sys.exit(app.exec_()) - - -def testSliderTreeDisplay(smpxPath): - """ - - Parameters - ---------- - smpxPath : - - - Returns - ------- - - """ - simp = Simplex.buildSystemFromSmpx(smpxPath) - - _redAttrs = { - "lowerLipDepressor_X", - "stretcher_X", - "platysmaFlex_X", - "cheekRaiser_X", - "jawOpen", - "lidTightener_X", - "outerBrowRaiser_X", - "eyesClosed_X", - "cornerPuller_X", - "noseWrinkler_X", - "lipsBlow_X", - "cornerDepressor_X", - "funneler", - "browLateral_X", - "innerBrowRaiser_X", - "upperLipRaiser_X", - "chinRaiser", - "cheek_SuckBlow_X", - "pucker", - "eyeGaze_DownUp_X", - "eyeGaze_RightLeft_X", - "upperLidTweak_X", - "lowerLidTweak_X", - } - - _greenAttrs = { - "nasolabialDeepener_X", - "neckStretcher_X", - "lipsPressed_T", - "lipsPressed_B", - "throatCompress", - "lipsRolled_InOut_B", - "lipsRolled_InOut_T", - "sharpCornerPuller_X", - "dimpler_X", - "eyeBlink_X", - "scalpSlide_BackFwd", - "browDown_X", - "mouthSwing_RightLeft", - "sternoFlex_X", - "throatOpen", - } - - _blueAttrs = { - "adamsApple", - "noseSwing_RightLeft", - "nostrilCompress_X", - "jawThrust_BackFwd", - "eyesWide_X", - "lipsVerticalT_X", - "lipsVerticalB_X", - "earPull_X", - "lipsTighten_T", - "lipsTighten_B", - "lipsCompress_T", - "lipsCompress_B", - "lipsShift_RightLeft_B", - "lipsShift_RightLeft_T", - "lipsNarrowT_X", - "lipsNarrowB_X", - "jawSwing_RightLeft", - "nostril_SuckFlare_X", - "lipsCorner_DownUp_X", - "jawClench", - } - - _greyAttrs = {"lipsTogether"} - - app = QApplication(sys.argv) - # tv = ChannelTree() - tv = QTreeView() - model = ChannelTreeModel(simp, None) - # delegate = ChannelBoxDelegate() - - # slideFilter = SlideFilter(tv.viewport()) - # slideFilter.slideButton = Qt.MouseButton.LeftButton - # tv.viewport().installEventFilter(slideFilter) - # slideFilter.slidePressed.connect(tv.slideStart) - # slideFilter.slideReleased.connect(tv.slideStop) - # slideFilter.slideTick.connect(tv.slideTick) - - # for g in simp.sliderGroups: - # for item in g.items: - # if item.name in redAttrs: - # item.color = QColor(178, 103, 103) - # elif item.name in greenAttrs: - # item.color = QColor(90, 161, 27) - # elif item.name in blueAttrs: - # item.color = QColor(103, 141, 178) - # elif item.name in greyAttrs: - # item.color = QColor(130, 130, 130) - - tv.setModel(model) - # tv.setItemDelegate(delegate) - - tv.show() - sys.exit(app.exec_()) - - -if __name__ == "__main__": - basePath = r"D:\Users\tyler\Documents\GitHub\Simplex\Useful" - smpxPath = os.path.join(basePath, "HeadMaleStandard_High_Unsplit.smpx") - - testSliderTreeDisplay(smpxPath) +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +"""The ChannelBox +A Super-minimal ui for interacting with a Simplex System +Currently VERY WIP. Probably shouldn't have committed it to master, but whatever +""" + +# pylint:disable=unused-import,relative-import,missing-docstring,unused-argument,no-self-use +import os +import sys + +from .interfaceModel import Group, Simplex, SimplexModel, Slider +from Qt.QtCore import ( + QAbstractItemModel, + QEvent, + QModelIndex, + QObject, + QRectF, + Qt, + QTimer, + Signal, +) +from Qt.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QTextOption +from Qt.QtWidgets import QApplication, QListView, QStyledItemDelegate, QTreeView + + +class SlideFilter(QObject): + """A simplified drag filter, specialized for this purpose""" + + SLIDE_ENABLED = 0 + + slideTick = Signal(float, float, float) # AbsValue, OffsetValue, Multiplier + slidePressed = Signal() + slideReleased = Signal() + + def __init__(self, parent): + super(SlideFilter, self).__init__(parent) + + self.slideCursor = Qt.CursorShape.SizeHorCursor + self.slideButton = Qt.MouseButton.LeftButton + + self.fastModifier = Qt.KeyboardModifier.ControlModifier + self.slowModifier = Qt.KeyboardModifier.ShiftModifier + + self.fastMultiplier = 5.0 + self.slowDivisor = 5.0 + + # private vars + self._slideStart = True + self._overridden = False + self._pressed = True + self._prevValue = None + + def doOverrideCursor(self): + """Override the cursor""" + if self._overridden: + return + QApplication.setOverrideCursor(self.slideCursor) + self._overridden = True + + def restoreOverrideCursor(self): + """Restore the overridden cursor""" + if not self._overridden: + return + QApplication.restoreOverrideCursor() + self._overridden = False + + def eventFilter(self, obj, event): + """Event filter override + + Parameters + ---------- + obj : QObject + The object to get events for + event : QEvent + The event being filtered + + Returns + ------- + + """ + if hasattr(self, "SLIDE_ENABLED"): + if event.type() == QEvent.Type.MouseButtonPress: + if event.button() & self.slideButton: + self.startSlide(obj, event) + self.doSlide(obj, event) + self._slideStart = True + + elif event.type() == QEvent.Type.MouseMove: + if self._slideStart: + try: + self.doSlide(obj, event) + except Exception: + # fix the cursor if there's an error during slideging + self.restoreOverrideCursor() + raise # re-raise the exception + return True + + elif event.type() == QEvent.Type.MouseButtonRelease: + if event.button() & self.slideButton: + self._pressed = False + self._slideStart = False + self.myendSlide(obj, event) + return True + + return super(SlideFilter, self).eventFilter(obj, event) + + def startSlide(self, obj, event): + """Start the slide operation + + Parameters + ---------- + obj : QObject + The object to get events for + event : QEvent + The event being filtered + + Returns + ------- + + """ + self.slidePressed.emit() + self.doOverrideCursor() + + def doSlide(self, obj, event): + """Do a slide tick + + Parameters + ---------- + obj : QObject + The object to get events for + event : QEvent + The event being filtered + + Returns + ------- + + """ + width = obj.width() + click = event.pos() + perc = click.x() / float(width) + + mul = 1.0 + if event.modifiers() & self.fastModifier: + mul = self.fastMultiplier + elif event.modifiers() & self.slowModifier: + mul = 1.0 / self.slowDivisor + + if self._prevValue is None: + offset = 0.0 + else: + offset = perc - self._prevValue + self._prevValue = perc + + self.slideTick.emit(perc, offset, mul) + + def myendSlide(self, obj, event): + """End the slide operation + + Parameters + ---------- + obj : QObject + The object to get events for + event : QEvent + The event being filtered + + Returns + ------- + + """ + self.restoreOverrideCursor() + self._slideStart = None + self.slideReleased.emit() + + +class ChannelBoxDelegate(QStyledItemDelegate): + """Delegate to draw the slider items""" + + def __init__(self, parent=None): + super(ChannelBoxDelegate, self).__init__(parent) + self.store = {} + + def paint(self, painter, opt, index): + """Overridden paint function""" + item = index.model().itemFromIndex(index) + if isinstance(item, Slider): + self.paintSlider(self, item, painter, opt.rect, opt.palette) + else: + super(ChannelBoxDelegate, self).paint(painter, opt, index) + + def roundedPath(self, width, height, left=True, right=True): + """Get a path with rounded corners for drawing + + Parameters + ---------- + width : float + The width of the rectangle + height : float + The height of the rectangle + left : bool + Round the left side of the rectangle (Default value = True) + right : bool + Round the right side of the rectangle (Default value = True) + + Returns + ------- + QPainterPath + The requested path + + """ + key = (round(width, 2), round(height, 2), round(left, 2), round(right, 2)) + if key in self.store: + return self.store[key] + + # off = 0.5 + off = 1.0 + ew = height - off # ellipse width + eh = height - 2 * off # ellipse height + ts = 0.0 + off # topside + bs = height - off # bottomside + ls = 0.0 + off # left side + rs = width - off # righSide + lc = height + off # left corner + rc = width - height - off # left corner + + # If we're too narrow then flatten the points + if left and right: + if width < 2 * ew: + lc = width * 0.5 + rc = lc + ew = lc + else: + if left: + if width < ew: + lc = rs + ew = width + elif right: + if width < ew: + rc = ls + ew = width + + bgPath = QPainterPath() + if left: + bgPath.moveTo(lc, ts) + else: + bgPath.moveTo(ls, ts) + + if right: + bgPath.lineTo(rc, ts) + bgPath.arcTo(rc, ts, ew, eh, 90, -180) + else: + bgPath.lineTo(rs, ts) + bgPath.lineTo(rs, bs) + + if left: + bgPath.lineTo(lc, bs) + bgPath.arcTo(ls, ts, ew, eh, -90, -180) + else: + bgPath.lineTo(ls, bs) + bgPath.lineTo(ls, ts) + + bgPath.closeSubpath() + self.store[key] = bgPath + return bgPath + + def paintSlider(self, delegate, slider, painter, rect, palette): + """Paint a slider + + Parameters + ---------- + delegate : QStyledItemDelegate + The paint delegate + slider : Slider + The slider to paint + painter : QPainter + The painter to paint with + rect : QRectF + The rectangle to fill + palette : QPalette + The palette to use + + Returns + ------- + + """ + painter.save() + try: + painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) + + fgColor = slider.color + bgColor = QColor(slider.color) + bgColor.setAlpha(128) + + fgBrush = QBrush(fgColor) + bgBrush = QBrush(bgColor) + painter.setPen(QPen(palette.foreground().color())) + + rx = rect.x() + ry = rect.y() + rw = rect.width() + rh = rect.height() + + bgLeft = slider.minValue != 0.0 + bgPath = self.roundedPath(rw, rh, left=bgLeft) + bgPath = bgPath.translated(rx, ry) + painter.fillPath(bgPath, bgBrush) + if bgLeft: + # Double sided slider + perc = slider.value + right = perc >= 0.0 + fgPath = self.roundedPath( + abs(perc) * rw * 0.5, rh, left=not right, right=right + ) + if right: + fgPath = fgPath.translated(rx + rw * 0.5, ry) + else: + fgPath = fgPath.translated(rx + rw * 0.5 * (1 + perc), ry) + painter.fillPath(fgPath, fgBrush) + + else: + # Positive only slider + perc = slider.value + perc = max(min(perc, 1.0), 0.0) # clamp between 0 and 1 + fgPath = self.roundedPath(rw * perc, rh, left=False) + fgPath = fgPath.translated(rx, ry) + painter.fillPath(fgPath, fgBrush) + + opts = QTextOption(Qt.AlignmentFlag.AlignCenter) + frect = QRectF(rx, ry, rw, rh) + painter.drawText(frect, slider.name, opts) + # painter.drawPath(bgPath) + finally: + painter.restore() + + +class ChannelListModel(QAbstractItemModel): + """A model to handle a list of sliders + Many functions will be un-documented. They're just overrides + for the QAbstractItemModel. Look at the Qt docs if you really + want to know + + Parameters + ---------- + simplex : Simplex + The simplex system + parent : QObject + The parent of this model + """ + + def __init__(self, simplex, parent): + super(ChannelListModel, self).__init__(parent) + self.simplex = simplex + self.simplex.models.append(self) + self.channels = [] + + def setChannels(self, channels): + """Set the channels to display in this model + + Parameters + ---------- + channels : [object, ...] + A list of tree objects to show in Channel Box + + Returns + ------- + + """ + self.beginResetModel() + self.channels = channels + self.endResetModel() + + def index(self, row, column=0, parIndex=None): + if parIndex is None: + parIndex = QModelIndex() + + try: + item = self.channels[row] + except IndexError: + return QModelIndex() + return self.createIndex(row, column, item) + + def parent(self, index): + return QModelIndex() + + def rowCount(self, parent): + return len(self.channels) + + def columnCount(self, parent): + return 1 + + def data(self, index, role): + if not index.isValid(): + return None + item = index.internalPointer() + + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): + if isinstance(item, (Group, Slider)): + return item.name + + elif role == Qt.ItemDataRole.TextAlignmentRole: + return Qt.AlignmentFlag.AlignCenter + + return None + + def flags(self, index): + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsEditable + | Qt.ItemFlag.ItemIsSelectable + ) + + def itemFromIndex(self, index): + return index.internalPointer() + + def indexFromItem(self, item): + try: + row = self.channels.index(item) + except ValueError: + return QModelIndex() + return self.index(row) + + def typeHandled(self, item): + if isinstance(item, Group): + return item.groupType == Slider + return isinstance(item, Slider) + + def itemDataChanged(self, item): + if self.typeHandled(item): + idx = self.indexFromItem(item) + if idx.isValid(): + self.dataChanged.emit(idx, idx) + + +class ChannelList(QListView): + """A list to display the chosen channels""" + + def __init__(self, parent=None): + super(ChannelList, self).__init__(parent) + self.slider = None + self._nxt = 0.0 + self.start = False + self.residual = 0.0 + + def slideStart(self): + """Handle user sliding values""" + p = self.mapFromGlobal(QCursor.pos()) + item = self.indexAt(p).internalPointer() + if isinstance(item, Slider): + self.slider = item + self.start = True + + def slideStop(self): + """End the user sliding values""" + self.slider = None + + def slideTick(self, val, offset, mul): + """Handle the ticks from the slider Filter""" + if self.slider is not None: + mx = self.slider.maxValue + mn = self.slider.minValue + + tick = 20.0 / mul + + if self.start or mul == 1.0: + self.start = False + val = (val * (mx - mn)) + mn + rn = round(val * tick) / tick + else: + # When working in relative mode, we keep track of the + # unused residual value and add it to the next tick. + # Because, unless each mouse move refresh is more than + # one full tick from the previous, we get no movement + val = offset * (mx - mn) + val = self.slider.value + (val * mul) + val += self.residual + rn = round(val * tick) / tick + self.residual = val - rn + + rn = min(max(rn, mn), mx) + # Do this to keep the ui snappy + self._nxt = rn + QTimer.singleShot(0, self.setval) + + def setval(self): + """Set the value of a slider""" + if self.slider is not None and self._nxt is not None: + self.slider.value = self._nxt + self._nxt = None + + +class ChannelTreeModel(SimplexModel): + """A model to handle a tree of sliders from a simplex system + Many functions will be un-documented. They're just overrides + for the QAbstractItemModel or the SimplexModel. + """ + + def getChildItem(self, parent, row): + try: + if isinstance(parent, Group): + return parent.items[row] + elif parent is None: + return self.simplex.sliderGroups[row] + except IndexError: + pass + return None + + def getItemRow(self, item): + row = None + if isinstance(item, Group): + row = item.simplex.sliderGroups.index(item) + elif isinstance(item, Slider): + row = item.group.items.index(item) + return row + + def getParentItem(self, item): + par = None + if isinstance(item, Slider): + par = item.group + return par + + def columnCount(self, parent): + return 1 + + def getItemRowCount(self, item): + if isinstance(item, Group): + return len(item.items) + elif item is None: + return len(self.simplex.sliderGroups) + return 0 + + def getItemAppendRow(self, item): + return self.getItemRowCount(item) + + def getItemData(self, item, column, role): + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): + if column == 0: + if isinstance(item, Group): + return item.name + elif isinstance(item, Slider): + return item.name + # return "{0}: {1:.2f}".format(item.name, item.value) + print("BAD ICR", item, column, role) + return "BAD" + return None + + def typeHandled(self, item): + if isinstance(item, Group): + return item.groupType == Slider + return isinstance(item, Slider) + + +class ChannelTree(QTreeView): + """Display the channels in a Tree form""" + + def __init__(self, parent=None): + super(ChannelTree, self).__init__(parent) + self.slider = None + self._nxt = 0.0 + self.start = False + self.residual = 0.0 + + def slideStart(self): + """Handle starting a slide drag operation""" + p = self.mapFromGlobal(QCursor.pos()) + item = self.indexAt(p).internalPointer() + if isinstance(item, Slider): + self.slider = item + self.start = True + + def slideStop(self): + """Handle ending a slide drag operation""" + self.slider = None + + def slideTick(self, val, offset, mul): + """Handle the ticks from the SliderFilter""" + if self.slider is not None: + mx = self.slider.maxValue + mn = self.slider.minValue + + tick = 20.0 / mul + + if self.start or mul == 1.0: + self.start = False + val = (val * (mx - mn)) + mn + rn = round(val * tick) / tick + else: + # When working in relative mode, we keep track of the + # unused residual value and add it to the next tick. + # Because, unless each mouse move refresh is more than + # one full tick from the previous, we get no movement + val = offset * (mx - mn) + val = self.slider.value + (val * mul) + val += self.residual + rn = round(val * tick) / tick + self.residual = val - rn + + rn = min(max(rn, mn), mx) + # Do this to keep the ui snappy + self._nxt = rn + QTimer.singleShot(0, self.setval) + + def setval(self): + """Set the value of a slider""" + if self.slider is not None and self._nxt is not None: + self.slider.value = self._nxt + self._nxt = None + + +# DISPLAY TESTS +def testSliderListDisplay(smpxPath): + """ + + Parameters + ---------- + smpxPath : + + + Returns + ------- + + """ + simp = Simplex.buildSystemFromSmpx(smpxPath) + channels = [] + + redAttrs = { + "lowerLipDepressor_X", + "stretcher_X", + "platysmaFlex_X", + "cheekRaiser_X", + "jawOpen", + "lidTightener_X", + "outerBrowRaiser_X", + "eyesClosed_X", + "cornerPuller_X", + "noseWrinkler_X", + "lipsBlow_X", + "cornerDepressor_X", + "funneler", + "browLateral_X", + "innerBrowRaiser_X", + "upperLipRaiser_X", + "chinRaiser", + "cheek_SuckBlow_X", + "pucker", + "eyeGaze_DownUp_X", + "eyeGaze_RightLeft_X", + "upperLidTweak_X", + "lowerLidTweak_X", + } + greenAttrs = { + "nasolabialDeepener_X", + "neckStretcher_X", + "lipsPressed_T", + "lipsPressed_B", + "throatCompress", + "lipsRolled_InOut_B", + "lipsRolled_InOut_T", + "sharpCornerPuller_X", + "dimpler_X", + "eyeBlink_X", + "scalpSlide_BackFwd", + "browDown_X", + "mouthSwing_RightLeft", + "sternoFlex_X", + "throatOpen", + } + blueAttrs = { + "adamsApple", + "noseSwing_RightLeft", + "nostrilCompress_X", + "jawThrust_BackFwd", + "eyesWide_X", + "lipsVerticalT_X", + "lipsVerticalB_X", + "earPull_X", + "lipsTighten_T", + "lipsTighten_B", + "lipsCompress_T", + "lipsCompress_B", + "lipsShift_RightLeft_B", + "lipsShift_RightLeft_T", + "lipsNarrowT_X", + "lipsNarrowB_X", + "jawSwing_RightLeft", + "nostril_SuckFlare_X", + "lipsCorner_DownUp_X", + "jawClench", + } + greyAttrs = {"lipsTogether"} + + app = QApplication(sys.argv) + tv = ChannelList() + model = ChannelListModel(simp, None) + delegate = ChannelBoxDelegate() + + slideFilter = SlideFilter(tv.viewport()) + slideFilter.slideButton = Qt.MouseButton.LeftButton + tv.viewport().installEventFilter(slideFilter) + slideFilter.slidePressed.connect(tv.slideStart) + slideFilter.slideReleased.connect(tv.slideStop) + slideFilter.slideTick.connect(tv.slideTick) + + for g in simp.sliderGroups: + channels.append(g) + for item in g.items: + if item.name in redAttrs: + item.color = QColor(178, 103, 103) + elif item.name in greenAttrs: + item.color = QColor(90, 161, 27) + elif item.name in blueAttrs: + item.color = QColor(103, 141, 178) + elif item.name in greyAttrs: + item.color = QColor(130, 130, 130) + channels.append(item) + + model.setChannels(channels) + + tv.setModel(model) + tv.setItemDelegate(delegate) + + tv.show() + sys.exit(app.exec_()) + + +def testSliderTreeDisplay(smpxPath): + """ + + Parameters + ---------- + smpxPath : + + + Returns + ------- + + """ + simp = Simplex.buildSystemFromSmpx(smpxPath) + + _redAttrs = { + "lowerLipDepressor_X", + "stretcher_X", + "platysmaFlex_X", + "cheekRaiser_X", + "jawOpen", + "lidTightener_X", + "outerBrowRaiser_X", + "eyesClosed_X", + "cornerPuller_X", + "noseWrinkler_X", + "lipsBlow_X", + "cornerDepressor_X", + "funneler", + "browLateral_X", + "innerBrowRaiser_X", + "upperLipRaiser_X", + "chinRaiser", + "cheek_SuckBlow_X", + "pucker", + "eyeGaze_DownUp_X", + "eyeGaze_RightLeft_X", + "upperLidTweak_X", + "lowerLidTweak_X", + } + + _greenAttrs = { + "nasolabialDeepener_X", + "neckStretcher_X", + "lipsPressed_T", + "lipsPressed_B", + "throatCompress", + "lipsRolled_InOut_B", + "lipsRolled_InOut_T", + "sharpCornerPuller_X", + "dimpler_X", + "eyeBlink_X", + "scalpSlide_BackFwd", + "browDown_X", + "mouthSwing_RightLeft", + "sternoFlex_X", + "throatOpen", + } + + _blueAttrs = { + "adamsApple", + "noseSwing_RightLeft", + "nostrilCompress_X", + "jawThrust_BackFwd", + "eyesWide_X", + "lipsVerticalT_X", + "lipsVerticalB_X", + "earPull_X", + "lipsTighten_T", + "lipsTighten_B", + "lipsCompress_T", + "lipsCompress_B", + "lipsShift_RightLeft_B", + "lipsShift_RightLeft_T", + "lipsNarrowT_X", + "lipsNarrowB_X", + "jawSwing_RightLeft", + "nostril_SuckFlare_X", + "lipsCorner_DownUp_X", + "jawClench", + } + + _greyAttrs = {"lipsTogether"} + + app = QApplication(sys.argv) + # tv = ChannelTree() + tv = QTreeView() + model = ChannelTreeModel(simp, None) + # delegate = ChannelBoxDelegate() + + # slideFilter = SlideFilter(tv.viewport()) + # slideFilter.slideButton = Qt.MouseButton.LeftButton + # tv.viewport().installEventFilter(slideFilter) + # slideFilter.slidePressed.connect(tv.slideStart) + # slideFilter.slideReleased.connect(tv.slideStop) + # slideFilter.slideTick.connect(tv.slideTick) + + # for g in simp.sliderGroups: + # for item in g.items: + # if item.name in redAttrs: + # item.color = QColor(178, 103, 103) + # elif item.name in greenAttrs: + # item.color = QColor(90, 161, 27) + # elif item.name in blueAttrs: + # item.color = QColor(103, 141, 178) + # elif item.name in greyAttrs: + # item.color = QColor(130, 130, 130) + + tv.setModel(model) + # tv.setItemDelegate(delegate) + + tv.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + basePath = r"D:\Users\tyler\Documents\GitHub\Simplex\Useful" + smpxPath = os.path.join(basePath, "HeadMaleStandard_High_Unsplit.smpx") + + testSliderTreeDisplay(smpxPath) diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index d1e64747..9e2ec81a 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -1,370 +1,370 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -from itertools import combinations, product - -from .dragFilter import DragFilter -from .items import Combo, Slider -from Qt import QtCompat -from Qt.QtCore import Qt -from Qt.QtGui import QBrush, QColor -from Qt.QtWidgets import ( - QCheckBox, - QDialog, - QGroupBox, - QLabel, - QListWidget, - QListWidgetItem, - QPushButton, - QSpinBox, - QTreeWidget, - QTreeWidgetItem, - QWidget, -) - -from .utils import getUiFile - - -class TooManyPossibilitiesError(Exception): - """Error raised when there are too many possibilities - Basically used as a stop-iteration - """ - - pass - - -def buildPossibleCombos( - simplex, sliders, minDepth, maxDepth, lockDict=None, maxPoss=100 -): - """Build a list of possible combos - - Parameters - ---------- - simplex : Simplex - The simplex system to check - sliders : Slider - The sliders to check - minDepth : int - The minimum number of sliders that will go into any combo - maxDepth : int - The maximum number of sliders that will go into any combo - lockDict : {Slider: (float, ...), ...} - An optional per-slider dict of possible values - maxPoss : float - The Maximum number of possibilities to return.(Default value = 100) - - Returns - ------- - bool - True if the maximum number of possibilities was exceeded - [([(Slider, float), ...], Combo), ...] - Grouped slider/value pairs to existing (or None) Combos - - """ - # Get the range values for each slider - allRanges = {} - sliderDict = {} - lockDict = lockDict or {} - - for slider in sliders: - rng = set(lockDict.get(slider, slider.prog.getRange())) - rng.discard(0) # ignore the zeros - allRanges[slider] = sorted(rng) - sliderDict[slider.name] = slider - - poss = [] - tooMany = False - try: - for size in range(minDepth, maxDepth + 1): - for grp in combinations(sliders, size): - names = [i.name for i in grp] - ranges = [allRanges[s] for s in grp] - for vals in product(*ranges): - poss.append(frozenset(list(zip(names, vals)))) - if len(poss) > maxPoss: - raise TooManyPossibilitiesError("Don't melt your computer") - except TooManyPossibilitiesError: - tooMany = True - - # Get the "only" combo sets - onlys = {} - for combo in simplex.combos: - sls = [i.slider for i in combo.pairs] - if all(r in sliders for r in sls): - key = frozenset([(i.slider.name, i.value) for i in combo.pairs]) - onlys[key] = combo - - toAdd = [] - for p in poss: - truePairs = [(sliderDict[n], v) for n, v in p] - toAdd.append((truePairs, onlys.get(p))) - return tooMany, toAdd - - -class ComboCheckItem(QListWidgetItem): - def __init__(self, pairs, combo, *args, **kwargs): - super(ComboCheckItem, self).__init__(*args, **kwargs) - self.pairs = pairs - self.combo = combo - - if self.combo is None: - # We can create it!! - sliders, vals = list(zip(*self.pairs)) - newName = Combo.buildComboName(sliders, vals) - self.setText(newName) - else: - self.setText(self.combo.name) - self.setForeground(QBrush(QColor(128, 128, 128))) - - -class ComboCheckDialog(QDialog): - """Dialog for checking what possible combos exist, and picking new combos - - This dialog displays the available combos for a number of input sliders - - In 'Create' mode, it provides a quick way of choosing the one specific combo - that the user is looking for - - In 'Check' mode, it provides a convenient way to explore the possibilites - and create any missing combos directly - - Parameters - ---------- - sliders : [Slider, ...] - A list of sliders to check - values : {Slider: (float, ...), ...} - A dictionary of values to use per slider - mode : str - The mode to display the dialog. Defaults to 'create' - parent : QObject - The Parent of the dialog. Must be a SimplexDialog - - Returns - ------- - - """ - - uiHeaderWID: QWidget - uiLimitGRP: QGroupBox - uiMinLimitSPIN: QSpinBox - uiMaxLimitSPIN: QSpinBox - uiAutoUpdateCHK: QCheckBox - uiManualUpdateBTN: QPushButton - uiEditTREE: QTreeWidget - uiComboCheckLIST: QListWidget - uiWarningLBL: QLabel - uiCancelBTN: QPushButton - uiCreateSelectedBTN: QPushButton - - def __init__(self, sliders, values=None, mode="create", parent=None): - if parent is None: - raise ValueError("Parent must not be None") - super(ComboCheckDialog, self).__init__(parent) - - uiPath = getUiFile(__file__) - QtCompat.loadUi(uiPath, self) - self.mode = mode.lower() - - # Store the Parent UI rather than relying on Qt's .parent() - # Could cause crashes otherwise - self.parUI = parent - - self.uiCreateSelectedBTN.clicked.connect(self.createMissing) - self.uiMinLimitSPIN.valueChanged.connect(self.populateWithoutUpdate) - self.uiMaxLimitSPIN.valueChanged.connect(self.populateWithoutUpdate) - self.uiCancelBTN.clicked.connect(self.close) - self.uiManualUpdateBTN.clicked.connect(self.populateWithUpdate) - self.uiEditTREE.itemChanged.connect(self.populateWithoutUpdate) - - self.dragFilter = DragFilter(self) - self.uiEditTREE.viewport().installEventFilter(self.dragFilter) - self.dragFilter.dragTick.connect(self.dragTick) - - self.parUI.uiSliderTREE.selectionModel().selectionChanged.connect( - self.populateWithCheck - ) - - self.valueDict = values or {} - self.setSliders(sliders) - if self.mode == "create": - self.uiAutoUpdateCHK.setCheckState(Qt.CheckState.Unchecked) - self.uiAutoUpdateCHK.hide() - self.uiManualUpdateBTN.hide() - - self.uiMaxLimitSPIN.setValue(max(len(sliders), 2)) - self.uiMinLimitSPIN.setValue(max(len(sliders) - 2, 2)) - - if sliders is None: - self.populateWithUpdate() - else: - self._populate() - - def dragTick(self, ticks, mul): - """Deal with the ticks coming from the drag handler - - Parameters - ---------- - ticks : int - The number of ticks since the last update - mul : float - The multiplier value from the drag handler - - Returns - ------- - - """ - items = self.uiEditTREE.selectedItems() - for item in items: - val = item.data(3, Qt.ItemDataRole.EditRole) - val += (0.05) * ticks * mul - if abs(val) < 1.0e-5: - val = 0.0 - val = max(min(val, 1.0), -1.0) - item.setData(3, Qt.ItemDataRole.EditRole, val) - self.uiEditTREE.viewport().update() - - def setSliders(self, val): - """Set the sliders displayed in this UI - - Parameters - ---------- - val : [Slider, ...] - The sliders to be displayed - - Returns - ------- - - """ - self.uiEditTREE.clear() - dvs = [None, -1.0, 1.0, 0.5] - roles = [ - Qt.ItemDataRole.UserRole, - Qt.ItemDataRole.UserRole, - Qt.ItemDataRole.UserRole, - Qt.ItemDataRole.EditRole, - ] - val = val or [] - for slider in val: - item = QTreeWidgetItem(self.uiEditTREE, [slider.name]) - item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) - vvv = self.valueDict.get(slider, [-1.0, 1.0]) - mvs = [i for i in vvv if abs(i) != 1.0] - mvs = mvs[0] if mvs else 0.5 - - item.setData(0, Qt.ItemDataRole.UserRole, slider) - for col in range(1, 4): - val = mvs if col == 3 else dvs[col] - item.setData(col, roles[col], val) - rng = slider.prog.getRange() - if val in rng or col == 3: - chk = ( - Qt.CheckState.Checked if val in vvv else Qt.CheckState.Unchecked - ) - item.setCheckState(col, chk) - - for col in reversed(list(range(4))): - self.uiEditTREE.resizeColumnToContents(col) - - def closeEvent(self, event): - """Override the Qt close event""" - self.parUI.uiSliderTREE.selectionModel().selectionChanged.disconnect( - self.populateWithCheck - ) - super(ComboCheckDialog, self).closeEvent(event) - - def populateWithUpdate(self): - """Populate the list from the main dialog selection""" - self.setSliders(self.parUI.uiSliderTREE.getSelectedItems(typ=Slider)) - self._populate() - - def populateWithoutUpdate(self): - """Populate the list and but don't look at the main dialog""" - self._populate() - - def populateWithCheck(self): - """Populate the list from the main dialog selection, only if the AutoUpdate checkbox is checked""" - if self.uiAutoUpdateCHK.isChecked(): - self.setSliders(self.parUI.uiSliderTREE.getSelectedItems(typ=Slider)) - self._populate() - - def _populate(self): - """Populate the list widgets in the UI""" - minDepth = self.uiMinLimitSPIN.value() - maxDepth = self.uiMaxLimitSPIN.value() - maxPoss = 100 - - root = self.uiEditTREE.invisibleRootItem() - lockDict = {} - sliderList = [] - roles = [ - Qt.ItemDataRole.UserRole, - Qt.ItemDataRole.UserRole, - Qt.ItemDataRole.UserRole, - Qt.ItemDataRole.EditRole, - ] - for row in range(root.childCount()): - item = root.child(row) - slider = item.data(0, Qt.ItemDataRole.UserRole) - if slider is not None: - sliderList.append(slider) - lv = [ - item.data(col, roles[col]) - for col in range(1, 4) - if item.checkState(col) - ] - lockDict[slider] = lv - - tooMany, toAdd = buildPossibleCombos( - self.parUI.simplex, - sliderList, - minDepth, - maxDepth, - lockDict=lockDict, - maxPoss=maxPoss, - ) - - lbl = ( - "Too many possibilities. Limiting to {0}".format(maxPoss) if tooMany else "" - ) - self.uiWarningLBL.setText(lbl) - - self.uiComboCheckLIST.clear() - for pairs, combo in reversed(toAdd): - item = ComboCheckItem(pairs, combo) - self.uiComboCheckLIST.addItem(item) - - if self.mode == "create": - if self.uiComboCheckLIST.count() > 0: - self.uiComboCheckLIST.item(0).setSelected(True) - - def createMissing(self): - """Create selected combos if they don't already exist""" - simplex = self.parUI.simplex - created = [] - for item in self.uiComboCheckLIST.selectedItems(): - name = item.text() - sliders, vals = list(zip(*item.pairs)) - # Double check that the user didn't create any extra sliders - if Combo.comboAlreadyExists(simplex, sliders, vals) is None: - c = Combo.createCombo(name, simplex, sliders, vals) - created.append(c) - - self.parUI.uiComboTREE.setItemSelection(created) - if self.mode == "create": - self.close() - else: - self.populateWithoutUpdate() +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +from itertools import combinations, product + +from .dragFilter import DragFilter +from .items import Combo, Slider +from Qt import QtCompat +from Qt.QtCore import Qt +from Qt.QtGui import QBrush, QColor +from Qt.QtWidgets import ( + QCheckBox, + QDialog, + QGroupBox, + QLabel, + QListWidget, + QListWidgetItem, + QPushButton, + QSpinBox, + QTreeWidget, + QTreeWidgetItem, + QWidget, +) + +from .utils import getUiFile + + +class TooManyPossibilitiesError(Exception): + """Error raised when there are too many possibilities + Basically used as a stop-iteration + """ + + pass + + +def buildPossibleCombos( + simplex, sliders, minDepth, maxDepth, lockDict=None, maxPoss=100 +): + """Build a list of possible combos + + Parameters + ---------- + simplex : Simplex + The simplex system to check + sliders : Slider + The sliders to check + minDepth : int + The minimum number of sliders that will go into any combo + maxDepth : int + The maximum number of sliders that will go into any combo + lockDict : {Slider: (float, ...), ...} + An optional per-slider dict of possible values + maxPoss : float + The Maximum number of possibilities to return.(Default value = 100) + + Returns + ------- + bool + True if the maximum number of possibilities was exceeded + [([(Slider, float), ...], Combo), ...] + Grouped slider/value pairs to existing (or None) Combos + + """ + # Get the range values for each slider + allRanges = {} + sliderDict = {} + lockDict = lockDict or {} + + for slider in sliders: + rng = set(lockDict.get(slider, slider.prog.getRange())) + rng.discard(0) # ignore the zeros + allRanges[slider] = sorted(rng) + sliderDict[slider.name] = slider + + poss = [] + tooMany = False + try: + for size in range(minDepth, maxDepth + 1): + for grp in combinations(sliders, size): + names = [i.name for i in grp] + ranges = [allRanges[s] for s in grp] + for vals in product(*ranges): + poss.append(frozenset(list(zip(names, vals)))) + if len(poss) > maxPoss: + raise TooManyPossibilitiesError("Don't melt your computer") + except TooManyPossibilitiesError: + tooMany = True + + # Get the "only" combo sets + onlys = {} + for combo in simplex.combos: + sls = [i.slider for i in combo.pairs] + if all(r in sliders for r in sls): + key = frozenset([(i.slider.name, i.value) for i in combo.pairs]) + onlys[key] = combo + + toAdd = [] + for p in poss: + truePairs = [(sliderDict[n], v) for n, v in p] + toAdd.append((truePairs, onlys.get(p))) + return tooMany, toAdd + + +class ComboCheckItem(QListWidgetItem): + def __init__(self, pairs, combo, *args, **kwargs): + super(ComboCheckItem, self).__init__(*args, **kwargs) + self.pairs = pairs + self.combo = combo + + if self.combo is None: + # We can create it!! + sliders, vals = list(zip(*self.pairs)) + newName = Combo.buildComboName(sliders, vals) + self.setText(newName) + else: + self.setText(self.combo.name) + self.setForeground(QBrush(QColor(128, 128, 128))) + + +class ComboCheckDialog(QDialog): + """Dialog for checking what possible combos exist, and picking new combos + + This dialog displays the available combos for a number of input sliders + + In 'Create' mode, it provides a quick way of choosing the one specific combo + that the user is looking for + + In 'Check' mode, it provides a convenient way to explore the possibilites + and create any missing combos directly + + Parameters + ---------- + sliders : [Slider, ...] + A list of sliders to check + values : {Slider: (float, ...), ...} + A dictionary of values to use per slider + mode : str + The mode to display the dialog. Defaults to 'create' + parent : QObject + The Parent of the dialog. Must be a SimplexDialog + + Returns + ------- + + """ + + uiHeaderWID: QWidget + uiLimitGRP: QGroupBox + uiMinLimitSPIN: QSpinBox + uiMaxLimitSPIN: QSpinBox + uiAutoUpdateCHK: QCheckBox + uiManualUpdateBTN: QPushButton + uiEditTREE: QTreeWidget + uiComboCheckLIST: QListWidget + uiWarningLBL: QLabel + uiCancelBTN: QPushButton + uiCreateSelectedBTN: QPushButton + + def __init__(self, sliders, values=None, mode="create", parent=None): + if parent is None: + raise ValueError("Parent must not be None") + super(ComboCheckDialog, self).__init__(parent) + + uiPath = getUiFile(__file__) + QtCompat.loadUi(uiPath, self) + self.mode = mode.lower() + + # Store the Parent UI rather than relying on Qt's .parent() + # Could cause crashes otherwise + self.parUI = parent + + self.uiCreateSelectedBTN.clicked.connect(self.createMissing) + self.uiMinLimitSPIN.valueChanged.connect(self.populateWithoutUpdate) + self.uiMaxLimitSPIN.valueChanged.connect(self.populateWithoutUpdate) + self.uiCancelBTN.clicked.connect(self.close) + self.uiManualUpdateBTN.clicked.connect(self.populateWithUpdate) + self.uiEditTREE.itemChanged.connect(self.populateWithoutUpdate) + + self.dragFilter = DragFilter(self) + self.uiEditTREE.viewport().installEventFilter(self.dragFilter) + self.dragFilter.dragTick.connect(self.dragTick) + + self.parUI.uiSliderTREE.selectionModel().selectionChanged.connect( + self.populateWithCheck + ) + + self.valueDict = values or {} + self.setSliders(sliders) + if self.mode == "create": + self.uiAutoUpdateCHK.setCheckState(Qt.CheckState.Unchecked) + self.uiAutoUpdateCHK.hide() + self.uiManualUpdateBTN.hide() + + self.uiMaxLimitSPIN.setValue(max(len(sliders), 2)) + self.uiMinLimitSPIN.setValue(max(len(sliders) - 2, 2)) + + if sliders is None: + self.populateWithUpdate() + else: + self._populate() + + def dragTick(self, ticks, mul): + """Deal with the ticks coming from the drag handler + + Parameters + ---------- + ticks : int + The number of ticks since the last update + mul : float + The multiplier value from the drag handler + + Returns + ------- + + """ + items = self.uiEditTREE.selectedItems() + for item in items: + val = item.data(3, Qt.ItemDataRole.EditRole) + val += (0.05) * ticks * mul + if abs(val) < 1.0e-5: + val = 0.0 + val = max(min(val, 1.0), -1.0) + item.setData(3, Qt.ItemDataRole.EditRole, val) + self.uiEditTREE.viewport().update() + + def setSliders(self, val): + """Set the sliders displayed in this UI + + Parameters + ---------- + val : [Slider, ...] + The sliders to be displayed + + Returns + ------- + + """ + self.uiEditTREE.clear() + dvs = [None, -1.0, 1.0, 0.5] + roles = [ + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.EditRole, + ] + val = val or [] + for slider in val: + item = QTreeWidgetItem(self.uiEditTREE, [slider.name]) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) + vvv = self.valueDict.get(slider, [-1.0, 1.0]) + mvs = [i for i in vvv if abs(i) != 1.0] + mvs = mvs[0] if mvs else 0.5 + + item.setData(0, Qt.ItemDataRole.UserRole, slider) + for col in range(1, 4): + val = mvs if col == 3 else dvs[col] + item.setData(col, roles[col], val) + rng = slider.prog.getRange() + if val in rng or col == 3: + chk = ( + Qt.CheckState.Checked if val in vvv else Qt.CheckState.Unchecked + ) + item.setCheckState(col, chk) + + for col in reversed(list(range(4))): + self.uiEditTREE.resizeColumnToContents(col) + + def closeEvent(self, event): + """Override the Qt close event""" + self.parUI.uiSliderTREE.selectionModel().selectionChanged.disconnect( + self.populateWithCheck + ) + super(ComboCheckDialog, self).closeEvent(event) + + def populateWithUpdate(self): + """Populate the list from the main dialog selection""" + self.setSliders(self.parUI.uiSliderTREE.getSelectedItems(typ=Slider)) + self._populate() + + def populateWithoutUpdate(self): + """Populate the list and but don't look at the main dialog""" + self._populate() + + def populateWithCheck(self): + """Populate the list from the main dialog selection, only if the AutoUpdate checkbox is checked""" + if self.uiAutoUpdateCHK.isChecked(): + self.setSliders(self.parUI.uiSliderTREE.getSelectedItems(typ=Slider)) + self._populate() + + def _populate(self): + """Populate the list widgets in the UI""" + minDepth = self.uiMinLimitSPIN.value() + maxDepth = self.uiMaxLimitSPIN.value() + maxPoss = 100 + + root = self.uiEditTREE.invisibleRootItem() + lockDict = {} + sliderList = [] + roles = [ + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.EditRole, + ] + for row in range(root.childCount()): + item = root.child(row) + slider = item.data(0, Qt.ItemDataRole.UserRole) + if slider is not None: + sliderList.append(slider) + lv = [ + item.data(col, roles[col]) + for col in range(1, 4) + if item.checkState(col) + ] + lockDict[slider] = lv + + tooMany, toAdd = buildPossibleCombos( + self.parUI.simplex, + sliderList, + minDepth, + maxDepth, + lockDict=lockDict, + maxPoss=maxPoss, + ) + + lbl = ( + "Too many possibilities. Limiting to {0}".format(maxPoss) if tooMany else "" + ) + self.uiWarningLBL.setText(lbl) + + self.uiComboCheckLIST.clear() + for pairs, combo in reversed(toAdd): + item = ComboCheckItem(pairs, combo) + self.uiComboCheckLIST.addItem(item) + + if self.mode == "create": + if self.uiComboCheckLIST.count() > 0: + self.uiComboCheckLIST.item(0).setSelected(True) + + def createMissing(self): + """Create selected combos if they don't already exist""" + simplex = self.parUI.simplex + created = [] + for item in self.uiComboCheckLIST.selectedItems(): + name = item.text() + sliders, vals = list(zip(*item.pairs)) + # Double check that the user didn't create any extra sliders + if Combo.comboAlreadyExists(simplex, sliders, vals) is None: + c = Combo.createCombo(name, simplex, sliders, vals) + created.append(c) + + self.parUI.uiComboTREE.setItemSelection(created) + if self.mode == "create": + self.close() + else: + self.populateWithoutUpdate() diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 23775d24..a5758b7f 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -1,1082 +1,1082 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -"""Alembic files can be difficult to work with, and can be *very* slow in Python -This is a library of convenience functions with the numpy speed optimizations -""" - -import os - -from alembic.Abc import ( - IArchive, - OArchive, - OStringProperty, - IObject, - OCompoundProperty, - ICompoundProperty, -) -from alembic.AbcGeom import ( - GeometryScope, - IPolyMesh, - IXform, - ON3fGeomParamSample, - OPolyMesh, - OPolyMeshSchema, - OPolyMeshSchemaSample, - OV2fGeomParamSample, - OXform, -) -from imath import IntArray, UnsignedIntArray, V2f, V2fArray, V3fArray, Box3d -import numpy as np -import numpy.typing as npt - -try: - from imathnumpy import arrayToNumpy # pylint:disable=no-name-in-module -except ImportError: - arrayToNumpy = None - - -from typing import ( - TYPE_CHECKING, - Optional, - Union, - Type, - TypeVar, - overload, -) - - -if TYPE_CHECKING: - from Qt.QtWidgets import QProgressDialog - from alembic.AbcGeom import _IBase # A helper typing-only class - - -npfloat = npt.NDArray[Union[np.float32, np.float64]] -npint = npt.NDArray[Union[np.int32, np.int64]] - -T = TypeVar("T", bound=Union[IntArray, UnsignedIntArray, V2fArray, V3fArray]) - - -def pbPrint( - pBar: Optional[QProgressDialog], - message: Optional[str] = None, - val: Optional[int] = None, - maxVal: Optional[int] = None, - _pbPrintLastComma=None, -): - """A function that handles displaying messages in a QProgressDialog or printing to stdout - - Don't forget to call QApplication.processEvents() after using this function - - Parameters - ---------- - pBar : QProgressDialog or None - An optional progress bar - message : str or None - An optional message to display - val : int or None - An optional progress value to display - maxVal : int or None - An optional maximum value to display - _pbPrintLastComma: object - INTERNAL USE ONLY - """ - _pbPrintLastComma = [] if _pbPrintLastComma is None else _pbPrintLastComma - - if pBar is not None: - if val is not None: - pBar.setValue(val) - if message is not None: - pBar.setLabelText(message) - else: - if message is not None: - if val is not None: - if maxVal is not None: - print( - message, "{0: <4} of {1: <4}\r".format(val + 1, maxVal), end=" " - ) - else: - print(message, "{0: <4}\r".format(val + 1), end=" ") - # This is the ugliest, most terrible thing I think I've ever written - # Abusing the static default object to check if the last time - # this function was used, there was a trailing comma - # But damn if it doesn't make me laugh - if not _pbPrintLastComma: - _pbPrintLastComma.append("") - else: - if _pbPrintLastComma: - print(_pbPrintLastComma.pop()) - print(message) - - -def mkArray(aType: Type[T], iList: npt.NDArray) -> T: - """Makes the alembic-usable c++ typed 2-d arrays - - Parameters - ---------- - aType : imath type - The type of the output array - iList : list or np.array - The input iterable. - - Returns - ------- - : aType - The input list translated into an aType array - """ - if isinstance(iList, aType): - return iList - - size = len(iList) - array = aType(size) - if arrayToNumpy is None: - if isinstance(iList, np.ndarray): - for i in range(size): - array[i] = tuple(iList[i].tolist()) - else: - for i in range(size): - array[i] = tuple(iList[i]) - else: - nplist = np.array(iList) - memView = arrayToNumpy(array) - np.copyto(memView, nplist) - return array - - -def mk1dArray(aType: Type[T], iList: npt.NDArray) -> T: - """Makes the alembic-usable c++ typed 1-d arrays - - Parameters - ---------- - aType : imath type - The type of the output array - iList : list or np.array - The input iterable. - - Returns - ------- - : aType - The input list translated into an aType array - """ - if isinstance(iList, aType): - return iList - - array = aType(len(iList)) - if arrayToNumpy is None or aType is UnsignedIntArray: - for i in range(len(iList)): - # Gotta cast to int because an "int" from numpy has - # the type np.int32, which makes this conversion angry - array[i] = int(iList[i]) - return array - else: - nplist = np.array(iList) - memView = arrayToNumpy(array) - np.copyto(memView, nplist) - return array - - -def mkSampleVertexPoints(pts: npfloat) -> V3fArray: - """Make an imath array of vertices - - Parameters - ---------- - pts : list or np.array - The input points - - Returns - ------- - : V3fArray - The output list - """ - return mkArray(V3fArray, pts) - - -def mkSampleIntArray(vals: npint) -> IntArray: - """Make an imath array of integers - - Parameters - ---------- - pts : list or np.array - The input integers - - Returns - ------- - : IntArray - The output list - """ - return mk1dArray(IntArray, vals) - - -def mkSampleUIntArray(vals: npint) -> UnsignedIntArray: - """Make an imath array of unsigned integers - - Parameters - ---------- - pts : list or np.array - The input unsigned integers - - Returns - ------- - : UnsignedIntArray - The output list - """ - return mk1dArray(UnsignedIntArray, vals) - - -def mkSampleUvArray(uvs: npfloat) -> V2fArray: - """Make an imath array of uvs - - Parameters - ---------- - uvs : list or np.array - The input uvs - - Returns - ------- - : V2fArray - The output list - """ - array = V2fArray(len(uvs)) - setter = V2f(0, 0) - for i in range(len(uvs)): - setter.setValue(float(uvs[i][0]), float(uvs[i][1])) - array[i] = setter - return array - - -def mkUvSample(uvs: npfloat, indexes: Optional[npint] = None) -> OV2fGeomParamSample: - """Take an array, and make a poly mesh sample of the uvs - - Parameters - ---------- - uvs : list or np.array - The input uvs - indexes : list or np.array or None - The optional face indices of the uvs - - Returns - ------- - : OV2fGeomParamSample - The UV sample - """ - ary = mkSampleUvArray(uvs) - if indexes is None: - return OV2fGeomParamSample(ary, GeometryScope.kFacevaryingScope) - idxs = mkSampleUIntArray(indexes) - return OV2fGeomParamSample(ary, idxs, GeometryScope.kFacevaryingScope) - - -def mkNormalSample( - norms: npfloat, indexes: Optional[npint] = None -) -> ON3fGeomParamSample: - """Take an array, and make a poly mesh sample of the normals - - Parameters - ---------- - norms : list or np.array - The input normals - indexes : list or np.array or None - The optional face indices of the normals - - Returns - ------- - : ON3fGeomParamSample - The Normal sample - """ - ary = mkArray(V3fArray, norms) - if indexes is None: - return ON3fGeomParamSample(ary, GeometryScope.kFacevaryingScope) - idxs = mkSampleUIntArray(indexes) - return ON3fGeomParamSample(ary, idxs, GeometryScope.kFacevaryingScope) - - -def setAlembicSample( - omeshSch: OPolyMeshSchema, - points: V3fArray, - faceCount: IntArray, - faceIndex: IntArray, - bounds: Optional[Box3d] = None, - uvs: Optional[OV2fGeomParamSample] = None, - normals: Optional[ON3fGeomParamSample] = None, -): - """Set an alembic sample to the output mesh with the given properties""" - # Do it this way because the defaults for these arguments are some value other than None - kwargs = {} - if uvs is not None: - kwargs["iUVs"] = uvs - if normals is not None: - kwargs["iNormals"] = normals - - s = OPolyMeshSchemaSample(points, faceIndex, faceCount, **kwargs) - if bounds is not None: - omeshSch.getChildBoundsProperty().setValue(bounds) - omeshSch.set(s) - - -def getSampleArray(imesh: IPolyMesh, pBar: Optional[QProgressDialog] = None) -> npfloat: - """Get the per-frame vertex positions for a mesh - - Parameters - ---------- - imesh : IPolyMesh - The input alembic mesh object - - Returns - ------- - : np.array or list - The per-frame vertex positions - """ - meshSchema = imesh.getSchema() - posProp = meshSchema.getPositionsProperty() - numShapes = len(posProp.samples) - if arrayToNumpy is not None: - shapes = np.empty((len(posProp.samples), len(posProp.samples[0]), 3)) - for i, s in enumerate(posProp.samples): - pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) - shapes[i] = arrayToNumpy(s) - return shapes - else: - shapes = [] - for i, s in enumerate(posProp.samples): - pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) - shapes.append((list(s.x), list(s.y), list(s.z))) - shapes = np.array(shapes) - shapes = shapes.transpose((0, 2, 1)) - return shapes - - -def getStaticMeshData(imesh: IPolyMesh) -> tuple[IntArray, IntArray]: - """Get all the generally non-changing data for a mesh - - Parameters - ---------- - imesh : IPolyMesh - The input alembic mesh object - - Returns - ------- - : IntArray - A flat alembic array of vertex indices for the faces - : IntArray - The number of vertices per face - """ - sch = imesh.getSchema() - faces = sch.getFaceIndicesProperty().samples[0] - counts = sch.getFaceCountsProperty().samples[0] - return faces, counts - - -def getStaticMeshArrays(imesh: IPolyMesh) -> tuple[npint, npint]: - """Get all the generally non-changing data for a mesh as numpy arrays - - Parameters - ---------- - imesh : IPolyMesh - The input alembic mesh object - - Returns - ------- - : np.array or list - A flat of vertex indices for the faces as np.array if possible - : np.array or list - The number of vertices per face as np.array if possible - """ - faces, counts = getStaticMeshData(imesh) - if arrayToNumpy is not None: - faces = arrayToNumpy(faces).copy() - counts = arrayToNumpy(counts).copy() - else: - faces, counts = np.array(faces), np.array(counts) - return faces, counts - - -def getUvSample(imesh: IPolyMesh) -> Optional[OV2fGeomParamSample]: - """Get the UV's for a mesh - - Parameters - ---------- - imesh : IPolyMesh - The input alembic mesh object - - Returns - ------- - : OV2fGeomParamSample - The UV Sample - """ - imeshsch = imesh.getSchema() - uvParam = imeshsch.getUVsParam() - - if not uvParam.valid(): - return None - - uvValue = uvParam.getValueProperty().getValue() - if uvParam.isIndexed(): - idxValue = uvParam.getIndexProperty().getValue() - uv = OV2fGeomParamSample(uvValue, idxValue, GeometryScope.kFacevaryingScope) - else: - uv = OV2fGeomParamSample(uvValue, GeometryScope.kFacevaryingScope) - return uv - - -def getUvArray(imesh: IPolyMesh) -> Optional[npfloat]: - """Get the uv positions for a mesh - - Parameters - ---------- - imesh : IPolyMesh - The input alembic mesh object - - Returns - ------- - : list or np.array or None - The UVs if they exist as a numpy array if possible - """ - imeshsch = imesh.getSchema() - uvParam = imeshsch.getUVsParam() - if uvParam.valid(): - uvProp = uvParam.getValueProperty() - uvVals = uvProp.getValue() - # imathNumpy doesn't work on V2f arrays - # so I have to use one of the slow ways - uv = list(zip(uvVals.x, uvVals.y)) - uv = np.array(uv) - else: - uv = None - return uv - - -def getFlatUvFaces(imesh: IPolyMesh) -> tuple[Optional[npint], Optional[bool]]: - """Get the UV structure for a mesh if it's indexed. If un-indexed, return None - This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed - and can handle the data appropriately for export without keeping track of index-ness - - Parameters - ---------- - imesh : IPolyMesh - The input alembic mesh object - - Returns - ------- - : [int, ...] or np.array - The UVFace structure - : bool - Whether we share uvs between uvFaces (True), or we have a uv per face-vertex (False) - """ - sch = imesh.getSchema() - iuvs = sch.getUVsParam() - idxs = None - indexed = None - if iuvs.valid(): - if iuvs.isIndexed(): - indexed = True - idxs = iuvs.getIndexProperty().getValue() - idxs = np.array(idxs) - else: - indexed = False - rawCount = sum(list(sch.getFaceCountsProperty().samples[0])) - idxs = np.arange(rawCount) - - return idxs, indexed - - -def getUvFaces(imesh: IPolyMesh) -> Optional[list[list[int]]]: - """Get the UV structure for a mesh if it's indexed. If un-indexed, return None - This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed - and can handle the data appropriately for export without keeping track of index-ness - - Parameters - ---------- - imesh : IPolyMesh - The input alembic mesh object - - Returns - ------- - : [[int, ...], ...] - The UVFace structure - """ - sch = imesh.getSchema() - rawCounts = sch.getFaceCountsProperty().samples[0] - iuvs = sch.getUVsParam() - uvFaces = None - if iuvs.valid(): - uvFaces = [] - uvCounter = 0 - if iuvs.isIndexed(): - idxs = list(iuvs.getIndexProperty().getValue()) - for count in rawCounts: - uvFaces.append(list(idxs[uvCounter : uvCounter + count])) - uvCounter += count - return uvFaces - - -def getMeshFaces(imesh: IPolyMesh) -> list[list[int]]: - """Get The vertex indices used per face - - Parameters - ---------- - imesh : IPolyMesh - The input alembic mesh object - - Returns - ------- - : [[int, ...], ...] - The UVFace structure - """ - rawFaces, rawCounts = getStaticMeshData(imesh) - faces = [] - ptr = 0 - for count in rawCounts: - chunk = rawFaces[ptr : ptr + count] - faces.append(list(chunk)) - ptr += count - return faces - - -def getPointCount(imesh: IPolyMesh) -> int: - """Get the number of vertices in a mesh - - Parameters - ---------- - imesh : IPolyMesh - The input alembic mesh object - - Returns - ------- - : int - The number of vertices in the mesh - """ - meshSchema = imesh.getSchema() - posProp = meshSchema.getPositionsProperty() - return len(posProp.samples[0]) - - -B = TypeVar("B", bound=_IBase) - - -@overload -def findAlembicObject( - obj: IObject, abcType: None = None, name: Optional[str] = None -) -> Optional[IObject]: ... - - -@overload -def findAlembicObject( - obj: IObject, abcType: Type[B], name: Optional[str] = None -) -> Optional[B]: ... - - -def findAlembicObject( - obj: IObject, abcType: Optional[Type[B]] = None, name: Optional[str] = None -) -> Optional[Union[IObject, B]]: - """Finds a single object in an alembic archive by name and/or type - If only type is specified, then the first object of that type - encountered will be returned - """ - md = obj.getMetaData() - if abcType is None: - if name is None or obj.getName() == name: - return obj - elif abcType.matches(md): - if name is None or obj.getName() == name: - return abcType(obj.getParent(), obj.getName()) - for child in obj.children: - out = findAlembicObject(child, abcType, name) - if out is not None: - return out - return None - - -def findAllAlembicObjects( - obj: IObject, - abcType: Optional[Type[_IBase]] = None, - out: Optional[list[IObject]] = None, -) -> list[IObject]: - """Finds all objects of a type in an alembic archive""" - md = obj.getMetaData() - out = [] if out is None else out - if abcType is None: - out.append(obj) - elif abcType.matches(md): - out.append(abcType(obj.getParent(), obj.getName())) - for child in obj.children: - findAllAlembicObjects(child, abcType, out) - return out - - -def getTypedIObject(obj: IObject) -> Optional[_IBase]: - from alembic.AbcGeom import ( - ICamera, - ICurves, - ILight, - INuPatch, - IPoints, - IPolyMesh, - ISubD, - IXform, - ) - - md = obj.getMetaData() - for abcType in ( - IXform, - IPolyMesh, - ICamera, - ICurves, - ILight, - INuPatch, - IPoints, - ISubD, - ): - if abcType.matches(md): - return abcType(obj.getParent(), obj.getName()) - return None - - -def getMesh(infile: str) -> Optional[IPolyMesh]: - """Get the first found mesh object from the alembic filepath""" - iarch = IArchive(str(infile)) - ipolymsh = findAlembicObject(iarch.getTop(), abcType=IPolyMesh) - return ipolymsh - - -def writeStringProperty( - props: OCompoundProperty, key: str, value: str, ogawa: bool = True -) -> None: - """Write the definition string to an alembic OObject - - HDF5 (which we must still support) has a character limit - to string properties. Splitting the string must be handled - in a uniform way, so this function must be used - - Parameters - ---------- - props : OCompoundProperty - The alembic OObject properties - value : str - The simplex definition string - ogawa : bool - If the output is ogawa - - """ - if len(value) > 65000 and not ogawa: - value = str(value) - numChunks = (len(value) // 65000) + 1 - chunkSize = (len(value) // numChunks) + 1 - for c in range(numChunks): - prop = OStringProperty(props, "{0}{1}".format(key, c)) - prop.setValue(value[chunkSize * c : chunkSize * (c + 1)]) - else: - prop = OStringProperty(props, str(key)) - prop.setValue(str(value)) - - -def readStringProperty(props: ICompoundProperty, key: str) -> str: - """Read the definition string from an alembic OObject - - HDF5 (which we must still support) has a character limit - to string properties. Splitting the string must be handled - in a uniform way, so this function must be used - - Parameters - ---------- - props : ICompoundProperty - The alembic IObject properties - - Returns - ------- - : str - The simplex definition string - """ - if not props.valid(): - raise ValueError(".smpx file is missing the alembic user properties") - - try: - prop = props.getProperty(key) - except KeyError: - parts = [] - for c in range(10): - try: - prop = props.getProperty("{0}{1}".format(key, c)) - except KeyError: - if c == 0: - raise - break - else: - parts.append(prop.getValue()) - else: - raise ValueError("That is a HELL of a long simplex definition") - jsString = "".join(parts) - else: - jsString = prop.getValue() - - return jsString - - -def flattenFaces(faces: list[list[int]]) -> tuple[npint, npint]: - """Take a nested list representation of faces - and turn it into a flat face/count representation - - Parameters - ---------- - faces : [[int, ...], ...] - The nested list representation - - Returns - ------- - : np.array or list - The flat list of face connectivity - : np.array or list - The flat list of vertices per face - - """ - faceCounts, faceIdxs = [], [] - for f in faces: - faceCounts.append(len(f)) - faceIdxs.extend(f) - return np.array(faceCounts), np.array(faceIdxs) - - -def unflattenFaces(faces: npint, counts: npint) -> list[list[int]]: - """Take a flat face/count representation of faces - and turn it into a nested list representation - - Parameters - ---------- - faces : np.array - The flat list of face connectivity - counts : np.array - The flat list of vertices per face - - Returns - ------- - : [[int, ...], ...] - The nested list representation - """ - out, ptr = [], 0 - for c in counts: - out.append(faces[ptr : ptr + c].tolist()) - ptr += c - return out - - -def buildAbc( - outPath: str, - points: npfloat, - faces: npint, - faceCounts: Optional[npint] = None, - uvs: Optional[npfloat] = None, - uvFaces: Optional[npint] = None, - normals: Optional[npfloat] = None, - normFaces: Optional[npint] = None, - name: str = "polymsh", - shapeSuffix: str = "Shape", - transformSuffix: str = "", - propDict: Optional[dict[str, str]] = None, - ogawa: bool = True, - pBar: Optional[QProgressDialog] = None, -): - """ - Build a single-mesh alembic file from all of the non-alembic raw data - - Parameters - ---------- - outPath: str - The output path for the alembic file - points: list or ndarray - The list or array of points. Single multiple frames supported - faces: list - A list of lists of face indices, or a flattened list of indices. - If flat, then faceCounts must be provided - faceCounts: list - A list of the number of vertices per face. Defaults to None - uvs: list or ndarray - The Uvs for this mesh. Defaults to None - uvFaces: list - A list of lists of face indices, or a flattened list of indices. - If flat, then faceCounts must be provided. Defaults to None - normals: list or ndarray - The Normals for this mesh. Defaults to None - normFaces: list - A list of lists of face indices, or a flattened list of indices. - If flat, then faceCounts must be provided. Defaults to None - name: str - The name to give this mesh. Defaults to "polymsh" - shapeSuffix: str - The suffix to add to the shape of this mesh. Defaults to "Shape" - transformSuffix: str - The suffix to add to the transform of this mesh. Defaults to "" - propDict: dict - A dictionary of properties to add to the xform object - ogawa : bool - Whether to write to the Ogawa (True) or HDF5 (False) backend - pBar : QProgressDialog, optional - An optional progress dialog - """ - if faceCounts is None: - # All the faces are in list-of-list format - # put them in index-count format - faceCounts, faces = flattenFaces(faces) - if uvFaces is not None: - _, uvFaces = flattenFaces(uvFaces) - if normFaces is not None: - _, normFaces = flattenFaces(normFaces) - - faceCounts = mkSampleIntArray(faceCounts) - faces = mkSampleIntArray(faces) - - if not isinstance(uvs, OV2fGeomParamSample): - if uvFaces is not None and uvs is not None: - uvs = mkUvSample(uvs, indexes=uvFaces) - - if not isinstance(normals, ON3fGeomParamSample): - if normFaces is not None and normals is not None: - normals = mkNormalSample(normals, indexes=normFaces) - - oarch = OArchive(str(outPath), ogawa) - parent, opar, props, omesh, sch = None, None, None, None, None - try: - parent = oarch.getTop() - opar = OXform(parent, str(name + transformSuffix)) - if propDict: - props = opar.getSchema().getUserProperties() - for k, v in propDict.items(): - writeStringProperty(props, str(k), str(v), ogawa=ogawa) - - omesh = OPolyMesh(opar, str(name + shapeSuffix)) - - points = np.array(points) - if len(points.shape) == 2: - points = points[None, ...] - - sch = omesh.getSchema() - for i, frame in enumerate(points): - pbPrint(pBar, message="Exporting Shape", val=i, maxVal=len(points)) - abcFrame = mkSampleVertexPoints(frame) - setAlembicSample(sch, abcFrame, faceCounts, faces, uvs=uvs, normals=normals) - - pbPrint(pBar, message="Done Exporting") - finally: - # Make sure all this gets deleted so the file is freed - del parent, opar, props, omesh, sch - - -# Simplex format specific stuff -def getSmpxArchiveData(abcPath: str) -> tuple[IArchive, IPolyMesh, str]: - """Read and return the low level relevant data from a simplex alembic - - Parameters - ---------- - abcPath : str - The path to the .smpx file - - Returns - ------- - : IArchive - An opened Alembic IArchive object handle - : IPolyMesh - An Alembic Mesh handle - : str - The json definition string - """ - if not os.path.isfile(str(abcPath)): - raise IOError("File does not exist: " + str(abcPath)) - iarch = IArchive(str(abcPath)) # because alembic hates unicode - top, par, abcMesh = [None] * 3 - try: - top = iarch.getTop() - par = top.children[0] - par = IXform(top, par.getName()) - abcMesh = par.children[0] - abcMesh = IPolyMesh(par, abcMesh.getName()) - # I *could* come up with a generic property reader - # but it's useless for me at this time - sch = par.getSchema() - props = sch.getUserProperties() - jsString = readStringProperty(props, "simplex") - - except Exception: # pylint: disable=broad-except - # ensure that the .smpx file is released - iarch, top, par, abcMesh = [None] * 4 - raise - - # Must return the archive, otherwise it gets GC'd - return iarch, abcMesh, jsString - - -def readSmpx( - path: str, pBar: Optional[QProgressDialog] = None -) -> tuple[str, npint, npfloat, npint, Optional[npfloat], Optional[npint]]: - """Read and return the raw alembic vertex/face data in the flat alembic style - - Parameters - ---------- - abcPath : str - The path to the .smpx file - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - : str - The simplex definition string - : [int, ...] or np.array - The number of vertices per face - : [[(float*3), ...], ...] or np.array - The vertex positions per shape - : [int, ...] or np.array - The flat indexes per face - : [(float*2), ...] or np.array or None - The UV's - : [int, ...] or np.array - The flat indexes per uv-face - """ - iarch, abcMesh, jsString = getSmpxArchiveData(path) - try: - faces, counts = getStaticMeshArrays(abcMesh) - verts = getSampleArray(abcMesh, pBar=pBar) - uvs = getUvArray(abcMesh) - uvFaces, _ = getFlatUvFaces(abcMesh) - finally: - del iarch, abcMesh - return jsString, counts, verts, faces, uvs, uvFaces - - -def buildSmpx( - outPath: str, - points: npfloat, - faces: npint, - jsString: str, - name: str, - faceCounts: Optional[npint] = None, - uvs: Optional[npfloat] = None, - uvFaces: Optional[npint] = None, - ogawa: bool = True, - pBar: Optional[QProgressDialog] = None, -) -> None: - """ - Build a simplex output from raw data - - Parameters - ---------- - outPath: str - The output path for the alembic file - points: list or ndarray - The list or array of points. Single multiple frames supported - faces: list - A list of lists of face indices, or a flattened list of indices. - If flat, then faceCounts must be provided - jsString : str - The simplex definition string - name: str - The name to give this mesh - faceCounts: list - A list of the number of vertices per face. Defaults to None - uvs: list or ndarray - The Uvs for this mesh. Defaults to None - uvFaces: list - A list of lists of face indices, or a flattened list of indices. - If flat, then faceCounts must be provided. Defaults to None - ogawa : bool - Whether to write to the Ogawa (True) or HDF5 (False) backend - pBar : QProgressDialog, optional - An optional progress dialog - """ - buildAbc( - outPath, - points, - faces, - faceCounts=faceCounts, - uvs=uvs, - uvFaces=uvFaces, - name=name, - shapeSuffix="", - transformSuffix="", - propDict={"simplex": jsString}, - ogawa=ogawa, - pBar=pBar, - ) - - -def buildAlembicArchiveData( - path: str, name: str, jsString: str, ogawa: bool -) -> tuple[Optional[OArchive], Optional[OPolyMesh]]: - """Set up an output alembic archive with a mesh ready for writing - - Parameters - ---------- - path : str - The output file path - name : str - The name of the system - jsString : str - The simplex definition string - ogawa : bool - Whether to open in Ogawa (True) or HDF5 (False) mode - - Returns - ------- - : OArchive - The opened alembic output archive - : OPolyMesh - The mesh to write the shape data to - - """ - arch = OArchive(str(path), ogawa) - par, props, abcMesh = [None] * 3 - try: - par = OXform(arch.getTop(), str(name)) - props = par.getSchema().getUserProperties() - writeStringProperty(props, "simplex", jsString, ogawa=ogawa) - abcMesh = OPolyMesh(par, str(name)) - except Exception: - arch, par, props, abcMesh = [None] * 4 - raise - return arch, abcMesh - - -def readFalloffData(abcPath: str) -> dict[str, npfloat]: - """Load the relevant data from a simplex alembic - - Parameters - ---------- - abcPath : str - Path to the .smpx file - - """ - if not os.path.isfile(str(abcPath)): - raise IOError("File does not exist: " + str(abcPath)) - iarch = IArchive(str(abcPath)) # because alembic hates unicode - top, par, systemSchema, foPropPar, foProp = [None] * 5 - try: - top = iarch.getTop() - par = top.children[0] - par = IXform(top, par.getName()) - systemSchema = par.getSchema() - props = systemSchema.getUserProperties() - foDict = {} - try: - foPropPar = props.getProperty("falloffs") - except KeyError: - pass - else: - nps = foPropPar.getNumProperties() - for i in range(nps): - foProp = foPropPar.getProperty(i) - foDict[foProp.getName()] = np.array(foProp.getValue()) - finally: - iarch, top, par, systemSchema, foPropPar, foProp = [None] * 6 - - return foDict +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +"""Alembic files can be difficult to work with, and can be *very* slow in Python +This is a library of convenience functions with the numpy speed optimizations +""" + +import os + +from alembic.Abc import ( + IArchive, + OArchive, + OStringProperty, + IObject, + OCompoundProperty, + ICompoundProperty, +) +from alembic.AbcGeom import ( + GeometryScope, + IPolyMesh, + IXform, + ON3fGeomParamSample, + OPolyMesh, + OPolyMeshSchema, + OPolyMeshSchemaSample, + OV2fGeomParamSample, + OXform, +) +from imath import IntArray, UnsignedIntArray, V2f, V2fArray, V3fArray, Box3d +import numpy as np +import numpy.typing as npt + +try: + from imathnumpy import arrayToNumpy # pylint:disable=no-name-in-module +except ImportError: + arrayToNumpy = None + + +from typing import ( + TYPE_CHECKING, + Optional, + Union, + Type, + TypeVar, + overload, +) + + +if TYPE_CHECKING: + from Qt.QtWidgets import QProgressDialog + from alembic.AbcGeom import _IBase # A helper typing-only class + + +npfloat = npt.NDArray[Union[np.float32, np.float64]] +npint = npt.NDArray[Union[np.int32, np.int64]] + +T = TypeVar("T", bound=Union[IntArray, UnsignedIntArray, V2fArray, V3fArray]) + + +def pbPrint( + pBar: Optional[QProgressDialog], + message: Optional[str] = None, + val: Optional[int] = None, + maxVal: Optional[int] = None, + _pbPrintLastComma=None, +): + """A function that handles displaying messages in a QProgressDialog or printing to stdout + + Don't forget to call QApplication.processEvents() after using this function + + Parameters + ---------- + pBar : QProgressDialog or None + An optional progress bar + message : str or None + An optional message to display + val : int or None + An optional progress value to display + maxVal : int or None + An optional maximum value to display + _pbPrintLastComma: object + INTERNAL USE ONLY + """ + _pbPrintLastComma = [] if _pbPrintLastComma is None else _pbPrintLastComma + + if pBar is not None: + if val is not None: + pBar.setValue(val) + if message is not None: + pBar.setLabelText(message) + else: + if message is not None: + if val is not None: + if maxVal is not None: + print( + message, "{0: <4} of {1: <4}\r".format(val + 1, maxVal), end=" " + ) + else: + print(message, "{0: <4}\r".format(val + 1), end=" ") + # This is the ugliest, most terrible thing I think I've ever written + # Abusing the static default object to check if the last time + # this function was used, there was a trailing comma + # But damn if it doesn't make me laugh + if not _pbPrintLastComma: + _pbPrintLastComma.append("") + else: + if _pbPrintLastComma: + print(_pbPrintLastComma.pop()) + print(message) + + +def mkArray(aType: Type[T], iList: npt.NDArray) -> T: + """Makes the alembic-usable c++ typed 2-d arrays + + Parameters + ---------- + aType : imath type + The type of the output array + iList : list or np.array + The input iterable. + + Returns + ------- + : aType + The input list translated into an aType array + """ + if isinstance(iList, aType): + return iList + + size = len(iList) + array = aType(size) + if arrayToNumpy is None: + if isinstance(iList, np.ndarray): + for i in range(size): + array[i] = tuple(iList[i].tolist()) + else: + for i in range(size): + array[i] = tuple(iList[i]) + else: + nplist = np.array(iList) + memView = arrayToNumpy(array) + np.copyto(memView, nplist) + return array + + +def mk1dArray(aType: Type[T], iList: npt.NDArray) -> T: + """Makes the alembic-usable c++ typed 1-d arrays + + Parameters + ---------- + aType : imath type + The type of the output array + iList : list or np.array + The input iterable. + + Returns + ------- + : aType + The input list translated into an aType array + """ + if isinstance(iList, aType): + return iList + + array = aType(len(iList)) + if arrayToNumpy is None or aType is UnsignedIntArray: + for i in range(len(iList)): + # Gotta cast to int because an "int" from numpy has + # the type np.int32, which makes this conversion angry + array[i] = int(iList[i]) + return array + else: + nplist = np.array(iList) + memView = arrayToNumpy(array) + np.copyto(memView, nplist) + return array + + +def mkSampleVertexPoints(pts: npfloat) -> V3fArray: + """Make an imath array of vertices + + Parameters + ---------- + pts : list or np.array + The input points + + Returns + ------- + : V3fArray + The output list + """ + return mkArray(V3fArray, pts) + + +def mkSampleIntArray(vals: npint) -> IntArray: + """Make an imath array of integers + + Parameters + ---------- + pts : list or np.array + The input integers + + Returns + ------- + : IntArray + The output list + """ + return mk1dArray(IntArray, vals) + + +def mkSampleUIntArray(vals: npint) -> UnsignedIntArray: + """Make an imath array of unsigned integers + + Parameters + ---------- + pts : list or np.array + The input unsigned integers + + Returns + ------- + : UnsignedIntArray + The output list + """ + return mk1dArray(UnsignedIntArray, vals) + + +def mkSampleUvArray(uvs: npfloat) -> V2fArray: + """Make an imath array of uvs + + Parameters + ---------- + uvs : list or np.array + The input uvs + + Returns + ------- + : V2fArray + The output list + """ + array = V2fArray(len(uvs)) + setter = V2f(0, 0) + for i in range(len(uvs)): + setter.setValue(float(uvs[i][0]), float(uvs[i][1])) + array[i] = setter + return array + + +def mkUvSample(uvs: npfloat, indexes: Optional[npint] = None) -> OV2fGeomParamSample: + """Take an array, and make a poly mesh sample of the uvs + + Parameters + ---------- + uvs : list or np.array + The input uvs + indexes : list or np.array or None + The optional face indices of the uvs + + Returns + ------- + : OV2fGeomParamSample + The UV sample + """ + ary = mkSampleUvArray(uvs) + if indexes is None: + return OV2fGeomParamSample(ary, GeometryScope.kFacevaryingScope) + idxs = mkSampleUIntArray(indexes) + return OV2fGeomParamSample(ary, idxs, GeometryScope.kFacevaryingScope) + + +def mkNormalSample( + norms: npfloat, indexes: Optional[npint] = None +) -> ON3fGeomParamSample: + """Take an array, and make a poly mesh sample of the normals + + Parameters + ---------- + norms : list or np.array + The input normals + indexes : list or np.array or None + The optional face indices of the normals + + Returns + ------- + : ON3fGeomParamSample + The Normal sample + """ + ary = mkArray(V3fArray, norms) + if indexes is None: + return ON3fGeomParamSample(ary, GeometryScope.kFacevaryingScope) + idxs = mkSampleUIntArray(indexes) + return ON3fGeomParamSample(ary, idxs, GeometryScope.kFacevaryingScope) + + +def setAlembicSample( + omeshSch: OPolyMeshSchema, + points: V3fArray, + faceCount: IntArray, + faceIndex: IntArray, + bounds: Optional[Box3d] = None, + uvs: Optional[OV2fGeomParamSample] = None, + normals: Optional[ON3fGeomParamSample] = None, +): + """Set an alembic sample to the output mesh with the given properties""" + # Do it this way because the defaults for these arguments are some value other than None + kwargs = {} + if uvs is not None: + kwargs["iUVs"] = uvs + if normals is not None: + kwargs["iNormals"] = normals + + s = OPolyMeshSchemaSample(points, faceIndex, faceCount, **kwargs) + if bounds is not None: + omeshSch.getChildBoundsProperty().setValue(bounds) + omeshSch.set(s) + + +def getSampleArray(imesh: IPolyMesh, pBar: Optional[QProgressDialog] = None) -> npfloat: + """Get the per-frame vertex positions for a mesh + + Parameters + ---------- + imesh : IPolyMesh + The input alembic mesh object + + Returns + ------- + : np.array or list + The per-frame vertex positions + """ + meshSchema = imesh.getSchema() + posProp = meshSchema.getPositionsProperty() + numShapes = len(posProp.samples) + if arrayToNumpy is not None: + shapes = np.empty((len(posProp.samples), len(posProp.samples[0]), 3)) + for i, s in enumerate(posProp.samples): + pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) + shapes[i] = arrayToNumpy(s) + return shapes + else: + shapes = [] + for i, s in enumerate(posProp.samples): + pbPrint(pBar, message="Reading Shape", val=i, maxVal=numShapes) + shapes.append((list(s.x), list(s.y), list(s.z))) + shapes = np.array(shapes) + shapes = shapes.transpose((0, 2, 1)) + return shapes + + +def getStaticMeshData(imesh: IPolyMesh) -> tuple[IntArray, IntArray]: + """Get all the generally non-changing data for a mesh + + Parameters + ---------- + imesh : IPolyMesh + The input alembic mesh object + + Returns + ------- + : IntArray + A flat alembic array of vertex indices for the faces + : IntArray + The number of vertices per face + """ + sch = imesh.getSchema() + faces = sch.getFaceIndicesProperty().samples[0] + counts = sch.getFaceCountsProperty().samples[0] + return faces, counts + + +def getStaticMeshArrays(imesh: IPolyMesh) -> tuple[npint, npint]: + """Get all the generally non-changing data for a mesh as numpy arrays + + Parameters + ---------- + imesh : IPolyMesh + The input alembic mesh object + + Returns + ------- + : np.array or list + A flat of vertex indices for the faces as np.array if possible + : np.array or list + The number of vertices per face as np.array if possible + """ + faces, counts = getStaticMeshData(imesh) + if arrayToNumpy is not None: + faces = arrayToNumpy(faces).copy() + counts = arrayToNumpy(counts).copy() + else: + faces, counts = np.array(faces), np.array(counts) + return faces, counts + + +def getUvSample(imesh: IPolyMesh) -> Optional[OV2fGeomParamSample]: + """Get the UV's for a mesh + + Parameters + ---------- + imesh : IPolyMesh + The input alembic mesh object + + Returns + ------- + : OV2fGeomParamSample + The UV Sample + """ + imeshsch = imesh.getSchema() + uvParam = imeshsch.getUVsParam() + + if not uvParam.valid(): + return None + + uvValue = uvParam.getValueProperty().getValue() + if uvParam.isIndexed(): + idxValue = uvParam.getIndexProperty().getValue() + uv = OV2fGeomParamSample(uvValue, idxValue, GeometryScope.kFacevaryingScope) + else: + uv = OV2fGeomParamSample(uvValue, GeometryScope.kFacevaryingScope) + return uv + + +def getUvArray(imesh: IPolyMesh) -> Optional[npfloat]: + """Get the uv positions for a mesh + + Parameters + ---------- + imesh : IPolyMesh + The input alembic mesh object + + Returns + ------- + : list or np.array or None + The UVs if they exist as a numpy array if possible + """ + imeshsch = imesh.getSchema() + uvParam = imeshsch.getUVsParam() + if uvParam.valid(): + uvProp = uvParam.getValueProperty() + uvVals = uvProp.getValue() + # imathNumpy doesn't work on V2f arrays + # so I have to use one of the slow ways + uv = list(zip(uvVals.x, uvVals.y)) + uv = np.array(uv) + else: + uv = None + return uv + + +def getFlatUvFaces(imesh: IPolyMesh) -> tuple[Optional[npint], Optional[bool]]: + """Get the UV structure for a mesh if it's indexed. If un-indexed, return None + This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed + and can handle the data appropriately for export without keeping track of index-ness + + Parameters + ---------- + imesh : IPolyMesh + The input alembic mesh object + + Returns + ------- + : [int, ...] or np.array + The UVFace structure + : bool + Whether we share uvs between uvFaces (True), or we have a uv per face-vertex (False) + """ + sch = imesh.getSchema() + iuvs = sch.getUVsParam() + idxs = None + indexed = None + if iuvs.valid(): + if iuvs.isIndexed(): + indexed = True + idxs = iuvs.getIndexProperty().getValue() + idxs = np.array(idxs) + else: + indexed = False + rawCount = sum(list(sch.getFaceCountsProperty().samples[0])) + idxs = np.arange(rawCount) + + return idxs, indexed + + +def getUvFaces(imesh: IPolyMesh) -> Optional[list[list[int]]]: + """Get the UV structure for a mesh if it's indexed. If un-indexed, return None + This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed + and can handle the data appropriately for export without keeping track of index-ness + + Parameters + ---------- + imesh : IPolyMesh + The input alembic mesh object + + Returns + ------- + : [[int, ...], ...] + The UVFace structure + """ + sch = imesh.getSchema() + rawCounts = sch.getFaceCountsProperty().samples[0] + iuvs = sch.getUVsParam() + uvFaces = None + if iuvs.valid(): + uvFaces = [] + uvCounter = 0 + if iuvs.isIndexed(): + idxs = list(iuvs.getIndexProperty().getValue()) + for count in rawCounts: + uvFaces.append(list(idxs[uvCounter : uvCounter + count])) + uvCounter += count + return uvFaces + + +def getMeshFaces(imesh: IPolyMesh) -> list[list[int]]: + """Get The vertex indices used per face + + Parameters + ---------- + imesh : IPolyMesh + The input alembic mesh object + + Returns + ------- + : [[int, ...], ...] + The UVFace structure + """ + rawFaces, rawCounts = getStaticMeshData(imesh) + faces = [] + ptr = 0 + for count in rawCounts: + chunk = rawFaces[ptr : ptr + count] + faces.append(list(chunk)) + ptr += count + return faces + + +def getPointCount(imesh: IPolyMesh) -> int: + """Get the number of vertices in a mesh + + Parameters + ---------- + imesh : IPolyMesh + The input alembic mesh object + + Returns + ------- + : int + The number of vertices in the mesh + """ + meshSchema = imesh.getSchema() + posProp = meshSchema.getPositionsProperty() + return len(posProp.samples[0]) + + +B = TypeVar("B", bound=_IBase) + + +@overload +def findAlembicObject( + obj: IObject, abcType: None = None, name: Optional[str] = None +) -> Optional[IObject]: ... + + +@overload +def findAlembicObject( + obj: IObject, abcType: Type[B], name: Optional[str] = None +) -> Optional[B]: ... + + +def findAlembicObject( + obj: IObject, abcType: Optional[Type[B]] = None, name: Optional[str] = None +) -> Optional[Union[IObject, B]]: + """Finds a single object in an alembic archive by name and/or type + If only type is specified, then the first object of that type + encountered will be returned + """ + md = obj.getMetaData() + if abcType is None: + if name is None or obj.getName() == name: + return obj + elif abcType.matches(md): + if name is None or obj.getName() == name: + return abcType(obj.getParent(), obj.getName()) + for child in obj.children: + out = findAlembicObject(child, abcType, name) + if out is not None: + return out + return None + + +def findAllAlembicObjects( + obj: IObject, + abcType: Optional[Type[_IBase]] = None, + out: Optional[list[IObject]] = None, +) -> list[IObject]: + """Finds all objects of a type in an alembic archive""" + md = obj.getMetaData() + out = [] if out is None else out + if abcType is None: + out.append(obj) + elif abcType.matches(md): + out.append(abcType(obj.getParent(), obj.getName())) + for child in obj.children: + findAllAlembicObjects(child, abcType, out) + return out + + +def getTypedIObject(obj: IObject) -> Optional[_IBase]: + from alembic.AbcGeom import ( + ICamera, + ICurves, + ILight, + INuPatch, + IPoints, + IPolyMesh, + ISubD, + IXform, + ) + + md = obj.getMetaData() + for abcType in ( + IXform, + IPolyMesh, + ICamera, + ICurves, + ILight, + INuPatch, + IPoints, + ISubD, + ): + if abcType.matches(md): + return abcType(obj.getParent(), obj.getName()) + return None + + +def getMesh(infile: str) -> Optional[IPolyMesh]: + """Get the first found mesh object from the alembic filepath""" + iarch = IArchive(str(infile)) + ipolymsh = findAlembicObject(iarch.getTop(), abcType=IPolyMesh) + return ipolymsh + + +def writeStringProperty( + props: OCompoundProperty, key: str, value: str, ogawa: bool = True +) -> None: + """Write the definition string to an alembic OObject + + HDF5 (which we must still support) has a character limit + to string properties. Splitting the string must be handled + in a uniform way, so this function must be used + + Parameters + ---------- + props : OCompoundProperty + The alembic OObject properties + value : str + The simplex definition string + ogawa : bool + If the output is ogawa + + """ + if len(value) > 65000 and not ogawa: + value = str(value) + numChunks = (len(value) // 65000) + 1 + chunkSize = (len(value) // numChunks) + 1 + for c in range(numChunks): + prop = OStringProperty(props, "{0}{1}".format(key, c)) + prop.setValue(value[chunkSize * c : chunkSize * (c + 1)]) + else: + prop = OStringProperty(props, str(key)) + prop.setValue(str(value)) + + +def readStringProperty(props: ICompoundProperty, key: str) -> str: + """Read the definition string from an alembic OObject + + HDF5 (which we must still support) has a character limit + to string properties. Splitting the string must be handled + in a uniform way, so this function must be used + + Parameters + ---------- + props : ICompoundProperty + The alembic IObject properties + + Returns + ------- + : str + The simplex definition string + """ + if not props.valid(): + raise ValueError(".smpx file is missing the alembic user properties") + + try: + prop = props.getProperty(key) + except KeyError: + parts = [] + for c in range(10): + try: + prop = props.getProperty("{0}{1}".format(key, c)) + except KeyError: + if c == 0: + raise + break + else: + parts.append(prop.getValue()) + else: + raise ValueError("That is a HELL of a long simplex definition") + jsString = "".join(parts) + else: + jsString = prop.getValue() + + return jsString + + +def flattenFaces(faces: list[list[int]]) -> tuple[npint, npint]: + """Take a nested list representation of faces + and turn it into a flat face/count representation + + Parameters + ---------- + faces : [[int, ...], ...] + The nested list representation + + Returns + ------- + : np.array or list + The flat list of face connectivity + : np.array or list + The flat list of vertices per face + + """ + faceCounts, faceIdxs = [], [] + for f in faces: + faceCounts.append(len(f)) + faceIdxs.extend(f) + return np.array(faceCounts), np.array(faceIdxs) + + +def unflattenFaces(faces: npint, counts: npint) -> list[list[int]]: + """Take a flat face/count representation of faces + and turn it into a nested list representation + + Parameters + ---------- + faces : np.array + The flat list of face connectivity + counts : np.array + The flat list of vertices per face + + Returns + ------- + : [[int, ...], ...] + The nested list representation + """ + out, ptr = [], 0 + for c in counts: + out.append(faces[ptr : ptr + c].tolist()) + ptr += c + return out + + +def buildAbc( + outPath: str, + points: npfloat, + faces: npint, + faceCounts: Optional[npint] = None, + uvs: Optional[npfloat] = None, + uvFaces: Optional[npint] = None, + normals: Optional[npfloat] = None, + normFaces: Optional[npint] = None, + name: str = "polymsh", + shapeSuffix: str = "Shape", + transformSuffix: str = "", + propDict: Optional[dict[str, str]] = None, + ogawa: bool = True, + pBar: Optional[QProgressDialog] = None, +): + """ + Build a single-mesh alembic file from all of the non-alembic raw data + + Parameters + ---------- + outPath: str + The output path for the alembic file + points: list or ndarray + The list or array of points. Single multiple frames supported + faces: list + A list of lists of face indices, or a flattened list of indices. + If flat, then faceCounts must be provided + faceCounts: list + A list of the number of vertices per face. Defaults to None + uvs: list or ndarray + The Uvs for this mesh. Defaults to None + uvFaces: list + A list of lists of face indices, or a flattened list of indices. + If flat, then faceCounts must be provided. Defaults to None + normals: list or ndarray + The Normals for this mesh. Defaults to None + normFaces: list + A list of lists of face indices, or a flattened list of indices. + If flat, then faceCounts must be provided. Defaults to None + name: str + The name to give this mesh. Defaults to "polymsh" + shapeSuffix: str + The suffix to add to the shape of this mesh. Defaults to "Shape" + transformSuffix: str + The suffix to add to the transform of this mesh. Defaults to "" + propDict: dict + A dictionary of properties to add to the xform object + ogawa : bool + Whether to write to the Ogawa (True) or HDF5 (False) backend + pBar : QProgressDialog, optional + An optional progress dialog + """ + if faceCounts is None: + # All the faces are in list-of-list format + # put them in index-count format + faceCounts, faces = flattenFaces(faces) + if uvFaces is not None: + _, uvFaces = flattenFaces(uvFaces) + if normFaces is not None: + _, normFaces = flattenFaces(normFaces) + + faceCounts = mkSampleIntArray(faceCounts) + faces = mkSampleIntArray(faces) + + if not isinstance(uvs, OV2fGeomParamSample): + if uvFaces is not None and uvs is not None: + uvs = mkUvSample(uvs, indexes=uvFaces) + + if not isinstance(normals, ON3fGeomParamSample): + if normFaces is not None and normals is not None: + normals = mkNormalSample(normals, indexes=normFaces) + + oarch = OArchive(str(outPath), ogawa) + parent, opar, props, omesh, sch = None, None, None, None, None + try: + parent = oarch.getTop() + opar = OXform(parent, str(name + transformSuffix)) + if propDict: + props = opar.getSchema().getUserProperties() + for k, v in propDict.items(): + writeStringProperty(props, str(k), str(v), ogawa=ogawa) + + omesh = OPolyMesh(opar, str(name + shapeSuffix)) + + points = np.array(points) + if len(points.shape) == 2: + points = points[None, ...] + + sch = omesh.getSchema() + for i, frame in enumerate(points): + pbPrint(pBar, message="Exporting Shape", val=i, maxVal=len(points)) + abcFrame = mkSampleVertexPoints(frame) + setAlembicSample(sch, abcFrame, faceCounts, faces, uvs=uvs, normals=normals) + + pbPrint(pBar, message="Done Exporting") + finally: + # Make sure all this gets deleted so the file is freed + del parent, opar, props, omesh, sch + + +# Simplex format specific stuff +def getSmpxArchiveData(abcPath: str) -> tuple[IArchive, IPolyMesh, str]: + """Read and return the low level relevant data from a simplex alembic + + Parameters + ---------- + abcPath : str + The path to the .smpx file + + Returns + ------- + : IArchive + An opened Alembic IArchive object handle + : IPolyMesh + An Alembic Mesh handle + : str + The json definition string + """ + if not os.path.isfile(str(abcPath)): + raise IOError("File does not exist: " + str(abcPath)) + iarch = IArchive(str(abcPath)) # because alembic hates unicode + top, par, abcMesh = [None] * 3 + try: + top = iarch.getTop() + par = top.children[0] + par = IXform(top, par.getName()) + abcMesh = par.children[0] + abcMesh = IPolyMesh(par, abcMesh.getName()) + # I *could* come up with a generic property reader + # but it's useless for me at this time + sch = par.getSchema() + props = sch.getUserProperties() + jsString = readStringProperty(props, "simplex") + + except Exception: # pylint: disable=broad-except + # ensure that the .smpx file is released + iarch, top, par, abcMesh = [None] * 4 + raise + + # Must return the archive, otherwise it gets GC'd + return iarch, abcMesh, jsString + + +def readSmpx( + path: str, pBar: Optional[QProgressDialog] = None +) -> tuple[str, npint, npfloat, npint, Optional[npfloat], Optional[npint]]: + """Read and return the raw alembic vertex/face data in the flat alembic style + + Parameters + ---------- + abcPath : str + The path to the .smpx file + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + : str + The simplex definition string + : [int, ...] or np.array + The number of vertices per face + : [[(float*3), ...], ...] or np.array + The vertex positions per shape + : [int, ...] or np.array + The flat indexes per face + : [(float*2), ...] or np.array or None + The UV's + : [int, ...] or np.array + The flat indexes per uv-face + """ + iarch, abcMesh, jsString = getSmpxArchiveData(path) + try: + faces, counts = getStaticMeshArrays(abcMesh) + verts = getSampleArray(abcMesh, pBar=pBar) + uvs = getUvArray(abcMesh) + uvFaces, _ = getFlatUvFaces(abcMesh) + finally: + del iarch, abcMesh + return jsString, counts, verts, faces, uvs, uvFaces + + +def buildSmpx( + outPath: str, + points: npfloat, + faces: npint, + jsString: str, + name: str, + faceCounts: Optional[npint] = None, + uvs: Optional[npfloat] = None, + uvFaces: Optional[npint] = None, + ogawa: bool = True, + pBar: Optional[QProgressDialog] = None, +) -> None: + """ + Build a simplex output from raw data + + Parameters + ---------- + outPath: str + The output path for the alembic file + points: list or ndarray + The list or array of points. Single multiple frames supported + faces: list + A list of lists of face indices, or a flattened list of indices. + If flat, then faceCounts must be provided + jsString : str + The simplex definition string + name: str + The name to give this mesh + faceCounts: list + A list of the number of vertices per face. Defaults to None + uvs: list or ndarray + The Uvs for this mesh. Defaults to None + uvFaces: list + A list of lists of face indices, or a flattened list of indices. + If flat, then faceCounts must be provided. Defaults to None + ogawa : bool + Whether to write to the Ogawa (True) or HDF5 (False) backend + pBar : QProgressDialog, optional + An optional progress dialog + """ + buildAbc( + outPath, + points, + faces, + faceCounts=faceCounts, + uvs=uvs, + uvFaces=uvFaces, + name=name, + shapeSuffix="", + transformSuffix="", + propDict={"simplex": jsString}, + ogawa=ogawa, + pBar=pBar, + ) + + +def buildAlembicArchiveData( + path: str, name: str, jsString: str, ogawa: bool +) -> tuple[Optional[OArchive], Optional[OPolyMesh]]: + """Set up an output alembic archive with a mesh ready for writing + + Parameters + ---------- + path : str + The output file path + name : str + The name of the system + jsString : str + The simplex definition string + ogawa : bool + Whether to open in Ogawa (True) or HDF5 (False) mode + + Returns + ------- + : OArchive + The opened alembic output archive + : OPolyMesh + The mesh to write the shape data to + + """ + arch = OArchive(str(path), ogawa) + par, props, abcMesh = [None] * 3 + try: + par = OXform(arch.getTop(), str(name)) + props = par.getSchema().getUserProperties() + writeStringProperty(props, "simplex", jsString, ogawa=ogawa) + abcMesh = OPolyMesh(par, str(name)) + except Exception: + arch, par, props, abcMesh = [None] * 4 + raise + return arch, abcMesh + + +def readFalloffData(abcPath: str) -> dict[str, npfloat]: + """Load the relevant data from a simplex alembic + + Parameters + ---------- + abcPath : str + Path to the .smpx file + + """ + if not os.path.isfile(str(abcPath)): + raise IOError("File does not exist: " + str(abcPath)) + iarch = IArchive(str(abcPath)) # because alembic hates unicode + top, par, systemSchema, foPropPar, foProp = [None] * 5 + try: + top = iarch.getTop() + par = top.children[0] + par = IXform(top, par.getName()) + systemSchema = par.getSchema() + props = systemSchema.getUserProperties() + foDict = {} + try: + foPropPar = props.getProperty("falloffs") + except KeyError: + pass + else: + nps = foPropPar.getNumProperties() + for i in range(nps): + foProp = foPropPar.getProperty(i) + foDict[foProp.getName()] = np.array(foProp.getValue()) + finally: + iarch, top, par, systemSchema, foPropPar, foProp = [None] * 6 + + return foDict diff --git a/src/python/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py index 226d408f..14877a89 100644 --- a/src/python/simplexui/commands/applyCorrectives.py +++ b/src/python/simplexui/commands/applyCorrectives.py @@ -1,451 +1,451 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint:disable=unused-variable -import itertools -import os - -from pysimplex import PySimplex - -from ..items import Combo, Simplex, Slider -from Qt.QtWidgets import QApplication -from .alembicCommon import ( - buildSmpx, - getSmpxArchiveData, - getStaticMeshArrays, - getUvSample, - readSmpx, -) - -try: - import numpy as np -except ImportError: - pass - - -def invertAll(matrixArray): - """Invert all the square sub-matrices in a numpy array - - Parameters - ---------- - matrixArray : np.array - An M*N*N numpy array - - Returns - ------- - : np.array - An M*N*N numpy array - """ - # Look into numpy to see if there is a way to ignore - # all the repeated sanity checks, and do them ourselves, once - return np.array([np.linalg.inv(a) for a in matrixArray]) - - -def applyReference(pts, restPts, restDelta, inv): - """Given a shape and an array of pre-inverted - per-point matrices return the deltas - - Parameters - ---------- - pts : np.array - Deformed point positions - restPts : np.array - Rest point positions - restDelta : np.array - The delta from rest - inv : np.array - An M*4*4 array of matrices - - Returns - ------- - : np.array - The new point positions - - """ - pts = pts + restPts + restDelta - preSize = pts.shape[-1] - if inv.shape[-2] > pts.shape[-1]: - oneShape = list(pts.shape) - oneShape[-1] = inv.shape[-2] - pts.shape[-1] - pts = np.concatenate((pts, np.ones(oneShape)), axis=-1) - - # Return the 3d points - return np.einsum("ij,ijk->ik", pts, inv)[..., :preSize] - - -def loadSimplex(shapePath): - """Load and parse all the data from a simplex file - - Parameters - ---------- - shapePath : str - The path to the .smpx file - - Returns - ------- - : str - The simplex JSON string - : Simplex - The simplex system - : pySimplex - The instantiated simplex solver - : np.array - A Numpy array of the shape point positions - : np.array - A Numpy array of the rest pose of the system - - """ - if not os.path.isfile(str(shapePath)): - raise IOError("File does not exist: " + str(shapePath)) - - jsString, counts, verts, faces, uvs, uvFaces = readSmpx(shapePath) - - simplex = Simplex.buildSystemFromJsonString(jsString, None, forceDummy=True) - solver = PySimplex(jsString) - - # return as delta shapes - restIdx = simplex.shapes.index(simplex.restShape) - restPts = verts[restIdx] - verts = verts - restPts[None, ...] # reshape for broadcasting - - return jsString, simplex, solver, verts, restPts - - -def writeSimplex(inPath, outPath, newShapes, name="Face", pBar=None): - """Write a simplex file with new shapes - - Parameters - ---------- - inPath : str - The input .smpx file path - outPath : str - The output .smpx file path - newShapes : np.array - A numpy array of shapes to write - name : str - The name of the new system - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - - """ - if not os.path.isfile(str(inPath)): - raise IOError("File does not exist: " + str(inPath)) - - iarch, abcMesh, jsString = getSmpxArchiveData(inPath) - faces, counts = getStaticMeshArrays(abcMesh) - uvs = getUvSample(abcMesh) - del iarch, abcMesh - - buildSmpx( - outPath, - newShapes, - faces, - jsString, - name, - faceCounts=counts, - uvs=uvs, - ) - - -######################################################################### -#### Deform Reference #### -######################################################################### - - -def _buildSolverInputs(simplex, item, value, indexBySlider): - """Build an input vector for the solver that will - produce a required progression value on an item - """ - inVec = [0.0] * len(simplex.sliders) - if isinstance(item, Slider): - inVec[indexBySlider[item]] = value - return inVec - elif isinstance(item, Combo): - for pair in item.pairs: - inVec[indexBySlider[pair.slider]] = pair.value * abs(value) - return inVec - else: - raise ValueError( - "Not a slider or combo. Got type {0}: {1}".format(type(item), item) - ) - - -def buildFullShapes(simplex, shapeObjs, shapes, solver, pBar=None): - """Given shape inputs, build the full output shape from the deltas - We use shapes here because a shape implies both the progression - and the value of the inputs (with a little figuring) - - Parameters - ---------- - simplex : Simplex - A Simplex system - shapeObjs : [Shape, ...] - The Simplex system Shape objects - shapes : np.array - A numpy array of the shapes - solver : PySimplex - An instantiated simplex solver - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - : {Shape: np.array, ...} - A dictionary of point positions indexed by the shape - : {Shape: [float, ...], ...} - A dictionary of solver inputs indexed by the shape - """ - ########################################### - # Manipulate all the input lists and caches - indexBySlider = {s: i for i, s in enumerate(simplex.sliders)} - indexByShape = {s: i for i, s in enumerate(simplex.shapes)} - floaters = set(simplex.getFloatingShapes()) - floatIdxs = {indexByShape[s] for s in floaters} - - shapeDict = {} - for item in itertools.chain(simplex.sliders, simplex.combos): - for pair in item.prog.pairs: - if not pair.shape.isRest: - shapeDict[pair.shape] = (item, pair.value) - - ###################### - # Actually do the work - vecByShape = {} # store this for later use - ptsByShape = {} - - if pBar is not None: - pBar.setMaximum(len(shapeObjs)) - pBar.setValue(0) - QApplication.processEvents() - - flatShapes = shapes.reshape((len(shapes), -1)) - for i, shape in enumerate(shapeObjs): - if pBar is not None: - pBar.setValue(i) - QApplication.processEvents() - else: - print("Building {0} of {1}\r".format(i + 1, len(shapeObjs)), end=" ") - - item, value = shapeDict[shape] - inVec = _buildSolverInputs(simplex, item, value, indexBySlider) - outVec = solver.solve(inVec) - if shape not in floaters: - for fi in floatIdxs: - outVec[fi] = 0.0 - outVec = np.array(outVec) - outVec[np.where(np.isclose(outVec, 0))] = 0 - outVec[np.where(np.isclose(outVec, 1))] = 1 - vecByShape[shape] = outVec - pts = np.dot(outVec, flatShapes) - pts = pts.reshape((-1, 3)) - ptsByShape[shape] = pts - if pBar is None: - print() - - return ptsByShape, vecByShape - - -def collapseFullShapes(simplex, allPts, ptsByShape, vecByShape, pBar=None): - """Given a set of shapes that are full-on shapes (not just deltas) - Collapse them back into deltas in the simplex shape list - - Parameters - ---------- - simplex : Simplex - A simplex system - allPts : np.array - All the point positions - ptsByShape : {Shape: np.array, ...} - A dictionary of point positions indexed by the shape - vecByShape : {Shape: [float, ...], ...} - A dictionary of solver inputs indexed by the shape - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - : np.array - The collapsed shapes - - """ - # Figure out what order to build the deltas - # so that the deltas exist when I try to combine them - ctrlOrder = simplex.controllersByDepth() - shapeOrder = [pp.shape for ctrl in ctrlOrder for pp in ctrl.prog.pairs] - shapeOrder = [i for i in shapeOrder if not i.isRest] - - # Incrementally Build the numpy array of delta shapes - # build deltaShapeArray as a 2d array because numpy is like 10x faster on 2d arrays - indexByShape = {v: k for k, v in enumerate(simplex.shapes)} - deltaShapeArray = np.zeros((len(simplex.shapes), allPts.shape[1] * 3)) - - if pBar is not None: - pBar.setValue(0) - pBar.setMaximum(len(shapeOrder)) - pBar.setLabelText("Building Corrected Deltas") - QApplication.processEvents() - - for shpOrderIdx, shape in enumerate(shapeOrder): - if pBar is not None: - pBar.setValue(shpOrderIdx) - pBar.setLabelText("Building Corrected Deltas\n{}".format(shape.name)) - QApplication.processEvents() - else: - print( - "Collapsing {0} of {1}\r".format(shpOrderIdx + 1, len(shapeOrder)), - end=" ", - ) - - shpIdx = indexByShape[shape] - if shape in ptsByShape: - base = np.dot(vecByShape[shape], deltaShapeArray) - deltaShapeArray[shpIdx] = ( - ptsByShape[shape] - base.reshape((-1, 3)) - ).flatten() - else: - deltaShapeArray[shpIdx] = allPts[shpIdx].flatten() - - return deltaShapeArray.reshape((len(deltaShapeArray), -1, 3)) - - -def applyCorrectives( - simplex, allShapePts, restPts, solver, shapes, refIdxs, references, pBar=None -): - """Loop over the shapes and references, apply them, and return a new np.array - of shape points - - Parameters - ---------- - simplex : Simplex - Simplex system - allShapePts : np.array - deltas per shape - restPts : np.array - The rest point positions - solver : PySimplex - The Python Simplex solver object - shapes : [Shape, ...] - The simplex shape objects we care about - refIdxs : [int, ...] - The reference index per shape - references : np.array - A list of matrix-per-points - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - : np.array - The new shape points with correctives applied - - """ - # The rule of thumb is "THE SHAPE IS ALWAYS A DELTA" - - if pBar is not None: - pBar.setLabelText("Inverting References") - pBar.setValue(0) - pBar.setMaximum(len(references)) - QApplication.processEvents() - else: - print("Inverting References") - - # The initial reference is the rig rest shape - # This way we can handle a difference between - # The .smpx rest shape, and the rig rest shape - - # shape 0, all points, the tranform row of the matrix, the first 3 values in that row - rigRest = references[0, :, 3, :3] - restDelta = rigRest - restPts - - inverses = [] - for i, r in enumerate(references): - if pBar is not None: - pBar.setValue(i) - QApplication.processEvents() - inverses.append(invertAll(r)) - - if pBar is not None: - pBar.setLabelText("Building Full Shapes") - QApplication.processEvents() - else: - print("Building Full Shapes") - ptsByShape, vecByShape = buildFullShapes(simplex, shapes, allShapePts, solver, pBar) - - if pBar is not None: - pBar.setLabelText("Correcting Shapes") - pBar.setValue(0) - pBar.setMaximum(len(shapes)) - newPtsByShape = {} - for i, (shape, refIdx) in enumerate(zip(shapes, refIdxs)): - if pBar is not None: - pBar.setValue(i) - QApplication.processEvents() - else: - print("Correcting {0} of {1}: {2}".format(i + 1, len(shapes), shape.name)) - - inv = inverses[refIdx] - pts = ptsByShape[shape] - newPts = applyReference(pts, restPts, restDelta, inv) - newPtsByShape[shape] = newPts - - newShapePts = collapseFullShapes( - simplex, allShapePts, newPtsByShape, vecByShape, pBar - ) - newShapePts = newShapePts + restPts[None, ...] - - return newShapePts - - -def readAndApplyCorrectives(inPath, namePath, refPath, outPath, pBar=None): - """Read the provided files, apply the correctives, then output a new file - - Parameters - ---------- - inPath : str - The input path - namePath : str - A file correlating the shape names and indices - refPath : str - The reference matrices per point of deformation - outPath : str - The output path - pBar : QProgressDialog, optional - An optional progress dialog - """ - - if pBar is not None: - pBar.setLabelText("Reading reference data") - QApplication.processEvents() - - jsString, simplex, solver, allShapePts, restPts = loadSimplex(inPath) - with open(namePath, "r") as f: - nr = f.read() - nr = [i.split(";") for i in nr.split("\n") if i] - nr = nr[1:] # ignore the rest shape for this stuff - names, refIdxs = list(zip(*nr)) - refIdxs = list(map(int, refIdxs)) - refs = np.load(refPath, allow_pickle=True) - shapeByName = {i.name: i for i in simplex.shapes} - shapes = [shapeByName[n] for n in names] - newPts = applyCorrectives( - simplex, allShapePts, restPts, solver, shapes, refIdxs, refs, pBar - ) - writeSimplex(inPath, outPath, newPts, pBar=pBar) - print("DONE") +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint:disable=unused-variable +import itertools +import os + +from pysimplex import PySimplex + +from ..items import Combo, Simplex, Slider +from Qt.QtWidgets import QApplication +from .alembicCommon import ( + buildSmpx, + getSmpxArchiveData, + getStaticMeshArrays, + getUvSample, + readSmpx, +) + +try: + import numpy as np +except ImportError: + pass + + +def invertAll(matrixArray): + """Invert all the square sub-matrices in a numpy array + + Parameters + ---------- + matrixArray : np.array + An M*N*N numpy array + + Returns + ------- + : np.array + An M*N*N numpy array + """ + # Look into numpy to see if there is a way to ignore + # all the repeated sanity checks, and do them ourselves, once + return np.array([np.linalg.inv(a) for a in matrixArray]) + + +def applyReference(pts, restPts, restDelta, inv): + """Given a shape and an array of pre-inverted + per-point matrices return the deltas + + Parameters + ---------- + pts : np.array + Deformed point positions + restPts : np.array + Rest point positions + restDelta : np.array + The delta from rest + inv : np.array + An M*4*4 array of matrices + + Returns + ------- + : np.array + The new point positions + + """ + pts = pts + restPts + restDelta + preSize = pts.shape[-1] + if inv.shape[-2] > pts.shape[-1]: + oneShape = list(pts.shape) + oneShape[-1] = inv.shape[-2] - pts.shape[-1] + pts = np.concatenate((pts, np.ones(oneShape)), axis=-1) + + # Return the 3d points + return np.einsum("ij,ijk->ik", pts, inv)[..., :preSize] + + +def loadSimplex(shapePath): + """Load and parse all the data from a simplex file + + Parameters + ---------- + shapePath : str + The path to the .smpx file + + Returns + ------- + : str + The simplex JSON string + : Simplex + The simplex system + : pySimplex + The instantiated simplex solver + : np.array + A Numpy array of the shape point positions + : np.array + A Numpy array of the rest pose of the system + + """ + if not os.path.isfile(str(shapePath)): + raise IOError("File does not exist: " + str(shapePath)) + + jsString, counts, verts, faces, uvs, uvFaces = readSmpx(shapePath) + + simplex = Simplex.buildSystemFromJsonString(jsString, None, forceDummy=True) + solver = PySimplex(jsString) + + # return as delta shapes + restIdx = simplex.shapes.index(simplex.restShape) + restPts = verts[restIdx] + verts = verts - restPts[None, ...] # reshape for broadcasting + + return jsString, simplex, solver, verts, restPts + + +def writeSimplex(inPath, outPath, newShapes, name="Face", pBar=None): + """Write a simplex file with new shapes + + Parameters + ---------- + inPath : str + The input .smpx file path + outPath : str + The output .smpx file path + newShapes : np.array + A numpy array of shapes to write + name : str + The name of the new system + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + + """ + if not os.path.isfile(str(inPath)): + raise IOError("File does not exist: " + str(inPath)) + + iarch, abcMesh, jsString = getSmpxArchiveData(inPath) + faces, counts = getStaticMeshArrays(abcMesh) + uvs = getUvSample(abcMesh) + del iarch, abcMesh + + buildSmpx( + outPath, + newShapes, + faces, + jsString, + name, + faceCounts=counts, + uvs=uvs, + ) + + +######################################################################### +#### Deform Reference #### +######################################################################### + + +def _buildSolverInputs(simplex, item, value, indexBySlider): + """Build an input vector for the solver that will + produce a required progression value on an item + """ + inVec = [0.0] * len(simplex.sliders) + if isinstance(item, Slider): + inVec[indexBySlider[item]] = value + return inVec + elif isinstance(item, Combo): + for pair in item.pairs: + inVec[indexBySlider[pair.slider]] = pair.value * abs(value) + return inVec + else: + raise ValueError( + "Not a slider or combo. Got type {0}: {1}".format(type(item), item) + ) + + +def buildFullShapes(simplex, shapeObjs, shapes, solver, pBar=None): + """Given shape inputs, build the full output shape from the deltas + We use shapes here because a shape implies both the progression + and the value of the inputs (with a little figuring) + + Parameters + ---------- + simplex : Simplex + A Simplex system + shapeObjs : [Shape, ...] + The Simplex system Shape objects + shapes : np.array + A numpy array of the shapes + solver : PySimplex + An instantiated simplex solver + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + : {Shape: np.array, ...} + A dictionary of point positions indexed by the shape + : {Shape: [float, ...], ...} + A dictionary of solver inputs indexed by the shape + """ + ########################################### + # Manipulate all the input lists and caches + indexBySlider = {s: i for i, s in enumerate(simplex.sliders)} + indexByShape = {s: i for i, s in enumerate(simplex.shapes)} + floaters = set(simplex.getFloatingShapes()) + floatIdxs = {indexByShape[s] for s in floaters} + + shapeDict = {} + for item in itertools.chain(simplex.sliders, simplex.combos): + for pair in item.prog.pairs: + if not pair.shape.isRest: + shapeDict[pair.shape] = (item, pair.value) + + ###################### + # Actually do the work + vecByShape = {} # store this for later use + ptsByShape = {} + + if pBar is not None: + pBar.setMaximum(len(shapeObjs)) + pBar.setValue(0) + QApplication.processEvents() + + flatShapes = shapes.reshape((len(shapes), -1)) + for i, shape in enumerate(shapeObjs): + if pBar is not None: + pBar.setValue(i) + QApplication.processEvents() + else: + print("Building {0} of {1}\r".format(i + 1, len(shapeObjs)), end=" ") + + item, value = shapeDict[shape] + inVec = _buildSolverInputs(simplex, item, value, indexBySlider) + outVec = solver.solve(inVec) + if shape not in floaters: + for fi in floatIdxs: + outVec[fi] = 0.0 + outVec = np.array(outVec) + outVec[np.where(np.isclose(outVec, 0))] = 0 + outVec[np.where(np.isclose(outVec, 1))] = 1 + vecByShape[shape] = outVec + pts = np.dot(outVec, flatShapes) + pts = pts.reshape((-1, 3)) + ptsByShape[shape] = pts + if pBar is None: + print() + + return ptsByShape, vecByShape + + +def collapseFullShapes(simplex, allPts, ptsByShape, vecByShape, pBar=None): + """Given a set of shapes that are full-on shapes (not just deltas) + Collapse them back into deltas in the simplex shape list + + Parameters + ---------- + simplex : Simplex + A simplex system + allPts : np.array + All the point positions + ptsByShape : {Shape: np.array, ...} + A dictionary of point positions indexed by the shape + vecByShape : {Shape: [float, ...], ...} + A dictionary of solver inputs indexed by the shape + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + : np.array + The collapsed shapes + + """ + # Figure out what order to build the deltas + # so that the deltas exist when I try to combine them + ctrlOrder = simplex.controllersByDepth() + shapeOrder = [pp.shape for ctrl in ctrlOrder for pp in ctrl.prog.pairs] + shapeOrder = [i for i in shapeOrder if not i.isRest] + + # Incrementally Build the numpy array of delta shapes + # build deltaShapeArray as a 2d array because numpy is like 10x faster on 2d arrays + indexByShape = {v: k for k, v in enumerate(simplex.shapes)} + deltaShapeArray = np.zeros((len(simplex.shapes), allPts.shape[1] * 3)) + + if pBar is not None: + pBar.setValue(0) + pBar.setMaximum(len(shapeOrder)) + pBar.setLabelText("Building Corrected Deltas") + QApplication.processEvents() + + for shpOrderIdx, shape in enumerate(shapeOrder): + if pBar is not None: + pBar.setValue(shpOrderIdx) + pBar.setLabelText("Building Corrected Deltas\n{}".format(shape.name)) + QApplication.processEvents() + else: + print( + "Collapsing {0} of {1}\r".format(shpOrderIdx + 1, len(shapeOrder)), + end=" ", + ) + + shpIdx = indexByShape[shape] + if shape in ptsByShape: + base = np.dot(vecByShape[shape], deltaShapeArray) + deltaShapeArray[shpIdx] = ( + ptsByShape[shape] - base.reshape((-1, 3)) + ).flatten() + else: + deltaShapeArray[shpIdx] = allPts[shpIdx].flatten() + + return deltaShapeArray.reshape((len(deltaShapeArray), -1, 3)) + + +def applyCorrectives( + simplex, allShapePts, restPts, solver, shapes, refIdxs, references, pBar=None +): + """Loop over the shapes and references, apply them, and return a new np.array + of shape points + + Parameters + ---------- + simplex : Simplex + Simplex system + allShapePts : np.array + deltas per shape + restPts : np.array + The rest point positions + solver : PySimplex + The Python Simplex solver object + shapes : [Shape, ...] + The simplex shape objects we care about + refIdxs : [int, ...] + The reference index per shape + references : np.array + A list of matrix-per-points + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + : np.array + The new shape points with correctives applied + + """ + # The rule of thumb is "THE SHAPE IS ALWAYS A DELTA" + + if pBar is not None: + pBar.setLabelText("Inverting References") + pBar.setValue(0) + pBar.setMaximum(len(references)) + QApplication.processEvents() + else: + print("Inverting References") + + # The initial reference is the rig rest shape + # This way we can handle a difference between + # The .smpx rest shape, and the rig rest shape + + # shape 0, all points, the tranform row of the matrix, the first 3 values in that row + rigRest = references[0, :, 3, :3] + restDelta = rigRest - restPts + + inverses = [] + for i, r in enumerate(references): + if pBar is not None: + pBar.setValue(i) + QApplication.processEvents() + inverses.append(invertAll(r)) + + if pBar is not None: + pBar.setLabelText("Building Full Shapes") + QApplication.processEvents() + else: + print("Building Full Shapes") + ptsByShape, vecByShape = buildFullShapes(simplex, shapes, allShapePts, solver, pBar) + + if pBar is not None: + pBar.setLabelText("Correcting Shapes") + pBar.setValue(0) + pBar.setMaximum(len(shapes)) + newPtsByShape = {} + for i, (shape, refIdx) in enumerate(zip(shapes, refIdxs)): + if pBar is not None: + pBar.setValue(i) + QApplication.processEvents() + else: + print("Correcting {0} of {1}: {2}".format(i + 1, len(shapes), shape.name)) + + inv = inverses[refIdx] + pts = ptsByShape[shape] + newPts = applyReference(pts, restPts, restDelta, inv) + newPtsByShape[shape] = newPts + + newShapePts = collapseFullShapes( + simplex, allShapePts, newPtsByShape, vecByShape, pBar + ) + newShapePts = newShapePts + restPts[None, ...] + + return newShapePts + + +def readAndApplyCorrectives(inPath, namePath, refPath, outPath, pBar=None): + """Read the provided files, apply the correctives, then output a new file + + Parameters + ---------- + inPath : str + The input path + namePath : str + A file correlating the shape names and indices + refPath : str + The reference matrices per point of deformation + outPath : str + The output path + pBar : QProgressDialog, optional + An optional progress dialog + """ + + if pBar is not None: + pBar.setLabelText("Reading reference data") + QApplication.processEvents() + + jsString, simplex, solver, allShapePts, restPts = loadSimplex(inPath) + with open(namePath, "r") as f: + nr = f.read() + nr = [i.split(";") for i in nr.split("\n") if i] + nr = nr[1:] # ignore the rest shape for this stuff + names, refIdxs = list(zip(*nr)) + refIdxs = list(map(int, refIdxs)) + refs = np.load(refPath, allow_pickle=True) + shapeByName = {i.name: i for i in simplex.shapes} + shapes = [shapeByName[n] for n in names] + newPts = applyCorrectives( + simplex, allShapePts, restPts, solver, shapes, refIdxs, refs, pBar + ) + writeSimplex(inPath, outPath, newPts, pBar=pBar) + print("DONE") diff --git a/src/python/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py index 2470ee2e..b807ca7d 100644 --- a/src/python/simplexui/commands/correctiveInterface.py +++ b/src/python/simplexui/commands/correctiveInterface.py @@ -1,261 +1,261 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -from Qt.QtWidgets import QApplication - -try: - import numpy as np -except ImportError: - pass - -from .mayaCorrectiveInterface import getShiftValues, resetPose, setPose - -dcc = "maya" - - -def getRefForPoses(mesh, poses, multipliers): - """Given a set of poses and a multiplier, get the reference - - Parameters - ---------- - mesh : object - The DCC object - poses : [[(str, float), ...], ...] - Property/value pairs for different rig poses - multiplier : float - The percent of the pose to apply - - Returns - ------- - : np.array - The point reference matrices in pose - - """ - - for pose, mul in zip(poses, multipliers): - setPose(pose, mul) - - ref = getDeformReference(mesh) - - for pose in poses: - resetPose(pose) - return ref - - -def getDeformReference(mesh): - """Build the 4x4 deformation reference matrices given a mesh - - Parameters - ---------- - mesh : object - The DCC mesh object - - Returns - ------- - : np.array: - The point reference matrices in pose - - """ - zero, oneX, oneY, oneZ = getShiftValues(mesh) - - zero = np.array(zero) - dx = np.array(oneX) - zero - dy = np.array(oneY) - zero - dz = np.array(oneZ) - zero - - # Maya has numpy 1.09, but np.stack comes from 1.10 - # mats = np.stack((dx, dy, dz, zero), axis=1) - - # Make the new axis to concatenate on - zero = zero[:, None] - dx = dx[:, None] - dy = dy[:, None] - dz = dz[:, None] - mats = np.concatenate((dx, dy, dz, zero), axis=1) - - # Turn the Nx4x3 matrix into a Nx4x4 - zzz = np.zeros((len(mats), 4, 1)) - zzz[:, 3] = 1.0 - mats = np.concatenate((mats, zzz), axis=2) - - return mats - - -def buildCorrectiveReferences(mesh, simplex, poses, sliders, pBar=None): - """Take correlated poses and sliders, and expand down the - simplex combo tree, building references for each required shape - - Parameters - ---------- - mesh : object - The DCC mesh object - simplex : Simplex - The Simplex system - poses : [[(str, float), ...], ...] - Property/value pairs for different rig poses - sliders : [Slider, ...] - Simplex slider objects that are controlled by the poses - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - : np.array - The output matrix-per-point arrays - : [Shape, ...] - The shapes active - : [int, ...] - The Reference pose per shape - """ - # cache the pose search - - # Pre-cache the combo search - allCombosBySliderValue = {} - for c in simplex.combos: - for p in c.pairs: - allCombosBySliderValue.setdefault((p.slider, p.value), []).append(c) - - # This is only my subset set of downstreams - # Get the downstreams by slider and value - sliderValuesByCombo = {} - for slider in sliders: - for p in slider.prog.pairs: - combos = allCombosBySliderValue.get((slider, p.value), []) - for combo in combos: - sliderValuesByCombo.setdefault(combo, []).append((slider, p.value)) - - # out = [] - refCache = {} - refs, shapes, refIdxs = [], [], [] - - # get the slider outputs - if pBar is not None: - pBar.setLabelText("Building Shape References") - pBar.setValue(0) - mv = 0 - for slider in sliders: - for p in slider.prog.pairs: - if not p.shape.isRest: - mv += 1 - pBar.setMaximum(mv) - QApplication.processEvents() - - # Make sure to export the rest reference first - ref = getRefForPoses(mesh, [], []) - refIdxs.append(len(refs)) - cacheKey = frozenset([("", 0.0)]) - refCache[cacheKey] = len(refs) - refs.append(ref) - shapes.append(simplex.restShape) - - # Now export everything else - poseBySlider = {} - for slider, pose in zip(sliders, poses): - poseBySlider[slider] = pose - for p in slider.prog.pairs: - if not p.shape.isRest: - if pBar is not None: - pBar.setValue(pBar.value()) - QApplication.processEvents() - cacheKey = frozenset([(slider, p.value)]) - if cacheKey in refCache: - idx = refCache[cacheKey] - refIdxs.append(idx) - else: - ref = getRefForPoses(mesh, [pose], [p.value]) - refIdxs.append(len(refs)) - refCache[cacheKey] = len(refs) - refs.append(ref) - shapes.append(p.shape) - - # Get the combo outputs - if pBar is not None: - pBar.setLabelText("Building Combo References") - pBar.setValue(0) - mv = 0 - for combo in sliderValuesByCombo: - for p in combo.prog.pairs: - if not p.shape.isRest: - mv += 1 - pBar.setMaximum(mv) - QApplication.processEvents() - - for combo, sliderVals in sliderValuesByCombo.items(): - # components = frozenset(sliderVals) - poses = [poseBySlider[s] for s, _ in sliderVals] - for p in combo.prog.pairs: - if not p.shape.isRest: - if pBar is not None: - pBar.setValue(pBar.value()) - QApplication.processEvents() - - cacheKey = frozenset(sliderVals) - if cacheKey in refCache: - idx = refCache[cacheKey] - refIdxs.append(idx) - else: - vals = [p.value * v[1] for v in sliderVals] - ref = getRefForPoses(mesh, poses, vals) - refIdxs.append(len(refs)) - refCache[cacheKey] = len(refs) - refs.append(ref) - shapes.append(p.shape) - - return np.array(refs), shapes, refIdxs - - -def outputCorrectiveReferences( - outNames, outRefs, simplex, mesh, poses, sliders, pBar=None -): - """Output the proper files for an external corrective application - - Parameters - ---------- - outNames : str - The filepath for the output shape and reference indices - outRefs : str - The filepath for the deformation references - simplex : Simplex - A simplex system - mesh : object - The mesh object to deform - poses : [[(str, float), ...], ...] - Lists of parameter - sliders : [Slider, ...] - The simplex sliders that correspond to the poses - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - - """ - refs, shapes, refIdxs = buildCorrectiveReferences( - mesh, simplex, poses, sliders, pBar - ) - - if pBar is not None: - pBar.setLabelText("Writing Names") - QApplication.processEvents() - nameWrite = ["{};{}".format(s.name, r) for s, r in zip(shapes, refIdxs)] - with open(outNames, "w") as f: - f.write("\n".join(nameWrite)) - - if pBar is not None: - pBar.setLabelText("Writing References") - QApplication.processEvents() - refs.dump(outRefs) +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +from Qt.QtWidgets import QApplication + +try: + import numpy as np +except ImportError: + pass + +from .mayaCorrectiveInterface import getShiftValues, resetPose, setPose + +dcc = "maya" + + +def getRefForPoses(mesh, poses, multipliers): + """Given a set of poses and a multiplier, get the reference + + Parameters + ---------- + mesh : object + The DCC object + poses : [[(str, float), ...], ...] + Property/value pairs for different rig poses + multiplier : float + The percent of the pose to apply + + Returns + ------- + : np.array + The point reference matrices in pose + + """ + + for pose, mul in zip(poses, multipliers): + setPose(pose, mul) + + ref = getDeformReference(mesh) + + for pose in poses: + resetPose(pose) + return ref + + +def getDeformReference(mesh): + """Build the 4x4 deformation reference matrices given a mesh + + Parameters + ---------- + mesh : object + The DCC mesh object + + Returns + ------- + : np.array: + The point reference matrices in pose + + """ + zero, oneX, oneY, oneZ = getShiftValues(mesh) + + zero = np.array(zero) + dx = np.array(oneX) - zero + dy = np.array(oneY) - zero + dz = np.array(oneZ) - zero + + # Maya has numpy 1.09, but np.stack comes from 1.10 + # mats = np.stack((dx, dy, dz, zero), axis=1) + + # Make the new axis to concatenate on + zero = zero[:, None] + dx = dx[:, None] + dy = dy[:, None] + dz = dz[:, None] + mats = np.concatenate((dx, dy, dz, zero), axis=1) + + # Turn the Nx4x3 matrix into a Nx4x4 + zzz = np.zeros((len(mats), 4, 1)) + zzz[:, 3] = 1.0 + mats = np.concatenate((mats, zzz), axis=2) + + return mats + + +def buildCorrectiveReferences(mesh, simplex, poses, sliders, pBar=None): + """Take correlated poses and sliders, and expand down the + simplex combo tree, building references for each required shape + + Parameters + ---------- + mesh : object + The DCC mesh object + simplex : Simplex + The Simplex system + poses : [[(str, float), ...], ...] + Property/value pairs for different rig poses + sliders : [Slider, ...] + Simplex slider objects that are controlled by the poses + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + : np.array + The output matrix-per-point arrays + : [Shape, ...] + The shapes active + : [int, ...] + The Reference pose per shape + """ + # cache the pose search + + # Pre-cache the combo search + allCombosBySliderValue = {} + for c in simplex.combos: + for p in c.pairs: + allCombosBySliderValue.setdefault((p.slider, p.value), []).append(c) + + # This is only my subset set of downstreams + # Get the downstreams by slider and value + sliderValuesByCombo = {} + for slider in sliders: + for p in slider.prog.pairs: + combos = allCombosBySliderValue.get((slider, p.value), []) + for combo in combos: + sliderValuesByCombo.setdefault(combo, []).append((slider, p.value)) + + # out = [] + refCache = {} + refs, shapes, refIdxs = [], [], [] + + # get the slider outputs + if pBar is not None: + pBar.setLabelText("Building Shape References") + pBar.setValue(0) + mv = 0 + for slider in sliders: + for p in slider.prog.pairs: + if not p.shape.isRest: + mv += 1 + pBar.setMaximum(mv) + QApplication.processEvents() + + # Make sure to export the rest reference first + ref = getRefForPoses(mesh, [], []) + refIdxs.append(len(refs)) + cacheKey = frozenset([("", 0.0)]) + refCache[cacheKey] = len(refs) + refs.append(ref) + shapes.append(simplex.restShape) + + # Now export everything else + poseBySlider = {} + for slider, pose in zip(sliders, poses): + poseBySlider[slider] = pose + for p in slider.prog.pairs: + if not p.shape.isRest: + if pBar is not None: + pBar.setValue(pBar.value()) + QApplication.processEvents() + cacheKey = frozenset([(slider, p.value)]) + if cacheKey in refCache: + idx = refCache[cacheKey] + refIdxs.append(idx) + else: + ref = getRefForPoses(mesh, [pose], [p.value]) + refIdxs.append(len(refs)) + refCache[cacheKey] = len(refs) + refs.append(ref) + shapes.append(p.shape) + + # Get the combo outputs + if pBar is not None: + pBar.setLabelText("Building Combo References") + pBar.setValue(0) + mv = 0 + for combo in sliderValuesByCombo: + for p in combo.prog.pairs: + if not p.shape.isRest: + mv += 1 + pBar.setMaximum(mv) + QApplication.processEvents() + + for combo, sliderVals in sliderValuesByCombo.items(): + # components = frozenset(sliderVals) + poses = [poseBySlider[s] for s, _ in sliderVals] + for p in combo.prog.pairs: + if not p.shape.isRest: + if pBar is not None: + pBar.setValue(pBar.value()) + QApplication.processEvents() + + cacheKey = frozenset(sliderVals) + if cacheKey in refCache: + idx = refCache[cacheKey] + refIdxs.append(idx) + else: + vals = [p.value * v[1] for v in sliderVals] + ref = getRefForPoses(mesh, poses, vals) + refIdxs.append(len(refs)) + refCache[cacheKey] = len(refs) + refs.append(ref) + shapes.append(p.shape) + + return np.array(refs), shapes, refIdxs + + +def outputCorrectiveReferences( + outNames, outRefs, simplex, mesh, poses, sliders, pBar=None +): + """Output the proper files for an external corrective application + + Parameters + ---------- + outNames : str + The filepath for the output shape and reference indices + outRefs : str + The filepath for the deformation references + simplex : Simplex + A simplex system + mesh : object + The mesh object to deform + poses : [[(str, float), ...], ...] + Lists of parameter + sliders : [Slider, ...] + The simplex sliders that correspond to the poses + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + + """ + refs, shapes, refIdxs = buildCorrectiveReferences( + mesh, simplex, poses, sliders, pBar + ) + + if pBar is not None: + pBar.setLabelText("Writing Names") + QApplication.processEvents() + nameWrite = ["{};{}".format(s.name, r) for s, r in zip(shapes, refIdxs)] + with open(outNames, "w") as f: + f.write("\n".join(nameWrite)) + + if pBar is not None: + pBar.setLabelText("Writing References") + QApplication.processEvents() + refs.dump(outRefs) diff --git a/src/python/simplexui/commands/expandedExport.py b/src/python/simplexui/commands/expandedExport.py index 1f7411e8..bbc0f48f 100644 --- a/src/python/simplexui/commands/expandedExport.py +++ b/src/python/simplexui/commands/expandedExport.py @@ -1,436 +1,436 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -from pysimplex import PySimplex - -from ..interface.mayaInterface import DCC, disconnected -from ..items import Combo, Slider, Traversal, Simplex -from .alembicCommon import buildSmpx - -try: - import numpy as np -except ImportError: - np = None - - -def _setSliders(ctrl, val, svs): - slis, vals = svs.setdefault(ctrl.simplex, ([], [])) - - if isinstance(ctrl, Slider): - slis.append(ctrl) - vals.append(val) - elif isinstance(ctrl, Combo): - for cp in ctrl.pairs: - slis.append(cp.slider) - vals.append(cp.value * abs(val)) - elif isinstance(ctrl, Traversal): - # set the ctrl.multiplierCtrl.controller to 1 - multCtrl = ctrl.multiplierCtrl.controller - multVal = ctrl.multiplierCtrl.value if val != 0.0 else 0.0 - progCtrl = ctrl.progressCtrl.controller - _setSliders(multCtrl, multVal, svs) - _setSliders(progCtrl, val, svs) - - -def setSliderGroup(ctrls, val): - """Set a group of controls to a given value - - Parameters - ---------- - ctrls : [object, ...] - A list of simplex objects - val : float - The value to set - - Returns - ------- - - """ - svs = {} - for ctrl in ctrls: - _setSliders(ctrl, val, svs) - - for smpx, (slis, vals) in svs.items(): - smpx.setSlidersWeights(slis, vals) - - -def clientPartition(master, clients): - """ - - Parameters - ---------- - master : Simplex - The master simplex system - clients : [Simplex, ...] - The client simplex systems - - Returns - ------- - : {str: [Slider, ...]} - Dictionary of name to slider - : {str: [Combo, ...]} - Dictionary of name to combo - : {str: [Traversal, ...]} - Dictionary of name to traversal - - """ - sliders, combos, traversals = {}, {}, {} - for cli in [master] + clients: - for sli in cli.sliders: - sliders.setdefault(sli.name, []).append(sli) - - for com in cli.combos: - combos.setdefault(com.name, []).append(com) - - for trav in cli.traversals: - traversals.setdefault(trav.name, []).append(trav) - return sliders, combos, traversals - - -def zeroAll(smpxs): - """Set all sliders on the given simplex systems to 0 - - Parameters - ---------- - smpxs : [Simplex, ...] - A list of Simplex systems - - Returns - ------- - - """ - for smpx in smpxs: - smpx.setSlidersWeights(smpx.sliders, [0.0] * len(smpx.sliders)) - - -def getExpandedData(master, clients, mesh): - """Get the fully expanded shape data for each slider, combo, and traversal - at each of its underlying shapes - - Parameters - ---------- - master : Simplex - The master simplex system - clients : [Simplex, ...] - The client simplex systems - mesh : str - The name of the maya shape node - - Returns - ------- - : np.array - The rest shape point positions - : np.array - The slider shape point positions - : np.array - The combo shape point positions - : np.array - The traversal shape point positions - - """ - # zero everything - zeroAll([master] + clients) - sliPart, cmbPart, travPart = clientPartition(master, clients) - sliderShapes = {} - for slider in master.sliders: - ss = {} - sliderShapes[slider] = ss - for pp in slider.prog.pairs: - if pp.shape.isRest: - continue - setSliderGroup(sliPart[slider.name], pp.value) - ss[pp] = DCC.getNumpyShape(mesh) - setSliderGroup(sliPart[slider.name], 0.0) - - # Disable traversals - zeroAll([master] + clients) - travShapeThings = [] - comboShapes = {} - for trav in master.traversals: - for pp in trav.prog.pairs: - if pp.shape.isRest: - continue - travShapeThings.append(pp.shape) - travShapeThings = [i.thing for i in travShapeThings] - - # Get the combos with traversals disconnected - with disconnected(travShapeThings): - for combo in master.combos: - ss = {} - comboShapes[combo] = ss - for pp in combo.prog.pairs: - if pp.shape.isRest: - continue - setSliderGroup(cmbPart[combo.name], pp.value) - ss[pp] = DCC.getNumpyShape(mesh) - setSliderGroup(cmbPart[combo.name], 0.0) - - zeroAll([master] + clients) - travShapes = {} - for trav in master.traversals: - ss = {} - travShapes[trav] = ss - progCtrl = trav.progressCtrl.controller - progVal = trav.progressCtrl.value - for pp in progCtrl.prog.pairs: - if pp.shape.isRest: - continue - if pp.value * progVal <= 0.0: - # Traversals only activate if the progression is in the same - # pos/neg direction as the value. Otherwise we just skip - continue - - setSliderGroup(travPart[trav.name], pp.value) - ss[pp] = DCC.getNumpyShape(mesh) - setSliderGroup(travPart[trav.name], 0.0) - - zeroAll([master] + clients) - restShape = DCC.getNumpyShape(mesh) - - return restShape, sliderShapes, comboShapes, travShapes - - -def _setInputs(inVec, item, indexBySlider, value): - """Being clever - Sliders or Combos just set the value and return - Traversals recursively call this function with the controllers (that only either sliders or combos) - """ - if isinstance(item, Slider): - inVec[indexBySlider[item]] = value - return inVec - elif isinstance(item, Combo): - for pair in item.pairs: - inVec[indexBySlider[pair.slider]] = pair.value * abs(value) - return inVec - elif isinstance(item, Traversal): - inVec = _setInputs( - inVec, - item.multiplierCtrl.controller, - indexBySlider, - item.multiplierCtrl.value, - ) - inVec = _setInputs( - inVec, - item.progressCtrl.controller, - indexBySlider, - item.progressCtrl.value * value, - ) - return inVec - raise ValueError( - "Not a Slider, Combo, or Traversal. Got type {0}: {1}".format(type(item), item) - ) - - -def _buildSolverInputs(simplex, item, value, indexBySlider): - """Build an input vector for the solver that will - produce a required progression value on an item - """ - inVec = [0.0] * len(simplex.sliders) - return _setInputs(inVec, item, indexBySlider, value) - - -def getTravDepth(trav): - """Get the depth of a traversal object - - Parameters - ---------- - trav : Traversal - The traversal object - - Returns - ------- - : int - The depth of the given Traversal - - """ - inputs = [] - mult = trav.multiplierCtrl.controller - prog = trav.progressCtrl.controller - for item in (mult, prog): - if isinstance(item, Slider): - inputs.append(item) - elif isinstance(item, Combo): - for cp in item.pairs: - inputs.append(cp.slider) - return len(set(inputs)) - - -def parseExpandedData(smpx, restShape, sliderShapes, comboShapes, travShapes): - """Turn the expanded data into shapeDeltas connected to the actual Shape objects - - Parameters - ---------- - smpx : Simplex - A simplex system - restShape : np.array - The rest point positions - sliderShapes : np.array - The slider shape point positions - comboShapes : np.array - The combo shape point positions - travShapes : np.array - The traversal shape point positions - - Returns - ------- - : np.array - The point positions for a new set of shapes - - """ - if np is None: - raise RuntimeError("Numpy is not available") - - solver = PySimplex(smpx.dump()) - shapeArray = np.zeros((len(smpx.shapes), len(restShape), 3)) - - indexBySlider = {s: i for i, s in enumerate(smpx.sliders)} - indexByShape = {s: i for i, s in enumerate(smpx.shapes)} - - floatShapeSet = set(smpx.getFloatingShapes()) - floatIdxs = sorted({indexByShape[s] for s in floatShapeSet}) - travShapeSet = {pp.shape for t in smpx.traversals for pp in t.prog.pairs} - travIdxs = sorted({indexByShape[s] for s in travShapeSet}) - - # Sliders are simple, just set their shapes directly - for ppDict in sliderShapes.values(): - for pp, shp in ppDict.items(): - shapeArray[indexByShape[pp.shape]] = shp - restShape - - # First sort the combos by depth - comboByDepth = {} - for combo in smpx.combos: - comboByDepth.setdefault(len(combo.pairs), []).append(combo) - - for depth in sorted(comboByDepth.keys()): - for combo in comboByDepth[depth]: - for pp, shp in comboShapes[combo].items(): - inVec = _buildSolverInputs(smpx, combo, pp.value, indexBySlider) - outVec = np.array(solver.solve(inVec)) - outVec[np.where(np.isclose(outVec, 0.0))] = 0.0 - outVec[np.where(np.isclose(outVec, 1.0))] = 1.0 - outVec[indexByShape[pp.shape]] = 0.0 - - # ignore any traversals - outVec[travIdxs] = 0.0 - - # ignore floaters if we're not currently checking floaters - if pp.shape not in floatShapeSet: - outVec[floatIdxs] = 0.0 - - # set the shape delta to the output - baseShape = np.dot(outVec, shapeArray.swapaxes(0, 1)) - shapeArray[indexByShape[pp.shape]] = shp - restShape - baseShape - - # First the traversals by depth - travByDepth = {} - for trav in smpx.traversals: - travByDepth.setdefault(getTravDepth(trav), []).append(trav) - - for depth in sorted(travByDepth.keys()): - for trav in travByDepth[depth]: - for pp, shp in travShapes[trav].items(): - inVec = _buildSolverInputs(smpx, trav, pp.value, indexBySlider) - outVec = np.array(solver.solve(inVec)) - outVec[np.where(np.isclose(outVec, 0.0))] = 0.0 - outVec[np.where(np.isclose(outVec, 1.0))] = 1.0 - outVec[indexByShape[pp.shape]] = 0.0 - - # set the shape delta to the output - baseShape = np.dot(outVec, shapeArray.swapaxes(0, 1)) - shapeArray[indexByShape[pp.shape]] = shp - restShape - baseShape - return shapeArray - - -def buildShapeArray(mesh, master, clients): - """Build the outpu shape array - - Parameters - ---------- - mesh : str - The maya shape node name - master : Simplex - The master simplex system - clients : - The client simplex systems - - Returns - ------- - : np.array - The full output shape array - - """ - restShape, sliderShapes, comboShapes, travShapes = getExpandedData( - master, clients, mesh - ) - shapeArray = parseExpandedData( - master, restShape, sliderShapes, comboShapes, travShapes - ) - shapeArray += restShape[None, ...] - return shapeArray - - -def expandedExportAbc(path, mesh, master, clients=()): - """Export the alembic by re-building the deltas from all of the full shapes - This is required for delta-mushing a system, because the sum of mushed shapes - is not the same as the mushed sum-of-shapes - - Parameters - ---------- - path : str - Output path for the .smpx - mesh : str - The maya shape node name - master : Simplex - The master simplex system - clients : [Simplex, ...] - The client simplex systems - - Returns - ------- - - """ - # Convert clients to a list if need be - if not clients: - clients = [] - elif not isinstance(clients, list): - if isinstance(clients, tuple): - clients = list(clients) - else: - clients = [clients] - - faces, counts, uvs = DCC.getAbcFaces(mesh) - shapeArray = buildShapeArray(mesh, master, clients) - jsString = str(master.dump()) - - buildSmpx( - path, - shapeArray, - faces, - jsString, - master.name, - faceCounts=counts, - uvs=uvs, - ) - - -if __name__ == "__main__": - # get the smpx from the UI - - master = Simplex.buildSystemFromMesh("Face_SIMPLEX", "Face") - client = Simplex.buildSystemFromMesh("Face_SIMPLEX2", "Face2") - outPath = r"D:\Users\tyler\Desktop\TEST\expanded.smpx" - expandedExportAbc(outPath, "Face_SIMPLEX", master, client) +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +from pysimplex import PySimplex + +from ..interface.mayaInterface import DCC, disconnected +from ..items import Combo, Slider, Traversal, Simplex +from .alembicCommon import buildSmpx + +try: + import numpy as np +except ImportError: + np = None + + +def _setSliders(ctrl, val, svs): + slis, vals = svs.setdefault(ctrl.simplex, ([], [])) + + if isinstance(ctrl, Slider): + slis.append(ctrl) + vals.append(val) + elif isinstance(ctrl, Combo): + for cp in ctrl.pairs: + slis.append(cp.slider) + vals.append(cp.value * abs(val)) + elif isinstance(ctrl, Traversal): + # set the ctrl.multiplierCtrl.controller to 1 + multCtrl = ctrl.multiplierCtrl.controller + multVal = ctrl.multiplierCtrl.value if val != 0.0 else 0.0 + progCtrl = ctrl.progressCtrl.controller + _setSliders(multCtrl, multVal, svs) + _setSliders(progCtrl, val, svs) + + +def setSliderGroup(ctrls, val): + """Set a group of controls to a given value + + Parameters + ---------- + ctrls : [object, ...] + A list of simplex objects + val : float + The value to set + + Returns + ------- + + """ + svs = {} + for ctrl in ctrls: + _setSliders(ctrl, val, svs) + + for smpx, (slis, vals) in svs.items(): + smpx.setSlidersWeights(slis, vals) + + +def clientPartition(master, clients): + """ + + Parameters + ---------- + master : Simplex + The master simplex system + clients : [Simplex, ...] + The client simplex systems + + Returns + ------- + : {str: [Slider, ...]} + Dictionary of name to slider + : {str: [Combo, ...]} + Dictionary of name to combo + : {str: [Traversal, ...]} + Dictionary of name to traversal + + """ + sliders, combos, traversals = {}, {}, {} + for cli in [master] + clients: + for sli in cli.sliders: + sliders.setdefault(sli.name, []).append(sli) + + for com in cli.combos: + combos.setdefault(com.name, []).append(com) + + for trav in cli.traversals: + traversals.setdefault(trav.name, []).append(trav) + return sliders, combos, traversals + + +def zeroAll(smpxs): + """Set all sliders on the given simplex systems to 0 + + Parameters + ---------- + smpxs : [Simplex, ...] + A list of Simplex systems + + Returns + ------- + + """ + for smpx in smpxs: + smpx.setSlidersWeights(smpx.sliders, [0.0] * len(smpx.sliders)) + + +def getExpandedData(master, clients, mesh): + """Get the fully expanded shape data for each slider, combo, and traversal + at each of its underlying shapes + + Parameters + ---------- + master : Simplex + The master simplex system + clients : [Simplex, ...] + The client simplex systems + mesh : str + The name of the maya shape node + + Returns + ------- + : np.array + The rest shape point positions + : np.array + The slider shape point positions + : np.array + The combo shape point positions + : np.array + The traversal shape point positions + + """ + # zero everything + zeroAll([master] + clients) + sliPart, cmbPart, travPart = clientPartition(master, clients) + sliderShapes = {} + for slider in master.sliders: + ss = {} + sliderShapes[slider] = ss + for pp in slider.prog.pairs: + if pp.shape.isRest: + continue + setSliderGroup(sliPart[slider.name], pp.value) + ss[pp] = DCC.getNumpyShape(mesh) + setSliderGroup(sliPart[slider.name], 0.0) + + # Disable traversals + zeroAll([master] + clients) + travShapeThings = [] + comboShapes = {} + for trav in master.traversals: + for pp in trav.prog.pairs: + if pp.shape.isRest: + continue + travShapeThings.append(pp.shape) + travShapeThings = [i.thing for i in travShapeThings] + + # Get the combos with traversals disconnected + with disconnected(travShapeThings): + for combo in master.combos: + ss = {} + comboShapes[combo] = ss + for pp in combo.prog.pairs: + if pp.shape.isRest: + continue + setSliderGroup(cmbPart[combo.name], pp.value) + ss[pp] = DCC.getNumpyShape(mesh) + setSliderGroup(cmbPart[combo.name], 0.0) + + zeroAll([master] + clients) + travShapes = {} + for trav in master.traversals: + ss = {} + travShapes[trav] = ss + progCtrl = trav.progressCtrl.controller + progVal = trav.progressCtrl.value + for pp in progCtrl.prog.pairs: + if pp.shape.isRest: + continue + if pp.value * progVal <= 0.0: + # Traversals only activate if the progression is in the same + # pos/neg direction as the value. Otherwise we just skip + continue + + setSliderGroup(travPart[trav.name], pp.value) + ss[pp] = DCC.getNumpyShape(mesh) + setSliderGroup(travPart[trav.name], 0.0) + + zeroAll([master] + clients) + restShape = DCC.getNumpyShape(mesh) + + return restShape, sliderShapes, comboShapes, travShapes + + +def _setInputs(inVec, item, indexBySlider, value): + """Being clever + Sliders or Combos just set the value and return + Traversals recursively call this function with the controllers (that only either sliders or combos) + """ + if isinstance(item, Slider): + inVec[indexBySlider[item]] = value + return inVec + elif isinstance(item, Combo): + for pair in item.pairs: + inVec[indexBySlider[pair.slider]] = pair.value * abs(value) + return inVec + elif isinstance(item, Traversal): + inVec = _setInputs( + inVec, + item.multiplierCtrl.controller, + indexBySlider, + item.multiplierCtrl.value, + ) + inVec = _setInputs( + inVec, + item.progressCtrl.controller, + indexBySlider, + item.progressCtrl.value * value, + ) + return inVec + raise ValueError( + "Not a Slider, Combo, or Traversal. Got type {0}: {1}".format(type(item), item) + ) + + +def _buildSolverInputs(simplex, item, value, indexBySlider): + """Build an input vector for the solver that will + produce a required progression value on an item + """ + inVec = [0.0] * len(simplex.sliders) + return _setInputs(inVec, item, indexBySlider, value) + + +def getTravDepth(trav): + """Get the depth of a traversal object + + Parameters + ---------- + trav : Traversal + The traversal object + + Returns + ------- + : int + The depth of the given Traversal + + """ + inputs = [] + mult = trav.multiplierCtrl.controller + prog = trav.progressCtrl.controller + for item in (mult, prog): + if isinstance(item, Slider): + inputs.append(item) + elif isinstance(item, Combo): + for cp in item.pairs: + inputs.append(cp.slider) + return len(set(inputs)) + + +def parseExpandedData(smpx, restShape, sliderShapes, comboShapes, travShapes): + """Turn the expanded data into shapeDeltas connected to the actual Shape objects + + Parameters + ---------- + smpx : Simplex + A simplex system + restShape : np.array + The rest point positions + sliderShapes : np.array + The slider shape point positions + comboShapes : np.array + The combo shape point positions + travShapes : np.array + The traversal shape point positions + + Returns + ------- + : np.array + The point positions for a new set of shapes + + """ + if np is None: + raise RuntimeError("Numpy is not available") + + solver = PySimplex(smpx.dump()) + shapeArray = np.zeros((len(smpx.shapes), len(restShape), 3)) + + indexBySlider = {s: i for i, s in enumerate(smpx.sliders)} + indexByShape = {s: i for i, s in enumerate(smpx.shapes)} + + floatShapeSet = set(smpx.getFloatingShapes()) + floatIdxs = sorted({indexByShape[s] for s in floatShapeSet}) + travShapeSet = {pp.shape for t in smpx.traversals for pp in t.prog.pairs} + travIdxs = sorted({indexByShape[s] for s in travShapeSet}) + + # Sliders are simple, just set their shapes directly + for ppDict in sliderShapes.values(): + for pp, shp in ppDict.items(): + shapeArray[indexByShape[pp.shape]] = shp - restShape + + # First sort the combos by depth + comboByDepth = {} + for combo in smpx.combos: + comboByDepth.setdefault(len(combo.pairs), []).append(combo) + + for depth in sorted(comboByDepth.keys()): + for combo in comboByDepth[depth]: + for pp, shp in comboShapes[combo].items(): + inVec = _buildSolverInputs(smpx, combo, pp.value, indexBySlider) + outVec = np.array(solver.solve(inVec)) + outVec[np.where(np.isclose(outVec, 0.0))] = 0.0 + outVec[np.where(np.isclose(outVec, 1.0))] = 1.0 + outVec[indexByShape[pp.shape]] = 0.0 + + # ignore any traversals + outVec[travIdxs] = 0.0 + + # ignore floaters if we're not currently checking floaters + if pp.shape not in floatShapeSet: + outVec[floatIdxs] = 0.0 + + # set the shape delta to the output + baseShape = np.dot(outVec, shapeArray.swapaxes(0, 1)) + shapeArray[indexByShape[pp.shape]] = shp - restShape - baseShape + + # First the traversals by depth + travByDepth = {} + for trav in smpx.traversals: + travByDepth.setdefault(getTravDepth(trav), []).append(trav) + + for depth in sorted(travByDepth.keys()): + for trav in travByDepth[depth]: + for pp, shp in travShapes[trav].items(): + inVec = _buildSolverInputs(smpx, trav, pp.value, indexBySlider) + outVec = np.array(solver.solve(inVec)) + outVec[np.where(np.isclose(outVec, 0.0))] = 0.0 + outVec[np.where(np.isclose(outVec, 1.0))] = 1.0 + outVec[indexByShape[pp.shape]] = 0.0 + + # set the shape delta to the output + baseShape = np.dot(outVec, shapeArray.swapaxes(0, 1)) + shapeArray[indexByShape[pp.shape]] = shp - restShape - baseShape + return shapeArray + + +def buildShapeArray(mesh, master, clients): + """Build the outpu shape array + + Parameters + ---------- + mesh : str + The maya shape node name + master : Simplex + The master simplex system + clients : + The client simplex systems + + Returns + ------- + : np.array + The full output shape array + + """ + restShape, sliderShapes, comboShapes, travShapes = getExpandedData( + master, clients, mesh + ) + shapeArray = parseExpandedData( + master, restShape, sliderShapes, comboShapes, travShapes + ) + shapeArray += restShape[None, ...] + return shapeArray + + +def expandedExportAbc(path, mesh, master, clients=()): + """Export the alembic by re-building the deltas from all of the full shapes + This is required for delta-mushing a system, because the sum of mushed shapes + is not the same as the mushed sum-of-shapes + + Parameters + ---------- + path : str + Output path for the .smpx + mesh : str + The maya shape node name + master : Simplex + The master simplex system + clients : [Simplex, ...] + The client simplex systems + + Returns + ------- + + """ + # Convert clients to a list if need be + if not clients: + clients = [] + elif not isinstance(clients, list): + if isinstance(clients, tuple): + clients = list(clients) + else: + clients = [clients] + + faces, counts, uvs = DCC.getAbcFaces(mesh) + shapeArray = buildShapeArray(mesh, master, clients) + jsString = str(master.dump()) + + buildSmpx( + path, + shapeArray, + faces, + jsString, + master.name, + faceCounts=counts, + uvs=uvs, + ) + + +if __name__ == "__main__": + # get the smpx from the UI + + master = Simplex.buildSystemFromMesh("Face_SIMPLEX", "Face") + client = Simplex.buildSystemFromMesh("Face_SIMPLEX2", "Face2") + outPath = r"D:\Users\tyler\Desktop\TEST\expanded.smpx" + expandedExportAbc(outPath, "Face_SIMPLEX", master, client) diff --git a/src/python/simplexui/commands/hdf5Convert.py b/src/python/simplexui/commands/hdf5Convert.py index 17194d0f..46f2ea50 100644 --- a/src/python/simplexui/commands/hdf5Convert.py +++ b/src/python/simplexui/commands/hdf5Convert.py @@ -1,60 +1,60 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -import json - -from .alembicCommon import buildSmpx, readSmpx - - -def hdf5Convert(inPath, outPath, ogawa=False): - """Load and parse all the data from a simplex file - - Parameters - ---------- - inPath : str - The input .smpx file path - outPath : str - The output .smpx file path - ogawa : bool - Whether to write out in Ogawa format. Defaults False - - Returns - ------- - - """ - jsString, counts, verts, faces, uvs, uvFaces = readSmpx(inPath) - - js = json.loads(jsString) - name = js["systemName"] - - buildSmpx( - outPath, - verts, - faces, - jsString, - name, - faceCounts=counts, - uvs=uvs, - uvFaces=uvFaces, - ogawa=ogawa, - ) - - -if __name__ == "__main__": - inPath = r"D:\Users\tyler\Desktop\Head_Morphs_Main_Head-Face_v0010.smpx" - outPath = r"D:\Users\tyler\Desktop\Head_ogawa.smpx" - hdf5Convert(inPath, outPath, ogawa=True) +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +import json + +from .alembicCommon import buildSmpx, readSmpx + + +def hdf5Convert(inPath, outPath, ogawa=False): + """Load and parse all the data from a simplex file + + Parameters + ---------- + inPath : str + The input .smpx file path + outPath : str + The output .smpx file path + ogawa : bool + Whether to write out in Ogawa format. Defaults False + + Returns + ------- + + """ + jsString, counts, verts, faces, uvs, uvFaces = readSmpx(inPath) + + js = json.loads(jsString) + name = js["systemName"] + + buildSmpx( + outPath, + verts, + faces, + jsString, + name, + faceCounts=counts, + uvs=uvs, + uvFaces=uvFaces, + ogawa=ogawa, + ) + + +if __name__ == "__main__": + inPath = r"D:\Users\tyler\Desktop\Head_Morphs_Main_Head-Face_v0010.smpx" + outPath = r"D:\Users\tyler\Desktop\Head_ogawa.smpx" + hdf5Convert(inPath, outPath, ogawa=True) diff --git a/src/python/simplexui/commands/mayaCorrectiveInterface.py b/src/python/simplexui/commands/mayaCorrectiveInterface.py index f4258139..f928d361 100644 --- a/src/python/simplexui/commands/mayaCorrectiveInterface.py +++ b/src/python/simplexui/commands/mayaCorrectiveInterface.py @@ -1,161 +1,161 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -"""Get the corrective deltas from a rig in Maya""" - -from ctypes import c_float - -from maya import OpenMaya as om -from maya import cmds - -try: - import numpy as np -except ImportError: - pass - - -def setPose(pvp, multiplier): - """Set a percentage of a pose - - Parameters - ---------- - pvp : [(str, float), ...] - A list of property/value pairs - multiplier : float - The percentage multiplier of the pose - """ - for prop, val in pvp: - cmds.setAttr(prop, val * multiplier) - - -def resetPose(pvp): - """Reset everything back to rest - - Parameters - ---------- - pvp : [(str, float), ...] - A list of property/value pairs - """ - for prop, _val in pvp: - cmds.setAttr(prop, 0) - - -def _getDagPath(mesh): - sl = om.MSelectionList() - sl.add(mesh) - dagPath = om.MDagPath() - sl.getDagPath(0, dagPath) - return dagPath - - -def _getMayaPoints(meshFn): - rawPts = meshFn.getRawPoints() - ptCount = meshFn.numVertices() - cta = (c_float * 3 * ptCount).from_address(int(rawPts)) - out = np.ctypeslib.as_array(cta) - out = np.copy(out) - out = out.reshape((-1, 3)) - return out - - -def getDeformerChain(chkObj): - # Follow the deformer chain - memo = [] - while chkObj and chkObj not in memo: - memo.append(chkObj) - - typ = cmds.nodeType(chkObj) - if typ == "mesh": - cnx = cmds.listConnections( - chkObj + ".inMesh", destination=False, shapes=True - ) or [None] - chkObj = cnx[0] - elif typ == "groupParts": - cnx = cmds.listConnections( - chkObj + ".inputGeometry", destination=False, shapes=True - ) or [None] - chkObj = cnx[0] - elif typ == "polySoftEdge": - cnx = cmds.listConnections( - chkObj + ".inputPolymesh", destination=False, shapes=True - ) or [None] - chkObj = cnx[0] - elif typ == "AlembicNode": - # Alembic nodes aren't part of the deformer chain - # Cut it off, and return - return memo[:-1] - else: - cnx = cmds.ls(chkObj, type="geometryFilter") or [None] - chkObj = cnx[0] - if chkObj: # we have a deformer - # Get the mesh index of this deformer - cnx = cmds.listConnections( - chkObj, connections=True, plugs=True, source=False - ) - prev = cmds.ls(memo[-2])[0] # Get the minimal unique name for testing - defIdx = 0 - for i in range(0, len(cnx), 2): - if cnx[i + 1].startswith(prev): - defIdx = int(cnx[i].split("[")[-1][:-1]) - break - # Use that mesh index to get the output - cnx = cmds.listConnections( - chkObj + ".input[{0}].inputGeometry".format(defIdx), - destination=False, - shapes=True, - ) or [None] - chkObj = cnx[0] - - return memo - - -def getShiftValues(thing): - """Shift the vertices along each axis *before* the skinning - op in the deformer history - - Parameters - ---------- - mesh : str - The name of a mesh - - Returns - ------- - : [vert, ...] - A list of un-shifted vertices - : [vert, ...] - A list of vertices pre-shifted by 1 along the X axis - : [vert, ...] - A list of vertices pre-shifted by 1 along the Y axis - : [vert, ...] - A list of vertices pre-shifted by 1 along the Z axis - """ - orig = getDeformerChain(thing)[-1] - - dp = _getDagPath(thing) - meshFn = om.MFnMesh(dp) - allVerts = "{0}.vtx[*]".format(orig) - - zero = _getMayaPoints(meshFn) - cmds.move(1, 0, 0, allVerts, relative=1, objectSpace=1) - oneX = _getMayaPoints(meshFn) - cmds.move(-1, 1, 0, allVerts, relative=1, objectSpace=1) - oneY = _getMayaPoints(meshFn) - cmds.move(0, -1, 1, allVerts, relative=1, objectSpace=1) - oneZ = _getMayaPoints(meshFn) - cmds.move(0, 0, -1, allVerts, relative=1, objectSpace=1) - - return zero, oneX, oneY, oneZ +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +"""Get the corrective deltas from a rig in Maya""" + +from ctypes import c_float + +from maya import OpenMaya as om +from maya import cmds + +try: + import numpy as np +except ImportError: + pass + + +def setPose(pvp, multiplier): + """Set a percentage of a pose + + Parameters + ---------- + pvp : [(str, float), ...] + A list of property/value pairs + multiplier : float + The percentage multiplier of the pose + """ + for prop, val in pvp: + cmds.setAttr(prop, val * multiplier) + + +def resetPose(pvp): + """Reset everything back to rest + + Parameters + ---------- + pvp : [(str, float), ...] + A list of property/value pairs + """ + for prop, _val in pvp: + cmds.setAttr(prop, 0) + + +def _getDagPath(mesh): + sl = om.MSelectionList() + sl.add(mesh) + dagPath = om.MDagPath() + sl.getDagPath(0, dagPath) + return dagPath + + +def _getMayaPoints(meshFn): + rawPts = meshFn.getRawPoints() + ptCount = meshFn.numVertices() + cta = (c_float * 3 * ptCount).from_address(int(rawPts)) + out = np.ctypeslib.as_array(cta) + out = np.copy(out) + out = out.reshape((-1, 3)) + return out + + +def getDeformerChain(chkObj): + # Follow the deformer chain + memo = [] + while chkObj and chkObj not in memo: + memo.append(chkObj) + + typ = cmds.nodeType(chkObj) + if typ == "mesh": + cnx = cmds.listConnections( + chkObj + ".inMesh", destination=False, shapes=True + ) or [None] + chkObj = cnx[0] + elif typ == "groupParts": + cnx = cmds.listConnections( + chkObj + ".inputGeometry", destination=False, shapes=True + ) or [None] + chkObj = cnx[0] + elif typ == "polySoftEdge": + cnx = cmds.listConnections( + chkObj + ".inputPolymesh", destination=False, shapes=True + ) or [None] + chkObj = cnx[0] + elif typ == "AlembicNode": + # Alembic nodes aren't part of the deformer chain + # Cut it off, and return + return memo[:-1] + else: + cnx = cmds.ls(chkObj, type="geometryFilter") or [None] + chkObj = cnx[0] + if chkObj: # we have a deformer + # Get the mesh index of this deformer + cnx = cmds.listConnections( + chkObj, connections=True, plugs=True, source=False + ) + prev = cmds.ls(memo[-2])[0] # Get the minimal unique name for testing + defIdx = 0 + for i in range(0, len(cnx), 2): + if cnx[i + 1].startswith(prev): + defIdx = int(cnx[i].split("[")[-1][:-1]) + break + # Use that mesh index to get the output + cnx = cmds.listConnections( + chkObj + ".input[{0}].inputGeometry".format(defIdx), + destination=False, + shapes=True, + ) or [None] + chkObj = cnx[0] + + return memo + + +def getShiftValues(thing): + """Shift the vertices along each axis *before* the skinning + op in the deformer history + + Parameters + ---------- + mesh : str + The name of a mesh + + Returns + ------- + : [vert, ...] + A list of un-shifted vertices + : [vert, ...] + A list of vertices pre-shifted by 1 along the X axis + : [vert, ...] + A list of vertices pre-shifted by 1 along the Y axis + : [vert, ...] + A list of vertices pre-shifted by 1 along the Z axis + """ + orig = getDeformerChain(thing)[-1] + + dp = _getDagPath(thing) + meshFn = om.MFnMesh(dp) + allVerts = "{0}.vtx[*]".format(orig) + + zero = _getMayaPoints(meshFn) + cmds.move(1, 0, 0, allVerts, relative=1, objectSpace=1) + oneX = _getMayaPoints(meshFn) + cmds.move(-1, 1, 0, allVerts, relative=1, objectSpace=1) + oneY = _getMayaPoints(meshFn) + cmds.move(0, -1, 1, allVerts, relative=1, objectSpace=1) + oneZ = _getMayaPoints(meshFn) + cmds.move(0, 0, -1, allVerts, relative=1, objectSpace=1) + + return zero, oneX, oneY, oneZ diff --git a/src/python/simplexui/commands/reorderSimplexPoints.py b/src/python/simplexui/commands/reorderSimplexPoints.py index 82ca7eef..11a1d977 100644 --- a/src/python/simplexui/commands/reorderSimplexPoints.py +++ b/src/python/simplexui/commands/reorderSimplexPoints.py @@ -1,102 +1,102 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -"""Transfer shapes between mismatched models - -Given a 1:1 point correspondence, transfer the shapes from one -geometry to another. Figuring out the correspondence is currently -outside the scope of this tool, though I may release one later. - -The point correspondence should look like an unordered range, and -will be used as a numpy index to get the output values. It's also -possible to invert the range if you think you've got it backwards -""" - -# pylint:disable=wrong-import-position -import json - -from .alembicCommon import buildSmpx, readSmpx - -try: - import numpy as np -except ImportError: - pass - - -def reorderSimplexPoints(sourcePath, matchPath, outPath, invertMatch=False): - """Transfer shape data from the sourcePath using the numpy int array - at matchPath to make the final output at outPath - - Parameters - ---------- - sourcePath : str - The source .smpx file path - matchPath : str - The new vert order in numpy, or json format. The data should be an Nx2 array - of integers - outPath : str - The new output .smpx path - invertMatch : bool - Whether to directly apply the match from matchPath, or whether to invert it - - Returns - ------- - - """ - jsString, counts, verts, faces, uvs, uvFaces = readSmpx(sourcePath) - - js = json.loads(jsString) - name = js["systemName"] - - print("Loading Correspondence") - if matchPath.endswith(".json"): - with open(matchPath, "r") as f: - c = json.load(f) - c = np.array(c) - else: - c = np.load(matchPath) - - c = c[c[:, 0].argsort()].T[1] - ci = c.argsort() - if invertMatch: - ci, c = c, ci - - print("Reordering") - verts = verts[:, c, :] - faces = ci[faces] - - buildSmpx( - outPath, - verts, - faces, - jsString, - name, - faceCounts=counts, - uvs=uvs, - uvFaces=uvFaces, - ) - - -if __name__ == "__main__": - import os - - base = r"K:\Departments\CharacterModeling\Library\Head\MaleHead_Standard\005" - _sourcePath = os.path.join(base, "HeadMaleStandard_High_Split_BadOrder.smpx") - _matchPath = os.path.join(base, "Reorder.np") - _outPath = os.path.join(base, "HeadMaleStandard_High_Split2.smpx") - - reorderSimplexPoints(_sourcePath, _matchPath, _outPath) +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +"""Transfer shapes between mismatched models + +Given a 1:1 point correspondence, transfer the shapes from one +geometry to another. Figuring out the correspondence is currently +outside the scope of this tool, though I may release one later. + +The point correspondence should look like an unordered range, and +will be used as a numpy index to get the output values. It's also +possible to invert the range if you think you've got it backwards +""" + +# pylint:disable=wrong-import-position +import json + +from .alembicCommon import buildSmpx, readSmpx + +try: + import numpy as np +except ImportError: + pass + + +def reorderSimplexPoints(sourcePath, matchPath, outPath, invertMatch=False): + """Transfer shape data from the sourcePath using the numpy int array + at matchPath to make the final output at outPath + + Parameters + ---------- + sourcePath : str + The source .smpx file path + matchPath : str + The new vert order in numpy, or json format. The data should be an Nx2 array + of integers + outPath : str + The new output .smpx path + invertMatch : bool + Whether to directly apply the match from matchPath, or whether to invert it + + Returns + ------- + + """ + jsString, counts, verts, faces, uvs, uvFaces = readSmpx(sourcePath) + + js = json.loads(jsString) + name = js["systemName"] + + print("Loading Correspondence") + if matchPath.endswith(".json"): + with open(matchPath, "r") as f: + c = json.load(f) + c = np.array(c) + else: + c = np.load(matchPath) + + c = c[c[:, 0].argsort()].T[1] + ci = c.argsort() + if invertMatch: + ci, c = c, ci + + print("Reordering") + verts = verts[:, c, :] + faces = ci[faces] + + buildSmpx( + outPath, + verts, + faces, + jsString, + name, + faceCounts=counts, + uvs=uvs, + uvFaces=uvFaces, + ) + + +if __name__ == "__main__": + import os + + base = r"K:\Departments\CharacterModeling\Library\Head\MaleHead_Standard\005" + _sourcePath = os.path.join(base, "HeadMaleStandard_High_Split_BadOrder.smpx") + _matchPath = os.path.join(base, "Reorder.np") + _outPath = os.path.join(base, "HeadMaleStandard_High_Split2.smpx") + + reorderSimplexPoints(_sourcePath, _matchPath, _outPath) diff --git a/src/python/simplexui/commands/rigidAlign.py b/src/python/simplexui/commands/rigidAlign.py index e51acb6a..02466960 100644 --- a/src/python/simplexui/commands/rigidAlign.py +++ b/src/python/simplexui/commands/rigidAlign.py @@ -1,99 +1,99 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -try: - import numpy as np -except ImportError: - pass - - -def rigidAlign(P, Q, iters=10): - """Rigidly align meshes with matching vert order by a least-squares error. - Uses a variation of an algorithm by Umeyama - Relevant links: - * https://gist.github.com/nh2/bc4e2981b0e213fefd4aaa33edfb3893 (this code) - * http://stackoverflow.com/a/32244818/263061 (solution with scale) - - Parameters - ---------- - P : np.array - Static set of points - Q : np.array - Points to align with non-uniform scale - iters : int - The number of iterations (Defaults to 10) - - Returns - ------- - : np.array - The 4x4 transformation matrix that most closely aligns Q to P - """ - # pylint:disable=invalid-name - assert P.shape == Q.shape - - n, dim = P.shape - assert dim == 3 - - if iters <= 1: - raise ValueError("Must run at least 1 iteration") - - # Get the centroid of each object - Qm = Q.mean(axis=0) - Pm = P.mean(axis=0) - - # Subtract out the centroid to get the basic aligned mesh - cP = P - Pm # centeredP - cQRaw = Q - Qm # centeredQ - - cQ = cQRaw.copy() - cumulation = np.eye(3) # build an accumulator for the rotation - - # Here, we find an approximate rotation and scaling, but only - # keep track of the accumulated rotations. - # Then we apply the non-uniform scale by comparing bounding boxes - # This way we don't get any shear in our matrix, and we relatively - # quickly walk our way towards a minimum - for _ in range(iters): - # Magic? - C = np.dot(cP.T, cQ) / n - V, S, W = np.linalg.svd(C) - - # Handle negative scaling - d = (np.linalg.det(V) * np.linalg.det(W)) < 0.0 - if d: - S[-1] = -S[-1] - V[:, -1] = -V[:, -1] - - # build the rotation matrix for this iteration - # and add it to the accumulation - R = np.dot(V, W) - cumulation = np.dot(cumulation, R.T) - - # Now apply the accumulated rotation to the raw point positions - # Then grab the non-uniform scaling from the bounding box - # And set up cQ for the next iteration - cQ = np.dot(cQRaw, cumulation) - sf = (cP.max(axis=0) - cP.min(axis=0)) / (cQ.max(axis=0) - cQ.min(axis=0)) - cQ = cQ * sf - - # Build the final transformation - csf = cumulation * sf - tran = Pm - Qm.dot(csf) - outMat = np.eye(4) - outMat[:3, :3] = csf - outMat[3, :3] = tran - return outMat +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +try: + import numpy as np +except ImportError: + pass + + +def rigidAlign(P, Q, iters=10): + """Rigidly align meshes with matching vert order by a least-squares error. + Uses a variation of an algorithm by Umeyama + Relevant links: + * https://gist.github.com/nh2/bc4e2981b0e213fefd4aaa33edfb3893 (this code) + * http://stackoverflow.com/a/32244818/263061 (solution with scale) + + Parameters + ---------- + P : np.array + Static set of points + Q : np.array + Points to align with non-uniform scale + iters : int + The number of iterations (Defaults to 10) + + Returns + ------- + : np.array + The 4x4 transformation matrix that most closely aligns Q to P + """ + # pylint:disable=invalid-name + assert P.shape == Q.shape + + n, dim = P.shape + assert dim == 3 + + if iters <= 1: + raise ValueError("Must run at least 1 iteration") + + # Get the centroid of each object + Qm = Q.mean(axis=0) + Pm = P.mean(axis=0) + + # Subtract out the centroid to get the basic aligned mesh + cP = P - Pm # centeredP + cQRaw = Q - Qm # centeredQ + + cQ = cQRaw.copy() + cumulation = np.eye(3) # build an accumulator for the rotation + + # Here, we find an approximate rotation and scaling, but only + # keep track of the accumulated rotations. + # Then we apply the non-uniform scale by comparing bounding boxes + # This way we don't get any shear in our matrix, and we relatively + # quickly walk our way towards a minimum + for _ in range(iters): + # Magic? + C = np.dot(cP.T, cQ) / n + V, S, W = np.linalg.svd(C) + + # Handle negative scaling + d = (np.linalg.det(V) * np.linalg.det(W)) < 0.0 + if d: + S[-1] = -S[-1] + V[:, -1] = -V[:, -1] + + # build the rotation matrix for this iteration + # and add it to the accumulation + R = np.dot(V, W) + cumulation = np.dot(cumulation, R.T) + + # Now apply the accumulated rotation to the raw point positions + # Then grab the non-uniform scaling from the bounding box + # And set up cQ for the next iteration + cQ = np.dot(cQRaw, cumulation) + sf = (cP.max(axis=0) - cP.min(axis=0)) / (cQ.max(axis=0) - cQ.min(axis=0)) + cQ = cQ * sf + + # Build the final transformation + csf = cumulation * sf + tran = Pm - Qm.dot(csf) + outMat = np.eye(4) + outMat[:3, :3] = csf + outMat[3, :3] = tran + return outMat diff --git a/src/python/simplexui/commands/smpxBlend.py b/src/python/simplexui/commands/smpxBlend.py index d8c1b772..e9730db7 100644 --- a/src/python/simplexui/commands/smpxBlend.py +++ b/src/python/simplexui/commands/smpxBlend.py @@ -1,629 +1,629 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -from ..items import ( - Combo, - ComboPair, - Falloff, - Group, - ProgPair, - Progression, - Shape, - Simplex, - Slider, - Traversal, - TravPair, -) - - -class Skip(object): - """A pseudo-singleton object to signify completely skipping the un-matched shape - This will be used by running ``smpxMismatchCheck``, taking the dict, and manually - telling it to ``Skip`` certain shapes - """ - - pass - - -def smpxMismatchCheck(simpA, simpB): - """Search for mismatches between two simplex systems - - Parameters - ---------- - simpA : Simplex - A simplex system - simpB : Simplex - A simplex system - - Returns - ------- - : {str: {str: (object, object)}} - A structure of dict[objectType][objectName] - That returns an ordered pair of objects. The object is None if it doesn't - exist in (simplexA, simplexB) - """ - mmDict = {} - - # saShapeNames = {sa.name: sa for sa in simpA.shapes} - # sbShapeNames = {sb.name: sb for sb in simpB.shapes} - # saShapeNameUnique = saShapeNames.viewkeys() - sbShapeNames.viewkeys() - # sbShapeNameUnique = sbShapeNames.viewkeys() - saShapeNames.viewkeys() - # slnMatch = [(san, None) for san in saShapeNameUnique] + [(None, sbn) for sbn in sbShapeNameUnique] - # mmDict['shape'] = slnMatch - - saSliderNames = {sa.name: sa for sa in simpA.sliders} - sbSliderNames = {sb.name: sb for sb in simpB.sliders} - saSliderNameUnique = saSliderNames.keys() - sbSliderNames.keys() - sbSliderNameUnique = sbSliderNames.keys() - saSliderNames.keys() - slnMatch = {} - for san in saSliderNameUnique: - slnMatch[san] = (san, None) - for sbn in sbSliderNameUnique: - slnMatch[sbn] = (None, sbn) - mmDict["slider"] = slnMatch - - saComboNames = {sa.name: sa for sa in simpA.combos} - sbComboNames = {sb.name: sb for sb in simpB.combos} - saComboNameUnique = saComboNames.keys() - sbComboNames.keys() - sbComboNameUnique = sbComboNames.keys() - saComboNames.keys() - # TODO search for combos with mis-ordered inputs - cnMatch = {} - for can in saComboNameUnique: - cnMatch[can] = (can, None) - for cbn in sbComboNameUnique: - cnMatch[cbn] = (None, cbn) - mmDict["combo"] = cnMatch - - saTravNames = {sa.name: sa for sa in simpA.traversals} - sbTravNames = {sb.name: sb for sb in simpB.traversals} - saTravNameUnique = saTravNames.keys() - sbTravNames.keys() - sbTravNameUnique = sbTravNames.keys() - saTravNames.keys() - # TODO search for travs with mis-ordered inputs - tnMatch = {} - for tan in saTravNameUnique: - tnMatch[tan] = (tan, None) - for tbn in sbTravNameUnique: - tnMatch[tbn] = (None, tbn) - mmDict["traversal"] = tnMatch - - return mmDict - - -def orderedMerge(va, vb): - """Merge two sets, keeping some semblance of order - - Parameters - ---------- - va : set - A set - vb : set - A set - - Returns - ------- - : list - An ordered list - """ - # come up with a better way that keeps some of the input structure - return sorted(set(va) | set(vb)) - - -# Falloffs -def mergeFalloffs(simpA, simpB, outSimp, translation, nameOnly=True): - """Merge the falloffs between two simplex systems - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - A simplex system to merge - outSimp : Simplex - The output simplex that is being built - translation : {object: object} - A dictionary of input objects to output objects - nameOnly : bool - Whether to match the falloffs by name only. - Defaults to True. False is not implemented yet - """ - if not nameOnly: - raise ValueError("Not implemented yet") - aNames = [f.name for f in simpA.falloffs] - aDict = dict(zip(aNames, simpA.falloffs)) - bNames = [f.name for f in simpB.falloffs] - bDict = dict(zip(bNames, simpB.falloffs)) - oNames = orderedMerge(aNames, bNames) - for oName in oNames: - baseFo = aDict.get(oName, bDict[oName]) - if baseFo.splitType == "planar": - data = ( - baseFo.splitType, - baseFo.axis, - baseFo.maxVal, - baseFo.maxHandle, - baseFo.minHandle, - baseFo.minVal, - ) - else: - data = (baseFo.splitType, baseFo.mapName) - fo = Falloff(oName, outSimp, *data) - translation[aDict.get(oName)] = fo - translation[bDict.get(oName)] = fo - - -# Groups -def _mergeGroupSubset(aTypedGroups, bTypedGroups, outSimp, gType, translation): - asG = [g.name for g in aTypedGroups] - bsG = [g.name for g in bTypedGroups] - asGDict = dict(zip(asG, aTypedGroups)) - bsGDict = dict(zip(bsG, bTypedGroups)) - osG = orderedMerge(asG, bsG) - for groupName in osG: - newGroup = Group(groupName, outSimp, gType) - translation[asGDict.get(groupName)] = newGroup - translation[bsGDict.get(groupName)] = newGroup - - -def mergeGroups(simpA, simpB, outSimp, translation): - """Merge the groups from simpA and simpB - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - A simplex system to merge - outSimp : Simplex - The output simplex that is being built - translation : {object: object} - A dictionary of input objects to output objects - """ - _mergeGroupSubset( - simpA.sliderGroups, simpB.sliderGroups, outSimp, Slider, translation - ) - _mergeGroupSubset(simpA.comboGroups, simpB.comboGroups, outSimp, Combo, translation) - _mergeGroupSubset( - simpA.traversalGroups, simpB.traversalGroups, outSimp, Traversal, translation - ) - - -# Shapes -def _blendShape(aShape, bShape, outSimp, blendVal, translation): - if aShape in translation or bShape in translation: - return translation.get(aShape, translation[bShape]) - - newShape = Shape(aShape.name, outSimp, create=True) - - aDeltas = aShape.verts - aShape.simplex.restShape.verts - bDeltas = bShape.verts - bShape.simplex.restShape.verts - deltas = aDeltas * (1.0 - blendVal) + bDeltas * blendVal - newShape.verts = outSimp.restShape.verts + deltas - - translation[aShape] = newShape - translation[bShape] = newShape - return newShape - - -def _copyShape(shape, outSimp, translation, deltaOverride=None): - if shape in translation: - return translation[shape] - newShape = Shape(shape.name, outSimp, create=True) - - if deltaOverride is None: - deltas = shape.verts - shape.simplex.restShape.verts - newShape.verts = outSimp.restShape.verts + deltas - else: - newShape.verts = deltaOverride - - translation[shape] = newShape - return newShape - - -# Progs -def _deltaProg(shape, srcMin, srcMax, tarMin, tarMax, tVal): - srcExtDelta = tVal * (srcMax.verts - srcMin.verts) - srcShpDelta = shape.verts - srcMin.verts - srcOffset = srcShpDelta - srcExtDelta - ret = tVal * (tarMax.verts - tarMin.verts) + srcOffset - return ret - - -def _blendProg(aProg, bProg, outSimp, blendVal, translation): - aVals = [float(pp.value) for pp in aProg.pairs] - bVals = [float(pp.value) for pp in bProg.pairs] - commonVals = sorted(set(aVals) & set(bVals)) - aUniq = set(aVals) - set(commonVals) - bUniq = set(bVals) - set(commonVals) - aValDict = dict(zip(aVals, aProg.pairs)) - bValDict = dict(zip(bVals, bProg.pairs)) - - # First handle all the common values - outValDict = {} - for c in commonVals: - newShape = _blendShape( - aValDict[c].shape, bValDict[c].shape, outSimp, blendVal, translation - ) - outValDict[c] = newShape - - # Then handle any unique extremes - if 1.0 in aUniq: - outValDict[1.0] = _copyShape(aValDict[1.0].shape, outSimp, translation) - if -1.0 in aUniq: - outValDict[-1.0] = _copyShape(aValDict[-1.0].shape, outSimp, translation) - if 1.0 in bUniq: - outValDict[1.0] = _copyShape(bValDict[1.0].shape, outSimp, translation) - if -1.0 in bUniq: - outValDict[-1.0] = _copyShape(bValDict[-1.0].shape, outSimp, translation) - - # Finally handle the unique progressions as deltas off the extremes - outRest = outValDict[0.0] - aRest = aValDict[0.0].shape - for c in aUniq: - aShp = aValDict[c].shape - mm = 1.0 if c > 0.0 else -1.0 - dd = _deltaProg( - aShp, aRest, aValDict[mm].shape, outRest, outValDict[mm], mm * c - ) - outValDict[c] = _copyShape(aShp, outSimp, translation, deltaOverride=dd) - - bRest = bValDict[0.0].shape - for c in bUniq: - bShp = bValDict[c].shape - mm = 1.0 if c > 0.0 else -1.0 - dd = _deltaProg( - bShp, bRest, bValDict[mm].shape, outRest, outValDict[mm], mm * c - ) - outValDict[c] = _copyShape(bShp, outSimp, translation, deltaOverride=dd) - - outPairs = [ - ProgPair(outSimp, outValDict[val], val) for val in sorted(outValDict.keys()) - ] - prog = Progression(aProg.name, outSimp, pairs=outPairs, interp=aProg.interp) - translation[aProg] = prog - translation[bProg] = prog - return prog - - -def _copyProg(prog, outSimp, translation): - pairs = [] - for pp in prog.pairs: - newShape = _copyShape(pp.shape, outSimp, translation) - pair = ProgPair(outSimp, newShape, pp.value) - pairs.append(pair) - outProg = Progression(prog.name, outSimp, pairs=pairs, interp=prog.interp) - translation[prog] = outProg - return outProg - - -# Sliders -def _blendSliders(aSlider, bSlider, outSimp, blendVal, translation): - if aSlider in translation or bSlider in translation: - return translation.get(aSlider, translation[bSlider]) - - group = translation.get(aSlider.group, translation[bSlider.group]) - prog = _blendProg(aSlider.prog, bSlider.prog, outSimp, blendVal, translation) - outSlider = Slider(aSlider.name, outSimp, prog, group) - translation[aSlider] = outSlider - translation[bSlider] = outSlider - return outSlider - - -def _copySlider(slider, outSimp, translation): - if slider in translation: - return translation[slider] - - group = translation[slider.group] - prog = _copyProg(slider.prog, outSimp, translation) - outSlider = Slider(slider.name, outSimp, prog, group) - translation[slider] = outSlider - return outSlider - - -def sliderBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): - """Blend between the sliders of simpA and simpB - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - A simplex system to merge - outSimp : Simplex - The output simplex that is being built - blendVal : float - The 0 to 1 blend value between the simplices - translation : {object: object} - A dictionary of input objects to output objects - mismatch : dict - The crazy mismatch dict from ``smpxMismatchCheck`` - """ - aNames = [s.name for s in simpA.sliders] - aDict = dict(zip(aNames, simpA.sliders)) - bNames = [s.name for s in simpB.sliders] - bDict = dict(zip(bNames, simpB.sliders)) - oNames = orderedMerge(aNames, bNames) - - for oIdx, oName in enumerate(oNames): - print("Copying Slider {0} of {1}: {2}".format(oIdx, len(oNames), oName)) - aName, bName = mismatch["slider"].get(oName, (oName, oName)) - if aName is Skip or bName is Skip: - continue - elif aName is None: - _copySlider(bDict[bName], outSimp, translation) - elif bName is None: - _copySlider(aDict[aName], outSimp, translation) - else: - _blendSliders(aDict[aName], bDict[bName], outSimp, blendVal, translation) - - -# Combos -def _blendCombos(aCombo, bCombo, outSimp, blendVal, translation): - if aCombo in translation or bCombo in translation: - return translation.get(aCombo, translation[bCombo]) - - group = translation.get(aCombo.group, translation[bCombo.group]) - - cPairs = [] - for aPair, bPair in zip(aCombo.pairs, bCombo.pairs): - slider = _blendSliders( - aPair.slider, bPair.slider, outSimp, blendVal, translation - ) - cPairs.append(ComboPair(slider, aPair.value)) - - prog = _blendProg(aCombo.prog, bCombo.prog, outSimp, blendVal, translation) - outCombo = Combo(aCombo.name, outSimp, cPairs, prog, group, aCombo.solveType) - translation[aCombo] = outCombo - translation[bCombo] = outCombo - return outCombo - - -def _copyCombo(combo, outSimp, translation): - if combo in translation: - return translation[combo] - group = translation[combo.group] - - cPairs = [] - for pair in combo.pairs: - slider = _copySlider(pair.slider, outSimp, translation) - cPairs.append(ComboPair(slider, pair.value)) - prog = _copyProg(combo.prog, outSimp, translation) - - outCombo = Combo(combo.name, outSimp, cPairs, prog, group, combo.solveType) - translation[combo] = outCombo - return outCombo - - -def comboBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): - """Blend between the combos of simpA and simpB - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - A simplex system to merge - outSimp : Simplex - The output simplex that is being built - blendVal : float - The 0 to 1 blend value between the simplices - translation : {object: object} - A dictionary of input objects to output objects - mismatch : dict - The crazy mismatch dict from ``smpxMismatchCheck`` - """ - aNames = [s.name for s in simpA.combos] - aDict = dict(zip(aNames, simpA.combos)) - bNames = [s.name for s in simpB.combos] - bDict = dict(zip(bNames, simpB.combos)) - oNames = orderedMerge(aNames, bNames) - - for oIdx, oName in enumerate(oNames): - print("Copying Combo {0} of {1}: {2}".format(oIdx, len(oNames), oName)) - aName, bName = mismatch["combo"].get(oName, (oName, oName)) - - if oName in mismatch: - print("Getting", oName) - print("Mismatch Get", oName in mismatch, aName, bName) - - if aName is Skip or bName is Skip: - continue - elif aName is None: - _copyCombo(bDict[bName], outSimp, translation) - elif bName is None: - _copyCombo(aDict[aName], outSimp, translation) - else: - _blendCombos(aDict[aName], bDict[bName], outSimp, blendVal, translation) - - -# Traversals -def _blendController(aItem, bItem, outSimp, blendVal, translation): - aCtrl = aItem.controller - bCtrl = bItem.controller - - if isinstance(aCtrl, Slider): - ctrl = _blendSliders(aCtrl, bCtrl, outSimp, blendVal, translation) - elif isinstance(aCtrl, Combo): - ctrl = _blendCombos(aCtrl, bCtrl, outSimp, blendVal, translation) - else: - raise ValueError("Bad object type: {0} {1}".format(aCtrl, type(aCtrl))) - return TravPair(ctrl, aItem.value, aItem.usage) - - -def _copyController(item, outSimp, translation): - iCtrl = item.controller - if isinstance(iCtrl, Slider): - ctrl = _copySlider(iCtrl, outSimp, translation) - elif isinstance(iCtrl, Combo): - ctrl = _copyCombo(iCtrl, outSimp, translation) - else: - raise ValueError("Bad object type: {0} {1}".format(iCtrl, type(iCtrl))) - return TravPair(ctrl, item.value, item.usage) - - -def _blendTraversals(aTrav, bTrav, outSimp, blendVal, translation): - group = translation.get(aTrav.group, translation[bTrav.group]) - multCtrl = _blendController( - aTrav.multiplierCtrl, bTrav.multiplierCtrl, outSimp, blendVal, translation - ) - progCtrl = _blendController( - aTrav.progressCtrl, bTrav.progressCtrl, outSimp, blendVal, translation - ) - prog = _blendProg(aTrav.prog, bTrav.prog, outSimp, blendVal, translation) - outTrav = Traversal(aTrav.name, outSimp, multCtrl, progCtrl, prog, group) - translation[aTrav] = outTrav - translation[bTrav] = outTrav - return outTrav - - -def _copyTraversal(traversal, outSimp, translation): - group = translation[traversal.group] - multCtrl = _copyController(traversal.multiplierCtrl, outSimp, translation) - progCtrl = _copyController(traversal.progressCtrl, outSimp, translation) - prog = _copyProg(traversal.prog, outSimp, translation) - outTrav = Traversal(traversal.name, outSimp, multCtrl, progCtrl, prog, group) - translation[traversal] = outTrav - return outTrav - - -def traversalBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): - """Blend between the traversals of simpA and simpB - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - A simplex system to merge - outSimp : Simplex - The output simplex that is being built - blendVal : float - The 0 to 1 blend value between the simplices - translation : {object: object} - A dictionary of input objects to output objects - mismatch : dict - The crazy mismatch dict from ``smpxMismatchCheck`` - """ - aNames = [s.name for s in simpA.traversals] - aDict = dict(zip(aNames, simpA.traversals)) - bNames = [s.name for s in simpB.traversals] - bDict = dict(zip(bNames, simpB.traversals)) - oNames = orderedMerge(aNames, bNames) - - for oIdx, oName in enumerate(oNames): - print("Copying Traversal {0} of {1}: {2}".format(oIdx, len(oNames), oName)) - aName, bName = mismatch["traversal"].get(oName, (oName, oName)) - if aName is Skip or bName is Skip: - continue - elif aName is None: - _copyTraversal(bDict[bName], outSimp, translation) - elif bName is None: - _copyTraversal(aDict[aName], outSimp, translation) - else: - _blendTraversals(aDict[aName], bDict[bName], outSimp, blendVal, translation) - - -# Simplex -def smpxBlend(simpA, simpB, blendVal=0.50, mismatchDict=None, name="Face"): - """Blend the deltas from simplexA and simplexB. Apply the output to simplexA - Equal falloffs will be combined. Others will be given suffixes - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - The Simplex to be merged into simpA - blendVal : float - A value between 0 and 1, where 0 is fully simpA, and 1 is fully simpB. Defaults to 0.5 - mismatchDict : dict or None - A dictionary like the one returned from ``spxMismatchCheck``. Defaults to None - name : str - The new name of the output system - - Returns - ------- - : Simplex - A new simplex system blended between the two inputs - - """ - mismatchDict = mismatchDict or {} - simpA.stack.enabled = False - simpB.stack.enabled = False - - outSimp = Simplex(name=name, forceDummy=True) - outSimp.stack.enabled = False - - dcc = outSimp.DCC - # TODO: Maybe add accessors to the simplex or DCC - dcc._faces = simpA.DCC._faces - dcc._counts = simpA.DCC._counts - dcc._uvs = simpA.DCC._uvs - dcc._falloffs = simpA.DCC._falloffs - dcc._numVerts = simpA.DCC._numVerts - - rest = Shape.buildRest(outSimp) - rest.verts = ( - simpA.restShape.verts * (1 - blendVal) + simpB.restShape.verts * blendVal - ) - - outSimp.restShape = rest - - # When walking through this smpx file, keep a "translation" dictionary - # where translation[InputObjectID] = OutputObject - translation = {} - translation[simpA.restShape] = rest - translation[simpB.restShape] = rest - - # Merge the groups. This one is easy. Merge by name - mergeGroups(simpA, simpB, outSimp, translation) - - # Merge the falloffs. We can either require them to have equal values to "merge" - # Or we can just merge by name. The nameOnly kwarg handles this. True for now - mergeFalloffs(simpA, simpB, outSimp, translation, nameOnly=True) - - # Copying one of the "big" object types should create and blend its child prog and shapes - print("Copying Sliders") - sliderBlend(simpA, simpB, outSimp, blendVal, translation, mismatchDict) - print("Copying Combos") - comboBlend(simpA, simpB, outSimp, blendVal, translation, mismatchDict) - print("Copying Traversals") - traversalBlend(simpA, simpB, outSimp, blendVal, translation, mismatchDict) - - # push the verts to the dummy dcc for later export - dcc.pushAllShapeVertices(outSimp.shapes) - return outSimp - - -if __name__ == "__main__": - pathA = r"D:\Users\tyler\Desktop\TEST\GumboA.smpx" - pathB = r"D:\Users\tyler\Desktop\TEST\GumboB.smpx" - outPath = r"D:\Users\tyler\Desktop\TEST\hoping2.smpx" - - print("Loading SimpA") - simpA = Simplex.buildSystemFromSmpx(pathA, forceDummy=True) - print("Loading SimpB") - simpB = Simplex.buildSystemFromSmpx(pathB, forceDummy=True) - - print("Matching A:B") - mismatch = smpxMismatchCheck(simpA, simpB) - outSmpx = smpxBlend(simpA, simpB, blendVal=0.5, mismatchDict=mismatch) - - print("Exporting") - print("OUT SMPX", outSmpx) - outSmpx.exportAbc(outPath) - - print("DONE") +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +from ..items import ( + Combo, + ComboPair, + Falloff, + Group, + ProgPair, + Progression, + Shape, + Simplex, + Slider, + Traversal, + TravPair, +) + + +class Skip(object): + """A pseudo-singleton object to signify completely skipping the un-matched shape + This will be used by running ``smpxMismatchCheck``, taking the dict, and manually + telling it to ``Skip`` certain shapes + """ + + pass + + +def smpxMismatchCheck(simpA, simpB): + """Search for mismatches between two simplex systems + + Parameters + ---------- + simpA : Simplex + A simplex system + simpB : Simplex + A simplex system + + Returns + ------- + : {str: {str: (object, object)}} + A structure of dict[objectType][objectName] + That returns an ordered pair of objects. The object is None if it doesn't + exist in (simplexA, simplexB) + """ + mmDict = {} + + # saShapeNames = {sa.name: sa for sa in simpA.shapes} + # sbShapeNames = {sb.name: sb for sb in simpB.shapes} + # saShapeNameUnique = saShapeNames.viewkeys() - sbShapeNames.viewkeys() + # sbShapeNameUnique = sbShapeNames.viewkeys() - saShapeNames.viewkeys() + # slnMatch = [(san, None) for san in saShapeNameUnique] + [(None, sbn) for sbn in sbShapeNameUnique] + # mmDict['shape'] = slnMatch + + saSliderNames = {sa.name: sa for sa in simpA.sliders} + sbSliderNames = {sb.name: sb for sb in simpB.sliders} + saSliderNameUnique = saSliderNames.keys() - sbSliderNames.keys() + sbSliderNameUnique = sbSliderNames.keys() - saSliderNames.keys() + slnMatch = {} + for san in saSliderNameUnique: + slnMatch[san] = (san, None) + for sbn in sbSliderNameUnique: + slnMatch[sbn] = (None, sbn) + mmDict["slider"] = slnMatch + + saComboNames = {sa.name: sa for sa in simpA.combos} + sbComboNames = {sb.name: sb for sb in simpB.combos} + saComboNameUnique = saComboNames.keys() - sbComboNames.keys() + sbComboNameUnique = sbComboNames.keys() - saComboNames.keys() + # TODO search for combos with mis-ordered inputs + cnMatch = {} + for can in saComboNameUnique: + cnMatch[can] = (can, None) + for cbn in sbComboNameUnique: + cnMatch[cbn] = (None, cbn) + mmDict["combo"] = cnMatch + + saTravNames = {sa.name: sa for sa in simpA.traversals} + sbTravNames = {sb.name: sb for sb in simpB.traversals} + saTravNameUnique = saTravNames.keys() - sbTravNames.keys() + sbTravNameUnique = sbTravNames.keys() - saTravNames.keys() + # TODO search for travs with mis-ordered inputs + tnMatch = {} + for tan in saTravNameUnique: + tnMatch[tan] = (tan, None) + for tbn in sbTravNameUnique: + tnMatch[tbn] = (None, tbn) + mmDict["traversal"] = tnMatch + + return mmDict + + +def orderedMerge(va, vb): + """Merge two sets, keeping some semblance of order + + Parameters + ---------- + va : set + A set + vb : set + A set + + Returns + ------- + : list + An ordered list + """ + # come up with a better way that keeps some of the input structure + return sorted(set(va) | set(vb)) + + +# Falloffs +def mergeFalloffs(simpA, simpB, outSimp, translation, nameOnly=True): + """Merge the falloffs between two simplex systems + + Parameters + ---------- + simpA : Simplex + The master Simplex + simpB : Simplex + A simplex system to merge + outSimp : Simplex + The output simplex that is being built + translation : {object: object} + A dictionary of input objects to output objects + nameOnly : bool + Whether to match the falloffs by name only. + Defaults to True. False is not implemented yet + """ + if not nameOnly: + raise ValueError("Not implemented yet") + aNames = [f.name for f in simpA.falloffs] + aDict = dict(zip(aNames, simpA.falloffs)) + bNames = [f.name for f in simpB.falloffs] + bDict = dict(zip(bNames, simpB.falloffs)) + oNames = orderedMerge(aNames, bNames) + for oName in oNames: + baseFo = aDict.get(oName, bDict[oName]) + if baseFo.splitType == "planar": + data = ( + baseFo.splitType, + baseFo.axis, + baseFo.maxVal, + baseFo.maxHandle, + baseFo.minHandle, + baseFo.minVal, + ) + else: + data = (baseFo.splitType, baseFo.mapName) + fo = Falloff(oName, outSimp, *data) + translation[aDict.get(oName)] = fo + translation[bDict.get(oName)] = fo + + +# Groups +def _mergeGroupSubset(aTypedGroups, bTypedGroups, outSimp, gType, translation): + asG = [g.name for g in aTypedGroups] + bsG = [g.name for g in bTypedGroups] + asGDict = dict(zip(asG, aTypedGroups)) + bsGDict = dict(zip(bsG, bTypedGroups)) + osG = orderedMerge(asG, bsG) + for groupName in osG: + newGroup = Group(groupName, outSimp, gType) + translation[asGDict.get(groupName)] = newGroup + translation[bsGDict.get(groupName)] = newGroup + + +def mergeGroups(simpA, simpB, outSimp, translation): + """Merge the groups from simpA and simpB + + Parameters + ---------- + simpA : Simplex + The master Simplex + simpB : Simplex + A simplex system to merge + outSimp : Simplex + The output simplex that is being built + translation : {object: object} + A dictionary of input objects to output objects + """ + _mergeGroupSubset( + simpA.sliderGroups, simpB.sliderGroups, outSimp, Slider, translation + ) + _mergeGroupSubset(simpA.comboGroups, simpB.comboGroups, outSimp, Combo, translation) + _mergeGroupSubset( + simpA.traversalGroups, simpB.traversalGroups, outSimp, Traversal, translation + ) + + +# Shapes +def _blendShape(aShape, bShape, outSimp, blendVal, translation): + if aShape in translation or bShape in translation: + return translation.get(aShape, translation[bShape]) + + newShape = Shape(aShape.name, outSimp, create=True) + + aDeltas = aShape.verts - aShape.simplex.restShape.verts + bDeltas = bShape.verts - bShape.simplex.restShape.verts + deltas = aDeltas * (1.0 - blendVal) + bDeltas * blendVal + newShape.verts = outSimp.restShape.verts + deltas + + translation[aShape] = newShape + translation[bShape] = newShape + return newShape + + +def _copyShape(shape, outSimp, translation, deltaOverride=None): + if shape in translation: + return translation[shape] + newShape = Shape(shape.name, outSimp, create=True) + + if deltaOverride is None: + deltas = shape.verts - shape.simplex.restShape.verts + newShape.verts = outSimp.restShape.verts + deltas + else: + newShape.verts = deltaOverride + + translation[shape] = newShape + return newShape + + +# Progs +def _deltaProg(shape, srcMin, srcMax, tarMin, tarMax, tVal): + srcExtDelta = tVal * (srcMax.verts - srcMin.verts) + srcShpDelta = shape.verts - srcMin.verts + srcOffset = srcShpDelta - srcExtDelta + ret = tVal * (tarMax.verts - tarMin.verts) + srcOffset + return ret + + +def _blendProg(aProg, bProg, outSimp, blendVal, translation): + aVals = [float(pp.value) for pp in aProg.pairs] + bVals = [float(pp.value) for pp in bProg.pairs] + commonVals = sorted(set(aVals) & set(bVals)) + aUniq = set(aVals) - set(commonVals) + bUniq = set(bVals) - set(commonVals) + aValDict = dict(zip(aVals, aProg.pairs)) + bValDict = dict(zip(bVals, bProg.pairs)) + + # First handle all the common values + outValDict = {} + for c in commonVals: + newShape = _blendShape( + aValDict[c].shape, bValDict[c].shape, outSimp, blendVal, translation + ) + outValDict[c] = newShape + + # Then handle any unique extremes + if 1.0 in aUniq: + outValDict[1.0] = _copyShape(aValDict[1.0].shape, outSimp, translation) + if -1.0 in aUniq: + outValDict[-1.0] = _copyShape(aValDict[-1.0].shape, outSimp, translation) + if 1.0 in bUniq: + outValDict[1.0] = _copyShape(bValDict[1.0].shape, outSimp, translation) + if -1.0 in bUniq: + outValDict[-1.0] = _copyShape(bValDict[-1.0].shape, outSimp, translation) + + # Finally handle the unique progressions as deltas off the extremes + outRest = outValDict[0.0] + aRest = aValDict[0.0].shape + for c in aUniq: + aShp = aValDict[c].shape + mm = 1.0 if c > 0.0 else -1.0 + dd = _deltaProg( + aShp, aRest, aValDict[mm].shape, outRest, outValDict[mm], mm * c + ) + outValDict[c] = _copyShape(aShp, outSimp, translation, deltaOverride=dd) + + bRest = bValDict[0.0].shape + for c in bUniq: + bShp = bValDict[c].shape + mm = 1.0 if c > 0.0 else -1.0 + dd = _deltaProg( + bShp, bRest, bValDict[mm].shape, outRest, outValDict[mm], mm * c + ) + outValDict[c] = _copyShape(bShp, outSimp, translation, deltaOverride=dd) + + outPairs = [ + ProgPair(outSimp, outValDict[val], val) for val in sorted(outValDict.keys()) + ] + prog = Progression(aProg.name, outSimp, pairs=outPairs, interp=aProg.interp) + translation[aProg] = prog + translation[bProg] = prog + return prog + + +def _copyProg(prog, outSimp, translation): + pairs = [] + for pp in prog.pairs: + newShape = _copyShape(pp.shape, outSimp, translation) + pair = ProgPair(outSimp, newShape, pp.value) + pairs.append(pair) + outProg = Progression(prog.name, outSimp, pairs=pairs, interp=prog.interp) + translation[prog] = outProg + return outProg + + +# Sliders +def _blendSliders(aSlider, bSlider, outSimp, blendVal, translation): + if aSlider in translation or bSlider in translation: + return translation.get(aSlider, translation[bSlider]) + + group = translation.get(aSlider.group, translation[bSlider.group]) + prog = _blendProg(aSlider.prog, bSlider.prog, outSimp, blendVal, translation) + outSlider = Slider(aSlider.name, outSimp, prog, group) + translation[aSlider] = outSlider + translation[bSlider] = outSlider + return outSlider + + +def _copySlider(slider, outSimp, translation): + if slider in translation: + return translation[slider] + + group = translation[slider.group] + prog = _copyProg(slider.prog, outSimp, translation) + outSlider = Slider(slider.name, outSimp, prog, group) + translation[slider] = outSlider + return outSlider + + +def sliderBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): + """Blend between the sliders of simpA and simpB + + Parameters + ---------- + simpA : Simplex + The master Simplex + simpB : Simplex + A simplex system to merge + outSimp : Simplex + The output simplex that is being built + blendVal : float + The 0 to 1 blend value between the simplices + translation : {object: object} + A dictionary of input objects to output objects + mismatch : dict + The crazy mismatch dict from ``smpxMismatchCheck`` + """ + aNames = [s.name for s in simpA.sliders] + aDict = dict(zip(aNames, simpA.sliders)) + bNames = [s.name for s in simpB.sliders] + bDict = dict(zip(bNames, simpB.sliders)) + oNames = orderedMerge(aNames, bNames) + + for oIdx, oName in enumerate(oNames): + print("Copying Slider {0} of {1}: {2}".format(oIdx, len(oNames), oName)) + aName, bName = mismatch["slider"].get(oName, (oName, oName)) + if aName is Skip or bName is Skip: + continue + elif aName is None: + _copySlider(bDict[bName], outSimp, translation) + elif bName is None: + _copySlider(aDict[aName], outSimp, translation) + else: + _blendSliders(aDict[aName], bDict[bName], outSimp, blendVal, translation) + + +# Combos +def _blendCombos(aCombo, bCombo, outSimp, blendVal, translation): + if aCombo in translation or bCombo in translation: + return translation.get(aCombo, translation[bCombo]) + + group = translation.get(aCombo.group, translation[bCombo.group]) + + cPairs = [] + for aPair, bPair in zip(aCombo.pairs, bCombo.pairs): + slider = _blendSliders( + aPair.slider, bPair.slider, outSimp, blendVal, translation + ) + cPairs.append(ComboPair(slider, aPair.value)) + + prog = _blendProg(aCombo.prog, bCombo.prog, outSimp, blendVal, translation) + outCombo = Combo(aCombo.name, outSimp, cPairs, prog, group, aCombo.solveType) + translation[aCombo] = outCombo + translation[bCombo] = outCombo + return outCombo + + +def _copyCombo(combo, outSimp, translation): + if combo in translation: + return translation[combo] + group = translation[combo.group] + + cPairs = [] + for pair in combo.pairs: + slider = _copySlider(pair.slider, outSimp, translation) + cPairs.append(ComboPair(slider, pair.value)) + prog = _copyProg(combo.prog, outSimp, translation) + + outCombo = Combo(combo.name, outSimp, cPairs, prog, group, combo.solveType) + translation[combo] = outCombo + return outCombo + + +def comboBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): + """Blend between the combos of simpA and simpB + + Parameters + ---------- + simpA : Simplex + The master Simplex + simpB : Simplex + A simplex system to merge + outSimp : Simplex + The output simplex that is being built + blendVal : float + The 0 to 1 blend value between the simplices + translation : {object: object} + A dictionary of input objects to output objects + mismatch : dict + The crazy mismatch dict from ``smpxMismatchCheck`` + """ + aNames = [s.name for s in simpA.combos] + aDict = dict(zip(aNames, simpA.combos)) + bNames = [s.name for s in simpB.combos] + bDict = dict(zip(bNames, simpB.combos)) + oNames = orderedMerge(aNames, bNames) + + for oIdx, oName in enumerate(oNames): + print("Copying Combo {0} of {1}: {2}".format(oIdx, len(oNames), oName)) + aName, bName = mismatch["combo"].get(oName, (oName, oName)) + + if oName in mismatch: + print("Getting", oName) + print("Mismatch Get", oName in mismatch, aName, bName) + + if aName is Skip or bName is Skip: + continue + elif aName is None: + _copyCombo(bDict[bName], outSimp, translation) + elif bName is None: + _copyCombo(aDict[aName], outSimp, translation) + else: + _blendCombos(aDict[aName], bDict[bName], outSimp, blendVal, translation) + + +# Traversals +def _blendController(aItem, bItem, outSimp, blendVal, translation): + aCtrl = aItem.controller + bCtrl = bItem.controller + + if isinstance(aCtrl, Slider): + ctrl = _blendSliders(aCtrl, bCtrl, outSimp, blendVal, translation) + elif isinstance(aCtrl, Combo): + ctrl = _blendCombos(aCtrl, bCtrl, outSimp, blendVal, translation) + else: + raise ValueError("Bad object type: {0} {1}".format(aCtrl, type(aCtrl))) + return TravPair(ctrl, aItem.value, aItem.usage) + + +def _copyController(item, outSimp, translation): + iCtrl = item.controller + if isinstance(iCtrl, Slider): + ctrl = _copySlider(iCtrl, outSimp, translation) + elif isinstance(iCtrl, Combo): + ctrl = _copyCombo(iCtrl, outSimp, translation) + else: + raise ValueError("Bad object type: {0} {1}".format(iCtrl, type(iCtrl))) + return TravPair(ctrl, item.value, item.usage) + + +def _blendTraversals(aTrav, bTrav, outSimp, blendVal, translation): + group = translation.get(aTrav.group, translation[bTrav.group]) + multCtrl = _blendController( + aTrav.multiplierCtrl, bTrav.multiplierCtrl, outSimp, blendVal, translation + ) + progCtrl = _blendController( + aTrav.progressCtrl, bTrav.progressCtrl, outSimp, blendVal, translation + ) + prog = _blendProg(aTrav.prog, bTrav.prog, outSimp, blendVal, translation) + outTrav = Traversal(aTrav.name, outSimp, multCtrl, progCtrl, prog, group) + translation[aTrav] = outTrav + translation[bTrav] = outTrav + return outTrav + + +def _copyTraversal(traversal, outSimp, translation): + group = translation[traversal.group] + multCtrl = _copyController(traversal.multiplierCtrl, outSimp, translation) + progCtrl = _copyController(traversal.progressCtrl, outSimp, translation) + prog = _copyProg(traversal.prog, outSimp, translation) + outTrav = Traversal(traversal.name, outSimp, multCtrl, progCtrl, prog, group) + translation[traversal] = outTrav + return outTrav + + +def traversalBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): + """Blend between the traversals of simpA and simpB + + Parameters + ---------- + simpA : Simplex + The master Simplex + simpB : Simplex + A simplex system to merge + outSimp : Simplex + The output simplex that is being built + blendVal : float + The 0 to 1 blend value between the simplices + translation : {object: object} + A dictionary of input objects to output objects + mismatch : dict + The crazy mismatch dict from ``smpxMismatchCheck`` + """ + aNames = [s.name for s in simpA.traversals] + aDict = dict(zip(aNames, simpA.traversals)) + bNames = [s.name for s in simpB.traversals] + bDict = dict(zip(bNames, simpB.traversals)) + oNames = orderedMerge(aNames, bNames) + + for oIdx, oName in enumerate(oNames): + print("Copying Traversal {0} of {1}: {2}".format(oIdx, len(oNames), oName)) + aName, bName = mismatch["traversal"].get(oName, (oName, oName)) + if aName is Skip or bName is Skip: + continue + elif aName is None: + _copyTraversal(bDict[bName], outSimp, translation) + elif bName is None: + _copyTraversal(aDict[aName], outSimp, translation) + else: + _blendTraversals(aDict[aName], bDict[bName], outSimp, blendVal, translation) + + +# Simplex +def smpxBlend(simpA, simpB, blendVal=0.50, mismatchDict=None, name="Face"): + """Blend the deltas from simplexA and simplexB. Apply the output to simplexA + Equal falloffs will be combined. Others will be given suffixes + + Parameters + ---------- + simpA : Simplex + The master Simplex + simpB : Simplex + The Simplex to be merged into simpA + blendVal : float + A value between 0 and 1, where 0 is fully simpA, and 1 is fully simpB. Defaults to 0.5 + mismatchDict : dict or None + A dictionary like the one returned from ``spxMismatchCheck``. Defaults to None + name : str + The new name of the output system + + Returns + ------- + : Simplex + A new simplex system blended between the two inputs + + """ + mismatchDict = mismatchDict or {} + simpA.stack.enabled = False + simpB.stack.enabled = False + + outSimp = Simplex(name=name, forceDummy=True) + outSimp.stack.enabled = False + + dcc = outSimp.DCC + # TODO: Maybe add accessors to the simplex or DCC + dcc._faces = simpA.DCC._faces + dcc._counts = simpA.DCC._counts + dcc._uvs = simpA.DCC._uvs + dcc._falloffs = simpA.DCC._falloffs + dcc._numVerts = simpA.DCC._numVerts + + rest = Shape.buildRest(outSimp) + rest.verts = ( + simpA.restShape.verts * (1 - blendVal) + simpB.restShape.verts * blendVal + ) + + outSimp.restShape = rest + + # When walking through this smpx file, keep a "translation" dictionary + # where translation[InputObjectID] = OutputObject + translation = {} + translation[simpA.restShape] = rest + translation[simpB.restShape] = rest + + # Merge the groups. This one is easy. Merge by name + mergeGroups(simpA, simpB, outSimp, translation) + + # Merge the falloffs. We can either require them to have equal values to "merge" + # Or we can just merge by name. The nameOnly kwarg handles this. True for now + mergeFalloffs(simpA, simpB, outSimp, translation, nameOnly=True) + + # Copying one of the "big" object types should create and blend its child prog and shapes + print("Copying Sliders") + sliderBlend(simpA, simpB, outSimp, blendVal, translation, mismatchDict) + print("Copying Combos") + comboBlend(simpA, simpB, outSimp, blendVal, translation, mismatchDict) + print("Copying Traversals") + traversalBlend(simpA, simpB, outSimp, blendVal, translation, mismatchDict) + + # push the verts to the dummy dcc for later export + dcc.pushAllShapeVertices(outSimp.shapes) + return outSimp + + +if __name__ == "__main__": + pathA = r"D:\Users\tyler\Desktop\TEST\GumboA.smpx" + pathB = r"D:\Users\tyler\Desktop\TEST\GumboB.smpx" + outPath = r"D:\Users\tyler\Desktop\TEST\hoping2.smpx" + + print("Loading SimpA") + simpA = Simplex.buildSystemFromSmpx(pathA, forceDummy=True) + print("Loading SimpB") + simpB = Simplex.buildSystemFromSmpx(pathB, forceDummy=True) + + print("Matching A:B") + mismatch = smpxMismatchCheck(simpA, simpB) + outSmpx = smpxBlend(simpA, simpB, blendVal=0.5, mismatchDict=mismatch) + + print("Exporting") + print("OUT SMPX", outSmpx) + outSmpx.exportAbc(outPath) + + print("DONE") diff --git a/src/python/simplexui/commands/unsubdivide.py b/src/python/simplexui/commands/unsubdivide.py index 26178ae7..46a0c1d3 100644 --- a/src/python/simplexui/commands/unsubdivide.py +++ b/src/python/simplexui/commands/unsubdivide.py @@ -1,1227 +1,1227 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint: disable=unused-argument, too-many-locals -# pylint:disable=E0611,E0401 - -import json -from itertools import chain, zip_longest - -from Qt.QtWidgets import QApplication -from .alembicCommon import buildSmpx, pbPrint, readSmpx - -try: - import numpy as np -except ImportError: - np = None - - -def mergeCycles(groups): - """Take a list of ordered items, and sort them so the last item of each list matches the first of the next list - - Then return the groups of lists mashed together - For instance, with two cycles: - +---------+------------------------------------------------------------+ - | input | [(1, 2), (11, 12), (3, 1), (10, 11), (2, 3), (12, 10)] | - +---------+------------------------------------------------------------+ - | reorder | [[(1, 2), (2, 3), (3, 1)], [(10, 11), (11, 12), (12, 13)]] | - +---------+------------------------------------------------------------+ - | output | [[1, 2, 3], [10, 11, 12, 13]] | - +---------+------------------------------------------------------------+ - - Also, return whether the cycles merged form a single closed group - - Parameters - ---------- - groups : [(int, int), ...] - A list of pairs of integers - - Returns - ------- - : [[int, ...], ...] - The ordered cycles - """ - groups = [list(g) for g in groups] - heads = {g[0]: g for g in groups} - tails = {g[-1]: g for g in groups} - - headGetter = lambda x: heads.get(x[-1]) # noqa: E731 - headSetter = lambda x, y: x + y[1:] # noqa: E731 - - tailGetter = lambda x: tails.get(x[0]) # noqa: E731 - tailSetter = lambda x, y: y + x[1:] # noqa: E731 - - searches = ((headGetter, headSetter), (tailGetter, tailSetter)) - - out = [] - cycles = [] - while groups: - g = groups.pop() - del heads[g[0]] - del tails[g[-1]] - - for getter, setter in searches: - while True: - adder = getter(g) - if adder is None: - break - g = setter(g, adder) - del heads[adder[0]] - del tails[adder[-1]] - adder[:] = [] - groups = [x for x in groups if x] - - cycle = False - if g[0] == g[-1]: - g.pop() - cycle = True - cycles.append(cycle) - out.append(g) - return out, cycles - - -def grow(neigh, verts, exclude): - """Grow the vertex set, also keeping track of which vertices to ignore for the next iteration - - Parameters - ---------- - neigh : {int: [int, ...]} - A dict mapping a vert index to a list of neighbor vert indices - verts : set(int) - A set of vert indices to grow - exclude : set(int) - A set of vert indices to ignore - - Returns - ------- - : set(int) - The newly grown vertices - : set(int) - ``exclude`` combined with ``verts`` - - """ - grown = set() - growSet = verts - exclude - for v in growSet: - grown.update(neigh[v]) - newGrown = grown - exclude - newExclude = exclude | growSet - return newGrown, newExclude - - -def buildHint(island, neigh, borders): - """Find star points that are an even number of grows from an edge - - Parameters - ---------- - island : [int, ...] - A list of vertices as part of a mesh island - neigh : {int: [int, ...]} - The dictionary of vertex neighbors - borders : set(int) - A set of border vertices - - Returns - ------- - : int - The first star point encountered at an even grow from the given borders - """ - borders = borders & island - if not borders: - # Well ... we don't have any good way of dealing with this - # Best thing I can do is search for a point with the least - # number of similar valences, and return that - d = {} - for v in island: - d.setdefault(len(neigh[v]), []).append(v) - - dd = {} - for k, v in d.items(): - dd.setdefault(len(v), []).append(k) - - mkey = min(dd.keys()) - return d[dd[mkey][0]][0] - - exclude = set() - while borders: - borders, exclude = grow(neigh, borders, exclude) - borders, exclude = grow(neigh, borders, exclude) - for b in borders: - if len(neigh[b]) != 4: - return b - return None - - -def partitionIslands(faces, neigh, pBar=None): - """Find all groups of connected verts - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - neigh : {int: [int, ...]} - The dict of vertex neighbors - pBar : QProgressDialog or None - An optional progress bar - - Returns - ------- - : [set(int), ...] - A list of sets of non-connected vertex islands - """ - allVerts = set(chain.from_iterable(faces)) - islands = [] - count = float(len(allVerts)) - - if pBar is not None: - pBar.setValue(0) - pBar.setMaximum(count) - QApplication.processEvents() - - while allVerts: - verts = {allVerts.pop()} - exclude = set() - while verts: - verts, exclude = grow(neigh, verts, exclude) - islands.append(exclude) - allVerts.difference_update(exclude) - - if pBar is not None: - pBar.setValue(count - len(allVerts)) - QApplication.processEvents() - - return islands - - -def buildUnsubdivideHints(faces, neigh, borders, pBar=None): - """Get one vertex per island that was part of the original mesh - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - neigh : {int: [int, ...]} - The dict of vertex neighbors - borders : set(int) - A set of border vertices - pBar : QProgressDialog or None - An optional progress bar - - Returns - ------- - : [int, ...] - A list of star-points (one per island) to un-subdivide from - """ - islands = partitionIslands(faces, neigh, pBar=pBar) - hints = [] - - if pBar is not None: - pBar.setValue(0) - pBar.setMaximum(len(islands)) - QApplication.processEvents() - - for i, isle in enumerate(islands): - if pBar is not None: - pBar.setValue(i) - QApplication.processEvents() - hints.append(buildHint(isle, neigh, borders)) - - hints = [h for h in hints if h is not None] - return hints - - -def getFaceCenterDel(faces, eNeigh, hints, pBar=None): - """Given a list of hint "keeper" points - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - eNeigh : {int: [int, ...]} - The dict of vertex neighbors - hints : [int, ...] - A list of star-points (one per island) to un-subdivide from - pBar : QProgressDialog or None - An optional progress bar - - Returns - ------- - : set(int) - Centers of the original faces during a subdivision - : bool - Whether the operation failed(True) or not(False) - """ - vertToFaces = {} - vc = set() - for i, face in enumerate(faces): - for f in face: - vertToFaces.setdefault(f, []).append(i) - vc.add(f) - - count = len(vc) - centers = set() - midpoints = set() - originals = set(hints) - queue = set(hints) - - if pBar is not None: - pBar.setValue(0) - pBar.setMaximum(count) - QApplication.processEvents() - - i = 0 - fail = False - while queue: - cur = queue.pop() - if cur in midpoints: - continue - - if pBar is not None: - pBar.setValue(i) - QApplication.processEvents() - i += 2 # Add 2 because I *shouldn't* get any midpoints - - midpoints.update(eNeigh[cur]) - t = centers if cur in originals else originals - - for f in vertToFaces[cur]: - nVerts = faces[f] - - if len(nVerts) != 4: - fail = True - continue - - curFaceIndex = nVerts.index(cur) - - half = int(len(nVerts) / 2) - diag = nVerts[curFaceIndex - half] - - isOrig = diag in originals - isCtr = diag in centers - if not isOrig and not isCtr: - t.add(diag) - queue.add(diag) - elif ( - (isCtr and t is originals) - or (isOrig and t is centers) - or (diag in midpoints) - ): - fail = True - - return centers, fail - - -def getBorders(faces): - """Get the indices of verts along the borders of a mesh - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - - Returns - ------- - : set(int) - A set of border vertices - """ - edgePairs = set() - for face in faces: - for f in range(len(face)): - edgePairs.add((face[f], face[f - 1])) - borders = set() - for ep in edgePairs: - if (ep[1], ep[0]) not in edgePairs: - borders.update(ep) - return borders - - -def buildEdgeDict(faces): - """Build a dictionary of un-ordered neighboring vertices along edges - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - - Returns - ------- - : {int: set(int)} - A dictionary of neighboring vertices along edges - """ - edgeDict = {} - for face in faces: - for f in range(len(face)): - ff = edgeDict.setdefault(face[f - 1], set()) - ff.add(face[f]) - ff.add(face[f - 2]) - return edgeDict - - -def buildNeighborDict(faces): - """Build a structure to ask for edge and face neighboring vertices - The returned neighbor list starts with an edge neighbor, and - proceeds counter clockwise, alternating between edge and face neighbors - Also, while I'm here, grab the border verts - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - - Returns - ------- - : {int: [[int, ...], ...]} - A dictionary keyed from a vert index whose - values are ordered cycles or fans - : set(int) - A set of vertices that are on the border - """ - fanDict = {} - edgeDict = {} - for face in faces: - for i in range(len(face)): - fanDict.setdefault(face[i], []).append(face[i + 1 :] + face[:i]) - ff = edgeDict.setdefault(face[i - 1], set()) - ff.add(face[i]) - ff.add(face[i - 2]) - - borders = set() - out = {} - for k, v in fanDict.items(): - fans, cycles = mergeCycles(v) - for f, c in zip(fans, cycles): - if not c: - borders.update((f[0], f[-1], k)) - out[k] = fans - return out, edgeDict, borders - - -def _fanMatch(fan, uFan, dWings): - """Twist a single fan so it matches the uFan if it can""" - uIdx = uFan[0] - for f, fIdx in enumerate(fan): - dw = dWings.get(fIdx, []) - if uIdx in dw: - return fan[f:] + fan[:f] - return None - - -def _align(neigh, uNeigh, dWings): - """Twist all the neighs so they match the uNeigh""" - out = [] - for uFan in uNeigh: - for fan in neigh: - fm = _fanMatch(fan, uFan, dWings) - if fm is not None: - out.append(fm) - break - return out - - -def buildLayeredNeighborDicts(faces, uFaces, dWings): - """Build and align two neighbor dicts for both faces and uFaces - This guarantees that the neighbors at the same index are analogous - (ie. they go in the same direction) - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - uFaces : [[int, ...], ...] - The list of lists of vertex indices making up un-subdivided faces - dWings : ??? - ??? - - Returns - ------- - - """ - neighDict, edgeDict, borders = buildNeighborDict(faces) - uNeighDict, uEdgeDict, uBorders = buildNeighborDict(uFaces) - - assert ( - borders >= uBorders - ), "Somehow the unsubdivided borders contain different vIdxs" - - for k, uNeigh in uNeighDict.items(): - neighDict[k] = _align(neighDict[k], uNeigh, dWings) - - return neighDict, uNeighDict, edgeDict, uEdgeDict, borders - - -def _findOldPositionBorder( - faces, - uFaces, - verts, - uVerts, - neighDict, - uNeighDict, - edgeDict, - uEdgeDict, - borders, - vIdx, - computed, -): - """Find the position of the un-subdivided mesh if the vertex was on the border""" - nei = neighDict[vIdx][0] - nei = [i for i in nei if i in borders] - assert len(nei) == 2, "Found multi border, {}".format(nei) - uVerts[vIdx] = 2 * verts[vIdx] - ((verts[nei[0]] + verts[nei[1]]) / 2) - computed.add(vIdx) - - -def _findOldPositionSimple( - faces, - uFaces, - verts, - uVerts, - neighDict, - uNeighDict, - edgeDict, - uEdgeDict, - vIdx, - computed, -): - """Find the position of the un-subdivided mesh if the vertex has at least 4 neighbors. - Updates uVerts in-place - """ - neigh = neighDict[vIdx][0] - - eTest = edgeDict[vIdx] - e = [p for p in neigh if p in eTest] - f = [p for p in neigh if p not in eTest] - - es = verts[e].sum(axis=0) - fs = verts[f].sum(axis=0) - - n = len(e) - term1 = verts[vIdx] * (n / (n - 3.0)) - term2 = es * (4 / (n * (n - 3.0))) - term3 = fs * (1 / (n * (n - 3.0))) - vk = term1 - term2 + term3 - - uVerts[vIdx] = vk - computed.add(vIdx) - - -def _findOldPosition3Valence( - faces, - uFaces, - verts, - uVerts, - neighDict, - uNeighDict, - edgeDict, - uEdgeDict, - vIdx, - computed, -): - """Find the position of the un-subdivided mesh if the vertex has exactly 3 neighbors - Updates uVerts in-place. It is possible for this to fail. - - Returns - ------- - : bool - Whether an update happened - """ - neigh = neighDict[vIdx][0] - uNeigh = uNeighDict[vIdx][0] - - eTest = edgeDict[vIdx] - eNeigh = [n for n in neigh if n in eTest] - fNeigh = [n for n in neigh if n not in eTest] - - ueTest = uEdgeDict[vIdx] - ueNeigh = [n for n in uNeigh if n in ueTest] - # ufNeigh = [n for n in uNeigh if n not in ueTest] - - intr = computed.intersection(ueNeigh) - if intr: - # Easy valence 3 case. I only need - # The computed new neighbor - # The midpoint on the edge to that neighbor - # The "face" verts neighboring the midpoint - - # Get the matching subbed an unsubbed neighbor indexes - uNIdx = intr.pop() - nIdx = eNeigh[ueNeigh.index(uNIdx)] - - # Get the "face" verts next to the subbed neighbor - xx = neigh.index(nIdx) - fnIdxs = (neigh[xx - 1], neigh[(xx + 1) % len(neigh)]) - - # Then compute - # vk = 4*k1e - ke - k1fNs[0] - k1fNs[1] - vka = uVerts[uNIdx] + verts[fnIdxs[0]] + verts[fnIdxs[1]] - vkb = verts[nIdx] * 4 - uVerts[vIdx] = vkb - vka - computed.add(vIdx) - return True - - else: - # The Hard valence 3 case. Made even harder - # because the paper has a mistake in it - - # vk = 4*ejk1 + 4*ejpk1 - fjnk1 - fjpk1 - 6*fjk1 + sum(fik) - # where k1 means subdivided mesh - # where j means an index, jn and jp are next/prev adjacents - # sum(fik) is the sum of all the points of the face that - # *aren't* the original, or edge-adjacent - # There could be more than 1 if an n-gon was subdivided - # - # I wonder: If it was a triangle that was subdivided, what - # would sum(fik) because there are no verts that fit that - # description. I think this is a degenerate case - - # First, find an adjacent face on the unsub mesh that - # is only missing the neighbors of the vIdx - - fnIdx = None - fik = None - fCtrIdx = None - for x, v in enumerate(fNeigh): - # working with neigh, but should only ever contain uNeigh indexes - eTest = edgeDict[vIdx] - origFace = {n for n in neighDict[v][0] if n not in eTest} - - check = (origFace - set(ueNeigh)) - {vIdx} - if computed >= check: - fCtrIdx = v - fnIdx = x - fik = sorted(check) - break - - if fnIdx is None: - # No possiblity found - return False - - # Then apply the equation from above - neighIdx = neigh.index(fCtrIdx) - ejnk1 = neigh[(neighIdx + 1) % len(neigh)] - ejpk1 = neigh[neighIdx - 1] - fjnk1 = fNeigh[(fnIdx + 1) % len(fNeigh)] - fjpk1 = fNeigh[fnIdx - 1] - fjk1 = verts[fCtrIdx] - sumFik = uVerts[fik].sum(axis=0) - vk = 4 * ejnk1 + 4 * ejpk1 - fjnk1 - fjpk1 - 6 * fjk1 + sumFik - uVerts[vIdx] = vk - computed.add(vIdx) - return True - return False - - -def deleteCenters(meshFaces, uvFaces, centerDel, pBar=None): - """Delete the given vertices and connected edges from a face representation - to give a new representation. - - Parameters - ---------- - meshFaces : [[int, ...], ...] - The list of lists of vertex indices making up faces - centerDel : set(int) - A set of vertices to delete. These were the vertices added - to the centers of the faces when subdividing. - uvFaces : [[int, ...], ...] - The list of lists of uv indices making up uvFaces - pBar : QProgressDialog or None - An optional progress bar - - Returns - ------- - : [[int, ...], ...] - The new list of faces of the mesh - : [[int, ...], ...] - The new list of uvfaces of the mesh - : {int: (int, int)} - A dict of a deleted edge-midpoint to its two existing neighbor verts - : {int: (int, int)} - A dict of a deleted uv edge-midpoint to its two existing neighbor uvs - """ - # For each deleted index, grab the neighboring faces, - # and twist the faces so the deleted index is first - cds = set(centerDel) - faceDelDict = {} - uvDelDict = {} - uvFaces = uvFaces or [] - for face, uvFace in zip_longest(meshFaces, uvFaces): - fi = cds.intersection(face) - # If we are a subdivided mesh, Then each face will have exactly one - # vertex that is part of the deletion set - if len(fi) != 1: - raise ValueError("Found a face with an unrecognized connectivity") - # Get that one vert - idx = fi.pop() - # Each face is a cycle. Rotate the cycle - # so that idx is first in the list - rv = face.index(idx) - rFace = face[rv:] + face[:rv] - faceDelDict.setdefault(idx, []).append(rFace) - - if uvFace is not None: - rUVFace = uvFace[rv:] + uvFace[:rv] - uvDelDict.setdefault(idx, []).append(rUVFace) - - newFaces = [] - nUVFaces = [] - wings = {} - uvWings = {} - - if pBar is not None: - pBar.setValue(0) - pBar.setMaximum(len(faceDelDict)) - - chk = -1 - for idx, rFaces in faceDelDict.items(): - chk += 1 - if pBar is not None: - pBar.setValue(chk) - QApplication.processEvents() - - ruvFaces = uvDelDict.get(idx, []) - # The faces are guaranteed to be in a single loop cycle - # so I don't have to handle any annoying edge cases! Yay! - faceEnds = { - f[1]: (f[2], f[3], uvf) for f, uvf in zip_longest(rFaces, ruvFaces) - } # face ends - - end = rFaces[-1][-1] # get an arbitrary face to start with - newFace = [] - nUVFace = [] - while faceEnds: - try: - diag, nxt, uvf = faceEnds.pop(end) - except KeyError: - print("rFaces", rFaces) - print("fe", faceEnds) - raise - if uvf is not None: - try: - nUVFace.append(uvf[2]) - uvWings.setdefault(uvf[1], []).append(uvf[2]) - uvWings.setdefault(uvf[3], []).append(uvf[2]) - except IndexError: - print("UVF", uvf, chk) - raise - - newFace.append(diag) - wings.setdefault(end, []).append(diag) - wings.setdefault(nxt, []).append(diag) - - end = nxt - newFaces.append(newFace) - if nUVFace: - nUVFaces.append(nUVFace) - nUVFaces = nUVFaces or None - - return newFaces, nUVFaces, wings, uvWings - - -def fixVerts( - faces, - uFaces, - verts, - neighDict, - uNeighDict, - edgeDict, - uEdgeDict, - borders, - pinned, - pBar=None, -): - """Given the faces, vertex positions, and the point indices that - were created at the face centers for a subdivision step - Return the faces and verts of the mesh from before the - subdivision step. This algorithm doesn't handle UV's yet - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - verts : np.array - The Nx3 array of vertices - centerDel : set(int) - A set of vertices to delete - uFaces : [[int, ...], ...] - The list of lists of vertex indices making up the - un-subdivided faces. Not all numbers in the vert range are in this structure - neighDict : {int: [int, ...]} - The dictionary of vertex neighbors based on ``faces`` - uNeighDict : {int: [int, ...]} - The dictionary of vertex neighbors based on ``uFaces`` - edgeDict : TODO - Some kind of dictionary representing individual edges - uEdgeDict : TODO - Some kind of dictionary representing individual edges in the unsub mesh - borders : set(int) - The set of vertices along the borders - pinned : set(int) - The set of vertices that will not be moved by this process - pBar : QProgressDialog or None - An optional progress bar - - Returns - ------- - : np.array - An array of vertex positions - """ - uVerts = verts.copy() - uIdxs = sorted(set(chain.from_iterable(uFaces))) - - v3Idxs = [] - # bowtie verts are pinned - bowTieIdxs = [] - computed = set() - i = 0 - if pBar is not None: - pBar.setValue(0) - pBar.setMaximum(len(uIdxs)) - - for idx in uIdxs: - if pBar is not None: - pBar.setValue(i) - QApplication.processEvents() - if len(uNeighDict[idx]) > 1: - bowTieIdxs.append(idx) - i += 1 - elif idx in pinned: - pass - elif idx in borders: - _findOldPositionBorder( - faces, - uFaces, - verts, - uVerts, - neighDict, - uNeighDict, - edgeDict, - uEdgeDict, - borders, - idx, - computed, - ) - i += 1 - elif sum(map(len, neighDict[idx])) > 6: # if valence > 3 - _findOldPositionSimple( - faces, - uFaces, - verts, - uVerts, - neighDict, - uNeighDict, - edgeDict, - uEdgeDict, - idx, - computed, - ) - i += 1 - else: - v3Idxs.append(idx) - - updated = True - while updated: - updated = False - rem = set() - for idx in v3Idxs: - up = _findOldPosition3Valence( - faces, - uFaces, - verts, - uVerts, - neighDict, - uNeighDict, - edgeDict, - uEdgeDict, - idx, - computed, - ) - if not up: - continue - if pBar is not None: - pBar.setValue(i) - QApplication.processEvents() - i += 1 - updated = True - rem.add(idx) - v3Idxs = list(set(v3Idxs) - rem) - - return uVerts - - -def getUVPins(faces, borders, uvFaces, uvBorders, pinBorders): - """Find which uvBorders are also mesh borders - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - borders : set(int) - The set of vertices along the borders - uvFaces : [[int, ...], ...] - The list of lists of uv indices making up uvFaces - uvBorders : set(int) - The set of vertices along the borders of uvs - pinBorders : bool - Whether to just pin all uv borders - - Returns - ------- - : set(int) - The vertex indices to pin - """ - if uvFaces is None: - return set() - if pinBorders: - return set(uvBorders) - - pinnit = set() - for face, uvFace in zip(faces, uvFaces): - for i in range(len(face)): - f = face[i] - pf = face[i - 1] - - uv = uvFace[i] - puv = uvFace[i - 1] - - if not (f in borders and pf in borders): - if uv in uvBorders and puv in uvBorders: - pinnit.add(puv) - pinnit.add(uv) - - return uvBorders & pinnit - - -def collapse(faces, verts, uvFaces, uvs): - """Take a mesh representation with unused vertex indices, and remove those vertices - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - The set of integers used here will be non-contiguous - verts : np.array - The N*3 array of vertices - uvFaces : [[int, ...], ...] - The list of lists of uv indices making up uvFaces - The set of integers used here will be non-contiguous - uvs : np.array - The N*2 array of uvs - - Returns - ------- - : [[int, ...], ...] - The list of lists of vertex indices making up faces - The set of integers used here will be contiguous - : np.array - The new N*3 array of vertices - : [[int, ...], ...] - The list of lists of uv indices making up uvFaces - The set of integers used here will be non-contiguous - : np.array - The new N*2 array of uvs - """ - vset = sorted(set(chain.from_iterable(faces))) - nVerts = verts[vset] - vDict = {v: i for i, v in enumerate(vset)} - nFaces = [[vDict[f] for f in face] for face in faces] - - if uvFaces is not None: - uvset = sorted(set(chain.from_iterable(uvFaces))) - nUVs = uvs[uvset] - uvDict = {v: i for i, v in enumerate(uvset)} - nUVFaces = [[uvDict[f] for f in face] for face in uvFaces] - else: - nUVs = None - nUVFaces = None - - return nFaces, nVerts, nUVFaces, nUVs - - -def getCenters(faces, hints=None, pBar=None): - """Given a set of faces, find the face-center vertices from the subdivision - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - hints : set(int) or None - An optional list of star points that were part of the original un-subdivided mesh - pBar : QProgressDialog or None - An optional progress bar - - Returns - ------- - : set(int) - The vertices that were added to the centers of the faces as part of the subdivision process - """ - if pBar is not None: - pBar.setLabelText("Crawling Edges") - pBar.show() - QApplication.processEvents() - - eNeigh = buildEdgeDict(faces) - if hints is None: - borders = getBorders(faces) - hints = buildUnsubdivideHints( - faces, eNeigh, borders, pBar=None - ) # purposely no PBar - centerDel, fail = getFaceCenterDel(faces, eNeigh, hints, pBar=pBar) - assert not fail, "Could not detect subdivided topology with the provided hints" - - if pBar is not None: - pBar.close() - - return centerDel - - -def unSubdivide( - faces, - verts, - uvFaces, - uvs, - hints=None, - repositionVerts=True, - pinBorders=False, - pBar=None, -): - """Given a mesh representation (faces and vertices) remove the edges added - by a subdivision, and optionally reposition the verts - - Parameters - ---------- - faces : [[int, ...], ...] - The list of lists of vertex indices making up faces - verts : np.array - The N*3 array of vertices - uvFaces : [[int, ...], ...] - The list of lists of uv indices making up uvFaces - uvs : np.array - The N*2 array of uvs - hints : set(int) or None - An optional list of star points that were part of the original un-subdivided mesh - repositionVerts : bool - Whether to also estimate the positions of the un-subdivided verts. Defaults to True - pinBorders : bool - Whether or not to pin the border vertices. Defaults to False - pBar : QProgressDialog or None - An optional progress bar - - Returns - ------- - : [[vIdx ...], ...] - The un-subdivided face structure - : np.array - The un-subdivided vertex positions - : [[vIdx ...], ...] or None - The un-subdivided uv-face structure if it exists - : np.array or None - The un-subdivided uvs if they exist - """ - if pBar is not None: - pBar.show() - pBar.setLabelText("Finding Neighbors") - QApplication.processEvents() - - eNeigh = buildEdgeDict(faces) - - if hints is None: - if pBar is not None: - pBar.show() - pBar.setLabelText("Getting Hints") - QApplication.processEvents() - borders = getBorders(faces) - hints = buildUnsubdivideHints( - faces, eNeigh, borders, pBar=None - ) # Purposely no PBar - - if pBar is not None: - pBar.setLabelText("Crawling Edges") - QApplication.processEvents() - centerDel, fail = getFaceCenterDel(faces, eNeigh, hints, pBar=pBar) - assert not fail, "Could not detect subdivided topology with the provided hints" - - if pBar is not None: - pBar.setLabelText("Deleting Edges") - QApplication.processEvents() - uFaces, uUVFaces, dWings, uvDWings = deleteCenters( - faces, uvFaces, centerDel, pBar=pBar - ) - - uVerts = verts - uUVs = uvs - if repositionVerts: - # Handle the verts - if pBar is not None: - pBar.setLabelText("Building Correspondences") - QApplication.processEvents() - neighDict, uNeighDict, edgeDict, uEdgeDict, borders = buildLayeredNeighborDicts( - faces, uFaces, dWings - ) - pinned = set(borders) if pinBorders else [] - if pBar is not None: - pBar.setLabelText("Fixing Vert Positions") - QApplication.processEvents() - uVerts = fixVerts( - faces, - uFaces, - verts, - neighDict, - uNeighDict, - edgeDict, - uEdgeDict, - borders, - pinned, - pBar=pBar, - ) - - # Handle the UVs - if uvFaces is not None: - ( - uvNeighDict, - uUVNeighDict, - uvEdgeDict, - uvUEdgeDict, - uvBorders, - ) = buildLayeredNeighborDicts(uvFaces, uUVFaces, uvDWings) - uvPinned = getUVPins(faces, borders, uvFaces, uvBorders, pinBorders) - if pBar is not None: - pBar.setLabelText("Fixing UV Positions") - QApplication.processEvents() - uUVs = fixVerts( - uvFaces, - uUVFaces, - uvs, - uvNeighDict, - uUVNeighDict, - uvEdgeDict, - uvUEdgeDict, - uvBorders, - uvPinned, - pBar=pBar, - ) - - rFaces, rVerts, rUVFaces, rUVs = collapse(uFaces, uVerts, uUVFaces, uUVs) - - if pBar is not None: - pBar.close() - - return rFaces, rVerts, rUVFaces, rUVs - - -#################################################################### -# Handle .smpx files here # -#################################################################### - - -def _ussmpx(faces, verts, uvFaces, uvs, pBar=None): - """Unsubdivide a simplex""" - pbPrint(pBar, message="Finding Neighbors") - eNeigh = buildEdgeDict(faces) - - pbPrint(pBar, message="Getting Hints") - borders = getBorders(faces) - hints = buildUnsubdivideHints( - faces, eNeigh, borders, pBar=None - ) # Purposely no PBar - - pbPrint(pBar, message="Crawling Edges") - centerDel, fail = getFaceCenterDel(faces, eNeigh, hints, pBar=pBar) - assert not fail, "Could not detect subdivided topology with the provided hints" - - pbPrint(pBar, message="Deleting Edges") - uFaces, uUVFaces, _, _ = deleteCenters(faces, uvFaces, centerDel, pBar=pBar) - - pbPrint(pBar, message="Collapsing Indexes") - uVerts = verts - uUVs = uvs - - rFaces, rVerts, rUVFaces, rUVs = collapse(uFaces, uVerts, uUVFaces, uUVs) - - if pBar is not None: - pBar.close() - else: - print("Done") - - return rFaces, rVerts, rUVFaces, rUVs - - -def _applyShapePrefix(shapePrefix, jsString): - """Apply a prefix to the shape names. - For XSI where shape names must be unique - """ - if shapePrefix is not None: - d = json.loads(jsString) - if d["encodingVersion"] > 1: - for shape in d["shapes"]: - shape["name"] = shapePrefix + shape["name"] - else: - d["shapes"] = [shapePrefix + i for i in d["shapes"]] - jsString = json.dumps(d) - return jsString - - -def _unflattenFaces(flatFaces, counts): - faces = [] - ptr = 0 - for c in counts: - faces.append(flatFaces[ptr : ptr + c].tolist()) - ptr += c - return faces - - -def unsubdivideSimplex(inPath, outPath, shapePrefix=None, pBar=None): - """Unsubdivde a .smpx file on disk - - Parameters - ---------- - inPath : str - The input .smpx file path - outPath : str - The output .smpx file path - shapePrefix : str or None - An optional string to prefix the shape names with - pBar : QProgressDialog or None - An optional progress bar - """ - if np is None: - raise RuntimeError("Un-Subdivide requires numpy, and it is not available here") - - pbPrint(pBar, message="Loading smpx") - jsString, counts, verts, flatFaces, uvs, flatUVFaces = readSmpx(inPath) - jsString = _applyShapePrefix(shapePrefix, jsString) - - js = json.loads(jsString) - name = js["systemName"] - - faces = _unflattenFaces(flatFaces, counts) - uvFaces = None - if flatUVFaces is not None: - uvFaces = _unflattenFaces(flatUVFaces, counts) - - pbPrint(pBar, message="Unsubdividing") - verts = verts.swapaxes(0, 1) - uFaces, uVerts, uUVFaces, uUVs = _ussmpx(faces, verts, uvFaces, uvs, pBar=pBar) - - pbPrint(pBar, message="Exporting") - buildSmpx( - outPath, - uVerts.swapaxes(0, 1), - uFaces, - jsString, - name, - uvs=uUVs, - uvFaces=uUVFaces, - ) +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint: disable=unused-argument, too-many-locals +# pylint:disable=E0611,E0401 + +import json +from itertools import chain, zip_longest + +from Qt.QtWidgets import QApplication +from .alembicCommon import buildSmpx, pbPrint, readSmpx + +try: + import numpy as np +except ImportError: + np = None + + +def mergeCycles(groups): + """Take a list of ordered items, and sort them so the last item of each list matches the first of the next list + + Then return the groups of lists mashed together + For instance, with two cycles: + +---------+------------------------------------------------------------+ + | input | [(1, 2), (11, 12), (3, 1), (10, 11), (2, 3), (12, 10)] | + +---------+------------------------------------------------------------+ + | reorder | [[(1, 2), (2, 3), (3, 1)], [(10, 11), (11, 12), (12, 13)]] | + +---------+------------------------------------------------------------+ + | output | [[1, 2, 3], [10, 11, 12, 13]] | + +---------+------------------------------------------------------------+ + + Also, return whether the cycles merged form a single closed group + + Parameters + ---------- + groups : [(int, int), ...] + A list of pairs of integers + + Returns + ------- + : [[int, ...], ...] + The ordered cycles + """ + groups = [list(g) for g in groups] + heads = {g[0]: g for g in groups} + tails = {g[-1]: g for g in groups} + + headGetter = lambda x: heads.get(x[-1]) # noqa: E731 + headSetter = lambda x, y: x + y[1:] # noqa: E731 + + tailGetter = lambda x: tails.get(x[0]) # noqa: E731 + tailSetter = lambda x, y: y + x[1:] # noqa: E731 + + searches = ((headGetter, headSetter), (tailGetter, tailSetter)) + + out = [] + cycles = [] + while groups: + g = groups.pop() + del heads[g[0]] + del tails[g[-1]] + + for getter, setter in searches: + while True: + adder = getter(g) + if adder is None: + break + g = setter(g, adder) + del heads[adder[0]] + del tails[adder[-1]] + adder[:] = [] + groups = [x for x in groups if x] + + cycle = False + if g[0] == g[-1]: + g.pop() + cycle = True + cycles.append(cycle) + out.append(g) + return out, cycles + + +def grow(neigh, verts, exclude): + """Grow the vertex set, also keeping track of which vertices to ignore for the next iteration + + Parameters + ---------- + neigh : {int: [int, ...]} + A dict mapping a vert index to a list of neighbor vert indices + verts : set(int) + A set of vert indices to grow + exclude : set(int) + A set of vert indices to ignore + + Returns + ------- + : set(int) + The newly grown vertices + : set(int) + ``exclude`` combined with ``verts`` + + """ + grown = set() + growSet = verts - exclude + for v in growSet: + grown.update(neigh[v]) + newGrown = grown - exclude + newExclude = exclude | growSet + return newGrown, newExclude + + +def buildHint(island, neigh, borders): + """Find star points that are an even number of grows from an edge + + Parameters + ---------- + island : [int, ...] + A list of vertices as part of a mesh island + neigh : {int: [int, ...]} + The dictionary of vertex neighbors + borders : set(int) + A set of border vertices + + Returns + ------- + : int + The first star point encountered at an even grow from the given borders + """ + borders = borders & island + if not borders: + # Well ... we don't have any good way of dealing with this + # Best thing I can do is search for a point with the least + # number of similar valences, and return that + d = {} + for v in island: + d.setdefault(len(neigh[v]), []).append(v) + + dd = {} + for k, v in d.items(): + dd.setdefault(len(v), []).append(k) + + mkey = min(dd.keys()) + return d[dd[mkey][0]][0] + + exclude = set() + while borders: + borders, exclude = grow(neigh, borders, exclude) + borders, exclude = grow(neigh, borders, exclude) + for b in borders: + if len(neigh[b]) != 4: + return b + return None + + +def partitionIslands(faces, neigh, pBar=None): + """Find all groups of connected verts + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + neigh : {int: [int, ...]} + The dict of vertex neighbors + pBar : QProgressDialog or None + An optional progress bar + + Returns + ------- + : [set(int), ...] + A list of sets of non-connected vertex islands + """ + allVerts = set(chain.from_iterable(faces)) + islands = [] + count = float(len(allVerts)) + + if pBar is not None: + pBar.setValue(0) + pBar.setMaximum(count) + QApplication.processEvents() + + while allVerts: + verts = {allVerts.pop()} + exclude = set() + while verts: + verts, exclude = grow(neigh, verts, exclude) + islands.append(exclude) + allVerts.difference_update(exclude) + + if pBar is not None: + pBar.setValue(count - len(allVerts)) + QApplication.processEvents() + + return islands + + +def buildUnsubdivideHints(faces, neigh, borders, pBar=None): + """Get one vertex per island that was part of the original mesh + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + neigh : {int: [int, ...]} + The dict of vertex neighbors + borders : set(int) + A set of border vertices + pBar : QProgressDialog or None + An optional progress bar + + Returns + ------- + : [int, ...] + A list of star-points (one per island) to un-subdivide from + """ + islands = partitionIslands(faces, neigh, pBar=pBar) + hints = [] + + if pBar is not None: + pBar.setValue(0) + pBar.setMaximum(len(islands)) + QApplication.processEvents() + + for i, isle in enumerate(islands): + if pBar is not None: + pBar.setValue(i) + QApplication.processEvents() + hints.append(buildHint(isle, neigh, borders)) + + hints = [h for h in hints if h is not None] + return hints + + +def getFaceCenterDel(faces, eNeigh, hints, pBar=None): + """Given a list of hint "keeper" points + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + eNeigh : {int: [int, ...]} + The dict of vertex neighbors + hints : [int, ...] + A list of star-points (one per island) to un-subdivide from + pBar : QProgressDialog or None + An optional progress bar + + Returns + ------- + : set(int) + Centers of the original faces during a subdivision + : bool + Whether the operation failed(True) or not(False) + """ + vertToFaces = {} + vc = set() + for i, face in enumerate(faces): + for f in face: + vertToFaces.setdefault(f, []).append(i) + vc.add(f) + + count = len(vc) + centers = set() + midpoints = set() + originals = set(hints) + queue = set(hints) + + if pBar is not None: + pBar.setValue(0) + pBar.setMaximum(count) + QApplication.processEvents() + + i = 0 + fail = False + while queue: + cur = queue.pop() + if cur in midpoints: + continue + + if pBar is not None: + pBar.setValue(i) + QApplication.processEvents() + i += 2 # Add 2 because I *shouldn't* get any midpoints + + midpoints.update(eNeigh[cur]) + t = centers if cur in originals else originals + + for f in vertToFaces[cur]: + nVerts = faces[f] + + if len(nVerts) != 4: + fail = True + continue + + curFaceIndex = nVerts.index(cur) + + half = int(len(nVerts) / 2) + diag = nVerts[curFaceIndex - half] + + isOrig = diag in originals + isCtr = diag in centers + if not isOrig and not isCtr: + t.add(diag) + queue.add(diag) + elif ( + (isCtr and t is originals) + or (isOrig and t is centers) + or (diag in midpoints) + ): + fail = True + + return centers, fail + + +def getBorders(faces): + """Get the indices of verts along the borders of a mesh + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + + Returns + ------- + : set(int) + A set of border vertices + """ + edgePairs = set() + for face in faces: + for f in range(len(face)): + edgePairs.add((face[f], face[f - 1])) + borders = set() + for ep in edgePairs: + if (ep[1], ep[0]) not in edgePairs: + borders.update(ep) + return borders + + +def buildEdgeDict(faces): + """Build a dictionary of un-ordered neighboring vertices along edges + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + + Returns + ------- + : {int: set(int)} + A dictionary of neighboring vertices along edges + """ + edgeDict = {} + for face in faces: + for f in range(len(face)): + ff = edgeDict.setdefault(face[f - 1], set()) + ff.add(face[f]) + ff.add(face[f - 2]) + return edgeDict + + +def buildNeighborDict(faces): + """Build a structure to ask for edge and face neighboring vertices + The returned neighbor list starts with an edge neighbor, and + proceeds counter clockwise, alternating between edge and face neighbors + Also, while I'm here, grab the border verts + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + + Returns + ------- + : {int: [[int, ...], ...]} + A dictionary keyed from a vert index whose + values are ordered cycles or fans + : set(int) + A set of vertices that are on the border + """ + fanDict = {} + edgeDict = {} + for face in faces: + for i in range(len(face)): + fanDict.setdefault(face[i], []).append(face[i + 1 :] + face[:i]) + ff = edgeDict.setdefault(face[i - 1], set()) + ff.add(face[i]) + ff.add(face[i - 2]) + + borders = set() + out = {} + for k, v in fanDict.items(): + fans, cycles = mergeCycles(v) + for f, c in zip(fans, cycles): + if not c: + borders.update((f[0], f[-1], k)) + out[k] = fans + return out, edgeDict, borders + + +def _fanMatch(fan, uFan, dWings): + """Twist a single fan so it matches the uFan if it can""" + uIdx = uFan[0] + for f, fIdx in enumerate(fan): + dw = dWings.get(fIdx, []) + if uIdx in dw: + return fan[f:] + fan[:f] + return None + + +def _align(neigh, uNeigh, dWings): + """Twist all the neighs so they match the uNeigh""" + out = [] + for uFan in uNeigh: + for fan in neigh: + fm = _fanMatch(fan, uFan, dWings) + if fm is not None: + out.append(fm) + break + return out + + +def buildLayeredNeighborDicts(faces, uFaces, dWings): + """Build and align two neighbor dicts for both faces and uFaces + This guarantees that the neighbors at the same index are analogous + (ie. they go in the same direction) + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + uFaces : [[int, ...], ...] + The list of lists of vertex indices making up un-subdivided faces + dWings : ??? + ??? + + Returns + ------- + + """ + neighDict, edgeDict, borders = buildNeighborDict(faces) + uNeighDict, uEdgeDict, uBorders = buildNeighborDict(uFaces) + + assert ( + borders >= uBorders + ), "Somehow the unsubdivided borders contain different vIdxs" + + for k, uNeigh in uNeighDict.items(): + neighDict[k] = _align(neighDict[k], uNeigh, dWings) + + return neighDict, uNeighDict, edgeDict, uEdgeDict, borders + + +def _findOldPositionBorder( + faces, + uFaces, + verts, + uVerts, + neighDict, + uNeighDict, + edgeDict, + uEdgeDict, + borders, + vIdx, + computed, +): + """Find the position of the un-subdivided mesh if the vertex was on the border""" + nei = neighDict[vIdx][0] + nei = [i for i in nei if i in borders] + assert len(nei) == 2, "Found multi border, {}".format(nei) + uVerts[vIdx] = 2 * verts[vIdx] - ((verts[nei[0]] + verts[nei[1]]) / 2) + computed.add(vIdx) + + +def _findOldPositionSimple( + faces, + uFaces, + verts, + uVerts, + neighDict, + uNeighDict, + edgeDict, + uEdgeDict, + vIdx, + computed, +): + """Find the position of the un-subdivided mesh if the vertex has at least 4 neighbors. + Updates uVerts in-place + """ + neigh = neighDict[vIdx][0] + + eTest = edgeDict[vIdx] + e = [p for p in neigh if p in eTest] + f = [p for p in neigh if p not in eTest] + + es = verts[e].sum(axis=0) + fs = verts[f].sum(axis=0) + + n = len(e) + term1 = verts[vIdx] * (n / (n - 3.0)) + term2 = es * (4 / (n * (n - 3.0))) + term3 = fs * (1 / (n * (n - 3.0))) + vk = term1 - term2 + term3 + + uVerts[vIdx] = vk + computed.add(vIdx) + + +def _findOldPosition3Valence( + faces, + uFaces, + verts, + uVerts, + neighDict, + uNeighDict, + edgeDict, + uEdgeDict, + vIdx, + computed, +): + """Find the position of the un-subdivided mesh if the vertex has exactly 3 neighbors + Updates uVerts in-place. It is possible for this to fail. + + Returns + ------- + : bool + Whether an update happened + """ + neigh = neighDict[vIdx][0] + uNeigh = uNeighDict[vIdx][0] + + eTest = edgeDict[vIdx] + eNeigh = [n for n in neigh if n in eTest] + fNeigh = [n for n in neigh if n not in eTest] + + ueTest = uEdgeDict[vIdx] + ueNeigh = [n for n in uNeigh if n in ueTest] + # ufNeigh = [n for n in uNeigh if n not in ueTest] + + intr = computed.intersection(ueNeigh) + if intr: + # Easy valence 3 case. I only need + # The computed new neighbor + # The midpoint on the edge to that neighbor + # The "face" verts neighboring the midpoint + + # Get the matching subbed an unsubbed neighbor indexes + uNIdx = intr.pop() + nIdx = eNeigh[ueNeigh.index(uNIdx)] + + # Get the "face" verts next to the subbed neighbor + xx = neigh.index(nIdx) + fnIdxs = (neigh[xx - 1], neigh[(xx + 1) % len(neigh)]) + + # Then compute + # vk = 4*k1e - ke - k1fNs[0] - k1fNs[1] + vka = uVerts[uNIdx] + verts[fnIdxs[0]] + verts[fnIdxs[1]] + vkb = verts[nIdx] * 4 + uVerts[vIdx] = vkb - vka + computed.add(vIdx) + return True + + else: + # The Hard valence 3 case. Made even harder + # because the paper has a mistake in it + + # vk = 4*ejk1 + 4*ejpk1 - fjnk1 - fjpk1 - 6*fjk1 + sum(fik) + # where k1 means subdivided mesh + # where j means an index, jn and jp are next/prev adjacents + # sum(fik) is the sum of all the points of the face that + # *aren't* the original, or edge-adjacent + # There could be more than 1 if an n-gon was subdivided + # + # I wonder: If it was a triangle that was subdivided, what + # would sum(fik) because there are no verts that fit that + # description. I think this is a degenerate case + + # First, find an adjacent face on the unsub mesh that + # is only missing the neighbors of the vIdx + + fnIdx = None + fik = None + fCtrIdx = None + for x, v in enumerate(fNeigh): + # working with neigh, but should only ever contain uNeigh indexes + eTest = edgeDict[vIdx] + origFace = {n for n in neighDict[v][0] if n not in eTest} + + check = (origFace - set(ueNeigh)) - {vIdx} + if computed >= check: + fCtrIdx = v + fnIdx = x + fik = sorted(check) + break + + if fnIdx is None: + # No possiblity found + return False + + # Then apply the equation from above + neighIdx = neigh.index(fCtrIdx) + ejnk1 = neigh[(neighIdx + 1) % len(neigh)] + ejpk1 = neigh[neighIdx - 1] + fjnk1 = fNeigh[(fnIdx + 1) % len(fNeigh)] + fjpk1 = fNeigh[fnIdx - 1] + fjk1 = verts[fCtrIdx] + sumFik = uVerts[fik].sum(axis=0) + vk = 4 * ejnk1 + 4 * ejpk1 - fjnk1 - fjpk1 - 6 * fjk1 + sumFik + uVerts[vIdx] = vk + computed.add(vIdx) + return True + return False + + +def deleteCenters(meshFaces, uvFaces, centerDel, pBar=None): + """Delete the given vertices and connected edges from a face representation + to give a new representation. + + Parameters + ---------- + meshFaces : [[int, ...], ...] + The list of lists of vertex indices making up faces + centerDel : set(int) + A set of vertices to delete. These were the vertices added + to the centers of the faces when subdividing. + uvFaces : [[int, ...], ...] + The list of lists of uv indices making up uvFaces + pBar : QProgressDialog or None + An optional progress bar + + Returns + ------- + : [[int, ...], ...] + The new list of faces of the mesh + : [[int, ...], ...] + The new list of uvfaces of the mesh + : {int: (int, int)} + A dict of a deleted edge-midpoint to its two existing neighbor verts + : {int: (int, int)} + A dict of a deleted uv edge-midpoint to its two existing neighbor uvs + """ + # For each deleted index, grab the neighboring faces, + # and twist the faces so the deleted index is first + cds = set(centerDel) + faceDelDict = {} + uvDelDict = {} + uvFaces = uvFaces or [] + for face, uvFace in zip_longest(meshFaces, uvFaces): + fi = cds.intersection(face) + # If we are a subdivided mesh, Then each face will have exactly one + # vertex that is part of the deletion set + if len(fi) != 1: + raise ValueError("Found a face with an unrecognized connectivity") + # Get that one vert + idx = fi.pop() + # Each face is a cycle. Rotate the cycle + # so that idx is first in the list + rv = face.index(idx) + rFace = face[rv:] + face[:rv] + faceDelDict.setdefault(idx, []).append(rFace) + + if uvFace is not None: + rUVFace = uvFace[rv:] + uvFace[:rv] + uvDelDict.setdefault(idx, []).append(rUVFace) + + newFaces = [] + nUVFaces = [] + wings = {} + uvWings = {} + + if pBar is not None: + pBar.setValue(0) + pBar.setMaximum(len(faceDelDict)) + + chk = -1 + for idx, rFaces in faceDelDict.items(): + chk += 1 + if pBar is not None: + pBar.setValue(chk) + QApplication.processEvents() + + ruvFaces = uvDelDict.get(idx, []) + # The faces are guaranteed to be in a single loop cycle + # so I don't have to handle any annoying edge cases! Yay! + faceEnds = { + f[1]: (f[2], f[3], uvf) for f, uvf in zip_longest(rFaces, ruvFaces) + } # face ends + + end = rFaces[-1][-1] # get an arbitrary face to start with + newFace = [] + nUVFace = [] + while faceEnds: + try: + diag, nxt, uvf = faceEnds.pop(end) + except KeyError: + print("rFaces", rFaces) + print("fe", faceEnds) + raise + if uvf is not None: + try: + nUVFace.append(uvf[2]) + uvWings.setdefault(uvf[1], []).append(uvf[2]) + uvWings.setdefault(uvf[3], []).append(uvf[2]) + except IndexError: + print("UVF", uvf, chk) + raise + + newFace.append(diag) + wings.setdefault(end, []).append(diag) + wings.setdefault(nxt, []).append(diag) + + end = nxt + newFaces.append(newFace) + if nUVFace: + nUVFaces.append(nUVFace) + nUVFaces = nUVFaces or None + + return newFaces, nUVFaces, wings, uvWings + + +def fixVerts( + faces, + uFaces, + verts, + neighDict, + uNeighDict, + edgeDict, + uEdgeDict, + borders, + pinned, + pBar=None, +): + """Given the faces, vertex positions, and the point indices that + were created at the face centers for a subdivision step + Return the faces and verts of the mesh from before the + subdivision step. This algorithm doesn't handle UV's yet + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + verts : np.array + The Nx3 array of vertices + centerDel : set(int) + A set of vertices to delete + uFaces : [[int, ...], ...] + The list of lists of vertex indices making up the + un-subdivided faces. Not all numbers in the vert range are in this structure + neighDict : {int: [int, ...]} + The dictionary of vertex neighbors based on ``faces`` + uNeighDict : {int: [int, ...]} + The dictionary of vertex neighbors based on ``uFaces`` + edgeDict : TODO + Some kind of dictionary representing individual edges + uEdgeDict : TODO + Some kind of dictionary representing individual edges in the unsub mesh + borders : set(int) + The set of vertices along the borders + pinned : set(int) + The set of vertices that will not be moved by this process + pBar : QProgressDialog or None + An optional progress bar + + Returns + ------- + : np.array + An array of vertex positions + """ + uVerts = verts.copy() + uIdxs = sorted(set(chain.from_iterable(uFaces))) + + v3Idxs = [] + # bowtie verts are pinned + bowTieIdxs = [] + computed = set() + i = 0 + if pBar is not None: + pBar.setValue(0) + pBar.setMaximum(len(uIdxs)) + + for idx in uIdxs: + if pBar is not None: + pBar.setValue(i) + QApplication.processEvents() + if len(uNeighDict[idx]) > 1: + bowTieIdxs.append(idx) + i += 1 + elif idx in pinned: + pass + elif idx in borders: + _findOldPositionBorder( + faces, + uFaces, + verts, + uVerts, + neighDict, + uNeighDict, + edgeDict, + uEdgeDict, + borders, + idx, + computed, + ) + i += 1 + elif sum(map(len, neighDict[idx])) > 6: # if valence > 3 + _findOldPositionSimple( + faces, + uFaces, + verts, + uVerts, + neighDict, + uNeighDict, + edgeDict, + uEdgeDict, + idx, + computed, + ) + i += 1 + else: + v3Idxs.append(idx) + + updated = True + while updated: + updated = False + rem = set() + for idx in v3Idxs: + up = _findOldPosition3Valence( + faces, + uFaces, + verts, + uVerts, + neighDict, + uNeighDict, + edgeDict, + uEdgeDict, + idx, + computed, + ) + if not up: + continue + if pBar is not None: + pBar.setValue(i) + QApplication.processEvents() + i += 1 + updated = True + rem.add(idx) + v3Idxs = list(set(v3Idxs) - rem) + + return uVerts + + +def getUVPins(faces, borders, uvFaces, uvBorders, pinBorders): + """Find which uvBorders are also mesh borders + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + borders : set(int) + The set of vertices along the borders + uvFaces : [[int, ...], ...] + The list of lists of uv indices making up uvFaces + uvBorders : set(int) + The set of vertices along the borders of uvs + pinBorders : bool + Whether to just pin all uv borders + + Returns + ------- + : set(int) + The vertex indices to pin + """ + if uvFaces is None: + return set() + if pinBorders: + return set(uvBorders) + + pinnit = set() + for face, uvFace in zip(faces, uvFaces): + for i in range(len(face)): + f = face[i] + pf = face[i - 1] + + uv = uvFace[i] + puv = uvFace[i - 1] + + if not (f in borders and pf in borders): + if uv in uvBorders and puv in uvBorders: + pinnit.add(puv) + pinnit.add(uv) + + return uvBorders & pinnit + + +def collapse(faces, verts, uvFaces, uvs): + """Take a mesh representation with unused vertex indices, and remove those vertices + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + The set of integers used here will be non-contiguous + verts : np.array + The N*3 array of vertices + uvFaces : [[int, ...], ...] + The list of lists of uv indices making up uvFaces + The set of integers used here will be non-contiguous + uvs : np.array + The N*2 array of uvs + + Returns + ------- + : [[int, ...], ...] + The list of lists of vertex indices making up faces + The set of integers used here will be contiguous + : np.array + The new N*3 array of vertices + : [[int, ...], ...] + The list of lists of uv indices making up uvFaces + The set of integers used here will be non-contiguous + : np.array + The new N*2 array of uvs + """ + vset = sorted(set(chain.from_iterable(faces))) + nVerts = verts[vset] + vDict = {v: i for i, v in enumerate(vset)} + nFaces = [[vDict[f] for f in face] for face in faces] + + if uvFaces is not None: + uvset = sorted(set(chain.from_iterable(uvFaces))) + nUVs = uvs[uvset] + uvDict = {v: i for i, v in enumerate(uvset)} + nUVFaces = [[uvDict[f] for f in face] for face in uvFaces] + else: + nUVs = None + nUVFaces = None + + return nFaces, nVerts, nUVFaces, nUVs + + +def getCenters(faces, hints=None, pBar=None): + """Given a set of faces, find the face-center vertices from the subdivision + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + hints : set(int) or None + An optional list of star points that were part of the original un-subdivided mesh + pBar : QProgressDialog or None + An optional progress bar + + Returns + ------- + : set(int) + The vertices that were added to the centers of the faces as part of the subdivision process + """ + if pBar is not None: + pBar.setLabelText("Crawling Edges") + pBar.show() + QApplication.processEvents() + + eNeigh = buildEdgeDict(faces) + if hints is None: + borders = getBorders(faces) + hints = buildUnsubdivideHints( + faces, eNeigh, borders, pBar=None + ) # purposely no PBar + centerDel, fail = getFaceCenterDel(faces, eNeigh, hints, pBar=pBar) + assert not fail, "Could not detect subdivided topology with the provided hints" + + if pBar is not None: + pBar.close() + + return centerDel + + +def unSubdivide( + faces, + verts, + uvFaces, + uvs, + hints=None, + repositionVerts=True, + pinBorders=False, + pBar=None, +): + """Given a mesh representation (faces and vertices) remove the edges added + by a subdivision, and optionally reposition the verts + + Parameters + ---------- + faces : [[int, ...], ...] + The list of lists of vertex indices making up faces + verts : np.array + The N*3 array of vertices + uvFaces : [[int, ...], ...] + The list of lists of uv indices making up uvFaces + uvs : np.array + The N*2 array of uvs + hints : set(int) or None + An optional list of star points that were part of the original un-subdivided mesh + repositionVerts : bool + Whether to also estimate the positions of the un-subdivided verts. Defaults to True + pinBorders : bool + Whether or not to pin the border vertices. Defaults to False + pBar : QProgressDialog or None + An optional progress bar + + Returns + ------- + : [[vIdx ...], ...] + The un-subdivided face structure + : np.array + The un-subdivided vertex positions + : [[vIdx ...], ...] or None + The un-subdivided uv-face structure if it exists + : np.array or None + The un-subdivided uvs if they exist + """ + if pBar is not None: + pBar.show() + pBar.setLabelText("Finding Neighbors") + QApplication.processEvents() + + eNeigh = buildEdgeDict(faces) + + if hints is None: + if pBar is not None: + pBar.show() + pBar.setLabelText("Getting Hints") + QApplication.processEvents() + borders = getBorders(faces) + hints = buildUnsubdivideHints( + faces, eNeigh, borders, pBar=None + ) # Purposely no PBar + + if pBar is not None: + pBar.setLabelText("Crawling Edges") + QApplication.processEvents() + centerDel, fail = getFaceCenterDel(faces, eNeigh, hints, pBar=pBar) + assert not fail, "Could not detect subdivided topology with the provided hints" + + if pBar is not None: + pBar.setLabelText("Deleting Edges") + QApplication.processEvents() + uFaces, uUVFaces, dWings, uvDWings = deleteCenters( + faces, uvFaces, centerDel, pBar=pBar + ) + + uVerts = verts + uUVs = uvs + if repositionVerts: + # Handle the verts + if pBar is not None: + pBar.setLabelText("Building Correspondences") + QApplication.processEvents() + neighDict, uNeighDict, edgeDict, uEdgeDict, borders = buildLayeredNeighborDicts( + faces, uFaces, dWings + ) + pinned = set(borders) if pinBorders else [] + if pBar is not None: + pBar.setLabelText("Fixing Vert Positions") + QApplication.processEvents() + uVerts = fixVerts( + faces, + uFaces, + verts, + neighDict, + uNeighDict, + edgeDict, + uEdgeDict, + borders, + pinned, + pBar=pBar, + ) + + # Handle the UVs + if uvFaces is not None: + ( + uvNeighDict, + uUVNeighDict, + uvEdgeDict, + uvUEdgeDict, + uvBorders, + ) = buildLayeredNeighborDicts(uvFaces, uUVFaces, uvDWings) + uvPinned = getUVPins(faces, borders, uvFaces, uvBorders, pinBorders) + if pBar is not None: + pBar.setLabelText("Fixing UV Positions") + QApplication.processEvents() + uUVs = fixVerts( + uvFaces, + uUVFaces, + uvs, + uvNeighDict, + uUVNeighDict, + uvEdgeDict, + uvUEdgeDict, + uvBorders, + uvPinned, + pBar=pBar, + ) + + rFaces, rVerts, rUVFaces, rUVs = collapse(uFaces, uVerts, uUVFaces, uUVs) + + if pBar is not None: + pBar.close() + + return rFaces, rVerts, rUVFaces, rUVs + + +#################################################################### +# Handle .smpx files here # +#################################################################### + + +def _ussmpx(faces, verts, uvFaces, uvs, pBar=None): + """Unsubdivide a simplex""" + pbPrint(pBar, message="Finding Neighbors") + eNeigh = buildEdgeDict(faces) + + pbPrint(pBar, message="Getting Hints") + borders = getBorders(faces) + hints = buildUnsubdivideHints( + faces, eNeigh, borders, pBar=None + ) # Purposely no PBar + + pbPrint(pBar, message="Crawling Edges") + centerDel, fail = getFaceCenterDel(faces, eNeigh, hints, pBar=pBar) + assert not fail, "Could not detect subdivided topology with the provided hints" + + pbPrint(pBar, message="Deleting Edges") + uFaces, uUVFaces, _, _ = deleteCenters(faces, uvFaces, centerDel, pBar=pBar) + + pbPrint(pBar, message="Collapsing Indexes") + uVerts = verts + uUVs = uvs + + rFaces, rVerts, rUVFaces, rUVs = collapse(uFaces, uVerts, uUVFaces, uUVs) + + if pBar is not None: + pBar.close() + else: + print("Done") + + return rFaces, rVerts, rUVFaces, rUVs + + +def _applyShapePrefix(shapePrefix, jsString): + """Apply a prefix to the shape names. + For XSI where shape names must be unique + """ + if shapePrefix is not None: + d = json.loads(jsString) + if d["encodingVersion"] > 1: + for shape in d["shapes"]: + shape["name"] = shapePrefix + shape["name"] + else: + d["shapes"] = [shapePrefix + i for i in d["shapes"]] + jsString = json.dumps(d) + return jsString + + +def _unflattenFaces(flatFaces, counts): + faces = [] + ptr = 0 + for c in counts: + faces.append(flatFaces[ptr : ptr + c].tolist()) + ptr += c + return faces + + +def unsubdivideSimplex(inPath, outPath, shapePrefix=None, pBar=None): + """Unsubdivde a .smpx file on disk + + Parameters + ---------- + inPath : str + The input .smpx file path + outPath : str + The output .smpx file path + shapePrefix : str or None + An optional string to prefix the shape names with + pBar : QProgressDialog or None + An optional progress bar + """ + if np is None: + raise RuntimeError("Un-Subdivide requires numpy, and it is not available here") + + pbPrint(pBar, message="Loading smpx") + jsString, counts, verts, flatFaces, uvs, flatUVFaces = readSmpx(inPath) + jsString = _applyShapePrefix(shapePrefix, jsString) + + js = json.loads(jsString) + name = js["systemName"] + + faces = _unflattenFaces(flatFaces, counts) + uvFaces = None + if flatUVFaces is not None: + uvFaces = _unflattenFaces(flatUVFaces, counts) + + pbPrint(pBar, message="Unsubdividing") + verts = verts.swapaxes(0, 1) + uFaces, uVerts, uUVFaces, uUVs = _ussmpx(faces, verts, uvFaces, uvs, pBar=pBar) + + pbPrint(pBar, message="Exporting") + buildSmpx( + outPath, + uVerts.swapaxes(0, 1), + uFaces, + jsString, + name, + uvs=uUVs, + uvFaces=uUVFaces, + ) diff --git a/src/python/simplexui/commands/uvTransfer.py b/src/python/simplexui/commands/uvTransfer.py index ff69dfbd..a601ea27 100644 --- a/src/python/simplexui/commands/uvTransfer.py +++ b/src/python/simplexui/commands/uvTransfer.py @@ -1,977 +1,977 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint: disable=invalid-name -import numpy as np - -INF = float("inf") -EPS = 1e-7 - - -######################## -# Mean Value Coords -######################## - - -def _lerp(idx, corners, p): - # If I'm here, I know that p lies on the line - # between the corners at idx and idx+1. - # Return the lerp value - ip = (idx + 1) % len(corners) - c1 = corners[idx] - c2 = corners[ip] - base = c1 - c2 - proj = np.dot(p - c2, base) - b = proj / (base**2).sum() - ret = np.zeros(len(corners)) - ret[idx] = b - ret[ip] = 1.0 - b - return ret - - -def mvc(corners, p, tol=EPS): - """Get the Mean Value Coordinates of a point p in the polygon defined by corners - - Parameters - ---------- - corners : [int, ...] - A list of corner indices - p : np.array - The array of points - tol : float - A small tolerance value, defaulting to the global EPS - - Returns - ------- - : list - The normalized list of barycentric weights for this point in the polygon - - """ - spokes = corners - p - spokeLens = (spokes * spokes).sum(axis=1) - - # Check if p is on top of a vertex - for i, v in enumerate(spokeLens): - if v < tol: - bary = np.zeros(len(corners)) - bary[i] = 1.0 - return bary - - rspokes = np.roll(spokes, -1, axis=0) - areas = np.cross(spokes, rspokes) * 0.5 - dots = (spokes * rspokes).sum(axis=1) - - for i, v in enumerate(areas): - if v < tol and dots[i] < 0.0: - return _lerp(i, corners, p) - - spokeLens = spokeLens**0.5 - rspokeLens = np.roll(spokeLens, -1) - t = areas / (spokeLens * rspokeLens + dots) - rawWeights = (np.roll(t, 1) + t) / spokeLens - return rawWeights / sum(rawWeights) - - -def mmvc(rawFaces, points, samples, uvToFace, tol=EPS): - """Multi-MeanValueCoords - Get the MVC's of many points over many faces using numpy - - Parameters - ---------- - rawFaces : [[int, ...], ...] - A list of face index lists - points : np.array - A numpy array of uv points - samples : np.array - A numpy array of points to get the barycentric coords for - uvToFace : {int: [int, ...], ...} - A dictionary of UV indices to a list of Face Indices - tol : float - A small tolerance value, defaulting to the global EPS - - Returns - ------- - : {fc: (wh, barys)} - A dictionary of face counts to a tuple containing the indices of the face indices - checked, and the barycentric coords - - """ - uvIdxs, faceIdxs = sorted(zip(*list(uvToFace.items()))) - uvIdxs = np.array(uvIdxs) - faces = [rawFaces[fi] for fi in faceIdxs] - faceLens = np.array([len(i) for i in faces]) - fcs = np.unique(faceLens) - - out = {} - for fc in fcs: - wh = np.where(faceLens == fc)[0] - cIdxs = np.array([faces[i] for i in wh]) - barys = np.zeros(cIdxs.shape) - qIdxs = uvIdxs[wh] - out[fc] = (qIdxs, barys) # edit barys in-place - - # cornerses is a [f, fc, 2] array - # where f is num of faces, fc is verts per face, and 2 because uv is 2d - # pts is [f, 2] array where f is num faces - cornerses = points[cIdxs] - pts = samples[qIdxs] - - # Get the "spokes" from the query point to the face corners - # and get their squared lengths - spokes = cornerses - pts[:, None, :] - spokeLens2 = (spokes * spokes).sum(axis=-1) - - # Handle any samples that are directly on top of a corner - # (where the spoke length is zero) - zeros = np.any(spokeLens2 < tol, axis=1) - onPoint = np.where(zeros) - offPoint = np.where(~zeros) - if onPoint[0].size: - pIdxs = spokeLens2[onPoint].argmin(axis=1) - barys[onPoint, pIdxs] = 1.0 - - if not offPoint[0].size: - continue - - # Ignore those points that have already been computed - idxs = offPoint[0] - spokes = spokes[offPoint] - spokeLens2 = spokeLens2[offPoint] - - # Get the signed area of each triangle created by the spokes - # and the dot product for the angle between each - rspokes = np.roll(spokes, -1, axis=1) - areas = np.cross(spokes, rspokes) * 0.5 - dots = (spokes * rspokes).sum(axis=-1) - - # Handle any samples that are directly on an edge between - # two corners. (Where the triangle area is 0, and the - # dot product is negative) - aareas = abs(areas) - zeros = aareas < tol - ndots = dots < 0.0 - zn = zeros & ndots - toLerp = np.any(zn, axis=-1) - onEdge = np.where(toLerp) - offEdge = np.where(~toLerp) - if onEdge[0].size: - subIdx = idxs[onEdge] - pIdxs = zn[onEdge].argmax(axis=-1) - xIdxs = (pIdxs + 1) % fc - - ppIdxs = cIdxs[subIdx, pIdxs] - xpIdxs = cIdxs[subIdx, xIdxs] - - cp = pts[subIdx] - pp = points[ppIdxs] - xp = points[xpIdxs] - - bases = pp - xp - diff = cp - xp - proj = (bases * diff).sum(axis=-1) - b = proj / (bases**2).sum(axis=1) - mb = 1.0 - b - barys[subIdx, pIdxs] = b - barys[subIdx, xIdxs] = mb - - if not offEdge[0].size: - continue - - idxs = idxs[offEdge] - dots = dots[offEdge] - areas = areas[offEdge] - spokeLens2 = spokeLens2[offEdge] - spokeLens = spokeLens2**0.5 - rspokeLens = np.roll(spokeLens, -1, axis=1) - - t = areas / (spokeLens * rspokeLens + dots) - rawWeights = (np.roll(t, -1, axis=1) + t) / spokeLens - b = rawWeights / rawWeights.sum(axis=1)[..., None] - barys[idxs] = b - - ret = {} - for qIdxs, barys in out.values(): - for qi, b in zip(qIdxs, barys): - ret[qi] = (uvToFace[qi], b) - - return ret - - -######################## -# Sweep algorithm -######################## - - -def triArea(a, b, c): - """Use the cross-ish-product to find the area of the triangle - Depending on the winding, the area could be positive or negative - - Parameters - ---------- - a : [float, float, float] - The first point - b : [float, float, float] - The second point - c : [float, float, float] - The third point - - Returns - ------- - : float - The area of the triangle - - """ - return (a[0] * (c[1] - b[1]) + b[0] * (a[1] - c[1]) + c[0] * (b[1] - a[1])) / 2.0 - - -def pointInTri(p, a, b, c, tol=EPS): - """Check that the point is inside the triangle - - Parameters - ---------- - p : [float, float, float] - The point to check is inside the triangle - a : [float, float, float] - The first point of the triangle - b : [float, float, float] - The second point of the triangle - c : [float, float, float] - The third point of the triangle - tol : float - A small tolerance value, defaulting to the global EPS - - Returns - ------- - : bool - Whether the point is inside the given triangle - - """ - area = abs(triArea(a, b, c)) - chis = abs(triArea(p, b, c)) + abs(triArea(a, p, c)) + abs(triArea(a, b, p)) - return abs(area - chis) < tol - - -def sweep(qPoints, uvs, tris, pBar=None): - """Get what triangle each query point is inside - - Runs a sweep-line algorithm to check for points in triangles. - Imagine a uv layout, and a bunch of points on the layout. - Now imagine a vertical line sweeping across the uv plane from the left - to the right. The sorted u-values of certain properties then handled - one-by-one. - - The algorithm keeps track of what triangles the vertical line is currently intersecting. - - * This is done by storing the min and max u-values for each triangle. - * If the u-value encountered is the min for a tri, that tri is added to the list - * If the u-value encountered is the max for a tri, that tri is removed from the list - - Then, each time a query point is encountered, it only has to check the intersection list - - Parameters - ---------- - qPoints : np.array - The points to get barycentric coordinates of - uvs : np.array - The UVs we're searching - tris : np.array - The Nx3 array of triangle indices - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - : {int: int, ...} - A dictionary of out[pointIndex] = triIndex - : set() - A set containing the pointIndexes that weren't found in a triangle - - """ - tpts = uvs[tris] # [tIdx, abc, xy] - - allmxs, allmns = tpts.max(axis=1), tpts.min(axis=1) - ymxs, ymns = allmxs[:, 1], allmns[:, 1] - mxs, mns = allmxs[:, 0], allmns[:, 0] - qpx = qPoints[:, 0] - qpSIdxs, mxSIdxs, mnSIdxs = np.argsort(qpx), np.argsort(mxs), np.argsort(mns) - qpSIdx, mxSIdx, mnSIdx = 0, 0, 0 - qpIdx, mxIdx, mnIdx = qpSIdxs[qpSIdx], mxSIdxs[mxSIdx], mnSIdxs[mnSIdx] - qp, mx, mn = qpx[qpIdx], mxs[mxIdx], mns[mnIdx] - - out = {} - missing = set() - - if pBar is not None: - pBar.setValue(0) - allVals = len(qpSIdxs) + len(mxSIdxs) + len(mnSIdxs) - pBar.setRange(0, allVals) - pBar.setLabelText("Sweeping ...") - from Qt.QtWidgets import QApplication - - QApplication.processEvents() - - # skip any triangles to the left of the first query point - # The algorithm will stop once we hit the last query point - skip = [qp > m for m in mxs] - activeTris = [False for _ in mxs] # For fast membership testing - atSet = set() # For fast iteration - - while True: - if pBar is not None: - cVal = qpSIdx + mnSIdx + mxSIdx - if cVal % 1283 == 0: # Just a random prime - pBar.setValue(cVal) - pBar.setLabelText("Sweeping ...\n{0}/{1}".format(cVal, allVals)) - QApplication.processEvents() - if pBar.wasCanceled(): - raise RuntimeError("Cancelled!") - - if mn <= mx and mn <= qp: - # Always add triangles first if possible - aTriIdx = mnSIdxs[mnSIdx] - if not activeTris[aTriIdx] and not skip[aTriIdx]: - activeTris[aTriIdx] = True - atSet.add(aTriIdx) - - mnSIdx += 1 - if mnSIdx != len(mnSIdxs): - mnIdx = mnSIdxs[mnSIdx] - mn = mns[mnIdx] - else: - mn = mxs[mxSIdxs[-1]] + 1 - - elif qp <= mx: - # Check query points between adding and removing - qPoint = qPoints[qpIdx] - yv = qPoint[1] - # A linear search is faster than more complex collections here - for t in atSet: - if ymns[t] <= yv <= ymxs[t]: - a, b, c = uvs[tris[t]] - if pointInTri(qPoint, a, b, c): - out[qpIdx] = t - break - else: - missing.add(qpIdx) - - qpSIdx += 1 - if qpSIdx == len(qpSIdxs): - break - qpIdx = qpSIdxs[qpSIdx] - qp = qpx[qpIdx] - - else: - # always remove triangles last - aTriIdx = mxSIdxs[mxSIdx] - if activeTris[aTriIdx] and not skip[aTriIdx]: - activeTris[aTriIdx] = False - atSet.remove(aTriIdx) - - mxSIdx += 1 - if mxSIdx != len(mxSIdxs): - mxIdx = mxSIdxs[mxSIdx] - mx = mxs[mxIdx] - - return out, missing - - -######################## -# Triangulation -######################## - - -def inBox(point, mxs, mns): - """Check if a point is inside the bounding box - - Parameters - ---------- - point : np.array - A 3d point - mxs : np.array - The max values of all the coordinates per axis - mns : np.array - The min values of all the coordinates per axis - - Returns - ------- - : bool - Whether the point is in the given bounding box - - """ - return np.all(point <= mxs) and np.all(point >= mns) - - -def _isEar(a, b, c, polygon, tol=EPS): - """Check if the points a,b,c of the polygon could be their own triangle""" - signedArea = triArea(a, b, c) - - # Check that the triange is wound the correct way. - if signedArea > 0: - return False - - # Check that the triangle has non-zero area - # we already know the area is negative from above - if -signedArea < tol: - return False - - # Check that none of the other points in the polygon are contained in triangle - for p in polygon: - if p not in (a, b, c): - if pointInTri(p, a, b, c, tol=tol): - return False - return True - - -def earclip(idxs, verts): - """Simple earclipping algorithm - For a polygon with n points it will return n-2 triangles. - - Parameters - ---------- - idxs : [int, ...] - The indices that make up a polygon - verts : np.array - All the UV positions - - Returns - ------- - : [[int, int, int], ...] - A list of triangle indices - - """ - earVerts = [] - tris = [] - idxs = list(idxs) - polygon = [tuple(verts[i]) for i in idxs] - - numPts = len(polygon) - for i in range(numPts): - prev = polygon[i - 2] - cur = polygon[i - 1] - nxt = polygon[i] - if _isEar(prev, cur, nxt, polygon): - earVerts.append(cur) - - while earVerts and numPts >= 3: - ear = earVerts.pop() - i = polygon.index(ear) - pi, ni = i - 1, (i + 1) % numPts - - prevPt = polygon[pi] - nxtPt = polygon[ni] - tris.append((idxs[pi], idxs[i], idxs[ni])) - - polygon.pop(i) - idxs.pop(i) - numPts -= 1 - if numPts > 3: - prePrePt = polygon[i - 2] - nxtNxtPt = polygon[(i + 1) % numPts] - - groups = [ - (prePrePt, prevPt, nxtPt, polygon), - (prevPt, nxtPt, nxtNxtPt, polygon), - ] - - for group in groups: - p = group[1] - if _isEar(*group): - if p not in earVerts: - earVerts.append(p) - elif p in earVerts: - earVerts.remove(p) - return tris - - -def triangulateUVs(faces, uvs): - """Take a set of uvFaces and uv points, and triangulate it - - Parameters - ---------- - faces : [[int, ...], ...] - A uv face structure - uvs : np.array - The uv positions - - Returns - ------- - : np.array - A Nx3 array of uv indexes making triangles - : [int, ...] - A list where the index is the triangle index, and the value is the face index - : {(int, int): int, ...} - A dictionary of border edge pairs to border faces - - """ - uvs = np.array(uvs) - triMap = [] - tris = [] - borderFaceMap = {} - # Build the naiive triangulation - # And get the borders while I'm looping - for f, face in enumerate(faces): - for i in range(2, len(face)): - triMap.append(f) - tris.append((face[0], face[i - 1], face[i])) - - for i in range(len(face)): - ep = (face[i - 1], face[i]) - if ep[0] > ep[1]: - ep = (ep[1], ep[0]) - if ep in borderFaceMap: - borderFaceMap.pop(ep) - else: - borderFaceMap[ep] = f - - tris = np.array(tris) - tuvs = uvs[tris] - - # Get the area using a 2d-pseudo-cross-product - a = tuvs[:, 0] - tuvs[:, 1] - b = tuvs[:, 2] - tuvs[:, 1] - signedAreas = a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0] - - # Things with negative area are wound backwards in this case - negArea = np.where(signedAreas < 0) - - # Do we need to retriangulate any of the polys - retri = sorted({triMap[i] for i in negArea[0]}) - if retri: - tris = tris.tolist() - tmpFaceMap = {} - # tmpFaceMap is only good while we re-triangulate - # it has to be re-built after - for t, f in enumerate(triMap): - tmpFaceMap.setdefault(f, []).append(t) - - for f in retri[::-1]: - good = earclip(faces[f], uvs) - tidxs = tmpFaceMap[f] - tris[tidxs[0] : tidxs[-1] + 1] = good - triMap[tidxs[0] : tidxs[-1] + 1] = [f] * len(good) - tris = np.array(tris) - - return tris, triMap, borderFaceMap - - -######################## -# UV Transferring -######################## - -MISSING = {} - - -def cooSparseMul(M, v): - """Multply the sparse matrix by the vector - - Parameters - ---------- - M : SparseMatrix - My own sparse matrix representation - v : np.array - A vector - - Returns - ------- - : np.array - The result of the multiplication - """ - # Using scipy is at least 2x faster than the - # pure numpy implementation. But even that - # is about 5x faster than using a dense matrix - try: - from scipy import sparse - except ImportError: - # No scipy, use numpy - # The numpy universal function (ufunc) .add.at() - # makes this possible without any python looping - row, col, val, shape = M - v = v.swapaxes(0, -2) - vshape = val.shape + tuple([1] * (len(v) - 1)) - val = val.reshape(vshape) - out = np.zeros(shape[:1] + v.shape[1:]) - np.add.at(out, row, val * v[col]) - out = out.swapaxes(0, -2) - else: - row, col, val, shape = M - spM = sparse.coo_matrix((val, (row, col)), shape=shape) - spM = sparse.csr_matrix(spM) - if len(v.shape) == 3: - out = np.array([spM.dot(frm) for frm in v]) - else: - out = spM.dot(v) - return out - - -def getUvCorrelation(samples, points, faces, tol=0.0001, handleMissing=True, pBar=None): - """Get the per-face correlation of the sample points in uv space. - - Parameters - ---------- - samples : np.array - The sample points - points : np.array - The background points - faces : [[int, ...], ...] - The Face list - tol : float - A small tolerance value, defaulting to the global EPS - handleMissing : - (Default value = True) - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - : {idx: (idx, [float, ...]), ...} - A dictionary from the sample index to a faceIdx/cornerWeights pair - - """ - tris, triMap, borderMap = triangulateUVs(faces, points) - swept, missing = sweep(samples, points, tris, pBar=pBar) - uvToFace = {uvI: triMap[tI] for uvI, tI in swept.items()} - # import __main__ - # __main__.__dict__.update(locals()) - # raise RuntimeError("STOPPIT") - if pBar is not None: - pBar.setValue(0) - pBar.setRange(0, len(uvToFace)) - pBar.setLabelText("Calculate Mean Value Coords") - from Qt.QtWidgets import QApplication - - QApplication.processEvents() - - # mvcDict = {} - # for i, (uvIdx, faceIdx) in enumerate(uvToFace.iteritems()): - # if pBar is not None: - # pBar.setValue(i) - # pBar.setLabelText("Calculate Mean Value Coords\n{0}/{1}".format(i, len(uvToFace))) - # QApplication.processEvents() - # uv = samples[uvIdx] - # corners = points[faces[faceIdx]] - # bary = mvc(corners, uv) - # mvcDict[uvIdx] = (faceIdx, bary) - - mvcDict = mmvc(faces, points, samples, uvToFace) - - # find the closest border - # Just use a brute-force search - # ... for now - - if missing and handleMissing: - if pBar is not None: - pBar.setValue(0) - pBar.setLabelText("Handle Missing") - QApplication.processEvents() - tol = tol**2 - bk = list(borderMap.keys()) - borders = np.array(bk) - bStarts = points[borders[:, 0]] - bEnds = points[borders[:, 1]] - - bDiff = bEnds - bStarts - bLens2 = (bDiff * bDiff).sum(axis=1) - for mIdx in missing: - mp = samples[mIdx] - dSquared = _mpCheck(bStarts, bDiff, bLens2, mp) - minIdx = np.argmin(dSquared) - if dSquared[mIdx] < tol: - faceIdx = borderMap[bk[minIdx]] - corners = points[faces[faceIdx]] - bary = mvc(corners, mp) - mvcDict[mIdx] = (faceIdx, bary) - - return mvcDict - - -def _mpCheck(a, d, dr2, pt): - """Point to Multi-segment squared distance. Uses pre-computed values""" - lerp = ((pt - a) * d).sum(axis=1) / dr2 - lerp = np.clip(lerp, 0, 1) - xy = (lerp[:, None] * d) + a - _dxy = xy - pt - return (_dxy * _dxy).sum(axis=1) - - -def getVertCorrelation( - sUvFaces, sUvs, tVertFaces, tUvFaces, tUvs, tol=0.0001, pBar=None -): - """Build the vertex position correlation between two meshes - by looking through UV-space. Handle combining multiple uvs - per vertex, and having samples that live outside the coverage - - Parameters - ---------- - sUvFaces : [[int, ...], ...] - The source UVFace list - sUvs : np.array - The source UV positions - tVertFaces : [[int, ...], ...] - The target vertex face list - tUvFaces : [[int, ...], ...] - The target UV face list - tUvs : np.array - The target UV positions - tol : float - A small tolerance value, defaulting to the global EPS - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - : {idx: (idx, [float, ...]), ...} - A dictionary from the sample index to a faceIdx/cornerWeights pair - """ - childUvToVert = {} - cNumVerts = -1 - for vF, uvF in zip(tVertFaces, tUvFaces): - for vIdx, uvIdx in zip(vF, uvF): - cNumVerts = max(cNumVerts, vIdx) - childUvToVert[uvIdx] = vIdx - - mvcUvDict = getUvCorrelation(tUvs, sUvs, sUvFaces, tol=tol, pBar=pBar) - - mvcVertDict = {} - for uvIdx in range(len(tUvs)): - if uvIdx not in mvcUvDict: - continue - mvcVertDict.setdefault(childUvToVert[uvIdx], []).append(mvcUvDict[uvIdx]) - - missing = set(range(cNumVerts)) - mvcVertDict.keys() - if missing: - import time - - v = time.time() - print( - "Missing correspondences found. Stored in uvTransfer.MISSING[{0}]".format(v) - ) - MISSING[v] = missing - - return mvcVertDict - - -def applyTransfer(parVerts, parFaces, correlation, outputSize): - """Given a vertex corelation, a driver, and driven points, - Apply the driver deformation to the driven. This could be - for one frame, or many frames - - Parameters - ---------- - parVerts : np.array - The "parent" vertex positions - parFaces : [[int, ...], ...] - The "parent" face list - correlation : {int: [int, ...], ...} - A dict correlatng the vertIdx to its possible correlations - outputSize : (int, ...) - A numpy output size tuple - - Returns - ------- - : np.array - The new vertex positions - - """ - if len(parVerts.shape) == 2: - parVerts = parVerts[None, ...] - - rows, cols, vals = [], [], [] - for cVertIdx, corrPoss in correlation.items(): - if len(corrPoss) == 1: - pFaceIdx, bary = corrPoss[0] - else: - # pick the one with the highest sum-of-squares - x = [sum(bary * bary) for _, bary in corrPoss] - idx = x.index(max(x)) - pFaceIdx, bary = corrPoss[idx] - - rows.extend([cVertIdx] * len(bary)) - vals.extend(bary) - cols.extend(parFaces[pFaceIdx]) - M = np.array(rows), np.array(cols), np.array(vals), (outputSize, parVerts.shape[-2]) - out = cooSparseMul(M, parVerts) - return out - - -def uvTransfer( - srcFaces, - srcUvFaces, - srcVerts, - srcUvs, - tarFaces, - tarUvFaces, - tarVerts, - tarUvs, - tol=0.0001, - pBar=None, -): - """A helper function that transfers pre-loaded data. - The source data will be transferred onto the tar data - - Parameters - ---------- - srcFaces : [[int, ...], ...] - The source vertex face list - srcUvFaces : [[int, ...], ...] - The source uv face list - srcUvs : np.array - The source UV positions - tarFaces : [[int, ...], ...] - The target vertex face list - tarUvFaces : [[int, ...], ...] - The target uv face list - tarUvs : np.array - The Target UV Positions - srcVerts : np.array - The source Vertex positions - tarVerts : np.array - The target Vertex positions - tol : float - A small tolerance value, defaulting to the global EPS - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - : np.array - The new target vert positions - - """ - corr = getVertCorrelation( - srcUvFaces, srcUvs, tarFaces, tarUvFaces, tarUvs, tol=tol, pBar=pBar - ) - if pBar is not None: - pBar.setValue(0) - pBar.setLabelText("Apply Transfer") - from Qt.QtWidgets import QApplication - - QApplication.processEvents() - - return applyTransfer(srcVerts, srcFaces, corr, len(tarVerts)) - - -def uvTransferLoad( - srcPath, tarPath, srcUvSet="default", tarUvSet="default", tol=0.0001, pBar=None -): - """Transfer the shape from the source to the target through uv space - Return the data needed to write out the result - - Parameters - ---------- - srcPath : str - The source mesh path (obj, abc, or smpx) - tarPath : str - The target mesh path (obj, abc, or smpx) - srcUvSet : str - The name of the uv set to use on the source - tarUvSet : str - The name of the uv set to use on the target - tol : float - A small tolerance value, defaulting to the global EPS - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - : np.array - The target vertex positions - : list - The target vertex faces - : np.array - The target uvs - : list - The target uv faces - - """ - from . import alembicCommon as abc - from .mesh import Mesh - - if srcPath.endswith(".abc") or srcPath.endswith(".smpx"): - src = Mesh.loadAbc(srcPath, ensureWinding=False) - srcVerts = abc.getSampleArray(abc.getMesh(srcPath)) - elif srcPath.endswith(".obj"): - src = Mesh.loadObj(srcPath, ensureWinding=False) - srcVerts = np.array(src.vertArray) - - if tarPath.endswith(".abc"): - tar = Mesh.loadAbc(tarPath, ensureWinding=False) - elif tarPath.endswith(".obj"): - tar = Mesh.loadObj(tarPath, ensureWinding=False) - - srcFaces = src.faceVertArray - srcUvFaces = src.uvFaceMap[srcUvSet] - srcUvs = np.array(src.uvMap[srcUvSet]) - - tarFaces = tar.faceVertArray - tarUvFaces = tar.uvFaceMap[tarUvSet] - tarUvs = np.array(tar.uvMap[tarUvSet]) - oldTarVerts = np.array(tar.vertArray) - tarVerts = uvTransfer( - srcFaces, - srcUvFaces, - srcVerts, - srcUvs, - tarFaces, - tarUvFaces, - oldTarVerts, - tarUvs, - tol=tol, - pBar=pBar, - ) - - return tarVerts, tarFaces, tarUvs, tarUvFaces - - -def uvTransferFiles( - srcPath, - tarPath, - outAbcPath, - srcUvSet="default", - tarUvSet="default", - tol=0.0001, - pBar=None, -): - """Transfer the shape from the source to the target through uv space - and write out the result - - Parameters - ---------- - srcPath : str - The source mesh path (obj, abc, or smpx) - tarPath : str - The target mesh path (obj, abc, or smpx) - outAbcPath : str - The path to the output .abc file - srcUvSet : str - The name of the uv set to use on the source - tarUvSet : str - The name of the uv set to use on the target - tol : float - A small tolerance value, defaulting to the global EPS - pBar : QProgressDialog, optional - An optional progress dialog - - Returns - ------- - - """ - from . import alembicCommon as abc - - tarVerts, tarFaces, tarUvs, tarUvFaces = uvTransferLoad( - srcPath, tarPath, srcUvSet=srcUvSet, tarUvSet=tarUvSet, tol=tol, pBar=pBar - ) - abc.buildAbc(outAbcPath, tarVerts, tarFaces, uvs=tarUvs, uvFaces=tarUvFaces) +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint: disable=invalid-name +import numpy as np + +INF = float("inf") +EPS = 1e-7 + + +######################## +# Mean Value Coords +######################## + + +def _lerp(idx, corners, p): + # If I'm here, I know that p lies on the line + # between the corners at idx and idx+1. + # Return the lerp value + ip = (idx + 1) % len(corners) + c1 = corners[idx] + c2 = corners[ip] + base = c1 - c2 + proj = np.dot(p - c2, base) + b = proj / (base**2).sum() + ret = np.zeros(len(corners)) + ret[idx] = b + ret[ip] = 1.0 - b + return ret + + +def mvc(corners, p, tol=EPS): + """Get the Mean Value Coordinates of a point p in the polygon defined by corners + + Parameters + ---------- + corners : [int, ...] + A list of corner indices + p : np.array + The array of points + tol : float + A small tolerance value, defaulting to the global EPS + + Returns + ------- + : list + The normalized list of barycentric weights for this point in the polygon + + """ + spokes = corners - p + spokeLens = (spokes * spokes).sum(axis=1) + + # Check if p is on top of a vertex + for i, v in enumerate(spokeLens): + if v < tol: + bary = np.zeros(len(corners)) + bary[i] = 1.0 + return bary + + rspokes = np.roll(spokes, -1, axis=0) + areas = np.cross(spokes, rspokes) * 0.5 + dots = (spokes * rspokes).sum(axis=1) + + for i, v in enumerate(areas): + if v < tol and dots[i] < 0.0: + return _lerp(i, corners, p) + + spokeLens = spokeLens**0.5 + rspokeLens = np.roll(spokeLens, -1) + t = areas / (spokeLens * rspokeLens + dots) + rawWeights = (np.roll(t, 1) + t) / spokeLens + return rawWeights / sum(rawWeights) + + +def mmvc(rawFaces, points, samples, uvToFace, tol=EPS): + """Multi-MeanValueCoords + Get the MVC's of many points over many faces using numpy + + Parameters + ---------- + rawFaces : [[int, ...], ...] + A list of face index lists + points : np.array + A numpy array of uv points + samples : np.array + A numpy array of points to get the barycentric coords for + uvToFace : {int: [int, ...], ...} + A dictionary of UV indices to a list of Face Indices + tol : float + A small tolerance value, defaulting to the global EPS + + Returns + ------- + : {fc: (wh, barys)} + A dictionary of face counts to a tuple containing the indices of the face indices + checked, and the barycentric coords + + """ + uvIdxs, faceIdxs = sorted(zip(*list(uvToFace.items()))) + uvIdxs = np.array(uvIdxs) + faces = [rawFaces[fi] for fi in faceIdxs] + faceLens = np.array([len(i) for i in faces]) + fcs = np.unique(faceLens) + + out = {} + for fc in fcs: + wh = np.where(faceLens == fc)[0] + cIdxs = np.array([faces[i] for i in wh]) + barys = np.zeros(cIdxs.shape) + qIdxs = uvIdxs[wh] + out[fc] = (qIdxs, barys) # edit barys in-place + + # cornerses is a [f, fc, 2] array + # where f is num of faces, fc is verts per face, and 2 because uv is 2d + # pts is [f, 2] array where f is num faces + cornerses = points[cIdxs] + pts = samples[qIdxs] + + # Get the "spokes" from the query point to the face corners + # and get their squared lengths + spokes = cornerses - pts[:, None, :] + spokeLens2 = (spokes * spokes).sum(axis=-1) + + # Handle any samples that are directly on top of a corner + # (where the spoke length is zero) + zeros = np.any(spokeLens2 < tol, axis=1) + onPoint = np.where(zeros) + offPoint = np.where(~zeros) + if onPoint[0].size: + pIdxs = spokeLens2[onPoint].argmin(axis=1) + barys[onPoint, pIdxs] = 1.0 + + if not offPoint[0].size: + continue + + # Ignore those points that have already been computed + idxs = offPoint[0] + spokes = spokes[offPoint] + spokeLens2 = spokeLens2[offPoint] + + # Get the signed area of each triangle created by the spokes + # and the dot product for the angle between each + rspokes = np.roll(spokes, -1, axis=1) + areas = np.cross(spokes, rspokes) * 0.5 + dots = (spokes * rspokes).sum(axis=-1) + + # Handle any samples that are directly on an edge between + # two corners. (Where the triangle area is 0, and the + # dot product is negative) + aareas = abs(areas) + zeros = aareas < tol + ndots = dots < 0.0 + zn = zeros & ndots + toLerp = np.any(zn, axis=-1) + onEdge = np.where(toLerp) + offEdge = np.where(~toLerp) + if onEdge[0].size: + subIdx = idxs[onEdge] + pIdxs = zn[onEdge].argmax(axis=-1) + xIdxs = (pIdxs + 1) % fc + + ppIdxs = cIdxs[subIdx, pIdxs] + xpIdxs = cIdxs[subIdx, xIdxs] + + cp = pts[subIdx] + pp = points[ppIdxs] + xp = points[xpIdxs] + + bases = pp - xp + diff = cp - xp + proj = (bases * diff).sum(axis=-1) + b = proj / (bases**2).sum(axis=1) + mb = 1.0 - b + barys[subIdx, pIdxs] = b + barys[subIdx, xIdxs] = mb + + if not offEdge[0].size: + continue + + idxs = idxs[offEdge] + dots = dots[offEdge] + areas = areas[offEdge] + spokeLens2 = spokeLens2[offEdge] + spokeLens = spokeLens2**0.5 + rspokeLens = np.roll(spokeLens, -1, axis=1) + + t = areas / (spokeLens * rspokeLens + dots) + rawWeights = (np.roll(t, -1, axis=1) + t) / spokeLens + b = rawWeights / rawWeights.sum(axis=1)[..., None] + barys[idxs] = b + + ret = {} + for qIdxs, barys in out.values(): + for qi, b in zip(qIdxs, barys): + ret[qi] = (uvToFace[qi], b) + + return ret + + +######################## +# Sweep algorithm +######################## + + +def triArea(a, b, c): + """Use the cross-ish-product to find the area of the triangle + Depending on the winding, the area could be positive or negative + + Parameters + ---------- + a : [float, float, float] + The first point + b : [float, float, float] + The second point + c : [float, float, float] + The third point + + Returns + ------- + : float + The area of the triangle + + """ + return (a[0] * (c[1] - b[1]) + b[0] * (a[1] - c[1]) + c[0] * (b[1] - a[1])) / 2.0 + + +def pointInTri(p, a, b, c, tol=EPS): + """Check that the point is inside the triangle + + Parameters + ---------- + p : [float, float, float] + The point to check is inside the triangle + a : [float, float, float] + The first point of the triangle + b : [float, float, float] + The second point of the triangle + c : [float, float, float] + The third point of the triangle + tol : float + A small tolerance value, defaulting to the global EPS + + Returns + ------- + : bool + Whether the point is inside the given triangle + + """ + area = abs(triArea(a, b, c)) + chis = abs(triArea(p, b, c)) + abs(triArea(a, p, c)) + abs(triArea(a, b, p)) + return abs(area - chis) < tol + + +def sweep(qPoints, uvs, tris, pBar=None): + """Get what triangle each query point is inside + + Runs a sweep-line algorithm to check for points in triangles. + Imagine a uv layout, and a bunch of points on the layout. + Now imagine a vertical line sweeping across the uv plane from the left + to the right. The sorted u-values of certain properties then handled + one-by-one. + + The algorithm keeps track of what triangles the vertical line is currently intersecting. + + * This is done by storing the min and max u-values for each triangle. + * If the u-value encountered is the min for a tri, that tri is added to the list + * If the u-value encountered is the max for a tri, that tri is removed from the list + + Then, each time a query point is encountered, it only has to check the intersection list + + Parameters + ---------- + qPoints : np.array + The points to get barycentric coordinates of + uvs : np.array + The UVs we're searching + tris : np.array + The Nx3 array of triangle indices + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + : {int: int, ...} + A dictionary of out[pointIndex] = triIndex + : set() + A set containing the pointIndexes that weren't found in a triangle + + """ + tpts = uvs[tris] # [tIdx, abc, xy] + + allmxs, allmns = tpts.max(axis=1), tpts.min(axis=1) + ymxs, ymns = allmxs[:, 1], allmns[:, 1] + mxs, mns = allmxs[:, 0], allmns[:, 0] + qpx = qPoints[:, 0] + qpSIdxs, mxSIdxs, mnSIdxs = np.argsort(qpx), np.argsort(mxs), np.argsort(mns) + qpSIdx, mxSIdx, mnSIdx = 0, 0, 0 + qpIdx, mxIdx, mnIdx = qpSIdxs[qpSIdx], mxSIdxs[mxSIdx], mnSIdxs[mnSIdx] + qp, mx, mn = qpx[qpIdx], mxs[mxIdx], mns[mnIdx] + + out = {} + missing = set() + + if pBar is not None: + pBar.setValue(0) + allVals = len(qpSIdxs) + len(mxSIdxs) + len(mnSIdxs) + pBar.setRange(0, allVals) + pBar.setLabelText("Sweeping ...") + from Qt.QtWidgets import QApplication + + QApplication.processEvents() + + # skip any triangles to the left of the first query point + # The algorithm will stop once we hit the last query point + skip = [qp > m for m in mxs] + activeTris = [False for _ in mxs] # For fast membership testing + atSet = set() # For fast iteration + + while True: + if pBar is not None: + cVal = qpSIdx + mnSIdx + mxSIdx + if cVal % 1283 == 0: # Just a random prime + pBar.setValue(cVal) + pBar.setLabelText("Sweeping ...\n{0}/{1}".format(cVal, allVals)) + QApplication.processEvents() + if pBar.wasCanceled(): + raise RuntimeError("Cancelled!") + + if mn <= mx and mn <= qp: + # Always add triangles first if possible + aTriIdx = mnSIdxs[mnSIdx] + if not activeTris[aTriIdx] and not skip[aTriIdx]: + activeTris[aTriIdx] = True + atSet.add(aTriIdx) + + mnSIdx += 1 + if mnSIdx != len(mnSIdxs): + mnIdx = mnSIdxs[mnSIdx] + mn = mns[mnIdx] + else: + mn = mxs[mxSIdxs[-1]] + 1 + + elif qp <= mx: + # Check query points between adding and removing + qPoint = qPoints[qpIdx] + yv = qPoint[1] + # A linear search is faster than more complex collections here + for t in atSet: + if ymns[t] <= yv <= ymxs[t]: + a, b, c = uvs[tris[t]] + if pointInTri(qPoint, a, b, c): + out[qpIdx] = t + break + else: + missing.add(qpIdx) + + qpSIdx += 1 + if qpSIdx == len(qpSIdxs): + break + qpIdx = qpSIdxs[qpSIdx] + qp = qpx[qpIdx] + + else: + # always remove triangles last + aTriIdx = mxSIdxs[mxSIdx] + if activeTris[aTriIdx] and not skip[aTriIdx]: + activeTris[aTriIdx] = False + atSet.remove(aTriIdx) + + mxSIdx += 1 + if mxSIdx != len(mxSIdxs): + mxIdx = mxSIdxs[mxSIdx] + mx = mxs[mxIdx] + + return out, missing + + +######################## +# Triangulation +######################## + + +def inBox(point, mxs, mns): + """Check if a point is inside the bounding box + + Parameters + ---------- + point : np.array + A 3d point + mxs : np.array + The max values of all the coordinates per axis + mns : np.array + The min values of all the coordinates per axis + + Returns + ------- + : bool + Whether the point is in the given bounding box + + """ + return np.all(point <= mxs) and np.all(point >= mns) + + +def _isEar(a, b, c, polygon, tol=EPS): + """Check if the points a,b,c of the polygon could be their own triangle""" + signedArea = triArea(a, b, c) + + # Check that the triange is wound the correct way. + if signedArea > 0: + return False + + # Check that the triangle has non-zero area + # we already know the area is negative from above + if -signedArea < tol: + return False + + # Check that none of the other points in the polygon are contained in triangle + for p in polygon: + if p not in (a, b, c): + if pointInTri(p, a, b, c, tol=tol): + return False + return True + + +def earclip(idxs, verts): + """Simple earclipping algorithm + For a polygon with n points it will return n-2 triangles. + + Parameters + ---------- + idxs : [int, ...] + The indices that make up a polygon + verts : np.array + All the UV positions + + Returns + ------- + : [[int, int, int], ...] + A list of triangle indices + + """ + earVerts = [] + tris = [] + idxs = list(idxs) + polygon = [tuple(verts[i]) for i in idxs] + + numPts = len(polygon) + for i in range(numPts): + prev = polygon[i - 2] + cur = polygon[i - 1] + nxt = polygon[i] + if _isEar(prev, cur, nxt, polygon): + earVerts.append(cur) + + while earVerts and numPts >= 3: + ear = earVerts.pop() + i = polygon.index(ear) + pi, ni = i - 1, (i + 1) % numPts + + prevPt = polygon[pi] + nxtPt = polygon[ni] + tris.append((idxs[pi], idxs[i], idxs[ni])) + + polygon.pop(i) + idxs.pop(i) + numPts -= 1 + if numPts > 3: + prePrePt = polygon[i - 2] + nxtNxtPt = polygon[(i + 1) % numPts] + + groups = [ + (prePrePt, prevPt, nxtPt, polygon), + (prevPt, nxtPt, nxtNxtPt, polygon), + ] + + for group in groups: + p = group[1] + if _isEar(*group): + if p not in earVerts: + earVerts.append(p) + elif p in earVerts: + earVerts.remove(p) + return tris + + +def triangulateUVs(faces, uvs): + """Take a set of uvFaces and uv points, and triangulate it + + Parameters + ---------- + faces : [[int, ...], ...] + A uv face structure + uvs : np.array + The uv positions + + Returns + ------- + : np.array + A Nx3 array of uv indexes making triangles + : [int, ...] + A list where the index is the triangle index, and the value is the face index + : {(int, int): int, ...} + A dictionary of border edge pairs to border faces + + """ + uvs = np.array(uvs) + triMap = [] + tris = [] + borderFaceMap = {} + # Build the naiive triangulation + # And get the borders while I'm looping + for f, face in enumerate(faces): + for i in range(2, len(face)): + triMap.append(f) + tris.append((face[0], face[i - 1], face[i])) + + for i in range(len(face)): + ep = (face[i - 1], face[i]) + if ep[0] > ep[1]: + ep = (ep[1], ep[0]) + if ep in borderFaceMap: + borderFaceMap.pop(ep) + else: + borderFaceMap[ep] = f + + tris = np.array(tris) + tuvs = uvs[tris] + + # Get the area using a 2d-pseudo-cross-product + a = tuvs[:, 0] - tuvs[:, 1] + b = tuvs[:, 2] - tuvs[:, 1] + signedAreas = a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0] + + # Things with negative area are wound backwards in this case + negArea = np.where(signedAreas < 0) + + # Do we need to retriangulate any of the polys + retri = sorted({triMap[i] for i in negArea[0]}) + if retri: + tris = tris.tolist() + tmpFaceMap = {} + # tmpFaceMap is only good while we re-triangulate + # it has to be re-built after + for t, f in enumerate(triMap): + tmpFaceMap.setdefault(f, []).append(t) + + for f in retri[::-1]: + good = earclip(faces[f], uvs) + tidxs = tmpFaceMap[f] + tris[tidxs[0] : tidxs[-1] + 1] = good + triMap[tidxs[0] : tidxs[-1] + 1] = [f] * len(good) + tris = np.array(tris) + + return tris, triMap, borderFaceMap + + +######################## +# UV Transferring +######################## + +MISSING = {} + + +def cooSparseMul(M, v): + """Multply the sparse matrix by the vector + + Parameters + ---------- + M : SparseMatrix + My own sparse matrix representation + v : np.array + A vector + + Returns + ------- + : np.array + The result of the multiplication + """ + # Using scipy is at least 2x faster than the + # pure numpy implementation. But even that + # is about 5x faster than using a dense matrix + try: + from scipy import sparse + except ImportError: + # No scipy, use numpy + # The numpy universal function (ufunc) .add.at() + # makes this possible without any python looping + row, col, val, shape = M + v = v.swapaxes(0, -2) + vshape = val.shape + tuple([1] * (len(v) - 1)) + val = val.reshape(vshape) + out = np.zeros(shape[:1] + v.shape[1:]) + np.add.at(out, row, val * v[col]) + out = out.swapaxes(0, -2) + else: + row, col, val, shape = M + spM = sparse.coo_matrix((val, (row, col)), shape=shape) + spM = sparse.csr_matrix(spM) + if len(v.shape) == 3: + out = np.array([spM.dot(frm) for frm in v]) + else: + out = spM.dot(v) + return out + + +def getUvCorrelation(samples, points, faces, tol=0.0001, handleMissing=True, pBar=None): + """Get the per-face correlation of the sample points in uv space. + + Parameters + ---------- + samples : np.array + The sample points + points : np.array + The background points + faces : [[int, ...], ...] + The Face list + tol : float + A small tolerance value, defaulting to the global EPS + handleMissing : + (Default value = True) + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + : {idx: (idx, [float, ...]), ...} + A dictionary from the sample index to a faceIdx/cornerWeights pair + + """ + tris, triMap, borderMap = triangulateUVs(faces, points) + swept, missing = sweep(samples, points, tris, pBar=pBar) + uvToFace = {uvI: triMap[tI] for uvI, tI in swept.items()} + # import __main__ + # __main__.__dict__.update(locals()) + # raise RuntimeError("STOPPIT") + if pBar is not None: + pBar.setValue(0) + pBar.setRange(0, len(uvToFace)) + pBar.setLabelText("Calculate Mean Value Coords") + from Qt.QtWidgets import QApplication + + QApplication.processEvents() + + # mvcDict = {} + # for i, (uvIdx, faceIdx) in enumerate(uvToFace.iteritems()): + # if pBar is not None: + # pBar.setValue(i) + # pBar.setLabelText("Calculate Mean Value Coords\n{0}/{1}".format(i, len(uvToFace))) + # QApplication.processEvents() + # uv = samples[uvIdx] + # corners = points[faces[faceIdx]] + # bary = mvc(corners, uv) + # mvcDict[uvIdx] = (faceIdx, bary) + + mvcDict = mmvc(faces, points, samples, uvToFace) + + # find the closest border + # Just use a brute-force search + # ... for now + + if missing and handleMissing: + if pBar is not None: + pBar.setValue(0) + pBar.setLabelText("Handle Missing") + QApplication.processEvents() + tol = tol**2 + bk = list(borderMap.keys()) + borders = np.array(bk) + bStarts = points[borders[:, 0]] + bEnds = points[borders[:, 1]] + + bDiff = bEnds - bStarts + bLens2 = (bDiff * bDiff).sum(axis=1) + for mIdx in missing: + mp = samples[mIdx] + dSquared = _mpCheck(bStarts, bDiff, bLens2, mp) + minIdx = np.argmin(dSquared) + if dSquared[mIdx] < tol: + faceIdx = borderMap[bk[minIdx]] + corners = points[faces[faceIdx]] + bary = mvc(corners, mp) + mvcDict[mIdx] = (faceIdx, bary) + + return mvcDict + + +def _mpCheck(a, d, dr2, pt): + """Point to Multi-segment squared distance. Uses pre-computed values""" + lerp = ((pt - a) * d).sum(axis=1) / dr2 + lerp = np.clip(lerp, 0, 1) + xy = (lerp[:, None] * d) + a + _dxy = xy - pt + return (_dxy * _dxy).sum(axis=1) + + +def getVertCorrelation( + sUvFaces, sUvs, tVertFaces, tUvFaces, tUvs, tol=0.0001, pBar=None +): + """Build the vertex position correlation between two meshes + by looking through UV-space. Handle combining multiple uvs + per vertex, and having samples that live outside the coverage + + Parameters + ---------- + sUvFaces : [[int, ...], ...] + The source UVFace list + sUvs : np.array + The source UV positions + tVertFaces : [[int, ...], ...] + The target vertex face list + tUvFaces : [[int, ...], ...] + The target UV face list + tUvs : np.array + The target UV positions + tol : float + A small tolerance value, defaulting to the global EPS + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + : {idx: (idx, [float, ...]), ...} + A dictionary from the sample index to a faceIdx/cornerWeights pair + """ + childUvToVert = {} + cNumVerts = -1 + for vF, uvF in zip(tVertFaces, tUvFaces): + for vIdx, uvIdx in zip(vF, uvF): + cNumVerts = max(cNumVerts, vIdx) + childUvToVert[uvIdx] = vIdx + + mvcUvDict = getUvCorrelation(tUvs, sUvs, sUvFaces, tol=tol, pBar=pBar) + + mvcVertDict = {} + for uvIdx in range(len(tUvs)): + if uvIdx not in mvcUvDict: + continue + mvcVertDict.setdefault(childUvToVert[uvIdx], []).append(mvcUvDict[uvIdx]) + + missing = set(range(cNumVerts)) - mvcVertDict.keys() + if missing: + import time + + v = time.time() + print( + "Missing correspondences found. Stored in uvTransfer.MISSING[{0}]".format(v) + ) + MISSING[v] = missing + + return mvcVertDict + + +def applyTransfer(parVerts, parFaces, correlation, outputSize): + """Given a vertex corelation, a driver, and driven points, + Apply the driver deformation to the driven. This could be + for one frame, or many frames + + Parameters + ---------- + parVerts : np.array + The "parent" vertex positions + parFaces : [[int, ...], ...] + The "parent" face list + correlation : {int: [int, ...], ...} + A dict correlatng the vertIdx to its possible correlations + outputSize : (int, ...) + A numpy output size tuple + + Returns + ------- + : np.array + The new vertex positions + + """ + if len(parVerts.shape) == 2: + parVerts = parVerts[None, ...] + + rows, cols, vals = [], [], [] + for cVertIdx, corrPoss in correlation.items(): + if len(corrPoss) == 1: + pFaceIdx, bary = corrPoss[0] + else: + # pick the one with the highest sum-of-squares + x = [sum(bary * bary) for _, bary in corrPoss] + idx = x.index(max(x)) + pFaceIdx, bary = corrPoss[idx] + + rows.extend([cVertIdx] * len(bary)) + vals.extend(bary) + cols.extend(parFaces[pFaceIdx]) + M = np.array(rows), np.array(cols), np.array(vals), (outputSize, parVerts.shape[-2]) + out = cooSparseMul(M, parVerts) + return out + + +def uvTransfer( + srcFaces, + srcUvFaces, + srcVerts, + srcUvs, + tarFaces, + tarUvFaces, + tarVerts, + tarUvs, + tol=0.0001, + pBar=None, +): + """A helper function that transfers pre-loaded data. + The source data will be transferred onto the tar data + + Parameters + ---------- + srcFaces : [[int, ...], ...] + The source vertex face list + srcUvFaces : [[int, ...], ...] + The source uv face list + srcUvs : np.array + The source UV positions + tarFaces : [[int, ...], ...] + The target vertex face list + tarUvFaces : [[int, ...], ...] + The target uv face list + tarUvs : np.array + The Target UV Positions + srcVerts : np.array + The source Vertex positions + tarVerts : np.array + The target Vertex positions + tol : float + A small tolerance value, defaulting to the global EPS + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + : np.array + The new target vert positions + + """ + corr = getVertCorrelation( + srcUvFaces, srcUvs, tarFaces, tarUvFaces, tarUvs, tol=tol, pBar=pBar + ) + if pBar is not None: + pBar.setValue(0) + pBar.setLabelText("Apply Transfer") + from Qt.QtWidgets import QApplication + + QApplication.processEvents() + + return applyTransfer(srcVerts, srcFaces, corr, len(tarVerts)) + + +def uvTransferLoad( + srcPath, tarPath, srcUvSet="default", tarUvSet="default", tol=0.0001, pBar=None +): + """Transfer the shape from the source to the target through uv space + Return the data needed to write out the result + + Parameters + ---------- + srcPath : str + The source mesh path (obj, abc, or smpx) + tarPath : str + The target mesh path (obj, abc, or smpx) + srcUvSet : str + The name of the uv set to use on the source + tarUvSet : str + The name of the uv set to use on the target + tol : float + A small tolerance value, defaulting to the global EPS + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + : np.array + The target vertex positions + : list + The target vertex faces + : np.array + The target uvs + : list + The target uv faces + + """ + from . import alembicCommon as abc + from .mesh import Mesh + + if srcPath.endswith(".abc") or srcPath.endswith(".smpx"): + src = Mesh.loadAbc(srcPath, ensureWinding=False) + srcVerts = abc.getSampleArray(abc.getMesh(srcPath)) + elif srcPath.endswith(".obj"): + src = Mesh.loadObj(srcPath, ensureWinding=False) + srcVerts = np.array(src.vertArray) + + if tarPath.endswith(".abc"): + tar = Mesh.loadAbc(tarPath, ensureWinding=False) + elif tarPath.endswith(".obj"): + tar = Mesh.loadObj(tarPath, ensureWinding=False) + + srcFaces = src.faceVertArray + srcUvFaces = src.uvFaceMap[srcUvSet] + srcUvs = np.array(src.uvMap[srcUvSet]) + + tarFaces = tar.faceVertArray + tarUvFaces = tar.uvFaceMap[tarUvSet] + tarUvs = np.array(tar.uvMap[tarUvSet]) + oldTarVerts = np.array(tar.vertArray) + tarVerts = uvTransfer( + srcFaces, + srcUvFaces, + srcVerts, + srcUvs, + tarFaces, + tarUvFaces, + oldTarVerts, + tarUvs, + tol=tol, + pBar=pBar, + ) + + return tarVerts, tarFaces, tarUvs, tarUvFaces + + +def uvTransferFiles( + srcPath, + tarPath, + outAbcPath, + srcUvSet="default", + tarUvSet="default", + tol=0.0001, + pBar=None, +): + """Transfer the shape from the source to the target through uv space + and write out the result + + Parameters + ---------- + srcPath : str + The source mesh path (obj, abc, or smpx) + tarPath : str + The target mesh path (obj, abc, or smpx) + outAbcPath : str + The path to the output .abc file + srcUvSet : str + The name of the uv set to use on the source + tarUvSet : str + The name of the uv set to use on the target + tol : float + A small tolerance value, defaulting to the global EPS + pBar : QProgressDialog, optional + An optional progress dialog + + Returns + ------- + + """ + from . import alembicCommon as abc + + tarVerts, tarFaces, tarUvs, tarUvFaces = uvTransferLoad( + srcPath, tarPath, srcUvSet=srcUvSet, tarUvSet=tarUvSet, tol=tol, pBar=pBar + ) + abc.buildAbc(outAbcPath, tarVerts, tarFaces, uvs=tarUvs, uvFaces=tarUvFaces) diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index f53c4fdb..8ffde6d2 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -1,299 +1,299 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - - -from Qt.QtCore import QEvent, QObject, QPoint, Qt, Signal -from Qt.QtGui import QCursor, QMouseEvent -from Qt.QtWidgets import QApplication - - -class DragFilter(QObject): - """Drag Event Filter - - This class provides an event filter that can be installed - on a Qt Widget to take click/drag events and give them slider - type interaction. - - Signals: - dragPressed = Signal() - Emitted when the drag passes the startSensitivity - dragTick (int, float): - NumberOfTicks, TickMultiplier - Emitted after dragPressed every time the drag passes dragSensitivity - dragReleased = Signal() - Emitted when the mouse is released after dragPressed - - Instance Options: - dragSensitivity(int): default=5 - The number of pixels the cursor has to travel to emit a tick - startSensitivity(int): default=10 - The number of pixels the cursor has to travel to enter into drag mode - cursorLock(bool): default=False - Whether the cursor returns to its start position every tick - wrapBoundary(int): default=10 - When wrapping around the current screen, this is the number of pixels - in from the edge of the screen that the cursor will appear - dragCursor(int): default=self.CURSOR_ARROWS - The cursor that will be displayed while dragging - CURSOR_ARROWS will show horizontal/vertical drag - CURSOR_BLANK will hid the cursor - dragButton(Qt.MouseButton): default=Qt.MouseButton.MiddleButton - The button that will kick off the drag behavior - fastModifier(Qt.KeyboardModifier): default=Qt.KeyboardModifier.ControlModifier - The modifier key that will cause the multiplier to emit with the signal - fastMultiplier(float): default=5.0 - The size of the multiplier - slowModifier(Qt.KeyboardModifier): default=Qt.KeyboardModifier.ShiftModifier - The modifier key that will cause the divisor to emit with the signal - slowDivisor(float): default=5.0 - The size of the divisor - isSpinbox(bool): default=False - This must be set to True if you are setting this as the handler - for a QSpinBox. The QSpinBox will tick every 0.25s as long as your mouse is - held down. This option turns that off - """ - - DRAG_ENABLED = 0 - DRAG_NONE = 0 - DRAG_HORIZONTAL = 1 - DRAG_VERTICAL = 2 - - CURSOR_NONE = 0 - CURSOR_BLANK = 1 - CURSOR_ARROWS = 2 - - dragTick = Signal(int, float) # NumberOfTicks, TickMultiplier - dragPressed = Signal() - dragReleased = Signal() - - def __init__(self, parent): - super(DragFilter, self).__init__(parent) - - self.dragSensitivity = 5 # pixels for one step - self.startSensitivity = 10 # pixel move to start the dragging - self.cursorLock = False - self.wrapBoundary = 10 # wrap when within boundary of screen edge - self.dragCursor = self.CURSOR_ARROWS - self.dragButton = Qt.MouseButton.MiddleButton - - # The QSpinbox has an option where, if you hold down the mouse button - # it will continually increment. This flag enables a workaround - # for that problem - self.isSpinbox = False - - self.fastModifier = Qt.KeyboardModifier.ControlModifier - self.slowModifier = Qt.KeyboardModifier.ShiftModifier - - self.fastMultiplier = 5.0 - self.slowDivisor = 5.0 - - # private vars - self._lastPos = QPoint() - self._leftover = 0 - self._dragStart = None - self._firstDrag = False - self._dragType = self.DRAG_NONE - self._overridden = False - self._screen = None - self._isDragging = False - - def doOverrideCursor(self): - """Change the cursor based on the current drag type""" - if self._overridden: - return - if self.dragCursor == self.CURSOR_BLANK: - QApplication.setOverrideCursor(Qt.CursorShape.BlankCursor) - elif self.dragCursor == self.CURSOR_ARROWS: - if self._dragType == self.DRAG_VERTICAL: - QApplication.setOverrideCursor(Qt.CursorShape.SizeVerCursor) - elif self._dragType == self.DRAG_HORIZONTAL: - QApplication.setOverrideCursor(Qt.CursorShape.SizeHorCursor) - - self._overridden = True - - def restoreOverrideCursor(self): - """Restore the cursor to the normal pointer""" - if not self._overridden: - return - QApplication.restoreOverrideCursor() - self._overridden = False - - def doDrag(self, o, e): - """Handle a mouse drag event - - Parameters - ---------- - o : QObject - The object that is being dragged - e : QEvent - The QEvent of the mouse drag - """ - if self._dragType == self.DRAG_HORIZONTAL: - delta = e.pos().x() - self._lastPos.x() - else: - delta = self._lastPos.y() - e.pos().y() - - self._leftover += delta - self._lastPos = e.pos() - - count = int(self._leftover / self.dragSensitivity) - if count: - mul = 1.0 - if e.modifiers() & self.fastModifier: - mul = self.fastMultiplier - elif e.modifiers() & self.slowModifier: - mul = 1.0 / self.slowDivisor - self.dragTick.emit(count, mul) - - # If we start off with negative leftover, then this modulo - # needs to return negative leftover steps - neg = self._leftover < 0 - self._leftover %= self.dragSensitivity - if neg and self._leftover > 0: - self._leftover -= self.dragSensitivity - - if self.cursorLock: - QCursor.setPos(self.mapToGlobal(self._dragStart)) - self._lastPos = self._dragStart - else: - r = self._screen - b = self.wrapBoundary - p = o.mapToGlobal(e.pos()) - - # when wrapping move to the other side in by 2*boundary - # so we don't loop the wrapping - if p.x() > r.right() - b: - p.setX(r.left() + 2 * b) - - if p.x() < r.left() + b: - p.setX(r.right() - 2 * b) - - if p.y() > r.bottom() - b: - p.setY(r.top() + 2 * b) - - if p.y() < r.top() + b: - p.setY(r.bottom() - 2 * b) - - if p != e.globalPos(): - QCursor.setPos(p) - self._lastPos = self.parent().mapFromGlobal(p) - self._leftover = 0 - - def startDrag(self, o, e): - """Start the drag event handling - - Parameters - ---------- - o : QObject - The object that is being dragged - e : QEvent - The QEvent of the mouse drag - """ - if self._dragStart is None: - self._dragStart = e.pos() - dtop = QApplication.desktop() - sn = dtop.screenNumber(o.mapToGlobal(e.pos())) - self._screen = dtop.availableGeometry(sn) - - if abs(e.x() - self._dragStart.x()) > self.startSensitivity: - self._dragType = self.DRAG_HORIZONTAL - elif abs(e.y() - self._dragStart.y()) > self.startSensitivity: - self._dragType = self.DRAG_VERTICAL - - if self._dragType: - self._leftover = 0 - self._lastPos = e.pos() - self._firstDrag = True - - self.dragPressed.emit() - self.doOverrideCursor() - - if self.isSpinbox: - if e.buttons() & self.dragButton: - # Send mouseRelease to spin buttons when dragging - # otherwise the spinbox will keep ticking. @longClickFix - # There's gotta be a better way to do this :-/ - mouseup = QMouseEvent( - QEvent.Type.MouseButtonRelease, - e.pos(), - self.dragButton, - e.buttons(), - e.modifiers(), - ) - QApplication.sendEvent(o, mouseup) - - def myendDrag(self, o, e): - """End the drag event handling. Can't call it endDrag because that's taken - - Parameters - ---------- - o : QObject - The object that is being dragged - e : QEvent - The QEvent of the mouse drag - """ - - # Only end dragging if it's *not* the first mouse release. See @longClickFix - if self._firstDrag and self.isSpinbox: - self._firstDrag = False - - elif self._dragType: - self.restoreOverrideCursor() - self._dragType = self.DRAG_NONE - self._lastPos = QPoint() - self._dragStart = None - self._screen = None - self.dragReleased.emit() - - def eventFilter(self, o, e): - """Overridden Qt eventFilter - - Parameters - ---------- - o : QObject - The object that is being dragged - e : QEvent - The QEvent of the mouse drag - """ - if hasattr(self, "DRAG_ENABLED"): - if e.type() == QEvent.Type.MouseMove: - if self._isDragging: - try: - if self._dragType != self.DRAG_NONE: - self.doDrag(o, e) - elif e.buttons() & self.dragButton: - self.startDrag(o, e) - except Exception: - # fix the cursor if there's an error during dragging - self.restoreOverrideCursor() - raise # re-raise the exception - return True - - elif e.type() == QEvent.Type.MouseButtonRelease: - self.myendDrag(o, e) - if e.button() & self.dragButton: - # Catch any dragbutton releases and handle them - self._isDragging = False - return True - - elif e.type() == QEvent.Type.MouseButtonPress: - if e.button() & self.dragButton: - # Catch any dragbutton presses and handle them - self._isDragging = True - return True - - return super(DragFilter, self).eventFilter(o, e) +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + + +from Qt.QtCore import QEvent, QObject, QPoint, Qt, Signal +from Qt.QtGui import QCursor, QMouseEvent +from Qt.QtWidgets import QApplication + + +class DragFilter(QObject): + """Drag Event Filter + + This class provides an event filter that can be installed + on a Qt Widget to take click/drag events and give them slider + type interaction. + + Signals: + dragPressed = Signal() + Emitted when the drag passes the startSensitivity + dragTick (int, float): + NumberOfTicks, TickMultiplier + Emitted after dragPressed every time the drag passes dragSensitivity + dragReleased = Signal() + Emitted when the mouse is released after dragPressed + + Instance Options: + dragSensitivity(int): default=5 + The number of pixels the cursor has to travel to emit a tick + startSensitivity(int): default=10 + The number of pixels the cursor has to travel to enter into drag mode + cursorLock(bool): default=False + Whether the cursor returns to its start position every tick + wrapBoundary(int): default=10 + When wrapping around the current screen, this is the number of pixels + in from the edge of the screen that the cursor will appear + dragCursor(int): default=self.CURSOR_ARROWS + The cursor that will be displayed while dragging + CURSOR_ARROWS will show horizontal/vertical drag + CURSOR_BLANK will hid the cursor + dragButton(Qt.MouseButton): default=Qt.MouseButton.MiddleButton + The button that will kick off the drag behavior + fastModifier(Qt.KeyboardModifier): default=Qt.KeyboardModifier.ControlModifier + The modifier key that will cause the multiplier to emit with the signal + fastMultiplier(float): default=5.0 + The size of the multiplier + slowModifier(Qt.KeyboardModifier): default=Qt.KeyboardModifier.ShiftModifier + The modifier key that will cause the divisor to emit with the signal + slowDivisor(float): default=5.0 + The size of the divisor + isSpinbox(bool): default=False + This must be set to True if you are setting this as the handler + for a QSpinBox. The QSpinBox will tick every 0.25s as long as your mouse is + held down. This option turns that off + """ + + DRAG_ENABLED = 0 + DRAG_NONE = 0 + DRAG_HORIZONTAL = 1 + DRAG_VERTICAL = 2 + + CURSOR_NONE = 0 + CURSOR_BLANK = 1 + CURSOR_ARROWS = 2 + + dragTick = Signal(int, float) # NumberOfTicks, TickMultiplier + dragPressed = Signal() + dragReleased = Signal() + + def __init__(self, parent): + super(DragFilter, self).__init__(parent) + + self.dragSensitivity = 5 # pixels for one step + self.startSensitivity = 10 # pixel move to start the dragging + self.cursorLock = False + self.wrapBoundary = 10 # wrap when within boundary of screen edge + self.dragCursor = self.CURSOR_ARROWS + self.dragButton = Qt.MouseButton.MiddleButton + + # The QSpinbox has an option where, if you hold down the mouse button + # it will continually increment. This flag enables a workaround + # for that problem + self.isSpinbox = False + + self.fastModifier = Qt.KeyboardModifier.ControlModifier + self.slowModifier = Qt.KeyboardModifier.ShiftModifier + + self.fastMultiplier = 5.0 + self.slowDivisor = 5.0 + + # private vars + self._lastPos = QPoint() + self._leftover = 0 + self._dragStart = None + self._firstDrag = False + self._dragType = self.DRAG_NONE + self._overridden = False + self._screen = None + self._isDragging = False + + def doOverrideCursor(self): + """Change the cursor based on the current drag type""" + if self._overridden: + return + if self.dragCursor == self.CURSOR_BLANK: + QApplication.setOverrideCursor(Qt.CursorShape.BlankCursor) + elif self.dragCursor == self.CURSOR_ARROWS: + if self._dragType == self.DRAG_VERTICAL: + QApplication.setOverrideCursor(Qt.CursorShape.SizeVerCursor) + elif self._dragType == self.DRAG_HORIZONTAL: + QApplication.setOverrideCursor(Qt.CursorShape.SizeHorCursor) + + self._overridden = True + + def restoreOverrideCursor(self): + """Restore the cursor to the normal pointer""" + if not self._overridden: + return + QApplication.restoreOverrideCursor() + self._overridden = False + + def doDrag(self, o, e): + """Handle a mouse drag event + + Parameters + ---------- + o : QObject + The object that is being dragged + e : QEvent + The QEvent of the mouse drag + """ + if self._dragType == self.DRAG_HORIZONTAL: + delta = e.pos().x() - self._lastPos.x() + else: + delta = self._lastPos.y() - e.pos().y() + + self._leftover += delta + self._lastPos = e.pos() + + count = int(self._leftover / self.dragSensitivity) + if count: + mul = 1.0 + if e.modifiers() & self.fastModifier: + mul = self.fastMultiplier + elif e.modifiers() & self.slowModifier: + mul = 1.0 / self.slowDivisor + self.dragTick.emit(count, mul) + + # If we start off with negative leftover, then this modulo + # needs to return negative leftover steps + neg = self._leftover < 0 + self._leftover %= self.dragSensitivity + if neg and self._leftover > 0: + self._leftover -= self.dragSensitivity + + if self.cursorLock: + QCursor.setPos(self.mapToGlobal(self._dragStart)) + self._lastPos = self._dragStart + else: + r = self._screen + b = self.wrapBoundary + p = o.mapToGlobal(e.pos()) + + # when wrapping move to the other side in by 2*boundary + # so we don't loop the wrapping + if p.x() > r.right() - b: + p.setX(r.left() + 2 * b) + + if p.x() < r.left() + b: + p.setX(r.right() - 2 * b) + + if p.y() > r.bottom() - b: + p.setY(r.top() + 2 * b) + + if p.y() < r.top() + b: + p.setY(r.bottom() - 2 * b) + + if p != e.globalPos(): + QCursor.setPos(p) + self._lastPos = self.parent().mapFromGlobal(p) + self._leftover = 0 + + def startDrag(self, o, e): + """Start the drag event handling + + Parameters + ---------- + o : QObject + The object that is being dragged + e : QEvent + The QEvent of the mouse drag + """ + if self._dragStart is None: + self._dragStart = e.pos() + dtop = QApplication.desktop() + sn = dtop.screenNumber(o.mapToGlobal(e.pos())) + self._screen = dtop.availableGeometry(sn) + + if abs(e.x() - self._dragStart.x()) > self.startSensitivity: + self._dragType = self.DRAG_HORIZONTAL + elif abs(e.y() - self._dragStart.y()) > self.startSensitivity: + self._dragType = self.DRAG_VERTICAL + + if self._dragType: + self._leftover = 0 + self._lastPos = e.pos() + self._firstDrag = True + + self.dragPressed.emit() + self.doOverrideCursor() + + if self.isSpinbox: + if e.buttons() & self.dragButton: + # Send mouseRelease to spin buttons when dragging + # otherwise the spinbox will keep ticking. @longClickFix + # There's gotta be a better way to do this :-/ + mouseup = QMouseEvent( + QEvent.Type.MouseButtonRelease, + e.pos(), + self.dragButton, + e.buttons(), + e.modifiers(), + ) + QApplication.sendEvent(o, mouseup) + + def myendDrag(self, o, e): + """End the drag event handling. Can't call it endDrag because that's taken + + Parameters + ---------- + o : QObject + The object that is being dragged + e : QEvent + The QEvent of the mouse drag + """ + + # Only end dragging if it's *not* the first mouse release. See @longClickFix + if self._firstDrag and self.isSpinbox: + self._firstDrag = False + + elif self._dragType: + self.restoreOverrideCursor() + self._dragType = self.DRAG_NONE + self._lastPos = QPoint() + self._dragStart = None + self._screen = None + self.dragReleased.emit() + + def eventFilter(self, o, e): + """Overridden Qt eventFilter + + Parameters + ---------- + o : QObject + The object that is being dragged + e : QEvent + The QEvent of the mouse drag + """ + if hasattr(self, "DRAG_ENABLED"): + if e.type() == QEvent.Type.MouseMove: + if self._isDragging: + try: + if self._dragType != self.DRAG_NONE: + self.doDrag(o, e) + elif e.buttons() & self.dragButton: + self.startDrag(o, e) + except Exception: + # fix the cursor if there's an error during dragging + self.restoreOverrideCursor() + raise # re-raise the exception + return True + + elif e.type() == QEvent.Type.MouseButtonRelease: + self.myendDrag(o, e) + if e.button() & self.dragButton: + # Catch any dragbutton releases and handle them + self._isDragging = False + return True + + elif e.type() == QEvent.Type.MouseButtonPress: + if e.button() & self.dragButton: + # Catch any dragbutton presses and handle them + self._isDragging = True + return True + + return super(DragFilter, self).eventFilter(o, e) diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index 699faa2b..e33df074 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -1,490 +1,490 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# This module imports QT from PyQt4, PySide or PySide2 -# Depending on what's available -import os -import re - -import Qt as QtLib -from .interfaceModel import FalloffDataModel -from .items import Falloff -from Qt import QtCompat -from Qt.QtCore import ( - QByteArray, - QLineF, - QPoint, - QPointF, - QRectF, - Qt, - Signal, -) -from Qt.QtGui import ( - QBrush, - QColor, - QPainter, - QPainterPath, - QPalette, - QPen, - QStandardItemModel, -) -from Qt.QtWidgets import ( - QComboBox, - QDataWidgetMapper, - QDialog, - QDoubleSpinBox, - QGroupBox, - QInputDialog, - QLabel, - QMessageBox, - QPushButton, - QSizePolicy, - QVBoxLayout, - QWidget, -) -from .utils import getNextName, getUiFile, Prefs - -AT_BLUR = os.environ.get("SIMPLEX_AT_BLUR") == "true" -NAME_CHECK = re.compile(r"[A-Za-z][\w.]*") - - -class CurveEditWidget(QWidget): - tangentUpdated = Signal(float, float) - - def __init__(self, parent): - super(CurveEditWidget, self).__init__(parent) - self.leftTan = None - self.rightTan = None - self._controlPoints = [ - QPointF(0, 1), - QPointF(0, 1), - QPointF(1, 0), - QPointF(1, 0), - ] - self.setTangent(leftTan=1 / 3.0, rightTan=2 / 3.0) - - self._activeControlPoint = None - self.mouseDrag = False - self.mousePress = QPoint() - self.startDragDistance = 20 - - self.canvasMargin = 16 - self.setMinimumHeight(2 * self.canvasMargin) - - self.bgColor = Qt.GlobalColor.white - self.lineColor = Qt.GlobalColor.black - self.limitColor = Qt.GlobalColor.gray - - def setTangent(self, leftTan=None, rightTan=None): - """Set the falloff tangents, clamped 0 to 1 - - Parameters - ---------- - leftTan : float - The x-value of the left tangent point - rightTan : float - The x-value of the right tangent point - """ - if leftTan is not None: - self.leftTan = max(min(leftTan, 1.0), 0.0) - self._controlPoints[1] = QPointF(self.leftTan, 1) - if rightTan is not None: - self.rightTan = max(min(rightTan, 1.0), 0.0) - self._controlPoints[2] = QPointF(self.rightTan, 0) - self.update() - - def mapToCanvas(self, point): - """Map a point from widget space to canvas space - The "canvas" is a 0-1 parameterized space, centered in the widget - The size of the canvas relative to the widget is dictated by the canvasMargin - - Parameters - ---------- - point : QPointF - The point to map - - Returns - ------- - : QPointF - The mapped point - """ - canvasWidth = self.width() - 2 * self.canvasMargin - canvasHeight = self.height() - 2 * self.canvasMargin - - x = point.x() * canvasWidth + self.canvasMargin - y = canvasHeight - point.y() * canvasHeight + self.canvasMargin - return QPointF(x, y) - - def mapFromCanvas(self, point): - """Map a point from canvas space to widget space - The "canvas" is a 0-1 parameterized space, centered in the widget - The size of the canvas relative to the widget is dictated by the canvasMargin - - Parameters - ---------- - point : QPointF - The point to map - - Returns - ------- - : QPointF - The mapped point - """ - canvasWidth = self.width() - 2 * self.canvasMargin - canvasHeight = self.height() - 2 * self.canvasMargin - - x = (point.x() - self.canvasMargin) / float(canvasWidth) - y = 1.0 - (point.y() - self.canvasMargin) / float(canvasHeight) - return QPointF(x, y) - - def _drawCleanLine(self, painter, p1, p2): - painter.drawLine(p1 + QPointF(0.5, 0.5), p2 + QPointF(0.5, 0.5)) - - def _paintBG(self, painter): - painter.save() - painter.setBrush(self.palette().color(QPalette.ColorRole.Window)) - painter.drawRect(0, 0, self.width(), self.height()) - painter.restore() - - def _paintLimits(self, painter): - painter.save() - # pen = QPen(self.limitColor) - baseColor = self.palette().color(QPalette.ColorRole.Base) - pen = QPen(baseColor) - pen.setWidth(1) - pen.setStyle(Qt.PenStyle.DashLine) - painter.setPen(pen) - self._drawCleanLine( - painter, self.mapToCanvas(QPoint(0, 0)), self.mapToCanvas(QPoint(1, 0)) - ) - self._drawCleanLine( - painter, self.mapToCanvas(QPoint(0, 1)), self.mapToCanvas(QPoint(1, 1)) - ) - painter.restore() - - def _paintPath(self, painter, p0, p1, p2, p3): - painter.save() - path = QPainterPath() - path.moveTo(p0) - path.cubicTo(p1, p2, p3) - # painter.strokePath(path, QPen(QBrush(self.lineColor), 2)) - foregroundColor = self.palette().color(QPalette.ColorRole.WindowText) - painter.strokePath(path, QPen(QBrush(foregroundColor), 2)) - painter.restore() - - def _paintTangents(self, painter, p0, p1, p2, p3): - # draw the tangent lines - foregroundColor = self.palette().color(QPalette.ColorRole.WindowText) - pen = QPen(foregroundColor) - pen.setWidth(1) - pen.setStyle(Qt.PenStyle.DashLine) - painter.setPen(pen) - painter.drawLine(p0, p1) - painter.drawLine(p3, p2) - - for i in range(len(self._controlPoints)): - online = self.indexIsRealPoint(i) - active = i == self._activeControlPoint - self.paintControlPoint(self._controlPoints[i], painter, online, active) - - def paintEvent(self, e): - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - - self._paintBG(painter) - self._paintLimits(painter) - - # load the points - p0 = self.mapToCanvas(self._controlPoints[0]) - p1 = self.mapToCanvas(self._controlPoints[1]) - p2 = self.mapToCanvas(self._controlPoints[2]) - p3 = self.mapToCanvas(self._controlPoints[3]) - - self._paintPath(painter, p0, p1, p2, p3) - self._paintTangents(painter, p0, p1, p2, p3) - - def indexIsRealPoint(self, i): - return (i % 3) == 0 - - def paintControlPoint(self, point, painter, real, active): - pointSize = 4 - - if real: - pointSize = 6 - painter.setBrush(QColor(80, 80, 210, 150)) - elif active: - painter.setBrush(QColor(140, 140, 240, 255)) - else: - painter.setBrush(QColor(120, 120, 220, 255)) - - painter.setPen(QColor(50, 50, 50, 140)) - - painter.drawRect( - QRectF( - self.mapToCanvas(point).x() - pointSize + 0.5, - self.mapToCanvas(point).y() - pointSize + 0.5, - pointSize * 2, - pointSize * 2, - ) - ) - - def findControlPoint(self, point, tolerance=10): - d = QLineF(self.mapToCanvas(self._controlPoints[1]), point).length() - if d < tolerance: - return 1 - - d = QLineF(self.mapToCanvas(self._controlPoints[2]), point).length() - if d < tolerance: - return 2 - return None - - def mousePressEvent(self, e): - if e.button() == Qt.MouseButton.LeftButton: - self._activeControlPoint = self.findControlPoint(e.pos()) - if self._activeControlPoint is not None: - self.mouseMoveEvent(e) - self.mousePress = e.pos() - e.accept() - - def mouseReleaseEvent(self, e): - if e.button() == Qt.MouseButton.LeftButton: - self._activeControlPoint = None - self.mouseDrag = False - e.accept() - - def mouseMoveEvent(self, e): - if ( - not self.mouseDrag - and QPoint(self.mousePress - e.pos()).manhattanLength() - > self.startDragDistance - ): - self.mouseDrag = True - - p = self.mapFromCanvas(e.pos()) - if self.mouseDrag and self._activeControlPoint is not None: - if self._activeControlPoint == 1: - self.setTangent(leftTan=min(max(p.x(), 0.0), 1.0)) - else: - self.setTangent(rightTan=min(max(p.x(), 0.0), 1.0)) - self.tangentUpdated.emit(self.leftTan, self.rightTan) - self.update() - - -class FalloffDialog(QDialog): - """The ui for interacting with Falloffs""" - - uiFalloffSettingsGRP: QGroupBox - uiShapeFalloffLBL: QLabel - uiShapeFalloffCBOX: QComboBox - uiShapeFalloffRenameBTN: QPushButton - uiShapeFalloffNewBTN: QPushButton - uiShapeFalloffDuplicateBTN: QPushButton - uiShapeFalloffDeleteBTN: QPushButton - uiFalloffLAY: QVBoxLayout - uiFalloffTypeCBOX: QComboBox - uiFalloffAxisCBOX: QComboBox - uiFalloffMaxSPN: QDoubleSpinBox - uiFalloffMaxHandleSPN: QDoubleSpinBox - uiFalloffMinHandleSPN: QDoubleSpinBox - uiFalloffMinSPN: QDoubleSpinBox - - def __init__(self, parent): - super(FalloffDialog, self).__init__(parent) - uiPath = getUiFile(__file__) - QtCompat.loadUi(uiPath, self) - self.parUI = parent - - self.simplex = None - self.parUI.simplexLoaded.connect(self.loadSimplex) - self.foModel = QStandardItemModel() - - self.uiFalloffWID = CurveEditWidget(self) - policy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding) - # policy.setVerticalStretch(1) - self.uiFalloffWID.setSizePolicy(policy) - self.uiFalloffWID.tangentUpdated.connect(self.updateTangents) - - self.uiFalloffLAY.addWidget(self.uiFalloffWID) - - self._falloffMapper = QDataWidgetMapper(self) - self.uiShapeFalloffCBOX.currentIndexChanged.connect( - self._falloffMapper.setCurrentIndex - ) - - # Falloff connections - self.uiShapeFalloffNewBTN.clicked.connect(self.newFalloff) - self.uiShapeFalloffDuplicateBTN.clicked.connect(self.duplicateFalloff) - self.uiShapeFalloffDeleteBTN.clicked.connect(self.deleteFalloff) - self.uiShapeFalloffRenameBTN.clicked.connect(self.renameFalloff) - - self.uiFalloffMaxHandleSPN.valueChanged.connect(self.setLeftTangent) - self.uiFalloffMinHandleSPN.valueChanged.connect(self.setRightTangent) - self.loadSimplex() - - def updateTangents(self, leftTangent, rightTangent): - self.uiFalloffMaxHandleSPN.setValue(leftTangent) - self.uiFalloffMinHandleSPN.setValue(rightTangent) - - cbIdx = self.uiShapeFalloffCBOX.currentIndex() - - leftTanIdx = self.foModel.index(cbIdx, 5) - rightTanIdx = self.foModel.index(cbIdx, 4) - - self.foModel.setData(leftTanIdx, leftTangent, role=Qt.ItemDataRole.EditRole) - self.foModel.setData(rightTanIdx, rightTangent, role=Qt.ItemDataRole.EditRole) - - def setLeftTangent(self, val): - self.uiFalloffWID.setTangent(leftTan=val) - - def setRightTangent(self, val): - self.uiFalloffWID.setTangent(rightTan=val) - - def loadSimplex(self): - """Load the Simplex system from the parent UI""" - system = self.parUI.simplex - if system == self.simplex: - return - - if system is None: - self.foModel = QStandardItemModel() - self.uiShapeFalloffCBOX.setModel(self.foModel) - if self._falloffMapper is not None: - self._falloffMapper.clearMapping() - self._falloffMapper.setModel(self.foModel) - self.uiFalloffSettingsGRP.setEnabled(False) - return - else: - self.uiFalloffSettingsGRP.setEnabled(True) - - self.simplex = system - - # Populate Settings widgets - self.foModel = FalloffDataModel(self.simplex, self) - self.simplex.falloffModels.append(self.foModel) - self.uiShapeFalloffCBOX.setModel(self.foModel) - self._falloffMapper.setModel(self.foModel) - - currentIndex = QByteArray("currentIndex".encode()) - - self._falloffMapper.addMapping(self.uiFalloffTypeCBOX, 1, currentIndex) - self._falloffMapper.addMapping(self.uiFalloffAxisCBOX, 2, currentIndex) - self._falloffMapper.addMapping(self.uiFalloffMinSPN, 3) - self._falloffMapper.addMapping(self.uiFalloffMinHandleSPN, 4) - self._falloffMapper.addMapping(self.uiFalloffMaxHandleSPN, 5) - self._falloffMapper.addMapping(self.uiFalloffMaxSPN, 6) - - self.uiShapeFalloffCBOX.setCurrentIndex(0) - self._falloffMapper.setCurrentIndex(0) - - # Falloff Settings - def newFalloff(self): - """Create a new Falloff object""" - if self.simplex is None: - return - foNames = [f.name for f in self.simplex.falloffs] - tempName = getNextName("NewFalloff", foNames) - - newName, good = QInputDialog.getText( - self, "Rename Falloff", "Enter a new name for the Falloff", text=tempName - ) - if not good: - return - - if not NAME_CHECK.match(newName): - message = "Falloff name can only contain letters and numbers, and cannot start with a number" - QMessageBox.warning(self, "Warning", message) - return - - nn = getNextName(newName, foNames) - Falloff.createPlanar(nn, self.simplex, "X", 1.0, 0.66, 0.33, -1.0) - - def duplicateFalloff(self): - """Duplicate the selected falloff""" - if self.simplex is None: - return - if not self.simplex.falloffs: - self.newFalloff() - return - - idx = self.uiShapeFalloffCBOX.currentIndex() - if idx < 0: - return - - fo = self.simplex.falloffs[idx] - - foNames = [f.name for f in self.simplex.falloffs] - nn = getNextName(fo.name, foNames) - fo.duplicate(nn) - - def deleteFalloff(self): - """Delete the selected falloff""" - if not self.simplex or not self.simplex.falloffs: - return - idx = self.uiShapeFalloffCBOX.currentIndex() - if idx < 0: - return - - fo = self.simplex.falloffs[idx] - fo.delete() - - def renameFalloff(self): - """Rename the selected falloff""" - if not self.simplex or not self.simplex.falloffs: - return - idx = self.uiShapeFalloffCBOX.currentIndex() - if idx < 0: - return - fo = self.simplex.falloffs[idx] - foNames = [f.name for f in self.simplex.falloffs] - foNames.pop(idx) - - newName, good = QInputDialog.getText( - self, "Rename Falloff", "Enter a new name for the Falloff", text=fo.name - ) - if not good: - return - - if not NAME_CHECK.match(newName): - message = "Falloff name can only contain letters and numbers, and cannot start with a number" - QMessageBox.warning(self, "Warning", message) - return - - nn = getNextName(newName, foNames) - fo.name = nn - - def storeSettings(self): - """Store the UI settings for this dialog""" - pref = Prefs() - pref.recordProperty("fogeometry", self.saveGeometry()) - pref.save() - - def loadSettings(self): - """Load the UI settings for this dialog""" - pref = Prefs() - geo = pref.restoreProperty("fogeometry", None) - if geo is not None: - self.restoreGeometry(geo) - - def hideEvent(self, event): - """Override the hide event to store settings""" - self.storeSettings() - super(FalloffDialog, self).hideEvent(event) - - def showEvent(self, event): - """Override the show event to restore settings""" - super(FalloffDialog, self).showEvent(event) - self.loadSettings() +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# This module imports QT from PyQt4, PySide or PySide2 +# Depending on what's available +import os +import re + +import Qt as QtLib +from .interfaceModel import FalloffDataModel +from .items import Falloff +from Qt import QtCompat +from Qt.QtCore import ( + QByteArray, + QLineF, + QPoint, + QPointF, + QRectF, + Qt, + Signal, +) +from Qt.QtGui import ( + QBrush, + QColor, + QPainter, + QPainterPath, + QPalette, + QPen, + QStandardItemModel, +) +from Qt.QtWidgets import ( + QComboBox, + QDataWidgetMapper, + QDialog, + QDoubleSpinBox, + QGroupBox, + QInputDialog, + QLabel, + QMessageBox, + QPushButton, + QSizePolicy, + QVBoxLayout, + QWidget, +) +from .utils import getNextName, getUiFile, Prefs + +AT_BLUR = os.environ.get("SIMPLEX_AT_BLUR") == "true" +NAME_CHECK = re.compile(r"[A-Za-z][\w.]*") + + +class CurveEditWidget(QWidget): + tangentUpdated = Signal(float, float) + + def __init__(self, parent): + super(CurveEditWidget, self).__init__(parent) + self.leftTan = None + self.rightTan = None + self._controlPoints = [ + QPointF(0, 1), + QPointF(0, 1), + QPointF(1, 0), + QPointF(1, 0), + ] + self.setTangent(leftTan=1 / 3.0, rightTan=2 / 3.0) + + self._activeControlPoint = None + self.mouseDrag = False + self.mousePress = QPoint() + self.startDragDistance = 20 + + self.canvasMargin = 16 + self.setMinimumHeight(2 * self.canvasMargin) + + self.bgColor = Qt.GlobalColor.white + self.lineColor = Qt.GlobalColor.black + self.limitColor = Qt.GlobalColor.gray + + def setTangent(self, leftTan=None, rightTan=None): + """Set the falloff tangents, clamped 0 to 1 + + Parameters + ---------- + leftTan : float + The x-value of the left tangent point + rightTan : float + The x-value of the right tangent point + """ + if leftTan is not None: + self.leftTan = max(min(leftTan, 1.0), 0.0) + self._controlPoints[1] = QPointF(self.leftTan, 1) + if rightTan is not None: + self.rightTan = max(min(rightTan, 1.0), 0.0) + self._controlPoints[2] = QPointF(self.rightTan, 0) + self.update() + + def mapToCanvas(self, point): + """Map a point from widget space to canvas space + The "canvas" is a 0-1 parameterized space, centered in the widget + The size of the canvas relative to the widget is dictated by the canvasMargin + + Parameters + ---------- + point : QPointF + The point to map + + Returns + ------- + : QPointF + The mapped point + """ + canvasWidth = self.width() - 2 * self.canvasMargin + canvasHeight = self.height() - 2 * self.canvasMargin + + x = point.x() * canvasWidth + self.canvasMargin + y = canvasHeight - point.y() * canvasHeight + self.canvasMargin + return QPointF(x, y) + + def mapFromCanvas(self, point): + """Map a point from canvas space to widget space + The "canvas" is a 0-1 parameterized space, centered in the widget + The size of the canvas relative to the widget is dictated by the canvasMargin + + Parameters + ---------- + point : QPointF + The point to map + + Returns + ------- + : QPointF + The mapped point + """ + canvasWidth = self.width() - 2 * self.canvasMargin + canvasHeight = self.height() - 2 * self.canvasMargin + + x = (point.x() - self.canvasMargin) / float(canvasWidth) + y = 1.0 - (point.y() - self.canvasMargin) / float(canvasHeight) + return QPointF(x, y) + + def _drawCleanLine(self, painter, p1, p2): + painter.drawLine(p1 + QPointF(0.5, 0.5), p2 + QPointF(0.5, 0.5)) + + def _paintBG(self, painter): + painter.save() + painter.setBrush(self.palette().color(QPalette.ColorRole.Window)) + painter.drawRect(0, 0, self.width(), self.height()) + painter.restore() + + def _paintLimits(self, painter): + painter.save() + # pen = QPen(self.limitColor) + baseColor = self.palette().color(QPalette.ColorRole.Base) + pen = QPen(baseColor) + pen.setWidth(1) + pen.setStyle(Qt.PenStyle.DashLine) + painter.setPen(pen) + self._drawCleanLine( + painter, self.mapToCanvas(QPoint(0, 0)), self.mapToCanvas(QPoint(1, 0)) + ) + self._drawCleanLine( + painter, self.mapToCanvas(QPoint(0, 1)), self.mapToCanvas(QPoint(1, 1)) + ) + painter.restore() + + def _paintPath(self, painter, p0, p1, p2, p3): + painter.save() + path = QPainterPath() + path.moveTo(p0) + path.cubicTo(p1, p2, p3) + # painter.strokePath(path, QPen(QBrush(self.lineColor), 2)) + foregroundColor = self.palette().color(QPalette.ColorRole.WindowText) + painter.strokePath(path, QPen(QBrush(foregroundColor), 2)) + painter.restore() + + def _paintTangents(self, painter, p0, p1, p2, p3): + # draw the tangent lines + foregroundColor = self.palette().color(QPalette.ColorRole.WindowText) + pen = QPen(foregroundColor) + pen.setWidth(1) + pen.setStyle(Qt.PenStyle.DashLine) + painter.setPen(pen) + painter.drawLine(p0, p1) + painter.drawLine(p3, p2) + + for i in range(len(self._controlPoints)): + online = self.indexIsRealPoint(i) + active = i == self._activeControlPoint + self.paintControlPoint(self._controlPoints[i], painter, online, active) + + def paintEvent(self, e): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + self._paintBG(painter) + self._paintLimits(painter) + + # load the points + p0 = self.mapToCanvas(self._controlPoints[0]) + p1 = self.mapToCanvas(self._controlPoints[1]) + p2 = self.mapToCanvas(self._controlPoints[2]) + p3 = self.mapToCanvas(self._controlPoints[3]) + + self._paintPath(painter, p0, p1, p2, p3) + self._paintTangents(painter, p0, p1, p2, p3) + + def indexIsRealPoint(self, i): + return (i % 3) == 0 + + def paintControlPoint(self, point, painter, real, active): + pointSize = 4 + + if real: + pointSize = 6 + painter.setBrush(QColor(80, 80, 210, 150)) + elif active: + painter.setBrush(QColor(140, 140, 240, 255)) + else: + painter.setBrush(QColor(120, 120, 220, 255)) + + painter.setPen(QColor(50, 50, 50, 140)) + + painter.drawRect( + QRectF( + self.mapToCanvas(point).x() - pointSize + 0.5, + self.mapToCanvas(point).y() - pointSize + 0.5, + pointSize * 2, + pointSize * 2, + ) + ) + + def findControlPoint(self, point, tolerance=10): + d = QLineF(self.mapToCanvas(self._controlPoints[1]), point).length() + if d < tolerance: + return 1 + + d = QLineF(self.mapToCanvas(self._controlPoints[2]), point).length() + if d < tolerance: + return 2 + return None + + def mousePressEvent(self, e): + if e.button() == Qt.MouseButton.LeftButton: + self._activeControlPoint = self.findControlPoint(e.pos()) + if self._activeControlPoint is not None: + self.mouseMoveEvent(e) + self.mousePress = e.pos() + e.accept() + + def mouseReleaseEvent(self, e): + if e.button() == Qt.MouseButton.LeftButton: + self._activeControlPoint = None + self.mouseDrag = False + e.accept() + + def mouseMoveEvent(self, e): + if ( + not self.mouseDrag + and QPoint(self.mousePress - e.pos()).manhattanLength() + > self.startDragDistance + ): + self.mouseDrag = True + + p = self.mapFromCanvas(e.pos()) + if self.mouseDrag and self._activeControlPoint is not None: + if self._activeControlPoint == 1: + self.setTangent(leftTan=min(max(p.x(), 0.0), 1.0)) + else: + self.setTangent(rightTan=min(max(p.x(), 0.0), 1.0)) + self.tangentUpdated.emit(self.leftTan, self.rightTan) + self.update() + + +class FalloffDialog(QDialog): + """The ui for interacting with Falloffs""" + + uiFalloffSettingsGRP: QGroupBox + uiShapeFalloffLBL: QLabel + uiShapeFalloffCBOX: QComboBox + uiShapeFalloffRenameBTN: QPushButton + uiShapeFalloffNewBTN: QPushButton + uiShapeFalloffDuplicateBTN: QPushButton + uiShapeFalloffDeleteBTN: QPushButton + uiFalloffLAY: QVBoxLayout + uiFalloffTypeCBOX: QComboBox + uiFalloffAxisCBOX: QComboBox + uiFalloffMaxSPN: QDoubleSpinBox + uiFalloffMaxHandleSPN: QDoubleSpinBox + uiFalloffMinHandleSPN: QDoubleSpinBox + uiFalloffMinSPN: QDoubleSpinBox + + def __init__(self, parent): + super(FalloffDialog, self).__init__(parent) + uiPath = getUiFile(__file__) + QtCompat.loadUi(uiPath, self) + self.parUI = parent + + self.simplex = None + self.parUI.simplexLoaded.connect(self.loadSimplex) + self.foModel = QStandardItemModel() + + self.uiFalloffWID = CurveEditWidget(self) + policy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding) + # policy.setVerticalStretch(1) + self.uiFalloffWID.setSizePolicy(policy) + self.uiFalloffWID.tangentUpdated.connect(self.updateTangents) + + self.uiFalloffLAY.addWidget(self.uiFalloffWID) + + self._falloffMapper = QDataWidgetMapper(self) + self.uiShapeFalloffCBOX.currentIndexChanged.connect( + self._falloffMapper.setCurrentIndex + ) + + # Falloff connections + self.uiShapeFalloffNewBTN.clicked.connect(self.newFalloff) + self.uiShapeFalloffDuplicateBTN.clicked.connect(self.duplicateFalloff) + self.uiShapeFalloffDeleteBTN.clicked.connect(self.deleteFalloff) + self.uiShapeFalloffRenameBTN.clicked.connect(self.renameFalloff) + + self.uiFalloffMaxHandleSPN.valueChanged.connect(self.setLeftTangent) + self.uiFalloffMinHandleSPN.valueChanged.connect(self.setRightTangent) + self.loadSimplex() + + def updateTangents(self, leftTangent, rightTangent): + self.uiFalloffMaxHandleSPN.setValue(leftTangent) + self.uiFalloffMinHandleSPN.setValue(rightTangent) + + cbIdx = self.uiShapeFalloffCBOX.currentIndex() + + leftTanIdx = self.foModel.index(cbIdx, 5) + rightTanIdx = self.foModel.index(cbIdx, 4) + + self.foModel.setData(leftTanIdx, leftTangent, role=Qt.ItemDataRole.EditRole) + self.foModel.setData(rightTanIdx, rightTangent, role=Qt.ItemDataRole.EditRole) + + def setLeftTangent(self, val): + self.uiFalloffWID.setTangent(leftTan=val) + + def setRightTangent(self, val): + self.uiFalloffWID.setTangent(rightTan=val) + + def loadSimplex(self): + """Load the Simplex system from the parent UI""" + system = self.parUI.simplex + if system == self.simplex: + return + + if system is None: + self.foModel = QStandardItemModel() + self.uiShapeFalloffCBOX.setModel(self.foModel) + if self._falloffMapper is not None: + self._falloffMapper.clearMapping() + self._falloffMapper.setModel(self.foModel) + self.uiFalloffSettingsGRP.setEnabled(False) + return + else: + self.uiFalloffSettingsGRP.setEnabled(True) + + self.simplex = system + + # Populate Settings widgets + self.foModel = FalloffDataModel(self.simplex, self) + self.simplex.falloffModels.append(self.foModel) + self.uiShapeFalloffCBOX.setModel(self.foModel) + self._falloffMapper.setModel(self.foModel) + + currentIndex = QByteArray("currentIndex".encode()) + + self._falloffMapper.addMapping(self.uiFalloffTypeCBOX, 1, currentIndex) + self._falloffMapper.addMapping(self.uiFalloffAxisCBOX, 2, currentIndex) + self._falloffMapper.addMapping(self.uiFalloffMinSPN, 3) + self._falloffMapper.addMapping(self.uiFalloffMinHandleSPN, 4) + self._falloffMapper.addMapping(self.uiFalloffMaxHandleSPN, 5) + self._falloffMapper.addMapping(self.uiFalloffMaxSPN, 6) + + self.uiShapeFalloffCBOX.setCurrentIndex(0) + self._falloffMapper.setCurrentIndex(0) + + # Falloff Settings + def newFalloff(self): + """Create a new Falloff object""" + if self.simplex is None: + return + foNames = [f.name for f in self.simplex.falloffs] + tempName = getNextName("NewFalloff", foNames) + + newName, good = QInputDialog.getText( + self, "Rename Falloff", "Enter a new name for the Falloff", text=tempName + ) + if not good: + return + + if not NAME_CHECK.match(newName): + message = "Falloff name can only contain letters and numbers, and cannot start with a number" + QMessageBox.warning(self, "Warning", message) + return + + nn = getNextName(newName, foNames) + Falloff.createPlanar(nn, self.simplex, "X", 1.0, 0.66, 0.33, -1.0) + + def duplicateFalloff(self): + """Duplicate the selected falloff""" + if self.simplex is None: + return + if not self.simplex.falloffs: + self.newFalloff() + return + + idx = self.uiShapeFalloffCBOX.currentIndex() + if idx < 0: + return + + fo = self.simplex.falloffs[idx] + + foNames = [f.name for f in self.simplex.falloffs] + nn = getNextName(fo.name, foNames) + fo.duplicate(nn) + + def deleteFalloff(self): + """Delete the selected falloff""" + if not self.simplex or not self.simplex.falloffs: + return + idx = self.uiShapeFalloffCBOX.currentIndex() + if idx < 0: + return + + fo = self.simplex.falloffs[idx] + fo.delete() + + def renameFalloff(self): + """Rename the selected falloff""" + if not self.simplex or not self.simplex.falloffs: + return + idx = self.uiShapeFalloffCBOX.currentIndex() + if idx < 0: + return + fo = self.simplex.falloffs[idx] + foNames = [f.name for f in self.simplex.falloffs] + foNames.pop(idx) + + newName, good = QInputDialog.getText( + self, "Rename Falloff", "Enter a new name for the Falloff", text=fo.name + ) + if not good: + return + + if not NAME_CHECK.match(newName): + message = "Falloff name can only contain letters and numbers, and cannot start with a number" + QMessageBox.warning(self, "Warning", message) + return + + nn = getNextName(newName, foNames) + fo.name = nn + + def storeSettings(self): + """Store the UI settings for this dialog""" + pref = Prefs() + pref.recordProperty("fogeometry", self.saveGeometry()) + pref.save() + + def loadSettings(self): + """Load the UI settings for this dialog""" + pref = Prefs() + geo = pref.restoreProperty("fogeometry", None) + if geo is not None: + self.restoreGeometry(geo) + + def hideEvent(self, event): + """Override the hide event to store settings""" + self.storeSettings() + super(FalloffDialog, self).hideEvent(event) + + def showEvent(self, event): + """Override the show event to restore settings""" + super(FalloffDialog, self).showEvent(event) + self.loadSettings() diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index 3546163c..f467c157 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -1,1585 +1,1585 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint: disable=invalid-name, unused-argument -"""A placeholder interface that takes arguments and does nothing with them""" - -import copy -from contextlib import contextmanager -from functools import wraps - -from Qt import QtCore -from Qt.QtCore import Signal - -try: - import numpy as np -except ImportError: - np = None - -from alembic.AbcGeom import OPolyMeshSchemaSample - -from ..commands.alembicCommon import ( - getSampleArray, - getStaticMeshData, - getUvSample, - mkSampleIntArray, - mkSampleVertexPoints, - mkUvSample, -) -from Qt.QtWidgets import QApplication - - -# UNDO STACK INTEGRATION -@contextmanager -def undoContext(inst=None): - """A context that wraps undo chunks - - Parameters - ---------- - inst : DCC - An instantiated DCC if available. (Default value = None) - - """ - if inst is None: - DCC.staticUndoOpen() - else: - inst.undoOpen() - try: - yield - finally: - if inst is None: - DCC.staticUndoClose() - else: - inst.undoClose() - - -def undoable(f): - """A decorator that wraps a function in an undoContext""" - - @wraps(f) - def stacker(self, *args, **kwargs): - """The wrapper closure""" - with undoContext(): - return f(self, *args, **kwargs) - - return stacker - - -class DummyScene(object): - """A dcc scene containing existing dummy objects""" - - def __init__(self): - self.items = {} - - def get(self, tpe, name): - return self.items.get(tpe, {}).get(name) - - def add(self, item): - typeDict = self.items.setdefault(type(item), {}) - typeDict[item.name] = item - - def remove(self, item): - typeDict = self.items.setdefault(type(item), {}) - typeDict.pop(item.name, None) - - -DB = DummyScene() # Default module level dummy scene - - -class DummyAttr(object): - """A generic named object attribute""" - - def __init__(self, name, value, parent): - self.name = name - self.value = value - self.parent = parent - parent.attrs[name] = self - - -class DummyNode(object): - """A generic DCC node""" - - def __init__(self, name, db=DB): - self.name = name - self.attrs = {} - self.ops = [] - if db is not None: - db.add(self) - - -class DummySimplex(DummyNode): - """A generic simplex node""" - - def __init__(self, name, parent, db=DB): - super(DummySimplex, self).__init__(name, db) - self.definition = "" - parent.ops.append(self) - - -class DummyFalloff(DummyNode): - """A generic simplex node""" - - def __init__(self, name, parent, db=DB): - super(DummyFalloff, self).__init__(name, db) - self.weightmap = None - - -class DummyShape(object): - """A generic named blendshape shape""" - - def __init__(self, name, shapeNode): - self.name = name - self.value = 0.0 - self.points = None - self.shapeNode = shapeNode - shapeNode.shapes[name] = self - - -class DummyBlendshape(DummyNode): - """A generic blendshape node""" - - def __init__(self, name, parent, db=DB): - # TODO: Just reuse the DummyAttr instead of making an equivalent - super(DummyBlendshape, self).__init__(name, db) - self.shapes = {} - parent.ops.append(self) - - -class DummyMesh(DummyNode): - """A generic mesh""" - - def __init__(self, name, db=DB): - super(DummyMesh, self).__init__(name, db) - self.importPath = "" - self.faces = None - self.counts = None - self.uvs = None - self.uvFaces = None - self.verts = None - - -class DCC(object): - """ """ - - program = "dummy" - - def __init__(self, simplex, stack=None): - self.simplex = simplex # the abstract representation of the setup - self.name = simplex.name - - self.scene = DummyScene() - self.mesh = None - self.ctrl = None - self.shapeNode = None - self.op = None - - self._live = True - self._revision = 0 - # self._shapes = {} # hold the shapes from the .smpx file as a dict - # self._faces = None # Faces for the mesh (Alembic-style) - # self._counts = None # Face counts for the mesh (Alembic-style) - # self._uvs = None # UV data for the mesh - # self._numVerts = None - self._falloffs = {} # weightPerVert values - self.sliderMul = self.simplex.sliderMul - - def dummyLoad(self, other, pBar=None): - """Method to copy the information in a DCC to a DummyDCC - - Parameters - ---------- - other : DCC - The DCC to load into the dummy - pBar : QProgressDialog, optional - An optional progress dialog (Default value = None) - - Returns - ------- - : DummyDCC : - The new DummyDCC - """ - other.getAllShapeVertices(other.simplex.shapes, pBar=pBar) - - # Load all the data onto the new dummy mesh - points, faces, counts, uvs, uvFaces = other.getMeshTopology(other.mesh) - dummyMesh = self.buildRawTopology( - other.name, points, faces, counts, uvs, uvFaces - ) - self.loadNodes(self.simplex, dummyMesh) - - # Re-create the dummy shapes and sliders - for shape in self.simplex.shapes: - shape.thing = DummyShape(shape.name, self.shapeNode) - - for oShape, nShape in zip(other.simplex.shapes, self.simplex.shapes): - nShape.verts = copy.copy(oShape.verts) - - self.pushAllShapeVertices(self.simplex.shapes) - restVerts = self.simplex.restShape.verts - - for slider in self.simplex.sliders: - slider.thing = DummyAttr(slider.name, 0.0, self.ctrl) - - for fo in self.simplex.falloffs: - fo.thing = DummyFalloff(fo.name, self.scene) - fo.verts = restVerts - - def preLoad(self, simp, simpDict, create=True, pBar=None): - """Code to execute before loading a simplex system into a dcc - - Parameters - ---------- - simp : Simplex - The simplex system that will be created - simpDict : dict - The dictionary definition of the simplex to be created - create : bool - Whether to create the missing objects in the DCC (Default value = True) - pBar : QProgressDialog, optional - An optional progress dialog (Default value = None) - - Returns - ------- - : object : - A generic python object to be used in the post-load - """ - return None - - def postLoad(self, simp, preRet): - """Code to execute after loading a simplex system into a dcc - - Parameters - ---------- - simp : Simplex - The simplex system that was created - preRet : object - The object returnd from the preload method - - """ - pass - - def checkForErrors(self, window): - """Check for any DCC specific errors - - Parameters - ---------- - window : QMainWindow - The simplex window - """ - pass - - # System IO - @undoable - def loadNodes(self, simp, thing, create=True, pBar=None): - """Load the nodes from a simplex system onto a thing - - Parameters - ---------- - simp : Simplex - The system we're loading for - thing : object - The DCC thing we're reading - create : bool - Whether to create the missing nodes (Default value = True) - pBar : QProgressDialog, optional - An optional progress dialog (Default value = None) - - """ - if thing is None: - thing = DummyMesh(simp.name) - - self.name = simp.name - self.mesh = thing - self.scene.add(self.mesh) - - self.shapeNode = self.scene.get(DummyBlendshape, self.name) - - if self.shapeNode is None: - if not create: - raise RuntimeError( - "Blendshape operator not found with creation turned off: {0}".format( - self.name - ) - ) - self.shapeNode = DummyBlendshape(self.name, self.mesh, self.scene) - - self.op = self.scene.get(DummySimplex, self.name) - if self.op is None: - if not create: - raise RuntimeError( - "Simplex operator not found with creation turned off" - ) - self.op = DummySimplex(self.name, self.mesh, self.scene) - - self.ctrl = self.scene.get(DummyNode, self.name) - if self.ctrl is None: - if not create: - raise RuntimeError("Control object not found with creation turned off") - self.ctrl = DummyNode(self.name, self.scene) - - def loadConnections(self, simp, pBar=None): - """Load the connections that exist in a simplex system - - Parameters - ---------- - simp : Simplex - The simplex system to load connections for - pBar : QProgressDialog, optional - An optional progress dialog (Default value = None) - - """ - pass - - def getShapeThing(self, shapeName): - """Get the DCC Thing for a given shape name - - Parameters - ---------- - shapeName : str - The name of the shape to get - - Returns - ------- - : object : - The DCC Thing - - """ - return self.shapeNode.shapes.get(shapeName) - - def getSliderThing(self, sliderName): - """Get the DCC Thing for a given slider name - - Parameters - ---------- - sliderName : str - The name of the slider to get - - Returns - ------- - : object : - The DCC Thing - """ - return self.ctrl.attrs.get(sliderName) - - @staticmethod - @undoable - def buildRestAbc(abcMesh, name): - """Build the rest Alembic node in the dcc - - Parameters - ---------- - abcMesh : IPolyMesh - The Alembic mesh object - name : str - The name of the object to create - - """ - mesh = DummyMesh(name) # don't add it to a scene - - sa = getSampleArray(abcMesh) - if len(sa.shape) == 3: - sa = sa[0] - mesh.verts = sa - faces, counts = getStaticMeshData(abcMesh) - uvs = getUvSample(abcMesh) - - aryType = list if np is None else np.array - if uvs is not None: - mesh.uvs = aryType(uvs.getVals()) - mesh.uvFaces = aryType(uvs.getIndices()) - mesh.faces = aryType(faces) - mesh.counts = aryType(counts) - - return mesh - - @staticmethod - @undoable - def buildRawTopology(name, points, faces, counts, uvs=None, uvFaces=None): - """Build a mesh directly from raw numerical data""" - # TODO: Move this guy out to the rest of the DCC's - mesh = DummyMesh(name) # don't add it to a scene - - aryType = list if np is None else np.array - mesh.points = aryType(points) - mesh.faces = aryType(faces) - mesh.counts = aryType(counts) - if uvs is not None and uvFaces is not None: - mesh.uvs = aryType(uvs) - mesh.uvFaces = aryType(uvFaces) - return mesh - - @staticmethod - def vertCount(mesh): - """Get the vert count of the given DCC Object - - Parameters - ---------- - mesh : object - The mesh to check - - Returns - ------- - : int : - The Number of verts - """ - return len(mesh.verts) - - @undoable - def loadAbcPoses(self, abcMesh, js, pBar=None): - """Load the joints/skin from an alembic file onto an already-created system - - Parameters - ---------- - abcMesh : IPolyMesh - The Alembic mesh to load shapes from - js : dict - The simplex definition dictionary - pBar : QProgressDialog, optional - An optional progress dialog (Default value = None) - - """ - pass - - @undoable - def loadAbc(self, abcMesh, js, pBar=None): - """Load the shapes from an alembic file onto an already-created system - - Parameters - ---------- - abcMesh : IPolyMesh - The Alembic mesh to load shapes from - js : dict - The simplex definition dictionary - pBar : QProgressDialog, optional - An optional progress dialog (Default value = None) - - """ - shapes = js["shapes"] - if js["encodingVersion"] > 1: - shapes = [i["name"] for i in shapes] - pointPositions = getSampleArray(abcMesh) - for name, ppos in zip(shapes, pointPositions): - dummyShape = self.shapeNode.shapes[name] - dummyShape.points = ppos - - def getAllShapeVertices(self, shapes, pBar=None): - """Load all shape vertices into the simplex system for processing - - Parameters - ---------- - shapes : [Shape, ...] - A list of simplex Shape objects to get positions for - - pBar : QProgressDialog, optional - An optional progress dialog (Default value = None) - - """ - for shape in shapes: - verts = self.getShapeVertices(shape) - shape.verts = verts - - def getShapeVertices(self, shape): - """Get the point positions of a shape - - Parameters - ---------- - shape : Shape - A simplex Shape object to get the vertices for - - Returns - ------- - : np.array : - A numpy array of the point positions - - """ - return shape.thing.points - - def pushAllShapeVertices(self, shapes, pBar=None): - """Push the computed vertex positions for the given shapes back to the DCC - - Parameters - ---------- - shapes : [Shape, ...] - A list of simplex Shape objects - - pBar : QProgressDialog, optional - An optional progress dialog (Default value = None) - - """ - for shape in shapes: - self.pushShapeVertices(shape) - - def pushShapeVertices(self, shape): - """Push the computed vertex positions for the given shape back to the DCC - Parameters - ---------- - shape : Shape - The Simplex Shape object to update - - """ - shape.thing.points = shape.verts - - def loadMeshTopology(self): - """Load the mesh topology from the DCC into the simplex interface""" - # Here in Dummy I either have the data already or I don't, So nothing to do - pass - - @staticmethod - def getMeshTopology(mesh, uvName=None): - """Get the topology of a mesh - - Parameters - ---------- - mesh : object - The DCC Mesh to read - uvName : str, optional - The name of the uv set to read - - Returns - ------- - : np.array : - The vertex array - : np.array : - The "faces" array - : np.array : - The "counts" array - : np.array : - The uv positions - : np.array : - The "uvFaces" array - """ - return mesh.verts, mesh.faces, mesh.counts, mesh.uvs, mesh.uvFaces - - def exportAbc( - self, dccMesh, abcMesh, js, world=False, ensureCorrect=False, pBar=None - ): - """Export a .smpx file - - Parameters - ---------- - dccMesh : object - The DCC Mesh to export - abcMesh : OPolyMesh - The Alembic output mesh - js : dict - The definition dictionary - world : bool - Do the export in worldspace (Default value = False) - pBar : QProgressDialog, optional - An optional progress dialog (Default value = None) - - """ - # export the data to alembic - if dccMesh is None: - dccMesh = self.mesh - - shapeDict = {i.name: i for i in self.simplex.shapes} - - shapeNames = js["shapes"] - if js["encodingVersion"] > 1: - shapeNames = [i["name"] for i in shapeNames] - shapes = [shapeDict[i] for i in shapeNames] - schema = abcMesh.getSchema() - - if pBar is not None: - pBar.show() - pBar.setMaximum(len(shapes)) - spacerName = "_" * max(list(map(len, shapeNames))) - pBar.setLabelText("Exporting:\n{0}".format(spacerName)) - QApplication.processEvents() - - faces = mkSampleIntArray(self.mesh.faces) - counts = mkSampleIntArray(self.mesh.counts) - uvs = None - if self.mesh.uvs is not None and self.mesh.uvFaces is not None: - uvs = mkUvSample(self.mesh.uvs, self.mesh.uvFaces) - - for i, shape in enumerate(shapes): - if pBar is not None: - pBar.setLabelText("Exporting:\n{0}".format(shape.name)) - pBar.setValue(i) - QApplication.processEvents() - if pBar.wasCanceled(): - return - verts = mkSampleVertexPoints(shape.thing.points) - if uvs is not None: - # Alembic doesn't allow for uvs=None for some reason - abcSample = OPolyMeshSchemaSample(verts, faces, counts, uvs) - else: - abcSample = OPolyMeshSchemaSample(verts, faces, counts) - schema.set(abcSample) - - def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): - """Export a .smpx file of a mesh other than self.mesh - - Parameters - ---------- - dccMesh : object - The DCC Mesh to export - abcMesh : OPolyMesh - The Alembic output mesh - js : dict - The definition dictionary - world : bool - Do the export in worldspace (Default value = False) - pBar : QProgressDialog, optional - An optional progress dialog (Default value = None) - - """ - if dccMesh is None: - raise ValueError( - "Export Other requires an explicitly defined mesh to export" - ) - self.exportAbc( - dccMesh, abcMesh, js, world=world, ensureCorrect=False, pBar=pBar - ) - - def deleteObj(self, dccMesh, path): - """Export a mesh to the given path""" - pass - - def exportMesh(self, dccMesh, path): - """Export a mesh to the given path""" - pass - - # Revision tracking - def getRevision(self): - """Get the simplex revision number""" - return self._revision - - def incrementRevision(self): - """Increment the revision number""" - self._revision += 1 - return self._revision - - def setRevision(self, val): - """Manually set the revision numer - - Parameters - ---------- - val : int - The value to set - - """ - self._revision = val - - # System level - @undoable - def renameSystem(self, name): - """Rename a simplex system - - Parameters - ---------- - name : str - The new name - - """ - # TODO - # oldName = self.name - self.name = name - # for dd in (DB.nodes, DB.ops, DB.bss, DB.meshes): - # oo = dd.get(oldName) - # if oo is not None: - # oo.name = self.name - # dd[self.name] = oo - # dd.pop(oldName, None) - - @undoable - def deleteSystem(self): - """Delete the current system""" - # for dd in (DB.nodes, DB.ops, DB.bss, DB.meshes): - # if self.name in dd: - # dd.pop(self.name, None) - # TODO - self.name = None - self.simplex = None - - # Shapes - @undoable - def createShape(self, shape, live=False, offset=10): - """Create a dcc shape - - Parameters - ---------- - shape : Shape - A simplex shape object - live : bool - Whether this shape is live-connected (Default value = False) - offset : float - The offset of the created shape (Default value = 10) - - """ - newShape = DummyShape(shape.name, self.shapeNode) - newShape.points = copy.copy(self.mesh.verts) - return newShape - - @undoable - def extractWithDeltaShape(self, shape, live=True, offset=10.0): - """Make a mesh representing a shape. Can be live or not. - Also, make a shapenode that is the delta of the change being made - - Parameters - ---------- - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - """ - pass - - @undoable - def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0): - """Extract a shape with a live partial delta added in. - Useful for updating progressive shapes - - Parameters - ---------- - shape : - - delta : - - value : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - """ - pass - - @undoable - def extractShape(self, shape, live=True, offset=10.0): - """Make a mesh representing a shape. Can be live or not. - Can also store its starting shape and delta data - - Parameters - ---------- - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - """ - pass - - @undoable - def connectShape(self, shape, mesh=None, live=False, delete=False): - """Force a shape to match a mesh - The "connect shape" button is: - mesh=None, delete=True - The "match shape" button is: - mesh=someMesh, delete=False - There is a possibility of a "make live" button: - live=True, delete=False - - Parameters - ---------- - shape : - - mesh : - (Default value = None) - live : - (Default value = False) - delete : - (Default value = False) - - """ - pass - - @undoable - def extractPosedShape(self, shape): - """??? - - Parameters - ---------- - shape : - - """ - pass - - @undoable - def zeroShape(self, shape): - """Set a shape back to rest - - Parameters - ---------- - shape : Shape - The simplex shpae to zero out - - """ - shape.thing.points = self.getShapeVertices(self.simplex.restShape) - - @undoable - def deleteShape(self, toDelShape): - """Delete a shape from the system - - Parameters - ---------- - toDelShape : - - """ - self.shapeNode.shapes.pop(toDelShape.name, None) - - @undoable - def renameShape(self, shape, name): - """Rename a shape - - Parameters - ---------- - shape : Shape - The simplex Shape object to rename - name : str - The new name - - """ - self.shapeNode.shapes.pop(shape.thing.name, None) - shape.thing.name = name - self.shapeNode.shapes[name] = shape.thing - - @undoable - def convertShapeToCorrective(self, shape): - """??? - - Parameters - ---------- - shape : - - """ - pass - - # Falloffs - def createFalloff(self, falloff): - """Create a per-vert falloff weightmap - - Parameters - ---------- - falloff : Falloff - The simplex Falloff object to create - - """ - fo = DummyFalloff(falloff.name, self.scene) - fo.weights = np.zeros(len(self.mesh.verts)) - - def duplicateFalloff(self, falloff, newFalloff): - """Create a new falloff from an already existing one - - Parameters - ---------- - falloff : Falloff - The already existing falloff - newFalloff : Falloff - The newly created falloff to store the newly duplicated data - - """ - fo = DummyFalloff(newFalloff.name, self.scene) - fo.weights = copy.copy(falloff.thing.weights) - - def deleteFalloff(self, falloff): - """Delete a falloff object - - Parameters - ---------- - falloff : Falloff - The Falloff object to delete - - """ - self.scene.remove(falloff.thing) - - def setFalloffData( - self, falloff, splitType, axis, minVal, minHandle, maxHandle, maxVal, mapName - ): - """Set the data of a falloff object - - Parameters - ---------- - falloff : - - splitType : - - axis : - - minVal : - - minHandle : - - maxHandle : - - maxVal : - - mapName : - - """ - # TODO: set the per-vert falloffs - pass # for eventual live splits - - def getFalloffThing(self, falloff): - """Get the thing for a given falloff - - Parameters - ---------- - falloff : Falloff - The simplex falloff object to get - - """ - return self.scene.get(DummyFalloff, falloff.name) - - # Sliders - @undoable - def createSlider(self, slider): - """Create a slider object - - Parameters - ---------- - slider : Slider - The simplex slider object to create - - """ - return DummyAttr(slider.name, 0.0, self.ctrl) - - @undoable - def renameSlider(self, slider, name): - """Rename a slider - - Parameters - ---------- - slider : Slider - The slider to rename - name : str - The new name - - """ - self.ctrl.attrs.pop(slider.thing.name, None) - self.ctrl.attrs[name] = slider.thing - slider.thing.name = name - - @undoable - def setSliderRange(self, slider): - """Set the min and max of a slider - - Parameters - ---------- - slider : Slider - The slider to set - - """ - pass - - @undoable - def deleteSlider(self, toDelSlider): - """Delete a slider - - Parameters - ---------- - toDelSlider : Slider - The slider to delete - - """ - self.ctrl.attrs.pop(toDelSlider.name, None) - - @undoable - def addProgFalloff(self, prog, falloff): - """ - - Parameters - ---------- - prog : - - falloff : - - """ - pass # for eventual live splits - - @undoable - def removeProgFalloff(self, prog, falloff): - """ - - Parameters - ---------- - prog : - - falloff : - - """ - pass # for eventual live splits - - @undoable - def setSlidersWeights(self, sliders, weights): - """Set the values for the given sliders - - Parameters - ---------- - sliders : [Slider, ...] - The sliders to set values for - weights : [float, ...] - The values to set - - """ - for slider, val in zip(sliders, weights): - slider.thing.value = val - - @undoable - def setSliderWeight(self, slider, weight): - """Set the value for a given slider - - Parameters - ---------- - slider : Slider - The slider - weight : float - The value - - """ - slider.thing.value = weight - - @undoable - def updateSlidersRange(self, sliders): - """Update the range of the given sliders - - Parameters - ---------- - sliders : - - """ - pass - - @undoable - def extractTraversalShape(self, trav, shape, live=True, offset=10.0): - """Extract a shape from a traversal progression - - Parameters - ---------- - trav : - - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - """ - pass - - @undoable - def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False): - """Connect a shape to a traversal progression - - Parameters - ---------- - trav : - - shape : - - mesh : - (Default value = None) - live : - (Default value = True) - delete : - (Default value = False) - - """ - pass - - # Combos - @undoable - def extractComboShape(self, combo, shape, live=True, offset=10.0): - """Extract a shape from a combo progression - - Parameters - ---------- - combo : - - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - """ - pass - - @undoable - def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): - """Connect a shape to a combo progression - - Parameters - ---------- - combo : - - shape : - - mesh : - (Default value = None) - live : - (Default value = True) - delete : - (Default value = False) - - """ - pass - - @staticmethod - def setDisabled(op): - """Disable the output of any simplex systems - - Parameters - ---------- - op : The operator to disable - - - Returns - ------- - : object : - Some object that will allow us to re-enable the system - - """ - return None - - @staticmethod - def reEnable(helpers): - """Re-enable a simplex system - - Parameters - ---------- - helpers : object - The helper object returned from setDisabled - - """ - pass - - @undoable - def renameCombo(self, combo, name): - """Set the name of a Combo - - Parameters - ---------- - combo : Combo - The combo to rename - name : str - The new name - - """ - pass - - # Data Access - @staticmethod - def getSimplexOperators(): - """Get all simplex operators in the DCC""" - return list(DB.ops.values()) - - @staticmethod - def getSimplexOperatorsByName(name): - """Get a simplex operator - - Parameters - ---------- - name : str - The name to search for - - - Returns - ------- - : object : - The simplex operator for the DCC - - """ - return DB.ops.get(name) - - @staticmethod - def getSimplexOperatorsOnObject(thing): - """Get all simplex operators controlling an object - - Parameters - ---------- - thing : object - A dcc object to check for simplex operators - - Returns - ------- - : [object, ...] : - A list of simplex operators - - """ - return [o for o in thing.ops if isinstance(o, DummySimplex)] - - @staticmethod - def getSimplexString(op): - """Get the simplex string from the given operator - - Parameters - ---------- - op : object - The Simplex operator to get the definition from - - Returns - ------- - : str : - The simplex definition - - """ - return op.definition - - @staticmethod - def getSimplexStringOnThing(thing, systemName): - """Get the definition on an object by name - - Parameters - ---------- - thing : object - The DCC object to check for a simplex operator - systemName : str - The system name to check for - - Returns - ------- - : str : - The simplex definition - - """ - for op in thing.ops: - if op.name == systemName: - return op.definition - return None - - @staticmethod - def setSimplexString(op, val): - """Set the definition string on an object - - Parameters - ---------- - op : object - The operator to set the definition on - val : str - The definition to set - - Returns - ------- - - """ - op.definition = val - - @staticmethod - def selectObject(thing): - """Select an object in the DCC - - Parameters - ---------- - thing : - - """ - pass - - def selectCtrl(self): - """Select the system's control object""" - pass - - @staticmethod - def getObjectByName(name): - """Get an object by name - - Parameters - ---------- - name : str - The name to search for - - Returns - ------- - : object : - The found object - - """ - # TODO: maybe also filter by type?? - # return DB.meshes.get(name) - return DB.get(DummyMesh, name) - - @staticmethod - def getObjectName(thing): - """Get the name of an object - - Parameters - ---------- - thing : object - The dcc object to get the name for - - - Returns - ------- - : str : - The Object Name - - """ - return thing.name - - @staticmethod - def staticUndoOpen(): - """Open an undo chunk without knowledge of Simplex""" - pass - - @staticmethod - def staticUndoClose(): - """Close an undo chunk without knowledge of Simplex""" - pass - - def undoOpen(self): - """Open an undo chunk with knowledge of Simplex""" - pass - - def undoClose(self): - """Close an undo chunk with knowledge of Simplex""" - pass - - @classmethod - def getPersistentFalloff(cls, thing): - """Get a representation of the given object that won't get deleted or garbage collected - - Parameters - ---------- - thing : object - The thing to get a persistent representation of - - Returns - ------- - : object : - The requested persistent object - - """ - return cls.getObjectName(thing) - - @classmethod - def loadPersistentFalloff(cls, thing): - """Get the usable representation of the given persistent thing - - Parameters - ---------- - thing : object - A persistent representation - - Returns - ------- - : object : - The requested volatile object - - """ - return cls.getObjectByName(thing) - - @classmethod - def getPersistentShape(cls, thing): - """Get a representation of the given object that won't get deleted or garbage collected - - Parameters - ---------- - thing : object - The thing to get a persistent representation of - - Returns - ------- - : object : - The requested persistent object - - """ - return cls.getObjectName(thing) - - @classmethod - def loadPersistentShape(cls, thing): - """Get the usable representation of the given persistent thing - - Parameters - ---------- - thing : object - A persistent representation - - Returns - ------- - : object : - The requested volatile object - - """ - return cls.getObjectByName(thing) - - @classmethod - def getPersistentSlider(cls, thing): - """Get a representation of the given object that won't get deleted or garbage collected - - Parameters - ---------- - thing : object - The thing to get a persistent representation of - - Returns - ------- - : object : - The requested persistent object - - """ - return cls.getObjectName(thing) - - @classmethod - def loadPersistentSlider(cls, thing): - """Get the usable representation of the given persistent thing - - Parameters - ---------- - thing : object - A persistent representation - - Returns - ------- - : object : - The requested volatile object - - """ - return cls.getObjectByName(thing) - - @staticmethod - def getSelectedObjects(): - """Get the selected objects""" - # Here in the dummy interface, we short-circuit this - # And return a default selected object called "thing" - return [DummyNode("thing")] - - def getFreezeThing(self, combo): - return [] - - -class SliderDispatch(QtCore.QObject): - """ """ - - valueChanged = Signal() - - def __init__(self, node, parent=None): - super(SliderDispatch, self).__init__(parent) - - def emitValueChanged(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ - self.valueChanged.emit() - - -class Dispatch(QtCore.QObject): - """ """ - - beforeNew = Signal() - afterNew = Signal() - beforeOpen = Signal() - afterOpen = Signal() - undo = Signal() - redo = Signal() - - def __init__(self, parent=None): - super(Dispatch, self).__init__(parent) - - def connectCallbacks(self): - """ """ - pass - - def disconnectCallbacks(self): - """ """ - pass - - def emitBeforeNew(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ - self.beforeNew.emit() - - def emitAfterNew(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ - self.afterNew.emit() - - def emitBeforeOpen(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ - self.beforeOpen.emit() - - def emitAfterOpen(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ - self.afterOpen.emit() - - def emitUndo(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ - self.undo.emit() - - def emitRedo(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ - self.redo.emit() - - -DISPATCH = Dispatch() - - -def rootWindow(): - """ """ - return None +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint: disable=invalid-name, unused-argument +"""A placeholder interface that takes arguments and does nothing with them""" + +import copy +from contextlib import contextmanager +from functools import wraps + +from Qt import QtCore +from Qt.QtCore import Signal + +try: + import numpy as np +except ImportError: + np = None + +from alembic.AbcGeom import OPolyMeshSchemaSample + +from ..commands.alembicCommon import ( + getSampleArray, + getStaticMeshData, + getUvSample, + mkSampleIntArray, + mkSampleVertexPoints, + mkUvSample, +) +from Qt.QtWidgets import QApplication + + +# UNDO STACK INTEGRATION +@contextmanager +def undoContext(inst=None): + """A context that wraps undo chunks + + Parameters + ---------- + inst : DCC + An instantiated DCC if available. (Default value = None) + + """ + if inst is None: + DCC.staticUndoOpen() + else: + inst.undoOpen() + try: + yield + finally: + if inst is None: + DCC.staticUndoClose() + else: + inst.undoClose() + + +def undoable(f): + """A decorator that wraps a function in an undoContext""" + + @wraps(f) + def stacker(self, *args, **kwargs): + """The wrapper closure""" + with undoContext(): + return f(self, *args, **kwargs) + + return stacker + + +class DummyScene(object): + """A dcc scene containing existing dummy objects""" + + def __init__(self): + self.items = {} + + def get(self, tpe, name): + return self.items.get(tpe, {}).get(name) + + def add(self, item): + typeDict = self.items.setdefault(type(item), {}) + typeDict[item.name] = item + + def remove(self, item): + typeDict = self.items.setdefault(type(item), {}) + typeDict.pop(item.name, None) + + +DB = DummyScene() # Default module level dummy scene + + +class DummyAttr(object): + """A generic named object attribute""" + + def __init__(self, name, value, parent): + self.name = name + self.value = value + self.parent = parent + parent.attrs[name] = self + + +class DummyNode(object): + """A generic DCC node""" + + def __init__(self, name, db=DB): + self.name = name + self.attrs = {} + self.ops = [] + if db is not None: + db.add(self) + + +class DummySimplex(DummyNode): + """A generic simplex node""" + + def __init__(self, name, parent, db=DB): + super(DummySimplex, self).__init__(name, db) + self.definition = "" + parent.ops.append(self) + + +class DummyFalloff(DummyNode): + """A generic simplex node""" + + def __init__(self, name, parent, db=DB): + super(DummyFalloff, self).__init__(name, db) + self.weightmap = None + + +class DummyShape(object): + """A generic named blendshape shape""" + + def __init__(self, name, shapeNode): + self.name = name + self.value = 0.0 + self.points = None + self.shapeNode = shapeNode + shapeNode.shapes[name] = self + + +class DummyBlendshape(DummyNode): + """A generic blendshape node""" + + def __init__(self, name, parent, db=DB): + # TODO: Just reuse the DummyAttr instead of making an equivalent + super(DummyBlendshape, self).__init__(name, db) + self.shapes = {} + parent.ops.append(self) + + +class DummyMesh(DummyNode): + """A generic mesh""" + + def __init__(self, name, db=DB): + super(DummyMesh, self).__init__(name, db) + self.importPath = "" + self.faces = None + self.counts = None + self.uvs = None + self.uvFaces = None + self.verts = None + + +class DCC(object): + """ """ + + program = "dummy" + + def __init__(self, simplex, stack=None): + self.simplex = simplex # the abstract representation of the setup + self.name = simplex.name + + self.scene = DummyScene() + self.mesh = None + self.ctrl = None + self.shapeNode = None + self.op = None + + self._live = True + self._revision = 0 + # self._shapes = {} # hold the shapes from the .smpx file as a dict + # self._faces = None # Faces for the mesh (Alembic-style) + # self._counts = None # Face counts for the mesh (Alembic-style) + # self._uvs = None # UV data for the mesh + # self._numVerts = None + self._falloffs = {} # weightPerVert values + self.sliderMul = self.simplex.sliderMul + + def dummyLoad(self, other, pBar=None): + """Method to copy the information in a DCC to a DummyDCC + + Parameters + ---------- + other : DCC + The DCC to load into the dummy + pBar : QProgressDialog, optional + An optional progress dialog (Default value = None) + + Returns + ------- + : DummyDCC : + The new DummyDCC + """ + other.getAllShapeVertices(other.simplex.shapes, pBar=pBar) + + # Load all the data onto the new dummy mesh + points, faces, counts, uvs, uvFaces = other.getMeshTopology(other.mesh) + dummyMesh = self.buildRawTopology( + other.name, points, faces, counts, uvs, uvFaces + ) + self.loadNodes(self.simplex, dummyMesh) + + # Re-create the dummy shapes and sliders + for shape in self.simplex.shapes: + shape.thing = DummyShape(shape.name, self.shapeNode) + + for oShape, nShape in zip(other.simplex.shapes, self.simplex.shapes): + nShape.verts = copy.copy(oShape.verts) + + self.pushAllShapeVertices(self.simplex.shapes) + restVerts = self.simplex.restShape.verts + + for slider in self.simplex.sliders: + slider.thing = DummyAttr(slider.name, 0.0, self.ctrl) + + for fo in self.simplex.falloffs: + fo.thing = DummyFalloff(fo.name, self.scene) + fo.verts = restVerts + + def preLoad(self, simp, simpDict, create=True, pBar=None): + """Code to execute before loading a simplex system into a dcc + + Parameters + ---------- + simp : Simplex + The simplex system that will be created + simpDict : dict + The dictionary definition of the simplex to be created + create : bool + Whether to create the missing objects in the DCC (Default value = True) + pBar : QProgressDialog, optional + An optional progress dialog (Default value = None) + + Returns + ------- + : object : + A generic python object to be used in the post-load + """ + return None + + def postLoad(self, simp, preRet): + """Code to execute after loading a simplex system into a dcc + + Parameters + ---------- + simp : Simplex + The simplex system that was created + preRet : object + The object returnd from the preload method + + """ + pass + + def checkForErrors(self, window): + """Check for any DCC specific errors + + Parameters + ---------- + window : QMainWindow + The simplex window + """ + pass + + # System IO + @undoable + def loadNodes(self, simp, thing, create=True, pBar=None): + """Load the nodes from a simplex system onto a thing + + Parameters + ---------- + simp : Simplex + The system we're loading for + thing : object + The DCC thing we're reading + create : bool + Whether to create the missing nodes (Default value = True) + pBar : QProgressDialog, optional + An optional progress dialog (Default value = None) + + """ + if thing is None: + thing = DummyMesh(simp.name) + + self.name = simp.name + self.mesh = thing + self.scene.add(self.mesh) + + self.shapeNode = self.scene.get(DummyBlendshape, self.name) + + if self.shapeNode is None: + if not create: + raise RuntimeError( + "Blendshape operator not found with creation turned off: {0}".format( + self.name + ) + ) + self.shapeNode = DummyBlendshape(self.name, self.mesh, self.scene) + + self.op = self.scene.get(DummySimplex, self.name) + if self.op is None: + if not create: + raise RuntimeError( + "Simplex operator not found with creation turned off" + ) + self.op = DummySimplex(self.name, self.mesh, self.scene) + + self.ctrl = self.scene.get(DummyNode, self.name) + if self.ctrl is None: + if not create: + raise RuntimeError("Control object not found with creation turned off") + self.ctrl = DummyNode(self.name, self.scene) + + def loadConnections(self, simp, pBar=None): + """Load the connections that exist in a simplex system + + Parameters + ---------- + simp : Simplex + The simplex system to load connections for + pBar : QProgressDialog, optional + An optional progress dialog (Default value = None) + + """ + pass + + def getShapeThing(self, shapeName): + """Get the DCC Thing for a given shape name + + Parameters + ---------- + shapeName : str + The name of the shape to get + + Returns + ------- + : object : + The DCC Thing + + """ + return self.shapeNode.shapes.get(shapeName) + + def getSliderThing(self, sliderName): + """Get the DCC Thing for a given slider name + + Parameters + ---------- + sliderName : str + The name of the slider to get + + Returns + ------- + : object : + The DCC Thing + """ + return self.ctrl.attrs.get(sliderName) + + @staticmethod + @undoable + def buildRestAbc(abcMesh, name): + """Build the rest Alembic node in the dcc + + Parameters + ---------- + abcMesh : IPolyMesh + The Alembic mesh object + name : str + The name of the object to create + + """ + mesh = DummyMesh(name) # don't add it to a scene + + sa = getSampleArray(abcMesh) + if len(sa.shape) == 3: + sa = sa[0] + mesh.verts = sa + faces, counts = getStaticMeshData(abcMesh) + uvs = getUvSample(abcMesh) + + aryType = list if np is None else np.array + if uvs is not None: + mesh.uvs = aryType(uvs.getVals()) + mesh.uvFaces = aryType(uvs.getIndices()) + mesh.faces = aryType(faces) + mesh.counts = aryType(counts) + + return mesh + + @staticmethod + @undoable + def buildRawTopology(name, points, faces, counts, uvs=None, uvFaces=None): + """Build a mesh directly from raw numerical data""" + # TODO: Move this guy out to the rest of the DCC's + mesh = DummyMesh(name) # don't add it to a scene + + aryType = list if np is None else np.array + mesh.points = aryType(points) + mesh.faces = aryType(faces) + mesh.counts = aryType(counts) + if uvs is not None and uvFaces is not None: + mesh.uvs = aryType(uvs) + mesh.uvFaces = aryType(uvFaces) + return mesh + + @staticmethod + def vertCount(mesh): + """Get the vert count of the given DCC Object + + Parameters + ---------- + mesh : object + The mesh to check + + Returns + ------- + : int : + The Number of verts + """ + return len(mesh.verts) + + @undoable + def loadAbcPoses(self, abcMesh, js, pBar=None): + """Load the joints/skin from an alembic file onto an already-created system + + Parameters + ---------- + abcMesh : IPolyMesh + The Alembic mesh to load shapes from + js : dict + The simplex definition dictionary + pBar : QProgressDialog, optional + An optional progress dialog (Default value = None) + + """ + pass + + @undoable + def loadAbc(self, abcMesh, js, pBar=None): + """Load the shapes from an alembic file onto an already-created system + + Parameters + ---------- + abcMesh : IPolyMesh + The Alembic mesh to load shapes from + js : dict + The simplex definition dictionary + pBar : QProgressDialog, optional + An optional progress dialog (Default value = None) + + """ + shapes = js["shapes"] + if js["encodingVersion"] > 1: + shapes = [i["name"] for i in shapes] + pointPositions = getSampleArray(abcMesh) + for name, ppos in zip(shapes, pointPositions): + dummyShape = self.shapeNode.shapes[name] + dummyShape.points = ppos + + def getAllShapeVertices(self, shapes, pBar=None): + """Load all shape vertices into the simplex system for processing + + Parameters + ---------- + shapes : [Shape, ...] + A list of simplex Shape objects to get positions for + + pBar : QProgressDialog, optional + An optional progress dialog (Default value = None) + + """ + for shape in shapes: + verts = self.getShapeVertices(shape) + shape.verts = verts + + def getShapeVertices(self, shape): + """Get the point positions of a shape + + Parameters + ---------- + shape : Shape + A simplex Shape object to get the vertices for + + Returns + ------- + : np.array : + A numpy array of the point positions + + """ + return shape.thing.points + + def pushAllShapeVertices(self, shapes, pBar=None): + """Push the computed vertex positions for the given shapes back to the DCC + + Parameters + ---------- + shapes : [Shape, ...] + A list of simplex Shape objects + + pBar : QProgressDialog, optional + An optional progress dialog (Default value = None) + + """ + for shape in shapes: + self.pushShapeVertices(shape) + + def pushShapeVertices(self, shape): + """Push the computed vertex positions for the given shape back to the DCC + Parameters + ---------- + shape : Shape + The Simplex Shape object to update + + """ + shape.thing.points = shape.verts + + def loadMeshTopology(self): + """Load the mesh topology from the DCC into the simplex interface""" + # Here in Dummy I either have the data already or I don't, So nothing to do + pass + + @staticmethod + def getMeshTopology(mesh, uvName=None): + """Get the topology of a mesh + + Parameters + ---------- + mesh : object + The DCC Mesh to read + uvName : str, optional + The name of the uv set to read + + Returns + ------- + : np.array : + The vertex array + : np.array : + The "faces" array + : np.array : + The "counts" array + : np.array : + The uv positions + : np.array : + The "uvFaces" array + """ + return mesh.verts, mesh.faces, mesh.counts, mesh.uvs, mesh.uvFaces + + def exportAbc( + self, dccMesh, abcMesh, js, world=False, ensureCorrect=False, pBar=None + ): + """Export a .smpx file + + Parameters + ---------- + dccMesh : object + The DCC Mesh to export + abcMesh : OPolyMesh + The Alembic output mesh + js : dict + The definition dictionary + world : bool + Do the export in worldspace (Default value = False) + pBar : QProgressDialog, optional + An optional progress dialog (Default value = None) + + """ + # export the data to alembic + if dccMesh is None: + dccMesh = self.mesh + + shapeDict = {i.name: i for i in self.simplex.shapes} + + shapeNames = js["shapes"] + if js["encodingVersion"] > 1: + shapeNames = [i["name"] for i in shapeNames] + shapes = [shapeDict[i] for i in shapeNames] + schema = abcMesh.getSchema() + + if pBar is not None: + pBar.show() + pBar.setMaximum(len(shapes)) + spacerName = "_" * max(list(map(len, shapeNames))) + pBar.setLabelText("Exporting:\n{0}".format(spacerName)) + QApplication.processEvents() + + faces = mkSampleIntArray(self.mesh.faces) + counts = mkSampleIntArray(self.mesh.counts) + uvs = None + if self.mesh.uvs is not None and self.mesh.uvFaces is not None: + uvs = mkUvSample(self.mesh.uvs, self.mesh.uvFaces) + + for i, shape in enumerate(shapes): + if pBar is not None: + pBar.setLabelText("Exporting:\n{0}".format(shape.name)) + pBar.setValue(i) + QApplication.processEvents() + if pBar.wasCanceled(): + return + verts = mkSampleVertexPoints(shape.thing.points) + if uvs is not None: + # Alembic doesn't allow for uvs=None for some reason + abcSample = OPolyMeshSchemaSample(verts, faces, counts, uvs) + else: + abcSample = OPolyMeshSchemaSample(verts, faces, counts) + schema.set(abcSample) + + def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): + """Export a .smpx file of a mesh other than self.mesh + + Parameters + ---------- + dccMesh : object + The DCC Mesh to export + abcMesh : OPolyMesh + The Alembic output mesh + js : dict + The definition dictionary + world : bool + Do the export in worldspace (Default value = False) + pBar : QProgressDialog, optional + An optional progress dialog (Default value = None) + + """ + if dccMesh is None: + raise ValueError( + "Export Other requires an explicitly defined mesh to export" + ) + self.exportAbc( + dccMesh, abcMesh, js, world=world, ensureCorrect=False, pBar=pBar + ) + + def deleteObj(self, dccMesh, path): + """Export a mesh to the given path""" + pass + + def exportMesh(self, dccMesh, path): + """Export a mesh to the given path""" + pass + + # Revision tracking + def getRevision(self): + """Get the simplex revision number""" + return self._revision + + def incrementRevision(self): + """Increment the revision number""" + self._revision += 1 + return self._revision + + def setRevision(self, val): + """Manually set the revision numer + + Parameters + ---------- + val : int + The value to set + + """ + self._revision = val + + # System level + @undoable + def renameSystem(self, name): + """Rename a simplex system + + Parameters + ---------- + name : str + The new name + + """ + # TODO + # oldName = self.name + self.name = name + # for dd in (DB.nodes, DB.ops, DB.bss, DB.meshes): + # oo = dd.get(oldName) + # if oo is not None: + # oo.name = self.name + # dd[self.name] = oo + # dd.pop(oldName, None) + + @undoable + def deleteSystem(self): + """Delete the current system""" + # for dd in (DB.nodes, DB.ops, DB.bss, DB.meshes): + # if self.name in dd: + # dd.pop(self.name, None) + # TODO + self.name = None + self.simplex = None + + # Shapes + @undoable + def createShape(self, shape, live=False, offset=10): + """Create a dcc shape + + Parameters + ---------- + shape : Shape + A simplex shape object + live : bool + Whether this shape is live-connected (Default value = False) + offset : float + The offset of the created shape (Default value = 10) + + """ + newShape = DummyShape(shape.name, self.shapeNode) + newShape.points = copy.copy(self.mesh.verts) + return newShape + + @undoable + def extractWithDeltaShape(self, shape, live=True, offset=10.0): + """Make a mesh representing a shape. Can be live or not. + Also, make a shapenode that is the delta of the change being made + + Parameters + ---------- + shape : + + live : + (Default value = True) + offset : + (Default value = 10.0) + + """ + pass + + @undoable + def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0): + """Extract a shape with a live partial delta added in. + Useful for updating progressive shapes + + Parameters + ---------- + shape : + + delta : + + value : + + live : + (Default value = True) + offset : + (Default value = 10.0) + + """ + pass + + @undoable + def extractShape(self, shape, live=True, offset=10.0): + """Make a mesh representing a shape. Can be live or not. + Can also store its starting shape and delta data + + Parameters + ---------- + shape : + + live : + (Default value = True) + offset : + (Default value = 10.0) + + """ + pass + + @undoable + def connectShape(self, shape, mesh=None, live=False, delete=False): + """Force a shape to match a mesh + The "connect shape" button is: + mesh=None, delete=True + The "match shape" button is: + mesh=someMesh, delete=False + There is a possibility of a "make live" button: + live=True, delete=False + + Parameters + ---------- + shape : + + mesh : + (Default value = None) + live : + (Default value = False) + delete : + (Default value = False) + + """ + pass + + @undoable + def extractPosedShape(self, shape): + """??? + + Parameters + ---------- + shape : + + """ + pass + + @undoable + def zeroShape(self, shape): + """Set a shape back to rest + + Parameters + ---------- + shape : Shape + The simplex shpae to zero out + + """ + shape.thing.points = self.getShapeVertices(self.simplex.restShape) + + @undoable + def deleteShape(self, toDelShape): + """Delete a shape from the system + + Parameters + ---------- + toDelShape : + + """ + self.shapeNode.shapes.pop(toDelShape.name, None) + + @undoable + def renameShape(self, shape, name): + """Rename a shape + + Parameters + ---------- + shape : Shape + The simplex Shape object to rename + name : str + The new name + + """ + self.shapeNode.shapes.pop(shape.thing.name, None) + shape.thing.name = name + self.shapeNode.shapes[name] = shape.thing + + @undoable + def convertShapeToCorrective(self, shape): + """??? + + Parameters + ---------- + shape : + + """ + pass + + # Falloffs + def createFalloff(self, falloff): + """Create a per-vert falloff weightmap + + Parameters + ---------- + falloff : Falloff + The simplex Falloff object to create + + """ + fo = DummyFalloff(falloff.name, self.scene) + fo.weights = np.zeros(len(self.mesh.verts)) + + def duplicateFalloff(self, falloff, newFalloff): + """Create a new falloff from an already existing one + + Parameters + ---------- + falloff : Falloff + The already existing falloff + newFalloff : Falloff + The newly created falloff to store the newly duplicated data + + """ + fo = DummyFalloff(newFalloff.name, self.scene) + fo.weights = copy.copy(falloff.thing.weights) + + def deleteFalloff(self, falloff): + """Delete a falloff object + + Parameters + ---------- + falloff : Falloff + The Falloff object to delete + + """ + self.scene.remove(falloff.thing) + + def setFalloffData( + self, falloff, splitType, axis, minVal, minHandle, maxHandle, maxVal, mapName + ): + """Set the data of a falloff object + + Parameters + ---------- + falloff : + + splitType : + + axis : + + minVal : + + minHandle : + + maxHandle : + + maxVal : + + mapName : + + """ + # TODO: set the per-vert falloffs + pass # for eventual live splits + + def getFalloffThing(self, falloff): + """Get the thing for a given falloff + + Parameters + ---------- + falloff : Falloff + The simplex falloff object to get + + """ + return self.scene.get(DummyFalloff, falloff.name) + + # Sliders + @undoable + def createSlider(self, slider): + """Create a slider object + + Parameters + ---------- + slider : Slider + The simplex slider object to create + + """ + return DummyAttr(slider.name, 0.0, self.ctrl) + + @undoable + def renameSlider(self, slider, name): + """Rename a slider + + Parameters + ---------- + slider : Slider + The slider to rename + name : str + The new name + + """ + self.ctrl.attrs.pop(slider.thing.name, None) + self.ctrl.attrs[name] = slider.thing + slider.thing.name = name + + @undoable + def setSliderRange(self, slider): + """Set the min and max of a slider + + Parameters + ---------- + slider : Slider + The slider to set + + """ + pass + + @undoable + def deleteSlider(self, toDelSlider): + """Delete a slider + + Parameters + ---------- + toDelSlider : Slider + The slider to delete + + """ + self.ctrl.attrs.pop(toDelSlider.name, None) + + @undoable + def addProgFalloff(self, prog, falloff): + """ + + Parameters + ---------- + prog : + + falloff : + + """ + pass # for eventual live splits + + @undoable + def removeProgFalloff(self, prog, falloff): + """ + + Parameters + ---------- + prog : + + falloff : + + """ + pass # for eventual live splits + + @undoable + def setSlidersWeights(self, sliders, weights): + """Set the values for the given sliders + + Parameters + ---------- + sliders : [Slider, ...] + The sliders to set values for + weights : [float, ...] + The values to set + + """ + for slider, val in zip(sliders, weights): + slider.thing.value = val + + @undoable + def setSliderWeight(self, slider, weight): + """Set the value for a given slider + + Parameters + ---------- + slider : Slider + The slider + weight : float + The value + + """ + slider.thing.value = weight + + @undoable + def updateSlidersRange(self, sliders): + """Update the range of the given sliders + + Parameters + ---------- + sliders : + + """ + pass + + @undoable + def extractTraversalShape(self, trav, shape, live=True, offset=10.0): + """Extract a shape from a traversal progression + + Parameters + ---------- + trav : + + shape : + + live : + (Default value = True) + offset : + (Default value = 10.0) + + """ + pass + + @undoable + def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False): + """Connect a shape to a traversal progression + + Parameters + ---------- + trav : + + shape : + + mesh : + (Default value = None) + live : + (Default value = True) + delete : + (Default value = False) + + """ + pass + + # Combos + @undoable + def extractComboShape(self, combo, shape, live=True, offset=10.0): + """Extract a shape from a combo progression + + Parameters + ---------- + combo : + + shape : + + live : + (Default value = True) + offset : + (Default value = 10.0) + + """ + pass + + @undoable + def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): + """Connect a shape to a combo progression + + Parameters + ---------- + combo : + + shape : + + mesh : + (Default value = None) + live : + (Default value = True) + delete : + (Default value = False) + + """ + pass + + @staticmethod + def setDisabled(op): + """Disable the output of any simplex systems + + Parameters + ---------- + op : The operator to disable + + + Returns + ------- + : object : + Some object that will allow us to re-enable the system + + """ + return None + + @staticmethod + def reEnable(helpers): + """Re-enable a simplex system + + Parameters + ---------- + helpers : object + The helper object returned from setDisabled + + """ + pass + + @undoable + def renameCombo(self, combo, name): + """Set the name of a Combo + + Parameters + ---------- + combo : Combo + The combo to rename + name : str + The new name + + """ + pass + + # Data Access + @staticmethod + def getSimplexOperators(): + """Get all simplex operators in the DCC""" + return list(DB.ops.values()) + + @staticmethod + def getSimplexOperatorsByName(name): + """Get a simplex operator + + Parameters + ---------- + name : str + The name to search for + + + Returns + ------- + : object : + The simplex operator for the DCC + + """ + return DB.ops.get(name) + + @staticmethod + def getSimplexOperatorsOnObject(thing): + """Get all simplex operators controlling an object + + Parameters + ---------- + thing : object + A dcc object to check for simplex operators + + Returns + ------- + : [object, ...] : + A list of simplex operators + + """ + return [o for o in thing.ops if isinstance(o, DummySimplex)] + + @staticmethod + def getSimplexString(op): + """Get the simplex string from the given operator + + Parameters + ---------- + op : object + The Simplex operator to get the definition from + + Returns + ------- + : str : + The simplex definition + + """ + return op.definition + + @staticmethod + def getSimplexStringOnThing(thing, systemName): + """Get the definition on an object by name + + Parameters + ---------- + thing : object + The DCC object to check for a simplex operator + systemName : str + The system name to check for + + Returns + ------- + : str : + The simplex definition + + """ + for op in thing.ops: + if op.name == systemName: + return op.definition + return None + + @staticmethod + def setSimplexString(op, val): + """Set the definition string on an object + + Parameters + ---------- + op : object + The operator to set the definition on + val : str + The definition to set + + Returns + ------- + + """ + op.definition = val + + @staticmethod + def selectObject(thing): + """Select an object in the DCC + + Parameters + ---------- + thing : + + """ + pass + + def selectCtrl(self): + """Select the system's control object""" + pass + + @staticmethod + def getObjectByName(name): + """Get an object by name + + Parameters + ---------- + name : str + The name to search for + + Returns + ------- + : object : + The found object + + """ + # TODO: maybe also filter by type?? + # return DB.meshes.get(name) + return DB.get(DummyMesh, name) + + @staticmethod + def getObjectName(thing): + """Get the name of an object + + Parameters + ---------- + thing : object + The dcc object to get the name for + + + Returns + ------- + : str : + The Object Name + + """ + return thing.name + + @staticmethod + def staticUndoOpen(): + """Open an undo chunk without knowledge of Simplex""" + pass + + @staticmethod + def staticUndoClose(): + """Close an undo chunk without knowledge of Simplex""" + pass + + def undoOpen(self): + """Open an undo chunk with knowledge of Simplex""" + pass + + def undoClose(self): + """Close an undo chunk with knowledge of Simplex""" + pass + + @classmethod + def getPersistentFalloff(cls, thing): + """Get a representation of the given object that won't get deleted or garbage collected + + Parameters + ---------- + thing : object + The thing to get a persistent representation of + + Returns + ------- + : object : + The requested persistent object + + """ + return cls.getObjectName(thing) + + @classmethod + def loadPersistentFalloff(cls, thing): + """Get the usable representation of the given persistent thing + + Parameters + ---------- + thing : object + A persistent representation + + Returns + ------- + : object : + The requested volatile object + + """ + return cls.getObjectByName(thing) + + @classmethod + def getPersistentShape(cls, thing): + """Get a representation of the given object that won't get deleted or garbage collected + + Parameters + ---------- + thing : object + The thing to get a persistent representation of + + Returns + ------- + : object : + The requested persistent object + + """ + return cls.getObjectName(thing) + + @classmethod + def loadPersistentShape(cls, thing): + """Get the usable representation of the given persistent thing + + Parameters + ---------- + thing : object + A persistent representation + + Returns + ------- + : object : + The requested volatile object + + """ + return cls.getObjectByName(thing) + + @classmethod + def getPersistentSlider(cls, thing): + """Get a representation of the given object that won't get deleted or garbage collected + + Parameters + ---------- + thing : object + The thing to get a persistent representation of + + Returns + ------- + : object : + The requested persistent object + + """ + return cls.getObjectName(thing) + + @classmethod + def loadPersistentSlider(cls, thing): + """Get the usable representation of the given persistent thing + + Parameters + ---------- + thing : object + A persistent representation + + Returns + ------- + : object : + The requested volatile object + + """ + return cls.getObjectByName(thing) + + @staticmethod + def getSelectedObjects(): + """Get the selected objects""" + # Here in the dummy interface, we short-circuit this + # And return a default selected object called "thing" + return [DummyNode("thing")] + + def getFreezeThing(self, combo): + return [] + + +class SliderDispatch(QtCore.QObject): + """ """ + + valueChanged = Signal() + + def __init__(self, node, parent=None): + super(SliderDispatch, self).__init__(parent) + + def emitValueChanged(self, *args, **kwargs): + """ + + Parameters + ---------- + *args : + + **kwargs : + + """ + self.valueChanged.emit() + + +class Dispatch(QtCore.QObject): + """ """ + + beforeNew = Signal() + afterNew = Signal() + beforeOpen = Signal() + afterOpen = Signal() + undo = Signal() + redo = Signal() + + def __init__(self, parent=None): + super(Dispatch, self).__init__(parent) + + def connectCallbacks(self): + """ """ + pass + + def disconnectCallbacks(self): + """ """ + pass + + def emitBeforeNew(self, *args, **kwargs): + """ + + Parameters + ---------- + *args : + + **kwargs : + + """ + self.beforeNew.emit() + + def emitAfterNew(self, *args, **kwargs): + """ + + Parameters + ---------- + *args : + + **kwargs : + + """ + self.afterNew.emit() + + def emitBeforeOpen(self, *args, **kwargs): + """ + + Parameters + ---------- + *args : + + **kwargs : + + """ + self.beforeOpen.emit() + + def emitAfterOpen(self, *args, **kwargs): + """ + + Parameters + ---------- + *args : + + **kwargs : + + """ + self.afterOpen.emit() + + def emitUndo(self, *args, **kwargs): + """ + + Parameters + ---------- + *args : + + **kwargs : + + """ + self.undo.emit() + + def emitRedo(self, *args, **kwargs): + """ + + Parameters + ---------- + *args : + + **kwargs : + + """ + self.redo.emit() + + +DISPATCH = Dispatch() + + +def rootWindow(): + """ """ + return None diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 54a01916..49ad21c1 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -1,3271 +1,3271 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint: disable=invalid-name -from __future__ import annotations -import json -import re -from contextlib import contextmanager -from functools import wraps -from typing import TYPE_CHECKING - -import maya.cmds as cmds -import maya.OpenMaya as om -from alembic.AbcGeom import GeometryScope, OPolyMeshSchemaSample, OV2fGeomParamSample -from imath import IntArray, UnsignedIntArray, V2fArray, V3fArray - -from ..commands.alembicCommon import mkSampleVertexPoints, buildAbc -from Qt import QtCore -from Qt.QtCore import Signal -from Qt.QtWidgets import ( - QApplication, - QDialog, - QMainWindow, - QMessageBox, - QSplashScreen, -) - -if TYPE_CHECKING: - from ..items.simplex import Simplex - -try: - import numpy as np - from ..commands.numpytoimath import numpyToImath - from ..commands.mayatonumpy import mayaToNumpy -except ImportError: - np = None - numpyToImath = None - mayaToNumpy = None - - -# UNDO STACK INTEGRATION -@contextmanager -def undoContext(inst=None): - """ - - Parameters - ---------- - inst : - (Default value = None) - - Returns - ------- - - """ - if inst is None: - DCC.staticUndoOpen() - else: - inst.undoOpen() - try: - yield - finally: - if inst is None: - DCC.staticUndoClose() - else: - inst.undoClose() - - -def undoable(f): - """ - - Parameters - ---------- - f : - - - Returns - ------- - - """ - - @wraps(f) - def stacker(*args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - - Returns - ------- - - """ - inst = None - if args and isinstance(args[0], DCC): - inst = args[0] - with undoContext(inst): - return f(*args, **kwargs) - - return stacker - - -def doDisconnect(targets, testCnxType=("double", "float")): - """Temporarily disconnect inputs from a list of nodes and plugs - - Parameters - ---------- - targets : - - testCnxType : - (Default value = ("double") - "float") : - - - Returns - ------- - - """ - if not isinstance(targets, (list, tuple)): - targets = [targets] - targets = list(set(targets)) - cnxs = {} - for target in targets: - tcnx = {} - cnxs[target] = tcnx - - cnx = cmds.listConnections( - target, plugs=True, destination=False, source=True, connections=True - ) - if cnx is None: - cnx = [] - - for i in range(0, len(cnx), 2): - cnxType = cmds.getAttr(cnx[i], type=True) - if cnxType not in testCnxType: - continue - tcnx[cnx[i + 1]] = cnx[i] - cmds.disconnectAttr(cnx[i + 1], cnx[i]) - return cnxs - - -def doReconnect(cnxs): - """ - - Parameters - ---------- - cnxs : - - - Returns - ------- - - """ - for tdict in cnxs.values(): - for s, d in tdict.items(): - if not cmds.isConnected(s, d): - cmds.connectAttr(s, d, force=True) - - -@contextmanager -def disconnected(targets, testCnxType=("double", "float")): - """ - - Parameters - ---------- - targets : - - testCnxType : - (Default value = ("double") - "float") : - - - Returns - ------- - - """ - cnxs = doDisconnect(targets, testCnxType=testCnxType) - try: - yield cnxs - finally: - doReconnect(cnxs) - - -def split_trailing_digits(s: str) -> tuple[str, str]: - """Split trailing digits from a string into (prefix, digits).""" - match = re.match(r"^(.*?)(\d+)?$", s) - if match: - return match.group(1), match.group(2) or "" - return s, "" - - -class DCC(object): - """ """ - - program = "maya" - - def __init__(self, simplex, stack=None): - if not cmds.pluginInfo("simplex_maya", query=True, loaded=True): - cmds.loadPlugin("simplex_maya") - self.undoDepth: int = 0 - self.name: str = "" # the name of the system - self.mesh: str = "" # the mesh object with the system - self.ctrl: str = "" # the object that has all the controllers on it - self.shapeNode: str = "" # the deformer object - self.hasPoseNode = False - self.poseNode: str = "" # the pose deformer object - self.op: str = "" # the simplex object - self.simplex: Simplex = simplex # the abstract representation of the setup - self._live: bool = True - self.sliderMul: float = self.simplex.sliderMul - - # def __deepcopy__(self, memo): - # ''' - # I don't actually need to define this here because I know that - # all of the maya "objects" store here are just strings - # But if they *weren't* (like in XSI) I would need to skip - # the maya objects when deepcopying, otherwise I might access - # a deleted scene node and crash everything - # And if we did skip things, I would also need to store a - # persistent accessor to use in case we get back to here - # through an undo - # ''' - # pass - - def _checkAllShapeValidity(self, shapeNames): - """Check shapes to see if they exist, and either gather the missing files, or - Load the proper data onto the shapes - - Parameters - ---------- - shapeNames : - - - Returns - ------- - - """ - # Keep the set ordered, but make a set for quick checking - missingNameSet = set() - missingNames = [] - seen = set() - - # Get the blendshape weight names - try: - # GOOD GOD. This is because in maya 2016.5, if you delete a multi-instance - # then listAttr, *it lists the deleted ones, and skips the ones at the end* - # So I have to use aliasAttr and filter for the weights - aliases = cmds.aliasAttr(self.shapeNode, query=True) or [] - attrs = [ - aliases[i] - for i in range(0, len(aliases), 2) - if aliases[i + 1].startswith("weight[") - ] - attrs = set(attrs) - - except ValueError: - attrs = set() - - for shapeName in shapeNames: - if shapeName in seen: - continue - seen.add(shapeName) - if shapeName not in attrs: - if shapeName not in missingNameSet: - missingNameSet.add(shapeName) - missingNames.append(shapeName) - return missingNames, len(attrs) - - @classmethod - def _removeExtraShapeNodes(cls, tfm): - shapeNodes = cmds.listRelatives(tfm, shapes=True, noIntermediate=True) - if len(shapeNodes) > 1: - keeper = None - todel = [] - for sn in shapeNodes: - tfmChk = "".join(sn.rsplit("Shape", 1)) - if tfmChk == tfm: - keeper = sn - else: - todel.append(sn) - if keeper is not None: - cmds.delete(todel) - - def preLoad(self, simp, simpDict, create=True, pBar=None): - """ - - Parameters - ---------- - simp : - - simpDict : - - create : - (Default value = True) - pBar : - (Default value = None) - - Returns - ------- - - """ - cmds.undoInfo(state=False) - try: - if pBar is not None: - pBar.setLabelText("Loading Connections") - QApplication.processEvents() - ev = simpDict["encodingVersion"] - - shapeNames = simpDict.get("shapes") - if not shapeNames: - return - - if ev > 1: - shapeNames = [i["name"] for i in shapeNames] - - toMake, nextIndex = self._checkAllShapeValidity(shapeNames) - - if not toMake: - return - - if not create: - if pBar is not None: - msg = "Some shapes are Missing:\n{}\n\nCreate them?" - msg = msg.format(", ".join(toMake)) - btns = ( - QMessageBox.StandardButton.Yes - | QMessageBox.StandardButton.Cancel - ) - bret = QMessageBox.question(pBar, "Missing Shapes", msg, btns) - if not bret & QMessageBox.StandardButton.Yes: - raise RuntimeError("Missing Shapes: {}".format(toMake)) - else: - raise RuntimeError("Missing Shapes: {}".format(toMake)) - - if pBar is not None: - spacer = "_" * max(list(map(len, toMake))) - pBar.setMaximum(len(toMake)) - pBar.setLabelText("Creating Empty Shape:\n{0}".format(spacer)) - pBar.setValue(0) - QApplication.processEvents() - - baseShape = cmds.duplicate(self.mesh)[0] - cmds.delete(baseShape, constructionHistory=True) - self._removeExtraShapeNodes(baseShape) - - for i, shapeName in enumerate(toMake): - if pBar is not None: - pBar.setLabelText("Creating Empty Shape:\n{0}".format(shapeName)) - pBar.setValue(i) - QApplication.processEvents() - - baseShape = cmds.rename(baseShape, shapeName) - - index = self._firstAvailableIndex() - cmds.blendShape( - self.shapeNode, edit=True, target=(self.mesh, index, baseShape, 1.0) - ) - weightAttr = "{0}.weight[{1}]".format(self.shapeNode, index) - thing = cmds.ls(weightAttr)[0] - - cmds.connectAttr("{0}.weights[{1}]".format(self.op, nextIndex), thing) - nextIndex += 1 - - cmds.delete(baseShape) - except Exception: - cmds.undoInfo(state=True) - raise - - def postLoad(self, simp, preRet): - """ - - Parameters - ---------- - simp : - - preRet : - - Returns - ------- - - """ - cmds.undoInfo(state=True) - - def checkForErrors(self, window): - """Check for any DCC specific errors - - Parameters - ---------- - window : QMainWindow - The simplex window - """ - shapeNodes = cmds.listRelatives(self.mesh, shapes=True, noIntermediate=True) - if len(shapeNodes) > 1: - msg = ( - "The current mesh has multiple shape nodes.", - "The UI will still mostly work, but extracting/connecting shapes" - "may fail in unexpected ways.", - ) - QMessageBox.warning(window, "Multiple Shape Nodes", "\n".join(msg)) - - def _getOpNodes(self, thing: str): - hist: list[str] = cmds.listHistory(thing) - rawShapeNodes = cmds.ls(hist, type="blendShape") - rawPoseNodes = cmds.ls(hist, type="blendPose") - - # Find any simplex ops connected to the history - # that have the given name - ops: list[str] = [] - rawOps: list[str] = [] - for sn in rawShapeNodes + rawPoseNodes: - op = ( - cmds.listConnections( - "{0}.{1}".format(sn, "message"), - source=False, - destination=True, - type="simplex_maya", - ) - or [] - ) - rawOps.extend(op) - rawOps = list(set(rawOps)) - - for op in rawOps: - js = cmds.getAttr(op + ".definition") or "" - sn = json.loads(js).get("systemName") - if sn == self.name: - ops.append(op) - - if len(ops) > 1: - raise RuntimeError( - "Found too many Simplex systems with the same name on the same object" - ) - return ops - - def _getShapeNodes(self, ops): - shapeNodes = [] - for op in ops: - try: - sn = cmds.listConnections( - "{0}.{1}".format(op, "shapeMsg"), - source=True, - destination=False, - type="blendShape", - ) - except ValueError: - continue - if sn: - shapeNodes.append(sn[0]) - return shapeNodes - - def _getPoseNodes(self, ops): - poseNodes = [] - for op in ops: - try: - sn = cmds.listConnections( - "{0}.{1}".format(op, "poseMsg"), - source=True, - destination=False, - type="blendPose", - ) - except ValueError: - continue - if sn: - poseNodes.append(sn[0]) - return poseNodes - - def _getCtrlNodes(self, ops): - ctrlCnx = [] - for op in ops: - ccnx = cmds.listConnections( - "{0}.{1}".format(op, "ctrlMsg"), - source=True, - destination=False, - ) - if ccnx: - ctrlCnx.append(ccnx[0]) - return ctrlCnx - - def _createShapeNode(self, name, mesh) -> str: - intermediates = [ - shp - for shp in cmds.listRelatives(mesh, shapes=True, path=True) - if cmds.getAttr(shp + ".intermediateObject") - ] - meshToFreeze = mesh if not intermediates else intermediates[0] - isIntermediate = cmds.getAttr(meshToFreeze + ".intermediateObject") - - # Unlock the normals on the rest head because blendshapes don't work with locked normals - # and you can't really do this after the blendshape has been created - cmds.polyNormalPerVertex(meshToFreeze, unFreezeNormal=True) - cmds.polySoftEdge(meshToFreeze, angle=180, constructionHistory=True) - cmds.setAttr(meshToFreeze + ".intermediateObject", 0) - cmds.delete(meshToFreeze, constructionHistory=True) - cmds.setAttr(meshToFreeze + ".intermediateObject", isIntermediate) - - name = "{}_BS".format(name) - return cmds.blendShape(mesh, name=name, frontOfChain=True)[0] - - def _createPoseNode(self, name) -> str: - name = "{}_BP".format(name) - return cmds.createNode("blendPose", name=name) - - def _createSimplexNode(self, name): - op = cmds.createNode("simplex_maya", name=name) - cmds.addAttr(op, longName="revision", attributeType="long") - cmds.addAttr(op, longName="shapeMsg", attributeType="message") - cmds.addAttr(op, longName="poseMsg", attributeType="message") - cmds.addAttr(op, longName="ctrlMsg", attributeType="message") - return op - - def _createControlNode(self, name, op): - tfmAttrs = [".tx", ".ty", ".tz", ".rx", ".ry", ".rz", ".sx", ".sy", ".sz", ".v"] - ctrl = cmds.group(empty=True, name="{0}_CTRL".format(name)) - for attr in tfmAttrs: - cmds.setAttr(ctrl + attr, keyable=False, channelBox=False) - cmds.addAttr(ctrl, longName="solver", attributeType="message") - cmds.connectAttr( - "{0}.{1}".format(ctrl, "solver"), - "{0}.{1}".format(op, "ctrlMsg"), - ) - return ctrl - - # System IO - @undoable - def loadNodes(self, simp, thing, create=True, pBar=None): - """Create a new system based on the simplex tree - Build any DCC objects that are missing if create=True - Raises a runtime error if missing objects are found and - create=False - - Parameters - ---------- - simp : - - thing : - - create : - (Default value = True) - pBar : - (Default value = None) - - Returns - ------- - - """ - self.name = simp.name - self.mesh = thing - - ops = self._getOpNodes(thing) - sns = self._getShapeNodes(ops) - cc = self._getCtrlNodes(ops) - - if not create and (not sns or not ops or not cc): - types = [] - if not sns: - types.append("blendShape") - if not ops: - types.append("simplex_maya") - if not cc: - types.append("CTRL") - - raise RuntimeError( - "Creation turned off and some objects are missing: {}".format( - ", ".join(types) - ) - ) - - self.op = ops[0] if ops else self._createSimplexNode(self.name) - self.ctrl = cc[0] if cc else self._createControlNode(self.name, self.op) - - self.shapeNode = sns[0] if sns else self._createShapeNode(self.name, self.mesh) - if not cmds.isConnected(f"{self.shapeNode}.message", f"{self.op}.shapeMsg"): - cmds.connectAttr(f"{self.shapeNode}.message", f"{self.op}.shapeMsg") - - if self.hasPoseNode: - pns = self._getPoseNodes(ops) - self.poseNode = pns[0] if pns else self._createPoseNode(self.name) - if not cmds.isConnected(f"{self.poseNode}.message", f"{self.op}.poseMsg"): - cmds.connectAttr(f"{self.poseNode}.message", f"{self.op}.poseMsg") - - def getShapeThing(self, shapeName): - """ - - Parameters - ---------- - shapeName : - - - Returns - ------- - - """ - s = cmds.ls("{0}.{1}".format(self.shapeNode, shapeName)) - if not s: - return None - return s[0] - - def getSliderThing(self, sliderName): - """ - - Parameters - ---------- - sliderName : - - - Returns - ------- - - """ - things = cmds.ls("{0}.{1}".format(self.ctrl, sliderName)) - if not things: - return None - return things[0] - - @classmethod - def buildDummyMesh(cls, name: str): - importHeadShape = cmds.createNode("mesh", name=name + "Shape") - badPar = cmds.listRelatives(importHeadShape, parent=True)[0] - importHead = cmds.rename(badPar, name) - return importHead, importHeadShape - - @classmethod - @undoable - def buildRestAbc(cls, abcMesh, name): - """ - - Parameters - ---------- - abcMesh : - - name : - - - Returns - ------- - - """ - if not cmds.pluginInfo("AbcImport", query=True, loaded=True): - cmds.loadPlugin("AbcImport") - if not cmds.pluginInfo("AbcImport", query=True, loaded=True): - raise RuntimeError("Unable to load the AbcImport plugin") - - abcPath = str(abcMesh.getArchive()) - - abcNode = cmds.createNode("AlembicNode") - cmds.setAttr(abcNode + ".abc_File", abcPath, type="string") - cmds.setAttr(abcNode + ".speed", 24) # Is this needed anymore? - cmds.setAttr(abcNode + ".time", 0) - - importHead, importHeadShape = cls.buildDummyMesh("{0}_SIMPLEX".format(name)) - - importHead = "{0}_SIMPLEX".format(name) - importHeadShape = cmds.createNode("mesh", name=importHead + "Shape") - badPar = cmds.listRelatives(importHeadShape, parent=True)[0] - importHead = cmds.rename(badPar, importHead) - - cmds.connectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") - cmds.polyEvaluate(importHead, vertex=True) # Force a refresh - cmds.disconnectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") - cmds.sets(importHead, edit=True, forceElement="initialShadingGroup") - cmds.delete(abcNode) - return importHead - - @classmethod - def vertCount(cls, mesh): - return cmds.polyEvaluate(mesh, vertex=True) - - @undoable - def loadAbcPoses(self, abcMesh, js, pBar=None): - pass - - @undoable - def loadAbc(self, abcMesh, js, pBar=None): - """ - - Parameters - ---------- - abcMesh : - - js : - - pBar : - (Default value = None) - - Returns - ------- - - """ - # UGH, I *REALLY* hate that this is faster - # But if I want to be "pure" about it, I should just bite the bullet - # and do the direct alembic manipulation in C++ - - if not cmds.pluginInfo("AbcImport", query=True, loaded=True): - cmds.loadPlugin("AbcImport") - if not cmds.pluginInfo("AbcImport", query=True, loaded=True): - raise RuntimeError("Unable to load the AbcImport plugin") - - abcPath = str(abcMesh.getArchive()) - - abcNode = cmds.createNode("AlembicNode") - cmds.setAttr(abcNode + ".abc_File", abcPath, type="string") - - timeUnits = { - "game": 15, - "film": 24, - "pal": 25, - "ntsc": 30, - "show": 48, - "palf": 50, - "ntscf": 60, - } - - fps = cmds.currentUnit(time=True, query=True) - if isinstance(fps, str): - if fps.endswith("fps"): - fps = fps[:-3] - if fps in timeUnits: - fps = timeUnits[fps] - fps = float(fps) - - cmds.setAttr(abcNode + ".speed", fps) - - shapes = js["shapes"] - shapeDict = {i.name: i for i in self.simplex.shapes} - - if js["encodingVersion"] > 1: - shapes = [i["name"] for i in shapes] - - importHead, importHeadShape = self.buildDummyMesh("importHead") - - cmds.connectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") - cmds.polyEvaluate(importHead, vertex=True) # Force a refresh - cmds.disconnectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") - - importRest = cmds.duplicate(self.mesh, name="importRest")[0] - cmds.delete(importRest, constructionHistory=True) - self._removeExtraShapeNodes(importRest) - - importBS: str = cmds.blendShape(importRest, importHead)[0] - cmds.blendShape(importBS, edit=True, weight=[(0, 1.0)]) - # Maybe get shapeNode from self.mesh?? - importOrig = [ - i for i in cmds.listRelatives(importHead, shapes=True) if i.endswith("Orig") - ][0] - cmds.connectAttr(abcNode + ".outPolyMesh[0]", importOrig + ".inMesh") - cmds.delete(importRest) - - if pBar is not None: - pBar.show() - pBar.setMaximum(len(shapes)) - longName = max(shapes, key=len) - pBar.setValue(1) - pBar.setLabelText("Loading:\n{0}".format("_" * len(longName))) - - for i, shapeName in enumerate(shapes): - if pBar is not None: - pBar.setValue(i) - pBar.setLabelText("Loading:\n{0}".format(shapeName)) - QApplication.processEvents() - if pBar.wasCanceled(): - return - index = self.getShapeIndex(shapeDict[shapeName]) - cmds.setAttr(abcNode + ".time", i) - - outAttr = "{0}.worldMesh[0]".format(importHead) - tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format( - self.shapeNode, index - ) - inAttr = "{0}.inputTargetItem[6000].inputGeomTarget".format(tgn) - - cmds.connectAttr(outAttr, inAttr, force=True) - cmds.disconnectAttr(outAttr, inAttr) - cmds.delete(abcNode) - cmds.delete(importHead) - - def getAllShapeVertices(self, shapes, pBar=None): - """ - - Parameters - ---------- - shapes : - - pBar : - (Default value = None) - - Returns - ------- - - """ - sl = om.MSelectionList() - sl.add(self.mesh) - thing = om.MDagPath() - sl.getDagPath(0, thing) - meshFn = om.MFnMesh(thing) - _ptCount = meshFn.numVertices() - with disconnected(self.shapeNode) as cnx: - shapeCnx = cnx[self.shapeNode] - for v in shapeCnx.values(): - cmds.setAttr(v, 0.0) - - if pBar is not None: - # find the longest name for displaying stuff - sns = "_" * max(list(map(len, [s.name for s in shapes]))) - pBar.setLabelText("Getting Shape:\n{0}".format(sns)) - pBar.setMaximum(len(shapes)) - QApplication.processEvents() - - for i, shape in enumerate(shapes): - if pBar is not None: - pBar.setLabelText("Getting Shape:\n{0}".format(shape.name)) - pBar.setValue(i) - QApplication.processEvents() - - cmds.setAttr(shape.thing, 1.0) - - if np is not None and mayaToNumpy is not None: - out = mayaToNumpy(meshFn.getRawPoints()) - else: - flatverts = cmds.xform( - "{0}.vtx[*]".format(self.mesh), - translation=1, - query=1, - worldSpace=False, - ) - args = [iter(flatverts)] * 3 - out = list(zip(*args)) - - cmds.setAttr(shape.thing, 0.0) - shape.verts = out - - def getShapeVertices(self, shape): - """ - - Parameters - ---------- - shape : - - Returns - ------- - - """ - with disconnected(self.shapeNode) as cnx: - shapeCnx = cnx[self.shapeNode] - for v in shapeCnx.values(): - cmds.setAttr(v, 0.0) - cmds.setAttr(shape.thing, 1.0) - if np is None: - flatverts = cmds.xform( - "{0}.vtx[*]".format(self.mesh), - translation=1, - query=1, - worldSpace=False, - ) - args = [iter(flatverts)] * 3 - out = list(zip(*args)) - else: - out = self.getNumpyShape(self.mesh) - return out - - def pushAllShapeVertices(self, shapes, pBar=None): - """ - - Parameters - ---------- - shapes : - - pBar : - (Default value = None) - - Returns - ------- - - """ - # take all the verts stored on the shapes - # and push them back to the DCC - for shape in shapes: - self.pushShapeVertices(shape) - - def pushShapeVertices(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ - # Push the vertices for a specific shape back to the DCC - pass - - @classmethod - def getMeshTopology(cls, mesh, uvName=None): - """Get the topology of a mesh - - Parameters - ---------- - mesh : object - The DCC Mesh to read - uvName : str, optional - The name of the uv set to read - - Returns - ------- - np.array : - The vertex array - np.array : - The "faces" array - np.array : - The "counts" array - np.array : - The uv positions - np.array : - The "uvFaces" array - """ - # Get the MDagPath from the name of the mesh - sl = om.MSelectionList() - sl.add(mesh) - thing = om.MDagPath() - sl.getDagPath(0, thing) - meshFn = om.MFnMesh(thing) - - vts = om.MPointArray() - meshFn.getPoints(vts, om.MSpace.kObject) - verts = [(vts[i].x, vts[i].y, vts[i].z) for i in range(vts.length())] - - faces = [] - counts = [] - rawUvFaces = [] - - vIdx = om.MIntArray() - - util = om.MScriptUtil() - util.createFromInt(0) - uvIdxPtr = util.asIntPtr() - uArray = om.MFloatArray() - vArray = om.MFloatArray() - meshFn.getUVs(uArray, vArray) - hasUvs = uArray.length() > 0 - - for i in range(meshFn.numPolygons()): - meshFn.getPolygonVertices(i, vIdx) - face = [] - for j in reversed(range(vIdx.length())): - face.append(vIdx[j]) - if hasUvs: - meshFn.getPolygonUVid(i, j, uvIdxPtr) - uvIdx = util.getInt(uvIdxPtr) - if uvIdx >= uArray.length() or uvIdx < 0: - uvIdx = 0 - rawUvFaces.append(uvIdx) - - face = [vIdx[j] for j in reversed(range(vIdx.length()))] - faces.extend(face) - counts.append(vIdx.length()) - - if hasUvs: - uvs = [(uArray[i], vArray[i]) for i in range(len(vArray))] - uvFaces = rawUvFaces - else: - uvs = None - uvFaces = None - - return verts, faces, counts, uvs, uvFaces - - def loadMeshTopology(self): - """ """ - self._faces, self._counts, self._uvs = self.getAbcFaces(self.mesh) - - @classmethod - def getNumpyShape(cls, mesh, world=False): - """Get the np.array shape of the mesh connected to the smpx - - Parameters - ---------- - mesh : str - The name of the maya shape object - world : bool - Whether to get the points in worldspace, or local space - - Returns - ------- - : np.array - The point positions of the mesh - - """ - if np is None or mayaToNumpy is None: - raise RuntimeError("Can't do numpy stuff if its not importable") - vts = cls._getMeshVertices(mesh, world=world) - ret = mayaToNumpy(vts) - return ret[..., :3].copy() - - @classmethod - def _getMeshVertices(cls, mesh, world=False): - """ """ - # Get the MDagPath from the name of the mesh - sl = om.MSelectionList() - sl.add(mesh) - thing = om.MDagPath() - sl.getDagPath(0, thing) - meshFn = om.MFnMesh(thing) - vts = om.MPointArray() - if world: - space = om.MSpace.kWorld - else: - space = om.MSpace.kObject - meshFn.getPoints(vts, space) - return vts - - @classmethod - def _exportAbcVertices(cls, mesh, world=False): - """ """ - if np is None or numpyToImath is None: - vts = cls._getMeshVertices(mesh, world=world) - vertices = V3fArray(vts.length()) - for i in range(vts.length()): - vertices[i] = (vts[i].x, vts[i].y, vts[i].z) - else: - vts = cls.getNumpyShape(mesh, world=world) - vertices = mkSampleVertexPoints(vts) - return vertices - - @classmethod - def getAbcFaces(cls, mesh): - """ """ - # Get the MDagPath from the name of the mesh - sl = om.MSelectionList() - sl.add(mesh) - thing = om.MDagPath() - sl.getDagPath(0, thing) - meshFn = om.MFnMesh(thing) - - faces = [] - faceCounts = [] - # uvArray = [] - uvIdxArray = [] - vIdx = om.MIntArray() - - util = om.MScriptUtil() - util.createFromInt(0) - uvIdxPtr = util.asIntPtr() - uArray = om.MFloatArray() - vArray = om.MFloatArray() - meshFn.getUVs(uArray, vArray) - hasUvs = uArray.length() > 0 - - for i in range(meshFn.numPolygons()): - meshFn.getPolygonVertices(i, vIdx) - face = [] - for j in reversed(range(vIdx.length())): - face.append(vIdx[j]) - if hasUvs: - meshFn.getPolygonUVid(i, j, uvIdxPtr) - uvIdx = util.getInt(uvIdxPtr) - if uvIdx >= uArray.length() or uvIdx < 0: - uvIdx = 0 - uvIdxArray.append(uvIdx) - - face = [vIdx[j] for j in reversed(range(vIdx.length()))] - faces.extend(face) - faceCounts.append(vIdx.length()) - - abcFaceIndices = IntArray(len(faces)) - for i in range(len(faces)): - abcFaceIndices[i] = faces[i] - - abcFaceCounts = IntArray(len(faceCounts)) - for i in range(len(faceCounts)): - abcFaceCounts[i] = faceCounts[i] - - if hasUvs: - abcUVArray = V2fArray(len(uArray)) - for i in range(len(vArray)): - abcUVArray[i] = (uArray[i], vArray[i]) - abcUVIdxArray = UnsignedIntArray(len(uvIdxArray)) - for i in range(len(uvIdxArray)): - abcUVIdxArray[i] = uvIdxArray[i] - uv = OV2fGeomParamSample( - abcUVArray, abcUVIdxArray, GeometryScope.kFacevaryingScope - ) - else: - uv = None - - return abcFaceIndices, abcFaceCounts, uv - - def exportAbc( - self, dccMesh, abcMesh, js, world=False, ensureCorrect=False, pBar=None - ): - """ """ - # export the data to alembic - if dccMesh is None: - dccMesh = self.mesh - - shapeDict = {i.name: i for i in self.simplex.shapes} - - shapeNames = js["shapes"] - if js["encodingVersion"] > 1: - shapeNames = [i["name"] for i in shapeNames] - shapes = [shapeDict[i] for i in shapeNames] - - faces, counts, uvs = self.getAbcFaces(dccMesh) - schema = abcMesh.getSchema() - - if pBar is not None: - pBar.show() - pBar.setMaximum(len(shapes)) - spacerName = "_" * max(list(map(len, shapeNames))) - pBar.setLabelText("Exporting:\n{0}".format(spacerName)) - QApplication.processEvents() - - if ensureCorrect: - # Since this code is used to both export and exportOther - # I only want to ensure that everything is correct only if - # I'm doing a normal export - envelope = cmds.getAttr(self.shapeNode + ".envelope") - cmds.setAttr(self.shapeNode + ".envelope", 1.0) - - with disconnected(self.shapeNode) as cnx: - shapeCnx = cnx[self.shapeNode] - for v in shapeCnx.values(): - cmds.setAttr(v, 0.0) - for i, shape in enumerate(shapes): - if pBar is not None: - pBar.setLabelText("Exporting:\n{0}".format(shape.name)) - pBar.setValue(i) - QApplication.processEvents() - if pBar.wasCanceled(): - return - cmds.setAttr(shape.thing, 1.0) - verts = self._exportAbcVertices(dccMesh, world=world) - if uvs is not None: - abcSample = OPolyMeshSchemaSample(verts, faces, counts, uvs) - else: - abcSample = OPolyMeshSchemaSample(verts, faces, counts) - schema.set(abcSample) - cmds.setAttr(shape.thing, 0.0) - - if ensureCorrect: - cmds.setAttr(self.shapeNode + ".envelope", envelope) - - def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): - """ """ - shapeNames = js["shapes"] - if js["encodingVersion"] > 1: - shapeNames = [i["name"] for i in shapeNames] - - if pBar is not None: - pBar.show() - pBar.setMaximum(len(shapeNames)) - spacerName = "_" * max(list(map(len, shapeNames))) - pBar.setLabelText("Exporting:\n{0}".format(spacerName)) - QApplication.processEvents() - - # Get all the sliderVecs - shapeNames, inVecs, keyIdxs = self.simplex.buildInputVectors() - sliderVecs = [ - [0.0] * len(self.simplex.sliders) for i in range(len(self.simplex.shapes)) - ] - for iv, idx in zip(inVecs, keyIdxs): - sliderVecs[idx] = iv - - # Get all the fully expanded shapes, and the activations per shape - with disconnected(self.op) as allSliderCnx: - sliderCnx = allSliderCnx[self.op] - # zero all slider vals on the op to get the rest shape - for a in sliderCnx.values(): - cmds.setAttr(a, 0.0) - restVerts = self.getNumpyShape(dccMesh, world=world) - - fullShapes = np.zeros((len(self.simplex.shapes), len(restVerts), 3)) - shpValArray = np.zeros((len(self.simplex.shapes), len(self.simplex.shapes))) - for shpIdx, shape in enumerate(self.simplex.shapes): - if pBar is not None: - pBar.setLabelText("Reading Full Shapes:\n{0}".format(shape.name)) - pBar.setValue(shpIdx) - QApplication.processEvents() - if pBar.wasCanceled(): - raise RuntimeError("Cancelled!") - - # Set the full vec for this shape - inVec = sliderVecs[shpIdx] - for vi, vv in enumerate(inVec): - cmds.setAttr(sliderCnx[self.simplex.sliders[vi].thing], vv) - - fullShapes[shpIdx] = self.getNumpyShape(dccMesh, world=world) - - ary = np.array(cmds.getAttr(self.op + ".weights")[0]) - # Get rid of some floating point inaccuracies - ary[np.isclose(ary, 1.0)] = 1.0 - ary[np.isclose(ary, 0.0)] = 0.0 - shpValArray[shpIdx] = ary - - # Figure out what order to build the deltas - # so that the deltas exist when I try to combine them - ctrlOrder = self.simplex.controllersByDepth() - shapeOrder = [pp.shape for ctrl in ctrlOrder for pp in ctrl.prog.pairs] - shapeOrder = [i for i in shapeOrder if not i.isRest] - - # Incrementally Build the numpy array of delta shapes - # build deltaShapeArray as a 2d array because numpy is like 10x faster on 2d arrays - indexByShape = {v: k for k, v in enumerate(self.simplex.shapes)} - deltaShapeArray = np.zeros((len(self.simplex.shapes), len(restVerts) * 3)) - for shpOrderIdx, shape in enumerate(shapeOrder): - if pBar is not None: - pBar.setLabelText("Collapsing to Deltas:\n{0}".format(shape.name)) - pBar.setValue(shpOrderIdx) - QApplication.processEvents() - if pBar.wasCanceled(): - raise RuntimeError("Cancelled!") - - shpIdx = indexByShape[shape] - base = np.dot(shpValArray[shpIdx], deltaShapeArray) - deltaShapeArray[shpIdx] = ( - fullShapes[shpIdx] - restVerts - base.reshape((-1, 3)) - ).flatten() - - # And move that 2d array back into 3d - deltaShapeArray = deltaShapeArray.reshape((len(self.simplex.shapes), -1, 3)) - - # Finally write the outputs - faces, counts, uvs = self.getAbcFaces(dccMesh) - schema = abcMesh.getSchema() - for shpIdx, shape in enumerate(self.simplex.shapes): - if pBar is not None: - pBar.setLabelText("writing:\n{0}".format(shape.name)) - pBar.setValue(shpIdx) - QApplication.processEvents() - if pBar.wasCanceled(): - raise RuntimeError("Cancelled!") - shpVerts = restVerts + deltaShapeArray[shpIdx] - shpVerts = mkSampleVertexPoints(shpVerts) - if uvs is not None: - abcSample = OPolyMeshSchemaSample(shpVerts, faces, counts, uvs) - else: - abcSample = OPolyMeshSchemaSample(shpVerts, faces, counts) - schema.set(abcSample) - - def deleteObj(self, thing): - """Delete the given object""" - cmds.delete(thing) - - def exportMesh(self, mesh, path): - """Export a mesh to the given path""" - faces, counts, uvs = self.getAbcFaces(mesh) - shape = self.getNumpyShape(mesh) - name = mesh.split('|')[-1] - buildAbc(path, shape, faces, counts, uvs, name=name) - - # Revision tracking - def getRevision(self): - """ """ - try: - return cmds.getAttr("{0}.{1}".format(self.op, "revision")) - except ValueError: - # object does not exist - return None - - @undoable - def incrementRevision(self): - """ """ - value = self.getRevision() - if value is None: - return - cmds.setAttr("{0}.{1}".format(self.op, "revision"), value + 1) - jsString = self.simplex.dump() - self.setSimplexString(self.op, jsString) - return value + 1 - - @undoable - def setRevision(self, val): - """ - - Parameters - ---------- - val : - - - Returns - ------- - - """ - cmds.setAttr("{0}.{1}".format(self.op, "revision"), val) - - # System level - @undoable - def renameSystem(self, name): - """ - - Parameters - ---------- - name : - - - Returns - ------- - - """ - if ( - self.mesh is None - or self.ctrl is None - or self.shapeNode is None - or self.op is None - or self.simplex is None - ): - raise ValueError("System is not set up. Cannot rename") - - nn = self.mesh.replace(self.name, name) - self.mesh = cmds.rename(self.mesh, nn) - - nn = self.ctrl.replace(self.name, name) - self.ctrl = cmds.rename(self.ctrl, nn) - - oldNodeName = self.shapeNode - nn = self.shapeNode.replace(self.name, name) - self.shapeNode = cmds.rename(self.shapeNode, nn) - - nn = self.op.replace(self.name, name) - self.op = cmds.rename(self.op, nn) - - for shape in self.simplex.shapes: - shape.thing = shape.thing.replace(oldNodeName, self.shapeNode) - - self.name = name - - @undoable - def deleteSystem(self): - """ """ - cmds.delete(self.ctrl) - cmds.delete(self.shapeNode) - cmds.delete(self.op) - self.ctrl = None # the object that has all the controllers on it - self.shapeNode = None # the deformer object - self.op = None # the simplex object - self.simplex = None - - # Shapes - @undoable - def createShape(self, shape, live=False, offset=10): - """ - - Parameters - ---------- - shape : - - live : - (Default value = False) - offset : - (Default value = 10) - - Returns - ------- - - """ - with disconnected(self.shapeNode): - try: - attrs = cmds.listAttr("{0}.weight[*]".format(self.shapeNode)) - except ValueError: - pass - # Maya throws an error if there aren't any instead of - # just returning an empty list - else: - for attr in attrs: - cmds.setAttr("{0}.{1}".format(self.shapeNode, attr), 0.0) - newShape = cmds.duplicate(self.mesh, name=shape.name)[0] - - cmds.delete(newShape, constructionHistory=True) - index = self._firstAvailableIndex() - cmds.blendShape( - self.shapeNode, edit=True, target=(self.mesh, index, newShape, 1.0) - ) - weightAttr = "{0}.weight[{1}]".format(self.shapeNode, index) - thing = cmds.ls(weightAttr)[0] - - shapeIndex = len(shape.simplex.shapes) - 1 - cmds.connectAttr("{0}.weights[{1}]".format(self.op, shapeIndex), thing) - - if live: - cmds.xform(newShape, relative=True, translation=[offset, 0, 0]) - else: - cmds.delete(newShape) - - return thing - - def _firstAvailableIndex(self): - """ """ - aliases = cmds.aliasAttr(self.shapeNode, query=True) - idxs = set() - if not aliases: - return 0 - for alias in aliases: - match = re.search(r"\[\d+\]", alias) - if not match: - continue # No index found for the current shape - idxs.add(int(match.group().strip("[]"))) - - for i in range(len(idxs) + 1): - if i not in idxs: - return i - # there should be no way to get here, but just in case: - return len(idxs) + 1 - - def getShapeIndex(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ - aName = cmds.attributeName(shape.thing) - aliases = cmds.aliasAttr(self.shapeNode, query=True) - idx = aliases.index(aName) - raw = aliases[idx + 1] - matches = re.findall(r"\[\d+\]", raw) - if not matches: - raise IndexError("No index found for the current shape") - return int(matches[-1].strip("[]")) - - @undoable - def extractWithDeltaShape(self, shape, live=True, offset=10.0): - """Make a mesh representing a shape. Can be live or not. - Also, make a shapenode that is the delta of the change being made - - Parameters - ---------- - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ - with disconnected(self.shapeNode) as cnx: - shapeCnx = cnx[self.shapeNode] - for v in shapeCnx.values(): - cmds.setAttr(v, 0.0) - - # store the delta shape - delta = cmds.duplicate(self.mesh, name="{0}_Delta".format(shape.name))[0] - - # Extract the shape - cmds.setAttr(shape.thing, 1.0) - extracted = cmds.duplicate( - self.mesh, name="{0}_Extract".format(shape.name) - )[0] - - # Store the initial shape - init = cmds.duplicate(extracted, name="{0}_Init".format(shape.name))[0] - - # clear old orig objects - for item in [delta, extracted, init]: - self._clearShapes(item, doOrig=True) - - # build the deltaObj system - bs = cmds.blendShape(delta, name="{0}_DeltaBS".format(shape.name))[0] - - cmds.blendShape(bs, edit=True, target=(delta, 0, init, 1.0)) - cmds.blendShape(bs, edit=True, target=(delta, 1, extracted, 1.0)) - - cmds.setAttr("{0}.{1}".format(bs, init), -1.0) - cmds.setAttr("{0}.{1}".format(bs, extracted), 1.0) - - # Cleanup - nodeDict = {"Delta": delta, "Init": init} - repDict = self._reparentDeltaShapes(extracted, nodeDict, bs) - - # Shift the extracted shape to the side - cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) - - if live: - self.connectShape(shape, extracted, live, delete=False) - - return extracted, repDict["Delta"] - - @undoable - def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0): - """Extract a shape with a live partial delta added in. - Useful for updating progressive shapes - - Parameters - ---------- - shape : - - delta : - - value : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ - with disconnected(self.shapeNode): - for attr in cmds.listAttr("{0}.weight[*]".format(self.shapeNode)): - cmds.setAttr("{0}.{1}".format(self.shapeNode, attr), 0.0) - - # Pull out the rest shape. we will blend this guy to the extraction - extracted = cmds.duplicate( - self.mesh, name="{0}_Extract".format(shape.name) - )[0] - - cmds.setAttr(shape.thing, 1.0) - # Store the initial shape - init = cmds.duplicate(self.mesh, name="{0}_Init".format(shape.name))[0] - - # clear old orig objects - for item in [init, extracted]: - self._clearShapes(item, doOrig=True) - - deltaPar = cmds.listRelatives(delta, parent=True)[0] - - # build the restObj system - cmds.select(clear=True) # 'cause maya - bs = cmds.blendShape(extracted, name="{0}_DeltaBS".format(shape.name))[0] - cmds.blendShape(bs, edit=True, target=(extracted, 0, init, 1.0)) - cmds.blendShape(bs, edit=True, target=(extracted, 1, deltaPar, 1.0)) - - cmds.setAttr("{0}.{1}".format(bs, init), 1.0) - cmds.setAttr("{0}.{1}".format(bs, deltaPar), value) - - outCnx = "{0}.worldMesh[0]".format(delta) - inCnx = "{0}.inputTarget[0].inputTargetGroup[{1}].inputTargetItem[6000].inputGeomTarget".format( - bs, 1 - ) - cmds.connectAttr(outCnx, inCnx, force=True) - cmds.aliasAttr(delta, "{0}.{1}".format(bs, deltaPar)) - - # Cleanup - nodeDict = {"Init": init} - self._reparentDeltaShapes(extracted, nodeDict, bs) - - # Remove the tweak node, otherwise editing the input progressives - # *inverts* the shape - exShape = cmds.listRelatives(extracted, noIntermediate=1, shapes=1)[0] - tweak = cmds.listConnections( - exShape + ".tweakLocation", source=1, destination=0 - ) - if tweak: - cmds.delete(tweak) - - # Shift the extracted shape to the side - cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) - self.connectShape(shape, extracted, live, delete=False) - - return extracted - - @undoable - def extractShape(self, shape, live=True, offset=10.0): - """Make a mesh representing a shape. Can be live or not. - Can also store its starting shape and delta data - - Parameters - ---------- - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ - with disconnected(self.shapeNode): - for attr in cmds.listAttr("{0}.weight[*]".format(self.shapeNode)): - cmds.setAttr("{0}.{1}".format(self.shapeNode, attr), 0.0) - - cmds.setAttr(shape.thing, 1.0) - extracted = cmds.duplicate( - self.mesh, name="{0}_Extract".format(shape.name) - )[0] - - # Shift the extracted shape to the side - cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) - if live: - self.connectShape(shape, extracted, live, delete=False) - return extracted - - @undoable - def connectShape(self, shape, mesh=None, live=False, delete=False): - """Force a shape to match a mesh - The "connect shape" button is: - mesh=None, delete=True - The "match shape" button is: - mesh=someMesh, delete=False - There is a possibility of a "make live" button: - live=True, delete=False - - Parameters - ---------- - shape : - - mesh : - (Default value = None) - live : - (Default value = False) - delete : - (Default value = False) - - Returns - ------- - - """ - if mesh is None: - attrName = cmds.attributeName(shape.thing, long=True) - mesh = "{0}_Extract".format(attrName) - - chk = cmds.ls(mesh) - if not chk: - return - if len(chk) > 1: - msg = "Multiple objects with the same name found in file:\n" - msg += '\n'.join(chk) - raise ValueError(msg) - - index = self.getShapeIndex(shape) - tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) - cnx = mesh + "Shape" if cmds.nodeType(mesh) == "transform" else mesh - - outAttr = "{0}.worldMesh[0]".format( - cnx - ) # Make sure to check the right shape object - inAttr = "{0}.inputTargetItem[6000].inputGeomTarget".format(tgn) - - if not cmds.isConnected(outAttr, inAttr): - cmds.connectAttr(outAttr, inAttr, force=True) - - if not live: - cmds.disconnectAttr(outAttr, inAttr) - - if delete: - cmds.delete(mesh) - - @undoable - def extractPosedShape(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ - pass - - @undoable - def zeroShape(self, shape): - """Set the shape to be completely zeroed - - Parameters - ---------- - shape : - - - Returns - ------- - - """ - index = self.getShapeIndex(shape) - tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) - shapeInput = "{0}.inputTargetItem[6000]".format(tgn) - cmds.setAttr( - "{0}.inputPointsTarget".format(shapeInput), 0, (), type="pointArray" - ) - cmds.setAttr( - "{0}.inputComponentsTarget".format(shapeInput), 0, "", type="componentList" - ) - - @undoable - def deleteShape(self, toDelShape): - """Remove a shape from the system - - Parameters - ---------- - toDelShape : - - - Returns - ------- - - """ - index = self.getShapeIndex(toDelShape) - tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) - cmds.removeMultiInstance(toDelShape.thing, b=True) - cmds.removeMultiInstance(tgn, b=True) - cmds.aliasAttr(toDelShape.thing, remove=True) - self._rebuildShapeConnections() - - def _rebuildShapeConnections(self): - """ """ - # Rebuild the shape connections in the proper order - cnxs = ( - cmds.listConnections( - self.op, plugs=True, source=False, destination=True, connections=True - ) - or [] - ) - for i, cnx in enumerate(cnxs): - if i % 2 == 0 and cnx.startswith("{0}.weights[".format(self.op)): - cmds.disconnectAttr(cnxs[i], cnxs[i + 1]) - - for i, shape in enumerate(self.simplex.shapes): - cmds.connectAttr( - "{0}.weights[{1}]".format(self.op, i), shape.thing, force=True - ) - - @undoable - def forceRebuildShapeConnections(self): - """ """ - self._rebuildShapeConnections() - - @undoable - def renameShape(self, shape, name): - """Change the name of the shape - - Parameters - ---------- - shape : - - name : - - - Returns - ------- - - """ - cmds.aliasAttr(name, shape.thing) - shape.thing = "{0}.{1}".format(self.shapeNode, name) - - @undoable - def convertShapeToCorrective(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ - pass - - # Falloffs - def createFalloff(self, name): - """ - - Parameters - ---------- - name : - - - Returns - ------- - - """ - pass # for eventual live splits - - def duplicateFalloff(self, falloff, newFalloff, newName): - """ - - Parameters - ---------- - falloff : - - newFalloff : - - newName : - - - Returns - ------- - - """ - pass # for eventual live splits - - def deleteFalloff(self, falloff): - """ - - Parameters - ---------- - falloff : - - - Returns - ------- - - """ - pass # for eventual live splits - - def setFalloffData( - self, falloff, splitType, axis, minVal, minHandle, maxHandle, maxVal, mapName - ): - """ - - Parameters - ---------- - falloff : - - splitType : - - axis : - - minVal : - - minHandle : - - maxHandle : - - maxVal : - - mapName : - - - Returns - ------- - - """ - pass # for eventual live splits - - def getFalloffThing(self, falloff): - """ - - Parameters - ---------- - falloff : - - - Returns - ------- - - """ - shape = cmds.listRelatives(self.mesh, shapes=True)[0] - return shape + "." + falloff.name - - # Sliders - @undoable - def createSlider(self, slider): - """ - - Parameters - ---------- - slider : - - - Returns - ------- - - """ - index = slider.simplex.sliders.index(slider) - cmds.addAttr( - self.ctrl, - longName=slider.name, - attributeType="double", - keyable=True, - min=slider.minValue * self.sliderMul, - max=slider.maxValue * self.sliderMul, - ) - thing = "{0}.{1}".format(self.ctrl, slider.name) - cmds.connectAttr(thing, "{0}.sliders[{1}]".format(self.op, index)) - return thing - - @undoable - def renameSlider(self, slider, name): - """Set the name of a slider - - Parameters - ---------- - slider : - - name : - - - Returns - ------- - - """ - vals = [v.value for v in slider.prog.pairs] - cnx = cmds.listConnections( - slider.thing, plugs=True, source=False, destination=True - ) - cmds.deleteAttr(slider.thing) - cmds.addAttr( - self.ctrl, - longName=name, - attributeType="double", - keyable=True, - min=self.sliderMul * min(vals), - max=self.sliderMul * max(vals), - ) - newThing = "{0}.{1}".format(self.ctrl, name) - slider.thing = newThing - for c in cnx: - cmds.connectAttr(newThing, c) - - @undoable - def setSliderRange(self, slider): - """Set the range of a slider - - Parameters - ---------- - slider : - - - Returns - ------- - - """ - vals = [v.value for v in slider.prog.pairs] - attrName = "{0}.{1}".format(self.ctrl, slider.name) - cmds.addAttr( - attrName, - edit=True, - min=self.sliderMul * min(vals), - max=self.sliderMul * max(vals), - ) - - @undoable - def deleteSlider(self, toDelSlider): - """ - - Parameters - ---------- - toDelSlider : - - - Returns - ------- - - """ - cmds.deleteAttr(toDelSlider.thing) - - # Rebuild the slider connections in the proper order - # Get the sliders connections - cnxs = cmds.listConnections( - self.op, plugs=True, source=True, destination=False, connections=True - ) - for i, cnx in enumerate(cnxs): - if cnx.startswith("{0}.sliders".format(self.op)): - cmds.disconnectAttr(cnxs[i + 1], cnxs[i]) - - for i, slider in enumerate(self.simplex.sliders): - cmds.connectAttr(slider.thing, "{0}.sliders[{1}]".format(self.op, i)) - - @undoable - def addProgFalloff(self, prog, falloff): - """ - - Parameters - ---------- - prog : - - falloff : - - - Returns - ------- - - """ - pass # for eventual live splits - - @undoable - def removeProgFalloff(self, prog, falloff): - """ - - Parameters - ---------- - prog : - - falloff : - - - Returns - ------- - - """ - pass # for eventual live splits - - @undoable - def setSlidersWeights(self, sliders, weights): - """Set the weight of a slider. This does not change the definition - - Parameters - ---------- - sliders : - - weights : - - - Returns - ------- - - """ - for slider, weight in zip(sliders, weights): - try: - cmds.setAttr(slider.thing, weight) - except RuntimeError: - # Probably locked or connected. Just skip it - pass - - @undoable - def setSliderWeight(self, slider, weight): - """ - - Parameters - ---------- - slider : - - weight : - - - Returns - ------- - - """ - try: - cmds.setAttr(slider.thing, weight) - except RuntimeError: - # Probably locked or connected. Just skip it - pass - - @undoable - def updateSlidersRange(self, sliders): - """ - - Parameters - ---------- - sliders : - - - Returns - ------- - - """ - for slider in sliders: - vals = [v.value for v in slider.prog.pairs] - cmds.addAttr( - slider.thing, - edit=True, - min=min(vals) * self.sliderMul, - max=max(vals) * self.sliderMul, - ) - - def _doesDeltaExist(self, combo, target): - """ - - Parameters - ---------- - combo : - - target : - - - Returns - ------- - - """ - dshape = "{0}_DeltaShape".format(combo.name) - if not cmds.ls(dshape): - return None - par = cmds.listRelatives(dshape, allParents=1) - if not par: - # there is apparently a transform object with the name - return None - - par = cmds.ls(par[0], absoluteName=1) - tar = cmds.ls(target, absoluteName=1) - - if par != tar: - # the shape exists under a different transform ... ugh - return None - return par + "|" + dshape - - def _clearShapes(self, item, doOrig=False): - """ - - Parameters - ---------- - item : - - doOrig : - (Default value = False) - - Returns - ------- - - """ - aname = cmds.ls(item, long=1)[0] - shapes = cmds.ls(cmds.listRelatives(item, shapes=1), long=1) - baseName = aname.split("|")[-1] - baseName, digits = split_trailing_digits(baseName) - - primary = "{0}|{1}Shape{2}".format(aname, baseName, digits) - - origs = [] - others = [] - for shape in shapes: - base, digits = split_trailing_digits(shape) - if base.endswith('Orig'): - origs.append(shape) - else: - others.append(shape) - - for shape in others: - if shape == primary: - continue - cmds.delete(shape) - - if doOrig: - cmds.delete(origs) - else: - # Don't delete the first orig - if len(origs) > 1: - origs = sorted(origs) - cmds.delete(origs[1:]) - - @undoable - def forceRebuildSliderConnections(self): - """ """ - self._rebuildSliderConnections() - - def _rebuildSliderConnections(self): - # disconnect all outputs from the ctrl - rcnx = cmds.listConnections( - self.ctrl, source=False, plugs=True, connections=True - ) - for i in range(0, len(rcnx), 2): - src, dst = rcnx[i], rcnx[i + 1] - if cmds.getAttr(src, type=True) != "double": - # only disconnect doubles - continue - cmds.disconnectAttr(src, dst) - - # Reconnect by name - for i, sli in enumerate(self.simplex.sliders): - thing = self.getSliderThing(sli.name) - cmds.connectAttr(thing, self.op + ".sliders[{0}]".format(i)) - - # Combos - def _reparentDeltaShapes(self, par, nodeDict, bsNode, toDelete=None): - """Reparent and clean up a single-transform delta system - - Put all the relevant shape nodes from the nodeDict under the par, - and rename the shapes to maya's convention. Then build a callback - to ensure the blendshape node isn't left floating - - par: The parent transform node - nodeDict: A {simpleName: node} dictionary. - bsNode: The blendshape node. - toDelete: Any extra nodes to delte after all the node twiddling - - Parameters - ---------- - par : - - nodeDict : - - bsNode : - - toDelete : - (Default value = None) - - Returns - ------- - - """ - # Get the shapes and origs - shapeDict = {} - origDict = {} - - for name, node in nodeDict.items(): - shape = cmds.listRelatives(node, noIntermediate=1, shapes=1)[0] - shape = cmds.ls(shape, absoluteName=1)[0] - if shape: - shapeDict[name] = shape - - orig = shape + "Orig" - orig = cmds.ls(orig) - if orig: - origDict[name] = orig - - for name in nodeDict: - for d, fmt in [(shapeDict, "{0}Shape{1}"), (origDict, "{0}Shape{1}Orig")]: - shape = d.get(name) - if shape is None: - continue - shapeUUID = cmds.ls(shape, uuid=1)[0] - cmds.parent(shape, par, shape=True, relative=True) - newShape = cmds.rename(cmds.ls(shapeUUID)[0], fmt.format(par, name)) - d[name] = newShape - cmds.setAttr(newShape + ".intermediateObject", 1) - cmds.hide(newShape) - - cmds.delete(nodeDict[name]) - - if toDelete: - cmds.delete(toDelete) - - # Use the simplexDelete message attribute to keep track of what nodes - # will need to be delete-linked when the file is reopened - sdNode = par + ".simplexDelete" - if not cmds.ls(sdNode): - cmds.addAttr(par, longName="simplexDelete", attributeType="message") - cmds.connectAttr(bsNode + ".message", sdNode) - - # build the callback setup so the blendshape is deleted with the delta setup - # along with a persistent scriptjob - buildDeleterCallback(par, bsNode) - buildDeleterScriptJob() - - return shapeDict - - def _createTravDelta(self, trav, target, tVal, doReparent=True): - """Part of the traversal extraction process. - Very similar to the combo extraction - - Parameters - ---------- - trav : - - target : - - tVal : - - - Returns - ------- - - """ - exists = self._doesDeltaExist(trav, target) - if exists is not None: - return exists - - # Traversals *MAY* depend on floaters, but that's complicated - # I'm just gonna ignore them for now - floatShapes = [i.thing for i in self.simplex.getFloatingShapes()] - - # Get all traversal shapes - tShapes = [] - for oTrav in self.simplex.traversals: - tShapes.extend([i.thing for i in oTrav.prog.getShapes()]) - - with disconnected(self.op) as cnx: - sliderCnx = cnx[self.op] - - # zero all slider vals on the op - for a in sliderCnx.values(): - cmds.setAttr(a, 0.0) - - with disconnected(floatShapes + tShapes): - # pull out the rest shape - rest = cmds.duplicate(self.mesh, name="{0}_Rest".format(trav.name))[0] - - sliDict = {} - for pair in trav.startPoint.pairs: - sliDict[pair.slider] = [pair.value] - for pair in trav.endPoint.pairs: - sliDict[pair.slider].append(pair.value) - - for slider, (start, end) in sliDict.items(): - vv = start + tVal * (end - start) - cmds.setAttr(sliderCnx[slider.thing], vv) - - deltaObj = cmds.duplicate( - self.mesh, name="{0}_Delta".format(trav.name) - )[0] - base = cmds.duplicate(deltaObj, name="{0}_Base".format(trav.name))[0] - - # clear out all non-primary shapes so we don't have those 'Orig1' things floating around - for item in [rest, deltaObj, base]: - self._clearShapes(item, doOrig=True) - - # Build the delta blendshape setup - bs = cmds.blendShape(deltaObj, name="{0}_DeltaBS".format(trav.name))[0] - cmds.blendShape(bs, edit=True, target=(deltaObj, 0, target, 1.0)) - cmds.blendShape(bs, edit=True, target=(deltaObj, 1, base, 1.0)) - cmds.blendShape(bs, edit=True, target=(deltaObj, 2, rest, 1.0)) - cmds.setAttr("{0}.{1}".format(bs, target), 1.0) - cmds.setAttr("{0}.{1}".format(bs, base), 1.0) - cmds.setAttr("{0}.{1}".format(bs, rest), 1.0) - - # Cleanup - if doReparent: - nodeDict = {"Delta": deltaObj} - repDict = self._reparentDeltaShapes(target, nodeDict, bs, [rest, base]) - return repDict["Delta"] - return deltaObj - - @undoable - def extractTraversalShape(self, trav, shape, live=True, offset=10.0): - """Extract a shape from a Traversal progression - - Parameters - ---------- - trav : - shape : - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ - floatShapes = self.simplex.getFloatingShapes() - floatShapes = [i.thing for i in floatShapes] - - shapeIdx = trav.prog.getShapeIndex(shape) - val = trav.prog.pairs[shapeIdx].value - - # TODO: Do traversals interact? Should I turn off any other traversals? - # For now, no, but it may be a thing - # tShapes = [] - # for oTrav in self.simplex.traversals: - # if oTrav is trav: continue - # tShapes.extend([i.thing for i in oTrav.prog.getShapes()]) - - with disconnected(self.op) as cnx: - sliderCnx = cnx[self.op] - # zero all slider vals on the op - for a in sliderCnx.values(): - cmds.setAttr(a, 0.0) - - with disconnected(floatShapes): # tShapes - sliDict = {} - for pair in trav.startPoint.pairs: - sliDict[pair.slider] = [pair.value] - for pair in trav.endPoint.pairs: - sliDict[pair.slider].append(pair.value) - - for slider, (start, end) in sliDict.items(): - vv = start + val * (end - start) - cmds.setAttr(sliderCnx[slider.thing], vv) - - extracted = cmds.duplicate( - self.mesh, name="{0}_Extract".format(shape.name) - ) - extracted = extracted[0] - self._clearShapes(extracted) - cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) - if live: - self.connectTraversalShape(trav, shape, extracted, live=live, delete=False) - cmds.select(extracted) - return extracted - - @undoable - def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False): - """Connect a shape into a Traversal progression - - Parameters - ---------- - trav : - - shape : - - mesh : - (Default value = None) - live : - (Default value = True) - delete : - (Default value = False) - - Returns - ------- - - """ - if mesh is None: - attrName = cmds.attributeName(shape.thing, long=True) - mesh = "{0}_Extract".format(attrName) - - chk = cmds.ls(mesh) - if not chk: - return - if len(chk) > 1: - msg = "Multiple objects with the same name found in file:\n" - msg += '\n'.join(chk) - raise ValueError(msg) - - shapeIdx = trav.prog.getShapeIndex(shape) - tVal = trav.prog.pairs[shapeIdx].value - delta = self._createTravDelta(trav, mesh, tVal) - - if live: - self.connectShape(shape, delta, live, delete) - - if delete: - cmds.delete(mesh) - - def _createComboDelta(self, combo, target, tVal, doReparent=True): - """Part of the combo extraction process. - Combo shapes are fixit shapes added on top of any sliders. - This means that the actual combo-shape by itself will not look good by itself, - and that's bad for artist interaction. - So we must create a setup to take the final sculpted shape, and subtract - the any direct slider deformations to get the actual "combo shape" as a delta - It is this delta shape that is then plugged into the system - - Parameters - ---------- - combo : - - target : - - tVal : - - - Returns - ------- - - """ - exists = self._doesDeltaExist(combo, target) - if exists is not None: - return exists - - # get floaters - # As floaters can appear anywhere along any combo, they must - # always be evaluated in isolation. For this reason, we will - # always disconnect all floaters - floatShapes = [i.thing for i in self.simplex.getFloatingShapes()] - - # get my shapes - myShapes = [i.thing for i in combo.prog.getShapes()] - - with disconnected([self.op] + floatShapes + myShapes) as cnx: - sliderCnx = cnx[self.op] - - # zero all slider vals on the op - for a in sliderCnx.values(): - cmds.setAttr(a, 0.0) - - # pull out the rest shape - rest = cmds.duplicate(self.mesh, name="{0}_Rest".format(combo.name))[0] - - # set the combo values - for pair in combo.pairs: - cmds.setAttr(sliderCnx[pair.slider.thing], pair.value * tVal) - - # Get the resulting slider values for later - # weightPairs = [] - # self.shapeNode = None # the deformer object - - deltaObj = cmds.duplicate(self.mesh, name="{0}_Delta".format(combo.name))[0] - base = cmds.duplicate(deltaObj, name="{0}_Base".format(combo.name))[0] - - # clear out all non-primary shapes so we don't have those 'Orig1' things floating around - for item in [rest, deltaObj, base]: - self._clearShapes(item, doOrig=True) - - # Build the delta blendshape setup - bs = cmds.blendShape(deltaObj, name="{0}_DeltaBS".format(combo.name))[0] - cmds.blendShape(bs, edit=True, target=(deltaObj, 0, target, 1.0)) - cmds.blendShape(bs, edit=True, target=(deltaObj, 1, base, 1.0)) - cmds.blendShape(bs, edit=True, target=(deltaObj, 2, rest, 1.0)) - cmds.setAttr("{0}.{1}".format(bs, target), 1.0) - cmds.setAttr("{0}.{1}".format(bs, base), 1.0) - cmds.setAttr("{0}.{1}".format(bs, rest), 1.0) - - # Cleanup - if doReparent: - nodeDict = {"Delta": deltaObj} - repDict = self._reparentDeltaShapes(target, nodeDict, bs, [rest, base]) - return repDict["Delta"] - return deltaObj - - @undoable - def extractComboShape(self, combo, shape, live=True, offset=10.0): - """Extract a shape from a combo progression - - Parameters - ---------- - combo : - - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ - floatShapes = self.simplex.getFloatingShapes() - floatShapes = [i.thing for i in floatShapes] - - shapeIdx = combo.prog.getShapeIndex(shape) - tVal = combo.prog.pairs[shapeIdx].value - - with disconnected(self.op) as cnx: - sliderCnx = cnx[self.op] - # zero all slider vals on the op - for a in sliderCnx.values(): - cmds.setAttr(a, 0.0) - - with disconnected(floatShapes): - # set the combo values - for pair in combo.pairs: - cmds.setAttr(sliderCnx[pair.slider.thing], pair.value * tVal) - - extracted = cmds.duplicate( - self.mesh, name="{0}_Extract".format(shape.name) - )[0] - - self._clearShapes(extracted) - cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) - - if live: - self.connectComboShape(combo, shape, extracted, live=live, delete=False) - - cmds.select(extracted) - return extracted - - @undoable - def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): - """Connect a shape into a combo progression - - Parameters - ---------- - combo : - - shape : - - mesh : - (Default value = None) - live : - (Default value = True) - delete : - (Default value = False) - - Returns - ------- - - """ - if mesh is None: - attrName = cmds.attributeName(shape.thing, long=True) - mesh = "{0}_Extract".format(attrName) - - chk = cmds.ls(mesh) - if not chk: - return - if len(chk) > 1: - msg = "Multiple objects with the same name found in file:\n" - msg += '\n'.join(chk) - raise ValueError(msg) - mesh = chk[0] - - progShapeIdx = combo.prog.getShapeIndex(shape) - tVal = combo.prog.pairs[progShapeIdx].value - - delta = self._createComboDelta(combo, mesh, tVal) - if live: - self.connectShape(shape, delta, live, delete) - - if delete: - cmds.delete(mesh) - - @classmethod - def setDisabled(cls, op): - """ - - Parameters - ---------- - op : - - - Returns - ------- - - """ - bss = list(set(cmds.listConnections(op, type="blendShape"))) - helpers = [] - for bs in bss: - prop = "{0}.envelope".format(bs) - val = cmds.getAttr(prop) - cmds.setAttr(prop, 0.0) - if val != 0.0: - helpers.append((prop, val)) - return helpers - - @classmethod - def reEnable(cls, helpers): - """ - - Parameters - ---------- - helpers : - - - Returns - ------- - - """ - for prop, val in helpers: - cmds.setAttr(prop, val) - - @undoable - def renameCombo(self, combo, name): - """Set the name of a Combo - - Parameters - ---------- - combo : - - name : - - - Returns - ------- - - """ - pass - - # Data Access - @classmethod - def getSimplexOperators(cls): - """ """ - return cmds.ls(type="simplex_maya") - - @classmethod - def getSimplexOperatorsByName(cls, name): - """ - - Parameters - ---------- - name : - - - Returns - ------- - type - - - """ - return cmds.ls(name, type="simplex_maya") - - @classmethod - def getSimplexOperatorsOnObject(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - type - - - """ - ops = cmds.ls(type="simplex_maya") - out = [] - for op in ops: - shapeNode = cmds.listConnections( - "{0}.{1}".format(op, "shapeMsg"), source=True, destination=False - ) - if not shapeNode: - continue - - # Now that I've got the connected blendshape node, I can check the deformer history - # to see if I find it. Eventually, I should probably set this up to deal with - # multi-objects, or branched hierarchies. But for now, it works - if shapeNode[0] in (cmds.listHistory(thing, pruneDagObjects=True) or []): - out.append(op) - return out - - @classmethod - def getSimplexString(cls, op): - """ - - Parameters - ---------- - op : - - - Returns - ------- - type - - - """ - return cmds.getAttr(op + ".definition") - - @classmethod - def getSimplexStringOnThing(cls, thing, systemName): - """ - - Parameters - ---------- - thing : - - systemName : - - - Returns - ------- - type - - - """ - ops = DCC.getSimplexOperatorsOnObject(thing) - for op in ops: - js = DCC.getSimplexString(op) - jdict = json.loads(js) - if jdict["systemName"] == systemName: - return js - return None - - @classmethod - def setSimplexString(cls, op, val): - """ - - Parameters - ---------- - op : - - val : - - - Returns - ------- - type - - - """ - return cmds.setAttr(op + ".definition", val, type="string") - - @classmethod - def selectObject(cls, thing): - """Select an object in the DCC - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - cmds.select([thing]) - - def selectCtrl(self): - """Select the system's control object""" - if self.ctrl: - self.selectObject(self.ctrl) - - @classmethod - def getObjectByName(cls, name): - """ - - Parameters - ---------- - name : - - - Returns - ------- - type - - - """ - objs = cmds.ls(name) - if not objs: - return None - if len(objs) > 1: - raise ValueError("Multiple objects with the same name found") - return objs[0] - - @classmethod - def getObjectName(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - type - - - """ - return thing - - @classmethod - def staticUndoOpen(cls): - """ """ - cmds.undoInfo(chunkName="SimplexOperation", openChunk=True) - - @classmethod - def staticUndoClose(cls): - """ """ - cmds.undoInfo(closeChunk=True) - - def undoOpen(self): - """ """ - if self.undoDepth == 0: - self.staticUndoOpen() - self.undoDepth += 1 - - def undoClose(self): - """ """ - self.undoDepth -= 1 - if self.undoDepth == 0: - self.staticUndoClose() - - @classmethod - def getPersistentFalloff(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - return cls.getObjectName(thing) - - @classmethod - def loadPersistentFalloff(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - return cls.getObjectByName(thing) - - @classmethod - def getPersistentShape(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - return cls.getObjectName(thing) - - @classmethod - def loadPersistentShape(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - return cls.getObjectByName(thing) - - @classmethod - def getPersistentSlider(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - return cls.getObjectName(thing) - - @classmethod - def loadPersistentSlider(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ - return cls.getObjectByName(thing) - - @classmethod - def getSelectedObjects(cls): - """ """ - # For maya, only return transform nodes - return cmds.ls(sl=True, transforms=True) - - @undoable - def importObj(self, path): - """ - - Parameters - ---------- - path : - - - Returns - ------- - - """ - current = set(cmds.ls(transforms=True)) - cmds.file(path, i=True, type="OBJ", ignoreVersion=True) - new = set(cmds.ls(transforms=True)) - shapes = set(cmds.ls(shapes=True)) - new = new - current - shapes - imp = new.pop() - return imp - - @classmethod - def _getDeformerChain(cls, chkObj): - # Get a deformer chain - memo = [] - while chkObj and chkObj not in memo: - memo.append(chkObj) - - typ = cmds.nodeType(chkObj) - if typ == "mesh": - cnx = cmds.listConnections(chkObj + ".inMesh") or [None] - chkObj = cnx[0] - elif typ == "groupParts": - cnx = cmds.listConnections( - chkObj + ".inputGeometry", destination=False, shapes=True - ) or [None] - chkObj = cnx[0] - else: - cnx = cmds.ls(chkObj, type="geometryFilter") or [None] - chkObj = cnx[0] - if chkObj: # we have a deformer - cnx = cmds.listConnections(chkObj + ".input[0].inputGeometry") or [ - None - ] - chkObj = cnx[0] - return memo - - # Freezing stuff - def primeShapes(self, combo): - """Make sure the upstream shapes of this combo are primed and ready. - Priming here means the deltas are stored and available on the blendshape node - """ - # Maya doesn't populate the delta plugs on the blendshape node unless - # you have a mesh connection while the value for that shape is turned to 1 - upstreams = [] - comboUps = self.simplex.getComboUpstreams(combo) - for u in comboUps: - upstreams.append(u.prog.getShapeAtValue(1.0)) - for pair in combo.pairs: - sli, val = pair.slider, pair.value - upstreams.append(sli.prog.getShapeAtValue(val)) - - with disconnected(self.shapeNode) as cnx: - shapeCnx = cnx[self.shapeNode] - for v in shapeCnx.values(): - cmds.setAttr(v, 0.0) - - for shape in upstreams: - cmds.setAttr(shape.thing, 1.0) - try: - # Make sure to check for any already incoming connections - index = self.getShapeIndex(shape) - tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format( - self.shapeNode, index - ) - isConnected = cmds.listConnections( - tgn, source=True, destination=False - ) - - if not isConnected: - shapeGeo = cmds.duplicate(self.mesh, name=shape.name)[0] - shape.connectShape(mesh=shapeGeo, live=False, delete=True) - - finally: - cmds.setAttr(shape.thing, 0.0) - - def getFreezeThing(self, combo): - # If the blendshape shape has an incoming connection whose shape name - # ends with 'FreezeShape' and the shape's parent is the ctrl - ret = [] - shapes = combo.prog.getShapes() - shapes = [i for i in shapes if not i.isRest] - - shapePlugFmt = ( - ".inputTarget[{meshIdx}].inputTargetGroup[{shapeIdx}].inputTargetItem[6000]" - ) - - for shape in shapes: - shpIdx = self.getShapeIndex(shape) - shpPlug = ( - self.shapeNode - + shapePlugFmt.format(meshIdx=0, shapeIdx=shpIdx) - + ".inputGeomTarget" - ) - - cnx = cmds.listConnections(shpPlug, shapes=True, destination=False) or [] - for cc in cnx: - if not cc.endswith("FreezeShape"): - continue - par = cmds.listRelatives(cc, parent=True) - if par and par[0] == self.ctrl: - # Can't use list history to get the chain because it's a pseudo-cycle - ret.extend(self._getDeformerChain(cc)) - - if ret: - self.primeShapes(combo) - - return ret - - -class SliderDispatch(QtCore.QObject): - valueChanged = Signal() - - def __init__(self, node, parent=None): - super(SliderDispatch, self).__init__(parent) - mObject = getMObject(node) - self.callbackID = om.MNodeMessage.addAttributeChangedCallback( - mObject, self.emitValueChanged - ) - - def emitValueChanged(self, *args, **kwargs): - self.valueChanged.emit() - - def disconnectCallbacks(self): - om.MMessage.removeCallback(self.callbackID) - self.callbackID = None - - def __del__(self): - self.disconnectCallbacks() - - -class Dispatch(QtCore.QObject): - beforeNew = Signal() - afterNew = Signal() - beforeOpen = Signal() - afterOpen = Signal() - undo = Signal() - redo = Signal() - - def __init__(self, parent=None): - super(Dispatch, self).__init__(parent) - self.callbackIDs = [] - self.connectCallbacks() - - def connectCallbacks(self): - if self.callbackIDs: - self.disconnectCallbacks() - - self.callbackIDs.append( - om.MSceneMessage.addCallback( - om.MSceneMessage.kBeforeNew, self.emitBeforeNew - ) - ) - self.callbackIDs.append( - om.MSceneMessage.addCallback(om.MSceneMessage.kAfterNew, self.emitAfterNew) - ) - self.callbackIDs.append( - om.MSceneMessage.addCallback( - om.MSceneMessage.kBeforeOpen, self.emitBeforeOpen - ) - ) - self.callbackIDs.append( - om.MSceneMessage.addCallback( - om.MSceneMessage.kAfterOpen, self.emitAfterOpen - ) - ) - self.callbackIDs.append( - om.MEventMessage.addEventCallback("Undo", self.emitUndo) - ) - self.callbackIDs.append( - om.MEventMessage.addEventCallback("Redo", self.emitRedo) - ) - - def disconnectCallbacks(self): - for i in self.callbackIDs: - om.MMessage.removeCallback(i) - self.callbackIDs = [] - - def emitBeforeNew(self, *args, **kwargs): - self.beforeNew.emit() - - def emitAfterNew(self, *args, **kwargs): - self.afterNew.emit() - - def emitBeforeOpen(self, *args, **kwargs): - self.beforeOpen.emit() - - def emitAfterOpen(self, *args, **kwargs): - self.afterOpen.emit() - - def emitUndo(self, *args, **kwargs): - self.undo.emit() - - def emitRedo(self, *args, **kwargs): - self.redo.emit() - - def __del__(self): - self.disconnectCallbacks() - - -DISPATCH = Dispatch() - - -def rootWindow(): - """Returns the currently active QT main window - Only works for QT UI's like Maya - """ - # for MFC apps there should be no root window - window = None - if QApplication.instance(): - inst = QApplication.instance() - window = inst.activeWindow() - # Ignore QSplashScreen's, they should never be considered the root window. - if isinstance(window, QSplashScreen): - return None - # If the application does not have focus try to find A top level widget - # that doesn't have a parent and is a QMainWindow or QDialog - if window is None: - windows = [] - dialogs = [] - for w in QApplication.instance().topLevelWidgets(): - if w.parent() is None: - if isinstance(w, QMainWindow): - windows.append(w) - elif isinstance(w, QDialog): - dialogs.append(w) - if windows: - window = windows[0] - elif dialogs: - window = dialogs[0] - - # grab the root window - if window: - while True: - parent = window.parent() - if not parent: - break - if isinstance(parent, QSplashScreen): - break - window = parent - - return window - - -SIMPLEX_RESET_SCRIPTJOB = """ -import maya.cmds as cmds -import maya.OpenMaya as om - -def simplexDelCB(node, dgMod, clientData): - xNode, dName = clientData - dNode = getMObject(dName) - if dNode and not dNode.isNull(): - dgMod.deleteNode(dNode) - -def getMObject(name): - selected = om.MSelectionList() - try: - selected.add(name, True) - except RuntimeError: - return None - if selected.isEmpty(): - return None - thing = om.MObject() - selected.getDependNode(0, thing) - return thing - -# get all .simplexDelete message attributes -delAttrs = cmds.ls("*.simplexDelete") -if delAttrs: - # get all their connections - cnx = cmds.listConnections(delAttrs, plugs=True, connections=True, destination=False) - - # Set up the deletion callback - mmIds = [] - for i in range(0, len(cnx), 2): - parName, delName = cmds.ls(cnx[i:i+2], long=True, objectsOnly=True) - pNode = getMObject(parName) - dNode = getMObject(delName) - om.MNodeMessage.addNodeAboutToDeleteCallback(pNode, simplexDelCB, (dNode, delName)) -""" - - -def buildDeleterScriptJob(): - dcbName = "SimplexDeleterCallback" - if not cmds.ls(dcbName): - cmds.scriptNode( - scriptType=1, - beforeScript=SIMPLEX_RESET_SCRIPTJOB, - name=dcbName, - sourceType="python", - ) - - -def simplexDelCB(_node, dgMod, clientData): - _xNode, dName = clientData - dNode = getMObject(dName) - if dNode and not dNode.isNull(): - dgMod.deleteNode(dNode) - - -def getMObject(name): - selected = om.MSelectionList() - try: - selected.add(name, True) - except RuntimeError: - return None - if selected.isEmpty(): - return None - thing = om.MObject() - selected.getDependNode(0, thing) - return thing - - -def buildDeleterCallback(parName, delName): - pNode = getMObject(parName) - dNode = getMObject(delName) - idNum = om.MNodeMessage.addNodeAboutToDeleteCallback( - pNode, simplexDelCB, (dNode, delName) - ) - return idNum +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint: disable=invalid-name +from __future__ import annotations +import json +import re +from contextlib import contextmanager +from functools import wraps +from typing import TYPE_CHECKING + +import maya.cmds as cmds +import maya.OpenMaya as om +from alembic.AbcGeom import GeometryScope, OPolyMeshSchemaSample, OV2fGeomParamSample +from imath import IntArray, UnsignedIntArray, V2fArray, V3fArray + +from ..commands.alembicCommon import mkSampleVertexPoints, buildAbc +from Qt import QtCore +from Qt.QtCore import Signal +from Qt.QtWidgets import ( + QApplication, + QDialog, + QMainWindow, + QMessageBox, + QSplashScreen, +) + +if TYPE_CHECKING: + from ..items.simplex import Simplex + +try: + import numpy as np + from ..commands.numpytoimath import numpyToImath + from ..commands.mayatonumpy import mayaToNumpy +except ImportError: + np = None + numpyToImath = None + mayaToNumpy = None + + +# UNDO STACK INTEGRATION +@contextmanager +def undoContext(inst=None): + """ + + Parameters + ---------- + inst : + (Default value = None) + + Returns + ------- + + """ + if inst is None: + DCC.staticUndoOpen() + else: + inst.undoOpen() + try: + yield + finally: + if inst is None: + DCC.staticUndoClose() + else: + inst.undoClose() + + +def undoable(f): + """ + + Parameters + ---------- + f : + + + Returns + ------- + + """ + + @wraps(f) + def stacker(*args, **kwargs): + """ + + Parameters + ---------- + *args : + + **kwargs : + + + Returns + ------- + + """ + inst = None + if args and isinstance(args[0], DCC): + inst = args[0] + with undoContext(inst): + return f(*args, **kwargs) + + return stacker + + +def doDisconnect(targets, testCnxType=("double", "float")): + """Temporarily disconnect inputs from a list of nodes and plugs + + Parameters + ---------- + targets : + + testCnxType : + (Default value = ("double") + "float") : + + + Returns + ------- + + """ + if not isinstance(targets, (list, tuple)): + targets = [targets] + targets = list(set(targets)) + cnxs = {} + for target in targets: + tcnx = {} + cnxs[target] = tcnx + + cnx = cmds.listConnections( + target, plugs=True, destination=False, source=True, connections=True + ) + if cnx is None: + cnx = [] + + for i in range(0, len(cnx), 2): + cnxType = cmds.getAttr(cnx[i], type=True) + if cnxType not in testCnxType: + continue + tcnx[cnx[i + 1]] = cnx[i] + cmds.disconnectAttr(cnx[i + 1], cnx[i]) + return cnxs + + +def doReconnect(cnxs): + """ + + Parameters + ---------- + cnxs : + + + Returns + ------- + + """ + for tdict in cnxs.values(): + for s, d in tdict.items(): + if not cmds.isConnected(s, d): + cmds.connectAttr(s, d, force=True) + + +@contextmanager +def disconnected(targets, testCnxType=("double", "float")): + """ + + Parameters + ---------- + targets : + + testCnxType : + (Default value = ("double") + "float") : + + + Returns + ------- + + """ + cnxs = doDisconnect(targets, testCnxType=testCnxType) + try: + yield cnxs + finally: + doReconnect(cnxs) + + +def split_trailing_digits(s: str) -> tuple[str, str]: + """Split trailing digits from a string into (prefix, digits).""" + match = re.match(r"^(.*?)(\d+)?$", s) + if match: + return match.group(1), match.group(2) or "" + return s, "" + + +class DCC(object): + """ """ + + program = "maya" + + def __init__(self, simplex, stack=None): + if not cmds.pluginInfo("simplex_maya", query=True, loaded=True): + cmds.loadPlugin("simplex_maya") + self.undoDepth: int = 0 + self.name: str = "" # the name of the system + self.mesh: str = "" # the mesh object with the system + self.ctrl: str = "" # the object that has all the controllers on it + self.shapeNode: str = "" # the deformer object + self.hasPoseNode = False + self.poseNode: str = "" # the pose deformer object + self.op: str = "" # the simplex object + self.simplex: Simplex = simplex # the abstract representation of the setup + self._live: bool = True + self.sliderMul: float = self.simplex.sliderMul + + # def __deepcopy__(self, memo): + # ''' + # I don't actually need to define this here because I know that + # all of the maya "objects" store here are just strings + # But if they *weren't* (like in XSI) I would need to skip + # the maya objects when deepcopying, otherwise I might access + # a deleted scene node and crash everything + # And if we did skip things, I would also need to store a + # persistent accessor to use in case we get back to here + # through an undo + # ''' + # pass + + def _checkAllShapeValidity(self, shapeNames): + """Check shapes to see if they exist, and either gather the missing files, or + Load the proper data onto the shapes + + Parameters + ---------- + shapeNames : + + + Returns + ------- + + """ + # Keep the set ordered, but make a set for quick checking + missingNameSet = set() + missingNames = [] + seen = set() + + # Get the blendshape weight names + try: + # GOOD GOD. This is because in maya 2016.5, if you delete a multi-instance + # then listAttr, *it lists the deleted ones, and skips the ones at the end* + # So I have to use aliasAttr and filter for the weights + aliases = cmds.aliasAttr(self.shapeNode, query=True) or [] + attrs = [ + aliases[i] + for i in range(0, len(aliases), 2) + if aliases[i + 1].startswith("weight[") + ] + attrs = set(attrs) + + except ValueError: + attrs = set() + + for shapeName in shapeNames: + if shapeName in seen: + continue + seen.add(shapeName) + if shapeName not in attrs: + if shapeName not in missingNameSet: + missingNameSet.add(shapeName) + missingNames.append(shapeName) + return missingNames, len(attrs) + + @classmethod + def _removeExtraShapeNodes(cls, tfm): + shapeNodes = cmds.listRelatives(tfm, shapes=True, noIntermediate=True) + if len(shapeNodes) > 1: + keeper = None + todel = [] + for sn in shapeNodes: + tfmChk = "".join(sn.rsplit("Shape", 1)) + if tfmChk == tfm: + keeper = sn + else: + todel.append(sn) + if keeper is not None: + cmds.delete(todel) + + def preLoad(self, simp, simpDict, create=True, pBar=None): + """ + + Parameters + ---------- + simp : + + simpDict : + + create : + (Default value = True) + pBar : + (Default value = None) + + Returns + ------- + + """ + cmds.undoInfo(state=False) + try: + if pBar is not None: + pBar.setLabelText("Loading Connections") + QApplication.processEvents() + ev = simpDict["encodingVersion"] + + shapeNames = simpDict.get("shapes") + if not shapeNames: + return + + if ev > 1: + shapeNames = [i["name"] for i in shapeNames] + + toMake, nextIndex = self._checkAllShapeValidity(shapeNames) + + if not toMake: + return + + if not create: + if pBar is not None: + msg = "Some shapes are Missing:\n{}\n\nCreate them?" + msg = msg.format(", ".join(toMake)) + btns = ( + QMessageBox.StandardButton.Yes + | QMessageBox.StandardButton.Cancel + ) + bret = QMessageBox.question(pBar, "Missing Shapes", msg, btns) + if not bret & QMessageBox.StandardButton.Yes: + raise RuntimeError("Missing Shapes: {}".format(toMake)) + else: + raise RuntimeError("Missing Shapes: {}".format(toMake)) + + if pBar is not None: + spacer = "_" * max(list(map(len, toMake))) + pBar.setMaximum(len(toMake)) + pBar.setLabelText("Creating Empty Shape:\n{0}".format(spacer)) + pBar.setValue(0) + QApplication.processEvents() + + baseShape = cmds.duplicate(self.mesh)[0] + cmds.delete(baseShape, constructionHistory=True) + self._removeExtraShapeNodes(baseShape) + + for i, shapeName in enumerate(toMake): + if pBar is not None: + pBar.setLabelText("Creating Empty Shape:\n{0}".format(shapeName)) + pBar.setValue(i) + QApplication.processEvents() + + baseShape = cmds.rename(baseShape, shapeName) + + index = self._firstAvailableIndex() + cmds.blendShape( + self.shapeNode, edit=True, target=(self.mesh, index, baseShape, 1.0) + ) + weightAttr = "{0}.weight[{1}]".format(self.shapeNode, index) + thing = cmds.ls(weightAttr)[0] + + cmds.connectAttr("{0}.weights[{1}]".format(self.op, nextIndex), thing) + nextIndex += 1 + + cmds.delete(baseShape) + except Exception: + cmds.undoInfo(state=True) + raise + + def postLoad(self, simp, preRet): + """ + + Parameters + ---------- + simp : + + preRet : + + Returns + ------- + + """ + cmds.undoInfo(state=True) + + def checkForErrors(self, window): + """Check for any DCC specific errors + + Parameters + ---------- + window : QMainWindow + The simplex window + """ + shapeNodes = cmds.listRelatives(self.mesh, shapes=True, noIntermediate=True) + if len(shapeNodes) > 1: + msg = ( + "The current mesh has multiple shape nodes.", + "The UI will still mostly work, but extracting/connecting shapes" + "may fail in unexpected ways.", + ) + QMessageBox.warning(window, "Multiple Shape Nodes", "\n".join(msg)) + + def _getOpNodes(self, thing: str): + hist: list[str] = cmds.listHistory(thing) + rawShapeNodes = cmds.ls(hist, type="blendShape") + rawPoseNodes = cmds.ls(hist, type="blendPose") + + # Find any simplex ops connected to the history + # that have the given name + ops: list[str] = [] + rawOps: list[str] = [] + for sn in rawShapeNodes + rawPoseNodes: + op = ( + cmds.listConnections( + "{0}.{1}".format(sn, "message"), + source=False, + destination=True, + type="simplex_maya", + ) + or [] + ) + rawOps.extend(op) + rawOps = list(set(rawOps)) + + for op in rawOps: + js = cmds.getAttr(op + ".definition") or "" + sn = json.loads(js).get("systemName") + if sn == self.name: + ops.append(op) + + if len(ops) > 1: + raise RuntimeError( + "Found too many Simplex systems with the same name on the same object" + ) + return ops + + def _getShapeNodes(self, ops): + shapeNodes = [] + for op in ops: + try: + sn = cmds.listConnections( + "{0}.{1}".format(op, "shapeMsg"), + source=True, + destination=False, + type="blendShape", + ) + except ValueError: + continue + if sn: + shapeNodes.append(sn[0]) + return shapeNodes + + def _getPoseNodes(self, ops): + poseNodes = [] + for op in ops: + try: + sn = cmds.listConnections( + "{0}.{1}".format(op, "poseMsg"), + source=True, + destination=False, + type="blendPose", + ) + except ValueError: + continue + if sn: + poseNodes.append(sn[0]) + return poseNodes + + def _getCtrlNodes(self, ops): + ctrlCnx = [] + for op in ops: + ccnx = cmds.listConnections( + "{0}.{1}".format(op, "ctrlMsg"), + source=True, + destination=False, + ) + if ccnx: + ctrlCnx.append(ccnx[0]) + return ctrlCnx + + def _createShapeNode(self, name, mesh) -> str: + intermediates = [ + shp + for shp in cmds.listRelatives(mesh, shapes=True, path=True) + if cmds.getAttr(shp + ".intermediateObject") + ] + meshToFreeze = mesh if not intermediates else intermediates[0] + isIntermediate = cmds.getAttr(meshToFreeze + ".intermediateObject") + + # Unlock the normals on the rest head because blendshapes don't work with locked normals + # and you can't really do this after the blendshape has been created + cmds.polyNormalPerVertex(meshToFreeze, unFreezeNormal=True) + cmds.polySoftEdge(meshToFreeze, angle=180, constructionHistory=True) + cmds.setAttr(meshToFreeze + ".intermediateObject", 0) + cmds.delete(meshToFreeze, constructionHistory=True) + cmds.setAttr(meshToFreeze + ".intermediateObject", isIntermediate) + + name = "{}_BS".format(name) + return cmds.blendShape(mesh, name=name, frontOfChain=True)[0] + + def _createPoseNode(self, name) -> str: + name = "{}_BP".format(name) + return cmds.createNode("blendPose", name=name) + + def _createSimplexNode(self, name): + op = cmds.createNode("simplex_maya", name=name) + cmds.addAttr(op, longName="revision", attributeType="long") + cmds.addAttr(op, longName="shapeMsg", attributeType="message") + cmds.addAttr(op, longName="poseMsg", attributeType="message") + cmds.addAttr(op, longName="ctrlMsg", attributeType="message") + return op + + def _createControlNode(self, name, op): + tfmAttrs = [".tx", ".ty", ".tz", ".rx", ".ry", ".rz", ".sx", ".sy", ".sz", ".v"] + ctrl = cmds.group(empty=True, name="{0}_CTRL".format(name)) + for attr in tfmAttrs: + cmds.setAttr(ctrl + attr, keyable=False, channelBox=False) + cmds.addAttr(ctrl, longName="solver", attributeType="message") + cmds.connectAttr( + "{0}.{1}".format(ctrl, "solver"), + "{0}.{1}".format(op, "ctrlMsg"), + ) + return ctrl + + # System IO + @undoable + def loadNodes(self, simp, thing, create=True, pBar=None): + """Create a new system based on the simplex tree + Build any DCC objects that are missing if create=True + Raises a runtime error if missing objects are found and + create=False + + Parameters + ---------- + simp : + + thing : + + create : + (Default value = True) + pBar : + (Default value = None) + + Returns + ------- + + """ + self.name = simp.name + self.mesh = thing + + ops = self._getOpNodes(thing) + sns = self._getShapeNodes(ops) + cc = self._getCtrlNodes(ops) + + if not create and (not sns or not ops or not cc): + types = [] + if not sns: + types.append("blendShape") + if not ops: + types.append("simplex_maya") + if not cc: + types.append("CTRL") + + raise RuntimeError( + "Creation turned off and some objects are missing: {}".format( + ", ".join(types) + ) + ) + + self.op = ops[0] if ops else self._createSimplexNode(self.name) + self.ctrl = cc[0] if cc else self._createControlNode(self.name, self.op) + + self.shapeNode = sns[0] if sns else self._createShapeNode(self.name, self.mesh) + if not cmds.isConnected(f"{self.shapeNode}.message", f"{self.op}.shapeMsg"): + cmds.connectAttr(f"{self.shapeNode}.message", f"{self.op}.shapeMsg") + + if self.hasPoseNode: + pns = self._getPoseNodes(ops) + self.poseNode = pns[0] if pns else self._createPoseNode(self.name) + if not cmds.isConnected(f"{self.poseNode}.message", f"{self.op}.poseMsg"): + cmds.connectAttr(f"{self.poseNode}.message", f"{self.op}.poseMsg") + + def getShapeThing(self, shapeName): + """ + + Parameters + ---------- + shapeName : + + + Returns + ------- + + """ + s = cmds.ls("{0}.{1}".format(self.shapeNode, shapeName)) + if not s: + return None + return s[0] + + def getSliderThing(self, sliderName): + """ + + Parameters + ---------- + sliderName : + + + Returns + ------- + + """ + things = cmds.ls("{0}.{1}".format(self.ctrl, sliderName)) + if not things: + return None + return things[0] + + @classmethod + def buildDummyMesh(cls, name: str): + importHeadShape = cmds.createNode("mesh", name=name + "Shape") + badPar = cmds.listRelatives(importHeadShape, parent=True)[0] + importHead = cmds.rename(badPar, name) + return importHead, importHeadShape + + @classmethod + @undoable + def buildRestAbc(cls, abcMesh, name): + """ + + Parameters + ---------- + abcMesh : + + name : + + + Returns + ------- + + """ + if not cmds.pluginInfo("AbcImport", query=True, loaded=True): + cmds.loadPlugin("AbcImport") + if not cmds.pluginInfo("AbcImport", query=True, loaded=True): + raise RuntimeError("Unable to load the AbcImport plugin") + + abcPath = str(abcMesh.getArchive()) + + abcNode = cmds.createNode("AlembicNode") + cmds.setAttr(abcNode + ".abc_File", abcPath, type="string") + cmds.setAttr(abcNode + ".speed", 24) # Is this needed anymore? + cmds.setAttr(abcNode + ".time", 0) + + importHead, importHeadShape = cls.buildDummyMesh("{0}_SIMPLEX".format(name)) + + importHead = "{0}_SIMPLEX".format(name) + importHeadShape = cmds.createNode("mesh", name=importHead + "Shape") + badPar = cmds.listRelatives(importHeadShape, parent=True)[0] + importHead = cmds.rename(badPar, importHead) + + cmds.connectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") + cmds.polyEvaluate(importHead, vertex=True) # Force a refresh + cmds.disconnectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") + cmds.sets(importHead, edit=True, forceElement="initialShadingGroup") + cmds.delete(abcNode) + return importHead + + @classmethod + def vertCount(cls, mesh): + return cmds.polyEvaluate(mesh, vertex=True) + + @undoable + def loadAbcPoses(self, abcMesh, js, pBar=None): + pass + + @undoable + def loadAbc(self, abcMesh, js, pBar=None): + """ + + Parameters + ---------- + abcMesh : + + js : + + pBar : + (Default value = None) + + Returns + ------- + + """ + # UGH, I *REALLY* hate that this is faster + # But if I want to be "pure" about it, I should just bite the bullet + # and do the direct alembic manipulation in C++ + + if not cmds.pluginInfo("AbcImport", query=True, loaded=True): + cmds.loadPlugin("AbcImport") + if not cmds.pluginInfo("AbcImport", query=True, loaded=True): + raise RuntimeError("Unable to load the AbcImport plugin") + + abcPath = str(abcMesh.getArchive()) + + abcNode = cmds.createNode("AlembicNode") + cmds.setAttr(abcNode + ".abc_File", abcPath, type="string") + + timeUnits = { + "game": 15, + "film": 24, + "pal": 25, + "ntsc": 30, + "show": 48, + "palf": 50, + "ntscf": 60, + } + + fps = cmds.currentUnit(time=True, query=True) + if isinstance(fps, str): + if fps.endswith("fps"): + fps = fps[:-3] + if fps in timeUnits: + fps = timeUnits[fps] + fps = float(fps) + + cmds.setAttr(abcNode + ".speed", fps) + + shapes = js["shapes"] + shapeDict = {i.name: i for i in self.simplex.shapes} + + if js["encodingVersion"] > 1: + shapes = [i["name"] for i in shapes] + + importHead, importHeadShape = self.buildDummyMesh("importHead") + + cmds.connectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") + cmds.polyEvaluate(importHead, vertex=True) # Force a refresh + cmds.disconnectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") + + importRest = cmds.duplicate(self.mesh, name="importRest")[0] + cmds.delete(importRest, constructionHistory=True) + self._removeExtraShapeNodes(importRest) + + importBS: str = cmds.blendShape(importRest, importHead)[0] + cmds.blendShape(importBS, edit=True, weight=[(0, 1.0)]) + # Maybe get shapeNode from self.mesh?? + importOrig = [ + i for i in cmds.listRelatives(importHead, shapes=True) if i.endswith("Orig") + ][0] + cmds.connectAttr(abcNode + ".outPolyMesh[0]", importOrig + ".inMesh") + cmds.delete(importRest) + + if pBar is not None: + pBar.show() + pBar.setMaximum(len(shapes)) + longName = max(shapes, key=len) + pBar.setValue(1) + pBar.setLabelText("Loading:\n{0}".format("_" * len(longName))) + + for i, shapeName in enumerate(shapes): + if pBar is not None: + pBar.setValue(i) + pBar.setLabelText("Loading:\n{0}".format(shapeName)) + QApplication.processEvents() + if pBar.wasCanceled(): + return + index = self.getShapeIndex(shapeDict[shapeName]) + cmds.setAttr(abcNode + ".time", i) + + outAttr = "{0}.worldMesh[0]".format(importHead) + tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format( + self.shapeNode, index + ) + inAttr = "{0}.inputTargetItem[6000].inputGeomTarget".format(tgn) + + cmds.connectAttr(outAttr, inAttr, force=True) + cmds.disconnectAttr(outAttr, inAttr) + cmds.delete(abcNode) + cmds.delete(importHead) + + def getAllShapeVertices(self, shapes, pBar=None): + """ + + Parameters + ---------- + shapes : + + pBar : + (Default value = None) + + Returns + ------- + + """ + sl = om.MSelectionList() + sl.add(self.mesh) + thing = om.MDagPath() + sl.getDagPath(0, thing) + meshFn = om.MFnMesh(thing) + _ptCount = meshFn.numVertices() + with disconnected(self.shapeNode) as cnx: + shapeCnx = cnx[self.shapeNode] + for v in shapeCnx.values(): + cmds.setAttr(v, 0.0) + + if pBar is not None: + # find the longest name for displaying stuff + sns = "_" * max(list(map(len, [s.name for s in shapes]))) + pBar.setLabelText("Getting Shape:\n{0}".format(sns)) + pBar.setMaximum(len(shapes)) + QApplication.processEvents() + + for i, shape in enumerate(shapes): + if pBar is not None: + pBar.setLabelText("Getting Shape:\n{0}".format(shape.name)) + pBar.setValue(i) + QApplication.processEvents() + + cmds.setAttr(shape.thing, 1.0) + + if np is not None and mayaToNumpy is not None: + out = mayaToNumpy(meshFn.getRawPoints()) + else: + flatverts = cmds.xform( + "{0}.vtx[*]".format(self.mesh), + translation=1, + query=1, + worldSpace=False, + ) + args = [iter(flatverts)] * 3 + out = list(zip(*args)) + + cmds.setAttr(shape.thing, 0.0) + shape.verts = out + + def getShapeVertices(self, shape): + """ + + Parameters + ---------- + shape : + + Returns + ------- + + """ + with disconnected(self.shapeNode) as cnx: + shapeCnx = cnx[self.shapeNode] + for v in shapeCnx.values(): + cmds.setAttr(v, 0.0) + cmds.setAttr(shape.thing, 1.0) + if np is None: + flatverts = cmds.xform( + "{0}.vtx[*]".format(self.mesh), + translation=1, + query=1, + worldSpace=False, + ) + args = [iter(flatverts)] * 3 + out = list(zip(*args)) + else: + out = self.getNumpyShape(self.mesh) + return out + + def pushAllShapeVertices(self, shapes, pBar=None): + """ + + Parameters + ---------- + shapes : + + pBar : + (Default value = None) + + Returns + ------- + + """ + # take all the verts stored on the shapes + # and push them back to the DCC + for shape in shapes: + self.pushShapeVertices(shape) + + def pushShapeVertices(self, shape): + """ + + Parameters + ---------- + shape : + + + Returns + ------- + + """ + # Push the vertices for a specific shape back to the DCC + pass + + @classmethod + def getMeshTopology(cls, mesh, uvName=None): + """Get the topology of a mesh + + Parameters + ---------- + mesh : object + The DCC Mesh to read + uvName : str, optional + The name of the uv set to read + + Returns + ------- + np.array : + The vertex array + np.array : + The "faces" array + np.array : + The "counts" array + np.array : + The uv positions + np.array : + The "uvFaces" array + """ + # Get the MDagPath from the name of the mesh + sl = om.MSelectionList() + sl.add(mesh) + thing = om.MDagPath() + sl.getDagPath(0, thing) + meshFn = om.MFnMesh(thing) + + vts = om.MPointArray() + meshFn.getPoints(vts, om.MSpace.kObject) + verts = [(vts[i].x, vts[i].y, vts[i].z) for i in range(vts.length())] + + faces = [] + counts = [] + rawUvFaces = [] + + vIdx = om.MIntArray() + + util = om.MScriptUtil() + util.createFromInt(0) + uvIdxPtr = util.asIntPtr() + uArray = om.MFloatArray() + vArray = om.MFloatArray() + meshFn.getUVs(uArray, vArray) + hasUvs = uArray.length() > 0 + + for i in range(meshFn.numPolygons()): + meshFn.getPolygonVertices(i, vIdx) + face = [] + for j in reversed(range(vIdx.length())): + face.append(vIdx[j]) + if hasUvs: + meshFn.getPolygonUVid(i, j, uvIdxPtr) + uvIdx = util.getInt(uvIdxPtr) + if uvIdx >= uArray.length() or uvIdx < 0: + uvIdx = 0 + rawUvFaces.append(uvIdx) + + face = [vIdx[j] for j in reversed(range(vIdx.length()))] + faces.extend(face) + counts.append(vIdx.length()) + + if hasUvs: + uvs = [(uArray[i], vArray[i]) for i in range(len(vArray))] + uvFaces = rawUvFaces + else: + uvs = None + uvFaces = None + + return verts, faces, counts, uvs, uvFaces + + def loadMeshTopology(self): + """ """ + self._faces, self._counts, self._uvs = self.getAbcFaces(self.mesh) + + @classmethod + def getNumpyShape(cls, mesh, world=False): + """Get the np.array shape of the mesh connected to the smpx + + Parameters + ---------- + mesh : str + The name of the maya shape object + world : bool + Whether to get the points in worldspace, or local space + + Returns + ------- + : np.array + The point positions of the mesh + + """ + if np is None or mayaToNumpy is None: + raise RuntimeError("Can't do numpy stuff if its not importable") + vts = cls._getMeshVertices(mesh, world=world) + ret = mayaToNumpy(vts) + return ret[..., :3].copy() + + @classmethod + def _getMeshVertices(cls, mesh, world=False): + """ """ + # Get the MDagPath from the name of the mesh + sl = om.MSelectionList() + sl.add(mesh) + thing = om.MDagPath() + sl.getDagPath(0, thing) + meshFn = om.MFnMesh(thing) + vts = om.MPointArray() + if world: + space = om.MSpace.kWorld + else: + space = om.MSpace.kObject + meshFn.getPoints(vts, space) + return vts + + @classmethod + def _exportAbcVertices(cls, mesh, world=False): + """ """ + if np is None or numpyToImath is None: + vts = cls._getMeshVertices(mesh, world=world) + vertices = V3fArray(vts.length()) + for i in range(vts.length()): + vertices[i] = (vts[i].x, vts[i].y, vts[i].z) + else: + vts = cls.getNumpyShape(mesh, world=world) + vertices = mkSampleVertexPoints(vts) + return vertices + + @classmethod + def getAbcFaces(cls, mesh): + """ """ + # Get the MDagPath from the name of the mesh + sl = om.MSelectionList() + sl.add(mesh) + thing = om.MDagPath() + sl.getDagPath(0, thing) + meshFn = om.MFnMesh(thing) + + faces = [] + faceCounts = [] + # uvArray = [] + uvIdxArray = [] + vIdx = om.MIntArray() + + util = om.MScriptUtil() + util.createFromInt(0) + uvIdxPtr = util.asIntPtr() + uArray = om.MFloatArray() + vArray = om.MFloatArray() + meshFn.getUVs(uArray, vArray) + hasUvs = uArray.length() > 0 + + for i in range(meshFn.numPolygons()): + meshFn.getPolygonVertices(i, vIdx) + face = [] + for j in reversed(range(vIdx.length())): + face.append(vIdx[j]) + if hasUvs: + meshFn.getPolygonUVid(i, j, uvIdxPtr) + uvIdx = util.getInt(uvIdxPtr) + if uvIdx >= uArray.length() or uvIdx < 0: + uvIdx = 0 + uvIdxArray.append(uvIdx) + + face = [vIdx[j] for j in reversed(range(vIdx.length()))] + faces.extend(face) + faceCounts.append(vIdx.length()) + + abcFaceIndices = IntArray(len(faces)) + for i in range(len(faces)): + abcFaceIndices[i] = faces[i] + + abcFaceCounts = IntArray(len(faceCounts)) + for i in range(len(faceCounts)): + abcFaceCounts[i] = faceCounts[i] + + if hasUvs: + abcUVArray = V2fArray(len(uArray)) + for i in range(len(vArray)): + abcUVArray[i] = (uArray[i], vArray[i]) + abcUVIdxArray = UnsignedIntArray(len(uvIdxArray)) + for i in range(len(uvIdxArray)): + abcUVIdxArray[i] = uvIdxArray[i] + uv = OV2fGeomParamSample( + abcUVArray, abcUVIdxArray, GeometryScope.kFacevaryingScope + ) + else: + uv = None + + return abcFaceIndices, abcFaceCounts, uv + + def exportAbc( + self, dccMesh, abcMesh, js, world=False, ensureCorrect=False, pBar=None + ): + """ """ + # export the data to alembic + if dccMesh is None: + dccMesh = self.mesh + + shapeDict = {i.name: i for i in self.simplex.shapes} + + shapeNames = js["shapes"] + if js["encodingVersion"] > 1: + shapeNames = [i["name"] for i in shapeNames] + shapes = [shapeDict[i] for i in shapeNames] + + faces, counts, uvs = self.getAbcFaces(dccMesh) + schema = abcMesh.getSchema() + + if pBar is not None: + pBar.show() + pBar.setMaximum(len(shapes)) + spacerName = "_" * max(list(map(len, shapeNames))) + pBar.setLabelText("Exporting:\n{0}".format(spacerName)) + QApplication.processEvents() + + if ensureCorrect: + # Since this code is used to both export and exportOther + # I only want to ensure that everything is correct only if + # I'm doing a normal export + envelope = cmds.getAttr(self.shapeNode + ".envelope") + cmds.setAttr(self.shapeNode + ".envelope", 1.0) + + with disconnected(self.shapeNode) as cnx: + shapeCnx = cnx[self.shapeNode] + for v in shapeCnx.values(): + cmds.setAttr(v, 0.0) + for i, shape in enumerate(shapes): + if pBar is not None: + pBar.setLabelText("Exporting:\n{0}".format(shape.name)) + pBar.setValue(i) + QApplication.processEvents() + if pBar.wasCanceled(): + return + cmds.setAttr(shape.thing, 1.0) + verts = self._exportAbcVertices(dccMesh, world=world) + if uvs is not None: + abcSample = OPolyMeshSchemaSample(verts, faces, counts, uvs) + else: + abcSample = OPolyMeshSchemaSample(verts, faces, counts) + schema.set(abcSample) + cmds.setAttr(shape.thing, 0.0) + + if ensureCorrect: + cmds.setAttr(self.shapeNode + ".envelope", envelope) + + def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): + """ """ + shapeNames = js["shapes"] + if js["encodingVersion"] > 1: + shapeNames = [i["name"] for i in shapeNames] + + if pBar is not None: + pBar.show() + pBar.setMaximum(len(shapeNames)) + spacerName = "_" * max(list(map(len, shapeNames))) + pBar.setLabelText("Exporting:\n{0}".format(spacerName)) + QApplication.processEvents() + + # Get all the sliderVecs + shapeNames, inVecs, keyIdxs = self.simplex.buildInputVectors() + sliderVecs = [ + [0.0] * len(self.simplex.sliders) for i in range(len(self.simplex.shapes)) + ] + for iv, idx in zip(inVecs, keyIdxs): + sliderVecs[idx] = iv + + # Get all the fully expanded shapes, and the activations per shape + with disconnected(self.op) as allSliderCnx: + sliderCnx = allSliderCnx[self.op] + # zero all slider vals on the op to get the rest shape + for a in sliderCnx.values(): + cmds.setAttr(a, 0.0) + restVerts = self.getNumpyShape(dccMesh, world=world) + + fullShapes = np.zeros((len(self.simplex.shapes), len(restVerts), 3)) + shpValArray = np.zeros((len(self.simplex.shapes), len(self.simplex.shapes))) + for shpIdx, shape in enumerate(self.simplex.shapes): + if pBar is not None: + pBar.setLabelText("Reading Full Shapes:\n{0}".format(shape.name)) + pBar.setValue(shpIdx) + QApplication.processEvents() + if pBar.wasCanceled(): + raise RuntimeError("Cancelled!") + + # Set the full vec for this shape + inVec = sliderVecs[shpIdx] + for vi, vv in enumerate(inVec): + cmds.setAttr(sliderCnx[self.simplex.sliders[vi].thing], vv) + + fullShapes[shpIdx] = self.getNumpyShape(dccMesh, world=world) + + ary = np.array(cmds.getAttr(self.op + ".weights")[0]) + # Get rid of some floating point inaccuracies + ary[np.isclose(ary, 1.0)] = 1.0 + ary[np.isclose(ary, 0.0)] = 0.0 + shpValArray[shpIdx] = ary + + # Figure out what order to build the deltas + # so that the deltas exist when I try to combine them + ctrlOrder = self.simplex.controllersByDepth() + shapeOrder = [pp.shape for ctrl in ctrlOrder for pp in ctrl.prog.pairs] + shapeOrder = [i for i in shapeOrder if not i.isRest] + + # Incrementally Build the numpy array of delta shapes + # build deltaShapeArray as a 2d array because numpy is like 10x faster on 2d arrays + indexByShape = {v: k for k, v in enumerate(self.simplex.shapes)} + deltaShapeArray = np.zeros((len(self.simplex.shapes), len(restVerts) * 3)) + for shpOrderIdx, shape in enumerate(shapeOrder): + if pBar is not None: + pBar.setLabelText("Collapsing to Deltas:\n{0}".format(shape.name)) + pBar.setValue(shpOrderIdx) + QApplication.processEvents() + if pBar.wasCanceled(): + raise RuntimeError("Cancelled!") + + shpIdx = indexByShape[shape] + base = np.dot(shpValArray[shpIdx], deltaShapeArray) + deltaShapeArray[shpIdx] = ( + fullShapes[shpIdx] - restVerts - base.reshape((-1, 3)) + ).flatten() + + # And move that 2d array back into 3d + deltaShapeArray = deltaShapeArray.reshape((len(self.simplex.shapes), -1, 3)) + + # Finally write the outputs + faces, counts, uvs = self.getAbcFaces(dccMesh) + schema = abcMesh.getSchema() + for shpIdx, shape in enumerate(self.simplex.shapes): + if pBar is not None: + pBar.setLabelText("writing:\n{0}".format(shape.name)) + pBar.setValue(shpIdx) + QApplication.processEvents() + if pBar.wasCanceled(): + raise RuntimeError("Cancelled!") + shpVerts = restVerts + deltaShapeArray[shpIdx] + shpVerts = mkSampleVertexPoints(shpVerts) + if uvs is not None: + abcSample = OPolyMeshSchemaSample(shpVerts, faces, counts, uvs) + else: + abcSample = OPolyMeshSchemaSample(shpVerts, faces, counts) + schema.set(abcSample) + + def deleteObj(self, thing): + """Delete the given object""" + cmds.delete(thing) + + def exportMesh(self, mesh, path): + """Export a mesh to the given path""" + faces, counts, uvs = self.getAbcFaces(mesh) + shape = self.getNumpyShape(mesh) + name = mesh.split('|')[-1] + buildAbc(path, shape, faces, counts, uvs, name=name) + + # Revision tracking + def getRevision(self): + """ """ + try: + return cmds.getAttr("{0}.{1}".format(self.op, "revision")) + except ValueError: + # object does not exist + return None + + @undoable + def incrementRevision(self): + """ """ + value = self.getRevision() + if value is None: + return + cmds.setAttr("{0}.{1}".format(self.op, "revision"), value + 1) + jsString = self.simplex.dump() + self.setSimplexString(self.op, jsString) + return value + 1 + + @undoable + def setRevision(self, val): + """ + + Parameters + ---------- + val : + + + Returns + ------- + + """ + cmds.setAttr("{0}.{1}".format(self.op, "revision"), val) + + # System level + @undoable + def renameSystem(self, name): + """ + + Parameters + ---------- + name : + + + Returns + ------- + + """ + if ( + self.mesh is None + or self.ctrl is None + or self.shapeNode is None + or self.op is None + or self.simplex is None + ): + raise ValueError("System is not set up. Cannot rename") + + nn = self.mesh.replace(self.name, name) + self.mesh = cmds.rename(self.mesh, nn) + + nn = self.ctrl.replace(self.name, name) + self.ctrl = cmds.rename(self.ctrl, nn) + + oldNodeName = self.shapeNode + nn = self.shapeNode.replace(self.name, name) + self.shapeNode = cmds.rename(self.shapeNode, nn) + + nn = self.op.replace(self.name, name) + self.op = cmds.rename(self.op, nn) + + for shape in self.simplex.shapes: + shape.thing = shape.thing.replace(oldNodeName, self.shapeNode) + + self.name = name + + @undoable + def deleteSystem(self): + """ """ + cmds.delete(self.ctrl) + cmds.delete(self.shapeNode) + cmds.delete(self.op) + self.ctrl = None # the object that has all the controllers on it + self.shapeNode = None # the deformer object + self.op = None # the simplex object + self.simplex = None + + # Shapes + @undoable + def createShape(self, shape, live=False, offset=10): + """ + + Parameters + ---------- + shape : + + live : + (Default value = False) + offset : + (Default value = 10) + + Returns + ------- + + """ + with disconnected(self.shapeNode): + try: + attrs = cmds.listAttr("{0}.weight[*]".format(self.shapeNode)) + except ValueError: + pass + # Maya throws an error if there aren't any instead of + # just returning an empty list + else: + for attr in attrs: + cmds.setAttr("{0}.{1}".format(self.shapeNode, attr), 0.0) + newShape = cmds.duplicate(self.mesh, name=shape.name)[0] + + cmds.delete(newShape, constructionHistory=True) + index = self._firstAvailableIndex() + cmds.blendShape( + self.shapeNode, edit=True, target=(self.mesh, index, newShape, 1.0) + ) + weightAttr = "{0}.weight[{1}]".format(self.shapeNode, index) + thing = cmds.ls(weightAttr)[0] + + shapeIndex = len(shape.simplex.shapes) - 1 + cmds.connectAttr("{0}.weights[{1}]".format(self.op, shapeIndex), thing) + + if live: + cmds.xform(newShape, relative=True, translation=[offset, 0, 0]) + else: + cmds.delete(newShape) + + return thing + + def _firstAvailableIndex(self): + """ """ + aliases = cmds.aliasAttr(self.shapeNode, query=True) + idxs = set() + if not aliases: + return 0 + for alias in aliases: + match = re.search(r"\[\d+\]", alias) + if not match: + continue # No index found for the current shape + idxs.add(int(match.group().strip("[]"))) + + for i in range(len(idxs) + 1): + if i not in idxs: + return i + # there should be no way to get here, but just in case: + return len(idxs) + 1 + + def getShapeIndex(self, shape): + """ + + Parameters + ---------- + shape : + + + Returns + ------- + + """ + aName = cmds.attributeName(shape.thing) + aliases = cmds.aliasAttr(self.shapeNode, query=True) + idx = aliases.index(aName) + raw = aliases[idx + 1] + matches = re.findall(r"\[\d+\]", raw) + if not matches: + raise IndexError("No index found for the current shape") + return int(matches[-1].strip("[]")) + + @undoable + def extractWithDeltaShape(self, shape, live=True, offset=10.0): + """Make a mesh representing a shape. Can be live or not. + Also, make a shapenode that is the delta of the change being made + + Parameters + ---------- + shape : + + live : + (Default value = True) + offset : + (Default value = 10.0) + + Returns + ------- + + """ + with disconnected(self.shapeNode) as cnx: + shapeCnx = cnx[self.shapeNode] + for v in shapeCnx.values(): + cmds.setAttr(v, 0.0) + + # store the delta shape + delta = cmds.duplicate(self.mesh, name="{0}_Delta".format(shape.name))[0] + + # Extract the shape + cmds.setAttr(shape.thing, 1.0) + extracted = cmds.duplicate( + self.mesh, name="{0}_Extract".format(shape.name) + )[0] + + # Store the initial shape + init = cmds.duplicate(extracted, name="{0}_Init".format(shape.name))[0] + + # clear old orig objects + for item in [delta, extracted, init]: + self._clearShapes(item, doOrig=True) + + # build the deltaObj system + bs = cmds.blendShape(delta, name="{0}_DeltaBS".format(shape.name))[0] + + cmds.blendShape(bs, edit=True, target=(delta, 0, init, 1.0)) + cmds.blendShape(bs, edit=True, target=(delta, 1, extracted, 1.0)) + + cmds.setAttr("{0}.{1}".format(bs, init), -1.0) + cmds.setAttr("{0}.{1}".format(bs, extracted), 1.0) + + # Cleanup + nodeDict = {"Delta": delta, "Init": init} + repDict = self._reparentDeltaShapes(extracted, nodeDict, bs) + + # Shift the extracted shape to the side + cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) + + if live: + self.connectShape(shape, extracted, live, delete=False) + + return extracted, repDict["Delta"] + + @undoable + def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0): + """Extract a shape with a live partial delta added in. + Useful for updating progressive shapes + + Parameters + ---------- + shape : + + delta : + + value : + + live : + (Default value = True) + offset : + (Default value = 10.0) + + Returns + ------- + + """ + with disconnected(self.shapeNode): + for attr in cmds.listAttr("{0}.weight[*]".format(self.shapeNode)): + cmds.setAttr("{0}.{1}".format(self.shapeNode, attr), 0.0) + + # Pull out the rest shape. we will blend this guy to the extraction + extracted = cmds.duplicate( + self.mesh, name="{0}_Extract".format(shape.name) + )[0] + + cmds.setAttr(shape.thing, 1.0) + # Store the initial shape + init = cmds.duplicate(self.mesh, name="{0}_Init".format(shape.name))[0] + + # clear old orig objects + for item in [init, extracted]: + self._clearShapes(item, doOrig=True) + + deltaPar = cmds.listRelatives(delta, parent=True)[0] + + # build the restObj system + cmds.select(clear=True) # 'cause maya + bs = cmds.blendShape(extracted, name="{0}_DeltaBS".format(shape.name))[0] + cmds.blendShape(bs, edit=True, target=(extracted, 0, init, 1.0)) + cmds.blendShape(bs, edit=True, target=(extracted, 1, deltaPar, 1.0)) + + cmds.setAttr("{0}.{1}".format(bs, init), 1.0) + cmds.setAttr("{0}.{1}".format(bs, deltaPar), value) + + outCnx = "{0}.worldMesh[0]".format(delta) + inCnx = "{0}.inputTarget[0].inputTargetGroup[{1}].inputTargetItem[6000].inputGeomTarget".format( + bs, 1 + ) + cmds.connectAttr(outCnx, inCnx, force=True) + cmds.aliasAttr(delta, "{0}.{1}".format(bs, deltaPar)) + + # Cleanup + nodeDict = {"Init": init} + self._reparentDeltaShapes(extracted, nodeDict, bs) + + # Remove the tweak node, otherwise editing the input progressives + # *inverts* the shape + exShape = cmds.listRelatives(extracted, noIntermediate=1, shapes=1)[0] + tweak = cmds.listConnections( + exShape + ".tweakLocation", source=1, destination=0 + ) + if tweak: + cmds.delete(tweak) + + # Shift the extracted shape to the side + cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) + self.connectShape(shape, extracted, live, delete=False) + + return extracted + + @undoable + def extractShape(self, shape, live=True, offset=10.0): + """Make a mesh representing a shape. Can be live or not. + Can also store its starting shape and delta data + + Parameters + ---------- + shape : + + live : + (Default value = True) + offset : + (Default value = 10.0) + + Returns + ------- + + """ + with disconnected(self.shapeNode): + for attr in cmds.listAttr("{0}.weight[*]".format(self.shapeNode)): + cmds.setAttr("{0}.{1}".format(self.shapeNode, attr), 0.0) + + cmds.setAttr(shape.thing, 1.0) + extracted = cmds.duplicate( + self.mesh, name="{0}_Extract".format(shape.name) + )[0] + + # Shift the extracted shape to the side + cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) + if live: + self.connectShape(shape, extracted, live, delete=False) + return extracted + + @undoable + def connectShape(self, shape, mesh=None, live=False, delete=False): + """Force a shape to match a mesh + The "connect shape" button is: + mesh=None, delete=True + The "match shape" button is: + mesh=someMesh, delete=False + There is a possibility of a "make live" button: + live=True, delete=False + + Parameters + ---------- + shape : + + mesh : + (Default value = None) + live : + (Default value = False) + delete : + (Default value = False) + + Returns + ------- + + """ + if mesh is None: + attrName = cmds.attributeName(shape.thing, long=True) + mesh = "{0}_Extract".format(attrName) + + chk = cmds.ls(mesh) + if not chk: + return + if len(chk) > 1: + msg = "Multiple objects with the same name found in file:\n" + msg += '\n'.join(chk) + raise ValueError(msg) + + index = self.getShapeIndex(shape) + tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) + cnx = mesh + "Shape" if cmds.nodeType(mesh) == "transform" else mesh + + outAttr = "{0}.worldMesh[0]".format( + cnx + ) # Make sure to check the right shape object + inAttr = "{0}.inputTargetItem[6000].inputGeomTarget".format(tgn) + + if not cmds.isConnected(outAttr, inAttr): + cmds.connectAttr(outAttr, inAttr, force=True) + + if not live: + cmds.disconnectAttr(outAttr, inAttr) + + if delete: + cmds.delete(mesh) + + @undoable + def extractPosedShape(self, shape): + """ + + Parameters + ---------- + shape : + + + Returns + ------- + + """ + pass + + @undoable + def zeroShape(self, shape): + """Set the shape to be completely zeroed + + Parameters + ---------- + shape : + + + Returns + ------- + + """ + index = self.getShapeIndex(shape) + tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) + shapeInput = "{0}.inputTargetItem[6000]".format(tgn) + cmds.setAttr( + "{0}.inputPointsTarget".format(shapeInput), 0, (), type="pointArray" + ) + cmds.setAttr( + "{0}.inputComponentsTarget".format(shapeInput), 0, "", type="componentList" + ) + + @undoable + def deleteShape(self, toDelShape): + """Remove a shape from the system + + Parameters + ---------- + toDelShape : + + + Returns + ------- + + """ + index = self.getShapeIndex(toDelShape) + tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) + cmds.removeMultiInstance(toDelShape.thing, b=True) + cmds.removeMultiInstance(tgn, b=True) + cmds.aliasAttr(toDelShape.thing, remove=True) + self._rebuildShapeConnections() + + def _rebuildShapeConnections(self): + """ """ + # Rebuild the shape connections in the proper order + cnxs = ( + cmds.listConnections( + self.op, plugs=True, source=False, destination=True, connections=True + ) + or [] + ) + for i, cnx in enumerate(cnxs): + if i % 2 == 0 and cnx.startswith("{0}.weights[".format(self.op)): + cmds.disconnectAttr(cnxs[i], cnxs[i + 1]) + + for i, shape in enumerate(self.simplex.shapes): + cmds.connectAttr( + "{0}.weights[{1}]".format(self.op, i), shape.thing, force=True + ) + + @undoable + def forceRebuildShapeConnections(self): + """ """ + self._rebuildShapeConnections() + + @undoable + def renameShape(self, shape, name): + """Change the name of the shape + + Parameters + ---------- + shape : + + name : + + + Returns + ------- + + """ + cmds.aliasAttr(name, shape.thing) + shape.thing = "{0}.{1}".format(self.shapeNode, name) + + @undoable + def convertShapeToCorrective(self, shape): + """ + + Parameters + ---------- + shape : + + + Returns + ------- + + """ + pass + + # Falloffs + def createFalloff(self, name): + """ + + Parameters + ---------- + name : + + + Returns + ------- + + """ + pass # for eventual live splits + + def duplicateFalloff(self, falloff, newFalloff, newName): + """ + + Parameters + ---------- + falloff : + + newFalloff : + + newName : + + + Returns + ------- + + """ + pass # for eventual live splits + + def deleteFalloff(self, falloff): + """ + + Parameters + ---------- + falloff : + + + Returns + ------- + + """ + pass # for eventual live splits + + def setFalloffData( + self, falloff, splitType, axis, minVal, minHandle, maxHandle, maxVal, mapName + ): + """ + + Parameters + ---------- + falloff : + + splitType : + + axis : + + minVal : + + minHandle : + + maxHandle : + + maxVal : + + mapName : + + + Returns + ------- + + """ + pass # for eventual live splits + + def getFalloffThing(self, falloff): + """ + + Parameters + ---------- + falloff : + + + Returns + ------- + + """ + shape = cmds.listRelatives(self.mesh, shapes=True)[0] + return shape + "." + falloff.name + + # Sliders + @undoable + def createSlider(self, slider): + """ + + Parameters + ---------- + slider : + + + Returns + ------- + + """ + index = slider.simplex.sliders.index(slider) + cmds.addAttr( + self.ctrl, + longName=slider.name, + attributeType="double", + keyable=True, + min=slider.minValue * self.sliderMul, + max=slider.maxValue * self.sliderMul, + ) + thing = "{0}.{1}".format(self.ctrl, slider.name) + cmds.connectAttr(thing, "{0}.sliders[{1}]".format(self.op, index)) + return thing + + @undoable + def renameSlider(self, slider, name): + """Set the name of a slider + + Parameters + ---------- + slider : + + name : + + + Returns + ------- + + """ + vals = [v.value for v in slider.prog.pairs] + cnx = cmds.listConnections( + slider.thing, plugs=True, source=False, destination=True + ) + cmds.deleteAttr(slider.thing) + cmds.addAttr( + self.ctrl, + longName=name, + attributeType="double", + keyable=True, + min=self.sliderMul * min(vals), + max=self.sliderMul * max(vals), + ) + newThing = "{0}.{1}".format(self.ctrl, name) + slider.thing = newThing + for c in cnx: + cmds.connectAttr(newThing, c) + + @undoable + def setSliderRange(self, slider): + """Set the range of a slider + + Parameters + ---------- + slider : + + + Returns + ------- + + """ + vals = [v.value for v in slider.prog.pairs] + attrName = "{0}.{1}".format(self.ctrl, slider.name) + cmds.addAttr( + attrName, + edit=True, + min=self.sliderMul * min(vals), + max=self.sliderMul * max(vals), + ) + + @undoable + def deleteSlider(self, toDelSlider): + """ + + Parameters + ---------- + toDelSlider : + + + Returns + ------- + + """ + cmds.deleteAttr(toDelSlider.thing) + + # Rebuild the slider connections in the proper order + # Get the sliders connections + cnxs = cmds.listConnections( + self.op, plugs=True, source=True, destination=False, connections=True + ) + for i, cnx in enumerate(cnxs): + if cnx.startswith("{0}.sliders".format(self.op)): + cmds.disconnectAttr(cnxs[i + 1], cnxs[i]) + + for i, slider in enumerate(self.simplex.sliders): + cmds.connectAttr(slider.thing, "{0}.sliders[{1}]".format(self.op, i)) + + @undoable + def addProgFalloff(self, prog, falloff): + """ + + Parameters + ---------- + prog : + + falloff : + + + Returns + ------- + + """ + pass # for eventual live splits + + @undoable + def removeProgFalloff(self, prog, falloff): + """ + + Parameters + ---------- + prog : + + falloff : + + + Returns + ------- + + """ + pass # for eventual live splits + + @undoable + def setSlidersWeights(self, sliders, weights): + """Set the weight of a slider. This does not change the definition + + Parameters + ---------- + sliders : + + weights : + + + Returns + ------- + + """ + for slider, weight in zip(sliders, weights): + try: + cmds.setAttr(slider.thing, weight) + except RuntimeError: + # Probably locked or connected. Just skip it + pass + + @undoable + def setSliderWeight(self, slider, weight): + """ + + Parameters + ---------- + slider : + + weight : + + + Returns + ------- + + """ + try: + cmds.setAttr(slider.thing, weight) + except RuntimeError: + # Probably locked or connected. Just skip it + pass + + @undoable + def updateSlidersRange(self, sliders): + """ + + Parameters + ---------- + sliders : + + + Returns + ------- + + """ + for slider in sliders: + vals = [v.value for v in slider.prog.pairs] + cmds.addAttr( + slider.thing, + edit=True, + min=min(vals) * self.sliderMul, + max=max(vals) * self.sliderMul, + ) + + def _doesDeltaExist(self, combo, target): + """ + + Parameters + ---------- + combo : + + target : + + + Returns + ------- + + """ + dshape = "{0}_DeltaShape".format(combo.name) + if not cmds.ls(dshape): + return None + par = cmds.listRelatives(dshape, allParents=1) + if not par: + # there is apparently a transform object with the name + return None + + par = cmds.ls(par[0], absoluteName=1) + tar = cmds.ls(target, absoluteName=1) + + if par != tar: + # the shape exists under a different transform ... ugh + return None + return par + "|" + dshape + + def _clearShapes(self, item, doOrig=False): + """ + + Parameters + ---------- + item : + + doOrig : + (Default value = False) + + Returns + ------- + + """ + aname = cmds.ls(item, long=1)[0] + shapes = cmds.ls(cmds.listRelatives(item, shapes=1), long=1) + baseName = aname.split("|")[-1] + baseName, digits = split_trailing_digits(baseName) + + primary = "{0}|{1}Shape{2}".format(aname, baseName, digits) + + origs = [] + others = [] + for shape in shapes: + base, digits = split_trailing_digits(shape) + if base.endswith('Orig'): + origs.append(shape) + else: + others.append(shape) + + for shape in others: + if shape == primary: + continue + cmds.delete(shape) + + if doOrig: + cmds.delete(origs) + else: + # Don't delete the first orig + if len(origs) > 1: + origs = sorted(origs) + cmds.delete(origs[1:]) + + @undoable + def forceRebuildSliderConnections(self): + """ """ + self._rebuildSliderConnections() + + def _rebuildSliderConnections(self): + # disconnect all outputs from the ctrl + rcnx = cmds.listConnections( + self.ctrl, source=False, plugs=True, connections=True + ) + for i in range(0, len(rcnx), 2): + src, dst = rcnx[i], rcnx[i + 1] + if cmds.getAttr(src, type=True) != "double": + # only disconnect doubles + continue + cmds.disconnectAttr(src, dst) + + # Reconnect by name + for i, sli in enumerate(self.simplex.sliders): + thing = self.getSliderThing(sli.name) + cmds.connectAttr(thing, self.op + ".sliders[{0}]".format(i)) + + # Combos + def _reparentDeltaShapes(self, par, nodeDict, bsNode, toDelete=None): + """Reparent and clean up a single-transform delta system + + Put all the relevant shape nodes from the nodeDict under the par, + and rename the shapes to maya's convention. Then build a callback + to ensure the blendshape node isn't left floating + + par: The parent transform node + nodeDict: A {simpleName: node} dictionary. + bsNode: The blendshape node. + toDelete: Any extra nodes to delte after all the node twiddling + + Parameters + ---------- + par : + + nodeDict : + + bsNode : + + toDelete : + (Default value = None) + + Returns + ------- + + """ + # Get the shapes and origs + shapeDict = {} + origDict = {} + + for name, node in nodeDict.items(): + shape = cmds.listRelatives(node, noIntermediate=1, shapes=1)[0] + shape = cmds.ls(shape, absoluteName=1)[0] + if shape: + shapeDict[name] = shape + + orig = shape + "Orig" + orig = cmds.ls(orig) + if orig: + origDict[name] = orig + + for name in nodeDict: + for d, fmt in [(shapeDict, "{0}Shape{1}"), (origDict, "{0}Shape{1}Orig")]: + shape = d.get(name) + if shape is None: + continue + shapeUUID = cmds.ls(shape, uuid=1)[0] + cmds.parent(shape, par, shape=True, relative=True) + newShape = cmds.rename(cmds.ls(shapeUUID)[0], fmt.format(par, name)) + d[name] = newShape + cmds.setAttr(newShape + ".intermediateObject", 1) + cmds.hide(newShape) + + cmds.delete(nodeDict[name]) + + if toDelete: + cmds.delete(toDelete) + + # Use the simplexDelete message attribute to keep track of what nodes + # will need to be delete-linked when the file is reopened + sdNode = par + ".simplexDelete" + if not cmds.ls(sdNode): + cmds.addAttr(par, longName="simplexDelete", attributeType="message") + cmds.connectAttr(bsNode + ".message", sdNode) + + # build the callback setup so the blendshape is deleted with the delta setup + # along with a persistent scriptjob + buildDeleterCallback(par, bsNode) + buildDeleterScriptJob() + + return shapeDict + + def _createTravDelta(self, trav, target, tVal, doReparent=True): + """Part of the traversal extraction process. + Very similar to the combo extraction + + Parameters + ---------- + trav : + + target : + + tVal : + + + Returns + ------- + + """ + exists = self._doesDeltaExist(trav, target) + if exists is not None: + return exists + + # Traversals *MAY* depend on floaters, but that's complicated + # I'm just gonna ignore them for now + floatShapes = [i.thing for i in self.simplex.getFloatingShapes()] + + # Get all traversal shapes + tShapes = [] + for oTrav in self.simplex.traversals: + tShapes.extend([i.thing for i in oTrav.prog.getShapes()]) + + with disconnected(self.op) as cnx: + sliderCnx = cnx[self.op] + + # zero all slider vals on the op + for a in sliderCnx.values(): + cmds.setAttr(a, 0.0) + + with disconnected(floatShapes + tShapes): + # pull out the rest shape + rest = cmds.duplicate(self.mesh, name="{0}_Rest".format(trav.name))[0] + + sliDict = {} + for pair in trav.startPoint.pairs: + sliDict[pair.slider] = [pair.value] + for pair in trav.endPoint.pairs: + sliDict[pair.slider].append(pair.value) + + for slider, (start, end) in sliDict.items(): + vv = start + tVal * (end - start) + cmds.setAttr(sliderCnx[slider.thing], vv) + + deltaObj = cmds.duplicate( + self.mesh, name="{0}_Delta".format(trav.name) + )[0] + base = cmds.duplicate(deltaObj, name="{0}_Base".format(trav.name))[0] + + # clear out all non-primary shapes so we don't have those 'Orig1' things floating around + for item in [rest, deltaObj, base]: + self._clearShapes(item, doOrig=True) + + # Build the delta blendshape setup + bs = cmds.blendShape(deltaObj, name="{0}_DeltaBS".format(trav.name))[0] + cmds.blendShape(bs, edit=True, target=(deltaObj, 0, target, 1.0)) + cmds.blendShape(bs, edit=True, target=(deltaObj, 1, base, 1.0)) + cmds.blendShape(bs, edit=True, target=(deltaObj, 2, rest, 1.0)) + cmds.setAttr("{0}.{1}".format(bs, target), 1.0) + cmds.setAttr("{0}.{1}".format(bs, base), 1.0) + cmds.setAttr("{0}.{1}".format(bs, rest), 1.0) + + # Cleanup + if doReparent: + nodeDict = {"Delta": deltaObj} + repDict = self._reparentDeltaShapes(target, nodeDict, bs, [rest, base]) + return repDict["Delta"] + return deltaObj + + @undoable + def extractTraversalShape(self, trav, shape, live=True, offset=10.0): + """Extract a shape from a Traversal progression + + Parameters + ---------- + trav : + shape : + live : + (Default value = True) + offset : + (Default value = 10.0) + + Returns + ------- + + """ + floatShapes = self.simplex.getFloatingShapes() + floatShapes = [i.thing for i in floatShapes] + + shapeIdx = trav.prog.getShapeIndex(shape) + val = trav.prog.pairs[shapeIdx].value + + # TODO: Do traversals interact? Should I turn off any other traversals? + # For now, no, but it may be a thing + # tShapes = [] + # for oTrav in self.simplex.traversals: + # if oTrav is trav: continue + # tShapes.extend([i.thing for i in oTrav.prog.getShapes()]) + + with disconnected(self.op) as cnx: + sliderCnx = cnx[self.op] + # zero all slider vals on the op + for a in sliderCnx.values(): + cmds.setAttr(a, 0.0) + + with disconnected(floatShapes): # tShapes + sliDict = {} + for pair in trav.startPoint.pairs: + sliDict[pair.slider] = [pair.value] + for pair in trav.endPoint.pairs: + sliDict[pair.slider].append(pair.value) + + for slider, (start, end) in sliDict.items(): + vv = start + val * (end - start) + cmds.setAttr(sliderCnx[slider.thing], vv) + + extracted = cmds.duplicate( + self.mesh, name="{0}_Extract".format(shape.name) + ) + extracted = extracted[0] + self._clearShapes(extracted) + cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) + if live: + self.connectTraversalShape(trav, shape, extracted, live=live, delete=False) + cmds.select(extracted) + return extracted + + @undoable + def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False): + """Connect a shape into a Traversal progression + + Parameters + ---------- + trav : + + shape : + + mesh : + (Default value = None) + live : + (Default value = True) + delete : + (Default value = False) + + Returns + ------- + + """ + if mesh is None: + attrName = cmds.attributeName(shape.thing, long=True) + mesh = "{0}_Extract".format(attrName) + + chk = cmds.ls(mesh) + if not chk: + return + if len(chk) > 1: + msg = "Multiple objects with the same name found in file:\n" + msg += '\n'.join(chk) + raise ValueError(msg) + + shapeIdx = trav.prog.getShapeIndex(shape) + tVal = trav.prog.pairs[shapeIdx].value + delta = self._createTravDelta(trav, mesh, tVal) + + if live: + self.connectShape(shape, delta, live, delete) + + if delete: + cmds.delete(mesh) + + def _createComboDelta(self, combo, target, tVal, doReparent=True): + """Part of the combo extraction process. + Combo shapes are fixit shapes added on top of any sliders. + This means that the actual combo-shape by itself will not look good by itself, + and that's bad for artist interaction. + So we must create a setup to take the final sculpted shape, and subtract + the any direct slider deformations to get the actual "combo shape" as a delta + It is this delta shape that is then plugged into the system + + Parameters + ---------- + combo : + + target : + + tVal : + + + Returns + ------- + + """ + exists = self._doesDeltaExist(combo, target) + if exists is not None: + return exists + + # get floaters + # As floaters can appear anywhere along any combo, they must + # always be evaluated in isolation. For this reason, we will + # always disconnect all floaters + floatShapes = [i.thing for i in self.simplex.getFloatingShapes()] + + # get my shapes + myShapes = [i.thing for i in combo.prog.getShapes()] + + with disconnected([self.op] + floatShapes + myShapes) as cnx: + sliderCnx = cnx[self.op] + + # zero all slider vals on the op + for a in sliderCnx.values(): + cmds.setAttr(a, 0.0) + + # pull out the rest shape + rest = cmds.duplicate(self.mesh, name="{0}_Rest".format(combo.name))[0] + + # set the combo values + for pair in combo.pairs: + cmds.setAttr(sliderCnx[pair.slider.thing], pair.value * tVal) + + # Get the resulting slider values for later + # weightPairs = [] + # self.shapeNode = None # the deformer object + + deltaObj = cmds.duplicate(self.mesh, name="{0}_Delta".format(combo.name))[0] + base = cmds.duplicate(deltaObj, name="{0}_Base".format(combo.name))[0] + + # clear out all non-primary shapes so we don't have those 'Orig1' things floating around + for item in [rest, deltaObj, base]: + self._clearShapes(item, doOrig=True) + + # Build the delta blendshape setup + bs = cmds.blendShape(deltaObj, name="{0}_DeltaBS".format(combo.name))[0] + cmds.blendShape(bs, edit=True, target=(deltaObj, 0, target, 1.0)) + cmds.blendShape(bs, edit=True, target=(deltaObj, 1, base, 1.0)) + cmds.blendShape(bs, edit=True, target=(deltaObj, 2, rest, 1.0)) + cmds.setAttr("{0}.{1}".format(bs, target), 1.0) + cmds.setAttr("{0}.{1}".format(bs, base), 1.0) + cmds.setAttr("{0}.{1}".format(bs, rest), 1.0) + + # Cleanup + if doReparent: + nodeDict = {"Delta": deltaObj} + repDict = self._reparentDeltaShapes(target, nodeDict, bs, [rest, base]) + return repDict["Delta"] + return deltaObj + + @undoable + def extractComboShape(self, combo, shape, live=True, offset=10.0): + """Extract a shape from a combo progression + + Parameters + ---------- + combo : + + shape : + + live : + (Default value = True) + offset : + (Default value = 10.0) + + Returns + ------- + + """ + floatShapes = self.simplex.getFloatingShapes() + floatShapes = [i.thing for i in floatShapes] + + shapeIdx = combo.prog.getShapeIndex(shape) + tVal = combo.prog.pairs[shapeIdx].value + + with disconnected(self.op) as cnx: + sliderCnx = cnx[self.op] + # zero all slider vals on the op + for a in sliderCnx.values(): + cmds.setAttr(a, 0.0) + + with disconnected(floatShapes): + # set the combo values + for pair in combo.pairs: + cmds.setAttr(sliderCnx[pair.slider.thing], pair.value * tVal) + + extracted = cmds.duplicate( + self.mesh, name="{0}_Extract".format(shape.name) + )[0] + + self._clearShapes(extracted) + cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) + + if live: + self.connectComboShape(combo, shape, extracted, live=live, delete=False) + + cmds.select(extracted) + return extracted + + @undoable + def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): + """Connect a shape into a combo progression + + Parameters + ---------- + combo : + + shape : + + mesh : + (Default value = None) + live : + (Default value = True) + delete : + (Default value = False) + + Returns + ------- + + """ + if mesh is None: + attrName = cmds.attributeName(shape.thing, long=True) + mesh = "{0}_Extract".format(attrName) + + chk = cmds.ls(mesh) + if not chk: + return + if len(chk) > 1: + msg = "Multiple objects with the same name found in file:\n" + msg += '\n'.join(chk) + raise ValueError(msg) + mesh = chk[0] + + progShapeIdx = combo.prog.getShapeIndex(shape) + tVal = combo.prog.pairs[progShapeIdx].value + + delta = self._createComboDelta(combo, mesh, tVal) + if live: + self.connectShape(shape, delta, live, delete) + + if delete: + cmds.delete(mesh) + + @classmethod + def setDisabled(cls, op): + """ + + Parameters + ---------- + op : + + + Returns + ------- + + """ + bss = list(set(cmds.listConnections(op, type="blendShape"))) + helpers = [] + for bs in bss: + prop = "{0}.envelope".format(bs) + val = cmds.getAttr(prop) + cmds.setAttr(prop, 0.0) + if val != 0.0: + helpers.append((prop, val)) + return helpers + + @classmethod + def reEnable(cls, helpers): + """ + + Parameters + ---------- + helpers : + + + Returns + ------- + + """ + for prop, val in helpers: + cmds.setAttr(prop, val) + + @undoable + def renameCombo(self, combo, name): + """Set the name of a Combo + + Parameters + ---------- + combo : + + name : + + + Returns + ------- + + """ + pass + + # Data Access + @classmethod + def getSimplexOperators(cls): + """ """ + return cmds.ls(type="simplex_maya") + + @classmethod + def getSimplexOperatorsByName(cls, name): + """ + + Parameters + ---------- + name : + + + Returns + ------- + type + + + """ + return cmds.ls(name, type="simplex_maya") + + @classmethod + def getSimplexOperatorsOnObject(cls, thing): + """ + + Parameters + ---------- + thing : + + + Returns + ------- + type + + + """ + ops = cmds.ls(type="simplex_maya") + out = [] + for op in ops: + shapeNode = cmds.listConnections( + "{0}.{1}".format(op, "shapeMsg"), source=True, destination=False + ) + if not shapeNode: + continue + + # Now that I've got the connected blendshape node, I can check the deformer history + # to see if I find it. Eventually, I should probably set this up to deal with + # multi-objects, or branched hierarchies. But for now, it works + if shapeNode[0] in (cmds.listHistory(thing, pruneDagObjects=True) or []): + out.append(op) + return out + + @classmethod + def getSimplexString(cls, op): + """ + + Parameters + ---------- + op : + + + Returns + ------- + type + + + """ + return cmds.getAttr(op + ".definition") + + @classmethod + def getSimplexStringOnThing(cls, thing, systemName): + """ + + Parameters + ---------- + thing : + + systemName : + + + Returns + ------- + type + + + """ + ops = DCC.getSimplexOperatorsOnObject(thing) + for op in ops: + js = DCC.getSimplexString(op) + jdict = json.loads(js) + if jdict["systemName"] == systemName: + return js + return None + + @classmethod + def setSimplexString(cls, op, val): + """ + + Parameters + ---------- + op : + + val : + + + Returns + ------- + type + + + """ + return cmds.setAttr(op + ".definition", val, type="string") + + @classmethod + def selectObject(cls, thing): + """Select an object in the DCC + + Parameters + ---------- + thing : + + + Returns + ------- + + """ + cmds.select([thing]) + + def selectCtrl(self): + """Select the system's control object""" + if self.ctrl: + self.selectObject(self.ctrl) + + @classmethod + def getObjectByName(cls, name): + """ + + Parameters + ---------- + name : + + + Returns + ------- + type + + + """ + objs = cmds.ls(name) + if not objs: + return None + if len(objs) > 1: + raise ValueError("Multiple objects with the same name found") + return objs[0] + + @classmethod + def getObjectName(cls, thing): + """ + + Parameters + ---------- + thing : + + + Returns + ------- + type + + + """ + return thing + + @classmethod + def staticUndoOpen(cls): + """ """ + cmds.undoInfo(chunkName="SimplexOperation", openChunk=True) + + @classmethod + def staticUndoClose(cls): + """ """ + cmds.undoInfo(closeChunk=True) + + def undoOpen(self): + """ """ + if self.undoDepth == 0: + self.staticUndoOpen() + self.undoDepth += 1 + + def undoClose(self): + """ """ + self.undoDepth -= 1 + if self.undoDepth == 0: + self.staticUndoClose() + + @classmethod + def getPersistentFalloff(cls, thing): + """ + + Parameters + ---------- + thing : + + + Returns + ------- + + """ + return cls.getObjectName(thing) + + @classmethod + def loadPersistentFalloff(cls, thing): + """ + + Parameters + ---------- + thing : + + + Returns + ------- + + """ + return cls.getObjectByName(thing) + + @classmethod + def getPersistentShape(cls, thing): + """ + + Parameters + ---------- + thing : + + + Returns + ------- + + """ + return cls.getObjectName(thing) + + @classmethod + def loadPersistentShape(cls, thing): + """ + + Parameters + ---------- + thing : + + + Returns + ------- + + """ + return cls.getObjectByName(thing) + + @classmethod + def getPersistentSlider(cls, thing): + """ + + Parameters + ---------- + thing : + + + Returns + ------- + + """ + return cls.getObjectName(thing) + + @classmethod + def loadPersistentSlider(cls, thing): + """ + + Parameters + ---------- + thing : + + + Returns + ------- + + """ + return cls.getObjectByName(thing) + + @classmethod + def getSelectedObjects(cls): + """ """ + # For maya, only return transform nodes + return cmds.ls(sl=True, transforms=True) + + @undoable + def importObj(self, path): + """ + + Parameters + ---------- + path : + + + Returns + ------- + + """ + current = set(cmds.ls(transforms=True)) + cmds.file(path, i=True, type="OBJ", ignoreVersion=True) + new = set(cmds.ls(transforms=True)) + shapes = set(cmds.ls(shapes=True)) + new = new - current - shapes + imp = new.pop() + return imp + + @classmethod + def _getDeformerChain(cls, chkObj): + # Get a deformer chain + memo = [] + while chkObj and chkObj not in memo: + memo.append(chkObj) + + typ = cmds.nodeType(chkObj) + if typ == "mesh": + cnx = cmds.listConnections(chkObj + ".inMesh") or [None] + chkObj = cnx[0] + elif typ == "groupParts": + cnx = cmds.listConnections( + chkObj + ".inputGeometry", destination=False, shapes=True + ) or [None] + chkObj = cnx[0] + else: + cnx = cmds.ls(chkObj, type="geometryFilter") or [None] + chkObj = cnx[0] + if chkObj: # we have a deformer + cnx = cmds.listConnections(chkObj + ".input[0].inputGeometry") or [ + None + ] + chkObj = cnx[0] + return memo + + # Freezing stuff + def primeShapes(self, combo): + """Make sure the upstream shapes of this combo are primed and ready. + Priming here means the deltas are stored and available on the blendshape node + """ + # Maya doesn't populate the delta plugs on the blendshape node unless + # you have a mesh connection while the value for that shape is turned to 1 + upstreams = [] + comboUps = self.simplex.getComboUpstreams(combo) + for u in comboUps: + upstreams.append(u.prog.getShapeAtValue(1.0)) + for pair in combo.pairs: + sli, val = pair.slider, pair.value + upstreams.append(sli.prog.getShapeAtValue(val)) + + with disconnected(self.shapeNode) as cnx: + shapeCnx = cnx[self.shapeNode] + for v in shapeCnx.values(): + cmds.setAttr(v, 0.0) + + for shape in upstreams: + cmds.setAttr(shape.thing, 1.0) + try: + # Make sure to check for any already incoming connections + index = self.getShapeIndex(shape) + tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format( + self.shapeNode, index + ) + isConnected = cmds.listConnections( + tgn, source=True, destination=False + ) + + if not isConnected: + shapeGeo = cmds.duplicate(self.mesh, name=shape.name)[0] + shape.connectShape(mesh=shapeGeo, live=False, delete=True) + + finally: + cmds.setAttr(shape.thing, 0.0) + + def getFreezeThing(self, combo): + # If the blendshape shape has an incoming connection whose shape name + # ends with 'FreezeShape' and the shape's parent is the ctrl + ret = [] + shapes = combo.prog.getShapes() + shapes = [i for i in shapes if not i.isRest] + + shapePlugFmt = ( + ".inputTarget[{meshIdx}].inputTargetGroup[{shapeIdx}].inputTargetItem[6000]" + ) + + for shape in shapes: + shpIdx = self.getShapeIndex(shape) + shpPlug = ( + self.shapeNode + + shapePlugFmt.format(meshIdx=0, shapeIdx=shpIdx) + + ".inputGeomTarget" + ) + + cnx = cmds.listConnections(shpPlug, shapes=True, destination=False) or [] + for cc in cnx: + if not cc.endswith("FreezeShape"): + continue + par = cmds.listRelatives(cc, parent=True) + if par and par[0] == self.ctrl: + # Can't use list history to get the chain because it's a pseudo-cycle + ret.extend(self._getDeformerChain(cc)) + + if ret: + self.primeShapes(combo) + + return ret + + +class SliderDispatch(QtCore.QObject): + valueChanged = Signal() + + def __init__(self, node, parent=None): + super(SliderDispatch, self).__init__(parent) + mObject = getMObject(node) + self.callbackID = om.MNodeMessage.addAttributeChangedCallback( + mObject, self.emitValueChanged + ) + + def emitValueChanged(self, *args, **kwargs): + self.valueChanged.emit() + + def disconnectCallbacks(self): + om.MMessage.removeCallback(self.callbackID) + self.callbackID = None + + def __del__(self): + self.disconnectCallbacks() + + +class Dispatch(QtCore.QObject): + beforeNew = Signal() + afterNew = Signal() + beforeOpen = Signal() + afterOpen = Signal() + undo = Signal() + redo = Signal() + + def __init__(self, parent=None): + super(Dispatch, self).__init__(parent) + self.callbackIDs = [] + self.connectCallbacks() + + def connectCallbacks(self): + if self.callbackIDs: + self.disconnectCallbacks() + + self.callbackIDs.append( + om.MSceneMessage.addCallback( + om.MSceneMessage.kBeforeNew, self.emitBeforeNew + ) + ) + self.callbackIDs.append( + om.MSceneMessage.addCallback(om.MSceneMessage.kAfterNew, self.emitAfterNew) + ) + self.callbackIDs.append( + om.MSceneMessage.addCallback( + om.MSceneMessage.kBeforeOpen, self.emitBeforeOpen + ) + ) + self.callbackIDs.append( + om.MSceneMessage.addCallback( + om.MSceneMessage.kAfterOpen, self.emitAfterOpen + ) + ) + self.callbackIDs.append( + om.MEventMessage.addEventCallback("Undo", self.emitUndo) + ) + self.callbackIDs.append( + om.MEventMessage.addEventCallback("Redo", self.emitRedo) + ) + + def disconnectCallbacks(self): + for i in self.callbackIDs: + om.MMessage.removeCallback(i) + self.callbackIDs = [] + + def emitBeforeNew(self, *args, **kwargs): + self.beforeNew.emit() + + def emitAfterNew(self, *args, **kwargs): + self.afterNew.emit() + + def emitBeforeOpen(self, *args, **kwargs): + self.beforeOpen.emit() + + def emitAfterOpen(self, *args, **kwargs): + self.afterOpen.emit() + + def emitUndo(self, *args, **kwargs): + self.undo.emit() + + def emitRedo(self, *args, **kwargs): + self.redo.emit() + + def __del__(self): + self.disconnectCallbacks() + + +DISPATCH = Dispatch() + + +def rootWindow(): + """Returns the currently active QT main window + Only works for QT UI's like Maya + """ + # for MFC apps there should be no root window + window = None + if QApplication.instance(): + inst = QApplication.instance() + window = inst.activeWindow() + # Ignore QSplashScreen's, they should never be considered the root window. + if isinstance(window, QSplashScreen): + return None + # If the application does not have focus try to find A top level widget + # that doesn't have a parent and is a QMainWindow or QDialog + if window is None: + windows = [] + dialogs = [] + for w in QApplication.instance().topLevelWidgets(): + if w.parent() is None: + if isinstance(w, QMainWindow): + windows.append(w) + elif isinstance(w, QDialog): + dialogs.append(w) + if windows: + window = windows[0] + elif dialogs: + window = dialogs[0] + + # grab the root window + if window: + while True: + parent = window.parent() + if not parent: + break + if isinstance(parent, QSplashScreen): + break + window = parent + + return window + + +SIMPLEX_RESET_SCRIPTJOB = """ +import maya.cmds as cmds +import maya.OpenMaya as om + +def simplexDelCB(node, dgMod, clientData): + xNode, dName = clientData + dNode = getMObject(dName) + if dNode and not dNode.isNull(): + dgMod.deleteNode(dNode) + +def getMObject(name): + selected = om.MSelectionList() + try: + selected.add(name, True) + except RuntimeError: + return None + if selected.isEmpty(): + return None + thing = om.MObject() + selected.getDependNode(0, thing) + return thing + +# get all .simplexDelete message attributes +delAttrs = cmds.ls("*.simplexDelete") +if delAttrs: + # get all their connections + cnx = cmds.listConnections(delAttrs, plugs=True, connections=True, destination=False) + + # Set up the deletion callback + mmIds = [] + for i in range(0, len(cnx), 2): + parName, delName = cmds.ls(cnx[i:i+2], long=True, objectsOnly=True) + pNode = getMObject(parName) + dNode = getMObject(delName) + om.MNodeMessage.addNodeAboutToDeleteCallback(pNode, simplexDelCB, (dNode, delName)) +""" + + +def buildDeleterScriptJob(): + dcbName = "SimplexDeleterCallback" + if not cmds.ls(dcbName): + cmds.scriptNode( + scriptType=1, + beforeScript=SIMPLEX_RESET_SCRIPTJOB, + name=dcbName, + sourceType="python", + ) + + +def simplexDelCB(_node, dgMod, clientData): + _xNode, dName = clientData + dNode = getMObject(dName) + if dNode and not dNode.isNull(): + dgMod.deleteNode(dNode) + + +def getMObject(name): + selected = om.MSelectionList() + try: + selected.add(name, True) + except RuntimeError: + return None + if selected.isEmpty(): + return None + thing = om.MObject() + selected.getDependNode(0, thing) + return thing + + +def buildDeleterCallback(parName, delName): + pNode = getMObject(parName) + dNode = getMObject(delName) + idNum = om.MNodeMessage.addNodeAboutToDeleteCallback( + pNode, simplexDelCB, (dNode, delName) + ) + return idNum diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index a2cfab02..f22b25fb 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -1,1053 +1,1053 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use,too-many-return-statements -import re -from contextlib import contextmanager - -from .items import ( - Combo, - ComboPair, - Group, - ProgPair, - Progression, - Slider, - Traversal, - TravPair, -) -from Qt import IsPyQt5, IsPySide2 -from Qt.QtCore import QAbstractItemModel, QModelIndex, QSortFilterProxyModel, Qt - - -# Hierarchy Helpers -def coerceIndexToType(indexes, typ): - """Get a list of indices of a specific type based on a given index list - Items containing parents of the type fall down to their children - Items containing children of the type climb up to their parents - - Parameters - ---------- - indexes : [QModelIndex, ...] - A list of indexes to coerce - typ : Type - The type to coerce to - - Returns - ------- - [QModelIndex, ...] - The coerced list - """ - targetDepth = typ.classDepth - - children = [] - parents = [] - out = [] - for idx in indexes: - item = idx.model().itemFromIndex(idx) - depth = item.classDepth - if depth < targetDepth: - parents.append(idx) - elif depth > targetDepth: - children.append(idx) - else: - out.append(idx) - - out.extend(coerceIndexToChildType(parents, typ)) - out.extend(coerceIndexToParentType(children, typ)) - out = list(set(out)) - return out - - -def coerceIndexToChildType(indexes, typ): - """Get a list of indices of a specific type based on a given index list - Lists containing parents of the type fall down to their children - - Parameters - ---------- - indexes : [QModelIndex, ...] - A list of indexes to coerce - typ : Type - The type to coerce to - - Returns - ------- - [QModelIndex, ...] - The coerced list - """ - targetDepth = typ.classDepth - out = [] - - for idx in indexes: - model = idx.model() - item = idx.model().itemFromIndex(idx) - if item.classDepth < targetDepth: - # Too high up, grab children - queue = [idx] - depthIdxs = [] - while queue: - checkIdx = queue.pop() - checkItem = checkIdx.model().itemFromIndex(checkIdx) - if checkItem.classDepth < targetDepth: - for row in range(model.rowCount(checkIdx)): - queue.append(model.index(row, 0, checkIdx)) - elif checkItem.classDepth == targetDepth: - depthIdxs.append(checkIdx) - out.extend(depthIdxs) - elif item.classDepth == targetDepth: - out.append(idx) - - out = list(set(out)) - return out - - -def coerceIndexToParentType(indexes, typ): - """Get a list of indices of a specific type based on a given index list - Lists containing children of the type climb up to their parents - - Parameters - ---------- - indexes : [QModelIndex, ...] - A list of indexes to coerce - typ : Type - The type to coerce to - - Returns - ------- - [QModelIndex, ...] - The coerced list - """ - targetDepth = typ.classDepth - out = [] - for idx in indexes: - item = idx.model().itemFromIndex(idx) - depth = item.classDepth - if depth > targetDepth: - parIdx = idx - parItem = parIdx.model().itemFromIndex(parIdx) - while parItem.classDepth > targetDepth: - parIdx = parIdx.parent() - parItem = parIdx.model().itemFromIndex(parIdx) - if parItem.classDepth == targetDepth: - out.append(parIdx) - elif depth == targetDepth: - out.append(idx) - - out = list(set(out)) - return out - - -def coerceIndexToRoots(indexes): - """Get the topmost indexes for each brach in the hierarchy - - Parameters - ---------- - indexes : [QModelIndex, ...] - A list of indexes to coerce - - Returns - ------- - [QModelIndex, ...] - The coerced list - """ - indexes = [i for i in indexes if i.column() == 0] - indexes = sorted( - indexes, key=lambda x: x.model().itemFromIndex(x).classDepth, reverse=True - ) - # Check each item to see if any of it's ancestors - # are in the selection list. If not, it's a root - roots = [] - for idx in indexes: - par = idx.parent() - while par.isValid(): - if par in indexes: - break - par = par.parent() - else: - roots.append(idx) - - return roots - - -# BASE MODEL -class ContextModel(QAbstractItemModel): - """A sub-class of QAbstractItemModel with built-in contextmanagers - that handle calling the begin/end signals for adding/removing/moving/resettting - """ - - @contextmanager - def insertItemManager(self, parent, row=-1): - """ContextManager for inserting items into the model - - Parameters - ---------- - parent : object - The item in the tree that will be the parent - row : int - The row to insert into. Pass -1 to append to the list (Default value = -1) - """ - parIdx = self.indexFromItem(parent) - if row == -1: - row = self.getItemAppendRow(parent) - self.beginInsertRows(parIdx, row, row) - try: - yield - finally: - self.endInsertRows() - - @contextmanager - def removeItemManager(self, item): - """ContextManager for removing items from the model - - Parameters - ---------- - item : object - The item to remove from the model - """ - idx = self.indexFromItem(item) - valid = idx.isValid() - if valid: - parIdx = idx.parent() - self.beginRemoveRows(parIdx, idx.row(), idx.row()) - try: - yield - finally: - if valid: - self.endRemoveRows() - - @contextmanager - def moveItemManager(self, item, destPar, destRow=-1): - """ContextManager for moving items within the model - - Parameters - ---------- - item : object - The item to move in the model - destPar : object - The object that will be the new parent - destRow : int - The row to move to. Pass -1 to move to the end (Default value = -1) - """ - itemIdx = self.indexFromItem(item) - destParIdx = self.indexFromItem(destPar) - handled = False - if itemIdx.isValid() and destParIdx.isValid(): - handled = True - srcParIdx = itemIdx.parent() - row = itemIdx.row() - if destRow == -1: - destRow = self.getItemAppendRow(destPar) - self.beginMoveRows(srcParIdx, row, row, destParIdx, destRow) - try: - yield - finally: - if handled: - self.endMoveRows() - - @contextmanager - def resetModelManager(self): - """ContextManager for resetting the entire model""" - self.beginResetModel() - try: - yield - finally: - self.endResetModel() - - def indexFromItem(self, item, column=0): - """Return the index for the given item - - Parameters - ---------- - item : object - The item to move in the model - column : int - The column to get the index for. Defaults to 0 - - Returns - ------- - QModelIndex - The index of the item - """ - row = self.getItemRow(item) - if row is None: - return QModelIndex() - return self.createIndex(row, column, item) - - def itemFromIndex(self, index): - """Return the item for the given index - - Parameters - ---------- - index : QModelIndex - The index to get the item of - - Returns - ------- - object - The item in the tree - """ - return index.internalPointer() - - def itemDataChanged(self, item): - """Emit the itemDataChanged signal. - - This must be done through this interface because, unfortunately, I can't quite figure out how - to make the empty `roles` list pass properly for Qt5. So I have to change behavior based - on the Qt backend - - Parameters - ---------- - item : object - The object whose data has changed - """ - idx = self.indexFromItem(item) - self.emitDataChanged(idx) - - def _emitDataChangedQt5(self, index): - if index.isValid(): - self.dataChanged.emit(index, index, []) - - def _emitDataChangedQt4(self, index): - if index.isValid(): - self.dataChanged.emit(index, index) - - emitDataChanged = ( - _emitDataChangedQt5 if IsPySide2 or IsPyQt5 else _emitDataChangedQt4 - ) - - -class SimplexModel(ContextModel): - """The base model for all interaction with a simplex system. - All ui interactions with a simplex system must go through this model - Any special requirements, or reorganizations of the trees will only - be implemented as proxy models. - - There will be little documentation for this class, as all methods - are virtual overrides of the underlying Qt class - - Parameters - ---------- - simplex : Simplex - The Simplex system for this model - parent : QObject - The parent for this model - - """ - - def __init__(self, simplex, parent): - super(SimplexModel, self).__init__(parent) - self.simplex = simplex - self.simplex.models.append(self) - - def index(self, row, column, parIndex): - par = parIndex.internalPointer() - child = self.getChildItem(par, row) - if child is None: - return QModelIndex() - return self.createIndex(row, column, child) - - def parent(self, index): - if not index.isValid(): - return QModelIndex() - item = index.internalPointer() - if item is None: - return QModelIndex() - par = self.getParentItem(item) - if par is None: - return QModelIndex() - row = self.getItemRow(par) - if row is None: - return QModelIndex() - return self.createIndex(row, 0, par) - - def rowCount(self, parIndex): - parent = parIndex.internalPointer() - ret = self.getItemRowCount(parent) - return ret - - def columnCount(self, parIndex): - return 3 - - def data(self, index, role): - if not index.isValid(): - return None - item = index.internalPointer() - return self.getItemData(item, index.column(), role) - - def flags(self, index): - if not index.isValid(): - return Qt.ItemFlag.ItemIsEnabled - if index.column() == 0: - item = index.internalPointer() - if isinstance(item, (Slider, Combo, Traversal)): - return ( - Qt.ItemFlag.ItemIsEnabled - | Qt.ItemFlag.ItemIsSelectable - | Qt.ItemFlag.ItemIsEditable - | Qt.ItemFlag.ItemIsUserCheckable - ) - # TODO: make the SHAPES object under a combo or traversal not-editable - return ( - Qt.ItemFlag.ItemIsEnabled - | Qt.ItemFlag.ItemIsSelectable - | Qt.ItemFlag.ItemIsEditable - ) - - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): - if not index.isValid(): - return False - if role == Qt.ItemDataRole.CheckStateRole: - item = index.internalPointer() - if index.column() == 0: - if isinstance(item, (Slider, Combo, Traversal)): - item.enabled = value == Qt.CheckState.Checked - return True - elif role == Qt.ItemDataRole.EditRole: - item = index.internalPointer() - if index.column() == 0: - if isinstance(item, (Group, Slider, Combo, Traversal, ProgPair)): - item.name = value - return True - - elif index.column() == 1: - if isinstance(item, Slider): - item.value = value - elif isinstance(item, ComboPair): - item.value = value - elif isinstance(item, TravPair): - item.value = value - - elif index.column() == 2: - if isinstance(item, ProgPair): - item.value = value - return False - - def headerData(self, section, orientation, role): - if orientation == Qt.Orientation.Horizontal: - if role == Qt.ItemDataRole.DisplayRole: - sects = ("Items", "Slide", "Value") - return sects[section] - return None - - # Methods for dealing with items only - # These will be used to build the indexes - # and will be public for utility needs - def getChildItem(self, parent, row): - if parent is None: - if row == 0: - return self.simplex - else: - return None - return parent.treeChild(row) - - def getItemRow(self, item): - if item is None: - return None - return item.treeRow() - - def getParentItem(self, item): - if item is None: - return None - return item.treeParent() - - def getItemRowCount(self, item): - # Null parent means 1 row that is the simplex object - if item is None: - ret = 1 - else: - ret = item.treeChildCount() - return ret - - def getItemData(self, item, column, role): - if item is None: - return None - - if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): - return item.treeData(column) - - elif role == Qt.ItemDataRole.CheckStateRole: - chk = None - if column == 0: - chk = item.treeChecked() - if chk is not None: - chk = Qt.CheckState.Checked if chk else Qt.CheckState.Unchecked - return chk - elif role == Qt.ItemDataRole.DecorationRole: - if column == 0: - return item.icon() - return None - - def getItemAppendRow(self, item): - if isinstance(item, Combo): - # insert before the special "SHAPES" item - # getItemRowCount returns len(item.pairs) + 1 - return len(item.pairs) - return self.getItemRowCount(item) - - -# VIEW MODELS -class BaseProxyModel(QSortFilterProxyModel): - """Holds the common item/index translation code for my filter models - Again, This is just a concrete implementation of a Qt base class, so - documentation will be lacking - """ - - def __init__(self, model, parent=None): - super(BaseProxyModel, self).__init__(parent) - self.setSourceModel(model) - - def indexFromItem(self, item, column=0): - sourceModel = self.sourceModel() - sourceIndex = sourceModel.indexFromItem(item, column) - return self.mapFromSource(sourceIndex) - - def itemFromIndex(self, index): - sourceModel = self.sourceModel() - sIndex = self.mapToSource(index) - return sourceModel.itemFromIndex(sIndex) - - def invalidate(self): - source = self.sourceModel() - if isinstance(source, QSortFilterProxyModel): - source.invalidate() - super(BaseProxyModel, self).invalidate() - - def invalidateFilter(self): - source = self.sourceModel() - if isinstance(source, QSortFilterProxyModel): - source.invalidateFilter() - super(BaseProxyModel, self).invalidateFilter() - - def filterAcceptsRow(self, sourceRow, sourceParent) -> bool: - return True - - -class SliderModel(BaseProxyModel): - def filterAcceptsRow(self, sourceRow, sourceParent): - sourceIndex = self.sourceModel().index(sourceRow, 0, sourceParent) - if sourceIndex.isValid(): - item = self.sourceModel().itemFromIndex(sourceIndex) - if isinstance(item, Group): - if item.groupType is not Slider: - return False - return super(SliderModel, self).filterAcceptsRow(sourceRow, sourceParent) - - -class ComboModel(BaseProxyModel): - def filterAcceptsRow(self, sourceRow, sourceParent): - sourceIndex = self.sourceModel().index(sourceRow, 0, sourceParent) - if sourceIndex.isValid(): - item = self.sourceModel().itemFromIndex(sourceIndex) - if isinstance(item, Group): - if item.groupType is not Combo: - return False - return super(ComboModel, self).filterAcceptsRow(sourceRow, sourceParent) - - -class TraversalModel(BaseProxyModel): - def filterAcceptsRow(self, sourceRow, sourceParent): - sourceIndex = self.sourceModel().index(sourceRow, 0, sourceParent) - if sourceIndex.isValid(): - item = self.sourceModel().itemFromIndex(sourceIndex) - if isinstance(item, Group): - if item.groupType is not Traversal: - return False - return super(TraversalModel, self).filterAcceptsRow(sourceRow, sourceParent) - - -# FILTER MODELS -class SimplexFilterModel(BaseProxyModel): - """Filter a model based off of a given string - Set the `filterString` object property to filter the model - """ - - def __init__(self, model, parent=None): - super(SimplexFilterModel, self).__init__(model, parent) - self.setSourceModel(model) - self._filterString = [] - self._filterReg = [] - self.isolateList = [] - - @property - def filterString(self): - return " ".join(self._filterString) - - @filterString.setter - def filterString(self, val): - self._filterString = val.split() - - self._filterReg = [] - for sp in self._filterString: - if sp[0] == "*": - self._filterReg.append(re.compile(sp, flags=re.I)) - else: - self._filterReg.append(re.compile(".*?".join(sp), flags=re.I)) - - def filterAcceptsRow(self, sourceRow, sourceParent): - column = 0 # always sort by the first column #column = self.filterKeyColumn() - sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) - if sourceIndex.isValid(): - if self.filterString or self.isolateList: - sourceItem = self.sourceModel().itemFromIndex(sourceIndex) - if isinstance( - sourceItem, (ProgPair, Slider, Combo, ComboPair, Progression) - ): - if not self.checkChildren(sourceItem): - return False - - return super(SimplexFilterModel, self).filterAcceptsRow(sourceRow, sourceParent) - - def matchFilterString(self, itemString): - if not self._filterString: - return True - for reg in self._filterReg: - if reg.search(itemString): - return True - return False - - def matchIsolation(self, itemString): - if self.isolateList: - return itemString in self.isolateList - return True - - def checkChildren(self, sourceItem): - itemString = sourceItem.name - if self.matchFilterString(itemString) and self.matchIsolation(itemString): - return True - - sourceModel = self.sourceModel().sourceModel() - for row in range(sourceModel.getItemRowCount(sourceItem)): - childItem = sourceModel.getChildItem(sourceItem, row) - if childItem is not None: - return self.checkChildren(childItem) - - return False - - -class SliderFilterModel(SimplexFilterModel): - """Hide single shapes under a slider""" - - def __init__(self, model, parent=None): - super(SliderFilterModel, self).__init__(model, parent) - self.requires = [] - self.filterRequiresAny = False - self.filterRequiresAll = False - - self.doFilter = True - - def filterAcceptsRow(self, sourceRow, sourceParent): - # always sort by the first column #column = self.filterKeyColumn() - column = 0 - sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) - if sourceIndex.isValid(): - data = self.sourceModel().itemFromIndex(sourceIndex) - if self.doFilter: - if isinstance(data, ProgPair): - if len(data.prog.pairs) <= 2: - return False - elif data.shape.isRest: - return False - - if (self.filterRequiresAny or self.filterRequiresAll) and self.requires: - # Ignore items that aren't part of the required combos, if requested - if isinstance(data, Slider): - sliGroups = [[i.slider for i in c.pairs] for c in self.requires] - if self.filterRequiresAny: - if not any(data in s for s in sliGroups): - return False - elif self.filterRequiresAll: - if not all(data in s for s in sliGroups): - return False - - return super(SliderFilterModel, self).filterAcceptsRow(sourceRow, sourceParent) - - -class ComboFilterModel(SimplexFilterModel): - """Filter by slider when Show Dependent Combos is checked""" - - def __init__(self, model, parent=None): - super(ComboFilterModel, self).__init__(model, parent) - self.requires = [] - self.filterRequiresAll = False - self.filterRequiresAny = False - self.filterRequiresOnly = False - - self.filterShapes = True - - def filterAcceptsRow(self, sourceRow, sourceParent): - # always sort by the first column #column = self.filterKeyColumn() - column = 0 - sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) - if sourceIndex.isValid(): - data = self.sourceModel().itemFromIndex(sourceIndex) - if self.filterShapes: - # ignore the SHAPE par if there's nothing under there - if isinstance(data, Progression): - if len(data.pairs) <= 2: - return False - # Ignore shape things if requested - if isinstance(data, ProgPair): - if len(data.prog.pairs) <= 2: - return False - elif data.shape.isRest: - return False - if ( - self.filterRequiresAny - or self.filterRequiresAll - or self.filterRequiresOnly - ) and self.requires: - # Ignore items that don't use the required sliders if requested - if isinstance(data, Combo): - sliders = [i.slider for i in data.pairs] - if self.filterRequiresAll: - if not all(r in sliders for r in self.requires): - return False - elif self.filterRequiresAny: - if not any(r in sliders for r in self.requires): - return False - elif self.filterRequiresOnly: - if not all(r in self.requires for r in sliders): - return False - - return super(ComboFilterModel, self).filterAcceptsRow(sourceRow, sourceParent) - - -class TraversalFilterModel(SimplexFilterModel): - """Hide single shapes under a slider""" - - def __init__(self, model, parent=None): - super(TraversalFilterModel, self).__init__(model, parent) - self.doFilter = True - - def filterAcceptsRow(self, sourceRow, sourceParent): - column = 0 # always sort by the first column #column = self.filterKeyColumn() - sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) - if sourceIndex.isValid(): - if self.doFilter: - data = self.sourceModel().itemFromIndex(sourceIndex) - if isinstance(data, ProgPair): - if len(data.prog.pairs) <= 2: - return False - elif data.shape.isRest: - return False - - return super(TraversalFilterModel, self).filterAcceptsRow( - sourceRow, sourceParent - ) - - -# SETTINGS MODELS -class SliderGroupModel(ContextModel): - """A model for displaying Group objects that contain Sliders""" - - def __init__(self, simplex, parent): - super(SliderGroupModel, self).__init__(parent) - self.simplex = simplex - self.simplex.models.append(self) - - def getItemRow(self, item): - try: - idx = self.simplex.sliderGroups.index(item) - except ValueError: - return None - return idx + 1 - - def getItemAppendRow(self, item): - return len(self.simplex.sliderGroups) + 1 - - def index(self, row, column=0, parIndex=None): - parIndex = QModelIndex() if parIndex is None else parIndex - if row <= 0: - return self.createIndex(row, column, None) - try: - falloff = self.simplex.sliderGroups[row - 1] - except IndexError: - return QModelIndex() - return self.createIndex(row, column, falloff) - - def parent(self, index): - return QModelIndex() - - def rowCount(self, parent): - return len(self.simplex.sliderGroups) + 1 - - def columnCount(self, parent): - return 1 - - def data(self, index, role): - if not index.isValid(): - return None - group = index.internalPointer() - if group and role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): - return group.name - return None - - def flags(self, index): - return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable - - def itemFromIndex(self, index): - return index.internalPointer() - - -class FalloffModel(ContextModel): - """A model for displaying Falloff objects connected to Sliders""" - - def __init__(self, simplex, parent): - super(FalloffModel, self).__init__(parent) - self.simplex = simplex - if self.simplex is not None: - self.simplex.falloffModels.append(self) - self.sliders = [] - self._checks = {} - self.line = "" - - def setSliders(self, sliders): - self.beginResetModel() - self.sliders = sliders - self._checks = {} - for slider in self.sliders: - for fo in slider.prog.falloffs: - self._checks.setdefault(fo, []).append(slider) - self.endResetModel() - self.buildLine() - - def buildLine(self): - if not self.sliders: - self.line = "" - return - fulls = [] - partials = [] - for fo in self.simplex.falloffs: - cs = self._getCheckState(fo) - if cs == Qt.CheckState.Checked: - fulls.append(fo.name) - elif cs == Qt.CheckState.PartiallyChecked: - partials.append(fo.name) - if partials: - title = "{0} <<{1}>>".format(",".join(fulls), ",".join(partials)) - else: - title = ",".join(fulls) - self.line = title - - def getItemRow(self, item): - try: - idx = self.simplex.falloffs.index(item) - except ValueError: - return None - except AttributeError: - return None - return idx + 1 - - def getItemAppendRow(self, item): - try: - return len(self.simplex.falloffs) - except AttributeError: - return 0 - - def index(self, row, column=0, parIndex=None): - parIndex = QModelIndex() if parIndex is None else parIndex - if row <= 0: - return self.createIndex(row, column, None) - try: - falloff = self.simplex.falloffs[row - 1] - except IndexError: - return QModelIndex() - except AttributeError: - return QModelIndex() - return self.createIndex(row, column, falloff) - - def parent(self, index): - return QModelIndex() - - def rowCount(self, parent): - try: - return len(self.simplex.falloffs) + 1 - except AttributeError: - return 0 - - def columnCount(self, parent): - return 1 - - def _getCheckState(self, fo): - sli = self._checks.get(fo, []) - if len(sli) == len(self.sliders): - return Qt.CheckState.Checked - elif len(sli) == 0: - return Qt.CheckState.Unchecked - return Qt.CheckState.PartiallyChecked - - def data(self, index, role): - if not index.isValid(): - return None - falloff = index.internalPointer() - if not falloff: - return None - - if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): - return falloff.name - elif role == Qt.ItemDataRole.CheckStateRole: - return self._getCheckState(falloff) - return None - - def setData(self, index, value, role): - if role == Qt.ItemDataRole.CheckStateRole: - fo = index.internalPointer() - if not fo: - return - if value == Qt.CheckState.Checked: - for s in self.sliders: - if fo not in s.prog.falloffs: - s.prog.addFalloff(fo) - self._checks.setdefault(fo, []).append(s) - elif value == Qt.CheckState.Unchecked: - for s in self.sliders: - if fo in s.prog.falloffs: - s.prog.removeFalloff(fo) - if s in self._checks[fo]: - self._checks[fo].remove(s) - self.buildLine() - self.emitDataChanged(index) - return True - return False - - def flags(self, index): - return ( - Qt.ItemFlag.ItemIsEnabled - | Qt.ItemFlag.ItemIsEditable - | Qt.ItemFlag.ItemIsUserCheckable - ) - - def itemFromIndex(self, index): - return index.internalPointer() - - -class FalloffDataModel(ContextModel): - """A model for displaying the data of Falloff objects""" - - def __init__(self, simplex, parent): - super(FalloffDataModel, self).__init__(parent) - self.simplex = simplex - if self.simplex is not None: - self.simplex.falloffModels.append(self) - - def getItemAppendRow(self, item): - try: - return len(self.simplex.falloffs) - except AttributeError: - return 0 - - def index(self, row, column=0, parIndex=None): - parIndex = QModelIndex() if parIndex is None else parIndex - if row < 0: - return QModelIndex() - try: - falloff = self.simplex.falloffs[row] - except IndexError: - return QModelIndex() - except AttributeError: - return QModelIndex() - return self.createIndex(row, column, falloff) - - def parent(self, index): - return QModelIndex() - - def rowCount(self, parent): - try: - return len(self.simplex.falloffs) - except AttributeError: - return 0 - - def columnCount(self, parent): - return 8 - - def data(self, index, role): - if not index.isValid(): - return None - falloff = index.internalPointer() - if not falloff: - return None - - if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): - if index.column() == 0: - return falloff.name - elif index.column() == 1: - st = falloff.splitType - sti = ("planar", "map").index(st.lower()) - return sti - elif index.column() == 2: - ax = falloff.axis - axi = "xyz".index(ax.lower()) - return axi - elif index.column() == 3: - return falloff.maxVal - elif index.column() == 4: - return falloff.maxHandle - elif index.column() == 5: - return falloff.minHandle - elif index.column() == 6: - return falloff.minVal - elif index.column() == 7: - return falloff.mapName - return None - - def setData(self, index, value, role): - if not index.isValid(): - return False - falloff = index.internalPointer() - if not falloff: - return False - if role == Qt.ItemDataRole.EditRole: - if index.column() == 0: - falloff.name = value - elif index.column() == 1: - if value in [0, 1]: - value = ("planar", "map")[value] - falloff.splitType = value - elif index.column() == 2: - if value in [0, 1, 2]: - value = "XYZ"[value] - falloff.axis = value - elif index.column() == 3: - falloff.maxVal = value - elif index.column() == 4: - falloff.maxHandle = value - elif index.column() == 5: - falloff.minHandle = value - elif index.column() == 6: - falloff.minVal = value - elif index.column() == 7: - falloff.mapName = value - return True - return False - - def flags(self, index): - return ( - Qt.ItemFlag.ItemIsEnabled - | Qt.ItemFlag.ItemIsEditable - | Qt.ItemFlag.ItemIsSelectable - ) - - def itemFromIndex(self, index): - return index.internalPointer() - - def getItemRow(self, item): - try: - idx = self.simplex.falloffs.index(item) - except ValueError: - return None - except AttributeError: - return None - return idx +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint:disable=missing-docstring,unused-argument,no-self-use,too-many-return-statements +import re +from contextlib import contextmanager + +from .items import ( + Combo, + ComboPair, + Group, + ProgPair, + Progression, + Slider, + Traversal, + TravPair, +) +from Qt import IsPyQt5, IsPySide2 +from Qt.QtCore import QAbstractItemModel, QModelIndex, QSortFilterProxyModel, Qt + + +# Hierarchy Helpers +def coerceIndexToType(indexes, typ): + """Get a list of indices of a specific type based on a given index list + Items containing parents of the type fall down to their children + Items containing children of the type climb up to their parents + + Parameters + ---------- + indexes : [QModelIndex, ...] + A list of indexes to coerce + typ : Type + The type to coerce to + + Returns + ------- + [QModelIndex, ...] + The coerced list + """ + targetDepth = typ.classDepth + + children = [] + parents = [] + out = [] + for idx in indexes: + item = idx.model().itemFromIndex(idx) + depth = item.classDepth + if depth < targetDepth: + parents.append(idx) + elif depth > targetDepth: + children.append(idx) + else: + out.append(idx) + + out.extend(coerceIndexToChildType(parents, typ)) + out.extend(coerceIndexToParentType(children, typ)) + out = list(set(out)) + return out + + +def coerceIndexToChildType(indexes, typ): + """Get a list of indices of a specific type based on a given index list + Lists containing parents of the type fall down to their children + + Parameters + ---------- + indexes : [QModelIndex, ...] + A list of indexes to coerce + typ : Type + The type to coerce to + + Returns + ------- + [QModelIndex, ...] + The coerced list + """ + targetDepth = typ.classDepth + out = [] + + for idx in indexes: + model = idx.model() + item = idx.model().itemFromIndex(idx) + if item.classDepth < targetDepth: + # Too high up, grab children + queue = [idx] + depthIdxs = [] + while queue: + checkIdx = queue.pop() + checkItem = checkIdx.model().itemFromIndex(checkIdx) + if checkItem.classDepth < targetDepth: + for row in range(model.rowCount(checkIdx)): + queue.append(model.index(row, 0, checkIdx)) + elif checkItem.classDepth == targetDepth: + depthIdxs.append(checkIdx) + out.extend(depthIdxs) + elif item.classDepth == targetDepth: + out.append(idx) + + out = list(set(out)) + return out + + +def coerceIndexToParentType(indexes, typ): + """Get a list of indices of a specific type based on a given index list + Lists containing children of the type climb up to their parents + + Parameters + ---------- + indexes : [QModelIndex, ...] + A list of indexes to coerce + typ : Type + The type to coerce to + + Returns + ------- + [QModelIndex, ...] + The coerced list + """ + targetDepth = typ.classDepth + out = [] + for idx in indexes: + item = idx.model().itemFromIndex(idx) + depth = item.classDepth + if depth > targetDepth: + parIdx = idx + parItem = parIdx.model().itemFromIndex(parIdx) + while parItem.classDepth > targetDepth: + parIdx = parIdx.parent() + parItem = parIdx.model().itemFromIndex(parIdx) + if parItem.classDepth == targetDepth: + out.append(parIdx) + elif depth == targetDepth: + out.append(idx) + + out = list(set(out)) + return out + + +def coerceIndexToRoots(indexes): + """Get the topmost indexes for each brach in the hierarchy + + Parameters + ---------- + indexes : [QModelIndex, ...] + A list of indexes to coerce + + Returns + ------- + [QModelIndex, ...] + The coerced list + """ + indexes = [i for i in indexes if i.column() == 0] + indexes = sorted( + indexes, key=lambda x: x.model().itemFromIndex(x).classDepth, reverse=True + ) + # Check each item to see if any of it's ancestors + # are in the selection list. If not, it's a root + roots = [] + for idx in indexes: + par = idx.parent() + while par.isValid(): + if par in indexes: + break + par = par.parent() + else: + roots.append(idx) + + return roots + + +# BASE MODEL +class ContextModel(QAbstractItemModel): + """A sub-class of QAbstractItemModel with built-in contextmanagers + that handle calling the begin/end signals for adding/removing/moving/resettting + """ + + @contextmanager + def insertItemManager(self, parent, row=-1): + """ContextManager for inserting items into the model + + Parameters + ---------- + parent : object + The item in the tree that will be the parent + row : int + The row to insert into. Pass -1 to append to the list (Default value = -1) + """ + parIdx = self.indexFromItem(parent) + if row == -1: + row = self.getItemAppendRow(parent) + self.beginInsertRows(parIdx, row, row) + try: + yield + finally: + self.endInsertRows() + + @contextmanager + def removeItemManager(self, item): + """ContextManager for removing items from the model + + Parameters + ---------- + item : object + The item to remove from the model + """ + idx = self.indexFromItem(item) + valid = idx.isValid() + if valid: + parIdx = idx.parent() + self.beginRemoveRows(parIdx, idx.row(), idx.row()) + try: + yield + finally: + if valid: + self.endRemoveRows() + + @contextmanager + def moveItemManager(self, item, destPar, destRow=-1): + """ContextManager for moving items within the model + + Parameters + ---------- + item : object + The item to move in the model + destPar : object + The object that will be the new parent + destRow : int + The row to move to. Pass -1 to move to the end (Default value = -1) + """ + itemIdx = self.indexFromItem(item) + destParIdx = self.indexFromItem(destPar) + handled = False + if itemIdx.isValid() and destParIdx.isValid(): + handled = True + srcParIdx = itemIdx.parent() + row = itemIdx.row() + if destRow == -1: + destRow = self.getItemAppendRow(destPar) + self.beginMoveRows(srcParIdx, row, row, destParIdx, destRow) + try: + yield + finally: + if handled: + self.endMoveRows() + + @contextmanager + def resetModelManager(self): + """ContextManager for resetting the entire model""" + self.beginResetModel() + try: + yield + finally: + self.endResetModel() + + def indexFromItem(self, item, column=0): + """Return the index for the given item + + Parameters + ---------- + item : object + The item to move in the model + column : int + The column to get the index for. Defaults to 0 + + Returns + ------- + QModelIndex + The index of the item + """ + row = self.getItemRow(item) + if row is None: + return QModelIndex() + return self.createIndex(row, column, item) + + def itemFromIndex(self, index): + """Return the item for the given index + + Parameters + ---------- + index : QModelIndex + The index to get the item of + + Returns + ------- + object + The item in the tree + """ + return index.internalPointer() + + def itemDataChanged(self, item): + """Emit the itemDataChanged signal. + + This must be done through this interface because, unfortunately, I can't quite figure out how + to make the empty `roles` list pass properly for Qt5. So I have to change behavior based + on the Qt backend + + Parameters + ---------- + item : object + The object whose data has changed + """ + idx = self.indexFromItem(item) + self.emitDataChanged(idx) + + def _emitDataChangedQt5(self, index): + if index.isValid(): + self.dataChanged.emit(index, index, []) + + def _emitDataChangedQt4(self, index): + if index.isValid(): + self.dataChanged.emit(index, index) + + emitDataChanged = ( + _emitDataChangedQt5 if IsPySide2 or IsPyQt5 else _emitDataChangedQt4 + ) + + +class SimplexModel(ContextModel): + """The base model for all interaction with a simplex system. + All ui interactions with a simplex system must go through this model + Any special requirements, or reorganizations of the trees will only + be implemented as proxy models. + + There will be little documentation for this class, as all methods + are virtual overrides of the underlying Qt class + + Parameters + ---------- + simplex : Simplex + The Simplex system for this model + parent : QObject + The parent for this model + + """ + + def __init__(self, simplex, parent): + super(SimplexModel, self).__init__(parent) + self.simplex = simplex + self.simplex.models.append(self) + + def index(self, row, column, parIndex): + par = parIndex.internalPointer() + child = self.getChildItem(par, row) + if child is None: + return QModelIndex() + return self.createIndex(row, column, child) + + def parent(self, index): + if not index.isValid(): + return QModelIndex() + item = index.internalPointer() + if item is None: + return QModelIndex() + par = self.getParentItem(item) + if par is None: + return QModelIndex() + row = self.getItemRow(par) + if row is None: + return QModelIndex() + return self.createIndex(row, 0, par) + + def rowCount(self, parIndex): + parent = parIndex.internalPointer() + ret = self.getItemRowCount(parent) + return ret + + def columnCount(self, parIndex): + return 3 + + def data(self, index, role): + if not index.isValid(): + return None + item = index.internalPointer() + return self.getItemData(item, index.column(), role) + + def flags(self, index): + if not index.isValid(): + return Qt.ItemFlag.ItemIsEnabled + if index.column() == 0: + item = index.internalPointer() + if isinstance(item, (Slider, Combo, Traversal)): + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsEditable + | Qt.ItemFlag.ItemIsUserCheckable + ) + # TODO: make the SHAPES object under a combo or traversal not-editable + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsEditable + ) + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + if not index.isValid(): + return False + if role == Qt.ItemDataRole.CheckStateRole: + item = index.internalPointer() + if index.column() == 0: + if isinstance(item, (Slider, Combo, Traversal)): + item.enabled = value == Qt.CheckState.Checked + return True + elif role == Qt.ItemDataRole.EditRole: + item = index.internalPointer() + if index.column() == 0: + if isinstance(item, (Group, Slider, Combo, Traversal, ProgPair)): + item.name = value + return True + + elif index.column() == 1: + if isinstance(item, Slider): + item.value = value + elif isinstance(item, ComboPair): + item.value = value + elif isinstance(item, TravPair): + item.value = value + + elif index.column() == 2: + if isinstance(item, ProgPair): + item.value = value + return False + + def headerData(self, section, orientation, role): + if orientation == Qt.Orientation.Horizontal: + if role == Qt.ItemDataRole.DisplayRole: + sects = ("Items", "Slide", "Value") + return sects[section] + return None + + # Methods for dealing with items only + # These will be used to build the indexes + # and will be public for utility needs + def getChildItem(self, parent, row): + if parent is None: + if row == 0: + return self.simplex + else: + return None + return parent.treeChild(row) + + def getItemRow(self, item): + if item is None: + return None + return item.treeRow() + + def getParentItem(self, item): + if item is None: + return None + return item.treeParent() + + def getItemRowCount(self, item): + # Null parent means 1 row that is the simplex object + if item is None: + ret = 1 + else: + ret = item.treeChildCount() + return ret + + def getItemData(self, item, column, role): + if item is None: + return None + + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): + return item.treeData(column) + + elif role == Qt.ItemDataRole.CheckStateRole: + chk = None + if column == 0: + chk = item.treeChecked() + if chk is not None: + chk = Qt.CheckState.Checked if chk else Qt.CheckState.Unchecked + return chk + elif role == Qt.ItemDataRole.DecorationRole: + if column == 0: + return item.icon() + return None + + def getItemAppendRow(self, item): + if isinstance(item, Combo): + # insert before the special "SHAPES" item + # getItemRowCount returns len(item.pairs) + 1 + return len(item.pairs) + return self.getItemRowCount(item) + + +# VIEW MODELS +class BaseProxyModel(QSortFilterProxyModel): + """Holds the common item/index translation code for my filter models + Again, This is just a concrete implementation of a Qt base class, so + documentation will be lacking + """ + + def __init__(self, model, parent=None): + super(BaseProxyModel, self).__init__(parent) + self.setSourceModel(model) + + def indexFromItem(self, item, column=0): + sourceModel = self.sourceModel() + sourceIndex = sourceModel.indexFromItem(item, column) + return self.mapFromSource(sourceIndex) + + def itemFromIndex(self, index): + sourceModel = self.sourceModel() + sIndex = self.mapToSource(index) + return sourceModel.itemFromIndex(sIndex) + + def invalidate(self): + source = self.sourceModel() + if isinstance(source, QSortFilterProxyModel): + source.invalidate() + super(BaseProxyModel, self).invalidate() + + def invalidateFilter(self): + source = self.sourceModel() + if isinstance(source, QSortFilterProxyModel): + source.invalidateFilter() + super(BaseProxyModel, self).invalidateFilter() + + def filterAcceptsRow(self, sourceRow, sourceParent) -> bool: + return True + + +class SliderModel(BaseProxyModel): + def filterAcceptsRow(self, sourceRow, sourceParent): + sourceIndex = self.sourceModel().index(sourceRow, 0, sourceParent) + if sourceIndex.isValid(): + item = self.sourceModel().itemFromIndex(sourceIndex) + if isinstance(item, Group): + if item.groupType is not Slider: + return False + return super(SliderModel, self).filterAcceptsRow(sourceRow, sourceParent) + + +class ComboModel(BaseProxyModel): + def filterAcceptsRow(self, sourceRow, sourceParent): + sourceIndex = self.sourceModel().index(sourceRow, 0, sourceParent) + if sourceIndex.isValid(): + item = self.sourceModel().itemFromIndex(sourceIndex) + if isinstance(item, Group): + if item.groupType is not Combo: + return False + return super(ComboModel, self).filterAcceptsRow(sourceRow, sourceParent) + + +class TraversalModel(BaseProxyModel): + def filterAcceptsRow(self, sourceRow, sourceParent): + sourceIndex = self.sourceModel().index(sourceRow, 0, sourceParent) + if sourceIndex.isValid(): + item = self.sourceModel().itemFromIndex(sourceIndex) + if isinstance(item, Group): + if item.groupType is not Traversal: + return False + return super(TraversalModel, self).filterAcceptsRow(sourceRow, sourceParent) + + +# FILTER MODELS +class SimplexFilterModel(BaseProxyModel): + """Filter a model based off of a given string + Set the `filterString` object property to filter the model + """ + + def __init__(self, model, parent=None): + super(SimplexFilterModel, self).__init__(model, parent) + self.setSourceModel(model) + self._filterString = [] + self._filterReg = [] + self.isolateList = [] + + @property + def filterString(self): + return " ".join(self._filterString) + + @filterString.setter + def filterString(self, val): + self._filterString = val.split() + + self._filterReg = [] + for sp in self._filterString: + if sp[0] == "*": + self._filterReg.append(re.compile(sp, flags=re.I)) + else: + self._filterReg.append(re.compile(".*?".join(sp), flags=re.I)) + + def filterAcceptsRow(self, sourceRow, sourceParent): + column = 0 # always sort by the first column #column = self.filterKeyColumn() + sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) + if sourceIndex.isValid(): + if self.filterString or self.isolateList: + sourceItem = self.sourceModel().itemFromIndex(sourceIndex) + if isinstance( + sourceItem, (ProgPair, Slider, Combo, ComboPair, Progression) + ): + if not self.checkChildren(sourceItem): + return False + + return super(SimplexFilterModel, self).filterAcceptsRow(sourceRow, sourceParent) + + def matchFilterString(self, itemString): + if not self._filterString: + return True + for reg in self._filterReg: + if reg.search(itemString): + return True + return False + + def matchIsolation(self, itemString): + if self.isolateList: + return itemString in self.isolateList + return True + + def checkChildren(self, sourceItem): + itemString = sourceItem.name + if self.matchFilterString(itemString) and self.matchIsolation(itemString): + return True + + sourceModel = self.sourceModel().sourceModel() + for row in range(sourceModel.getItemRowCount(sourceItem)): + childItem = sourceModel.getChildItem(sourceItem, row) + if childItem is not None: + return self.checkChildren(childItem) + + return False + + +class SliderFilterModel(SimplexFilterModel): + """Hide single shapes under a slider""" + + def __init__(self, model, parent=None): + super(SliderFilterModel, self).__init__(model, parent) + self.requires = [] + self.filterRequiresAny = False + self.filterRequiresAll = False + + self.doFilter = True + + def filterAcceptsRow(self, sourceRow, sourceParent): + # always sort by the first column #column = self.filterKeyColumn() + column = 0 + sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) + if sourceIndex.isValid(): + data = self.sourceModel().itemFromIndex(sourceIndex) + if self.doFilter: + if isinstance(data, ProgPair): + if len(data.prog.pairs) <= 2: + return False + elif data.shape.isRest: + return False + + if (self.filterRequiresAny or self.filterRequiresAll) and self.requires: + # Ignore items that aren't part of the required combos, if requested + if isinstance(data, Slider): + sliGroups = [[i.slider for i in c.pairs] for c in self.requires] + if self.filterRequiresAny: + if not any(data in s for s in sliGroups): + return False + elif self.filterRequiresAll: + if not all(data in s for s in sliGroups): + return False + + return super(SliderFilterModel, self).filterAcceptsRow(sourceRow, sourceParent) + + +class ComboFilterModel(SimplexFilterModel): + """Filter by slider when Show Dependent Combos is checked""" + + def __init__(self, model, parent=None): + super(ComboFilterModel, self).__init__(model, parent) + self.requires = [] + self.filterRequiresAll = False + self.filterRequiresAny = False + self.filterRequiresOnly = False + + self.filterShapes = True + + def filterAcceptsRow(self, sourceRow, sourceParent): + # always sort by the first column #column = self.filterKeyColumn() + column = 0 + sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) + if sourceIndex.isValid(): + data = self.sourceModel().itemFromIndex(sourceIndex) + if self.filterShapes: + # ignore the SHAPE par if there's nothing under there + if isinstance(data, Progression): + if len(data.pairs) <= 2: + return False + # Ignore shape things if requested + if isinstance(data, ProgPair): + if len(data.prog.pairs) <= 2: + return False + elif data.shape.isRest: + return False + if ( + self.filterRequiresAny + or self.filterRequiresAll + or self.filterRequiresOnly + ) and self.requires: + # Ignore items that don't use the required sliders if requested + if isinstance(data, Combo): + sliders = [i.slider for i in data.pairs] + if self.filterRequiresAll: + if not all(r in sliders for r in self.requires): + return False + elif self.filterRequiresAny: + if not any(r in sliders for r in self.requires): + return False + elif self.filterRequiresOnly: + if not all(r in self.requires for r in sliders): + return False + + return super(ComboFilterModel, self).filterAcceptsRow(sourceRow, sourceParent) + + +class TraversalFilterModel(SimplexFilterModel): + """Hide single shapes under a slider""" + + def __init__(self, model, parent=None): + super(TraversalFilterModel, self).__init__(model, parent) + self.doFilter = True + + def filterAcceptsRow(self, sourceRow, sourceParent): + column = 0 # always sort by the first column #column = self.filterKeyColumn() + sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) + if sourceIndex.isValid(): + if self.doFilter: + data = self.sourceModel().itemFromIndex(sourceIndex) + if isinstance(data, ProgPair): + if len(data.prog.pairs) <= 2: + return False + elif data.shape.isRest: + return False + + return super(TraversalFilterModel, self).filterAcceptsRow( + sourceRow, sourceParent + ) + + +# SETTINGS MODELS +class SliderGroupModel(ContextModel): + """A model for displaying Group objects that contain Sliders""" + + def __init__(self, simplex, parent): + super(SliderGroupModel, self).__init__(parent) + self.simplex = simplex + self.simplex.models.append(self) + + def getItemRow(self, item): + try: + idx = self.simplex.sliderGroups.index(item) + except ValueError: + return None + return idx + 1 + + def getItemAppendRow(self, item): + return len(self.simplex.sliderGroups) + 1 + + def index(self, row, column=0, parIndex=None): + parIndex = QModelIndex() if parIndex is None else parIndex + if row <= 0: + return self.createIndex(row, column, None) + try: + falloff = self.simplex.sliderGroups[row - 1] + except IndexError: + return QModelIndex() + return self.createIndex(row, column, falloff) + + def parent(self, index): + return QModelIndex() + + def rowCount(self, parent): + return len(self.simplex.sliderGroups) + 1 + + def columnCount(self, parent): + return 1 + + def data(self, index, role): + if not index.isValid(): + return None + group = index.internalPointer() + if group and role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): + return group.name + return None + + def flags(self, index): + return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable + + def itemFromIndex(self, index): + return index.internalPointer() + + +class FalloffModel(ContextModel): + """A model for displaying Falloff objects connected to Sliders""" + + def __init__(self, simplex, parent): + super(FalloffModel, self).__init__(parent) + self.simplex = simplex + if self.simplex is not None: + self.simplex.falloffModels.append(self) + self.sliders = [] + self._checks = {} + self.line = "" + + def setSliders(self, sliders): + self.beginResetModel() + self.sliders = sliders + self._checks = {} + for slider in self.sliders: + for fo in slider.prog.falloffs: + self._checks.setdefault(fo, []).append(slider) + self.endResetModel() + self.buildLine() + + def buildLine(self): + if not self.sliders: + self.line = "" + return + fulls = [] + partials = [] + for fo in self.simplex.falloffs: + cs = self._getCheckState(fo) + if cs == Qt.CheckState.Checked: + fulls.append(fo.name) + elif cs == Qt.CheckState.PartiallyChecked: + partials.append(fo.name) + if partials: + title = "{0} <<{1}>>".format(",".join(fulls), ",".join(partials)) + else: + title = ",".join(fulls) + self.line = title + + def getItemRow(self, item): + try: + idx = self.simplex.falloffs.index(item) + except ValueError: + return None + except AttributeError: + return None + return idx + 1 + + def getItemAppendRow(self, item): + try: + return len(self.simplex.falloffs) + except AttributeError: + return 0 + + def index(self, row, column=0, parIndex=None): + parIndex = QModelIndex() if parIndex is None else parIndex + if row <= 0: + return self.createIndex(row, column, None) + try: + falloff = self.simplex.falloffs[row - 1] + except IndexError: + return QModelIndex() + except AttributeError: + return QModelIndex() + return self.createIndex(row, column, falloff) + + def parent(self, index): + return QModelIndex() + + def rowCount(self, parent): + try: + return len(self.simplex.falloffs) + 1 + except AttributeError: + return 0 + + def columnCount(self, parent): + return 1 + + def _getCheckState(self, fo): + sli = self._checks.get(fo, []) + if len(sli) == len(self.sliders): + return Qt.CheckState.Checked + elif len(sli) == 0: + return Qt.CheckState.Unchecked + return Qt.CheckState.PartiallyChecked + + def data(self, index, role): + if not index.isValid(): + return None + falloff = index.internalPointer() + if not falloff: + return None + + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): + return falloff.name + elif role == Qt.ItemDataRole.CheckStateRole: + return self._getCheckState(falloff) + return None + + def setData(self, index, value, role): + if role == Qt.ItemDataRole.CheckStateRole: + fo = index.internalPointer() + if not fo: + return + if value == Qt.CheckState.Checked: + for s in self.sliders: + if fo not in s.prog.falloffs: + s.prog.addFalloff(fo) + self._checks.setdefault(fo, []).append(s) + elif value == Qt.CheckState.Unchecked: + for s in self.sliders: + if fo in s.prog.falloffs: + s.prog.removeFalloff(fo) + if s in self._checks[fo]: + self._checks[fo].remove(s) + self.buildLine() + self.emitDataChanged(index) + return True + return False + + def flags(self, index): + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsEditable + | Qt.ItemFlag.ItemIsUserCheckable + ) + + def itemFromIndex(self, index): + return index.internalPointer() + + +class FalloffDataModel(ContextModel): + """A model for displaying the data of Falloff objects""" + + def __init__(self, simplex, parent): + super(FalloffDataModel, self).__init__(parent) + self.simplex = simplex + if self.simplex is not None: + self.simplex.falloffModels.append(self) + + def getItemAppendRow(self, item): + try: + return len(self.simplex.falloffs) + except AttributeError: + return 0 + + def index(self, row, column=0, parIndex=None): + parIndex = QModelIndex() if parIndex is None else parIndex + if row < 0: + return QModelIndex() + try: + falloff = self.simplex.falloffs[row] + except IndexError: + return QModelIndex() + except AttributeError: + return QModelIndex() + return self.createIndex(row, column, falloff) + + def parent(self, index): + return QModelIndex() + + def rowCount(self, parent): + try: + return len(self.simplex.falloffs) + except AttributeError: + return 0 + + def columnCount(self, parent): + return 8 + + def data(self, index, role): + if not index.isValid(): + return None + falloff = index.internalPointer() + if not falloff: + return None + + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): + if index.column() == 0: + return falloff.name + elif index.column() == 1: + st = falloff.splitType + sti = ("planar", "map").index(st.lower()) + return sti + elif index.column() == 2: + ax = falloff.axis + axi = "xyz".index(ax.lower()) + return axi + elif index.column() == 3: + return falloff.maxVal + elif index.column() == 4: + return falloff.maxHandle + elif index.column() == 5: + return falloff.minHandle + elif index.column() == 6: + return falloff.minVal + elif index.column() == 7: + return falloff.mapName + return None + + def setData(self, index, value, role): + if not index.isValid(): + return False + falloff = index.internalPointer() + if not falloff: + return False + if role == Qt.ItemDataRole.EditRole: + if index.column() == 0: + falloff.name = value + elif index.column() == 1: + if value in [0, 1]: + value = ("planar", "map")[value] + falloff.splitType = value + elif index.column() == 2: + if value in [0, 1, 2]: + value = "XYZ"[value] + falloff.axis = value + elif index.column() == 3: + falloff.maxVal = value + elif index.column() == 4: + falloff.maxHandle = value + elif index.column() == 5: + falloff.minHandle = value + elif index.column() == 6: + falloff.minVal = value + elif index.column() == 7: + falloff.mapName = value + return True + return False + + def flags(self, index): + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsEditable + | Qt.ItemFlag.ItemIsSelectable + ) + + def itemFromIndex(self, index): + return index.internalPointer() + + def getItemRow(self, item): + try: + idx = self.simplex.falloffs.index(item) + except ValueError: + return None + except AttributeError: + return None + return idx diff --git a/src/python/simplexui/interfaceModelTrees.py b/src/python/simplexui/interfaceModelTrees.py index f11d7439..8e602386 100644 --- a/src/python/simplexui/interfaceModelTrees.py +++ b/src/python/simplexui/interfaceModelTrees.py @@ -1,441 +1,441 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -from .dragFilter import DragFilter -from .items import Group -from Qt.QtCore import QItemSelection, QItemSelectionModel, QModelIndex, QRegExp, Qt -from Qt.QtGui import QRegExpValidator -from Qt.QtWidgets import QApplication, QLineEdit, QMenu, QStyledItemDelegate, QTreeView - - -class SimplexNameDelegate(QStyledItemDelegate): - """An QStyledItemDelegate subclass that implements a Regex validator""" - - def __init__(self, parent=None): - super(SimplexNameDelegate, self).__init__(parent) - self._rx = QRegExp(r"[A-Za-z][A-Za-z0-9_]*") - - def createEditor(self, parent, option, index): - editor = QLineEdit(parent) - rxv = QRegExpValidator(self._rx, editor) - editor.setValidator(rxv) - return editor - - -class SimplexTree(QTreeView): - """Abstract base tree displaying Simplex objects""" - - def __init__(self, parent): - super(SimplexTree, self).__init__(parent) - - self.expandModifier = Qt.KeyboardModifier.ControlModifier - self.depthModifier = Qt.KeyboardModifier.ShiftModifier - - self._menu = None - self._plugins = [] - - self.expanded.connect(self.expandTree) - self.collapsed.connect(self.collapseTree) - self.connectMenus() - - self.dragFilter = DragFilter(self.viewport()) - self.viewport().installEventFilter(self.dragFilter) - - self.dragFilter.dragTick.connect(self.dragTick) - - self.delegate = SimplexNameDelegate(self) - self.setItemDelegateForColumn(0, self.delegate) - - self.setColumnWidth(1, 50) - self.setColumnWidth(2, 20) - - def setPlugins(self, plugins): - """Set the right-click menu plugins for the tree - - Parameters - ---------- - plugins : list - The list of plugins for the tree - """ - self._plugins = plugins - - def unifySelection(self): - """Handle selection across multiple Trees. - The other tree's selectionChanged signal will be connected to this - And it will clear the selection on this tree if no modifiers are being held - """ - mods = QApplication.keyboardModifiers() - if not mods & ( - Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier - ): - selModel = self.selectionModel() - selModel.blockSignals(True) - try: - selModel.clearSelection() - finally: - selModel.blockSignals(False) - self.viewport().update() - - def hideRedundant(self, check): - """Update the filter model to show/hide single shapes - - Parameters - ---------- - check : bool - Whether to hid redundant - """ - model = self.model() - model.filterShapes = check - model.invalidateFilter() - - def stringFilter(self, filterString): - """Update the filter model with a filter string - - Parameters - ---------- - filterString : str - The string to filter on - """ - model = self.model() - model.filterString = str(filterString) - model.invalidateFilter() - - def isolate(self, sliderNames): - """Update the filter model with a whitelist of names - - Parameters - ---------- - sliderNames : [str, ...] - The names items to show - """ - model = self.model() - model.isolateList = sliderNames - model.invalidateFilter() - - def isolateSelected(self): - """Isolate the selected items""" - items = self.getSelectedItems() - isoList = [i.name for i in items] - self.isolate(isoList) - - def exitIsolate(self): - """Remove all items from isolation""" - self.isolate([]) - - # Tree expansion/collapse code - def expandTree(self, index): - """Expand all items under index - - Parameters - ---------- - index : QModelIndex - The index to recursively expand - """ - self.toggleTree(index, True) - - def collapseTree(self, index): - """Collapse all items under index - - Parameters - ---------- - index : QModelIndex - The index to recursively collapse - """ - self.toggleTree(index, False) - - def resizeColumns(self): - '''Resize all columns to their contents "smartly"''' - model = self.model() - for i in range(model.columnCount() - 1): - oldcw = self.columnWidth(i) - self.resizeColumnToContents(i) - newcw = self.columnWidth(i) + 10 - self.setColumnWidth(i, max(oldcw, newcw, 30)) - self.setColumnWidth(model.columnCount() - 1, 5) - - def toggleTree(self, index, expand): - """Recursively expand or collapse an entire sub-tree of an - index. If certain modifiers are held, then only a partial - sub-tree will be expanded - - Parameters - ---------- - index : QModelIndex - The index to change expansion - expand : bool - Whether to expand or collapse the item - """ - # Separate function to deal with filtering capability - if not index.isValid(): - return - - model = self.model() - mods = QApplication.keyboardModifiers() - thing = model.itemFromIndex(index) - thing.expanded[id(self)] = expand - - if mods & (self.expandModifier | self.depthModifier): - queue = [index] - self.blockSignals(True) - try: - while queue: - idx = queue.pop() - thing = model.itemFromIndex(idx) - thing.expanded[id(self)] = expand - self.setExpanded(idx, expand) - if mods & self.depthModifier: - if isinstance(thing, Group): - continue - for i in range(model.rowCount(idx)): - child = model.index(i, 0, idx) - if child and child.isValid(): - queue.append(child) - finally: - self.blockSignals(False) - - if expand: - self.resizeColumns() - - def expandToItem(self, item): - """Make sure that all parents leading to `item` are expanded - - Parameters - ---------- - item : object - The item to expand to - """ - model = self.model() - index = model.indexFromItem(item) - self.expandToIndex(index) - - def expandToIndex(self, index): - """Make sure that all parents leading to `index` are expanded - - Parameters - ---------- - index : QModelIndex - The index to expand to - """ - model = self.model() - while index and index.isValid(): - self.setExpanded(index, True) - thing = model.itemFromIndex(index) - thing.expanded[id(self)] = True - index = index.parent() - self.resizeColumns() - - def scrollToItem(self, item): - """Ensure that the item is scrolled to in the tree - - Parameters - ---------- - item : object - The Item to expand to - """ - model = self.model() - index = model.indexFromItem(item) - self.scrollToIndex(index) - - def scrollToIndex(self, index): - """Ensure that the index is scrolled to in the tree - - Parameters - ---------- - index : QModelIndex - The Index to expand to - - """ - self.expandToIndex(index) - self.scrollTo(index) - - def storeExpansion(self): - """Store the expansion state of the tree for the undo stack""" - model = self.model() - queue = [model.index(0, 0, QModelIndex())] - while queue: - index = queue.pop() - item = model.itemFromIndex(index) - item.expanded[id(self)] = self.isExpanded(index) - for row in range(model.rowCount(index)): - queue.append(model.index(row, 0, index)) - - def setItemExpansion(self): - """Load the stored expansions onto the tree""" - model = self.model() - queue = [model.index(0, 0, QModelIndex())] - self.blockSignals(True) - try: - while queue: - index = queue.pop() - item = model.itemFromIndex(index) - exp = item.expanded.get(id(self), False) - self.setExpanded(index, exp) - for row in range(model.rowCount(index)): - queue.append(model.index(row, 0, index)) - finally: - self.blockSignals(False) - - def dragTick(self, ticks, mul): - """Deal with the ticks coming from the drag handler - - Parameters - ---------- - ticks : int - The number and direction of update ticks coming from the drag handler - mul : float - The multiplier based on user hotkeys - """ - selModel = self.selectionModel() - if not selModel: - return - items = self.getSelectedItems() - for item in items: - if hasattr(item, "valueTick"): - item.valueTick(ticks, mul) - self.viewport().update() - - # Menus and Actions - def connectMenus(self): - """Setup the QT signal/slot connections to the context menus""" - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.customContextMenuRequested.connect(self.openMenu) - - def openMenu(self, pos): - """Handle getting the data to show the context menu - - Parameters - ---------- - pos : QPoint - The position of the click - """ - clickIdx = self.indexAt(pos) - selIdxs = self.getSelectedIndexes() - if self.window().simplex is None: - return - self.showContextMenu(clickIdx, selIdxs, self.viewport().mapToGlobal(pos)) - - def showContextMenu(self, clickIdx, indexes, pos): - """Handle showing the context menu items from the plugins - - Parameters - ---------- - clickIdx : QModelIndex - The model index that was clicked - indexes : [QModelIndex, ...] - The indexes that were selected - pos : QPoint - The position of the click - """ - menu = QMenu() - for plug in self._plugins: - plug.registerContext(self, clickIdx, indexes, menu) - menu.exec_(pos) - - # Selection - def getSelectedItems(self, typ=None): - """Get the selected tree items - - Parameters - ---------- - typ : Type - Only return selected items of this type. Optional - - Returns - ------- - [object, ...] - A list of selected tree items - """ - selModel = self.selectionModel() - if not selModel: - return [] - selIdxs = selModel.selectedIndexes() - selIdxs = [i for i in selIdxs if i.column() == 0] - model = self.model() - items = [model.itemFromIndex(i) for i in selIdxs] - if typ is not None: - items = [i for i in items if isinstance(i, typ)] - return items - - def getSelectedIndexes(self, filtered=False): - """Get selected indexes for either the filtered or unfiltered models - - Parameters - ---------- - filtered : bool - Whether the model is filtered or not. Defaults to False - - Returns - ------- - [QModelIndex, ...] - A list of selected indexes - """ - selModel = self.selectionModel() - if not selModel: - return [] - selIdxs = selModel.selectedIndexes() - if filtered: - return selIdxs - - model = self.model() - indexes = [model.mapToSource(i) for i in selIdxs] - return indexes - - def setItemSelection(self, items): - """Set the selection based on a list of items - - Parameters - ---------- - items : [object, ...] - List of items to select - """ - model = self.model() - idxs = [model.indexFromItem(i) for i in items] - idxs = [i for i in idxs if i and i.isValid()] - - toSel = QItemSelection() - for idx in idxs: - toSel.merge( - QItemSelection(idx, idx), QItemSelectionModel.SelectionFlag.Select - ) - - for idx in idxs: - par = idx.parent() - if par.isValid(): - self.scrollToIndex(par) - - selModel = self.selectionModel() - selModel.select(toSel, QItemSelectionModel.SelectionFlag.ClearAndSelect) - - -# Currently, there's no difference between these -# Later on, though, they may be different -class SliderTree(SimplexTree): - """A SimplexTree sub-class for sliders""" - - pass - - -class ComboTree(SimplexTree): - """A SimplexTree sub-class for combos""" - - pass - - -class TraversalTree(SimplexTree): - """A SimplexTree sub-class for traversals""" - - pass +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +from .dragFilter import DragFilter +from .items import Group +from Qt.QtCore import QItemSelection, QItemSelectionModel, QModelIndex, QRegExp, Qt +from Qt.QtGui import QRegExpValidator +from Qt.QtWidgets import QApplication, QLineEdit, QMenu, QStyledItemDelegate, QTreeView + + +class SimplexNameDelegate(QStyledItemDelegate): + """An QStyledItemDelegate subclass that implements a Regex validator""" + + def __init__(self, parent=None): + super(SimplexNameDelegate, self).__init__(parent) + self._rx = QRegExp(r"[A-Za-z][A-Za-z0-9_]*") + + def createEditor(self, parent, option, index): + editor = QLineEdit(parent) + rxv = QRegExpValidator(self._rx, editor) + editor.setValidator(rxv) + return editor + + +class SimplexTree(QTreeView): + """Abstract base tree displaying Simplex objects""" + + def __init__(self, parent): + super(SimplexTree, self).__init__(parent) + + self.expandModifier = Qt.KeyboardModifier.ControlModifier + self.depthModifier = Qt.KeyboardModifier.ShiftModifier + + self._menu = None + self._plugins = [] + + self.expanded.connect(self.expandTree) + self.collapsed.connect(self.collapseTree) + self.connectMenus() + + self.dragFilter = DragFilter(self.viewport()) + self.viewport().installEventFilter(self.dragFilter) + + self.dragFilter.dragTick.connect(self.dragTick) + + self.delegate = SimplexNameDelegate(self) + self.setItemDelegateForColumn(0, self.delegate) + + self.setColumnWidth(1, 50) + self.setColumnWidth(2, 20) + + def setPlugins(self, plugins): + """Set the right-click menu plugins for the tree + + Parameters + ---------- + plugins : list + The list of plugins for the tree + """ + self._plugins = plugins + + def unifySelection(self): + """Handle selection across multiple Trees. + The other tree's selectionChanged signal will be connected to this + And it will clear the selection on this tree if no modifiers are being held + """ + mods = QApplication.keyboardModifiers() + if not mods & ( + Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier + ): + selModel = self.selectionModel() + selModel.blockSignals(True) + try: + selModel.clearSelection() + finally: + selModel.blockSignals(False) + self.viewport().update() + + def hideRedundant(self, check): + """Update the filter model to show/hide single shapes + + Parameters + ---------- + check : bool + Whether to hid redundant + """ + model = self.model() + model.filterShapes = check + model.invalidateFilter() + + def stringFilter(self, filterString): + """Update the filter model with a filter string + + Parameters + ---------- + filterString : str + The string to filter on + """ + model = self.model() + model.filterString = str(filterString) + model.invalidateFilter() + + def isolate(self, sliderNames): + """Update the filter model with a whitelist of names + + Parameters + ---------- + sliderNames : [str, ...] + The names items to show + """ + model = self.model() + model.isolateList = sliderNames + model.invalidateFilter() + + def isolateSelected(self): + """Isolate the selected items""" + items = self.getSelectedItems() + isoList = [i.name for i in items] + self.isolate(isoList) + + def exitIsolate(self): + """Remove all items from isolation""" + self.isolate([]) + + # Tree expansion/collapse code + def expandTree(self, index): + """Expand all items under index + + Parameters + ---------- + index : QModelIndex + The index to recursively expand + """ + self.toggleTree(index, True) + + def collapseTree(self, index): + """Collapse all items under index + + Parameters + ---------- + index : QModelIndex + The index to recursively collapse + """ + self.toggleTree(index, False) + + def resizeColumns(self): + '''Resize all columns to their contents "smartly"''' + model = self.model() + for i in range(model.columnCount() - 1): + oldcw = self.columnWidth(i) + self.resizeColumnToContents(i) + newcw = self.columnWidth(i) + 10 + self.setColumnWidth(i, max(oldcw, newcw, 30)) + self.setColumnWidth(model.columnCount() - 1, 5) + + def toggleTree(self, index, expand): + """Recursively expand or collapse an entire sub-tree of an + index. If certain modifiers are held, then only a partial + sub-tree will be expanded + + Parameters + ---------- + index : QModelIndex + The index to change expansion + expand : bool + Whether to expand or collapse the item + """ + # Separate function to deal with filtering capability + if not index.isValid(): + return + + model = self.model() + mods = QApplication.keyboardModifiers() + thing = model.itemFromIndex(index) + thing.expanded[id(self)] = expand + + if mods & (self.expandModifier | self.depthModifier): + queue = [index] + self.blockSignals(True) + try: + while queue: + idx = queue.pop() + thing = model.itemFromIndex(idx) + thing.expanded[id(self)] = expand + self.setExpanded(idx, expand) + if mods & self.depthModifier: + if isinstance(thing, Group): + continue + for i in range(model.rowCount(idx)): + child = model.index(i, 0, idx) + if child and child.isValid(): + queue.append(child) + finally: + self.blockSignals(False) + + if expand: + self.resizeColumns() + + def expandToItem(self, item): + """Make sure that all parents leading to `item` are expanded + + Parameters + ---------- + item : object + The item to expand to + """ + model = self.model() + index = model.indexFromItem(item) + self.expandToIndex(index) + + def expandToIndex(self, index): + """Make sure that all parents leading to `index` are expanded + + Parameters + ---------- + index : QModelIndex + The index to expand to + """ + model = self.model() + while index and index.isValid(): + self.setExpanded(index, True) + thing = model.itemFromIndex(index) + thing.expanded[id(self)] = True + index = index.parent() + self.resizeColumns() + + def scrollToItem(self, item): + """Ensure that the item is scrolled to in the tree + + Parameters + ---------- + item : object + The Item to expand to + """ + model = self.model() + index = model.indexFromItem(item) + self.scrollToIndex(index) + + def scrollToIndex(self, index): + """Ensure that the index is scrolled to in the tree + + Parameters + ---------- + index : QModelIndex + The Index to expand to + + """ + self.expandToIndex(index) + self.scrollTo(index) + + def storeExpansion(self): + """Store the expansion state of the tree for the undo stack""" + model = self.model() + queue = [model.index(0, 0, QModelIndex())] + while queue: + index = queue.pop() + item = model.itemFromIndex(index) + item.expanded[id(self)] = self.isExpanded(index) + for row in range(model.rowCount(index)): + queue.append(model.index(row, 0, index)) + + def setItemExpansion(self): + """Load the stored expansions onto the tree""" + model = self.model() + queue = [model.index(0, 0, QModelIndex())] + self.blockSignals(True) + try: + while queue: + index = queue.pop() + item = model.itemFromIndex(index) + exp = item.expanded.get(id(self), False) + self.setExpanded(index, exp) + for row in range(model.rowCount(index)): + queue.append(model.index(row, 0, index)) + finally: + self.blockSignals(False) + + def dragTick(self, ticks, mul): + """Deal with the ticks coming from the drag handler + + Parameters + ---------- + ticks : int + The number and direction of update ticks coming from the drag handler + mul : float + The multiplier based on user hotkeys + """ + selModel = self.selectionModel() + if not selModel: + return + items = self.getSelectedItems() + for item in items: + if hasattr(item, "valueTick"): + item.valueTick(ticks, mul) + self.viewport().update() + + # Menus and Actions + def connectMenus(self): + """Setup the QT signal/slot connections to the context menus""" + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.customContextMenuRequested.connect(self.openMenu) + + def openMenu(self, pos): + """Handle getting the data to show the context menu + + Parameters + ---------- + pos : QPoint + The position of the click + """ + clickIdx = self.indexAt(pos) + selIdxs = self.getSelectedIndexes() + if self.window().simplex is None: + return + self.showContextMenu(clickIdx, selIdxs, self.viewport().mapToGlobal(pos)) + + def showContextMenu(self, clickIdx, indexes, pos): + """Handle showing the context menu items from the plugins + + Parameters + ---------- + clickIdx : QModelIndex + The model index that was clicked + indexes : [QModelIndex, ...] + The indexes that were selected + pos : QPoint + The position of the click + """ + menu = QMenu() + for plug in self._plugins: + plug.registerContext(self, clickIdx, indexes, menu) + menu.exec_(pos) + + # Selection + def getSelectedItems(self, typ=None): + """Get the selected tree items + + Parameters + ---------- + typ : Type + Only return selected items of this type. Optional + + Returns + ------- + [object, ...] + A list of selected tree items + """ + selModel = self.selectionModel() + if not selModel: + return [] + selIdxs = selModel.selectedIndexes() + selIdxs = [i for i in selIdxs if i.column() == 0] + model = self.model() + items = [model.itemFromIndex(i) for i in selIdxs] + if typ is not None: + items = [i for i in items if isinstance(i, typ)] + return items + + def getSelectedIndexes(self, filtered=False): + """Get selected indexes for either the filtered or unfiltered models + + Parameters + ---------- + filtered : bool + Whether the model is filtered or not. Defaults to False + + Returns + ------- + [QModelIndex, ...] + A list of selected indexes + """ + selModel = self.selectionModel() + if not selModel: + return [] + selIdxs = selModel.selectedIndexes() + if filtered: + return selIdxs + + model = self.model() + indexes = [model.mapToSource(i) for i in selIdxs] + return indexes + + def setItemSelection(self, items): + """Set the selection based on a list of items + + Parameters + ---------- + items : [object, ...] + List of items to select + """ + model = self.model() + idxs = [model.indexFromItem(i) for i in items] + idxs = [i for i in idxs if i and i.isValid()] + + toSel = QItemSelection() + for idx in idxs: + toSel.merge( + QItemSelection(idx, idx), QItemSelectionModel.SelectionFlag.Select + ) + + for idx in idxs: + par = idx.parent() + if par.isValid(): + self.scrollToIndex(par) + + selModel = self.selectionModel() + selModel.select(toSel, QItemSelectionModel.SelectionFlag.ClearAndSelect) + + +# Currently, there's no difference between these +# Later on, though, they may be different +class SliderTree(SimplexTree): + """A SimplexTree sub-class for sliders""" + + pass + + +class ComboTree(SimplexTree): + """A SimplexTree sub-class for combos""" + + pass + + +class TraversalTree(SimplexTree): + """A SimplexTree sub-class for traversals""" + + pass diff --git a/src/python/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py index 3a0ae2f9..a912427d 100644 --- a/src/python/simplexui/items/accessor.py +++ b/src/python/simplexui/items/accessor.py @@ -1,233 +1,233 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . -from __future__ import annotations -import copy - - -from typing import TYPE_CHECKING, Optional, Any - -if TYPE_CHECKING: - from .simplex import Simplex - from .stack import Stack - - -class SimplexAccessor(object): - """The base object for all Simplex System object types - This class provides access to the simplex system, draggability, - name getters/setters/unifiers, proper deepcopying, and abstract tree lookup - - Parameters - ---------- - - Returns - ------- - - """ - - def __init__(self, simplex: Simplex): - self.simplex: Simplex = simplex - self._name: str = "" - self._splitApplied = set() - - @property - def name(self) -> str: - """ """ - return self._name - - @name.setter - def name(self, val: str): - """The name of the current object""" - self._name = val - - @property - def models(self) -> list: - """ """ - return self.simplex.models - - @property - def falloffModels(self) -> list: - """ """ - return self.simplex.falloffModels - - @property - def DCC(self): - """ """ - return self.simplex.DCC - - @property - def stack(self) -> Stack: - """ """ - return self.simplex.stack - - def __deepcopy__(self, memo): - cls = self.__class__ - result = cls.__new__(cls) - memo[id(self)] = result - for k, v in self.__dict__.items(): - if k == "_thing" and self.DCC.program != "dummy": - # DO NOT make a copy of the DCC thing (unless its a dummy dcc) - # as it may or may not be a persistent object - # setattr(result, k, self._thing) - setattr(result, k, None) - elif k == "expanded": - # Skip the expanded dict because it deals with the Qt models - setattr(result, k, {}) - else: - setattr(result, k, copy.deepcopy(v, memo)) - return result - - def _buildLinkedRename( - self, - newName: str, - maxDepth: int, - currentLinks: dict[type, dict[str, tuple[SimplexAccessor, int]]], - ): - """Build the proposed set of renames specifically for this object - This allows sub-classes to override the linked name behavior - - Parameters - ---------- - newName : - - maxDepth : - - currentLinks : - - - Returns - ------- - - """ - return currentLinks - - def buildLinkedRename( - self, - newName: str, - maxDepth: int = 5, - currentLinks: Optional[ - dict[type, dict[str, tuple[SimplexAccessor, int]]] - ] = None, - ) -> dict[type, dict[str, tuple[SimplexAccessor, int]]]: - """For the Shape, Slider, Combo, and Traversal items, build a linked rename - dictionary like {itemType: {newName: (item, maxDepth)}} recursively up to a - maximum given depth - - The dictionary is structured like that to easily check for name clashes - - Parameters - ---------- - newName : str - The new suggested name for this object - maxDepth : int - The maximum depth of recursion when resolving names (Default value = 5) - currentLinks : dict - The current set of proposed renames. (Default value = None) - - Returns - ------- - dict : - A new set of propsed renames - - """ - # Build the output dict if not done already - if currentLinks is None: - currentLinks = {} - - # return at depth - if maxDepth <= 0: - return currentLinks - - # If this doesn't need renamed, then we can prune this branch - if self.name == newName: - return currentLinks - - # Check for conflicts, or other short circuits - typeLinks = currentLinks.setdefault(type(self), {}) - if newName in typeLinks: - tlPair = typeLinks[newName] - if tlPair[0] is not self: - # Error out if a name conflict is found. - msg = "Linked rename produced a conflict: Trying to rename {0} {1} and {2} to {3}" - msg = msg.format( - type(self), typeLinks[newName][0].name, self.name, newName - ) - raise ValueError(msg) - elif tlPair[1] >= maxDepth: - # If we've been here before with more available depth - # then we can just return because we've done this before - return currentLinks - - # Finally add myself to the rename - typeLinks[newName] = (self, maxDepth) - - # And now handle the type-specific stuff - return self._buildLinkedRename(newName, maxDepth, currentLinks) - - def treeChild(self, row): - """ """ - return None - - def treeRow(self): - """ """ - return None - - def treeParent(self): - """ """ - return None - - def treeChildCount(self) -> int: - """ """ - return 0 - - def treeData(self, column) -> Any: - """ """ - if column == 0: - return self.name - return None - - def treeChecked(self): - """ """ - return None - - def icon(self): - return None - - -class SimplexTickAccessor(SimplexAccessor): - value: float - - def __init__(self, simplex): - super().__init__(simplex) - self.dragStep: float = 0.05 - self.maxValue: float = 1.0 - self.minValue: float = 0.0 - - def valueTick(self, ticks: int, mul: float): - """Change the value of the current object by some number of ticks - with some given multiplier. This is the interface for the MMB drag - - Parameters - ---------- - ticks : int - The number of dragStep ticks to apply - mul : float - An overall multiplier - """ - val = self.value + (self.dragStep * ticks * mul) - val = 0.0 if abs(val) < 1e-5 else val - val = max(min(val, self.maxValue), self.minValue) - self.value = val +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . +from __future__ import annotations +import copy + + +from typing import TYPE_CHECKING, Optional, Any + +if TYPE_CHECKING: + from .simplex import Simplex + from .stack import Stack + + +class SimplexAccessor(object): + """The base object for all Simplex System object types + This class provides access to the simplex system, draggability, + name getters/setters/unifiers, proper deepcopying, and abstract tree lookup + + Parameters + ---------- + + Returns + ------- + + """ + + def __init__(self, simplex: Simplex): + self.simplex: Simplex = simplex + self._name: str = "" + self._splitApplied = set() + + @property + def name(self) -> str: + """ """ + return self._name + + @name.setter + def name(self, val: str): + """The name of the current object""" + self._name = val + + @property + def models(self) -> list: + """ """ + return self.simplex.models + + @property + def falloffModels(self) -> list: + """ """ + return self.simplex.falloffModels + + @property + def DCC(self): + """ """ + return self.simplex.DCC + + @property + def stack(self) -> Stack: + """ """ + return self.simplex.stack + + def __deepcopy__(self, memo): + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k == "_thing" and self.DCC.program != "dummy": + # DO NOT make a copy of the DCC thing (unless its a dummy dcc) + # as it may or may not be a persistent object + # setattr(result, k, self._thing) + setattr(result, k, None) + elif k == "expanded": + # Skip the expanded dict because it deals with the Qt models + setattr(result, k, {}) + else: + setattr(result, k, copy.deepcopy(v, memo)) + return result + + def _buildLinkedRename( + self, + newName: str, + maxDepth: int, + currentLinks: dict[type, dict[str, tuple[SimplexAccessor, int]]], + ): + """Build the proposed set of renames specifically for this object + This allows sub-classes to override the linked name behavior + + Parameters + ---------- + newName : + + maxDepth : + + currentLinks : + + + Returns + ------- + + """ + return currentLinks + + def buildLinkedRename( + self, + newName: str, + maxDepth: int = 5, + currentLinks: Optional[ + dict[type, dict[str, tuple[SimplexAccessor, int]]] + ] = None, + ) -> dict[type, dict[str, tuple[SimplexAccessor, int]]]: + """For the Shape, Slider, Combo, and Traversal items, build a linked rename + dictionary like {itemType: {newName: (item, maxDepth)}} recursively up to a + maximum given depth + + The dictionary is structured like that to easily check for name clashes + + Parameters + ---------- + newName : str + The new suggested name for this object + maxDepth : int + The maximum depth of recursion when resolving names (Default value = 5) + currentLinks : dict + The current set of proposed renames. (Default value = None) + + Returns + ------- + dict : + A new set of propsed renames + + """ + # Build the output dict if not done already + if currentLinks is None: + currentLinks = {} + + # return at depth + if maxDepth <= 0: + return currentLinks + + # If this doesn't need renamed, then we can prune this branch + if self.name == newName: + return currentLinks + + # Check for conflicts, or other short circuits + typeLinks = currentLinks.setdefault(type(self), {}) + if newName in typeLinks: + tlPair = typeLinks[newName] + if tlPair[0] is not self: + # Error out if a name conflict is found. + msg = "Linked rename produced a conflict: Trying to rename {0} {1} and {2} to {3}" + msg = msg.format( + type(self), typeLinks[newName][0].name, self.name, newName + ) + raise ValueError(msg) + elif tlPair[1] >= maxDepth: + # If we've been here before with more available depth + # then we can just return because we've done this before + return currentLinks + + # Finally add myself to the rename + typeLinks[newName] = (self, maxDepth) + + # And now handle the type-specific stuff + return self._buildLinkedRename(newName, maxDepth, currentLinks) + + def treeChild(self, row): + """ """ + return None + + def treeRow(self): + """ """ + return None + + def treeParent(self): + """ """ + return None + + def treeChildCount(self) -> int: + """ """ + return 0 + + def treeData(self, column) -> Any: + """ """ + if column == 0: + return self.name + return None + + def treeChecked(self): + """ """ + return None + + def icon(self): + return None + + +class SimplexTickAccessor(SimplexAccessor): + value: float + + def __init__(self, simplex): + super().__init__(simplex) + self.dragStep: float = 0.05 + self.maxValue: float = 1.0 + self.minValue: float = 0.0 + + def valueTick(self, ticks: int, mul: float): + """Change the value of the current object by some number of ticks + with some given multiplier. This is the interface for the MMB drag + + Parameters + ---------- + ticks : int + The number of dragStep ticks to apply + mul : float + An overall multiplier + """ + val = self.value + (self.dragStep * ticks * mul) + val = 0.0 if abs(val) < 1e-5 else val + val = max(min(val, self.maxValue), self.minValue) + self.value = val diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index 8343bb86..fd8db1a1 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -1,852 +1,852 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -from ..interface import undoContext - -# pylint:disable=missing-docstring,unused-argument,no-self-use -from Qt.QtGui import QColor -from ..utils import getIcon, nested -from .accessor import SimplexAccessor, SimplexTickAccessor -from .stack import stackable - - -# Abstract Items -class ComboPair(SimplexTickAccessor): - """A Slider/Value pair for use in Combos""" - - classDepth = 6 - - def __init__(self, slider, value): - simplex = slider.simplex - super(ComboPair, self).__init__(simplex) - self.slider = slider - self._value = float(value) - self.minValue = -1.0 - self.maxValue = 1.0 - self.combo = None - self.expanded = {} - - @property - def models(self): - """ """ - return self.combo.simplex.models - - @property - def name(self): - """ """ - return self.slider.name - - @property - def value(self): - """ """ - return self._value - - @value.setter - @stackable - def value(self, val): - """ - - Parameters - ---------- - val : - - - Returns - ------- - - """ - self._value = val - for model in self.models: - model.itemDataChanged(self) - - def buildDefinition(self, simpDict, legacy): - """ - - Parameters - ---------- - simpDict : - - legacy : - - - Returns - ------- - - """ - sIdx = self.slider.buildDefinition(simpDict, legacy) - return sIdx, self.value - - def treeRow(self): - """ """ - return self.combo.pairs.index(self) - - def treeParent(self): - """ """ - return self.combo - - def treeData(self, column): - """ - - Parameters - ---------- - column : - - - Returns - ------- - - """ - if column == 0: - return self.name - if column == 1: - return self.value - return None - - -class Combo(SimplexAccessor): - """A group of Slider/Value pairs that control a Progression through some solver - - Combos allow for fixit shapes to be created for any number of user inputs. - They also allow for fixits along the progression, and in "floating space" where - the inputs are not -1 or 1 - - Parameters - ---------- - name : str - The name of this Combo - simplex : Simplex - The parent Simplex system - pairs : [ComboPair - The Slider/Value pairs that make up this combo - prog : Progression - The Progression that this Combo controls - group : Group - The Group to create this combo in - solveType : str - The solve type for this combo. See Combo.solveTypes for a list - color : QColor - The color of this item in the UI - - Returns - ------- - - """ - - classDepth = 5 - solveTypes = ( - ("Minimum", "min"), - ("Multiply All", "allMul"), - ("Multiply Extremes", "extMul"), - ("Multiply Avg of Extremes", "mulAvgExt"), - ("Multiply Avg", "mulAvgAll"), - ("None", "min"), - ) - _freezeIcon = None - - def __init__(self, name, simplex, pairs, prog, group, solveType, color=None): - super(Combo, self).__init__(simplex) - color = QColor(128, 128, 128) if color is None else color - - with self.stack.store(self): - if group.groupType is not type(self): - raise ValueError("Cannot add this slider to a combo group") - self._name = name - self.pairs = pairs - self.prog = prog - self._solveType = solveType - self._buildIdx = None - self.expanded = {} - self._enabled = True - self.color = color - - self._freezeThing = None - - mgrs = [model.insertItemManager(group) for model in self.models] - with nested(*mgrs): - self.group = group - for p in self.pairs: - p.combo = self - self.prog.controller = self - self.group.items.append(self) - self.simplex.combos.append(self) - - @property - def enabled(self): - """Get whether this Combo is evaluated in the solver""" - return self._enabled - - @enabled.setter - @stackable - def enabled(self, value): - """Set whether this Combo is evaluated in the solver""" - self._enabled = value - for model in self.models: - model.itemDataChanged(self) - - @property - def frozen(self): - """Get whether this Combo is frozen""" - return bool(self.freezeThing) - - @property - def freezeThing(self): - """Get whether this Combo is frozen""" - if self._freezeThing is None: - self._freezeThing = self.DCC.getFreezeThing(self) - return self._freezeThing - - @freezeThing.setter - def freezeThing(self, value): - self._freezeThing = value - for model in self.models: - model.itemDataChanged(self) - - def icon(self): - if self.frozen: - if self._freezeIcon is None: - type(self)._freezeIcon = getIcon("frozen.png") - return self._freezeIcon - return None - - @classmethod - def comboAlreadyExists(cls, simplex, sliders, values): - """Classmethod to check whether a combo already exists with these sliders and values - - Parameters - ---------- - simplex : Simplex - The system to check within - sliders : [Slider - The Sliders to check - values : [float - The values to zip with the sliders - - Returns - ------- - : Combo or None - The combo that exists with the given values, or None if none exist - - """ - checker = {(s.name, v) for s, v in zip(sliders, values)} - for combo in simplex.combos: - tester = {(p.slider.name, p.value) for p in combo.pairs} - if checker == tester: - return combo - return None - - @classmethod - def createCombo( - cls, - name, - simplex, - sliders, - values, - group=None, - shape=None, - solveType=None, - tVal=1.0, - ): - """Classmethod to create Combo with some hard-coded defaults - - Parameters - ---------- - name : str - The name of the Combo - simplex : Simplex - The Simplex system - sliders : [Slider - The Sliders that will control this combo - values : [float - The values at which the sliders will activate this combo - group : Group or None - A Group to organize this combo. - If None, the combo will be sorted into a "DEPTH" group (Default value = None) - shape : Shape or None - A Shape for this Combo's Progression. If None, then a default shape will be created - solveType : str or None - The solve type for this Combo. if None, defaults to 'min' in the solver - tVal : float - The slideValue where the Shape will be created/added. Defaults to 1.0 - - Returns - ------- - : Combo - The newly created Combo - - """ - if simplex.restShape is None: - raise RuntimeError("Simplex system is missing rest shape") - - # Make sure to check if this combo already exists. If so, just return it - exist = cls.comboAlreadyExists(simplex, sliders, values) - if exist is not None: - return exist - from .group import Group - from .progression import ProgPair, Progression - - if group is None: - gname = "DEPTH_{0}".format(len(sliders)) - matches = [i for i in simplex.comboGroups if i.name == gname] - if matches: - group = matches[0] - else: - group = Group(gname, simplex, Combo) - - cPairs = [ComboPair(slider, value) for slider, value in zip(sliders, values)] - prog = Progression(name, simplex) - if shape: - prog.pairs.append(ProgPair(simplex, shape, tVal)) - - cmb = Combo(name, simplex, cPairs, prog, group, solveType) - - if shape is None: - pp = prog.createShape(name, tVal) - simplex.DCC.zeroShape(pp.shape) - - return cmb - - @staticmethod - def buildComboName(sliders, values): - """Build the name for a combo based on the input Sliders and values - The sliders will be alphabetically sorted - Values not at Shape increments within the progression will get numeric - suffixes. Negative values will have suffixes like "n75" - - Parameters - ---------- - sliders : [Slider - The sliders to check - values : [float - The values for the sliders - - Returns - ------- - : str - The suggested combo name - - """ - pairs = list(zip(sliders, values)) - pairs = sorted(pairs, key=lambda x: x[0].name) - parts = [] - for slider, value in pairs: - shape = slider.prog.getShapeAtValue(value) - if shape is not None: - parts.append(shape.name) - else: - # get the extreme shape and percentage-ize its name - extVal = 1.0 if value > 0.0 else -1.0 - - valName = "{}".format(abs(int(value * 100))) - valName = "n" + valName if value < 0.0 else valName - - shape = slider.prog.getShapeAtValue(extVal) - if shape is not None: - sn = shape.name - sn = sn.split("_") - if sn[-1].isnumeric() or ( - sn[-1][0] == "n" and sn[-1][1:].isnumeric() - ): - sn[-1] = valName - else: - sn.append(valName) - nsn = "_".join(sn) - parts.append(nsn) - else: - parts.append(slider.name) - - return "_".join(parts) - - @property - def name(self): - """Get the name of a combo""" - return self._name - - @name.setter - @stackable - def name(self, value): - """Set the name of a combo - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self._name = value - self.prog.name = value - self.DCC.renameCombo(self, value) - for model in self.models: - model.itemDataChanged(self) - - @property - def solveType(self): - """ """ - return self._solveType - - @solveType.setter - @stackable - def solveType(self, newType): - """Set the solveType of the combo - - Parameters - ---------- - newType : - - - Returns - ------- - - """ - stNames, stVals = list(zip(*self.solveTypes)) - if newType not in stVals: - raise ValueError( - "Solve Type {0} not in allowed types {1}".format(newType, stVals) - ) - self._solveType = newType - for model in self.models: - model.itemDataChanged(self) - - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - - Returns - ------- - type - - - """ - if row == len(self.pairs): - return self.prog - return self.pairs[row] - - def treeRow(self): - """ """ - return self.group.items.index(self) - - def treeParent(self): - """ """ - return self.group - - def treeChildCount(self): - """ """ - return len(self.pairs) + 1 - - def treeChecked(self): - """ """ - return self.enabled - - def sliderNameLinks(self): - """ """ - sliNames = ["_{0}_".format(i.slider.name) for i in self.pairs] - surr = "_{0}_".format(self.name) - return [sn in surr for sn in sliNames] - - def nameLinks(self): - """ - - Parameters - ---------- - - Returns - ------- - : type - progression depends on this slider's name - - """ - # In this case, these names will *NOT* have the possibility of - # a pos/neg name. Only the combo name, and possibly a percentage - shapeNames = [] - shapes = [i.shape for i in self.prog.pairs] - for s in shapes: - x = s.name.rsplit("_", 1) - if len(x) == 2: - base, sfx = x - x = base if sfx.isdigit() else s.name - shapeNames.append(x) - return [i == self.name for i in shapeNames] - - def getSliderIndex(self, slider): - """ - - Parameters - ---------- - slider : - - - Returns - ------- - type - - - """ - for i, p in enumerate(self.pairs): - if p.slider == slider: - return i - raise ValueError("Provided slider:{0} is not in the list".format(slider.name)) - - def isFloating(self): - """ - - Parameters - ---------- - - Returns - ------- - : type - Floating combos are combos that Slider values that are between 0 and 1 - - """ - for pair in self.pairs: - if abs(pair.value) != 1.0: - return True - return False - - def getSliders(self): - """ """ - return [i.slider for i in self.pairs] - - @classmethod - def loadV2(cls, simplex, progs, data): - """Load the data from a version2 formatted json dictionary - - Parameters - ---------- - simplex : Simplex - The Simplex system that's being built - progs : [Progression - The progressions that have already been built - data : dict - The chunk of the json dict used to build this object - - Returns - ------- - : Combo - The specified combo - - """ - name = data["name"] - prog = progs[data["prog"]] - group = simplex.groups[data.get("group", 1)] - color = QColor(*data.get("color", (128, 128, 128))) - pairs = [ComboPair(simplex.sliders[s], v) for s, v in data["pairs"]] - solveType = data.get("solveType") - return cls(name, simplex, pairs, prog, group, solveType, color=color) - - def buildDefinition(self, simpDict, legacy): - """Output a dictionary definition of this object - - Parameters - ---------- - simpDict : dict - The dictionary that is being built - legacy : bool - Whether to write out the legacy definition, or the newer one - - Returns - ------- - - """ - if self._buildIdx is None: - self._buildIdx = len(simpDict["combos"]) - if legacy: - gIdx = self.group.buildDefinition(simpDict, legacy) - pIdx = self.prog.buildDefinition(simpDict, legacy) - idxPairs = [p.buildDefinition(simpDict, legacy) for p in self.pairs] - x = [self.name, pIdx, idxPairs, gIdx] - simpDict.setdefault("combos", []).append(x) - else: - x = { - "name": self.name, - "prog": self.prog.buildDefinition(simpDict, legacy), - "pairs": [p.buildDefinition(simpDict, legacy) for p in self.pairs], - "group": self.group.buildDefinition(simpDict, legacy), - "color": self.color.getRgb()[:3], - "enabled": self._enabled, - "solveType": str(self._solveType), - } - simpDict.setdefault("combos", []).append(x) - return self._buildIdx - - def clearBuildIndex(self): - """Clear the build index of this object - - The buildIndex is stored when building a definition dictionary - that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - - """ - self._buildIdx = None - self.prog.clearBuildIndex() - self.group.clearBuildIndex() - - def extractProgressive(self, live=True, offset=10.0, separation=5.0): - """ - - Parameters - ---------- - live : - (Default value = True) - offset : - (Default value = 10.0) - separation : - (Default value = 5.0) - - Returns - ------- - - """ - raise RuntimeError("Currently just copied from Sliders, Not actually real") - with undoContext(self.DCC): - pos, neg = [], [] - for pp in sorted(self.prog.pairs): - if pp.value < 0.0: - neg.append((pp.value, pp.shape, offset)) - offset += separation - elif pp.value > 0.0: - pos.append((pp.value, pp.shape, offset)) - offset += separation - # skip the rest value at == 0.0 - neg = reversed(neg) - - for prog in [pos, neg]: - xtVal, shape, shift = prog[-1] - ext, deltaShape = self.DCC.extractWithDeltaShape(shape, live, shift) - for value, shape, shift in prog[:-1]: - self.DCC.extractWithDeltaConnection( - shape, deltaShape, value / xtVal, live, shift - ) - - def extractShape(self, shape, live=True, offset=10.0): - """Extract a shape from a combo progression - - Parameters - ---------- - shape : Shape - The Shape object to extract as a mesh - live : bool - Whether to maintain a live connection to the extracted mesh in the DCC (Default value = True) - offset : float - The offset to give the extracted mesh in the DCC (Default value = 10.0) - - Returns - ------- - : object - The DCC mesh just created - - """ - return self.DCC.extractComboShape(self, shape, live, offset) - - def connectShape(self, shape, mesh=None, live=False, delete=False): - """Connect a shape into a combo progression - - Parameters - ---------- - shape : Shape - The shape to connect the mesh to - mesh : object or None - A DCC mesh to connect into a Shape - If None, tries to connect by name (Default value = None) - live : bool - Whether to maintain a live connecto to the mesh in the DCC (Default value = False) - delete : bool - Whether to delete the DCC mesh after it was connected (Default value = False) - - Returns - ------- - - """ - self.DCC.connectComboShape(self, shape, mesh, live, delete) - - @stackable - def delete(self): - """Delete this combo and any shapes it contains""" - self.simplex.deleteDownstream(self) - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - g = self.group - if self not in g.items: - return # Can happen when deleting multiple groups - g.items.remove(self) - self.group = None - self.simplex.combos.remove(self) - pairs = self.prog.pairs[:] # gotta make a copy - for pp in pairs: - if not pp.shape.isRest: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) - - @stackable - def setInterpolation(self, interp): - """Set the interpolation of a combo - - Parameters - ---------- - interp : str - The interpolation for this combo's progression - - Returns - ------- - - """ - self.prog.interp = interp - for model in self.models: - model.itemDataChanged(self) - - @stackable - def setComboValue(self, slider, value): - """Set the Slider/value pairs for a combo - - Parameters - ---------- - slider : Slider - The slider to set the value for - value : float - The value to set the Slider to - - Returns - ------- - - """ - idx = self.getSliderIndex(slider) - pair = self.pairs[idx] - pair.value = value - for model in self.models: - model.itemDataChanged(pair) - - @stackable - def appendComboValue(self, slider, value): - """Append a Slider/value pair for a combo - - Parameters - ---------- - slider : Slider - The slider to insert - value : float - The value to set the Slider to - - Returns - ------- - - """ - cp = ComboPair(slider, value) - mgrs = [model.insertItemManager(self) for model in self.models] - with nested(*mgrs): - self.pairs.append(cp) - cp.combo = self - - @stackable - def deleteComboPair(self, comboPair): - """Delete a Slider/value pair for a combo - - Parameters - ---------- - comboPair : ComboPair - The ComboPair to delete - - Returns - ------- - - """ - mgrs = [model.removeItemManager(comboPair) for model in self.models] - with nested(*mgrs): - # We specifically don't move the combo to the proper depth group - # That way the user can make multiple changes to the combo without - # it popping all over in the heirarchy - self.pairs.remove(comboPair) - comboPair.combo = None - - @stackable - def setGroup(self, grp): - """Set the group for this Combo - - Parameters - ---------- - grp : Group - The group to set - - Returns - ------- - - """ - if grp.groupType is None: - grp.groupType = type(self) - - if not isinstance(self, grp.groupType): - raise ValueError( - "All items in this group must be of type: {}".format(grp.groupType) - ) - - mgrs = [model.moveItemManager(self, grp) for model in self.models] - with nested(*mgrs): - if self.group: - self.group.items.remove(self) - grp.items.append(self) - self.group = grp - - @stackable - def createShape(self, shapeName=None, tVal=None): - """Create a shape and add it to a progression - - Parameters - ---------- - shapeName : str or None - The name of the shape to create. - If None, give it a default name - tVal : float or None - The progression value to set for the new Shape. - If None, it gets a "smart" default value - - Returns - ------- - - """ - pp, idx = self.prog.newProgPair(shapeName, tVal) - mgrs = [model.insertItemManager(self.prog, idx) for model in self.models] - with nested(*mgrs): - pp.prog = self.prog - self.prog.pairs.insert(idx, pp) - return pp - - def getInputVector(self): - """Get the input to the Solver that would fully activate this Combo - - Parameters - ---------- - - Returns - ------- - : [float, ...] - The ordered slider values - - """ - inVec = [0.0] * len(self.simplex.sliders) - for cp in self.pairs: - inVec[self.simplex.sliders.index(cp.slider)] = cp.value - return inVec +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +from ..interface import undoContext + +# pylint:disable=missing-docstring,unused-argument,no-self-use +from Qt.QtGui import QColor +from ..utils import getIcon, nested +from .accessor import SimplexAccessor, SimplexTickAccessor +from .stack import stackable + + +# Abstract Items +class ComboPair(SimplexTickAccessor): + """A Slider/Value pair for use in Combos""" + + classDepth = 6 + + def __init__(self, slider, value): + simplex = slider.simplex + super(ComboPair, self).__init__(simplex) + self.slider = slider + self._value = float(value) + self.minValue = -1.0 + self.maxValue = 1.0 + self.combo = None + self.expanded = {} + + @property + def models(self): + """ """ + return self.combo.simplex.models + + @property + def name(self): + """ """ + return self.slider.name + + @property + def value(self): + """ """ + return self._value + + @value.setter + @stackable + def value(self, val): + """ + + Parameters + ---------- + val : + + + Returns + ------- + + """ + self._value = val + for model in self.models: + model.itemDataChanged(self) + + def buildDefinition(self, simpDict, legacy): + """ + + Parameters + ---------- + simpDict : + + legacy : + + + Returns + ------- + + """ + sIdx = self.slider.buildDefinition(simpDict, legacy) + return sIdx, self.value + + def treeRow(self): + """ """ + return self.combo.pairs.index(self) + + def treeParent(self): + """ """ + return self.combo + + def treeData(self, column): + """ + + Parameters + ---------- + column : + + + Returns + ------- + + """ + if column == 0: + return self.name + if column == 1: + return self.value + return None + + +class Combo(SimplexAccessor): + """A group of Slider/Value pairs that control a Progression through some solver + + Combos allow for fixit shapes to be created for any number of user inputs. + They also allow for fixits along the progression, and in "floating space" where + the inputs are not -1 or 1 + + Parameters + ---------- + name : str + The name of this Combo + simplex : Simplex + The parent Simplex system + pairs : [ComboPair + The Slider/Value pairs that make up this combo + prog : Progression + The Progression that this Combo controls + group : Group + The Group to create this combo in + solveType : str + The solve type for this combo. See Combo.solveTypes for a list + color : QColor + The color of this item in the UI + + Returns + ------- + + """ + + classDepth = 5 + solveTypes = ( + ("Minimum", "min"), + ("Multiply All", "allMul"), + ("Multiply Extremes", "extMul"), + ("Multiply Avg of Extremes", "mulAvgExt"), + ("Multiply Avg", "mulAvgAll"), + ("None", "min"), + ) + _freezeIcon = None + + def __init__(self, name, simplex, pairs, prog, group, solveType, color=None): + super(Combo, self).__init__(simplex) + color = QColor(128, 128, 128) if color is None else color + + with self.stack.store(self): + if group.groupType is not type(self): + raise ValueError("Cannot add this slider to a combo group") + self._name = name + self.pairs = pairs + self.prog = prog + self._solveType = solveType + self._buildIdx = None + self.expanded = {} + self._enabled = True + self.color = color + + self._freezeThing = None + + mgrs = [model.insertItemManager(group) for model in self.models] + with nested(*mgrs): + self.group = group + for p in self.pairs: + p.combo = self + self.prog.controller = self + self.group.items.append(self) + self.simplex.combos.append(self) + + @property + def enabled(self): + """Get whether this Combo is evaluated in the solver""" + return self._enabled + + @enabled.setter + @stackable + def enabled(self, value): + """Set whether this Combo is evaluated in the solver""" + self._enabled = value + for model in self.models: + model.itemDataChanged(self) + + @property + def frozen(self): + """Get whether this Combo is frozen""" + return bool(self.freezeThing) + + @property + def freezeThing(self): + """Get whether this Combo is frozen""" + if self._freezeThing is None: + self._freezeThing = self.DCC.getFreezeThing(self) + return self._freezeThing + + @freezeThing.setter + def freezeThing(self, value): + self._freezeThing = value + for model in self.models: + model.itemDataChanged(self) + + def icon(self): + if self.frozen: + if self._freezeIcon is None: + type(self)._freezeIcon = getIcon("frozen.png") + return self._freezeIcon + return None + + @classmethod + def comboAlreadyExists(cls, simplex, sliders, values): + """Classmethod to check whether a combo already exists with these sliders and values + + Parameters + ---------- + simplex : Simplex + The system to check within + sliders : [Slider + The Sliders to check + values : [float + The values to zip with the sliders + + Returns + ------- + : Combo or None + The combo that exists with the given values, or None if none exist + + """ + checker = {(s.name, v) for s, v in zip(sliders, values)} + for combo in simplex.combos: + tester = {(p.slider.name, p.value) for p in combo.pairs} + if checker == tester: + return combo + return None + + @classmethod + def createCombo( + cls, + name, + simplex, + sliders, + values, + group=None, + shape=None, + solveType=None, + tVal=1.0, + ): + """Classmethod to create Combo with some hard-coded defaults + + Parameters + ---------- + name : str + The name of the Combo + simplex : Simplex + The Simplex system + sliders : [Slider + The Sliders that will control this combo + values : [float + The values at which the sliders will activate this combo + group : Group or None + A Group to organize this combo. + If None, the combo will be sorted into a "DEPTH" group (Default value = None) + shape : Shape or None + A Shape for this Combo's Progression. If None, then a default shape will be created + solveType : str or None + The solve type for this Combo. if None, defaults to 'min' in the solver + tVal : float + The slideValue where the Shape will be created/added. Defaults to 1.0 + + Returns + ------- + : Combo + The newly created Combo + + """ + if simplex.restShape is None: + raise RuntimeError("Simplex system is missing rest shape") + + # Make sure to check if this combo already exists. If so, just return it + exist = cls.comboAlreadyExists(simplex, sliders, values) + if exist is not None: + return exist + from .group import Group + from .progression import ProgPair, Progression + + if group is None: + gname = "DEPTH_{0}".format(len(sliders)) + matches = [i for i in simplex.comboGroups if i.name == gname] + if matches: + group = matches[0] + else: + group = Group(gname, simplex, Combo) + + cPairs = [ComboPair(slider, value) for slider, value in zip(sliders, values)] + prog = Progression(name, simplex) + if shape: + prog.pairs.append(ProgPair(simplex, shape, tVal)) + + cmb = Combo(name, simplex, cPairs, prog, group, solveType) + + if shape is None: + pp = prog.createShape(name, tVal) + simplex.DCC.zeroShape(pp.shape) + + return cmb + + @staticmethod + def buildComboName(sliders, values): + """Build the name for a combo based on the input Sliders and values + The sliders will be alphabetically sorted + Values not at Shape increments within the progression will get numeric + suffixes. Negative values will have suffixes like "n75" + + Parameters + ---------- + sliders : [Slider + The sliders to check + values : [float + The values for the sliders + + Returns + ------- + : str + The suggested combo name + + """ + pairs = list(zip(sliders, values)) + pairs = sorted(pairs, key=lambda x: x[0].name) + parts = [] + for slider, value in pairs: + shape = slider.prog.getShapeAtValue(value) + if shape is not None: + parts.append(shape.name) + else: + # get the extreme shape and percentage-ize its name + extVal = 1.0 if value > 0.0 else -1.0 + + valName = "{}".format(abs(int(value * 100))) + valName = "n" + valName if value < 0.0 else valName + + shape = slider.prog.getShapeAtValue(extVal) + if shape is not None: + sn = shape.name + sn = sn.split("_") + if sn[-1].isnumeric() or ( + sn[-1][0] == "n" and sn[-1][1:].isnumeric() + ): + sn[-1] = valName + else: + sn.append(valName) + nsn = "_".join(sn) + parts.append(nsn) + else: + parts.append(slider.name) + + return "_".join(parts) + + @property + def name(self): + """Get the name of a combo""" + return self._name + + @name.setter + @stackable + def name(self, value): + """Set the name of a combo + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self._name = value + self.prog.name = value + self.DCC.renameCombo(self, value) + for model in self.models: + model.itemDataChanged(self) + + @property + def solveType(self): + """ """ + return self._solveType + + @solveType.setter + @stackable + def solveType(self, newType): + """Set the solveType of the combo + + Parameters + ---------- + newType : + + + Returns + ------- + + """ + stNames, stVals = list(zip(*self.solveTypes)) + if newType not in stVals: + raise ValueError( + "Solve Type {0} not in allowed types {1}".format(newType, stVals) + ) + self._solveType = newType + for model in self.models: + model.itemDataChanged(self) + + def treeChild(self, row): + """ + + Parameters + ---------- + row : + + + Returns + ------- + type + + + """ + if row == len(self.pairs): + return self.prog + return self.pairs[row] + + def treeRow(self): + """ """ + return self.group.items.index(self) + + def treeParent(self): + """ """ + return self.group + + def treeChildCount(self): + """ """ + return len(self.pairs) + 1 + + def treeChecked(self): + """ """ + return self.enabled + + def sliderNameLinks(self): + """ """ + sliNames = ["_{0}_".format(i.slider.name) for i in self.pairs] + surr = "_{0}_".format(self.name) + return [sn in surr for sn in sliNames] + + def nameLinks(self): + """ + + Parameters + ---------- + + Returns + ------- + : type + progression depends on this slider's name + + """ + # In this case, these names will *NOT* have the possibility of + # a pos/neg name. Only the combo name, and possibly a percentage + shapeNames = [] + shapes = [i.shape for i in self.prog.pairs] + for s in shapes: + x = s.name.rsplit("_", 1) + if len(x) == 2: + base, sfx = x + x = base if sfx.isdigit() else s.name + shapeNames.append(x) + return [i == self.name for i in shapeNames] + + def getSliderIndex(self, slider): + """ + + Parameters + ---------- + slider : + + + Returns + ------- + type + + + """ + for i, p in enumerate(self.pairs): + if p.slider == slider: + return i + raise ValueError("Provided slider:{0} is not in the list".format(slider.name)) + + def isFloating(self): + """ + + Parameters + ---------- + + Returns + ------- + : type + Floating combos are combos that Slider values that are between 0 and 1 + + """ + for pair in self.pairs: + if abs(pair.value) != 1.0: + return True + return False + + def getSliders(self): + """ """ + return [i.slider for i in self.pairs] + + @classmethod + def loadV2(cls, simplex, progs, data): + """Load the data from a version2 formatted json dictionary + + Parameters + ---------- + simplex : Simplex + The Simplex system that's being built + progs : [Progression + The progressions that have already been built + data : dict + The chunk of the json dict used to build this object + + Returns + ------- + : Combo + The specified combo + + """ + name = data["name"] + prog = progs[data["prog"]] + group = simplex.groups[data.get("group", 1)] + color = QColor(*data.get("color", (128, 128, 128))) + pairs = [ComboPair(simplex.sliders[s], v) for s, v in data["pairs"]] + solveType = data.get("solveType") + return cls(name, simplex, pairs, prog, group, solveType, color=color) + + def buildDefinition(self, simpDict, legacy): + """Output a dictionary definition of this object + + Parameters + ---------- + simpDict : dict + The dictionary that is being built + legacy : bool + Whether to write out the legacy definition, or the newer one + + Returns + ------- + + """ + if self._buildIdx is None: + self._buildIdx = len(simpDict["combos"]) + if legacy: + gIdx = self.group.buildDefinition(simpDict, legacy) + pIdx = self.prog.buildDefinition(simpDict, legacy) + idxPairs = [p.buildDefinition(simpDict, legacy) for p in self.pairs] + x = [self.name, pIdx, idxPairs, gIdx] + simpDict.setdefault("combos", []).append(x) + else: + x = { + "name": self.name, + "prog": self.prog.buildDefinition(simpDict, legacy), + "pairs": [p.buildDefinition(simpDict, legacy) for p in self.pairs], + "group": self.group.buildDefinition(simpDict, legacy), + "color": self.color.getRgb()[:3], + "enabled": self._enabled, + "solveType": str(self._solveType), + } + simpDict.setdefault("combos", []).append(x) + return self._buildIdx + + def clearBuildIndex(self): + """Clear the build index of this object + + The buildIndex is stored when building a definition dictionary + that keeps track of its index for later referencing + + Parameters + ---------- + + Returns + ------- + + """ + self._buildIdx = None + self.prog.clearBuildIndex() + self.group.clearBuildIndex() + + def extractProgressive(self, live=True, offset=10.0, separation=5.0): + """ + + Parameters + ---------- + live : + (Default value = True) + offset : + (Default value = 10.0) + separation : + (Default value = 5.0) + + Returns + ------- + + """ + raise RuntimeError("Currently just copied from Sliders, Not actually real") + with undoContext(self.DCC): + pos, neg = [], [] + for pp in sorted(self.prog.pairs): + if pp.value < 0.0: + neg.append((pp.value, pp.shape, offset)) + offset += separation + elif pp.value > 0.0: + pos.append((pp.value, pp.shape, offset)) + offset += separation + # skip the rest value at == 0.0 + neg = reversed(neg) + + for prog in [pos, neg]: + xtVal, shape, shift = prog[-1] + ext, deltaShape = self.DCC.extractWithDeltaShape(shape, live, shift) + for value, shape, shift in prog[:-1]: + self.DCC.extractWithDeltaConnection( + shape, deltaShape, value / xtVal, live, shift + ) + + def extractShape(self, shape, live=True, offset=10.0): + """Extract a shape from a combo progression + + Parameters + ---------- + shape : Shape + The Shape object to extract as a mesh + live : bool + Whether to maintain a live connection to the extracted mesh in the DCC (Default value = True) + offset : float + The offset to give the extracted mesh in the DCC (Default value = 10.0) + + Returns + ------- + : object + The DCC mesh just created + + """ + return self.DCC.extractComboShape(self, shape, live, offset) + + def connectShape(self, shape, mesh=None, live=False, delete=False): + """Connect a shape into a combo progression + + Parameters + ---------- + shape : Shape + The shape to connect the mesh to + mesh : object or None + A DCC mesh to connect into a Shape + If None, tries to connect by name (Default value = None) + live : bool + Whether to maintain a live connecto to the mesh in the DCC (Default value = False) + delete : bool + Whether to delete the DCC mesh after it was connected (Default value = False) + + Returns + ------- + + """ + self.DCC.connectComboShape(self, shape, mesh, live, delete) + + @stackable + def delete(self): + """Delete this combo and any shapes it contains""" + self.simplex.deleteDownstream(self) + mgrs = [model.removeItemManager(self) for model in self.models] + with nested(*mgrs): + g = self.group + if self not in g.items: + return # Can happen when deleting multiple groups + g.items.remove(self) + self.group = None + self.simplex.combos.remove(self) + pairs = self.prog.pairs[:] # gotta make a copy + for pp in pairs: + if not pp.shape.isRest: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) + + @stackable + def setInterpolation(self, interp): + """Set the interpolation of a combo + + Parameters + ---------- + interp : str + The interpolation for this combo's progression + + Returns + ------- + + """ + self.prog.interp = interp + for model in self.models: + model.itemDataChanged(self) + + @stackable + def setComboValue(self, slider, value): + """Set the Slider/value pairs for a combo + + Parameters + ---------- + slider : Slider + The slider to set the value for + value : float + The value to set the Slider to + + Returns + ------- + + """ + idx = self.getSliderIndex(slider) + pair = self.pairs[idx] + pair.value = value + for model in self.models: + model.itemDataChanged(pair) + + @stackable + def appendComboValue(self, slider, value): + """Append a Slider/value pair for a combo + + Parameters + ---------- + slider : Slider + The slider to insert + value : float + The value to set the Slider to + + Returns + ------- + + """ + cp = ComboPair(slider, value) + mgrs = [model.insertItemManager(self) for model in self.models] + with nested(*mgrs): + self.pairs.append(cp) + cp.combo = self + + @stackable + def deleteComboPair(self, comboPair): + """Delete a Slider/value pair for a combo + + Parameters + ---------- + comboPair : ComboPair + The ComboPair to delete + + Returns + ------- + + """ + mgrs = [model.removeItemManager(comboPair) for model in self.models] + with nested(*mgrs): + # We specifically don't move the combo to the proper depth group + # That way the user can make multiple changes to the combo without + # it popping all over in the heirarchy + self.pairs.remove(comboPair) + comboPair.combo = None + + @stackable + def setGroup(self, grp): + """Set the group for this Combo + + Parameters + ---------- + grp : Group + The group to set + + Returns + ------- + + """ + if grp.groupType is None: + grp.groupType = type(self) + + if not isinstance(self, grp.groupType): + raise ValueError( + "All items in this group must be of type: {}".format(grp.groupType) + ) + + mgrs = [model.moveItemManager(self, grp) for model in self.models] + with nested(*mgrs): + if self.group: + self.group.items.remove(self) + grp.items.append(self) + self.group = grp + + @stackable + def createShape(self, shapeName=None, tVal=None): + """Create a shape and add it to a progression + + Parameters + ---------- + shapeName : str or None + The name of the shape to create. + If None, give it a default name + tVal : float or None + The progression value to set for the new Shape. + If None, it gets a "smart" default value + + Returns + ------- + + """ + pp, idx = self.prog.newProgPair(shapeName, tVal) + mgrs = [model.insertItemManager(self.prog, idx) for model in self.models] + with nested(*mgrs): + pp.prog = self.prog + self.prog.pairs.insert(idx, pp) + return pp + + def getInputVector(self): + """Get the input to the Solver that would fully activate this Combo + + Parameters + ---------- + + Returns + ------- + : [float, ...] + The ordered slider values + + """ + inVec = [0.0] * len(self.simplex.sliders) + for cp in self.pairs: + inVec[self.simplex.sliders.index(cp.slider)] = cp.value + return inVec diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index f92a1d1a..d6e85af7 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -1,778 +1,778 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use -import copy -import math - -try: - import numpy as np -except ImportError: - np = None -from Qt.QtGui import QColor -from ..utils import nested -from .accessor import SimplexAccessor -from .stack import stackable - - -class Falloff(SimplexAccessor): - """Falloffs define how shapes are split left/right, front/back, top/bottom, or however else - - Falloffs are the core of the splitting algorithm built into Simplex. - When splitting, they define how shapes are duplicated and renamed, as well as - how the deltas are multiplied. - - Currently only axis-aligned planar falloffs are fully defined and supported, but there are plans - for per-point falloffs (weightmap), oriented planes, and other implicit geometric shapes. - - A planar falloff is a worldspace value field where any point past falloff min has the min value, - any point past the max has the max value, and any point in between is defined by user-controlled curve - Maya users might think of this as a planar projection of a ramp. - - A group of static variables controls how splits are detected and renamed. Names are split by - Falloff.SEP and the split chunks are matched to the items below. In some cases, rather than - lists of characters, I just use a string. - - These define the strings identifying their eponymous sides: - LEFTSIDE, RIGHTSIDE, TOPSIDE, BOTTOMSIDE, FRONTSIDE, BACKSIDE - - This is a character list definint the single-character values that define centered shapes: - CENTERS - - These define the strings that are detected to find splits. - For instance, if HORIZONTAL_SPLIT="X", then cornerPuller_X would split across the X axis, - and the new items would be given names - - :: - "cornerPuller_{}".format(LEFTSIDE) - "cornerPuller_{}".format(RIGHTSIDE) - - VERTICAL_SPLIT, VERTICAL_AXIS, VERTICAL_AXISINDEX - HORIZONTAL_SPLIT, HORIZONTAL_AXIS, HORIZONTAL_AXISINDEX - DEPTH_SPLIT, DEPTH_AXIS, DEPTH_AXISINDEX, - - When UNsplitting a simplex system, this value controls the tolerance - UNSPLIT_GUESS_TOLERANCE - - Parameters - ---------- - name : str - The name of the falloff - simplex : Simplex - The Simplex system - *data : list - The data used to build this falloff. - You should use one of the classmethod like Falloff.createPlanar or Falloff.createMap instead - - Returns - ------- - - """ - - LEFTSIDE = "L" - RIGHTSIDE = "R" - TOPSIDE = "U" - BOTTOMSIDE = "D" - FRONTSIDE = "F" - BACKSIDE = "B" - ALLSIDES = LEFTSIDE + RIGHTSIDE + TOPSIDE + BOTTOMSIDE + FRONTSIDE + BACKSIDE - - CENTERS = "MC" - - VERTICAL_SPLIT = "V" - VERTICAL_RESULTS = TOPSIDE + BOTTOMSIDE - VERTICAL_AXIS = "Y" - VERTICAL_AXISINDEX = 1 - - HORIZONTAL_SPLIT = "X" - HORIZONTAL_RESULTS = LEFTSIDE + RIGHTSIDE - HORIZONTAL_AXIS = "X" - HORIZONTAL_AXISINDEX = 0 - - DEPTH_SPLIT = "Z" - DEPTH_RESULTS = FRONTSIDE + BACKSIDE - DEPTH_AXIS = "Z" - DEPTH_AXISINDEX = 2 - - RESTNAME = "Rest" - SEP = "_" - - UNSPLIT_GUESS_TOLERANCE = 0.33 - - def __init__(self, name, simplex, *data): - super(Falloff, self).__init__(simplex) - with self.stack.store(self): - self._splitType = str(data[0]).lower() - self._axis = None - self._maxVal = None - self._maxHandle = None - self._minHandle = None - self._minVal = None - self._mapName = None - - self._bezier = None - self._search = None - self._rep = None - self._weights = None - self._verts = None - self._thing = None - self._thingRepr = None - - if self._splitType == "planar": - self._axis = data[1] - self._maxVal = data[2] - self._maxHandle = data[3] - self._minHandle = data[4] - self._minVal = data[5] - elif self._splitType == "map": - self._mapName = data[1] - self._axis = data[2] - - self._name = name - self.children = [] - self._buildIdx = None - self.expanded = {} - self.color = QColor(128, 128, 128) - - mgrs = [model.insertItemManager(None) for model in self.falloffModels] - with nested(*mgrs): - self.simplex.falloffs.append(self) - - # newThing = self.DCC.getFalloffThing(self) - # if newThing is None: - # self.thing = self.DCC.createFalloff(self) - # else: - # self.thing = newThing - - # @property - # def thing(self): - ## if this is a deepcopied object, then self._thing will - ## be None. Rebuild the thing connection by its representation - # if self._thing is None and self._thingRepr: - # self._thing = self.DCC.loadPersistentFalloff(self._thingRepr) - # return self._thing - - # @thing.setter - # def thing(self, value): - # self._thing = value - # self._thingRepr = self.DCC.getPersistentFalloff(value) - - @property - def name(self): - """Get the name of a Falloff""" - return self._name - - @name.setter - @stackable - def name(self, value): - """Set the name of a Falloff - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self._name = value - for model in self.falloffModels: - model.itemDataChanged(self) - - @classmethod - def createPlanar(cls, name, simplex, axis, maxVal, maxHandle, minHandle, minVal): - """Create a planar falloff - - Parameters - ---------- - name : str - The name to give the falloff - simplex : Simplex - The Simplex system - axis : str - The axis to align the falloff to. X, Y, or Z - maxVal : float - The value past which the falloff is 1.0 - maxHandle : float - The (0, 1) range of the max cubic falloff handle - minHandle : float - The (0, 1) range of the min cubic falloff handle - minVal : float - The value past which the falloff is 0.0 - - Returns - ------- - - """ - return cls(name, simplex, "planar", axis, maxVal, maxHandle, minHandle, minVal) - - @classmethod - def createMap(cls, name, simplex, mapName, axis): - """Create a weightmap falloff - - Parameters - ---------- - name : str - The name to give the falloff - simplex : Simplex - The Simplex system - mapName : str - The name of the weightmap - axis : str - The axis to align the falloff to. X, Y, or Z - - Returns - ------- - - """ - return cls(name, simplex, "map", mapName, axis) - - @classmethod - def loadV2(cls, simplex, data): - """Load the falloff from the version 2 json specification - - Parameters - ---------- - simplex : Simplex - The Simplex system - data : dict - The data to load - - Returns - ------- - : Falloff - The specified Falloff - - """ - tpe = data["type"] - name = data["name"] - axis = data["axis"] - if tpe == "map": - return cls.createMap(name, simplex, data["mapName"], axis) - elif tpe == "planar": - maxVal = data["maxVal"] - maxHandle = data["maxHandle"] - minHandle = data["minHandle"] - minVal = data["minVal"] - return cls.createPlanar( - name, simplex, axis, maxVal, maxHandle, minHandle, minVal - ) - - raise ValueError("Bad data passed to Falloff creation") - - def buildDefinition(self, simpDict, legacy): - """Output a dictionary definition of this object - - Parameters - ---------- - simpDict : dict - The dictionary that is being built - legacy : bool - Whether to write out the legacy definition, or the newer one - - Returns - ------- - - """ - if self._buildIdx is None: - self._buildIdx = len(simpDict["falloffs"]) - if legacy: - if self.splitType == "planar": - line = [ - "planar", - self.axis, - self.maxVal, - self.maxHandle, - self.minHandle, - self.minVal, - ] - else: - line = ["map", self.mapName] - simpDict.setdefault("falloffs", []).append([self.name] + line) - else: - x = { - "name": self.name, - "type": self.splitType, - "axis": self.axis, - "maxVal": self.maxVal, - "maxHandle": self.maxHandle, - "minHandle": self.minHandle, - "minVal": self.minVal, - "mapName": self.mapName, - "color": self.color.getRgb()[:3], - } - simpDict.setdefault("falloffs", []).append(x) - return self._buildIdx - - def clearBuildIndex(self): - """Clear the build index of this object - - The buildIndex is stored when building a definition dictionary - that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - - """ - self._buildIdx = None - - @stackable - def duplicate(self, newName): - """Duplicate a Falloff with a new name - - Parameters - ---------- - newName : str - The name to give the new Falloff - - Returns - ------- - : Falloff - The newly duplicated Falloff - - """ - nf = copy.copy(self) - nf.name = newName - nf.children = [] - nf.clearBuildIndex() - mgrs = [model.insertItemManager(self) for model in self.falloffModels] - with nested(*mgrs): - self.simplex.falloffs.append(nf) - self.DCC.duplicateFalloff(self, nf) - return nf - - @stackable - def delete(self): - """Delete the Falloff""" - fIdx = self.simplex.falloffs.index(self) - for child in self.children: - child.falloff = None - - mgrs = [model.removeItemManager(self) for model in self.falloffModels] - with nested(*mgrs): - self.simplex.falloffs.pop(fIdx) - self.DCC.deleteFalloff(self) - - @stackable - def setPlanarData(self, axis, minVal, minHandle, maxHandle, maxVal): - """Set the type/data for a planar Falloff - - Parameters - ---------- - axis : str - The axis to align the falloff to. X, Y, or Z - maxVal : float - The value past which the falloff is 1.0 - maxHandle : float - The (0, 1) range of the max cubic falloff handle - minHandle : float - The (0, 1) range of the min cubic falloff handle - minVal : float - The value past which the falloff is 0.0 - - Returns - ------- - - """ - self.splitType = "planar" - self.axis = axis - self.minVal = minVal - self.minHandle = minHandle - self.maxHandle = maxHandle - self.maxVal = maxVal - self.mapName = None - self._updateDCC() - - @stackable - def setMapData(self, mapName): - """Set the type/data for a map Falloff - - Parameters - ---------- - mapName : str - The name of the weightmap - - Returns - ------- - - """ - self.splitType = "map" - self.axis = None - self.minVal = None - self.minHandle = None - self.maxHandle = None - self.maxVal = None - self.mapName = mapName - self._updateDCC() - - @property - def splitType(self): - return self._splitType - - @splitType.setter - @stackable - def splitType(self, value): - self._splitType = str(value).lower() - for model in self.falloffModels: - model.itemDataChanged(self) - self._updateDCC() - - @property - def axis(self): - return self._axis - - @axis.setter - @stackable - def axis(self, value): - self._axis = value - for model in self.falloffModels: - model.itemDataChanged(self) - self._updateDCC() - - @property - def maxVal(self): - return self._maxVal - - @maxVal.setter - @stackable - def maxVal(self, value): - self._maxVal = value - for model in self.falloffModels: - model.itemDataChanged(self) - self._updateDCC() - - @property - def maxHandle(self): - return self._maxHandle - - @maxHandle.setter - @stackable - def maxHandle(self, value): - self._maxHandle = value - for model in self.falloffModels: - model.itemDataChanged(self) - self._updateDCC() - - @property - def minHandle(self): - return self._minHandle - - @minHandle.setter - @stackable - def minHandle(self, value): - self._minHandle = value - for model in self.falloffModels: - model.itemDataChanged(self) - self._updateDCC() - - @property - def minVal(self): - return self._minVal - - @minVal.setter - @stackable - def minVal(self, value): - self._minVal = value - for model in self.falloffModels: - model.itemDataChanged(self) - self._updateDCC() - - @property - def mapName(self): - return self._mapName - - @mapName.setter - @stackable - def mapName(self, value): - self._mapName = value - for model in self.falloffModels: - model.itemDataChanged(self) - self._updateDCC() - - def _updateDCC(self): - """ """ - self.DCC.setFalloffData( - self, - self.splitType, - self.axis, - self.minVal, - self.minHandle, - self.maxHandle, - self.maxVal, - self.mapName, - ) - - # Split code - @property - def bezier(self): - """Pre-build a factorization of the cubic bezier curve that is being used for a falloff - Based on method described at https://pomax.github.io/bezierinfo/#yforx - - Parameters - ---------- - - Returns - ------- - - """ - if self._bezier is None: - p0x = 0.0 - p1x = self.minHandle - p2x = self.maxHandle - p3x = 1.0 - - f = p1x - p0x - g = p3x - p2x - d = 3 * f + 3 * g - 2 - n = 2 * f + g - 1 - r = (n * n - f * d) / (d * d) - qq = (3 * f * d * n - 2 * n * n * n) / (d * d * d) - self._bezier = (qq, r, d, n) - return self._bezier - - def getMultiplier(self, xVal): - """Get the weight value for the given X - - Parameters - ---------- - xVal : float - The value to get the weight for - - Returns - ------- - : float - The weight - - """ - # Vertices are assumed to be at (0,0) and (1,1) - if xVal <= self.minVal: - return 0.0 - if xVal >= self.maxVal: - return 1.0 - - tVal = float(xVal - self.minVal) / float(self.maxVal - self.minVal) - qq, r, d, n = self.bezier - q = qq - tVal / d - discriminant = q * q - 4 * r * r * r - if discriminant >= 0: - pm = (discriminant**0.5) / 2 - w = (-q / 2 + pm) ** (1 / 3.0) - u = w + r / w - else: - theta = math.acos(-q / (2 * r ** (3 / 2.0))) - phi = theta / 3 + 4 * math.pi / 3 - u = 2 * r ** (0.5) * math.cos(phi) - t = u + n / d - t1 = 1 - t - return 3 * t1 * t**2 * 1 + t**3 * 1 - - def _setSearchRep(self): - """ """ - if self.axis.lower() == self.HORIZONTAL_AXIS.lower(): - self._search = self.HORIZONTAL_SPLIT - self._rep = self.HORIZONTAL_RESULTS - elif self.axis.lower() == self.VERTICAL_AXIS.lower(): - self._search = self.VERTICAL_SPLIT - self._rep = self.VERTICAL_RESULTS - elif self.axis.lower() == self.DEPTH_AXIS.lower(): - self._search = self.DEPTH_SPLIT - self._rep = self.DEPTH_RESULTS - - @property - def search(self): - """The values this fallof searches for""" - if self._search is None: - self._setSearchRep() - return self._search - - @property - def rep(self): - """The values this falloff replaces with""" - if self._rep is None: - self._setSearchRep() - return self._rep - - @property - def verts(self): - """Get the stored vertex values""" - return self._verts - - @verts.setter - def verts(self, vals): - """Input the vertices into this falloff and compute the weights - - Parameters - ---------- - vals : np.array - A (Nx3) numpy array of vertices - - Returns - ------- - - """ - if self.splitType != "map": - # Clear out any auto-computed weights - # when setting verts on a non-map falloff - self._weights = None - self._verts = vals - - @property - def weights(self): - """Get the per-vertex weight values""" - - if self._weights is None: - if self.splitType == "map": - raise ValueError( - "Attempted to auto-compute weights of a map falloff: {}".format( - self.name - ) - ) - - if self._verts is None: - raise ValueError( - "Attempted to auto-compute weights of a procedural falloff without setting verts: {0}".format( - self.name - ) - ) - - if self.axis.lower() == self.HORIZONTAL_AXIS.lower(): - component = self.HORIZONTAL_AXISINDEX - elif self.axis.lower() == self.VERTICAL_AXIS.lower(): - component = self.VERTICAL_AXISINDEX - elif self.axis.lower() == self.DEPTH_AXIS.lower(): - component = self.DEPTH_AXISINDEX - else: - raise ValueError("Falloff found with no axis set") - - self._weights = np.array( - [self.getMultiplier(v[component]) for v in self._verts] - ) - - return self._weights - - @weights.setter - def weights(self, val): - """Set the per-vertex weight values - - Parameters - ---------- - val : A list or numpy array of values between 0 and 1 - """ - self._weights = np.asarray(val) - - def getSidedName(self, name, sIdx): - """Take name to split along some axis, and replace the fields based on the index - For instance, this could take cp_X and return cp_L for sIdx=1 and cp_R for sIdx=2 - - Parameters - ---------- - name : str - The name to "split" with this falloff - sIdx : int - The index of the replacement value - - Returns - ------- - : str - The newly sided name - - """ - search = self.search - replace = self.rep[sIdx] - - nn = name - s = "{0}{1}{0}".format(self.SEP, search) - r = "{0}{1}{0}".format(self.SEP, replace) - nn = nn.replace(s, r) - - s = "{0}{1}".format(self.SEP, search) # handle Postfix - r = "{0}{1}".format(self.SEP, replace) - if nn.endswith(s): - nn = r.join(nn.rsplit(s, 1)) - - s = "{1}{0}".format(self.SEP, search) # handle Prefix - r = "{1}{0}".format(self.SEP, replace) - if nn.startswith(s): - nn = nn.replace(s, r, 1) - return nn - - def canRename(self, item): - """Check if the item can be renamed by this Falloff - - Parameters - ---------- - item : object - The named simplex object to check - - Returns - ------- - : bool - Whether this object can be renamed - - """ - nn = self.getSidedName(item.name, 0) - return nn != item.name - - def splitRename(self, item, sIdx): - """Actually run the rename for a particular item - - Parameters - ---------- - item : object - The named Simplex Item - sIdx : int - The replacement index - - Returns - ------- - - """ - from .combo import Combo - from .shape import Shape - from .slider import Slider - from .traversal import Traversal - - if isinstance(item, (Shape, Slider, Combo, Traversal)): - item.name = self.getSidedName(item.name, sIdx) - - def applyFalloff(self, shape, sIdx): - """Apply the falloff to the vertices of a shape - - Parameters - ---------- - shape : Shape - The shape to apply to - sIdx : int - The replacement index - - Returns - ------- - - """ - rest = self.simplex.restShape - restVerts = rest.verts - - weights = self.weights - if sIdx == 1: - weights = 1 - weights - - weightedDeltas = (shape.verts - restVerts) * weights[:, None] - shape.verts = weightedDeltas + restVerts +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint:disable=missing-docstring,unused-argument,no-self-use +import copy +import math + +try: + import numpy as np +except ImportError: + np = None +from Qt.QtGui import QColor +from ..utils import nested +from .accessor import SimplexAccessor +from .stack import stackable + + +class Falloff(SimplexAccessor): + """Falloffs define how shapes are split left/right, front/back, top/bottom, or however else + + Falloffs are the core of the splitting algorithm built into Simplex. + When splitting, they define how shapes are duplicated and renamed, as well as + how the deltas are multiplied. + + Currently only axis-aligned planar falloffs are fully defined and supported, but there are plans + for per-point falloffs (weightmap), oriented planes, and other implicit geometric shapes. + + A planar falloff is a worldspace value field where any point past falloff min has the min value, + any point past the max has the max value, and any point in between is defined by user-controlled curve + Maya users might think of this as a planar projection of a ramp. + + A group of static variables controls how splits are detected and renamed. Names are split by + Falloff.SEP and the split chunks are matched to the items below. In some cases, rather than + lists of characters, I just use a string. + + These define the strings identifying their eponymous sides: + LEFTSIDE, RIGHTSIDE, TOPSIDE, BOTTOMSIDE, FRONTSIDE, BACKSIDE + + This is a character list definint the single-character values that define centered shapes: + CENTERS + + These define the strings that are detected to find splits. + For instance, if HORIZONTAL_SPLIT="X", then cornerPuller_X would split across the X axis, + and the new items would be given names + + :: + "cornerPuller_{}".format(LEFTSIDE) + "cornerPuller_{}".format(RIGHTSIDE) + + VERTICAL_SPLIT, VERTICAL_AXIS, VERTICAL_AXISINDEX + HORIZONTAL_SPLIT, HORIZONTAL_AXIS, HORIZONTAL_AXISINDEX + DEPTH_SPLIT, DEPTH_AXIS, DEPTH_AXISINDEX, + + When UNsplitting a simplex system, this value controls the tolerance + UNSPLIT_GUESS_TOLERANCE + + Parameters + ---------- + name : str + The name of the falloff + simplex : Simplex + The Simplex system + *data : list + The data used to build this falloff. + You should use one of the classmethod like Falloff.createPlanar or Falloff.createMap instead + + Returns + ------- + + """ + + LEFTSIDE = "L" + RIGHTSIDE = "R" + TOPSIDE = "U" + BOTTOMSIDE = "D" + FRONTSIDE = "F" + BACKSIDE = "B" + ALLSIDES = LEFTSIDE + RIGHTSIDE + TOPSIDE + BOTTOMSIDE + FRONTSIDE + BACKSIDE + + CENTERS = "MC" + + VERTICAL_SPLIT = "V" + VERTICAL_RESULTS = TOPSIDE + BOTTOMSIDE + VERTICAL_AXIS = "Y" + VERTICAL_AXISINDEX = 1 + + HORIZONTAL_SPLIT = "X" + HORIZONTAL_RESULTS = LEFTSIDE + RIGHTSIDE + HORIZONTAL_AXIS = "X" + HORIZONTAL_AXISINDEX = 0 + + DEPTH_SPLIT = "Z" + DEPTH_RESULTS = FRONTSIDE + BACKSIDE + DEPTH_AXIS = "Z" + DEPTH_AXISINDEX = 2 + + RESTNAME = "Rest" + SEP = "_" + + UNSPLIT_GUESS_TOLERANCE = 0.33 + + def __init__(self, name, simplex, *data): + super(Falloff, self).__init__(simplex) + with self.stack.store(self): + self._splitType = str(data[0]).lower() + self._axis = None + self._maxVal = None + self._maxHandle = None + self._minHandle = None + self._minVal = None + self._mapName = None + + self._bezier = None + self._search = None + self._rep = None + self._weights = None + self._verts = None + self._thing = None + self._thingRepr = None + + if self._splitType == "planar": + self._axis = data[1] + self._maxVal = data[2] + self._maxHandle = data[3] + self._minHandle = data[4] + self._minVal = data[5] + elif self._splitType == "map": + self._mapName = data[1] + self._axis = data[2] + + self._name = name + self.children = [] + self._buildIdx = None + self.expanded = {} + self.color = QColor(128, 128, 128) + + mgrs = [model.insertItemManager(None) for model in self.falloffModels] + with nested(*mgrs): + self.simplex.falloffs.append(self) + + # newThing = self.DCC.getFalloffThing(self) + # if newThing is None: + # self.thing = self.DCC.createFalloff(self) + # else: + # self.thing = newThing + + # @property + # def thing(self): + ## if this is a deepcopied object, then self._thing will + ## be None. Rebuild the thing connection by its representation + # if self._thing is None and self._thingRepr: + # self._thing = self.DCC.loadPersistentFalloff(self._thingRepr) + # return self._thing + + # @thing.setter + # def thing(self, value): + # self._thing = value + # self._thingRepr = self.DCC.getPersistentFalloff(value) + + @property + def name(self): + """Get the name of a Falloff""" + return self._name + + @name.setter + @stackable + def name(self, value): + """Set the name of a Falloff + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self._name = value + for model in self.falloffModels: + model.itemDataChanged(self) + + @classmethod + def createPlanar(cls, name, simplex, axis, maxVal, maxHandle, minHandle, minVal): + """Create a planar falloff + + Parameters + ---------- + name : str + The name to give the falloff + simplex : Simplex + The Simplex system + axis : str + The axis to align the falloff to. X, Y, or Z + maxVal : float + The value past which the falloff is 1.0 + maxHandle : float + The (0, 1) range of the max cubic falloff handle + minHandle : float + The (0, 1) range of the min cubic falloff handle + minVal : float + The value past which the falloff is 0.0 + + Returns + ------- + + """ + return cls(name, simplex, "planar", axis, maxVal, maxHandle, minHandle, minVal) + + @classmethod + def createMap(cls, name, simplex, mapName, axis): + """Create a weightmap falloff + + Parameters + ---------- + name : str + The name to give the falloff + simplex : Simplex + The Simplex system + mapName : str + The name of the weightmap + axis : str + The axis to align the falloff to. X, Y, or Z + + Returns + ------- + + """ + return cls(name, simplex, "map", mapName, axis) + + @classmethod + def loadV2(cls, simplex, data): + """Load the falloff from the version 2 json specification + + Parameters + ---------- + simplex : Simplex + The Simplex system + data : dict + The data to load + + Returns + ------- + : Falloff + The specified Falloff + + """ + tpe = data["type"] + name = data["name"] + axis = data["axis"] + if tpe == "map": + return cls.createMap(name, simplex, data["mapName"], axis) + elif tpe == "planar": + maxVal = data["maxVal"] + maxHandle = data["maxHandle"] + minHandle = data["minHandle"] + minVal = data["minVal"] + return cls.createPlanar( + name, simplex, axis, maxVal, maxHandle, minHandle, minVal + ) + + raise ValueError("Bad data passed to Falloff creation") + + def buildDefinition(self, simpDict, legacy): + """Output a dictionary definition of this object + + Parameters + ---------- + simpDict : dict + The dictionary that is being built + legacy : bool + Whether to write out the legacy definition, or the newer one + + Returns + ------- + + """ + if self._buildIdx is None: + self._buildIdx = len(simpDict["falloffs"]) + if legacy: + if self.splitType == "planar": + line = [ + "planar", + self.axis, + self.maxVal, + self.maxHandle, + self.minHandle, + self.minVal, + ] + else: + line = ["map", self.mapName] + simpDict.setdefault("falloffs", []).append([self.name] + line) + else: + x = { + "name": self.name, + "type": self.splitType, + "axis": self.axis, + "maxVal": self.maxVal, + "maxHandle": self.maxHandle, + "minHandle": self.minHandle, + "minVal": self.minVal, + "mapName": self.mapName, + "color": self.color.getRgb()[:3], + } + simpDict.setdefault("falloffs", []).append(x) + return self._buildIdx + + def clearBuildIndex(self): + """Clear the build index of this object + + The buildIndex is stored when building a definition dictionary + that keeps track of its index for later referencing + + Parameters + ---------- + + Returns + ------- + + """ + self._buildIdx = None + + @stackable + def duplicate(self, newName): + """Duplicate a Falloff with a new name + + Parameters + ---------- + newName : str + The name to give the new Falloff + + Returns + ------- + : Falloff + The newly duplicated Falloff + + """ + nf = copy.copy(self) + nf.name = newName + nf.children = [] + nf.clearBuildIndex() + mgrs = [model.insertItemManager(self) for model in self.falloffModels] + with nested(*mgrs): + self.simplex.falloffs.append(nf) + self.DCC.duplicateFalloff(self, nf) + return nf + + @stackable + def delete(self): + """Delete the Falloff""" + fIdx = self.simplex.falloffs.index(self) + for child in self.children: + child.falloff = None + + mgrs = [model.removeItemManager(self) for model in self.falloffModels] + with nested(*mgrs): + self.simplex.falloffs.pop(fIdx) + self.DCC.deleteFalloff(self) + + @stackable + def setPlanarData(self, axis, minVal, minHandle, maxHandle, maxVal): + """Set the type/data for a planar Falloff + + Parameters + ---------- + axis : str + The axis to align the falloff to. X, Y, or Z + maxVal : float + The value past which the falloff is 1.0 + maxHandle : float + The (0, 1) range of the max cubic falloff handle + minHandle : float + The (0, 1) range of the min cubic falloff handle + minVal : float + The value past which the falloff is 0.0 + + Returns + ------- + + """ + self.splitType = "planar" + self.axis = axis + self.minVal = minVal + self.minHandle = minHandle + self.maxHandle = maxHandle + self.maxVal = maxVal + self.mapName = None + self._updateDCC() + + @stackable + def setMapData(self, mapName): + """Set the type/data for a map Falloff + + Parameters + ---------- + mapName : str + The name of the weightmap + + Returns + ------- + + """ + self.splitType = "map" + self.axis = None + self.minVal = None + self.minHandle = None + self.maxHandle = None + self.maxVal = None + self.mapName = mapName + self._updateDCC() + + @property + def splitType(self): + return self._splitType + + @splitType.setter + @stackable + def splitType(self, value): + self._splitType = str(value).lower() + for model in self.falloffModels: + model.itemDataChanged(self) + self._updateDCC() + + @property + def axis(self): + return self._axis + + @axis.setter + @stackable + def axis(self, value): + self._axis = value + for model in self.falloffModels: + model.itemDataChanged(self) + self._updateDCC() + + @property + def maxVal(self): + return self._maxVal + + @maxVal.setter + @stackable + def maxVal(self, value): + self._maxVal = value + for model in self.falloffModels: + model.itemDataChanged(self) + self._updateDCC() + + @property + def maxHandle(self): + return self._maxHandle + + @maxHandle.setter + @stackable + def maxHandle(self, value): + self._maxHandle = value + for model in self.falloffModels: + model.itemDataChanged(self) + self._updateDCC() + + @property + def minHandle(self): + return self._minHandle + + @minHandle.setter + @stackable + def minHandle(self, value): + self._minHandle = value + for model in self.falloffModels: + model.itemDataChanged(self) + self._updateDCC() + + @property + def minVal(self): + return self._minVal + + @minVal.setter + @stackable + def minVal(self, value): + self._minVal = value + for model in self.falloffModels: + model.itemDataChanged(self) + self._updateDCC() + + @property + def mapName(self): + return self._mapName + + @mapName.setter + @stackable + def mapName(self, value): + self._mapName = value + for model in self.falloffModels: + model.itemDataChanged(self) + self._updateDCC() + + def _updateDCC(self): + """ """ + self.DCC.setFalloffData( + self, + self.splitType, + self.axis, + self.minVal, + self.minHandle, + self.maxHandle, + self.maxVal, + self.mapName, + ) + + # Split code + @property + def bezier(self): + """Pre-build a factorization of the cubic bezier curve that is being used for a falloff + Based on method described at https://pomax.github.io/bezierinfo/#yforx + + Parameters + ---------- + + Returns + ------- + + """ + if self._bezier is None: + p0x = 0.0 + p1x = self.minHandle + p2x = self.maxHandle + p3x = 1.0 + + f = p1x - p0x + g = p3x - p2x + d = 3 * f + 3 * g - 2 + n = 2 * f + g - 1 + r = (n * n - f * d) / (d * d) + qq = (3 * f * d * n - 2 * n * n * n) / (d * d * d) + self._bezier = (qq, r, d, n) + return self._bezier + + def getMultiplier(self, xVal): + """Get the weight value for the given X + + Parameters + ---------- + xVal : float + The value to get the weight for + + Returns + ------- + : float + The weight + + """ + # Vertices are assumed to be at (0,0) and (1,1) + if xVal <= self.minVal: + return 0.0 + if xVal >= self.maxVal: + return 1.0 + + tVal = float(xVal - self.minVal) / float(self.maxVal - self.minVal) + qq, r, d, n = self.bezier + q = qq - tVal / d + discriminant = q * q - 4 * r * r * r + if discriminant >= 0: + pm = (discriminant**0.5) / 2 + w = (-q / 2 + pm) ** (1 / 3.0) + u = w + r / w + else: + theta = math.acos(-q / (2 * r ** (3 / 2.0))) + phi = theta / 3 + 4 * math.pi / 3 + u = 2 * r ** (0.5) * math.cos(phi) + t = u + n / d + t1 = 1 - t + return 3 * t1 * t**2 * 1 + t**3 * 1 + + def _setSearchRep(self): + """ """ + if self.axis.lower() == self.HORIZONTAL_AXIS.lower(): + self._search = self.HORIZONTAL_SPLIT + self._rep = self.HORIZONTAL_RESULTS + elif self.axis.lower() == self.VERTICAL_AXIS.lower(): + self._search = self.VERTICAL_SPLIT + self._rep = self.VERTICAL_RESULTS + elif self.axis.lower() == self.DEPTH_AXIS.lower(): + self._search = self.DEPTH_SPLIT + self._rep = self.DEPTH_RESULTS + + @property + def search(self): + """The values this fallof searches for""" + if self._search is None: + self._setSearchRep() + return self._search + + @property + def rep(self): + """The values this falloff replaces with""" + if self._rep is None: + self._setSearchRep() + return self._rep + + @property + def verts(self): + """Get the stored vertex values""" + return self._verts + + @verts.setter + def verts(self, vals): + """Input the vertices into this falloff and compute the weights + + Parameters + ---------- + vals : np.array + A (Nx3) numpy array of vertices + + Returns + ------- + + """ + if self.splitType != "map": + # Clear out any auto-computed weights + # when setting verts on a non-map falloff + self._weights = None + self._verts = vals + + @property + def weights(self): + """Get the per-vertex weight values""" + + if self._weights is None: + if self.splitType == "map": + raise ValueError( + "Attempted to auto-compute weights of a map falloff: {}".format( + self.name + ) + ) + + if self._verts is None: + raise ValueError( + "Attempted to auto-compute weights of a procedural falloff without setting verts: {0}".format( + self.name + ) + ) + + if self.axis.lower() == self.HORIZONTAL_AXIS.lower(): + component = self.HORIZONTAL_AXISINDEX + elif self.axis.lower() == self.VERTICAL_AXIS.lower(): + component = self.VERTICAL_AXISINDEX + elif self.axis.lower() == self.DEPTH_AXIS.lower(): + component = self.DEPTH_AXISINDEX + else: + raise ValueError("Falloff found with no axis set") + + self._weights = np.array( + [self.getMultiplier(v[component]) for v in self._verts] + ) + + return self._weights + + @weights.setter + def weights(self, val): + """Set the per-vertex weight values + + Parameters + ---------- + val : A list or numpy array of values between 0 and 1 + """ + self._weights = np.asarray(val) + + def getSidedName(self, name, sIdx): + """Take name to split along some axis, and replace the fields based on the index + For instance, this could take cp_X and return cp_L for sIdx=1 and cp_R for sIdx=2 + + Parameters + ---------- + name : str + The name to "split" with this falloff + sIdx : int + The index of the replacement value + + Returns + ------- + : str + The newly sided name + + """ + search = self.search + replace = self.rep[sIdx] + + nn = name + s = "{0}{1}{0}".format(self.SEP, search) + r = "{0}{1}{0}".format(self.SEP, replace) + nn = nn.replace(s, r) + + s = "{0}{1}".format(self.SEP, search) # handle Postfix + r = "{0}{1}".format(self.SEP, replace) + if nn.endswith(s): + nn = r.join(nn.rsplit(s, 1)) + + s = "{1}{0}".format(self.SEP, search) # handle Prefix + r = "{1}{0}".format(self.SEP, replace) + if nn.startswith(s): + nn = nn.replace(s, r, 1) + return nn + + def canRename(self, item): + """Check if the item can be renamed by this Falloff + + Parameters + ---------- + item : object + The named simplex object to check + + Returns + ------- + : bool + Whether this object can be renamed + + """ + nn = self.getSidedName(item.name, 0) + return nn != item.name + + def splitRename(self, item, sIdx): + """Actually run the rename for a particular item + + Parameters + ---------- + item : object + The named Simplex Item + sIdx : int + The replacement index + + Returns + ------- + + """ + from .combo import Combo + from .shape import Shape + from .slider import Slider + from .traversal import Traversal + + if isinstance(item, (Shape, Slider, Combo, Traversal)): + item.name = self.getSidedName(item.name, sIdx) + + def applyFalloff(self, shape, sIdx): + """Apply the falloff to the vertices of a shape + + Parameters + ---------- + shape : Shape + The shape to apply to + sIdx : int + The replacement index + + Returns + ------- + + """ + rest = self.simplex.restShape + restVerts = rest.verts + + weights = self.weights + if sIdx == 1: + weights = 1 - weights + + weightedDeltas = (shape.verts - restVerts) * weights[:, None] + shape.verts = weightedDeltas + restVerts diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index f6ba556e..fa66a4bb 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -1,297 +1,297 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use -from Qt.QtGui import QColor -from ..utils import nested -from .accessor import SimplexAccessor -from .stack import stackable - - -class Group(SimplexAccessor): - """Groups organize Simplex items - - Groups have no purpose in the solver. They don't do anything other than organize - the items in a system. - - Each group can hold only one type of item (Slider, Combo, or Traversal) - - Parameters - ---------- - name : str - The name of the group - simplex : Simplex - The Simplex system - groupType : type - The type that this group can hold - color : QColor - The color of this item in the Ui - - Returns - ------- - - """ - - classDepth = 1 - - def __init__(self, name, simplex, groupType, color=None): - super(Group, self).__init__(simplex) - from .combo import Combo - from .slider import Slider - from .traversal import Traversal - - color = QColor(128, 128, 128) if color is None else color - - with self.stack.store(self): - self._name = name - self.items = [] - self._buildIdx = None - self.expanded = {} - self.color = color - self.groupType = groupType - - mgrs = [ - model.insertItemManager(simplex, row=self._getInsertionRow()) - for model in self.models - ] - with nested(*mgrs): - if self.groupType is Slider: - self.simplex.sliderGroups.append(self) - elif self.groupType is Combo: - self.simplex.comboGroups.append(self) - elif self.groupType is Traversal: - self.simplex.traversalGroups.append(self) - - def _getInsertionRow(self): - """ """ - from .combo import Combo - from .slider import Slider - from .traversal import Traversal - - c = len(self.simplex.sliderGroups) - if self.groupType is Slider: - return c - c += len(self.simplex.comboGroups) - if self.groupType is Combo: - return c - c += len(self.simplex.traversalGroups) - if self.groupType is Traversal: - return c - - @property - def name(self): - """Get the name of the Group""" - return self._name - - @name.setter - @stackable - def name(self, value): - """Set the name of the Group - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self._name = value - for model in self.models: - model.itemDataChanged(self) - - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - - Returns - ------- - - """ - return self.items[row] - - def treeRow(self): - """ """ - return self.simplex.groups.index(self) - - def treeParent(self): - """ """ - return self.simplex - - def treeChildCount(self): - """ """ - return len(self.items) - - @classmethod - def createGroup(cls, name, simplex, things=None, groupType=None): - """Convenience method for creating a group - - Parameters - ---------- - name : str - The name to give the new Group - simpelx : Simplex - The Simplex system - things : [object - The things to add to this Group (Default value = None) - groupType : type - The type that the new Group can hold (Default value = None) - simplex : - - - Returns - ------- - - """ - g = cls(name, simplex, groupType) - if things is not None: - g.take(things) - return g - - @classmethod - def loadV2(cls, simplex, data): - """Load the data from a version2 formatted json dictionary - - Parameters - ---------- - simplex : Simplex - The Simplex system that's being built - data : dict - The chunk of the json dict used to build this object - - Returns - ------- - : Group - The specified Group - - """ - from .combo import Combo - from .slider import Slider - from .traversal import Traversal - - name = data["name"] - color = data.get("color", (0, 0, 0)) - typeName = data["type"] - if typeName == "Slider": - groupType = Slider - elif typeName == "Combo": - groupType = Combo - elif typeName == "Traversal": - groupType = Traversal - else: - raise RuntimeError("Malformed simplex json string: Improper group type") - return cls(name, simplex, groupType, QColor(*color)) - - def buildDefinition(self, simpDict, legacy): - """Output a dictionary definition of this object - - Parameters - ---------- - simpDict : dict - The dictionary that is being built - legacy : bool - Whether to write out the legacy definition, or the newer one - - Returns - ------- - - """ - if self._buildIdx is None: - self._buildIdx = len(simpDict["groups"]) - if legacy: - simpDict.setdefault("groups", []).append(self.name) - else: - x = { - "name": self.name, - "color": self.color.getRgb()[:3], - "type": self.groupType.__name__, - } - simpDict.setdefault("groups", []).append(x) - return self._buildIdx - - def clearBuildIndex(self): - """Clear the build index of this object - - The buildIndex is stored when building a definition dictionary - that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - - """ - self._buildIdx = None - - @stackable - def delete(self): - """Delete a group. Any objects in this group will be deleted""" - from .combo import Combo - from .slider import Slider - - if self.groupType is Slider: - if len(self.simplex.sliderGroups) == 1: - return - gList = self.simplex.sliderGroups - elif self.groupType is Combo: - if len(self.simplex.comboGroups) == 1: - return - gList = self.simplex.comboGroups - else: - raise RuntimeError("Somehow this group has no type") - - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - # Delete the children first - # Gotta iterate over copies of the lists - # as .delete removes the items from the list - for item in self.items[:]: - item.delete() - - gList.remove(self) - - @stackable - def take(self, things): - """Remove some items from their current groups and put them in this one - - Parameters - ---------- - things : [object - A list of things to put in this group - - Returns - ------- - - """ - if self.groupType is None: - self.groupType = type(things[0]) - - if not all(isinstance(i, self.groupType) for i in things): - raise ValueError( - "All items in this group must be of type: {}".format(self.groupType) - ) - - # do it this way instead of using set() to keep order - for thing in things: - if thing not in self.items: - thing.setGroup(self) +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint:disable=missing-docstring,unused-argument,no-self-use +from Qt.QtGui import QColor +from ..utils import nested +from .accessor import SimplexAccessor +from .stack import stackable + + +class Group(SimplexAccessor): + """Groups organize Simplex items + + Groups have no purpose in the solver. They don't do anything other than organize + the items in a system. + + Each group can hold only one type of item (Slider, Combo, or Traversal) + + Parameters + ---------- + name : str + The name of the group + simplex : Simplex + The Simplex system + groupType : type + The type that this group can hold + color : QColor + The color of this item in the Ui + + Returns + ------- + + """ + + classDepth = 1 + + def __init__(self, name, simplex, groupType, color=None): + super(Group, self).__init__(simplex) + from .combo import Combo + from .slider import Slider + from .traversal import Traversal + + color = QColor(128, 128, 128) if color is None else color + + with self.stack.store(self): + self._name = name + self.items = [] + self._buildIdx = None + self.expanded = {} + self.color = color + self.groupType = groupType + + mgrs = [ + model.insertItemManager(simplex, row=self._getInsertionRow()) + for model in self.models + ] + with nested(*mgrs): + if self.groupType is Slider: + self.simplex.sliderGroups.append(self) + elif self.groupType is Combo: + self.simplex.comboGroups.append(self) + elif self.groupType is Traversal: + self.simplex.traversalGroups.append(self) + + def _getInsertionRow(self): + """ """ + from .combo import Combo + from .slider import Slider + from .traversal import Traversal + + c = len(self.simplex.sliderGroups) + if self.groupType is Slider: + return c + c += len(self.simplex.comboGroups) + if self.groupType is Combo: + return c + c += len(self.simplex.traversalGroups) + if self.groupType is Traversal: + return c + + @property + def name(self): + """Get the name of the Group""" + return self._name + + @name.setter + @stackable + def name(self, value): + """Set the name of the Group + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self._name = value + for model in self.models: + model.itemDataChanged(self) + + def treeChild(self, row): + """ + + Parameters + ---------- + row : + + + Returns + ------- + + """ + return self.items[row] + + def treeRow(self): + """ """ + return self.simplex.groups.index(self) + + def treeParent(self): + """ """ + return self.simplex + + def treeChildCount(self): + """ """ + return len(self.items) + + @classmethod + def createGroup(cls, name, simplex, things=None, groupType=None): + """Convenience method for creating a group + + Parameters + ---------- + name : str + The name to give the new Group + simpelx : Simplex + The Simplex system + things : [object + The things to add to this Group (Default value = None) + groupType : type + The type that the new Group can hold (Default value = None) + simplex : + + + Returns + ------- + + """ + g = cls(name, simplex, groupType) + if things is not None: + g.take(things) + return g + + @classmethod + def loadV2(cls, simplex, data): + """Load the data from a version2 formatted json dictionary + + Parameters + ---------- + simplex : Simplex + The Simplex system that's being built + data : dict + The chunk of the json dict used to build this object + + Returns + ------- + : Group + The specified Group + + """ + from .combo import Combo + from .slider import Slider + from .traversal import Traversal + + name = data["name"] + color = data.get("color", (0, 0, 0)) + typeName = data["type"] + if typeName == "Slider": + groupType = Slider + elif typeName == "Combo": + groupType = Combo + elif typeName == "Traversal": + groupType = Traversal + else: + raise RuntimeError("Malformed simplex json string: Improper group type") + return cls(name, simplex, groupType, QColor(*color)) + + def buildDefinition(self, simpDict, legacy): + """Output a dictionary definition of this object + + Parameters + ---------- + simpDict : dict + The dictionary that is being built + legacy : bool + Whether to write out the legacy definition, or the newer one + + Returns + ------- + + """ + if self._buildIdx is None: + self._buildIdx = len(simpDict["groups"]) + if legacy: + simpDict.setdefault("groups", []).append(self.name) + else: + x = { + "name": self.name, + "color": self.color.getRgb()[:3], + "type": self.groupType.__name__, + } + simpDict.setdefault("groups", []).append(x) + return self._buildIdx + + def clearBuildIndex(self): + """Clear the build index of this object + + The buildIndex is stored when building a definition dictionary + that keeps track of its index for later referencing + + Parameters + ---------- + + Returns + ------- + + """ + self._buildIdx = None + + @stackable + def delete(self): + """Delete a group. Any objects in this group will be deleted""" + from .combo import Combo + from .slider import Slider + + if self.groupType is Slider: + if len(self.simplex.sliderGroups) == 1: + return + gList = self.simplex.sliderGroups + elif self.groupType is Combo: + if len(self.simplex.comboGroups) == 1: + return + gList = self.simplex.comboGroups + else: + raise RuntimeError("Somehow this group has no type") + + mgrs = [model.removeItemManager(self) for model in self.models] + with nested(*mgrs): + # Delete the children first + # Gotta iterate over copies of the lists + # as .delete removes the items from the list + for item in self.items[:]: + item.delete() + + gList.remove(self) + + @stackable + def take(self, things): + """Remove some items from their current groups and put them in this one + + Parameters + ---------- + things : [object + A list of things to put in this group + + Returns + ------- + + """ + if self.groupType is None: + self.groupType = type(things[0]) + + if not all(isinstance(i, self.groupType) for i in things): + raise ValueError( + "All items in this group must be of type: {}".format(self.groupType) + ) + + # do it this way instead of using set() to keep order + for thing in things: + if thing not in self.items: + thing.setGroup(self) diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index db0447f2..afb5a5a9 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -1,743 +1,743 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - - -# pylint:disable=missing-docstring,unused-argument,no-self-use -from ..utils import getNextName, nested -from .accessor import SimplexAccessor, SimplexTickAccessor -from .stack import stackable - - -class ProgPair(SimplexTickAccessor): - """ """ - - classDepth = 9 - - def __init__(self, simplex, shape, value): - super(ProgPair, self).__init__(simplex) - self.shape = shape - self._value = value - self.prog = None - self.minValue = -1.0 - self.maxValue = 1.0 - self.expanded = {} - if not shape.isRest and self not in self.shape.progPairs: - self.shape.progPairs.append(self) - - @property - def name(self): - """ """ - return self.shape.name - - @name.setter - def name(self, value): - """ - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self.shape.name = value - - def buildDefinition(self, simpDict, legacy): - """ - - Parameters - ---------- - simpDict : - - legacy : - - - Returns - ------- - - """ - idx = self.shape.buildDefinition(simpDict, legacy) - return idx, self.value - - def __lt__(self, other): - return self.value < other.value - - @property - def value(self): - """ """ - return self._value - - @value.setter - @stackable - def value(self, val): - """ - - Parameters - ---------- - val : - - - Returns - ------- - - """ - from .slider import Slider - - self._value = val - for model in self.models: - model.itemDataChanged(self) - if isinstance(self.prog.controller, Slider): - self.prog.controller.setRange() - - @stackable - def delete(self): - """ """ - ppairs = self.prog.pairs - ridx = ppairs.index(self) - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - pp = ppairs.pop(ridx) - if not pp.shape.isRest: - pp.shape.progPairs.remove(pp) - if not pp.shape.progPairs: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) - - def treeRow(self): - """ """ - return self.prog.pairs.index(self) - - def treeParent(self): - """ """ - from .slider import Slider - - par = self.prog - if isinstance(par.controller, Slider): - par = par.controller - return par - - def treeData(self, column): - """ - - Parameters - ---------- - column : - - - Returns - ------- - - """ - if column == 0: - return self.name - if column == 2: - return self.value - return None - - -class Progression(SimplexAccessor): - """A set of shapes to interpolate between - - A Progression is a collection of shape/value pairs, and an interpolation type. - Progressions don't exist on their own, they are always part of a higher-level object - like a Combo, Slider, or Traversal. The ProgPairs are always sorted by value - - Progressions should always have a shape at 0 (which is almost always the rest shape) - and a shape at either -1 or 1. - They can also have other shapes at any value between 0 and the extremes. - - Sliders give users direct control over the value that is passed to the progression. - Combos and Traversals use input values to control their progressions. - - Progressions can use different interpolations. - The simplest is 'linear', which blends in a straight line between shapes. - The 'spline' interp uses Catmull-Rom spline values. - The 'splitspline' builds separate Catmull-Rom splines for positive and negative values - - Parameters - ---------- - name : str - The name for the Progression. Usually just copies the name of its controller - simplex : Simplex - The Simplex system - pairs : [ProgPair - The ProgPairs that will make up this Progression. - If None, the a default Rest at 0.0 pair will be created. - interp : str - The interpolation for this Progression. Defaults to 'spline' - falloffs : [Falloff - A list of Fallofs to apply to the progression - Defaults to None - - Returns - ------- - - """ - - classDepth = 8 - interpTypes = ( - ("Linear", "linear"), - ("Spline", "spline"), - ("Split Spline", "splitspline"), - ) - - def __init__(self, name, simplex, pairs=None, interp="spline", falloffs=None): - super(Progression, self).__init__(simplex) - with self.stack.store(self): - self._name = name - self._interp = interp - self.falloffs = falloffs or [] - self.controller = None - - if pairs is None: - self.pairs = [ProgPair(self.simplex, self.simplex.restShape, 0.0)] - else: - self.pairs = pairs - - for pair in self.pairs: - pair.prog = self - - for falloff in self.falloffs: - falloff.children.append(self) - self._buildIdx = None - self.expanded = {} - - @property - def interp(self): - """Get the interp for this Progression""" - return self._interp - - @interp.setter - @stackable - def interp(self, value): - """Set the interp for this Progression - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self._interp = value - - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - - Returns - ------- - - """ - return self.pairs[row] - - def treeRow(self): - """ """ - from .combo import Combo - from .traversal import Traversal - - if isinstance(self.controller, Traversal): - # Show the progression after the mult and prog - return 2 - elif isinstance(self.controller, Combo): - # Show the progression after the comboPairs - return len(self.controller.pairs) - return None - - def treeParent(self): - """ """ - return self.controller - - def treeChildCount(self): - """ """ - return len(self.pairs) - - def treeData(self, column): - """ - - Parameters - ---------- - column : - - - Returns - ------- - - """ - if column == 0: - return "SHAPES" - return None - - def getShapeIndex(self, shape): - """Get the index of the given shape in this progression - - Parameters - ---------- - shape : Shape - The shape to get the index of - - Returns - ------- - : int - The index of the ProgPair that contains the given shape - - """ - for i, p in enumerate(self.pairs): - if p.shape == shape: - return i - raise ValueError("Provided shape:{0} is not in the list".format(shape.name)) - - def getShapes(self): - """Return the Shapes in this Progression - - Parameters - ---------- - - Returns - ------- - : type - ([Shape, ....]): The shapes in the Progression - - """ - return [i.shape for i in self.pairs] - - def getValues(self): - """Return the values in this Progression - - Parameters - ---------- - - Returns - ------- - : type - ([float, ....]): The values in the Progression - - """ - return [i.value for i in self.pairs] - - def getInsertIndex(self, tVal): - """Get the index to insert a pair with value tVal - - Parameters - ---------- - tVal : float - The value to get the insertion index for - - Returns - ------- - : int - The insertion index - - """ - values = self.getValues() - if not values: - return 0 - elif tVal <= values[0]: - return 0 - elif tVal >= values[-1]: - return len(self.pairs) - else: - for i in range(1, len(values)): - if values[i - 1] <= tVal < values[i]: - return i - return 0 - - def getShapeAtValue(self, val, tol=0.0001): - """Return the shape at the given value - - Parameters - ---------- - val : - float - tol : - float (Default value = 0.0001) - - Returns - ------- - : type - (Shape or None): The shape found with the given value, or None if nothing was found - - """ - for pp in self.pairs: - if abs(pp.value - val) < tol: - return pp.shape - return None - - @classmethod - def loadV2(cls, simplex, data): - """Load the data from a version2 formatted json dictionary - - Parameters - ---------- - simplex : Simplex - The Simplex system that's being built - data : dict - The chunk of the json dict used to build this object - - Returns - ------- - : Progression - The specified Progression - - """ - name = data["name"] - pairs = data["pairs"] - interp = data.get("interp", "spline") - foIdxs = data.get("falloffs", []) - pairs = [ProgPair(simplex, simplex.shapes[s], v) for s, v in pairs] - fos = [simplex.falloffs[i] for i in foIdxs] - return cls(name, simplex, pairs=pairs, interp=interp, falloffs=fos) - - def buildDefinition(self, simpDict, legacy): - """Output a dictionary definition of this object - - Parameters - ---------- - simpDict : dict - The dictionary that is being built - legacy : bool - Whether to write out the legacy definition, or the newer one - - Returns - ------- - - """ - if self._buildIdx is None: - idxPairs = [pair.buildDefinition(simpDict, legacy) for pair in self.pairs] - idxPairs.sort(key=lambda x: x[1]) - idxs, values = list(zip(*idxPairs)) - foIdxs = [f.buildDefinition(simpDict, legacy) for f in self.falloffs] - self._buildIdx = len(simpDict["progressions"]) - if legacy: - x = [self.name, idxs, values, self.interp, foIdxs] - simpDict.setdefault("progressions", []).append(x) - else: - x = { - "name": self.name, - "pairs": idxPairs, - "interp": self.interp, - "falloffs": foIdxs, - } - simpDict.setdefault("progressions", []).append(x) - return self._buildIdx - - def clearBuildIndex(self): - """Clear the build index of this object - - The buildIndex is stored when building a definition dictionary - that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - - """ - self._buildIdx = None - for pair in self.pairs: - pair.shape.clearBuildIndex() - for fo in self.falloffs: - fo.clearBuildIndex() - - @stackable - def moveShapeToProgression(self, shapePair): - """Remove the shapePair from its current progression and set it in a new progression - - Parameters - ---------- - shapePair : progPair - The ProgPair to take - shapePair): ### Moves Rows (Slider : - - Combo : - - - Returns - ------- - - """ - oldProg = shapePair.prog - oldProg.pairs.remove(shapePair) - self.pairs.append(shapePair) - shapePair.prog = self - - @stackable - def setShapesValues(self, values): - """Set all the Shape's values - - Parameters - ---------- - values : [float - The values to set - - Returns - ------- - - """ - from .slider import Slider - - for pp, val in zip(self.pairs, values): - pp.value = val - for model in self.models: - model.itemDataChanged(pp) - - if isinstance(self.controller, Slider): - self.controller.updateRange() - for model in self.models: - model.itemDataChanged(self.controller) - - def siblingRename(self, shape, newName, currentLinks): - """ - - Parameters - ---------- - shape : - - newName : - - currentLinks : - - - Returns - ------- - - """ - # This is part of the in-progress linked naming system - # get name change - pass - - @stackable - def addFalloff(self, falloff): - """Add a falloff to a slider's falloff list - - Parameters - ---------- - falloff : Falloff - The falloff to add - - Returns - ------- - - """ - if falloff not in self.falloffs: - self.falloffs.append(falloff) - falloff.children.append(self) - self.DCC.addProgFalloff(self, falloff) - - @stackable - def removeFalloff(self, falloff): - """Remove a falloff from a slider's falloff list - - Parameters - ---------- - falloff : Falloff - The falloff to remove - - Returns - ------- - - """ - if falloff in self.falloffs: - self.falloffs.remove(falloff) - falloff.children.remove(self) - self.DCC.removeProgFalloff(self, falloff) - - @stackable - def createShape(self, shapeName=None, tVal=None): - """Create a shape and add it to a progression - - Parameters - ---------- - shapeName : str or None - The name to give the shape - If None, give it a default value - tVal : float or None - The value to give the new ProgPair - if None, give it a "smart" default - - Returns - ------- - : ProgPair - The newly created ProgPair - - """ - from .slider import Slider - - pp, idx = self.newProgPair(shapeName, tVal) - mgrs = [model.insertItemManager(self, idx) for model in self.models] - with nested(*mgrs): - pp.prog = self - self.pairs.insert(idx, pp) - - if isinstance(self.controller, Slider): - self.controller.updateRange() - - return pp - - def newProgPair(self, shapeName=None, tVal=None): - """Create a shape and DO NOT add it to a progression - - Parameters - ---------- - shapeName : str or None - The name to give the shape - If None, give it a default value - tVal : float or None - The value to give the new ProgPair - if None, give it a "smart" default - - Returns - ------- - : ProgPair - The newly created ProgPair - : int - The insertion index for this ProgPair into this Progression - - """ - from .shape import Shape - - if tVal is None: - tVal = self.guessNextTVal() - - if shapeName is None: - if abs(tVal) == 1.0: - shapeName = self.controller.name - else: - neg = "n" if tVal < 0.0 else "" - shapeName = "{0}_{1}{2}".format( - self.controller.name, neg, int(abs(tVal) * 100) - ) - - currentNames = [i.name for i in self.simplex.shapes] - shapeName = getNextName(shapeName, currentNames) - - idx = self.getInsertIndex(tVal) - shape = Shape(shapeName, self.simplex) - pp = ProgPair(self.simplex, shape, tVal) - return pp, idx - - def guessNextTVal(self): - """Given the current progression values, make an educated guess what's next. - - Parameters - ---------- - - Returns - ------- - : float - The "smart" guess for the next tVal - - """ - # The question remains if negative or - # intermediate values are more important - # I think intermediate - vals = [i.value for i in self.pairs] - mnv = min(vals) - mxv = max(vals) - if mnv == 0.0 and mxv == 1.0: - for c in [0.5, 0.25, 0.75, -1.0]: - if c not in vals: - return c - if mnv == -1.0 and mxv == 1.0: - for c in [0.5, -0.5, 0.25, -0.25, 0.75, -0.75]: - if c not in vals: - return c - return 1.0 - - @stackable - def deleteShape(self, shape): - """Delete a shape from the system and the DCC - - Parameters - ---------- - shape : Shape - The shape to delete - - Returns - ------- - - """ - ridx = None - for i, pp in enumerate(self.pairs): - if pp.shape == shape: - ridx = i - if ridx is None: - raise RuntimeError("Shape does not exist to remove") - - pp = self.pairs[ridx] - mgrs = [model.removeItemManager(pp) for model in self.models] - with nested(*mgrs): - self.pairs.pop(ridx) - if not shape.isRest: - self.simplex.shapes.remove(shape) - self.DCC.deleteShape(shape) - - @stackable - def delete(self): - """Delete the Progression and all its Shapes""" - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - for pp in self.pairs[:]: - if pp.shape.isRest: - continue - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) - - def getRange(self): - """Get the range for this Progression - - Parameters - ---------- - - Returns - ------- - : float - The minimum value - : float - The maximum value - - """ - vals = [i.value for i in self.pairs] - return min(vals), max(vals) - - def getExtremePairs(self): - """Get the ProgPairs where the value is -1 or 1 - - Parameters - ---------- - - Returns - ------- - : [ProgPair, ...] - ProgPairs whose values are -1 or 1 - - """ - ret = [] - for pp in self.pairs: - if abs(pp.value) != 1.0: - continue - ret.append(pp) - return ret +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + + +# pylint:disable=missing-docstring,unused-argument,no-self-use +from ..utils import getNextName, nested +from .accessor import SimplexAccessor, SimplexTickAccessor +from .stack import stackable + + +class ProgPair(SimplexTickAccessor): + """ """ + + classDepth = 9 + + def __init__(self, simplex, shape, value): + super(ProgPair, self).__init__(simplex) + self.shape = shape + self._value = value + self.prog = None + self.minValue = -1.0 + self.maxValue = 1.0 + self.expanded = {} + if not shape.isRest and self not in self.shape.progPairs: + self.shape.progPairs.append(self) + + @property + def name(self): + """ """ + return self.shape.name + + @name.setter + def name(self, value): + """ + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self.shape.name = value + + def buildDefinition(self, simpDict, legacy): + """ + + Parameters + ---------- + simpDict : + + legacy : + + + Returns + ------- + + """ + idx = self.shape.buildDefinition(simpDict, legacy) + return idx, self.value + + def __lt__(self, other): + return self.value < other.value + + @property + def value(self): + """ """ + return self._value + + @value.setter + @stackable + def value(self, val): + """ + + Parameters + ---------- + val : + + + Returns + ------- + + """ + from .slider import Slider + + self._value = val + for model in self.models: + model.itemDataChanged(self) + if isinstance(self.prog.controller, Slider): + self.prog.controller.setRange() + + @stackable + def delete(self): + """ """ + ppairs = self.prog.pairs + ridx = ppairs.index(self) + mgrs = [model.removeItemManager(self) for model in self.models] + with nested(*mgrs): + pp = ppairs.pop(ridx) + if not pp.shape.isRest: + pp.shape.progPairs.remove(pp) + if not pp.shape.progPairs: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) + + def treeRow(self): + """ """ + return self.prog.pairs.index(self) + + def treeParent(self): + """ """ + from .slider import Slider + + par = self.prog + if isinstance(par.controller, Slider): + par = par.controller + return par + + def treeData(self, column): + """ + + Parameters + ---------- + column : + + + Returns + ------- + + """ + if column == 0: + return self.name + if column == 2: + return self.value + return None + + +class Progression(SimplexAccessor): + """A set of shapes to interpolate between + + A Progression is a collection of shape/value pairs, and an interpolation type. + Progressions don't exist on their own, they are always part of a higher-level object + like a Combo, Slider, or Traversal. The ProgPairs are always sorted by value + + Progressions should always have a shape at 0 (which is almost always the rest shape) + and a shape at either -1 or 1. + They can also have other shapes at any value between 0 and the extremes. + + Sliders give users direct control over the value that is passed to the progression. + Combos and Traversals use input values to control their progressions. + + Progressions can use different interpolations. + The simplest is 'linear', which blends in a straight line between shapes. + The 'spline' interp uses Catmull-Rom spline values. + The 'splitspline' builds separate Catmull-Rom splines for positive and negative values + + Parameters + ---------- + name : str + The name for the Progression. Usually just copies the name of its controller + simplex : Simplex + The Simplex system + pairs : [ProgPair + The ProgPairs that will make up this Progression. + If None, the a default Rest at 0.0 pair will be created. + interp : str + The interpolation for this Progression. Defaults to 'spline' + falloffs : [Falloff + A list of Fallofs to apply to the progression + Defaults to None + + Returns + ------- + + """ + + classDepth = 8 + interpTypes = ( + ("Linear", "linear"), + ("Spline", "spline"), + ("Split Spline", "splitspline"), + ) + + def __init__(self, name, simplex, pairs=None, interp="spline", falloffs=None): + super(Progression, self).__init__(simplex) + with self.stack.store(self): + self._name = name + self._interp = interp + self.falloffs = falloffs or [] + self.controller = None + + if pairs is None: + self.pairs = [ProgPair(self.simplex, self.simplex.restShape, 0.0)] + else: + self.pairs = pairs + + for pair in self.pairs: + pair.prog = self + + for falloff in self.falloffs: + falloff.children.append(self) + self._buildIdx = None + self.expanded = {} + + @property + def interp(self): + """Get the interp for this Progression""" + return self._interp + + @interp.setter + @stackable + def interp(self, value): + """Set the interp for this Progression + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self._interp = value + + def treeChild(self, row): + """ + + Parameters + ---------- + row : + + + Returns + ------- + + """ + return self.pairs[row] + + def treeRow(self): + """ """ + from .combo import Combo + from .traversal import Traversal + + if isinstance(self.controller, Traversal): + # Show the progression after the mult and prog + return 2 + elif isinstance(self.controller, Combo): + # Show the progression after the comboPairs + return len(self.controller.pairs) + return None + + def treeParent(self): + """ """ + return self.controller + + def treeChildCount(self): + """ """ + return len(self.pairs) + + def treeData(self, column): + """ + + Parameters + ---------- + column : + + + Returns + ------- + + """ + if column == 0: + return "SHAPES" + return None + + def getShapeIndex(self, shape): + """Get the index of the given shape in this progression + + Parameters + ---------- + shape : Shape + The shape to get the index of + + Returns + ------- + : int + The index of the ProgPair that contains the given shape + + """ + for i, p in enumerate(self.pairs): + if p.shape == shape: + return i + raise ValueError("Provided shape:{0} is not in the list".format(shape.name)) + + def getShapes(self): + """Return the Shapes in this Progression + + Parameters + ---------- + + Returns + ------- + : type + ([Shape, ....]): The shapes in the Progression + + """ + return [i.shape for i in self.pairs] + + def getValues(self): + """Return the values in this Progression + + Parameters + ---------- + + Returns + ------- + : type + ([float, ....]): The values in the Progression + + """ + return [i.value for i in self.pairs] + + def getInsertIndex(self, tVal): + """Get the index to insert a pair with value tVal + + Parameters + ---------- + tVal : float + The value to get the insertion index for + + Returns + ------- + : int + The insertion index + + """ + values = self.getValues() + if not values: + return 0 + elif tVal <= values[0]: + return 0 + elif tVal >= values[-1]: + return len(self.pairs) + else: + for i in range(1, len(values)): + if values[i - 1] <= tVal < values[i]: + return i + return 0 + + def getShapeAtValue(self, val, tol=0.0001): + """Return the shape at the given value + + Parameters + ---------- + val : + float + tol : + float (Default value = 0.0001) + + Returns + ------- + : type + (Shape or None): The shape found with the given value, or None if nothing was found + + """ + for pp in self.pairs: + if abs(pp.value - val) < tol: + return pp.shape + return None + + @classmethod + def loadV2(cls, simplex, data): + """Load the data from a version2 formatted json dictionary + + Parameters + ---------- + simplex : Simplex + The Simplex system that's being built + data : dict + The chunk of the json dict used to build this object + + Returns + ------- + : Progression + The specified Progression + + """ + name = data["name"] + pairs = data["pairs"] + interp = data.get("interp", "spline") + foIdxs = data.get("falloffs", []) + pairs = [ProgPair(simplex, simplex.shapes[s], v) for s, v in pairs] + fos = [simplex.falloffs[i] for i in foIdxs] + return cls(name, simplex, pairs=pairs, interp=interp, falloffs=fos) + + def buildDefinition(self, simpDict, legacy): + """Output a dictionary definition of this object + + Parameters + ---------- + simpDict : dict + The dictionary that is being built + legacy : bool + Whether to write out the legacy definition, or the newer one + + Returns + ------- + + """ + if self._buildIdx is None: + idxPairs = [pair.buildDefinition(simpDict, legacy) for pair in self.pairs] + idxPairs.sort(key=lambda x: x[1]) + idxs, values = list(zip(*idxPairs)) + foIdxs = [f.buildDefinition(simpDict, legacy) for f in self.falloffs] + self._buildIdx = len(simpDict["progressions"]) + if legacy: + x = [self.name, idxs, values, self.interp, foIdxs] + simpDict.setdefault("progressions", []).append(x) + else: + x = { + "name": self.name, + "pairs": idxPairs, + "interp": self.interp, + "falloffs": foIdxs, + } + simpDict.setdefault("progressions", []).append(x) + return self._buildIdx + + def clearBuildIndex(self): + """Clear the build index of this object + + The buildIndex is stored when building a definition dictionary + that keeps track of its index for later referencing + + Parameters + ---------- + + Returns + ------- + + """ + self._buildIdx = None + for pair in self.pairs: + pair.shape.clearBuildIndex() + for fo in self.falloffs: + fo.clearBuildIndex() + + @stackable + def moveShapeToProgression(self, shapePair): + """Remove the shapePair from its current progression and set it in a new progression + + Parameters + ---------- + shapePair : progPair + The ProgPair to take + shapePair): ### Moves Rows (Slider : + + Combo : + + + Returns + ------- + + """ + oldProg = shapePair.prog + oldProg.pairs.remove(shapePair) + self.pairs.append(shapePair) + shapePair.prog = self + + @stackable + def setShapesValues(self, values): + """Set all the Shape's values + + Parameters + ---------- + values : [float + The values to set + + Returns + ------- + + """ + from .slider import Slider + + for pp, val in zip(self.pairs, values): + pp.value = val + for model in self.models: + model.itemDataChanged(pp) + + if isinstance(self.controller, Slider): + self.controller.updateRange() + for model in self.models: + model.itemDataChanged(self.controller) + + def siblingRename(self, shape, newName, currentLinks): + """ + + Parameters + ---------- + shape : + + newName : + + currentLinks : + + + Returns + ------- + + """ + # This is part of the in-progress linked naming system + # get name change + pass + + @stackable + def addFalloff(self, falloff): + """Add a falloff to a slider's falloff list + + Parameters + ---------- + falloff : Falloff + The falloff to add + + Returns + ------- + + """ + if falloff not in self.falloffs: + self.falloffs.append(falloff) + falloff.children.append(self) + self.DCC.addProgFalloff(self, falloff) + + @stackable + def removeFalloff(self, falloff): + """Remove a falloff from a slider's falloff list + + Parameters + ---------- + falloff : Falloff + The falloff to remove + + Returns + ------- + + """ + if falloff in self.falloffs: + self.falloffs.remove(falloff) + falloff.children.remove(self) + self.DCC.removeProgFalloff(self, falloff) + + @stackable + def createShape(self, shapeName=None, tVal=None): + """Create a shape and add it to a progression + + Parameters + ---------- + shapeName : str or None + The name to give the shape + If None, give it a default value + tVal : float or None + The value to give the new ProgPair + if None, give it a "smart" default + + Returns + ------- + : ProgPair + The newly created ProgPair + + """ + from .slider import Slider + + pp, idx = self.newProgPair(shapeName, tVal) + mgrs = [model.insertItemManager(self, idx) for model in self.models] + with nested(*mgrs): + pp.prog = self + self.pairs.insert(idx, pp) + + if isinstance(self.controller, Slider): + self.controller.updateRange() + + return pp + + def newProgPair(self, shapeName=None, tVal=None): + """Create a shape and DO NOT add it to a progression + + Parameters + ---------- + shapeName : str or None + The name to give the shape + If None, give it a default value + tVal : float or None + The value to give the new ProgPair + if None, give it a "smart" default + + Returns + ------- + : ProgPair + The newly created ProgPair + : int + The insertion index for this ProgPair into this Progression + + """ + from .shape import Shape + + if tVal is None: + tVal = self.guessNextTVal() + + if shapeName is None: + if abs(tVal) == 1.0: + shapeName = self.controller.name + else: + neg = "n" if tVal < 0.0 else "" + shapeName = "{0}_{1}{2}".format( + self.controller.name, neg, int(abs(tVal) * 100) + ) + + currentNames = [i.name for i in self.simplex.shapes] + shapeName = getNextName(shapeName, currentNames) + + idx = self.getInsertIndex(tVal) + shape = Shape(shapeName, self.simplex) + pp = ProgPair(self.simplex, shape, tVal) + return pp, idx + + def guessNextTVal(self): + """Given the current progression values, make an educated guess what's next. + + Parameters + ---------- + + Returns + ------- + : float + The "smart" guess for the next tVal + + """ + # The question remains if negative or + # intermediate values are more important + # I think intermediate + vals = [i.value for i in self.pairs] + mnv = min(vals) + mxv = max(vals) + if mnv == 0.0 and mxv == 1.0: + for c in [0.5, 0.25, 0.75, -1.0]: + if c not in vals: + return c + if mnv == -1.0 and mxv == 1.0: + for c in [0.5, -0.5, 0.25, -0.25, 0.75, -0.75]: + if c not in vals: + return c + return 1.0 + + @stackable + def deleteShape(self, shape): + """Delete a shape from the system and the DCC + + Parameters + ---------- + shape : Shape + The shape to delete + + Returns + ------- + + """ + ridx = None + for i, pp in enumerate(self.pairs): + if pp.shape == shape: + ridx = i + if ridx is None: + raise RuntimeError("Shape does not exist to remove") + + pp = self.pairs[ridx] + mgrs = [model.removeItemManager(pp) for model in self.models] + with nested(*mgrs): + self.pairs.pop(ridx) + if not shape.isRest: + self.simplex.shapes.remove(shape) + self.DCC.deleteShape(shape) + + @stackable + def delete(self): + """Delete the Progression and all its Shapes""" + mgrs = [model.removeItemManager(self) for model in self.models] + with nested(*mgrs): + for pp in self.pairs[:]: + if pp.shape.isRest: + continue + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) + + def getRange(self): + """Get the range for this Progression + + Parameters + ---------- + + Returns + ------- + : float + The minimum value + : float + The maximum value + + """ + vals = [i.value for i in self.pairs] + return min(vals), max(vals) + + def getExtremePairs(self): + """Get the ProgPairs where the value is -1 or 1 + + Parameters + ---------- + + Returns + ------- + : [ProgPair, ...] + ProgPairs whose values are -1 or 1 + + """ + ret = [] + for pp in self.pairs: + if abs(pp.value) != 1.0: + continue + ret.append(pp) + return ret diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index 421f4473..37a842cc 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -1,443 +1,443 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - - -from ..interface import DCC, undoContext - -# pylint:disable=missing-docstring,unused-argument,no-self-use -from Qt.QtGui import QColor -from .accessor import SimplexAccessor -from .stack import stackable - - -class Shape(SimplexAccessor): - """A representation of a single blendshape - - For every Shape object in a system, there will be one blendshape. - The Simplex solver takes an ordered list of Slider values, and outputs - an ordered list of shape values. - - Shapes hold references to their DCC objects, and can also hold - the vertex positions in certain cases - - Parameters - ---------- - name : str - The name for the new Shape - simplex : Simplex - The Simplex system - create : bool - Whether to create the DCC Shape, or look for it already in-scene - color : QColor - The color of this item in the Ui - - Returns - ------- - - """ - - classDepth = 10 - - def __init__(self, name, simplex, create=True, color=None): - super(Shape, self).__init__(simplex) - color = QColor(128, 128, 128) if color is None else color - with self.stack.store(self): - self._thing = None - self._verts = None - self._thingRepr = None - self._name = name - self._buildIdx = None - simplex.shapes.append(self) - self.isRest = False - self.expanded = {} - self.color = color - self.progPairs = [] - - newThing = self.DCC.getShapeThing(self._name) - if newThing is None: - if create: - self.thing = self.DCC.createShape(self) - else: - raise RuntimeError( - "Unable to find existing shape: {0}".format(self.name) - ) - else: - self.thing = newThing - - @classmethod - def createShape(cls, name, simplex, slider=None): - """Convenience method for creating a new shape - This will create all required parent objects to have a new shape - - Parameters - ---------- - name : str - The name for the new Shape - simplex : Simplex - The Simplex system - slider : Slider or None - The slider to add this shape to. - If None, A new Slider will be created (Default value = None) - - Returns - ------- - : Shape - The new Shape - - """ - if simplex.restShape is None: - raise RuntimeError("Simplex system is missing rest shape") - - if slider is None: - # Implicitly creates a shape - from .slider import Slider - - slider = Slider.createSlider(name, simplex) - for p in slider.prog.pairs: - if p.shape.name == name: - return p.shape - raise RuntimeError("Problem creating shape with proper name") - else: - if slider.simplex != simplex: - raise RuntimeError("Slider does not belong to the provided Simplex") - tVal = slider.prog.guessNextTVal() - pp = slider.prog.createShape(name, tVal) - return pp.shape - - @classmethod - def buildRest(cls, simplex): - """Create/find the system's rest shape - - Parameters - ---------- - simplex : Simplex - The Simplex system - - Returns - ------- - : Shape - The system's rest Shape - - """ - rest = cls(simplex.getRestName(), simplex, create=True) - rest.isRest = True - return rest - - @property - def name(self): - """Get the Shape's name""" - return self._name - - @name.setter - @stackable - def name(self, value): - """Set the Shape's name - - Parameters - ---------- - value : - - - Returns - ------- - - """ - if value == self._name: - return - self.DCC.renameShape(self, value) - self._name = value - for model in self.models: - model.itemDataChanged(self) - - def strippedName(self): - """Get the name of this shape with any progressive numbers stripped from the end""" - sp = self.name.split("_") - if self.isNumberField(sp[-1]): - sp = sp[:-1] - return "_".join(sp) - - def _buildLinkedRename(self, newName, maxDepth, currentLinks): - """ - - Parameters - ---------- - newName : - - maxDepth : - - currentLinks : - - - Returns - ------- - - """ - # Now that all the bookkeeping has been handled by the main method - # I can handle recursing for the object specific stuff here - shape = None # TEMP - - from .combo import Combo - from .slider import Slider - from .traversal import Traversal - - for pp in self.progPairs: - currentLinks = pp.prog.siblingRename(shape, newName, currentLinks) - - ctrl = pp.prog.controller - if isinstance(ctrl, Slider): - nn = None - currentLinks = ctrl.buildLinkedRename( - nn, maxDepth=maxDepth - 1, currentLinks=currentLinks - ) - elif isinstance(ctrl, Combo): - nn = None - currentLinks = ctrl.buildLinkedRename( - nn, maxDepth=maxDepth - 1, currentLinks=currentLinks - ) - elif isinstance(ctrl, Traversal): - nn = None - currentLinks = ctrl.buildLinkedRename( - nn, maxDepth=maxDepth - 1, currentLinks=currentLinks - ) - - return currentLinks - - # First, check for a slider rename, - # if so, recurse into that slider - # Check for combo renames (because combos use the shape names) - - # if isinstance(item, Shape): - # Check if the parent is a slider - # Check if the slider needs renamed - # Check if the item's siblings need renamed - # Check if there are any combos that depend on this shape name - # If so, rename *both* the combo and its linked children - # Check if the parent is a combo - # Check if the combo needs renamed - # If so, check if the item's siblings need renamed too - # Check if the parent is a traversal - # Check if the traversal needs renamed - # If so, check if the item's siblings need renamed too - # elif isinstance(item, Slider): - # Check if the name change is linked to any of my shapes - # Go through the shape linked rename for one of those instead, maybe? - # There are possible ambiguities if you do a *full* slider rename - # with both positive and negatively named shapes. - # Otherwise - # Check for linked combos, and rename down that branch - # Check for linked traversals, and rename down that branch - # elif isinstance(item, Combo): - # pass - # elif isinstance(item, Traversal): - # pass - - @property - def thing(self): - """Get the stored reference to the DCC object""" - # if this is a deepcopied object, then self._thing will - # be None. Rebuild the thing connection by its representation - if self._thing is None and self._thingRepr: - self._thing = DCC.loadPersistentShape(self._thingRepr) - return self._thing - - @thing.setter - def thing(self, value): - """Set the stored reference to the DCC object - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self._thing = value - self._thingRepr = self.DCC.getPersistentShape(value) - - @classmethod - def loadV2(cls, simplex, data, create): - """Load the data from a version2 formatted json dictionary - - Parameters - ---------- - simplex : Simplex - The Simplex system that's being built - data : dict - The chunk of the json dict used to build this object - create : bool - Whether to create the DCC Shape, or look for it already in-scene - - Returns - ------- - : Shape - The specified Shape - - """ - return cls(data["name"], simplex, create, QColor(*data.get("color", (0, 0, 0)))) - - def buildDefinition(self, simpDict, legacy): - """Output a dictionary definition of this object - - Parameters - ---------- - simpDict : dict - The dictionary that is being built - legacy : bool - Whether to write out the legacy definition, or the newer one - - Returns - ------- - - """ - if self._buildIdx is None: - self._buildIdx = len(simpDict["shapes"]) - if legacy: - simpDict.setdefault("shapes", []).append(self.name) - else: - x = { - "name": self.name, - "color": self.color.getRgb()[:3], - } - simpDict.setdefault("shapes", []).append(x) - return self._buildIdx - - def clearBuildIndex(self): - """Clear the build index of this object - - The buildIndex is stored when building a definition dictionary - that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - - """ - self._buildIdx = None - - def zeroShape(self): - """Set the shape to be equal to the rest shape""" - self.DCC.zeroShape(self) - - @staticmethod - def zeroShapes(shapes): - """Set the shapes to be equal to the rest shape - - Parameters - ---------- - shapes : [Shape - Shapes to be zeroed - - Returns - ------- - - """ - for shape in shapes: - if not shape.isRest: - shape.zeroShape() - - def connectShape(self, mesh=None, live=False, delete=False): - """Force a shape to match a mesh - The "connect shape" button is: mesh=None, delete=True - The "match shape" button is: mesh=someMesh, delete=False - There is a possibility of a "make live" button: live=True, delete=False - - Parameters - ---------- - mesh : object or None - The DCC Mesh object. If None, it's searched for by name in scene (Default value = None) - live : bool - Whether or not to create a live connection in the DCC. Defaults False - delete : bool - Whether to delete the DCC Mesh after its connection. Defaults False - - Returns - ------- - - """ - self.DCC.connectShape(self, mesh, live, delete) - - @staticmethod - def connectShapes(shapes, meshes, live=False, delete=False): - """Connect multiple meshes to multiple Shapes - - Parameters - ---------- - shapes : [Shape - The shapes to connect to - meshes : [object - The DCC Meshes - live : bool - Whether or not to create a live connection in the DCC. Defaults False - delete : bool - Whether to delete the DCC Mesh after its connection. Defaults False - - Returns - ------- - - """ - with undoContext(): - for shape, mesh in zip(shapes, meshes): - shape.connectShape(mesh, live, delete) - - @staticmethod - def isNumberField(val): - """A utility function to check if a field is numeric - Also, this allows for the "n" prefix for negative numbers because - many DCC's don't allow "-" in an object name - - Parameters - ---------- - val : str - The string to check - - Returns - ------- - : bool - Whether the field is numeric - - """ - if not val: - return False - if val[0].lower() == "n": - val = val[1:] - return val.isdigit() - - @property - def verts(self): - """Get the stored vertices""" - if self._verts is None: - self._verts = self.DCC.getShapeVertices(self) - return self._verts - - @verts.setter - def verts(self, value): - """Set the stored vertices - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self._verts = value +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + + +from ..interface import DCC, undoContext + +# pylint:disable=missing-docstring,unused-argument,no-self-use +from Qt.QtGui import QColor +from .accessor import SimplexAccessor +from .stack import stackable + + +class Shape(SimplexAccessor): + """A representation of a single blendshape + + For every Shape object in a system, there will be one blendshape. + The Simplex solver takes an ordered list of Slider values, and outputs + an ordered list of shape values. + + Shapes hold references to their DCC objects, and can also hold + the vertex positions in certain cases + + Parameters + ---------- + name : str + The name for the new Shape + simplex : Simplex + The Simplex system + create : bool + Whether to create the DCC Shape, or look for it already in-scene + color : QColor + The color of this item in the Ui + + Returns + ------- + + """ + + classDepth = 10 + + def __init__(self, name, simplex, create=True, color=None): + super(Shape, self).__init__(simplex) + color = QColor(128, 128, 128) if color is None else color + with self.stack.store(self): + self._thing = None + self._verts = None + self._thingRepr = None + self._name = name + self._buildIdx = None + simplex.shapes.append(self) + self.isRest = False + self.expanded = {} + self.color = color + self.progPairs = [] + + newThing = self.DCC.getShapeThing(self._name) + if newThing is None: + if create: + self.thing = self.DCC.createShape(self) + else: + raise RuntimeError( + "Unable to find existing shape: {0}".format(self.name) + ) + else: + self.thing = newThing + + @classmethod + def createShape(cls, name, simplex, slider=None): + """Convenience method for creating a new shape + This will create all required parent objects to have a new shape + + Parameters + ---------- + name : str + The name for the new Shape + simplex : Simplex + The Simplex system + slider : Slider or None + The slider to add this shape to. + If None, A new Slider will be created (Default value = None) + + Returns + ------- + : Shape + The new Shape + + """ + if simplex.restShape is None: + raise RuntimeError("Simplex system is missing rest shape") + + if slider is None: + # Implicitly creates a shape + from .slider import Slider + + slider = Slider.createSlider(name, simplex) + for p in slider.prog.pairs: + if p.shape.name == name: + return p.shape + raise RuntimeError("Problem creating shape with proper name") + else: + if slider.simplex != simplex: + raise RuntimeError("Slider does not belong to the provided Simplex") + tVal = slider.prog.guessNextTVal() + pp = slider.prog.createShape(name, tVal) + return pp.shape + + @classmethod + def buildRest(cls, simplex): + """Create/find the system's rest shape + + Parameters + ---------- + simplex : Simplex + The Simplex system + + Returns + ------- + : Shape + The system's rest Shape + + """ + rest = cls(simplex.getRestName(), simplex, create=True) + rest.isRest = True + return rest + + @property + def name(self): + """Get the Shape's name""" + return self._name + + @name.setter + @stackable + def name(self, value): + """Set the Shape's name + + Parameters + ---------- + value : + + + Returns + ------- + + """ + if value == self._name: + return + self.DCC.renameShape(self, value) + self._name = value + for model in self.models: + model.itemDataChanged(self) + + def strippedName(self): + """Get the name of this shape with any progressive numbers stripped from the end""" + sp = self.name.split("_") + if self.isNumberField(sp[-1]): + sp = sp[:-1] + return "_".join(sp) + + def _buildLinkedRename(self, newName, maxDepth, currentLinks): + """ + + Parameters + ---------- + newName : + + maxDepth : + + currentLinks : + + + Returns + ------- + + """ + # Now that all the bookkeeping has been handled by the main method + # I can handle recursing for the object specific stuff here + shape = None # TEMP + + from .combo import Combo + from .slider import Slider + from .traversal import Traversal + + for pp in self.progPairs: + currentLinks = pp.prog.siblingRename(shape, newName, currentLinks) + + ctrl = pp.prog.controller + if isinstance(ctrl, Slider): + nn = None + currentLinks = ctrl.buildLinkedRename( + nn, maxDepth=maxDepth - 1, currentLinks=currentLinks + ) + elif isinstance(ctrl, Combo): + nn = None + currentLinks = ctrl.buildLinkedRename( + nn, maxDepth=maxDepth - 1, currentLinks=currentLinks + ) + elif isinstance(ctrl, Traversal): + nn = None + currentLinks = ctrl.buildLinkedRename( + nn, maxDepth=maxDepth - 1, currentLinks=currentLinks + ) + + return currentLinks + + # First, check for a slider rename, + # if so, recurse into that slider + # Check for combo renames (because combos use the shape names) + + # if isinstance(item, Shape): + # Check if the parent is a slider + # Check if the slider needs renamed + # Check if the item's siblings need renamed + # Check if there are any combos that depend on this shape name + # If so, rename *both* the combo and its linked children + # Check if the parent is a combo + # Check if the combo needs renamed + # If so, check if the item's siblings need renamed too + # Check if the parent is a traversal + # Check if the traversal needs renamed + # If so, check if the item's siblings need renamed too + # elif isinstance(item, Slider): + # Check if the name change is linked to any of my shapes + # Go through the shape linked rename for one of those instead, maybe? + # There are possible ambiguities if you do a *full* slider rename + # with both positive and negatively named shapes. + # Otherwise + # Check for linked combos, and rename down that branch + # Check for linked traversals, and rename down that branch + # elif isinstance(item, Combo): + # pass + # elif isinstance(item, Traversal): + # pass + + @property + def thing(self): + """Get the stored reference to the DCC object""" + # if this is a deepcopied object, then self._thing will + # be None. Rebuild the thing connection by its representation + if self._thing is None and self._thingRepr: + self._thing = DCC.loadPersistentShape(self._thingRepr) + return self._thing + + @thing.setter + def thing(self, value): + """Set the stored reference to the DCC object + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self._thing = value + self._thingRepr = self.DCC.getPersistentShape(value) + + @classmethod + def loadV2(cls, simplex, data, create): + """Load the data from a version2 formatted json dictionary + + Parameters + ---------- + simplex : Simplex + The Simplex system that's being built + data : dict + The chunk of the json dict used to build this object + create : bool + Whether to create the DCC Shape, or look for it already in-scene + + Returns + ------- + : Shape + The specified Shape + + """ + return cls(data["name"], simplex, create, QColor(*data.get("color", (0, 0, 0)))) + + def buildDefinition(self, simpDict, legacy): + """Output a dictionary definition of this object + + Parameters + ---------- + simpDict : dict + The dictionary that is being built + legacy : bool + Whether to write out the legacy definition, or the newer one + + Returns + ------- + + """ + if self._buildIdx is None: + self._buildIdx = len(simpDict["shapes"]) + if legacy: + simpDict.setdefault("shapes", []).append(self.name) + else: + x = { + "name": self.name, + "color": self.color.getRgb()[:3], + } + simpDict.setdefault("shapes", []).append(x) + return self._buildIdx + + def clearBuildIndex(self): + """Clear the build index of this object + + The buildIndex is stored when building a definition dictionary + that keeps track of its index for later referencing + + Parameters + ---------- + + Returns + ------- + + """ + self._buildIdx = None + + def zeroShape(self): + """Set the shape to be equal to the rest shape""" + self.DCC.zeroShape(self) + + @staticmethod + def zeroShapes(shapes): + """Set the shapes to be equal to the rest shape + + Parameters + ---------- + shapes : [Shape + Shapes to be zeroed + + Returns + ------- + + """ + for shape in shapes: + if not shape.isRest: + shape.zeroShape() + + def connectShape(self, mesh=None, live=False, delete=False): + """Force a shape to match a mesh + The "connect shape" button is: mesh=None, delete=True + The "match shape" button is: mesh=someMesh, delete=False + There is a possibility of a "make live" button: live=True, delete=False + + Parameters + ---------- + mesh : object or None + The DCC Mesh object. If None, it's searched for by name in scene (Default value = None) + live : bool + Whether or not to create a live connection in the DCC. Defaults False + delete : bool + Whether to delete the DCC Mesh after its connection. Defaults False + + Returns + ------- + + """ + self.DCC.connectShape(self, mesh, live, delete) + + @staticmethod + def connectShapes(shapes, meshes, live=False, delete=False): + """Connect multiple meshes to multiple Shapes + + Parameters + ---------- + shapes : [Shape + The shapes to connect to + meshes : [object + The DCC Meshes + live : bool + Whether or not to create a live connection in the DCC. Defaults False + delete : bool + Whether to delete the DCC Mesh after its connection. Defaults False + + Returns + ------- + + """ + with undoContext(): + for shape, mesh in zip(shapes, meshes): + shape.connectShape(mesh, live, delete) + + @staticmethod + def isNumberField(val): + """A utility function to check if a field is numeric + Also, this allows for the "n" prefix for negative numbers because + many DCC's don't allow "-" in an object name + + Parameters + ---------- + val : str + The string to check + + Returns + ------- + : bool + Whether the field is numeric + + """ + if not val: + return False + if val[0].lower() == "n": + val = val[1:] + return val.isdigit() + + @property + def verts(self): + """Get the stored vertices""" + if self._verts is None: + self._verts = self.DCC.getShapeVertices(self) + return self._verts + + @verts.setter + def verts(self, value): + """Set the stored vertices + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self._verts = value diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 0b3878db..c6293b00 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -1,1764 +1,1764 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use -import copy -import itertools -import json - -try: - import numpy as np -except ImportError: - np = None - -from ..commands.alembicCommon import ( - buildAlembicArchiveData, - getPointCount, - getSmpxArchiveData, - readFalloffData, -) -from ..interface import DCC, undoContext -from ..interface.dummyInterface import DCC as DummyDCC -from Qt.QtGui import QColor -from Qt.QtWidgets import QApplication -from ..utils import nested -from .combo import Combo, ComboPair -from .falloff import Falloff -from .group import Group -from .progression import ProgPair, Progression -from .shape import Shape -from .slider import Slider -from .stack import Stack, stackable -from .traversal import Traversal, TravPair - - -class Simplex(object): - """The main Top-level abstract object that controls an entire setup - - Simplex objects contain and manage the entire hierarchy. They have methods - to import from and export to disk. Simplex objects also mange the connections - to the DCC and the UI, setting up connections with the undo stack, the dispatcher, - and all of the ui TreeModels. - - Finally Simplex systems handle splitting, which will be covered more in depth - in the documentation for the split method. - - """ - - classDepth = 0 - - def __init__( - self, name="", models=None, falloffModels=None, forceDummy=False, sliderMul=1.0 - ): - """Constructor - - Parameters - ---------- - name : str, optional - The name of the new system. Defaults to "" - models : [QAbstractItemModel, ....], optional - The ui models that read this system. Defaults to [] - falloffModels : [QAbstractItemModel, ....], optional - The ui models for managing Falloffs. - Defaults to [] - forceDummy : bool, optional - When loading, don't make a connection to the actual DCC. Instead use the - "dummy" DCC. Defaults False - sliderMul : float, optional - A multiplier for the range of sliders. Simplex will only define values - between -1 and 1. This multiplier will let the attribute range in the DCC be larger so - animators can push the extremes - - Returns - ------- - """ - self._name = name # The name of the system - self.sliders = [] # List of contained sliders - self.combos = [] # List of contained combos - self.traversals = [] # list of contained traversals - self.sliderGroups = [] # List of groups containing sliders - self.comboGroups = [] # List of groups containing combos - self.traversalGroups = [] # List of groups containing traversals - self.falloffs = [] # List of contained falloff objects - self.shapes = [] # List of contained shape objects - self.models = models or [] # connected Qt Item Models - self.falloffModels = falloffModels or [] # connected Qt Falloff Models - self.restShape = None # Name of the rest shape - self.clusterName = "Shape" # Name of the cluster (XSI use only) - self.expanded = {} # Am I expanded by model - self.comboExpanded = False # Am I expanded in the combo tree - self.sliderExpanded = False # Am I expanded in the slider tree - self.sliderMul = sliderMul - self.DCC = DummyDCC(self) if forceDummy else DCC(self) # Interface to the DCC - self.stack = Stack() # Reference to the Undo stack - self._extras = {} # Any extra key data to store in the output json - self._legacy = False # whether to write the legacy types - - def __deepcopy__(self, memo): - """Gotta be really picky about what gets deepcopied. - Especially since I pretty much abuse the deepcopy mechanism to do splitting - Deep-copied systems have no reference to a UI, or a DCC - """ - cls = self.__class__ - result = cls.__new__(cls) - memo[id(self)] = result - for k, v in self.__dict__.items(): - if k == "models": - # do not make a copy of the connected models - # a deepcopied simplex won't be connected to a UI - setattr(result, k, []) - elif k == "falloffModels": - # do not make a copy of the connected models - # a deepcopied simplex won't be connected to a UI - setattr(result, k, []) - elif k == "stack": - # Make a disabled stack for new simplex - s = Stack() - s.enabled = False - setattr(result, k, s) - elif k == "DCC": - # do not connect the deepcopied simplex to the DCC - # we will want to change it without affecting the current scene - # Requires the name be copied already - setattr(result, "_name", copy.deepcopy(self._name, memo)) # noqa: B010 - if v.program == "dummy": - # If it's already a dummy, go ahead and just deepcopy - setattr(result, k, copy.deepcopy(v, memo)) - else: - setattr(result, k, DummyDCC(result)) - elif k == "expanded": - # do not make a copy of the expansion - # because it's keyed off the un-copied models - setattr(result, k, {}) - else: - setattr(result, k, copy.deepcopy(v, memo)) - return result - - def _initValues(self): - """Re-initialize the variables to that of an empy system""" - self._name = "" # The name of the system - self.sliders = [] # List of contained sliders - self.combos = [] # List of contained combos - self.sliderGroups = [] # List of groups containing sliders - self.comboGroups = [] # List of groups containing combos - self.traversalGroups = [] # List of groups containing combos - self.falloffs = [] # List of contained falloff objects - self.shapes = [] # List of contained shape objects - self.restShape = None # Name of the rest shape - self.clusterName = "Shape" # Name of the cluster (XSI use only) - self.expanded = {} # Am I expanded? (Keep around for consistent interface) - self.color = QColor(128, 128, 128) - self.comboExpanded = False # Am I expanded in the combo tree - self.sliderExpanded = False # Am I expanded in the slider tree - - # Alternate Constructors - @classmethod - def buildBaseObject(cls, smpxPath, name=None, forceDummy=False): - """Build the rest object from a .smpx file - - Parameters - ---------- - smpxPath : str - The path to the .smpx file - name : str, optional - The Name of the object to create. Defaults to the name of the simplex system - - Returns - ------- - : object - A reference to the DCC mesh - - """ - iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) - try: - if name is None: - js = json.loads(jsString) - name = js["systemName"] - if forceDummy: - return DummyDCC.buildRestAbc(abcMesh, name) - else: - return DCC.buildRestAbc(abcMesh, name) - finally: - del iarch - - @classmethod - def buildEmptySystem(cls, thing, name, sliderMul=1.0, forceDummy=False): - """Create a new, empty system on a given mesh - - Parameters - ---------- - thing : object - The DCC mesh to build the system on - name : str - The name of the new Simplex system - sliderMul : float, optional - A multiplier for the range of sliders. Simplex will only define values - between -1 and 1. This multiplier will let the attribute range in the DCC be larger so - animators can push the extremes (Default value = 1.0) - forceDummy : bool, optional - When loading, don't make a connection to the actual DCC. Instead use the - "dummy" DCC. Defaults False - - Returns - ------- - : Simplex - A newly created Simplex system - - """ - self = cls(name, forceDummy=forceDummy, sliderMul=sliderMul) - self.DCC.loadNodes(self, thing, create=True) - self.restShape = Shape.buildRest(self) - return self - - @classmethod - def buildSystemFromJsonString( - cls, jsString, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None - ): - """Build a system from a json encoded string - - Parameters - ---------- - jsString : str - The json encoded simplex definition - thing : object - The DCC mesh to build the system on (Default value = None) - name : str - The name of the new Simplex system (Default value = None) - forceDummy : bool - When loading, don't make a connection to the actual DCC. Instead use the - "dummy" DCC. Defaults False - sliderMul : float - A multiplier for the range of sliders. Simplex will only define values - between -1 and 1. This multiplier will let the attribute range in the DCC be larger so - animators can push the extremes (Default value = 1.0) - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - : Simplex - A newly created Simplex system - - """ - js = json.loads(jsString) - if name is None: - name = js["systemName"] - return cls.buildSystemFromDict( - js, thing, name=name, forceDummy=forceDummy, sliderMul=sliderMul, pBar=pBar - ) - - @classmethod - def buildSystemFromJson( - cls, jsPath, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None - ): - """Build a system from .json file - - Parameters - ---------- - jsPath : str - The .json file to load - thing : object - The DCC mesh to build the system on (Default value = None) - name : str - The name of the new Simplex system (Default value = None) - forceDummy : bool - When loading, don't make a connection to the actual DCC. Instead use the - "dummy" DCC. Defaults False - sliderMul : float - A multiplier for the range of sliders. Simplex will only define values - between -1 and 1. This multiplier will let the attribute range in the DCC be larger so - animators can push the extremes (Default value = 1.0) - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - : Simplex - A newly created Simplex system - - """ - with open(jsPath, "r") as f: - jsString = f.read() - return cls.buildSystemFromJsonString( - jsString, - thing, - name=name, - forceDummy=forceDummy, - sliderMul=sliderMul, - pBar=pBar, - ) - - @classmethod - def buildSystemFromSmpx( - cls, smpxPath, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None - ): - """Build a system from a .smpx file - SMPX files are (under-the-hood) alembic caches with each shape delta stored as a frame of animation, - and the json string stored in a property on the mesh. - - Parameters - ---------- - smxpPath : str - The .smpx file to load - thing : object - The DCC mesh to build the system on (Default value = None) - name : str - The name of the new Simplex system (Default value = None) - forceDummy : bool - When loading, don't make a connection to the actual DCC. Instead use the - "dummy" DCC. Defaults False - sliderMul : float - A multiplier for the range of sliders. Simplex will only define values - between -1 and 1. This multiplier will let the attribute range in the DCC be larger so - animators can push the extremes (Default value = 1.0) - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - : Simplex - A newly created Simplex system - - """ - if thing is None: - thing = cls.buildBaseObject(smpxPath, forceDummy=forceDummy) - iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) - js = json.loads(jsString) - - if not forceDummy: - smpxCount = getPointCount(abcMesh) - dccCount = DCC.vertCount(thing) - if smpxCount != dccCount: - raise RuntimeError( - "Point Count Mismatch. Smpx File:{0} DCC:{1}".format( - smpxCount, dccCount - ) - ) - - del iarch, abcMesh # release the files - if name is None: - name = js["systemName"] - self = cls.buildSystemFromDict( - js, thing, name=name, forceDummy=forceDummy, sliderMul=sliderMul, pBar=pBar - ) - self.loadSmpxShapes(smpxPath, pBar=pBar) - self.loadSmpxFalloffs(smpxPath, pBar=pBar) - return self - - @classmethod - def buildSystemFromFile( - cls, path, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None - ): - """Build a system from a file - - Parameters - ---------- - path : str - The file to load. Either .json or .smpx - thing : object - The DCC mesh to build the system on (Default value = None) - name : str - The name of the new Simplex system (Default value = None) - forceDummy : bool - When loading, don't make a connection to the actual DCC. Instead use the - "dummy" DCC. Defaults False - sliderMul : float - A multiplier for the range of sliders. Simplex will only define values - between -1 and 1. This multiplier will let the attribute range in the DCC be larger so - animators can push the extremes (Default value = 1.0) - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - : Simplex - A newly created Simplex system - - """ - if path.endswith(".json"): - return cls.buildSystemFromJson( - path, - thing=thing, - name=name, - forceDummy=forceDummy, - sliderMul=sliderMul, - pBar=pBar, - ) - elif path.endswith(".smpx"): - return cls.buildSystemFromSmpx( - path, - thing=thing, - name=name, - forceDummy=forceDummy, - sliderMul=sliderMul, - pBar=pBar, - ) - else: - raise ValueError( - "The filepath provided is not a .json or .smpx: {0}".format(path) - ) - - @classmethod - def buildSystemFromMesh( - cls, thing, name, forceDummy=False, sliderMul=1.0, pBar=None - ): - """Build a system from the data already built in the DCC - - Parameters - ---------- - thing : object - The DCC mesh to load the system from - name : str - The name of the new Simplex system - forceDummy : bool - When loading, don't make a connection to the actual DCC. Instead use the - "dummy" DCC. Defaults False - sliderMul : float - A multiplier for the range of sliders. Simplex will only define values - between -1 and 1. This multiplier will let the attribute range in the DCC be larger so - animators can push the extremes (Default value = 1.0) - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - : Simplex - A newly created Simplex system - - """ - jsDict = json.loads(DCC.getSimplexStringOnThing(thing, name)) - return cls.buildSystemFromDict( - jsDict, - thing, - name=name, - create=False, - forceDummy=forceDummy, - sliderMul=sliderMul, - pBar=pBar, - ) - - @classmethod - def buildSystemFromDict( - cls, - jsDict, - thing, - name=None, - create=True, - forceDummy=False, - sliderMul=1.0, - pBar=None, - ): - """Utility for building a cleared system from a dictionary - - Parameters - ---------- - jsDict : dict - The definition dictonary (parsed from a json string) - thing : object - The DCC mesh to load the system on - name : str, optional - The name of the new Simplex system. If None, default to the system name - create : bool, optional - Create any missing blendshapes as the system is loaded. If False, error on missing. - Defaults to True - forceDummy : bool, optional - When loading, don't make a connection to the actual DCC. Instead use the - "dummy" DCC. Defaults to False - sliderMul : float, optional - A multiplier for the range of sliders. Simplex will only define values - between -1 and 1. This multiplier will let the attribute range in the DCC be larger so - animators can push the extremes (Default value = 1.0) - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - : Simplex - A newly created Simplex system - - """ - if name is None: - name = jsDict["systemName"] - self = cls(name, forceDummy=forceDummy, sliderMul=sliderMul) - self.DCC.loadNodes(self, thing, create=create) - self.loadDefinition(jsDict, create=create, pBar=pBar) - return self - - def loadSmpxShapes(self, smpxPath, pBar=None): - """Load the Shapes from a .smpx file onto an already loaded system - This is the "We got updated shapes from the modelers" method - - Parameters - ---------- - smpxPath : str - The path to the .smpx file - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - - """ - iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) - js = json.loads(jsString) - - try: - self.DCC.loadAbc(abcMesh, js, pBar=pBar) - finally: - del abcMesh, iarch - - def loadSmpxPoses(self, smpxPath, pBar=None): - """Load the Poses from a .smpx file onto an already loaded system - This is the "Update the joints and skin" method - - Parameters - ---------- - smpxPath : str - The path to the .smpx file - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - - """ - iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) - js = json.loads(jsString) - - try: - self.DCC.loadAbcPoses(abcMesh, js, pBar=pBar) - finally: - del abcMesh, iarch - - def loadSmpxFalloffs(self, abcPath, pBar=None): - """Load the relevant data from a simplex alembic - - Parameters - ---------- - abcPath : str - Path to the .smpx file - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - - """ - foDict = readFalloffData(abcPath) - - for fo in self.falloffs: - foData = foDict.get(fo.name, None) - if foData is not None: - fo.weights = foData - - # Properties - @property - def name(self): - """Get the system name""" - return self._name - - @name.setter - @stackable - def name(self, value): - """Set the system name""" - if value == self._name: - return - - self._name = value - self.DCC.renameSystem(value) # ??? probably needs work - if self.restShape is not None: - self.restShape.name = self.getRestName() - - for model in self.models: - model.itemDataChanged(self) - - @property - def progs(self): - """Get all Progressions defined in the system""" - out = [] - for slider in self.sliders: - out.append(slider.prog) - for combo in self.combos: - out.append(combo.prog) - for trav in self.traversals: - out.append(trav.prog) - return out - - @property - def groups(self): - """Get all Groups defined in the system""" - return self.sliderGroups + self.comboGroups + self.traversalGroups - - def treeChild(self, row): - """ """ - return self.groups[row] - - def treeRow(self): - """ """ - return 0 - - def treeParent(self): - """ """ - return None - - def treeChildCount(self): - """ """ - return len(self.groups) - - def treeData(self, column): - """ """ - if column == 0: - return self.name - return None - - def treeChecked(self): - """ """ - return None - - def icon(self): - return None - - # HELPER - def comboExists(self, sliders, values): - """Check if a combo exists with these specific sliders and values - Because combo names aren't necessarily always in the same order - - Parameters - ---------- - sliders : [Slider, ....] - The sliders to check - values : [float, ....] - The values to check - - Returns - ------- - : Combo or None - The Combo with those sliders and values, or None if none found - - """ - checkSet = {(s.name, v) for s, v in zip(sliders, values)} - for cmb in self.combos: - cmbSet = {(p.slider.name, p.value) for p in cmb.pairs} - if checkSet == cmbSet: - return cmb - return None - - # DESTRUCTOR - def deleteSystem(self): - """Delete an existing system from the DCC""" - # Store the models as temp so the model doesn't go crazy with the signals - models, self.models = self.models, None - mgrs = [model.resetModelManager() for model in models] - with nested(*mgrs): - self.DCC.deleteSystem() - self._initValues() - self.DCC = DCC(self) - self.models = models - - def getComboUpstreams(self, combo): - """Get a list of only combos that are upstream to the given combo - In this case, "upstream" means that when the given combo is active, - then any returned combos are also active. - - I am currently ignoring floating combos in this function - The upstream sliders are available trivially through combo.pairs - - Parameters - ---------- - combo : Combo - The given combo - - Returns - ------- - : [Combo, ....] - The list of upstream combos - - """ - pairDict = {p.slider: p.value for p in combo.pairs} - - upstreams = [] - for c in self.combos: - if c.isFloating(): - continue - - if len(c.pairs) >= len(pairDict): - continue - - if not all(p.slider in pairDict for p in c.pairs): - continue - - fail = False - for p in c.pairs: - if p.slider not in pairDict: - fail = True - break - if pairDict[p.slider] * p.value < 0.0: - # opposite Signs - fail = True - break - - if not fail: - upstreams.append(c) - - return upstreams - - def getDownstreamTraversals(self, slider): - """Get a list of any traversals that depend on the given slider - - Parameters - ---------- - slider : Slider - The system slider to check - - Returns - ------- - : [Traversal, ....] - The list of dependent Traversals - - """ - downstream = [] - for t in self.traversals: - for pair in t.startPoint.pairs + t.endPoint.pairs: - if slider == pair.slider: - downstream.append(t) - break - downstream = list(set(downstream)) - return downstream - - def getDownstreamCombos(self, slider): - """Get a list of any Combos that depend on the given slider - - Parameters - ---------- - slider : Slider - The Slider item to check - - Returns - ------- - : [Combo, ....] - The list of dependent Combos - - """ - downstream = [] - if not isinstance(slider, Slider): - return downstream - for c in self.combos: - for pair in c.pairs: - if pair.slider == slider: - downstream.append(c) - break - downstream = list(set(downstream)) - return downstream - - def deleteDownstream(self, item): - """Delete all items from the system that depend on the given item - - Parameters - ---------- - item : object - The system item to check - - Returns - ------- - - """ - todel = [] - todel.extend(self.getDownstreamCombos(item)) - todel.extend(self.getDownstreamTraversals(item)) - for c in todel: - c.delete() - - # USER METHODS - def setLegacy(self, legacy): - """Set whether to use the legacy .json format - - Parameters - ---------- - legacy : bool - Whether to use the legacy .json format - - Returns - ------- - - """ - self._legacy = legacy - - def getFloatingShapes(self): - """Find Combos with values other than -1 and 1 - - Parameters - ---------- - - Returns - ------- - : [Combo, ....] - Combos that don't have fully extreme activations - - """ - floaters = [c for c in self.combos if c.isFloating()] - floatShapes = [] - for f in floaters: - floatShapes.extend(f.prog.getShapes()) - return floatShapes - - def buildDefinition(self): - """Create a simplex definition dictionary - Loop through all the objects managed by this simplex system, and build a dictionary that defines it - - Parameters - ---------- - - Returns - ------- - : dict - The simplex definition dictionary - - """ - things = [ - self.shapes, - self.sliders, - self.combos, - self.traversals, - self.groups, - self.falloffs, - ] - for thing in things: - for i in thing: - i.clearBuildIndex() - - # Make sure we start with the extras in - # case we're overwriting with new data - d = copy.deepcopy(self._extras) - - # Then set all the top-level system keys - d["encodingVersion"] = 1 if self._legacy else 3 - d["systemName"] = self.name - d["clusterName"] = self.clusterName - d.setdefault("falloffs", []) - d.setdefault("combos", []) - d.setdefault("shapes", []) - d.setdefault("sliders", []) - d.setdefault("groups", []) - d.setdefault("progressions", []) - d.setdefault("traversals", []) - - # rest shape should *ALWAYS* be index 0 - for shape in self.shapes: - shape.buildDefinition(d, self._legacy) - - for group in self.groups: - group.buildDefinition(d, self._legacy) - - for falloff in self.falloffs: - falloff.buildDefinition(d, self._legacy) - - for slider in self.sliders: - slider.buildDefinition(d, self._legacy) - - for combo in self.combos: - combo.buildDefinition(d, self._legacy) - - for trav in self.traversals: - trav.buildDefinition(d, self._legacy) - - return d - - @stackable - def loadDefinition(self, simpDict, create=True, pBar=None): - """Build the structure of objects in this system - based on a provided dictionary - - Parameters - ---------- - simpDict : dict - The dictionary to load - create : bool, optional - Create any missing blendshapes as the system is loaded. If False, error on missing. - Defaults to True - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - - """ - - self.name = simpDict["systemName"] - self.clusterName = simpDict["clusterName"] # for XSI - if simpDict["encodingVersion"] == 1: - self.loadV1(simpDict, create=create, pBar=pBar) - elif simpDict["encodingVersion"] == 2: - self.loadV2(simpDict, create=create, pBar=pBar) - elif simpDict["encodingVersion"] == 3: - self.loadV3(simpDict, create=create, pBar=pBar) - self.storeExtras(simpDict) - - def _incPBar(self, pBar, txt, inc=1): - """Increment the progress bar and return False if the user cancelled - - Parameters - ---------- - pBar : - - txt : - - inc : - (Default value = 1) - - Returns - ------- - - """ - if pBar is not None: - pBar.setValue(pBar.value() + inc) - pBar.setLabelText("Building:\n" + txt) - QApplication.processEvents() - return not pBar.wasCanceled() - return True - - def loadV3(self, simpDict, create=True, pBar=None): - """Load the version 3 simplex definition - V3 is just the same as V2, except for an update Traversal definition - - Parameters - ---------- - simpDict : dict - The simplex definition dictionary - create : bool, optional - Create any missing blendshapes as the system is loaded. If False, error on missing. - Defaults to True - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - - """ - preRet = self.DCC.preLoad(self, simpDict, create=create, pBar=pBar) - try: - fos = simpDict.get("falloffs", []) - gs = simpDict.get("groups", []) - for f in fos: - Falloff.loadV2(self, f) - if gs: - for g in gs: - Group.loadV2(self, g) - else: - Group("Group_0", self, Slider) - Group("Group_1", self, Combo) - Group("Group_2", self, Traversal) - - if pBar is not None: - maxLen = max(len(i["name"]) for i in simpDict["shapes"]) - pBar.setLabelText("_" * maxLen) - pBar.setValue(0) - pBar.setMaximum(len(simpDict["shapes"]) + 1) - self.shapes = [] - for s in simpDict["shapes"]: - if not self._incPBar(pBar, s["name"]): - return - Shape.loadV2(self, s, create) - - self.restShape = self.shapes[0] - self.restShape.isRest = True - - progs = [Progression.loadV2(self, p) for p in simpDict["progressions"]] - - for s in simpDict["sliders"]: - Slider.loadV2(self, progs, s, create) - for c in simpDict["combos"]: - Combo.loadV2(self, progs, c) - for t in simpDict["traversals"]: - Traversal.loadV3(self, progs, t) - - for x in itertools.chain(self.sliders, self.combos, self.traversals): - x.prog.name = x.name - finally: - self.DCC.postLoad(self, preRet) - - def loadV2(self, simpDict, create=True, pBar=None): - """Load the version 2 simplex definition - - Parameters - ---------- - simpDict : dict - The simplex definition dictionary - create : bool, optional - Create any missing blendshapes as the system is loaded. If False, error on missing. - Defaults to True - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - - """ - preRet = self.DCC.preLoad(self, simpDict, create=create, pBar=pBar) - try: - fos = simpDict.get("falloffs", []) - gs = simpDict.get("groups", []) - for f in fos: - Falloff.loadV2(self, f) - if gs: - for g in gs: - Group.loadV2(self, g) - else: - Group("Group_0", self, Slider) - Group("Group_1", self, Combo) - Group("Group_2", self, Traversal) - - if pBar is not None: - maxLen = max(len(i["name"]) for i in simpDict["shapes"]) - pBar.setLabelText("_" * maxLen) - pBar.setValue(0) - pBar.setMaximum(len(simpDict["shapes"]) + 1) - self.shapes = [] - for s in simpDict["shapes"]: - if not self._incPBar(pBar, s["name"]): - return - Shape.loadV2(self, s, create) - - self.restShape = self.shapes[0] - self.restShape.isRest = True - - progs = [Progression.loadV2(self, p) for p in simpDict["progressions"]] - - for s in simpDict["sliders"]: - Slider.loadV2(self, progs, s, create) - for c in simpDict["combos"]: - Combo.loadV2(self, progs, c) - for t in simpDict["traversals"]: - Traversal.loadV2(self, progs, t) - - for x in itertools.chain(self.sliders, self.combos, self.traversals): - x.prog.name = x.name - finally: - self.DCC.postLoad(self, preRet) - - def loadV1(self, simpDict, create=True, pBar=None): - """Load the version 1 simplex definition - - Parameters - ---------- - simpDict : dict - The simplex definition dictionary - create : bool, optional - Create any missing blendshapes as the system is loaded. If False, error on missing. - Defaults to True - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - - """ - preRet = self.DCC.preLoad(self, simpDict, create=create, pBar=pBar) - try: - self.falloffs = [Falloff(f[0], self, *f[1:]) for f in simpDict["falloffs"]] - groupNames = simpDict["groups"] - - if pBar is not None: - maxLen = max(list(map(len, simpDict["shapes"]))) - pBar.setLabelText("_" * maxLen) - pBar.setValue(0) - pBar.setMaximum(len(simpDict["shapes"]) + 1) - - shapes = [] - for s in simpDict["shapes"]: - if not self._incPBar(pBar, s): - return - shapes.append(Shape(s, self)) - - self.restShape = shapes[0] - self.restShape.isRest = True - - progs = [] - for p in simpDict["progressions"]: - progShapes = [shapes[i] for i in p[1]] - progFalloffs = [self.falloffs[i] for i in p[4]] - progPairs = [ProgPair(self, s, pv) for s, pv in zip(progShapes, p[2])] - progs.append(Progression(p[0], self, progPairs, p[3], progFalloffs)) - - self.sliders = [] - self.sliderGroups = [] - createdSlidergroups = {} - for s in simpDict["sliders"]: - sliderProg = progs[s[1]] - - gn = groupNames[s[2]] - if gn in createdSlidergroups: - sliderGroup = createdSlidergroups[gn] - else: - sliderGroup = Group(gn, self, Slider) - createdSlidergroups[gn] = sliderGroup - - Slider(s[0], self, sliderProg, sliderGroup) - - self.combos = [] - self.comboGroups = [] - createdComboGroups = {} - for c in simpDict["combos"]: - prog = progs[c[1]] - sliderIdxs, sliderVals = list(zip(*c[2])) - sliders = [self.sliders[i] for i in sliderIdxs] - pairs = list(map(ComboPair, sliders, sliderVals)) - if len(c) >= 4: - gn = groupNames[c[3]] - else: - gn = "DEPTH_0" - - if gn in createdComboGroups: - comboGroup = createdComboGroups[gn] - else: - comboGroup = Group(gn, self, Combo) - createdComboGroups[gn] = comboGroup - - cmb = Combo(c[0], self, pairs, prog, comboGroup, None) - cmb.simplex = self - - self.traversals = [] - self.traversalGroups = [] - createdTraversalGroups = {} - if "traversals" in simpDict: - for t in simpDict["traversals"]: - name = t["name"] - prog = progs[t["prog"]] - - pcIdx = t["progressControl"] - pcSearch = ( - self.sliders - if t["progressType"].lower() == "slider" - else self.combos - ) - pc = pcSearch[pcIdx] - pFlip = t["progressFlip"] - pp = TravPair(pc, -1 if pFlip else 1, "progress") - - mcIdx = t["multiplierControl"] - mcSearch = ( - self.sliders - if t["multiplierType"].lower() == "slider" - else self.combos - ) - mc = mcSearch[mcIdx] - mFlip = t["multiplierFlip"] - mm = TravPair(mc, -1 if mFlip else 1, "multiplier") - - gn = groupNames[t.get("group", 2)] - if gn in createdTraversalGroups: - travGroup = createdTraversalGroups[gn] - else: - travGroup = Group(gn, self, Traversal) - createdTraversalGroups[gn] = travGroup - - color = QColor(*t.get("color", (0, 0, 0))) - - trav = Traversal(name, self, mm, pp, prog, travGroup, color) - trav.simplex = self - - for x in itertools.chain(self.sliders, self.combos, self.traversals): - x.prog.name = x.name - finally: - self.DCC.postLoad(self, preRet) - - def storeExtras(self, simpDict): - """Store any unknown keys when dumping, just in case they're important elsewhere - - Parameters - ---------- - simpDict : dict - The simplex definition dictionary - - Returns - ------- - - """ - sd = copy.deepcopy(simpDict) - knownTopLevel = [ - "encodingVersion", - "systemName", - "clusterName", - "falloffs", - "combos", - "shapes", - "sliders", - "groups", - "traversals", - "progressions", - ] - - for ktn in knownTopLevel: - if ktn in sd: - del sd[ktn] - self._extras = sd - - def loadJSON(self, jsString): - """Convenience method to load a JSON string definition - - Parameters - ---------- - jsString : str - The json formatted definition string - - Returns - ------- - - """ - self.loadDefinition(json.loads(jsString)) - - def getRestName(self): - """Get the default rest shape name - - Parameters - ---------- - - Returns - ------- - : str - The default rest shape name - - """ - return "Rest_{0}".format(self.name) - - def dump(self): - """Dump the definition dictionary to a json string - - Parameters - ---------- - - Returns - ------- - : str - The json formatted definition string - - """ - return json.dumps(self.buildDefinition()) - - def exportAbc(self, path, pBar=None): - """Export the current mesh to a .smpx formatted file - - Parameters - ---------- - path : str - The path to export to - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - - """ - defDict = self.buildDefinition() - jsString = json.dumps(defDict) - - arch, abcMesh = buildAlembicArchiveData(path, self.name, jsString, True) - try: - self.DCC.exportAbc( - self.DCC.mesh, - abcMesh, - defDict, - world=False, - ensureCorrect=True, - pBar=pBar, - ) - finally: - del arch, abcMesh - - def exportOther(self, path, dccMesh, world=False, ensureCorrect=False, pBar=None): - """Export shapes from a mesh that isn't part of the current system - - The export process for shapes differs from DCC to DCC. - In Maya, every blendshape is activated, one by one and the point posisitions - are read from the target mesh. This allows exportOther to work - - In XSI, the blendshape properties are read directly, so there's no chance - to process the new shapes (which means this won't work in XSI) - - Parameters - ---------- - path : str - The output path for the .smpx file - dccMesh : object - The system-external DCC mesh to export - world : bool, optional - Whether to do the export in worldspace. Defaults to False - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - """ - defDict = self.buildDefinition() - jsString = json.dumps(defDict) - - arch, abcMesh = buildAlembicArchiveData(path, self.name, jsString, True) - try: - self.DCC.exportOtherAbc(dccMesh, abcMesh, defDict, world=world, pBar=pBar) - finally: - del arch, abcMesh - - def setSlidersWeights(self, sliders, weights): - """Set the weights of multiple sliders as one method - - Parameters - ---------- - sliders : [Slider, ....] - The list of Sliders to set weights for - weights : [float, ....] - The weights to set - - Returns - ------- - - """ - with undoContext(self.DCC): - for slider, weight in zip(sliders, weights): - slider.value = weight - self.DCC.setSlidersWeights(sliders, weights) - for model in self.models: - for slider in sliders: - model.itemDataChanged(slider) - - def extractRestShape(self, offset=0): - """Extract the rest shape to a mesh in the DCC - - Parameters - ---------- - offset : float - The offset value given to the extracted mesh (Default value = 0) - - Returns - ------- - - """ - if self.restShape is None: - return None - return self.DCC.extractShape(self.restShape, live=False, offset=offset) - - def buildRestShape(self): - """Build and store a rest shape for this system""" - self.restShape = Shape.buildRest(self) - return self.restShape - - def buildInputVectors( - self, - keepSliders=None, - ignoreSliders=None, - depthCutoff=None, - ignoreFloaters=False, - ignoreTraversals=False, - extremes=False, - ): - """This is kind of a specialized function. Often, I have to sum combo deltas - into the full sculpted shape. But to do that, I have to build the inputs to the - solver that enable each shape, and I need a name for each shape. - This function gives me both. - - Parameters - ---------- - keepSliders : set([str, ....]), optional - The returned sliders will have names that are part of this list - ignoreSliders : set([str, ....]), optional - A set of Slider names to ignore as part of this process - depthCutoff : int, optional - The maximum number of Sliders allowed per Combo - (Defaults to None which allows all Combos) - ignoreFloaters : bool, optional - Ignore Combos with values other than -1 and 1 - extremes : bool, optional - Only build activations for sliders and combos at -1 and 1 - - Returns - ------- - : [str, ...] - The names of the shapes that are activated in each list - : [[float, ...], ...] - A list of activation values for the entire system - - """ - # InputVector comes from the c++ std::vector - # Get all endpoint shapes from the progressions - shapeNames = [] - inVecs = [] - keyIdxs = [] - ignoreSliders = ignoreSliders or set() - ignoreSliders = set(ignoreSliders) - indexByShape = {shape: idx for idx, shape in enumerate(self.shapes)} - - for slIdx, slider in enumerate(self.sliders): - if slider.name in ignoreSliders: - continue - if keepSliders is not None and slider.name not in keepSliders: - continue - - pairs = [p for p in slider.prog.pairs if not p.shape.isRest] - if extremes: - pairs = [p for p in pairs if abs(p.value) == 1.0] - - for pp in pairs: - inVec = [0.0] * len(self.sliders) - inVec[slIdx] = pp.value - shapeNames.append(pp.shape.name) - inVecs.append(inVec) - keyIdxs.append(indexByShape[pp.shape]) - - for combo in self.combos: - if ignoreFloaters and combo.isFloating(): - continue - if depthCutoff is not None and len(combo.pairs) > depthCutoff: - continue - if ignoreSliders & {i.name for i in combo.getSliders()}: - # if the combo's sliders are in ignoreSliders - continue - - pairs = [p for p in combo.prog.pairs if not p.shape.isRest] - if extremes: - pairs = [p for p in pairs if abs(p.value) == 1.0] - - iv = combo.getInputVector() - for pp in pairs: - inVecs.append([x * pp.value for x in iv]) - shapeNames.append(pp.shape.name) - keyIdxs.append(indexByShape[pp.shape]) - - for trav in self.traversals: - # Extremes doesn't make sense at all for traversals - if ignoreTraversals: - continue - pairs = [p for p in trav.prog.pairs if not p.shape.isRest] - if extremes: - pairs = [p for p in pairs if abs(p.value) == 1.0] - - for pp in pairs: - inVecs.append(trav.getInputVector(pp.value)) - shapeNames.append(pp.shape.name) - keyIdxs.append(indexByShape[pp.shape]) - - return shapeNames, inVecs, keyIdxs - - def evaluateInputs(self, inVecs): - """Get the shape activation vectors that are paired with the given input vectors - It will probably be useful to pass the returned inVecs from `buildInputVectors` - - This will use the compiled python solver, and may not be available - - Parameters - ---------- - inVecs : [[SliderVal, ...], ...] - A list of lists of sliderValues. Each sub-list has to have the same number of - items as there are sliders in the current simplex system - - Returns - ------- - : [[ShapeVal, ...], ...] - A list of lists of shapeValues resulting from the given inVecs. This returns - the activations of all the shape values. Each sub-list will have the same - number of items as there are shapes in the current system. - """ - from pysimplex import PySimplex - - solver = PySimplex(self.dump()) - return [solver.solve(iv) for iv in inVecs] - - def controllersByDepth(self): - """Get the shapes ordered by the depth of their controllers - in the simplex hierarchy - This is often useful when doing vertex position computations - - Returns - ------- - : [Shape, ...] - A list of all shapes in the depth order - """ - ctrlOrder = self.sliders[:] - combosByDepth = {} - for c in self.combos: - combosByDepth.setdefault(len(c.pairs), []).append(c) - - for depth in sorted(combosByDepth.keys()): - combos = combosByDepth[depth] - regular, floating = [], [] - for c in combos: - lst = floating if c.isFloating() else regular - lst.append(c) - ctrlOrder.extend(regular + floating) - - travByDepth = {} - for t in self.traversals: - travByDepth.setdefault(len(t.startPoint.pairs), []).append(t) - - for depth in sorted(travByDepth.keys()): - ctrlOrder.extend(travByDepth[depth]) - - return ctrlOrder - - # SPLIT CODE - def buildSplitterList(self, foList): - """The way deepcopy works is that every object visited is added to the 'memo' dictionary, - keyed by its id(). This way, you don't have to re-copy an object if you've already seen - it. This means that if I make a memo that already contains objects that I don't want copied, - then I should just be able to use deepcopy, and that will handle keeping references to the - un-copied objects. - - For example: If X references A, and I want to split X into L and R, then I would still want - L to reference A *and* R to reference A. This process just auto-handles that - I think that's is kinda neat. - - Parameters - ---------- - foList : [Falloff, ....] - A list of Falloff objects *that all share the same split axis* - - Returns - ------- - : [object, ....] - A list of objects to be split - : dict - A dict of {object: Falloff} saying what falloff should be used to split each objct - : dict - The memo to start the deepcopy with - - """ - # Add all items to the memo. - memo = {} - memo[id(self)] = self - stack = Stack() - stack.enabled = False - memo[id(self.stack)] = stack - memo[id(self.DCC)] = self.DCC - - memList = [ - self.groups, - self.sliders, - self.combos, - self.traversals, - self.falloffs, - self.shapes, - self.progs, - ] - - for lst in memList: - for item in lst: - memo[id(item)] = item - # splitApplied needs to be part of the item so it persists - # through the copy. Otherwise I'd have to keep track of it - item._splitApplied = set() - - # Using the memo from here would mean that nothing got copied - # because all items are in it - - toSplit = [] # A list of objects to be split along the shared foList axis - - # A dictionary of {object: set(falloff)}. When recursing down the hierarchy, - # some objects may be split by many falloffs. Keep track of that in this dict - splitBySet = {} - - for prog in self.progs: - # I take pains to this with lists (rather than sets) so it keeps order - sect = [i for i in foList if i in prog.falloffs] - if sect: - # Because I can only split an item once on an axis - # Assume that the foList is in priority order - # ... So I can just grab the first (highest priority) - # falloff affecting this item - splitFalloff = sect[0] - ctrl = prog.controller - - toSplit.append(prog) - toSplit.append(ctrl) - splitBySet.setdefault(prog, set()).add(splitFalloff) - splitBySet.setdefault(ctrl, set()).add(splitFalloff) - for pair in prog.pairs: - toSplit.append(pair.shape) - splitBySet.setdefault(pair.shape, set()).add(splitFalloff) - - # Also add the downstream combos, and their progs - dss = [] - if isinstance(ctrl, Slider): - dss.extend(self.getDownstreamCombos(ctrl)) - dss.extend(self.getDownstreamTraversals(ctrl)) - - for ds in dss: - toSplit.append(ds) - toSplit.append(ds.prog) - splitBySet.setdefault(ds, set()).add(splitFalloff) - splitBySet.setdefault(ds.prog, set()).add(splitFalloff) - for pair in ds.prog.pairs: - toSplit.append(pair.shape) - splitBySet.setdefault(pair.shape, set()).add(splitFalloff) - - # Dict of {item : falloff} - splitBy = {} - for item, foSet in splitBySet.items(): - # Because I can only split an item once on an axis - # Assume that the foList is in priority order - # So get the min-indexed falloff in the set - idx = min([foList.index(f) for f in foSet]) - splitBy[item] = foList[idx] - - toSplit = set(toSplit) - toSplit.discard(self.restShape) - # Get the items to split that haven't already had a split applied along this axis - toSplit = [i for i in toSplit if foList[0].axis not in i._splitApplied] - # If I can't apply a sided name ot this item, then it can't be split - toSplit = [i for i in toSplit if foList[0].canRename(i)] - - # Remove the splitter items from the memo, ensuring they actually get copied - for sp in toSplit: - try: - del memo[id(sp)] - except KeyError: - print("SP", sp, sp.name) - raise - sp._splitApplied.add(foList[0].axis.lower()) - - return toSplit, splitBy, memo - - def split(self, pBar=None): - """Return a split deepcopy of the system. - The new system will be a dummy system containing all the shapes as numpy arrays which can be - exported to a .smpx file - - Parameters - ---------- - pBar : QProgressDialog, optional - If provided, display progress in this dialog - - Returns - ------- - : Simplex : - A newly split system - - """ - if np is None: - raise RuntimeError("Numpy is not available, and splitting requires it") - - if pBar is not None: - pBar.setValue(0) - pBar.setLabelText("Building Split System") - - # Very first thing: Ensure that every object with a falloff is fully splittable - # Meaning that splittable progs only contain splittable shapes. And splittable progs - # are only controlled by splittable controllers - for fo in self.falloffs: - controllers = self.sliders + self.combos + self.traversals - for ctrl in controllers: - prog = ctrl.prog - - pSplit = fo.canRename(prog) - cSplit = fo.canRename(ctrl) - sSplit = [s for s in prog.getShapes() if not s.isRest] - sSplit = [fo.canRename(shape) for shape in sSplit] - sSplitSame = all(i == sSplit[0] for i in sSplit) - sSplit = sSplit[0] - if not sSplitSame: - shapes = [i.name for i in prog.getShapes()] - msg = "Bad shapes: {0}".format(", ".join(shapes)) - raise ValueError( - "Mix of splittable and un-splittable shapes in a progression\n" - + msg - ) - - if pSplit != sSplit: - msg = "Bad Prog: {0}".format(prog.name) - raise ValueError("A progression is not fully splittable\n" + msg) - - if pSplit != cSplit: - msg = "Bad prog: {0}\nBad Controller:{1}".format( - prog.name, ctrl.name - ) - raise ValueError("A controller is not fully splittable\n" + msg) - - # Create the initial deepcopy - splitSmpx = copy.deepcopy(self) - splitSmpx.DCC.dummyLoad(self.DCC, pBar=pBar) - - # Sort the falloffs by which axis the split on - foByAxis = {} - for fo in splitSmpx.falloffs: - foByAxis.setdefault(fo.axis.lower(), []).append(fo) - - for axis, foList in foByAxis.items(): - if pBar is not None: - pBar.setLabelText("Splitting On {0} axis".format(axis)) - QApplication.processEvents() - else: - print("Splitting On {0} axis".format(axis)) - - # Get the items to split, and the memo that ensures *only* those items will be copied - toSplit, splitBy, memo = splitSmpx.buildSplitterList(foList) - - # DeepCopy the items twice. Once for each side of the split - lSideSplitList = copy.deepcopy(toSplit, memo=copy.copy(memo)) - rSideSplitList = copy.deepcopy(toSplit, memo=copy.copy(memo)) - - if pBar is not None: - pBar.setMaximum(len(toSplit)) - QApplication.processEvents() - - # Loop through the copied items and make the replacements - for i, (oldItem, lItem, rItem) in enumerate( - zip(toSplit, lSideSplitList, rSideSplitList) - ): - if pBar is not None: - pBar.setValue(i + 1) - QApplication.processEvents() - - # Get thefalloff that will split oldItem into lItem and rItem - fo = splitBy[oldItem] - - # Rename the newly split items - fo.splitRename(lItem, 0) - fo.splitRename(rItem, 1) - - # Apply the falloff weights to any shapes - if isinstance(oldItem, Shape): - fo.applyFalloff(lItem, 0) - fo.applyFalloff(rItem, 1) - - # Maybe for all this, get the index of the oldItem in the group - # and insert rather than append?? - - # Remove the oldItem from any groups and add the newItems - if hasattr(oldItem, "group"): - oldItem.group.items.remove(oldItem) - oldItem.group = None - lItem.group.items.append(lItem) - rItem.group.items.append(rItem) - - # Remove the oldItem from the simplex storage, and add the newItems - if isinstance(oldItem, Slider): - splitSmpx.sliders.remove(oldItem) - splitSmpx.sliders.append(lItem) - splitSmpx.sliders.append(rItem) - elif isinstance(oldItem, Combo): - splitSmpx.combos.remove(oldItem) - splitSmpx.combos.append(lItem) - splitSmpx.combos.append(rItem) - elif isinstance(oldItem, Traversal): - splitSmpx.traversals.remove(oldItem) - splitSmpx.traversals.append(lItem) - splitSmpx.traversals.append(rItem) - elif isinstance(oldItem, Shape): - splitSmpx.shapes.remove(oldItem) - # Part of deepCopy ensures the new system uses the DummyDCC - # So this just removes the shape verts from the DummyDCC dictionary - # and doesn't actually delete the shape from anywhere important - splitSmpx.DCC.deleteShape(oldItem) - splitSmpx.shapes.append(lItem) - splitSmpx.shapes.append(rItem) - - splitSmpx.DCC.pushAllShapeVertices(splitSmpx.shapes) - return splitSmpx +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint:disable=missing-docstring,unused-argument,no-self-use +import copy +import itertools +import json + +try: + import numpy as np +except ImportError: + np = None + +from ..commands.alembicCommon import ( + buildAlembicArchiveData, + getPointCount, + getSmpxArchiveData, + readFalloffData, +) +from ..interface import DCC, undoContext +from ..interface.dummyInterface import DCC as DummyDCC +from Qt.QtGui import QColor +from Qt.QtWidgets import QApplication +from ..utils import nested +from .combo import Combo, ComboPair +from .falloff import Falloff +from .group import Group +from .progression import ProgPair, Progression +from .shape import Shape +from .slider import Slider +from .stack import Stack, stackable +from .traversal import Traversal, TravPair + + +class Simplex(object): + """The main Top-level abstract object that controls an entire setup + + Simplex objects contain and manage the entire hierarchy. They have methods + to import from and export to disk. Simplex objects also mange the connections + to the DCC and the UI, setting up connections with the undo stack, the dispatcher, + and all of the ui TreeModels. + + Finally Simplex systems handle splitting, which will be covered more in depth + in the documentation for the split method. + + """ + + classDepth = 0 + + def __init__( + self, name="", models=None, falloffModels=None, forceDummy=False, sliderMul=1.0 + ): + """Constructor + + Parameters + ---------- + name : str, optional + The name of the new system. Defaults to "" + models : [QAbstractItemModel, ....], optional + The ui models that read this system. Defaults to [] + falloffModels : [QAbstractItemModel, ....], optional + The ui models for managing Falloffs. + Defaults to [] + forceDummy : bool, optional + When loading, don't make a connection to the actual DCC. Instead use the + "dummy" DCC. Defaults False + sliderMul : float, optional + A multiplier for the range of sliders. Simplex will only define values + between -1 and 1. This multiplier will let the attribute range in the DCC be larger so + animators can push the extremes + + Returns + ------- + """ + self._name = name # The name of the system + self.sliders = [] # List of contained sliders + self.combos = [] # List of contained combos + self.traversals = [] # list of contained traversals + self.sliderGroups = [] # List of groups containing sliders + self.comboGroups = [] # List of groups containing combos + self.traversalGroups = [] # List of groups containing traversals + self.falloffs = [] # List of contained falloff objects + self.shapes = [] # List of contained shape objects + self.models = models or [] # connected Qt Item Models + self.falloffModels = falloffModels or [] # connected Qt Falloff Models + self.restShape = None # Name of the rest shape + self.clusterName = "Shape" # Name of the cluster (XSI use only) + self.expanded = {} # Am I expanded by model + self.comboExpanded = False # Am I expanded in the combo tree + self.sliderExpanded = False # Am I expanded in the slider tree + self.sliderMul = sliderMul + self.DCC = DummyDCC(self) if forceDummy else DCC(self) # Interface to the DCC + self.stack = Stack() # Reference to the Undo stack + self._extras = {} # Any extra key data to store in the output json + self._legacy = False # whether to write the legacy types + + def __deepcopy__(self, memo): + """Gotta be really picky about what gets deepcopied. + Especially since I pretty much abuse the deepcopy mechanism to do splitting + Deep-copied systems have no reference to a UI, or a DCC + """ + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k == "models": + # do not make a copy of the connected models + # a deepcopied simplex won't be connected to a UI + setattr(result, k, []) + elif k == "falloffModels": + # do not make a copy of the connected models + # a deepcopied simplex won't be connected to a UI + setattr(result, k, []) + elif k == "stack": + # Make a disabled stack for new simplex + s = Stack() + s.enabled = False + setattr(result, k, s) + elif k == "DCC": + # do not connect the deepcopied simplex to the DCC + # we will want to change it without affecting the current scene + # Requires the name be copied already + setattr(result, "_name", copy.deepcopy(self._name, memo)) # noqa: B010 + if v.program == "dummy": + # If it's already a dummy, go ahead and just deepcopy + setattr(result, k, copy.deepcopy(v, memo)) + else: + setattr(result, k, DummyDCC(result)) + elif k == "expanded": + # do not make a copy of the expansion + # because it's keyed off the un-copied models + setattr(result, k, {}) + else: + setattr(result, k, copy.deepcopy(v, memo)) + return result + + def _initValues(self): + """Re-initialize the variables to that of an empy system""" + self._name = "" # The name of the system + self.sliders = [] # List of contained sliders + self.combos = [] # List of contained combos + self.sliderGroups = [] # List of groups containing sliders + self.comboGroups = [] # List of groups containing combos + self.traversalGroups = [] # List of groups containing combos + self.falloffs = [] # List of contained falloff objects + self.shapes = [] # List of contained shape objects + self.restShape = None # Name of the rest shape + self.clusterName = "Shape" # Name of the cluster (XSI use only) + self.expanded = {} # Am I expanded? (Keep around for consistent interface) + self.color = QColor(128, 128, 128) + self.comboExpanded = False # Am I expanded in the combo tree + self.sliderExpanded = False # Am I expanded in the slider tree + + # Alternate Constructors + @classmethod + def buildBaseObject(cls, smpxPath, name=None, forceDummy=False): + """Build the rest object from a .smpx file + + Parameters + ---------- + smpxPath : str + The path to the .smpx file + name : str, optional + The Name of the object to create. Defaults to the name of the simplex system + + Returns + ------- + : object + A reference to the DCC mesh + + """ + iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) + try: + if name is None: + js = json.loads(jsString) + name = js["systemName"] + if forceDummy: + return DummyDCC.buildRestAbc(abcMesh, name) + else: + return DCC.buildRestAbc(abcMesh, name) + finally: + del iarch + + @classmethod + def buildEmptySystem(cls, thing, name, sliderMul=1.0, forceDummy=False): + """Create a new, empty system on a given mesh + + Parameters + ---------- + thing : object + The DCC mesh to build the system on + name : str + The name of the new Simplex system + sliderMul : float, optional + A multiplier for the range of sliders. Simplex will only define values + between -1 and 1. This multiplier will let the attribute range in the DCC be larger so + animators can push the extremes (Default value = 1.0) + forceDummy : bool, optional + When loading, don't make a connection to the actual DCC. Instead use the + "dummy" DCC. Defaults False + + Returns + ------- + : Simplex + A newly created Simplex system + + """ + self = cls(name, forceDummy=forceDummy, sliderMul=sliderMul) + self.DCC.loadNodes(self, thing, create=True) + self.restShape = Shape.buildRest(self) + return self + + @classmethod + def buildSystemFromJsonString( + cls, jsString, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None + ): + """Build a system from a json encoded string + + Parameters + ---------- + jsString : str + The json encoded simplex definition + thing : object + The DCC mesh to build the system on (Default value = None) + name : str + The name of the new Simplex system (Default value = None) + forceDummy : bool + When loading, don't make a connection to the actual DCC. Instead use the + "dummy" DCC. Defaults False + sliderMul : float + A multiplier for the range of sliders. Simplex will only define values + between -1 and 1. This multiplier will let the attribute range in the DCC be larger so + animators can push the extremes (Default value = 1.0) + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + : Simplex + A newly created Simplex system + + """ + js = json.loads(jsString) + if name is None: + name = js["systemName"] + return cls.buildSystemFromDict( + js, thing, name=name, forceDummy=forceDummy, sliderMul=sliderMul, pBar=pBar + ) + + @classmethod + def buildSystemFromJson( + cls, jsPath, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None + ): + """Build a system from .json file + + Parameters + ---------- + jsPath : str + The .json file to load + thing : object + The DCC mesh to build the system on (Default value = None) + name : str + The name of the new Simplex system (Default value = None) + forceDummy : bool + When loading, don't make a connection to the actual DCC. Instead use the + "dummy" DCC. Defaults False + sliderMul : float + A multiplier for the range of sliders. Simplex will only define values + between -1 and 1. This multiplier will let the attribute range in the DCC be larger so + animators can push the extremes (Default value = 1.0) + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + : Simplex + A newly created Simplex system + + """ + with open(jsPath, "r") as f: + jsString = f.read() + return cls.buildSystemFromJsonString( + jsString, + thing, + name=name, + forceDummy=forceDummy, + sliderMul=sliderMul, + pBar=pBar, + ) + + @classmethod + def buildSystemFromSmpx( + cls, smpxPath, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None + ): + """Build a system from a .smpx file + SMPX files are (under-the-hood) alembic caches with each shape delta stored as a frame of animation, + and the json string stored in a property on the mesh. + + Parameters + ---------- + smxpPath : str + The .smpx file to load + thing : object + The DCC mesh to build the system on (Default value = None) + name : str + The name of the new Simplex system (Default value = None) + forceDummy : bool + When loading, don't make a connection to the actual DCC. Instead use the + "dummy" DCC. Defaults False + sliderMul : float + A multiplier for the range of sliders. Simplex will only define values + between -1 and 1. This multiplier will let the attribute range in the DCC be larger so + animators can push the extremes (Default value = 1.0) + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + : Simplex + A newly created Simplex system + + """ + if thing is None: + thing = cls.buildBaseObject(smpxPath, forceDummy=forceDummy) + iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) + js = json.loads(jsString) + + if not forceDummy: + smpxCount = getPointCount(abcMesh) + dccCount = DCC.vertCount(thing) + if smpxCount != dccCount: + raise RuntimeError( + "Point Count Mismatch. Smpx File:{0} DCC:{1}".format( + smpxCount, dccCount + ) + ) + + del iarch, abcMesh # release the files + if name is None: + name = js["systemName"] + self = cls.buildSystemFromDict( + js, thing, name=name, forceDummy=forceDummy, sliderMul=sliderMul, pBar=pBar + ) + self.loadSmpxShapes(smpxPath, pBar=pBar) + self.loadSmpxFalloffs(smpxPath, pBar=pBar) + return self + + @classmethod + def buildSystemFromFile( + cls, path, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None + ): + """Build a system from a file + + Parameters + ---------- + path : str + The file to load. Either .json or .smpx + thing : object + The DCC mesh to build the system on (Default value = None) + name : str + The name of the new Simplex system (Default value = None) + forceDummy : bool + When loading, don't make a connection to the actual DCC. Instead use the + "dummy" DCC. Defaults False + sliderMul : float + A multiplier for the range of sliders. Simplex will only define values + between -1 and 1. This multiplier will let the attribute range in the DCC be larger so + animators can push the extremes (Default value = 1.0) + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + : Simplex + A newly created Simplex system + + """ + if path.endswith(".json"): + return cls.buildSystemFromJson( + path, + thing=thing, + name=name, + forceDummy=forceDummy, + sliderMul=sliderMul, + pBar=pBar, + ) + elif path.endswith(".smpx"): + return cls.buildSystemFromSmpx( + path, + thing=thing, + name=name, + forceDummy=forceDummy, + sliderMul=sliderMul, + pBar=pBar, + ) + else: + raise ValueError( + "The filepath provided is not a .json or .smpx: {0}".format(path) + ) + + @classmethod + def buildSystemFromMesh( + cls, thing, name, forceDummy=False, sliderMul=1.0, pBar=None + ): + """Build a system from the data already built in the DCC + + Parameters + ---------- + thing : object + The DCC mesh to load the system from + name : str + The name of the new Simplex system + forceDummy : bool + When loading, don't make a connection to the actual DCC. Instead use the + "dummy" DCC. Defaults False + sliderMul : float + A multiplier for the range of sliders. Simplex will only define values + between -1 and 1. This multiplier will let the attribute range in the DCC be larger so + animators can push the extremes (Default value = 1.0) + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + : Simplex + A newly created Simplex system + + """ + jsDict = json.loads(DCC.getSimplexStringOnThing(thing, name)) + return cls.buildSystemFromDict( + jsDict, + thing, + name=name, + create=False, + forceDummy=forceDummy, + sliderMul=sliderMul, + pBar=pBar, + ) + + @classmethod + def buildSystemFromDict( + cls, + jsDict, + thing, + name=None, + create=True, + forceDummy=False, + sliderMul=1.0, + pBar=None, + ): + """Utility for building a cleared system from a dictionary + + Parameters + ---------- + jsDict : dict + The definition dictonary (parsed from a json string) + thing : object + The DCC mesh to load the system on + name : str, optional + The name of the new Simplex system. If None, default to the system name + create : bool, optional + Create any missing blendshapes as the system is loaded. If False, error on missing. + Defaults to True + forceDummy : bool, optional + When loading, don't make a connection to the actual DCC. Instead use the + "dummy" DCC. Defaults to False + sliderMul : float, optional + A multiplier for the range of sliders. Simplex will only define values + between -1 and 1. This multiplier will let the attribute range in the DCC be larger so + animators can push the extremes (Default value = 1.0) + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + : Simplex + A newly created Simplex system + + """ + if name is None: + name = jsDict["systemName"] + self = cls(name, forceDummy=forceDummy, sliderMul=sliderMul) + self.DCC.loadNodes(self, thing, create=create) + self.loadDefinition(jsDict, create=create, pBar=pBar) + return self + + def loadSmpxShapes(self, smpxPath, pBar=None): + """Load the Shapes from a .smpx file onto an already loaded system + This is the "We got updated shapes from the modelers" method + + Parameters + ---------- + smpxPath : str + The path to the .smpx file + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + + """ + iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) + js = json.loads(jsString) + + try: + self.DCC.loadAbc(abcMesh, js, pBar=pBar) + finally: + del abcMesh, iarch + + def loadSmpxPoses(self, smpxPath, pBar=None): + """Load the Poses from a .smpx file onto an already loaded system + This is the "Update the joints and skin" method + + Parameters + ---------- + smpxPath : str + The path to the .smpx file + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + + """ + iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) + js = json.loads(jsString) + + try: + self.DCC.loadAbcPoses(abcMesh, js, pBar=pBar) + finally: + del abcMesh, iarch + + def loadSmpxFalloffs(self, abcPath, pBar=None): + """Load the relevant data from a simplex alembic + + Parameters + ---------- + abcPath : str + Path to the .smpx file + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + + """ + foDict = readFalloffData(abcPath) + + for fo in self.falloffs: + foData = foDict.get(fo.name, None) + if foData is not None: + fo.weights = foData + + # Properties + @property + def name(self): + """Get the system name""" + return self._name + + @name.setter + @stackable + def name(self, value): + """Set the system name""" + if value == self._name: + return + + self._name = value + self.DCC.renameSystem(value) # ??? probably needs work + if self.restShape is not None: + self.restShape.name = self.getRestName() + + for model in self.models: + model.itemDataChanged(self) + + @property + def progs(self): + """Get all Progressions defined in the system""" + out = [] + for slider in self.sliders: + out.append(slider.prog) + for combo in self.combos: + out.append(combo.prog) + for trav in self.traversals: + out.append(trav.prog) + return out + + @property + def groups(self): + """Get all Groups defined in the system""" + return self.sliderGroups + self.comboGroups + self.traversalGroups + + def treeChild(self, row): + """ """ + return self.groups[row] + + def treeRow(self): + """ """ + return 0 + + def treeParent(self): + """ """ + return None + + def treeChildCount(self): + """ """ + return len(self.groups) + + def treeData(self, column): + """ """ + if column == 0: + return self.name + return None + + def treeChecked(self): + """ """ + return None + + def icon(self): + return None + + # HELPER + def comboExists(self, sliders, values): + """Check if a combo exists with these specific sliders and values + Because combo names aren't necessarily always in the same order + + Parameters + ---------- + sliders : [Slider, ....] + The sliders to check + values : [float, ....] + The values to check + + Returns + ------- + : Combo or None + The Combo with those sliders and values, or None if none found + + """ + checkSet = {(s.name, v) for s, v in zip(sliders, values)} + for cmb in self.combos: + cmbSet = {(p.slider.name, p.value) for p in cmb.pairs} + if checkSet == cmbSet: + return cmb + return None + + # DESTRUCTOR + def deleteSystem(self): + """Delete an existing system from the DCC""" + # Store the models as temp so the model doesn't go crazy with the signals + models, self.models = self.models, None + mgrs = [model.resetModelManager() for model in models] + with nested(*mgrs): + self.DCC.deleteSystem() + self._initValues() + self.DCC = DCC(self) + self.models = models + + def getComboUpstreams(self, combo): + """Get a list of only combos that are upstream to the given combo + In this case, "upstream" means that when the given combo is active, + then any returned combos are also active. + + I am currently ignoring floating combos in this function + The upstream sliders are available trivially through combo.pairs + + Parameters + ---------- + combo : Combo + The given combo + + Returns + ------- + : [Combo, ....] + The list of upstream combos + + """ + pairDict = {p.slider: p.value for p in combo.pairs} + + upstreams = [] + for c in self.combos: + if c.isFloating(): + continue + + if len(c.pairs) >= len(pairDict): + continue + + if not all(p.slider in pairDict for p in c.pairs): + continue + + fail = False + for p in c.pairs: + if p.slider not in pairDict: + fail = True + break + if pairDict[p.slider] * p.value < 0.0: + # opposite Signs + fail = True + break + + if not fail: + upstreams.append(c) + + return upstreams + + def getDownstreamTraversals(self, slider): + """Get a list of any traversals that depend on the given slider + + Parameters + ---------- + slider : Slider + The system slider to check + + Returns + ------- + : [Traversal, ....] + The list of dependent Traversals + + """ + downstream = [] + for t in self.traversals: + for pair in t.startPoint.pairs + t.endPoint.pairs: + if slider == pair.slider: + downstream.append(t) + break + downstream = list(set(downstream)) + return downstream + + def getDownstreamCombos(self, slider): + """Get a list of any Combos that depend on the given slider + + Parameters + ---------- + slider : Slider + The Slider item to check + + Returns + ------- + : [Combo, ....] + The list of dependent Combos + + """ + downstream = [] + if not isinstance(slider, Slider): + return downstream + for c in self.combos: + for pair in c.pairs: + if pair.slider == slider: + downstream.append(c) + break + downstream = list(set(downstream)) + return downstream + + def deleteDownstream(self, item): + """Delete all items from the system that depend on the given item + + Parameters + ---------- + item : object + The system item to check + + Returns + ------- + + """ + todel = [] + todel.extend(self.getDownstreamCombos(item)) + todel.extend(self.getDownstreamTraversals(item)) + for c in todel: + c.delete() + + # USER METHODS + def setLegacy(self, legacy): + """Set whether to use the legacy .json format + + Parameters + ---------- + legacy : bool + Whether to use the legacy .json format + + Returns + ------- + + """ + self._legacy = legacy + + def getFloatingShapes(self): + """Find Combos with values other than -1 and 1 + + Parameters + ---------- + + Returns + ------- + : [Combo, ....] + Combos that don't have fully extreme activations + + """ + floaters = [c for c in self.combos if c.isFloating()] + floatShapes = [] + for f in floaters: + floatShapes.extend(f.prog.getShapes()) + return floatShapes + + def buildDefinition(self): + """Create a simplex definition dictionary + Loop through all the objects managed by this simplex system, and build a dictionary that defines it + + Parameters + ---------- + + Returns + ------- + : dict + The simplex definition dictionary + + """ + things = [ + self.shapes, + self.sliders, + self.combos, + self.traversals, + self.groups, + self.falloffs, + ] + for thing in things: + for i in thing: + i.clearBuildIndex() + + # Make sure we start with the extras in + # case we're overwriting with new data + d = copy.deepcopy(self._extras) + + # Then set all the top-level system keys + d["encodingVersion"] = 1 if self._legacy else 3 + d["systemName"] = self.name + d["clusterName"] = self.clusterName + d.setdefault("falloffs", []) + d.setdefault("combos", []) + d.setdefault("shapes", []) + d.setdefault("sliders", []) + d.setdefault("groups", []) + d.setdefault("progressions", []) + d.setdefault("traversals", []) + + # rest shape should *ALWAYS* be index 0 + for shape in self.shapes: + shape.buildDefinition(d, self._legacy) + + for group in self.groups: + group.buildDefinition(d, self._legacy) + + for falloff in self.falloffs: + falloff.buildDefinition(d, self._legacy) + + for slider in self.sliders: + slider.buildDefinition(d, self._legacy) + + for combo in self.combos: + combo.buildDefinition(d, self._legacy) + + for trav in self.traversals: + trav.buildDefinition(d, self._legacy) + + return d + + @stackable + def loadDefinition(self, simpDict, create=True, pBar=None): + """Build the structure of objects in this system + based on a provided dictionary + + Parameters + ---------- + simpDict : dict + The dictionary to load + create : bool, optional + Create any missing blendshapes as the system is loaded. If False, error on missing. + Defaults to True + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + + """ + + self.name = simpDict["systemName"] + self.clusterName = simpDict["clusterName"] # for XSI + if simpDict["encodingVersion"] == 1: + self.loadV1(simpDict, create=create, pBar=pBar) + elif simpDict["encodingVersion"] == 2: + self.loadV2(simpDict, create=create, pBar=pBar) + elif simpDict["encodingVersion"] == 3: + self.loadV3(simpDict, create=create, pBar=pBar) + self.storeExtras(simpDict) + + def _incPBar(self, pBar, txt, inc=1): + """Increment the progress bar and return False if the user cancelled + + Parameters + ---------- + pBar : + + txt : + + inc : + (Default value = 1) + + Returns + ------- + + """ + if pBar is not None: + pBar.setValue(pBar.value() + inc) + pBar.setLabelText("Building:\n" + txt) + QApplication.processEvents() + return not pBar.wasCanceled() + return True + + def loadV3(self, simpDict, create=True, pBar=None): + """Load the version 3 simplex definition + V3 is just the same as V2, except for an update Traversal definition + + Parameters + ---------- + simpDict : dict + The simplex definition dictionary + create : bool, optional + Create any missing blendshapes as the system is loaded. If False, error on missing. + Defaults to True + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + + """ + preRet = self.DCC.preLoad(self, simpDict, create=create, pBar=pBar) + try: + fos = simpDict.get("falloffs", []) + gs = simpDict.get("groups", []) + for f in fos: + Falloff.loadV2(self, f) + if gs: + for g in gs: + Group.loadV2(self, g) + else: + Group("Group_0", self, Slider) + Group("Group_1", self, Combo) + Group("Group_2", self, Traversal) + + if pBar is not None: + maxLen = max(len(i["name"]) for i in simpDict["shapes"]) + pBar.setLabelText("_" * maxLen) + pBar.setValue(0) + pBar.setMaximum(len(simpDict["shapes"]) + 1) + self.shapes = [] + for s in simpDict["shapes"]: + if not self._incPBar(pBar, s["name"]): + return + Shape.loadV2(self, s, create) + + self.restShape = self.shapes[0] + self.restShape.isRest = True + + progs = [Progression.loadV2(self, p) for p in simpDict["progressions"]] + + for s in simpDict["sliders"]: + Slider.loadV2(self, progs, s, create) + for c in simpDict["combos"]: + Combo.loadV2(self, progs, c) + for t in simpDict["traversals"]: + Traversal.loadV3(self, progs, t) + + for x in itertools.chain(self.sliders, self.combos, self.traversals): + x.prog.name = x.name + finally: + self.DCC.postLoad(self, preRet) + + def loadV2(self, simpDict, create=True, pBar=None): + """Load the version 2 simplex definition + + Parameters + ---------- + simpDict : dict + The simplex definition dictionary + create : bool, optional + Create any missing blendshapes as the system is loaded. If False, error on missing. + Defaults to True + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + + """ + preRet = self.DCC.preLoad(self, simpDict, create=create, pBar=pBar) + try: + fos = simpDict.get("falloffs", []) + gs = simpDict.get("groups", []) + for f in fos: + Falloff.loadV2(self, f) + if gs: + for g in gs: + Group.loadV2(self, g) + else: + Group("Group_0", self, Slider) + Group("Group_1", self, Combo) + Group("Group_2", self, Traversal) + + if pBar is not None: + maxLen = max(len(i["name"]) for i in simpDict["shapes"]) + pBar.setLabelText("_" * maxLen) + pBar.setValue(0) + pBar.setMaximum(len(simpDict["shapes"]) + 1) + self.shapes = [] + for s in simpDict["shapes"]: + if not self._incPBar(pBar, s["name"]): + return + Shape.loadV2(self, s, create) + + self.restShape = self.shapes[0] + self.restShape.isRest = True + + progs = [Progression.loadV2(self, p) for p in simpDict["progressions"]] + + for s in simpDict["sliders"]: + Slider.loadV2(self, progs, s, create) + for c in simpDict["combos"]: + Combo.loadV2(self, progs, c) + for t in simpDict["traversals"]: + Traversal.loadV2(self, progs, t) + + for x in itertools.chain(self.sliders, self.combos, self.traversals): + x.prog.name = x.name + finally: + self.DCC.postLoad(self, preRet) + + def loadV1(self, simpDict, create=True, pBar=None): + """Load the version 1 simplex definition + + Parameters + ---------- + simpDict : dict + The simplex definition dictionary + create : bool, optional + Create any missing blendshapes as the system is loaded. If False, error on missing. + Defaults to True + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + + """ + preRet = self.DCC.preLoad(self, simpDict, create=create, pBar=pBar) + try: + self.falloffs = [Falloff(f[0], self, *f[1:]) for f in simpDict["falloffs"]] + groupNames = simpDict["groups"] + + if pBar is not None: + maxLen = max(list(map(len, simpDict["shapes"]))) + pBar.setLabelText("_" * maxLen) + pBar.setValue(0) + pBar.setMaximum(len(simpDict["shapes"]) + 1) + + shapes = [] + for s in simpDict["shapes"]: + if not self._incPBar(pBar, s): + return + shapes.append(Shape(s, self)) + + self.restShape = shapes[0] + self.restShape.isRest = True + + progs = [] + for p in simpDict["progressions"]: + progShapes = [shapes[i] for i in p[1]] + progFalloffs = [self.falloffs[i] for i in p[4]] + progPairs = [ProgPair(self, s, pv) for s, pv in zip(progShapes, p[2])] + progs.append(Progression(p[0], self, progPairs, p[3], progFalloffs)) + + self.sliders = [] + self.sliderGroups = [] + createdSlidergroups = {} + for s in simpDict["sliders"]: + sliderProg = progs[s[1]] + + gn = groupNames[s[2]] + if gn in createdSlidergroups: + sliderGroup = createdSlidergroups[gn] + else: + sliderGroup = Group(gn, self, Slider) + createdSlidergroups[gn] = sliderGroup + + Slider(s[0], self, sliderProg, sliderGroup) + + self.combos = [] + self.comboGroups = [] + createdComboGroups = {} + for c in simpDict["combos"]: + prog = progs[c[1]] + sliderIdxs, sliderVals = list(zip(*c[2])) + sliders = [self.sliders[i] for i in sliderIdxs] + pairs = list(map(ComboPair, sliders, sliderVals)) + if len(c) >= 4: + gn = groupNames[c[3]] + else: + gn = "DEPTH_0" + + if gn in createdComboGroups: + comboGroup = createdComboGroups[gn] + else: + comboGroup = Group(gn, self, Combo) + createdComboGroups[gn] = comboGroup + + cmb = Combo(c[0], self, pairs, prog, comboGroup, None) + cmb.simplex = self + + self.traversals = [] + self.traversalGroups = [] + createdTraversalGroups = {} + if "traversals" in simpDict: + for t in simpDict["traversals"]: + name = t["name"] + prog = progs[t["prog"]] + + pcIdx = t["progressControl"] + pcSearch = ( + self.sliders + if t["progressType"].lower() == "slider" + else self.combos + ) + pc = pcSearch[pcIdx] + pFlip = t["progressFlip"] + pp = TravPair(pc, -1 if pFlip else 1, "progress") + + mcIdx = t["multiplierControl"] + mcSearch = ( + self.sliders + if t["multiplierType"].lower() == "slider" + else self.combos + ) + mc = mcSearch[mcIdx] + mFlip = t["multiplierFlip"] + mm = TravPair(mc, -1 if mFlip else 1, "multiplier") + + gn = groupNames[t.get("group", 2)] + if gn in createdTraversalGroups: + travGroup = createdTraversalGroups[gn] + else: + travGroup = Group(gn, self, Traversal) + createdTraversalGroups[gn] = travGroup + + color = QColor(*t.get("color", (0, 0, 0))) + + trav = Traversal(name, self, mm, pp, prog, travGroup, color) + trav.simplex = self + + for x in itertools.chain(self.sliders, self.combos, self.traversals): + x.prog.name = x.name + finally: + self.DCC.postLoad(self, preRet) + + def storeExtras(self, simpDict): + """Store any unknown keys when dumping, just in case they're important elsewhere + + Parameters + ---------- + simpDict : dict + The simplex definition dictionary + + Returns + ------- + + """ + sd = copy.deepcopy(simpDict) + knownTopLevel = [ + "encodingVersion", + "systemName", + "clusterName", + "falloffs", + "combos", + "shapes", + "sliders", + "groups", + "traversals", + "progressions", + ] + + for ktn in knownTopLevel: + if ktn in sd: + del sd[ktn] + self._extras = sd + + def loadJSON(self, jsString): + """Convenience method to load a JSON string definition + + Parameters + ---------- + jsString : str + The json formatted definition string + + Returns + ------- + + """ + self.loadDefinition(json.loads(jsString)) + + def getRestName(self): + """Get the default rest shape name + + Parameters + ---------- + + Returns + ------- + : str + The default rest shape name + + """ + return "Rest_{0}".format(self.name) + + def dump(self): + """Dump the definition dictionary to a json string + + Parameters + ---------- + + Returns + ------- + : str + The json formatted definition string + + """ + return json.dumps(self.buildDefinition()) + + def exportAbc(self, path, pBar=None): + """Export the current mesh to a .smpx formatted file + + Parameters + ---------- + path : str + The path to export to + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + + """ + defDict = self.buildDefinition() + jsString = json.dumps(defDict) + + arch, abcMesh = buildAlembicArchiveData(path, self.name, jsString, True) + try: + self.DCC.exportAbc( + self.DCC.mesh, + abcMesh, + defDict, + world=False, + ensureCorrect=True, + pBar=pBar, + ) + finally: + del arch, abcMesh + + def exportOther(self, path, dccMesh, world=False, ensureCorrect=False, pBar=None): + """Export shapes from a mesh that isn't part of the current system + + The export process for shapes differs from DCC to DCC. + In Maya, every blendshape is activated, one by one and the point posisitions + are read from the target mesh. This allows exportOther to work + + In XSI, the blendshape properties are read directly, so there's no chance + to process the new shapes (which means this won't work in XSI) + + Parameters + ---------- + path : str + The output path for the .smpx file + dccMesh : object + The system-external DCC mesh to export + world : bool, optional + Whether to do the export in worldspace. Defaults to False + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + """ + defDict = self.buildDefinition() + jsString = json.dumps(defDict) + + arch, abcMesh = buildAlembicArchiveData(path, self.name, jsString, True) + try: + self.DCC.exportOtherAbc(dccMesh, abcMesh, defDict, world=world, pBar=pBar) + finally: + del arch, abcMesh + + def setSlidersWeights(self, sliders, weights): + """Set the weights of multiple sliders as one method + + Parameters + ---------- + sliders : [Slider, ....] + The list of Sliders to set weights for + weights : [float, ....] + The weights to set + + Returns + ------- + + """ + with undoContext(self.DCC): + for slider, weight in zip(sliders, weights): + slider.value = weight + self.DCC.setSlidersWeights(sliders, weights) + for model in self.models: + for slider in sliders: + model.itemDataChanged(slider) + + def extractRestShape(self, offset=0): + """Extract the rest shape to a mesh in the DCC + + Parameters + ---------- + offset : float + The offset value given to the extracted mesh (Default value = 0) + + Returns + ------- + + """ + if self.restShape is None: + return None + return self.DCC.extractShape(self.restShape, live=False, offset=offset) + + def buildRestShape(self): + """Build and store a rest shape for this system""" + self.restShape = Shape.buildRest(self) + return self.restShape + + def buildInputVectors( + self, + keepSliders=None, + ignoreSliders=None, + depthCutoff=None, + ignoreFloaters=False, + ignoreTraversals=False, + extremes=False, + ): + """This is kind of a specialized function. Often, I have to sum combo deltas + into the full sculpted shape. But to do that, I have to build the inputs to the + solver that enable each shape, and I need a name for each shape. + This function gives me both. + + Parameters + ---------- + keepSliders : set([str, ....]), optional + The returned sliders will have names that are part of this list + ignoreSliders : set([str, ....]), optional + A set of Slider names to ignore as part of this process + depthCutoff : int, optional + The maximum number of Sliders allowed per Combo + (Defaults to None which allows all Combos) + ignoreFloaters : bool, optional + Ignore Combos with values other than -1 and 1 + extremes : bool, optional + Only build activations for sliders and combos at -1 and 1 + + Returns + ------- + : [str, ...] + The names of the shapes that are activated in each list + : [[float, ...], ...] + A list of activation values for the entire system + + """ + # InputVector comes from the c++ std::vector + # Get all endpoint shapes from the progressions + shapeNames = [] + inVecs = [] + keyIdxs = [] + ignoreSliders = ignoreSliders or set() + ignoreSliders = set(ignoreSliders) + indexByShape = {shape: idx for idx, shape in enumerate(self.shapes)} + + for slIdx, slider in enumerate(self.sliders): + if slider.name in ignoreSliders: + continue + if keepSliders is not None and slider.name not in keepSliders: + continue + + pairs = [p for p in slider.prog.pairs if not p.shape.isRest] + if extremes: + pairs = [p for p in pairs if abs(p.value) == 1.0] + + for pp in pairs: + inVec = [0.0] * len(self.sliders) + inVec[slIdx] = pp.value + shapeNames.append(pp.shape.name) + inVecs.append(inVec) + keyIdxs.append(indexByShape[pp.shape]) + + for combo in self.combos: + if ignoreFloaters and combo.isFloating(): + continue + if depthCutoff is not None and len(combo.pairs) > depthCutoff: + continue + if ignoreSliders & {i.name for i in combo.getSliders()}: + # if the combo's sliders are in ignoreSliders + continue + + pairs = [p for p in combo.prog.pairs if not p.shape.isRest] + if extremes: + pairs = [p for p in pairs if abs(p.value) == 1.0] + + iv = combo.getInputVector() + for pp in pairs: + inVecs.append([x * pp.value for x in iv]) + shapeNames.append(pp.shape.name) + keyIdxs.append(indexByShape[pp.shape]) + + for trav in self.traversals: + # Extremes doesn't make sense at all for traversals + if ignoreTraversals: + continue + pairs = [p for p in trav.prog.pairs if not p.shape.isRest] + if extremes: + pairs = [p for p in pairs if abs(p.value) == 1.0] + + for pp in pairs: + inVecs.append(trav.getInputVector(pp.value)) + shapeNames.append(pp.shape.name) + keyIdxs.append(indexByShape[pp.shape]) + + return shapeNames, inVecs, keyIdxs + + def evaluateInputs(self, inVecs): + """Get the shape activation vectors that are paired with the given input vectors + It will probably be useful to pass the returned inVecs from `buildInputVectors` + + This will use the compiled python solver, and may not be available + + Parameters + ---------- + inVecs : [[SliderVal, ...], ...] + A list of lists of sliderValues. Each sub-list has to have the same number of + items as there are sliders in the current simplex system + + Returns + ------- + : [[ShapeVal, ...], ...] + A list of lists of shapeValues resulting from the given inVecs. This returns + the activations of all the shape values. Each sub-list will have the same + number of items as there are shapes in the current system. + """ + from pysimplex import PySimplex + + solver = PySimplex(self.dump()) + return [solver.solve(iv) for iv in inVecs] + + def controllersByDepth(self): + """Get the shapes ordered by the depth of their controllers + in the simplex hierarchy + This is often useful when doing vertex position computations + + Returns + ------- + : [Shape, ...] + A list of all shapes in the depth order + """ + ctrlOrder = self.sliders[:] + combosByDepth = {} + for c in self.combos: + combosByDepth.setdefault(len(c.pairs), []).append(c) + + for depth in sorted(combosByDepth.keys()): + combos = combosByDepth[depth] + regular, floating = [], [] + for c in combos: + lst = floating if c.isFloating() else regular + lst.append(c) + ctrlOrder.extend(regular + floating) + + travByDepth = {} + for t in self.traversals: + travByDepth.setdefault(len(t.startPoint.pairs), []).append(t) + + for depth in sorted(travByDepth.keys()): + ctrlOrder.extend(travByDepth[depth]) + + return ctrlOrder + + # SPLIT CODE + def buildSplitterList(self, foList): + """The way deepcopy works is that every object visited is added to the 'memo' dictionary, + keyed by its id(). This way, you don't have to re-copy an object if you've already seen + it. This means that if I make a memo that already contains objects that I don't want copied, + then I should just be able to use deepcopy, and that will handle keeping references to the + un-copied objects. + + For example: If X references A, and I want to split X into L and R, then I would still want + L to reference A *and* R to reference A. This process just auto-handles that + I think that's is kinda neat. + + Parameters + ---------- + foList : [Falloff, ....] + A list of Falloff objects *that all share the same split axis* + + Returns + ------- + : [object, ....] + A list of objects to be split + : dict + A dict of {object: Falloff} saying what falloff should be used to split each objct + : dict + The memo to start the deepcopy with + + """ + # Add all items to the memo. + memo = {} + memo[id(self)] = self + stack = Stack() + stack.enabled = False + memo[id(self.stack)] = stack + memo[id(self.DCC)] = self.DCC + + memList = [ + self.groups, + self.sliders, + self.combos, + self.traversals, + self.falloffs, + self.shapes, + self.progs, + ] + + for lst in memList: + for item in lst: + memo[id(item)] = item + # splitApplied needs to be part of the item so it persists + # through the copy. Otherwise I'd have to keep track of it + item._splitApplied = set() + + # Using the memo from here would mean that nothing got copied + # because all items are in it + + toSplit = [] # A list of objects to be split along the shared foList axis + + # A dictionary of {object: set(falloff)}. When recursing down the hierarchy, + # some objects may be split by many falloffs. Keep track of that in this dict + splitBySet = {} + + for prog in self.progs: + # I take pains to this with lists (rather than sets) so it keeps order + sect = [i for i in foList if i in prog.falloffs] + if sect: + # Because I can only split an item once on an axis + # Assume that the foList is in priority order + # ... So I can just grab the first (highest priority) + # falloff affecting this item + splitFalloff = sect[0] + ctrl = prog.controller + + toSplit.append(prog) + toSplit.append(ctrl) + splitBySet.setdefault(prog, set()).add(splitFalloff) + splitBySet.setdefault(ctrl, set()).add(splitFalloff) + for pair in prog.pairs: + toSplit.append(pair.shape) + splitBySet.setdefault(pair.shape, set()).add(splitFalloff) + + # Also add the downstream combos, and their progs + dss = [] + if isinstance(ctrl, Slider): + dss.extend(self.getDownstreamCombos(ctrl)) + dss.extend(self.getDownstreamTraversals(ctrl)) + + for ds in dss: + toSplit.append(ds) + toSplit.append(ds.prog) + splitBySet.setdefault(ds, set()).add(splitFalloff) + splitBySet.setdefault(ds.prog, set()).add(splitFalloff) + for pair in ds.prog.pairs: + toSplit.append(pair.shape) + splitBySet.setdefault(pair.shape, set()).add(splitFalloff) + + # Dict of {item : falloff} + splitBy = {} + for item, foSet in splitBySet.items(): + # Because I can only split an item once on an axis + # Assume that the foList is in priority order + # So get the min-indexed falloff in the set + idx = min([foList.index(f) for f in foSet]) + splitBy[item] = foList[idx] + + toSplit = set(toSplit) + toSplit.discard(self.restShape) + # Get the items to split that haven't already had a split applied along this axis + toSplit = [i for i in toSplit if foList[0].axis not in i._splitApplied] + # If I can't apply a sided name ot this item, then it can't be split + toSplit = [i for i in toSplit if foList[0].canRename(i)] + + # Remove the splitter items from the memo, ensuring they actually get copied + for sp in toSplit: + try: + del memo[id(sp)] + except KeyError: + print("SP", sp, sp.name) + raise + sp._splitApplied.add(foList[0].axis.lower()) + + return toSplit, splitBy, memo + + def split(self, pBar=None): + """Return a split deepcopy of the system. + The new system will be a dummy system containing all the shapes as numpy arrays which can be + exported to a .smpx file + + Parameters + ---------- + pBar : QProgressDialog, optional + If provided, display progress in this dialog + + Returns + ------- + : Simplex : + A newly split system + + """ + if np is None: + raise RuntimeError("Numpy is not available, and splitting requires it") + + if pBar is not None: + pBar.setValue(0) + pBar.setLabelText("Building Split System") + + # Very first thing: Ensure that every object with a falloff is fully splittable + # Meaning that splittable progs only contain splittable shapes. And splittable progs + # are only controlled by splittable controllers + for fo in self.falloffs: + controllers = self.sliders + self.combos + self.traversals + for ctrl in controllers: + prog = ctrl.prog + + pSplit = fo.canRename(prog) + cSplit = fo.canRename(ctrl) + sSplit = [s for s in prog.getShapes() if not s.isRest] + sSplit = [fo.canRename(shape) for shape in sSplit] + sSplitSame = all(i == sSplit[0] for i in sSplit) + sSplit = sSplit[0] + if not sSplitSame: + shapes = [i.name for i in prog.getShapes()] + msg = "Bad shapes: {0}".format(", ".join(shapes)) + raise ValueError( + "Mix of splittable and un-splittable shapes in a progression\n" + + msg + ) + + if pSplit != sSplit: + msg = "Bad Prog: {0}".format(prog.name) + raise ValueError("A progression is not fully splittable\n" + msg) + + if pSplit != cSplit: + msg = "Bad prog: {0}\nBad Controller:{1}".format( + prog.name, ctrl.name + ) + raise ValueError("A controller is not fully splittable\n" + msg) + + # Create the initial deepcopy + splitSmpx = copy.deepcopy(self) + splitSmpx.DCC.dummyLoad(self.DCC, pBar=pBar) + + # Sort the falloffs by which axis the split on + foByAxis = {} + for fo in splitSmpx.falloffs: + foByAxis.setdefault(fo.axis.lower(), []).append(fo) + + for axis, foList in foByAxis.items(): + if pBar is not None: + pBar.setLabelText("Splitting On {0} axis".format(axis)) + QApplication.processEvents() + else: + print("Splitting On {0} axis".format(axis)) + + # Get the items to split, and the memo that ensures *only* those items will be copied + toSplit, splitBy, memo = splitSmpx.buildSplitterList(foList) + + # DeepCopy the items twice. Once for each side of the split + lSideSplitList = copy.deepcopy(toSplit, memo=copy.copy(memo)) + rSideSplitList = copy.deepcopy(toSplit, memo=copy.copy(memo)) + + if pBar is not None: + pBar.setMaximum(len(toSplit)) + QApplication.processEvents() + + # Loop through the copied items and make the replacements + for i, (oldItem, lItem, rItem) in enumerate( + zip(toSplit, lSideSplitList, rSideSplitList) + ): + if pBar is not None: + pBar.setValue(i + 1) + QApplication.processEvents() + + # Get thefalloff that will split oldItem into lItem and rItem + fo = splitBy[oldItem] + + # Rename the newly split items + fo.splitRename(lItem, 0) + fo.splitRename(rItem, 1) + + # Apply the falloff weights to any shapes + if isinstance(oldItem, Shape): + fo.applyFalloff(lItem, 0) + fo.applyFalloff(rItem, 1) + + # Maybe for all this, get the index of the oldItem in the group + # and insert rather than append?? + + # Remove the oldItem from any groups and add the newItems + if hasattr(oldItem, "group"): + oldItem.group.items.remove(oldItem) + oldItem.group = None + lItem.group.items.append(lItem) + rItem.group.items.append(rItem) + + # Remove the oldItem from the simplex storage, and add the newItems + if isinstance(oldItem, Slider): + splitSmpx.sliders.remove(oldItem) + splitSmpx.sliders.append(lItem) + splitSmpx.sliders.append(rItem) + elif isinstance(oldItem, Combo): + splitSmpx.combos.remove(oldItem) + splitSmpx.combos.append(lItem) + splitSmpx.combos.append(rItem) + elif isinstance(oldItem, Traversal): + splitSmpx.traversals.remove(oldItem) + splitSmpx.traversals.append(lItem) + splitSmpx.traversals.append(rItem) + elif isinstance(oldItem, Shape): + splitSmpx.shapes.remove(oldItem) + # Part of deepCopy ensures the new system uses the DummyDCC + # So this just removes the shape verts from the DummyDCC dictionary + # and doesn't actually delete the shape from anywhere important + splitSmpx.DCC.deleteShape(oldItem) + splitSmpx.shapes.append(lItem) + splitSmpx.shapes.append(rItem) + + splitSmpx.DCC.pushAllShapeVertices(splitSmpx.shapes) + return splitSmpx diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index 72c7d9f1..02ad430e 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -1,738 +1,738 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use -import itertools - -from ..interface import undoContext -from Qt.QtGui import QColor -from ..utils import caseSplit, getNextName, makeUnique, nested, singleShot -from .accessor import SimplexTickAccessor -from .group import Group -from .progression import ProgPair, Progression -from .shape import Shape -from .stack import stackable - - -class Slider(SimplexTickAccessor): - """A user-input to the simplex system that directly controls a Progression - - Parameters - ---------- - name : str - The name of this Slider - simplex : Simplex - The parent Simplex system - prog : Progression - The Progression that this Slider controls - group : Group - The Group to create this Slider in - color : QColor - The color of this item in the UI - create : bool - Whether to create a DCC Shape, or look for it already in-scene - - Returns - ------- - - """ - - classDepth = 7 - - def __init__(self, name, simplex, prog, group, color=None, create=True): - if group.groupType is not type(self): - raise ValueError("Cannot add this slider to a combo group") - - super(Slider, self).__init__(simplex) - color = QColor(128, 128, 128) if color is None else color - with self.stack.store(self): - self._name = name - self._thing = None - self._thingRepr = None - self.prog = prog - self.split = False - self.prog.controller = self - self._buildIdx = None - self._value = 0.0 - self.expanded = {} - self.color = color - self._enabled = True - - mn, mx = self.prog.getRange() - self.minValue = mn - self.maxValue = mx - - mgrs = [model.insertItemManager(group) for model in self.models] - with nested(*mgrs): - self.group = group - self.group.items.append(self) - - self.simplex.sliders.append(self) - - newThing = self.DCC.getSliderThing(self._name) - if newThing is None: - if create: - self.thing = simplex.DCC.createSlider(self) - else: - raise RuntimeError( - "Unable to find existing shape: {0}".format(self.name) - ) - else: - self.thing = newThing - - @property - def enabled(self): - """Get whether this Slider is evaluated in the solver""" - return self._enabled - - @enabled.setter - @stackable - def enabled(self, value): - """Get whether this Slider is evaluated in the solver - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self._enabled = value - for model in self.models: - model.itemDataChanged(self) - - @classmethod - def createSlider(cls, name, simplex, group=None, shape=None, tVal=1.0): - """Create a new slider with a name in a group. - Possibly create a single default shape for this slider - - Parameters - ---------- - name : str - The name of this Slider - simplex : Simplex - The parent Simplex system - group : Group or None - The group to add the Slider to. - If None, create a default group. Defaults to None - shape : Shape or None - What shape to add to the new Slider's Progression at the given tVal - if None, create a default shape. Defaults to None - tVal : float - The value for the new shape in the Slider's Progression. Defaults to 1.0 - - Returns - ------- - Slider - The newly created Slider - - """ - if simplex.restShape is None: - raise RuntimeError("Simplex system is missing rest shape") - - if group is None: - if simplex.sliderGroups: - group = simplex.sliderGroups[0] - else: - group = Group("{0}_GROUP".format(name), simplex, Slider) - - currentNames = [s.name for s in simplex.sliders] - name = getNextName(name, currentNames) - - prog = Progression(name, simplex) - if shape is None: - prog.createShape(name, tVal) - else: - prog.pairs.append(ProgPair(simplex, shape, tVal)) - - sli = cls(name, simplex, prog, group) - return sli - - @classmethod - def createMultiSlider(cls, name, simplex, shapes, tVals, group=None): - """Create a new slider with a name (possibly in a custom group) with the - provided shapes and t-values. - - Parameters - ---------- - name : str - The name of this Slider - simplex : Simplex - The parent Simplex system - shape : list of Shape - What shape s to add to the new Slider's Progression at the given tVals - tVal : list of float - The values for the new shapes in the Slider's Progression - group : Group or None - The group to add the Slider to. - If None, create a default group. Defaults to None - Returns - ------- - Slider - The newly created Slider - - """ - if simplex.restShape is None: - raise RuntimeError("Simplex system is missing rest shape") - - if group is None: - if simplex.sliderGroups: - group = simplex.sliderGroups[0] - else: - group = Group("{0}_GROUP".format(name), simplex, Slider) - - currentNames = [s.name for s in simplex.sliders] - name = getNextName(name, currentNames) - - prog = Progression(name, simplex) - for shape, tVal in zip(shapes, tVals): - prog.pairs.append(ProgPair(simplex, shape, tVal)) - - sli = cls(name, simplex, prog, group) - return sli - - @property - def name(self): - """Get the name of a Slider""" - return self._name - - @name.setter - @stackable - def name(self, value): - """Set the name of a Slider - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self._name = value - self.prog.name = value - self.DCC.renameSlider(self, value) - # TODO Also rename the combos - for model in self.models: - model.itemDataChanged(self) - - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - - Returns - ------- - - """ - return self.prog.pairs[row] - - def treeRow(self): - """ """ - return self.group.items.index(self) - - def treeParent(self): - """ """ - return self.group - - def treeChildCount(self): - """ """ - return len(self.prog.pairs) - - def treeData(self, column): - """ - - Parameters - ---------- - column : - - - Returns - ------- - - """ - if column == 0: - return self.name - if column == 1: - return self.value - return None - - def treeChecked(self): - """ """ - return self.enabled - - def nameLinks(self): - """ - - Parameters - ---------- - - Returns - ------- - : type - Name Linking is currenly in-development - - """ - # split by underscore - sp = self._name.split("_") - sliderPoss = [] - for orig in sp: - s = caseSplit(orig) - if len(s) > 1: - s = orig + s - sliderPoss.append(s) - - # remove numbered chunks from the end - shapeNames = [] - shapes = [p.shape for p in self.prog.pairs] - for s in shapes: - x = s.name.rsplit("_", 1) - if len(x) == 2: - base, sfx = x - if sfx[0].lower() == "n": - sfx = sfx[1:] - x = base if sfx.isdigit() else s.name - shapeNames.append(x) - - out = [False] * len(shapeNames) - for poss in itertools.product(*sliderPoss): - check = "".join(poss) - for i, s in enumerate(shapeNames): - if check == s: - out[i] = True - if all(out): - break - return out - - @classmethod - def buildSliderName(cls, pairs): - """Figure out then name for a slider with given shapes - - This will mostly be used to figure out what the new name - for a slider will be if its shapes are renamed - - Parameters - ---------- - pairs : [(str - A list of (name, value) pairs - - Returns - ------- - : str - A newly created name - - """ - # In this case, pairs is *not* a list of ProgPairs - # but a list of (name, value) tuples - - # First, I'm just going to ignore anything with values that aren't 1.0 - # This simplifies the logic greatly - extPairs = [p for p in pairs if abs(p[1]) == 1.0] - if len(extPairs) == 1: - return extPairs[0] - if extPairs[0] == 1: - extPairs = extPairs.reversed() - - names = [] - for ep in extPairs: - sp = ep[0].split("_") - if Shape.isNumberField(sp[-1]): - sp = sp[:-1] - names.append(sp) - - return "_".join(["".join(makeUnique(n)) for n in names]) - - @property - def thing(self): - """Get the stored reference to the DCC attribute""" - # if this is a deepcopied object, then self._thing will - # be None. Rebuild the thing connection by its representation - if self._thing is None and self._thingRepr: - self._thing = self.DCC.loadPersistentSlider(self._thingRepr) - return self._thing - - @thing.setter - def thing(self, value): - """Set the stored reference to the DCC attribute - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self._thing = value - self._thingRepr = self.DCC.getPersistentSlider(value) - - @property - def value(self): - """Get the current value for this Slider""" - return self._value - - @value.setter - def value(self, val): - """Set the current value for this Slider - - Parameters - ---------- - val : - - - Returns - ------- - - """ - self._value = val - for model in self.models: - model.itemDataChanged(self) - self._setAllSliders(self) - - @singleShot() - def _setAllSliders(self, sliders): - """ - - Parameters - ---------- - sliders : - - - Returns - ------- - - """ - with undoContext(self.DCC): - for slider in sliders: - self.DCC.setSliderWeight(slider, slider.value) - - def updateValue(self): - """ """ - pass - - @classmethod - def loadV2(cls, simplex, progs, data, create): - """Load the data from a version2 formatted json dictionary - - Parameters - ---------- - simplex : Simplex - The Simplex system that's being built - progs : [Progression - The progressions that have already been built - data : dict - The chunk of the json dict used to build this object - create : bool - Whether to create the DCC Shape, or look for it already in-scene - - Returns - ------- - : Slider - The specified Slider - - """ - name = data["name"] - prog = progs[data["prog"]] - group = simplex.groups[data.get("group", 0)] - color = QColor(*data.get("color", (128, 128, 128))) - return cls(name, simplex, prog, group, create=create, color=color) - - def buildDefinition(self, simpDict, legacy): - """Output a dictionary definition of this object - - Parameters - ---------- - simpDict : dict - The dictionary that is being built - legacy : bool - Whether to write out the legacy definition, or the newer one - - Returns - ------- - - """ - if self._buildIdx is None: - self._buildIdx = len(simpDict["sliders"]) - if legacy: - gIdx = self.group.buildDefinition(simpDict, legacy) - pIdx = self.prog.buildDefinition(simpDict, legacy) - simpDict.setdefault("sliders", []).append([self.name, pIdx, gIdx]) - else: - x = { - "name": self.name, - "prog": self.prog.buildDefinition(simpDict, legacy), - "group": self.group.buildDefinition(simpDict, legacy), - "color": self.color.getRgb()[:3], - "enabled": self._enabled, - } - simpDict.setdefault("sliders", []).append(x) - return self._buildIdx - - def clearBuildIndex(self): - """Clear the build index of this object - - The buildIndex is stored when building a definition dictionary - that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - - """ - self._buildIdx = None - self.prog.clearBuildIndex() - self.group.clearBuildIndex() - - def setRange(self): - """Set the range of this Slider based on the progPair values""" - values = [i.value for i in self.prog.pairs] - self.minValue = min(values) - self.maxValue = max(values) - self.DCC.setSliderRange(self) - - @stackable - def delete(self): - """Delete a slider, any shapes it contains, and all downstream Combos and Traversals""" - self.simplex.deleteDownstream(self) - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - g = self.group - g.items.remove(self) - self.group = None - self.simplex.sliders.remove(self) - - pairs = self.prog.pairs[:] # gotta make a copy - for pp in pairs: - if not pp.shape.isRest: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) - - self.DCC.deleteSlider(self) - - @stackable - def setInterpolation(self, interp): - """Set the interpolation of a single Slider - - Parameters - ---------- - interp : str - The interpolation for this Slider's Progression - - Returns - ------- - - """ - self.prog.interp = interp - - @stackable - def setInterps(self, sliders, interp): - """Set the interpolation of multiple Sliders - - Parameters - ---------- - sliders : [Slider - List of sliders to set interpolations on - interp : str - The interpolation to set on the list of Sliders - - Returns - ------- - - """ - # This uses an instantiated slider to set the values - # of multiple sliders. This is so we don't update the - # DCC over and over again - if not sliders: - return - with undoContext(self.DCC): - for slider in sliders: - slider.prog.interp = interp - - @stackable - def createShape(self, shapeName=None, tVal=None): - """Create a shape and add it to a progression - - Parameters - ---------- - shapeName : str or None - The new Shape name in this Slider's Progression. - If None, give it a default name. Defaults to None - tVal : float or None - The value to give the shape in this Slider's Progression. - If None, give it a "smart" default. Defaults to None - - Returns - ------- - : ProgPair - The newly created Shape in a ProgPair already added to the Progression - - """ - pp, idx = self.prog.newProgPair(shapeName, tVal) - mgrs = [model.insertItemManager(self, idx) for model in self.models] - with nested(*mgrs): - pp.prog = self.prog - self.prog.pairs.insert(idx, pp) - self.updateRange() - return pp - - def extractProgressive(self, live=True, offset=10.0, separation=5.0): - """Extract all of the Shapes in this Slider's Progression - - Parameters - ---------- - live : bool - Whether to maintain a live connection in the DCC for the extracted meshes - Defaults to True - offset : float - The offset value for the first extracted mesh in the DCC (Default value = 10.0) - separation : float - The offset to add between any two extracted meshes (Default value = 5.0) - - Returns - ------- - - """ - with undoContext(self.DCC): - pos, neg = [], [] - for pp in sorted(self.prog.pairs): - if pp.value < 0.0: - neg.append((pp.value, pp.shape, offset)) - offset += separation - elif pp.value > 0.0: - pos.append((pp.value, pp.shape, offset)) - offset += separation - # skip the rest value at == 0.0 - neg = list(reversed(neg)) - - for prog in [pos, neg]: - if not prog: - continue - xtVal, shape, shift = prog[-1] - ext, deltaShape = self.DCC.extractWithDeltaShape(shape, live, shift) - for value, shape, shift in prog[:-1]: - self.DCC.extractWithDeltaConnection( - shape, deltaShape, value / xtVal, live, shift - ) - - def extractShape(self, shape, live=True, offset=10.0): - """Extract a Shape that is controlled by a Slider to a DCC mesh - - This is on Slider (vs being on Shape) because live connections are handled - differently based on the different Progression controllers - - Parameters - ---------- - shape : Shape - The shape to extract - offset : float - The offset value for the first extracted mesh in the DCC (Default value = 10.0) - separation : float - The offset to add between any two extracted meshes - live : - (Default value = True) - - Returns - ------- - - """ - return self.DCC.extractShape(shape, live, offset) - - def connectShape(self, shape, mesh=None, live=False, delete=False): - """Connect a Shape that is controlled by a Slider to a DCC mesh - - This is on Slider (vs being on Shape) because live connections are handled - differently based on the different Progression controllers - - Parameters - ---------- - shape : Shape - The shape to connect - mesh : object or None - The DCC mesh to connect to the shape. - If None, search the DCC by name. Defaults to None - live : bool - Whether to maintain a live connection in the DCC for the extracted meshes - Defaults to True - delete : bool - Whether to delete the DCC mesh after it was connected (Default value = False) - - Returns - ------- - - """ - self.DCC.connectShape(shape, mesh, live, delete) - - def updateRange(self): - """Update the range in the DCC for this Slider""" - self.DCC.updateSlidersRange([self]) - - @stackable - def setGroup(self, grp): - """Set the Group for this Slider - - Parameters - ---------- - grp : Group - The Group to put this Slider under - - Returns - ------- - - """ - if grp.groupType is None: - grp.groupType = type(self) - - if not isinstance(self, grp.groupType): - raise ValueError( - "All items in this group must be of type: {}".format(grp.groupType) - ) - - mgrs = [model.moveItemManager(self, grp) for model in self.models] - with nested(*mgrs): - if self.group: - self.group.items.remove(self) - grp.items.append(self) - self.group = grp - - def getInputVectors(self): - """Get the ordered values for input to the solver that activate this Slider - Multiple outputs are possible if the slider has -1 to 1 range - - Parameters - ---------- - - Returns - ------- - : [[float, ....], ....] - The ordered slider values - - """ - inVecs = [] - for pp in self.prog.getExtremePairs(): - inVec = [0.0] * len(self.simplex.sliders) - inVec[self.simplex.sliders.index(self)] = pp.value - inVecs.append(inVec) - return inVecs +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint:disable=missing-docstring,unused-argument,no-self-use +import itertools + +from ..interface import undoContext +from Qt.QtGui import QColor +from ..utils import caseSplit, getNextName, makeUnique, nested, singleShot +from .accessor import SimplexTickAccessor +from .group import Group +from .progression import ProgPair, Progression +from .shape import Shape +from .stack import stackable + + +class Slider(SimplexTickAccessor): + """A user-input to the simplex system that directly controls a Progression + + Parameters + ---------- + name : str + The name of this Slider + simplex : Simplex + The parent Simplex system + prog : Progression + The Progression that this Slider controls + group : Group + The Group to create this Slider in + color : QColor + The color of this item in the UI + create : bool + Whether to create a DCC Shape, or look for it already in-scene + + Returns + ------- + + """ + + classDepth = 7 + + def __init__(self, name, simplex, prog, group, color=None, create=True): + if group.groupType is not type(self): + raise ValueError("Cannot add this slider to a combo group") + + super(Slider, self).__init__(simplex) + color = QColor(128, 128, 128) if color is None else color + with self.stack.store(self): + self._name = name + self._thing = None + self._thingRepr = None + self.prog = prog + self.split = False + self.prog.controller = self + self._buildIdx = None + self._value = 0.0 + self.expanded = {} + self.color = color + self._enabled = True + + mn, mx = self.prog.getRange() + self.minValue = mn + self.maxValue = mx + + mgrs = [model.insertItemManager(group) for model in self.models] + with nested(*mgrs): + self.group = group + self.group.items.append(self) + + self.simplex.sliders.append(self) + + newThing = self.DCC.getSliderThing(self._name) + if newThing is None: + if create: + self.thing = simplex.DCC.createSlider(self) + else: + raise RuntimeError( + "Unable to find existing shape: {0}".format(self.name) + ) + else: + self.thing = newThing + + @property + def enabled(self): + """Get whether this Slider is evaluated in the solver""" + return self._enabled + + @enabled.setter + @stackable + def enabled(self, value): + """Get whether this Slider is evaluated in the solver + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self._enabled = value + for model in self.models: + model.itemDataChanged(self) + + @classmethod + def createSlider(cls, name, simplex, group=None, shape=None, tVal=1.0): + """Create a new slider with a name in a group. + Possibly create a single default shape for this slider + + Parameters + ---------- + name : str + The name of this Slider + simplex : Simplex + The parent Simplex system + group : Group or None + The group to add the Slider to. + If None, create a default group. Defaults to None + shape : Shape or None + What shape to add to the new Slider's Progression at the given tVal + if None, create a default shape. Defaults to None + tVal : float + The value for the new shape in the Slider's Progression. Defaults to 1.0 + + Returns + ------- + Slider + The newly created Slider + + """ + if simplex.restShape is None: + raise RuntimeError("Simplex system is missing rest shape") + + if group is None: + if simplex.sliderGroups: + group = simplex.sliderGroups[0] + else: + group = Group("{0}_GROUP".format(name), simplex, Slider) + + currentNames = [s.name for s in simplex.sliders] + name = getNextName(name, currentNames) + + prog = Progression(name, simplex) + if shape is None: + prog.createShape(name, tVal) + else: + prog.pairs.append(ProgPair(simplex, shape, tVal)) + + sli = cls(name, simplex, prog, group) + return sli + + @classmethod + def createMultiSlider(cls, name, simplex, shapes, tVals, group=None): + """Create a new slider with a name (possibly in a custom group) with the + provided shapes and t-values. + + Parameters + ---------- + name : str + The name of this Slider + simplex : Simplex + The parent Simplex system + shape : list of Shape + What shape s to add to the new Slider's Progression at the given tVals + tVal : list of float + The values for the new shapes in the Slider's Progression + group : Group or None + The group to add the Slider to. + If None, create a default group. Defaults to None + Returns + ------- + Slider + The newly created Slider + + """ + if simplex.restShape is None: + raise RuntimeError("Simplex system is missing rest shape") + + if group is None: + if simplex.sliderGroups: + group = simplex.sliderGroups[0] + else: + group = Group("{0}_GROUP".format(name), simplex, Slider) + + currentNames = [s.name for s in simplex.sliders] + name = getNextName(name, currentNames) + + prog = Progression(name, simplex) + for shape, tVal in zip(shapes, tVals): + prog.pairs.append(ProgPair(simplex, shape, tVal)) + + sli = cls(name, simplex, prog, group) + return sli + + @property + def name(self): + """Get the name of a Slider""" + return self._name + + @name.setter + @stackable + def name(self, value): + """Set the name of a Slider + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self._name = value + self.prog.name = value + self.DCC.renameSlider(self, value) + # TODO Also rename the combos + for model in self.models: + model.itemDataChanged(self) + + def treeChild(self, row): + """ + + Parameters + ---------- + row : + + + Returns + ------- + + """ + return self.prog.pairs[row] + + def treeRow(self): + """ """ + return self.group.items.index(self) + + def treeParent(self): + """ """ + return self.group + + def treeChildCount(self): + """ """ + return len(self.prog.pairs) + + def treeData(self, column): + """ + + Parameters + ---------- + column : + + + Returns + ------- + + """ + if column == 0: + return self.name + if column == 1: + return self.value + return None + + def treeChecked(self): + """ """ + return self.enabled + + def nameLinks(self): + """ + + Parameters + ---------- + + Returns + ------- + : type + Name Linking is currenly in-development + + """ + # split by underscore + sp = self._name.split("_") + sliderPoss = [] + for orig in sp: + s = caseSplit(orig) + if len(s) > 1: + s = orig + s + sliderPoss.append(s) + + # remove numbered chunks from the end + shapeNames = [] + shapes = [p.shape for p in self.prog.pairs] + for s in shapes: + x = s.name.rsplit("_", 1) + if len(x) == 2: + base, sfx = x + if sfx[0].lower() == "n": + sfx = sfx[1:] + x = base if sfx.isdigit() else s.name + shapeNames.append(x) + + out = [False] * len(shapeNames) + for poss in itertools.product(*sliderPoss): + check = "".join(poss) + for i, s in enumerate(shapeNames): + if check == s: + out[i] = True + if all(out): + break + return out + + @classmethod + def buildSliderName(cls, pairs): + """Figure out then name for a slider with given shapes + + This will mostly be used to figure out what the new name + for a slider will be if its shapes are renamed + + Parameters + ---------- + pairs : [(str + A list of (name, value) pairs + + Returns + ------- + : str + A newly created name + + """ + # In this case, pairs is *not* a list of ProgPairs + # but a list of (name, value) tuples + + # First, I'm just going to ignore anything with values that aren't 1.0 + # This simplifies the logic greatly + extPairs = [p for p in pairs if abs(p[1]) == 1.0] + if len(extPairs) == 1: + return extPairs[0] + if extPairs[0] == 1: + extPairs = extPairs.reversed() + + names = [] + for ep in extPairs: + sp = ep[0].split("_") + if Shape.isNumberField(sp[-1]): + sp = sp[:-1] + names.append(sp) + + return "_".join(["".join(makeUnique(n)) for n in names]) + + @property + def thing(self): + """Get the stored reference to the DCC attribute""" + # if this is a deepcopied object, then self._thing will + # be None. Rebuild the thing connection by its representation + if self._thing is None and self._thingRepr: + self._thing = self.DCC.loadPersistentSlider(self._thingRepr) + return self._thing + + @thing.setter + def thing(self, value): + """Set the stored reference to the DCC attribute + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self._thing = value + self._thingRepr = self.DCC.getPersistentSlider(value) + + @property + def value(self): + """Get the current value for this Slider""" + return self._value + + @value.setter + def value(self, val): + """Set the current value for this Slider + + Parameters + ---------- + val : + + + Returns + ------- + + """ + self._value = val + for model in self.models: + model.itemDataChanged(self) + self._setAllSliders(self) + + @singleShot() + def _setAllSliders(self, sliders): + """ + + Parameters + ---------- + sliders : + + + Returns + ------- + + """ + with undoContext(self.DCC): + for slider in sliders: + self.DCC.setSliderWeight(slider, slider.value) + + def updateValue(self): + """ """ + pass + + @classmethod + def loadV2(cls, simplex, progs, data, create): + """Load the data from a version2 formatted json dictionary + + Parameters + ---------- + simplex : Simplex + The Simplex system that's being built + progs : [Progression + The progressions that have already been built + data : dict + The chunk of the json dict used to build this object + create : bool + Whether to create the DCC Shape, or look for it already in-scene + + Returns + ------- + : Slider + The specified Slider + + """ + name = data["name"] + prog = progs[data["prog"]] + group = simplex.groups[data.get("group", 0)] + color = QColor(*data.get("color", (128, 128, 128))) + return cls(name, simplex, prog, group, create=create, color=color) + + def buildDefinition(self, simpDict, legacy): + """Output a dictionary definition of this object + + Parameters + ---------- + simpDict : dict + The dictionary that is being built + legacy : bool + Whether to write out the legacy definition, or the newer one + + Returns + ------- + + """ + if self._buildIdx is None: + self._buildIdx = len(simpDict["sliders"]) + if legacy: + gIdx = self.group.buildDefinition(simpDict, legacy) + pIdx = self.prog.buildDefinition(simpDict, legacy) + simpDict.setdefault("sliders", []).append([self.name, pIdx, gIdx]) + else: + x = { + "name": self.name, + "prog": self.prog.buildDefinition(simpDict, legacy), + "group": self.group.buildDefinition(simpDict, legacy), + "color": self.color.getRgb()[:3], + "enabled": self._enabled, + } + simpDict.setdefault("sliders", []).append(x) + return self._buildIdx + + def clearBuildIndex(self): + """Clear the build index of this object + + The buildIndex is stored when building a definition dictionary + that keeps track of its index for later referencing + + Parameters + ---------- + + Returns + ------- + + """ + self._buildIdx = None + self.prog.clearBuildIndex() + self.group.clearBuildIndex() + + def setRange(self): + """Set the range of this Slider based on the progPair values""" + values = [i.value for i in self.prog.pairs] + self.minValue = min(values) + self.maxValue = max(values) + self.DCC.setSliderRange(self) + + @stackable + def delete(self): + """Delete a slider, any shapes it contains, and all downstream Combos and Traversals""" + self.simplex.deleteDownstream(self) + mgrs = [model.removeItemManager(self) for model in self.models] + with nested(*mgrs): + g = self.group + g.items.remove(self) + self.group = None + self.simplex.sliders.remove(self) + + pairs = self.prog.pairs[:] # gotta make a copy + for pp in pairs: + if not pp.shape.isRest: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) + + self.DCC.deleteSlider(self) + + @stackable + def setInterpolation(self, interp): + """Set the interpolation of a single Slider + + Parameters + ---------- + interp : str + The interpolation for this Slider's Progression + + Returns + ------- + + """ + self.prog.interp = interp + + @stackable + def setInterps(self, sliders, interp): + """Set the interpolation of multiple Sliders + + Parameters + ---------- + sliders : [Slider + List of sliders to set interpolations on + interp : str + The interpolation to set on the list of Sliders + + Returns + ------- + + """ + # This uses an instantiated slider to set the values + # of multiple sliders. This is so we don't update the + # DCC over and over again + if not sliders: + return + with undoContext(self.DCC): + for slider in sliders: + slider.prog.interp = interp + + @stackable + def createShape(self, shapeName=None, tVal=None): + """Create a shape and add it to a progression + + Parameters + ---------- + shapeName : str or None + The new Shape name in this Slider's Progression. + If None, give it a default name. Defaults to None + tVal : float or None + The value to give the shape in this Slider's Progression. + If None, give it a "smart" default. Defaults to None + + Returns + ------- + : ProgPair + The newly created Shape in a ProgPair already added to the Progression + + """ + pp, idx = self.prog.newProgPair(shapeName, tVal) + mgrs = [model.insertItemManager(self, idx) for model in self.models] + with nested(*mgrs): + pp.prog = self.prog + self.prog.pairs.insert(idx, pp) + self.updateRange() + return pp + + def extractProgressive(self, live=True, offset=10.0, separation=5.0): + """Extract all of the Shapes in this Slider's Progression + + Parameters + ---------- + live : bool + Whether to maintain a live connection in the DCC for the extracted meshes + Defaults to True + offset : float + The offset value for the first extracted mesh in the DCC (Default value = 10.0) + separation : float + The offset to add between any two extracted meshes (Default value = 5.0) + + Returns + ------- + + """ + with undoContext(self.DCC): + pos, neg = [], [] + for pp in sorted(self.prog.pairs): + if pp.value < 0.0: + neg.append((pp.value, pp.shape, offset)) + offset += separation + elif pp.value > 0.0: + pos.append((pp.value, pp.shape, offset)) + offset += separation + # skip the rest value at == 0.0 + neg = list(reversed(neg)) + + for prog in [pos, neg]: + if not prog: + continue + xtVal, shape, shift = prog[-1] + ext, deltaShape = self.DCC.extractWithDeltaShape(shape, live, shift) + for value, shape, shift in prog[:-1]: + self.DCC.extractWithDeltaConnection( + shape, deltaShape, value / xtVal, live, shift + ) + + def extractShape(self, shape, live=True, offset=10.0): + """Extract a Shape that is controlled by a Slider to a DCC mesh + + This is on Slider (vs being on Shape) because live connections are handled + differently based on the different Progression controllers + + Parameters + ---------- + shape : Shape + The shape to extract + offset : float + The offset value for the first extracted mesh in the DCC (Default value = 10.0) + separation : float + The offset to add between any two extracted meshes + live : + (Default value = True) + + Returns + ------- + + """ + return self.DCC.extractShape(shape, live, offset) + + def connectShape(self, shape, mesh=None, live=False, delete=False): + """Connect a Shape that is controlled by a Slider to a DCC mesh + + This is on Slider (vs being on Shape) because live connections are handled + differently based on the different Progression controllers + + Parameters + ---------- + shape : Shape + The shape to connect + mesh : object or None + The DCC mesh to connect to the shape. + If None, search the DCC by name. Defaults to None + live : bool + Whether to maintain a live connection in the DCC for the extracted meshes + Defaults to True + delete : bool + Whether to delete the DCC mesh after it was connected (Default value = False) + + Returns + ------- + + """ + self.DCC.connectShape(shape, mesh, live, delete) + + def updateRange(self): + """Update the range in the DCC for this Slider""" + self.DCC.updateSlidersRange([self]) + + @stackable + def setGroup(self, grp): + """Set the Group for this Slider + + Parameters + ---------- + grp : Group + The Group to put this Slider under + + Returns + ------- + + """ + if grp.groupType is None: + grp.groupType = type(self) + + if not isinstance(self, grp.groupType): + raise ValueError( + "All items in this group must be of type: {}".format(grp.groupType) + ) + + mgrs = [model.moveItemManager(self, grp) for model in self.models] + with nested(*mgrs): + if self.group: + self.group.items.remove(self) + grp.items.append(self) + self.group = grp + + def getInputVectors(self): + """Get the ordered values for input to the solver that activate this Slider + Multiple outputs are possible if the slider has -1 to 1 range + + Parameters + ---------- + + Returns + ------- + : [[float, ....], ....] + The ordered slider values + + """ + inVecs = [] + for pp in self.prog.getExtremePairs(): + inVec = [0.0] * len(self.simplex.sliders) + inVec[self.simplex.sliders.index(self)] = pp.value + inVecs.append(inVec) + return inVecs diff --git a/src/python/simplexui/items/stack.py b/src/python/simplexui/items/stack.py index 75d6d25c..f2b3e66b 100644 --- a/src/python/simplexui/items/stack.py +++ b/src/python/simplexui/items/stack.py @@ -1,154 +1,154 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use -import copy -from collections import OrderedDict -from contextlib import contextmanager -from functools import wraps - -from ..interface import undoContext - - -# UNDO STACK SETUP -class Stack(object): - """Integrate simplex into the DCC undo stack""" - - def __init__(self): - self._stack = OrderedDict() - self.depth = 0 - self.currentRevision = 0 - self.enabled = True - - def __setitem__(self, key, value): - gt = [] - # when setting a new key, remove all keys from - # the previous branch - for k in reversed(self._stack): # pylint: disable=bad-reversed-sequence - if k > key: - gt.append(k) - else: - # yay ordered dict - break - for k in gt: - del self._stack[k] - # traceback.print_stack() - self._stack[key] = value - - def getRevision(self, revision): - """Every time a change is made to the simplex definition, - the revision counter is updated, and the revision/definition - pair is put on the undo stack - - Parameters - ---------- - revision : int - The revision number to get - - Returns - ------- - : Simplex or None - The stored Simplex system for the given revision - or None if nothing found - - """ - # This method will ***ONLY*** be called by the undo callback - # Seriously, don't call this yourself - if revision != self.currentRevision: - if revision in self._stack: - data = self._stack[revision] - self.currentRevision = revision - return data - return None - - def purge(self): - """Clear the undo stack. This should be done on new-file""" - self._stack = OrderedDict() - self.depth = 0 - self.currentRevision = 0 - - @contextmanager - def store(self, wrapObj): - """A context manager That will store changes to a Simplex system - Nested calls to this manager will only store the first one - - Parameters - ---------- - wrapObj : object - A system object that has a reference to the Simplex - - Returns - ------- - - """ - from .simplex import Simplex - - if self.enabled: - with undoContext(wrapObj.DCC): - self.depth += 1 - try: - yield - finally: - self.depth -= 1 - - if self.depth == 0: - # Only store the top Level of the stack - srevision = wrapObj.DCC.incrementRevision() - if not isinstance(wrapObj, Simplex): - wrapObj = wrapObj.simplex - self[srevision] = copy.deepcopy(wrapObj) - else: - yield - - -def stackable(method): - """A Decorator to make a method auto update the stack - This decorator can only be used on methods of an object - that has its .simplex value set with a stack. If you need - to wrap an init method, use the stack.store contextmanager - - Parameters - ---------- - method : - - - Returns - ------- - - """ - - @wraps(method) - def stacked(self, *data, **kwdata): - """Decorator closure that handles the stack - - Parameters - ---------- - *data : - - **kwdata : - - - Returns - ------- - - """ - ret = None - with self.stack.store(self): - ret = method(self, *data, **kwdata) - return ret - - return stacked +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint:disable=missing-docstring,unused-argument,no-self-use +import copy +from collections import OrderedDict +from contextlib import contextmanager +from functools import wraps + +from ..interface import undoContext + + +# UNDO STACK SETUP +class Stack(object): + """Integrate simplex into the DCC undo stack""" + + def __init__(self): + self._stack = OrderedDict() + self.depth = 0 + self.currentRevision = 0 + self.enabled = True + + def __setitem__(self, key, value): + gt = [] + # when setting a new key, remove all keys from + # the previous branch + for k in reversed(self._stack): # pylint: disable=bad-reversed-sequence + if k > key: + gt.append(k) + else: + # yay ordered dict + break + for k in gt: + del self._stack[k] + # traceback.print_stack() + self._stack[key] = value + + def getRevision(self, revision): + """Every time a change is made to the simplex definition, + the revision counter is updated, and the revision/definition + pair is put on the undo stack + + Parameters + ---------- + revision : int + The revision number to get + + Returns + ------- + : Simplex or None + The stored Simplex system for the given revision + or None if nothing found + + """ + # This method will ***ONLY*** be called by the undo callback + # Seriously, don't call this yourself + if revision != self.currentRevision: + if revision in self._stack: + data = self._stack[revision] + self.currentRevision = revision + return data + return None + + def purge(self): + """Clear the undo stack. This should be done on new-file""" + self._stack = OrderedDict() + self.depth = 0 + self.currentRevision = 0 + + @contextmanager + def store(self, wrapObj): + """A context manager That will store changes to a Simplex system + Nested calls to this manager will only store the first one + + Parameters + ---------- + wrapObj : object + A system object that has a reference to the Simplex + + Returns + ------- + + """ + from .simplex import Simplex + + if self.enabled: + with undoContext(wrapObj.DCC): + self.depth += 1 + try: + yield + finally: + self.depth -= 1 + + if self.depth == 0: + # Only store the top Level of the stack + srevision = wrapObj.DCC.incrementRevision() + if not isinstance(wrapObj, Simplex): + wrapObj = wrapObj.simplex + self[srevision] = copy.deepcopy(wrapObj) + else: + yield + + +def stackable(method): + """A Decorator to make a method auto update the stack + This decorator can only be used on methods of an object + that has its .simplex value set with a stack. If you need + to wrap an init method, use the stack.store contextmanager + + Parameters + ---------- + method : + + + Returns + ------- + + """ + + @wraps(method) + def stacked(self, *data, **kwdata): + """Decorator closure that handles the stack + + Parameters + ---------- + *data : + + **kwdata : + + + Returns + ------- + + """ + ret = None + with self.stack.store(self): + ret = method(self, *data, **kwdata) + return ret + + return stacked diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index adf6cc28..e128e38b 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -1,971 +1,971 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use -from Qt.QtGui import QColor -from ..utils import nested -from .accessor import SimplexAccessor, SimplexTickAccessor -from .combo import Combo -from .group import Group -from .progression import Progression -from .slider import Slider -from .stack import stackable - - -class TravPair(SimplexTickAccessor): - """ """ - - classDepth = 4 - - def __init__(self, slider, value): - simplex = slider.simplex - super(TravPair, self).__init__(simplex) - self.slider = slider - self._value = float(value) - self.minValue = -1.0 - self.maxValue = 1.0 - self._tickDelta = 0.0 - self.travPoint = None - self.expanded = {} - - def valueTick(self, ticks, mul): - """ """ - self._tickDelta += self.dragStep * ticks * mul - if (self._tickDelta + self.value) <= self.slider.minValue: - self._tickDelta = self.slider.minValue - self.value - if self.value != self.slider.minValue: - self.value = self.slider.minValue - self._tickDelta = 0.0 - elif (self._tickDelta + self.value) >= self.slider.maxValue: - self._tickDelta = self.slider.maxValue - self.value - if self.value != self.slider.maxValue: - self._tickDelta = 0.0 - self.value = self.slider.maxValue - elif abs(self._tickDelta + self.value) <= 1.0e-5: - if self.value != 0.0: - self._tickDelta = 0.0 - self.value = 0.0 - - @property - def models(self): - """ """ - return self.simplex.models - - @property - def name(self): - """ """ - return self.slider.name - - @property - def value(self): - """ """ - return self._value - - @value.setter - @stackable - def value(self, val): - """ """ - self._value = val - for model in self.models: - model.itemDataChanged(self) - - def buildDefinition(self, simpDict, legacy): - """ """ - sIdx = self.slider.buildDefinition(simpDict, legacy) - return sIdx, self.value - - def treeRow(self): - """ """ - return self.travPoint.pairs.index(self) - - def treeParent(self): - """ """ - return self.travPoint - - def treeData(self, column): - """ """ - if column == 0: - return self.name - if column == 1: - return self.value - return None - - @stackable - def remove(self): - """ """ - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - self.travPoint.pairs.remove(self) - self.travPoint = None - - @stackable - def delete(self): - """ """ - self.travPoint.traversal.removePairs([self]) - - @staticmethod - def removeAll(pairs): - """ """ - travs = list({p.travPoint.traversal for p in pairs}) - for trav in travs: - trav.removePairs(pairs) - - -class TravPoint(SimplexAccessor): - """ """ - - classDepth = 3 - - def __init__(self, pairs, row): - if not pairs: - raise ValueError("Pairs must be provided for a TravPoint") - simplex = pairs[0].slider.simplex - super(TravPoint, self).__init__(simplex) - - self.pairs = pairs - for pair in pairs: - pair.travPoint = self - self.row = row - self.traversal = None - self.expanded = {} - - def sliders(self): - """ """ - return [i.slider for i in self.pairs] - - @staticmethod - def _wideCeiling(val, eps=0.001): - """ """ - if val > eps: - return 1.0 - elif val < -eps: - return -1.0 - return 0.0 - - @stackable - def addPair(self, pair): - """ - - Parameters - ---------- - pair : - - - Returns - ------- - - """ - mgrs = [model.insertItemManager(self) for model in self.models] - with nested(*mgrs): - self.pairs.append(pair) - pair.travPoint = self - - def removePair(self, pair): - """ - - Parameters - ---------- - pair : - - - Returns - ------- - - """ - pair.remove() - - def addSlider(self, slider, val=None): - """ - - Parameters - ---------- - slider : - - val : - (Default value = None) - - Returns - ------- - - """ - val = val if val is not None else slider.value - val = self._wideCeiling(val) - sliders = self.sliders() - try: - idx = sliders.index(slider) - except ValueError: - self.addPair(TravPair(slider, val)) - else: - self.pairs[idx].value = val - - def addItem(self, item): - """ - - Parameters - ---------- - item : - - - Returns - ------- - - """ - if isinstance(item, Slider): - self.addSlider(item) - elif isinstance(item, Combo): - for cp in item.pairs: - self.addSlider(cp.slider, cp.value) - - @property - def name(self): - """ """ - return "START" if self.row == 0 else "END" - - @name.setter - def name(self, val): - # Do nothing. This is just here to make the linter happy - return - - def treeData(self, column): - """ - - Parameters - ---------- - column : - - - Returns - ------- - - """ - if column == 0: - return self.name - return None - - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - - Returns - ------- - - """ - return self.pairs[row] - - def treeRow(self): - """ """ - return self.row - - def treeParent(self): - """ """ - return self.traversal - - def treeChildCount(self): - """ """ - return len(self.pairs) - - def buildDefinition(self, simpDict, legacy): - """ - - Parameters - ---------- - simpDict : - - legacy : - - - Returns - ------- - - """ - return [p.buildDefinition(simpDict, legacy) for p in self.pairs] - - def getInputVector(self): - """get the input to the solver that would fully activate this point of the traversal - - parameters - ---------- - - returns - ------- - : [float, ...] - the ordered slider values - - """ - invec = [0.0] * len(self.simplex.sliders) - for cp in self.pairs: - invec[self.simplex.sliders.index(cp.slider)] = cp.value - return invec - - -class Traversal(SimplexAccessor): - """Traversals control a Progression based on any 2 points in the Solver space. - - Traversals only make sense with intermediate shapes in the progression of the sliders - that control it. - - Traversals should never have a shape at 100%. That shape should be handled by a Combo - - First: A "point in solver space" just means a list of slider/value pairs. - The Slider/Value pairs that make a up a Combo are just a "Point in solver space" as well. - So technically Combos could be thought of as a special-case of Traversals. Combos control - a progression between the "Rest Point" where all sliders are at 0, and the Combo point - - Outside of the context of Traversals, I just call solver space points "Combos", because - I don't need to be crazy specific like I do here. - - The initial use-case for Traversals was dealing with eye combo shapes with incremental - Progressions. The eyeLookDown and the eyeClosed shapes both pull the upper lid down a great - deal, and the eyeClosed was a 4-shape progression. So, when transitioning from eyeLookDown - to eyeLookDown+eyeClosed, the deltas for all the progressive shapes were being triggered as - the combo was coming on, causing major wobbles in the eyelid. So we needed shapes that - countered those incrementals, but *only* on the transition from eyeLookDown to - eyeLookDown+eyeClosed (NOT on the transition from eyeClosed to eyeLookDown+eyeClosed) - - Early setups used floating Combos, but those have linearinterpolation, and I wanted a - cleaner solution. That solution is the Traversal - - Parameters - ---------- - name : str - The name of this Combo - simplex : Simplex - The parent Simplex system - startPoint : TravPoint - A set of Slider/Value pairs where the Traversal solves to 0 - endPoint : TravPoint - A set of Slider/Value pairs where the Traversal solves to 1 - prog : Progression - The Progression that this Combo controls - group : Group - The Group to create this combo in - color : QColor - The color of this item in the UI - - Returns - ------- - - """ - - classDepth = 2 - - def __init__( - self, - name, - simplex, - startPoint, - endPoint, - prog, - group, - color=None, - ): - super(Traversal, self).__init__(simplex) - color = QColor(128, 128, 128) if color is None else color - with self.stack.store(self): - if group.groupType is not type(self): - raise ValueError( - "Cannot add this Traversal to a group of a different type" - ) - self._name = name - self.startPoint = startPoint - self.endPoint = endPoint - self.prog = prog - self._buildIdx = None - self.expanded = {} - self._enabled = True - self.color = color - - mgrs = [model.insertItemManager(group) for model in self.models] - with nested(*mgrs): - self.group = group - self.startPoint.traversal = self - self.endPoint.traversal = self - self.prog.controller = self - self.group.items.append(self) - self.simplex.traversals.append(self) - - @classmethod - def createTraversal(cls, name, simplex, startPairs, endPairs, group=None, count=4): - """Create a Traversal between two lists of pairs - - Parameters - ---------- - name : str - The name of this Combo - simplex : Simplex - The parent Simplex system - startPairs : [(Slider - A list of Slider/Value pairs to make the startPoint - endPairs : [(Slider - A list of Slider/Value pairs to make the endPoint - group : Group - The Group to create this combo in (Default value = None) - count : int - The number of incrementals to create (including the 100%) (Default value = 4) - - Returns - ------- - - """ - if simplex.restShape is None: - raise RuntimeError("Simplex system is missing rest shape") - - if group is None: - gname = "TRAVERSALS" - matches = [i for i in simplex.traversalGroups if i.name == gname] - if matches: - group = matches[0] - else: - group = Group(gname, simplex, Traversal) - - startPairs = [TravPair(p[0], p[1]) for p in startPairs] - endPairs = [TravPair(p[0], p[1]) for p in endPairs] - - startPoint = TravPoint(startPairs, 0) - endPoint = TravPoint(endPairs, 1) - - prog = Progression(name, simplex) - trav = cls(name, simplex, startPoint, endPoint, prog, group) - - for c in reversed(list(range(count))): - val = (100 * (c + 1)) // count - pp = prog.createShape("{0}_{1}".format(name, val), val / 100.0) - simplex.DCC.zeroShape(pp.shape) - return trav - - @property - def enabled(self): - """Get whether this Traversal is evaluated in the solver""" - return self._enabled - - @enabled.setter - @stackable - def enabled(self, value): - """Set whether this Traversal is evaluated in the solver - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self._enabled = value - for model in self.models: - model.itemDataChanged(self) - - @property - def name(self): - """Get the name of a Traversal""" - return self._name - - @name.setter - @stackable - def name(self, value): - """Set the name of a Traversal - - Parameters - ---------- - value : - - - Returns - ------- - - """ - self._name = value - self.prog.name = value - # self.DCC.renameTraversal(self, value) - # for model in self.models: - # model.itemDataChanged(self) - - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - - Returns - ------- - - """ - if row == 0: - return self.startPoint - elif row == 1: - return self.endPoint - elif row == 2: - return self.prog - return None - - def treeRow(self): - """ """ - return self.group.items.index(self) - - def treeParent(self): - """ """ - return self.group - - def treeChildCount(self): - """ """ - return 3 - - def treeChecked(self): - """ """ - return self.enabled - - def allSliders(self): - """Get the list of all Sliders that control this Traversal - - Parameters - ---------- - - Returns - ------- - : [Slider, ...] - The list of all Sliders that control this Traversal - - """ - startSliders = [p.slider for p in self.startPoint.pairs] - endSliders = [ - p.slider for p in self.endPoint.pairs if p.slider not in startSliders - ] - return startSliders + endSliders - - def dynamicSliders(self): - """Get a list of sliders that have different values at the start and end""" - return [sli for sli, rng in self.ranges() if rng[0] != rng[1]] - - def staticSliders(self): - """Get a list of sliders that have the same values at the start and end""" - return [sli for sli, rng in self.ranges() if rng[0] == rng[1]] - - def ranges(self): - """Get the range per Slider for this Traversal - - Parameters - ---------- - - Returns - ------- - : type - (dict): A {Slider: range} dict - - """ - startDict = {p.slider: p.value for p in self.startPoint.pairs} - endDict = {p.slider: p.value for p in self.endPoint.pairs} - allSliders = startDict.keys() | endDict.keys() - - rangeDict = {} - for sli in allSliders: - rangeDict[sli] = (startDict.get(sli, 0.0), endDict.get(sli, 0.0)) - return rangeDict - - @staticmethod - def buildTraversalName(ranges): - """Given the range dict (like from Traversal.ranges()) come up with a name - - Parameters - ---------- - ranges : dict - A {Slider: range} dict - - Returns - ------- - : str - The suggested Traversal name - - """ - static, dynamic = [], [] - for sli, rng in ranges.items(): - if rng[0] == rng[1]: - static.append(sli) - else: - dynamic.append(sli) - - parts = [] - for grp in static, dynamic: - for slider in sorted(grp, key=lambda x: x.name): - prefix = None - start, end = ranges[slider] - if start == end: - # prefix = 'St' # St for Static - if start == 0: - continue - shp = slider.prog.getShapeAtValue(start) - if shp is None: - continue - name = shp.strippedName() - else: - prefix = "Dy" # Dy for Dynamic - if start == 0: - shp = slider.prog.getShapeAtValue(end) - if shp is None: - continue - name = shp.strippedName() - elif end == 0: - shp = slider.prog.getShapeAtValue(start) - if shp is None: - continue - name = shp.strippedName() - else: - name = slider.name - - if prefix is not None: - parts.append(prefix) - parts.append(name) - - return "Tv_" + "_".join(parts) - - def controllerNameLinks(self): - """ """ - surr = "_{0}_".format(self.name) - return ["_{0}_".format(sli) in surr for sli in self.allSliders()] - - def nameLinks(self): - """ - - Parameters - ---------- - - Returns - ------- - : type - progression depends on this traversal's name - - """ - # In this case, these names will *NOT* have the possibility of - # a pos/neg name. Only the traversal name, and possibly a percentage - shapeNames = [] - shapes = [i.shape for i in self.prog.pairs] - for s in shapes: - x = s.name.rsplit("_", 1) - if len(x) == 2: - base, sfx = x - x = base if sfx.isdigit() else s.name - shapeNames.append(x) - return [i == self.name for i in shapeNames] - - @stackable - def createShape(self, shapeName=None, tVal=None): - """Create a shape and add it to a progression - - Parameters - ---------- - shapeName : str or None - The name of the shape to create. - If None, give it a default name - tVal : float or None - The progression value to set for the new Shape. - If None, it gets a "smart" default value - - Returns - ------- - - """ - pp, idx = self.prog.newProgPair(shapeName, tVal) - mgrs = [model.insertItemManager(self.prog, idx) for model in self.models] - with nested(*mgrs): - pp.prog = self.prog - self.prog.pairs.insert(idx, pp) - return pp - - @classmethod - def loadV2(cls, simplex, progs, data): - """Load the data from a version2 formatted json dictionary - - Parameters - ---------- - simplex : Simplex - The Simplex system that's being built - progs : [Progression - The progressions that have already been built - data : dict - The chunk of the json dict used to build this object - - Returns - ------- - : Traversal - The specified Traversal - - """ - name = data["name"] - prog = progs[data["prog"]] - group = simplex.groups[data.get("group", 2)] - color = QColor(*data.get("color", (0, 0, 0))) - - rangeDict = {} # slider: [startVal, endVal] - - pFlip = -1.0 if data["progressFlip"] else 1.0 - pcIdx = data["progressControl"] - if data["progressType"].lower() == "slider": - sli = simplex.sliders[pcIdx] - rangeDict[sli] = (0.0, pFlip) - else: - cmb = simplex.combos[pcIdx] - for cp in cmb.pairs: - rangeDict[cp.slider] = (0.0, cp.value) - - mFlip = -1.0 if data["multiplierFlip"] else 1.0 - mcIdx = data["multiplierControl"] - if data["multiplierType"].lower() == "slider": - sli = simplex.sliders[mcIdx] - rangeDict[sli] = (mFlip, mFlip) - else: - cmb = simplex.combos[mcIdx] - for cp in cmb.pairs: - rangeDict[cp.slider] = (cp.value, cp.value) - - ssli = sorted((rangeDict.items()), key=lambda x: x[0].name) - startPairs, endPairs = [], [] - for slider, (startVal, endVal) in ssli: - startPairs.append(TravPair(slider, startVal)) - endPairs.append(TravPair(slider, endVal)) - - startPoint = TravPoint(startPairs, 0) - endPoint = TravPoint(endPairs, 1) - - return cls(name, simplex, startPoint, endPoint, prog, group, color) - - @classmethod - def loadV3(cls, simplex, progs, data): - """Load the data from a version3 formatted json dictionary - - Parameters - ---------- - simplex : Simplex - The Simplex system that's being built - progs : [Progression - The progressions that have already been built - data : dict - The chunk of the json dict used to build this object - - Returns - ------- - : Traversal - The specified Traversal - - """ - name = data["name"] - prog = progs[data["prog"]] - group = simplex.groups[data.get("group", 2)] - color = QColor(*data.get("color", (0, 0, 0))) - - startDict = dict(data["start"]) - endDict = dict(data["end"]) - sliIdxs = sorted(startDict.keys() | endDict.keys()) - startPairs, endPairs = [], [] - for idx in sliIdxs: - startPairs.append(TravPair(simplex.sliders[idx], startDict.get(idx, 0.0))) - endPairs.append(TravPair(simplex.sliders[idx], endDict.get(idx, 0.0))) - startPoint = TravPoint(startPairs, 0) - endPoint = TravPoint(endPairs, 1) - - return cls(name, simplex, startPoint, endPoint, prog, group, color) - - def buildDefinition(self, simpDict, legacy): - """Output a dictionary definition of this object - - Parameters - ---------- - simpDict : dict - The dictionary that is being built - legacy : bool - Whether to write out the legacy definition, or the newer one - This is ignored for Traversals. There is no legacy definition - - Returns - ------- - - """ - if self._buildIdx is None: - self._buildIdx = len(simpDict["traversals"]) - x = { - "name": self.name, - "prog": self.prog.buildDefinition(simpDict, legacy), - "start": self.startPoint.buildDefinition(simpDict, legacy), - "end": self.endPoint.buildDefinition(simpDict, legacy), - "group": self.group.buildDefinition(simpDict, legacy), - "color": self.color.getRgb()[:3], - "enabled": self._enabled, - } - simpDict.setdefault("traversals", []).append(x) - return self._buildIdx - - def clearBuildIndex(self): - """Clear the build index of this object - - The buildIndex is stored when building a definition dictionary - that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - - """ - self._buildIdx = None - self.prog.clearBuildIndex() - self.group.clearBuildIndex() - - @stackable - def delete(self): - """Delete a traversal and any shapes it contains""" - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - g = self.group - if self not in g.items: - return # Can happen when deleting multiple groups - g.items.remove(self) - self.group = None - self.simplex.traversals.remove(self) - - pairs = self.prog.pairs[:] # gotta make a copy - for pp in pairs: - if not pp.shape.isRest: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) - - def extractShape(self, shape, live=True, offset=10.0): - """Extract a shape from a Traversal progression - - Parameters - ---------- - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ - return self.DCC.extractTraversalShape(self, shape, live, offset) - - def addSlider(self, slider): - """Add a slider to both the startPoint and endPoint of this Traversal - - Parameters - ---------- - slider : Slider - The slider to add - - Returns - ------- - - """ - self.startPoint.addSlider(slider, val=0.0) - self.endPoint.addSlider(slider) - - def removePairs(self, pairs): - """Remove the given pairs from both the startPoint and endPoint of this Traversal - - Parameters - ---------- - pairs : [TravPair - The pairs to remove - - Returns - ------- - - """ - # Get only the pairs that are a part of this traversal - sPairs = [i for i in self.startPoint.pairs if i in pairs] - ePairs = [i for i in self.endPoint.pairs if i in pairs] - pairs = sPairs + ePairs - - # Get all the pairs that use the selected sliders - sliders = {p.slider for p in pairs} - sPairs = [i for i in self.startPoint.pairs if i.slider in sliders] - ePairs = [i for i in self.endPoint.pairs if i.slider in sliders] - - # do the removal - for pair in sPairs: - pair.remove() - - for pair in ePairs: - pair.remove() - - @staticmethod - def traversalAlreadyExists(simplex, sliders, ranges): - """In a given simplex syste, check if a traversal exists - with the given sliders and ranges - """ - chk = dict(zip(sliders, ranges)) - for trav in simplex.traversals: - if chk == trav.ranges(): - return trav - return None - - @staticmethod - def getCount(sliders, ranges): - """Get the count of shapes to create for a traversal with the given - sliders and ranges. It's the max number of shapes on a given side of 0 - """ - counts = [] - for sli, rng in zip(sliders, ranges): - if rng[0] == rng[1]: - continue - vals = sli.prog.getValues() - if max(rng) == 0: - count = len([v for v in vals if v < 0]) - else: - count = len([v for v in vals if v > 0]) - counts.append(count) - if not counts: - return 0 - return max(counts) - - def getInputVector(self, value): - """Get the input to the Solver that would set this traversal to - the given value - - Parameters - ---------- - value : float - The value to set the traversal to - - Returns - ------- - : [float, ...] - The ordered slider values - - """ - indexBySlider = {slider: idx for idx, slider in enumerate(self.simplex.sliders)} - - fullStart = [0.0] * len(self.simplex.sliders) - for pair in self.startPoint.pairs: - fullStart[indexBySlider[pair.slider]] = pair.value - - fullEnd = [0.0] * len(self.simplex.sliders) - for pair in self.endPoint.pairs: - fullEnd[indexBySlider[pair.slider]] = pair.value - - def _lerp(s, e, v): - return s * (1 - v) + e * v - - return [_lerp(fs, fe, value) for fs, fe in zip(fullStart, fullEnd)] +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +# pylint:disable=missing-docstring,unused-argument,no-self-use +from Qt.QtGui import QColor +from ..utils import nested +from .accessor import SimplexAccessor, SimplexTickAccessor +from .combo import Combo +from .group import Group +from .progression import Progression +from .slider import Slider +from .stack import stackable + + +class TravPair(SimplexTickAccessor): + """ """ + + classDepth = 4 + + def __init__(self, slider, value): + simplex = slider.simplex + super(TravPair, self).__init__(simplex) + self.slider = slider + self._value = float(value) + self.minValue = -1.0 + self.maxValue = 1.0 + self._tickDelta = 0.0 + self.travPoint = None + self.expanded = {} + + def valueTick(self, ticks, mul): + """ """ + self._tickDelta += self.dragStep * ticks * mul + if (self._tickDelta + self.value) <= self.slider.minValue: + self._tickDelta = self.slider.minValue - self.value + if self.value != self.slider.minValue: + self.value = self.slider.minValue + self._tickDelta = 0.0 + elif (self._tickDelta + self.value) >= self.slider.maxValue: + self._tickDelta = self.slider.maxValue - self.value + if self.value != self.slider.maxValue: + self._tickDelta = 0.0 + self.value = self.slider.maxValue + elif abs(self._tickDelta + self.value) <= 1.0e-5: + if self.value != 0.0: + self._tickDelta = 0.0 + self.value = 0.0 + + @property + def models(self): + """ """ + return self.simplex.models + + @property + def name(self): + """ """ + return self.slider.name + + @property + def value(self): + """ """ + return self._value + + @value.setter + @stackable + def value(self, val): + """ """ + self._value = val + for model in self.models: + model.itemDataChanged(self) + + def buildDefinition(self, simpDict, legacy): + """ """ + sIdx = self.slider.buildDefinition(simpDict, legacy) + return sIdx, self.value + + def treeRow(self): + """ """ + return self.travPoint.pairs.index(self) + + def treeParent(self): + """ """ + return self.travPoint + + def treeData(self, column): + """ """ + if column == 0: + return self.name + if column == 1: + return self.value + return None + + @stackable + def remove(self): + """ """ + mgrs = [model.removeItemManager(self) for model in self.models] + with nested(*mgrs): + self.travPoint.pairs.remove(self) + self.travPoint = None + + @stackable + def delete(self): + """ """ + self.travPoint.traversal.removePairs([self]) + + @staticmethod + def removeAll(pairs): + """ """ + travs = list({p.travPoint.traversal for p in pairs}) + for trav in travs: + trav.removePairs(pairs) + + +class TravPoint(SimplexAccessor): + """ """ + + classDepth = 3 + + def __init__(self, pairs, row): + if not pairs: + raise ValueError("Pairs must be provided for a TravPoint") + simplex = pairs[0].slider.simplex + super(TravPoint, self).__init__(simplex) + + self.pairs = pairs + for pair in pairs: + pair.travPoint = self + self.row = row + self.traversal = None + self.expanded = {} + + def sliders(self): + """ """ + return [i.slider for i in self.pairs] + + @staticmethod + def _wideCeiling(val, eps=0.001): + """ """ + if val > eps: + return 1.0 + elif val < -eps: + return -1.0 + return 0.0 + + @stackable + def addPair(self, pair): + """ + + Parameters + ---------- + pair : + + + Returns + ------- + + """ + mgrs = [model.insertItemManager(self) for model in self.models] + with nested(*mgrs): + self.pairs.append(pair) + pair.travPoint = self + + def removePair(self, pair): + """ + + Parameters + ---------- + pair : + + + Returns + ------- + + """ + pair.remove() + + def addSlider(self, slider, val=None): + """ + + Parameters + ---------- + slider : + + val : + (Default value = None) + + Returns + ------- + + """ + val = val if val is not None else slider.value + val = self._wideCeiling(val) + sliders = self.sliders() + try: + idx = sliders.index(slider) + except ValueError: + self.addPair(TravPair(slider, val)) + else: + self.pairs[idx].value = val + + def addItem(self, item): + """ + + Parameters + ---------- + item : + + + Returns + ------- + + """ + if isinstance(item, Slider): + self.addSlider(item) + elif isinstance(item, Combo): + for cp in item.pairs: + self.addSlider(cp.slider, cp.value) + + @property + def name(self): + """ """ + return "START" if self.row == 0 else "END" + + @name.setter + def name(self, val): + # Do nothing. This is just here to make the linter happy + return + + def treeData(self, column): + """ + + Parameters + ---------- + column : + + + Returns + ------- + + """ + if column == 0: + return self.name + return None + + def treeChild(self, row): + """ + + Parameters + ---------- + row : + + + Returns + ------- + + """ + return self.pairs[row] + + def treeRow(self): + """ """ + return self.row + + def treeParent(self): + """ """ + return self.traversal + + def treeChildCount(self): + """ """ + return len(self.pairs) + + def buildDefinition(self, simpDict, legacy): + """ + + Parameters + ---------- + simpDict : + + legacy : + + + Returns + ------- + + """ + return [p.buildDefinition(simpDict, legacy) for p in self.pairs] + + def getInputVector(self): + """get the input to the solver that would fully activate this point of the traversal + + parameters + ---------- + + returns + ------- + : [float, ...] + the ordered slider values + + """ + invec = [0.0] * len(self.simplex.sliders) + for cp in self.pairs: + invec[self.simplex.sliders.index(cp.slider)] = cp.value + return invec + + +class Traversal(SimplexAccessor): + """Traversals control a Progression based on any 2 points in the Solver space. + + Traversals only make sense with intermediate shapes in the progression of the sliders + that control it. + + Traversals should never have a shape at 100%. That shape should be handled by a Combo + + First: A "point in solver space" just means a list of slider/value pairs. + The Slider/Value pairs that make a up a Combo are just a "Point in solver space" as well. + So technically Combos could be thought of as a special-case of Traversals. Combos control + a progression between the "Rest Point" where all sliders are at 0, and the Combo point + + Outside of the context of Traversals, I just call solver space points "Combos", because + I don't need to be crazy specific like I do here. + + The initial use-case for Traversals was dealing with eye combo shapes with incremental + Progressions. The eyeLookDown and the eyeClosed shapes both pull the upper lid down a great + deal, and the eyeClosed was a 4-shape progression. So, when transitioning from eyeLookDown + to eyeLookDown+eyeClosed, the deltas for all the progressive shapes were being triggered as + the combo was coming on, causing major wobbles in the eyelid. So we needed shapes that + countered those incrementals, but *only* on the transition from eyeLookDown to + eyeLookDown+eyeClosed (NOT on the transition from eyeClosed to eyeLookDown+eyeClosed) + + Early setups used floating Combos, but those have linearinterpolation, and I wanted a + cleaner solution. That solution is the Traversal + + Parameters + ---------- + name : str + The name of this Combo + simplex : Simplex + The parent Simplex system + startPoint : TravPoint + A set of Slider/Value pairs where the Traversal solves to 0 + endPoint : TravPoint + A set of Slider/Value pairs where the Traversal solves to 1 + prog : Progression + The Progression that this Combo controls + group : Group + The Group to create this combo in + color : QColor + The color of this item in the UI + + Returns + ------- + + """ + + classDepth = 2 + + def __init__( + self, + name, + simplex, + startPoint, + endPoint, + prog, + group, + color=None, + ): + super(Traversal, self).__init__(simplex) + color = QColor(128, 128, 128) if color is None else color + with self.stack.store(self): + if group.groupType is not type(self): + raise ValueError( + "Cannot add this Traversal to a group of a different type" + ) + self._name = name + self.startPoint = startPoint + self.endPoint = endPoint + self.prog = prog + self._buildIdx = None + self.expanded = {} + self._enabled = True + self.color = color + + mgrs = [model.insertItemManager(group) for model in self.models] + with nested(*mgrs): + self.group = group + self.startPoint.traversal = self + self.endPoint.traversal = self + self.prog.controller = self + self.group.items.append(self) + self.simplex.traversals.append(self) + + @classmethod + def createTraversal(cls, name, simplex, startPairs, endPairs, group=None, count=4): + """Create a Traversal between two lists of pairs + + Parameters + ---------- + name : str + The name of this Combo + simplex : Simplex + The parent Simplex system + startPairs : [(Slider + A list of Slider/Value pairs to make the startPoint + endPairs : [(Slider + A list of Slider/Value pairs to make the endPoint + group : Group + The Group to create this combo in (Default value = None) + count : int + The number of incrementals to create (including the 100%) (Default value = 4) + + Returns + ------- + + """ + if simplex.restShape is None: + raise RuntimeError("Simplex system is missing rest shape") + + if group is None: + gname = "TRAVERSALS" + matches = [i for i in simplex.traversalGroups if i.name == gname] + if matches: + group = matches[0] + else: + group = Group(gname, simplex, Traversal) + + startPairs = [TravPair(p[0], p[1]) for p in startPairs] + endPairs = [TravPair(p[0], p[1]) for p in endPairs] + + startPoint = TravPoint(startPairs, 0) + endPoint = TravPoint(endPairs, 1) + + prog = Progression(name, simplex) + trav = cls(name, simplex, startPoint, endPoint, prog, group) + + for c in reversed(list(range(count))): + val = (100 * (c + 1)) // count + pp = prog.createShape("{0}_{1}".format(name, val), val / 100.0) + simplex.DCC.zeroShape(pp.shape) + return trav + + @property + def enabled(self): + """Get whether this Traversal is evaluated in the solver""" + return self._enabled + + @enabled.setter + @stackable + def enabled(self, value): + """Set whether this Traversal is evaluated in the solver + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self._enabled = value + for model in self.models: + model.itemDataChanged(self) + + @property + def name(self): + """Get the name of a Traversal""" + return self._name + + @name.setter + @stackable + def name(self, value): + """Set the name of a Traversal + + Parameters + ---------- + value : + + + Returns + ------- + + """ + self._name = value + self.prog.name = value + # self.DCC.renameTraversal(self, value) + # for model in self.models: + # model.itemDataChanged(self) + + def treeChild(self, row): + """ + + Parameters + ---------- + row : + + + Returns + ------- + + """ + if row == 0: + return self.startPoint + elif row == 1: + return self.endPoint + elif row == 2: + return self.prog + return None + + def treeRow(self): + """ """ + return self.group.items.index(self) + + def treeParent(self): + """ """ + return self.group + + def treeChildCount(self): + """ """ + return 3 + + def treeChecked(self): + """ """ + return self.enabled + + def allSliders(self): + """Get the list of all Sliders that control this Traversal + + Parameters + ---------- + + Returns + ------- + : [Slider, ...] + The list of all Sliders that control this Traversal + + """ + startSliders = [p.slider for p in self.startPoint.pairs] + endSliders = [ + p.slider for p in self.endPoint.pairs if p.slider not in startSliders + ] + return startSliders + endSliders + + def dynamicSliders(self): + """Get a list of sliders that have different values at the start and end""" + return [sli for sli, rng in self.ranges() if rng[0] != rng[1]] + + def staticSliders(self): + """Get a list of sliders that have the same values at the start and end""" + return [sli for sli, rng in self.ranges() if rng[0] == rng[1]] + + def ranges(self): + """Get the range per Slider for this Traversal + + Parameters + ---------- + + Returns + ------- + : type + (dict): A {Slider: range} dict + + """ + startDict = {p.slider: p.value for p in self.startPoint.pairs} + endDict = {p.slider: p.value for p in self.endPoint.pairs} + allSliders = startDict.keys() | endDict.keys() + + rangeDict = {} + for sli in allSliders: + rangeDict[sli] = (startDict.get(sli, 0.0), endDict.get(sli, 0.0)) + return rangeDict + + @staticmethod + def buildTraversalName(ranges): + """Given the range dict (like from Traversal.ranges()) come up with a name + + Parameters + ---------- + ranges : dict + A {Slider: range} dict + + Returns + ------- + : str + The suggested Traversal name + + """ + static, dynamic = [], [] + for sli, rng in ranges.items(): + if rng[0] == rng[1]: + static.append(sli) + else: + dynamic.append(sli) + + parts = [] + for grp in static, dynamic: + for slider in sorted(grp, key=lambda x: x.name): + prefix = None + start, end = ranges[slider] + if start == end: + # prefix = 'St' # St for Static + if start == 0: + continue + shp = slider.prog.getShapeAtValue(start) + if shp is None: + continue + name = shp.strippedName() + else: + prefix = "Dy" # Dy for Dynamic + if start == 0: + shp = slider.prog.getShapeAtValue(end) + if shp is None: + continue + name = shp.strippedName() + elif end == 0: + shp = slider.prog.getShapeAtValue(start) + if shp is None: + continue + name = shp.strippedName() + else: + name = slider.name + + if prefix is not None: + parts.append(prefix) + parts.append(name) + + return "Tv_" + "_".join(parts) + + def controllerNameLinks(self): + """ """ + surr = "_{0}_".format(self.name) + return ["_{0}_".format(sli) in surr for sli in self.allSliders()] + + def nameLinks(self): + """ + + Parameters + ---------- + + Returns + ------- + : type + progression depends on this traversal's name + + """ + # In this case, these names will *NOT* have the possibility of + # a pos/neg name. Only the traversal name, and possibly a percentage + shapeNames = [] + shapes = [i.shape for i in self.prog.pairs] + for s in shapes: + x = s.name.rsplit("_", 1) + if len(x) == 2: + base, sfx = x + x = base if sfx.isdigit() else s.name + shapeNames.append(x) + return [i == self.name for i in shapeNames] + + @stackable + def createShape(self, shapeName=None, tVal=None): + """Create a shape and add it to a progression + + Parameters + ---------- + shapeName : str or None + The name of the shape to create. + If None, give it a default name + tVal : float or None + The progression value to set for the new Shape. + If None, it gets a "smart" default value + + Returns + ------- + + """ + pp, idx = self.prog.newProgPair(shapeName, tVal) + mgrs = [model.insertItemManager(self.prog, idx) for model in self.models] + with nested(*mgrs): + pp.prog = self.prog + self.prog.pairs.insert(idx, pp) + return pp + + @classmethod + def loadV2(cls, simplex, progs, data): + """Load the data from a version2 formatted json dictionary + + Parameters + ---------- + simplex : Simplex + The Simplex system that's being built + progs : [Progression + The progressions that have already been built + data : dict + The chunk of the json dict used to build this object + + Returns + ------- + : Traversal + The specified Traversal + + """ + name = data["name"] + prog = progs[data["prog"]] + group = simplex.groups[data.get("group", 2)] + color = QColor(*data.get("color", (0, 0, 0))) + + rangeDict = {} # slider: [startVal, endVal] + + pFlip = -1.0 if data["progressFlip"] else 1.0 + pcIdx = data["progressControl"] + if data["progressType"].lower() == "slider": + sli = simplex.sliders[pcIdx] + rangeDict[sli] = (0.0, pFlip) + else: + cmb = simplex.combos[pcIdx] + for cp in cmb.pairs: + rangeDict[cp.slider] = (0.0, cp.value) + + mFlip = -1.0 if data["multiplierFlip"] else 1.0 + mcIdx = data["multiplierControl"] + if data["multiplierType"].lower() == "slider": + sli = simplex.sliders[mcIdx] + rangeDict[sli] = (mFlip, mFlip) + else: + cmb = simplex.combos[mcIdx] + for cp in cmb.pairs: + rangeDict[cp.slider] = (cp.value, cp.value) + + ssli = sorted((rangeDict.items()), key=lambda x: x[0].name) + startPairs, endPairs = [], [] + for slider, (startVal, endVal) in ssli: + startPairs.append(TravPair(slider, startVal)) + endPairs.append(TravPair(slider, endVal)) + + startPoint = TravPoint(startPairs, 0) + endPoint = TravPoint(endPairs, 1) + + return cls(name, simplex, startPoint, endPoint, prog, group, color) + + @classmethod + def loadV3(cls, simplex, progs, data): + """Load the data from a version3 formatted json dictionary + + Parameters + ---------- + simplex : Simplex + The Simplex system that's being built + progs : [Progression + The progressions that have already been built + data : dict + The chunk of the json dict used to build this object + + Returns + ------- + : Traversal + The specified Traversal + + """ + name = data["name"] + prog = progs[data["prog"]] + group = simplex.groups[data.get("group", 2)] + color = QColor(*data.get("color", (0, 0, 0))) + + startDict = dict(data["start"]) + endDict = dict(data["end"]) + sliIdxs = sorted(startDict.keys() | endDict.keys()) + startPairs, endPairs = [], [] + for idx in sliIdxs: + startPairs.append(TravPair(simplex.sliders[idx], startDict.get(idx, 0.0))) + endPairs.append(TravPair(simplex.sliders[idx], endDict.get(idx, 0.0))) + startPoint = TravPoint(startPairs, 0) + endPoint = TravPoint(endPairs, 1) + + return cls(name, simplex, startPoint, endPoint, prog, group, color) + + def buildDefinition(self, simpDict, legacy): + """Output a dictionary definition of this object + + Parameters + ---------- + simpDict : dict + The dictionary that is being built + legacy : bool + Whether to write out the legacy definition, or the newer one + This is ignored for Traversals. There is no legacy definition + + Returns + ------- + + """ + if self._buildIdx is None: + self._buildIdx = len(simpDict["traversals"]) + x = { + "name": self.name, + "prog": self.prog.buildDefinition(simpDict, legacy), + "start": self.startPoint.buildDefinition(simpDict, legacy), + "end": self.endPoint.buildDefinition(simpDict, legacy), + "group": self.group.buildDefinition(simpDict, legacy), + "color": self.color.getRgb()[:3], + "enabled": self._enabled, + } + simpDict.setdefault("traversals", []).append(x) + return self._buildIdx + + def clearBuildIndex(self): + """Clear the build index of this object + + The buildIndex is stored when building a definition dictionary + that keeps track of its index for later referencing + + Parameters + ---------- + + Returns + ------- + + """ + self._buildIdx = None + self.prog.clearBuildIndex() + self.group.clearBuildIndex() + + @stackable + def delete(self): + """Delete a traversal and any shapes it contains""" + mgrs = [model.removeItemManager(self) for model in self.models] + with nested(*mgrs): + g = self.group + if self not in g.items: + return # Can happen when deleting multiple groups + g.items.remove(self) + self.group = None + self.simplex.traversals.remove(self) + + pairs = self.prog.pairs[:] # gotta make a copy + for pp in pairs: + if not pp.shape.isRest: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) + + def extractShape(self, shape, live=True, offset=10.0): + """Extract a shape from a Traversal progression + + Parameters + ---------- + shape : + + live : + (Default value = True) + offset : + (Default value = 10.0) + + Returns + ------- + + """ + return self.DCC.extractTraversalShape(self, shape, live, offset) + + def addSlider(self, slider): + """Add a slider to both the startPoint and endPoint of this Traversal + + Parameters + ---------- + slider : Slider + The slider to add + + Returns + ------- + + """ + self.startPoint.addSlider(slider, val=0.0) + self.endPoint.addSlider(slider) + + def removePairs(self, pairs): + """Remove the given pairs from both the startPoint and endPoint of this Traversal + + Parameters + ---------- + pairs : [TravPair + The pairs to remove + + Returns + ------- + + """ + # Get only the pairs that are a part of this traversal + sPairs = [i for i in self.startPoint.pairs if i in pairs] + ePairs = [i for i in self.endPoint.pairs if i in pairs] + pairs = sPairs + ePairs + + # Get all the pairs that use the selected sliders + sliders = {p.slider for p in pairs} + sPairs = [i for i in self.startPoint.pairs if i.slider in sliders] + ePairs = [i for i in self.endPoint.pairs if i.slider in sliders] + + # do the removal + for pair in sPairs: + pair.remove() + + for pair in ePairs: + pair.remove() + + @staticmethod + def traversalAlreadyExists(simplex, sliders, ranges): + """In a given simplex syste, check if a traversal exists + with the given sliders and ranges + """ + chk = dict(zip(sliders, ranges)) + for trav in simplex.traversals: + if chk == trav.ranges(): + return trav + return None + + @staticmethod + def getCount(sliders, ranges): + """Get the count of shapes to create for a traversal with the given + sliders and ranges. It's the max number of shapes on a given side of 0 + """ + counts = [] + for sli, rng in zip(sliders, ranges): + if rng[0] == rng[1]: + continue + vals = sli.prog.getValues() + if max(rng) == 0: + count = len([v for v in vals if v < 0]) + else: + count = len([v for v in vals if v > 0]) + counts.append(count) + if not counts: + return 0 + return max(counts) + + def getInputVector(self, value): + """Get the input to the Solver that would set this traversal to + the given value + + Parameters + ---------- + value : float + The value to set the traversal to + + Returns + ------- + : [float, ...] + The ordered slider values + + """ + indexBySlider = {slider: idx for idx, slider in enumerate(self.simplex.sliders)} + + fullStart = [0.0] * len(self.simplex.sliders) + for pair in self.startPoint.pairs: + fullStart[indexBySlider[pair.slider]] = pair.value + + fullEnd = [0.0] * len(self.simplex.sliders) + for pair in self.endPoint.pairs: + fullEnd[indexBySlider[pair.slider]] = pair.value + + def _lerp(s, e, v): + return s * (1 - v) + e * v + + return [_lerp(fs, fe, value) for fs, fe in zip(fullStart, fullEnd)] diff --git a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py index 90652d57..d4b12876 100644 --- a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py +++ b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py @@ -1,245 +1,245 @@ -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -from functools import partial - -from maya import cmds - -from ...interface.mayaInterface import disconnected -from ...interfaceModel import coerceIndexToType -from ...items import Combo - - -# UI stuff -def registerContext(tree, clickIdx, indexes, menu): - # The basicBlendshape deformer is included in simplex_maya - if not cmds.pluginInfo("simplex_maya", query=True, loaded=True): - try: - cmds.loadPlugin("simplex_maya") - except RuntimeError: - return False - - comboIdxs = coerceIndexToType(indexes, Combo) - combos = [cidx.model().itemFromIndex(cidx) for cidx in comboIdxs] - - if combos: - freezeAct = menu.addAction("Freeze Combos") - unfreezeAct = menu.addAction("Un-Freeze Combos") - freezeAct.triggered.connect(partial(freezeCombosContext, combos, tree, True)) - unfreezeAct.triggered.connect(partial(freezeCombosContext, combos, tree, False)) - return True - return False - - -def freezeCombosContext(combos, tree, doFreeze): - if doFreeze: - for combo in combos: - if not combo.frozen: - freezeCombo(combo) - else: - for combo in combos: - if combo.frozen: - unfreezeCombo(combo) - - tree.update() - - -def freezeCombo(combo): - """Freeze a combo so you can change the upstream combos and shapes - without affecting the result that you sculpted for the given combo - - In practice, this snapshots the combo, then live-reads the upstream - shapes from the main blendshape and builds an up-to-date combo. The - difference between these two meshes is added back into the combo shape - """ - simplex = combo.simplex - - tweakShapeGroups = [] - fullGeos = [] - ppFilter = [] - freezeShapes = [] - - # disconnect the controller from the operator - with disconnected(simplex.DCC.op) as sliderCnx: - for _shapeIdx, pp in enumerate(combo.prog.pairs): - tVal = pp.value - freezeShape = pp.shape - if freezeShape.isRest: - continue - - freezeShapes.append(freezeShape) - # zero all the sliders - cnx = sliderCnx[simplex.DCC.op] - for a in cnx.values(): - cmds.setAttr(a, 0.0) - - # set the combo values - for pair in combo.pairs: - cmds.setAttr(cnx[pair.slider.thing], pair.value * tVal) - - tweakPairs = [] - for shape in simplex.shapes[1:]: # skip the restShape - shapeVal = cmds.getAttr(shape.thing) - if abs(shapeVal) > 0.0001: - tweakPairs.append((shape, shapeVal)) - - # Extract this fully-on shape - fullGeo = cmds.duplicate( - simplex.DCC.mesh, name="{0}_Freeze".format(freezeShape.name) - )[0] - fullGeos.append(fullGeo) - - # Clean any orig shapes for now - interObjs = cmds.ls( - cmds.listRelatives(fullGeo, shapes=True), intermediateObjects=True - ) - cmds.delete(interObjs) - - tweakShapeGroups.append(tweakPairs) - ppFilter.append(pp) - - simplex.DCC.primeShapes(combo) - - shapePlugFmt = ( - ".inputTarget[{meshIdx}].inputTargetGroup[{shapeIdx}].inputTargetItem[6000]" - ) - endPlugs = [ - ".inputRelativePointsTarget", - ".inputRelativeComponentsTarget", - ".inputPointsTarget", - ".inputComponentsTarget", - "", - ] - shapeNode = simplex.DCC.shapeNode - helpers = [] - for geo, tweakPairs, pp, freezeShape in zip( - fullGeos, tweakShapeGroups, ppFilter, freezeShapes - ): - # build the basicBS node - bbs = cmds.deformer(geo, type="basicBlendShape")[0] - helpers.append(bbs) - idx = 0 - - for shape, val in tweakPairs: - if shape == freezeShape: - continue - # connect the output shape.thing to the basicBS - - # Create an empty shape. Do it like this to get the automated renaming stuff - gDup = cmds.duplicate(geo, name="{0}_DeltaCnx".format(shape.name))[0] - # The 4th value must be 1.0 so the blendshape auto-names - cmds.blendShape(bbs, edit=True, target=(geo, idx, gDup, 1.0)) - cmds.blendShape(bbs, edit=True, weight=(idx, -val)) - cmds.delete(gDup) - - # Connect the shape plugs - inPlug = bbs + shapePlugFmt - inPlug = inPlug.format(meshIdx=0, shapeIdx=idx) - - shapeIdx = simplex.DCC.getShapeIndex(shape) - outPlug = shapeNode + shapePlugFmt - outPlug = outPlug.format(meshIdx=0, shapeIdx=shapeIdx) - - # Must connect the individual child plugs rather than the top - # because otherwise the input geometry plug overrides these deltas - for ep in endPlugs: - cmds.connectAttr(outPlug + ep, inPlug + ep) - - idx += 1 - - # Connect the basicBS back into freezeShape - freezeShapeIdx = simplex.DCC.getShapeIndex(pp.shape) - freezeShapeTarget = shapeNode + shapePlugFmt + ".inputGeomTarget" - freezeShapeTarget = freezeShapeTarget.format(meshIdx=0, shapeIdx=freezeShapeIdx) - cmds.connectAttr(geo + ".outMesh", freezeShapeTarget) - - gShapes = cmds.listRelatives(geo, shapes=True) - if True: - # Hide the frozen shapenode under the ctrl as an intermediate shape - for gs in gShapes: - cmds.setAttr(gs + ".intermediateObject", 1) - nn = cmds.parent(gs, simplex.DCC.ctrl, shape=True, relative=True) - helpers.extend(nn) - - # Get rid of the extra transform object - cmds.delete(geo) - else: - helpers.extend(cmds.listRelatives(geo, shapes=True)) - - # keep track of the shapes under the ctrl object - combo.freezeThing = helpers - - -def unfreezeCombo(combo): - if combo.freezeThing: - cmds.delete(combo.freezeThing) - combo.freezeThing = [] - - -def _getDeformerChain(chkObj): - # Get a deformer chain - memo = [] - while chkObj and chkObj not in memo: - memo.append(chkObj) - - typ = cmds.nodeType(chkObj) - if typ == "mesh": - cnx = cmds.listConnections(chkObj + ".inMesh") or [None] - chkObj = cnx[0] - elif typ == "groupParts": - cnx = cmds.listConnections( - chkObj + ".inputGeometry", destination=False, shapes=True - ) or [None] - chkObj = cnx[0] - else: - cnx = cmds.ls(chkObj, type="geometryFilter") or [None] - chkObj = cnx[0] - if chkObj: # we have a deformer - cnx = cmds.listConnections(chkObj + ".input[0].inputGeometry") or [None] - chkObj = cnx[0] - return memo - - -def checkFrozen(combo): - # If the blendshape shape has an incoming connection whose shape name - # ends with 'FreezeShape' and the shape's parent is the ctrl - # - simplex = combo.simplex - - ret = [] - shapes = combo.prog.getShapes() - shapes = [i for i in shapes if not i.isRest] - - shapePlugFmt = ( - ".inputTarget[{meshIdx}].inputTargetGroup[{shapeIdx}].inputTargetItem[6000]" - ) - - for shape in shapes: - shpIdx = simplex.DCC.getShapeIndex(shape) - shpPlug = ( - simplex.DCC.shapeNode - + shapePlugFmt.format(meshIdx=0, shapeIdx=shpIdx) - + ".inputGeomTarget" - ) - - cnx = cmds.listConnections(shpPlug, shapes=True, destination=False) or [] - for cc in cnx: - if not cc.endswith("FreezeShape"): - continue - par = cmds.listRelatives(cc, parent=True) - if par and par[0] == simplex.DCC.ctrl: - # Can't use list history to get the chain because it's a pseudo-cycle - ret.extend(_getDeformerChain(cc)) - return ret +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +from functools import partial + +from maya import cmds + +from ...interface.mayaInterface import disconnected +from ...interfaceModel import coerceIndexToType +from ...items import Combo + + +# UI stuff +def registerContext(tree, clickIdx, indexes, menu): + # The basicBlendshape deformer is included in simplex_maya + if not cmds.pluginInfo("simplex_maya", query=True, loaded=True): + try: + cmds.loadPlugin("simplex_maya") + except RuntimeError: + return False + + comboIdxs = coerceIndexToType(indexes, Combo) + combos = [cidx.model().itemFromIndex(cidx) for cidx in comboIdxs] + + if combos: + freezeAct = menu.addAction("Freeze Combos") + unfreezeAct = menu.addAction("Un-Freeze Combos") + freezeAct.triggered.connect(partial(freezeCombosContext, combos, tree, True)) + unfreezeAct.triggered.connect(partial(freezeCombosContext, combos, tree, False)) + return True + return False + + +def freezeCombosContext(combos, tree, doFreeze): + if doFreeze: + for combo in combos: + if not combo.frozen: + freezeCombo(combo) + else: + for combo in combos: + if combo.frozen: + unfreezeCombo(combo) + + tree.update() + + +def freezeCombo(combo): + """Freeze a combo so you can change the upstream combos and shapes + without affecting the result that you sculpted for the given combo + + In practice, this snapshots the combo, then live-reads the upstream + shapes from the main blendshape and builds an up-to-date combo. The + difference between these two meshes is added back into the combo shape + """ + simplex = combo.simplex + + tweakShapeGroups = [] + fullGeos = [] + ppFilter = [] + freezeShapes = [] + + # disconnect the controller from the operator + with disconnected(simplex.DCC.op) as sliderCnx: + for _shapeIdx, pp in enumerate(combo.prog.pairs): + tVal = pp.value + freezeShape = pp.shape + if freezeShape.isRest: + continue + + freezeShapes.append(freezeShape) + # zero all the sliders + cnx = sliderCnx[simplex.DCC.op] + for a in cnx.values(): + cmds.setAttr(a, 0.0) + + # set the combo values + for pair in combo.pairs: + cmds.setAttr(cnx[pair.slider.thing], pair.value * tVal) + + tweakPairs = [] + for shape in simplex.shapes[1:]: # skip the restShape + shapeVal = cmds.getAttr(shape.thing) + if abs(shapeVal) > 0.0001: + tweakPairs.append((shape, shapeVal)) + + # Extract this fully-on shape + fullGeo = cmds.duplicate( + simplex.DCC.mesh, name="{0}_Freeze".format(freezeShape.name) + )[0] + fullGeos.append(fullGeo) + + # Clean any orig shapes for now + interObjs = cmds.ls( + cmds.listRelatives(fullGeo, shapes=True), intermediateObjects=True + ) + cmds.delete(interObjs) + + tweakShapeGroups.append(tweakPairs) + ppFilter.append(pp) + + simplex.DCC.primeShapes(combo) + + shapePlugFmt = ( + ".inputTarget[{meshIdx}].inputTargetGroup[{shapeIdx}].inputTargetItem[6000]" + ) + endPlugs = [ + ".inputRelativePointsTarget", + ".inputRelativeComponentsTarget", + ".inputPointsTarget", + ".inputComponentsTarget", + "", + ] + shapeNode = simplex.DCC.shapeNode + helpers = [] + for geo, tweakPairs, pp, freezeShape in zip( + fullGeos, tweakShapeGroups, ppFilter, freezeShapes + ): + # build the basicBS node + bbs = cmds.deformer(geo, type="basicBlendShape")[0] + helpers.append(bbs) + idx = 0 + + for shape, val in tweakPairs: + if shape == freezeShape: + continue + # connect the output shape.thing to the basicBS + + # Create an empty shape. Do it like this to get the automated renaming stuff + gDup = cmds.duplicate(geo, name="{0}_DeltaCnx".format(shape.name))[0] + # The 4th value must be 1.0 so the blendshape auto-names + cmds.blendShape(bbs, edit=True, target=(geo, idx, gDup, 1.0)) + cmds.blendShape(bbs, edit=True, weight=(idx, -val)) + cmds.delete(gDup) + + # Connect the shape plugs + inPlug = bbs + shapePlugFmt + inPlug = inPlug.format(meshIdx=0, shapeIdx=idx) + + shapeIdx = simplex.DCC.getShapeIndex(shape) + outPlug = shapeNode + shapePlugFmt + outPlug = outPlug.format(meshIdx=0, shapeIdx=shapeIdx) + + # Must connect the individual child plugs rather than the top + # because otherwise the input geometry plug overrides these deltas + for ep in endPlugs: + cmds.connectAttr(outPlug + ep, inPlug + ep) + + idx += 1 + + # Connect the basicBS back into freezeShape + freezeShapeIdx = simplex.DCC.getShapeIndex(pp.shape) + freezeShapeTarget = shapeNode + shapePlugFmt + ".inputGeomTarget" + freezeShapeTarget = freezeShapeTarget.format(meshIdx=0, shapeIdx=freezeShapeIdx) + cmds.connectAttr(geo + ".outMesh", freezeShapeTarget) + + gShapes = cmds.listRelatives(geo, shapes=True) + if True: + # Hide the frozen shapenode under the ctrl as an intermediate shape + for gs in gShapes: + cmds.setAttr(gs + ".intermediateObject", 1) + nn = cmds.parent(gs, simplex.DCC.ctrl, shape=True, relative=True) + helpers.extend(nn) + + # Get rid of the extra transform object + cmds.delete(geo) + else: + helpers.extend(cmds.listRelatives(geo, shapes=True)) + + # keep track of the shapes under the ctrl object + combo.freezeThing = helpers + + +def unfreezeCombo(combo): + if combo.freezeThing: + cmds.delete(combo.freezeThing) + combo.freezeThing = [] + + +def _getDeformerChain(chkObj): + # Get a deformer chain + memo = [] + while chkObj and chkObj not in memo: + memo.append(chkObj) + + typ = cmds.nodeType(chkObj) + if typ == "mesh": + cnx = cmds.listConnections(chkObj + ".inMesh") or [None] + chkObj = cnx[0] + elif typ == "groupParts": + cnx = cmds.listConnections( + chkObj + ".inputGeometry", destination=False, shapes=True + ) or [None] + chkObj = cnx[0] + else: + cnx = cmds.ls(chkObj, type="geometryFilter") or [None] + chkObj = cnx[0] + if chkObj: # we have a deformer + cnx = cmds.listConnections(chkObj + ".input[0].inputGeometry") or [None] + chkObj = cnx[0] + return memo + + +def checkFrozen(combo): + # If the blendshape shape has an incoming connection whose shape name + # ends with 'FreezeShape' and the shape's parent is the ctrl + # + simplex = combo.simplex + + ret = [] + shapes = combo.prog.getShapes() + shapes = [i for i in shapes if not i.isRest] + + shapePlugFmt = ( + ".inputTarget[{meshIdx}].inputTargetGroup[{shapeIdx}].inputTargetItem[6000]" + ) + + for shape in shapes: + shpIdx = simplex.DCC.getShapeIndex(shape) + shpPlug = ( + simplex.DCC.shapeNode + + shapePlugFmt.format(meshIdx=0, shapeIdx=shpIdx) + + ".inputGeomTarget" + ) + + cnx = cmds.listConnections(shpPlug, shapes=True, destination=False) or [] + for cc in cnx: + if not cc.endswith("FreezeShape"): + continue + par = cmds.listRelatives(cc, parent=True) + if par and par[0] == simplex.DCC.ctrl: + # Can't use list history to get the chain because it's a pseudo-cycle + ret.extend(_getDeformerChain(cc)) + return ret diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 1773a745..00ea550e 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -1,1543 +1,1543 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - - -# Ignore a bunch of linter warnings that show up because of my choice of abstraction -# pylint: disable=unused-argument,too-many-public-methods,relative-import -# pylint: disable=too-many-statements,no-self-use,missing-docstring -import json -import os -import re -import sys -import weakref -from contextlib import contextmanager - -from .comboCheckDialog import ComboCheckDialog -from .falloffDialog import FalloffDialog -from .interface import DCC -from .interfaceModel import ( - ComboFilterModel, - ComboModel, - SimplexModel, - SliderFilterModel, - SliderModel, - coerceIndexToChildType, - coerceIndexToParentType, - coerceIndexToRoots, -) -from .interfaceModelTrees import ComboTree, SliderTree -from .items import Combo, Group, ProgPair, Simplex, Slider, Stack -from .menu import buildToolMenu, loadPlugins - -# This module imports QT from PyQt4, PySide or PySide2 -# Depending on what's available -from Qt import QtCompat -from Qt.QtCore import Qt, Signal -from Qt.QtGui import QStandardItemModel, QAction -from Qt.QtWidgets import ( - QApplication, - QInputDialog, - QMessageBox, - QProgressDialog, - QCheckBox, - QComboBox, - QFrame, - QGroupBox, - QLabel, - QLineEdit, - QPushButton, - QRadioButton, - QVBoxLayout, - QWidget, -) - - -from .traversalDialog import TraversalDialog -from .utils import getNextName, getUiFile, makeUnique, naturalSortKey, Prefs - -if os.environ.get("SIMPLEX_AT_BLUR") == "true": - # If we're at blur, use our main window subclass - from blurdev.gui import Window -else: - from Qt.QtWidgets import QMainWindow as Window - -NAME_CHECK = re.compile(r"[A-Za-z][\w.]*") - -# If the decorated method is a slot for some Qt Signal -# and the method signature is *NOT* the same as the -# signal signature, you must double decorate the method like: -# -# @Slot(**signature) -# @stackable -# def method(**signature) - - -@contextmanager -def signalsBlocked(item): - """Context manager to block the qt signals on an item""" - item.blockSignals(True) - try: - yield - finally: - item.blockSignals(False) - - -class SimplexDialog(Window): - """The main ui for simplex - - Parameters - ---------- - parent : QWidget - The parent for this window - dispatch : Dispatch - An object that translates DCC events into - Qt Signals that the Simplex Ui can understand - - """ - uiConvertCorrectiveACT: QAction - uiDoubleSliderRangeACT: QAction - uiExportACT: QAction - uiExportObjPSDACT: QAction - uiExtractOnCreateACT: QAction - uiExtractPosedACT: QAction - uiHideRedundantACT: QAction - uiImportACT: QAction - uiImportObjPSDACT: QAction - uiLegacyJsonACT: QAction - uiLiveShapeConnectionACT: QAction - uiLiveUpdateACT: QAction - uiSetWorkingDirectoryACT: QAction - uiSplitShapePSDACT: QAction - - uiComboLAY: QVBoxLayout - uiSliderLAY: QVBoxLayout - uiAutoSetCombosCHK: QCheckBox - uiAutoSetSlidersCHK: QCheckBox - uiComboDependLockCHK: QCheckBox - uiSliderDependLockCHK: QCheckBox - uiCurrentSystemCBOX: QComboBox - uiComboButtonFRM: QFrame - uiSliderButtonFRM: QFrame - uiComboDependGRP: QGroupBox - uiComboShapesGRP: QGroupBox - uiMainShapesGRP: QGroupBox - uiObjectGRP: QGroupBox - uiSliderDependGRP: QGroupBox - uiSystemGRP: QGroupBox - uiCurrentObjectLBL: QLabel - uiCurrentSystemLBL: QLabel - uiComboFilterLINE: QLineEdit - uiCurrentObjectTXT: QLineEdit - uiSliderFilterLINE: QLineEdit - uiClearSelectedObjectBTN: QPushButton - uiComboExitIsolateBTN: QPushButton - uiComboFilterClearBTN: QPushButton - uiDeleteComboBTN: QPushButton - uiDeleteSystemBTN: QPushButton - uiGetSelectedObjectBTN: QPushButton - uiNewComboActiveBTN: QPushButton - uiNewComboGroupBTN: QPushButton - uiNewComboSelectBTN: QPushButton - uiNewComboShapeBTN: QPushButton - uiNewGroupBTN: QPushButton - uiNewShapeBTN: QPushButton - uiNewSliderBTN: QPushButton - uiNewSystemBTN: QPushButton - uiRenameSystemBTN: QPushButton - uiSelectCtrlBTN: QPushButton - uiSelectSlidersBTN: QPushButton - uiSetSliderValsBTN: QPushButton - uiShapeConnectBTN: QPushButton - uiShapeConnectSceneBTN: QPushButton - uiShapeExtractBTN: QPushButton - uiSliderDeleteBTN: QPushButton - uiSliderExitIsolateBTN: QPushButton - uiSliderFilterClearBTN: QPushButton - uiZeroAllBTN: QPushButton - uiZeroSelectedBTN: QPushButton - uiComboDependAllRDO: QRadioButton - uiComboDependAnyRDO: QRadioButton - uiComboDependOnlyRDO: QRadioButton - uiSliderDependAllRDO: QRadioButton - uiSliderDependAnyRDO: QRadioButton - uiConnectionGroupWID: QWidget - - simplexLoaded = Signal() - openedDialogs = [] - - def __init__(self, parent=None, dispatch=None): - super(SimplexDialog, self).__init__(parent) - - uiPath = getUiFile(__file__) - QtCompat.loadUi(uiPath, self) - - # Custom widgets aren't working properly, so I bring them in manually - self.uiSliderTREE = SliderTree(self.uiMainShapesGRP) - self.uiSliderTREE.setDragEnabled(False) - self.uiSliderTREE.setDragDropMode(SliderTree.DragDropMode.NoDragDrop) - self.uiSliderTREE.setSelectionMode(SliderTree.SelectionMode.ExtendedSelection) - self.uiSliderTREE.dragFilter.dragPressed.connect(self.dragStart) - self.uiSliderTREE.dragFilter.dragReleased.connect(self.dragStop) - - self.uiSliderLAY.addWidget(self.uiSliderTREE) - - self.uiComboTREE = ComboTree(self.uiComboShapesGRP) - self.uiComboTREE.setDragEnabled(False) - self.uiComboTREE.setDragDropMode(ComboTree.DragDropMode.NoDragDrop) - self.uiComboTREE.setSelectionMode(ComboTree.SelectionMode.ExtendedSelection) - self.uiComboTREE.dragFilter.dragPressed.connect(self.dragStart) - self.uiComboTREE.dragFilter.dragReleased.connect(self.dragStop) - self.uiComboLAY.addWidget(self.uiComboTREE) - - self._sliderMenu = None - self._comboMenu = None - self._currentObject = None - self._currentObjectName = None - - # Connect the combo boxes and spinners to the data model - # TODO: Figure out how to deal with the type/axis "enum" cboxes - # see: http://doc.qt.io/qt-5/qtwidgets-itemviews-combowidgetmapper-example.html - """ - self._falloffMapper = QDataWidgetMapper() - self.uiShapeFalloffCBOX.currentIndexChanged.connect(self._falloffMapper.setCurrentIndex) - """ - - # Make sure to connect the dispatcher to the undo control - # but only keep a weakref to it - self.dispatch = None - if dispatch is not None: - self.dispatch = weakref.ref(dispatch) - dispatch.undo.connect(self.handleUndo) - dispatch.redo.connect(self.handleUndo) - dispatch.beforeNew.connect(self.newScene) - dispatch.beforeOpen.connect(self.newScene) - - self.simplex = None - - self._itemMap = {} - self._sliderTreeMap = {} - self._comboTreeMap = {} - - self._sliderDrag = None - self._comboDrag = None - - self.uiSliderExitIsolateBTN.hide() - self.uiComboExitIsolateBTN.hide() - - self._makeConnections() - - self._toolPlugins, self._contextPlugins = loadPlugins() - buildToolMenu(self, self._toolPlugins) - self.uiSliderTREE.setPlugins(self._contextPlugins) - self.uiComboTREE.setPlugins(self._contextPlugins) - - if DCC.program == "dummy": - # self.getSelectedObject() - self.uiObjectGRP.setEnabled(False) - self.uiSystemGRP.setEnabled(False) - - self.uiClearSelectedObjectBTN.hide() - self.uiMainShapesGRP.setEnabled(False) - self.uiComboShapesGRP.setEnabled(False) - self.uiConnectionGroupWID.setEnabled(False) - self.loadSettings() - self._sliderMul = 2.0 if self.uiDoubleSliderRangeACT.isChecked() else 1.0 - - self.travDialog = TraversalDialog(self) - self.falloffDialog = FalloffDialog(self) - # self.showTraversalDialog() - type(self).openedDialogs.append(weakref.ref(self)) - - @classmethod - def lastOpenedDialog(cls): - """Returns the last currently opened dialog that still exists""" - for dlgRef in reversed(cls.openedDialogs): - dlg = dlgRef() - if dlg is not None: - return dlg - return None - - def showTraversalDialog(self): - """Display the traversal dialog""" - self.travDialog.show() - self.travDialog.setGeometry(30, 30, 400, 400) - - def showFalloffDialog(self): - """Display the Falloff Dialog""" - self.falloffDialog.show() - pp = self.falloffDialog.pos() - x, y = pp.x(), pp.y() - if x < 0 or y < 0: - self.falloffDialog.move(max(x, 0), max(y, 0)) - - def dragStart(self): - """Slot for handling the start of a MMB Drag event""" - if self.simplex is not None: - self.simplex.DCC.undoOpen() - - def dragStop(self): - """Slot for handling the end of a MMB Drag event""" - if self.simplex is not None: - self.simplex.DCC.undoClose() - - def storeSettings(self): - """Store the state of the UI for the next run""" - pref = Prefs() - pref.recordProperty("geometry", self.saveGeometry()) - pref.save() - - def loadSettings(self): - """Load the state of the UI from a previous run""" - pref = Prefs() - geo = pref.restoreProperty("geometry", None) - if geo is not None: - self.restoreGeometry(geo) - - def closeEvent(self, event): - """Handle the close event""" - self.storeSettings() - self.deleteLater() - - # Undo/Redo - def newScene(self): - """Call this before a new scene is created. Usually called from the stack""" - self.clearSelectedObject() - - def handleUndo(self): - """Call this after an undo/redo action. Usually called from the stack""" - if self.simplex is None: - return - - rev = self.simplex.DCC.getRevision() - data = self.simplex.stack.getRevision(rev) - if data is not None: - self.setSystem(data) - self.uiSliderTREE.setItemExpansion() - self.uiComboTREE.setItemExpansion() - - def currentSystemChanged(self, idx): - """Slot called when the current system changes""" - if idx == -1: - self.setSystem(None) - return - name = str(self.uiCurrentSystemCBOX.currentText()) - if not name: - self.setSystem(None) - return - if self.simplex is not None: - if self.simplex.name == name: - return # Do nothing - - pBar = QProgressDialog("Loading from Mesh", "Cancel", 0, 100, self) - system = Simplex.buildSystemFromMesh( - self._currentObject, name, sliderMul=self._sliderMul, pBar=pBar - ) - self.setSystem(system) - pBar.close() - - def setSystem(self, system): - """Set the system on this UI - - Parameters - ---------- - system : Simplex - The Simplex system to load into this UI - """ - if system == self.simplex: - return - - if self.simplex is not None: - # disconnect the previous stuff - sliderSelModel = self.uiSliderTREE.selectionModel() - sliderSelModel.selectionChanged.disconnect(self.unifySliderSelection) - sliderSelModel.selectionChanged.disconnect(self.populateComboRequirements) - sliderSelModel.selectionChanged.disconnect(self.autoSetSliders) - - comboSelModel = self.uiComboTREE.selectionModel() - comboSelModel.selectionChanged.disconnect(self.unifyComboSelection) - comboSelModel.selectionChanged.disconnect(self.populateSliderRequirements) - comboSelModel.selectionChanged.disconnect(self.autoSetComboSliders) - - oldStack = self.simplex.stack - else: - oldStack = Stack() - - if system is None: - # self.toolActions.simplex = None - self.uiSliderTREE.setModel(QStandardItemModel()) - self.uiComboTREE.setModel(QStandardItemModel()) - self.simplex = system - self.uiMainShapesGRP.setEnabled(False) - self.uiComboShapesGRP.setEnabled(False) - self.uiConnectionGroupWID.setEnabled(False) - self.falloffDialog.loadSimplex() - self.simplexLoaded.emit() - return - - # set and connect the new stuff - self.simplex = system - self.simplex.models = [] - self.simplex.falloffModels = [] - self.simplex.stack = oldStack - - # self.toolActions.simplex = self.simplex - - simplexModel = SimplexModel(self.simplex, None) - - sliderModel = SliderModel(simplexModel, None) - sliderProxModel = SliderFilterModel(sliderModel) - self.uiSliderTREE.setModel(sliderProxModel) - sliderSelModel = self.uiSliderTREE.selectionModel() - sliderSelModel.selectionChanged.connect(self.unifySliderSelection) - sliderSelModel.selectionChanged.connect(self.populateComboRequirements) - sliderSelModel.selectionChanged.connect(self.autoSetSliders) - - comboModel = ComboModel(simplexModel, None) - comboProxModel = ComboFilterModel(comboModel) - self.uiComboTREE.setModel(comboProxModel) - comboSelModel = self.uiComboTREE.selectionModel() - comboSelModel.selectionChanged.connect(self.unifyComboSelection) - comboSelModel.selectionChanged.connect(self.populateSliderRequirements) - comboSelModel.selectionChanged.connect(self.autoSetComboSliders) - - self.falloffDialog.loadSimplex() - - # Make sure the UI is up and running - self.enableComboRequirements() - self.enableSliderRequirements() - self.uiMainShapesGRP.setEnabled(True) - self.uiComboShapesGRP.setEnabled(True) - self.uiConnectionGroupWID.setEnabled(True) - - self.setSimplexLegacy() - self.simplexLoaded.emit() - - # UI Setup - def _makeConnections(self): - """Make all the ui connections""" - # Setup Trees! - self.uiSliderTREE.setColumnWidth(1, 50) - self.uiSliderTREE.setColumnWidth(2, 20) - self.uiSliderFilterLINE.textChanged.connect(self.sliderStringFilter) - self.uiSliderFilterClearBTN.clicked.connect(self.uiSliderFilterLINE.clear) - self.uiSliderFilterClearBTN.clicked.connect(self.sliderStringFilter) - - self.uiComboTREE.setColumnWidth(1, 50) - self.uiComboTREE.setColumnWidth(2, 20) - self.uiComboFilterLINE.textChanged.connect(self.comboStringFilter) - self.uiComboFilterClearBTN.clicked.connect(self.uiComboFilterLINE.clear) - self.uiComboFilterClearBTN.clicked.connect(self.comboStringFilter) - - # combo dependency filter setup - self.uiComboDependGRP.toggled.connect(self.enableComboRequirements) - self.uiComboDependAllRDO.toggled.connect(self.enableComboRequirements) - self.uiComboDependAnyRDO.toggled.connect(self.enableComboRequirements) - self.uiComboDependOnlyRDO.toggled.connect(self.enableComboRequirements) - self.uiComboDependLockCHK.toggled.connect(self.setLockComboRequirement) - - # slider dependency filter setup - self.uiSliderDependGRP.toggled.connect(self.enableSliderRequirements) - self.uiSliderDependAllRDO.toggled.connect(self.enableSliderRequirements) - self.uiSliderDependAnyRDO.toggled.connect(self.enableSliderRequirements) - self.uiSliderDependLockCHK.toggled.connect(self.setLockSliderRequirements) - - # Bottom Left Corner Buttons - self.uiZeroAllBTN.clicked.connect(self.zeroAllSliders) - self.uiZeroSelectedBTN.clicked.connect(self.zeroSelectedSliders) - self.uiSelectCtrlBTN.clicked.connect(self.selectCtrl) - - # Top Left Corner Buttons - self.uiNewGroupBTN.clicked.connect(self.newSliderGroup) - self.uiNewSliderBTN.clicked.connect(self.newSlider) - self.uiNewShapeBTN.clicked.connect(self.newSliderShape) - self.uiSliderDeleteBTN.clicked.connect(self.sliderTreeDelete) - - # Top Right Corner Buttons - self.uiDeleteComboBTN.clicked.connect(self.comboTreeDelete) - self.uiNewComboActiveBTN.clicked.connect(self.newActiveCombo) - self.uiNewComboSelectBTN.clicked.connect(self.newSelectedCombo) - self.uiNewComboShapeBTN.clicked.connect(self.newComboShape) - self.uiNewComboGroupBTN.clicked.connect(self.newComboGroup) - - # Bottom right corner buttons - self.uiSetSliderValsBTN.clicked.connect(self.setSliderVals) - self.uiSelectSlidersBTN.clicked.connect(self.selectSliders) - - # System level - self.uiCurrentObjectTXT.editingFinished.connect(self.currentObjectChanged) - self.uiGetSelectedObjectBTN.clicked.connect(self.getSelectedObject) - self.uiClearSelectedObjectBTN.clicked.connect(self.clearSelectedObject) - - self.uiNewSystemBTN.clicked.connect(self.newSystem) - self.uiRenameSystemBTN.clicked.connect(self.renameSystem) - self.uiCurrentSystemCBOX.currentIndexChanged[int].connect( - self.currentSystemChanged - ) - - # Extraction/connection - self.uiShapeExtractBTN.clicked.connect(self.shapeExtract) - self.uiShapeConnectBTN.clicked.connect(self.shapeConnect) - self.uiShapeConnectSceneBTN.clicked.connect(self.shapeConnectScene) - - # File Menu - self.uiImportACT.triggered.connect(self.importSystemFromFile) - self.uiExportACT.triggered.connect(self.exportSystemTemplate) - - # Edit Menu - self.uiHideRedundantACT.toggled.connect(self.hideRedundant) - self.uiDoubleSliderRangeACT.toggled.connect(self.setSliderRange) - - # Isolation - self.uiSliderExitIsolateBTN.clicked.connect(self.sliderTreeExitIsolate) - self.uiComboExitIsolateBTN.clicked.connect(self.comboTreeExitIsolate) - - self.uiLegacyJsonACT.toggled.connect(self.setSimplexLegacy) - - # Helpers - def getSelectedItems(self, tree, typ=None): - """Convenience function to get the selected system items - - Parameters - ---------- - tree : QTreeView - The tree to get selected items from - typ : Type - The type of objects to return - - Returns - ------- - [object, ...] - A list of selected items - """ - sel = tree.selectedIndexes() - sel = [i for i in sel if i.column() == 0] - model = tree.model() - items = [model.itemFromIndex(i) for i in sel] - if typ is not None: - items = [i for i in items if isinstance(i, typ)] - return items - - def getCurrentObject(self): - """Convenience function to get the current object loaded into the UI""" - return self._currentObject - - # Setup Trees! - def sliderStringFilter(self): - """Set the filter for the slider tree""" - filterString = str(self.uiSliderFilterLINE.text()) - sliderModel = self.uiSliderTREE.model() - sliderModel.filterString = str(filterString) - sliderModel.invalidateFilter() - - def comboStringFilter(self): - """Set the filter for the combo tree""" - filterString = str(self.uiComboFilterLINE.text()) - comboModel = self.uiComboTREE.model() - comboModel.filterString = str(filterString) - comboModel.invalidateFilter() - - # selection setup - def unifySliderSelection(self): - """Clear the selection of the combo tree when - an item on the slider tree is selected - """ - mods = QApplication.keyboardModifiers() - if not mods & ( - Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier - ): - comboSelModel = self.uiComboTREE.selectionModel() - if not comboSelModel: - return - - with signalsBlocked(comboSelModel): - comboSelModel.clearSelection() - self.uiComboTREE.viewport().update() - - def unifyComboSelection(self): - """Clear the selection of the slider tree when - an item on the combo tree is selected - """ - mods = QApplication.keyboardModifiers() - if not mods & ( - Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier - ): - sliderSelModel = self.uiSliderTREE.selectionModel() - if not sliderSelModel: - return - with signalsBlocked(sliderSelModel): - sliderSelModel.clearSelection() - self.uiSliderTREE.viewport().update() - - # dependency setup - def setLockComboRequirement(self): - """Refresh the combo selection filter with the new filterLock state""" - comboModel = self.uiComboTREE.model() - if not comboModel: - return - self.populateComboRequirements() - - def populateComboRequirements(self): - """Let the combo tree know the requirements from the slider tree""" - items = self.uiSliderTREE.getSelectedItems(Slider) - comboModel = self.uiComboTREE.model() - locked = self.uiComboDependLockCHK.isChecked() - if not locked: - comboModel.requires = items - if ( - comboModel.filterRequiresAll - or comboModel.filterRequiresAny - or comboModel.filterRequiresOnly - ): - comboModel.invalidateFilter() - - def enableComboRequirements(self): - """Set the requirements for the combo filter model""" - comboModel = self.uiComboTREE.model() - if not comboModel: - return - depCheck = self.uiComboDependGRP.isChecked() - comboModel.filterRequiresAll = self.uiComboDependAllRDO.isChecked() and depCheck - comboModel.filterRequiresAny = self.uiComboDependAnyRDO.isChecked() and depCheck - comboModel.filterRequiresOnly = ( - self.uiComboDependOnlyRDO.isChecked() and depCheck - ) - comboModel.invalidateFilter() - - def populateSliderRequirements(self): - """Let the slider tree know the requirements from the combo tree""" - items = self.uiComboTREE.getSelectedItems(Combo) - sliderModel = self.uiSliderTREE.model() - locked = self.uiSliderDependLockCHK.isChecked() - if not locked: - sliderModel.requires = items - if sliderModel.filterRequiresAny or sliderModel.filterRequiresAll: - sliderModel.invalidateFilter() - - def setLockSliderRequirements(self): - """Refresh the combo selection filter with the new filterLock state""" - sliderModel = self.uiSliderTREE.model() - if not sliderModel: - return - self.populateSliderRequirements() - - def enableSliderRequirements(self): - sliderModel = self.uiSliderTREE.model() - if not sliderModel: - return - - depCheck = self.uiSliderDependGRP.isChecked() - sliderModel.filterRequiresAny = ( - self.uiSliderDependAnyRDO.isChecked() and depCheck - ) - sliderModel.filterRequiresAll = ( - self.uiSliderDependAllRDO.isChecked() and depCheck - ) - sliderModel.invalidateFilter() - - # Bottom Left Corner Buttons - def zeroAllSliders(self): - """Slot to Zero all Sliders in Slider UI panel""" - if self.simplex is None: - return - sliders = self.simplex.sliders - weights = [0.0] * len(sliders) - self.simplex.setSlidersWeights(sliders, weights) - self.uiSliderTREE.repaint() - - def zeroSelectedSliders(self): - """Slot to Zero the selected sliders in the UI panel""" - if self.simplex is None: - return - items = self.uiSliderTREE.getSelectedItems(Slider) - values = [0.0] * len(items) - self.simplex.setSlidersWeights(items, values) - self.uiSliderTREE.repaint() - - def selectCtrl(self): - """Select the Control object in the DCC""" - if self.simplex is None: - return - self.simplex.DCC.selectCtrl() - - def autoSetSliders(self): - """Automatically set any selected sliders to 1.0""" - if self.simplex is None: - return - if not self.uiAutoSetSlidersCHK.isChecked(): - return - sel = set(self.uiSliderTREE.getSelectedItems(Slider)) - sliders = self.simplex.sliders - - weights = [0.0] * len(sliders) - for i, slider in enumerate(sliders): - if slider in sel: - weights[i] = 1.0 - self.simplex.setSlidersWeights(sliders, weights) - self.uiSliderTREE.repaint() - - def _getAName(self, tpe, default=None, taken=(), uniqueAccept=False): - """uniqueAccept forces the user to provide and accept a unique name - If the user enters a non-unique name, the name is uniquified, and the - dialog is re-shown with the unique name as the default suggestion - """ - tpe = tpe.lower() - uTpe = tpe[0].upper() + tpe[1:] - - unique = False - default = getNextName(default, taken) - while not unique: - eMsg = "Enter a name for the new {0}".format(tpe) - newName, good = QInputDialog.getText( - self, "New {0}".format(uTpe), eMsg, text=default - ) - if not good: - return None - if len(newName) < 3: - message = "Please use names longer than 2 letters" - QMessageBox.warning(self, "Warning", message) - return None - if not NAME_CHECK.match(newName): - message = "{0} name can only contain letters and numbers, and cannot start with a number" - message = message.format(uTpe) - QMessageBox.warning(self, "Warning", message) - return None - - unqName = getNextName(newName, taken) - unique = (unqName == newName) or uniqueAccept - default = unqName - - return default - - # Top Left Corner Buttons - def newSliderGroup(self): - """Slot to Create a new slider group""" - if self.simplex is None: - return - - newName = self._getAName("group", default="Group") - if newName is None: - return - - Group.createGroup(str(newName), self.simplex, groupType=Slider) - # self.uiSliderTREE.model().invalidateFilter() - # self.uiComboTREE.model().invalidateFilter() - - def newSlider(self): - """Slot to create a new slider""" - if self.simplex is None: - return - - newName = self._getAName("slider") - if newName is None: - return - - idxs = self.uiSliderTREE.getSelectedIndexes() - groups = coerceIndexToParentType(idxs, Group) - group = groups[0].model().itemFromIndex(groups[0]) if groups else None - - Slider.createSlider(str(newName), self.simplex, group=group) - - def newSliderShape(self): - """Slot to create a new Shape in a Slider's progression""" - pars = self.uiSliderTREE.getSelectedItems(Slider) - if not pars: - return - parItem = pars[0] - parItem.createShape() - self.uiSliderTREE.model().invalidateFilter() - - def sliderTreeDelete(self): - """Delete some objects in the slider tree""" - idxs = self.uiSliderTREE.getSelectedIndexes() - roots = coerceIndexToRoots(idxs) - if not roots: - QMessageBox.warning(self, "Warning", "Nothing Selected in Slider tree") - return - roots = makeUnique([i.model().itemFromIndex(i) for i in roots]) - for r in roots: - if isinstance(r, Simplex): - QMessageBox.warning( - self, "Warning", "Cannot delete a simplex system this way" - ) - return - - for r in roots: - r.delete() - self.uiSliderTREE.model().invalidateFilter() - - # Top Right Corner Buttons - def comboTreeDelete(self): - """Delete some objects in the Combo tree""" - idxs = self.uiComboTREE.getSelectedIndexes() - roots = coerceIndexToRoots(idxs) - if not roots: - QMessageBox.warning(self, "Warning", "Nothing Selected in Combo tree") - return - roots = makeUnique([i.model().itemFromIndex(i) for i in roots]) - for r in roots: - if isinstance(r, Simplex): - QMessageBox.warning( - self, "Warning", "Cannot delete a simplex system this way" - ) - return - - for r in roots: - r.delete() - self.uiComboTREE.model().invalidateFilter() - - def _newCombo(self, sliders, values): - if len(sliders) < 2: - message = "A combo must use at least 2 sliders" - QMessageBox.warning(self, "Warning", message) - return - - ccd = ComboCheckDialog(sliders, values=values, mode="create", parent=self) - ccd.move(self.pos()) - ccd.exec_() - - def newActiveCombo(self): - """Create a combo based on the UI Sliders that are currently nonzero""" - if self.simplex is None: - return - sliders = [] - values = {} - for s in self.simplex.sliders: - if s.value != 0.0: - sliders.append(s) - values[s] = [s.value] - self._newCombo(sliders, values) - - def newSelectedCombo(self): - """Create a combo based on the currently selected UI sliders""" - if self.simplex is None: - return - sliders = self.uiSliderTREE.getSelectedItems(Slider) - values = {s: [1.0] for s in sliders} - self._newCombo(sliders, values) - - def newComboShape(self): - """Create a new shape in the Combo's progression""" - parIdxs = self.uiComboTREE.getSelectedIndexes() - pars = coerceIndexToParentType(parIdxs, Combo) - if not pars: - return - - parItem = pars[0].model().itemFromIndex(pars[0]) if pars else None - parItem.createShape() - self.uiComboTREE.model().invalidateFilter() - - def newComboGroup(self): - """Create a new group for organizing combos""" - if self.simplex is None: - return - - newName = self._getAName("group", default="Group") - if newName is None: - return - - Group.createGroup(str(newName), self.simplex, groupType=Combo) - # self.uiComboTREE.model().invalidateFilter() - # self.uiSliderTREE.model().invalidateFilter() - - # Bottom right corner buttons - def setSliderVals(self): - """Set all slider values to those stored in the currently selected Combos""" - if self.simplex is None: - return - combos = self.uiComboTREE.getSelectedItems(Combo) - # maybe coerce to type instead?? - self.zeroAllSliders() - values = [] - sliders = [] - for combo in combos: - for pair in combo.pairs: - if pair.slider in sliders: - continue - sliders.append(pair.slider) - values.append(pair.value) - self.simplex.setSlidersWeights(sliders, values) - self.uiSliderTREE.repaint() - - def selectSliders(self): - """Select the sliders that are contained in the currently selected Combos""" - combos = self.uiComboTREE.getSelectedItems(Combo) - sliders = [] - for combo in combos: - for pair in combo.pairs: - sliders.append(pair.slider) - self.uiSliderTREE.setItemSelection(sliders) - - def autoSetComboSliders(self): - """Automatically set the DCC Slider values to activate the currently selected Combos""" - if self.simplex is None: - return - if not self.uiAutoSetCombosCHK.isChecked(): - return - sel = set(self.uiComboTREE.getSelectedItems(Combo)) - sv = {} - for combo in self.simplex.combos: - isSel = combo in sel - for pair in combo.pairs: - curVal = sv.get(pair.slider, 0.0) - newVal = pair.value if isSel else 0.0 - if abs(newVal) >= abs(curVal): - sv[pair.slider] = newVal - - sliders, weights = list(zip(*list(sv.items()))) - self.simplex.setSlidersWeights(sliders, weights) - self.uiSliderTREE.repaint() - - # Extraction/connection - def shapeConnectScene(self): - """Connect any selected meshes into the system based on the name""" - if self.simplex is None: - return - # make a dict of name:object - sel = DCC.getSelectedObjects() - selDict = {} - for s in sel: - name = DCC.getObjectName(s) - if name.endswith("_Extract"): - nn = name.rsplit("_Extract", 1)[0] - selDict[nn] = s - - pairDict = {} - for p in self.simplex.progs: - for pp in p.pairs: - pairDict[pp.shape.name] = pp - - # get all common names - common = selDict.keys() & pairDict.keys() - - # get those items - pairs = [pairDict[i] for i in common] - - # Set up the progress bar - pBar = QProgressDialog("Connecting Shapes", "Cancel", 0, 100, self) - pBar.setMaximum(len(pairs)) - - # Do the extractions - for pair in pairs: - c = pair.prog.controller - c.connectShape(pair.shape, delete=True) - - # ProgressBar - pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Connecting:\n{0}".format(pair.shape.name)) - QApplication.processEvents() - if pBar.wasCanceled(): - return - - pBar.close() - - def sliderShapeExtract(self): - """Create meshes that are possibly live-connected to the shapes""" - sliderIdxs = self.uiSliderTREE.getSelectedIndexes() - return self.shapeIndexExtract(sliderIdxs) - - def comboShapeExtract(self): - """Create meshes that are possibly live-connected to the shapes""" - comboIdxs = self.uiComboTREE.getSelectedIndexes() - return self.shapeIndexExtract(comboIdxs) - - def shapeExtract(self): - """Create meshes that are possibly live-connected to the shapes""" - sliderIdxs = self.uiSliderTREE.getSelectedIndexes() - comboIdxs = self.uiComboTREE.getSelectedIndexes() - return self.shapeIndexExtract(sliderIdxs + comboIdxs) - - def shapeExport(self): - """Export meshes""" - sliderIdxs = self.uiSliderTREE.getSelectedIndexes() - comboIdxs = self.uiComboTREE.getSelectedIndexes() - return self.shapeIndexExport(sliderIdxs + comboIdxs) - - def shapeIndexExport(self, indexes): - """Export meshes - - Parameters - ---------- - indexes : [QModelIndex, ...] - A list of indexes to extract - """ - # Importing this here to keep people from getting confused - # You should use self._fileDialog which does compatibility - # stuff for file saving. Since I'm looking for a folder here - # I don't need to do that - from Qt.QtWidgets import QFileDialog - - if self.simplex is None: - return - - pref = Prefs() - defaultPath = str( - pref.restoreProperty( - "systemExportFolder", os.path.join(os.path.expanduser("~")) - ) - ) - outfld = QFileDialog.getExistingDirectory( - self, "Pick Export Folder", defaultPath - ) - if not outfld: - return - - pref.recordProperty("systemExportFolder", outfld) - pref.save() - - pairs = coerceIndexToChildType(indexes, ProgPair) - pairs = [i.model().itemFromIndex(i) for i in pairs] - pairs = makeUnique([i for i in pairs if not i.shape.isRest]) - pairs.sort(key=lambda x: naturalSortKey(x.shape.name)) - - # Set up the progress bar - pBar = QProgressDialog("Exporting Shapes", "Cancel", 0, 100, self) - pBar.setMaximum(len(pairs)) - - extension = '.abc' - # Do the extractions - for pair in pairs: - c = pair.prog.controller - extracted = c.extractShape(pair.shape, live=False) - path = os.path.join(outfld, c.name + extension) - if os.path.exists(path): - os.remove(path) - - self.simplex.DCC.exportMesh(extracted, path) - self.simplex.DCC.deleteObj(extracted) - - # ProgressBar - pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Extracting:\n{0}".format(pair.shape.name)) - QApplication.processEvents() - if pBar.wasCanceled(): - return - - pBar.close() - - def shapeIndexExtract(self, indexes, live=None): - """Create meshes that are possibly live-connected to the shapes - - Parameters - ---------- - indexes : [QModelIndex, ...] - A list of indexes to extract - live : bool or None - Whether the connection is live. If None, check the UI properties - """ - if live is None: - live = self.uiLiveShapeConnectionACT.isChecked() - - pairs = coerceIndexToChildType(indexes, ProgPair) - pairs = [i.model().itemFromIndex(i) for i in pairs] - pairs = makeUnique([i for i in pairs if not i.shape.isRest]) - pairs.sort(key=lambda x: naturalSortKey(x.shape.name)) - - # Set up the progress bar - pBar = QProgressDialog("Extracting Shapes", "Cancel", 0, 100, self) - pBar.setMaximum(len(pairs)) - - # Do the extractions - offset = 10 - extracted = [] - for pair in pairs: - c = pair.prog.controller - ext = c.extractShape(pair.shape, live=live, offset=offset) - extracted.append(ext) - offset += 5 - - # ProgressBar - pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Extracting:\n{0}".format(pair.shape.name)) - QApplication.processEvents() - if pBar.wasCanceled(): - return extracted - - pBar.close() - return extracted - - def shapeConnect(self): - """Match any selected Shapes to DCC meshes based on their names, then delete the Meshes""" - sliderIdxs = self.uiSliderTREE.getSelectedIndexes() - comboIdxs = self.uiComboTREE.getSelectedIndexes() - self.shapeConnectIndexes(sliderIdxs + comboIdxs) - - def shapeConnectIndexes(self, indexes): - """Match the provided shapes to DCC meshes based on their names, then delete the Meshes - - Parameters - ---------- - indexes : list of QModelIndex - A list of selected model indexes - """ - pairs = coerceIndexToChildType(indexes, ProgPair) - pairs = [i.model().itemFromIndex(i) for i in pairs] - pairs = makeUnique([i for i in pairs if not i.shape.isRest]) - - # Set up the progress bar - pBar = QProgressDialog("Connecting Shapes", "Cancel", 0, 100, self) - pBar.setMaximum(len(pairs)) - - # Do the extractions - for pair in pairs: - c = pair.prog.controller - c.connectShape(pair.shape, delete=True) - - # ProgressBar - pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Extracting:\n{0}".format(pair.shape.name)) - QApplication.processEvents() - if pBar.wasCanceled(): - return - - pBar.close() - - def shapeMatch(self): - """Match any selected Shapes to A selected DCC mesh""" - sliderIdxs = self.uiSliderTREE.getSelectedIndexes() - comboIdxs = self.uiComboTREE.getSelectedIndexes() - self.shapeMatchIndexes(sliderIdxs + comboIdxs) - - def shapeMatchIndexes(self, indexes): - """Match any provided shapes to A selected DCC mesh - - Parameters - ---------- - indexes : list of QModelIndex - A list of selected model indexes - """ - # make a dict of name:object - sel = DCC.getSelectedObjects() - if not sel: - return - mesh = sel[0] - - pairs = coerceIndexToChildType(indexes, ProgPair) - pairs = [i.model().itemFromIndex(i) for i in pairs] - pairs = makeUnique([i for i in pairs if not i.shape.isRest]) - - # Set up the progress bar - pBar = QProgressDialog("Matching Shapes", "Cancel", 0, 100, self) - pBar.setMaximum(len(pairs)) - - # Do the extractions - for pair in pairs: - c = pair.prog.controller - c.connectShape(pair.shape, mesh=mesh) - - # ProgressBar - pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Matching:\n{0}".format(pair.shape.name)) - QApplication.processEvents() - if pBar.wasCanceled(): - return - - pBar.close() - - def shapeClear(self): - """Match all selected shapes to the rest""" - sliderIdxs = self.uiSliderTREE.getSelectedIndexes() - comboIdxs = self.uiComboTREE.getSelectedIndexes() - self.shapeClearIndexes(sliderIdxs + comboIdxs) - - def shapeClearIndexes(self, indexes): - """Match all provided shapes to the rest - - Parameters - ---------- - indexes : list of QModelIndex - A list of selected model indexes - """ - pairs = coerceIndexToChildType(indexes, ProgPair) - pairs = [i.model().itemFromIndex(i) for i in pairs] - pairs = makeUnique([i for i in pairs if not i.shape.isRest]) - - for pair in pairs: - pair.shape.zeroShape() - - # System level - def loadObject(self, thing): - """Load a DCC mesh into the UI - - Parameters - ---------- - thing : object - The DCC mesh to load into the UI - """ - if thing is None: - return - - self.uiClearSelectedObjectBTN.show() - self.uiCurrentSystemCBOX.clear() - objName = DCC.getObjectName(thing) - self._currentObject = thing - self._currentObjectName = objName - self.uiCurrentObjectTXT.setText(objName) - - ops = DCC.getSimplexOperatorsOnObject(self._currentObject) - - for op in ops: - js = DCC.getSimplexString(op) - if not js: - continue - d = json.loads(js) - name = d["systemName"] - self.uiCurrentSystemCBOX.addItem(name, (self._currentObject, name)) - - def currentObjectChanged(self): - """Slot called when the current DCC object is changed""" - name = str(self.uiCurrentObjectTXT.text()) - if self._currentObjectName == name: - return - if not name: - return - - newObject = DCC.getObjectByName(name) - if not newObject: - return - - self.loadObject(newObject) - - def getSelectedObject(self): - """Load the first selected DCC object into the UI""" - sel = DCC.getSelectedObjects() - if not sel: - return - newObj = sel[0] - if not newObj: - return - self.loadObject(newObj) - - def clearSelectedObject(self): - """Unload the current DCC object from the UI""" - self.uiClearSelectedObjectBTN.hide() - self.uiCurrentSystemCBOX.clear() - self._currentObject = None - self._currentObjectName = None - self.uiCurrentObjectTXT.setText("") - self.setSystem(None) - # Clear the current system - - def newSystem(self): - """Create a new system on the current DCC Object""" - if self._currentObject is None: - QMessageBox.warning(self, "Warning", "Must have a current object selection") - return - - newName = self._getAName("system") - if newName is None: - return - - newSystem = Simplex.buildEmptySystem( - self._currentObject, newName, sliderMul=self._sliderMul - ) - with signalsBlocked(self.uiCurrentSystemCBOX): - self.uiCurrentSystemCBOX.addItem(newName) - self.uiCurrentSystemCBOX.setCurrentIndex( - self.uiCurrentSystemCBOX.count() - 1 - ) - self.setSystem(newSystem) - - def renameSystem(self): - """Rename the current Simplex system""" - if self.simplex is None: - return - - sysNames = [ - str(self.uiCurrentSystemCBOX.itemText(i)) - for i in range(self.uiCurrentSystemCBOX.count()) - ] - newName = self._getAName("system", taken=sysNames) - if newName is None: - return - - self.simplex.name = newName - idx = self.uiCurrentSystemCBOX.currentIndex() - self.uiCurrentSystemCBOX.setItemText(idx, newName) - - self.currentSystemChanged(idx) - - def setSimplexLegacy(self): - """Slot to toggle the legacy behavior of the current Simplex system""" - if self.simplex is not None: - self.simplex.setLegacy(self.uiLegacyJsonACT.isChecked()) - self.simplex.DCC.incrementRevision() - - # File Menu - def importSystemFromFile(self): - """Open a File Dialog to load a simplex system from a file. - Systems can be in either .smpx, or .json formats - """ - if self._currentObject is None: - impTypes = ["smpx"] - else: - impTypes = ["smpx", "json"] - - pref = Prefs() - defaultPath = str( - pref.restoreProperty("systemImport", os.path.join(os.path.expanduser("~"))) - ) - path = self._fileDialog("Import Template", defaultPath, impTypes, save=False) - if not path: - return - pref.recordProperty("systemImport", os.path.dirname(path)) - pref.save() - - self.loadFile(path) - - def loadFile(self, path): - pBar = QProgressDialog("Loading Shapes", "Cancel", 0, 100, self) - pBar.show() - QApplication.processEvents() - - # TODO: Come up with a better list of possibilites for loading - # simplex files, and make the appropriate methods on the Simplex - if path.endswith(".smpx"): - newSystem = Simplex.buildSystemFromSmpx( - path, self._currentObject, sliderMul=self._sliderMul, pBar=pBar - ) - if newSystem is None: - QMessageBox.warning( - self, - "Point Count Mismatch", - "The .smpx file point count does not match the current object", - ) - pBar.close() - return - - elif path.endswith(".json"): - newSystem = Simplex.buildSystemFromJson( - path, self._currentObject, sliderMul=self._sliderMul, pBar=pBar - ) - else: - QMessageBox.warning(self, "Bad Filepath", "Path type not recognized") - return - - with signalsBlocked(self.uiCurrentSystemCBOX): - self.loadObject(newSystem.DCC.mesh) - idx = self.uiCurrentSystemCBOX.findText(self._currentObjectName) - if idx >= 0: - self.uiCurrentSystemCBOX.setCurrentIndex(idx) - else: - self.uiCurrentSystemCBOX.addItem(newSystem.name) - self.uiCurrentSystemCBOX.setCurrentIndex( - self.uiCurrentSystemCBOX.count() - 1 - ) - - self.setSystem(newSystem) - pBar.close() - self.simplex.DCC.checkForErrors(self) - - def _fileDialog(self, title, initPath, filters, save=True): - """Convenience function for displaying File Dialogs""" - filters = ["{0} (*.{0})".format(f) for f in filters] - if not save: - filters += ["All files (*.*)"] - filters = ";;".join(filters) - - if save: - path, _ = QtCompat.QFileDialog.getSaveFileName( - self, title, initPath, filters - ) - else: - path, _ = QtCompat.QFileDialog.getOpenFileName( - self, title, initPath, filters - ) - - if not path: - return "" - - if not save and not os.path.exists(path): - return "" - - return path - - def exportSystemTemplate(self): - """Open a file dialog and export a system to the chosen path""" - if self._currentObject is None: - QMessageBox.warning(self, "Warning", "Must have a current object selection") - return - - pref = Prefs() - defaultPath = str( - pref.restoreProperty("systemExport", os.path.join(os.path.expanduser("~"))) - ) - path = self._fileDialog( - "Export Template", defaultPath, ["smpx", "json"], save=True - ) - if not path: - return - pref.recordProperty("systemExport", os.path.dirname(path)) - pref.save() - - if self.simplex is None: - QMessageBox.warning(self, "Warning", "No simplex loaded") - return - - if path.endswith(".smpx"): - pBar = QProgressDialog("Exporting smpx File", "Cancel", 0, 100, self) - pBar.show() - self.simplex.exportAbc(path, pBar) - pBar.close() - elif path.endswith(".json"): - dump = self.simplex.dump() - with open(path, "w") as f: - f.write(dump) - - # Slider Settings - def setSelectedSliderGroups(self, group): - """Set the group for the selected Sliders - - Parameters - ---------- - group : Group - The group that will take the selected Sliders - """ - if not group: - return - sliders = self.uiSliderTREE.getSelectedItems(Slider) - group.take(sliders) - self.uiSliderTREE.viewport().update() - - def setSelectedSliderFalloff(self, falloff, state): - """Set the Falloffs for the selected Sliders - - Parameters - ---------- - falloff : Falloff - The falloff to set on the selected sliders - state : bool - Whether to add or remove this falloff from the selection - """ - if not falloff: - return - sliders = self.uiSliderTREE.getSelectedItems(Slider) - for s in sliders: - if state == Qt.CheckState.Checked: - s.prog.addFalloff(falloff) - else: - s.prog.removeFalloff(falloff) - self.uiSliderTREE.viewport().update() - - def setSelectedSliderInterp(self, interp): - """Set the interpolation for the selected sliders - - Parameters - ---------- - interp : str - The interpolation to set on the selection. - Could be 'linear', 'spline', or 'splitSpline' - """ - sliders = self.uiSliderTREE.getSelectedItems(Slider) - for s in sliders: - s.prog.interp = interp - - # Combo Settings - def setSelectedComboGroups(self, group): - """Set the group for the selected Combos - - Parameters - ---------- - group : Group - The group that will take the selected Combos - """ - if not group: - return - combos = self.uiComboTREE.getSelectedItems(Combo) - group.take(combos) - self.uiComboTREE.viewport().update() - - def setSelectedComboSolveType(self, stVal): - """Set the solve type for the selected combos - - Parameters - ---------- - stVal : str - The solve type to set for the combos. - See Combo.solveTypes for a list - """ - combos = self.uiComboTREE.getSelectedItems(Combo) - for c in combos: - c.solveType = stVal - - # Edit Menu - def hideRedundant(self): - """Hide redundant items from the Slider and Combo trees based on a user preference""" - check = self.uiHideRedundantACT.isChecked() - comboModel = self.uiComboTREE.model() - comboModel.filterShapes = check - comboModel.invalidateFilter() - sliderModel = self.uiSliderTREE.model() - sliderModel.doFilter = check - sliderModel.invalidateFilter() - - def setSliderRange(self): - """Double the range for the sliders *IN THE DCC ONLY* based on a user preference""" - self._sliderMul = 2.0 if self.uiDoubleSliderRangeACT.isChecked() else 1.0 - if self.simplex is None: - return - self.simplex.DCC.sliderMul = self._sliderMul - self.simplex.DCC.setSlidersRange(self.simplex.sliders) - - # Isolation - def isSliderIsolate(self): - """Check if the slider tree is currently isolated""" - model = self.uiSliderTREE.model() - if model: - return bool(model.isolateList) - return False - - def sliderIsolateSelected(self): - """Isolate the selected Sliders in the Slider Tree""" - self.uiSliderTREE.isolateSelected() - self.uiSliderExitIsolateBTN.show() - - def sliderTreeExitIsolate(self): - """Disable isolation mode in the Slider Tree""" - self.uiSliderTREE.exitIsolate() - self.uiSliderExitIsolateBTN.hide() - - def isComboIsolate(self): - """Check if the combo tree is currently isolated""" - model = self.uiComboTREE.model() - if model: - return bool(model.isolateList) - return False - - def comboIsolateSelected(self): - """Isolate the selected Combos in the Combo Tree""" - self.uiComboTREE.isolateSelected() - self.uiComboExitIsolateBTN.show() - - def comboTreeExitIsolate(self): - """Disable isolation mode in the Combo Tree""" - self.uiComboTREE.exitIsolate() - self.uiComboExitIsolateBTN.hide() - - -def _test(): - app = QApplication(sys.argv) - path = r"C:\Users\tfox\Documents\GitHub\Simplex\scripts\SimplexUI\build\HeadMaleStandard_High_Unsplit.smpx" - d = SimplexDialog() - newSystem = Simplex.buildSystemFromSmpx(path, d.getCurrentObject(), sliderMul=1.0) - d.setSystem(newSystem) - - d.show() - sys.exit(app.exec_()) - - -if __name__ == "__main__": - _test() +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + + +# Ignore a bunch of linter warnings that show up because of my choice of abstraction +# pylint: disable=unused-argument,too-many-public-methods,relative-import +# pylint: disable=too-many-statements,no-self-use,missing-docstring +import json +import os +import re +import sys +import weakref +from contextlib import contextmanager + +from .comboCheckDialog import ComboCheckDialog +from .falloffDialog import FalloffDialog +from .interface import DCC +from .interfaceModel import ( + ComboFilterModel, + ComboModel, + SimplexModel, + SliderFilterModel, + SliderModel, + coerceIndexToChildType, + coerceIndexToParentType, + coerceIndexToRoots, +) +from .interfaceModelTrees import ComboTree, SliderTree +from .items import Combo, Group, ProgPair, Simplex, Slider, Stack +from .menu import buildToolMenu, loadPlugins + +# This module imports QT from PyQt4, PySide or PySide2 +# Depending on what's available +from Qt import QtCompat +from Qt.QtCore import Qt, Signal +from Qt.QtGui import QStandardItemModel, QAction +from Qt.QtWidgets import ( + QApplication, + QInputDialog, + QMessageBox, + QProgressDialog, + QCheckBox, + QComboBox, + QFrame, + QGroupBox, + QLabel, + QLineEdit, + QPushButton, + QRadioButton, + QVBoxLayout, + QWidget, +) + + +from .traversalDialog import TraversalDialog +from .utils import getNextName, getUiFile, makeUnique, naturalSortKey, Prefs + +if os.environ.get("SIMPLEX_AT_BLUR") == "true": + # If we're at blur, use our main window subclass + from blurdev.gui import Window +else: + from Qt.QtWidgets import QMainWindow as Window + +NAME_CHECK = re.compile(r"[A-Za-z][\w.]*") + +# If the decorated method is a slot for some Qt Signal +# and the method signature is *NOT* the same as the +# signal signature, you must double decorate the method like: +# +# @Slot(**signature) +# @stackable +# def method(**signature) + + +@contextmanager +def signalsBlocked(item): + """Context manager to block the qt signals on an item""" + item.blockSignals(True) + try: + yield + finally: + item.blockSignals(False) + + +class SimplexDialog(Window): + """The main ui for simplex + + Parameters + ---------- + parent : QWidget + The parent for this window + dispatch : Dispatch + An object that translates DCC events into + Qt Signals that the Simplex Ui can understand + + """ + uiConvertCorrectiveACT: QAction + uiDoubleSliderRangeACT: QAction + uiExportACT: QAction + uiExportObjPSDACT: QAction + uiExtractOnCreateACT: QAction + uiExtractPosedACT: QAction + uiHideRedundantACT: QAction + uiImportACT: QAction + uiImportObjPSDACT: QAction + uiLegacyJsonACT: QAction + uiLiveShapeConnectionACT: QAction + uiLiveUpdateACT: QAction + uiSetWorkingDirectoryACT: QAction + uiSplitShapePSDACT: QAction + + uiComboLAY: QVBoxLayout + uiSliderLAY: QVBoxLayout + uiAutoSetCombosCHK: QCheckBox + uiAutoSetSlidersCHK: QCheckBox + uiComboDependLockCHK: QCheckBox + uiSliderDependLockCHK: QCheckBox + uiCurrentSystemCBOX: QComboBox + uiComboButtonFRM: QFrame + uiSliderButtonFRM: QFrame + uiComboDependGRP: QGroupBox + uiComboShapesGRP: QGroupBox + uiMainShapesGRP: QGroupBox + uiObjectGRP: QGroupBox + uiSliderDependGRP: QGroupBox + uiSystemGRP: QGroupBox + uiCurrentObjectLBL: QLabel + uiCurrentSystemLBL: QLabel + uiComboFilterLINE: QLineEdit + uiCurrentObjectTXT: QLineEdit + uiSliderFilterLINE: QLineEdit + uiClearSelectedObjectBTN: QPushButton + uiComboExitIsolateBTN: QPushButton + uiComboFilterClearBTN: QPushButton + uiDeleteComboBTN: QPushButton + uiDeleteSystemBTN: QPushButton + uiGetSelectedObjectBTN: QPushButton + uiNewComboActiveBTN: QPushButton + uiNewComboGroupBTN: QPushButton + uiNewComboSelectBTN: QPushButton + uiNewComboShapeBTN: QPushButton + uiNewGroupBTN: QPushButton + uiNewShapeBTN: QPushButton + uiNewSliderBTN: QPushButton + uiNewSystemBTN: QPushButton + uiRenameSystemBTN: QPushButton + uiSelectCtrlBTN: QPushButton + uiSelectSlidersBTN: QPushButton + uiSetSliderValsBTN: QPushButton + uiShapeConnectBTN: QPushButton + uiShapeConnectSceneBTN: QPushButton + uiShapeExtractBTN: QPushButton + uiSliderDeleteBTN: QPushButton + uiSliderExitIsolateBTN: QPushButton + uiSliderFilterClearBTN: QPushButton + uiZeroAllBTN: QPushButton + uiZeroSelectedBTN: QPushButton + uiComboDependAllRDO: QRadioButton + uiComboDependAnyRDO: QRadioButton + uiComboDependOnlyRDO: QRadioButton + uiSliderDependAllRDO: QRadioButton + uiSliderDependAnyRDO: QRadioButton + uiConnectionGroupWID: QWidget + + simplexLoaded = Signal() + openedDialogs = [] + + def __init__(self, parent=None, dispatch=None): + super(SimplexDialog, self).__init__(parent) + + uiPath = getUiFile(__file__) + QtCompat.loadUi(uiPath, self) + + # Custom widgets aren't working properly, so I bring them in manually + self.uiSliderTREE = SliderTree(self.uiMainShapesGRP) + self.uiSliderTREE.setDragEnabled(False) + self.uiSliderTREE.setDragDropMode(SliderTree.DragDropMode.NoDragDrop) + self.uiSliderTREE.setSelectionMode(SliderTree.SelectionMode.ExtendedSelection) + self.uiSliderTREE.dragFilter.dragPressed.connect(self.dragStart) + self.uiSliderTREE.dragFilter.dragReleased.connect(self.dragStop) + + self.uiSliderLAY.addWidget(self.uiSliderTREE) + + self.uiComboTREE = ComboTree(self.uiComboShapesGRP) + self.uiComboTREE.setDragEnabled(False) + self.uiComboTREE.setDragDropMode(ComboTree.DragDropMode.NoDragDrop) + self.uiComboTREE.setSelectionMode(ComboTree.SelectionMode.ExtendedSelection) + self.uiComboTREE.dragFilter.dragPressed.connect(self.dragStart) + self.uiComboTREE.dragFilter.dragReleased.connect(self.dragStop) + self.uiComboLAY.addWidget(self.uiComboTREE) + + self._sliderMenu = None + self._comboMenu = None + self._currentObject = None + self._currentObjectName = None + + # Connect the combo boxes and spinners to the data model + # TODO: Figure out how to deal with the type/axis "enum" cboxes + # see: http://doc.qt.io/qt-5/qtwidgets-itemviews-combowidgetmapper-example.html + """ + self._falloffMapper = QDataWidgetMapper() + self.uiShapeFalloffCBOX.currentIndexChanged.connect(self._falloffMapper.setCurrentIndex) + """ + + # Make sure to connect the dispatcher to the undo control + # but only keep a weakref to it + self.dispatch = None + if dispatch is not None: + self.dispatch = weakref.ref(dispatch) + dispatch.undo.connect(self.handleUndo) + dispatch.redo.connect(self.handleUndo) + dispatch.beforeNew.connect(self.newScene) + dispatch.beforeOpen.connect(self.newScene) + + self.simplex = None + + self._itemMap = {} + self._sliderTreeMap = {} + self._comboTreeMap = {} + + self._sliderDrag = None + self._comboDrag = None + + self.uiSliderExitIsolateBTN.hide() + self.uiComboExitIsolateBTN.hide() + + self._makeConnections() + + self._toolPlugins, self._contextPlugins = loadPlugins() + buildToolMenu(self, self._toolPlugins) + self.uiSliderTREE.setPlugins(self._contextPlugins) + self.uiComboTREE.setPlugins(self._contextPlugins) + + if DCC.program == "dummy": + # self.getSelectedObject() + self.uiObjectGRP.setEnabled(False) + self.uiSystemGRP.setEnabled(False) + + self.uiClearSelectedObjectBTN.hide() + self.uiMainShapesGRP.setEnabled(False) + self.uiComboShapesGRP.setEnabled(False) + self.uiConnectionGroupWID.setEnabled(False) + self.loadSettings() + self._sliderMul = 2.0 if self.uiDoubleSliderRangeACT.isChecked() else 1.0 + + self.travDialog = TraversalDialog(self) + self.falloffDialog = FalloffDialog(self) + # self.showTraversalDialog() + type(self).openedDialogs.append(weakref.ref(self)) + + @classmethod + def lastOpenedDialog(cls): + """Returns the last currently opened dialog that still exists""" + for dlgRef in reversed(cls.openedDialogs): + dlg = dlgRef() + if dlg is not None: + return dlg + return None + + def showTraversalDialog(self): + """Display the traversal dialog""" + self.travDialog.show() + self.travDialog.setGeometry(30, 30, 400, 400) + + def showFalloffDialog(self): + """Display the Falloff Dialog""" + self.falloffDialog.show() + pp = self.falloffDialog.pos() + x, y = pp.x(), pp.y() + if x < 0 or y < 0: + self.falloffDialog.move(max(x, 0), max(y, 0)) + + def dragStart(self): + """Slot for handling the start of a MMB Drag event""" + if self.simplex is not None: + self.simplex.DCC.undoOpen() + + def dragStop(self): + """Slot for handling the end of a MMB Drag event""" + if self.simplex is not None: + self.simplex.DCC.undoClose() + + def storeSettings(self): + """Store the state of the UI for the next run""" + pref = Prefs() + pref.recordProperty("geometry", self.saveGeometry()) + pref.save() + + def loadSettings(self): + """Load the state of the UI from a previous run""" + pref = Prefs() + geo = pref.restoreProperty("geometry", None) + if geo is not None: + self.restoreGeometry(geo) + + def closeEvent(self, event): + """Handle the close event""" + self.storeSettings() + self.deleteLater() + + # Undo/Redo + def newScene(self): + """Call this before a new scene is created. Usually called from the stack""" + self.clearSelectedObject() + + def handleUndo(self): + """Call this after an undo/redo action. Usually called from the stack""" + if self.simplex is None: + return + + rev = self.simplex.DCC.getRevision() + data = self.simplex.stack.getRevision(rev) + if data is not None: + self.setSystem(data) + self.uiSliderTREE.setItemExpansion() + self.uiComboTREE.setItemExpansion() + + def currentSystemChanged(self, idx): + """Slot called when the current system changes""" + if idx == -1: + self.setSystem(None) + return + name = str(self.uiCurrentSystemCBOX.currentText()) + if not name: + self.setSystem(None) + return + if self.simplex is not None: + if self.simplex.name == name: + return # Do nothing + + pBar = QProgressDialog("Loading from Mesh", "Cancel", 0, 100, self) + system = Simplex.buildSystemFromMesh( + self._currentObject, name, sliderMul=self._sliderMul, pBar=pBar + ) + self.setSystem(system) + pBar.close() + + def setSystem(self, system): + """Set the system on this UI + + Parameters + ---------- + system : Simplex + The Simplex system to load into this UI + """ + if system == self.simplex: + return + + if self.simplex is not None: + # disconnect the previous stuff + sliderSelModel = self.uiSliderTREE.selectionModel() + sliderSelModel.selectionChanged.disconnect(self.unifySliderSelection) + sliderSelModel.selectionChanged.disconnect(self.populateComboRequirements) + sliderSelModel.selectionChanged.disconnect(self.autoSetSliders) + + comboSelModel = self.uiComboTREE.selectionModel() + comboSelModel.selectionChanged.disconnect(self.unifyComboSelection) + comboSelModel.selectionChanged.disconnect(self.populateSliderRequirements) + comboSelModel.selectionChanged.disconnect(self.autoSetComboSliders) + + oldStack = self.simplex.stack + else: + oldStack = Stack() + + if system is None: + # self.toolActions.simplex = None + self.uiSliderTREE.setModel(QStandardItemModel()) + self.uiComboTREE.setModel(QStandardItemModel()) + self.simplex = system + self.uiMainShapesGRP.setEnabled(False) + self.uiComboShapesGRP.setEnabled(False) + self.uiConnectionGroupWID.setEnabled(False) + self.falloffDialog.loadSimplex() + self.simplexLoaded.emit() + return + + # set and connect the new stuff + self.simplex = system + self.simplex.models = [] + self.simplex.falloffModels = [] + self.simplex.stack = oldStack + + # self.toolActions.simplex = self.simplex + + simplexModel = SimplexModel(self.simplex, None) + + sliderModel = SliderModel(simplexModel, None) + sliderProxModel = SliderFilterModel(sliderModel) + self.uiSliderTREE.setModel(sliderProxModel) + sliderSelModel = self.uiSliderTREE.selectionModel() + sliderSelModel.selectionChanged.connect(self.unifySliderSelection) + sliderSelModel.selectionChanged.connect(self.populateComboRequirements) + sliderSelModel.selectionChanged.connect(self.autoSetSliders) + + comboModel = ComboModel(simplexModel, None) + comboProxModel = ComboFilterModel(comboModel) + self.uiComboTREE.setModel(comboProxModel) + comboSelModel = self.uiComboTREE.selectionModel() + comboSelModel.selectionChanged.connect(self.unifyComboSelection) + comboSelModel.selectionChanged.connect(self.populateSliderRequirements) + comboSelModel.selectionChanged.connect(self.autoSetComboSliders) + + self.falloffDialog.loadSimplex() + + # Make sure the UI is up and running + self.enableComboRequirements() + self.enableSliderRequirements() + self.uiMainShapesGRP.setEnabled(True) + self.uiComboShapesGRP.setEnabled(True) + self.uiConnectionGroupWID.setEnabled(True) + + self.setSimplexLegacy() + self.simplexLoaded.emit() + + # UI Setup + def _makeConnections(self): + """Make all the ui connections""" + # Setup Trees! + self.uiSliderTREE.setColumnWidth(1, 50) + self.uiSliderTREE.setColumnWidth(2, 20) + self.uiSliderFilterLINE.textChanged.connect(self.sliderStringFilter) + self.uiSliderFilterClearBTN.clicked.connect(self.uiSliderFilterLINE.clear) + self.uiSliderFilterClearBTN.clicked.connect(self.sliderStringFilter) + + self.uiComboTREE.setColumnWidth(1, 50) + self.uiComboTREE.setColumnWidth(2, 20) + self.uiComboFilterLINE.textChanged.connect(self.comboStringFilter) + self.uiComboFilterClearBTN.clicked.connect(self.uiComboFilterLINE.clear) + self.uiComboFilterClearBTN.clicked.connect(self.comboStringFilter) + + # combo dependency filter setup + self.uiComboDependGRP.toggled.connect(self.enableComboRequirements) + self.uiComboDependAllRDO.toggled.connect(self.enableComboRequirements) + self.uiComboDependAnyRDO.toggled.connect(self.enableComboRequirements) + self.uiComboDependOnlyRDO.toggled.connect(self.enableComboRequirements) + self.uiComboDependLockCHK.toggled.connect(self.setLockComboRequirement) + + # slider dependency filter setup + self.uiSliderDependGRP.toggled.connect(self.enableSliderRequirements) + self.uiSliderDependAllRDO.toggled.connect(self.enableSliderRequirements) + self.uiSliderDependAnyRDO.toggled.connect(self.enableSliderRequirements) + self.uiSliderDependLockCHK.toggled.connect(self.setLockSliderRequirements) + + # Bottom Left Corner Buttons + self.uiZeroAllBTN.clicked.connect(self.zeroAllSliders) + self.uiZeroSelectedBTN.clicked.connect(self.zeroSelectedSliders) + self.uiSelectCtrlBTN.clicked.connect(self.selectCtrl) + + # Top Left Corner Buttons + self.uiNewGroupBTN.clicked.connect(self.newSliderGroup) + self.uiNewSliderBTN.clicked.connect(self.newSlider) + self.uiNewShapeBTN.clicked.connect(self.newSliderShape) + self.uiSliderDeleteBTN.clicked.connect(self.sliderTreeDelete) + + # Top Right Corner Buttons + self.uiDeleteComboBTN.clicked.connect(self.comboTreeDelete) + self.uiNewComboActiveBTN.clicked.connect(self.newActiveCombo) + self.uiNewComboSelectBTN.clicked.connect(self.newSelectedCombo) + self.uiNewComboShapeBTN.clicked.connect(self.newComboShape) + self.uiNewComboGroupBTN.clicked.connect(self.newComboGroup) + + # Bottom right corner buttons + self.uiSetSliderValsBTN.clicked.connect(self.setSliderVals) + self.uiSelectSlidersBTN.clicked.connect(self.selectSliders) + + # System level + self.uiCurrentObjectTXT.editingFinished.connect(self.currentObjectChanged) + self.uiGetSelectedObjectBTN.clicked.connect(self.getSelectedObject) + self.uiClearSelectedObjectBTN.clicked.connect(self.clearSelectedObject) + + self.uiNewSystemBTN.clicked.connect(self.newSystem) + self.uiRenameSystemBTN.clicked.connect(self.renameSystem) + self.uiCurrentSystemCBOX.currentIndexChanged[int].connect( + self.currentSystemChanged + ) + + # Extraction/connection + self.uiShapeExtractBTN.clicked.connect(self.shapeExtract) + self.uiShapeConnectBTN.clicked.connect(self.shapeConnect) + self.uiShapeConnectSceneBTN.clicked.connect(self.shapeConnectScene) + + # File Menu + self.uiImportACT.triggered.connect(self.importSystemFromFile) + self.uiExportACT.triggered.connect(self.exportSystemTemplate) + + # Edit Menu + self.uiHideRedundantACT.toggled.connect(self.hideRedundant) + self.uiDoubleSliderRangeACT.toggled.connect(self.setSliderRange) + + # Isolation + self.uiSliderExitIsolateBTN.clicked.connect(self.sliderTreeExitIsolate) + self.uiComboExitIsolateBTN.clicked.connect(self.comboTreeExitIsolate) + + self.uiLegacyJsonACT.toggled.connect(self.setSimplexLegacy) + + # Helpers + def getSelectedItems(self, tree, typ=None): + """Convenience function to get the selected system items + + Parameters + ---------- + tree : QTreeView + The tree to get selected items from + typ : Type + The type of objects to return + + Returns + ------- + [object, ...] + A list of selected items + """ + sel = tree.selectedIndexes() + sel = [i for i in sel if i.column() == 0] + model = tree.model() + items = [model.itemFromIndex(i) for i in sel] + if typ is not None: + items = [i for i in items if isinstance(i, typ)] + return items + + def getCurrentObject(self): + """Convenience function to get the current object loaded into the UI""" + return self._currentObject + + # Setup Trees! + def sliderStringFilter(self): + """Set the filter for the slider tree""" + filterString = str(self.uiSliderFilterLINE.text()) + sliderModel = self.uiSliderTREE.model() + sliderModel.filterString = str(filterString) + sliderModel.invalidateFilter() + + def comboStringFilter(self): + """Set the filter for the combo tree""" + filterString = str(self.uiComboFilterLINE.text()) + comboModel = self.uiComboTREE.model() + comboModel.filterString = str(filterString) + comboModel.invalidateFilter() + + # selection setup + def unifySliderSelection(self): + """Clear the selection of the combo tree when + an item on the slider tree is selected + """ + mods = QApplication.keyboardModifiers() + if not mods & ( + Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier + ): + comboSelModel = self.uiComboTREE.selectionModel() + if not comboSelModel: + return + + with signalsBlocked(comboSelModel): + comboSelModel.clearSelection() + self.uiComboTREE.viewport().update() + + def unifyComboSelection(self): + """Clear the selection of the slider tree when + an item on the combo tree is selected + """ + mods = QApplication.keyboardModifiers() + if not mods & ( + Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier + ): + sliderSelModel = self.uiSliderTREE.selectionModel() + if not sliderSelModel: + return + with signalsBlocked(sliderSelModel): + sliderSelModel.clearSelection() + self.uiSliderTREE.viewport().update() + + # dependency setup + def setLockComboRequirement(self): + """Refresh the combo selection filter with the new filterLock state""" + comboModel = self.uiComboTREE.model() + if not comboModel: + return + self.populateComboRequirements() + + def populateComboRequirements(self): + """Let the combo tree know the requirements from the slider tree""" + items = self.uiSliderTREE.getSelectedItems(Slider) + comboModel = self.uiComboTREE.model() + locked = self.uiComboDependLockCHK.isChecked() + if not locked: + comboModel.requires = items + if ( + comboModel.filterRequiresAll + or comboModel.filterRequiresAny + or comboModel.filterRequiresOnly + ): + comboModel.invalidateFilter() + + def enableComboRequirements(self): + """Set the requirements for the combo filter model""" + comboModel = self.uiComboTREE.model() + if not comboModel: + return + depCheck = self.uiComboDependGRP.isChecked() + comboModel.filterRequiresAll = self.uiComboDependAllRDO.isChecked() and depCheck + comboModel.filterRequiresAny = self.uiComboDependAnyRDO.isChecked() and depCheck + comboModel.filterRequiresOnly = ( + self.uiComboDependOnlyRDO.isChecked() and depCheck + ) + comboModel.invalidateFilter() + + def populateSliderRequirements(self): + """Let the slider tree know the requirements from the combo tree""" + items = self.uiComboTREE.getSelectedItems(Combo) + sliderModel = self.uiSliderTREE.model() + locked = self.uiSliderDependLockCHK.isChecked() + if not locked: + sliderModel.requires = items + if sliderModel.filterRequiresAny or sliderModel.filterRequiresAll: + sliderModel.invalidateFilter() + + def setLockSliderRequirements(self): + """Refresh the combo selection filter with the new filterLock state""" + sliderModel = self.uiSliderTREE.model() + if not sliderModel: + return + self.populateSliderRequirements() + + def enableSliderRequirements(self): + sliderModel = self.uiSliderTREE.model() + if not sliderModel: + return + + depCheck = self.uiSliderDependGRP.isChecked() + sliderModel.filterRequiresAny = ( + self.uiSliderDependAnyRDO.isChecked() and depCheck + ) + sliderModel.filterRequiresAll = ( + self.uiSliderDependAllRDO.isChecked() and depCheck + ) + sliderModel.invalidateFilter() + + # Bottom Left Corner Buttons + def zeroAllSliders(self): + """Slot to Zero all Sliders in Slider UI panel""" + if self.simplex is None: + return + sliders = self.simplex.sliders + weights = [0.0] * len(sliders) + self.simplex.setSlidersWeights(sliders, weights) + self.uiSliderTREE.repaint() + + def zeroSelectedSliders(self): + """Slot to Zero the selected sliders in the UI panel""" + if self.simplex is None: + return + items = self.uiSliderTREE.getSelectedItems(Slider) + values = [0.0] * len(items) + self.simplex.setSlidersWeights(items, values) + self.uiSliderTREE.repaint() + + def selectCtrl(self): + """Select the Control object in the DCC""" + if self.simplex is None: + return + self.simplex.DCC.selectCtrl() + + def autoSetSliders(self): + """Automatically set any selected sliders to 1.0""" + if self.simplex is None: + return + if not self.uiAutoSetSlidersCHK.isChecked(): + return + sel = set(self.uiSliderTREE.getSelectedItems(Slider)) + sliders = self.simplex.sliders + + weights = [0.0] * len(sliders) + for i, slider in enumerate(sliders): + if slider in sel: + weights[i] = 1.0 + self.simplex.setSlidersWeights(sliders, weights) + self.uiSliderTREE.repaint() + + def _getAName(self, tpe, default=None, taken=(), uniqueAccept=False): + """uniqueAccept forces the user to provide and accept a unique name + If the user enters a non-unique name, the name is uniquified, and the + dialog is re-shown with the unique name as the default suggestion + """ + tpe = tpe.lower() + uTpe = tpe[0].upper() + tpe[1:] + + unique = False + default = getNextName(default, taken) + while not unique: + eMsg = "Enter a name for the new {0}".format(tpe) + newName, good = QInputDialog.getText( + self, "New {0}".format(uTpe), eMsg, text=default + ) + if not good: + return None + if len(newName) < 3: + message = "Please use names longer than 2 letters" + QMessageBox.warning(self, "Warning", message) + return None + if not NAME_CHECK.match(newName): + message = "{0} name can only contain letters and numbers, and cannot start with a number" + message = message.format(uTpe) + QMessageBox.warning(self, "Warning", message) + return None + + unqName = getNextName(newName, taken) + unique = (unqName == newName) or uniqueAccept + default = unqName + + return default + + # Top Left Corner Buttons + def newSliderGroup(self): + """Slot to Create a new slider group""" + if self.simplex is None: + return + + newName = self._getAName("group", default="Group") + if newName is None: + return + + Group.createGroup(str(newName), self.simplex, groupType=Slider) + # self.uiSliderTREE.model().invalidateFilter() + # self.uiComboTREE.model().invalidateFilter() + + def newSlider(self): + """Slot to create a new slider""" + if self.simplex is None: + return + + newName = self._getAName("slider") + if newName is None: + return + + idxs = self.uiSliderTREE.getSelectedIndexes() + groups = coerceIndexToParentType(idxs, Group) + group = groups[0].model().itemFromIndex(groups[0]) if groups else None + + Slider.createSlider(str(newName), self.simplex, group=group) + + def newSliderShape(self): + """Slot to create a new Shape in a Slider's progression""" + pars = self.uiSliderTREE.getSelectedItems(Slider) + if not pars: + return + parItem = pars[0] + parItem.createShape() + self.uiSliderTREE.model().invalidateFilter() + + def sliderTreeDelete(self): + """Delete some objects in the slider tree""" + idxs = self.uiSliderTREE.getSelectedIndexes() + roots = coerceIndexToRoots(idxs) + if not roots: + QMessageBox.warning(self, "Warning", "Nothing Selected in Slider tree") + return + roots = makeUnique([i.model().itemFromIndex(i) for i in roots]) + for r in roots: + if isinstance(r, Simplex): + QMessageBox.warning( + self, "Warning", "Cannot delete a simplex system this way" + ) + return + + for r in roots: + r.delete() + self.uiSliderTREE.model().invalidateFilter() + + # Top Right Corner Buttons + def comboTreeDelete(self): + """Delete some objects in the Combo tree""" + idxs = self.uiComboTREE.getSelectedIndexes() + roots = coerceIndexToRoots(idxs) + if not roots: + QMessageBox.warning(self, "Warning", "Nothing Selected in Combo tree") + return + roots = makeUnique([i.model().itemFromIndex(i) for i in roots]) + for r in roots: + if isinstance(r, Simplex): + QMessageBox.warning( + self, "Warning", "Cannot delete a simplex system this way" + ) + return + + for r in roots: + r.delete() + self.uiComboTREE.model().invalidateFilter() + + def _newCombo(self, sliders, values): + if len(sliders) < 2: + message = "A combo must use at least 2 sliders" + QMessageBox.warning(self, "Warning", message) + return + + ccd = ComboCheckDialog(sliders, values=values, mode="create", parent=self) + ccd.move(self.pos()) + ccd.exec_() + + def newActiveCombo(self): + """Create a combo based on the UI Sliders that are currently nonzero""" + if self.simplex is None: + return + sliders = [] + values = {} + for s in self.simplex.sliders: + if s.value != 0.0: + sliders.append(s) + values[s] = [s.value] + self._newCombo(sliders, values) + + def newSelectedCombo(self): + """Create a combo based on the currently selected UI sliders""" + if self.simplex is None: + return + sliders = self.uiSliderTREE.getSelectedItems(Slider) + values = {s: [1.0] for s in sliders} + self._newCombo(sliders, values) + + def newComboShape(self): + """Create a new shape in the Combo's progression""" + parIdxs = self.uiComboTREE.getSelectedIndexes() + pars = coerceIndexToParentType(parIdxs, Combo) + if not pars: + return + + parItem = pars[0].model().itemFromIndex(pars[0]) if pars else None + parItem.createShape() + self.uiComboTREE.model().invalidateFilter() + + def newComboGroup(self): + """Create a new group for organizing combos""" + if self.simplex is None: + return + + newName = self._getAName("group", default="Group") + if newName is None: + return + + Group.createGroup(str(newName), self.simplex, groupType=Combo) + # self.uiComboTREE.model().invalidateFilter() + # self.uiSliderTREE.model().invalidateFilter() + + # Bottom right corner buttons + def setSliderVals(self): + """Set all slider values to those stored in the currently selected Combos""" + if self.simplex is None: + return + combos = self.uiComboTREE.getSelectedItems(Combo) + # maybe coerce to type instead?? + self.zeroAllSliders() + values = [] + sliders = [] + for combo in combos: + for pair in combo.pairs: + if pair.slider in sliders: + continue + sliders.append(pair.slider) + values.append(pair.value) + self.simplex.setSlidersWeights(sliders, values) + self.uiSliderTREE.repaint() + + def selectSliders(self): + """Select the sliders that are contained in the currently selected Combos""" + combos = self.uiComboTREE.getSelectedItems(Combo) + sliders = [] + for combo in combos: + for pair in combo.pairs: + sliders.append(pair.slider) + self.uiSliderTREE.setItemSelection(sliders) + + def autoSetComboSliders(self): + """Automatically set the DCC Slider values to activate the currently selected Combos""" + if self.simplex is None: + return + if not self.uiAutoSetCombosCHK.isChecked(): + return + sel = set(self.uiComboTREE.getSelectedItems(Combo)) + sv = {} + for combo in self.simplex.combos: + isSel = combo in sel + for pair in combo.pairs: + curVal = sv.get(pair.slider, 0.0) + newVal = pair.value if isSel else 0.0 + if abs(newVal) >= abs(curVal): + sv[pair.slider] = newVal + + sliders, weights = list(zip(*list(sv.items()))) + self.simplex.setSlidersWeights(sliders, weights) + self.uiSliderTREE.repaint() + + # Extraction/connection + def shapeConnectScene(self): + """Connect any selected meshes into the system based on the name""" + if self.simplex is None: + return + # make a dict of name:object + sel = DCC.getSelectedObjects() + selDict = {} + for s in sel: + name = DCC.getObjectName(s) + if name.endswith("_Extract"): + nn = name.rsplit("_Extract", 1)[0] + selDict[nn] = s + + pairDict = {} + for p in self.simplex.progs: + for pp in p.pairs: + pairDict[pp.shape.name] = pp + + # get all common names + common = selDict.keys() & pairDict.keys() + + # get those items + pairs = [pairDict[i] for i in common] + + # Set up the progress bar + pBar = QProgressDialog("Connecting Shapes", "Cancel", 0, 100, self) + pBar.setMaximum(len(pairs)) + + # Do the extractions + for pair in pairs: + c = pair.prog.controller + c.connectShape(pair.shape, delete=True) + + # ProgressBar + pBar.setValue(pBar.value() + 1) + pBar.setLabelText("Connecting:\n{0}".format(pair.shape.name)) + QApplication.processEvents() + if pBar.wasCanceled(): + return + + pBar.close() + + def sliderShapeExtract(self): + """Create meshes that are possibly live-connected to the shapes""" + sliderIdxs = self.uiSliderTREE.getSelectedIndexes() + return self.shapeIndexExtract(sliderIdxs) + + def comboShapeExtract(self): + """Create meshes that are possibly live-connected to the shapes""" + comboIdxs = self.uiComboTREE.getSelectedIndexes() + return self.shapeIndexExtract(comboIdxs) + + def shapeExtract(self): + """Create meshes that are possibly live-connected to the shapes""" + sliderIdxs = self.uiSliderTREE.getSelectedIndexes() + comboIdxs = self.uiComboTREE.getSelectedIndexes() + return self.shapeIndexExtract(sliderIdxs + comboIdxs) + + def shapeExport(self): + """Export meshes""" + sliderIdxs = self.uiSliderTREE.getSelectedIndexes() + comboIdxs = self.uiComboTREE.getSelectedIndexes() + return self.shapeIndexExport(sliderIdxs + comboIdxs) + + def shapeIndexExport(self, indexes): + """Export meshes + + Parameters + ---------- + indexes : [QModelIndex, ...] + A list of indexes to extract + """ + # Importing this here to keep people from getting confused + # You should use self._fileDialog which does compatibility + # stuff for file saving. Since I'm looking for a folder here + # I don't need to do that + from Qt.QtWidgets import QFileDialog + + if self.simplex is None: + return + + pref = Prefs() + defaultPath = str( + pref.restoreProperty( + "systemExportFolder", os.path.join(os.path.expanduser("~")) + ) + ) + outfld = QFileDialog.getExistingDirectory( + self, "Pick Export Folder", defaultPath + ) + if not outfld: + return + + pref.recordProperty("systemExportFolder", outfld) + pref.save() + + pairs = coerceIndexToChildType(indexes, ProgPair) + pairs = [i.model().itemFromIndex(i) for i in pairs] + pairs = makeUnique([i for i in pairs if not i.shape.isRest]) + pairs.sort(key=lambda x: naturalSortKey(x.shape.name)) + + # Set up the progress bar + pBar = QProgressDialog("Exporting Shapes", "Cancel", 0, 100, self) + pBar.setMaximum(len(pairs)) + + extension = '.abc' + # Do the extractions + for pair in pairs: + c = pair.prog.controller + extracted = c.extractShape(pair.shape, live=False) + path = os.path.join(outfld, c.name + extension) + if os.path.exists(path): + os.remove(path) + + self.simplex.DCC.exportMesh(extracted, path) + self.simplex.DCC.deleteObj(extracted) + + # ProgressBar + pBar.setValue(pBar.value() + 1) + pBar.setLabelText("Extracting:\n{0}".format(pair.shape.name)) + QApplication.processEvents() + if pBar.wasCanceled(): + return + + pBar.close() + + def shapeIndexExtract(self, indexes, live=None): + """Create meshes that are possibly live-connected to the shapes + + Parameters + ---------- + indexes : [QModelIndex, ...] + A list of indexes to extract + live : bool or None + Whether the connection is live. If None, check the UI properties + """ + if live is None: + live = self.uiLiveShapeConnectionACT.isChecked() + + pairs = coerceIndexToChildType(indexes, ProgPair) + pairs = [i.model().itemFromIndex(i) for i in pairs] + pairs = makeUnique([i for i in pairs if not i.shape.isRest]) + pairs.sort(key=lambda x: naturalSortKey(x.shape.name)) + + # Set up the progress bar + pBar = QProgressDialog("Extracting Shapes", "Cancel", 0, 100, self) + pBar.setMaximum(len(pairs)) + + # Do the extractions + offset = 10 + extracted = [] + for pair in pairs: + c = pair.prog.controller + ext = c.extractShape(pair.shape, live=live, offset=offset) + extracted.append(ext) + offset += 5 + + # ProgressBar + pBar.setValue(pBar.value() + 1) + pBar.setLabelText("Extracting:\n{0}".format(pair.shape.name)) + QApplication.processEvents() + if pBar.wasCanceled(): + return extracted + + pBar.close() + return extracted + + def shapeConnect(self): + """Match any selected Shapes to DCC meshes based on their names, then delete the Meshes""" + sliderIdxs = self.uiSliderTREE.getSelectedIndexes() + comboIdxs = self.uiComboTREE.getSelectedIndexes() + self.shapeConnectIndexes(sliderIdxs + comboIdxs) + + def shapeConnectIndexes(self, indexes): + """Match the provided shapes to DCC meshes based on their names, then delete the Meshes + + Parameters + ---------- + indexes : list of QModelIndex + A list of selected model indexes + """ + pairs = coerceIndexToChildType(indexes, ProgPair) + pairs = [i.model().itemFromIndex(i) for i in pairs] + pairs = makeUnique([i for i in pairs if not i.shape.isRest]) + + # Set up the progress bar + pBar = QProgressDialog("Connecting Shapes", "Cancel", 0, 100, self) + pBar.setMaximum(len(pairs)) + + # Do the extractions + for pair in pairs: + c = pair.prog.controller + c.connectShape(pair.shape, delete=True) + + # ProgressBar + pBar.setValue(pBar.value() + 1) + pBar.setLabelText("Extracting:\n{0}".format(pair.shape.name)) + QApplication.processEvents() + if pBar.wasCanceled(): + return + + pBar.close() + + def shapeMatch(self): + """Match any selected Shapes to A selected DCC mesh""" + sliderIdxs = self.uiSliderTREE.getSelectedIndexes() + comboIdxs = self.uiComboTREE.getSelectedIndexes() + self.shapeMatchIndexes(sliderIdxs + comboIdxs) + + def shapeMatchIndexes(self, indexes): + """Match any provided shapes to A selected DCC mesh + + Parameters + ---------- + indexes : list of QModelIndex + A list of selected model indexes + """ + # make a dict of name:object + sel = DCC.getSelectedObjects() + if not sel: + return + mesh = sel[0] + + pairs = coerceIndexToChildType(indexes, ProgPair) + pairs = [i.model().itemFromIndex(i) for i in pairs] + pairs = makeUnique([i for i in pairs if not i.shape.isRest]) + + # Set up the progress bar + pBar = QProgressDialog("Matching Shapes", "Cancel", 0, 100, self) + pBar.setMaximum(len(pairs)) + + # Do the extractions + for pair in pairs: + c = pair.prog.controller + c.connectShape(pair.shape, mesh=mesh) + + # ProgressBar + pBar.setValue(pBar.value() + 1) + pBar.setLabelText("Matching:\n{0}".format(pair.shape.name)) + QApplication.processEvents() + if pBar.wasCanceled(): + return + + pBar.close() + + def shapeClear(self): + """Match all selected shapes to the rest""" + sliderIdxs = self.uiSliderTREE.getSelectedIndexes() + comboIdxs = self.uiComboTREE.getSelectedIndexes() + self.shapeClearIndexes(sliderIdxs + comboIdxs) + + def shapeClearIndexes(self, indexes): + """Match all provided shapes to the rest + + Parameters + ---------- + indexes : list of QModelIndex + A list of selected model indexes + """ + pairs = coerceIndexToChildType(indexes, ProgPair) + pairs = [i.model().itemFromIndex(i) for i in pairs] + pairs = makeUnique([i for i in pairs if not i.shape.isRest]) + + for pair in pairs: + pair.shape.zeroShape() + + # System level + def loadObject(self, thing): + """Load a DCC mesh into the UI + + Parameters + ---------- + thing : object + The DCC mesh to load into the UI + """ + if thing is None: + return + + self.uiClearSelectedObjectBTN.show() + self.uiCurrentSystemCBOX.clear() + objName = DCC.getObjectName(thing) + self._currentObject = thing + self._currentObjectName = objName + self.uiCurrentObjectTXT.setText(objName) + + ops = DCC.getSimplexOperatorsOnObject(self._currentObject) + + for op in ops: + js = DCC.getSimplexString(op) + if not js: + continue + d = json.loads(js) + name = d["systemName"] + self.uiCurrentSystemCBOX.addItem(name, (self._currentObject, name)) + + def currentObjectChanged(self): + """Slot called when the current DCC object is changed""" + name = str(self.uiCurrentObjectTXT.text()) + if self._currentObjectName == name: + return + if not name: + return + + newObject = DCC.getObjectByName(name) + if not newObject: + return + + self.loadObject(newObject) + + def getSelectedObject(self): + """Load the first selected DCC object into the UI""" + sel = DCC.getSelectedObjects() + if not sel: + return + newObj = sel[0] + if not newObj: + return + self.loadObject(newObj) + + def clearSelectedObject(self): + """Unload the current DCC object from the UI""" + self.uiClearSelectedObjectBTN.hide() + self.uiCurrentSystemCBOX.clear() + self._currentObject = None + self._currentObjectName = None + self.uiCurrentObjectTXT.setText("") + self.setSystem(None) + # Clear the current system + + def newSystem(self): + """Create a new system on the current DCC Object""" + if self._currentObject is None: + QMessageBox.warning(self, "Warning", "Must have a current object selection") + return + + newName = self._getAName("system") + if newName is None: + return + + newSystem = Simplex.buildEmptySystem( + self._currentObject, newName, sliderMul=self._sliderMul + ) + with signalsBlocked(self.uiCurrentSystemCBOX): + self.uiCurrentSystemCBOX.addItem(newName) + self.uiCurrentSystemCBOX.setCurrentIndex( + self.uiCurrentSystemCBOX.count() - 1 + ) + self.setSystem(newSystem) + + def renameSystem(self): + """Rename the current Simplex system""" + if self.simplex is None: + return + + sysNames = [ + str(self.uiCurrentSystemCBOX.itemText(i)) + for i in range(self.uiCurrentSystemCBOX.count()) + ] + newName = self._getAName("system", taken=sysNames) + if newName is None: + return + + self.simplex.name = newName + idx = self.uiCurrentSystemCBOX.currentIndex() + self.uiCurrentSystemCBOX.setItemText(idx, newName) + + self.currentSystemChanged(idx) + + def setSimplexLegacy(self): + """Slot to toggle the legacy behavior of the current Simplex system""" + if self.simplex is not None: + self.simplex.setLegacy(self.uiLegacyJsonACT.isChecked()) + self.simplex.DCC.incrementRevision() + + # File Menu + def importSystemFromFile(self): + """Open a File Dialog to load a simplex system from a file. + Systems can be in either .smpx, or .json formats + """ + if self._currentObject is None: + impTypes = ["smpx"] + else: + impTypes = ["smpx", "json"] + + pref = Prefs() + defaultPath = str( + pref.restoreProperty("systemImport", os.path.join(os.path.expanduser("~"))) + ) + path = self._fileDialog("Import Template", defaultPath, impTypes, save=False) + if not path: + return + pref.recordProperty("systemImport", os.path.dirname(path)) + pref.save() + + self.loadFile(path) + + def loadFile(self, path): + pBar = QProgressDialog("Loading Shapes", "Cancel", 0, 100, self) + pBar.show() + QApplication.processEvents() + + # TODO: Come up with a better list of possibilites for loading + # simplex files, and make the appropriate methods on the Simplex + if path.endswith(".smpx"): + newSystem = Simplex.buildSystemFromSmpx( + path, self._currentObject, sliderMul=self._sliderMul, pBar=pBar + ) + if newSystem is None: + QMessageBox.warning( + self, + "Point Count Mismatch", + "The .smpx file point count does not match the current object", + ) + pBar.close() + return + + elif path.endswith(".json"): + newSystem = Simplex.buildSystemFromJson( + path, self._currentObject, sliderMul=self._sliderMul, pBar=pBar + ) + else: + QMessageBox.warning(self, "Bad Filepath", "Path type not recognized") + return + + with signalsBlocked(self.uiCurrentSystemCBOX): + self.loadObject(newSystem.DCC.mesh) + idx = self.uiCurrentSystemCBOX.findText(self._currentObjectName) + if idx >= 0: + self.uiCurrentSystemCBOX.setCurrentIndex(idx) + else: + self.uiCurrentSystemCBOX.addItem(newSystem.name) + self.uiCurrentSystemCBOX.setCurrentIndex( + self.uiCurrentSystemCBOX.count() - 1 + ) + + self.setSystem(newSystem) + pBar.close() + self.simplex.DCC.checkForErrors(self) + + def _fileDialog(self, title, initPath, filters, save=True): + """Convenience function for displaying File Dialogs""" + filters = ["{0} (*.{0})".format(f) for f in filters] + if not save: + filters += ["All files (*.*)"] + filters = ";;".join(filters) + + if save: + path, _ = QtCompat.QFileDialog.getSaveFileName( + self, title, initPath, filters + ) + else: + path, _ = QtCompat.QFileDialog.getOpenFileName( + self, title, initPath, filters + ) + + if not path: + return "" + + if not save and not os.path.exists(path): + return "" + + return path + + def exportSystemTemplate(self): + """Open a file dialog and export a system to the chosen path""" + if self._currentObject is None: + QMessageBox.warning(self, "Warning", "Must have a current object selection") + return + + pref = Prefs() + defaultPath = str( + pref.restoreProperty("systemExport", os.path.join(os.path.expanduser("~"))) + ) + path = self._fileDialog( + "Export Template", defaultPath, ["smpx", "json"], save=True + ) + if not path: + return + pref.recordProperty("systemExport", os.path.dirname(path)) + pref.save() + + if self.simplex is None: + QMessageBox.warning(self, "Warning", "No simplex loaded") + return + + if path.endswith(".smpx"): + pBar = QProgressDialog("Exporting smpx File", "Cancel", 0, 100, self) + pBar.show() + self.simplex.exportAbc(path, pBar) + pBar.close() + elif path.endswith(".json"): + dump = self.simplex.dump() + with open(path, "w") as f: + f.write(dump) + + # Slider Settings + def setSelectedSliderGroups(self, group): + """Set the group for the selected Sliders + + Parameters + ---------- + group : Group + The group that will take the selected Sliders + """ + if not group: + return + sliders = self.uiSliderTREE.getSelectedItems(Slider) + group.take(sliders) + self.uiSliderTREE.viewport().update() + + def setSelectedSliderFalloff(self, falloff, state): + """Set the Falloffs for the selected Sliders + + Parameters + ---------- + falloff : Falloff + The falloff to set on the selected sliders + state : bool + Whether to add or remove this falloff from the selection + """ + if not falloff: + return + sliders = self.uiSliderTREE.getSelectedItems(Slider) + for s in sliders: + if state == Qt.CheckState.Checked: + s.prog.addFalloff(falloff) + else: + s.prog.removeFalloff(falloff) + self.uiSliderTREE.viewport().update() + + def setSelectedSliderInterp(self, interp): + """Set the interpolation for the selected sliders + + Parameters + ---------- + interp : str + The interpolation to set on the selection. + Could be 'linear', 'spline', or 'splitSpline' + """ + sliders = self.uiSliderTREE.getSelectedItems(Slider) + for s in sliders: + s.prog.interp = interp + + # Combo Settings + def setSelectedComboGroups(self, group): + """Set the group for the selected Combos + + Parameters + ---------- + group : Group + The group that will take the selected Combos + """ + if not group: + return + combos = self.uiComboTREE.getSelectedItems(Combo) + group.take(combos) + self.uiComboTREE.viewport().update() + + def setSelectedComboSolveType(self, stVal): + """Set the solve type for the selected combos + + Parameters + ---------- + stVal : str + The solve type to set for the combos. + See Combo.solveTypes for a list + """ + combos = self.uiComboTREE.getSelectedItems(Combo) + for c in combos: + c.solveType = stVal + + # Edit Menu + def hideRedundant(self): + """Hide redundant items from the Slider and Combo trees based on a user preference""" + check = self.uiHideRedundantACT.isChecked() + comboModel = self.uiComboTREE.model() + comboModel.filterShapes = check + comboModel.invalidateFilter() + sliderModel = self.uiSliderTREE.model() + sliderModel.doFilter = check + sliderModel.invalidateFilter() + + def setSliderRange(self): + """Double the range for the sliders *IN THE DCC ONLY* based on a user preference""" + self._sliderMul = 2.0 if self.uiDoubleSliderRangeACT.isChecked() else 1.0 + if self.simplex is None: + return + self.simplex.DCC.sliderMul = self._sliderMul + self.simplex.DCC.setSlidersRange(self.simplex.sliders) + + # Isolation + def isSliderIsolate(self): + """Check if the slider tree is currently isolated""" + model = self.uiSliderTREE.model() + if model: + return bool(model.isolateList) + return False + + def sliderIsolateSelected(self): + """Isolate the selected Sliders in the Slider Tree""" + self.uiSliderTREE.isolateSelected() + self.uiSliderExitIsolateBTN.show() + + def sliderTreeExitIsolate(self): + """Disable isolation mode in the Slider Tree""" + self.uiSliderTREE.exitIsolate() + self.uiSliderExitIsolateBTN.hide() + + def isComboIsolate(self): + """Check if the combo tree is currently isolated""" + model = self.uiComboTREE.model() + if model: + return bool(model.isolateList) + return False + + def comboIsolateSelected(self): + """Isolate the selected Combos in the Combo Tree""" + self.uiComboTREE.isolateSelected() + self.uiComboExitIsolateBTN.show() + + def comboTreeExitIsolate(self): + """Disable isolation mode in the Combo Tree""" + self.uiComboTREE.exitIsolate() + self.uiComboExitIsolateBTN.hide() + + +def _test(): + app = QApplication(sys.argv) + path = r"C:\Users\tfox\Documents\GitHub\Simplex\scripts\SimplexUI\build\HeadMaleStandard_High_Unsplit.smpx" + d = SimplexDialog() + newSystem = Simplex.buildSystemFromSmpx(path, d.getCurrentObject(), sliderMul=1.0) + d.setSystem(newSystem) + + d.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + _test() diff --git a/src/python/simplexui/travCheckDialog.py b/src/python/simplexui/travCheckDialog.py index fcc1147c..832883ba 100644 --- a/src/python/simplexui/travCheckDialog.py +++ b/src/python/simplexui/travCheckDialog.py @@ -1,422 +1,422 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -from itertools import combinations, product - -from .dragFilter import DragFilter -from .items import Slider, Traversal -from Qt import QtCompat -from Qt.QtCore import Qt -from Qt.QtGui import QBrush, QColor -from Qt.QtWidgets import ( - QDialog, - QTreeWidgetItem, - QCheckBox, - QGroupBox, - QLabel, - QPushButton, - QSpinBox, - QTreeWidget, -) - - -from .utils import getUiFile - - -class TooManyPossibilitiesError(Exception): - """Error raised when there are too many possibilities - Basically used as a stop-iteration - """ - - pass - - -def buildPossibleTraversals( - simplex, sliders, minDepth, maxDepth, lockDict=None, maxPoss=100 -): - """Build a list of possible traversals - - Parameters - ---------- - simplex : Simplex - The simplex system to check - sliders : Slider - The sliders to check - minDepth : int - The minimum number of sliders that will go into any traversals - maxDepth : int - The maximum number of sliders that will go into any traversals - lockDict : {Slider: ((float, ...), bool), ...} - An optional per-slider dict of possible values - maxPoss : float - The Maximum number of possibilities to return.(Default value = 100) - - Returns - ------- - : bool - True if the maximum number of possibilities was exceeded - : [([(Slider, (float, float)), ...], Traversal), ...] - Grouped slider/range pairs to existing (or None) Traversals - """ - allRanges = {} - allDyn = {} - sliderDict = {} - lockDict = lockDict or {} - - # Get the range values for each slider - for slider in sliders: - rng, dyn = lockDict.get(slider, (slider.prog.getRange(), True)) - rng = set(rng) - rng.discard(0) # ignore the zeros - allRanges[slider] = sorted(rng) - allDyn[slider] = dyn - sliderDict[slider.name] = slider - - poss = [] - tooMany = False - try: - for size in range(minDepth, maxDepth + 1): - for grp in combinations(sliders, size): - names = [i.name for i in grp] - ranges = [allRanges[s] for s in grp] - for vals in product(*ranges): - for dynIdx in range(len(grp)): - if not allDyn[grp[dynIdx]]: - continue - trng = list(zip(vals, vals)) - trng[dynIdx] = (0, trng[dynIdx][0]) - - count = Traversal.getCount(grp, trng) - if count == 0: - continue - - poss.append(frozenset(list(zip(names, trng)))) - if len(poss) > maxPoss: - raise TooManyPossibilitiesError("Don't melt your computer") - except TooManyPossibilitiesError: - tooMany = True - - # Build a dict of traversals that already exist - # but only if their sliders are in the list of sliders to check - onlys = {} - for trav in simplex.traversals: - sls = trav.allSliders() - if all(r in sliders for r in sls): - rngs = trav.ranges() - key = frozenset([(k.name, v) for k, v in rngs.items()]) - onlys[key] = trav - - toAdd = [] - for p in poss: - truePairs = [(sliderDict[n], r) for n, r in p] - toAdd.append((truePairs, onlys.get(p))) - return tooMany, toAdd - - -class TravCheckItem(QTreeWidgetItem): - def __init__(self, pairs, trav, *args, **kwargs): - super(TravCheckItem, self).__init__(*args, **kwargs) - self.pairs = pairs - self.trav = trav - - exists = False - grayBrush = QBrush(QColor(128, 128, 128)) - if self.trav is None: - ranges = dict(self.pairs) - newName = Traversal.buildTraversalName(ranges) - self.setText(0, newName) - else: - exists = True - self.setText(0, self.trav.name) - self.setForeground(0, grayBrush) - self.setForeground(1, grayBrush) - self.setForeground(2, grayBrush) - - # create the slider sub-rows - for slider, rng in pairs: - item = QTreeWidgetItem(self) - - item.setData(0, Qt.ItemDataRole.EditRole, slider.name) - item.setData(1, Qt.ItemDataRole.EditRole, rng[0]) - item.setData(2, Qt.ItemDataRole.EditRole, rng[1]) - if exists: - item.setForeground(0, grayBrush) - item.setForeground(1, grayBrush) - item.setForeground(2, grayBrush) - - self.setExpanded(True) - - -class TraversalCheckDialog(QDialog): - """Dialog for checking what possible traversals exist, and picking new traversals - In 'Create' mode, it provides a quick way of choosing the one specific traversal - that the user is looking for - - In 'Check' mode, it provides a convenient way to explore the possibilites - and create any missing traversals directly - - Parameters - ---------- - sliders : [Slider, ...] - A list of sliders to check - values : {Slider: (float, ...), ...} - A dictionary of values to use per slider - mode : str - The mode to display the dialog. Defaults to 'create' - parent : QObject - The Parent of the dialog. Must be a SimplexDialog - - Returns - ------- - """ - - uiLimitGRP: QGroupBox - uiMinLimitSPIN: QSpinBox - uiMaxLimitSPIN: QSpinBox - uiAutoUpdateCHK: QCheckBox - uiManualUpdateBTN: QPushButton - uiEditTREE: QTreeWidget - uiTravCheckTREE: QTreeWidget - uiWarningLBL: QLabel - uiCancelBTN: QPushButton - uiCreateSelectedBTN: QPushButton - - def __init__( - self, - sliders, - values=None, - dynamics=None, - mode="create", - parent=None, - grandparent=None, - ): - # Store the Parent UI rather than relying on Qt's .parent() - # Could cause crashes otherwise - if parent is None or grandparent is None: - raise ValueError("The parent and grandparent must be provided") - - super(TraversalCheckDialog, self).__init__(parent) - - uiPath = getUiFile(__file__) - QtCompat.loadUi(uiPath, self) - self.mode = mode.lower() - - self.parUI = parent - self.gparUI = grandparent - self.maxPoss = 100 - self.colCheckRoles = [ - Qt.ItemDataRole.UserRole, - Qt.ItemDataRole.UserRole, - Qt.ItemDataRole.UserRole, - Qt.ItemDataRole.EditRole, - ] - - self.uiCreateSelectedBTN.clicked.connect(self.createMissing) - self.uiMinLimitSPIN.valueChanged.connect(self.populateWithoutUpdate) - self.uiMaxLimitSPIN.valueChanged.connect(self.populateWithoutUpdate) - self.uiCancelBTN.clicked.connect(self.close) - self.uiManualUpdateBTN.clicked.connect(self.populateWithUpdate) - self.uiEditTREE.itemChanged.connect(self.populateWithoutUpdate) - - self.dragFilter = DragFilter(self) - self.uiEditTREE.viewport().installEventFilter(self.dragFilter) - self.dragFilter.dragTick.connect(self.dragTick) - - self.gparUI.uiSliderTREE.selectionModel().selectionChanged.connect( - self.populateWithCheck - ) - - self.valueDict = values or {} - self.dynDict = dynamics or {} - self.setSliders(sliders) - if self.mode == "create": - self.uiAutoUpdateCHK.setCheckState(Qt.CheckState.Unchecked) - self.uiAutoUpdateCHK.hide() - self.uiManualUpdateBTN.hide() - - self.uiMaxLimitSPIN.setValue(max(len(sliders), 2)) - self.uiMinLimitSPIN.setValue(max(len(sliders) - 2, 2)) - - if sliders is None: - self.populateWithUpdate() - else: - self._populate() - - def dragTick(self, ticks, mul): - """Deal with the ticks coming from the drag handler - - Parameters - ---------- - ticks : int - The number of ticks since the last update - mul : float - The multiplier value from the drag handler - - Returns - ------- - - """ - items = self.uiEditTREE.selectedItems() - for item in items: - val = item.data(3, Qt.ItemDataRole.EditRole) - val += (0.05) * ticks * mul - if abs(val) < 1.0e-5: - val = 0.0 - val = max(min(val, 1.0), -1.0) - item.setData(3, Qt.ItemDataRole.EditRole, val) - self.uiEditTREE.viewport().update() - - def setSliders(self, val): - """Set the sliders displayed in this UI - - Parameters - ---------- - val : [Slider, ...] - The sliders to be displayed - - Returns - ------- - - """ - self.uiEditTREE.clear() - dvs = [None, -1.0, 1.0, 0.5] - val = val or [] - for slider in val: - item = QTreeWidgetItem(self.uiEditTREE, [slider.name]) - item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) - rangeVals = self.valueDict.get(slider, [-1.0, 1.0]) - - item.setData(0, Qt.ItemDataRole.UserRole, slider) - for col in range(1, 3): - val = dvs[col] - item.setData(col, self.colCheckRoles[col], val) - rng = slider.prog.getRange() - if val in rng: - chk = ( - Qt.CheckState.Checked - if val in rangeVals - else Qt.CheckState.Unchecked - ) - item.setCheckState(col, chk) - item.setCheckState(3, self.dynDict.get(slider, Qt.CheckState.Checked)) - - for col in reversed(list(range(4))): - self.uiEditTREE.resizeColumnToContents(col) - - def closeEvent(self, event): - """Override the Qt close event""" - self.gparUI.uiSliderTREE.selectionModel().selectionChanged.disconnect( - self.populateWithCheck - ) - super(TraversalCheckDialog, self).closeEvent(event) - - def populateWithUpdate(self): - """Populate the list from the main dialog selection""" - self.setSliders(self.gparUI.uiSliderTREE.getSelectedItems(typ=Slider)) - self._populate() - - def populateWithoutUpdate(self): - """Populate the list and but don't look at the main dialog""" - self._populate() - - def populateWithCheck(self): - """Populate the list from the main dialog selection, only if the AutoUpdate checkbox is checked""" - if self.uiAutoUpdateCHK.isChecked(): - self.setSliders(self.gparUI.uiSliderTREE.getSelectedItems(typ=Slider)) - self._populate() - - def _populate(self): - """Populate the list widgets in the UI""" - minDepth = self.uiMinLimitSPIN.value() - maxDepth = self.uiMaxLimitSPIN.value() - - root = self.uiEditTREE.invisibleRootItem() - lockDict = {} - sliderList = [] - - for row in range(root.childCount()): - item = root.child(row) - slider = item.data(0, Qt.ItemDataRole.UserRole) - if slider is not None: - sliderList.append(slider) - lv = [ - item.data(col, self.colCheckRoles[col]) - for col in range(1, 3) - if item.checkState(col) == Qt.CheckState.Checked - ] - dyn = item.checkState(3) == Qt.CheckState.Checked - lockDict[slider] = (lv, dyn) - - tooMany, toAdd = buildPossibleTraversals( - self.parUI.simplex, - sliderList, - minDepth, - maxDepth, - lockDict=lockDict, - maxPoss=self.maxPoss, - ) - - lbl = ( - "Too many possibilities. Limiting to {0}".format(self.maxPoss) - if tooMany - else "" - ) - self.uiWarningLBL.setText(lbl) - - self.uiTravCheckTREE.clear() - for pairs, trav in reversed(toAdd): - TravCheckItem(pairs, trav, self.uiTravCheckTREE) - - for i in reversed(list(range(self.uiTravCheckTREE.columnCount()))): - self.uiTravCheckTREE.resizeColumnToContents(i) - - if self.mode == "create": - if self.uiTravCheckTREE.topLevelItemCount() > 0: - self.uiTravCheckTREE.topLevelItem(0).setSelected(True) - - def createMissing(self): - """Create selected traversals if they don't already exist""" - simplex = self.parUI.simplex - created = [] - - tops = [] - for item in self.uiTravCheckTREE.selectedItems(): - par = item.parent() - if par is not None: - item = par - if item not in tops: - tops.append(item) - - for item in tops: - name = item.text(0) - sliders, ranges = list(zip(*item.pairs)) - # Double check that the user didn't create any extra sliders - if Traversal.traversalAlreadyExists(simplex, sliders, ranges) is None: - count = Traversal.getCount(sliders, ranges) - startPairs, endPairs = list( - zip(*[((s, a), (s, b)) for s, (a, b) in item.pairs]) - ) - t = Traversal.createTraversal( - name, simplex, startPairs, endPairs, count=count - ) - created.append(t) - - self.parUI.uiTraversalTREE.setItemSelection(created) - if self.mode == "create": - self.close() - else: - self.populateWithoutUpdate() +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +from itertools import combinations, product + +from .dragFilter import DragFilter +from .items import Slider, Traversal +from Qt import QtCompat +from Qt.QtCore import Qt +from Qt.QtGui import QBrush, QColor +from Qt.QtWidgets import ( + QDialog, + QTreeWidgetItem, + QCheckBox, + QGroupBox, + QLabel, + QPushButton, + QSpinBox, + QTreeWidget, +) + + +from .utils import getUiFile + + +class TooManyPossibilitiesError(Exception): + """Error raised when there are too many possibilities + Basically used as a stop-iteration + """ + + pass + + +def buildPossibleTraversals( + simplex, sliders, minDepth, maxDepth, lockDict=None, maxPoss=100 +): + """Build a list of possible traversals + + Parameters + ---------- + simplex : Simplex + The simplex system to check + sliders : Slider + The sliders to check + minDepth : int + The minimum number of sliders that will go into any traversals + maxDepth : int + The maximum number of sliders that will go into any traversals + lockDict : {Slider: ((float, ...), bool), ...} + An optional per-slider dict of possible values + maxPoss : float + The Maximum number of possibilities to return.(Default value = 100) + + Returns + ------- + : bool + True if the maximum number of possibilities was exceeded + : [([(Slider, (float, float)), ...], Traversal), ...] + Grouped slider/range pairs to existing (or None) Traversals + """ + allRanges = {} + allDyn = {} + sliderDict = {} + lockDict = lockDict or {} + + # Get the range values for each slider + for slider in sliders: + rng, dyn = lockDict.get(slider, (slider.prog.getRange(), True)) + rng = set(rng) + rng.discard(0) # ignore the zeros + allRanges[slider] = sorted(rng) + allDyn[slider] = dyn + sliderDict[slider.name] = slider + + poss = [] + tooMany = False + try: + for size in range(minDepth, maxDepth + 1): + for grp in combinations(sliders, size): + names = [i.name for i in grp] + ranges = [allRanges[s] for s in grp] + for vals in product(*ranges): + for dynIdx in range(len(grp)): + if not allDyn[grp[dynIdx]]: + continue + trng = list(zip(vals, vals)) + trng[dynIdx] = (0, trng[dynIdx][0]) + + count = Traversal.getCount(grp, trng) + if count == 0: + continue + + poss.append(frozenset(list(zip(names, trng)))) + if len(poss) > maxPoss: + raise TooManyPossibilitiesError("Don't melt your computer") + except TooManyPossibilitiesError: + tooMany = True + + # Build a dict of traversals that already exist + # but only if their sliders are in the list of sliders to check + onlys = {} + for trav in simplex.traversals: + sls = trav.allSliders() + if all(r in sliders for r in sls): + rngs = trav.ranges() + key = frozenset([(k.name, v) for k, v in rngs.items()]) + onlys[key] = trav + + toAdd = [] + for p in poss: + truePairs = [(sliderDict[n], r) for n, r in p] + toAdd.append((truePairs, onlys.get(p))) + return tooMany, toAdd + + +class TravCheckItem(QTreeWidgetItem): + def __init__(self, pairs, trav, *args, **kwargs): + super(TravCheckItem, self).__init__(*args, **kwargs) + self.pairs = pairs + self.trav = trav + + exists = False + grayBrush = QBrush(QColor(128, 128, 128)) + if self.trav is None: + ranges = dict(self.pairs) + newName = Traversal.buildTraversalName(ranges) + self.setText(0, newName) + else: + exists = True + self.setText(0, self.trav.name) + self.setForeground(0, grayBrush) + self.setForeground(1, grayBrush) + self.setForeground(2, grayBrush) + + # create the slider sub-rows + for slider, rng in pairs: + item = QTreeWidgetItem(self) + + item.setData(0, Qt.ItemDataRole.EditRole, slider.name) + item.setData(1, Qt.ItemDataRole.EditRole, rng[0]) + item.setData(2, Qt.ItemDataRole.EditRole, rng[1]) + if exists: + item.setForeground(0, grayBrush) + item.setForeground(1, grayBrush) + item.setForeground(2, grayBrush) + + self.setExpanded(True) + + +class TraversalCheckDialog(QDialog): + """Dialog for checking what possible traversals exist, and picking new traversals + In 'Create' mode, it provides a quick way of choosing the one specific traversal + that the user is looking for + + In 'Check' mode, it provides a convenient way to explore the possibilites + and create any missing traversals directly + + Parameters + ---------- + sliders : [Slider, ...] + A list of sliders to check + values : {Slider: (float, ...), ...} + A dictionary of values to use per slider + mode : str + The mode to display the dialog. Defaults to 'create' + parent : QObject + The Parent of the dialog. Must be a SimplexDialog + + Returns + ------- + """ + + uiLimitGRP: QGroupBox + uiMinLimitSPIN: QSpinBox + uiMaxLimitSPIN: QSpinBox + uiAutoUpdateCHK: QCheckBox + uiManualUpdateBTN: QPushButton + uiEditTREE: QTreeWidget + uiTravCheckTREE: QTreeWidget + uiWarningLBL: QLabel + uiCancelBTN: QPushButton + uiCreateSelectedBTN: QPushButton + + def __init__( + self, + sliders, + values=None, + dynamics=None, + mode="create", + parent=None, + grandparent=None, + ): + # Store the Parent UI rather than relying on Qt's .parent() + # Could cause crashes otherwise + if parent is None or grandparent is None: + raise ValueError("The parent and grandparent must be provided") + + super(TraversalCheckDialog, self).__init__(parent) + + uiPath = getUiFile(__file__) + QtCompat.loadUi(uiPath, self) + self.mode = mode.lower() + + self.parUI = parent + self.gparUI = grandparent + self.maxPoss = 100 + self.colCheckRoles = [ + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.UserRole, + Qt.ItemDataRole.EditRole, + ] + + self.uiCreateSelectedBTN.clicked.connect(self.createMissing) + self.uiMinLimitSPIN.valueChanged.connect(self.populateWithoutUpdate) + self.uiMaxLimitSPIN.valueChanged.connect(self.populateWithoutUpdate) + self.uiCancelBTN.clicked.connect(self.close) + self.uiManualUpdateBTN.clicked.connect(self.populateWithUpdate) + self.uiEditTREE.itemChanged.connect(self.populateWithoutUpdate) + + self.dragFilter = DragFilter(self) + self.uiEditTREE.viewport().installEventFilter(self.dragFilter) + self.dragFilter.dragTick.connect(self.dragTick) + + self.gparUI.uiSliderTREE.selectionModel().selectionChanged.connect( + self.populateWithCheck + ) + + self.valueDict = values or {} + self.dynDict = dynamics or {} + self.setSliders(sliders) + if self.mode == "create": + self.uiAutoUpdateCHK.setCheckState(Qt.CheckState.Unchecked) + self.uiAutoUpdateCHK.hide() + self.uiManualUpdateBTN.hide() + + self.uiMaxLimitSPIN.setValue(max(len(sliders), 2)) + self.uiMinLimitSPIN.setValue(max(len(sliders) - 2, 2)) + + if sliders is None: + self.populateWithUpdate() + else: + self._populate() + + def dragTick(self, ticks, mul): + """Deal with the ticks coming from the drag handler + + Parameters + ---------- + ticks : int + The number of ticks since the last update + mul : float + The multiplier value from the drag handler + + Returns + ------- + + """ + items = self.uiEditTREE.selectedItems() + for item in items: + val = item.data(3, Qt.ItemDataRole.EditRole) + val += (0.05) * ticks * mul + if abs(val) < 1.0e-5: + val = 0.0 + val = max(min(val, 1.0), -1.0) + item.setData(3, Qt.ItemDataRole.EditRole, val) + self.uiEditTREE.viewport().update() + + def setSliders(self, val): + """Set the sliders displayed in this UI + + Parameters + ---------- + val : [Slider, ...] + The sliders to be displayed + + Returns + ------- + + """ + self.uiEditTREE.clear() + dvs = [None, -1.0, 1.0, 0.5] + val = val or [] + for slider in val: + item = QTreeWidgetItem(self.uiEditTREE, [slider.name]) + item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) + rangeVals = self.valueDict.get(slider, [-1.0, 1.0]) + + item.setData(0, Qt.ItemDataRole.UserRole, slider) + for col in range(1, 3): + val = dvs[col] + item.setData(col, self.colCheckRoles[col], val) + rng = slider.prog.getRange() + if val in rng: + chk = ( + Qt.CheckState.Checked + if val in rangeVals + else Qt.CheckState.Unchecked + ) + item.setCheckState(col, chk) + item.setCheckState(3, self.dynDict.get(slider, Qt.CheckState.Checked)) + + for col in reversed(list(range(4))): + self.uiEditTREE.resizeColumnToContents(col) + + def closeEvent(self, event): + """Override the Qt close event""" + self.gparUI.uiSliderTREE.selectionModel().selectionChanged.disconnect( + self.populateWithCheck + ) + super(TraversalCheckDialog, self).closeEvent(event) + + def populateWithUpdate(self): + """Populate the list from the main dialog selection""" + self.setSliders(self.gparUI.uiSliderTREE.getSelectedItems(typ=Slider)) + self._populate() + + def populateWithoutUpdate(self): + """Populate the list and but don't look at the main dialog""" + self._populate() + + def populateWithCheck(self): + """Populate the list from the main dialog selection, only if the AutoUpdate checkbox is checked""" + if self.uiAutoUpdateCHK.isChecked(): + self.setSliders(self.gparUI.uiSliderTREE.getSelectedItems(typ=Slider)) + self._populate() + + def _populate(self): + """Populate the list widgets in the UI""" + minDepth = self.uiMinLimitSPIN.value() + maxDepth = self.uiMaxLimitSPIN.value() + + root = self.uiEditTREE.invisibleRootItem() + lockDict = {} + sliderList = [] + + for row in range(root.childCount()): + item = root.child(row) + slider = item.data(0, Qt.ItemDataRole.UserRole) + if slider is not None: + sliderList.append(slider) + lv = [ + item.data(col, self.colCheckRoles[col]) + for col in range(1, 3) + if item.checkState(col) == Qt.CheckState.Checked + ] + dyn = item.checkState(3) == Qt.CheckState.Checked + lockDict[slider] = (lv, dyn) + + tooMany, toAdd = buildPossibleTraversals( + self.parUI.simplex, + sliderList, + minDepth, + maxDepth, + lockDict=lockDict, + maxPoss=self.maxPoss, + ) + + lbl = ( + "Too many possibilities. Limiting to {0}".format(self.maxPoss) + if tooMany + else "" + ) + self.uiWarningLBL.setText(lbl) + + self.uiTravCheckTREE.clear() + for pairs, trav in reversed(toAdd): + TravCheckItem(pairs, trav, self.uiTravCheckTREE) + + for i in reversed(list(range(self.uiTravCheckTREE.columnCount()))): + self.uiTravCheckTREE.resizeColumnToContents(i) + + if self.mode == "create": + if self.uiTravCheckTREE.topLevelItemCount() > 0: + self.uiTravCheckTREE.topLevelItem(0).setSelected(True) + + def createMissing(self): + """Create selected traversals if they don't already exist""" + simplex = self.parUI.simplex + created = [] + + tops = [] + for item in self.uiTravCheckTREE.selectedItems(): + par = item.parent() + if par is not None: + item = par + if item not in tops: + tops.append(item) + + for item in tops: + name = item.text(0) + sliders, ranges = list(zip(*item.pairs)) + # Double check that the user didn't create any extra sliders + if Traversal.traversalAlreadyExists(simplex, sliders, ranges) is None: + count = Traversal.getCount(sliders, ranges) + startPairs, endPairs = list( + zip(*[((s, a), (s, b)) for s, (a, b) in item.pairs]) + ) + t = Traversal.createTraversal( + name, simplex, startPairs, endPairs, count=count + ) + created.append(t) + + self.parUI.uiTraversalTREE.setItemSelection(created) + if self.mode == "create": + self.close() + else: + self.populateWithoutUpdate() diff --git a/src/python/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py index 93fd0fc0..2b99db0c 100644 --- a/src/python/simplexui/traversalDialog.py +++ b/src/python/simplexui/traversalDialog.py @@ -1,301 +1,301 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - - -# Ignore a bunch of linter warnings that show up because of my choice of abstraction -# pylint: disable=unused-argument,too-many-public-methods,relative-import -# pylint: disable=too-many-statements,no-self-use,missing-docstring -import re - -from .interface import DCC -from .interfaceModel import ( - TraversalFilterModel, - TraversalModel, - coerceIndexToRoots, - coerceIndexToType, -) -from .interfaceModelTrees import TraversalTree -from .items import Group, Simplex, Slider, Traversal, TravPair - -# This module imports QT from PyQt4, PySide or PySide2 -# Depending on what's available -from Qt import QtCompat -from Qt.QtGui import QStandardItemModel, QAction -from Qt.QtWidgets import ( - QApplication, - QDialog, - QFrame, - QInputDialog, - QMessageBox, - QProgressDialog, - QPushButton, - QVBoxLayout, -) -from .travCheckDialog import TraversalCheckDialog -from .utils import getUiFile, makeUnique - -NAME_CHECK = re.compile(r"[A-Za-z][\w.]*") - - -class TraversalDialog(QDialog): - """The dialog for dealing with Traversals - - Parameters - ---------- - parent : SimplexDialog - The parent simplex dialog - """ - - uiTraversalLAY: QVBoxLayout - uiTravButtonFRM: QFrame - uiTravNewBTN: QPushButton - uiTravAddSliderBTN: QPushButton - uiTravNewGroupBTN: QPushButton - uiTravNewShapeBTN: QPushButton - uiTravDeleteBTN: QPushButton - uiShapeExtractBTN: QPushButton - uiShapeConnectBTN: QPushButton - - uiConvertCorrectiveACT: QAction - uiDoubleSliderRangeACT: QAction - uiExportACT: QAction - uiExportObjPSDACT: QAction - uiExtractOnCreateACT: QAction - uiExtractPosedACT: QAction - uiHideRedundantACT: QAction - uiImportACT: QAction - uiImportObjPSDACT: QAction - uiLegacyJsonACT: QAction - uiLiveShapeConnectionACT: QAction - uiLiveUpdateACT: QAction - uiSetWorkingDirectoryACT: QAction - uiSplitShapePSDACT: QAction - - def __init__(self, parent): - super(TraversalDialog, self).__init__(parent) - - uiPath = getUiFile(__file__) - QtCompat.loadUi(uiPath, self) - self.parUI = parent - - # Load the custom tree manually - self.uiTraversalTREE = TraversalTree(self) - self.uiTraversalTREE.setDragEnabled(False) - self.uiTraversalTREE.setDragDropMode(TraversalTree.DragDropMode.NoDragDrop) - self.uiTraversalTREE.setSelectionMode( - TraversalTree.SelectionMode.ExtendedSelection - ) - self.uiTraversalTREE.dragFilter.dragPressed.connect(self.dragStart) - self.uiTraversalTREE.dragFilter.dragReleased.connect(self.dragStop) - - self.uiTraversalLAY.addWidget(self.uiTraversalTREE) - self.simplex = None - self.parUI.simplexLoaded.connect(self.loadSimplex) - - self.uiTravDeleteBTN.clicked.connect(self.deleteTrav) - self.uiTravNewBTN.clicked.connect(self.newTrav) - self.uiTravNewGroupBTN.clicked.connect(self.newGroup) - self.uiTravNewShapeBTN.clicked.connect(self.newShape) - self.uiTravAddSliderBTN.clicked.connect(self.addSlider) - self.uiShapeExtractBTN.clicked.connect(self.shapeExtract) - self.uiShapeConnectBTN.clicked.connect(self.shapeConnectFromSelection) - - self.parUI.uiHideRedundantACT.toggled.connect(self.hideRedundant) - - self.loadSimplex() - - def hideRedundant(self): - """Hide Redundant items in the ui based on the checkbox""" - check = self.uiHideRedundantACT.isChecked() - travModel = self.uiTraversalTREE.model() - travModel.doFilter = check - travModel.invalidateFilter() - - def dragStart(self): - """Slot for handling the start of a MMB Drag event""" - if self.simplex is not None: - self.simplex.DCC.undoOpen() - - def dragStop(self): - """Slot for handling the end of a MMB Drag event""" - if self.simplex is not None: - self.simplex.DCC.undoClose() - - def loadSimplex(self): - """Load the simplex system from the parent dialog""" - system = self.parUI.simplex - if system is None: - self.simplex = system - self.uiTraversalTREE.setModel(QStandardItemModel()) - - self.uiTravDeleteBTN.setEnabled(False) - self.uiTravNewBTN.setEnabled(False) - self.uiTravNewGroupBTN.setEnabled(False) - self.uiTravNewShapeBTN.setEnabled(False) - self.uiTravAddSliderBTN.setEnabled(False) - self.uiShapeExtractBTN.setEnabled(False) - self.uiShapeConnectBTN.setEnabled(False) - return - else: - self.uiTravDeleteBTN.setEnabled(True) - self.uiTravNewBTN.setEnabled(True) - self.uiTravNewGroupBTN.setEnabled(True) - self.uiTravNewShapeBTN.setEnabled(True) - self.uiTravAddSliderBTN.setEnabled(True) - self.uiShapeExtractBTN.setEnabled(True) - self.uiShapeConnectBTN.setEnabled(True) - - if system == self.simplex: - return - - self.simplex = system - - sliderProxyModel = self.parUI.uiSliderTREE.model() - if not sliderProxyModel: - self.uiTraversalTREE.setModel(None) - return - sliderModel = sliderProxyModel.sourceModel() - simplexModel = sliderModel.sourceModel() - - travModel = TraversalModel(simplexModel) - travProxModel = TraversalFilterModel(travModel) - self.uiTraversalTREE.setModel(travProxModel) - - def deleteTrav(self): - """Delete the selected traversals""" - idxs = self.uiTraversalTREE.getSelectedIndexes() - roots = coerceIndexToRoots(idxs) - if not roots: - QMessageBox.warning(self, "Warning", "Nothing Selected") - return - roots = makeUnique([i.model().itemFromIndex(i) for i in roots]) - for r in roots: - if isinstance(r, Simplex): - QMessageBox.warning( - self, "Warning", "Cannot delete a simplex system this way (for now)" - ) - return - - pairs = [r for r in roots if isinstance(r, TravPair)] - roots = [r for r in roots if not isinstance(r, TravPair)] - - for r in roots: - r.delete() - - TravPair.removeAll(pairs) - - self.uiTraversalTREE.model().invalidateFilter() - - def newGroup(self): - """Create a new group for organizing the traversals""" - if self.simplex is None: - return - newName, good = QInputDialog.getText( - self, "New Group", "Enter a name for the new group", text="Group" - ) - if not good: - return - if not NAME_CHECK.match(newName): - message = "Group name can only contain letters and numbers, and cannot start with a number" - QMessageBox.warning(self, "Warning", message) - return - - items = self.uiTraversalTREE.getSelectedItems(Slider) - Group.createGroup(str(newName), self.simplex, items) - - def newShape(self): - """Add a new shape to the traversal's Progression""" - pars = self.uiTraversalTREE.getSelectedIndexes() - if not pars: - return - travs = coerceIndexToType(pars, Traversal) - for travIdx in travs: - trav = travIdx.model().itemFromIndex(travIdx) - trav.prog.createShape() - - def shapeExtract(self): - """Extract a shape from the traversal's progression""" - indexes = self.uiTraversalTREE.getSelectedIndexes() - return self.parUI.shapeIndexExtract(indexes) - - def newTrav(self): - """Create a new traversal based on the selection in the main UI""" - sliders = self.parUI.uiSliderTREE.getSelectedItems(Slider) - if len(sliders) < 2: - message = "Must have at least 2 sliders selected" - QMessageBox.warning(self, "Warning", message) - return None - - tcd = TraversalCheckDialog( - sliders, mode="create", parent=self, grandparent=self.parUI - ) - tcd.move(self.pos()) - tcd.exec_() - - def addSlider(self): - """Add a slider to the traversal's definition""" - # add the slider to both the start and end - travs = self.uiTraversalTREE.getSelectedItems(Traversal) - sliders = self.parUI.uiSliderTREE.getSelectedItems(Slider) - if not travs: - return - if not sliders: - return - for slider in sliders: - travs[-1].addSlider(slider) - - def shapeConnectFromSelection(self): - """Connect a shape into the traversal based on the DCC scene selection""" - if self.simplex is None: - return - # make a dict of name:object - sel = DCC.getSelectedObjects() - selDict = {} - for s in sel: - name = DCC.getObjectName(s) - if name.endswith("_Extract"): - nn = name.rsplit("_Extract", 1)[0] - selDict[nn] = s - - pairDict = {} - for p in self.simplex.progs: - for pp in p.pairs: - pairDict[pp.shape.name] = pp - - # get all common names - common = selDict.keys() & pairDict.keys() - - # get those items - pairs = [pairDict[i] for i in common] - - # Set up the progress bar - pBar = QProgressDialog("Connecting Shapes", "Cancel", 0, 100, self) - pBar.setMaximum(len(pairs)) - - # Do the extractions - for pair in pairs: - c = pair.prog.controller - c.connectShape(pair.shape, delete=True) - - # ProgressBar - pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Connecting:\n{0}".format(pair.shape.name)) - QApplication.processEvents() - if pBar.wasCanceled(): - return - - pBar.close() +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + + +# Ignore a bunch of linter warnings that show up because of my choice of abstraction +# pylint: disable=unused-argument,too-many-public-methods,relative-import +# pylint: disable=too-many-statements,no-self-use,missing-docstring +import re + +from .interface import DCC +from .interfaceModel import ( + TraversalFilterModel, + TraversalModel, + coerceIndexToRoots, + coerceIndexToType, +) +from .interfaceModelTrees import TraversalTree +from .items import Group, Simplex, Slider, Traversal, TravPair + +# This module imports QT from PyQt4, PySide or PySide2 +# Depending on what's available +from Qt import QtCompat +from Qt.QtGui import QStandardItemModel, QAction +from Qt.QtWidgets import ( + QApplication, + QDialog, + QFrame, + QInputDialog, + QMessageBox, + QProgressDialog, + QPushButton, + QVBoxLayout, +) +from .travCheckDialog import TraversalCheckDialog +from .utils import getUiFile, makeUnique + +NAME_CHECK = re.compile(r"[A-Za-z][\w.]*") + + +class TraversalDialog(QDialog): + """The dialog for dealing with Traversals + + Parameters + ---------- + parent : SimplexDialog + The parent simplex dialog + """ + + uiTraversalLAY: QVBoxLayout + uiTravButtonFRM: QFrame + uiTravNewBTN: QPushButton + uiTravAddSliderBTN: QPushButton + uiTravNewGroupBTN: QPushButton + uiTravNewShapeBTN: QPushButton + uiTravDeleteBTN: QPushButton + uiShapeExtractBTN: QPushButton + uiShapeConnectBTN: QPushButton + + uiConvertCorrectiveACT: QAction + uiDoubleSliderRangeACT: QAction + uiExportACT: QAction + uiExportObjPSDACT: QAction + uiExtractOnCreateACT: QAction + uiExtractPosedACT: QAction + uiHideRedundantACT: QAction + uiImportACT: QAction + uiImportObjPSDACT: QAction + uiLegacyJsonACT: QAction + uiLiveShapeConnectionACT: QAction + uiLiveUpdateACT: QAction + uiSetWorkingDirectoryACT: QAction + uiSplitShapePSDACT: QAction + + def __init__(self, parent): + super(TraversalDialog, self).__init__(parent) + + uiPath = getUiFile(__file__) + QtCompat.loadUi(uiPath, self) + self.parUI = parent + + # Load the custom tree manually + self.uiTraversalTREE = TraversalTree(self) + self.uiTraversalTREE.setDragEnabled(False) + self.uiTraversalTREE.setDragDropMode(TraversalTree.DragDropMode.NoDragDrop) + self.uiTraversalTREE.setSelectionMode( + TraversalTree.SelectionMode.ExtendedSelection + ) + self.uiTraversalTREE.dragFilter.dragPressed.connect(self.dragStart) + self.uiTraversalTREE.dragFilter.dragReleased.connect(self.dragStop) + + self.uiTraversalLAY.addWidget(self.uiTraversalTREE) + self.simplex = None + self.parUI.simplexLoaded.connect(self.loadSimplex) + + self.uiTravDeleteBTN.clicked.connect(self.deleteTrav) + self.uiTravNewBTN.clicked.connect(self.newTrav) + self.uiTravNewGroupBTN.clicked.connect(self.newGroup) + self.uiTravNewShapeBTN.clicked.connect(self.newShape) + self.uiTravAddSliderBTN.clicked.connect(self.addSlider) + self.uiShapeExtractBTN.clicked.connect(self.shapeExtract) + self.uiShapeConnectBTN.clicked.connect(self.shapeConnectFromSelection) + + self.parUI.uiHideRedundantACT.toggled.connect(self.hideRedundant) + + self.loadSimplex() + + def hideRedundant(self): + """Hide Redundant items in the ui based on the checkbox""" + check = self.uiHideRedundantACT.isChecked() + travModel = self.uiTraversalTREE.model() + travModel.doFilter = check + travModel.invalidateFilter() + + def dragStart(self): + """Slot for handling the start of a MMB Drag event""" + if self.simplex is not None: + self.simplex.DCC.undoOpen() + + def dragStop(self): + """Slot for handling the end of a MMB Drag event""" + if self.simplex is not None: + self.simplex.DCC.undoClose() + + def loadSimplex(self): + """Load the simplex system from the parent dialog""" + system = self.parUI.simplex + if system is None: + self.simplex = system + self.uiTraversalTREE.setModel(QStandardItemModel()) + + self.uiTravDeleteBTN.setEnabled(False) + self.uiTravNewBTN.setEnabled(False) + self.uiTravNewGroupBTN.setEnabled(False) + self.uiTravNewShapeBTN.setEnabled(False) + self.uiTravAddSliderBTN.setEnabled(False) + self.uiShapeExtractBTN.setEnabled(False) + self.uiShapeConnectBTN.setEnabled(False) + return + else: + self.uiTravDeleteBTN.setEnabled(True) + self.uiTravNewBTN.setEnabled(True) + self.uiTravNewGroupBTN.setEnabled(True) + self.uiTravNewShapeBTN.setEnabled(True) + self.uiTravAddSliderBTN.setEnabled(True) + self.uiShapeExtractBTN.setEnabled(True) + self.uiShapeConnectBTN.setEnabled(True) + + if system == self.simplex: + return + + self.simplex = system + + sliderProxyModel = self.parUI.uiSliderTREE.model() + if not sliderProxyModel: + self.uiTraversalTREE.setModel(None) + return + sliderModel = sliderProxyModel.sourceModel() + simplexModel = sliderModel.sourceModel() + + travModel = TraversalModel(simplexModel) + travProxModel = TraversalFilterModel(travModel) + self.uiTraversalTREE.setModel(travProxModel) + + def deleteTrav(self): + """Delete the selected traversals""" + idxs = self.uiTraversalTREE.getSelectedIndexes() + roots = coerceIndexToRoots(idxs) + if not roots: + QMessageBox.warning(self, "Warning", "Nothing Selected") + return + roots = makeUnique([i.model().itemFromIndex(i) for i in roots]) + for r in roots: + if isinstance(r, Simplex): + QMessageBox.warning( + self, "Warning", "Cannot delete a simplex system this way (for now)" + ) + return + + pairs = [r for r in roots if isinstance(r, TravPair)] + roots = [r for r in roots if not isinstance(r, TravPair)] + + for r in roots: + r.delete() + + TravPair.removeAll(pairs) + + self.uiTraversalTREE.model().invalidateFilter() + + def newGroup(self): + """Create a new group for organizing the traversals""" + if self.simplex is None: + return + newName, good = QInputDialog.getText( + self, "New Group", "Enter a name for the new group", text="Group" + ) + if not good: + return + if not NAME_CHECK.match(newName): + message = "Group name can only contain letters and numbers, and cannot start with a number" + QMessageBox.warning(self, "Warning", message) + return + + items = self.uiTraversalTREE.getSelectedItems(Slider) + Group.createGroup(str(newName), self.simplex, items) + + def newShape(self): + """Add a new shape to the traversal's Progression""" + pars = self.uiTraversalTREE.getSelectedIndexes() + if not pars: + return + travs = coerceIndexToType(pars, Traversal) + for travIdx in travs: + trav = travIdx.model().itemFromIndex(travIdx) + trav.prog.createShape() + + def shapeExtract(self): + """Extract a shape from the traversal's progression""" + indexes = self.uiTraversalTREE.getSelectedIndexes() + return self.parUI.shapeIndexExtract(indexes) + + def newTrav(self): + """Create a new traversal based on the selection in the main UI""" + sliders = self.parUI.uiSliderTREE.getSelectedItems(Slider) + if len(sliders) < 2: + message = "Must have at least 2 sliders selected" + QMessageBox.warning(self, "Warning", message) + return None + + tcd = TraversalCheckDialog( + sliders, mode="create", parent=self, grandparent=self.parUI + ) + tcd.move(self.pos()) + tcd.exec_() + + def addSlider(self): + """Add a slider to the traversal's definition""" + # add the slider to both the start and end + travs = self.uiTraversalTREE.getSelectedItems(Traversal) + sliders = self.parUI.uiSliderTREE.getSelectedItems(Slider) + if not travs: + return + if not sliders: + return + for slider in sliders: + travs[-1].addSlider(slider) + + def shapeConnectFromSelection(self): + """Connect a shape into the traversal based on the DCC scene selection""" + if self.simplex is None: + return + # make a dict of name:object + sel = DCC.getSelectedObjects() + selDict = {} + for s in sel: + name = DCC.getObjectName(s) + if name.endswith("_Extract"): + nn = name.rsplit("_Extract", 1)[0] + selDict[nn] = s + + pairDict = {} + for p in self.simplex.progs: + for pp in p.pairs: + pairDict[pp.shape.name] = pp + + # get all common names + common = selDict.keys() & pairDict.keys() + + # get those items + pairs = [pairDict[i] for i in common] + + # Set up the progress bar + pBar = QProgressDialog("Connecting Shapes", "Cancel", 0, 100, self) + pBar.setMaximum(len(pairs)) + + # Do the extractions + for pair in pairs: + c = pair.prog.controller + c.connectShape(pair.shape, delete=True) + + # ProgressBar + pBar.setValue(pBar.value() + 1) + pBar.setLabelText("Connecting:\n{0}".format(pair.shape.name)) + QApplication.processEvents() + if pBar.wasCanceled(): + return + + pBar.close() From ebab1e38af43d95002d4a416b711c2289898e898 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 12 Aug 2026 12:24:28 -0700 Subject: [PATCH 12/47] Fix alembic typing and the call --- src/python/simplexui/commands/alembicCommon.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index a5758b7f..0926cf50 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -57,13 +57,15 @@ Type, TypeVar, overload, + Any, ) if TYPE_CHECKING: - from Qt.QtWidgets import QProgressDialog from alembic.AbcGeom import _IBase # A helper typing-only class - + from Qt.QtWidgets import QProgressDialog +else: + _IBase = QProgressDialog = Any npfloat = npt.NDArray[Union[np.float32, np.float64]] npint = npt.NDArray[Union[np.int32, np.int64]] From cd7c2a8db54adfaf4e285461bfa30fc7895ae336 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 12 Aug 2026 12:52:26 -0700 Subject: [PATCH 13/47] Fix the drag filter for qt6 --- src/python/simplexui/dragFilter.py | 70 ++++++++++++++++++------------ 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index 8ffde6d2..a601efcf 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -17,7 +17,7 @@ from Qt.QtCore import QEvent, QObject, QPoint, Qt, Signal -from Qt.QtGui import QCursor, QMouseEvent +from Qt.QtGui import QCursor, QMouseEvent, QGuiApplication from Qt.QtWidgets import QApplication @@ -142,13 +142,16 @@ def doDrag(self, o, e): e : QEvent The QEvent of the mouse drag """ - if self._dragType == self.DRAG_HORIZONTAL: - delta = e.pos().x() - self._lastPos.x() - else: - delta = self._lastPos.y() - e.pos().y() + delta = 0.0 + + if self._lastPos is not None: + if self._dragType == self.DRAG_HORIZONTAL: + delta = e.position().x() - self._lastPos.x() + else: + delta = self._lastPos.y() - e.position().y() self._leftover += delta - self._lastPos = e.pos() + self._lastPos = e.position() count = int(self._leftover / self.dragSensitivity) if count: @@ -170,9 +173,11 @@ def doDrag(self, o, e): QCursor.setPos(self.mapToGlobal(self._dragStart)) self._lastPos = self._dragStart else: + if self._screen is None: + raise RuntimeError("Could not determine screen") r = self._screen b = self.wrapBoundary - p = o.mapToGlobal(e.pos()) + p = o.mapToGlobal(e.position()) # when wrapping move to the other side in by 2*boundary # so we don't loop the wrapping @@ -190,8 +195,12 @@ def doDrag(self, o, e): if p != e.globalPos(): QCursor.setPos(p) - self._lastPos = self.parent().mapFromGlobal(p) self._leftover = 0 + par = self.parent() + if par is not None: + self._lastPos = par.mapFromGlobal(p) + else: + self._lastPos = QPoint() def startDrag(self, o, e): """Start the drag event handling @@ -203,38 +212,43 @@ def startDrag(self, o, e): e : QEvent The QEvent of the mouse drag """ + + epos = e.position() if self._dragStart is None: - self._dragStart = e.pos() - dtop = QApplication.desktop() - sn = dtop.screenNumber(o.mapToGlobal(e.pos())) - self._screen = dtop.availableGeometry(sn) + self._dragStart = epos + global_pos = o.mapToGlobal(epos) + screen = QGuiApplication.screenAt(global_pos) + + # Fallback to the widget's current screen just in case the coordinate + # somehow falls outside valid screen bounds + if screen is None: + screen = o.screen() + self._screen = screen.availableGeometry() - if abs(e.x() - self._dragStart.x()) > self.startSensitivity: + if abs(epos.x() - self._dragStart.x()) > self.startSensitivity: self._dragType = self.DRAG_HORIZONTAL - elif abs(e.y() - self._dragStart.y()) > self.startSensitivity: + elif abs(epos.y() - self._dragStart.y()) > self.startSensitivity: self._dragType = self.DRAG_VERTICAL if self._dragType: self._leftover = 0 - self._lastPos = e.pos() self._firstDrag = True self.dragPressed.emit() self.doOverrideCursor() - if self.isSpinbox: - if e.buttons() & self.dragButton: - # Send mouseRelease to spin buttons when dragging - # otherwise the spinbox will keep ticking. @longClickFix - # There's gotta be a better way to do this :-/ - mouseup = QMouseEvent( - QEvent.Type.MouseButtonRelease, - e.pos(), - self.dragButton, - e.buttons(), - e.modifiers(), - ) - QApplication.sendEvent(o, mouseup) + if self.isSpinbox and (e.buttons() & self.dragButton): + # Send mouseRelease to spin buttons when dragging + # otherwise the spinbox will keep ticking. @longClickFix + # There's gotta be a better way to do this :-/ + mouseup = QMouseEvent( + QEvent.Type.MouseButtonRelease, + e.position(), + self.dragButton, + e.buttons(), + e.modifiers(), + ) + QApplication.sendEvent(o, mouseup) def myendDrag(self, o, e): """End the drag event handling. Can't call it endDrag because that's taken From 0fbad038732cd3613d7ece593c0b1a41332313a9 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 12 Aug 2026 14:52:09 -0700 Subject: [PATCH 14/47] Fix a bunch of qt6 stuff, and a double mesh creation on import --- src/python/simplexui/dragFilter.py | 16 +++++++++------- src/python/simplexui/interface/mayaInterface.py | 5 ----- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index a601efcf..50dd3737 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -143,15 +143,17 @@ def doDrag(self, o, e): The QEvent of the mouse drag """ delta = 0.0 + epos = e.position().toPoint() + if self._lastPos is not None: if self._dragType == self.DRAG_HORIZONTAL: - delta = e.position().x() - self._lastPos.x() + delta = epos.x() - self._lastPos.x() else: - delta = self._lastPos.y() - e.position().y() + delta = self._lastPos.y() - epos.y() self._leftover += delta - self._lastPos = e.position() + self._lastPos = epos count = int(self._leftover / self.dragSensitivity) if count: @@ -177,7 +179,7 @@ def doDrag(self, o, e): raise RuntimeError("Could not determine screen") r = self._screen b = self.wrapBoundary - p = o.mapToGlobal(e.position()) + p = o.mapToGlobal(epos) # when wrapping move to the other side in by 2*boundary # so we don't loop the wrapping @@ -193,7 +195,7 @@ def doDrag(self, o, e): if p.y() < r.top() + b: p.setY(r.bottom() - 2 * b) - if p != e.globalPos(): + if p != e.globalPosition().toPoint(): QCursor.setPos(p) self._leftover = 0 par = self.parent() @@ -213,7 +215,7 @@ def startDrag(self, o, e): The QEvent of the mouse drag """ - epos = e.position() + epos = e.position().toPoint() if self._dragStart is None: self._dragStart = epos global_pos = o.mapToGlobal(epos) @@ -243,7 +245,7 @@ def startDrag(self, o, e): # There's gotta be a better way to do this :-/ mouseup = QMouseEvent( QEvent.Type.MouseButtonRelease, - e.position(), + epos, self.dragButton, e.buttons(), e.modifiers(), diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 49ad21c1..58849e46 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -665,11 +665,6 @@ def buildRestAbc(cls, abcMesh, name): importHead, importHeadShape = cls.buildDummyMesh("{0}_SIMPLEX".format(name)) - importHead = "{0}_SIMPLEX".format(name) - importHeadShape = cmds.createNode("mesh", name=importHead + "Shape") - badPar = cmds.listRelatives(importHeadShape, parent=True)[0] - importHead = cmds.rename(badPar, importHead) - cmds.connectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") cmds.polyEvaluate(importHead, vertex=True) # Force a refresh cmds.disconnectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") From 53b34a4c6b46117201bbe5a306673b17180576df Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 12 Aug 2026 15:48:41 -0700 Subject: [PATCH 15/47] Remove qt.py and make sure some of the plugin menu items don't try to run when nothing is loaded --- src/python/simplexui/comboCheckDialog.py | 15 ++++++++--- .../menu/genericPlugins/exportSplit.py | 14 ++++++++++- .../simplexui/menu/mayaPlugins/exportOther.py | 11 +++++++- .../simplexui/menu/mayaPlugins/importObjs.py | 8 ++++++ .../menu/mayaPlugins/linearizeTraversal.py | 11 +++++++- .../menu/mayaPlugins/reloadDefinition.py | 10 +++++++- .../menu/mayaPlugins/updateRestShape.py | 11 +++++++- src/python/simplexui/simplexDialog.py | 25 +++++++++++++------ src/python/simplexui/traversalDialog.py | 2 ++ 9 files changed, 90 insertions(+), 17 deletions(-) diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index 9e2ec81a..4ae1b250 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -182,6 +182,9 @@ def __init__(self, sliders, values=None, mode="create", parent=None): # Store the Parent UI rather than relying on Qt's .parent() # Could cause crashes otherwise self.parUI = parent + if self.parUI.simplex is None: + self.setDisabled(True) + return self.uiCreateSelectedBTN.clicked.connect(self.createMissing) self.uiMinLimitSPIN.valueChanged.connect(self.populateWithoutUpdate) @@ -281,10 +284,11 @@ def setSliders(self, val): def closeEvent(self, event): """Override the Qt close event""" - self.parUI.uiSliderTREE.selectionModel().selectionChanged.disconnect( - self.populateWithCheck - ) - super(ComboCheckDialog, self).closeEvent(event) + if self.isEnabled(): + self.parUI.uiSliderTREE.selectionModel().selectionChanged.disconnect( + self.populateWithCheck + ) + super().closeEvent(event) def populateWithUpdate(self): """Populate the list from the main dialog selection""" @@ -303,6 +307,9 @@ def populateWithCheck(self): def _populate(self): """Populate the list widgets in the UI""" + if self.parUI.simplex is None: + return + minDepth = self.uiMinLimitSPIN.value() maxDepth = self.uiMaxLimitSPIN.value() maxPoss = 100 diff --git a/src/python/simplexui/menu/genericPlugins/exportSplit.py b/src/python/simplexui/menu/genericPlugins/exportSplit.py index bd05a97d..bd562200 100644 --- a/src/python/simplexui/menu/genericPlugins/exportSplit.py +++ b/src/python/simplexui/menu/genericPlugins/exportSplit.py @@ -18,7 +18,8 @@ from functools import partial from Qt import QtCompat -from Qt.QtWidgets import QAction, QMessageBox, QProgressDialog +from Qt.QtWidgets import QMessageBox, QProgressDialog +from Qt.QtGui import QAction try: import numpy as np @@ -41,6 +42,16 @@ def exportSplitInterface(window): "Numpy is not available here, an it is required to split a system", ) return + + if window.simplex is None: + QMessageBox.warning( + window, + "Nothing Loaded", + "No simplex system is loaded", + ) + return + + path, _filter = QtCompat.QFileDialog.getSaveFileName( window, "Export Split", "", "Simplex (*.smpx)" ) @@ -48,6 +59,7 @@ def exportSplitInterface(window): if not path: return + pBar = QProgressDialog("Exporting Split smpx File", "Cancel", 0, 100, window) pBar.show() try: diff --git a/src/python/simplexui/menu/mayaPlugins/exportOther.py b/src/python/simplexui/menu/mayaPlugins/exportOther.py index 502b98e1..0d9083cf 100644 --- a/src/python/simplexui/menu/mayaPlugins/exportOther.py +++ b/src/python/simplexui/menu/mayaPlugins/exportOther.py @@ -20,7 +20,8 @@ import maya.cmds as cmds from Qt import QtCompat -from Qt.QtWidgets import QAction, QProgressDialog +from Qt.QtWidgets import QMessageBox, QProgressDialog +from Qt.QtGui import QAction def registerTool(window, menu): @@ -30,6 +31,14 @@ def registerTool(window, menu): def exportOtherInterface(window): + if window.simplex is None: + QMessageBox.warning( + window, + "Nothing Loaded", + "No simplex system is loaded", + ) + return + sel = cmds.ls(sl=True) path, _filter = QtCompat.QFileDialog.getSaveFileName( window, "Export Other", "", "Simplex (*.smpx)" diff --git a/src/python/simplexui/menu/mayaPlugins/importObjs.py b/src/python/simplexui/menu/mayaPlugins/importObjs.py index 4600ae19..a37261a9 100644 --- a/src/python/simplexui/menu/mayaPlugins/importObjs.py +++ b/src/python/simplexui/menu/mayaPlugins/importObjs.py @@ -170,6 +170,14 @@ def registerTool(window, menu): def importObjsInterface(window): + if window.simplex is None: + QMessageBox.warning( + window, + "Nothing Loaded", + "No simplex system is loaded", + ) + return + reorder = True if np is None or autoCrawlMeshes is None: reorder = False diff --git a/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py index dab8823e..bb8ad310 100644 --- a/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py +++ b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py @@ -17,7 +17,8 @@ from functools import partial import maya.cmds as cmds -from Qt.QtWidgets import QAction +from Qt.QtGui import QAction +from Qt.QtWidgets import QMessageBox def registerTool(window, menu): @@ -27,6 +28,14 @@ def registerTool(window, menu): def lineTrav(window): + if window.simplex is None: + QMessageBox.warning( + window, + "Nothing Loaded", + "No simplex system is loaded", + ) + return + simplex = window.simplex travDialog = window.travDialog sel = travDialog.uiTraversalTREE.getSelectedIndexes() diff --git a/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py index 5172228b..31b190f7 100644 --- a/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py +++ b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py @@ -16,7 +16,8 @@ # along with Simplex. If not, see . from functools import partial -from Qt.QtWidgets import QAction +from Qt.QtGui import QAction +from Qt.QtWidgets import QMessageBox def registerTool(window, menu): @@ -26,6 +27,13 @@ def registerTool(window, menu): def reloadDefinitionInterface(window): + if window.simplex is None: + QMessageBox.warning( + window, + "Nothing Loaded", + "No simplex system is loaded", + ) + return reloadDefinition(window.simplex) diff --git a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py index 00ba7cd1..a008b009 100644 --- a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py +++ b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py @@ -20,7 +20,8 @@ import maya.cmds as cmds -from Qt.QtWidgets import QAction, QMessageBox +from Qt.QtGui import QAction +from Qt.QtWidgets import QMessageBox def registerTool(window, menu): @@ -30,6 +31,14 @@ def registerTool(window, menu): def updateRestShapeInterface(window): + if window.simplex is None: + QMessageBox.warning( + window, + "Nothing Loaded", + "No simplex system is loaded", + ) + return + sel = cmds.ls(sl=True) if not sel: QMessageBox.warning(window, "Nothing Selected", "Nothing Selected") diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 00ea550e..47c72341 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -25,6 +25,7 @@ import sys import weakref from contextlib import contextmanager +from typing import TYPE_CHECKING from .comboCheckDialog import ComboCheckDialog from .falloffDialog import FalloffDialog @@ -42,6 +43,8 @@ from .interfaceModelTrees import ComboTree, SliderTree from .items import Combo, Group, ProgPair, Simplex, Slider, Stack from .menu import buildToolMenu, loadPlugins +from .traversalDialog import TraversalDialog +from .utils import getNextName, getUiFile, makeUnique, naturalSortKey, Prefs # This module imports QT from PyQt4, PySide or PySide2 # Depending on what's available @@ -65,16 +68,16 @@ QWidget, ) - -from .traversalDialog import TraversalDialog -from .utils import getNextName, getUiFile, makeUnique, naturalSortKey, Prefs - if os.environ.get("SIMPLEX_AT_BLUR") == "true": # If we're at blur, use our main window subclass from blurdev.gui import Window else: from Qt.QtWidgets import QMainWindow as Window +if TYPE_CHECKING: + # don't worry about blur stuff when typechecking + from Qt.QtWidgets import QMainWindow as Window + NAME_CHECK = re.compile(r"[A-Za-z][\w.]*") # If the decorated method is a slot for some Qt Signal @@ -108,6 +111,7 @@ class SimplexDialog(Window): Qt Signals that the Simplex Ui can understand """ + uiConvertCorrectiveACT: QAction uiDoubleSliderRangeACT: QAction uiExportACT: QAction @@ -273,11 +277,17 @@ def lastOpenedDialog(cls): def showTraversalDialog(self): """Display the traversal dialog""" + if self.simplex is None: + return + self.travDialog.show() self.travDialog.setGeometry(30, 30, 400, 400) def showFalloffDialog(self): """Display the Falloff Dialog""" + if self.simplex is None: + return + self.falloffDialog.show() pp = self.falloffDialog.pos() x, y = pp.x(), pp.y() @@ -733,8 +743,6 @@ def newSliderGroup(self): return Group.createGroup(str(newName), self.simplex, groupType=Slider) - # self.uiSliderTREE.model().invalidateFilter() - # self.uiComboTREE.model().invalidateFilter() def newSlider(self): """Slot to create a new slider""" @@ -850,8 +858,6 @@ def newComboGroup(self): return Group.createGroup(str(newName), self.simplex, groupType=Combo) - # self.uiComboTREE.model().invalidateFilter() - # self.uiSliderTREE.model().invalidateFilter() # Bottom right corner buttons def setSliderVals(self): @@ -1476,6 +1482,9 @@ def setSelectedComboSolveType(self, stVal): # Edit Menu def hideRedundant(self): """Hide redundant items from the Slider and Combo trees based on a user preference""" + if self.simplex is None: + return + check = self.uiHideRedundantACT.isChecked() comboModel = self.uiComboTREE.model() comboModel.filterShapes = check diff --git a/src/python/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py index 2b99db0c..a190491f 100644 --- a/src/python/simplexui/traversalDialog.py +++ b/src/python/simplexui/traversalDialog.py @@ -120,6 +120,8 @@ def __init__(self, parent): def hideRedundant(self): """Hide Redundant items in the ui based on the checkbox""" + if self.simplex is None: + return check = self.uiHideRedundantACT.isChecked() travModel = self.uiTraversalTREE.model() travModel.doFilter = check From 5d19a2d98243c6bbaf99b0a0201d98d2a4a3c418 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 12 Aug 2026 17:38:42 -0700 Subject: [PATCH 16/47] Add typing to the drag filter, and point importObjs to the tools repo --- src/python/simplexui/dragFilter.py | 139 ++++++++++-------- .../simplexui/menu/mayaPlugins/importObjs.py | 10 +- 2 files changed, 83 insertions(+), 66 deletions(-) diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index 50dd3737..40f49cd2 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -16,9 +16,25 @@ # along with Simplex. If not, see . -from Qt.QtCore import QEvent, QObject, QPoint, Qt, Signal +from Qt.QtCore import QEvent, QObject, QPoint, Qt, Signal, QRect from Qt.QtGui import QCursor, QMouseEvent, QGuiApplication -from Qt.QtWidgets import QApplication +from Qt.QtWidgets import QApplication, QWidget + +from enum import Enum + +from typing import Optional + + +class DragType(Enum): + DRAG_NONE = 0 + DRAG_HORIZONTAL = 1 + DRAG_VERTICAL = 2 + + +class DragCursor(Enum): + CURSOR_NONE = 0 + CURSOR_BLANK = 1 + CURSOR_ARROWS = 2 class DragFilter(QObject): @@ -67,60 +83,56 @@ class DragFilter(QObject): held down. This option turns that off """ - DRAG_ENABLED = 0 - DRAG_NONE = 0 - DRAG_HORIZONTAL = 1 - DRAG_VERTICAL = 2 - - CURSOR_NONE = 0 - CURSOR_BLANK = 1 - CURSOR_ARROWS = 2 + DRAG_ENABLED: int = 0 - dragTick = Signal(int, float) # NumberOfTicks, TickMultiplier - dragPressed = Signal() - dragReleased = Signal() + dragTick: Signal = Signal(int, float) # NumberOfTicks, TickMultiplier + dragPressed: Signal = Signal() + dragReleased: Signal = Signal() - def __init__(self, parent): - super(DragFilter, self).__init__(parent) + def __init__(self, parent: Optional[QWidget]): + self._parent: Optional[QWidget] = ( + parent # Hold onto this to keep the qwidget type + ) + super().__init__(parent) - self.dragSensitivity = 5 # pixels for one step - self.startSensitivity = 10 # pixel move to start the dragging - self.cursorLock = False - self.wrapBoundary = 10 # wrap when within boundary of screen edge - self.dragCursor = self.CURSOR_ARROWS - self.dragButton = Qt.MouseButton.MiddleButton + self.dragSensitivity: int = 5 # pixels for one step + self.startSensitivity: int = 10 # pixel move to start the dragging + self.cursorLock: bool = False + self.wrapBoundary: int = 10 # wrap when within boundary of screen edge + self.dragCursor: DragCursor = DragCursor.CURSOR_ARROWS + self.dragButton: Qt.MouseButton = Qt.MouseButton.MiddleButton # The QSpinbox has an option where, if you hold down the mouse button # it will continually increment. This flag enables a workaround # for that problem - self.isSpinbox = False + self.isSpinbox: bool = False - self.fastModifier = Qt.KeyboardModifier.ControlModifier - self.slowModifier = Qt.KeyboardModifier.ShiftModifier + self.fastModifier: Qt.KeyboardModifier = Qt.KeyboardModifier.ControlModifier + self.slowModifier: Qt.KeyboardModifier = Qt.KeyboardModifier.ShiftModifier - self.fastMultiplier = 5.0 - self.slowDivisor = 5.0 + self.fastMultiplier: float = 5.0 + self.slowDivisor: float = 5.0 # private vars - self._lastPos = QPoint() - self._leftover = 0 - self._dragStart = None - self._firstDrag = False - self._dragType = self.DRAG_NONE - self._overridden = False - self._screen = None - self._isDragging = False + self._lastPos: QPoint = QPoint() + self._leftover: float = 0.0 + self._dragStart: Optional[QPoint] = None + self._firstDrag: bool = False + self._dragType: DragType = DragType.DRAG_NONE + self._overridden: bool = False + self._screen: Optional[QRect] = None + self._isDragging: bool = False def doOverrideCursor(self): """Change the cursor based on the current drag type""" if self._overridden: return - if self.dragCursor == self.CURSOR_BLANK: + if self.dragCursor == DragCursor.CURSOR_BLANK: QApplication.setOverrideCursor(Qt.CursorShape.BlankCursor) - elif self.dragCursor == self.CURSOR_ARROWS: - if self._dragType == self.DRAG_VERTICAL: + elif self.dragCursor == DragCursor.CURSOR_ARROWS: + if self._dragType == DragType.DRAG_VERTICAL: QApplication.setOverrideCursor(Qt.CursorShape.SizeVerCursor) - elif self._dragType == self.DRAG_HORIZONTAL: + elif self._dragType == DragType.DRAG_HORIZONTAL: QApplication.setOverrideCursor(Qt.CursorShape.SizeHorCursor) self._overridden = True @@ -132,7 +144,7 @@ def restoreOverrideCursor(self): QApplication.restoreOverrideCursor() self._overridden = False - def doDrag(self, o, e): + def doDrag(self, o: QObject, e: QMouseEvent): """Handle a mouse drag event Parameters @@ -145,9 +157,8 @@ def doDrag(self, o, e): delta = 0.0 epos = e.position().toPoint() - if self._lastPos is not None: - if self._dragType == self.DRAG_HORIZONTAL: + if self._dragType == DragType.DRAG_HORIZONTAL: delta = epos.x() - self._lastPos.x() else: delta = self._lastPos.y() - epos.y() @@ -172,14 +183,15 @@ def doDrag(self, o, e): self._leftover -= self.dragSensitivity if self.cursorLock: - QCursor.setPos(self.mapToGlobal(self._dragStart)) - self._lastPos = self._dragStart + if self._dragStart is not None and self._parent is not None: + QCursor.setPos(self._parent.mapToGlobal(self._dragStart)) + self._lastPos = self._dragStart else: if self._screen is None: raise RuntimeError("Could not determine screen") r = self._screen b = self.wrapBoundary - p = o.mapToGlobal(epos) + p = e.globalPosition().toPoint() # when wrapping move to the other side in by 2*boundary # so we don't loop the wrapping @@ -198,13 +210,23 @@ def doDrag(self, o, e): if p != e.globalPosition().toPoint(): QCursor.setPos(p) self._leftover = 0 - par = self.parent() + par = self._parent if par is not None: self._lastPos = par.mapFromGlobal(p) else: self._lastPos = QPoint() - def startDrag(self, o, e): + def getCurrentScreen(self, o: QObject, global_pos: QPoint): + screen = QGuiApplication.screenAt(global_pos) + if screen is None: + if self._parent is not None: + screen = self._parent.windowHandle().screen() + else: + screen = QGuiApplication.primaryScreen() + self._screen = screen.availableGeometry() + return self._screen + + def startDrag(self, o: QObject, e: QMouseEvent): """Start the drag event handling Parameters @@ -218,19 +240,13 @@ def startDrag(self, o, e): epos = e.position().toPoint() if self._dragStart is None: self._dragStart = epos - global_pos = o.mapToGlobal(epos) - screen = QGuiApplication.screenAt(global_pos) - - # Fallback to the widget's current screen just in case the coordinate - # somehow falls outside valid screen bounds - if screen is None: - screen = o.screen() - self._screen = screen.availableGeometry() + global_pos = e.globalPosition().toPoint() + self.getCurrentScreen(o, global_pos) if abs(epos.x() - self._dragStart.x()) > self.startSensitivity: - self._dragType = self.DRAG_HORIZONTAL + self._dragType = DragType.DRAG_HORIZONTAL elif abs(epos.y() - self._dragStart.y()) > self.startSensitivity: - self._dragType = self.DRAG_VERTICAL + self._dragType = DragType.DRAG_VERTICAL if self._dragType: self._leftover = 0 @@ -252,7 +268,7 @@ def startDrag(self, o, e): ) QApplication.sendEvent(o, mouseup) - def myendDrag(self, o, e): + def myendDrag(self, o: QObject, e: QMouseEvent): """End the drag event handling. Can't call it endDrag because that's taken Parameters @@ -269,13 +285,13 @@ def myendDrag(self, o, e): elif self._dragType: self.restoreOverrideCursor() - self._dragType = self.DRAG_NONE + self._dragType = DragType.DRAG_NONE self._lastPos = QPoint() self._dragStart = None self._screen = None self.dragReleased.emit() - def eventFilter(self, o, e): + def eventFilter(self, o: QObject, e: QEvent) -> bool: """Overridden Qt eventFilter Parameters @@ -287,9 +303,10 @@ def eventFilter(self, o, e): """ if hasattr(self, "DRAG_ENABLED"): if e.type() == QEvent.Type.MouseMove: + assert isinstance(e, QMouseEvent) if self._isDragging: try: - if self._dragType != self.DRAG_NONE: + if self._dragType != DragType.DRAG_NONE: self.doDrag(o, e) elif e.buttons() & self.dragButton: self.startDrag(o, e) @@ -300,6 +317,7 @@ def eventFilter(self, o, e): return True elif e.type() == QEvent.Type.MouseButtonRelease: + assert isinstance(e, QMouseEvent) self.myendDrag(o, e) if e.button() & self.dragButton: # Catch any dragbutton releases and handle them @@ -307,6 +325,7 @@ def eventFilter(self, o, e): return True elif e.type() == QEvent.Type.MouseButtonPress: + assert isinstance(e, QMouseEvent) if e.button() & self.dragButton: # Catch any dragbutton presses and handle them self._isDragging = True diff --git a/src/python/simplexui/menu/mayaPlugins/importObjs.py b/src/python/simplexui/menu/mayaPlugins/importObjs.py index a37261a9..05e00f65 100644 --- a/src/python/simplexui/menu/mayaPlugins/importObjs.py +++ b/src/python/simplexui/menu/mayaPlugins/importObjs.py @@ -1,16 +1,14 @@ import os from functools import partial - import maya.cmds as cmds - from ...items import Combo, Slider, Traversal from Qt.QtWidgets import ( - QAction, QApplication, QFileDialog, QMessageBox, QProgressDialog, ) +from Qt.QtGui import QAction try: import numpy as np @@ -18,9 +16,9 @@ np = None try: - from MeshCrawler.commands import setAllVerts - from MeshCrawler.mesh import Mesh - from MeshCrawler.meshcrawlerGen import autoCrawlMeshes + from tools.MeshCrawler.commands import setAllVerts + from tools.MeshCrawler.mesh import Mesh + from tools.MeshCrawler.meshcrawlerGen import autoCrawlMeshes except ImportError: autoCrawlMeshes = None From 226864e4da7cd70311b83414ca15c020c8e53770 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 13 Aug 2026 12:43:58 -0700 Subject: [PATCH 17/47] Pull the qt model setup out of the tree --- src/python/simplexui/items/accessor.py | 99 +-- src/python/simplexui/items/combo.py | 360 +++-------- src/python/simplexui/items/falloff.py | 711 +++++++++++----------- src/python/simplexui/items/group.py | 166 ++--- src/python/simplexui/items/progression.py | 411 ++++--------- src/python/simplexui/items/shape.py | 132 ++-- src/python/simplexui/items/simplex.py | 314 +++++----- src/python/simplexui/items/slider.py | 334 ++++------ src/python/simplexui/items/stack.py | 64 +- src/python/simplexui/items/traversal.py | 549 +++++------------ 10 files changed, 1137 insertions(+), 2003 deletions(-) diff --git a/src/python/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py index a912427d..226d103d 100644 --- a/src/python/simplexui/items/accessor.py +++ b/src/python/simplexui/items/accessor.py @@ -18,7 +18,7 @@ import copy -from typing import TYPE_CHECKING, Optional, Any +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from .simplex import Simplex @@ -27,15 +27,8 @@ class SimplexAccessor(object): """The base object for all Simplex System object types - This class provides access to the simplex system, draggability, - name getters/setters/unifiers, proper deepcopying, and abstract tree lookup - - Parameters - ---------- - - Returns - ------- - + This class provides access to the simplex system + name getters/setters/unifiers, proper deepcopying, and abstract tree lookup """ def __init__(self, simplex: Simplex): @@ -45,32 +38,14 @@ def __init__(self, simplex: Simplex): @property def name(self) -> str: - """ """ return self._name - @name.setter - def name(self, val: str): - """The name of the current object""" - self._name = val - - @property - def models(self) -> list: - """ """ - return self.simplex.models - - @property - def falloffModels(self) -> list: - """ """ - return self.simplex.falloffModels - @property def DCC(self): - """ """ return self.simplex.DCC @property def stack(self) -> Stack: - """ """ return self.simplex.stack def __deepcopy__(self, memo): @@ -98,19 +73,6 @@ def _buildLinkedRename( ): """Build the proposed set of renames specifically for this object This allows sub-classes to override the linked name behavior - - Parameters - ---------- - newName : - - maxDepth : - - currentLinks : - - - Returns - ------- - """ return currentLinks @@ -176,58 +138,3 @@ def buildLinkedRename( # And now handle the type-specific stuff return self._buildLinkedRename(newName, maxDepth, currentLinks) - - def treeChild(self, row): - """ """ - return None - - def treeRow(self): - """ """ - return None - - def treeParent(self): - """ """ - return None - - def treeChildCount(self) -> int: - """ """ - return 0 - - def treeData(self, column) -> Any: - """ """ - if column == 0: - return self.name - return None - - def treeChecked(self): - """ """ - return None - - def icon(self): - return None - - -class SimplexTickAccessor(SimplexAccessor): - value: float - - def __init__(self, simplex): - super().__init__(simplex) - self.dragStep: float = 0.05 - self.maxValue: float = 1.0 - self.minValue: float = 0.0 - - def valueTick(self, ticks: int, mul: float): - """Change the value of the current object by some number of ticks - with some given multiplier. This is the interface for the MMB drag - - Parameters - ---------- - ticks : int - The number of dragStep ticks to apply - mul : float - An overall multiplier - """ - val = self.value + (self.dragStep * ticks * mul) - val = 0.0 if abs(val) < 1e-5 else val - val = max(min(val, self.maxValue), self.minValue) - self.value = val diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index fd8db1a1..b15742f5 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -15,107 +15,51 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -from ..interface import undoContext # pylint:disable=missing-docstring,unused-argument,no-self-use -from Qt.QtGui import QColor -from ..utils import getIcon, nested -from .accessor import SimplexAccessor, SimplexTickAccessor +from __future__ import annotations +from .accessor import SimplexAccessor from .stack import stackable +from typing import TYPE_CHECKING, Optional, Any + +if TYPE_CHECKING: + from .group import Group + from .simplex import Simplex + from .slider import Slider + from .shape import Shape + from .progression import Progression + # Abstract Items -class ComboPair(SimplexTickAccessor): +class ComboPair(SimplexAccessor): """A Slider/Value pair for use in Combos""" - classDepth = 6 + classDepth: int = 6 - def __init__(self, slider, value): - simplex = slider.simplex - super(ComboPair, self).__init__(simplex) - self.slider = slider - self._value = float(value) - self.minValue = -1.0 - self.maxValue = 1.0 - self.combo = None - self.expanded = {} + def __init__(self, slider: Slider, value: float): + super().__init__(slider.simplex) + self.slider: Slider = slider + self._value: float = float(value) + self.combo: Optional[Combo] = None @property - def models(self): - """ """ - return self.combo.simplex.models - - @property - def name(self): - """ """ + def name(self) -> str: return self.slider.name @property - def value(self): - """ """ + def value(self) -> float: return self._value @value.setter @stackable - def value(self, val): - """ - - Parameters - ---------- - val : - - - Returns - ------- - - """ + def value(self, val: float): self._value = val - for model in self.models: - model.itemDataChanged(self) - - def buildDefinition(self, simpDict, legacy): - """ - - Parameters - ---------- - simpDict : - - legacy : - - Returns - ------- - - """ + def buildDefinition(self, simpDict: dict, legacy: bool) -> tuple[int, float]: sIdx = self.slider.buildDefinition(simpDict, legacy) return sIdx, self.value - def treeRow(self): - """ """ - return self.combo.pairs.index(self) - - def treeParent(self): - """ """ - return self.combo - - def treeData(self, column): - """ - - Parameters - ---------- - column : - - - Returns - ------- - - """ - if column == 0: - return self.name - if column == 1: - return self.value - return None - class Combo(SimplexAccessor): """A group of Slider/Value pairs that control a Progression through some solver @@ -157,73 +101,66 @@ class Combo(SimplexAccessor): ) _freezeIcon = None - def __init__(self, name, simplex, pairs, prog, group, solveType, color=None): + def __init__( + self, + name: str, + simplex: Simplex, + pairs: list[ComboPair], + prog: Progression, + group: Group, + solveType: Optional[str], + ): super(Combo, self).__init__(simplex) - color = QColor(128, 128, 128) if color is None else color - - with self.stack.store(self): - if group.groupType is not type(self): - raise ValueError("Cannot add this slider to a combo group") - self._name = name - self.pairs = pairs - self.prog = prog - self._solveType = solveType - self._buildIdx = None - self.expanded = {} - self._enabled = True - self.color = color - - self._freezeThing = None - - mgrs = [model.insertItemManager(group) for model in self.models] - with nested(*mgrs): - self.group = group - for p in self.pairs: - p.combo = self - self.prog.controller = self - self.group.items.append(self) - self.simplex.combos.append(self) + + if group.groupType is not type(self): + raise ValueError("Cannot add this slider to a combo group") + + self._name: str = name + self.pairs = pairs + self.prog = prog + self._solveType: Optional[str] = solveType + self._buildIdx = None + self.expanded = {} + self._enabled = True + self._freezeThing = None + self.group: Group = group + for p in self.pairs: + p.combo = self + self.prog.controller = self + self.group.items.append(self) + self.simplex.combos.append(self) @property - def enabled(self): + def enabled(self) -> bool: """Get whether this Combo is evaluated in the solver""" return self._enabled @enabled.setter @stackable - def enabled(self, value): + def enabled(self, value: bool): """Set whether this Combo is evaluated in the solver""" self._enabled = value - for model in self.models: - model.itemDataChanged(self) @property - def frozen(self): + def frozen(self) -> bool: """Get whether this Combo is frozen""" return bool(self.freezeThing) @property - def freezeThing(self): + def freezeThing(self) -> Any: """Get whether this Combo is frozen""" if self._freezeThing is None: self._freezeThing = self.DCC.getFreezeThing(self) return self._freezeThing @freezeThing.setter - def freezeThing(self, value): + def freezeThing(self, value: Any): self._freezeThing = value - for model in self.models: - model.itemDataChanged(self) - - def icon(self): - if self.frozen: - if self._freezeIcon is None: - type(self)._freezeIcon = getIcon("frozen.png") - return self._freezeIcon - return None @classmethod - def comboAlreadyExists(cls, simplex, sliders, values): + def comboAlreadyExists( + cls, simplex: Simplex, sliders: list[Slider], values: list[float] + ) -> Optional[Combo]: """Classmethod to check whether a combo already exists with these sliders and values Parameters @@ -251,14 +188,14 @@ def comboAlreadyExists(cls, simplex, sliders, values): @classmethod def createCombo( cls, - name, - simplex, - sliders, - values, - group=None, - shape=None, - solveType=None, - tVal=1.0, + name: str, + simplex: Simplex, + sliders: list[Slider], + values: list[float], + group: Optional[Group]=None, + shape: Optional[Shape]=None, + solveType: Optional[str]=None, + tVal: float=1.0, ): """Classmethod to create Combo with some hard-coded defaults @@ -378,84 +315,26 @@ def name(self): @name.setter @stackable def name(self, value): - """Set the name of a combo - - Parameters - ---------- - value : - - - Returns - ------- - - """ + """Set the name of a combo""" self._name = value self.prog.name = value self.DCC.renameCombo(self, value) - for model in self.models: - model.itemDataChanged(self) @property def solveType(self): - """ """ + """Get the solveType of the combo""" return self._solveType @solveType.setter @stackable - def solveType(self, newType): - """Set the solveType of the combo - - Parameters - ---------- - newType : - - - Returns - ------- - - """ + def solveType(self, newType: str): + """Set the solveType of the combo""" stNames, stVals = list(zip(*self.solveTypes)) if newType not in stVals: raise ValueError( "Solve Type {0} not in allowed types {1}".format(newType, stVals) ) self._solveType = newType - for model in self.models: - model.itemDataChanged(self) - - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - - Returns - ------- - type - - - """ - if row == len(self.pairs): - return self.prog - return self.pairs[row] - - def treeRow(self): - """ """ - return self.group.items.index(self) - - def treeParent(self): - """ """ - return self.group - - def treeChildCount(self): - """ """ - return len(self.pairs) + 1 - - def treeChecked(self): - """ """ - return self.enabled def sliderNameLinks(self): """ """ @@ -549,10 +428,9 @@ def loadV2(cls, simplex, progs, data): name = data["name"] prog = progs[data["prog"]] group = simplex.groups[data.get("group", 1)] - color = QColor(*data.get("color", (128, 128, 128))) pairs = [ComboPair(simplex.sliders[s], v) for s, v in data["pairs"]] solveType = data.get("solveType") - return cls(name, simplex, pairs, prog, group, solveType, color=color) + return cls(name, simplex, pairs, prog, group, solveType) def buildDefinition(self, simpDict, legacy): """Output a dictionary definition of this object @@ -606,43 +484,6 @@ def clearBuildIndex(self): self.prog.clearBuildIndex() self.group.clearBuildIndex() - def extractProgressive(self, live=True, offset=10.0, separation=5.0): - """ - - Parameters - ---------- - live : - (Default value = True) - offset : - (Default value = 10.0) - separation : - (Default value = 5.0) - - Returns - ------- - - """ - raise RuntimeError("Currently just copied from Sliders, Not actually real") - with undoContext(self.DCC): - pos, neg = [], [] - for pp in sorted(self.prog.pairs): - if pp.value < 0.0: - neg.append((pp.value, pp.shape, offset)) - offset += separation - elif pp.value > 0.0: - pos.append((pp.value, pp.shape, offset)) - offset += separation - # skip the rest value at == 0.0 - neg = reversed(neg) - - for prog in [pos, neg]: - xtVal, shape, shift = prog[-1] - ext, deltaShape = self.DCC.extractWithDeltaShape(shape, live, shift) - for value, shape, shift in prog[:-1]: - self.DCC.extractWithDeltaConnection( - shape, deltaShape, value / xtVal, live, shift - ) - def extractShape(self, shape, live=True, offset=10.0): """Extract a shape from a combo progression @@ -688,19 +529,16 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): def delete(self): """Delete this combo and any shapes it contains""" self.simplex.deleteDownstream(self) - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - g = self.group - if self not in g.items: - return # Can happen when deleting multiple groups - g.items.remove(self) - self.group = None - self.simplex.combos.remove(self) - pairs = self.prog.pairs[:] # gotta make a copy - for pp in pairs: - if not pp.shape.isRest: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) + if self not in self.group.items: + return # Can happen when deleting multiple groups + self.group.items.remove(self) + self.group = None # type: ignore # Just clearing out references + self.simplex.combos.remove(self) + pairs = self.prog.pairs[:] # gotta make a copy + for pp in pairs: + if not pp.shape.isRest: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) @stackable def setInterpolation(self, interp): @@ -716,8 +554,6 @@ def setInterpolation(self, interp): """ self.prog.interp = interp - for model in self.models: - model.itemDataChanged(self) @stackable def setComboValue(self, slider, value): @@ -737,8 +573,6 @@ def setComboValue(self, slider, value): idx = self.getSliderIndex(slider) pair = self.pairs[idx] pair.value = value - for model in self.models: - model.itemDataChanged(pair) @stackable def appendComboValue(self, slider, value): @@ -756,10 +590,8 @@ def appendComboValue(self, slider, value): """ cp = ComboPair(slider, value) - mgrs = [model.insertItemManager(self) for model in self.models] - with nested(*mgrs): - self.pairs.append(cp) - cp.combo = self + self.pairs.append(cp) + cp.combo = self @stackable def deleteComboPair(self, comboPair): @@ -774,13 +606,11 @@ def deleteComboPair(self, comboPair): ------- """ - mgrs = [model.removeItemManager(comboPair) for model in self.models] - with nested(*mgrs): - # We specifically don't move the combo to the proper depth group - # That way the user can make multiple changes to the combo without - # it popping all over in the heirarchy - self.pairs.remove(comboPair) - comboPair.combo = None + # We specifically don't move the combo to the proper depth group + # That way the user can make multiple changes to the combo without + # it popping all over in the heirarchy + self.pairs.remove(comboPair) + comboPair.combo = None @stackable def setGroup(self, grp): @@ -803,12 +633,10 @@ def setGroup(self, grp): "All items in this group must be of type: {}".format(grp.groupType) ) - mgrs = [model.moveItemManager(self, grp) for model in self.models] - with nested(*mgrs): - if self.group: - self.group.items.remove(self) - grp.items.append(self) - self.group = grp + if self.group: + self.group.items.remove(self) + grp.items.append(self) + self.group = grp @stackable def createShape(self, shapeName=None, tVal=None): @@ -828,10 +656,8 @@ def createShape(self, shapeName=None, tVal=None): """ pp, idx = self.prog.newProgPair(shapeName, tVal) - mgrs = [model.insertItemManager(self.prog, idx) for model in self.models] - with nested(*mgrs): - pp.prog = self.prog - self.prog.pairs.insert(idx, pp) + pp.prog = self.prog + self.prog.pairs.insert(idx, pp) return pp def getInputVector(self): diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index d6e85af7..2195755d 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -16,18 +16,21 @@ # along with Simplex. If not, see . # pylint:disable=missing-docstring,unused-argument,no-self-use +from __future__ import annotations import copy import math -try: - import numpy as np -except ImportError: - np = None -from Qt.QtGui import QColor -from ..utils import nested +import numpy as np +from numpy import typing as npt from .accessor import SimplexAccessor from .stack import stackable +from typing import Union, Optional, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from .simplex import Simplex + from .shape import Shape + class Falloff(SimplexAccessor): """Falloffs define how shapes are split left/right, front/back, top/bottom, or however else @@ -83,35 +86,35 @@ class Falloff(SimplexAccessor): """ - LEFTSIDE = "L" - RIGHTSIDE = "R" - TOPSIDE = "U" - BOTTOMSIDE = "D" - FRONTSIDE = "F" - BACKSIDE = "B" - ALLSIDES = LEFTSIDE + RIGHTSIDE + TOPSIDE + BOTTOMSIDE + FRONTSIDE + BACKSIDE + LEFTSIDE: str = "L" + RIGHTSIDE: str = "R" + TOPSIDE: str = "U" + BOTTOMSIDE: str = "D" + FRONTSIDE: str = "F" + BACKSIDE: str = "B" + ALLSIDES: str = LEFTSIDE + RIGHTSIDE + TOPSIDE + BOTTOMSIDE + FRONTSIDE + BACKSIDE - CENTERS = "MC" + CENTERS: str = "MC" - VERTICAL_SPLIT = "V" - VERTICAL_RESULTS = TOPSIDE + BOTTOMSIDE - VERTICAL_AXIS = "Y" - VERTICAL_AXISINDEX = 1 + VERTICAL_SPLIT: str = "V" + VERTICAL_RESULTS: str = TOPSIDE + BOTTOMSIDE + VERTICAL_AXIS: str = "Y" + VERTICAL_AXISINDEX: int = 1 - HORIZONTAL_SPLIT = "X" - HORIZONTAL_RESULTS = LEFTSIDE + RIGHTSIDE - HORIZONTAL_AXIS = "X" - HORIZONTAL_AXISINDEX = 0 + HORIZONTAL_SPLIT: str = "X" + HORIZONTAL_RESULTS: str = LEFTSIDE + RIGHTSIDE + HORIZONTAL_AXIS: str = "X" + HORIZONTAL_AXISINDEX: int = 0 - DEPTH_SPLIT = "Z" - DEPTH_RESULTS = FRONTSIDE + BACKSIDE - DEPTH_AXIS = "Z" - DEPTH_AXISINDEX = 2 + DEPTH_SPLIT: str = "Z" + DEPTH_RESULTS: str = FRONTSIDE + BACKSIDE + DEPTH_AXIS: str = "Z" + DEPTH_AXISINDEX: int = 2 - RESTNAME = "Rest" - SEP = "_" + RESTNAME: str = "Rest" + SEP: str = "_" - UNSPLIT_GUESS_TOLERANCE = 0.33 + UNSPLIT_GUESS_TOLERANCE: float = 0.33 def __init__(self, name, simplex, *data): super(Falloff, self).__init__(simplex) @@ -128,7 +131,7 @@ def __init__(self, name, simplex, *data): self._search = None self._rep = None self._weights = None - self._verts = None + self._verts: Optional[npt.NDArray] = None self._thing = None self._thingRepr = None @@ -146,104 +149,21 @@ def __init__(self, name, simplex, *data): self.children = [] self._buildIdx = None self.expanded = {} - self.color = QColor(128, 128, 128) - - mgrs = [model.insertItemManager(None) for model in self.falloffModels] - with nested(*mgrs): - self.simplex.falloffs.append(self) - - # newThing = self.DCC.getFalloffThing(self) - # if newThing is None: - # self.thing = self.DCC.createFalloff(self) - # else: - # self.thing = newThing - - # @property - # def thing(self): - ## if this is a deepcopied object, then self._thing will - ## be None. Rebuild the thing connection by its representation - # if self._thing is None and self._thingRepr: - # self._thing = self.DCC.loadPersistentFalloff(self._thingRepr) - # return self._thing - - # @thing.setter - # def thing(self, value): - # self._thing = value - # self._thingRepr = self.DCC.getPersistentFalloff(value) + self.simplex.falloffs.append(self) @property - def name(self): + def name(self) -> str: """Get the name of a Falloff""" return self._name @name.setter @stackable - def name(self, value): - """Set the name of a Falloff - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def name(self, value: str): + """Set the name of a Falloff""" self._name = value - for model in self.falloffModels: - model.itemDataChanged(self) - - @classmethod - def createPlanar(cls, name, simplex, axis, maxVal, maxHandle, minHandle, minVal): - """Create a planar falloff - - Parameters - ---------- - name : str - The name to give the falloff - simplex : Simplex - The Simplex system - axis : str - The axis to align the falloff to. X, Y, or Z - maxVal : float - The value past which the falloff is 1.0 - maxHandle : float - The (0, 1) range of the max cubic falloff handle - minHandle : float - The (0, 1) range of the min cubic falloff handle - minVal : float - The value past which the falloff is 0.0 - - Returns - ------- - - """ - return cls(name, simplex, "planar", axis, maxVal, maxHandle, minHandle, minVal) - - @classmethod - def createMap(cls, name, simplex, mapName, axis): - """Create a weightmap falloff - - Parameters - ---------- - name : str - The name to give the falloff - simplex : Simplex - The Simplex system - mapName : str - The name of the weightmap - axis : str - The axis to align the falloff to. X, Y, or Z - - Returns - ------- - - """ - return cls(name, simplex, "map", mapName, axis) @classmethod - def loadV2(cls, simplex, data): + def loadV2(cls, simplex: Simplex, data: dict[str, Any]) -> Falloff: """Load the falloff from the version 2 json specification Parameters @@ -263,19 +183,19 @@ def loadV2(cls, simplex, data): name = data["name"] axis = data["axis"] if tpe == "map": - return cls.createMap(name, simplex, data["mapName"], axis) + return MapFalloff.createMap(name, simplex, data["mapName"], axis) elif tpe == "planar": maxVal = data["maxVal"] maxHandle = data["maxHandle"] minHandle = data["minHandle"] minVal = data["minVal"] - return cls.createPlanar( + return PlanarFalloff.createPlanar( name, simplex, axis, maxVal, maxHandle, minHandle, minVal ) raise ValueError("Bad data passed to Falloff creation") - def buildDefinition(self, simpDict, legacy): + def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: """Output a dictionary definition of this object Parameters @@ -284,10 +204,6 @@ def buildDefinition(self, simpDict, legacy): The dictionary that is being built legacy : bool Whether to write out the legacy definition, or the newer one - - Returns - ------- - """ if self._buildIdx is None: self._buildIdx = len(simpDict["falloffs"]) @@ -324,18 +240,11 @@ def clearBuildIndex(self): The buildIndex is stored when building a definition dictionary that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - """ self._buildIdx = None @stackable - def duplicate(self, newName): + def duplicate(self, newName: str) -> Falloff: """Duplicate a Falloff with a new name Parameters @@ -353,9 +262,7 @@ def duplicate(self, newName): nf.name = newName nf.children = [] nf.clearBuildIndex() - mgrs = [model.insertItemManager(self) for model in self.falloffModels] - with nested(*mgrs): - self.simplex.falloffs.append(nf) + self.simplex.falloffs.append(nf) self.DCC.duplicateFalloff(self, nf) return nf @@ -366,17 +273,220 @@ def delete(self): for child in self.children: child.falloff = None - mgrs = [model.removeItemManager(self) for model in self.falloffModels] - with nested(*mgrs): - self.simplex.falloffs.pop(fIdx) + self.simplex.falloffs.pop(fIdx) self.DCC.deleteFalloff(self) + @property + def splitType(self) -> str: + return self._splitType + + @splitType.setter @stackable - def setPlanarData(self, axis, minVal, minHandle, maxHandle, maxVal): - """Set the type/data for a planar Falloff + def splitType(self, value: str): + self._splitType = str(value).lower() + self._updateDCC() + + @property + def axis(self) -> str: + return self._axis + + @axis.setter + @stackable + def axis(self, value: str): + self._axis = value + # TODO: Does this need to update the dcc?? + + def _setSearchRep(self): + if self.axis.lower() == self.HORIZONTAL_AXIS.lower(): + self._search = self.HORIZONTAL_SPLIT + self._rep = self.HORIZONTAL_RESULTS + elif self.axis.lower() == self.VERTICAL_AXIS.lower(): + self._search = self.VERTICAL_SPLIT + self._rep = self.VERTICAL_RESULTS + elif self.axis.lower() == self.DEPTH_AXIS.lower(): + self._search = self.DEPTH_SPLIT + self._rep = self.DEPTH_RESULTS + + @property + def search(self) -> str: + """The values this fallof searches for""" + if self._search is None: + self._setSearchRep() + assert self._search is not None + return self._search + + @property + def rep(self) -> str: + """The values this falloff replaces with""" + if self._rep is None: + self._setSearchRep() + assert self._rep is not None + return self._rep + + @property + def verts(self) -> Optional[npt.NDArray]: + """Get the stored vertex values""" + return self._verts + + @verts.setter + def verts(self, vals: npt.NDArray): + """Input the vertices into this falloff and compute the weights + + Parameters + ---------- + vals : np.array + A (Nx3) numpy array of vertices + """ + if self.splitType != "map": + # TODO: Do this via inheritance + + # Clear out any auto-computed weights + # when setting verts on a non-map falloff + self._weights = None + self._verts = vals + + @property + def weights(self) -> Optional[npt.NDArray]: + """Get the per-vertex weight values""" + return self._weights + + @weights.setter + def weights(self, val: npt.ArrayLike): + """Set the per-vertex weight values + + Parameters + ---------- + val : A list or numpy array of values between 0 and 1 + """ + self._weights = np.asarray(val) + + def getSidedName(self, name: str, sIdx: int) -> str: + """Take name to split along some axis, and replace the fields based on the index + For instance, this could take cp_X and return cp_L for sIdx=1 and cp_R for sIdx=2 + + Parameters + ---------- + name : str + The name to "split" with this falloff + sIdx : int + The index of the replacement value + + Returns + ------- + : str + The newly sided name + + """ + search = self.search + replace = self.rep[sIdx] + + nn = name + s = "{0}{1}{0}".format(self.SEP, search) + r = "{0}{1}{0}".format(self.SEP, replace) + nn = nn.replace(s, r) + + s = "{0}{1}".format(self.SEP, search) # handle Postfix + r = "{0}{1}".format(self.SEP, replace) + if nn.endswith(s): + nn = r.join(nn.rsplit(s, 1)) + + s = "{1}{0}".format(self.SEP, search) # handle Prefix + r = "{1}{0}".format(self.SEP, replace) + if nn.startswith(s): + nn = nn.replace(s, r, 1) + return nn + + def canRename(self, item: SimplexAccessor) -> bool: + """Check if the item can be renamed by this Falloff Parameters ---------- + item : object + The named simplex object to check + + Returns + ------- + : bool + Whether this object can be renamed + + """ + nn = self.getSidedName(item.name, 0) + return nn != item.name + + def splitRename(self, item: SimplexAccessor, sIdx: int): + """Actually run the rename for a particular item + + Parameters + ---------- + item : object + The named Simplex Item + sIdx : int + The replacement index + + Returns + ------- + + """ + from .combo import Combo + from .shape import Shape + from .slider import Slider + from .traversal import Traversal + + if isinstance(item, (Shape, Slider, Combo, Traversal)): + item.name = self.getSidedName(item.name, sIdx) + + def applyFalloff(self, shape: Shape, sIdx: int): + """Apply the falloff to the vertices of a shape + + Parameters + ---------- + shape : Shape + The shape to apply to + sIdx : int + The replacement index + """ + rest = self.simplex.restShape + if rest is None: + raise ValueError("Trying to apply a falloff on system with no rest shape") + if rest.verts is None: + raise ValueError( + "Trying to apply a falloff to a shape with unset rest verts" + ) + weights = self.weights + if weights is None: + raise ValueError("Weights are not properly set for this falloff") + + if sIdx == 1: + weights = 1 - weights + + if shape.verts is None: + raise ValueError("Trying to apply a falloff to a shape with unset verts") + + weightedDeltas = (shape.verts - rest.verts) * weights[:, None] + + shape.verts = weightedDeltas + rest.verts + + +class PlanarFalloff(Falloff): + @classmethod + def createPlanar( + cls, + name: str, + simplex: Simplex, + axis: str, + maxVal: float, + maxHandle: float, + minHandle: float, + minVal: float, + ) -> PlanarFalloff: + """Create a planar falloff + + Parameters + ---------- + name : str + The name to give the falloff + simplex : Simplex + The Simplex system axis : str The axis to align the falloff to. X, Y, or Z maxVal : float @@ -392,123 +502,88 @@ def setPlanarData(self, axis, minVal, minHandle, maxHandle, maxVal): ------- """ - self.splitType = "planar" - self.axis = axis - self.minVal = minVal - self.minHandle = minHandle - self.maxHandle = maxHandle - self.maxVal = maxVal - self.mapName = None - self._updateDCC() + return cls(name, simplex, "planar", axis, maxVal, maxHandle, minHandle, minVal) @stackable - def setMapData(self, mapName): - """Set the type/data for a map Falloff + def setPlanarData( + self, + axis: str, + minVal: float, + minHandle: float, + maxHandle: float, + maxVal: float, + ): + """Set the type/data for a planar Falloff Parameters ---------- - mapName : str - The name of the weightmap + axis : str + The axis to align the falloff to. X, Y, or Z + maxVal : float + The value past which the falloff is 1.0 + maxHandle : float + The (0, 1) range of the max cubic falloff handle + minHandle : float + The (0, 1) range of the min cubic falloff handle + minVal : float + The value past which the falloff is 0.0 Returns ------- """ - self.splitType = "map" - self.axis = None - self.minVal = None - self.minHandle = None - self.maxHandle = None - self.maxVal = None - self.mapName = mapName - self._updateDCC() - - @property - def splitType(self): - return self._splitType - - @splitType.setter - @stackable - def splitType(self, value): - self._splitType = str(value).lower() - for model in self.falloffModels: - model.itemDataChanged(self) - self._updateDCC() - - @property - def axis(self): - return self._axis - - @axis.setter - @stackable - def axis(self, value): - self._axis = value - for model in self.falloffModels: - model.itemDataChanged(self) + self.splitType = "planar" + self.axis = axis + self.minVal = minVal + self.minHandle = minHandle + self.maxHandle = maxHandle + self.maxVal = maxVal + self.mapName = None self._updateDCC() @property - def maxVal(self): + def maxVal(self) -> float: return self._maxVal @maxVal.setter @stackable - def maxVal(self, value): + def maxVal(self, value: float): self._maxVal = value - for model in self.falloffModels: - model.itemDataChanged(self) self._updateDCC() @property - def maxHandle(self): + def maxHandle(self) -> float: return self._maxHandle @maxHandle.setter @stackable - def maxHandle(self, value): + def maxHandle(self, value: float): self._maxHandle = value - for model in self.falloffModels: - model.itemDataChanged(self) self._updateDCC() @property - def minHandle(self): + def minHandle(self) -> float: return self._minHandle @minHandle.setter @stackable - def minHandle(self, value): + def minHandle(self, value: float): self._minHandle = value - for model in self.falloffModels: - model.itemDataChanged(self) self._updateDCC() @property - def minVal(self): + def minVal(self) -> float: return self._minVal @minVal.setter @stackable - def minVal(self, value): + def minVal(self, value: float): self._minVal = value - for model in self.falloffModels: - model.itemDataChanged(self) - self._updateDCC() - - @property - def mapName(self): - return self._mapName - - @mapName.setter - @stackable - def mapName(self, value): - self._mapName = value - for model in self.falloffModels: - model.itemDataChanged(self) self._updateDCC() def _updateDCC(self): """ """ + # TODO: Separate Map and Planar falloff data self.DCC.setFalloffData( self, self.splitType, @@ -520,18 +595,10 @@ def _updateDCC(self): self.mapName, ) - # Split code @property - def bezier(self): + def bezier(self) -> tuple[float, float, float, float]: """Pre-build a factorization of the cubic bezier curve that is being used for a falloff Based on method described at https://pomax.github.io/bezierinfo/#yforx - - Parameters - ---------- - - Returns - ------- - """ if self._bezier is None: p0x = 0.0 @@ -548,7 +615,8 @@ def bezier(self): self._bezier = (qq, r, d, n) return self._bezier - def getMultiplier(self, xVal): + # Split code + def getMultiplier(self, xVal: float) -> float: """Get the weight value for the given X Parameters @@ -584,68 +652,11 @@ def getMultiplier(self, xVal): t1 = 1 - t return 3 * t1 * t**2 * 1 + t**3 * 1 - def _setSearchRep(self): - """ """ - if self.axis.lower() == self.HORIZONTAL_AXIS.lower(): - self._search = self.HORIZONTAL_SPLIT - self._rep = self.HORIZONTAL_RESULTS - elif self.axis.lower() == self.VERTICAL_AXIS.lower(): - self._search = self.VERTICAL_SPLIT - self._rep = self.VERTICAL_RESULTS - elif self.axis.lower() == self.DEPTH_AXIS.lower(): - self._search = self.DEPTH_SPLIT - self._rep = self.DEPTH_RESULTS - @property - def search(self): - """The values this fallof searches for""" - if self._search is None: - self._setSearchRep() - return self._search - - @property - def rep(self): - """The values this falloff replaces with""" - if self._rep is None: - self._setSearchRep() - return self._rep - - @property - def verts(self): - """Get the stored vertex values""" - return self._verts - - @verts.setter - def verts(self, vals): - """Input the vertices into this falloff and compute the weights - - Parameters - ---------- - vals : np.array - A (Nx3) numpy array of vertices - - Returns - ------- - - """ - if self.splitType != "map": - # Clear out any auto-computed weights - # when setting verts on a non-map falloff - self._weights = None - self._verts = vals - - @property - def weights(self): + def weights(self) -> npt.NDArray: """Get the per-vertex weight values""" if self._weights is None: - if self.splitType == "map": - raise ValueError( - "Attempted to auto-compute weights of a map falloff: {}".format( - self.name - ) - ) - if self._verts is None: raise ValueError( "Attempted to auto-compute weights of a procedural falloff without setting verts: {0}".format( @@ -669,7 +680,7 @@ def weights(self): return self._weights @weights.setter - def weights(self, val): + def weights(self, val: npt.ArrayLike): """Set the per-vertex weight values Parameters @@ -678,101 +689,91 @@ def weights(self, val): """ self._weights = np.asarray(val) - def getSidedName(self, name, sIdx): - """Take name to split along some axis, and replace the fields based on the index - For instance, this could take cp_X and return cp_L for sIdx=1 and cp_R for sIdx=2 + +class MapFalloff(Falloff): + @classmethod + def createMap( + cls, name: str, simplex: Simplex, mapName: str, axis: str + ) -> MapFalloff: + """Create a weightmap falloff Parameters ---------- name : str - The name to "split" with this falloff - sIdx : int - The index of the replacement value + The name to give the falloff + simplex : Simplex + The Simplex system + mapName : str + The name of the weightmap + axis : str + The axis to align the falloff to. X, Y, or Z Returns ------- - : str - The newly sided name """ - search = self.search - replace = self.rep[sIdx] - - nn = name - s = "{0}{1}{0}".format(self.SEP, search) - r = "{0}{1}{0}".format(self.SEP, replace) - nn = nn.replace(s, r) - - s = "{0}{1}".format(self.SEP, search) # handle Postfix - r = "{0}{1}".format(self.SEP, replace) - if nn.endswith(s): - nn = r.join(nn.rsplit(s, 1)) - - s = "{1}{0}".format(self.SEP, search) # handle Prefix - r = "{1}{0}".format(self.SEP, replace) - if nn.startswith(s): - nn = nn.replace(s, r, 1) - return nn + return cls(name, simplex, "map", mapName, axis) - def canRename(self, item): - """Check if the item can be renamed by this Falloff + @stackable + def setMapData(self, mapName: str): + """Set the type/data for a map Falloff Parameters ---------- - item : object - The named simplex object to check - - Returns - ------- - : bool - Whether this object can be renamed - + mapName : str + The name of the weightmap """ - nn = self.getSidedName(item.name, 0) - return nn != item.name + self.splitType = "map" + self.axis = None + self.minVal = None + self.minHandle = None + self.maxHandle = None + self.maxVal = None + self.mapName = mapName + self._updateDCC() - def splitRename(self, item, sIdx): - """Actually run the rename for a particular item + @property + def mapName(self) -> str: + return self._mapName - Parameters - ---------- - item : object - The named Simplex Item - sIdx : int - The replacement index + @mapName.setter + @stackable + def mapName(self, value: str): + self._mapName = value + self._updateDCC() - Returns - ------- + def _updateDCC(self): + """ """ + # TODO: Separate Map and Planar falloff data + self.DCC.setFalloffData( + self, + self.splitType, + self.axis, + self.minVal, + self.minHandle, + self.maxHandle, + self.maxVal, + self.mapName, + ) - """ - from .combo import Combo - from .shape import Shape - from .slider import Slider - from .traversal import Traversal + @property + def weights(self) -> npt.NDArray: + """Get the per-vertex weight values""" - if isinstance(item, (Shape, Slider, Combo, Traversal)): - item.name = self.getSidedName(item.name, sIdx) + if self._weights is None: + raise ValueError( + "Attempted to auto-compute weights of a map falloff: {}".format( + self.name + ) + ) + return self._weights - def applyFalloff(self, shape, sIdx): - """Apply the falloff to the vertices of a shape + @weights.setter + def weights(self, val: npt.ArrayLike): + """Set the per-vertex weight values Parameters ---------- - shape : Shape - The shape to apply to - sIdx : int - The replacement index - - Returns - ------- - + val : A list or numpy array of values between 0 and 1 """ - rest = self.simplex.restShape - restVerts = rest.verts - - weights = self.weights - if sIdx == 1: - weights = 1 - weights - - weightedDeltas = (shape.verts - restVerts) * weights[:, None] - shape.verts = weightedDeltas + restVerts + self._weights = np.asarray(val) diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index fa66a4bb..6a715275 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -16,11 +16,21 @@ # along with Simplex. If not, see . # pylint:disable=missing-docstring,unused-argument,no-self-use -from Qt.QtGui import QColor -from ..utils import nested +from __future__ import annotations from .accessor import SimplexAccessor from .stack import stackable +from typing import Optional, Union, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from .simplex import Simplex + from .combo import Combo + from .slider import Slider + from .traversal import Traversal + + GroupType = Union[type[Combo], type[Slider], type[Traversal]] + GroupMember = Union[Combo, Slider, Traversal] + class Group(SimplexAccessor): """Groups organize Simplex items @@ -38,111 +48,57 @@ class Group(SimplexAccessor): The Simplex system groupType : type The type that this group can hold - color : QColor - The color of this item in the Ui Returns ------- """ - classDepth = 1 + classDepth: int = 1 - def __init__(self, name, simplex, groupType, color=None): + def __init__( + self, + name: str, + simplex: Simplex, + groupType: GroupType, + ): super(Group, self).__init__(simplex) from .combo import Combo from .slider import Slider from .traversal import Traversal - color = QColor(128, 128, 128) if color is None else color - with self.stack.store(self): - self._name = name - self.items = [] - self._buildIdx = None - self.expanded = {} - self.color = color - self.groupType = groupType - - mgrs = [ - model.insertItemManager(simplex, row=self._getInsertionRow()) - for model in self.models - ] - with nested(*mgrs): - if self.groupType is Slider: - self.simplex.sliderGroups.append(self) - elif self.groupType is Combo: - self.simplex.comboGroups.append(self) - elif self.groupType is Traversal: - self.simplex.traversalGroups.append(self) - - def _getInsertionRow(self): - """ """ - from .combo import Combo - from .slider import Slider - from .traversal import Traversal - - c = len(self.simplex.sliderGroups) - if self.groupType is Slider: - return c - c += len(self.simplex.comboGroups) - if self.groupType is Combo: - return c - c += len(self.simplex.traversalGroups) - if self.groupType is Traversal: - return c + self._name: str = name + self.items: list[Union[Combo, Slider, Traversal]] = [] + self._buildIdx: Optional[int] = None + self.groupType: GroupType = groupType + + if self.groupType is Slider: + self.simplex.sliderGroups.append(self) + elif self.groupType is Combo: + self.simplex.comboGroups.append(self) + elif self.groupType is Traversal: + self.simplex.traversalGroups.append(self) @property - def name(self): + def name(self) -> str: """Get the name of the Group""" return self._name @name.setter @stackable - def name(self, value): - """Set the name of the Group - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def name(self, value: str): + """Set the name of the Group""" self._name = value - for model in self.models: - model.itemDataChanged(self) - - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - - Returns - ------- - - """ - return self.items[row] - - def treeRow(self): - """ """ - return self.simplex.groups.index(self) - - def treeParent(self): - """ """ - return self.simplex - - def treeChildCount(self): - """ """ - return len(self.items) @classmethod - def createGroup(cls, name, simplex, things=None, groupType=None): + def createGroup( + cls, + name: str, + simplex: Simplex, + things: Optional[list[GroupMember]] = None, + groupType: Optional[GroupType] = None, + ) -> Group: """Convenience method for creating a group Parameters @@ -156,19 +112,20 @@ def createGroup(cls, name, simplex, things=None, groupType=None): groupType : type The type that the new Group can hold (Default value = None) simplex : + """ + if groupType is None: + if not things: + raise ValueError("Cannot build a group without setting the type, or giving an object to infer from") + groupType = type(things[0]) - Returns - ------- - - """ g = cls(name, simplex, groupType) if things is not None: g.take(things) return g @classmethod - def loadV2(cls, simplex, data): + def loadV2(cls, simplex: Simplex, data: dict[str, Any]) -> Group: """Load the data from a version2 formatted json dictionary Parameters @@ -199,9 +156,9 @@ def loadV2(cls, simplex, data): groupType = Traversal else: raise RuntimeError("Malformed simplex json string: Improper group type") - return cls(name, simplex, groupType, QColor(*color)) + return cls(name, simplex, groupType) - def buildDefinition(self, simpDict, legacy): + def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: """Output a dictionary definition of this object Parameters @@ -210,10 +167,6 @@ def buildDefinition(self, simpDict, legacy): The dictionary that is being built legacy : bool Whether to write out the legacy definition, or the newer one - - Returns - ------- - """ if self._buildIdx is None: self._buildIdx = len(simpDict["groups"]) @@ -233,13 +186,6 @@ def clearBuildIndex(self): The buildIndex is stored when building a definition dictionary that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - """ self._buildIdx = None @@ -260,18 +206,16 @@ def delete(self): else: raise RuntimeError("Somehow this group has no type") - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - # Delete the children first - # Gotta iterate over copies of the lists - # as .delete removes the items from the list - for item in self.items[:]: - item.delete() + # Delete the children first + # Gotta iterate over copies of the lists + # as .delete removes the items from the list + for item in self.items[:]: + item.delete() - gList.remove(self) + gList.remove(self) @stackable - def take(self, things): + def take(self, things: list[GroupMember]): """Remove some items from their current groups and put them in this one Parameters diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index afb5a5a9..d06e2195 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -17,158 +17,96 @@ # pylint:disable=missing-docstring,unused-argument,no-self-use -from ..utils import getNextName, nested -from .accessor import SimplexAccessor, SimplexTickAccessor +from __future__ import annotations +from ..utils import getNextName +from .accessor import SimplexAccessor from .stack import stackable -class ProgPair(SimplexTickAccessor): - """ """ +from typing import Optional, Union, Any, TYPE_CHECKING - classDepth = 9 +if TYPE_CHECKING: + from .simplex import Simplex + from .shape import Shape + from .falloff import Falloff + from .slider import Slider + from .combo import Combo + from .traversal import Traversal - def __init__(self, simplex, shape, value): + +class ProgPair(SimplexAccessor): + classDepth: int = 9 + + def __init__(self, simplex: Simplex, shape: Shape, value: float): super(ProgPair, self).__init__(simplex) - self.shape = shape - self._value = value - self.prog = None - self.minValue = -1.0 - self.maxValue = 1.0 - self.expanded = {} + self.shape: Shape = shape + self._value: float = value + self.prog: Optional[Progression] = None + self.minValue: float = -1.0 + self.maxValue: float = 1.0 if not shape.isRest and self not in self.shape.progPairs: self.shape.progPairs.append(self) @property - def name(self): - """ """ + def name(self) -> str: return self.shape.name @name.setter - def name(self, value): - """ - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def name(self, value: str): self.shape.name = value - def buildDefinition(self, simpDict, legacy): - """ - - Parameters - ---------- - simpDict : - - legacy : - - - Returns - ------- - - """ + def buildDefinition( + self, simpDict: dict[str, Any], legacy: bool + ) -> tuple[int, float]: idx = self.shape.buildDefinition(simpDict, legacy) return idx, self.value - def __lt__(self, other): + def __lt__(self, other: Any) -> bool: return self.value < other.value @property - def value(self): - """ """ + def value(self) -> float: return self._value @value.setter @stackable - def value(self, val): - """ - - Parameters - ---------- - val : - - - Returns - ------- - - """ + def value(self, val: float): from .slider import Slider self._value = val - for model in self.models: - model.itemDataChanged(self) if isinstance(self.prog.controller, Slider): self.prog.controller.setRange() @stackable def delete(self): - """ """ ppairs = self.prog.pairs ridx = ppairs.index(self) - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - pp = ppairs.pop(ridx) - if not pp.shape.isRest: - pp.shape.progPairs.remove(pp) - if not pp.shape.progPairs: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) - - def treeRow(self): - """ """ - return self.prog.pairs.index(self) - - def treeParent(self): - """ """ - from .slider import Slider - - par = self.prog - if isinstance(par.controller, Slider): - par = par.controller - return par - - def treeData(self, column): - """ - - Parameters - ---------- - column : - - - Returns - ------- - - """ - if column == 0: - return self.name - if column == 2: - return self.value - return None + pp = ppairs.pop(ridx) + if not pp.shape.isRest: + pp.shape.progPairs.remove(pp) + if not pp.shape.progPairs: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) class Progression(SimplexAccessor): """A set of shapes to interpolate between - A Progression is a collection of shape/value pairs, and an interpolation type. - Progressions don't exist on their own, they are always part of a higher-level object - like a Combo, Slider, or Traversal. The ProgPairs are always sorted by value + A Progression is a collection of shape/value pairs, and an interpolation type. + Progressions don't exist on their own, they are always part of a higher-level object + like a Combo, Slider, or Traversal. The ProgPairs are always sorted by value - Progressions should always have a shape at 0 (which is almost always the rest shape) - and a shape at either -1 or 1. - They can also have other shapes at any value between 0 and the extremes. + Progressions should always have a shape at 0 (which is almost always the rest shape) + and a shape at either -1 or 1. + They can also have other shapes at any value between 0 and the extremes. - Sliders give users direct control over the value that is passed to the progression. - Combos and Traversals use input values to control their progressions. + Sliders give users direct control over the value that is passed to the progression. + Combos and Traversals use input values to control their progressions. - Progressions can use different interpolations. - The simplest is 'linear', which blends in a straight line between shapes. - The 'spline' interp uses Catmull-Rom spline values. - The 'splitspline' builds separate Catmull-Rom splines for positive and negative values + Progressions can use different interpolations. + The simplest is 'linear', which blends in a straight line between shapes. + The 'spline' interp uses Catmull-Rom spline values. + The 'splitspline' builds separate Catmull-Rom splines for positive and negative values Parameters ---------- @@ -190,21 +128,33 @@ class Progression(SimplexAccessor): """ - classDepth = 8 - interpTypes = ( + classDepth: int = 8 + interpTypes: tuple[tuple[str, str], ...] = ( ("Linear", "linear"), ("Spline", "spline"), ("Split Spline", "splitspline"), ) - def __init__(self, name, simplex, pairs=None, interp="spline", falloffs=None): + def __init__( + self, + name: str, + simplex: Simplex, + pairs: Optional[list[ProgPair]] = None, + interp: str = "spline", + falloffs: Optional[list[Falloff]] = None, + ): super(Progression, self).__init__(simplex) + + if self.simplex.restShape is None: + raise RuntimeError("Simplex is not initialized with a rest shape") + with self.stack.store(self): - self._name = name - self._interp = interp - self.falloffs = falloffs or [] - self.controller = None + self._name: str = name + self._interp: str = interp + self.falloffs: list[Falloff] = falloffs or [] + self.controller: Optional[Union[Slider, Combo, Traversal]] = None + self.pairs: list[ProgPair] if pairs is None: self.pairs = [ProgPair(self.simplex, self.simplex.restShape, 0.0)] else: @@ -215,82 +165,28 @@ def __init__(self, name, simplex, pairs=None, interp="spline", falloffs=None): for falloff in self.falloffs: falloff.children.append(self) - self._buildIdx = None - self.expanded = {} + self._buildIdx: Optional[int] = None @property - def interp(self): + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str): + self._name = value + + @property + def interp(self) -> str: """Get the interp for this Progression""" return self._interp @interp.setter @stackable - def interp(self, value): - """Set the interp for this Progression - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def interp(self, value: str): + """Set the interp for this Progression""" self._interp = value - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - - Returns - ------- - - """ - return self.pairs[row] - - def treeRow(self): - """ """ - from .combo import Combo - from .traversal import Traversal - - if isinstance(self.controller, Traversal): - # Show the progression after the mult and prog - return 2 - elif isinstance(self.controller, Combo): - # Show the progression after the comboPairs - return len(self.controller.pairs) - return None - - def treeParent(self): - """ """ - return self.controller - - def treeChildCount(self): - """ """ - return len(self.pairs) - - def treeData(self, column): - """ - - Parameters - ---------- - column : - - - Returns - ------- - - """ - if column == 0: - return "SHAPES" - return None - - def getShapeIndex(self, shape): + def getShapeIndex(self, shape: Shape) -> int: """Get the index of the given shape in this progression Parameters @@ -309,12 +205,9 @@ def getShapeIndex(self, shape): return i raise ValueError("Provided shape:{0} is not in the list".format(shape.name)) - def getShapes(self): + def getShapes(self) -> list[Shape]: """Return the Shapes in this Progression - Parameters - ---------- - Returns ------- : type @@ -323,12 +216,9 @@ def getShapes(self): """ return [i.shape for i in self.pairs] - def getValues(self): + def getValues(self) -> list[float]: """Return the values in this Progression - Parameters - ---------- - Returns ------- : type @@ -337,7 +227,7 @@ def getValues(self): """ return [i.value for i in self.pairs] - def getInsertIndex(self, tVal): + def getInsertIndex(self, tVal: float) -> int: """Get the index to insert a pair with value tVal Parameters @@ -364,7 +254,7 @@ def getInsertIndex(self, tVal): return i return 0 - def getShapeAtValue(self, val, tol=0.0001): + def getShapeAtValue(self, val: float, tol=0.0001) -> Optional[Shape]: """Return the shape at the given value Parameters @@ -386,7 +276,7 @@ def getShapeAtValue(self, val, tol=0.0001): return None @classmethod - def loadV2(cls, simplex, data): + def loadV2(cls, simplex: Simplex, data: dict[str, Any]) -> Progression: """Load the data from a version2 formatted json dictionary Parameters @@ -410,7 +300,7 @@ def loadV2(cls, simplex, data): fos = [simplex.falloffs[i] for i in foIdxs] return cls(name, simplex, pairs=pairs, interp=interp, falloffs=fos) - def buildDefinition(self, simpDict, legacy): + def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: """Output a dictionary definition of this object Parameters @@ -419,10 +309,6 @@ def buildDefinition(self, simpDict, legacy): The dictionary that is being built legacy : bool Whether to write out the legacy definition, or the newer one - - Returns - ------- - """ if self._buildIdx is None: idxPairs = [pair.buildDefinition(simpDict, legacy) for pair in self.pairs] @@ -447,14 +333,7 @@ def clearBuildIndex(self): """Clear the build index of this object The buildIndex is stored when building a definition dictionary - that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - + that keeps track of its index for later referencin """ self._buildIdx = None for pair in self.pairs: @@ -463,74 +342,44 @@ def clearBuildIndex(self): fo.clearBuildIndex() @stackable - def moveShapeToProgression(self, shapePair): + def moveShapeToProgression(self, shapePair: ProgPair): """Remove the shapePair from its current progression and set it in a new progression Parameters ---------- shapePair : progPair The ProgPair to take - shapePair): ### Moves Rows (Slider : - - Combo : - - - Returns - ------- - """ oldProg = shapePair.prog - oldProg.pairs.remove(shapePair) + if oldProg is not None: + oldProg.pairs.remove(shapePair) self.pairs.append(shapePair) shapePair.prog = self @stackable - def setShapesValues(self, values): + def setShapesValues(self, values: list[float]): """Set all the Shape's values Parameters ---------- values : [float The values to set - - Returns - ------- - """ from .slider import Slider for pp, val in zip(self.pairs, values): pp.value = val - for model in self.models: - model.itemDataChanged(pp) if isinstance(self.controller, Slider): self.controller.updateRange() - for model in self.models: - model.itemDataChanged(self.controller) - - def siblingRename(self, shape, newName, currentLinks): - """ - Parameters - ---------- - shape : - - newName : - - currentLinks : - - - Returns - ------- - - """ + def siblingRename(self, shape: Shape, newName: str, currentLinks: dict): # This is part of the in-progress linked naming system # get name change pass @stackable - def addFalloff(self, falloff): + def addFalloff(self, falloff: Falloff): """Add a falloff to a slider's falloff list Parameters @@ -548,7 +397,7 @@ def addFalloff(self, falloff): self.DCC.addProgFalloff(self, falloff) @stackable - def removeFalloff(self, falloff): + def removeFalloff(self, falloff: Falloff): """Remove a falloff from a slider's falloff list Parameters @@ -566,7 +415,9 @@ def removeFalloff(self, falloff): self.DCC.removeProgFalloff(self, falloff) @stackable - def createShape(self, shapeName=None, tVal=None): + def createShape( + self, shapeName: Optional[str] = None, tVal: Optional[float] = None + ) -> ProgPair: """Create a shape and add it to a progression Parameters @@ -587,17 +438,17 @@ def createShape(self, shapeName=None, tVal=None): from .slider import Slider pp, idx = self.newProgPair(shapeName, tVal) - mgrs = [model.insertItemManager(self, idx) for model in self.models] - with nested(*mgrs): - pp.prog = self - self.pairs.insert(idx, pp) + pp.prog = self + self.pairs.insert(idx, pp) if isinstance(self.controller, Slider): self.controller.updateRange() return pp - def newProgPair(self, shapeName=None, tVal=None): + def newProgPair( + self, shapeName: Optional[str] = None, tVal: Optional[float] = None + ) -> tuple[ProgPair, int]: """Create a shape and DO NOT add it to a progression Parameters @@ -623,13 +474,14 @@ def newProgPair(self, shapeName=None, tVal=None): tVal = self.guessNextTVal() if shapeName is None: - if abs(tVal) == 1.0: - shapeName = self.controller.name - else: - neg = "n" if tVal < 0.0 else "" - shapeName = "{0}_{1}{2}".format( - self.controller.name, neg, int(abs(tVal) * 100) - ) + if self.controller is not None: + if abs(tVal) == 1.0: + shapeName = self.controller.name + else: + neg = "n" if tVal < 0.0 else "" + shapeName = "{0}_{1}{2}".format( + self.controller.name, neg, int(abs(tVal) * 100) + ) currentNames = [i.name for i in self.simplex.shapes] shapeName = getNextName(shapeName, currentNames) @@ -639,12 +491,9 @@ def newProgPair(self, shapeName=None, tVal=None): pp = ProgPair(self.simplex, shape, tVal) return pp, idx - def guessNextTVal(self): + def guessNextTVal(self) -> float: """Given the current progression values, make an educated guess what's next. - Parameters - ---------- - Returns ------- : float @@ -668,17 +517,13 @@ def guessNextTVal(self): return 1.0 @stackable - def deleteShape(self, shape): + def deleteShape(self, shape: Shape): """Delete a shape from the system and the DCC Parameters ---------- shape : Shape The shape to delete - - Returns - ------- - """ ridx = None for i, pp in enumerate(self.pairs): @@ -688,30 +533,22 @@ def deleteShape(self, shape): raise RuntimeError("Shape does not exist to remove") pp = self.pairs[ridx] - mgrs = [model.removeItemManager(pp) for model in self.models] - with nested(*mgrs): - self.pairs.pop(ridx) - if not shape.isRest: - self.simplex.shapes.remove(shape) - self.DCC.deleteShape(shape) + self.pairs.pop(ridx) + if not shape.isRest: + self.simplex.shapes.remove(shape) + self.DCC.deleteShape(shape) @stackable def delete(self): """Delete the Progression and all its Shapes""" - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - for pp in self.pairs[:]: - if pp.shape.isRest: - continue - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) + for pp in self.pairs[:]: + if pp.shape.isRest: + continue + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) - def getRange(self): + def getRange(self) -> tuple[float, float]: """Get the range for this Progression - - Parameters - ---------- - Returns ------- : float @@ -723,17 +560,13 @@ def getRange(self): vals = [i.value for i in self.pairs] return min(vals), max(vals) - def getExtremePairs(self): + def getExtremePairs(self) -> list[ProgPair]: """Get the ProgPairs where the value is -1 or 1 - Parameters - ---------- - Returns ------- : [ProgPair, ...] ProgPairs whose values are -1 or 1 - """ ret = [] for pp in self.pairs: diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index 37a842cc..d0542ab9 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -14,15 +14,24 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from ..interface import DCC, undoContext # pylint:disable=missing-docstring,unused-argument,no-self-use -from Qt.QtGui import QColor from .accessor import SimplexAccessor from .stack import stackable +from numpy import typing as npt +from typing import Optional, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from .slider import Slider + from .simplex import Simplex, DCCObject + from .progression import Progression, ProgPair + from .combo import Combo + class Shape(SimplexAccessor): """A representation of a single blendshape @@ -50,22 +59,19 @@ class Shape(SimplexAccessor): """ - classDepth = 10 + classDepth: int = 10 - def __init__(self, name, simplex, create=True, color=None): + def __init__(self, name: str, simplex: Simplex, create: bool = True): super(Shape, self).__init__(simplex) - color = QColor(128, 128, 128) if color is None else color with self.stack.store(self): - self._thing = None - self._verts = None - self._thingRepr = None - self._name = name - self._buildIdx = None + self._thing: Optional[DCCObject] = None + self._verts: Optional[npt.NDArray] = None + self._thingRepr: Optional[str] = None + self._name: str = name + self._buildIdx: Optional[int] = None simplex.shapes.append(self) - self.isRest = False - self.expanded = {} - self.color = color - self.progPairs = [] + self.isRest: bool = False + self.progPairs: list[ProgPair] = [] newThing = self.DCC.getShapeThing(self._name) if newThing is None: @@ -79,7 +85,9 @@ def __init__(self, name, simplex, create=True, color=None): self.thing = newThing @classmethod - def createShape(cls, name, simplex, slider=None): + def createShape( + cls, name: str, simplex: Simplex, slider: Optional[Slider] = None + ) -> Shape: """Convenience method for creating a new shape This will create all required parent objects to have a new shape @@ -119,7 +127,7 @@ def createShape(cls, name, simplex, slider=None): return pp.shape @classmethod - def buildRest(cls, simplex): + def buildRest(cls, simplex: Simplex) -> Shape: """Create/find the system's rest shape Parameters @@ -138,54 +146,32 @@ def buildRest(cls, simplex): return rest @property - def name(self): + def name(self) -> str: """Get the Shape's name""" return self._name @name.setter @stackable - def name(self, value): - """Set the Shape's name - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def name(self, value: str): + """Set the Shape's name""" if value == self._name: return self.DCC.renameShape(self, value) self._name = value - for model in self.models: - model.itemDataChanged(self) - def strippedName(self): + def strippedName(self) -> str: """Get the name of this shape with any progressive numbers stripped from the end""" sp = self.name.split("_") if self.isNumberField(sp[-1]): sp = sp[:-1] return "_".join(sp) - def _buildLinkedRename(self, newName, maxDepth, currentLinks): - """ - - Parameters - ---------- - newName : - - maxDepth : - - currentLinks : - - - Returns - ------- - - """ + def _buildLinkedRename( + self, + newName: str, + maxDepth: int, + currentLinks: dict[type, dict[str, tuple[SimplexAccessor, int]]], + ): # Now that all the bookkeeping has been handled by the main method # I can handle recursing for the object specific stuff here shape = None # TEMP @@ -246,7 +232,7 @@ def _buildLinkedRename(self, newName, maxDepth, currentLinks): # pass @property - def thing(self): + def thing(self) -> DCCObject: """Get the stored reference to the DCC object""" # if this is a deepcopied object, then self._thing will # be None. Rebuild the thing connection by its representation @@ -255,23 +241,13 @@ def thing(self): return self._thing @thing.setter - def thing(self, value): - """Set the stored reference to the DCC object - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def thing(self, value: DCCObject): + """Set the stored reference to the DCC object""" self._thing = value self._thingRepr = self.DCC.getPersistentShape(value) @classmethod - def loadV2(cls, simplex, data, create): + def loadV2(cls, simplex: Simplex, data: dict[str, Any], create: bool) -> Shape: """Load the data from a version2 formatted json dictionary Parameters @@ -289,9 +265,9 @@ def loadV2(cls, simplex, data, create): The specified Shape """ - return cls(data["name"], simplex, create, QColor(*data.get("color", (0, 0, 0)))) + return cls(data["name"], simplex, create) - def buildDefinition(self, simpDict, legacy): + def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: """Output a dictionary definition of this object Parameters @@ -312,7 +288,6 @@ def buildDefinition(self, simpDict, legacy): else: x = { "name": self.name, - "color": self.color.getRgb()[:3], } simpDict.setdefault("shapes", []).append(x) return self._buildIdx @@ -353,7 +328,9 @@ def zeroShapes(shapes): if not shape.isRest: shape.zeroShape() - def connectShape(self, mesh=None, live=False, delete=False): + def connectShape( + self, mesh: Optional[DCCObject] = None, live: bool = False, delete: bool = False + ): """Force a shape to match a mesh The "connect shape" button is: mesh=None, delete=True The "match shape" button is: mesh=someMesh, delete=False @@ -375,7 +352,12 @@ def connectShape(self, mesh=None, live=False, delete=False): self.DCC.connectShape(self, mesh, live, delete) @staticmethod - def connectShapes(shapes, meshes, live=False, delete=False): + def connectShapes( + shapes: list[Shape], + meshes: list[DCCObject], + live: bool = False, + delete: bool = False, + ): """Connect multiple meshes to multiple Shapes Parameters @@ -398,7 +380,7 @@ def connectShapes(shapes, meshes, live=False, delete=False): shape.connectShape(mesh, live, delete) @staticmethod - def isNumberField(val): + def isNumberField(val: str) -> bool: """A utility function to check if a field is numeric Also, this allows for the "n" prefix for negative numbers because many DCC's don't allow "-" in an object name @@ -421,23 +403,13 @@ def isNumberField(val): return val.isdigit() @property - def verts(self): + def verts(self) -> Optional[npt.NDArray]: """Get the stored vertices""" if self._verts is None: self._verts = self.DCC.getShapeVertices(self) return self._verts @verts.setter - def verts(self, value): - """Set the stored vertices - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def verts(self, value: npt.NDArray): + """Set the stored vertices""" self._verts = value diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index c6293b00..8f0a88c9 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . # pylint:disable=missing-docstring,unused-argument,no-self-use +from __future__ import annotations import copy import itertools import json @@ -35,7 +36,6 @@ from ..interface.dummyInterface import DCC as DummyDCC from Qt.QtGui import QColor from Qt.QtWidgets import QApplication -from ..utils import nested from .combo import Combo, ComboPair from .falloff import Falloff from .group import Group @@ -45,14 +45,23 @@ from .stack import Stack, stackable from .traversal import Traversal, TravPair +from typing import Optional, Any, TYPE_CHECKING, Union + +if TYPE_CHECKING: + from Qt.QtWidgets import QProgressDialog + + +DCCObject = Any +Controllers = Union[Slider, Combo, Traversal] +Splittable = Union[Shape, Progression, Slider, Combo, Traversal] + class Simplex(object): """The main Top-level abstract object that controls an entire setup Simplex objects contain and manage the entire hierarchy. They have methods to import from and export to disk. Simplex objects also mange the connections - to the DCC and the UI, setting up connections with the undo stack, the dispatcher, - and all of the ui TreeModels. + to the DCC and the UI, setting up connections with the undo stack, and the dispatcher Finally Simplex systems handle splitting, which will be covered more in depth in the documentation for the split method. @@ -61,20 +70,13 @@ class Simplex(object): classDepth = 0 - def __init__( - self, name="", models=None, falloffModels=None, forceDummy=False, sliderMul=1.0 - ): + def __init__(self, name: str = "", forceDummy=False, sliderMul=1.0): """Constructor Parameters ---------- name : str, optional The name of the new system. Defaults to "" - models : [QAbstractItemModel, ....], optional - The ui models that read this system. Defaults to [] - falloffModels : [QAbstractItemModel, ....], optional - The ui models for managing Falloffs. - Defaults to [] forceDummy : bool, optional When loading, don't make a connection to the actual DCC. Instead use the "dummy" DCC. Defaults False @@ -86,27 +88,26 @@ def __init__( Returns ------- """ - self._name = name # The name of the system - self.sliders = [] # List of contained sliders - self.combos = [] # List of contained combos - self.traversals = [] # list of contained traversals - self.sliderGroups = [] # List of groups containing sliders - self.comboGroups = [] # List of groups containing combos - self.traversalGroups = [] # List of groups containing traversals - self.falloffs = [] # List of contained falloff objects - self.shapes = [] # List of contained shape objects - self.models = models or [] # connected Qt Item Models - self.falloffModels = falloffModels or [] # connected Qt Falloff Models - self.restShape = None # Name of the rest shape - self.clusterName = "Shape" # Name of the cluster (XSI use only) - self.expanded = {} # Am I expanded by model - self.comboExpanded = False # Am I expanded in the combo tree - self.sliderExpanded = False # Am I expanded in the slider tree - self.sliderMul = sliderMul + self._name: str = name # The name of the system + self.sliders: list[Slider] = [] # List of contained sliders + self.combos: list[Combo] = [] # List of contained combos + self.traversals: list[Traversal] = [] # list of contained traversals + self.sliderGroups: list[Group] = [] # List of groups containing sliders + self.comboGroups: list[Group] = [] # List of groups containing combos + self.traversalGroups: list[Group] = [] # List of groups containing traversals + self.falloffs: list[Falloff] = [] # List of contained falloff objects + self.shapes: list[Shape] = [] # List of contained shape objects + self.restShape: Optional[Shape] = None # Quick access to the rest shape + self.clusterName: str = "Shape" # Name of the cluster (XSI use only) self.DCC = DummyDCC(self) if forceDummy else DCC(self) # Interface to the DCC - self.stack = Stack() # Reference to the Undo stack - self._extras = {} # Any extra key data to store in the output json - self._legacy = False # whether to write the legacy types + self.stack: Stack = Stack() # Reference to the Undo stack + self._extras: dict[str, Any] = {} # extra key data to store in the output json + self._legacy: bool = False # whether to write the legacy types + + @property + def simplex(self): + """A uniform accessor so that we can always get the root from any object""" + return self def __deepcopy__(self, memo): """Gotta be really picky about what gets deepcopied. @@ -117,15 +118,7 @@ def __deepcopy__(self, memo): result = cls.__new__(cls) memo[id(self)] = result for k, v in self.__dict__.items(): - if k == "models": - # do not make a copy of the connected models - # a deepcopied simplex won't be connected to a UI - setattr(result, k, []) - elif k == "falloffModels": - # do not make a copy of the connected models - # a deepcopied simplex won't be connected to a UI - setattr(result, k, []) - elif k == "stack": + if k == "stack": # Make a disabled stack for new simplex s = Stack() s.enabled = False @@ -158,16 +151,14 @@ def _initValues(self): self.traversalGroups = [] # List of groups containing combos self.falloffs = [] # List of contained falloff objects self.shapes = [] # List of contained shape objects - self.restShape = None # Name of the rest shape + self.restShape = None # Quick access to the rest shape self.clusterName = "Shape" # Name of the cluster (XSI use only) - self.expanded = {} # Am I expanded? (Keep around for consistent interface) - self.color = QColor(128, 128, 128) - self.comboExpanded = False # Am I expanded in the combo tree - self.sliderExpanded = False # Am I expanded in the slider tree # Alternate Constructors @classmethod - def buildBaseObject(cls, smpxPath, name=None, forceDummy=False): + def buildBaseObject( + cls, smpxPath: str, name: Optional[str] = None, forceDummy: bool = False + ): """Build the rest object from a .smpx file Parameters @@ -196,7 +187,13 @@ def buildBaseObject(cls, smpxPath, name=None, forceDummy=False): del iarch @classmethod - def buildEmptySystem(cls, thing, name, sliderMul=1.0, forceDummy=False): + def buildEmptySystem( + cls, + thing: DCCObject, + name: str, + sliderMul: float = 1.0, + forceDummy: bool = False, + ): """Create a new, empty system on a given mesh Parameters @@ -226,8 +223,14 @@ def buildEmptySystem(cls, thing, name, sliderMul=1.0, forceDummy=False): @classmethod def buildSystemFromJsonString( - cls, jsString, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None - ): + cls, + jsString: str, + thing: Optional[DCCObject] = None, + name: Optional[str] = None, + forceDummy: bool = False, + sliderMul: float = 1.0, + pBar: Optional[QProgressDialog] = None, + ) -> Simplex: """Build a system from a json encoded string Parameters @@ -263,8 +266,14 @@ def buildSystemFromJsonString( @classmethod def buildSystemFromJson( - cls, jsPath, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None - ): + cls, + jsPath: str, + thing: Optional[DCCObject] = None, + name: Optional[str] = None, + forceDummy: bool = False, + sliderMul: float = 1.0, + pBar: Optional[QProgressDialog] = None, + ) -> Simplex: """Build a system from .json file Parameters @@ -304,8 +313,14 @@ def buildSystemFromJson( @classmethod def buildSystemFromSmpx( - cls, smpxPath, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None - ): + cls, + smpxPath: str, + thing: Optional[DCCObject] = None, + name: Optional[str] = None, + forceDummy: bool = False, + sliderMul: float = 1.0, + pBar: Optional[QProgressDialog] = None, + ) -> Simplex: """Build a system from a .smpx file SMPX files are (under-the-hood) alembic caches with each shape delta stored as a frame of animation, and the json string stored in a property on the mesh. @@ -361,8 +376,14 @@ def buildSystemFromSmpx( @classmethod def buildSystemFromFile( - cls, path, thing=None, name=None, forceDummy=False, sliderMul=1.0, pBar=None - ): + cls, + path: str, + thing: Optional[DCCObject] = None, + name: Optional[str] = None, + forceDummy: bool = False, + sliderMul: float = 1.0, + pBar: Optional[QProgressDialog] = None, + ) -> Simplex: """Build a system from a file Parameters @@ -414,8 +435,13 @@ def buildSystemFromFile( @classmethod def buildSystemFromMesh( - cls, thing, name, forceDummy=False, sliderMul=1.0, pBar=None - ): + cls, + thing: DCCObject, + name: str, + forceDummy: bool = False, + sliderMul: float = 1.0, + pBar: Optional[QProgressDialog] = None, + ) -> Simplex: """Build a system from the data already built in the DCC Parameters @@ -454,14 +480,14 @@ def buildSystemFromMesh( @classmethod def buildSystemFromDict( cls, - jsDict, - thing, - name=None, - create=True, - forceDummy=False, - sliderMul=1.0, - pBar=None, - ): + jsDict: dict[str, Any], + thing: DCCObject, + name: Optional[str] = None, + create: bool = True, + forceDummy: bool = False, + sliderMul: float = 1.0, + pBar: Optional[QProgressDialog] = None, + ) -> Simplex: """Utility for building a cleared system from a dictionary Parameters @@ -498,7 +524,7 @@ def buildSystemFromDict( self.loadDefinition(jsDict, create=create, pBar=pBar) return self - def loadSmpxShapes(self, smpxPath, pBar=None): + def loadSmpxShapes(self, smpxPath: str, pBar: Optional[QProgressDialog] = None): """Load the Shapes from a .smpx file onto an already loaded system This is the "We got updated shapes from the modelers" method @@ -521,7 +547,7 @@ def loadSmpxShapes(self, smpxPath, pBar=None): finally: del abcMesh, iarch - def loadSmpxPoses(self, smpxPath, pBar=None): + def loadSmpxPoses(self, smpxPath: str, pBar: Optional[QProgressDialog] = None): """Load the Poses from a .smpx file onto an already loaded system This is the "Update the joints and skin" method @@ -544,7 +570,7 @@ def loadSmpxPoses(self, smpxPath, pBar=None): finally: del abcMesh, iarch - def loadSmpxFalloffs(self, abcPath, pBar=None): + def loadSmpxFalloffs(self, abcPath: str, pBar: Optional[QProgressDialog] = None): """Load the relevant data from a simplex alembic Parameters @@ -567,13 +593,13 @@ def loadSmpxFalloffs(self, abcPath, pBar=None): # Properties @property - def name(self): + def name(self) -> str: """Get the system name""" return self._name @name.setter @stackable - def name(self, value): + def name(self, value: str): """Set the system name""" if value == self._name: return @@ -583,11 +609,8 @@ def name(self, value): if self.restShape is not None: self.restShape.name = self.getRestName() - for model in self.models: - model.itemDataChanged(self) - @property - def progs(self): + def progs(self) -> list[Progression]: """Get all Progressions defined in the system""" out = [] for slider in self.sliders: @@ -599,41 +622,14 @@ def progs(self): return out @property - def groups(self): + def groups(self) -> list[Group]: """Get all Groups defined in the system""" return self.sliderGroups + self.comboGroups + self.traversalGroups - def treeChild(self, row): - """ """ - return self.groups[row] - - def treeRow(self): - """ """ - return 0 - - def treeParent(self): - """ """ - return None - - def treeChildCount(self): - """ """ - return len(self.groups) - - def treeData(self, column): - """ """ - if column == 0: - return self.name - return None - - def treeChecked(self): - """ """ - return None - - def icon(self): - return None - # HELPER - def comboExists(self, sliders, values): + def comboExists( + self, sliders: list[Slider], values: list[float] + ) -> Optional[Combo]: """Check if a combo exists with these specific sliders and values Because combo names aren't necessarily always in the same order @@ -661,15 +657,11 @@ def comboExists(self, sliders, values): def deleteSystem(self): """Delete an existing system from the DCC""" # Store the models as temp so the model doesn't go crazy with the signals - models, self.models = self.models, None - mgrs = [model.resetModelManager() for model in models] - with nested(*mgrs): - self.DCC.deleteSystem() - self._initValues() - self.DCC = DCC(self) - self.models = models - - def getComboUpstreams(self, combo): + self.DCC.deleteSystem() + self._initValues() + self.DCC = DCC(self) + + def getComboUpstreams(self, combo: Combo) -> list[Combo]: """Get a list of only combos that are upstream to the given combo In this case, "upstream" means that when the given combo is active, then any returned combos are also active. @@ -716,7 +708,7 @@ def getComboUpstreams(self, combo): return upstreams - def getDownstreamTraversals(self, slider): + def getDownstreamTraversals(self, slider: Slider) -> list[Traversal]: """Get a list of any traversals that depend on the given slider Parameters @@ -739,7 +731,7 @@ def getDownstreamTraversals(self, slider): downstream = list(set(downstream)) return downstream - def getDownstreamCombos(self, slider): + def getDownstreamCombos(self, slider: Slider) -> list[Combo]: """Get a list of any Combos that depend on the given slider Parameters @@ -764,7 +756,7 @@ def getDownstreamCombos(self, slider): downstream = list(set(downstream)) return downstream - def deleteDownstream(self, item): + def deleteDownstream(self, item: Slider): """Delete all items from the system that depend on the given item Parameters @@ -783,7 +775,7 @@ def deleteDownstream(self, item): c.delete() # USER METHODS - def setLegacy(self, legacy): + def setLegacy(self, legacy: bool): """Set whether to use the legacy .json format Parameters @@ -797,7 +789,7 @@ def setLegacy(self, legacy): """ self._legacy = legacy - def getFloatingShapes(self): + def getFloatingShapes(self) -> list[Combo]: """Find Combos with values other than -1 and 1 Parameters @@ -815,7 +807,7 @@ def getFloatingShapes(self): floatShapes.extend(f.prog.getShapes()) return floatShapes - def buildDefinition(self): + def buildDefinition(self) -> dict[str, Any]: """Create a simplex definition dictionary Loop through all the objects managed by this simplex system, and build a dictionary that defines it @@ -878,7 +870,12 @@ def buildDefinition(self): return d @stackable - def loadDefinition(self, simpDict, create=True, pBar=None): + def loadDefinition( + self, + simpDict: dict[str, Any], + create: bool = True, + pBar: Optional[QProgressDialog] = None, + ): """Build the structure of objects in this system based on a provided dictionary @@ -907,7 +904,7 @@ def loadDefinition(self, simpDict, create=True, pBar=None): self.loadV3(simpDict, create=create, pBar=pBar) self.storeExtras(simpDict) - def _incPBar(self, pBar, txt, inc=1): + def _incPBar(self, pBar: QProgressDialog, txt: str, inc: int = 1) -> bool: """Increment the progress bar and return False if the user cancelled Parameters @@ -930,7 +927,12 @@ def _incPBar(self, pBar, txt, inc=1): return not pBar.wasCanceled() return True - def loadV3(self, simpDict, create=True, pBar=None): + def loadV3( + self, + simpDict: dict[str, Any], + create: bool = True, + pBar: Optional[QProgressDialog] = None, + ): """Load the version 3 simplex definition V3 is just the same as V2, except for an update Traversal definition @@ -990,7 +992,12 @@ def loadV3(self, simpDict, create=True, pBar=None): finally: self.DCC.postLoad(self, preRet) - def loadV2(self, simpDict, create=True, pBar=None): + def loadV2( + self, + simpDict: dict[str, Any], + create: bool = True, + pBar: Optional[QProgressDialog] = None, + ): """Load the version 2 simplex definition Parameters @@ -1049,7 +1056,12 @@ def loadV2(self, simpDict, create=True, pBar=None): finally: self.DCC.postLoad(self, preRet) - def loadV1(self, simpDict, create=True, pBar=None): + def loadV1( + self, + simpDict: dict[str, Any], + create: bool = True, + pBar: Optional[QProgressDialog] = None, + ): """Load the version 1 simplex definition Parameters @@ -1175,7 +1187,7 @@ def loadV1(self, simpDict, create=True, pBar=None): finally: self.DCC.postLoad(self, preRet) - def storeExtras(self, simpDict): + def storeExtras(self, simpDict: dict[str, Any]): """Store any unknown keys when dumping, just in case they're important elsewhere Parameters @@ -1206,7 +1218,7 @@ def storeExtras(self, simpDict): del sd[ktn] self._extras = sd - def loadJSON(self, jsString): + def loadJSON(self, jsString: str): """Convenience method to load a JSON string definition Parameters @@ -1220,7 +1232,7 @@ def loadJSON(self, jsString): """ self.loadDefinition(json.loads(jsString)) - def getRestName(self): + def getRestName(self) -> str: """Get the default rest shape name Parameters @@ -1234,7 +1246,7 @@ def getRestName(self): """ return "Rest_{0}".format(self.name) - def dump(self): + def dump(self) -> str: """Dump the definition dictionary to a json string Parameters @@ -1248,7 +1260,7 @@ def dump(self): """ return json.dumps(self.buildDefinition()) - def exportAbc(self, path, pBar=None): + def exportAbc(self, path: str, pBar: Optional[QProgressDialog] = None): """Export the current mesh to a .smpx formatted file Parameters @@ -1278,7 +1290,14 @@ def exportAbc(self, path, pBar=None): finally: del arch, abcMesh - def exportOther(self, path, dccMesh, world=False, ensureCorrect=False, pBar=None): + def exportOther( + self, + path: str, + dccMesh: DCCObject, + world: bool = False, + ensureCorrect: bool = False, + pBar: Optional[QProgressDialog] = None, + ): """Export shapes from a mesh that isn't part of the current system The export process for shapes differs from DCC to DCC. @@ -1309,7 +1328,7 @@ def exportOther(self, path, dccMesh, world=False, ensureCorrect=False, pBar=None finally: del arch, abcMesh - def setSlidersWeights(self, sliders, weights): + def setSlidersWeights(self, sliders: list[Slider], weights: list[float]): """Set the weights of multiple sliders as one method Parameters @@ -1327,11 +1346,8 @@ def setSlidersWeights(self, sliders, weights): for slider, weight in zip(sliders, weights): slider.value = weight self.DCC.setSlidersWeights(sliders, weights) - for model in self.models: - for slider in sliders: - model.itemDataChanged(slider) - def extractRestShape(self, offset=0): + def extractRestShape(self, offset: int = 0) -> Optional[DCCObject]: """Extract the rest shape to a mesh in the DCC Parameters @@ -1347,20 +1363,20 @@ def extractRestShape(self, offset=0): return None return self.DCC.extractShape(self.restShape, live=False, offset=offset) - def buildRestShape(self): + def buildRestShape(self) -> Shape: """Build and store a rest shape for this system""" self.restShape = Shape.buildRest(self) return self.restShape def buildInputVectors( self, - keepSliders=None, - ignoreSliders=None, - depthCutoff=None, - ignoreFloaters=False, - ignoreTraversals=False, - extremes=False, - ): + keepSliders: Optional[set[str]] = None, + ignoreSliders: Optional[set[str]] = None, + depthCutoff: Optional[int] = None, + ignoreFloaters: bool = False, + ignoreTraversals: bool = False, + extremes: bool = False, + ) -> tuple[list[str], list[list[float]]]: """This is kind of a specialized function. Often, I have to sum combo deltas into the full sculpted shape. But to do that, I have to build the inputs to the solver that enable each shape, and I need a name for each shape. @@ -1448,7 +1464,7 @@ def buildInputVectors( return shapeNames, inVecs, keyIdxs - def evaluateInputs(self, inVecs): + def evaluateInputs(self, inVecs: list[list[float]]) -> list[list[float]]: """Get the shape activation vectors that are paired with the given input vectors It will probably be useful to pass the returned inVecs from `buildInputVectors` @@ -1472,7 +1488,7 @@ def evaluateInputs(self, inVecs): solver = PySimplex(self.dump()) return [solver.solve(iv) for iv in inVecs] - def controllersByDepth(self): + def controllersByDepth(self) -> list[Controllers]: """Get the shapes ordered by the depth of their controllers in the simplex hierarchy This is often useful when doing vertex position computations @@ -1505,7 +1521,9 @@ def controllersByDepth(self): return ctrlOrder # SPLIT CODE - def buildSplitterList(self, foList): + def buildSplitterList( + self, foList: list[Falloff] + ) -> tuple[list[Splittable], dict[Splittable, Falloff], dict]: """The way deepcopy works is that every object visited is added to the 'memo' dictionary, keyed by its id(). This way, you don't have to re-copy an object if you've already seen it. This means that if I make a memo that already contains objects that I don't want copied, @@ -1626,7 +1644,7 @@ def buildSplitterList(self, foList): return toSplit, splitBy, memo - def split(self, pBar=None): + def split(self, pBar: Optional[QProgressDialog] = None) -> Simplex: """Return a split deepcopy of the system. The new system will be a dummy system containing all the shapes as numpy arrays which can be exported to a .smpx file diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index 02ad430e..b8f110a1 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -16,19 +16,25 @@ # along with Simplex. If not, see . # pylint:disable=missing-docstring,unused-argument,no-self-use +from __future__ import annotations import itertools from ..interface import undoContext from Qt.QtGui import QColor -from ..utils import caseSplit, getNextName, makeUnique, nested, singleShot -from .accessor import SimplexTickAccessor +from ..utils import caseSplit, getNextName, makeUnique, singleShot +from .accessor import SimplexAccessor from .group import Group from .progression import ProgPair, Progression from .shape import Shape from .stack import stackable +from typing import Union, Optional, Any, TYPE_CHECKING -class Slider(SimplexTickAccessor): +if TYPE_CHECKING: + from .simplex import DCCObject, Simplex + + +class Slider(SimplexAccessor): """A user-input to the simplex system that directly controls a Progression Parameters @@ -51,35 +57,37 @@ class Slider(SimplexTickAccessor): """ - classDepth = 7 + classDepth: int = 7 - def __init__(self, name, simplex, prog, group, color=None, create=True): + def __init__( + self, + name: str, + simplex: Simplex, + prog: Progression, + group: Group, + create: bool = True, + ): if group.groupType is not type(self): raise ValueError("Cannot add this slider to a combo group") super(Slider, self).__init__(simplex) - color = QColor(128, 128, 128) if color is None else color with self.stack.store(self): - self._name = name - self._thing = None - self._thingRepr = None - self.prog = prog - self.split = False + self._name: str = name + self._thing: Optional[DCCObject] = None + self._thingRepr: Optional[str] = None + self.prog: Progression = prog + self.split: bool = False self.prog.controller = self - self._buildIdx = None - self._value = 0.0 - self.expanded = {} - self.color = color - self._enabled = True + self._buildIdx: Optional[int] = None + self._value: float = 0.0 + self._enabled: bool = True mn, mx = self.prog.getRange() - self.minValue = mn - self.maxValue = mx + self.minValue: float = mn + self.maxValue: float = mx - mgrs = [model.insertItemManager(group) for model in self.models] - with nested(*mgrs): - self.group = group - self.group.items.append(self) + self.group: Group = group + self.group.items.append(self) self.simplex.sliders.append(self) @@ -95,30 +103,25 @@ def __init__(self, name, simplex, prog, group, color=None, create=True): self.thing = newThing @property - def enabled(self): + def enabled(self) -> bool: """Get whether this Slider is evaluated in the solver""" return self._enabled @enabled.setter @stackable - def enabled(self, value): - """Get whether this Slider is evaluated in the solver - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def enabled(self, value: bool): + """Get whether this Slider is evaluated in the solver""" self._enabled = value - for model in self.models: - model.itemDataChanged(self) @classmethod - def createSlider(cls, name, simplex, group=None, shape=None, tVal=1.0): + def createSlider( + cls, + name: str, + simplex: Simplex, + group: Optional[Group] = None, + shape: Optional[Shape] = None, + tVal: float = 1.0, + ) -> Slider: """Create a new slider with a name in a group. Possibly create a single default shape for this slider @@ -165,7 +168,14 @@ def createSlider(cls, name, simplex, group=None, shape=None, tVal=1.0): return sli @classmethod - def createMultiSlider(cls, name, simplex, shapes, tVals, group=None): + def createMultiSlider( + cls, + name: str, + simplex: Simplex, + shapes: list[Shape], + tVals: list[float], + group: Optional[Group] = None, + ) -> Slider: """Create a new slider with a name (possibly in a custom group) with the provided shapes and t-values. @@ -208,98 +218,26 @@ def createMultiSlider(cls, name, simplex, shapes, tVals, group=None): return sli @property - def name(self): + def name(self) -> str: """Get the name of a Slider""" return self._name @name.setter @stackable - def name(self, value): - """Set the name of a Slider - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def name(self, value: str): + """Set the name of a Slider""" self._name = value self.prog.name = value self.DCC.renameSlider(self, value) # TODO Also rename the combos - for model in self.models: - model.itemDataChanged(self) - - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - Returns - ------- - - """ - return self.prog.pairs[row] - - def treeRow(self): - """ """ - return self.group.items.index(self) - - def treeParent(self): - """ """ - return self.group - - def treeChildCount(self): - """ """ - return len(self.prog.pairs) - - def treeData(self, column): - """ - - Parameters - ---------- - column : - - - Returns - ------- - - """ - if column == 0: - return self.name - if column == 1: - return self.value - return None - - def treeChecked(self): - """ """ - return self.enabled - - def nameLinks(self): - """ - - Parameters - ---------- - - Returns - ------- - : type - Name Linking is currenly in-development - - """ - # split by underscore + def nameLinks(self) -> list[bool]: sp = self._name.split("_") sliderPoss = [] for orig in sp: s = caseSplit(orig) if len(s) > 1: - s = orig + s + s = [orig] + s sliderPoss.append(s) # remove numbered chunks from the end @@ -325,7 +263,7 @@ def nameLinks(self): return out @classmethod - def buildSliderName(cls, pairs): + def buildSliderName(cls, pairs: list[tuple[str, float]]) -> str: """Figure out then name for a slider with given shapes This will mostly be used to figure out what the new name @@ -333,7 +271,7 @@ def buildSliderName(cls, pairs): Parameters ---------- - pairs : [(str + pairs : [(str, float)] A list of (name, value) pairs Returns @@ -349,9 +287,9 @@ def buildSliderName(cls, pairs): # This simplifies the logic greatly extPairs = [p for p in pairs if abs(p[1]) == 1.0] if len(extPairs) == 1: - return extPairs[0] + return extPairs[0][0] if extPairs[0] == 1: - extPairs = extPairs.reversed() + extPairs = extPairs[::-1] names = [] for ep in extPairs: @@ -363,7 +301,7 @@ def buildSliderName(cls, pairs): return "_".join(["".join(makeUnique(n)) for n in names]) @property - def thing(self): + def thing(self) -> DCCObject: """Get the stored reference to the DCC attribute""" # if this is a deepcopied object, then self._thing will # be None. Rebuild the thing connection by its representation @@ -372,67 +310,38 @@ def thing(self): return self._thing @thing.setter - def thing(self, value): - """Set the stored reference to the DCC attribute - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def thing(self, value: DCCObject): self._thing = value self._thingRepr = self.DCC.getPersistentSlider(value) @property - def value(self): + def value(self) -> float: """Get the current value for this Slider""" return self._value @value.setter - def value(self, val): - """Set the current value for this Slider - - Parameters - ---------- - val : - - - Returns - ------- - - """ + def value(self, val: float): + """Set the current value for this Slider""" self._value = val - for model in self.models: - model.itemDataChanged(self) self._setAllSliders(self) @singleShot() - def _setAllSliders(self, sliders): - """ - - Parameters - ---------- - sliders : - - - Returns - ------- - - """ + def _setAllSliders(self, sliders: list[Slider]): with undoContext(self.DCC): for slider in sliders: self.DCC.setSliderWeight(slider, slider.value) def updateValue(self): - """ """ pass @classmethod - def loadV2(cls, simplex, progs, data, create): + def loadV2( + cls, + simplex: Simplex, + progs: list[Progression], + data: dict[str, Any], + create: bool, + ) -> Slider: """Load the data from a version2 formatted json dictionary Parameters @@ -455,10 +364,9 @@ def loadV2(cls, simplex, progs, data, create): name = data["name"] prog = progs[data["prog"]] group = simplex.groups[data.get("group", 0)] - color = QColor(*data.get("color", (128, 128, 128))) - return cls(name, simplex, prog, group, create=create, color=color) + return cls(name, simplex, prog, group, create=create) - def buildDefinition(self, simpDict, legacy): + def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: """Output a dictionary definition of this object Parameters @@ -517,38 +425,32 @@ def setRange(self): def delete(self): """Delete a slider, any shapes it contains, and all downstream Combos and Traversals""" self.simplex.deleteDownstream(self) - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - g = self.group - g.items.remove(self) - self.group = None - self.simplex.sliders.remove(self) + g = self.group + g.items.remove(self) + self.group = None # type: ignore + self.simplex.sliders.remove(self) - pairs = self.prog.pairs[:] # gotta make a copy - for pp in pairs: - if not pp.shape.isRest: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) + pairs = self.prog.pairs[:] # gotta make a copy + for pp in pairs: + if not pp.shape.isRest: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) - self.DCC.deleteSlider(self) + self.DCC.deleteSlider(self) @stackable - def setInterpolation(self, interp): + def setInterpolation(self, interp: str): """Set the interpolation of a single Slider Parameters ---------- interp : str The interpolation for this Slider's Progression - - Returns - ------- - """ self.prog.interp = interp @stackable - def setInterps(self, sliders, interp): + def setInterps(self, sliders: list[Slider], interp: str): """Set the interpolation of multiple Sliders Parameters @@ -557,10 +459,6 @@ def setInterps(self, sliders, interp): List of sliders to set interpolations on interp : str The interpolation to set on the list of Sliders - - Returns - ------- - """ # This uses an instantiated slider to set the values # of multiple sliders. This is so we don't update the @@ -572,7 +470,9 @@ def setInterps(self, sliders, interp): slider.prog.interp = interp @stackable - def createShape(self, shapeName=None, tVal=None): + def createShape( + self, shapeName: Optional[str] = None, tVal: Optional[float] = None + ) -> ProgPair: """Create a shape and add it to a progression Parameters @@ -591,14 +491,14 @@ def createShape(self, shapeName=None, tVal=None): """ pp, idx = self.prog.newProgPair(shapeName, tVal) - mgrs = [model.insertItemManager(self, idx) for model in self.models] - with nested(*mgrs): - pp.prog = self.prog - self.prog.pairs.insert(idx, pp) + pp.prog = self.prog + self.prog.pairs.insert(idx, pp) self.updateRange() return pp - def extractProgressive(self, live=True, offset=10.0, separation=5.0): + def extractProgressive( + self, live: bool = True, offset: float = 10.0, separation: float = 5.0 + ): """Extract all of the Shapes in this Slider's Progression Parameters @@ -610,10 +510,6 @@ def extractProgressive(self, live=True, offset=10.0, separation=5.0): The offset value for the first extracted mesh in the DCC (Default value = 10.0) separation : float The offset to add between any two extracted meshes (Default value = 5.0) - - Returns - ------- - """ with undoContext(self.DCC): pos, neg = [], [] @@ -637,7 +533,9 @@ def extractProgressive(self, live=True, offset=10.0, separation=5.0): shape, deltaShape, value / xtVal, live, shift ) - def extractShape(self, shape, live=True, offset=10.0): + def extractShape( + self, shape: Shape, live: bool = True, offset: float = 10.0 + ) -> DCCObject: """Extract a Shape that is controlled by a Slider to a DCC mesh This is on Slider (vs being on Shape) because live connections are handled @@ -647,20 +545,20 @@ def extractShape(self, shape, live=True, offset=10.0): ---------- shape : Shape The shape to extract - offset : float - The offset value for the first extracted mesh in the DCC (Default value = 10.0) - separation : float - The offset to add between any two extracted meshes live : (Default value = True) - - Returns - ------- - + offset : float + The offset value for the first extracted mesh in the DCC (Default value = 10.0) """ return self.DCC.extractShape(shape, live, offset) - def connectShape(self, shape, mesh=None, live=False, delete=False): + def connectShape( + self, + shape: Shape, + mesh: Optional[DCCObject] = None, + live: bool = False, + delete: bool = False, + ): """Connect a Shape that is controlled by a Slider to a DCC mesh This is on Slider (vs being on Shape) because live connections are handled @@ -678,10 +576,6 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): Defaults to True delete : bool Whether to delete the DCC mesh after it was connected (Default value = False) - - Returns - ------- - """ self.DCC.connectShape(shape, mesh, live, delete) @@ -690,17 +584,13 @@ def updateRange(self): self.DCC.updateSlidersRange([self]) @stackable - def setGroup(self, grp): + def setGroup(self, grp: Group): """Set the Group for this Slider Parameters ---------- grp : Group The Group to put this Slider under - - Returns - ------- - """ if grp.groupType is None: grp.groupType = type(self) @@ -710,14 +600,12 @@ def setGroup(self, grp): "All items in this group must be of type: {}".format(grp.groupType) ) - mgrs = [model.moveItemManager(self, grp) for model in self.models] - with nested(*mgrs): - if self.group: - self.group.items.remove(self) - grp.items.append(self) - self.group = grp + if self.group: + self.group.items.remove(self) + grp.items.append(self) + self.group = grp - def getInputVectors(self): + def getInputVectors(self) -> list[list[float]]: """Get the ordered values for input to the solver that activate this Slider Multiple outputs are possible if the slider has -1 to 1 range diff --git a/src/python/simplexui/items/stack.py b/src/python/simplexui/items/stack.py index f2b3e66b..011ce83b 100644 --- a/src/python/simplexui/items/stack.py +++ b/src/python/simplexui/items/stack.py @@ -20,25 +20,31 @@ from collections import OrderedDict from contextlib import contextmanager from functools import wraps +from typing import Any, TYPE_CHECKING, Optional from ..interface import undoContext +if TYPE_CHECKING: + from .simplex import Simplex + # UNDO STACK SETUP class Stack(object): """Integrate simplex into the DCC undo stack""" def __init__(self): - self._stack = OrderedDict() - self.depth = 0 - self.currentRevision = 0 - self.enabled = True - - def __setitem__(self, key, value): + # Technically not needed, but I depend on the ordering behavior + self._stack: OrderedDict[int, Any] = OrderedDict() + self.depth: int = 0 + self.currentRevision: int = 0 + self.enabled: bool = True + + def push(self, key: int, value: Any): + """Push the memento value onto the stack for the given revision key""" gt = [] # when setting a new key, remove all keys from # the previous branch - for k in reversed(self._stack): # pylint: disable=bad-reversed-sequence + for k in reversed(self._stack): if k > key: gt.append(k) else: @@ -46,10 +52,9 @@ def __setitem__(self, key, value): break for k in gt: del self._stack[k] - # traceback.print_stack() self._stack[key] = value - def getRevision(self, revision): + def getRevision(self, revision: int) -> Optional[Simplex]: """Every time a change is made to the simplex definition, the revision counter is updated, and the revision/definition pair is put on the undo stack @@ -82,7 +87,7 @@ def purge(self): self.currentRevision = 0 @contextmanager - def store(self, wrapObj): + def store(self, wrapObj: Any): """A context manager That will store changes to a Simplex system Nested calls to this manager will only store the first one @@ -90,13 +95,7 @@ def store(self, wrapObj): ---------- wrapObj : object A system object that has a reference to the Simplex - - Returns - ------- - """ - from .simplex import Simplex - if self.enabled: with undoContext(wrapObj.DCC): self.depth += 1 @@ -108,44 +107,21 @@ def store(self, wrapObj): if self.depth == 0: # Only store the top Level of the stack srevision = wrapObj.DCC.incrementRevision() - if not isinstance(wrapObj, Simplex): - wrapObj = wrapObj.simplex - self[srevision] = copy.deepcopy(wrapObj) + self.push(srevision, copy.deepcopy(wrapObj.simplex)) else: yield def stackable(method): """A Decorator to make a method auto update the stack - This decorator can only be used on methods of an object - that has its .simplex value set with a stack. If you need - to wrap an init method, use the stack.store contextmanager - - Parameters - ---------- - method : - - - Returns - ------- - + This decorator can only be used on methods of an object + that has its .simplex value set with a stack. If you need + to wrap an init method, use the stack.store contextmanager """ @wraps(method) def stacked(self, *data, **kwdata): - """Decorator closure that handles the stack - - Parameters - ---------- - *data : - - **kwdata : - - - Returns - ------- - - """ + """Decorator closure that handles the stack""" ret = None with self.stack.store(self): ret = method(self, *data, **kwdata) diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index e128e38b..d1b618b4 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -16,121 +16,81 @@ # along with Simplex. If not, see . # pylint:disable=missing-docstring,unused-argument,no-self-use -from Qt.QtGui import QColor -from ..utils import nested -from .accessor import SimplexAccessor, SimplexTickAccessor +from __future__ import annotations +from .accessor import SimplexAccessor from .combo import Combo from .group import Group from .progression import Progression from .slider import Slider from .stack import stackable +from enum import Enum -class TravPair(SimplexTickAccessor): - """ """ +from typing import Optional, Union, Any, TYPE_CHECKING - classDepth = 4 +if TYPE_CHECKING: + from .simplex import Simplex, DCCObject + from .progression import ProgPair - def __init__(self, slider, value): + +class TravSide(Enum): + Start = "START" + End = "END" + + +class TravPair(SimplexAccessor): + classDepth: int = 4 + + def __init__(self, slider: Slider, value: float): simplex = slider.simplex super(TravPair, self).__init__(simplex) - self.slider = slider - self._value = float(value) - self.minValue = -1.0 - self.maxValue = 1.0 - self._tickDelta = 0.0 - self.travPoint = None - self.expanded = {} - - def valueTick(self, ticks, mul): - """ """ - self._tickDelta += self.dragStep * ticks * mul - if (self._tickDelta + self.value) <= self.slider.minValue: - self._tickDelta = self.slider.minValue - self.value - if self.value != self.slider.minValue: - self.value = self.slider.minValue - self._tickDelta = 0.0 - elif (self._tickDelta + self.value) >= self.slider.maxValue: - self._tickDelta = self.slider.maxValue - self.value - if self.value != self.slider.maxValue: - self._tickDelta = 0.0 - self.value = self.slider.maxValue - elif abs(self._tickDelta + self.value) <= 1.0e-5: - if self.value != 0.0: - self._tickDelta = 0.0 - self.value = 0.0 - - @property - def models(self): - """ """ - return self.simplex.models + self.slider: Slider = slider + self._value: float = float(value) + self.minValue: float = -1.0 + self.maxValue: float = 1.0 + self._tickDelta: float = 0.0 + self.travPoint: Optional[TravPoint] = None @property - def name(self): - """ """ + def name(self) -> str: return self.slider.name @property - def value(self): - """ """ + def value(self) -> float: return self._value @value.setter @stackable - def value(self, val): - """ """ + def value(self, val: float): self._value = val - for model in self.models: - model.itemDataChanged(self) - def buildDefinition(self, simpDict, legacy): - """ """ + def buildDefinition( + self, simpDict: dict[str, Any], legacy: bool + ) -> tuple[int, float]: sIdx = self.slider.buildDefinition(simpDict, legacy) return sIdx, self.value - def treeRow(self): - """ """ - return self.travPoint.pairs.index(self) - - def treeParent(self): - """ """ - return self.travPoint - - def treeData(self, column): - """ """ - if column == 0: - return self.name - if column == 1: - return self.value - return None - @stackable def remove(self): - """ """ - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - self.travPoint.pairs.remove(self) - self.travPoint = None + self.travPoint.pairs.remove(self) + self.travPoint = None @stackable def delete(self): - """ """ self.travPoint.traversal.removePairs([self]) @staticmethod - def removeAll(pairs): - """ """ - travs = list({p.travPoint.traversal for p in pairs}) + def removeAll(pairs: list[TravPair]): + points = [i.travPoint for i in pairs if i.travPoint is not None] + travs = list({pp.traversal for pp in points}) for trav in travs: trav.removePairs(pairs) class TravPoint(SimplexAccessor): - """ """ + classDepth: int = 3 - classDepth = 3 - - def __init__(self, pairs, row): + def __init__(self, pairs: list[TravPair], side: TravSide): if not pairs: raise ValueError("Pairs must be provided for a TravPoint") simplex = pairs[0].slider.simplex @@ -139,17 +99,14 @@ def __init__(self, pairs, row): self.pairs = pairs for pair in pairs: pair.travPoint = self - self.row = row - self.traversal = None - self.expanded = {} + self.side: TravSide = side + self.traversal: Optional[Traversal] = None - def sliders(self): - """ """ + def sliders(self) -> list[Slider]: return [i.slider for i in self.pairs] @staticmethod - def _wideCeiling(val, eps=0.001): - """ """ + def _wideCeiling(val: float, eps: float = 0.001): if val > eps: return 1.0 elif val < -eps: @@ -157,51 +114,14 @@ def _wideCeiling(val, eps=0.001): return 0.0 @stackable - def addPair(self, pair): - """ - - Parameters - ---------- - pair : - + def addPair(self, pair: TravPair): + self.pairs.append(pair) + pair.travPoint = self - Returns - ------- - - """ - mgrs = [model.insertItemManager(self) for model in self.models] - with nested(*mgrs): - self.pairs.append(pair) - pair.travPoint = self - - def removePair(self, pair): - """ - - Parameters - ---------- - pair : - - - Returns - ------- - - """ + def removePair(self, pair: TravPair): pair.remove() - def addSlider(self, slider, val=None): - """ - - Parameters - ---------- - slider : - - val : - (Default value = None) - - Returns - ------- - - """ + def addSlider(self, slider: Slider, val: Optional[float] = None): val = val if val is not None else slider.value val = self._wideCeiling(val) sliders = self.sliders() @@ -212,18 +132,7 @@ def addSlider(self, slider, val=None): else: self.pairs[idx].value = val - def addItem(self, item): - """ - - Parameters - ---------- - item : - - - Returns - ------- - - """ + def addItem(self, item: Union[Slider, Combo]): if isinstance(item, Slider): self.addSlider(item) elif isinstance(item, Combo): @@ -231,84 +140,21 @@ def addItem(self, item): self.addSlider(cp.slider, cp.value) @property - def name(self): - """ """ - return "START" if self.row == 0 else "END" - - @name.setter - def name(self, val): - # Do nothing. This is just here to make the linter happy - return - - def treeData(self, column): - """ - - Parameters - ---------- - column : - - - Returns - ------- - - """ - if column == 0: - return self.name - return None - - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - - Returns - ------- - - """ - return self.pairs[row] - - def treeRow(self): - """ """ - return self.row - - def treeParent(self): - """ """ - return self.traversal - - def treeChildCount(self): - """ """ - return len(self.pairs) - - def buildDefinition(self, simpDict, legacy): - """ - - Parameters - ---------- - simpDict : - - legacy : - + def name(self) -> str: + return self.side.value - Returns - ------- - - """ + def buildDefinition( + self, simpDict: dict[str, Any], legacy: bool + ) -> list[tuple[int, float]]: return [p.buildDefinition(simpDict, legacy) for p in self.pairs] - def getInputVector(self): - """get the input to the solver that would fully activate this point of the traversal - - parameters - ---------- + def getInputVector(self) -> list[float]: + """Get the input to the solver that would fully activate this point of the traversal returns ------- : [float, ...] the ordered slider values - """ invec = [0.0] * len(self.simplex.sliders) for cp in self.pairs: @@ -365,45 +211,47 @@ class Traversal(SimplexAccessor): """ - classDepth = 2 + classDepth: int = 2 def __init__( self, - name, - simplex, - startPoint, - endPoint, - prog, - group, - color=None, + name: str, + simplex: Simplex, + startPoint: TravPoint, + endPoint: TravPoint, + prog: Progression, + group: Group, ): super(Traversal, self).__init__(simplex) - color = QColor(128, 128, 128) if color is None else color with self.stack.store(self): if group.groupType is not type(self): raise ValueError( "Cannot add this Traversal to a group of a different type" ) - self._name = name - self.startPoint = startPoint - self.endPoint = endPoint - self.prog = prog - self._buildIdx = None - self.expanded = {} - self._enabled = True - self.color = color - - mgrs = [model.insertItemManager(group) for model in self.models] - with nested(*mgrs): - self.group = group - self.startPoint.traversal = self - self.endPoint.traversal = self - self.prog.controller = self - self.group.items.append(self) - self.simplex.traversals.append(self) + self._name: str = name + self.startPoint: TravPoint = startPoint + self.endPoint: TravPoint = endPoint + self.prog: Progression = prog + self._buildIdx: Optional[int] = None + self._enabled: bool = True + + self.group: Group = group + self.startPoint.traversal = self + self.endPoint.traversal = self + self.prog.controller = self + self.group.items.append(self) + self.simplex.traversals.append(self) @classmethod - def createTraversal(cls, name, simplex, startPairs, endPairs, group=None, count=4): + def createTraversal( + cls, + name: str, + simplex: Simplex, + startPairs: list[TravPair], + endPairs: list[TravPair], + group: Optional[Group] = None, + count: int = 4, + ) -> Traversal: """Create a Traversal between two lists of pairs Parameters @@ -452,95 +300,31 @@ def createTraversal(cls, name, simplex, startPairs, endPairs, group=None, count= return trav @property - def enabled(self): + def enabled(self) -> bool: """Get whether this Traversal is evaluated in the solver""" return self._enabled @enabled.setter @stackable - def enabled(self, value): - """Set whether this Traversal is evaluated in the solver - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def enabled(self, value: bool): + """Set whether this Traversal is evaluated in the solver""" self._enabled = value - for model in self.models: - model.itemDataChanged(self) @property - def name(self): + def name(self) -> str: """Get the name of a Traversal""" return self._name @name.setter @stackable - def name(self, value): - """Set the name of a Traversal - - Parameters - ---------- - value : - - - Returns - ------- - - """ + def name(self, value: str): + """Set the name of a Traversal""" self._name = value self.prog.name = value # self.DCC.renameTraversal(self, value) - # for model in self.models: - # model.itemDataChanged(self) - - def treeChild(self, row): - """ - - Parameters - ---------- - row : - - - Returns - ------- - """ - if row == 0: - return self.startPoint - elif row == 1: - return self.endPoint - elif row == 2: - return self.prog - return None - - def treeRow(self): - """ """ - return self.group.items.index(self) - - def treeParent(self): - """ """ - return self.group - - def treeChildCount(self): - """ """ - return 3 - - def treeChecked(self): - """ """ - return self.enabled - - def allSliders(self): + def allSliders(self) -> list[Slider]: """Get the list of all Sliders that control this Traversal - - Parameters - ---------- - Returns ------- : [Slider, ...] @@ -553,25 +337,21 @@ def allSliders(self): ] return startSliders + endSliders - def dynamicSliders(self): + def dynamicSliders(self) -> list[Slider]: """Get a list of sliders that have different values at the start and end""" - return [sli for sli, rng in self.ranges() if rng[0] != rng[1]] + return [sli for sli, rng in self.ranges().items() if rng[0] != rng[1]] - def staticSliders(self): + def staticSliders(self) -> list[Slider]: """Get a list of sliders that have the same values at the start and end""" - return [sli for sli, rng in self.ranges() if rng[0] == rng[1]] + return [sli for sli, rng in self.ranges().items() if rng[0] == rng[1]] - def ranges(self): + def ranges(self) -> dict[Slider, tuple[float, float]]: """Get the range per Slider for this Traversal - Parameters - ---------- - Returns ------- : type (dict): A {Slider: range} dict - """ startDict = {p.slider: p.value for p in self.startPoint.pairs} endDict = {p.slider: p.value for p in self.endPoint.pairs} @@ -582,8 +362,30 @@ def ranges(self): rangeDict[sli] = (startDict.get(sli, 0.0), endDict.get(sli, 0.0)) return rangeDict + @stackable + def setGroup(self, grp: Group): + """Set the Group for this Slider + + Parameters + ---------- + grp : Group + The Group to put this Slider under + """ + if grp.groupType is None: + grp.groupType = type(self) + + if not isinstance(self, grp.groupType): + raise ValueError( + "All items in this group must be of type: {}".format(grp.groupType) + ) + + if self.group: + self.group.items.remove(self) + grp.items.append(self) + self.group = grp + @staticmethod - def buildTraversalName(ranges): + def buildTraversalName(ranges: dict[Slider, tuple[float, float]]) -> str: """Given the range dict (like from Traversal.ranges()) come up with a name Parameters @@ -638,23 +440,11 @@ def buildTraversalName(ranges): return "Tv_" + "_".join(parts) - def controllerNameLinks(self): - """ """ + def controllerNameLinks(self) -> list[bool]: surr = "_{0}_".format(self.name) return ["_{0}_".format(sli) in surr for sli in self.allSliders()] - def nameLinks(self): - """ - - Parameters - ---------- - - Returns - ------- - : type - progression depends on this traversal's name - - """ + def nameLinks(self) -> list[bool]: # In this case, these names will *NOT* have the possibility of # a pos/neg name. Only the traversal name, and possibly a percentage shapeNames = [] @@ -668,7 +458,9 @@ def nameLinks(self): return [i == self.name for i in shapeNames] @stackable - def createShape(self, shapeName=None, tVal=None): + def createShape( + self, shapeName: Optional[str] = None, tVal: Optional[float] = None + ) -> ProgPair: """Create a shape and add it to a progression Parameters @@ -685,14 +477,14 @@ def createShape(self, shapeName=None, tVal=None): """ pp, idx = self.prog.newProgPair(shapeName, tVal) - mgrs = [model.insertItemManager(self.prog, idx) for model in self.models] - with nested(*mgrs): - pp.prog = self.prog - self.prog.pairs.insert(idx, pp) + pp.prog = self.prog + self.prog.pairs.insert(idx, pp) return pp @classmethod - def loadV2(cls, simplex, progs, data): + def loadV2( + cls, simplex: Simplex, progs: list[Progression], data: dict[str, Any] + ) -> Traversal: """Load the data from a version2 formatted json dictionary Parameters @@ -713,7 +505,6 @@ def loadV2(cls, simplex, progs, data): name = data["name"] prog = progs[data["prog"]] group = simplex.groups[data.get("group", 2)] - color = QColor(*data.get("color", (0, 0, 0))) rangeDict = {} # slider: [startVal, endVal] @@ -743,13 +534,15 @@ def loadV2(cls, simplex, progs, data): startPairs.append(TravPair(slider, startVal)) endPairs.append(TravPair(slider, endVal)) - startPoint = TravPoint(startPairs, 0) - endPoint = TravPoint(endPairs, 1) + startPoint = TravPoint(startPairs, TravSide.Start) + endPoint = TravPoint(endPairs, TravSide.End) - return cls(name, simplex, startPoint, endPoint, prog, group, color) + return cls(name, simplex, startPoint, endPoint, prog, group) @classmethod - def loadV3(cls, simplex, progs, data): + def loadV3( + cls, simplex: Simplex, progs: list[Progression], data: dict[str, Any] + ) -> Traversal: """Load the data from a version3 formatted json dictionary Parameters @@ -765,12 +558,10 @@ def loadV3(cls, simplex, progs, data): ------- : Traversal The specified Traversal - """ name = data["name"] prog = progs[data["prog"]] group = simplex.groups[data.get("group", 2)] - color = QColor(*data.get("color", (0, 0, 0))) startDict = dict(data["start"]) endDict = dict(data["end"]) @@ -779,12 +570,12 @@ def loadV3(cls, simplex, progs, data): for idx in sliIdxs: startPairs.append(TravPair(simplex.sliders[idx], startDict.get(idx, 0.0))) endPairs.append(TravPair(simplex.sliders[idx], endDict.get(idx, 0.0))) - startPoint = TravPoint(startPairs, 0) - endPoint = TravPoint(endPairs, 1) + startPoint = TravPoint(startPairs, TravSide.Start) + endPoint = TravPoint(endPairs, TravSide.End) - return cls(name, simplex, startPoint, endPoint, prog, group, color) + return cls(name, simplex, startPoint, endPoint, prog, group) - def buildDefinition(self, simpDict, legacy): + def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: """Output a dictionary definition of this object Parameters @@ -833,65 +624,41 @@ def clearBuildIndex(self): @stackable def delete(self): """Delete a traversal and any shapes it contains""" - mgrs = [model.removeItemManager(self) for model in self.models] - with nested(*mgrs): - g = self.group - if self not in g.items: - return # Can happen when deleting multiple groups - g.items.remove(self) - self.group = None - self.simplex.traversals.remove(self) - - pairs = self.prog.pairs[:] # gotta make a copy - for pp in pairs: - if not pp.shape.isRest: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) - - def extractShape(self, shape, live=True, offset=10.0): - """Extract a shape from a Traversal progression - - Parameters - ---------- - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ + g = self.group + if self not in g.items: + return # Can happen when deleting multiple groups + g.items.remove(self) + self.group = None # type: ignore + self.simplex.traversals.remove(self) + + pairs = self.prog.pairs[:] # gotta make a copy + for pp in pairs: + if not pp.shape.isRest: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) + + def extractShape(self, shape, live=True, offset=10.0) -> DCCObject: + """Extract a shape from a Traversal progression""" return self.DCC.extractTraversalShape(self, shape, live, offset) - def addSlider(self, slider): + def addSlider(self, slider: Slider): """Add a slider to both the startPoint and endPoint of this Traversal Parameters ---------- slider : Slider The slider to add - - Returns - ------- - """ self.startPoint.addSlider(slider, val=0.0) self.endPoint.addSlider(slider) - def removePairs(self, pairs): + def removePairs(self, pairs: list[TravPair]): """Remove the given pairs from both the startPoint and endPoint of this Traversal Parameters ---------- pairs : [TravPair The pairs to remove - - Returns - ------- - """ # Get only the pairs that are a part of this traversal sPairs = [i for i in self.startPoint.pairs if i in pairs] @@ -911,7 +678,9 @@ def removePairs(self, pairs): pair.remove() @staticmethod - def traversalAlreadyExists(simplex, sliders, ranges): + def traversalAlreadyExists( + simplex: Simplex, sliders: list[Slider], ranges: list[tuple[float, float]] + ) -> Optional[Traversal]: """In a given simplex syste, check if a traversal exists with the given sliders and ranges """ @@ -922,7 +691,7 @@ def traversalAlreadyExists(simplex, sliders, ranges): return None @staticmethod - def getCount(sliders, ranges): + def getCount(sliders: list[Slider], ranges: list[tuple[float, float]]) -> int: """Get the count of shapes to create for a traversal with the given sliders and ranges. It's the max number of shapes on a given side of 0 """ @@ -940,7 +709,7 @@ def getCount(sliders, ranges): return 0 return max(counts) - def getInputVector(self, value): + def getInputVector(self, value: float) -> list[float]: """Get the input to the Solver that would set this traversal to the given value From a896fa131e2985dda16d507680a1dbe409e9f9c3 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 13 Aug 2026 13:05:08 -0700 Subject: [PATCH 18/47] Clear up a bunch of typechecking things. Next is falloffs --- src/python/simplexui/items/combo.py | 2 -- src/python/simplexui/items/falloff.py | 2 +- src/python/simplexui/items/group.py | 2 -- src/python/simplexui/items/progression.py | 14 ++++++++++++-- src/python/simplexui/items/shape.py | 17 +++++++++++------ src/python/simplexui/items/simplex.py | 2 ++ src/python/simplexui/items/slider.py | 18 +++++++++--------- src/python/simplexui/items/traversal.py | 23 +++++++++++++---------- 8 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index b15742f5..4e42c45a 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -460,7 +460,6 @@ def buildDefinition(self, simpDict, legacy): "prog": self.prog.buildDefinition(simpDict, legacy), "pairs": [p.buildDefinition(simpDict, legacy) for p in self.pairs], "group": self.group.buildDefinition(simpDict, legacy), - "color": self.color.getRgb()[:3], "enabled": self._enabled, "solveType": str(self._solveType), } @@ -528,7 +527,6 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): @stackable def delete(self): """Delete this combo and any shapes it contains""" - self.simplex.deleteDownstream(self) if self not in self.group.items: return # Can happen when deleting multiple groups self.group.items.remove(self) diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index 2195755d..3f6cbd5b 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -25,7 +25,7 @@ from .accessor import SimplexAccessor from .stack import stackable -from typing import Union, Optional, Any, TYPE_CHECKING +from typing import Optional, Any, TYPE_CHECKING if TYPE_CHECKING: from .simplex import Simplex diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index 6a715275..b8e21b9b 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -146,7 +146,6 @@ def loadV2(cls, simplex: Simplex, data: dict[str, Any]) -> Group: from .traversal import Traversal name = data["name"] - color = data.get("color", (0, 0, 0)) typeName = data["type"] if typeName == "Slider": groupType = Slider @@ -175,7 +174,6 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: else: x = { "name": self.name, - "color": self.color.getRgb()[:3], "type": self.groupType.__name__, } simpDict.setdefault("groups", []).append(x) diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index d06e2195..88826d05 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -74,11 +74,19 @@ def value(self, val: float): from .slider import Slider self._value = val + if self.prog is None: + raise ValueError( + "Can't set the value of a progPair that doesn't have a controller" + ) + if isinstance(self.prog.controller, Slider): self.prog.controller.setRange() @stackable def delete(self): + if self.prog is None: + return + ppairs = self.prog.pairs ridx = ppairs.index(self) pp = ppairs.pop(ridx) @@ -373,10 +381,12 @@ def setShapesValues(self, values: list[float]): if isinstance(self.controller, Slider): self.controller.updateRange() - def siblingRename(self, shape: Shape, newName: str, currentLinks: dict): + def siblingRename( + self, shape: Shape, newName: str, currentLinks: dict + ) -> dict[type, dict[str, tuple[SimplexAccessor, int]]]: # This is part of the in-progress linked naming system # get name change - pass + return {} @stackable def addFalloff(self, falloff: Falloff): diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index d0542ab9..243fbc45 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -29,8 +29,7 @@ if TYPE_CHECKING: from .slider import Slider from .simplex import Simplex, DCCObject - from .progression import Progression, ProgPair - from .combo import Combo + from .progression import ProgPair class Shape(SimplexAccessor): @@ -174,34 +173,39 @@ def _buildLinkedRename( ): # Now that all the bookkeeping has been handled by the main method # I can handle recursing for the object specific stuff here - shape = None # TEMP + + shape = self # TEMP for typechecking. This is very wrong from .combo import Combo from .slider import Slider from .traversal import Traversal for pp in self.progPairs: + if pp.prog is None: + continue + currentLinks = pp.prog.siblingRename(shape, newName, currentLinks) ctrl = pp.prog.controller if isinstance(ctrl, Slider): - nn = None + nn = "" currentLinks = ctrl.buildLinkedRename( nn, maxDepth=maxDepth - 1, currentLinks=currentLinks ) elif isinstance(ctrl, Combo): - nn = None + nn = "" currentLinks = ctrl.buildLinkedRename( nn, maxDepth=maxDepth - 1, currentLinks=currentLinks ) elif isinstance(ctrl, Traversal): - nn = None + nn = "" currentLinks = ctrl.buildLinkedRename( nn, maxDepth=maxDepth - 1, currentLinks=currentLinks ) return currentLinks + """ # First, check for a slider rename, # if so, recurse into that slider # Check for combo renames (because combos use the shape names) @@ -230,6 +234,7 @@ def _buildLinkedRename( # pass # elif isinstance(item, Traversal): # pass + """ @property def thing(self) -> DCCObject: diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 8f0a88c9..5288d7f8 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -723,6 +723,8 @@ def getDownstreamTraversals(self, slider: Slider) -> list[Traversal]: """ downstream = [] + if not isinstance(slider, Slider): + return downstream for t in self.traversals: for pair in t.startPoint.pairs + t.endPoint.pairs: if slider == pair.slider: diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index b8f110a1..aaab41c9 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -20,7 +20,6 @@ import itertools from ..interface import undoContext -from Qt.QtGui import QColor from ..utils import caseSplit, getNextName, makeUnique, singleShot from .accessor import SimplexAccessor from .group import Group @@ -28,7 +27,7 @@ from .shape import Shape from .stack import stackable -from typing import Union, Optional, Any, TYPE_CHECKING +from typing import Optional, Any, TYPE_CHECKING if TYPE_CHECKING: from .simplex import DCCObject, Simplex @@ -323,7 +322,7 @@ def value(self) -> float: def value(self, val: float): """Set the current value for this Slider""" self._value = val - self._setAllSliders(self) + self._setAllSliders([self]) @singleShot() def _setAllSliders(self, sliders: list[Slider]): @@ -391,7 +390,6 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: "name": self.name, "prog": self.prog.buildDefinition(simpDict, legacy), "group": self.group.buildDefinition(simpDict, legacy), - "color": self.color.getRgb()[:3], "enabled": self._enabled, } simpDict.setdefault("sliders", []).append(x) @@ -527,11 +525,13 @@ def extractProgressive( if not prog: continue xtVal, shape, shift = prog[-1] - ext, deltaShape = self.DCC.extractWithDeltaShape(shape, live, shift) - for value, shape, shift in prog[:-1]: - self.DCC.extractWithDeltaConnection( - shape, deltaShape, value / xtVal, live, shift - ) + ret = self.DCC.extractWithDeltaShape(shape, live, shift) + if ret is not None: + ext, deltaShape = ret + for value, shape, shift in prog[:-1]: + self.DCC.extractWithDeltaConnection( + shape, deltaShape, value / xtVal, live, shift + ) def extractShape( self, shape: Shape, live: bool = True, offset: float = 10.0 diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index d1b618b4..9bf04559 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -72,19 +72,23 @@ def buildDefinition( @stackable def remove(self): - self.travPoint.pairs.remove(self) + if self.travPoint is not None: + self.travPoint.pairs.remove(self) self.travPoint = None @stackable def delete(self): - self.travPoint.traversal.removePairs([self]) + if self.travPoint is not None: + if self.travPoint.traversal is not None: + self.travPoint.traversal.removePairs([self]) @staticmethod def removeAll(pairs: list[TravPair]): points = [i.travPoint for i in pairs if i.travPoint is not None] travs = list({pp.traversal for pp in points}) for trav in travs: - trav.removePairs(pairs) + if trav is not None: + trav.removePairs(pairs) class TravPoint(SimplexAccessor): @@ -247,8 +251,8 @@ def createTraversal( cls, name: str, simplex: Simplex, - startPairs: list[TravPair], - endPairs: list[TravPair], + startPairs: list[tuple[Slider, float]], + endPairs: list[tuple[Slider, float]], group: Optional[Group] = None, count: int = 4, ) -> Traversal: @@ -284,11 +288,11 @@ def createTraversal( else: group = Group(gname, simplex, Traversal) - startPairs = [TravPair(p[0], p[1]) for p in startPairs] - endPairs = [TravPair(p[0], p[1]) for p in endPairs] + startTPairs = [TravPair(p[0], p[1]) for p in startPairs] + endTPairs = [TravPair(p[0], p[1]) for p in endPairs] - startPoint = TravPoint(startPairs, 0) - endPoint = TravPoint(endPairs, 1) + startPoint = TravPoint(startTPairs, TravSide.Start) + endPoint = TravPoint(endTPairs, TravSide.End) prog = Progression(name, simplex) trav = cls(name, simplex, startPoint, endPoint, prog, group) @@ -598,7 +602,6 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: "start": self.startPoint.buildDefinition(simpDict, legacy), "end": self.endPoint.buildDefinition(simpDict, legacy), "group": self.group.buildDefinition(simpDict, legacy), - "color": self.color.getRgb()[:3], "enabled": self._enabled, } simpDict.setdefault("traversals", []).append(x) From c8f7ab4003cf86d03e12fd233d6d22060a5e3230 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 13 Aug 2026 14:40:32 -0700 Subject: [PATCH 19/47] Split the falloff into a base class and subclasses --- src/python/simplexui/items/falloff.py | 234 +++++++++++++++----------- 1 file changed, 139 insertions(+), 95 deletions(-) diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index 3f6cbd5b..0981c054 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -28,8 +28,9 @@ from typing import Optional, Any, TYPE_CHECKING if TYPE_CHECKING: - from .simplex import Simplex + from .simplex import Simplex, DCCObject from .shape import Shape + from .progression import Progression class Falloff(SimplexAccessor): @@ -116,39 +117,20 @@ class Falloff(SimplexAccessor): UNSPLIT_GUESS_TOLERANCE: float = 0.33 - def __init__(self, name, simplex, *data): - super(Falloff, self).__init__(simplex) + def __init__(self, name: str, simplex: Simplex, axis: str): + super().__init__(simplex) with self.stack.store(self): - self._splitType = str(data[0]).lower() - self._axis = None - self._maxVal = None - self._maxHandle = None - self._minHandle = None - self._minVal = None - self._mapName = None - - self._bezier = None - self._search = None - self._rep = None - self._weights = None + self._search: Optional[str] = None + self._rep: Optional[str] = None + self._weights: Optional[npt.NDArray] = None self._verts: Optional[npt.NDArray] = None - self._thing = None - self._thingRepr = None + self._thing: Optional[DCCObject] = None + self._thingRepr: Optional[str] = None - if self._splitType == "planar": - self._axis = data[1] - self._maxVal = data[2] - self._maxHandle = data[3] - self._minHandle = data[4] - self._minVal = data[5] - elif self._splitType == "map": - self._mapName = data[1] - self._axis = data[2] - - self._name = name - self.children = [] - self._buildIdx = None - self.expanded = {} + self._axis: str = axis + self._name: str = name + self.children: list[Progression] = [] + self._buildIdx: Optional[int] = None self.simplex.falloffs.append(self) @property @@ -205,35 +187,7 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: legacy : bool Whether to write out the legacy definition, or the newer one """ - if self._buildIdx is None: - self._buildIdx = len(simpDict["falloffs"]) - if legacy: - if self.splitType == "planar": - line = [ - "planar", - self.axis, - self.maxVal, - self.maxHandle, - self.minHandle, - self.minVal, - ] - else: - line = ["map", self.mapName] - simpDict.setdefault("falloffs", []).append([self.name] + line) - else: - x = { - "name": self.name, - "type": self.splitType, - "axis": self.axis, - "maxVal": self.maxVal, - "maxHandle": self.maxHandle, - "minHandle": self.minHandle, - "minVal": self.minVal, - "mapName": self.mapName, - "color": self.color.getRgb()[:3], - } - simpDict.setdefault("falloffs", []).append(x) - return self._buildIdx + raise NotImplementedError("Can't build definition on an untyped falloff") def clearBuildIndex(self): """Clear the build index of this object @@ -271,21 +225,11 @@ def delete(self): """Delete the Falloff""" fIdx = self.simplex.falloffs.index(self) for child in self.children: - child.falloff = None + child.removeFalloff(self) self.simplex.falloffs.pop(fIdx) self.DCC.deleteFalloff(self) - @property - def splitType(self) -> str: - return self._splitType - - @splitType.setter - @stackable - def splitType(self, value: str): - self._splitType = str(value).lower() - self._updateDCC() - @property def axis(self) -> str: return self._axis @@ -337,12 +281,6 @@ def verts(self, vals: npt.NDArray): vals : np.array A (Nx3) numpy array of vertices """ - if self.splitType != "map": - # TODO: Do this via inheritance - - # Clear out any auto-computed weights - # when setting verts on a non-map falloff - self._weights = None self._verts = vals @property @@ -468,6 +406,36 @@ def applyFalloff(self, shape: Shape, sIdx: int): class PlanarFalloff(Falloff): + def __init__( + self, + name: str, + simplex: Simplex, + axis: str, + maxVal: float, + maxHandle: float, + minHandle: float, + minVal: float, + ): + super().__init__(name, simplex, axis) + with self.stack.store(self): + self._maxVal = maxVal + self._maxHandle = maxHandle + self._minHandle = minHandle + self._minVal = minVal + + self._bezier = None + self._search = None + self._rep = None + self._weights = None + self._verts: Optional[npt.NDArray] = None + self._thing = None + self._thingRepr = None + self._name = name + self.children = [] + self._buildIdx = None + self.expanded = {} + self.simplex.falloffs.append(self) + @classmethod def createPlanar( cls, @@ -502,7 +470,7 @@ def createPlanar( ------- """ - return cls(name, simplex, "planar", axis, maxVal, maxHandle, minHandle, minVal) + return cls(name, simplex, axis, maxVal, maxHandle, minHandle, minVal) @stackable def setPlanarData( @@ -532,13 +500,11 @@ def setPlanarData( ------- """ - self.splitType = "planar" self.axis = axis self.minVal = minVal self.minHandle = minHandle self.maxHandle = maxHandle self.maxVal = maxVal - self.mapName = None self._updateDCC() @property @@ -581,18 +547,36 @@ def minVal(self, value: float): self._minVal = value self._updateDCC() + @property + def verts(self) -> Optional[npt.NDArray]: + """Get the stored vertex values""" + return self._verts + + @verts.setter + def verts(self, vals: npt.NDArray): + """Input the vertices into this falloff and compute the weights + + Parameters + ---------- + vals : np.array + A (Nx3) numpy array of vertices + """ + # Clear out the weights since they're calculated + self._weights = None + self._verts = vals + def _updateDCC(self): """ """ # TODO: Separate Map and Planar falloff data self.DCC.setFalloffData( self, - self.splitType, + 'planar', self.axis, self.minVal, self.minHandle, self.maxHandle, self.maxVal, - self.mapName, + None, ) @property @@ -689,8 +673,48 @@ def weights(self, val: npt.ArrayLike): """ self._weights = np.asarray(val) + def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: + """Output a dictionary definition of this object + + Parameters + ---------- + simpDict : dict + The dictionary that is being built + legacy : bool + Whether to write out the legacy definition, or the newer one + """ + if self._buildIdx is None: + self._buildIdx = len(simpDict["falloffs"]) + if legacy: + line = [ + "planar", + self.axis, + self.maxVal, + self.maxHandle, + self.minHandle, + self.minVal, + ] + simpDict.setdefault("falloffs", []).append([self.name] + line) + else: + x = { + "name": self.name, + "type": self.splitType, + "axis": self.axis, + "maxVal": self.maxVal, + "maxHandle": self.maxHandle, + "minHandle": self.minHandle, + "minVal": self.minVal, + } + simpDict.setdefault("falloffs", []).append(x) + return self._buildIdx + class MapFalloff(Falloff): + def __init__(self, name: str, simplex: Simplex, axis: str, mapName: str): + super().__init__(name, simplex, axis) + with self.stack.store(self): + self._mapName = mapName + @classmethod def createMap( cls, name: str, simplex: Simplex, mapName: str, axis: str @@ -712,10 +736,10 @@ def createMap( ------- """ - return cls(name, simplex, "map", mapName, axis) + return cls(name, simplex, mapName, axis) @stackable - def setMapData(self, mapName: str): + def setMapData(self, axis: str, mapName: str): """Set the type/data for a map Falloff Parameters @@ -723,12 +747,7 @@ def setMapData(self, mapName: str): mapName : str The name of the weightmap """ - self.splitType = "map" - self.axis = None - self.minVal = None - self.minHandle = None - self.maxHandle = None - self.maxVal = None + self.axis = axis self.mapName = mapName self._updateDCC() @@ -747,12 +766,12 @@ def _updateDCC(self): # TODO: Separate Map and Planar falloff data self.DCC.setFalloffData( self, - self.splitType, + "map", self.axis, - self.minVal, - self.minHandle, - self.maxHandle, - self.maxVal, + None, + None, + None, + None, self.mapName, ) @@ -777,3 +796,28 @@ def weights(self, val: npt.ArrayLike): val : A list or numpy array of values between 0 and 1 """ self._weights = np.asarray(val) + + def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: + """Output a dictionary definition of this object + + Parameters + ---------- + simpDict : dict + The dictionary that is being built + legacy : bool + Whether to write out the legacy definition, or the newer one + """ + if self._buildIdx is None: + self._buildIdx = len(simpDict["falloffs"]) + if legacy: + line = ["map", self.axis, self.mapName] # Just default it to x axis + simpDict.setdefault("falloffs", []).append([self.name] + line) + else: + x = { + "name": self.name, + "type": self.splitType, + "axis": self.axis, + "mapName": self.mapName, + } + simpDict.setdefault("falloffs", []).append(x) + return self._buildIdx From 6b6a2b1208e873de2188133331940a0f7a798ff3 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 13 Aug 2026 16:32:05 -0700 Subject: [PATCH 20/47] Expose more stuff --- src/python/simplexui/items/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/python/simplexui/items/__init__.py b/src/python/simplexui/items/__init__.py index 0284cd84..f55de987 100644 --- a/src/python/simplexui/items/__init__.py +++ b/src/python/simplexui/items/__init__.py @@ -23,7 +23,7 @@ from .simplex import Simplex from .slider import Slider from .stack import Stack, stackable -from .traversal import Traversal, TravPair +from .traversal import Traversal, TravPair, TravPoint, TravSide __all__ = [ "Combo", @@ -38,5 +38,7 @@ "Stack", "stackable", "Traversal", + "TravPoint", "TravPair", + "TravSide", ] From d3cd7f50ae72f19c57cfc853a108a57bb3ca5dd4 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 14 Aug 2026 12:32:54 -0700 Subject: [PATCH 21/47] Add the new qt model interface --- src/python/simplexui/items/treeItem.py | 342 +++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 src/python/simplexui/items/treeItem.py diff --git a/src/python/simplexui/items/treeItem.py b/src/python/simplexui/items/treeItem.py new file mode 100644 index 00000000..90ac4d44 --- /dev/null +++ b/src/python/simplexui/items/treeItem.py @@ -0,0 +1,342 @@ +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . +"""A Qt model that stays out of the way of the hierarchy you want to make +Your objects don't need to know anything about Qt. They just expose the few methods +that are required for the rest of the model to work. + +Why do this? +For some cases it makes sense to have a hierarchy built into your objects, but if you +don't want to tightly couple your hierarchy to Qt, you have limited choices on how to +proceed: + +One way would be to write a translation system that can map between your objects and the +Qt standard item hierarchy. This can be done in a non-intrusive way, but it requires +building and maintaining maps between the two separate hierarchies, or discovering the +relationship dynamically. It's totally doable, but the mapping feels fragile and it's +duplicating data (which I don't like) + +Another possible way is to create a custom model from your objects, which is extra work. +So I did that work and packaged it into this setup. + +Just inherit from TreeItem and override the methods that expose the tree structure. +Then you can attach the root of your hierarchy to the Adapter model, which translates +between your hierarchy and Qt. The TreeItem itself has nothing to do with QT. It +communicates to Qt via the OverserverServer. + +--- +I tried to plug as many leaks in this abstraction as I could, but there's still a couple +things that are required. You have to call self.notifyChanged() when a +value visible in the tree changes. And you have to use the managers when changing +the hierarchy. But I think that's it. + +This model doesn't handle the full "each cell can have its own separate table of +children" thing that the full QAbstractItemModel allows for. It's closer to the +QTreeWidget's model where each item is its own row. +""" + +from __future__ import annotations +from typing import Optional, Any, Generator +from Qt.QtGui import QIcon +from Qt.QtCore import QAbstractItemModel, QModelIndex, Qt, QObject +from contextlib import contextmanager, ExitStack + + +SimpleGenerator = Generator[None, None, None] + + +class ObserverServer: # it's just fun to say! + def __init__(self, model: AdapterModel): + self.model: AdapterModel = model + + def valueObserver(self, item: TreeItem) -> None: + self.model.itemDataChanged(item) + + @contextmanager + def insertManager(self, parent: TreeItem, row=-1) -> SimpleGenerator: + parIdx = self.model.indexFromItem(parent) + if row == -1: + row = parent.getItemAppendRow() + self.model.beginInsertRows(parIdx, row, row) + try: + yield + finally: + self.model.endInsertRows() + + @contextmanager + def removeManager(self, item: TreeItem) -> SimpleGenerator: + idx = self.model.indexFromItem(item) + valid = idx.isValid() + if valid: + parIdx = idx.parent() + self.model.beginRemoveRows(parIdx, idx.row(), idx.row()) + try: + yield + finally: + if valid: + self.model.endRemoveRows() + + @contextmanager + def moveManager( + self, item: TreeItem, destPar: TreeItem, destRow: int = -1 + ) -> SimpleGenerator: + itemIdx = self.model.indexFromItem(item) + destParIdx = self.model.indexFromItem(destPar) + handled = False + if itemIdx.isValid() and destParIdx.isValid(): + handled = True + srcParIdx = itemIdx.parent() + row = itemIdx.row() + if destRow == -1: + destRow = destPar.getItemAppendRow() + self.model.beginMoveRows(srcParIdx, row, row, destParIdx, destRow) + try: + yield + finally: + if handled: + self.model.endMoveRows() + + @contextmanager + def resetManager(self) -> SimpleGenerator: + self.model.beginResetModel() + try: + yield + finally: + self.model.endResetModel() + + +class TreeItem: + def __init__(self, root: TreeRootItem): + self.root: TreeRootItem = root + + @property + def observers(self) -> list[ObserverServer]: + return self.root.observerServers() + + def notifyChanged(self): + """Let the observers know that this item has changed""" + for ob in self.observers: + if ob.valueObserver is not None: + ob.valueObserver(self) + + @contextmanager + def insertItemManager(self, item: TreeItem, row: int = -1): + with ExitStack() as stack: + for ob in self.observers: + stack.enter_context(ob.insertManager(item, row=row)) + yield + + @contextmanager + def removeItemManager(self, item: TreeItem): + with ExitStack() as stack: + for ob in self.observers: + stack.enter_context(ob.removeManager(item)) + yield + + @contextmanager + def moveItemManager(self, item: TreeItem, destPar: TreeItem, destRow: int = -1): + with ExitStack() as stack: + for ob in self.observers: + stack.enter_context(ob.moveManager(item, destPar, destRow=destRow)) + yield + + @contextmanager + def resetManager(self, item: TreeItem, row: int = -1): + with ExitStack() as stack: + for ob in self.observers: + stack.enter_context(ob.resetManager()) + yield + + ################ + # Default implementations of the methods that are available to override + ################ + + def getItemAppendRow(self) -> int: + """Get the row to insert at to append a new item""" + return 0 + + def treeChild(self, row: int) -> Optional[TreeItem]: + """Return the child tree item at the given row if it exists""" + return None + + def treeRow(self) -> int: + """Return the row of the current item in the tree""" + return 0 + + def treeParent(self) -> Optional[TreeItem]: + """Return the parent of this TreeItem if it has one""" + return None + + def treeChildCount(self) -> int: + """Return the number of children this item has""" + return 0 + + def treeChecked(self) -> bool: + """Return whether this item is checked""" + return False + + def treeData(self, column: int) -> Optional[Any]: + """Return the data for the given column""" + return None + + def icon(self) -> Optional[QIcon]: + """Return the icon of this item, if it has one""" + return None + + +class TreeRootItem(TreeItem): + def __init__(self, observerServers: Optional[list[ObserverServer]] = None): + super().__init__(self) + # The root just keeps track of everybody's observers + if observerServers is None: + observerServers = [] + self._observerServers: list[ObserverServer] = observerServers + + def addObserver(self, observerServer: ObserverServer): + self._observerServers.append(observerServer) + + def removeObserver(self, observerServer: ObserverServer): + self._observerServers.remove(observerServer) + + def observerServers(self) -> list[ObserverServer]: + return self._observerServers[:] # Return a copy + + def columnCount(self) -> int: + return 1 + + +class AdapterModel(QAbstractItemModel): + """Model that adapts the values from the TreeItem to the AbstractItemModel interface""" + + def __init__( + self, rootItem: Optional[TreeRootItem] = None, parent: Optional[QObject] = None + ): + super().__init__(parent=parent) + self._rootItem: Optional[TreeRootItem] = None + self.observer = ObserverServer(self) + if rootItem is not None: + self.setRootItem(rootItem) + + def setRootItem(self, rootItem: TreeRootItem): + self.beginResetModel() + try: + if self._rootItem is not None: + self._rootItem.removeObserver(self.observer) + self._rootItem = rootItem + self._rootItem.addObserver(self.observer) + finally: + self.endResetModel() + + def getItemRow(self, item: Optional[TreeItem]) -> Optional[int]: + if item is None: + return None + return item.treeRow() + + def indexFromItem(self, item: TreeItem, column: int = 0) -> QModelIndex: + row = self.getItemRow(item) + if row is None: + return QModelIndex() + return self.createIndex(row, column, item) + + def itemFromIndex(self, index: QModelIndex) -> Optional[TreeItem]: + return index.internalPointer() + + def itemDataChanged(self, item: TreeItem): + idx = self.indexFromItem(item) + self.emitDataChanged(idx) + + def emitDataChanged(self, index: QModelIndex): + if index.isValid(): + self.dataChanged.emit(index, index, []) + + def getChildItem(self, parent: Optional[TreeItem], row: int) -> Optional[TreeItem]: + if parent is None: + if row == 0: + return self._rootItem + else: + return None + return parent.treeChild(row) + + def getParentItem(self, item: Optional[TreeItem]) -> Optional[TreeItem]: + if item is None: + return None + return item.treeParent() + + def getItemRowCount(self, item: Optional[TreeItem]) -> int: + if item is None: + # Null parent means return the only root item + ret = 1 + else: + ret = item.treeChildCount() + return ret + + def index(self, row: int, column: int, parIndex: QModelIndex) -> QModelIndex: + par = parIndex.internalPointer() + child = self.getChildItem(par, row) + if child is None: + return QModelIndex() + return self.createIndex(row, column, child) + + def parent(self, index: QModelIndex) -> QModelIndex: + if not index.isValid(): + return QModelIndex() + item = index.internalPointer() + if item is None: + return QModelIndex() + par = self.getParentItem(item) + if par is None: + return QModelIndex() + row = self.getItemRow(par) + if row is None: + return QModelIndex() + return self.createIndex(row, 0, par) + + def rowCount(self, parIndex: QModelIndex) -> int: + parent = parIndex.internalPointer() + ret = self.getItemRowCount(parent) + return ret + + def data(self, index: QModelIndex, role: int) -> Any: + if not index.isValid(): + return None + item = index.internalPointer() + return self.getItemData(item, index.column(), role) + + def columnCount(self, parIndex: QModelIndex) -> int: + if self._rootItem is None: + return 1 + return self._rootItem.columnCount() + + def getItemData( + self, item: Optional[TreeItem], column: int, role: int + ) -> Optional[Any]: + if item is None: + return None + + if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): + return item.treeData(column) + + elif role == Qt.ItemDataRole.CheckStateRole: + chk = None + if column == 0: + chk = item.treeChecked() + if chk is not None: + chk = Qt.CheckState.Checked if chk else Qt.CheckState.Unchecked + return chk + elif role == Qt.ItemDataRole.DecorationRole: + if column == 0: + return item.icon() + return None From 30278dea4c5dc9d52f977db1d8fa0cc932c1f71f Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 14 Aug 2026 15:24:55 -0700 Subject: [PATCH 22/47] Re-add the tree item stuff --- src/python/simplexui/items/combo.py | 45 +++++++++++-- src/python/simplexui/items/group.py | 75 ++++++++++++++------- src/python/simplexui/items/progression.py | 81 ++++++++++++++++------- src/python/simplexui/items/simplex.py | 27 ++++++-- src/python/simplexui/items/slider.py | 42 +++++++----- src/python/simplexui/items/traversal.py | 70 +++++++++++++++++++- src/python/simplexui/items/treeItem.py | 7 +- 7 files changed, 267 insertions(+), 80 deletions(-) diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index 4e42c45a..1eaf8357 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -20,6 +20,7 @@ from __future__ import annotations from .accessor import SimplexAccessor from .stack import stackable +from .treeItem import TreeItem from typing import TYPE_CHECKING, Optional, Any @@ -32,7 +33,7 @@ # Abstract Items -class ComboPair(SimplexAccessor): +class ComboPair(SimplexAccessor, TreeItem): """A Slider/Value pair for use in Combos""" classDepth: int = 6 @@ -60,8 +61,23 @@ def buildDefinition(self, simpDict: dict, legacy: bool) -> tuple[int, float]: sIdx = self.slider.buildDefinition(simpDict, legacy) return sIdx, self.value + def treeRow(self) -> int: + if self.combo is None: + return 0 + return self.combo.pairs.index(self) -class Combo(SimplexAccessor): + def treeParent(self) -> TreeItem: + return self.combo + + def treeData(self, column: int) -> Optional[Any]: + if column == 0: + return self.name + if column == 1: + return self.value + return None + + +class Combo(SimplexAccessor, TreeItem): """A group of Slider/Value pairs that control a Progression through some solver Combos allow for fixit shapes to be created for any number of user inputs. @@ -192,10 +208,10 @@ def createCombo( simplex: Simplex, sliders: list[Slider], values: list[float], - group: Optional[Group]=None, - shape: Optional[Shape]=None, - solveType: Optional[str]=None, - tVal: float=1.0, + group: Optional[Group] = None, + shape: Optional[Shape] = None, + solveType: Optional[str] = None, + tVal: float = 1.0, ): """Classmethod to create Combo with some hard-coded defaults @@ -674,3 +690,20 @@ def getInputVector(self): for cp in self.pairs: inVec[self.simplex.sliders.index(cp.slider)] = cp.value return inVec + + def treeChild(self, row: int) -> TreeItem: + if row == len(self.pairs): + return self.prog + return self.pairs[row] + + def treeRow(self) -> int: + return self.group.items.index(self) + + def treeParent(self) -> TreeItem: + return self.group + + def treeChildCount(self) -> int: + return len(self.pairs) + 1 + + def treeChecked(self) -> bool: + return self.enabled diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index b8e21b9b..dc7724d1 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -19,6 +19,7 @@ from __future__ import annotations from .accessor import SimplexAccessor from .stack import stackable +from .treeItem import TreeItem from typing import Optional, Union, Any, TYPE_CHECKING @@ -32,13 +33,13 @@ GroupMember = Union[Combo, Slider, Traversal] -class Group(SimplexAccessor): +class Group(SimplexAccessor, TreeItem): """Groups organize Simplex items - Groups have no purpose in the solver. They don't do anything other than organize - the items in a system. + Groups have no purpose in the solver. They don't do anything other than organize + the items in a system. - Each group can hold only one type of item (Slider, Combo, or Traversal) + Each group can hold only one type of item (Slider, Combo, or Traversal) Parameters ---------- @@ -48,10 +49,6 @@ class Group(SimplexAccessor): The Simplex system groupType : type The type that this group can hold - - Returns - ------- - """ classDepth: int = 1 @@ -73,12 +70,29 @@ def __init__( self._buildIdx: Optional[int] = None self.groupType: GroupType = groupType - if self.groupType is Slider: - self.simplex.sliderGroups.append(self) - elif self.groupType is Combo: - self.simplex.comboGroups.append(self) - elif self.groupType is Traversal: - self.simplex.traversalGroups.append(self) + with self.insertItemManager(simplex, row=self._getInsertionRow()): + if self.groupType is Slider: + self.simplex.sliderGroups.append(self) + elif self.groupType is Combo: + self.simplex.comboGroups.append(self) + elif self.groupType is Traversal: + self.simplex.traversalGroups.append(self) + + def _getInsertionRow(self) -> int: + from .combo import Combo + from .slider import Slider + from .traversal import Traversal + + c = len(self.simplex.sliderGroups) + if self.groupType is Slider: + return c + c += len(self.simplex.comboGroups) + if self.groupType is Combo: + return c + c += len(self.simplex.traversalGroups) + if self.groupType is Traversal: + return c + return 0 @property def name(self) -> str: @@ -116,7 +130,9 @@ def createGroup( if groupType is None: if not things: - raise ValueError("Cannot build a group without setting the type, or giving an object to infer from") + raise ValueError( + "Cannot build a group without setting the type, or giving an object to infer from" + ) groupType = type(things[0]) g = cls(name, simplex, groupType) @@ -204,11 +220,12 @@ def delete(self): else: raise RuntimeError("Somehow this group has no type") - # Delete the children first - # Gotta iterate over copies of the lists - # as .delete removes the items from the list - for item in self.items[:]: - item.delete() + with self.removeItemManager(self): + # Delete the children first + # Gotta iterate over copies of the lists + # as .delete removes the items from the list + for item in self.items[:]: + item.delete() gList.remove(self) @@ -220,10 +237,6 @@ def take(self, things: list[GroupMember]): ---------- things : [object A list of things to put in this group - - Returns - ------- - """ if self.groupType is None: self.groupType = type(things[0]) @@ -237,3 +250,17 @@ def take(self, things: list[GroupMember]): for thing in things: if thing not in self.items: thing.setGroup(self) + + + def treeChild(self, row) -> TreeItem: + return self.items[row] + + def treeRow(self) -> int: + return self.simplex.groups.index(self) + + def treeParent(self) -> TreeItem: + return self.simplex + + def treeChildCount(self) -> int: + return len(self.items) + diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index 88826d05..00e809d5 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -21,6 +21,7 @@ from ..utils import getNextName from .accessor import SimplexAccessor from .stack import stackable +from .treeItem import TreeItem from typing import Optional, Union, Any, TYPE_CHECKING @@ -34,7 +35,7 @@ from .traversal import Traversal -class ProgPair(SimplexAccessor): +class ProgPair(SimplexAccessor, TreeItem): classDepth: int = 9 def __init__(self, simplex: Simplex, shape: Shape, value: float): @@ -96,8 +97,32 @@ def delete(self): self.simplex.shapes.remove(pp.shape) self.DCC.deleteShape(pp.shape) + def treeRow(self) -> int: + if self.prog is None: + return 0 + return self.prog.pairs.index(self) + + def treeParent(self) -> TreeItem: + from .slider import Slider + + if self.prog is None: + raise ValueError( + "Somehow you're trying to display a ProgPair without a Prog" + ) + par = self.prog + if isinstance(par.controller, Slider): + par = par.controller + return par + + def treeData(self, column) -> Optional[Any]: + if column == 0: + return self.name + if column == 2: + return self.value + return None + -class Progression(SimplexAccessor): +class Progression(SimplexAccessor, TreeItem): """A set of shapes to interpolate between A Progression is a collection of shape/value pairs, and an interpolation type. @@ -130,10 +155,6 @@ class Progression(SimplexAccessor): falloffs : [Falloff A list of Fallofs to apply to the progression Defaults to None - - Returns - ------- - """ classDepth: int = 8 @@ -206,7 +227,6 @@ def getShapeIndex(self, shape: Shape) -> int: ------- : int The index of the ProgPair that contains the given shape - """ for i, p in enumerate(self.pairs): if p.shape == shape: @@ -220,7 +240,6 @@ def getShapes(self) -> list[Shape]: ------- : type ([Shape, ....]): The shapes in the Progression - """ return [i.shape for i in self.pairs] @@ -231,7 +250,6 @@ def getValues(self) -> list[float]: ------- : type ([float, ....]): The values in the Progression - """ return [i.value for i in self.pairs] @@ -247,7 +265,6 @@ def getInsertIndex(self, tVal: float) -> int: ------- : int The insertion index - """ values = self.getValues() if not values: @@ -276,7 +293,6 @@ def getShapeAtValue(self, val: float, tol=0.0001) -> Optional[Shape]: ------- : type (Shape or None): The shape found with the given value, or None if nothing was found - """ for pp in self.pairs: if abs(pp.value - val) < tol: @@ -298,7 +314,6 @@ def loadV2(cls, simplex: Simplex, data: dict[str, Any]) -> Progression: ------- : Progression The specified Progression - """ name = data["name"] pairs = data["pairs"] @@ -396,10 +411,6 @@ def addFalloff(self, falloff: Falloff): ---------- falloff : Falloff The falloff to add - - Returns - ------- - """ if falloff not in self.falloffs: self.falloffs.append(falloff) @@ -414,10 +425,6 @@ def removeFalloff(self, falloff: Falloff): ---------- falloff : Falloff The falloff to remove - - Returns - ------- - """ if falloff in self.falloffs: self.falloffs.remove(falloff) @@ -443,7 +450,6 @@ def createShape( ------- : ProgPair The newly created ProgPair - """ from .slider import Slider @@ -476,7 +482,6 @@ def newProgPair( The newly created ProgPair : int The insertion index for this ProgPair into this Progression - """ from .shape import Shape @@ -508,7 +513,6 @@ def guessNextTVal(self) -> float: ------- : float The "smart" guess for the next tVal - """ # The question remains if negative or # intermediate values are more important @@ -565,7 +569,6 @@ def getRange(self) -> tuple[float, float]: The minimum value : float The maximum value - """ vals = [i.value for i in self.pairs] return min(vals), max(vals) @@ -584,3 +587,33 @@ def getExtremePairs(self) -> list[ProgPair]: continue ret.append(pp) return ret + + def treeChild(self, row: int) -> TreeItem: + return self.pairs[row] + + def treeRow(self) -> int: + from .combo import Combo + from .traversal import Traversal + + if isinstance(self.controller, Traversal): + # Show the progression after the mult and prog + return 2 + elif isinstance(self.controller, Combo): + # Show the progression after the comboPairs + return len(self.controller.pairs) + return 0 + + def treeParent(self) -> TreeItem: + if self.controller is None: + raise ValueError( + "Somehow you're trying to display a Progression without a Controller" + ) + return self.controller + + def treeChildCount(self): + return len(self.pairs) + + def treeData(self, column: int) -> Optional[Any]: + if column == 0: + return "SHAPES" + return None diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 5288d7f8..88106c81 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -44,9 +44,11 @@ from .slider import Slider from .stack import Stack, stackable from .traversal import Traversal, TravPair +from .treeItem import TreeRootItem from typing import Optional, Any, TYPE_CHECKING, Union + if TYPE_CHECKING: from Qt.QtWidgets import QProgressDialog @@ -56,7 +58,7 @@ Splittable = Union[Shape, Progression, Slider, Combo, Traversal] -class Simplex(object): +class Simplex(TreeRootItem): """The main Top-level abstract object that controls an entire setup Simplex objects contain and manage the entire hierarchy. They have methods @@ -88,6 +90,7 @@ def __init__(self, name: str = "", forceDummy=False, sliderMul=1.0): Returns ------- """ + super().__init__() self._name: str = name # The name of the system self.sliders: list[Slider] = [] # List of contained sliders self.combos: list[Combo] = [] # List of contained combos @@ -104,6 +107,9 @@ def __init__(self, name: str = "", forceDummy=False, sliderMul=1.0): self._extras: dict[str, Any] = {} # extra key data to store in the output json self._legacy: bool = False # whether to write the legacy types + def columnCount(self): + return 3 + @property def simplex(self): """A uniform accessor so that we can always get the root from any object""" @@ -657,9 +663,10 @@ def comboExists( def deleteSystem(self): """Delete an existing system from the DCC""" # Store the models as temp so the model doesn't go crazy with the signals - self.DCC.deleteSystem() - self._initValues() - self.DCC = DCC(self) + with self.resetManager(): + self.DCC.deleteSystem() + self._initValues() + self.DCC = DCC(self) def getComboUpstreams(self, combo: Combo) -> list[Combo]: """Get a list of only combos that are upstream to the given combo @@ -1782,3 +1789,15 @@ def split(self, pBar: Optional[QProgressDialog] = None) -> Simplex: splitSmpx.DCC.pushAllShapeVertices(splitSmpx.shapes) return splitSmpx + + # TREE CODE + def treeChild(self, row): + return self.groups[row] + + def treeChildCount(self): + return len(self.groups) + + def treeData(self, column): + if column == 0: + return self.name + return None diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index aaab41c9..fe1dc146 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -26,6 +26,7 @@ from .progression import ProgPair, Progression from .shape import Shape from .stack import stackable +from .treeItem import TreeItem from typing import Optional, Any, TYPE_CHECKING @@ -33,7 +34,7 @@ from .simplex import DCCObject, Simplex -class Slider(SimplexAccessor): +class Slider(SimplexAccessor, TreeItem): """A user-input to the simplex system that directly controls a Progression Parameters @@ -50,10 +51,6 @@ class Slider(SimplexAccessor): The color of this item in the UI create : bool Whether to create a DCC Shape, or look for it already in-scene - - Returns - ------- - """ classDepth: int = 7 @@ -377,6 +374,8 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: Returns ------- + : int + The build index """ if self._buildIdx is None: @@ -400,13 +399,6 @@ def clearBuildIndex(self): The buildIndex is stored when building a definition dictionary that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - """ self._buildIdx = None self.prog.clearBuildIndex() @@ -609,14 +601,10 @@ def getInputVectors(self) -> list[list[float]]: """Get the ordered values for input to the solver that activate this Slider Multiple outputs are possible if the slider has -1 to 1 range - Parameters - ---------- - Returns ------- : [[float, ....], ....] The ordered slider values - """ inVecs = [] for pp in self.prog.getExtremePairs(): @@ -624,3 +612,25 @@ def getInputVectors(self) -> list[list[float]]: inVec[self.simplex.sliders.index(self)] = pp.value inVecs.append(inVec) return inVecs + + def treeChild(self, row: int) -> TreeItem: + return self.prog.pairs[row] + + def treeRow(self) -> int: + return self.group.items.index(self) + + def treeParent(self) -> TreeItem: + return self.group + + def treeChildCount(self) -> int: + return len(self.prog.pairs) + + def treeData(self, column: int) -> Optional[Any]: + if column == 0: + return self.name + if column == 1: + return self.value + return None + + def treeChecked(self) -> bool: + return self.enabled diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index 9bf04559..db151b48 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -23,6 +23,7 @@ from .progression import Progression from .slider import Slider from .stack import stackable +from .treeItem import TreeItem from enum import Enum @@ -38,7 +39,7 @@ class TravSide(Enum): End = "END" -class TravPair(SimplexAccessor): +class TravPair(SimplexAccessor, TreeItem): classDepth: int = 4 def __init__(self, slider: Slider, value: float): @@ -90,8 +91,27 @@ def removeAll(pairs: list[TravPair]): if trav is not None: trav.removePairs(pairs) + def treeRow(self) -> int: + if self.travPoint is None: + raise ValueError( + "Somehow you're trying to show a TravPair with no TravPoint" + ) + return self.travPoint.pairs.index(self) + + def treeParent(self) -> TreeItem: + if self.travPoint is None: + raise ValueError("") + return self.travPoint + + def treeData(self, column: int) -> Optional[Any]: + if column == 0: + return self.name + if column == 1: + return self.value + return None -class TravPoint(SimplexAccessor): + +class TravPoint(SimplexAccessor, TreeItem): classDepth: int = 3 def __init__(self, pairs: list[TravPair], side: TravSide): @@ -165,8 +185,31 @@ def getInputVector(self) -> list[float]: invec[self.simplex.sliders.index(cp.slider)] = cp.value return invec + def treeData(self, column: int) -> Optional[Any]: + if column == 0: + return self.name + return None + + def treeChild(self, row: int) -> TreeItem: + return self.pairs[row] + + def treeRow(self) -> int: + if self.side == TravSide.Start: + return 0 + return 1 + + def treeParent(self) -> TreeItem: + if self.traversal is None: + raise ValueError( + "Somehow you're trying to show a TravPoint without a Traversal" + ) + return self.traversal + + def treeChildCount(self) -> int: + return len(self.pairs) -class Traversal(SimplexAccessor): + +class Traversal(SimplexAccessor, TreeItem): """Traversals control a Progression based on any 2 points in the Solver space. Traversals only make sense with intermediate shapes in the progression of the sliders @@ -741,3 +784,24 @@ def _lerp(s, e, v): return s * (1 - v) + e * v return [_lerp(fs, fe, value) for fs, fe in zip(fullStart, fullEnd)] + + def treeChild(self, row: int) -> TreeItem: + if row == 0: + return self.startPoint + elif row == 1: + return self.endPoint + elif row == 2: + return self.prog + raise ValueError("Somehow have a Traversal item with more than 3 children") + + def treeRow(self) -> int: + return self.group.items.index(self) + + def treeParent(self) -> TreeItem: + return self.group + + def treeChildCount(self) -> int: + return 3 + + def treeChecked(self) -> bool: + return self.enabled diff --git a/src/python/simplexui/items/treeItem.py b/src/python/simplexui/items/treeItem.py index 90ac4d44..5cb2eca8 100644 --- a/src/python/simplexui/items/treeItem.py +++ b/src/python/simplexui/items/treeItem.py @@ -154,7 +154,7 @@ def moveItemManager(self, item: TreeItem, destPar: TreeItem, destRow: int = -1): yield @contextmanager - def resetManager(self, item: TreeItem, row: int = -1): + def resetManager(self): with ExitStack() as stack: for ob in self.observers: stack.enter_context(ob.resetManager()) @@ -184,9 +184,9 @@ def treeChildCount(self) -> int: """Return the number of children this item has""" return 0 - def treeChecked(self) -> bool: + def treeChecked(self) -> Optional[bool]: """Return whether this item is checked""" - return False + return None def treeData(self, column: int) -> Optional[Any]: """Return the data for the given column""" @@ -214,6 +214,7 @@ def removeObserver(self, observerServer: ObserverServer): def observerServers(self) -> list[ObserverServer]: return self._observerServers[:] # Return a copy + # Override this def columnCount(self) -> int: return 1 From 6139b5d109e7236f231eae1745ee6a75502dd548 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 14 Aug 2026 15:54:17 -0700 Subject: [PATCH 23/47] Make sure all the contextmanagers are run, and clean up the unused parts of the docstrings --- src/python/simplexui/items/accessor.py | 1 - src/python/simplexui/items/combo.py | 154 ++++++---------------- src/python/simplexui/items/falloff.py | 25 ---- src/python/simplexui/items/group.py | 1 - src/python/simplexui/items/progression.py | 43 +++--- src/python/simplexui/items/shape.py | 32 ----- src/python/simplexui/items/simplex.py | 93 ------------- src/python/simplexui/items/slider.py | 46 ++++--- src/python/simplexui/items/stack.py | 1 - src/python/simplexui/items/traversal.py | 82 +++++------- 10 files changed, 118 insertions(+), 360 deletions(-) diff --git a/src/python/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py index 226d103d..3988d04a 100644 --- a/src/python/simplexui/items/accessor.py +++ b/src/python/simplexui/items/accessor.py @@ -103,7 +103,6 @@ def buildLinkedRename( ------- dict : A new set of propsed renames - """ # Build the output dict if not done already if currentLinks is None: diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index 1eaf8357..3a193090 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -100,10 +100,6 @@ class Combo(SimplexAccessor, TreeItem): The solve type for this combo. See Combo.solveTypes for a list color : QColor The color of this item in the UI - - Returns - ------- - """ classDepth = 5 @@ -126,25 +122,27 @@ def __init__( group: Group, solveType: Optional[str], ): - super(Combo, self).__init__(simplex) + super().__init__(simplex) if group.groupType is not type(self): raise ValueError("Cannot add this slider to a combo group") - - self._name: str = name - self.pairs = pairs - self.prog = prog - self._solveType: Optional[str] = solveType - self._buildIdx = None - self.expanded = {} - self._enabled = True - self._freezeThing = None - self.group: Group = group - for p in self.pairs: - p.combo = self - self.prog.controller = self - self.group.items.append(self) - self.simplex.combos.append(self) + with self.stack.store(self): + self._name: str = name + self.pairs = pairs + self.prog = prog + self._solveType: Optional[str] = solveType + self._buildIdx = None + self.expanded = {} + self._enabled = True + self._freezeThing = None + + with self.insertItemManager(group): + self.group: Group = group + for p in self.pairs: + p.combo = self + self.prog.controller = self + self.group.items.append(self) + self.simplex.combos.append(self) @property def enabled(self) -> bool: @@ -192,7 +190,6 @@ def comboAlreadyExists( ------- : Combo or None The combo that exists with the given values, or None if none exist - """ checker = {(s.name, v) for s, v in zip(sliders, values)} for combo in simplex.combos: @@ -239,7 +236,6 @@ def createCombo( ------- : Combo The newly created Combo - """ if simplex.restShape is None: raise RuntimeError("Simplex system is missing rest shape") @@ -290,7 +286,6 @@ def buildComboName(sliders, values): ------- : str The suggested combo name - """ pairs = list(zip(sliders, values)) pairs = sorted(pairs, key=lambda x: x[0].name) @@ -368,7 +363,6 @@ def nameLinks(self): ------- : type progression depends on this slider's name - """ # In this case, these names will *NOT* have the possibility of # a pos/neg name. Only the combo name, and possibly a percentage @@ -383,36 +377,13 @@ def nameLinks(self): return [i == self.name for i in shapeNames] def getSliderIndex(self, slider): - """ - - Parameters - ---------- - slider : - - - Returns - ------- - type - - - """ for i, p in enumerate(self.pairs): if p.slider == slider: return i raise ValueError("Provided slider:{0} is not in the list".format(slider.name)) def isFloating(self): - """ - - Parameters - ---------- - - Returns - ------- - : type - Floating combos are combos that Slider values that are between 0 and 1 - - """ + """Floating combos are combos that Slider values that are between 0 and 1""" for pair in self.pairs: if abs(pair.value) != 1.0: return True @@ -439,7 +410,6 @@ def loadV2(cls, simplex, progs, data): ------- : Combo The specified combo - """ name = data["name"] prog = progs[data["prog"]] @@ -457,10 +427,6 @@ def buildDefinition(self, simpDict, legacy): The dictionary that is being built legacy : bool Whether to write out the legacy definition, or the newer one - - Returns - ------- - """ if self._buildIdx is None: self._buildIdx = len(simpDict["combos"]) @@ -487,13 +453,6 @@ def clearBuildIndex(self): The buildIndex is stored when building a definition dictionary that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - """ self._buildIdx = None self.prog.clearBuildIndex() @@ -515,7 +474,6 @@ def extractShape(self, shape, live=True, offset=10.0): ------- : object The DCC mesh just created - """ return self.DCC.extractComboShape(self, shape, live, offset) @@ -533,10 +491,6 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): Whether to maintain a live connecto to the mesh in the DCC (Default value = False) delete : bool Whether to delete the DCC mesh after it was connected (Default value = False) - - Returns - ------- - """ self.DCC.connectComboShape(self, shape, mesh, live, delete) @@ -545,14 +499,16 @@ def delete(self): """Delete this combo and any shapes it contains""" if self not in self.group.items: return # Can happen when deleting multiple groups - self.group.items.remove(self) - self.group = None # type: ignore # Just clearing out references - self.simplex.combos.remove(self) - pairs = self.prog.pairs[:] # gotta make a copy - for pp in pairs: - if not pp.shape.isRest: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) + + with self.removeItemManager(self): + self.group.items.remove(self) + self.group = None # type: ignore # Just clearing out references + self.simplex.combos.remove(self) + pairs = self.prog.pairs[:] # gotta make a copy + for pp in pairs: + if not pp.shape.isRest: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) @stackable def setInterpolation(self, interp): @@ -562,10 +518,6 @@ def setInterpolation(self, interp): ---------- interp : str The interpolation for this combo's progression - - Returns - ------- - """ self.prog.interp = interp @@ -579,10 +531,6 @@ def setComboValue(self, slider, value): The slider to set the value for value : float The value to set the Slider to - - Returns - ------- - """ idx = self.getSliderIndex(slider) pair = self.pairs[idx] @@ -598,14 +546,11 @@ def appendComboValue(self, slider, value): The slider to insert value : float The value to set the Slider to - - Returns - ------- - """ cp = ComboPair(slider, value) - self.pairs.append(cp) - cp.combo = self + with self.insertItemManager(self): + self.pairs.append(cp) + cp.combo = self @stackable def deleteComboPair(self, comboPair): @@ -615,16 +560,13 @@ def deleteComboPair(self, comboPair): ---------- comboPair : ComboPair The ComboPair to delete - - Returns - ------- - """ # We specifically don't move the combo to the proper depth group # That way the user can make multiple changes to the combo without # it popping all over in the heirarchy - self.pairs.remove(comboPair) - comboPair.combo = None + with self.removeItemManager(comboPair): + self.pairs.remove(comboPair) + comboPair.combo = None @stackable def setGroup(self, grp): @@ -634,10 +576,6 @@ def setGroup(self, grp): ---------- grp : Group The group to set - - Returns - ------- - """ if grp.groupType is None: grp.groupType = type(self) @@ -647,10 +585,11 @@ def setGroup(self, grp): "All items in this group must be of type: {}".format(grp.groupType) ) - if self.group: - self.group.items.remove(self) - grp.items.append(self) - self.group = grp + with self.moveItemManager(self, grp): + if self.group: + self.group.items.remove(self) + grp.items.append(self) + self.group = grp @stackable def createShape(self, shapeName=None, tVal=None): @@ -664,27 +603,20 @@ def createShape(self, shapeName=None, tVal=None): tVal : float or None The progression value to set for the new Shape. If None, it gets a "smart" default value - - Returns - ------- - """ pp, idx = self.prog.newProgPair(shapeName, tVal) - pp.prog = self.prog - self.prog.pairs.insert(idx, pp) + with self.insertItemManager(self.prog, row=idx): + pp.prog = self.prog + self.prog.pairs.insert(idx, pp) return pp def getInputVector(self): """Get the input to the Solver that would fully activate this Combo - Parameters - ---------- - Returns ------- : [float, ...] The ordered slider values - """ inVec = [0.0] * len(self.simplex.sliders) for cp in self.pairs: diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index 0981c054..268e6dfd 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -81,10 +81,6 @@ class Falloff(SimplexAccessor): *data : list The data used to build this falloff. You should use one of the classmethod like Falloff.createPlanar or Falloff.createMap instead - - Returns - ------- - """ LEFTSIDE: str = "L" @@ -159,7 +155,6 @@ def loadV2(cls, simplex: Simplex, data: dict[str, Any]) -> Falloff: ------- : Falloff The specified Falloff - """ tpe = data["type"] name = data["name"] @@ -210,7 +205,6 @@ def duplicate(self, newName: str) -> Falloff: ------- : Falloff The newly duplicated Falloff - """ nf = copy.copy(self) nf.name = newName @@ -313,7 +307,6 @@ def getSidedName(self, name: str, sIdx: int) -> str: ------- : str The newly sided name - """ search = self.search replace = self.rep[sIdx] @@ -346,7 +339,6 @@ def canRename(self, item: SimplexAccessor) -> bool: ------- : bool Whether this object can be renamed - """ nn = self.getSidedName(item.name, 0) return nn != item.name @@ -360,10 +352,6 @@ def splitRename(self, item: SimplexAccessor, sIdx: int): The named Simplex Item sIdx : int The replacement index - - Returns - ------- - """ from .combo import Combo from .shape import Shape @@ -465,10 +453,6 @@ def createPlanar( The (0, 1) range of the min cubic falloff handle minVal : float The value past which the falloff is 0.0 - - Returns - ------- - """ return cls(name, simplex, axis, maxVal, maxHandle, minHandle, minVal) @@ -495,10 +479,6 @@ def setPlanarData( The (0, 1) range of the min cubic falloff handle minVal : float The value past which the falloff is 0.0 - - Returns - ------- - """ self.axis = axis self.minVal = minVal @@ -612,7 +592,6 @@ def getMultiplier(self, xVal: float) -> float: ------- : float The weight - """ # Vertices are assumed to be at (0,0) and (1,1) if xVal <= self.minVal: @@ -731,10 +710,6 @@ def createMap( The name of the weightmap axis : str The axis to align the falloff to. X, Y, or Z - - Returns - ------- - """ return cls(name, simplex, mapName, axis) diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index dc7724d1..7afbdd38 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -155,7 +155,6 @@ def loadV2(cls, simplex: Simplex, data: dict[str, Any]) -> Group: ------- : Group The specified Group - """ from .combo import Combo from .slider import Slider diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index 00e809d5..c4c7cd6d 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -87,15 +87,15 @@ def value(self, val: float): def delete(self): if self.prog is None: return - - ppairs = self.prog.pairs - ridx = ppairs.index(self) - pp = ppairs.pop(ridx) - if not pp.shape.isRest: - pp.shape.progPairs.remove(pp) - if not pp.shape.progPairs: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) + with self.removeItemManager(self): + ppairs = self.prog.pairs + ridx = ppairs.index(self) + pp = ppairs.pop(ridx) + if not pp.shape.isRest: + pp.shape.progPairs.remove(pp) + if not pp.shape.progPairs: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) def treeRow(self) -> int: if self.prog is None: @@ -454,8 +454,9 @@ def createShape( from .slider import Slider pp, idx = self.newProgPair(shapeName, tVal) - pp.prog = self - self.pairs.insert(idx, pp) + with self.insertItemManager(self, row=idx): + pp.prog = self + self.pairs.insert(idx, pp) if isinstance(self.controller, Slider): self.controller.updateRange() @@ -547,19 +548,21 @@ def deleteShape(self, shape: Shape): raise RuntimeError("Shape does not exist to remove") pp = self.pairs[ridx] - self.pairs.pop(ridx) - if not shape.isRest: - self.simplex.shapes.remove(shape) - self.DCC.deleteShape(shape) + with self.removeItemManager(pp): + self.pairs.pop(ridx) + if not shape.isRest: + self.simplex.shapes.remove(shape) + self.DCC.deleteShape(shape) @stackable def delete(self): """Delete the Progression and all its Shapes""" - for pp in self.pairs[:]: - if pp.shape.isRest: - continue - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) + with self.removeItemManager(self): + for pp in self.pairs[:]: + if pp.shape.isRest: + continue + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) def getRange(self) -> tuple[float, float]: """Get the range for this Progression diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index 243fbc45..8ff56e77 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -52,10 +52,6 @@ class Shape(SimplexAccessor): Whether to create the DCC Shape, or look for it already in-scene color : QColor The color of this item in the Ui - - Returns - ------- - """ classDepth: int = 10 @@ -104,7 +100,6 @@ def createShape( ------- : Shape The new Shape - """ if simplex.restShape is None: raise RuntimeError("Simplex system is missing rest shape") @@ -138,7 +133,6 @@ def buildRest(cls, simplex: Simplex) -> Shape: ------- : Shape The system's rest Shape - """ rest = cls(simplex.getRestName(), simplex, create=True) rest.isRest = True @@ -204,7 +198,6 @@ def _buildLinkedRename( ) return currentLinks - """ # First, check for a slider rename, # if so, recurse into that slider @@ -268,7 +261,6 @@ def loadV2(cls, simplex: Simplex, data: dict[str, Any], create: bool) -> Shape: ------- : Shape The specified Shape - """ return cls(data["name"], simplex, create) @@ -281,10 +273,6 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: The dictionary that is being built legacy : bool Whether to write out the legacy definition, or the newer one - - Returns - ------- - """ if self._buildIdx is None: self._buildIdx = len(simpDict["shapes"]) @@ -302,13 +290,6 @@ def clearBuildIndex(self): The buildIndex is stored when building a definition dictionary that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - """ self._buildIdx = None @@ -324,10 +305,6 @@ def zeroShapes(shapes): ---------- shapes : [Shape Shapes to be zeroed - - Returns - ------- - """ for shape in shapes: if not shape.isRest: @@ -349,10 +326,6 @@ def connectShape( Whether or not to create a live connection in the DCC. Defaults False delete : bool Whether to delete the DCC Mesh after its connection. Defaults False - - Returns - ------- - """ self.DCC.connectShape(self, mesh, live, delete) @@ -375,10 +348,6 @@ def connectShapes( Whether or not to create a live connection in the DCC. Defaults False delete : bool Whether to delete the DCC Mesh after its connection. Defaults False - - Returns - ------- - """ with undoContext(): for shape, mesh in zip(shapes, meshes): @@ -399,7 +368,6 @@ def isNumberField(val: str) -> bool: ------- : bool Whether the field is numeric - """ if not val: return False diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 88106c81..2415af93 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -67,7 +67,6 @@ class Simplex(TreeRootItem): Finally Simplex systems handle splitting, which will be covered more in depth in the documentation for the split method. - """ classDepth = 0 @@ -178,7 +177,6 @@ def buildBaseObject( ------- : object A reference to the DCC mesh - """ iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) try: @@ -220,7 +218,6 @@ def buildEmptySystem( ------- : Simplex A newly created Simplex system - """ self = cls(name, forceDummy=forceDummy, sliderMul=sliderMul) self.DCC.loadNodes(self, thing, create=True) @@ -261,7 +258,6 @@ def buildSystemFromJsonString( ------- : Simplex A newly created Simplex system - """ js = json.loads(jsString) if name is None: @@ -304,7 +300,6 @@ def buildSystemFromJson( ------- : Simplex A newly created Simplex system - """ with open(jsPath, "r") as f: jsString = f.read() @@ -353,7 +348,6 @@ def buildSystemFromSmpx( ------- : Simplex A newly created Simplex system - """ if thing is None: thing = cls.buildBaseObject(smpxPath, forceDummy=forceDummy) @@ -414,7 +408,6 @@ def buildSystemFromFile( ------- : Simplex A newly created Simplex system - """ if path.endswith(".json"): return cls.buildSystemFromJson( @@ -470,7 +463,6 @@ def buildSystemFromMesh( ------- : Simplex A newly created Simplex system - """ jsDict = json.loads(DCC.getSimplexStringOnThing(thing, name)) return cls.buildSystemFromDict( @@ -521,7 +513,6 @@ def buildSystemFromDict( ------- : Simplex A newly created Simplex system - """ if name is None: name = jsDict["systemName"] @@ -540,10 +531,6 @@ def loadSmpxShapes(self, smpxPath: str, pBar: Optional[QProgressDialog] = None): The path to the .smpx file pBar : QProgressDialog, optional If provided, display progress in this dialog - - Returns - ------- - """ iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) js = json.loads(jsString) @@ -563,10 +550,6 @@ def loadSmpxPoses(self, smpxPath: str, pBar: Optional[QProgressDialog] = None): The path to the .smpx file pBar : QProgressDialog, optional If provided, display progress in this dialog - - Returns - ------- - """ iarch, abcMesh, jsString = getSmpxArchiveData(smpxPath) js = json.loads(jsString) @@ -585,10 +568,6 @@ def loadSmpxFalloffs(self, abcPath: str, pBar: Optional[QProgressDialog] = None) Path to the .smpx file pBar : QProgressDialog, optional If provided, display progress in this dialog - - Returns - ------- - """ foDict = readFalloffData(abcPath) @@ -650,7 +629,6 @@ def comboExists( ------- : Combo or None The Combo with those sliders and values, or None if none found - """ checkSet = {(s.name, v) for s, v in zip(sliders, values)} for cmb in self.combos: @@ -685,7 +663,6 @@ def getComboUpstreams(self, combo: Combo) -> list[Combo]: ------- : [Combo, ....] The list of upstream combos - """ pairDict = {p.slider: p.value for p in combo.pairs} @@ -727,7 +704,6 @@ def getDownstreamTraversals(self, slider: Slider) -> list[Traversal]: ------- : [Traversal, ....] The list of dependent Traversals - """ downstream = [] if not isinstance(slider, Slider): @@ -752,7 +728,6 @@ def getDownstreamCombos(self, slider: Slider) -> list[Combo]: ------- : [Combo, ....] The list of dependent Combos - """ downstream = [] if not isinstance(slider, Slider): @@ -772,10 +747,6 @@ def deleteDownstream(self, item: Slider): ---------- item : object The system item to check - - Returns - ------- - """ todel = [] todel.extend(self.getDownstreamCombos(item)) @@ -791,24 +762,16 @@ def setLegacy(self, legacy: bool): ---------- legacy : bool Whether to use the legacy .json format - - Returns - ------- - """ self._legacy = legacy def getFloatingShapes(self) -> list[Combo]: """Find Combos with values other than -1 and 1 - Parameters - ---------- - Returns ------- : [Combo, ....] Combos that don't have fully extreme activations - """ floaters = [c for c in self.combos if c.isFloating()] floatShapes = [] @@ -820,14 +783,10 @@ def buildDefinition(self) -> dict[str, Any]: """Create a simplex definition dictionary Loop through all the objects managed by this simplex system, and build a dictionary that defines it - Parameters - ---------- - Returns ------- : dict The simplex definition dictionary - """ things = [ self.shapes, @@ -897,10 +856,6 @@ def loadDefinition( Defaults to True pBar : QProgressDialog, optional If provided, display progress in this dialog - - Returns - ------- - """ self.name = simpDict["systemName"] @@ -924,10 +879,6 @@ def _incPBar(self, pBar: QProgressDialog, txt: str, inc: int = 1) -> bool: inc : (Default value = 1) - - Returns - ------- - """ if pBar is not None: pBar.setValue(pBar.value() + inc) @@ -954,10 +905,6 @@ def loadV3( Defaults to True pBar : QProgressDialog, optional If provided, display progress in this dialog - - Returns - ------- - """ preRet = self.DCC.preLoad(self, simpDict, create=create, pBar=pBar) try: @@ -1018,10 +965,6 @@ def loadV2( Defaults to True pBar : QProgressDialog, optional If provided, display progress in this dialog - - Returns - ------- - """ preRet = self.DCC.preLoad(self, simpDict, create=create, pBar=pBar) try: @@ -1082,10 +1025,6 @@ def loadV1( Defaults to True pBar : QProgressDialog, optional If provided, display progress in this dialog - - Returns - ------- - """ preRet = self.DCC.preLoad(self, simpDict, create=create, pBar=pBar) try: @@ -1203,10 +1142,6 @@ def storeExtras(self, simpDict: dict[str, Any]): ---------- simpDict : dict The simplex definition dictionary - - Returns - ------- - """ sd = copy.deepcopy(simpDict) knownTopLevel = [ @@ -1234,38 +1169,26 @@ def loadJSON(self, jsString: str): ---------- jsString : str The json formatted definition string - - Returns - ------- - """ self.loadDefinition(json.loads(jsString)) def getRestName(self) -> str: """Get the default rest shape name - Parameters - ---------- - Returns ------- : str The default rest shape name - """ return "Rest_{0}".format(self.name) def dump(self) -> str: """Dump the definition dictionary to a json string - Parameters - ---------- - Returns ------- : str The json formatted definition string - """ return json.dumps(self.buildDefinition()) @@ -1278,10 +1201,6 @@ def exportAbc(self, path: str, pBar: Optional[QProgressDialog] = None): The path to export to pBar : QProgressDialog, optional If provided, display progress in this dialog - - Returns - ------- - """ defDict = self.buildDefinition() jsString = json.dumps(defDict) @@ -1326,7 +1245,6 @@ def exportOther( Whether to do the export in worldspace. Defaults to False pBar : QProgressDialog, optional If provided, display progress in this dialog - """ defDict = self.buildDefinition() jsString = json.dumps(defDict) @@ -1346,10 +1264,6 @@ def setSlidersWeights(self, sliders: list[Slider], weights: list[float]): The list of Sliders to set weights for weights : [float, ....] The weights to set - - Returns - ------- - """ with undoContext(self.DCC): for slider, weight in zip(sliders, weights): @@ -1363,10 +1277,6 @@ def extractRestShape(self, offset: int = 0) -> Optional[DCCObject]: ---------- offset : float The offset value given to the extracted mesh (Default value = 0) - - Returns - ------- - """ if self.restShape is None: return None @@ -1411,7 +1321,6 @@ def buildInputVectors( The names of the shapes that are activated in each list : [[float, ...], ...] A list of activation values for the entire system - """ # InputVector comes from the c++ std::vector # Get all endpoint shapes from the progressions @@ -1556,7 +1465,6 @@ def buildSplitterList( A dict of {object: Falloff} saying what falloff should be used to split each objct : dict The memo to start the deepcopy with - """ # Add all items to the memo. memo = {} @@ -1667,7 +1575,6 @@ def split(self, pBar: Optional[QProgressDialog] = None) -> Simplex: ------- : Simplex : A newly split system - """ if np is None: raise RuntimeError("Numpy is not available, and splitting requires it") diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index fe1dc146..12210862 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -82,8 +82,9 @@ def __init__( self.minValue: float = mn self.maxValue: float = mx - self.group: Group = group - self.group.items.append(self) + with self.insertItemManager(group): + self.group: Group = group + self.group.items.append(self) self.simplex.sliders.append(self) @@ -140,7 +141,6 @@ def createSlider( ------- Slider The newly created Slider - """ if simplex.restShape is None: raise RuntimeError("Simplex system is missing rest shape") @@ -192,7 +192,6 @@ def createMultiSlider( ------- Slider The newly created Slider - """ if simplex.restShape is None: raise RuntimeError("Simplex system is missing rest shape") @@ -274,7 +273,6 @@ def buildSliderName(cls, pairs: list[tuple[str, float]]) -> str: ------- : str A newly created name - """ # In this case, pairs is *not* a list of ProgPairs # but a list of (name, value) tuples @@ -355,7 +353,6 @@ def loadV2( ------- : Slider The specified Slider - """ name = data["name"] prog = progs[data["prog"]] @@ -376,7 +373,6 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: ------- : int The build index - """ if self._buildIdx is None: self._buildIdx = len(simpDict["sliders"]) @@ -415,18 +411,19 @@ def setRange(self): def delete(self): """Delete a slider, any shapes it contains, and all downstream Combos and Traversals""" self.simplex.deleteDownstream(self) - g = self.group - g.items.remove(self) - self.group = None # type: ignore - self.simplex.sliders.remove(self) + with self.removeItemManager(self): + g = self.group + g.items.remove(self) + self.group = None # type: ignore + self.simplex.sliders.remove(self) - pairs = self.prog.pairs[:] # gotta make a copy - for pp in pairs: - if not pp.shape.isRest: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) + pairs = self.prog.pairs[:] # gotta make a copy + for pp in pairs: + if not pp.shape.isRest: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) - self.DCC.deleteSlider(self) + self.DCC.deleteSlider(self) @stackable def setInterpolation(self, interp: str): @@ -478,11 +475,11 @@ def createShape( ------- : ProgPair The newly created Shape in a ProgPair already added to the Progression - """ pp, idx = self.prog.newProgPair(shapeName, tVal) - pp.prog = self.prog - self.prog.pairs.insert(idx, pp) + with self.insertItemManager(self, row=idx): + pp.prog = self.prog + self.prog.pairs.insert(idx, pp) self.updateRange() return pp @@ -592,10 +589,11 @@ def setGroup(self, grp: Group): "All items in this group must be of type: {}".format(grp.groupType) ) - if self.group: - self.group.items.remove(self) - grp.items.append(self) - self.group = grp + with self.moveItemManager(self, grp): + if self.group: + self.group.items.remove(self) + grp.items.append(self) + self.group = grp def getInputVectors(self) -> list[list[float]]: """Get the ordered values for input to the solver that activate this Slider diff --git a/src/python/simplexui/items/stack.py b/src/python/simplexui/items/stack.py index 011ce83b..09be33cb 100644 --- a/src/python/simplexui/items/stack.py +++ b/src/python/simplexui/items/stack.py @@ -69,7 +69,6 @@ def getRevision(self, revision: int) -> Optional[Simplex]: : Simplex or None The stored Simplex system for the given revision or None if nothing found - """ # This method will ***ONLY*** be called by the undo callback # Seriously, don't call this yourself diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index db151b48..e6e69145 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -73,9 +73,10 @@ def buildDefinition( @stackable def remove(self): - if self.travPoint is not None: - self.travPoint.pairs.remove(self) - self.travPoint = None + with self.removeItemManager(self): + if self.travPoint is not None: + self.travPoint.pairs.remove(self) + self.travPoint = None @stackable def delete(self): @@ -139,8 +140,9 @@ def _wideCeiling(val: float, eps: float = 0.001): @stackable def addPair(self, pair: TravPair): - self.pairs.append(pair) - pair.travPoint = self + with self.insertItemManager(self): + self.pairs.append(pair) + pair.travPoint = self def removePair(self, pair: TravPair): pair.remove() @@ -252,10 +254,6 @@ class Traversal(SimplexAccessor, TreeItem): The Group to create this combo in color : QColor The color of this item in the UI - - Returns - ------- - """ classDepth: int = 2 @@ -282,12 +280,13 @@ def __init__( self._buildIdx: Optional[int] = None self._enabled: bool = True - self.group: Group = group - self.startPoint.traversal = self - self.endPoint.traversal = self - self.prog.controller = self - self.group.items.append(self) - self.simplex.traversals.append(self) + with self.insertItemManager(group): + self.group: Group = group + self.startPoint.traversal = self + self.endPoint.traversal = self + self.prog.controller = self + self.group.items.append(self) + self.simplex.traversals.append(self) @classmethod def createTraversal( @@ -315,10 +314,6 @@ def createTraversal( The Group to create this combo in (Default value = None) count : int The number of incrementals to create (including the 100%) (Default value = 4) - - Returns - ------- - """ if simplex.restShape is None: raise RuntimeError("Simplex system is missing rest shape") @@ -376,7 +371,6 @@ def allSliders(self) -> list[Slider]: ------- : [Slider, ...] The list of all Sliders that control this Traversal - """ startSliders = [p.slider for p in self.startPoint.pairs] endSliders = [ @@ -444,7 +438,6 @@ def buildTraversalName(ranges: dict[Slider, tuple[float, float]]) -> str: ------- : str The suggested Traversal name - """ static, dynamic = [], [] for sli, rng in ranges.items(): @@ -518,14 +511,11 @@ def createShape( tVal : float or None The progression value to set for the new Shape. If None, it gets a "smart" default value - - Returns - ------- - """ pp, idx = self.prog.newProgPair(shapeName, tVal) - pp.prog = self.prog - self.prog.pairs.insert(idx, pp) + with self.insertItemManager(self.prog, row=idx): + pp.prog = self.prog + self.prog.pairs.insert(idx, pp) return pp @classmethod @@ -547,7 +537,6 @@ def loadV2( ------- : Traversal The specified Traversal - """ name = data["name"] prog = progs[data["prog"]] @@ -632,10 +621,6 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: legacy : bool Whether to write out the legacy definition, or the newer one This is ignored for Traversals. There is no legacy definition - - Returns - ------- - """ if self._buildIdx is None: self._buildIdx = len(simpDict["traversals"]) @@ -655,13 +640,6 @@ def clearBuildIndex(self): The buildIndex is stored when building a definition dictionary that keeps track of its index for later referencing - - Parameters - ---------- - - Returns - ------- - """ self._buildIdx = None self.prog.clearBuildIndex() @@ -670,18 +648,19 @@ def clearBuildIndex(self): @stackable def delete(self): """Delete a traversal and any shapes it contains""" - g = self.group - if self not in g.items: - return # Can happen when deleting multiple groups - g.items.remove(self) - self.group = None # type: ignore - self.simplex.traversals.remove(self) - - pairs = self.prog.pairs[:] # gotta make a copy - for pp in pairs: - if not pp.shape.isRest: - self.simplex.shapes.remove(pp.shape) - self.DCC.deleteShape(pp.shape) + with self.removeItemManager(self): + g = self.group + if self not in g.items: + return # Can happen when deleting multiple groups + g.items.remove(self) + self.group = None # type: ignore + self.simplex.traversals.remove(self) + + pairs = self.prog.pairs[:] # gotta make a copy + for pp in pairs: + if not pp.shape.isRest: + self.simplex.shapes.remove(pp.shape) + self.DCC.deleteShape(pp.shape) def extractShape(self, shape, live=True, offset=10.0) -> DCCObject: """Extract a shape from a Traversal progression""" @@ -768,7 +747,6 @@ def getInputVector(self, value: float) -> list[float]: ------- : [float, ...] The ordered slider values - """ indexBySlider = {slider: idx for idx, slider in enumerate(self.simplex.sliders)} From 3864e42738b2ecbf9618282446accebf54bbb61a Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Mon, 17 Aug 2026 12:39:39 -0700 Subject: [PATCH 24/47] Update the class hierarchy to use less Qt by default. Start on how to hold expansions per-model --- src/python/simplexui/channelBox.py | 14 +- src/python/simplexui/comboCheckDialog.py | 4 +- src/python/simplexui/commands/mesh.py | 12 +- src/python/simplexui/dragFilter.py | 6 +- src/python/simplexui/falloffDialog.py | 8 +- .../simplexui/interface/dummyInterface.py | 12 +- .../simplexui/interface/mayaInterface.py | 4 +- src/python/simplexui/interfaceModel.py | 352 +--------- src/python/simplexui/interfaceModelTrees.py | 76 ++- src/python/simplexui/items/__init__.py | 2 + src/python/simplexui/items/accessor.py | 9 + src/python/simplexui/items/combo.py | 10 +- src/python/simplexui/items/dragItem.py | 24 + src/python/simplexui/items/falloff.py | 8 +- src/python/simplexui/items/group.py | 6 +- src/python/simplexui/items/progression.py | 11 +- src/python/simplexui/items/shape.py | 2 +- src/python/simplexui/items/simplex.py | 1 + src/python/simplexui/items/slider.py | 12 +- src/python/simplexui/items/stack.py | 1 + src/python/simplexui/items/traversal.py | 15 +- src/python/simplexui/items/treeItem.py | 98 ++- src/python/simplexui/simplexDialog.py | 2 +- src/python/simplexui/travCheckDialog.py | 6 +- src/python/simplexui/traversalDialog.py | 2 +- src/python/simplexui/utils.py | 609 +++++++++--------- 26 files changed, 571 insertions(+), 735 deletions(-) create mode 100644 src/python/simplexui/items/dragItem.py diff --git a/src/python/simplexui/channelBox.py b/src/python/simplexui/channelBox.py index 38c32790..95515261 100644 --- a/src/python/simplexui/channelBox.py +++ b/src/python/simplexui/channelBox.py @@ -49,7 +49,7 @@ class SlideFilter(QObject): slideReleased = Signal() def __init__(self, parent): - super(SlideFilter, self).__init__(parent) + super().__init__(parent) self.slideCursor = Qt.CursorShape.SizeHorCursor self.slideButton = Qt.MouseButton.LeftButton @@ -118,7 +118,7 @@ def eventFilter(self, obj, event): self.myendSlide(obj, event) return True - return super(SlideFilter, self).eventFilter(obj, event) + return super().eventFilter(obj, event) def startSlide(self, obj, event): """Start the slide operation @@ -192,7 +192,7 @@ class ChannelBoxDelegate(QStyledItemDelegate): """Delegate to draw the slider items""" def __init__(self, parent=None): - super(ChannelBoxDelegate, self).__init__(parent) + super().__init__(parent) self.store = {} def paint(self, painter, opt, index): @@ -201,7 +201,7 @@ def paint(self, painter, opt, index): if isinstance(item, Slider): self.paintSlider(self, item, painter, opt.rect, opt.palette) else: - super(ChannelBoxDelegate, self).paint(painter, opt, index) + super().paint(painter, opt, index) def roundedPath(self, width, height, left=True, right=True): """Get a path with rounded corners for drawing @@ -363,7 +363,7 @@ class ChannelListModel(QAbstractItemModel): """ def __init__(self, simplex, parent): - super(ChannelListModel, self).__init__(parent) + super().__init__(parent) self.simplex = simplex self.simplex.models.append(self) self.channels = [] @@ -450,7 +450,7 @@ class ChannelList(QListView): """A list to display the chosen channels""" def __init__(self, parent=None): - super(ChannelList, self).__init__(parent) + super().__init__(parent) self.slider = None self._nxt = 0.0 self.start = False @@ -568,7 +568,7 @@ class ChannelTree(QTreeView): """Display the channels in a Tree form""" def __init__(self, parent=None): - super(ChannelTree, self).__init__(parent) + super().__init__(parent) self.slider = None self._nxt = 0.0 self.start = False diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index 4ae1b250..b63e6db2 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -117,7 +117,7 @@ def buildPossibleCombos( class ComboCheckItem(QListWidgetItem): def __init__(self, pairs, combo, *args, **kwargs): - super(ComboCheckItem, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.pairs = pairs self.combo = combo @@ -173,7 +173,7 @@ class ComboCheckDialog(QDialog): def __init__(self, sliders, values=None, mode="create", parent=None): if parent is None: raise ValueError("Parent must not be None") - super(ComboCheckDialog, self).__init__(parent) + super().__init__(parent) uiPath = getUiFile(__file__) QtCompat.loadUi(uiPath, self) diff --git a/src/python/simplexui/commands/mesh.py b/src/python/simplexui/commands/mesh.py index b6a8e23a..22c3810d 100644 --- a/src/python/simplexui/commands/mesh.py +++ b/src/python/simplexui/commands/mesh.py @@ -782,7 +782,7 @@ class UV(MeshComponent): def __init__(self, mesh, name, index): self.name = name - super(UV, self).__init__(mesh, index) + super().__init__(mesh, index) def value(self): """Get the uv's position @@ -817,7 +817,7 @@ class UVFace(MeshComponent): def __init__(self, mesh, name, index): self.name = name - super(UVFace, self).__init__(mesh, index) + super().__init__(mesh, index) def __eq__(self, other): if isinstance(other, UVFace): @@ -905,7 +905,7 @@ def inner(self, *args): for attr in rnames: dct[attr] = wrap_closure(attr, True) - return super(MeshSetMeta, mcs).__new__(mcs, clsName, bases, dct) + return super().__new__(mcs, clsName, bases, dct) class MeshSet(set, metaclass=MeshSetMeta): @@ -913,7 +913,7 @@ class MeshSet(set, metaclass=MeshSetMeta): def __init__(self, mesh, indices=None): idxs = [] if indices is None else [int(i) for i in indices] - super(MeshSet, self).__init__(idxs) + super().__init__(idxs) self.mesh = mesh self.mesh.children.append(self) @@ -1032,7 +1032,7 @@ def partitionIslands(self): : [VertSet, ...] A list of interconnected object sets """ - return super(VertSet, self)._partitionIslands(self.mesh.adjacentVertsByFace) + return super()._partitionIslands(self.mesh.adjacentVertsByFace) class FaceSet(MeshSet): @@ -1078,4 +1078,4 @@ def partitionIslands(self): : [FaceSet, ...] A list of interconnected object sets """ - return super(FaceSet, self)._partitionIslands(self.mesh.adjacentFacesByVert) + return super()._partitionIslands(self.mesh.adjacentFacesByVert) diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index 40f49cd2..5e60df3a 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -301,6 +301,10 @@ def eventFilter(self, o: QObject, e: QEvent) -> bool: e : QEvent The QEvent of the mouse drag """ + + # Qt keeps references around and I don't know how to properly remove them + # so If I don't do this check for DRAG_ENABLED then + # Qt starts throwing errors when the UI gets deleted if hasattr(self, "DRAG_ENABLED"): if e.type() == QEvent.Type.MouseMove: assert isinstance(e, QMouseEvent) @@ -331,4 +335,4 @@ def eventFilter(self, o: QObject, e: QEvent) -> bool: self._isDragging = True return True - return super(DragFilter, self).eventFilter(o, e) + return super().eventFilter(o, e) diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index e33df074..11864b64 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -66,7 +66,7 @@ class CurveEditWidget(QWidget): tangentUpdated = Signal(float, float) def __init__(self, parent): - super(CurveEditWidget, self).__init__(parent) + super().__init__(parent) self.leftTan = None self.rightTan = None self._controlPoints = [ @@ -303,7 +303,7 @@ class FalloffDialog(QDialog): uiFalloffMinSPN: QDoubleSpinBox def __init__(self, parent): - super(FalloffDialog, self).__init__(parent) + super().__init__(parent) uiPath = getUiFile(__file__) QtCompat.loadUi(uiPath, self) self.parUI = parent @@ -482,9 +482,9 @@ def loadSettings(self): def hideEvent(self, event): """Override the hide event to store settings""" self.storeSettings() - super(FalloffDialog, self).hideEvent(event) + super().hideEvent(event) def showEvent(self, event): """Override the show event to restore settings""" - super(FalloffDialog, self).showEvent(event) + super().showEvent(event) self.loadSettings() diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index f467c157..142fe7d8 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -125,7 +125,7 @@ class DummySimplex(DummyNode): """A generic simplex node""" def __init__(self, name, parent, db=DB): - super(DummySimplex, self).__init__(name, db) + super().__init__(name, db) self.definition = "" parent.ops.append(self) @@ -134,7 +134,7 @@ class DummyFalloff(DummyNode): """A generic simplex node""" def __init__(self, name, parent, db=DB): - super(DummyFalloff, self).__init__(name, db) + super().__init__(name, db) self.weightmap = None @@ -154,7 +154,7 @@ class DummyBlendshape(DummyNode): def __init__(self, name, parent, db=DB): # TODO: Just reuse the DummyAttr instead of making an equivalent - super(DummyBlendshape, self).__init__(name, db) + super().__init__(name, db) self.shapes = {} parent.ops.append(self) @@ -163,7 +163,7 @@ class DummyMesh(DummyNode): """A generic mesh""" def __init__(self, name, db=DB): - super(DummyMesh, self).__init__(name, db) + super().__init__(name, db) self.importPath = "" self.faces = None self.counts = None @@ -1468,7 +1468,7 @@ class SliderDispatch(QtCore.QObject): valueChanged = Signal() def __init__(self, node, parent=None): - super(SliderDispatch, self).__init__(parent) + super().__init__(parent) def emitValueChanged(self, *args, **kwargs): """ @@ -1494,7 +1494,7 @@ class Dispatch(QtCore.QObject): redo = Signal() def __init__(self, parent=None): - super(Dispatch, self).__init__(parent) + super().__init__(parent) def connectCallbacks(self): """ """ diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 58849e46..97315130 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -3058,7 +3058,7 @@ class SliderDispatch(QtCore.QObject): valueChanged = Signal() def __init__(self, node, parent=None): - super(SliderDispatch, self).__init__(parent) + super().__init__(parent) mObject = getMObject(node) self.callbackID = om.MNodeMessage.addAttributeChangedCallback( mObject, self.emitValueChanged @@ -3084,7 +3084,7 @@ class Dispatch(QtCore.QObject): redo = Signal() def __init__(self, parent=None): - super(Dispatch, self).__init__(parent) + super().__init__(parent) self.callbackIDs = [] self.connectCallbacks() diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index f22b25fb..9c96d409 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -17,7 +17,6 @@ # pylint:disable=missing-docstring,unused-argument,no-self-use,too-many-return-statements import re -from contextlib import contextmanager from .items import ( Combo, @@ -27,10 +26,12 @@ Progression, Slider, Traversal, - TravPair, ) -from Qt import IsPyQt5, IsPySide2 -from Qt.QtCore import QAbstractItemModel, QModelIndex, QSortFilterProxyModel, Qt + +from .items.treeItem import AdapterModel + +from Qt.QtCore import QModelIndex, QSortFilterProxyModel, Qt +from typing import cast # Hierarchy Helpers @@ -183,153 +184,7 @@ def coerceIndexToRoots(indexes): # BASE MODEL -class ContextModel(QAbstractItemModel): - """A sub-class of QAbstractItemModel with built-in contextmanagers - that handle calling the begin/end signals for adding/removing/moving/resettting - """ - - @contextmanager - def insertItemManager(self, parent, row=-1): - """ContextManager for inserting items into the model - - Parameters - ---------- - parent : object - The item in the tree that will be the parent - row : int - The row to insert into. Pass -1 to append to the list (Default value = -1) - """ - parIdx = self.indexFromItem(parent) - if row == -1: - row = self.getItemAppendRow(parent) - self.beginInsertRows(parIdx, row, row) - try: - yield - finally: - self.endInsertRows() - - @contextmanager - def removeItemManager(self, item): - """ContextManager for removing items from the model - - Parameters - ---------- - item : object - The item to remove from the model - """ - idx = self.indexFromItem(item) - valid = idx.isValid() - if valid: - parIdx = idx.parent() - self.beginRemoveRows(parIdx, idx.row(), idx.row()) - try: - yield - finally: - if valid: - self.endRemoveRows() - - @contextmanager - def moveItemManager(self, item, destPar, destRow=-1): - """ContextManager for moving items within the model - - Parameters - ---------- - item : object - The item to move in the model - destPar : object - The object that will be the new parent - destRow : int - The row to move to. Pass -1 to move to the end (Default value = -1) - """ - itemIdx = self.indexFromItem(item) - destParIdx = self.indexFromItem(destPar) - handled = False - if itemIdx.isValid() and destParIdx.isValid(): - handled = True - srcParIdx = itemIdx.parent() - row = itemIdx.row() - if destRow == -1: - destRow = self.getItemAppendRow(destPar) - self.beginMoveRows(srcParIdx, row, row, destParIdx, destRow) - try: - yield - finally: - if handled: - self.endMoveRows() - - @contextmanager - def resetModelManager(self): - """ContextManager for resetting the entire model""" - self.beginResetModel() - try: - yield - finally: - self.endResetModel() - - def indexFromItem(self, item, column=0): - """Return the index for the given item - - Parameters - ---------- - item : object - The item to move in the model - column : int - The column to get the index for. Defaults to 0 - - Returns - ------- - QModelIndex - The index of the item - """ - row = self.getItemRow(item) - if row is None: - return QModelIndex() - return self.createIndex(row, column, item) - - def itemFromIndex(self, index): - """Return the item for the given index - - Parameters - ---------- - index : QModelIndex - The index to get the item of - - Returns - ------- - object - The item in the tree - """ - return index.internalPointer() - - def itemDataChanged(self, item): - """Emit the itemDataChanged signal. - - This must be done through this interface because, unfortunately, I can't quite figure out how - to make the empty `roles` list pass properly for Qt5. So I have to change behavior based - on the Qt backend - - Parameters - ---------- - item : object - The object whose data has changed - """ - idx = self.indexFromItem(item) - self.emitDataChanged(idx) - - def _emitDataChangedQt5(self, index): - if index.isValid(): - self.dataChanged.emit(index, index, []) - - def _emitDataChangedQt4(self, index): - if index.isValid(): - self.dataChanged.emit(index, index) - - emitDataChanged = ( - _emitDataChangedQt5 if IsPySide2 or IsPyQt5 else _emitDataChangedQt4 - ) - - -class SimplexModel(ContextModel): +class SimplexModel(AdapterModel): """The base model for all interaction with a simplex system. All ui interactions with a simplex system must go through this model Any special requirements, or reorganizations of the trees will only @@ -347,155 +202,9 @@ class SimplexModel(ContextModel): """ - def __init__(self, simplex, parent): - super(SimplexModel, self).__init__(parent) - self.simplex = simplex - self.simplex.models.append(self) - - def index(self, row, column, parIndex): - par = parIndex.internalPointer() - child = self.getChildItem(par, row) - if child is None: - return QModelIndex() - return self.createIndex(row, column, child) - - def parent(self, index): - if not index.isValid(): - return QModelIndex() - item = index.internalPointer() - if item is None: - return QModelIndex() - par = self.getParentItem(item) - if par is None: - return QModelIndex() - row = self.getItemRow(par) - if row is None: - return QModelIndex() - return self.createIndex(row, 0, par) - - def rowCount(self, parIndex): - parent = parIndex.internalPointer() - ret = self.getItemRowCount(parent) - return ret - - def columnCount(self, parIndex): - return 3 - - def data(self, index, role): - if not index.isValid(): - return None - item = index.internalPointer() - return self.getItemData(item, index.column(), role) - - def flags(self, index): - if not index.isValid(): - return Qt.ItemFlag.ItemIsEnabled - if index.column() == 0: - item = index.internalPointer() - if isinstance(item, (Slider, Combo, Traversal)): - return ( - Qt.ItemFlag.ItemIsEnabled - | Qt.ItemFlag.ItemIsSelectable - | Qt.ItemFlag.ItemIsEditable - | Qt.ItemFlag.ItemIsUserCheckable - ) - # TODO: make the SHAPES object under a combo or traversal not-editable - return ( - Qt.ItemFlag.ItemIsEnabled - | Qt.ItemFlag.ItemIsSelectable - | Qt.ItemFlag.ItemIsEditable - ) - - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): - if not index.isValid(): - return False - if role == Qt.ItemDataRole.CheckStateRole: - item = index.internalPointer() - if index.column() == 0: - if isinstance(item, (Slider, Combo, Traversal)): - item.enabled = value == Qt.CheckState.Checked - return True - elif role == Qt.ItemDataRole.EditRole: - item = index.internalPointer() - if index.column() == 0: - if isinstance(item, (Group, Slider, Combo, Traversal, ProgPair)): - item.name = value - return True - - elif index.column() == 1: - if isinstance(item, Slider): - item.value = value - elif isinstance(item, ComboPair): - item.value = value - elif isinstance(item, TravPair): - item.value = value - - elif index.column() == 2: - if isinstance(item, ProgPair): - item.value = value - return False - - def headerData(self, section, orientation, role): - if orientation == Qt.Orientation.Horizontal: - if role == Qt.ItemDataRole.DisplayRole: - sects = ("Items", "Slide", "Value") - return sects[section] - return None - - # Methods for dealing with items only - # These will be used to build the indexes - # and will be public for utility needs - def getChildItem(self, parent, row): - if parent is None: - if row == 0: - return self.simplex - else: - return None - return parent.treeChild(row) - - def getItemRow(self, item): - if item is None: - return None - return item.treeRow() - - def getParentItem(self, item): - if item is None: - return None - return item.treeParent() - - def getItemRowCount(self, item): - # Null parent means 1 row that is the simplex object - if item is None: - ret = 1 - else: - ret = item.treeChildCount() - return ret - - def getItemData(self, item, column, role): - if item is None: - return None - - if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): - return item.treeData(column) - - elif role == Qt.ItemDataRole.CheckStateRole: - chk = None - if column == 0: - chk = item.treeChecked() - if chk is not None: - chk = Qt.CheckState.Checked if chk else Qt.CheckState.Unchecked - return chk - elif role == Qt.ItemDataRole.DecorationRole: - if column == 0: - return item.icon() - return None - - def getItemAppendRow(self, item): - if isinstance(item, Combo): - # insert before the special "SHAPES" item - # getItemRowCount returns len(item.pairs) + 1 - return len(item.pairs) - return self.getItemRowCount(item) + @property + def simplex(self): + return self._rootItem # VIEW MODELS @@ -506,9 +215,12 @@ class BaseProxyModel(QSortFilterProxyModel): """ def __init__(self, model, parent=None): - super(BaseProxyModel, self).__init__(parent) + super().__init__(parent) self.setSourceModel(model) + def sourceModel(self) -> SimplexModel: + return cast(SimplexModel, super().sourceModel()) + def indexFromItem(self, item, column=0): sourceModel = self.sourceModel() sourceIndex = sourceModel.indexFromItem(item, column) @@ -523,13 +235,13 @@ def invalidate(self): source = self.sourceModel() if isinstance(source, QSortFilterProxyModel): source.invalidate() - super(BaseProxyModel, self).invalidate() + super().invalidate() def invalidateFilter(self): source = self.sourceModel() if isinstance(source, QSortFilterProxyModel): source.invalidateFilter() - super(BaseProxyModel, self).invalidateFilter() + super().invalidateFilter() def filterAcceptsRow(self, sourceRow, sourceParent) -> bool: return True @@ -543,7 +255,7 @@ def filterAcceptsRow(self, sourceRow, sourceParent): if isinstance(item, Group): if item.groupType is not Slider: return False - return super(SliderModel, self).filterAcceptsRow(sourceRow, sourceParent) + return super().filterAcceptsRow(sourceRow, sourceParent) class ComboModel(BaseProxyModel): @@ -554,7 +266,7 @@ def filterAcceptsRow(self, sourceRow, sourceParent): if isinstance(item, Group): if item.groupType is not Combo: return False - return super(ComboModel, self).filterAcceptsRow(sourceRow, sourceParent) + return super().filterAcceptsRow(sourceRow, sourceParent) class TraversalModel(BaseProxyModel): @@ -565,7 +277,7 @@ def filterAcceptsRow(self, sourceRow, sourceParent): if isinstance(item, Group): if item.groupType is not Traversal: return False - return super(TraversalModel, self).filterAcceptsRow(sourceRow, sourceParent) + return super().filterAcceptsRow(sourceRow, sourceParent) # FILTER MODELS @@ -575,7 +287,7 @@ class SimplexFilterModel(BaseProxyModel): """ def __init__(self, model, parent=None): - super(SimplexFilterModel, self).__init__(model, parent) + super().__init__(model, parent) self.setSourceModel(model) self._filterString = [] self._filterReg = [] @@ -608,7 +320,7 @@ def filterAcceptsRow(self, sourceRow, sourceParent): if not self.checkChildren(sourceItem): return False - return super(SimplexFilterModel, self).filterAcceptsRow(sourceRow, sourceParent) + return super().filterAcceptsRow(sourceRow, sourceParent) def matchFilterString(self, itemString): if not self._filterString: @@ -641,7 +353,7 @@ class SliderFilterModel(SimplexFilterModel): """Hide single shapes under a slider""" def __init__(self, model, parent=None): - super(SliderFilterModel, self).__init__(model, parent) + super().__init__(model, parent) self.requires = [] self.filterRequiresAny = False self.filterRequiresAll = False @@ -672,14 +384,14 @@ def filterAcceptsRow(self, sourceRow, sourceParent): if not all(data in s for s in sliGroups): return False - return super(SliderFilterModel, self).filterAcceptsRow(sourceRow, sourceParent) + return super().filterAcceptsRow(sourceRow, sourceParent) class ComboFilterModel(SimplexFilterModel): """Filter by slider when Show Dependent Combos is checked""" def __init__(self, model, parent=None): - super(ComboFilterModel, self).__init__(model, parent) + super().__init__(model, parent) self.requires = [] self.filterRequiresAll = False self.filterRequiresAny = False @@ -722,14 +434,14 @@ def filterAcceptsRow(self, sourceRow, sourceParent): if not all(r in self.requires for r in sliders): return False - return super(ComboFilterModel, self).filterAcceptsRow(sourceRow, sourceParent) + return super().filterAcceptsRow(sourceRow, sourceParent) class TraversalFilterModel(SimplexFilterModel): """Hide single shapes under a slider""" def __init__(self, model, parent=None): - super(TraversalFilterModel, self).__init__(model, parent) + super().__init__(model, parent) self.doFilter = True def filterAcceptsRow(self, sourceRow, sourceParent): @@ -744,17 +456,17 @@ def filterAcceptsRow(self, sourceRow, sourceParent): elif data.shape.isRest: return False - return super(TraversalFilterModel, self).filterAcceptsRow( + return super().filterAcceptsRow( sourceRow, sourceParent ) # SETTINGS MODELS -class SliderGroupModel(ContextModel): +class SliderGroupModel(AdapterModel): """A model for displaying Group objects that contain Sliders""" def __init__(self, simplex, parent): - super(SliderGroupModel, self).__init__(parent) + super().__init__(simplex, parent) self.simplex = simplex self.simplex.models.append(self) @@ -802,11 +514,11 @@ def itemFromIndex(self, index): return index.internalPointer() -class FalloffModel(ContextModel): +class FalloffModel(AdapterModel): """A model for displaying Falloff objects connected to Sliders""" def __init__(self, simplex, parent): - super(FalloffModel, self).__init__(parent) + super().__init__(simplex, parent) self.simplex = simplex if self.simplex is not None: self.simplex.falloffModels.append(self) @@ -934,11 +646,11 @@ def itemFromIndex(self, index): return index.internalPointer() -class FalloffDataModel(ContextModel): +class FalloffDataModel(AdapterModel): """A model for displaying the data of Falloff objects""" def __init__(self, simplex, parent): - super(FalloffDataModel, self).__init__(parent) + super().__init__(simplex, parent) self.simplex = simplex if self.simplex is not None: self.simplex.falloffModels.append(self) diff --git a/src/python/simplexui/interfaceModelTrees.py b/src/python/simplexui/interfaceModelTrees.py index 8e602386..1558ce8d 100644 --- a/src/python/simplexui/interfaceModelTrees.py +++ b/src/python/simplexui/interfaceModelTrees.py @@ -17,22 +17,37 @@ from .dragFilter import DragFilter from .items import Group -from Qt.QtCore import QItemSelection, QItemSelectionModel, QModelIndex, QRegExp, Qt -from Qt.QtGui import QRegExpValidator +from .items.treeItem import AdapterModel, CustomRoles +from .items.dragItem import Draggable +from Qt.QtCore import ( + QItemSelection, + QItemSelectionModel, + Qt, +) +from Qt.QtGui import QValidator from Qt.QtWidgets import QApplication, QLineEdit, QMenu, QStyledItemDelegate, QTreeView +from typing import cast + + +class NameValidator(QValidator): + def __init__(self, parent=None): + super().__init__(parent) + + def validate(self, input_str: str, pos: int) -> tuple[QValidator.State, str, int]: + if not input_str: + return QValidator.State.Intermediate, input_str, pos + if input_str.isidentifier(): + return QValidator.State.Acceptable, input_str, pos + return QValidator.State.Invalid, input_str, pos class SimplexNameDelegate(QStyledItemDelegate): """An QStyledItemDelegate subclass that implements a Regex validator""" - def __init__(self, parent=None): - super(SimplexNameDelegate, self).__init__(parent) - self._rx = QRegExp(r"[A-Za-z][A-Za-z0-9_]*") - def createEditor(self, parent, option, index): editor = QLineEdit(parent) - rxv = QRegExpValidator(self._rx, editor) - editor.setValidator(rxv) + validator = NameValidator(editor) + editor.setValidator(validator) return editor @@ -40,10 +55,11 @@ class SimplexTree(QTreeView): """Abstract base tree displaying Simplex objects""" def __init__(self, parent): - super(SimplexTree, self).__init__(parent) + super().__init__(parent) self.expandModifier = Qt.KeyboardModifier.ControlModifier self.depthModifier = Qt.KeyboardModifier.ShiftModifier + self._expansions: set[str] = set() self._menu = None self._plugins = [] @@ -63,6 +79,12 @@ def __init__(self, parent): self.setColumnWidth(1, 50) self.setColumnWidth(2, 20) + def setModel(self, model: AdapterModel): + super().setModel(model) + + def model(self) -> AdapterModel: + return cast(AdapterModel, super().model()) + def setPlugins(self, plugins): """Set the right-click menu plugins for the tree @@ -186,7 +208,6 @@ def toggleTree(self, index, expand): model = self.model() mods = QApplication.keyboardModifiers() thing = model.itemFromIndex(index) - thing.expanded[id(self)] = expand if mods & (self.expandModifier | self.depthModifier): queue = [index] @@ -195,7 +216,6 @@ def toggleTree(self, index, expand): while queue: idx = queue.pop() thing = model.itemFromIndex(idx) - thing.expanded[id(self)] = expand self.setExpanded(idx, expand) if mods & self.depthModifier: if isinstance(thing, Group): @@ -230,11 +250,8 @@ def expandToIndex(self, index): index : QModelIndex The index to expand to """ - model = self.model() while index and index.isValid(): self.setExpanded(index, True) - thing = model.itemFromIndex(index) - thing.expanded[id(self)] = True index = index.parent() self.resizeColumns() @@ -264,30 +281,21 @@ def scrollToIndex(self, index): def storeExpansion(self): """Store the expansion state of the tree for the undo stack""" + expanded_uids = set() model = self.model() - queue = [model.index(0, 0, QModelIndex())] - while queue: - index = queue.pop() - item = model.itemFromIndex(index) - item.expanded[id(self)] = self.isExpanded(index) - for row in range(model.rowCount(index)): - queue.append(model.index(row, 0, index)) + for index in model.iterindices(pred=lambda x: self.isExpanded(x)): + uid = model.data(index, CustomRoles.UID_ROLE) + if uid: + expanded_uids.add(uid) + self._expansions = expanded_uids def setItemExpansion(self): """Load the stored expansions onto the tree""" model = self.model() - queue = [model.index(0, 0, QModelIndex())] - self.blockSignals(True) - try: - while queue: - index = queue.pop() - item = model.itemFromIndex(index) - exp = item.expanded.get(id(self), False) - self.setExpanded(index, exp) - for row in range(model.rowCount(index)): - queue.append(model.index(row, 0, index)) - finally: - self.blockSignals(False) + for index in model.iterindices( + pred=lambda x: model.data(x, CustomRoles.UID_ROLE) in self._expansions + ): + self.setExpanded(index, True) def dragTick(self, ticks, mul): """Deal with the ticks coming from the drag handler @@ -304,7 +312,7 @@ def dragTick(self, ticks, mul): return items = self.getSelectedItems() for item in items: - if hasattr(item, "valueTick"): + if isinstance(item, Draggable): item.valueTick(ticks, mul) self.viewport().update() diff --git a/src/python/simplexui/items/__init__.py b/src/python/simplexui/items/__init__.py index f55de987..293c6f53 100644 --- a/src/python/simplexui/items/__init__.py +++ b/src/python/simplexui/items/__init__.py @@ -29,6 +29,8 @@ "Combo", "ComboPair", "Falloff", + "MapFalloff", + "PlanarFalloff" "Group", "ProgPair", "Progression", diff --git a/src/python/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py index 3988d04a..93ddc4ce 100644 --- a/src/python/simplexui/items/accessor.py +++ b/src/python/simplexui/items/accessor.py @@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Optional +from .treeItem import TreeItem if TYPE_CHECKING: from .simplex import Simplex @@ -137,3 +138,11 @@ def buildLinkedRename( # And now handle the type-specific stuff return self._buildLinkedRename(newName, maxDepth, currentLinks) + + +class SimplexTreeAccessor(SimplexAccessor, TreeItem): + def __init__(self, simplex: Simplex): + # Explicitly + SimplexAccessor.__init__(self, simplex) + TreeItem.__init__(self, simplex) + diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index 3a193090..add0649d 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -18,9 +18,11 @@ # pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations -from .accessor import SimplexAccessor +from .accessor import SimplexTreeAccessor from .stack import stackable from .treeItem import TreeItem +from .dragItem import Draggable + from typing import TYPE_CHECKING, Optional, Any @@ -33,7 +35,7 @@ # Abstract Items -class ComboPair(SimplexAccessor, TreeItem): +class ComboPair(SimplexTreeAccessor, Draggable): """A Slider/Value pair for use in Combos""" classDepth: int = 6 @@ -66,7 +68,7 @@ def treeRow(self) -> int: return 0 return self.combo.pairs.index(self) - def treeParent(self) -> TreeItem: + def treeParent(self) -> Optional[TreeItem]: return self.combo def treeData(self, column: int) -> Optional[Any]: @@ -77,7 +79,7 @@ def treeData(self, column: int) -> Optional[Any]: return None -class Combo(SimplexAccessor, TreeItem): +class Combo(SimplexTreeAccessor): """A group of Slider/Value pairs that control a Progression through some solver Combos allow for fixit shapes to be created for any number of user inputs. diff --git a/src/python/simplexui/items/dragItem.py b/src/python/simplexui/items/dragItem.py new file mode 100644 index 00000000..64de8be9 --- /dev/null +++ b/src/python/simplexui/items/dragItem.py @@ -0,0 +1,24 @@ +class Draggable: + """A mixin to make an item middle-click draggable""" + + # This is in a separate file from the DragFilter so that no Qt imports + # are required for the core Simplex items + dragStep: float = 0.05 + maxValue: float = 1.0 + minValue: float = 0.0 + + def valueTick(self, ticks: int, mul: float): + """Change the value of the current object by some number of ticks + with some given multiplier. This is the interface for the MMB drag + + Parameters + ---------- + ticks : int + The number of dragStep ticks to apply + mul : float + An overall multiplier + """ + val = self.value + (self.dragStep * ticks * mul) + val = 0.0 if abs(val) < 1e-5 else val + val = max(min(val, self.maxValue), self.minValue) + self.value = val diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index 268e6dfd..1e4ccf94 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -394,6 +394,8 @@ def applyFalloff(self, shape: Shape, sIdx: int): class PlanarFalloff(Falloff): + splitType: str = "planar" + def __init__( self, name: str, @@ -677,7 +679,7 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: else: x = { "name": self.name, - "type": self.splitType, + "type": "planar", "axis": self.axis, "maxVal": self.maxVal, "maxHandle": self.maxHandle, @@ -689,6 +691,8 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: class MapFalloff(Falloff): + splitType: str = "map" + def __init__(self, name: str, simplex: Simplex, axis: str, mapName: str): super().__init__(name, simplex, axis) with self.stack.store(self): @@ -790,7 +794,7 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: else: x = { "name": self.name, - "type": self.splitType, + "type": "map", "axis": self.axis, "mapName": self.mapName, } diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index 7afbdd38..cd82229b 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -17,7 +17,7 @@ # pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations -from .accessor import SimplexAccessor +from .accessor import SimplexTreeAccessor from .stack import stackable from .treeItem import TreeItem @@ -33,7 +33,7 @@ GroupMember = Union[Combo, Slider, Traversal] -class Group(SimplexAccessor, TreeItem): +class Group(SimplexTreeAccessor): """Groups organize Simplex items Groups have no purpose in the solver. They don't do anything other than organize @@ -59,7 +59,7 @@ def __init__( simplex: Simplex, groupType: GroupType, ): - super(Group, self).__init__(simplex) + super().__init__(simplex) from .combo import Combo from .slider import Slider from .traversal import Traversal diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index c4c7cd6d..7d9b34e4 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -19,9 +19,10 @@ # pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations from ..utils import getNextName -from .accessor import SimplexAccessor +from .accessor import SimplexTreeAccessor from .stack import stackable from .treeItem import TreeItem +from .dragItem import Draggable from typing import Optional, Union, Any, TYPE_CHECKING @@ -35,11 +36,11 @@ from .traversal import Traversal -class ProgPair(SimplexAccessor, TreeItem): +class ProgPair(SimplexTreeAccessor, Draggable): classDepth: int = 9 def __init__(self, simplex: Simplex, shape: Shape, value: float): - super(ProgPair, self).__init__(simplex) + super().__init__(simplex) self.shape: Shape = shape self._value: float = value self.prog: Optional[Progression] = None @@ -122,7 +123,7 @@ def treeData(self, column) -> Optional[Any]: return None -class Progression(SimplexAccessor, TreeItem): +class Progression(SimplexTreeAccessor): """A set of shapes to interpolate between A Progression is a collection of shape/value pairs, and an interpolation type. @@ -172,7 +173,7 @@ def __init__( interp: str = "spline", falloffs: Optional[list[Falloff]] = None, ): - super(Progression, self).__init__(simplex) + super().__init__(simplex) if self.simplex.restShape is None: raise RuntimeError("Simplex is not initialized with a rest shape") diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index 8ff56e77..46628b2e 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -57,7 +57,7 @@ class Shape(SimplexAccessor): classDepth: int = 10 def __init__(self, name: str, simplex: Simplex, create: bool = True): - super(Shape, self).__init__(simplex) + super().__init__(simplex) with self.stack.store(self): self._thing: Optional[DCCObject] = None self._verts: Optional[npt.NDArray] = None diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 2415af93..77d580f3 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -90,6 +90,7 @@ def __init__(self, name: str = "", forceDummy=False, sliderMul=1.0): ------- """ super().__init__() + self.sliderMul: float = 1.0 self._name: str = name # The name of the system self.sliders: list[Slider] = [] # List of contained sliders self.combos: list[Combo] = [] # List of contained combos diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index 12210862..6b775788 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -21,12 +21,13 @@ from ..interface import undoContext from ..utils import caseSplit, getNextName, makeUnique, singleShot -from .accessor import SimplexAccessor +from .accessor import SimplexTreeAccessor from .group import Group from .progression import ProgPair, Progression from .shape import Shape from .stack import stackable from .treeItem import TreeItem +from .dragItem import Draggable from typing import Optional, Any, TYPE_CHECKING @@ -34,7 +35,7 @@ from .simplex import DCCObject, Simplex -class Slider(SimplexAccessor, TreeItem): +class Slider(SimplexTreeAccessor, Draggable): """A user-input to the simplex system that directly controls a Progression Parameters @@ -66,7 +67,7 @@ def __init__( if group.groupType is not type(self): raise ValueError("Cannot add this slider to a combo group") - super(Slider, self).__init__(simplex) + super().__init__(simplex) with self.stack.store(self): self._name: str = name self._thing: Optional[DCCObject] = None @@ -317,10 +318,11 @@ def value(self) -> float: def value(self, val: float): """Set the current value for this Slider""" self._value = val - self._setAllSliders([self]) + # singleShot consolidates all + self._setAllSliders(self) # type: ignore @singleShot() - def _setAllSliders(self, sliders: list[Slider]): + def _setAllSliders(self, *sliders: Slider): with undoContext(self.DCC): for slider in sliders: self.DCC.setSliderWeight(slider, slider.value) diff --git a/src/python/simplexui/items/stack.py b/src/python/simplexui/items/stack.py index 09be33cb..f7706e53 100644 --- a/src/python/simplexui/items/stack.py +++ b/src/python/simplexui/items/stack.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . # pylint:disable=missing-docstring,unused-argument,no-self-use +from __future__ import annotations import copy from collections import OrderedDict from contextlib import contextmanager diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index e6e69145..8032a733 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -17,13 +17,14 @@ # pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations -from .accessor import SimplexAccessor +from .accessor import SimplexTreeAccessor from .combo import Combo from .group import Group from .progression import Progression from .slider import Slider from .stack import stackable from .treeItem import TreeItem +from .dragItem import Draggable from enum import Enum @@ -39,12 +40,12 @@ class TravSide(Enum): End = "END" -class TravPair(SimplexAccessor, TreeItem): +class TravPair(SimplexTreeAccessor, Draggable): classDepth: int = 4 def __init__(self, slider: Slider, value: float): simplex = slider.simplex - super(TravPair, self).__init__(simplex) + super().__init__(simplex) self.slider: Slider = slider self._value: float = float(value) self.minValue: float = -1.0 @@ -112,14 +113,14 @@ def treeData(self, column: int) -> Optional[Any]: return None -class TravPoint(SimplexAccessor, TreeItem): +class TravPoint(SimplexTreeAccessor): classDepth: int = 3 def __init__(self, pairs: list[TravPair], side: TravSide): if not pairs: raise ValueError("Pairs must be provided for a TravPoint") simplex = pairs[0].slider.simplex - super(TravPoint, self).__init__(simplex) + super().__init__(simplex) self.pairs = pairs for pair in pairs: @@ -211,7 +212,7 @@ def treeChildCount(self) -> int: return len(self.pairs) -class Traversal(SimplexAccessor, TreeItem): +class Traversal(SimplexTreeAccessor): """Traversals control a Progression based on any 2 points in the Solver space. Traversals only make sense with intermediate shapes in the progression of the sliders @@ -267,7 +268,7 @@ def __init__( prog: Progression, group: Group, ): - super(Traversal, self).__init__(simplex) + super().__init__(simplex) with self.stack.store(self): if group.groupType is not type(self): raise ValueError( diff --git a/src/python/simplexui/items/treeItem.py b/src/python/simplexui/items/treeItem.py index 5cb2eca8..f583d4ba 100644 --- a/src/python/simplexui/items/treeItem.py +++ b/src/python/simplexui/items/treeItem.py @@ -49,10 +49,17 @@ """ from __future__ import annotations -from typing import Optional, Any, Generator +from typing import Optional, Any, Generator, Callable, Iterator, Literal, overload from Qt.QtGui import QIcon from Qt.QtCore import QAbstractItemModel, QModelIndex, Qt, QObject +from Qt.QtWidgets import QTreeView from contextlib import contextmanager, ExitStack +import uuid +import enum + + +class CustomRoles(enum.IntEnum): + UID_ROLE = Qt.ItemDataRole.UserRole + 1 SimpleGenerator = Generator[None, None, None] @@ -121,6 +128,7 @@ def resetManager(self) -> SimpleGenerator: class TreeItem: def __init__(self, root: TreeRootItem): self.root: TreeRootItem = root + self.uid: str = uuid.uuid4().hex # Unique identifier for tree expansion @property def observers(self) -> list[ObserverServer]: @@ -321,6 +329,41 @@ def columnCount(self, parIndex: QModelIndex) -> int: return 1 return self._rootItem.columnCount() + @overload + def getItemData(self, item: None, column: int, role: int) -> None: ... + + @overload + def getItemData( + self, + item: TreeItem, + column: int, + role: Literal[Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole], + ) -> Optional[str]: ... + + @overload + def getItemData( + self, + item: TreeItem, + column: int, + role: Literal[Qt.ItemDataRole.CheckStateRole], + ) -> Optional[Qt.CheckState]: ... + + @overload + def getItemData( + self, + item: TreeItem, + column: int, + role: Literal[Qt.ItemDataRole.DecorationRole], + ) -> Optional[QIcon]: ... + + @overload + def getItemData( + self, + item: TreeItem, + column: int, + role: Literal[CustomRoles.UID_ROLE], + ) -> Optional[str]: ... + def getItemData( self, item: Optional[TreeItem], column: int, role: int ) -> Optional[Any]: @@ -340,4 +383,57 @@ def getItemData( elif role == Qt.ItemDataRole.DecorationRole: if column == 0: return item.icon() + elif role == CustomRoles.UID_ROLE: + return item.uid return None + + def iterindices( + self, pred: Optional[Callable[[QModelIndex], bool]] = None + ) -> Iterator[QModelIndex]: + """Iterate all indices of this model breadth-first""" + queue = [QModelIndex()] + while queue: + index = queue.pop() + if pred is None or pred(index): + yield index + for row in range(self.rowCount(index)): + queue.append(self.index(row, 0, index)) + + def iteritems( + self, pred: Optional[Callable[[TreeItem], bool]] = None + ) -> Iterator[Optional[TreeItem]]: + """Iterate all items of this model breadth-first""" + # The only way I could make the typechecker happy was by + # having 2 separate loops over self.iterindices + if pred is None: + for index in self.iterindices(pred=pred): + yield self.itemFromIndex(index) + return + + def itempred(x): + item = self.itemFromIndex(x) + return False if item is None else pred(item) + + for index in self.iterindices(pred=itempred): + yield self.itemFromIndex(index) + + +def save_view_expansion_state(tree_view: QTreeView) -> set[str]: + expanded_uids = set() + model = tree_view.model() + assert isinstance(model, AdapterModel) + + for index in model.iterindices(pred=lambda x: tree_view.isExpanded(x)): + uid = model.data(index, CustomRoles.UID_ROLE) + if uid: + expanded_uids.add(uid) + return expanded_uids + + +def restore_view_expansion_state(tree_view: QTreeView, expanded_uids: set[str]): + model = tree_view.model() + assert isinstance(model, AdapterModel) + for index in model.iterindices( + pred=lambda x: model.data(x, CustomRoles.UID_ROLE) in expanded_uids + ): + tree_view.setExpanded(index, True) diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 47c72341..11c337a6 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -184,7 +184,7 @@ class SimplexDialog(Window): openedDialogs = [] def __init__(self, parent=None, dispatch=None): - super(SimplexDialog, self).__init__(parent) + super().__init__(parent) uiPath = getUiFile(__file__) QtCompat.loadUi(uiPath, self) diff --git a/src/python/simplexui/travCheckDialog.py b/src/python/simplexui/travCheckDialog.py index 832883ba..d2c5c2e8 100644 --- a/src/python/simplexui/travCheckDialog.py +++ b/src/python/simplexui/travCheckDialog.py @@ -129,7 +129,7 @@ def buildPossibleTraversals( class TravCheckItem(QTreeWidgetItem): def __init__(self, pairs, trav, *args, **kwargs): - super(TravCheckItem, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.pairs = pairs self.trav = trav @@ -209,7 +209,7 @@ def __init__( if parent is None or grandparent is None: raise ValueError("The parent and grandparent must be provided") - super(TraversalCheckDialog, self).__init__(parent) + super().__init__(parent) uiPath = getUiFile(__file__) QtCompat.loadUi(uiPath, self) @@ -322,7 +322,7 @@ def closeEvent(self, event): self.gparUI.uiSliderTREE.selectionModel().selectionChanged.disconnect( self.populateWithCheck ) - super(TraversalCheckDialog, self).closeEvent(event) + super().closeEvent(event) def populateWithUpdate(self): """Populate the list from the main dialog selection""" diff --git a/src/python/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py index a190491f..ded8ec22 100644 --- a/src/python/simplexui/traversalDialog.py +++ b/src/python/simplexui/traversalDialog.py @@ -86,7 +86,7 @@ class TraversalDialog(QDialog): uiSplitShapePSDACT: QAction def __init__(self, parent): - super(TraversalDialog, self).__init__(parent) + super().__init__(parent) uiPath = getUiFile(__file__) QtCompat.loadUi(uiPath, self) diff --git a/src/python/simplexui/utils.py b/src/python/simplexui/utils.py index 8cae2ab8..20e8b322 100755 --- a/src/python/simplexui/utils.py +++ b/src/python/simplexui/utils.py @@ -1,320 +1,289 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -"""Utility functions.""" - -import os -import re -import sys - -from contextlib import contextmanager, ExitStack - -from Qt.QtCore import QObject, QTimer, QSettings -from Qt.QtGui import QIcon - -AT_BLUR = os.environ.get("SIMPLEX_AT_BLUR") == "true" - - -def toPyObject(thing): - """Because we could still be in the sip api 1.0 - I have to check and convert all Qt returns to python objects - - Parameters - ---------- - thing : object - The object, possibly Qt type - - Returns - ------- - object - The python object - - """ - try: - return thing.toPyObject() - except Exception: - return thing - - -def getUiFile(fileVar, subFolder="ui", uiName=None): - """Get the path to the .ui file - - Parameters - ---------- - fileVar : str - The __file__ variable passed from the invocation - subFolder : str - The folder to look in for the ui files. Defaults to 'ui' - uiName : str or None - The name of the .ui file. Defaults to the basename of - fileVar with .ui instead of .py - - Returns - ------- - str - The path to the .ui file - - """ - uiFolder, filename = os.path.split(fileVar) - if uiName is None: - uiName = os.path.splitext(filename)[0] - if not subFolder: - raise ValueError("A subfolder must be provided") - uiFile = os.path.join(uiFolder, subFolder, uiName + ".ui") - return uiFile - - -def getNextName(name, currentNames): - """Get the next available number-incremented name - - Parameters - ---------- - name : str - The name I want to check - currentNames : list - The names that currently exist - - Returns - ------- - str - The next available number-incremented name - - """ - i = 0 - s = set(currentNames) - while True: - if not i: - nn = name - else: - nn = name + str(i) - if nn not in s: - return nn - i += 1 - return name - - -def clearPathSymbols(paths, keepers=None): - """Removes path symbols from the environment. - - This means I can unload my tools from the current process and re-import them - rather than dealing with the always finicky reload() - - We use directory paths rather than module names because it gives us more control - over what is unloaded - - Parameters - ---------- - paths : list - List of directory paths that will have their modules removed - keepers : list or None - List of module names that will not be removed (Default value = None) - """ - keepers = keepers or [] - paths = [os.path.normcase(os.path.normpath(p)) for p in paths] - - for key, value in sys.modules.items(): - protected = False - - # Used by multiprocessing library, don't remove this. - if key == "__parents_main__": - protected = True - - # Protect submodules of protected packages - if key in keepers: - protected = True - - ckey = key - while not protected and "." in ckey: - ckey = ckey.rsplit(".", 1)[0] - if ckey in keepers: - protected = True - - if protected: - continue - - try: - packPath = value.__file__ - except AttributeError: - continue - - packPath = os.path.normcase(os.path.normpath(packPath)) - - isEnvPackage = any(packPath.startswith(p) for p in paths) - if isEnvPackage: - sys.modules.pop(key) - - -def caseSplit(name): - """Split CamelCase and dromedaryCase words - Taken From https://stackoverflow.com/questions/29916065/how-to-do-camelcase-split-in-python - - Parameters - ---------- - name : str - The string to split - - Returns - ------- - list - The split string - """ - matches = re.finditer(".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)", name) - return [m.group(0) for m in matches] - - -class singleShot(QObject): - """Decorator class used to implement a QTimer.singleShot(0, function) - - This is useful so your refresh function only gets called once even if - its connected to a signal that gets emitted several times at once. - - Note: - The values passed to the decorated method will be accumulated - and run all at once, then reset for the next go-round - - From the Qt Docs: - As a special case, a QTimer with a timeout of 0 will time out as - soon as all the events in the window system's event queue have - been processed. This can be used to do heavy work while providing - a snappy user interface - """ - - def __init__(self): - super(singleShot, self).__init__() - self._function = None - self._callScheduled = False - self._args = [] - self._inst = None - - def __call__(self, function): - self._function = function - - def newFunction(inst, *args): - """ - - Parameters - ---------- - inst : - - *args : - - - Returns - ------- - - """ - self._args.extend(args) - if not self._callScheduled: - self._inst = inst - self._callScheduled = True - QTimer.singleShot(0, self.callback) - - newFunction.__name__ = function.__name__ - newFunction.__doc__ = function.__doc__ - return newFunction - - def callback(self): - """Calls the decorated function and resets singleShot for the next group of calls""" - self._callScheduled = False - # self._args needs to be cleared before we call self._function - args = self._args - inst = self._inst - self._inst = None - self._args = [] - self._function(inst, args) - - -def makeUnique(seq): - """Make a sequence unique, keeping the first time each item is seen - - Parameters - ---------- - seq : list or tuple - A python sequence - - Returns - ------- - list - A list with unique items - """ - seen = set() - seen_add = seen.add # only resolve the method lookup once - return [x for x in seq if not (x in seen or seen_add(x))] - - -@contextmanager -def nested(*managers): - """Combine an arbitrary number of context managers into a single nested - context manager. - """ - with ExitStack() as stack: - yield [stack.enter_context(m) for m in managers] - - -def naturalSortKey(s, _nsre=re.compile("([0-9]+)")): - """Get a sort key that puts strings with numbers in numerical order - This is accomplished by splitting the string into groups of digits, and non-digits, - then converting the digit groups into integers. - - Parameters - ---------- - s : str - The string to get the key for - _nsre : - A hack argument to hold the compiled regex - - Returns - ------- - list - A list containing both strings and integers. - """ - return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)] - - -def getIcon(iconName): - path = os.path.join(os.path.dirname(__file__), "img", iconName) - return QIcon(path) - - -class Prefs(object): - """A wrapper for reading/writing prefs both internal and external to blur""" - - def __init__(self): - if AT_BLUR: - import blurdev.prefs - - self._pref = blurdev.prefs.find("tools/simplex3") - else: - self._pref = QSettings("Blur", "Simplex3") - - def restoreProperty(self, prop, default=None): - if AT_BLUR: - return self._pref.restoreProperty(prop, default) - else: - return toPyObject(self._pref.value(prop, default)) - - def recordProperty(self, prop, val): - if AT_BLUR: - self._pref.recordProperty(prop, val) - else: - self._pref.setValue(prop, val) - - def save(self): - if AT_BLUR: - self._pref.save() - else: - self._pref.sync() +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +"""Utility functions.""" + +from __future__ import annotations + +import os +import re +import sys +from typing import Union, TypeVar, Callable, Optional, cast, Sequence + +from Qt.QtCore import QObject, QTimer, QSettings +from Qt.QtGui import QIcon + +AT_BLUR = os.environ.get("SIMPLEX_AT_BLUR") == "true" + + +def getUiFile(fileVar: str, subFolder: str = "ui", uiName: Optional[str] = None) -> str: + """Get the path to the .ui file + + Parameters + ---------- + fileVar : str + The __file__ variable passed from the invocation + subFolder : str + The folder to look in for the ui files. Defaults to 'ui' + uiName : str or None + The name of the .ui file. Defaults to the basename of + fileVar with .ui instead of .py + + Returns + ------- + str + The path to the .ui file + """ + uiFolder, filename = os.path.split(fileVar) + if uiName is None: + uiName = os.path.splitext(filename)[0] + if not subFolder: + raise ValueError("A subfolder must be provided") + uiFile = os.path.join(uiFolder, subFolder, uiName + ".ui") + return uiFile + + +def getNextName(name: str, currentNames: Sequence[str]) -> str: + """Get the next available number-incremented name + + Parameters + ---------- + name : str + The name I want to check + currentNames : list + The names that currently exist + + Returns + ------- + str + The next available number-incremented name + """ + i = 0 + s = set(currentNames) + while True: + if not i: + nn = name + else: + nn = name + str(i) + if nn not in s: + return nn + i += 1 + return name + + +def clearPathSymbols(paths: list[str], keepers: Optional[list[str]] = None): + """Removes path symbols from the environment. + + This means I can unload my tools from the current process and re-import them + rather than dealing with the always finicky reload() + + We use directory paths rather than module names because it gives us more control + over what is unloaded + + Parameters + ---------- + paths : list + List of directory paths that will have their modules removed + keepers : list or None + List of module names that will not be removed (Default value = None) + """ + keepers = keepers or [] + paths = [os.path.normcase(os.path.normpath(p)) for p in paths] + + for key, value in sys.modules.items(): + protected = False + + # Used by multiprocessing library, don't remove this. + if key == "__parents_main__": + protected = True + + # Protect submodules of protected packages + if key in keepers: + protected = True + + ckey = key + while not protected and "." in ckey: + ckey = ckey.rsplit(".", 1)[0] + if ckey in keepers: + protected = True + + if protected: + continue + + try: + packPath = value.__file__ + except AttributeError: + continue + if packPath is None: + continue + + packPath = os.path.normcase(os.path.normpath(packPath)) + + isEnvPackage = any(packPath.startswith(p) for p in paths) + if isEnvPackage: + sys.modules.pop(key) + + +def caseSplit(name: str) -> list[str]: + """Split CamelCase and dromedaryCase words + Taken From https://stackoverflow.com/questions/29916065/how-to-do-camelcase-split-in-python + + Parameters + ---------- + name : str + The string to split + + Returns + ------- + list + The split string + """ + matches = re.finditer(".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)", name) + return [m.group(0) for m in matches] + + +F = TypeVar('F', bound=Callable) + + +class singleShot(QObject): + """Decorator class used to implement a QTimer.singleShot(0, function) + + This is useful so your refresh function only gets called once even if + its connected to a signal that gets emitted several times at once. + + Note: + The values passed to the decorated method will be accumulated + and run all at once, then reset for the next go-round + + From the Qt Docs: + As a special case, a QTimer with a timeout of 0 will time out as + soon as all the events in the window system's event queue have + been processed. This can be used to do heavy work while providing + a snappy user interface + """ + + def __init__(self): + super().__init__() + self._function: Optional[Callable] = None + self._callScheduled: bool = False + self._args = [] + self._inst = None + + def __call__(self, function: F) -> F: + self._function = function + + def newFunction(inst, *args): + self._args.extend(args) + if not self._callScheduled: + self._inst = inst + self._callScheduled = True + QTimer.singleShot(0, self.callback) + + newFunction.__name__ = function.__name__ + newFunction.__doc__ = function.__doc__ + + # 'cast' tricks the typechecker into preserving exact types, docstrings, + # and autocomplete features for the caller + return cast(F, newFunction) + + def callback(self): + """Calls the decorated function and resets singleShot for the next group of calls""" + self._callScheduled = False + # self._args needs to be cleared before we call self._function + args = self._args + inst = self._inst + self._inst = None + self._args = [] + if self._function is not None: + self._function(inst, args) + + +T = TypeVar('T') + + +def makeUnique(seq: Sequence[T]) -> Sequence[T]: + """Make a sequence unique, keeping the first time each item is seen + + Parameters + ---------- + seq : list or tuple + A python sequence + + Returns + ------- + list + A list with unique items + """ + seen = set() + seen_add = seen.add # only resolve the method lookup once + return [x for x in seq if not (x in seen or seen_add(x))] + + +def naturalSortKey(s: str, _nsre=re.compile("([0-9]+)")) -> list[Union[str, int]]: + """Get a sort key that puts strings with numbers in numerical order + This is accomplished by splitting the string into groups of digits, and non-digits, + then converting the digit groups into integers. + + Parameters + ---------- + s : str + The string to get the key for + _nsre : + A hack argument to hold the compiled regex + + Returns + ------- + list + A list containing both strings and integers. + """ + return [int(text) if text.isdigit() else text.lower() for text in _nsre.split(s)] + + +def getIcon(iconName: str) -> QIcon: + path = os.path.join(os.path.dirname(__file__), "img", iconName) + return QIcon(path) + + +class Prefs(object): + """A wrapper for reading/writing prefs both internal and external to blur""" + + def __init__(self): + self._pref: Union['blurdev.prefs.Preference', QSettings] + if AT_BLUR: + import blurdev.prefs + + self._pref = blurdev.prefs.find("tools/simplex3") + else: + self._pref = QSettings("Blur", "Simplex3") + + def restoreProperty(self, prop: str, default: Optional[str] = None): + if isinstance(self._pref, QSettings): + return self._pref.value(prop, default) + else: + return self._pref.restoreProperty(prop, default) + + def recordProperty(self, prop: str, val: Optional[str]): + if isinstance(self._pref, QSettings): + self._pref.setValue(prop, val) + else: + self._pref.recordProperty(prop, val) + + def save(self): + if isinstance(self._pref, QSettings): + self._pref.sync() + else: + self._pref.save() From 5e2fc2ad6fd8cc17d264b57731348fe89e113e09 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Mon, 17 Aug 2026 18:03:57 -0700 Subject: [PATCH 25/47] I don't need to keep track of the models. The adapter model and observers handle all of that Fix some exec_ stuff, clean up the singleshot as well --- src/python/simplexui/channelBox.py | 2 +- src/python/simplexui/dragFilter.py | 6 ++--- src/python/simplexui/falloffDialog.py | 27 ++++++++------------ src/python/simplexui/interfaceModel.py | 9 +++---- src/python/simplexui/interfaceModelTrees.py | 14 +++++++---- src/python/simplexui/items/combo.py | 6 +++++ src/python/simplexui/items/group.py | 5 +++- src/python/simplexui/items/simplex.py | 4 +++ src/python/simplexui/items/slider.py | 2 +- src/python/simplexui/items/stack.py | 22 +++++++++++++++- src/python/simplexui/simplexDialog.py | 10 ++++---- src/python/simplexui/traversalDialog.py | 5 ++-- src/python/simplexui/utils.py | 28 +++++++++++++++++---- 13 files changed, 94 insertions(+), 46 deletions(-) diff --git a/src/python/simplexui/channelBox.py b/src/python/simplexui/channelBox.py index 95515261..17d6d442 100644 --- a/src/python/simplexui/channelBox.py +++ b/src/python/simplexui/channelBox.py @@ -365,7 +365,7 @@ class ChannelListModel(QAbstractItemModel): def __init__(self, simplex, parent): super().__init__(parent) self.simplex = simplex - self.simplex.models.append(self) + # self.simplex.models.append(self) self.channels = [] def setChannels(self, channels): diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index 5e60df3a..2c07182f 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -114,7 +114,7 @@ def __init__(self, parent: Optional[QWidget]): self.slowDivisor: float = 5.0 # private vars - self._lastPos: QPoint = QPoint() + self._lastPos: Optional[QPoint] = None self._leftover: float = 0.0 self._dragStart: Optional[QPoint] = None self._firstDrag: bool = False @@ -214,7 +214,7 @@ def doDrag(self, o: QObject, e: QMouseEvent): if par is not None: self._lastPos = par.mapFromGlobal(p) else: - self._lastPos = QPoint() + self._lastPos = None def getCurrentScreen(self, o: QObject, global_pos: QPoint): screen = QGuiApplication.screenAt(global_pos) @@ -286,7 +286,7 @@ def myendDrag(self, o: QObject, e: QMouseEvent): elif self._dragType: self.restoreOverrideCursor() self._dragType = DragType.DRAG_NONE - self._lastPos = QPoint() + self._lastPos = None self._dragStart = None self._screen = None self.dragReleased.emit() diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index 11864b64..1fcb0c2d 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -17,12 +17,9 @@ # This module imports QT from PyQt4, PySide or PySide2 # Depending on what's available -import os -import re - -import Qt as QtLib +from __future__ import annotations from .interfaceModel import FalloffDataModel -from .items import Falloff +from .items.falloff import PlanarFalloff from Qt import QtCompat from Qt.QtCore import ( QByteArray, @@ -40,7 +37,6 @@ QPainterPath, QPalette, QPen, - QStandardItemModel, ) from Qt.QtWidgets import ( QComboBox, @@ -58,9 +54,6 @@ ) from .utils import getNextName, getUiFile, Prefs -AT_BLUR = os.environ.get("SIMPLEX_AT_BLUR") == "true" -NAME_CHECK = re.compile(r"[A-Za-z][\w.]*") - class CurveEditWidget(QWidget): tangentUpdated = Signal(float, float) @@ -310,7 +303,7 @@ def __init__(self, parent): self.simplex = None self.parUI.simplexLoaded.connect(self.loadSimplex) - self.foModel = QStandardItemModel() + self.foModel = FalloffDataModel(None, self) self.uiFalloffWID = CurveEditWidget(self) policy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding) @@ -360,7 +353,7 @@ def loadSimplex(self): return if system is None: - self.foModel = QStandardItemModel() + self.foModel = FalloffDataModel(None, self) self.uiShapeFalloffCBOX.setModel(self.foModel) if self._falloffMapper is not None: self._falloffMapper.clearMapping() @@ -374,7 +367,7 @@ def loadSimplex(self): # Populate Settings widgets self.foModel = FalloffDataModel(self.simplex, self) - self.simplex.falloffModels.append(self.foModel) + # self.simplex.falloffModels.append(self.foModel) self.uiShapeFalloffCBOX.setModel(self.foModel) self._falloffMapper.setModel(self.foModel) @@ -404,13 +397,13 @@ def newFalloff(self): if not good: return - if not NAME_CHECK.match(newName): + if not newName.isidentifier(): message = "Falloff name can only contain letters and numbers, and cannot start with a number" QMessageBox.warning(self, "Warning", message) return nn = getNextName(newName, foNames) - Falloff.createPlanar(nn, self.simplex, "X", 1.0, 0.66, 0.33, -1.0) + PlanarFalloff.createPlanar(nn, self.simplex, "X", 1.0, 0.66, 0.33, -1.0) def duplicateFalloff(self): """Duplicate the selected falloff""" @@ -458,7 +451,7 @@ def renameFalloff(self): if not good: return - if not NAME_CHECK.match(newName): + if not newName.isidentifier(): message = "Falloff name can only contain letters and numbers, and cannot start with a number" QMessageBox.warning(self, "Warning", message) return @@ -469,7 +462,8 @@ def renameFalloff(self): def storeSettings(self): """Store the UI settings for this dialog""" pref = Prefs() - pref.recordProperty("fogeometry", self.saveGeometry()) + geodata = bytes(self.saveGeometry().data()).decode() + pref.recordProperty("fogeometry", geodata) pref.save() def loadSettings(self): @@ -477,6 +471,7 @@ def loadSettings(self): pref = Prefs() geo = pref.restoreProperty("fogeometry", None) if geo is not None: + assert isinstance(geo, QByteArray) self.restoreGeometry(geo) def hideEvent(self, event): diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index 9c96d409..a36cfba6 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -289,6 +289,7 @@ class SimplexFilterModel(BaseProxyModel): def __init__(self, model, parent=None): super().__init__(model, parent) self.setSourceModel(model) + self.filterShapes = True self._filterString = [] self._filterReg = [] self.isolateList = [] @@ -397,7 +398,6 @@ def __init__(self, model, parent=None): self.filterRequiresAny = False self.filterRequiresOnly = False - self.filterShapes = True def filterAcceptsRow(self, sourceRow, sourceParent): # always sort by the first column #column = self.filterKeyColumn() @@ -468,7 +468,7 @@ class SliderGroupModel(AdapterModel): def __init__(self, simplex, parent): super().__init__(simplex, parent) self.simplex = simplex - self.simplex.models.append(self) + # self.simplex.models.append(self) def getItemRow(self, item): try: @@ -520,8 +520,7 @@ class FalloffModel(AdapterModel): def __init__(self, simplex, parent): super().__init__(simplex, parent) self.simplex = simplex - if self.simplex is not None: - self.simplex.falloffModels.append(self) + # if self.simplex is not None: self.simplex.falloffModels.append(self) self.sliders = [] self._checks = {} self.line = "" @@ -652,8 +651,6 @@ class FalloffDataModel(AdapterModel): def __init__(self, simplex, parent): super().__init__(simplex, parent) self.simplex = simplex - if self.simplex is not None: - self.simplex.falloffModels.append(self) def getItemAppendRow(self, item): try: diff --git a/src/python/simplexui/interfaceModelTrees.py b/src/python/simplexui/interfaceModelTrees.py index 1558ce8d..ea23cd3d 100644 --- a/src/python/simplexui/interfaceModelTrees.py +++ b/src/python/simplexui/interfaceModelTrees.py @@ -16,9 +16,12 @@ # along with Simplex. If not, see . from .dragFilter import DragFilter +from .utils import execmenu from .items import Group -from .items.treeItem import AdapterModel, CustomRoles +from .items.treeItem import CustomRoles from .items.dragItem import Draggable +from .interfaceModel import SimplexFilterModel + from Qt.QtCore import ( QItemSelection, QItemSelectionModel, @@ -79,11 +82,11 @@ def __init__(self, parent): self.setColumnWidth(1, 50) self.setColumnWidth(2, 20) - def setModel(self, model: AdapterModel): + def setModel(self, model: SimplexFilterModel): super().setModel(model) - def model(self) -> AdapterModel: - return cast(AdapterModel, super().model()) + def model(self) -> SimplexFilterModel: + return cast(SimplexFilterModel, super().model()) def setPlugins(self, plugins): """Set the right-click menu plugins for the tree @@ -351,7 +354,8 @@ def showContextMenu(self, clickIdx, indexes, pos): menu = QMenu() for plug in self._plugins: plug.registerContext(self, clickIdx, indexes, menu) - menu.exec_(pos) + + execmenu(menu, pos) # Selection def getSelectedItems(self, typ=None): diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index add0649d..2a4d0101 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -641,3 +641,9 @@ def treeChildCount(self) -> int: def treeChecked(self) -> bool: return self.enabled + + def treeData(self, column: int) -> Optional[Any]: + if column == 0: + return self.name + return None + diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index cd82229b..efffaaa3 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -250,7 +250,6 @@ def take(self, things: list[GroupMember]): if thing not in self.items: thing.setGroup(self) - def treeChild(self, row) -> TreeItem: return self.items[row] @@ -263,3 +262,7 @@ def treeParent(self) -> TreeItem: def treeChildCount(self) -> int: return len(self.items) + def treeData(self, column: int) -> Optional[str]: + if column == 0: + return self.name + return None diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 77d580f3..fb7b7985 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -139,6 +139,10 @@ def __deepcopy__(self, memo): setattr(result, k, copy.deepcopy(v, memo)) else: setattr(result, k, DummyDCC(result)) + elif k == '_observerServers': + # do not make a copy of the observers + # because they contain a bunch of Qt stuff + setattr(result, k, []) elif k == "expanded": # do not make a copy of the expansion # because it's keyed off the un-copied models diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index 6b775788..b300137c 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -20,7 +20,7 @@ import itertools from ..interface import undoContext -from ..utils import caseSplit, getNextName, makeUnique, singleShot +from ..utils import caseSplit, getNextName, makeUnique, singleShot, makeUnique from .accessor import SimplexTreeAccessor from .group import Group from .progression import ProgPair, Progression diff --git a/src/python/simplexui/items/stack.py b/src/python/simplexui/items/stack.py index f7706e53..163a36e9 100644 --- a/src/python/simplexui/items/stack.py +++ b/src/python/simplexui/items/stack.py @@ -107,11 +107,31 @@ def store(self, wrapObj: Any): if self.depth == 0: # Only store the top Level of the stack srevision = wrapObj.DCC.incrementRevision() - self.push(srevision, copy.deepcopy(wrapObj.simplex)) + + """ + print("COPYING", wrapObj.simplex) + for k in dir(wrapObj.simplex): + print(k, type(getattr(wrapObj.simplex, k))) + print("------------DONE") + """ + memo = {} + try: + self.push(srevision, copy.deepcopy(wrapObj.simplex, memo=memo)) + except Exception: + print("MEMO!", memo) + raise else: yield +class Memo(dict): + def __setitem__(self, key, value): + print("SETTING", key, value) + super().__setitem__(key, value) + + + + def stackable(method): """A Decorator to make a method auto update the stack This decorator can only be used on methods of an object diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 11c337a6..23440e17 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -44,11 +44,11 @@ from .items import Combo, Group, ProgPair, Simplex, Slider, Stack from .menu import buildToolMenu, loadPlugins from .traversalDialog import TraversalDialog -from .utils import getNextName, getUiFile, makeUnique, naturalSortKey, Prefs +from .utils import getNextName, getUiFile, makeUnique, naturalSortKey, Prefs, execwid # This module imports QT from PyQt4, PySide or PySide2 # Depending on what's available -from Qt import QtCompat +from Qt import QtCompat, IsPySide6, IsPyQt6 from Qt.QtCore import Qt, Signal from Qt.QtGui import QStandardItemModel, QAction from Qt.QtWidgets import ( @@ -400,8 +400,8 @@ def setSystem(self, system): # set and connect the new stuff self.simplex = system - self.simplex.models = [] - self.simplex.falloffModels = [] + # self.simplex.models = [] + # self.simplex.falloffModels = [] self.simplex.stack = oldStack # self.toolActions.simplex = self.simplex @@ -815,7 +815,7 @@ def _newCombo(self, sliders, values): ccd = ComboCheckDialog(sliders, values=values, mode="create", parent=self) ccd.move(self.pos()) - ccd.exec_() + execwid(ccd) def newActiveCombo(self): """Create a combo based on the UI Sliders that are currently nonzero""" diff --git a/src/python/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py index ded8ec22..8e18df50 100644 --- a/src/python/simplexui/traversalDialog.py +++ b/src/python/simplexui/traversalDialog.py @@ -33,6 +33,7 @@ # This module imports QT from PyQt4, PySide or PySide2 # Depending on what's available +from Qt import IsPySide6, IsPyQt6 from Qt import QtCompat from Qt.QtGui import QStandardItemModel, QAction from Qt.QtWidgets import ( @@ -46,7 +47,7 @@ QVBoxLayout, ) from .travCheckDialog import TraversalCheckDialog -from .utils import getUiFile, makeUnique +from .utils import getUiFile, makeUnique, execwid NAME_CHECK = re.compile(r"[A-Za-z][\w.]*") @@ -246,7 +247,7 @@ def newTrav(self): sliders, mode="create", parent=self, grandparent=self.parUI ) tcd.move(self.pos()) - tcd.exec_() + execwid(tcd) def addSlider(self): """Add a slider to the traversal's definition""" diff --git a/src/python/simplexui/utils.py b/src/python/simplexui/utils.py index 20e8b322..b2c78cb7 100755 --- a/src/python/simplexui/utils.py +++ b/src/python/simplexui/utils.py @@ -22,14 +22,31 @@ import os import re import sys -from typing import Union, TypeVar, Callable, Optional, cast, Sequence +from typing import Union, TypeVar, Callable, Optional, cast, Sequence, Any -from Qt.QtCore import QObject, QTimer, QSettings + +from Qt import IsPySide6, IsPyQt6 +from Qt.QtCore import QObject, QTimer, QSettings, QPoint from Qt.QtGui import QIcon +from Qt.QtWidgets import QMenu AT_BLUR = os.environ.get("SIMPLEX_AT_BLUR") == "true" +def execmenu(act: QMenu, pos: QPoint): + if IsPySide6 or IsPyQt6: + act.exec(pos) + else: + act.exec_() + + +def execwid(wid): + if IsPySide6 or IsPyQt6: + wid.exec() + else: + wid.exec_() + + def getUiFile(fileVar: str, subFolder: str = "ui", uiName: Optional[str] = None) -> str: """Get the path to the .ui file @@ -188,6 +205,7 @@ def __call__(self, function: F) -> F: def newFunction(inst, *args): self._args.extend(args) + self._args = makeUnique(self._args) if not self._callScheduled: self._inst = inst self._callScheduled = True @@ -209,7 +227,7 @@ def callback(self): self._inst = None self._args = [] if self._function is not None: - self._function(inst, args) + self._function(inst, *args) T = TypeVar('T') @@ -270,13 +288,13 @@ def __init__(self): else: self._pref = QSettings("Blur", "Simplex3") - def restoreProperty(self, prop: str, default: Optional[str] = None): + def restoreProperty(self, prop: str, default: Optional[str] = None) -> Any: if isinstance(self._pref, QSettings): return self._pref.value(prop, default) else: return self._pref.restoreProperty(prop, default) - def recordProperty(self, prop: str, val: Optional[str]): + def recordProperty(self, prop: str, val: Any): if isinstance(self._pref, QSettings): self._pref.setValue(prop, val) else: From a50b3e984f77f44d375107bf204751d3c36698dd Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 18 Aug 2026 12:20:50 -0700 Subject: [PATCH 26/47] Make Falloffs and splitting more robust --- src/python/simplexui/items/falloff.py | 154 ++++++-------- src/python/simplexui/items/simplex.py | 280 ++++++++++++++------------ 2 files changed, 214 insertions(+), 220 deletions(-) diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index 1e4ccf94..f56afbb1 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -14,8 +14,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations import copy import math @@ -33,6 +31,58 @@ from .progression import Progression +class SplitDefinition: + """A class defining how splits are applied to a system""" + + LEFTSIDE: str = "L" + RIGHTSIDE: str = "R" + TOPSIDE: str = "U" + BOTTOMSIDE: str = "D" + FRONTSIDE: str = "F" + BACKSIDE: str = "B" + ALLSIDES: str = LEFTSIDE + RIGHTSIDE + TOPSIDE + BOTTOMSIDE + FRONTSIDE + BACKSIDE + + CENTERS: str = "MC" + + VERTICAL_SPLIT: str = "V" + VERTICAL_RESULTS: str = TOPSIDE + BOTTOMSIDE + VERTICAL_AXIS: str = "Y" + VERTICAL_AXISINDEX: int = 1 + + HORIZONTAL_SPLIT: str = "X" + HORIZONTAL_RESULTS: str = LEFTSIDE + RIGHTSIDE + HORIZONTAL_AXIS: str = "X" + HORIZONTAL_AXISINDEX: int = 0 + + DEPTH_SPLIT: str = "Z" + DEPTH_RESULTS: str = FRONTSIDE + BACKSIDE + DEPTH_AXIS: str = "Z" + DEPTH_AXISINDEX: int = 2 + + RESTNAME: str = "Rest" + SEP: str = "_" + + def getComponentIndex(self, axis: str) -> int: + """Get the axis index. Usually x->1 y->2 z->3""" + axis = axis.lower() + if axis == self.HORIZONTAL_AXIS.lower(): + return self.HORIZONTAL_AXISINDEX + elif axis == self.VERTICAL_AXIS.lower(): + return self.VERTICAL_AXISINDEX + elif axis == self.DEPTH_AXIS.lower(): + return self.DEPTH_AXISINDEX + raise ValueError(f"Invalid Axis: {axis}") + + def getSearchRep(self, axis: str) -> tuple[str, str]: + if axis.lower() == self.HORIZONTAL_AXIS.lower(): + return self.HORIZONTAL_SPLIT, self.HORIZONTAL_RESULTS + elif axis.lower() == self.VERTICAL_AXIS.lower(): + return self.VERTICAL_SPLIT, self.VERTICAL_RESULTS + elif axis.lower() == self.DEPTH_AXIS.lower(): + return self.DEPTH_SPLIT, self.DEPTH_RESULTS + raise ValueError(f"Invalid Axis: {axis}") + + class Falloff(SimplexAccessor): """Falloffs define how shapes are split left/right, front/back, top/bottom, or however else @@ -69,9 +119,6 @@ class Falloff(SimplexAccessor): HORIZONTAL_SPLIT, HORIZONTAL_AXIS, HORIZONTAL_AXISINDEX DEPTH_SPLIT, DEPTH_AXIS, DEPTH_AXISINDEX, - When UNsplitting a simplex system, this value controls the tolerance - UNSPLIT_GUESS_TOLERANCE - Parameters ---------- name : str @@ -83,36 +130,6 @@ class Falloff(SimplexAccessor): You should use one of the classmethod like Falloff.createPlanar or Falloff.createMap instead """ - LEFTSIDE: str = "L" - RIGHTSIDE: str = "R" - TOPSIDE: str = "U" - BOTTOMSIDE: str = "D" - FRONTSIDE: str = "F" - BACKSIDE: str = "B" - ALLSIDES: str = LEFTSIDE + RIGHTSIDE + TOPSIDE + BOTTOMSIDE + FRONTSIDE + BACKSIDE - - CENTERS: str = "MC" - - VERTICAL_SPLIT: str = "V" - VERTICAL_RESULTS: str = TOPSIDE + BOTTOMSIDE - VERTICAL_AXIS: str = "Y" - VERTICAL_AXISINDEX: int = 1 - - HORIZONTAL_SPLIT: str = "X" - HORIZONTAL_RESULTS: str = LEFTSIDE + RIGHTSIDE - HORIZONTAL_AXIS: str = "X" - HORIZONTAL_AXISINDEX: int = 0 - - DEPTH_SPLIT: str = "Z" - DEPTH_RESULTS: str = FRONTSIDE + BACKSIDE - DEPTH_AXIS: str = "Z" - DEPTH_AXISINDEX: int = 2 - - RESTNAME: str = "Rest" - SEP: str = "_" - - UNSPLIT_GUESS_TOLERANCE: float = 0.33 - def __init__(self, name: str, simplex: Simplex, axis: str): super().__init__(simplex) with self.stack.store(self): @@ -234,33 +251,6 @@ def axis(self, value: str): self._axis = value # TODO: Does this need to update the dcc?? - def _setSearchRep(self): - if self.axis.lower() == self.HORIZONTAL_AXIS.lower(): - self._search = self.HORIZONTAL_SPLIT - self._rep = self.HORIZONTAL_RESULTS - elif self.axis.lower() == self.VERTICAL_AXIS.lower(): - self._search = self.VERTICAL_SPLIT - self._rep = self.VERTICAL_RESULTS - elif self.axis.lower() == self.DEPTH_AXIS.lower(): - self._search = self.DEPTH_SPLIT - self._rep = self.DEPTH_RESULTS - - @property - def search(self) -> str: - """The values this fallof searches for""" - if self._search is None: - self._setSearchRep() - assert self._search is not None - return self._search - - @property - def rep(self) -> str: - """The values this falloff replaces with""" - if self._rep is None: - self._setSearchRep() - assert self._rep is not None - return self._rep - @property def verts(self) -> Optional[npt.NDArray]: """Get the stored vertex values""" @@ -308,21 +298,22 @@ def getSidedName(self, name: str, sIdx: int) -> str: : str The newly sided name """ - search = self.search - replace = self.rep[sIdx] + sdef = self.simplex.sdef + search, replaces = sdef.getSearchRep(self.axis) + replace = replaces[sIdx] nn = name - s = "{0}{1}{0}".format(self.SEP, search) - r = "{0}{1}{0}".format(self.SEP, replace) + s = "{0}{1}{0}".format(sdef.SEP, search) + r = "{0}{1}{0}".format(sdef.SEP, replace) nn = nn.replace(s, r) - s = "{0}{1}".format(self.SEP, search) # handle Postfix - r = "{0}{1}".format(self.SEP, replace) + s = "{0}{1}".format(sdef.SEP, search) # handle Postfix + r = "{0}{1}".format(sdef.SEP, replace) if nn.endswith(s): nn = r.join(nn.rsplit(s, 1)) - s = "{1}{0}".format(self.SEP, search) # handle Prefix - r = "{1}{0}".format(self.SEP, replace) + s = "{1}{0}".format(sdef.SEP, search) # handle Prefix + r = "{1}{0}".format(sdef.SEP, replace) if nn.startswith(s): nn = nn.replace(s, r, 1) return nn @@ -412,19 +403,7 @@ def __init__( self._maxHandle = maxHandle self._minHandle = minHandle self._minVal = minVal - self._bezier = None - self._search = None - self._rep = None - self._weights = None - self._verts: Optional[npt.NDArray] = None - self._thing = None - self._thingRepr = None - self._name = name - self.children = [] - self._buildIdx = None - self.expanded = {} - self.simplex.falloffs.append(self) @classmethod def createPlanar( @@ -628,16 +607,7 @@ def weights(self) -> npt.NDArray: self.name ) ) - - if self.axis.lower() == self.HORIZONTAL_AXIS.lower(): - component = self.HORIZONTAL_AXISINDEX - elif self.axis.lower() == self.VERTICAL_AXIS.lower(): - component = self.VERTICAL_AXISINDEX - elif self.axis.lower() == self.DEPTH_AXIS.lower(): - component = self.DEPTH_AXISINDEX - else: - raise ValueError("Falloff found with no axis set") - + component = self.simplex.sdef.getComponentIndex(self.axis) self._weights = np.array( [self.getMultiplier(v[component]) for v in self._verts] ) diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index fb7b7985..cf1f994d 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -14,17 +14,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations import copy import itertools import json - -try: - import numpy as np -except ImportError: - np = None +import numpy as np from ..commands.alembicCommon import ( buildAlembicArchiveData, @@ -34,10 +28,9 @@ ) from ..interface import DCC, undoContext from ..interface.dummyInterface import DCC as DummyDCC -from Qt.QtGui import QColor -from Qt.QtWidgets import QApplication + from .combo import Combo, ComboPair -from .falloff import Falloff +from .falloff import Falloff, SplitDefinition from .group import Group from .progression import ProgPair, Progression from .shape import Shape @@ -46,6 +39,8 @@ from .traversal import Traversal, TravPair from .treeItem import TreeRootItem +from Qt.QtGui import QColor +from Qt.QtWidgets import QApplication from typing import Optional, Any, TYPE_CHECKING, Union @@ -85,9 +80,6 @@ def __init__(self, name: str = "", forceDummy=False, sliderMul=1.0): A multiplier for the range of sliders. Simplex will only define values between -1 and 1. This multiplier will let the attribute range in the DCC be larger so animators can push the extremes - - Returns - ------- """ super().__init__() self.sliderMul: float = 1.0 @@ -106,16 +98,14 @@ def __init__(self, name: str = "", forceDummy=False, sliderMul=1.0): self.stack: Stack = Stack() # Reference to the Undo stack self._extras: dict[str, Any] = {} # extra key data to store in the output json self._legacy: bool = False # whether to write the legacy types - - def columnCount(self): - return 3 + self.sdef: SplitDefinition = SplitDefinition() # Read by falloffs for splitting @property - def simplex(self): + def simplex(self) -> Simplex: """A uniform accessor so that we can always get the root from any object""" return self - def __deepcopy__(self, memo): + def __deepcopy__(self, memo: dict[int, Any]) -> Simplex: """Gotta be really picky about what gets deepcopied. Especially since I pretty much abuse the deepcopy mechanism to do splitting Deep-copied systems have no reference to a UI, or a DCC @@ -153,9 +143,11 @@ def __deepcopy__(self, memo): def _initValues(self): """Re-initialize the variables to that of an empy system""" + self.sliderMul = 1.0 self._name = "" # The name of the system self.sliders = [] # List of contained sliders self.combos = [] # List of contained combos + self.traversals = [] # list of contained traversals self.sliderGroups = [] # List of groups containing sliders self.comboGroups = [] # List of groups containing combos self.traversalGroups = [] # List of groups containing combos @@ -1566,13 +1558,19 @@ def buildSplitterList( return toSplit, splitBy, memo - def split(self, pBar: Optional[QProgressDialog] = None) -> Simplex: + def split( + self, + sdef: Optional[SplitDefinition] = None, + pBar: Optional[QProgressDialog] = None, + ) -> Simplex: """Return a split deepcopy of the system. The new system will be a dummy system containing all the shapes as numpy arrays which can be exported to a .smpx file Parameters ---------- + sdef : SplitDefinition, optional + If provided, use this split definition while splitting pBar : QProgressDialog, optional If provided, display progress in this dialog @@ -1588,128 +1586,154 @@ def split(self, pBar: Optional[QProgressDialog] = None) -> Simplex: pBar.setValue(0) pBar.setLabelText("Building Split System") - # Very first thing: Ensure that every object with a falloff is fully splittable - # Meaning that splittable progs only contain splittable shapes. And splittable progs - # are only controlled by splittable controllers - for fo in self.falloffs: - controllers = self.sliders + self.combos + self.traversals - for ctrl in controllers: - prog = ctrl.prog - - pSplit = fo.canRename(prog) - cSplit = fo.canRename(ctrl) - sSplit = [s for s in prog.getShapes() if not s.isRest] - sSplit = [fo.canRename(shape) for shape in sSplit] - sSplitSame = all(i == sSplit[0] for i in sSplit) - sSplit = sSplit[0] - if not sSplitSame: - shapes = [i.name for i in prog.getShapes()] - msg = "Bad shapes: {0}".format(", ".join(shapes)) - raise ValueError( - "Mix of splittable and un-splittable shapes in a progression\n" - + msg - ) + oldsdef = self.sdef + newsdef = self.sdef if sdef is None else sdef - if pSplit != sSplit: - msg = "Bad Prog: {0}".format(prog.name) - raise ValueError("A progression is not fully splittable\n" + msg) - - if pSplit != cSplit: - msg = "Bad prog: {0}\nBad Controller:{1}".format( - prog.name, ctrl.name - ) - raise ValueError("A controller is not fully splittable\n" + msg) - - # Create the initial deepcopy - splitSmpx = copy.deepcopy(self) - splitSmpx.DCC.dummyLoad(self.DCC, pBar=pBar) - - # Sort the falloffs by which axis the split on - foByAxis = {} - for fo in splitSmpx.falloffs: - foByAxis.setdefault(fo.axis.lower(), []).append(fo) - - for axis, foList in foByAxis.items(): - if pBar is not None: - pBar.setLabelText("Splitting On {0} axis".format(axis)) - QApplication.processEvents() - else: - print("Splitting On {0} axis".format(axis)) - - # Get the items to split, and the memo that ensures *only* those items will be copied - toSplit, splitBy, memo = splitSmpx.buildSplitterList(foList) + try: + self.sdef = newsdef + + # Ensure that every object with a falloff is fully splittable + # Meaning that splittable progs only contain splittable shapes. And splittable progs + # are only controlled by splittable controllers + for fo in self.falloffs: + controllers = self.sliders + self.combos + self.traversals + for ctrl in controllers: + prog = ctrl.prog + + pSplit = fo.canRename(prog) + cSplit = fo.canRename(ctrl) + sSplit = [s for s in prog.getShapes() if not s.isRest] + sSplit = [fo.canRename(shape) for shape in sSplit] + sSplitSame = all(i == sSplit[0] for i in sSplit) + sSplit = sSplit[0] + if not sSplitSame: + shapes = [i.name for i in prog.getShapes()] + msg = "Bad shapes: {0}".format(", ".join(shapes)) + raise ValueError( + "Mix of splittable and un-splittable shapes in a progression\n" + + msg + ) + + if pSplit != sSplit: + msg = "Bad Prog: {0}".format(prog.name) + raise ValueError( + "A progression is not fully splittable\n" + msg + ) + + if pSplit != cSplit: + msg = "Bad prog: {0}\nBad Controller:{1}".format( + prog.name, ctrl.name + ) + raise ValueError("A controller is not fully splittable\n" + msg) + + # Create the initial deepcopy + splitSmpx = copy.deepcopy(self) + splitSmpx.DCC.dummyLoad(self.DCC, pBar=pBar) + + # Sort the falloffs by which axis the split on + foByAxis = {} + for fo in splitSmpx.falloffs: + foByAxis.setdefault(fo.axis.lower(), []).append(fo) + + for axis, foList in foByAxis.items(): + if pBar is not None: + pBar.setLabelText("Splitting On {0} axis".format(axis)) + QApplication.processEvents() + else: + print("Splitting On {0} axis".format(axis)) - # DeepCopy the items twice. Once for each side of the split - lSideSplitList = copy.deepcopy(toSplit, memo=copy.copy(memo)) - rSideSplitList = copy.deepcopy(toSplit, memo=copy.copy(memo)) + # Get the items to split, and the memo that ensures *only* those + # items will be copied when we deepcopy + toSplit, splitBy, memo = splitSmpx.buildSplitterList(foList) - if pBar is not None: - pBar.setMaximum(len(toSplit)) - QApplication.processEvents() + # DeepCopy the items twice. Once for each side of the split + lSideSplitList = copy.deepcopy(toSplit, memo=copy.copy(memo)) + rSideSplitList = copy.deepcopy(toSplit, memo=copy.copy(memo)) - # Loop through the copied items and make the replacements - for i, (oldItem, lItem, rItem) in enumerate( - zip(toSplit, lSideSplitList, rSideSplitList) - ): if pBar is not None: - pBar.setValue(i + 1) + pBar.setMaximum(len(toSplit)) QApplication.processEvents() - # Get thefalloff that will split oldItem into lItem and rItem - fo = splitBy[oldItem] - - # Rename the newly split items - fo.splitRename(lItem, 0) - fo.splitRename(rItem, 1) - - # Apply the falloff weights to any shapes - if isinstance(oldItem, Shape): - fo.applyFalloff(lItem, 0) - fo.applyFalloff(rItem, 1) - - # Maybe for all this, get the index of the oldItem in the group - # and insert rather than append?? - - # Remove the oldItem from any groups and add the newItems - if hasattr(oldItem, "group"): - oldItem.group.items.remove(oldItem) - oldItem.group = None - lItem.group.items.append(lItem) - rItem.group.items.append(rItem) - - # Remove the oldItem from the simplex storage, and add the newItems - if isinstance(oldItem, Slider): - splitSmpx.sliders.remove(oldItem) - splitSmpx.sliders.append(lItem) - splitSmpx.sliders.append(rItem) - elif isinstance(oldItem, Combo): - splitSmpx.combos.remove(oldItem) - splitSmpx.combos.append(lItem) - splitSmpx.combos.append(rItem) - elif isinstance(oldItem, Traversal): - splitSmpx.traversals.remove(oldItem) - splitSmpx.traversals.append(lItem) - splitSmpx.traversals.append(rItem) - elif isinstance(oldItem, Shape): - splitSmpx.shapes.remove(oldItem) - # Part of deepCopy ensures the new system uses the DummyDCC - # So this just removes the shape verts from the DummyDCC dictionary - # and doesn't actually delete the shape from anywhere important - splitSmpx.DCC.deleteShape(oldItem) - splitSmpx.shapes.append(lItem) - splitSmpx.shapes.append(rItem) - - splitSmpx.DCC.pushAllShapeVertices(splitSmpx.shapes) - return splitSmpx + # Loop through the copied items and make the replacements + for i, (oldItem, lItem, rItem) in enumerate( + zip(toSplit, lSideSplitList, rSideSplitList) + ): + if pBar is not None: + pBar.setValue(i + 1) + QApplication.processEvents() + + # Get thefalloff that will split oldItem into lItem and rItem + fo = splitBy[oldItem] + + # Rename the newly split items + fo.splitRename(lItem, 0) + fo.splitRename(rItem, 1) + + # Apply the falloff weights to any shapes + if isinstance(oldItem, Shape): + assert isinstance(lItem, Shape) + assert isinstance(rItem, Shape) + fo.applyFalloff(lItem, 0) + fo.applyFalloff(rItem, 1) + + # Maybe for all this, get the index of the oldItem in the group + # and insert rather than append?? + + # Remove the oldItem from any groups and add the newItems + if isinstance(oldItem, (Slider, Combo, Traversal)): + assert isinstance(lItem, (Slider, Combo, Traversal)) + assert isinstance(rItem, (Slider, Combo, Traversal)) + oldItem.group.items.remove(oldItem) + oldItem.group = None # type: ignore + lItem.group.items.append(lItem) + rItem.group.items.append(rItem) + + # Remove the oldItem from the simplex storage, and add the newItems + if isinstance(oldItem, Slider): + assert isinstance(lItem, Slider) + assert isinstance(rItem, Slider) + splitSmpx.sliders.remove(oldItem) + splitSmpx.sliders.append(lItem) + splitSmpx.sliders.append(rItem) + elif isinstance(oldItem, Combo): + assert isinstance(lItem, Combo) + assert isinstance(rItem, Combo) + splitSmpx.combos.remove(oldItem) + splitSmpx.combos.append(lItem) + splitSmpx.combos.append(rItem) + elif isinstance(oldItem, Traversal): + assert isinstance(lItem, Traversal) + assert isinstance(rItem, Traversal) + splitSmpx.traversals.remove(oldItem) + splitSmpx.traversals.append(lItem) + splitSmpx.traversals.append(rItem) + elif isinstance(oldItem, Shape): + assert isinstance(lItem, Shape) + assert isinstance(rItem, Shape) + splitSmpx.shapes.remove(oldItem) + # Part of deepCopy ensures the new system uses the DummyDCC + # So this just removes the shape verts from the DummyDCC dictionary + # and doesn't actually delete the shape from anywhere important + splitSmpx.DCC.deleteShape(oldItem) + splitSmpx.shapes.append(lItem) + splitSmpx.shapes.append(rItem) + + splitSmpx.DCC.pushAllShapeVertices(splitSmpx.shapes) + return splitSmpx + finally: + self.sdef = oldsdef # TREE CODE - def treeChild(self, row): + def columnCount(self) -> int: + return 3 + + def treeChild(self, row: int) -> Group: return self.groups[row] - def treeChildCount(self): + def treeChildCount(self) -> int: return len(self.groups) - def treeData(self, column): + def treeData(self, column: int) -> Optional[str]: if column == 0: return self.name return None From 46a288c88ca075b9ea249fa241824dc22450f884 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 18 Aug 2026 12:21:20 -0700 Subject: [PATCH 27/47] Add future annotation imports, and remove old pylint directives --- src/python/simplexui/channelBox.py | 6 +++--- .../simplexui/commands/alembicCommon.py | 4 ++-- .../simplexui/commands/applyCorrectives.py | 2 -- .../commands/reorderSimplexPoints.py | 8 +------- src/python/simplexui/commands/rigidAlign.py | 1 - src/python/simplexui/commands/unsubdivide.py | 4 ---- src/python/simplexui/commands/uvTransfer.py | 1 - src/python/simplexui/dragFilter.py | 2 +- .../simplexui/interface/dummyInterface.py | 2 -- .../simplexui/interface/mayaInterface.py | 2 -- src/python/simplexui/interfaceModel.py | 2 +- src/python/simplexui/interfaceModelTrees.py | 1 + src/python/simplexui/items/combo.py | 4 ---- src/python/simplexui/items/dragItem.py | 20 +++++++++++++++++++ src/python/simplexui/items/group.py | 2 -- src/python/simplexui/items/progression.py | 3 --- src/python/simplexui/items/shape.py | 5 ----- src/python/simplexui/items/slider.py | 4 +--- src/python/simplexui/items/stack.py | 2 -- src/python/simplexui/items/traversal.py | 2 -- .../menu/genericPlugins/_builtins.py | 2 +- src/python/simplexui/simplexDialog.py | 12 +++-------- src/python/simplexui/traversalDialog.py | 8 +------- 23 files changed, 35 insertions(+), 64 deletions(-) diff --git a/src/python/simplexui/channelBox.py b/src/python/simplexui/channelBox.py index 17d6d442..16727653 100644 --- a/src/python/simplexui/channelBox.py +++ b/src/python/simplexui/channelBox.py @@ -19,12 +19,12 @@ A Super-minimal ui for interacting with a Simplex System Currently VERY WIP. Probably shouldn't have committed it to master, but whatever """ - -# pylint:disable=unused-import,relative-import,missing-docstring,unused-argument,no-self-use +from __future__ import annotations import os import sys -from .interfaceModel import Group, Simplex, SimplexModel, Slider +from .items.simplex import Simplex +from .interfaceModel import Group, SimplexModel, Slider from Qt.QtCore import ( QAbstractItemModel, QEvent, diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 0926cf50..003dd5a2 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -45,7 +45,7 @@ import numpy.typing as npt try: - from imathnumpy import arrayToNumpy # pylint:disable=no-name-in-module + from imathnumpy import arrayToNumpy except ImportError: arrayToNumpy = None @@ -909,7 +909,7 @@ def getSmpxArchiveData(abcPath: str) -> tuple[IArchive, IPolyMesh, str]: props = sch.getUserProperties() jsString = readStringProperty(props, "simplex") - except Exception: # pylint: disable=broad-except + except Exception: # ensure that the .smpx file is released iarch, top, par, abcMesh = [None] * 4 raise diff --git a/src/python/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py index 14877a89..383fb3a5 100644 --- a/src/python/simplexui/commands/applyCorrectives.py +++ b/src/python/simplexui/commands/applyCorrectives.py @@ -14,8 +14,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - -# pylint:disable=unused-variable import itertools import os diff --git a/src/python/simplexui/commands/reorderSimplexPoints.py b/src/python/simplexui/commands/reorderSimplexPoints.py index 11a1d977..3adadc63 100644 --- a/src/python/simplexui/commands/reorderSimplexPoints.py +++ b/src/python/simplexui/commands/reorderSimplexPoints.py @@ -26,15 +26,9 @@ possible to invert the range if you think you've got it backwards """ -# pylint:disable=wrong-import-position import json - from .alembicCommon import buildSmpx, readSmpx - -try: - import numpy as np -except ImportError: - pass +import numpy as np def reorderSimplexPoints(sourcePath, matchPath, outPath, invertMatch=False): diff --git a/src/python/simplexui/commands/rigidAlign.py b/src/python/simplexui/commands/rigidAlign.py index 02466960..4ae4cdb8 100644 --- a/src/python/simplexui/commands/rigidAlign.py +++ b/src/python/simplexui/commands/rigidAlign.py @@ -42,7 +42,6 @@ def rigidAlign(P, Q, iters=10): : np.array The 4x4 transformation matrix that most closely aligns Q to P """ - # pylint:disable=invalid-name assert P.shape == Q.shape n, dim = P.shape diff --git a/src/python/simplexui/commands/unsubdivide.py b/src/python/simplexui/commands/unsubdivide.py index 46a0c1d3..e9d37cc0 100644 --- a/src/python/simplexui/commands/unsubdivide.py +++ b/src/python/simplexui/commands/unsubdivide.py @@ -14,10 +14,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - -# pylint: disable=unused-argument, too-many-locals -# pylint:disable=E0611,E0401 - import json from itertools import chain, zip_longest diff --git a/src/python/simplexui/commands/uvTransfer.py b/src/python/simplexui/commands/uvTransfer.py index a601ea27..d5f3e62c 100644 --- a/src/python/simplexui/commands/uvTransfer.py +++ b/src/python/simplexui/commands/uvTransfer.py @@ -15,7 +15,6 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -# pylint: disable=invalid-name import numpy as np INF = float("inf") diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index 2c07182f..a11d36aa 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -14,7 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - +from __future__ import annotations from Qt.QtCore import QEvent, QObject, QPoint, Qt, Signal, QRect from Qt.QtGui import QCursor, QMouseEvent, QGuiApplication diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index 142fe7d8..e0b190d3 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -14,8 +14,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - -# pylint: disable=invalid-name, unused-argument """A placeholder interface that takes arguments and does nothing with them""" import copy diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 97315130..03a8927c 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -14,8 +14,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - -# pylint: disable=invalid-name from __future__ import annotations import json import re diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index a36cfba6..b5a515e2 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -# pylint:disable=missing-docstring,unused-argument,no-self-use,too-many-return-statements +from __future__ import annotations import re from .items import ( diff --git a/src/python/simplexui/interfaceModelTrees.py b/src/python/simplexui/interfaceModelTrees.py index ea23cd3d..bac3003c 100644 --- a/src/python/simplexui/interfaceModelTrees.py +++ b/src/python/simplexui/interfaceModelTrees.py @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from .dragFilter import DragFilter from .utils import execmenu diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index 2a4d0101..b87b0e66 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -14,9 +14,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - - -# pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations from .accessor import SimplexTreeAccessor from .stack import stackable @@ -646,4 +643,3 @@ def treeData(self, column: int) -> Optional[Any]: if column == 0: return self.name return None - diff --git a/src/python/simplexui/items/dragItem.py b/src/python/simplexui/items/dragItem.py index 64de8be9..369f7f38 100644 --- a/src/python/simplexui/items/dragItem.py +++ b/src/python/simplexui/items/dragItem.py @@ -1,3 +1,23 @@ +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + +from __future__ import annotations + + class Draggable: """A mixin to make an item middle-click draggable""" diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index efffaaa3..7d9b0a81 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -14,8 +14,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations from .accessor import SimplexTreeAccessor from .stack import stackable diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index 7d9b34e4..3d754eb3 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -14,9 +14,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - - -# pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations from ..utils import getNextName from .accessor import SimplexTreeAccessor diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index 46628b2e..a692c0ad 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -15,14 +15,9 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations - - from ..interface import DCC, undoContext - -# pylint:disable=missing-docstring,unused-argument,no-self-use from .accessor import SimplexAccessor from .stack import stackable - from numpy import typing as npt from typing import Optional, Any, TYPE_CHECKING diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index b300137c..8c6c1667 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -14,13 +14,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations import itertools from ..interface import undoContext -from ..utils import caseSplit, getNextName, makeUnique, singleShot, makeUnique +from ..utils import caseSplit, getNextName, makeUnique, singleShot from .accessor import SimplexTreeAccessor from .group import Group from .progression import ProgPair, Progression diff --git a/src/python/simplexui/items/stack.py b/src/python/simplexui/items/stack.py index 163a36e9..17ead486 100644 --- a/src/python/simplexui/items/stack.py +++ b/src/python/simplexui/items/stack.py @@ -14,8 +14,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations import copy from collections import OrderedDict diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index 8032a733..0a01bd90 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -14,8 +14,6 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - -# pylint:disable=missing-docstring,unused-argument,no-self-use from __future__ import annotations from .accessor import SimplexTreeAccessor from .combo import Combo diff --git a/src/python/simplexui/menu/genericPlugins/_builtins.py b/src/python/simplexui/menu/genericPlugins/_builtins.py index 8584943c..c7304d85 100644 --- a/src/python/simplexui/menu/genericPlugins/_builtins.py +++ b/src/python/simplexui/menu/genericPlugins/_builtins.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -# pylint:disable=unused-variable +from __future__ import annotations from functools import partial from ...items import Combo, ComboPair, ProgPair, Progression, Slider diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 23440e17..6e6f4f6c 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -14,11 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - - -# Ignore a bunch of linter warnings that show up because of my choice of abstraction -# pylint: disable=unused-argument,too-many-public-methods,relative-import -# pylint: disable=too-many-statements,no-self-use,missing-docstring +from __future__ import annotations import json import os import re @@ -46,9 +42,7 @@ from .traversalDialog import TraversalDialog from .utils import getNextName, getUiFile, makeUnique, naturalSortKey, Prefs, execwid -# This module imports QT from PyQt4, PySide or PySide2 -# Depending on what's available -from Qt import QtCompat, IsPySide6, IsPyQt6 +from Qt import QtCompat from Qt.QtCore import Qt, Signal from Qt.QtGui import QStandardItemModel, QAction from Qt.QtWidgets import ( @@ -181,7 +175,7 @@ class SimplexDialog(Window): uiConnectionGroupWID: QWidget simplexLoaded = Signal() - openedDialogs = [] + openedDialogs: list[weakref.ref[SimplexDialog]] = [] def __init__(self, parent=None, dispatch=None): super().__init__(parent) diff --git a/src/python/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py index 8e18df50..1b730ead 100644 --- a/src/python/simplexui/traversalDialog.py +++ b/src/python/simplexui/traversalDialog.py @@ -15,10 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . - -# Ignore a bunch of linter warnings that show up because of my choice of abstraction -# pylint: disable=unused-argument,too-many-public-methods,relative-import -# pylint: disable=too-many-statements,no-self-use,missing-docstring +from __future__ import annotations import re from .interface import DCC @@ -31,9 +28,6 @@ from .interfaceModelTrees import TraversalTree from .items import Group, Simplex, Slider, Traversal, TravPair -# This module imports QT from PyQt4, PySide or PySide2 -# Depending on what's available -from Qt import IsPySide6, IsPyQt6 from Qt import QtCompat from Qt.QtGui import QStandardItemModel, QAction from Qt.QtWidgets import ( From 5064aaff26648d1377f218f6cc72ade4eb7e34d1 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 18 Aug 2026 12:43:52 -0700 Subject: [PATCH 28/47] Clean up a bunch of useless docstrings --- .../simplexui/interface/dummyInterface.py | 307 +----- .../simplexui/interface/mayaInterface.py | 959 +----------------- 2 files changed, 22 insertions(+), 1244 deletions(-) diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index e0b190d3..76dcecc6 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -50,7 +50,6 @@ def undoContext(inst=None): ---------- inst : DCC An instantiated DCC if available. (Default value = None) - """ if inst is None: DCC.staticUndoOpen() @@ -171,8 +170,6 @@ def __init__(self, name, db=DB): class DCC(object): - """ """ - program = "dummy" def __init__(self, simplex, stack=None): @@ -210,6 +207,7 @@ def dummyLoad(self, other, pBar=None): : DummyDCC : The new DummyDCC """ + assert self.simplex is not None other.getAllShapeVertices(other.simplex.shapes, pBar=pBar) # Load all the data onto the new dummy mesh @@ -266,7 +264,6 @@ def postLoad(self, simp, preRet): The simplex system that was created preRet : object The object returnd from the preload method - """ pass @@ -295,7 +292,6 @@ def loadNodes(self, simp, thing, create=True, pBar=None): Whether to create the missing nodes (Default value = True) pBar : QProgressDialog, optional An optional progress dialog (Default value = None) - """ if thing is None: thing = DummyMesh(simp.name) @@ -338,7 +334,6 @@ def loadConnections(self, simp, pBar=None): The simplex system to load connections for pBar : QProgressDialog, optional An optional progress dialog (Default value = None) - """ pass @@ -354,7 +349,6 @@ def getShapeThing(self, shapeName): ------- : object : The DCC Thing - """ return self.shapeNode.shapes.get(shapeName) @@ -384,7 +378,6 @@ def buildRestAbc(abcMesh, name): The Alembic mesh object name : str The name of the object to create - """ mesh = DummyMesh(name) # don't add it to a scene @@ -448,7 +441,6 @@ def loadAbcPoses(self, abcMesh, js, pBar=None): The simplex definition dictionary pBar : QProgressDialog, optional An optional progress dialog (Default value = None) - """ pass @@ -464,7 +456,6 @@ def loadAbc(self, abcMesh, js, pBar=None): The simplex definition dictionary pBar : QProgressDialog, optional An optional progress dialog (Default value = None) - """ shapes = js["shapes"] if js["encodingVersion"] > 1: @@ -484,7 +475,6 @@ def getAllShapeVertices(self, shapes, pBar=None): pBar : QProgressDialog, optional An optional progress dialog (Default value = None) - """ for shape in shapes: verts = self.getShapeVertices(shape) @@ -502,7 +492,6 @@ def getShapeVertices(self, shape): ------- : np.array : A numpy array of the point positions - """ return shape.thing.points @@ -516,7 +505,6 @@ def pushAllShapeVertices(self, shapes, pBar=None): pBar : QProgressDialog, optional An optional progress dialog (Default value = None) - """ for shape in shapes: self.pushShapeVertices(shape) @@ -527,7 +515,6 @@ def pushShapeVertices(self, shape): ---------- shape : Shape The Simplex Shape object to update - """ shape.thing.points = shape.verts @@ -579,7 +566,6 @@ def exportAbc( Do the export in worldspace (Default value = False) pBar : QProgressDialog, optional An optional progress dialog (Default value = None) - """ # export the data to alembic if dccMesh is None: @@ -636,7 +622,6 @@ def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): Do the export in worldspace (Default value = False) pBar : QProgressDialog, optional An optional progress dialog (Default value = None) - """ if dccMesh is None: raise ValueError( @@ -645,7 +630,7 @@ def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): self.exportAbc( dccMesh, abcMesh, js, world=world, ensureCorrect=False, pBar=pBar ) - + def deleteObj(self, dccMesh, path): """Export a mesh to the given path""" pass @@ -671,7 +656,6 @@ def setRevision(self, val): ---------- val : int The value to set - """ self._revision = val @@ -684,7 +668,6 @@ def renameSystem(self, name): ---------- name : str The new name - """ # TODO # oldName = self.name @@ -719,7 +702,6 @@ def createShape(self, shape, live=False, offset=10): Whether this shape is live-connected (Default value = False) offset : float The offset of the created shape (Default value = 10) - """ newShape = DummyShape(shape.name, self.shapeNode) newShape.points = copy.copy(self.mesh.verts) @@ -729,16 +711,6 @@ def createShape(self, shape, live=False, offset=10): def extractWithDeltaShape(self, shape, live=True, offset=10.0): """Make a mesh representing a shape. Can be live or not. Also, make a shapenode that is the delta of the change being made - - Parameters - ---------- - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - """ pass @@ -746,20 +718,6 @@ def extractWithDeltaShape(self, shape, live=True, offset=10.0): def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0): """Extract a shape with a live partial delta added in. Useful for updating progressive shapes - - Parameters - ---------- - shape : - - delta : - - value : - - live : - (Default value = True) - offset : - (Default value = 10.0) - """ pass @@ -767,16 +725,6 @@ def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0 def extractShape(self, shape, live=True, offset=10.0): """Make a mesh representing a shape. Can be live or not. Can also store its starting shape and delta data - - Parameters - ---------- - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - """ pass @@ -789,30 +737,11 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): mesh=someMesh, delete=False There is a possibility of a "make live" button: live=True, delete=False - - Parameters - ---------- - shape : - - mesh : - (Default value = None) - live : - (Default value = False) - delete : - (Default value = False) - """ pass @undoable def extractPosedShape(self, shape): - """??? - - Parameters - ---------- - shape : - - """ pass @undoable @@ -823,7 +752,6 @@ def zeroShape(self, shape): ---------- shape : Shape The simplex shpae to zero out - """ shape.thing.points = self.getShapeVertices(self.simplex.restShape) @@ -834,7 +762,6 @@ def deleteShape(self, toDelShape): Parameters ---------- toDelShape : - """ self.shapeNode.shapes.pop(toDelShape.name, None) @@ -848,7 +775,6 @@ def renameShape(self, shape, name): The simplex Shape object to rename name : str The new name - """ self.shapeNode.shapes.pop(shape.thing.name, None) shape.thing.name = name @@ -856,13 +782,6 @@ def renameShape(self, shape, name): @undoable def convertShapeToCorrective(self, shape): - """??? - - Parameters - ---------- - shape : - - """ pass # Falloffs @@ -873,7 +792,6 @@ def createFalloff(self, falloff): ---------- falloff : Falloff The simplex Falloff object to create - """ fo = DummyFalloff(falloff.name, self.scene) fo.weights = np.zeros(len(self.mesh.verts)) @@ -887,7 +805,6 @@ def duplicateFalloff(self, falloff, newFalloff): The already existing falloff newFalloff : Falloff The newly created falloff to store the newly duplicated data - """ fo = DummyFalloff(newFalloff.name, self.scene) fo.weights = copy.copy(falloff.thing.weights) @@ -899,34 +816,13 @@ def deleteFalloff(self, falloff): ---------- falloff : Falloff The Falloff object to delete - """ self.scene.remove(falloff.thing) def setFalloffData( self, falloff, splitType, axis, minVal, minHandle, maxHandle, maxVal, mapName ): - """Set the data of a falloff object - - Parameters - ---------- - falloff : - - splitType : - - axis : - - minVal : - - minHandle : - - maxHandle : - - maxVal : - - mapName : - - """ + """Set the data of a falloff object""" # TODO: set the per-vert falloffs pass # for eventual live splits @@ -937,7 +833,6 @@ def getFalloffThing(self, falloff): ---------- falloff : Falloff The simplex falloff object to get - """ return self.scene.get(DummyFalloff, falloff.name) @@ -950,7 +845,6 @@ def createSlider(self, slider): ---------- slider : Slider The simplex slider object to create - """ return DummyAttr(slider.name, 0.0, self.ctrl) @@ -964,7 +858,6 @@ def renameSlider(self, slider, name): The slider to rename name : str The new name - """ self.ctrl.attrs.pop(slider.thing.name, None) self.ctrl.attrs[name] = slider.thing @@ -978,7 +871,6 @@ def setSliderRange(self, slider): ---------- slider : Slider The slider to set - """ pass @@ -990,34 +882,15 @@ def deleteSlider(self, toDelSlider): ---------- toDelSlider : Slider The slider to delete - """ self.ctrl.attrs.pop(toDelSlider.name, None) @undoable def addProgFalloff(self, prog, falloff): - """ - - Parameters - ---------- - prog : - - falloff : - - """ pass # for eventual live splits @undoable def removeProgFalloff(self, prog, falloff): - """ - - Parameters - ---------- - prog : - - falloff : - - """ pass # for eventual live splits @undoable @@ -1030,7 +903,6 @@ def setSlidersWeights(self, sliders, weights): The sliders to set values for weights : [float, ...] The values to set - """ for slider, val in zip(sliders, weights): slider.thing.value = val @@ -1045,96 +917,33 @@ def setSliderWeight(self, slider, weight): The slider weight : float The value - """ slider.thing.value = weight @undoable def updateSlidersRange(self, sliders): - """Update the range of the given sliders - - Parameters - ---------- - sliders : - - """ + """Update the range of the given sliders""" pass @undoable def extractTraversalShape(self, trav, shape, live=True, offset=10.0): - """Extract a shape from a traversal progression - - Parameters - ---------- - trav : - - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - """ + """Extract a shape from a traversal progression""" pass @undoable def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False): - """Connect a shape to a traversal progression - - Parameters - ---------- - trav : - - shape : - - mesh : - (Default value = None) - live : - (Default value = True) - delete : - (Default value = False) - - """ + """Connect a shape to a traversal progression""" pass # Combos @undoable def extractComboShape(self, combo, shape, live=True, offset=10.0): - """Extract a shape from a combo progression - - Parameters - ---------- - combo : - - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - """ + """Extract a shape from a combo progression""" pass @undoable def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): - """Connect a shape to a combo progression - - Parameters - ---------- - combo : - - shape : - - mesh : - (Default value = None) - live : - (Default value = True) - delete : - (Default value = False) - - """ + """Connect a shape to a combo progression""" pass @staticmethod @@ -1145,12 +954,10 @@ def setDisabled(op): ---------- op : The operator to disable - Returns ------- : object : Some object that will allow us to re-enable the system - """ return None @@ -1162,7 +969,6 @@ def reEnable(helpers): ---------- helpers : object The helper object returned from setDisabled - """ pass @@ -1176,7 +982,6 @@ def renameCombo(self, combo, name): The combo to rename name : str The new name - """ pass @@ -1195,12 +1000,10 @@ def getSimplexOperatorsByName(name): name : str The name to search for - Returns ------- : object : The simplex operator for the DCC - """ return DB.ops.get(name) @@ -1217,7 +1020,6 @@ def getSimplexOperatorsOnObject(thing): ------- : [object, ...] : A list of simplex operators - """ return [o for o in thing.ops if isinstance(o, DummySimplex)] @@ -1234,7 +1036,6 @@ def getSimplexString(op): ------- : str : The simplex definition - """ return op.definition @@ -1253,7 +1054,6 @@ def getSimplexStringOnThing(thing, systemName): ------- : str : The simplex definition - """ for op in thing.ops: if op.name == systemName: @@ -1270,22 +1070,12 @@ def setSimplexString(op, val): The operator to set the definition on val : str The definition to set - - Returns - ------- - """ op.definition = val @staticmethod def selectObject(thing): - """Select an object in the DCC - - Parameters - ---------- - thing : - - """ + """Select an object in the DCC""" pass def selectCtrl(self): @@ -1305,7 +1095,6 @@ def getObjectByName(name): ------- : object : The found object - """ # TODO: maybe also filter by type?? # return DB.meshes.get(name) @@ -1320,12 +1109,10 @@ def getObjectName(thing): thing : object The dcc object to get the name for - Returns ------- : str : The Object Name - """ return thing.name @@ -1360,7 +1147,6 @@ def getPersistentFalloff(cls, thing): ------- : object : The requested persistent object - """ return cls.getObjectName(thing) @@ -1377,7 +1163,6 @@ def loadPersistentFalloff(cls, thing): ------- : object : The requested volatile object - """ return cls.getObjectByName(thing) @@ -1394,7 +1179,6 @@ def getPersistentShape(cls, thing): ------- : object : The requested persistent object - """ return cls.getObjectName(thing) @@ -1411,7 +1195,6 @@ def loadPersistentShape(cls, thing): ------- : object : The requested volatile object - """ return cls.getObjectByName(thing) @@ -1428,7 +1211,6 @@ def getPersistentSlider(cls, thing): ------- : object : The requested persistent object - """ return cls.getObjectName(thing) @@ -1445,7 +1227,6 @@ def loadPersistentSlider(cls, thing): ------- : object : The requested volatile object - """ return cls.getObjectByName(thing) @@ -1461,29 +1242,16 @@ def getFreezeThing(self, combo): class SliderDispatch(QtCore.QObject): - """ """ - valueChanged = Signal() def __init__(self, node, parent=None): super().__init__(parent) def emitValueChanged(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ self.valueChanged.emit() class Dispatch(QtCore.QObject): - """ """ - beforeNew = Signal() afterNew = Signal() beforeOpen = Signal() @@ -1495,83 +1263,27 @@ def __init__(self, parent=None): super().__init__(parent) def connectCallbacks(self): - """ """ pass def disconnectCallbacks(self): - """ """ pass def emitBeforeNew(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ self.beforeNew.emit() def emitAfterNew(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ self.afterNew.emit() def emitBeforeOpen(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ self.beforeOpen.emit() def emitAfterOpen(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ self.afterOpen.emit() def emitUndo(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ self.undo.emit() def emitRedo(self, *args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - """ self.redo.emit() @@ -1579,5 +1291,4 @@ def emitRedo(self, *args, **kwargs): def rootWindow(): - """ """ return None diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 03a8927c..3bac812d 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -53,17 +53,6 @@ # UNDO STACK INTEGRATION @contextmanager def undoContext(inst=None): - """ - - Parameters - ---------- - inst : - (Default value = None) - - Returns - ------- - - """ if inst is None: DCC.staticUndoOpen() else: @@ -78,33 +67,8 @@ def undoContext(inst=None): def undoable(f): - """ - - Parameters - ---------- - f : - - - Returns - ------- - - """ - @wraps(f) def stacker(*args, **kwargs): - """ - - Parameters - ---------- - *args : - - **kwargs : - - - Returns - ------- - - """ inst = None if args and isinstance(args[0], DCC): inst = args[0] @@ -115,21 +79,7 @@ def stacker(*args, **kwargs): def doDisconnect(targets, testCnxType=("double", "float")): - """Temporarily disconnect inputs from a list of nodes and plugs - - Parameters - ---------- - targets : - - testCnxType : - (Default value = ("double") - "float") : - - - Returns - ------- - - """ + """Temporarily disconnect inputs from a list of nodes and plugs""" if not isinstance(targets, (list, tuple)): targets = [targets] targets = list(set(targets)) @@ -154,17 +104,6 @@ def doDisconnect(targets, testCnxType=("double", "float")): def doReconnect(cnxs): - """ - - Parameters - ---------- - cnxs : - - - Returns - ------- - - """ for tdict in cnxs.values(): for s, d in tdict.items(): if not cmds.isConnected(s, d): @@ -173,21 +112,6 @@ def doReconnect(cnxs): @contextmanager def disconnected(targets, testCnxType=("double", "float")): - """ - - Parameters - ---------- - targets : - - testCnxType : - (Default value = ("double") - "float") : - - - Returns - ------- - - """ cnxs = doDisconnect(targets, testCnxType=testCnxType) try: yield cnxs @@ -204,8 +128,6 @@ def split_trailing_digits(s: str) -> tuple[str, str]: class DCC(object): - """ """ - program = "maya" def __init__(self, simplex, stack=None): @@ -239,15 +161,6 @@ def __init__(self, simplex, stack=None): def _checkAllShapeValidity(self, shapeNames): """Check shapes to see if they exist, and either gather the missing files, or Load the proper data onto the shapes - - Parameters - ---------- - shapeNames : - - - Returns - ------- - """ # Keep the set ordered, but make a set for quick checking missingNameSet = set() @@ -296,23 +209,6 @@ def _removeExtraShapeNodes(cls, tfm): cmds.delete(todel) def preLoad(self, simp, simpDict, create=True, pBar=None): - """ - - Parameters - ---------- - simp : - - simpDict : - - create : - (Default value = True) - pBar : - (Default value = None) - - Returns - ------- - - """ cmds.undoInfo(state=False) try: if pBar is not None: @@ -381,18 +277,6 @@ def preLoad(self, simp, simpDict, create=True, pBar=None): raise def postLoad(self, simp, preRet): - """ - - Parameters - ---------- - simp : - - preRet : - - Returns - ------- - - """ cmds.undoInfo(state=True) def checkForErrors(self, window): @@ -541,21 +425,6 @@ def loadNodes(self, simp, thing, create=True, pBar=None): Build any DCC objects that are missing if create=True Raises a runtime error if missing objects are found and create=False - - Parameters - ---------- - simp : - - thing : - - create : - (Default value = True) - pBar : - (Default value = None) - - Returns - ------- - """ self.name = simp.name self.mesh = thing @@ -593,34 +462,12 @@ def loadNodes(self, simp, thing, create=True, pBar=None): cmds.connectAttr(f"{self.poseNode}.message", f"{self.op}.poseMsg") def getShapeThing(self, shapeName): - """ - - Parameters - ---------- - shapeName : - - - Returns - ------- - - """ s = cmds.ls("{0}.{1}".format(self.shapeNode, shapeName)) if not s: return None return s[0] def getSliderThing(self, sliderName): - """ - - Parameters - ---------- - sliderName : - - - Returns - ------- - - """ things = cmds.ls("{0}.{1}".format(self.ctrl, sliderName)) if not things: return None @@ -636,19 +483,6 @@ def buildDummyMesh(cls, name: str): @classmethod @undoable def buildRestAbc(cls, abcMesh, name): - """ - - Parameters - ---------- - abcMesh : - - name : - - - Returns - ------- - - """ if not cmds.pluginInfo("AbcImport", query=True, loaded=True): cmds.loadPlugin("AbcImport") if not cmds.pluginInfo("AbcImport", query=True, loaded=True): @@ -680,21 +514,6 @@ def loadAbcPoses(self, abcMesh, js, pBar=None): @undoable def loadAbc(self, abcMesh, js, pBar=None): - """ - - Parameters - ---------- - abcMesh : - - js : - - pBar : - (Default value = None) - - Returns - ------- - - """ # UGH, I *REALLY* hate that this is faster # But if I want to be "pure" about it, I should just bite the bullet # and do the direct alembic manipulation in C++ @@ -783,19 +602,6 @@ def loadAbc(self, abcMesh, js, pBar=None): cmds.delete(importHead) def getAllShapeVertices(self, shapes, pBar=None): - """ - - Parameters - ---------- - shapes : - - pBar : - (Default value = None) - - Returns - ------- - - """ sl = om.MSelectionList() sl.add(self.mesh) thing = om.MDagPath() @@ -838,16 +644,6 @@ def getAllShapeVertices(self, shapes, pBar=None): shape.verts = out def getShapeVertices(self, shape): - """ - - Parameters - ---------- - shape : - - Returns - ------- - - """ with disconnected(self.shapeNode) as cnx: shapeCnx = cnx[self.shapeNode] for v in shapeCnx.values(): @@ -867,36 +663,12 @@ def getShapeVertices(self, shape): return out def pushAllShapeVertices(self, shapes, pBar=None): - """ - - Parameters - ---------- - shapes : - - pBar : - (Default value = None) - - Returns - ------- - - """ # take all the verts stored on the shapes # and push them back to the DCC for shape in shapes: self.pushShapeVertices(shape) def pushShapeVertices(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ # Push the vertices for a specific shape back to the DCC pass @@ -975,7 +747,6 @@ def getMeshTopology(cls, mesh, uvName=None): return verts, faces, counts, uvs, uvFaces def loadMeshTopology(self): - """ """ self._faces, self._counts, self._uvs = self.getAbcFaces(self.mesh) @classmethod @@ -993,7 +764,6 @@ def getNumpyShape(cls, mesh, world=False): ------- : np.array The point positions of the mesh - """ if np is None or mayaToNumpy is None: raise RuntimeError("Can't do numpy stuff if its not importable") @@ -1020,7 +790,6 @@ def _getMeshVertices(cls, mesh, world=False): @classmethod def _exportAbcVertices(cls, mesh, world=False): - """ """ if np is None or numpyToImath is None: vts = cls._getMeshVertices(mesh, world=world) vertices = V3fArray(vts.length()) @@ -1033,7 +802,6 @@ def _exportAbcVertices(cls, mesh, world=False): @classmethod def getAbcFaces(cls, mesh): - """ """ # Get the MDagPath from the name of the mesh sl = om.MSelectionList() sl.add(mesh) @@ -1097,7 +865,6 @@ def getAbcFaces(cls, mesh): def exportAbc( self, dccMesh, abcMesh, js, world=False, ensureCorrect=False, pBar=None ): - """ """ # export the data to alembic if dccMesh is None: dccMesh = self.mesh @@ -1150,7 +917,6 @@ def exportAbc( cmds.setAttr(self.shapeNode + ".envelope", envelope) def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): - """ """ shapeNames = js["shapes"] if js["encodingVersion"] > 1: shapeNames = [i["name"] for i in shapeNames] @@ -1259,7 +1025,6 @@ def exportMesh(self, mesh, path): # Revision tracking def getRevision(self): - """ """ try: return cmds.getAttr("{0}.{1}".format(self.op, "revision")) except ValueError: @@ -1268,7 +1033,6 @@ def getRevision(self): @undoable def incrementRevision(self): - """ """ value = self.getRevision() if value is None: return @@ -1279,33 +1043,11 @@ def incrementRevision(self): @undoable def setRevision(self, val): - """ - - Parameters - ---------- - val : - - - Returns - ------- - - """ cmds.setAttr("{0}.{1}".format(self.op, "revision"), val) # System level @undoable def renameSystem(self, name): - """ - - Parameters - ---------- - name : - - - Returns - ------- - - """ if ( self.mesh is None or self.ctrl is None @@ -1335,7 +1077,6 @@ def renameSystem(self, name): @undoable def deleteSystem(self): - """ """ cmds.delete(self.ctrl) cmds.delete(self.shapeNode) cmds.delete(self.op) @@ -1347,21 +1088,6 @@ def deleteSystem(self): # Shapes @undoable def createShape(self, shape, live=False, offset=10): - """ - - Parameters - ---------- - shape : - - live : - (Default value = False) - offset : - (Default value = 10) - - Returns - ------- - - """ with disconnected(self.shapeNode): try: attrs = cmds.listAttr("{0}.weight[*]".format(self.shapeNode)) @@ -1393,7 +1119,6 @@ def createShape(self, shape, live=False, offset=10): return thing def _firstAvailableIndex(self): - """ """ aliases = cmds.aliasAttr(self.shapeNode, query=True) idxs = set() if not aliases: @@ -1411,17 +1136,6 @@ def _firstAvailableIndex(self): return len(idxs) + 1 def getShapeIndex(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ aName = cmds.attributeName(shape.thing) aliases = cmds.aliasAttr(self.shapeNode, query=True) idx = aliases.index(aName) @@ -1435,19 +1149,6 @@ def getShapeIndex(self, shape): def extractWithDeltaShape(self, shape, live=True, offset=10.0): """Make a mesh representing a shape. Can be live or not. Also, make a shapenode that is the delta of the change being made - - Parameters - ---------- - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - """ with disconnected(self.shapeNode) as cnx: shapeCnx = cnx[self.shapeNode] @@ -1495,23 +1196,6 @@ def extractWithDeltaShape(self, shape, live=True, offset=10.0): def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0): """Extract a shape with a live partial delta added in. Useful for updating progressive shapes - - Parameters - ---------- - shape : - - delta : - - value : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - """ with disconnected(self.shapeNode): for attr in cmds.listAttr("{0}.weight[*]".format(self.shapeNode)): @@ -1571,19 +1255,6 @@ def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0 def extractShape(self, shape, live=True, offset=10.0): """Make a mesh representing a shape. Can be live or not. Can also store its starting shape and delta data - - Parameters - ---------- - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - """ with disconnected(self.shapeNode): for attr in cmds.listAttr("{0}.weight[*]".format(self.shapeNode)): @@ -1609,21 +1280,6 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): mesh=someMesh, delete=False There is a possibility of a "make live" button: live=True, delete=False - - Parameters - ---------- - shape : - - mesh : - (Default value = None) - live : - (Default value = False) - delete : - (Default value = False) - - Returns - ------- - """ if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) @@ -1657,32 +1313,11 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): @undoable def extractPosedShape(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ pass @undoable def zeroShape(self, shape): - """Set the shape to be completely zeroed - - Parameters - ---------- - shape : - - - Returns - ------- - - """ + """Set the shape to be completely zeroed """ index = self.getShapeIndex(shape) tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) shapeInput = "{0}.inputTargetItem[6000]".format(tgn) @@ -1695,17 +1330,7 @@ def zeroShape(self, shape): @undoable def deleteShape(self, toDelShape): - """Remove a shape from the system - - Parameters - ---------- - toDelShape : - - - Returns - ------- - - """ + """Remove a shape from the system """ index = self.getShapeIndex(toDelShape) tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) cmds.removeMultiInstance(toDelShape.thing, b=True) @@ -1714,7 +1339,6 @@ def deleteShape(self, toDelShape): self._rebuildShapeConnections() def _rebuildShapeConnections(self): - """ """ # Rebuild the shape connections in the proper order cnxs = ( cmds.listConnections( @@ -1733,148 +1357,40 @@ def _rebuildShapeConnections(self): @undoable def forceRebuildShapeConnections(self): - """ """ self._rebuildShapeConnections() @undoable def renameShape(self, shape, name): - """Change the name of the shape - - Parameters - ---------- - shape : - - name : - - - Returns - ------- - - """ + """Change the name of the shape """ cmds.aliasAttr(name, shape.thing) shape.thing = "{0}.{1}".format(self.shapeNode, name) @undoable def convertShapeToCorrective(self, shape): - """ - - Parameters - ---------- - shape : - - - Returns - ------- - - """ pass # Falloffs def createFalloff(self, name): - """ - - Parameters - ---------- - name : - - - Returns - ------- - - """ pass # for eventual live splits def duplicateFalloff(self, falloff, newFalloff, newName): - """ - - Parameters - ---------- - falloff : - - newFalloff : - - newName : - - - Returns - ------- - - """ pass # for eventual live splits def deleteFalloff(self, falloff): - """ - - Parameters - ---------- - falloff : - - - Returns - ------- - - """ pass # for eventual live splits def setFalloffData( self, falloff, splitType, axis, minVal, minHandle, maxHandle, maxVal, mapName ): - """ - - Parameters - ---------- - falloff : - - splitType : - - axis : - - minVal : - - minHandle : - - maxHandle : - - maxVal : - - mapName : - - - Returns - ------- - - """ pass # for eventual live splits def getFalloffThing(self, falloff): - """ - - Parameters - ---------- - falloff : - - - Returns - ------- - - """ shape = cmds.listRelatives(self.mesh, shapes=True)[0] return shape + "." + falloff.name # Sliders @undoable def createSlider(self, slider): - """ - - Parameters - ---------- - slider : - - - Returns - ------- - - """ index = slider.simplex.sliders.index(slider) cmds.addAttr( self.ctrl, @@ -1890,19 +1406,7 @@ def createSlider(self, slider): @undoable def renameSlider(self, slider, name): - """Set the name of a slider - - Parameters - ---------- - slider : - - name : - - - Returns - ------- - - """ + """Set the name of a slider """ vals = [v.value for v in slider.prog.pairs] cnx = cmds.listConnections( slider.thing, plugs=True, source=False, destination=True @@ -1923,17 +1427,7 @@ def renameSlider(self, slider, name): @undoable def setSliderRange(self, slider): - """Set the range of a slider - - Parameters - ---------- - slider : - - - Returns - ------- - - """ + """Set the range of a slider """ vals = [v.value for v in slider.prog.pairs] attrName = "{0}.{1}".format(self.ctrl, slider.name) cmds.addAttr( @@ -1945,17 +1439,6 @@ def setSliderRange(self, slider): @undoable def deleteSlider(self, toDelSlider): - """ - - Parameters - ---------- - toDelSlider : - - - Returns - ------- - - """ cmds.deleteAttr(toDelSlider.thing) # Rebuild the slider connections in the proper order @@ -1972,53 +1455,15 @@ def deleteSlider(self, toDelSlider): @undoable def addProgFalloff(self, prog, falloff): - """ - - Parameters - ---------- - prog : - - falloff : - - - Returns - ------- - - """ pass # for eventual live splits @undoable def removeProgFalloff(self, prog, falloff): - """ - - Parameters - ---------- - prog : - - falloff : - - - Returns - ------- - - """ pass # for eventual live splits @undoable def setSlidersWeights(self, sliders, weights): - """Set the weight of a slider. This does not change the definition - - Parameters - ---------- - sliders : - - weights : - - - Returns - ------- - - """ + """Set the weight of a slider. This does not change the definition """ for slider, weight in zip(sliders, weights): try: cmds.setAttr(slider.thing, weight) @@ -2028,19 +1473,6 @@ def setSlidersWeights(self, sliders, weights): @undoable def setSliderWeight(self, slider, weight): - """ - - Parameters - ---------- - slider : - - weight : - - - Returns - ------- - - """ try: cmds.setAttr(slider.thing, weight) except RuntimeError: @@ -2049,17 +1481,6 @@ def setSliderWeight(self, slider, weight): @undoable def updateSlidersRange(self, sliders): - """ - - Parameters - ---------- - sliders : - - - Returns - ------- - - """ for slider in sliders: vals = [v.value for v in slider.prog.pairs] cmds.addAttr( @@ -2070,19 +1491,6 @@ def updateSlidersRange(self, sliders): ) def _doesDeltaExist(self, combo, target): - """ - - Parameters - ---------- - combo : - - target : - - - Returns - ------- - - """ dshape = "{0}_DeltaShape".format(combo.name) if not cmds.ls(dshape): return None @@ -2100,19 +1508,6 @@ def _doesDeltaExist(self, combo, target): return par + "|" + dshape def _clearShapes(self, item, doOrig=False): - """ - - Parameters - ---------- - item : - - doOrig : - (Default value = False) - - Returns - ------- - - """ aname = cmds.ls(item, long=1)[0] shapes = cmds.ls(cmds.listRelatives(item, shapes=1), long=1) baseName = aname.split("|")[-1] @@ -2144,7 +1539,6 @@ def _clearShapes(self, item, doOrig=False): @undoable def forceRebuildSliderConnections(self): - """ """ self._rebuildSliderConnections() def _rebuildSliderConnections(self): @@ -2176,21 +1570,6 @@ def _reparentDeltaShapes(self, par, nodeDict, bsNode, toDelete=None): nodeDict: A {simpleName: node} dictionary. bsNode: The blendshape node. toDelete: Any extra nodes to delte after all the node twiddling - - Parameters - ---------- - par : - - nodeDict : - - bsNode : - - toDelete : - (Default value = None) - - Returns - ------- - """ # Get the shapes and origs shapeDict = {} @@ -2241,19 +1620,6 @@ def _reparentDeltaShapes(self, par, nodeDict, bsNode, toDelete=None): def _createTravDelta(self, trav, target, tVal, doReparent=True): """Part of the traversal extraction process. Very similar to the combo extraction - - Parameters - ---------- - trav : - - target : - - tVal : - - - Returns - ------- - """ exists = self._doesDeltaExist(trav, target) if exists is not None: @@ -2316,21 +1682,7 @@ def _createTravDelta(self, trav, target, tVal, doReparent=True): @undoable def extractTraversalShape(self, trav, shape, live=True, offset=10.0): - """Extract a shape from a Traversal progression - - Parameters - ---------- - trav : - shape : - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ + """Extract a shape from a Traversal progression """ floatShapes = self.simplex.getFloatingShapes() floatShapes = [i.thing for i in floatShapes] @@ -2374,25 +1726,7 @@ def extractTraversalShape(self, trav, shape, live=True, offset=10.0): @undoable def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False): - """Connect a shape into a Traversal progression - - Parameters - ---------- - trav : - - shape : - - mesh : - (Default value = None) - live : - (Default value = True) - delete : - (Default value = False) - - Returns - ------- - - """ + """Connect a shape into a Traversal progression """ if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) mesh = "{0}_Extract".format(attrName) @@ -2423,19 +1757,6 @@ def _createComboDelta(self, combo, target, tVal, doReparent=True): So we must create a setup to take the final sculpted shape, and subtract the any direct slider deformations to get the actual "combo shape" as a delta It is this delta shape that is then plugged into the system - - Parameters - ---------- - combo : - - target : - - tVal : - - - Returns - ------- - """ exists = self._doesDeltaExist(combo, target) if exists is not None: @@ -2493,23 +1814,7 @@ def _createComboDelta(self, combo, target, tVal, doReparent=True): @undoable def extractComboShape(self, combo, shape, live=True, offset=10.0): - """Extract a shape from a combo progression - - Parameters - ---------- - combo : - - shape : - - live : - (Default value = True) - offset : - (Default value = 10.0) - - Returns - ------- - - """ + """Extract a shape from a combo progression """ floatShapes = self.simplex.getFloatingShapes() floatShapes = [i.thing for i in floatShapes] @@ -2542,25 +1847,7 @@ def extractComboShape(self, combo, shape, live=True, offset=10.0): @undoable def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): - """Connect a shape into a combo progression - - Parameters - ---------- - combo : - - shape : - - mesh : - (Default value = None) - live : - (Default value = True) - delete : - (Default value = False) - - Returns - ------- - - """ + """Connect a shape into a combo progression """ if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) mesh = "{0}_Extract".format(attrName) @@ -2586,17 +1873,6 @@ def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): @classmethod def setDisabled(cls, op): - """ - - Parameters - ---------- - op : - - - Returns - ------- - - """ bss = list(set(cmds.listConnections(op, type="blendShape"))) helpers = [] for bs in bss: @@ -2609,35 +1885,12 @@ def setDisabled(cls, op): @classmethod def reEnable(cls, helpers): - """ - - Parameters - ---------- - helpers : - - - Returns - ------- - - """ for prop, val in helpers: cmds.setAttr(prop, val) @undoable def renameCombo(self, combo, name): - """Set the name of a Combo - - Parameters - ---------- - combo : - - name : - - - Returns - ------- - - """ + """Set the name of a Combo """ pass # Data Access @@ -2648,36 +1901,10 @@ def getSimplexOperators(cls): @classmethod def getSimplexOperatorsByName(cls, name): - """ - - Parameters - ---------- - name : - - - Returns - ------- - type - - - """ return cmds.ls(name, type="simplex_maya") @classmethod def getSimplexOperatorsOnObject(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - type - - - """ ops = cmds.ls(type="simplex_maya") out = [] for op in ops: @@ -2696,38 +1923,10 @@ def getSimplexOperatorsOnObject(cls, thing): @classmethod def getSimplexString(cls, op): - """ - - Parameters - ---------- - op : - - - Returns - ------- - type - - - """ return cmds.getAttr(op + ".definition") @classmethod def getSimplexStringOnThing(cls, thing, systemName): - """ - - Parameters - ---------- - thing : - - systemName : - - - Returns - ------- - type - - - """ ops = DCC.getSimplexOperatorsOnObject(thing) for op in ops: js = DCC.getSimplexString(op) @@ -2738,36 +1937,11 @@ def getSimplexStringOnThing(cls, thing, systemName): @classmethod def setSimplexString(cls, op, val): - """ - - Parameters - ---------- - op : - - val : - - - Returns - ------- - type - - - """ return cmds.setAttr(op + ".definition", val, type="string") @classmethod def selectObject(cls, thing): - """Select an object in the DCC - - Parameters - ---------- - thing : - - - Returns - ------- - - """ + """Select an object in the DCC """ cmds.select([thing]) def selectCtrl(self): @@ -2777,19 +1951,6 @@ def selectCtrl(self): @classmethod def getObjectByName(cls, name): - """ - - Parameters - ---------- - name : - - - Returns - ------- - type - - - """ objs = cmds.ls(name) if not objs: return None @@ -2799,131 +1960,48 @@ def getObjectByName(cls, name): @classmethod def getObjectName(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - type - - - """ return thing @classmethod def staticUndoOpen(cls): - """ """ cmds.undoInfo(chunkName="SimplexOperation", openChunk=True) @classmethod def staticUndoClose(cls): - """ """ cmds.undoInfo(closeChunk=True) def undoOpen(self): - """ """ if self.undoDepth == 0: self.staticUndoOpen() self.undoDepth += 1 def undoClose(self): - """ """ self.undoDepth -= 1 if self.undoDepth == 0: self.staticUndoClose() @classmethod def getPersistentFalloff(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ return cls.getObjectName(thing) @classmethod def loadPersistentFalloff(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ return cls.getObjectByName(thing) @classmethod def getPersistentShape(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ return cls.getObjectName(thing) @classmethod def loadPersistentShape(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ return cls.getObjectByName(thing) @classmethod def getPersistentSlider(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ return cls.getObjectName(thing) @classmethod def loadPersistentSlider(cls, thing): - """ - - Parameters - ---------- - thing : - - - Returns - ------- - - """ return cls.getObjectByName(thing) @classmethod @@ -2934,17 +2012,6 @@ def getSelectedObjects(cls): @undoable def importObj(self, path): - """ - - Parameters - ---------- - path : - - - Returns - ------- - - """ current = set(cmds.ls(transforms=True)) cmds.file(path, i=True, type="OBJ", ignoreVersion=True) new = set(cmds.ls(transforms=True)) From 31ad97645a7bc7e008ab15fa6197732c91df5647 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 18 Aug 2026 12:49:01 -0700 Subject: [PATCH 29/47] Numpy is a hard requirement --- .../simplexui/commands/applyCorrectives.py | 6 +-- .../simplexui/commands/correctiveInterface.py | 7 +-- .../simplexui/commands/expandedExport.py | 6 +-- .../commands/mayaCorrectiveInterface.py | 5 +- src/python/simplexui/commands/rigidAlign.py | 5 +- src/python/simplexui/commands/unsubdivide.py | 5 +- .../simplexui/interface/dummyInterface.py | 5 +- .../simplexui/interface/mayaInterface.py | 54 +++++++++---------- .../menu/genericPlugins/exportSplit.py | 5 +- .../menu/genericPlugins/simplexUvTransfer.py | 5 +- .../simplexui/menu/mayaPlugins/importObjs.py | 5 +- 11 files changed, 35 insertions(+), 73 deletions(-) diff --git a/src/python/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py index 383fb3a5..5e5af325 100644 --- a/src/python/simplexui/commands/applyCorrectives.py +++ b/src/python/simplexui/commands/applyCorrectives.py @@ -16,7 +16,7 @@ # along with Simplex. If not, see . import itertools import os - +import numpy as np from pysimplex import PySimplex from ..items import Combo, Simplex, Slider @@ -29,10 +29,6 @@ readSmpx, ) -try: - import numpy as np -except ImportError: - pass def invertAll(matrixArray): diff --git a/src/python/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py index b807ca7d..0107fdd1 100644 --- a/src/python/simplexui/commands/correctiveInterface.py +++ b/src/python/simplexui/commands/correctiveInterface.py @@ -16,12 +16,7 @@ # along with Simplex. If not, see . from Qt.QtWidgets import QApplication - -try: - import numpy as np -except ImportError: - pass - +import numpy as np from .mayaCorrectiveInterface import getShiftValues, resetPose, setPose dcc = "maya" diff --git a/src/python/simplexui/commands/expandedExport.py b/src/python/simplexui/commands/expandedExport.py index bbc0f48f..323847fc 100644 --- a/src/python/simplexui/commands/expandedExport.py +++ b/src/python/simplexui/commands/expandedExport.py @@ -20,11 +20,7 @@ from ..interface.mayaInterface import DCC, disconnected from ..items import Combo, Slider, Traversal, Simplex from .alembicCommon import buildSmpx - -try: - import numpy as np -except ImportError: - np = None +import numpy as np def _setSliders(ctrl, val, svs): diff --git a/src/python/simplexui/commands/mayaCorrectiveInterface.py b/src/python/simplexui/commands/mayaCorrectiveInterface.py index f928d361..11186574 100644 --- a/src/python/simplexui/commands/mayaCorrectiveInterface.py +++ b/src/python/simplexui/commands/mayaCorrectiveInterface.py @@ -22,10 +22,7 @@ from maya import OpenMaya as om from maya import cmds -try: - import numpy as np -except ImportError: - pass +import numpy as np def setPose(pvp, multiplier): diff --git a/src/python/simplexui/commands/rigidAlign.py b/src/python/simplexui/commands/rigidAlign.py index 4ae4cdb8..f3ec217b 100644 --- a/src/python/simplexui/commands/rigidAlign.py +++ b/src/python/simplexui/commands/rigidAlign.py @@ -15,10 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . -try: - import numpy as np -except ImportError: - pass +import numpy as np def rigidAlign(P, Q, iters=10): diff --git a/src/python/simplexui/commands/unsubdivide.py b/src/python/simplexui/commands/unsubdivide.py index e9d37cc0..25e2b829 100644 --- a/src/python/simplexui/commands/unsubdivide.py +++ b/src/python/simplexui/commands/unsubdivide.py @@ -20,10 +20,7 @@ from Qt.QtWidgets import QApplication from .alembicCommon import buildSmpx, pbPrint, readSmpx -try: - import numpy as np -except ImportError: - np = None +import numpy as np def mergeCycles(groups): diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index 76dcecc6..3fa933c5 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -23,10 +23,7 @@ from Qt import QtCore from Qt.QtCore import Signal -try: - import numpy as np -except ImportError: - np = None +import numpy as np from alembic.AbcGeom import OPolyMeshSchemaSample diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 3bac812d..16f5bd62 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -20,6 +20,7 @@ from contextlib import contextmanager from functools import wraps from typing import TYPE_CHECKING +import numpy as np import maya.cmds as cmds import maya.OpenMaya as om @@ -37,18 +38,16 @@ QSplashScreen, ) -if TYPE_CHECKING: - from ..items.simplex import Simplex - try: - import numpy as np from ..commands.numpytoimath import numpyToImath from ..commands.mayatonumpy import mayaToNumpy except ImportError: - np = None numpyToImath = None mayaToNumpy = None +if TYPE_CHECKING: + from ..items.simplex import Simplex + # UNDO STACK INTEGRATION @contextmanager @@ -146,7 +145,6 @@ def __init__(self, simplex, stack=None): self.sliderMul: float = self.simplex.sliderMul # def __deepcopy__(self, memo): - # ''' # I don't actually need to define this here because I know that # all of the maya "objects" store here are just strings # But if they *weren't* (like in XSI) I would need to skip @@ -155,8 +153,6 @@ def __init__(self, simplex, stack=None): # And if we did skip things, I would also need to store a # persistent accessor to use in case we get back to here # through an undo - # ''' - # pass def _checkAllShapeValidity(self, shapeNames): """Check shapes to see if they exist, and either gather the missing files, or @@ -1148,7 +1144,7 @@ def getShapeIndex(self, shape): @undoable def extractWithDeltaShape(self, shape, live=True, offset=10.0): """Make a mesh representing a shape. Can be live or not. - Also, make a shapenode that is the delta of the change being made + Also, make a shapenode that is the delta of the change being made """ with disconnected(self.shapeNode) as cnx: shapeCnx = cnx[self.shapeNode] @@ -1195,7 +1191,7 @@ def extractWithDeltaShape(self, shape, live=True, offset=10.0): @undoable def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0): """Extract a shape with a live partial delta added in. - Useful for updating progressive shapes + Useful for updating progressive shapes """ with disconnected(self.shapeNode): for attr in cmds.listAttr("{0}.weight[*]".format(self.shapeNode)): @@ -1254,7 +1250,7 @@ def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0 @undoable def extractShape(self, shape, live=True, offset=10.0): """Make a mesh representing a shape. Can be live or not. - Can also store its starting shape and delta data + Can also store its starting shape and delta data """ with disconnected(self.shapeNode): for attr in cmds.listAttr("{0}.weight[*]".format(self.shapeNode)): @@ -1274,12 +1270,12 @@ def extractShape(self, shape, live=True, offset=10.0): @undoable def connectShape(self, shape, mesh=None, live=False, delete=False): """Force a shape to match a mesh - The "connect shape" button is: - mesh=None, delete=True - The "match shape" button is: - mesh=someMesh, delete=False - There is a possibility of a "make live" button: - live=True, delete=False + The "connect shape" button is: + mesh=None, delete=True + The "match shape" button is: + mesh=someMesh, delete=False + There is a possibility of a "make live" button: + live=True, delete=False """ if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) @@ -1317,7 +1313,7 @@ def extractPosedShape(self, shape): @undoable def zeroShape(self, shape): - """Set the shape to be completely zeroed """ + """Set the shape to be completely zeroed""" index = self.getShapeIndex(shape) tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) shapeInput = "{0}.inputTargetItem[6000]".format(tgn) @@ -1330,7 +1326,7 @@ def zeroShape(self, shape): @undoable def deleteShape(self, toDelShape): - """Remove a shape from the system """ + """Remove a shape from the system""" index = self.getShapeIndex(toDelShape) tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) cmds.removeMultiInstance(toDelShape.thing, b=True) @@ -1361,7 +1357,7 @@ def forceRebuildShapeConnections(self): @undoable def renameShape(self, shape, name): - """Change the name of the shape """ + """Change the name of the shape""" cmds.aliasAttr(name, shape.thing) shape.thing = "{0}.{1}".format(self.shapeNode, name) @@ -1406,7 +1402,7 @@ def createSlider(self, slider): @undoable def renameSlider(self, slider, name): - """Set the name of a slider """ + """Set the name of a slider""" vals = [v.value for v in slider.prog.pairs] cnx = cmds.listConnections( slider.thing, plugs=True, source=False, destination=True @@ -1427,7 +1423,7 @@ def renameSlider(self, slider, name): @undoable def setSliderRange(self, slider): - """Set the range of a slider """ + """Set the range of a slider""" vals = [v.value for v in slider.prog.pairs] attrName = "{0}.{1}".format(self.ctrl, slider.name) cmds.addAttr( @@ -1463,7 +1459,7 @@ def removeProgFalloff(self, prog, falloff): @undoable def setSlidersWeights(self, sliders, weights): - """Set the weight of a slider. This does not change the definition """ + """Set the weight of a slider. This does not change the definition""" for slider, weight in zip(sliders, weights): try: cmds.setAttr(slider.thing, weight) @@ -1682,7 +1678,7 @@ def _createTravDelta(self, trav, target, tVal, doReparent=True): @undoable def extractTraversalShape(self, trav, shape, live=True, offset=10.0): - """Extract a shape from a Traversal progression """ + """Extract a shape from a Traversal progression""" floatShapes = self.simplex.getFloatingShapes() floatShapes = [i.thing for i in floatShapes] @@ -1726,7 +1722,7 @@ def extractTraversalShape(self, trav, shape, live=True, offset=10.0): @undoable def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False): - """Connect a shape into a Traversal progression """ + """Connect a shape into a Traversal progression""" if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) mesh = "{0}_Extract".format(attrName) @@ -1814,7 +1810,7 @@ def _createComboDelta(self, combo, target, tVal, doReparent=True): @undoable def extractComboShape(self, combo, shape, live=True, offset=10.0): - """Extract a shape from a combo progression """ + """Extract a shape from a combo progression""" floatShapes = self.simplex.getFloatingShapes() floatShapes = [i.thing for i in floatShapes] @@ -1847,7 +1843,7 @@ def extractComboShape(self, combo, shape, live=True, offset=10.0): @undoable def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): - """Connect a shape into a combo progression """ + """Connect a shape into a combo progression""" if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) mesh = "{0}_Extract".format(attrName) @@ -1890,7 +1886,7 @@ def reEnable(cls, helpers): @undoable def renameCombo(self, combo, name): - """Set the name of a Combo """ + """Set the name of a Combo""" pass # Data Access @@ -1941,7 +1937,7 @@ def setSimplexString(cls, op, val): @classmethod def selectObject(cls, thing): - """Select an object in the DCC """ + """Select an object in the DCC""" cmds.select([thing]) def selectCtrl(self): diff --git a/src/python/simplexui/menu/genericPlugins/exportSplit.py b/src/python/simplexui/menu/genericPlugins/exportSplit.py index bd562200..14b079c3 100644 --- a/src/python/simplexui/menu/genericPlugins/exportSplit.py +++ b/src/python/simplexui/menu/genericPlugins/exportSplit.py @@ -21,10 +21,7 @@ from Qt.QtWidgets import QMessageBox, QProgressDialog from Qt.QtGui import QAction -try: - import numpy as np -except ImportError: - np = None +import numpy as np def registerTool(window, menu): diff --git a/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py b/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py index 7592d8ea..b8344a63 100644 --- a/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py +++ b/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py @@ -21,10 +21,7 @@ from ...commands.mesh import Mesh from ...commands.uvTransfer import applyTransfer, getVertCorrelation -try: - import numpy as np -except ImportError: - np = None +import numpy as np def simplexUvTransfer( diff --git a/src/python/simplexui/menu/mayaPlugins/importObjs.py b/src/python/simplexui/menu/mayaPlugins/importObjs.py index 05e00f65..4d5eae71 100644 --- a/src/python/simplexui/menu/mayaPlugins/importObjs.py +++ b/src/python/simplexui/menu/mayaPlugins/importObjs.py @@ -1,4 +1,5 @@ import os +import numpy as np from functools import partial import maya.cmds as cmds from ...items import Combo, Slider, Traversal @@ -10,10 +11,6 @@ ) from Qt.QtGui import QAction -try: - import numpy as np -except ImportError: - np = None try: from tools.MeshCrawler.commands import setAllVerts From 6865db01952196564d2ec98109e9c18279c9d8c7 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Wed, 2 Sep 2026 17:59:04 -0700 Subject: [PATCH 30/47] Fix the combo check dialog and a couple order of operation issues --- src/python/simplexui/comboCheckDialog.py | 75 +++++++++++++++-------- src/python/simplexui/items/combo.py | 8 +-- src/python/simplexui/items/falloff.py | 14 ++--- src/python/simplexui/items/progression.py | 13 ++-- src/python/simplexui/items/slider.py | 5 +- src/python/simplexui/items/traversal.py | 14 ++--- 6 files changed, 70 insertions(+), 59 deletions(-) diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index b63e6db2..8cdf0b76 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from itertools import combinations, product @@ -38,6 +39,13 @@ from .utils import getUiFile +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from .items import Simplex + from .simplexDialog import SimplexDialog + from Qt.QtGui import QCloseEvent + class TooManyPossibilitiesError(Exception): """Error raised when there are too many possibilities @@ -48,8 +56,13 @@ class TooManyPossibilitiesError(Exception): def buildPossibleCombos( - simplex, sliders, minDepth, maxDepth, lockDict=None, maxPoss=100 -): + simplex: Simplex, + sliders: list[Slider], + minDepth: int, + maxDepth: int, + lockDict: Optional[dict[Slider, tuple[float, ...]]] = None, + maxPoss: int = 100, +) -> tuple[bool, list[tuple[list[tuple[Slider, float]], Combo]]]: """Build a list of possible combos Parameters @@ -116,7 +129,9 @@ def buildPossibleCombos( class ComboCheckItem(QListWidgetItem): - def __init__(self, pairs, combo, *args, **kwargs): + def __init__( + self, pairs: list[tuple[Slider, float]], combo: Combo, *args, **kwargs + ): super().__init__(*args, **kwargs) self.pairs = pairs self.combo = combo @@ -170,7 +185,13 @@ class ComboCheckDialog(QDialog): uiCancelBTN: QPushButton uiCreateSelectedBTN: QPushButton - def __init__(self, sliders, values=None, mode="create", parent=None): + def __init__( + self, + sliders: list[Slider], + values: Optional[dict[Slider, list[float]]] = None, + mode: str = "create", + parent: Optional[SimplexDialog] = None, + ): if parent is None: raise ValueError("Parent must not be None") super().__init__(parent) @@ -216,7 +237,7 @@ def __init__(self, sliders, values=None, mode="create", parent=None): else: self._populate() - def dragTick(self, ticks, mul): + def dragTick(self, ticks: int, mul: float): """Deal with the ticks coming from the drag handler Parameters @@ -225,10 +246,6 @@ def dragTick(self, ticks, mul): The number of ticks since the last update mul : float The multiplier value from the drag handler - - Returns - ------- - """ items = self.uiEditTREE.selectedItems() for item in items: @@ -240,49 +257,52 @@ def dragTick(self, ticks, mul): item.setData(3, Qt.ItemDataRole.EditRole, val) self.uiEditTREE.viewport().update() - def setSliders(self, val): + def setSliders(self, sliders: list[Slider]): """Set the sliders displayed in this UI Parameters ---------- - val : [Slider, ...] + sliders : [Slider, ...] The sliders to be displayed - - Returns - ------- - """ self.uiEditTREE.clear() - dvs = [None, -1.0, 1.0, 0.5] + defaultVals = [-1.0, 1.0, 0.5] roles = [ Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole, Qt.ItemDataRole.EditRole, ] - val = val or [] - for slider in val: - item = QTreeWidgetItem(self.uiEditTREE, [slider.name]) + + sliders = sliders or [] + for slider in sliders: + #item = QTreeWidgetItem(self.uiEditTREE, [slider.name]) + item = QTreeWidgetItem([slider.name]) item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) - vvv = self.valueDict.get(slider, [-1.0, 1.0]) - mvs = [i for i in vvv if abs(i) != 1.0] - mvs = mvs[0] if mvs else 0.5 + + valRange = self.valueDict.get(slider, [-1.0, 1.0]) + midVals = [i for i in valRange if abs(i) != 1.0] + slidef = defaultVals[:] + slidef[-1] = midVals[0] if midVals else 0.5 item.setData(0, Qt.ItemDataRole.UserRole, slider) for col in range(1, 4): - val = mvs if col == 3 else dvs[col] - item.setData(col, roles[col], val) + slival = slidef[col - 1] + item.setData(col, roles[col], slival) rng = slider.prog.getRange() - if val in rng or col == 3: + if slival in rng or col == 3: chk = ( - Qt.CheckState.Checked if val in vvv else Qt.CheckState.Unchecked + Qt.CheckState.Checked + if slival in valRange + else Qt.CheckState.Unchecked ) item.setCheckState(col, chk) + self.uiEditTREE.addTopLevelItem(item) for col in reversed(list(range(4))): self.uiEditTREE.resizeColumnToContents(col) - def closeEvent(self, event): + def closeEvent(self, event: QCloseEvent): """Override the Qt close event""" if self.isEnabled(): self.parUI.uiSliderTREE.selectionModel().selectionChanged.disconnect( @@ -323,6 +343,7 @@ def _populate(self): Qt.ItemDataRole.UserRole, Qt.ItemDataRole.EditRole, ] + for row in range(root.childCount()): item = root.child(row) slider = item.data(0, Qt.ItemDataRole.UserRole) diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index b87b0e66..9c2f7c86 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -129,19 +129,19 @@ def __init__( self._name: str = name self.pairs = pairs self.prog = prog + self.prog.controller = self self._solveType: Optional[str] = solveType self._buildIdx = None self.expanded = {} self._enabled = True self._freezeThing = None + for p in self.pairs: + p.combo = self + self.simplex.combos.append(self) with self.insertItemManager(group): self.group: Group = group - for p in self.pairs: - p.combo = self - self.prog.controller = self self.group.items.append(self) - self.simplex.combos.append(self) @property def enabled(self) -> bool: diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index f56afbb1..3455fa36 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -397,13 +397,12 @@ def __init__( minHandle: float, minVal: float, ): + self._maxVal = maxVal + self._maxHandle = maxHandle + self._minHandle = minHandle + self._minVal = minVal + self._bezier = None super().__init__(name, simplex, axis) - with self.stack.store(self): - self._maxVal = maxVal - self._maxHandle = maxHandle - self._minHandle = minHandle - self._minVal = minVal - self._bezier = None @classmethod def createPlanar( @@ -664,9 +663,8 @@ class MapFalloff(Falloff): splitType: str = "map" def __init__(self, name: str, simplex: Simplex, axis: str, mapName: str): + self._mapName = mapName super().__init__(name, simplex, axis) - with self.stack.store(self): - self._mapName = mapName @classmethod def createMap( diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index 3d754eb3..b7b1fe57 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -100,13 +100,12 @@ def treeRow(self) -> int: return 0 return self.prog.pairs.index(self) - def treeParent(self) -> TreeItem: + def treeParent(self) -> Optional[TreeItem]: from .slider import Slider if self.prog is None: - raise ValueError( - "Somehow you're trying to display a ProgPair without a Prog" - ) + return None + par = self.prog if isinstance(par.controller, Slider): par = par.controller @@ -604,11 +603,7 @@ def treeRow(self) -> int: return len(self.controller.pairs) return 0 - def treeParent(self) -> TreeItem: - if self.controller is None: - raise ValueError( - "Somehow you're trying to display a Progression without a Controller" - ) + def treeParent(self) -> Optional[TreeItem]: return self.controller def treeChildCount(self): diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index 8c6c1667..114f5513 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -66,13 +66,14 @@ def __init__( raise ValueError("Cannot add this slider to a combo group") super().__init__(simplex) + with self.stack.store(self): self._name: str = name self._thing: Optional[DCCObject] = None self._thingRepr: Optional[str] = None self.prog: Progression = prog - self.split: bool = False self.prog.controller = self + self.split: bool = False self._buildIdx: Optional[int] = None self._value: float = 0.0 self._enabled: bool = True @@ -80,12 +81,12 @@ def __init__( mn, mx = self.prog.getRange() self.minValue: float = mn self.maxValue: float = mx + self.simplex.sliders.append(self) with self.insertItemManager(group): self.group: Group = group self.group.items.append(self) - self.simplex.sliders.append(self) newThing = self.DCC.getSliderThing(self._name) if newThing is None: diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index 0a01bd90..ae93915f 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -199,11 +199,7 @@ def treeRow(self) -> int: return 0 return 1 - def treeParent(self) -> TreeItem: - if self.traversal is None: - raise ValueError( - "Somehow you're trying to show a TravPoint without a Traversal" - ) + def treeParent(self) -> Optional[TreeItem]: return self.traversal def treeChildCount(self) -> int: @@ -276,16 +272,16 @@ def __init__( self.startPoint: TravPoint = startPoint self.endPoint: TravPoint = endPoint self.prog: Progression = prog + self.prog.controller = self self._buildIdx: Optional[int] = None self._enabled: bool = True + self.startPoint.traversal = self + self.endPoint.traversal = self + self.simplex.traversals.append(self) with self.insertItemManager(group): self.group: Group = group - self.startPoint.traversal = self - self.endPoint.traversal = self - self.prog.controller = self self.group.items.append(self) - self.simplex.traversals.append(self) @classmethod def createTraversal( From e303d1de2b462edb6e032b520b57a165a9f1256a Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 3 Sep 2026 12:17:46 -0700 Subject: [PATCH 31/47] Fix the combo checkstate --- src/python/simplexui/comboCheckDialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index 8cdf0b76..05832c4d 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -86,7 +86,6 @@ def buildPossibleCombos( True if the maximum number of possibilities was exceeded [([(Slider, float), ...], Combo), ...] Grouped slider/value pairs to existing (or None) Combos - """ # Get the range values for each slider allRanges = {} @@ -125,6 +124,7 @@ def buildPossibleCombos( for p in poss: truePairs = [(sliderDict[n], v) for n, v in p] toAdd.append((truePairs, onlys.get(p))) + return tooMany, toAdd @@ -276,7 +276,7 @@ def setSliders(self, sliders: list[Slider]): sliders = sliders or [] for slider in sliders: - #item = QTreeWidgetItem(self.uiEditTREE, [slider.name]) + # item = QTreeWidgetItem(self.uiEditTREE, [slider.name]) item = QTreeWidgetItem([slider.name]) item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) @@ -352,7 +352,7 @@ def _populate(self): lv = [ item.data(col, roles[col]) for col in range(1, 4) - if item.checkState(col) + if item.checkState(col) == Qt.CheckState.Checked ] lockDict[slider] = lv From c2abb12559e53219afac16a3b85fc364e8dfc505 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 3 Sep 2026 13:02:37 -0700 Subject: [PATCH 32/47] Fix splitting and the falloff dialog --- src/python/simplexui/falloffDialog.py | 102 ++++++++++-------- .../menu/genericPlugins/exportSplit.py | 2 +- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index 1fcb0c2d..eace0132 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -18,6 +18,7 @@ # This module imports QT from PyQt4, PySide or PySide2 # Depending on what's available from __future__ import annotations +import struct from .interfaceModel import FalloffDataModel from .items.falloff import PlanarFalloff from Qt import QtCompat @@ -37,6 +38,10 @@ QPainterPath, QPalette, QPen, + QPaintEvent, + QMouseEvent, + QHideEvent, + QShowEvent, ) from Qt.QtWidgets import ( QComboBox, @@ -53,16 +58,21 @@ QWidget, ) from .utils import getNextName, getUiFile, Prefs +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from .simplexDialog import SimplexDialog + from .items import Simplex class CurveEditWidget(QWidget): tangentUpdated = Signal(float, float) - def __init__(self, parent): + def __init__(self, parent: Optional[QWidget]): super().__init__(parent) - self.leftTan = None - self.rightTan = None - self._controlPoints = [ + self.leftTan: Optional[float] = None + self.rightTan: Optional[float] = None + self._controlPoints: list[QPointF] = [ QPointF(0, 1), QPointF(0, 1), QPointF(1, 0), @@ -70,19 +80,21 @@ def __init__(self, parent): ] self.setTangent(leftTan=1 / 3.0, rightTan=2 / 3.0) - self._activeControlPoint = None - self.mouseDrag = False - self.mousePress = QPoint() - self.startDragDistance = 20 + self._activeControlPoint: Optional[int] = None + self.mouseDrag: bool = False + self.mousePress: QPoint = QPoint() + self.startDragDistance: int = 20 - self.canvasMargin = 16 + self.canvasMargin: int = 16 self.setMinimumHeight(2 * self.canvasMargin) - self.bgColor = Qt.GlobalColor.white - self.lineColor = Qt.GlobalColor.black - self.limitColor = Qt.GlobalColor.gray + self.bgColor: Qt.GlobalColor = Qt.GlobalColor.white + self.lineColor: Qt.GlobalColor = Qt.GlobalColor.black + self.limitColor: Qt.GlobalColor = Qt.GlobalColor.gray - def setTangent(self, leftTan=None, rightTan=None): + def setTangent( + self, leftTan: Optional[float] = None, rightTan: Optional[float] = None + ): """Set the falloff tangents, clamped 0 to 1 Parameters @@ -100,7 +112,7 @@ def setTangent(self, leftTan=None, rightTan=None): self._controlPoints[2] = QPointF(self.rightTan, 0) self.update() - def mapToCanvas(self, point): + def mapToCanvas(self, point: QPointF) -> QPointF: """Map a point from widget space to canvas space The "canvas" is a 0-1 parameterized space, centered in the widget The size of the canvas relative to the widget is dictated by the canvasMargin @@ -122,7 +134,7 @@ def mapToCanvas(self, point): y = canvasHeight - point.y() * canvasHeight + self.canvasMargin return QPointF(x, y) - def mapFromCanvas(self, point): + def mapFromCanvas(self, point: QPointF) -> QPointF: """Map a point from canvas space to widget space The "canvas" is a 0-1 parameterized space, centered in the widget The size of the canvas relative to the widget is dictated by the canvasMargin @@ -144,16 +156,16 @@ def mapFromCanvas(self, point): y = 1.0 - (point.y() - self.canvasMargin) / float(canvasHeight) return QPointF(x, y) - def _drawCleanLine(self, painter, p1, p2): + def _drawCleanLine(self, painter: QPainter, p1: QPointF, p2: QPointF): painter.drawLine(p1 + QPointF(0.5, 0.5), p2 + QPointF(0.5, 0.5)) - def _paintBG(self, painter): + def _paintBG(self, painter: QPainter): painter.save() painter.setBrush(self.palette().color(QPalette.ColorRole.Window)) painter.drawRect(0, 0, self.width(), self.height()) painter.restore() - def _paintLimits(self, painter): + def _paintLimits(self, painter: QPainter): painter.save() # pen = QPen(self.limitColor) baseColor = self.palette().color(QPalette.ColorRole.Base) @@ -162,14 +174,16 @@ def _paintLimits(self, painter): pen.setStyle(Qt.PenStyle.DashLine) painter.setPen(pen) self._drawCleanLine( - painter, self.mapToCanvas(QPoint(0, 0)), self.mapToCanvas(QPoint(1, 0)) + painter, self.mapToCanvas(QPointF(0, 0)), self.mapToCanvas(QPointF(1, 0)) ) self._drawCleanLine( - painter, self.mapToCanvas(QPoint(0, 1)), self.mapToCanvas(QPoint(1, 1)) + painter, self.mapToCanvas(QPointF(0, 1)), self.mapToCanvas(QPointF(1, 1)) ) painter.restore() - def _paintPath(self, painter, p0, p1, p2, p3): + def _paintPath( + self, painter: QPainter, p0: QPointF, p1: QPointF, p2: QPointF, p3: QPointF + ): painter.save() path = QPainterPath() path.moveTo(p0) @@ -179,7 +193,9 @@ def _paintPath(self, painter, p0, p1, p2, p3): painter.strokePath(path, QPen(QBrush(foregroundColor), 2)) painter.restore() - def _paintTangents(self, painter, p0, p1, p2, p3): + def _paintTangents( + self, painter: QPainter, p0: QPointF, p1: QPointF, p2: QPointF, p3: QPointF + ): # draw the tangent lines foregroundColor = self.palette().color(QPalette.ColorRole.WindowText) pen = QPen(foregroundColor) @@ -194,7 +210,7 @@ def _paintTangents(self, painter, p0, p1, p2, p3): active = i == self._activeControlPoint self.paintControlPoint(self._controlPoints[i], painter, online, active) - def paintEvent(self, e): + def paintEvent(self, e: QPaintEvent): painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) @@ -210,10 +226,12 @@ def paintEvent(self, e): self._paintPath(painter, p0, p1, p2, p3) self._paintTangents(painter, p0, p1, p2, p3) - def indexIsRealPoint(self, i): + def indexIsRealPoint(self, i: int): return (i % 3) == 0 - def paintControlPoint(self, point, painter, real, active): + def paintControlPoint( + self, point: QPointF, painter: QPainter, real: bool, active: bool + ): pointSize = 4 if real: @@ -235,7 +253,7 @@ def paintControlPoint(self, point, painter, real, active): ) ) - def findControlPoint(self, point, tolerance=10): + def findControlPoint(self, point: QPointF, tolerance: int = 10) -> Optional[int]: d = QLineF(self.mapToCanvas(self._controlPoints[1]), point).length() if d < tolerance: return 1 @@ -245,21 +263,21 @@ def findControlPoint(self, point, tolerance=10): return 2 return None - def mousePressEvent(self, e): + def mousePressEvent(self, e: QMouseEvent): if e.button() == Qt.MouseButton.LeftButton: - self._activeControlPoint = self.findControlPoint(e.pos()) + self._activeControlPoint = self.findControlPoint(e.position()) if self._activeControlPoint is not None: self.mouseMoveEvent(e) self.mousePress = e.pos() e.accept() - def mouseReleaseEvent(self, e): + def mouseReleaseEvent(self, e: QMouseEvent): if e.button() == Qt.MouseButton.LeftButton: self._activeControlPoint = None self.mouseDrag = False e.accept() - def mouseMoveEvent(self, e): + def mouseMoveEvent(self, e: QMouseEvent): if ( not self.mouseDrag and QPoint(self.mousePress - e.pos()).manhattanLength() @@ -267,7 +285,7 @@ def mouseMoveEvent(self, e): ): self.mouseDrag = True - p = self.mapFromCanvas(e.pos()) + p = self.mapFromCanvas(e.position()) if self.mouseDrag and self._activeControlPoint is not None: if self._activeControlPoint == 1: self.setTangent(leftTan=min(max(p.x(), 0.0), 1.0)) @@ -295,13 +313,13 @@ class FalloffDialog(QDialog): uiFalloffMinHandleSPN: QDoubleSpinBox uiFalloffMinSPN: QDoubleSpinBox - def __init__(self, parent): + def __init__(self, parent: SimplexDialog): super().__init__(parent) uiPath = getUiFile(__file__) QtCompat.loadUi(uiPath, self) - self.parUI = parent + self.parUI: SimplexDialog = parent - self.simplex = None + self.simplex: Optional[Simplex] = None self.parUI.simplexLoaded.connect(self.loadSimplex) self.foModel = FalloffDataModel(None, self) @@ -328,7 +346,7 @@ def __init__(self, parent): self.uiFalloffMinHandleSPN.valueChanged.connect(self.setRightTangent) self.loadSimplex() - def updateTangents(self, leftTangent, rightTangent): + def updateTangents(self, leftTangent: float, rightTangent: float): self.uiFalloffMaxHandleSPN.setValue(leftTangent) self.uiFalloffMinHandleSPN.setValue(rightTangent) @@ -340,10 +358,10 @@ def updateTangents(self, leftTangent, rightTangent): self.foModel.setData(leftTanIdx, leftTangent, role=Qt.ItemDataRole.EditRole) self.foModel.setData(rightTanIdx, rightTangent, role=Qt.ItemDataRole.EditRole) - def setLeftTangent(self, val): + def setLeftTangent(self, val: float): self.uiFalloffWID.setTangent(leftTan=val) - def setRightTangent(self, val): + def setRightTangent(self, val: float): self.uiFalloffWID.setTangent(rightTan=val) def loadSimplex(self): @@ -462,8 +480,7 @@ def renameFalloff(self): def storeSettings(self): """Store the UI settings for this dialog""" pref = Prefs() - geodata = bytes(self.saveGeometry().data()).decode() - pref.recordProperty("fogeometry", geodata) + pref.recordProperty("fogeometry", self.saveGeometry().data()) pref.save() def loadSettings(self): @@ -471,15 +488,16 @@ def loadSettings(self): pref = Prefs() geo = pref.restoreProperty("fogeometry", None) if geo is not None: - assert isinstance(geo, QByteArray) + if isinstance(geo, bytes): + geo = QByteArray(geo) self.restoreGeometry(geo) - def hideEvent(self, event): + def hideEvent(self, event: QHideEvent): """Override the hide event to store settings""" self.storeSettings() super().hideEvent(event) - def showEvent(self, event): + def showEvent(self, event: QShowEvent): """Override the show event to restore settings""" super().showEvent(event) self.loadSettings() diff --git a/src/python/simplexui/menu/genericPlugins/exportSplit.py b/src/python/simplexui/menu/genericPlugins/exportSplit.py index 14b079c3..64cd3935 100644 --- a/src/python/simplexui/menu/genericPlugins/exportSplit.py +++ b/src/python/simplexui/menu/genericPlugins/exportSplit.py @@ -60,7 +60,7 @@ def exportSplitInterface(window): pBar = QProgressDialog("Exporting Split smpx File", "Cancel", 0, 100, window) pBar.show() try: - split = window.simplex.split(pBar) + split = window.simplex.split(pBar=pBar) split.exportAbc(path, pBar) except ValueError as e: QMessageBox.warning(window, "Unsplittable", str(e)) From 06257387cd2287a9f1d769a59e1f78e8456d399c Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 3 Sep 2026 14:59:41 -0700 Subject: [PATCH 33/47] Fix split exports --- src/python/simplexui/interface/dummyInterface.py | 1 + src/python/simplexui/interface/mayaInterface.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index 3fa933c5..d998dc62 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -593,6 +593,7 @@ def exportAbc( if pBar is not None: pBar.setLabelText("Exporting:\n{0}".format(shape.name)) pBar.setValue(i) + pBar.repaint() # Required to properly show the percentage. Don't know why QApplication.processEvents() if pBar.wasCanceled(): return diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 16f5bd62..36475cde 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -625,7 +625,9 @@ def getAllShapeVertices(self, shapes, pBar=None): cmds.setAttr(shape.thing, 1.0) if np is not None and mayaToNumpy is not None: - out = mayaToNumpy(meshFn.getRawPoints()) + mpts = om.MPointArray() + meshFn.getPoints(mpts) + out = mayaToNumpy(mpts)[:, :3] else: flatverts = cmds.xform( "{0}.vtx[*]".format(self.mesh), From e641a1e0c96a1de923195e5fc630b6e98c964d2b Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 3 Sep 2026 15:22:11 -0700 Subject: [PATCH 34/47] Fix checkability and header values --- src/python/simplexui/falloffDialog.py | 1 - src/python/simplexui/interfaceModel.py | 246 ++++++------------------- 2 files changed, 58 insertions(+), 189 deletions(-) diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index eace0132..e0473cc5 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -385,7 +385,6 @@ def loadSimplex(self): # Populate Settings widgets self.foModel = FalloffDataModel(self.simplex, self) - # self.simplex.falloffModels.append(self.foModel) self.uiShapeFalloffCBOX.setModel(self.foModel) self._falloffMapper.setModel(self.foModel) diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index b5a515e2..fd016766 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -26,6 +26,7 @@ Progression, Slider, Traversal, + TravPair, ) from .items.treeItem import AdapterModel @@ -206,6 +207,62 @@ class SimplexModel(AdapterModel): def simplex(self): return self._rootItem + def headerData(self, section, orientation, role): + if orientation == Qt.Orientation.Horizontal: + if role == Qt.ItemDataRole.DisplayRole: + sects = ("Items", "Slide", "Value") + return sects[section] + return None + + def flags(self, index): + if not index.isValid(): + return Qt.ItemFlag.ItemIsEnabled + if index.column() == 0: + item = index.internalPointer() + if isinstance(item, (Slider, Combo, Traversal)): + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsEditable + | Qt.ItemFlag.ItemIsUserCheckable + ) + elif isinstance(item, Progression): + return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable + return ( + Qt.ItemFlag.ItemIsEnabled + | Qt.ItemFlag.ItemIsSelectable + | Qt.ItemFlag.ItemIsEditable + ) + + def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + if not index.isValid(): + return False + if role == Qt.ItemDataRole.CheckStateRole: + item = index.internalPointer() + if index.column() == 0: + if isinstance(item, (Slider, Combo, Traversal)): + item.enabled = value == Qt.CheckState.Checked.value + return True + elif role == Qt.ItemDataRole.EditRole: + item = index.internalPointer() + if index.column() == 0: + if isinstance(item, (Group, Slider, Combo, Traversal, ProgPair)): + item.name = value + return True + + elif index.column() == 1: + if isinstance(item, Slider): + item.value = value + elif isinstance(item, ComboPair): + item.value = value + elif isinstance(item, TravPair): + item.value = value + + elif index.column() == 2: + if isinstance(item, ProgPair): + item.value = value + return False + # VIEW MODELS class BaseProxyModel(QSortFilterProxyModel): @@ -398,7 +455,6 @@ def __init__(self, model, parent=None): self.filterRequiresAny = False self.filterRequiresOnly = False - def filterAcceptsRow(self, sourceRow, sourceParent): # always sort by the first column #column = self.filterKeyColumn() column = 0 @@ -456,193 +512,7 @@ def filterAcceptsRow(self, sourceRow, sourceParent): elif data.shape.isRest: return False - return super().filterAcceptsRow( - sourceRow, sourceParent - ) - - -# SETTINGS MODELS -class SliderGroupModel(AdapterModel): - """A model for displaying Group objects that contain Sliders""" - - def __init__(self, simplex, parent): - super().__init__(simplex, parent) - self.simplex = simplex - # self.simplex.models.append(self) - - def getItemRow(self, item): - try: - idx = self.simplex.sliderGroups.index(item) - except ValueError: - return None - return idx + 1 - - def getItemAppendRow(self, item): - return len(self.simplex.sliderGroups) + 1 - - def index(self, row, column=0, parIndex=None): - parIndex = QModelIndex() if parIndex is None else parIndex - if row <= 0: - return self.createIndex(row, column, None) - try: - falloff = self.simplex.sliderGroups[row - 1] - except IndexError: - return QModelIndex() - return self.createIndex(row, column, falloff) - - def parent(self, index): - return QModelIndex() - - def rowCount(self, parent): - return len(self.simplex.sliderGroups) + 1 - - def columnCount(self, parent): - return 1 - - def data(self, index, role): - if not index.isValid(): - return None - group = index.internalPointer() - if group and role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): - return group.name - return None - - def flags(self, index): - return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable - - def itemFromIndex(self, index): - return index.internalPointer() - - -class FalloffModel(AdapterModel): - """A model for displaying Falloff objects connected to Sliders""" - - def __init__(self, simplex, parent): - super().__init__(simplex, parent) - self.simplex = simplex - # if self.simplex is not None: self.simplex.falloffModels.append(self) - self.sliders = [] - self._checks = {} - self.line = "" - - def setSliders(self, sliders): - self.beginResetModel() - self.sliders = sliders - self._checks = {} - for slider in self.sliders: - for fo in slider.prog.falloffs: - self._checks.setdefault(fo, []).append(slider) - self.endResetModel() - self.buildLine() - - def buildLine(self): - if not self.sliders: - self.line = "" - return - fulls = [] - partials = [] - for fo in self.simplex.falloffs: - cs = self._getCheckState(fo) - if cs == Qt.CheckState.Checked: - fulls.append(fo.name) - elif cs == Qt.CheckState.PartiallyChecked: - partials.append(fo.name) - if partials: - title = "{0} <<{1}>>".format(",".join(fulls), ",".join(partials)) - else: - title = ",".join(fulls) - self.line = title - - def getItemRow(self, item): - try: - idx = self.simplex.falloffs.index(item) - except ValueError: - return None - except AttributeError: - return None - return idx + 1 - - def getItemAppendRow(self, item): - try: - return len(self.simplex.falloffs) - except AttributeError: - return 0 - - def index(self, row, column=0, parIndex=None): - parIndex = QModelIndex() if parIndex is None else parIndex - if row <= 0: - return self.createIndex(row, column, None) - try: - falloff = self.simplex.falloffs[row - 1] - except IndexError: - return QModelIndex() - except AttributeError: - return QModelIndex() - return self.createIndex(row, column, falloff) - - def parent(self, index): - return QModelIndex() - - def rowCount(self, parent): - try: - return len(self.simplex.falloffs) + 1 - except AttributeError: - return 0 - - def columnCount(self, parent): - return 1 - - def _getCheckState(self, fo): - sli = self._checks.get(fo, []) - if len(sli) == len(self.sliders): - return Qt.CheckState.Checked - elif len(sli) == 0: - return Qt.CheckState.Unchecked - return Qt.CheckState.PartiallyChecked - - def data(self, index, role): - if not index.isValid(): - return None - falloff = index.internalPointer() - if not falloff: - return None - - if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole): - return falloff.name - elif role == Qt.ItemDataRole.CheckStateRole: - return self._getCheckState(falloff) - return None - - def setData(self, index, value, role): - if role == Qt.ItemDataRole.CheckStateRole: - fo = index.internalPointer() - if not fo: - return - if value == Qt.CheckState.Checked: - for s in self.sliders: - if fo not in s.prog.falloffs: - s.prog.addFalloff(fo) - self._checks.setdefault(fo, []).append(s) - elif value == Qt.CheckState.Unchecked: - for s in self.sliders: - if fo in s.prog.falloffs: - s.prog.removeFalloff(fo) - if s in self._checks[fo]: - self._checks[fo].remove(s) - self.buildLine() - self.emitDataChanged(index) - return True - return False - - def flags(self, index): - return ( - Qt.ItemFlag.ItemIsEnabled - | Qt.ItemFlag.ItemIsEditable - | Qt.ItemFlag.ItemIsUserCheckable - ) - - def itemFromIndex(self, index): - return index.internalPointer() + return super().filterAcceptsRow(sourceRow, sourceParent) class FalloffDataModel(AdapterModel): From a8b7eded812038dc0364b54cef2cc8d1e5ba1bac Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 3 Sep 2026 15:37:33 -0700 Subject: [PATCH 35/47] Add some explicit factory methods to the base Falloff to make the required subclasses --- src/python/simplexui/items/falloff.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index 3455fa36..d82414b5 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -157,6 +157,33 @@ def name(self, value: str): """Set the name of a Falloff""" self._name = value + @classmethod + def createMap( + cls, name: str, simplex: Simplex, mapName: str, axis: str + ) -> MapFalloff: + return MapFalloff.createMap(name, simplex, mapName, axis) + + @classmethod + def createPlanar( + cls, + name: str, + simplex: Simplex, + axis: str, + maxVal: float, + maxHandle: float, + minHandle: float, + minVal: float, + ) -> PlanarFalloff: + return PlanarFalloff.createPlanar( + name, + simplex, + axis, + maxVal, + maxHandle, + minHandle, + minVal, + ) + @classmethod def loadV2(cls, simplex: Simplex, data: dict[str, Any]) -> Falloff: """Load the falloff from the version 2 json specification From 1fa51246a755376ea4aeebf0cfdd5f4399aaab29 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Thu, 3 Sep 2026 16:34:09 -0700 Subject: [PATCH 36/47] Clean up the falloff factory methods --- src/python/simplexui/items/falloff.py | 59 +++------------------------ 1 file changed, 6 insertions(+), 53 deletions(-) diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index d82414b5..175622ed 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -161,7 +161,11 @@ def name(self, value: str): def createMap( cls, name: str, simplex: Simplex, mapName: str, axis: str ) -> MapFalloff: - return MapFalloff.createMap(name, simplex, mapName, axis) + # Note that MapName and Axis are swapped + # This is to provide backward compatibility. + # You should just make the MapFalloff object directly + assert len(axis) == 1, "Looks like you got mapName and axis swapped" + return MapFalloff(name, simplex, axis, mapName) @classmethod def createPlanar( @@ -174,7 +178,7 @@ def createPlanar( minHandle: float, minVal: float, ) -> PlanarFalloff: - return PlanarFalloff.createPlanar( + return PlanarFalloff( name, simplex, axis, @@ -431,38 +435,6 @@ def __init__( self._bezier = None super().__init__(name, simplex, axis) - @classmethod - def createPlanar( - cls, - name: str, - simplex: Simplex, - axis: str, - maxVal: float, - maxHandle: float, - minHandle: float, - minVal: float, - ) -> PlanarFalloff: - """Create a planar falloff - - Parameters - ---------- - name : str - The name to give the falloff - simplex : Simplex - The Simplex system - axis : str - The axis to align the falloff to. X, Y, or Z - maxVal : float - The value past which the falloff is 1.0 - maxHandle : float - The (0, 1) range of the max cubic falloff handle - minHandle : float - The (0, 1) range of the min cubic falloff handle - minVal : float - The value past which the falloff is 0.0 - """ - return cls(name, simplex, axis, maxVal, maxHandle, minHandle, minVal) - @stackable def setPlanarData( self, @@ -693,25 +665,6 @@ def __init__(self, name: str, simplex: Simplex, axis: str, mapName: str): self._mapName = mapName super().__init__(name, simplex, axis) - @classmethod - def createMap( - cls, name: str, simplex: Simplex, mapName: str, axis: str - ) -> MapFalloff: - """Create a weightmap falloff - - Parameters - ---------- - name : str - The name to give the falloff - simplex : Simplex - The Simplex system - mapName : str - The name of the weightmap - axis : str - The axis to align the falloff to. X, Y, or Z - """ - return cls(name, simplex, mapName, axis) - @stackable def setMapData(self, axis: str, mapName: str): """Set the type/data for a map Falloff From d4284ae2e76acd332fd5b4eb6599d2518d84067c Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 11 Sep 2026 10:43:04 -0700 Subject: [PATCH 37/47] Add a bunch of typehints, and handle newer arrayToNumpy --- .../simplexui/commands/alembicCommon.py | 9 +- src/python/simplexui/commands/mayatonumpy.py | 61 +++- src/python/simplexui/commands/numpytoimath.py | 319 ------------------ src/python/simplexui/interfaceModel.py | 146 ++++---- src/python/simplexui/items/__init__.py | 6 +- src/python/simplexui/items/treeItem.py | 2 + src/python/simplexui/simplexDialog.py | 4 +- 7 files changed, 149 insertions(+), 398 deletions(-) delete mode 100644 src/python/simplexui/commands/numpytoimath.py diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 003dd5a2..2812b0e6 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -452,10 +452,11 @@ def getUvArray(imesh: IPolyMesh) -> Optional[npfloat]: if uvParam.valid(): uvProp = uvParam.getValueProperty() uvVals = uvProp.getValue() - # imathNumpy doesn't work on V2f arrays - # so I have to use one of the slow ways - uv = list(zip(uvVals.x, uvVals.y)) - uv = np.array(uv) + if arrayToNumpy is None: + uv = list(zip(uvVals.x, uvVals.y)) + uv = np.array(uv) + else: + uv = arrayToNumpy(uvVals) else: uv = None return uv diff --git a/src/python/simplexui/commands/mayatonumpy.py b/src/python/simplexui/commands/mayatonumpy.py index be9afd3a..3212c739 100644 --- a/src/python/simplexui/commands/mayatonumpy.py +++ b/src/python/simplexui/commands/mayatonumpy.py @@ -1,7 +1,8 @@ -# pytype: disable=import-error +from __future__ import annotations from maya import OpenMaya as om import numpy as np from ctypes import c_float, c_double, c_int, c_uint +from typing import Union, TypeVar, TYPE_CHECKING # fmt: off _CONVERT_DICT = { @@ -16,8 +17,36 @@ } # fmt: on - -def _swigConnect(mArray, count, util): +OMArrays = Union[ + om.MPointArray, + om.MFloatPointArray, + om.MVectorArray, + om.MFloatVectorArray, + om.MDoubleArray, + om.MFloatArray, + om.MIntArray, + om.MUintArray, +] +OMPtrs = Union[ + om.MScriptUtil.asDouble4Ptr, + om.MScriptUtil.asFloat4Ptr, + om.MScriptUtil.asDouble3Ptr, + om.MScriptUtil.asFloat3Ptr, + om.MScriptUtil.asDoublePtr, + om.MScriptUtil.asFloatPtr, + om.MScriptUtil.asIntPtr, + om.MScriptUtil.asUintPtr, +] + +if TYPE_CHECKING: + + class SwigPyObject: + def __int__(self) -> int: ... + + +def _swigConnect( + mArray: OMArrays, count: int, util: om.MScriptUtil +) -> tuple[np.ndarray, OMPtrs]: """ Use an MScriptUtil to build SWIG array that we can read from and write to. Make sure to get the MScriptUtil from outside this function, otherwise @@ -55,9 +84,11 @@ def _swigConnect(mArray, count, util): return npArray, ptr -def _swigConnectMatrix(mat, ctp): +def _swigConnectMatrix( + mat: om.MMatrix, ctp: Union[type[c_double], type[c_float]] +) -> tuple[np.ndarray, SwigPyObject]: # With a matrix, you can just get the double[4][4] without an MScriptUtil - ptr = mat.matrix + ptr: SwigPyObject = mat.matrix cdata = ctp * 4 * 4 # int(ptr) gives the memory address @@ -69,7 +100,7 @@ def _swigConnectMatrix(mat, ctp): return npArray, ptr -def mayaToNumpy(mArray): +def mayaToNumpy(mArray: OMArrays) -> np.ndarray: """Convert a maya array to a numpy array Parameters @@ -81,7 +112,6 @@ def mayaToNumpy(mArray): ------- : np.array : A numpy array that contains the data from mArray - """ if isinstance(mArray, om.MMatrix): npArray, _ = _swigConnectMatrix(mArray, c_double) @@ -94,7 +124,10 @@ def mayaToNumpy(mArray): return np.copy(npArray) -def numpyToMaya(ary, mType): +T = TypeVar('T', bound=OMArrays) + + +def numpyToMaya(ary: np.ndarray, mType: type[T]) -> T: """Convert a numpy array to a specific maya type array Parameters @@ -178,7 +211,9 @@ def numpyToMaya(ary, mType): # fmt: on -def getNumpyAttr(attrName): +def getNumpyAttr( + attrName: Union[om.MPlug, str], +) -> Union[np.ndarray, float, int, tuple[int, ...], tuple[float, ...]]: """Read attribute data directly from the plugs into numpy This function will read most numeric data types directly into numpy arrays @@ -263,7 +298,10 @@ def getNumpyAttr(attrName): raise NotImplementedError("Fell all the way through") -def setNumpyAttr(attrName, value): +def setNumpyAttr( + attrName: Union[str, om.MPlug], + value: Union[np.ndarray, float, int, tuple[int, ...], tuple[float, ...]], +): """Write a numpy array directly into a maya plug This function will handle most numeric plug types. @@ -295,6 +333,7 @@ def setNumpyAttr(attrName, value): # So, at this point, you should really just use setattr ntype = mdh.numericType() if ntype in _NTYPE_DICT: + assert isinstance(value, tuple) _NTYPE_DICT[ntype][1](mdh, *value) plug.setMObject(mdh.data()) elif ntype == om.MFnNumericData.k4Double: @@ -316,6 +355,7 @@ def setNumpyAttr(attrName, value): # build the pointArrayData fnType, mType = _DTYPE_DICT[apiType] fn = fnType() + assert isinstance(value, np.ndarray) mPts = numpyToMaya(value, mType) dataObj = fn.create(mPts) plug.setMObject(dataObj) @@ -326,6 +366,7 @@ def setNumpyAttr(attrName, value): compList = fnCompList.create() fnIdx = om.MFnSingleIndexedComponent() idxObj = fnIdx.create(om.MFn.kMeshVertComponent) + assert isinstance(value, np.ndarray) mIdxs = numpyToMaya(value, om.MIntArray) fnIdx.addElements(mIdxs) fnCompList.add(idxObj) diff --git a/src/python/simplexui/commands/numpytoimath.py b/src/python/simplexui/commands/numpytoimath.py deleted file mode 100644 index 4a16b230..00000000 --- a/src/python/simplexui/commands/numpytoimath.py +++ /dev/null @@ -1,319 +0,0 @@ -import imath -import ctypes -import numpy as np -from typing import TypeVar, Type - - -NTYPEDICT: dict[type, type] = { - ctypes.c_bool: bool, - ctypes.c_byte: np.int8, - ctypes.c_double: np.float64, - ctypes.c_float: np.float32, - ctypes.c_long: np.int32, - ctypes.c_short: np.int16, - ctypes.c_ubyte: np.uint8, - ctypes.c_ulong: np.uint32, - ctypes.c_ushort: np.uint16, -} - -# fmt: off -TYPEDICT: dict[type, tuple[list[int], type, str]] = { - imath.BoolArray: ([], ctypes.c_bool, 'array'), - imath.DoubleArray: ([], ctypes.c_double, 'array'), - imath.FloatArray: ([], ctypes.c_float, 'array'), - imath.IntArray: ([], ctypes.c_long, 'array'), - imath.ShortArray: ([], ctypes.c_short, 'array'), - imath.SignedCharArray: ([], ctypes.c_byte, 'array'), - imath.UnsignedCharArray: ([], ctypes.c_ubyte, 'array'), - imath.UnsignedIntArray: ([], ctypes.c_ulong, 'array'), - imath.UnsignedShortArray: ([], ctypes.c_ushort, 'array'), - - imath.Box2dArray: ([2, 2], ctypes.c_double, 'array'), - imath.Box2fArray: ([2, 2], ctypes.c_float, 'array'), - imath.Box2iArray: ([2, 2], ctypes.c_long, 'array'), - imath.Box2sArray: ([2, 2], ctypes.c_short, 'array'), - imath.Box3dArray: ([2, 3], ctypes.c_double, 'array'), - imath.Box3fArray: ([2, 3], ctypes.c_float, 'array'), - imath.Box3iArray: ([2, 3], ctypes.c_long, 'array'), - imath.Box3sArray: ([2, 3], ctypes.c_short, 'array'), - imath.C3cArray: ([3], ctypes.c_byte, 'array'), - imath.C3fArray: ([3], ctypes.c_float, 'array'), - imath.C4cArray: ([4], ctypes.c_byte, 'array'), - imath.C4fArray: ([4], ctypes.c_float, 'array'), - imath.M22dArray: ([2, 2], ctypes.c_double, 'array'), - imath.M22fArray: ([2, 2], ctypes.c_float, 'array'), - imath.M33dArray: ([3, 3], ctypes.c_double, 'array'), - imath.M33fArray: ([3, 3], ctypes.c_float, 'array'), - imath.M44dArray: ([4, 4], ctypes.c_double, 'array'), - imath.M44fArray: ([4, 4], ctypes.c_float, 'array'), - imath.QuatdArray: ([4], ctypes.c_double, 'array'), - imath.QuatfArray: ([4], ctypes.c_float, 'array'), - imath.V2dArray: ([2], ctypes.c_double, 'array'), - imath.V2fArray: ([2], ctypes.c_float, 'array'), - imath.V2iArray: ([2], ctypes.c_long, 'array'), - imath.V2sArray: ([2], ctypes.c_short, 'array'), - imath.V3dArray: ([3], ctypes.c_double, 'array'), - imath.V3fArray: ([3], ctypes.c_float, 'array'), - imath.V3iArray: ([3], ctypes.c_long, 'array'), - imath.V3sArray: ([3], ctypes.c_short, 'array'), - imath.V4dArray: ([4], ctypes.c_double, 'array'), - imath.V4fArray: ([4], ctypes.c_float, 'array'), - imath.V4iArray: ([4], ctypes.c_long, 'array'), - imath.V4sArray: ([4], ctypes.c_short, 'array'), - - imath.Color4cArray2D: ([4], ctypes.c_byte, 'array2d'), - imath.Color4fArray2D: ([4], ctypes.c_float, 'array2d'), - imath.DoubleArray2D: ([], ctypes.c_double, 'array2d'), - imath.FloatArray2D: ([], ctypes.c_float, 'array2d'), - imath.IntArray2D: ([], ctypes.c_long, 'array2d'), - - imath.DoubleMatrix: ([], ctypes.c_double, 'matrix'), - imath.FloatMatrix: ([], ctypes.c_float, 'matrix'), - imath.IntMatrix: ([], ctypes.c_long, 'matrix'), - - imath.Box2d: ([2, 2], ctypes.c_double, 'box'), - imath.Box2f: ([2, 2], ctypes.c_float, 'box'), - imath.Box2i: ([2, 2], ctypes.c_long, 'box'), - imath.Box2s: ([2, 2], ctypes.c_short, 'box'), - imath.Box3d: ([2, 3], ctypes.c_double, 'box'), - imath.Box3f: ([2, 3], ctypes.c_float, 'box'), - imath.Box3i: ([2, 3], ctypes.c_long, 'box'), - imath.Box3s: ([2, 3], ctypes.c_short, 'box'), - - imath.Line3d: ([2, 3], ctypes.c_double, 'line'), - imath.Line3f: ([2, 3], ctypes.c_float, 'line'), - - imath.Color3c: ([3], ctypes.c_byte, ''), - imath.Color3f: ([3], ctypes.c_float, ''), - imath.Color4c: ([4], ctypes.c_byte, ''), - imath.Color4f: ([4], ctypes.c_float, ''), - imath.M22d: ([2, 2], ctypes.c_double, ''), - imath.M22dRow: ([2], ctypes.c_double, 'row'), - imath.M22f: ([2, 2], ctypes.c_float, ''), - imath.M22fRow: ([2], ctypes.c_float, 'row'), - imath.M33d: ([3, 3], ctypes.c_double, ''), - imath.M33dRow: ([3], ctypes.c_double, 'row'), - imath.M33f: ([3, 3], ctypes.c_float, ''), - imath.M33fRow: ([3], ctypes.c_float, 'row'), - imath.M44d: ([4, 4], ctypes.c_double, ''), - imath.M44dRow: ([4], ctypes.c_double, 'row'), - imath.M44f: ([4, 4], ctypes.c_float, ''), - imath.M44fRow: ([4], ctypes.c_float, 'row'), - imath.Quatd: ([4], ctypes.c_double, ''), - imath.Quatf: ([4], ctypes.c_float, ''), - imath.Shear6d: ([6], ctypes.c_double, ''), - imath.Shear6f: ([6], ctypes.c_float, ''), - imath.V2d: ([2], ctypes.c_double, ''), - imath.V2f: ([2], ctypes.c_float, ''), - imath.V2i: ([2], ctypes.c_long, ''), - imath.V2s: ([2], ctypes.c_short, ''), - imath.V3c: ([3], ctypes.c_byte, ''), - imath.V3d: ([3], ctypes.c_double, ''), - imath.V3f: ([3], ctypes.c_float, ''), - imath.V3i: ([3], ctypes.c_long, ''), - imath.V3s: ([3], ctypes.c_short, ''), - imath.V4c: ([4], ctypes.c_byte, ''), - imath.V4d: ([4], ctypes.c_double, ''), - imath.V4f: ([4], ctypes.c_float, ''), - imath.V4i: ([4], ctypes.c_long, ''), - imath.V4s: ([4], ctypes.c_short, ''), -} -# fmt: on - -# Define the in-memory structures of the python objects -# I'm *GUESSING* on most of the types here, so they could be refined -# in the future if required - -# Here's hoping these structs don't change with different versions -# of python or imath - - -class PyImoObj(ctypes.Structure): - _fields_ = [ - ("refcount", ctypes.c_ssize_t), # from the PyObject c struct - ("typeptr", ctypes.c_void_p), # from the PyObject c struct - ("unknown1", ctypes.c_ssize_t), # Seems always -48 for some reason - ("unknown2", ctypes.c_ssize_t), # Seems always 0 - ("unknown3", ctypes.c_ssize_t), # Seems always 0 - ("dataptr", ctypes.c_void_p), # pointer to the PyImoDataObj - ] - - -class PyImoDataObj(ctypes.Structure): - _fields_ = [ - ("magic", ctypes.c_ssize_t), # Some kind of type ID? - ("unknown1", ctypes.c_ssize_t), # Seems always 0 - ("dataptr", ctypes.c_void_p), # Pointer to the allocated memory - # There's MORE data after this, like the row/column count - # and some other pointers. But I don't need them - # Plus, there are some edge cases with the different types - # like rows or bounding boxes - ] - - -PyImoObjPtr = ctypes.POINTER(PyImoObj) -PyImoDataObjPtr = ctypes.POINTER(PyImoDataObj) - - -def _getImoPointer(imo, extra: str) -> int: - """Get the memory address to the actual imath data - - Args: - imo (imath object): The imath object to inspect - extra (str): The metadata of this current type - - Returns: - int: The memory address to the actual data - """ - # This is a scary function - # I found this stuff out by trial and error - pyStruct = ctypes.cast(id(imo), PyImoObjPtr).contents - if extra == "box": - # I'm guessing that since the bounding box data is a known - # size, they just put it directly into the structure - # And the data is stored where the dataptr would be. - # So I can just add the pyStruct.dataptr and the memory offset - # of the PyImoDataObj.dataptr to get memory address of the box - return pyStruct.dataptr + PyImoDataObj.dataptr.offset - - pyImoStruct = ctypes.cast(pyStruct.dataptr, PyImoDataObjPtr).contents - return pyImoStruct.dataptr - - -def _link(imo) -> tuple[np.ndarray, int]: - """Build a numpy object that's referencing the same memory - as the given imath object - - Args: - imo (imath object): The imath object to build a link to - - Returns: - np.ndarray: The numpy array - int: The pointer to the memory address where the data lives - """ - size, cdata, extra = TYPEDICT[type(imo)] - if extra == "array": - shape = [len(imo)] + size - elif extra == "array2d": - shape = list(imo.size()) + size - elif extra == "matrix": - shape = [imo.rows(), imo.columns()] + size - elif extra in ("box", "line"): - shape = size - else: - shape = [len(imo)] - - for s in shape[::-1]: - cdata = cdata * s # type: ignore - - ptr = _getImoPointer(imo, extra) - ctypearray = cdata.from_address(ptr) - nparray = np.ctypeslib.as_array(ctypearray) - return nparray, ptr - - -def imathToNumpy(imo) -> np.ndarray: - """Copy an imath object into a numpy array - - Args: - imo (imath object): The imath object to convert - - Returns: - np.ndarray: A copy of the imath object as a numpy array - """ - gcarray, _ptr = _link(imo) - return np.copy(gcarray) - - -T = TypeVar("T") - - -def numpyToImath(npo: np.ndarray, imtype: Type[T]) -> T: - """Convert a numpy array to the given imath type - - Args: - npo (array like): An object that can be cast to a numpy array - imtype (type): The imath type to convert to - - Returns: - imathObj: An instantiated imtype object with the numpy data loaded into it - """ - size, cdata, extra = TYPEDICT[imtype] - npo = np.asarray(npo, dtype=NTYPEDICT[cdata]) - if extra == "array": - assert npo.ndim == len(size) + 1 - imo = imtype(len(npo)) # type: ignore - elif extra in ("matrix", "array2d"): - assert npo.ndim == 2 + len(size) - imo = imtype(*npo.shape[:2]) - else: - imo = imtype() - - tret, _ptr = _link(imo) - np.copyto(tret, npo) - return imo - - -def _test(): - """A quick test for all this crazy stuff""" - - # By default the bounding box objects are set to - # The min/max for their types - box3dnp = np.empty((2, 3), dtype=np.float64) - box3dnp[0] = imath.DBL_MAX - box3dnp[1] = imath.DBL_MIN - - box3fnp = np.empty((2, 3), dtype=np.float32) - box3fnp[0] = imath.FLT_MAX - box3fnp[1] = imath.FLT_MIN - - box3fanp = np.empty((5, 2, 3), dtype=np.float32) - box3fanp[:] = box3fnp - - # M44fArray - eyes = np.zeros((13, 4, 4), dtype=np.float32) - eyes[:] = np.eye(4, dtype=np.float32) - - # Line3f - line = np.zeros((2, 3), dtype=np.float32) - line[1, 0] = 1.0 - - # M33fRow - im = imath.M33f() - nm = np.eye(3, dtype=np.float32) - - # equivalent to an np.empty(3, 5) - # so I have to set the values manually - dubm = imath.DoubleMatrix(3, 5) - tt = 0.0 - for i in range(3): - row = dubm[i] - for j in range(5): - row[j] = tt - tt += 1.0 - - # fmt: off - eqpairs = [ - (imath.V3dArray(11), np.zeros((11, 3))), - (imath.M44fArray(13), eyes), - (imath.V3d(), np.zeros(3)), - (imath.Color4cArray2D(5, 7), np.zeros((5, 7, 4), dtype=np.int8)), - (imath.Box3fArray(5), box3fanp), - (imath.Box3d(), box3dnp), - (imath.Box3f(), box3fnp), - (imath.FloatArray(13), np.zeros((13), dtype=np.float32)), - (imath.Line3f(), line), - (im[1], nm[1]), - (dubm, np.arange(15, dtype=float).reshape((3, 5))), - ] - # fmt: on - - for imo, chk in eqpairs: - nv = imathToNumpy(imo) - assert nv.dtype == chk.dtype - assert np.all(nv == chk) - - imtype = type(imo) - _size, _cdata, extra = TYPEDICT[imtype] - if extra != "row": # Can't directly build row objects - _iv = numpyToImath(chk, imtype) diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index fd016766..aa8b9b2c 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -25,18 +25,24 @@ ProgPair, Progression, Slider, + Simplex, Traversal, TravPair, + Falloff, ) -from .items.treeItem import AdapterModel +from .items.treeItem import TreeItem, TreeRootItem, AdapterModel +from Qt.QtWidgets import QWidget from Qt.QtCore import QModelIndex, QSortFilterProxyModel, Qt -from typing import cast +from typing import cast, TypeVar, Optional, Any + + +T = TypeVar('T', bound=TreeItem) # Hierarchy Helpers -def coerceIndexToType(indexes, typ): +def coerceIndexToType(indexes: list[QModelIndex], typ: type[T]) -> list[QModelIndex]: """Get a list of indices of a specific type based on a given index list Items containing parents of the type fall down to their children Items containing children of the type climb up to their parents @@ -74,7 +80,9 @@ def coerceIndexToType(indexes, typ): return out -def coerceIndexToChildType(indexes, typ): +def coerceIndexToChildType( + indexes: list[QModelIndex], typ: type[T] +) -> list[QModelIndex]: """Get a list of indices of a specific type based on a given index list Lists containing parents of the type fall down to their children @@ -116,7 +124,9 @@ def coerceIndexToChildType(indexes, typ): return out -def coerceIndexToParentType(indexes, typ): +def coerceIndexToParentType( + indexes: list[QModelIndex], typ: type[T] +) -> list[QModelIndex]: """Get a list of indices of a specific type based on a given index list Lists containing children of the type climb up to their parents @@ -152,7 +162,7 @@ def coerceIndexToParentType(indexes, typ): return out -def coerceIndexToRoots(indexes): +def coerceIndexToRoots(indexes: list[QModelIndex]) -> list[QModelIndex]: """Get the topmost indexes for each brach in the hierarchy Parameters @@ -204,17 +214,19 @@ class SimplexModel(AdapterModel): """ @property - def simplex(self): + def simplex(self) -> Optional[TreeRootItem]: return self._rootItem - def headerData(self, section, orientation, role): + def headerData( + self, section: int, orientation: Qt.Orientation, role: int + ) -> Optional[str]: if orientation == Qt.Orientation.Horizontal: if role == Qt.ItemDataRole.DisplayRole: sects = ("Items", "Slide", "Value") return sects[section] return None - def flags(self, index): + def flags(self, index: QModelIndex) -> Qt.ItemFlag: if not index.isValid(): return Qt.ItemFlag.ItemIsEnabled if index.column() == 0: @@ -234,7 +246,9 @@ def flags(self, index): | Qt.ItemFlag.ItemIsEditable ) - def setData(self, index, value, role=Qt.ItemDataRole.EditRole): + def setData( + self, index: QModelIndex, value: Any, role: int = Qt.ItemDataRole.EditRole + ) -> bool: if not index.isValid(): return False if role == Qt.ItemDataRole.CheckStateRole: @@ -271,19 +285,19 @@ class BaseProxyModel(QSortFilterProxyModel): documentation will be lacking """ - def __init__(self, model, parent=None): + def __init__(self, model: SimplexModel, parent: Optional[QWidget] = None): super().__init__(parent) self.setSourceModel(model) def sourceModel(self) -> SimplexModel: return cast(SimplexModel, super().sourceModel()) - def indexFromItem(self, item, column=0): + def indexFromItem(self, item: TreeItem, column: int = 0) -> QModelIndex: sourceModel = self.sourceModel() sourceIndex = sourceModel.indexFromItem(item, column) return self.mapFromSource(sourceIndex) - def itemFromIndex(self, index): + def itemFromIndex(self, index: QModelIndex) -> Optional[TreeItem]: sourceModel = self.sourceModel() sIndex = self.mapToSource(index) return sourceModel.itemFromIndex(sIndex) @@ -300,12 +314,12 @@ def invalidateFilter(self): source.invalidateFilter() super().invalidateFilter() - def filterAcceptsRow(self, sourceRow, sourceParent) -> bool: + def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: return True class SliderModel(BaseProxyModel): - def filterAcceptsRow(self, sourceRow, sourceParent): + def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: sourceIndex = self.sourceModel().index(sourceRow, 0, sourceParent) if sourceIndex.isValid(): item = self.sourceModel().itemFromIndex(sourceIndex) @@ -316,7 +330,7 @@ def filterAcceptsRow(self, sourceRow, sourceParent): class ComboModel(BaseProxyModel): - def filterAcceptsRow(self, sourceRow, sourceParent): + def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: sourceIndex = self.sourceModel().index(sourceRow, 0, sourceParent) if sourceIndex.isValid(): item = self.sourceModel().itemFromIndex(sourceIndex) @@ -327,7 +341,7 @@ def filterAcceptsRow(self, sourceRow, sourceParent): class TraversalModel(BaseProxyModel): - def filterAcceptsRow(self, sourceRow, sourceParent): + def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: sourceIndex = self.sourceModel().index(sourceRow, 0, sourceParent) if sourceIndex.isValid(): item = self.sourceModel().itemFromIndex(sourceIndex) @@ -343,30 +357,32 @@ class SimplexFilterModel(BaseProxyModel): Set the `filterString` object property to filter the model """ - def __init__(self, model, parent=None): + def __init__(self, model: SimplexModel, parent: Optional[QWidget] = None): super().__init__(model, parent) self.setSourceModel(model) - self.filterShapes = True - self._filterString = [] - self._filterReg = [] - self.isolateList = [] + self.filterShapes: bool = True + self._filterString: list[str] = [] + self._filterReg: list[re.Pattern[str]] = [] + self.isolateList: list[str] = [] @property - def filterString(self): + def filterString(self) -> str: return " ".join(self._filterString) @filterString.setter - def filterString(self, val): + def filterString(self, val: str): self._filterString = val.split() self._filterReg = [] for sp in self._filterString: if sp[0] == "*": - self._filterReg.append(re.compile(sp, flags=re.I)) + rex = re.compile(sp, flags=re.I) + self._filterReg.append(rex) else: - self._filterReg.append(re.compile(".*?".join(sp), flags=re.I)) + rex = re.compile(".*?".join(sp), flags=re.I) + self._filterReg.append(rex) - def filterAcceptsRow(self, sourceRow, sourceParent): + def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: column = 0 # always sort by the first column #column = self.filterKeyColumn() sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) if sourceIndex.isValid(): @@ -380,7 +396,7 @@ def filterAcceptsRow(self, sourceRow, sourceParent): return super().filterAcceptsRow(sourceRow, sourceParent) - def matchFilterString(self, itemString): + def matchFilterString(self, itemString: str) -> bool: if not self._filterString: return True for reg in self._filterReg: @@ -388,15 +404,16 @@ def matchFilterString(self, itemString): return True return False - def matchIsolation(self, itemString): + def matchIsolation(self, itemString: str) -> bool: if self.isolateList: return itemString in self.isolateList return True - def checkChildren(self, sourceItem): - itemString = sourceItem.name - if self.matchFilterString(itemString) and self.matchIsolation(itemString): - return True + def checkChildren(self, sourceItem: TreeItem) -> bool: + if hasattr(sourceItem, 'name'): + itemString = sourceItem.name # type: ignore + if self.matchFilterString(itemString) and self.matchIsolation(itemString): + return True sourceModel = self.sourceModel().sourceModel() for row in range(sourceModel.getItemRowCount(sourceItem)): @@ -410,15 +427,14 @@ def checkChildren(self, sourceItem): class SliderFilterModel(SimplexFilterModel): """Hide single shapes under a slider""" - def __init__(self, model, parent=None): + def __init__(self, model: SimplexModel, parent: Optional[QWidget] = None): super().__init__(model, parent) - self.requires = [] - self.filterRequiresAny = False - self.filterRequiresAll = False + self.requires: list[Combo] = [] + self.filterRequiresAny: bool = False + self.filterRequiresAll: bool = False + self.doFilter: bool = True - self.doFilter = True - - def filterAcceptsRow(self, sourceRow, sourceParent): + def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: # always sort by the first column #column = self.filterKeyColumn() column = 0 sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) @@ -426,7 +442,9 @@ def filterAcceptsRow(self, sourceRow, sourceParent): data = self.sourceModel().itemFromIndex(sourceIndex) if self.doFilter: if isinstance(data, ProgPair): - if len(data.prog.pairs) <= 2: + if data.prog is None: + return False + elif len(data.prog.pairs) <= 2: return False elif data.shape.isRest: return False @@ -448,14 +466,14 @@ def filterAcceptsRow(self, sourceRow, sourceParent): class ComboFilterModel(SimplexFilterModel): """Filter by slider when Show Dependent Combos is checked""" - def __init__(self, model, parent=None): + def __init__(self, model: SimplexModel, parent: Optional[QWidget] = None): super().__init__(model, parent) - self.requires = [] - self.filterRequiresAll = False - self.filterRequiresAny = False - self.filterRequiresOnly = False + self.requires: list[Slider] = [] + self.filterRequiresAll: bool = False + self.filterRequiresAny: bool = False + self.filterRequiresOnly: bool = False - def filterAcceptsRow(self, sourceRow, sourceParent): + def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: # always sort by the first column #column = self.filterKeyColumn() column = 0 sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) @@ -468,7 +486,9 @@ def filterAcceptsRow(self, sourceRow, sourceParent): return False # Ignore shape things if requested if isinstance(data, ProgPair): - if len(data.prog.pairs) <= 2: + if data.prog is None: + return False + elif len(data.prog.pairs) <= 2: return False elif data.shape.isRest: return False @@ -496,18 +516,20 @@ def filterAcceptsRow(self, sourceRow, sourceParent): class TraversalFilterModel(SimplexFilterModel): """Hide single shapes under a slider""" - def __init__(self, model, parent=None): + def __init__(self, model: SimplexModel, parent: Optional[QWidget] = None): super().__init__(model, parent) self.doFilter = True - def filterAcceptsRow(self, sourceRow, sourceParent): + def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: column = 0 # always sort by the first column #column = self.filterKeyColumn() sourceIndex = self.sourceModel().index(sourceRow, column, sourceParent) if sourceIndex.isValid(): if self.doFilter: data = self.sourceModel().itemFromIndex(sourceIndex) if isinstance(data, ProgPair): - if len(data.prog.pairs) <= 2: + if data.prog is None: + return False + elif len(data.prog.pairs) <= 2: return False elif data.shape.isRest: return False @@ -518,17 +540,19 @@ def filterAcceptsRow(self, sourceRow, sourceParent): class FalloffDataModel(AdapterModel): """A model for displaying the data of Falloff objects""" - def __init__(self, simplex, parent): + def __init__(self, simplex: Simplex, parent: Optional[QWidget]): super().__init__(simplex, parent) self.simplex = simplex - def getItemAppendRow(self, item): + def getItemAppendRow(self, item: TreeItem) -> int: try: return len(self.simplex.falloffs) except AttributeError: return 0 - def index(self, row, column=0, parIndex=None): + def index( + self, row: int, column: int = 0, parIndex: Optional[QModelIndex] = None + ) -> QModelIndex: parIndex = QModelIndex() if parIndex is None else parIndex if row < 0: return QModelIndex() @@ -540,7 +564,7 @@ def index(self, row, column=0, parIndex=None): return QModelIndex() return self.createIndex(row, column, falloff) - def parent(self, index): + def parent(self, index: QModelIndex) -> QModelIndex: return QModelIndex() def rowCount(self, parent): @@ -549,10 +573,10 @@ def rowCount(self, parent): except AttributeError: return 0 - def columnCount(self, parent): + def columnCount(self, parent: QModelIndex) -> int: return 8 - def data(self, index, role): + def data(self, index: QModelIndex, role: int) -> Any: if not index.isValid(): return None falloff = index.internalPointer() @@ -582,7 +606,7 @@ def data(self, index, role): return falloff.mapName return None - def setData(self, index, value, role): + def setData(self, index: QModelIndex, value: Any, role: int) -> bool: if not index.isValid(): return False falloff = index.internalPointer() @@ -612,17 +636,17 @@ def setData(self, index, value, role): return True return False - def flags(self, index): + def flags(self, index: QModelIndex) -> Qt.ItemFlag: return ( Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsSelectable ) - def itemFromIndex(self, index): + def itemFromIndex(self, index: QModelIndex) -> TreeItem: return index.internalPointer() - def getItemRow(self, item): + def getItemRow(self, item: Falloff) -> Optional[int]: try: idx = self.simplex.falloffs.index(item) except ValueError: diff --git a/src/python/simplexui/items/__init__.py b/src/python/simplexui/items/__init__.py index 293c6f53..fc19d069 100644 --- a/src/python/simplexui/items/__init__.py +++ b/src/python/simplexui/items/__init__.py @@ -16,7 +16,7 @@ # along with Simplex. If not, see . from .combo import Combo, ComboPair -from .falloff import Falloff +from .falloff import Falloff, MapFalloff, PlanarFalloff from .group import Group from .progression import ProgPair, Progression from .shape import Shape @@ -24,13 +24,14 @@ from .slider import Slider from .stack import Stack, stackable from .traversal import Traversal, TravPair, TravPoint, TravSide +from .treeItem import TreeItem __all__ = [ "Combo", "ComboPair", "Falloff", "MapFalloff", - "PlanarFalloff" + "PlanarFalloff", "Group", "ProgPair", "Progression", @@ -43,4 +44,5 @@ "TravPoint", "TravPair", "TravSide", + "TreeItem", ] diff --git a/src/python/simplexui/items/treeItem.py b/src/python/simplexui/items/treeItem.py index f583d4ba..d024da15 100644 --- a/src/python/simplexui/items/treeItem.py +++ b/src/python/simplexui/items/treeItem.py @@ -126,6 +126,8 @@ def resetManager(self) -> SimpleGenerator: class TreeItem: + classDepth = -1 + def __init__(self, root: TreeRootItem): self.root: TreeRootItem = root self.uid: str = uuid.uuid4().hex # Unique identifier for tree expansion diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 6e6f4f6c..5f73a6b0 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -382,8 +382,8 @@ def setSystem(self, system): if system is None: # self.toolActions.simplex = None - self.uiSliderTREE.setModel(QStandardItemModel()) - self.uiComboTREE.setModel(QStandardItemModel()) + self.uiSliderTREE.setModel(QStandardItemModel()) # type: ignore + self.uiComboTREE.setModel(QStandardItemModel()) # type: ignore self.simplex = system self.uiMainShapesGRP.setEnabled(False) self.uiComboShapesGRP.setEnabled(False) From be7ae7854c2bdcd5e66da5f2b451f5eb912ec2fd Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 11 Sep 2026 16:28:11 -0700 Subject: [PATCH 38/47] Switch from old Optional/Union to | and make sure we're getting future annotations --- src/python/simplexui/__init__.py | 1 + src/python/simplexui/channelBox.py | 1 + src/python/simplexui/comboCheckDialog.py | 6 +- .../simplexui/commands/alembicCommon.py | 1 + .../simplexui/commands/applyCorrectives.py | 2 +- .../simplexui/commands/correctiveInterface.py | 1 + .../simplexui/commands/expandedExport.py | 1 + src/python/simplexui/commands/hdf5Convert.py | 1 + .../commands/mayaCorrectiveInterface.py | 1 + src/python/simplexui/commands/mayatonumpy.py | 10 +-- src/python/simplexui/commands/mesh.py | 6 +- src/python/simplexui/commands/poseblendlib.py | 1 + .../commands/reorderSimplexPoints.py | 1 + src/python/simplexui/commands/rigidAlign.py | 1 + src/python/simplexui/commands/smpxBlend.py | 3 +- src/python/simplexui/commands/unsubdivide.py | 1 + src/python/simplexui/commands/uvTransfer.py | 1 + src/python/simplexui/dragFilter.py | 12 ++-- src/python/simplexui/falloffDialog.py | 16 ++--- src/python/simplexui/interface/__init__.py | 1 + .../simplexui/interface/dummyInterface.py | 29 ++++---- .../simplexui/interface/mayaInterface.py | 2 +- src/python/simplexui/interfaceModel.py | 22 +++--- src/python/simplexui/items/__init__.py | 1 + src/python/simplexui/items/accessor.py | 9 +-- src/python/simplexui/items/combo.py | 20 +++--- src/python/simplexui/items/falloff.py | 20 +++--- src/python/simplexui/items/group.py | 10 +-- src/python/simplexui/items/progression.py | 24 +++---- src/python/simplexui/items/shape.py | 14 ++-- src/python/simplexui/items/simplex.py | 70 +++++++++---------- src/python/simplexui/items/slider.py | 19 +++-- src/python/simplexui/items/stack.py | 6 +- src/python/simplexui/items/traversal.py | 22 +++--- src/python/simplexui/items/treeItem.py | 44 ++++++------ src/python/simplexui/menu/__init__.py | 1 + .../genericPlugins/checkPossibleCombos.py | 1 + .../menu/genericPlugins/exportSplit.py | 3 +- .../menu/genericPlugins/showFalloffs.py | 1 + .../menu/genericPlugins/showTraversals.py | 1 + .../menu/genericPlugins/simplexUvTransfer.py | 1 + .../menu/genericPlugins/unsubdivide.py | 1 + .../simplexui/menu/mayaPlugins/exportOther.py | 1 + .../menu/mayaPlugins/extractProgressives.py | 1 + .../simplexui/menu/mayaPlugins/freezeCombo.py | 1 + .../mayaPlugins/generateShapeIncrementals.py | 1 + .../simplexui/menu/mayaPlugins/importObjs.py | 1 + .../menu/mayaPlugins/linearizeTraversal.py | 1 + .../menu/mayaPlugins/makeShelfBtn.py | 1 + .../menu/mayaPlugins/relaxToSelection.py | 1 + .../menu/mayaPlugins/reloadDefinition.py | 1 + .../menu/mayaPlugins/snapToNeutral.py | 1 + .../menu/mayaPlugins/softSelectToCluster.py | 1 + .../simplexui/menu/mayaPlugins/tweakMix.py | 1 + .../menu/mayaPlugins/updateRestShape.py | 1 + src/python/simplexui/travCheckDialog.py | 1 + src/python/simplexui/utils.py | 14 ++-- 57 files changed, 220 insertions(+), 197 deletions(-) diff --git a/src/python/simplexui/__init__.py b/src/python/simplexui/__init__.py index 25d066cb..bb80ceff 100644 --- a/src/python/simplexui/__init__.py +++ b/src/python/simplexui/__init__.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from ._version import __version__ # noqa: F401 SIMPLEX_UI = None diff --git a/src/python/simplexui/channelBox.py b/src/python/simplexui/channelBox.py index 16727653..35054e49 100644 --- a/src/python/simplexui/channelBox.py +++ b/src/python/simplexui/channelBox.py @@ -19,6 +19,7 @@ A Super-minimal ui for interacting with a Simplex System Currently VERY WIP. Probably shouldn't have committed it to master, but whatever """ + from __future__ import annotations import os import sys diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index 05832c4d..e978359a 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -60,7 +60,7 @@ def buildPossibleCombos( sliders: list[Slider], minDepth: int, maxDepth: int, - lockDict: Optional[dict[Slider, tuple[float, ...]]] = None, + lockDict: dict[Slider, tuple[float, ...]] | None = None, maxPoss: int = 100, ) -> tuple[bool, list[tuple[list[tuple[Slider, float]], Combo]]]: """Build a list of possible combos @@ -188,9 +188,9 @@ class ComboCheckDialog(QDialog): def __init__( self, sliders: list[Slider], - values: Optional[dict[Slider, list[float]]] = None, + values: dict[Slider, list[float]] | None = None, mode: str = "create", - parent: Optional[SimplexDialog] = None, + parent: SimplexDialog | None = None, ): if parent is None: raise ValueError("Parent must not be None") diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 2812b0e6..f17ce353 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -19,6 +19,7 @@ This is a library of convenience functions with the numpy speed optimizations """ +from __future__ import annotations import os from alembic.Abc import ( diff --git a/src/python/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py index 5e5af325..dbf6f58b 100644 --- a/src/python/simplexui/commands/applyCorrectives.py +++ b/src/python/simplexui/commands/applyCorrectives.py @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import itertools import os import numpy as np @@ -30,7 +31,6 @@ ) - def invertAll(matrixArray): """Invert all the square sub-matrices in a numpy array diff --git a/src/python/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py index 0107fdd1..652e6dd8 100644 --- a/src/python/simplexui/commands/correctiveInterface.py +++ b/src/python/simplexui/commands/correctiveInterface.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from Qt.QtWidgets import QApplication import numpy as np from .mayaCorrectiveInterface import getShiftValues, resetPose, setPose diff --git a/src/python/simplexui/commands/expandedExport.py b/src/python/simplexui/commands/expandedExport.py index 323847fc..24f334d2 100644 --- a/src/python/simplexui/commands/expandedExport.py +++ b/src/python/simplexui/commands/expandedExport.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from pysimplex import PySimplex from ..interface.mayaInterface import DCC, disconnected diff --git a/src/python/simplexui/commands/hdf5Convert.py b/src/python/simplexui/commands/hdf5Convert.py index 46f2ea50..2aa9c343 100644 --- a/src/python/simplexui/commands/hdf5Convert.py +++ b/src/python/simplexui/commands/hdf5Convert.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import json from .alembicCommon import buildSmpx, readSmpx diff --git a/src/python/simplexui/commands/mayaCorrectiveInterface.py b/src/python/simplexui/commands/mayaCorrectiveInterface.py index 11186574..387182e1 100644 --- a/src/python/simplexui/commands/mayaCorrectiveInterface.py +++ b/src/python/simplexui/commands/mayaCorrectiveInterface.py @@ -17,6 +17,7 @@ """Get the corrective deltas from a rig in Maya""" +from __future__ import annotations from ctypes import c_float from maya import OpenMaya as om diff --git a/src/python/simplexui/commands/mayatonumpy.py b/src/python/simplexui/commands/mayatonumpy.py index 3212c739..1645e540 100644 --- a/src/python/simplexui/commands/mayatonumpy.py +++ b/src/python/simplexui/commands/mayatonumpy.py @@ -85,7 +85,7 @@ def _swigConnect( def _swigConnectMatrix( - mat: om.MMatrix, ctp: Union[type[c_double], type[c_float]] + mat: om.MMatrix, ctp: type[c_double] | type[c_float] ) -> tuple[np.ndarray, SwigPyObject]: # With a matrix, you can just get the double[4][4] without an MScriptUtil ptr: SwigPyObject = mat.matrix @@ -212,8 +212,8 @@ def numpyToMaya(ary: np.ndarray, mType: type[T]) -> T: def getNumpyAttr( - attrName: Union[om.MPlug, str], -) -> Union[np.ndarray, float, int, tuple[int, ...], tuple[float, ...]]: + attrName: om.MPlug | str, +) -> np.ndarray | float | int | tuple[int, ...] | tuple[float, ...]: """Read attribute data directly from the plugs into numpy This function will read most numeric data types directly into numpy arrays @@ -299,8 +299,8 @@ def getNumpyAttr( def setNumpyAttr( - attrName: Union[str, om.MPlug], - value: Union[np.ndarray, float, int, tuple[int, ...], tuple[float, ...]], + attrName: str | om.MPlug, + value: np.ndarray | float | int | tuple[int, ...] | tuple[float, ...], ): """Write a numpy array directly into a maya plug diff --git a/src/python/simplexui/commands/mesh.py b/src/python/simplexui/commands/mesh.py index 22c3810d..bb36eb33 100644 --- a/src/python/simplexui/commands/mesh.py +++ b/src/python/simplexui/commands/mesh.py @@ -28,8 +28,10 @@ VertSet and FaceSet classes are just sets that also contain references back to the mesh """ +from __future__ import annotations -class Mesh(object): + +class Mesh: """ The inputs to this mesh object are inspired by the .obj file format @@ -600,7 +602,7 @@ def clearCache(self): ####################################################################################### -class MeshComponent(object): +class MeshComponent: """Base class for all mesh components Handles keeping track of the mesh and index diff --git a/src/python/simplexui/commands/poseblendlib.py b/src/python/simplexui/commands/poseblendlib.py index c23b9263..f396ff9e 100644 --- a/src/python/simplexui/commands/poseblendlib.py +++ b/src/python/simplexui/commands/poseblendlib.py @@ -1,3 +1,4 @@ +from __future__ import annotations import numpy as np # I could have used the Scipy rotations library, but it doeosn't deal diff --git a/src/python/simplexui/commands/reorderSimplexPoints.py b/src/python/simplexui/commands/reorderSimplexPoints.py index 3adadc63..0730a701 100644 --- a/src/python/simplexui/commands/reorderSimplexPoints.py +++ b/src/python/simplexui/commands/reorderSimplexPoints.py @@ -26,6 +26,7 @@ possible to invert the range if you think you've got it backwards """ +from __future__ import annotations import json from .alembicCommon import buildSmpx, readSmpx import numpy as np diff --git a/src/python/simplexui/commands/rigidAlign.py b/src/python/simplexui/commands/rigidAlign.py index f3ec217b..6b457376 100644 --- a/src/python/simplexui/commands/rigidAlign.py +++ b/src/python/simplexui/commands/rigidAlign.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import numpy as np diff --git a/src/python/simplexui/commands/smpxBlend.py b/src/python/simplexui/commands/smpxBlend.py index e9730db7..d7673382 100644 --- a/src/python/simplexui/commands/smpxBlend.py +++ b/src/python/simplexui/commands/smpxBlend.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from ..items import ( Combo, ComboPair, @@ -30,7 +31,7 @@ ) -class Skip(object): +class Skip: """A pseudo-singleton object to signify completely skipping the un-matched shape This will be used by running ``smpxMismatchCheck``, taking the dict, and manually telling it to ``Skip`` certain shapes diff --git a/src/python/simplexui/commands/unsubdivide.py b/src/python/simplexui/commands/unsubdivide.py index 25e2b829..ea0003f3 100644 --- a/src/python/simplexui/commands/unsubdivide.py +++ b/src/python/simplexui/commands/unsubdivide.py @@ -14,6 +14,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import json from itertools import chain, zip_longest diff --git a/src/python/simplexui/commands/uvTransfer.py b/src/python/simplexui/commands/uvTransfer.py index d5f3e62c..479e78c4 100644 --- a/src/python/simplexui/commands/uvTransfer.py +++ b/src/python/simplexui/commands/uvTransfer.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import numpy as np INF = float("inf") diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index a11d36aa..91eb4edd 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -89,10 +89,8 @@ class DragFilter(QObject): dragPressed: Signal = Signal() dragReleased: Signal = Signal() - def __init__(self, parent: Optional[QWidget]): - self._parent: Optional[QWidget] = ( - parent # Hold onto this to keep the qwidget type - ) + def __init__(self, parent: QWidget | None): + self._parent: QWidget | None = parent # Hold onto this to keep the qwidget type super().__init__(parent) self.dragSensitivity: int = 5 # pixels for one step @@ -114,13 +112,13 @@ def __init__(self, parent: Optional[QWidget]): self.slowDivisor: float = 5.0 # private vars - self._lastPos: Optional[QPoint] = None + self._lastPos: QPoint | None = None self._leftover: float = 0.0 - self._dragStart: Optional[QPoint] = None + self._dragStart: QPoint | None = None self._firstDrag: bool = False self._dragType: DragType = DragType.DRAG_NONE self._overridden: bool = False - self._screen: Optional[QRect] = None + self._screen: QRect | None = None self._isDragging: bool = False def doOverrideCursor(self): diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index e0473cc5..f1ea151e 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -68,10 +68,10 @@ class CurveEditWidget(QWidget): tangentUpdated = Signal(float, float) - def __init__(self, parent: Optional[QWidget]): + def __init__(self, parent: QWidget | None): super().__init__(parent) - self.leftTan: Optional[float] = None - self.rightTan: Optional[float] = None + self.leftTan: float | None = None + self.rightTan: float | None = None self._controlPoints: list[QPointF] = [ QPointF(0, 1), QPointF(0, 1), @@ -80,7 +80,7 @@ def __init__(self, parent: Optional[QWidget]): ] self.setTangent(leftTan=1 / 3.0, rightTan=2 / 3.0) - self._activeControlPoint: Optional[int] = None + self._activeControlPoint: int | None = None self.mouseDrag: bool = False self.mousePress: QPoint = QPoint() self.startDragDistance: int = 20 @@ -92,9 +92,7 @@ def __init__(self, parent: Optional[QWidget]): self.lineColor: Qt.GlobalColor = Qt.GlobalColor.black self.limitColor: Qt.GlobalColor = Qt.GlobalColor.gray - def setTangent( - self, leftTan: Optional[float] = None, rightTan: Optional[float] = None - ): + def setTangent(self, leftTan: float | None = None, rightTan: float | None = None): """Set the falloff tangents, clamped 0 to 1 Parameters @@ -253,7 +251,7 @@ def paintControlPoint( ) ) - def findControlPoint(self, point: QPointF, tolerance: int = 10) -> Optional[int]: + def findControlPoint(self, point: QPointF, tolerance: int = 10) -> int | None: d = QLineF(self.mapToCanvas(self._controlPoints[1]), point).length() if d < tolerance: return 1 @@ -319,7 +317,7 @@ def __init__(self, parent: SimplexDialog): QtCompat.loadUi(uiPath, self) self.parUI: SimplexDialog = parent - self.simplex: Optional[Simplex] = None + self.simplex: Simplex | None = None self.parUI.simplexLoaded.connect(self.loadSimplex) self.foModel = FalloffDataModel(None, self) diff --git a/src/python/simplexui/interface/__init__.py b/src/python/simplexui/interface/__init__.py index d577438f..00f369a9 100644 --- a/src/python/simplexui/interface/__init__.py +++ b/src/python/simplexui/interface/__init__.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . # This file will serve as the only place where the choice of DCC will be chosen +from __future__ import annotations import os import sys diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index d998dc62..5bd6ff29 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . """A placeholder interface that takes arguments and does nothing with them""" +from __future__ import annotations import copy from contextlib import contextmanager from functools import wraps @@ -73,7 +74,7 @@ def stacker(self, *args, **kwargs): return stacker -class DummyScene(object): +class DummyScene: """A dcc scene containing existing dummy objects""" def __init__(self): @@ -94,7 +95,7 @@ def remove(self, item): DB = DummyScene() # Default module level dummy scene -class DummyAttr(object): +class DummyAttr: """A generic named object attribute""" def __init__(self, name, value, parent): @@ -104,7 +105,7 @@ def __init__(self, name, value, parent): parent.attrs[name] = self -class DummyNode(object): +class DummyNode: """A generic DCC node""" def __init__(self, name, db=DB): @@ -132,7 +133,7 @@ def __init__(self, name, parent, db=DB): self.weightmap = None -class DummyShape(object): +class DummyShape: """A generic named blendshape shape""" def __init__(self, name, shapeNode): @@ -166,7 +167,7 @@ def __init__(self, name, db=DB): self.verts = None -class DCC(object): +class DCC: program = "dummy" def __init__(self, simplex, stack=None): @@ -708,33 +709,33 @@ def createShape(self, shape, live=False, offset=10): @undoable def extractWithDeltaShape(self, shape, live=True, offset=10.0): """Make a mesh representing a shape. Can be live or not. - Also, make a shapenode that is the delta of the change being made + Also, make a shapenode that is the delta of the change being made """ pass @undoable def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0): """Extract a shape with a live partial delta added in. - Useful for updating progressive shapes + Useful for updating progressive shapes """ pass @undoable def extractShape(self, shape, live=True, offset=10.0): """Make a mesh representing a shape. Can be live or not. - Can also store its starting shape and delta data + Can also store its starting shape and delta data """ pass @undoable def connectShape(self, shape, mesh=None, live=False, delete=False): """Force a shape to match a mesh - The "connect shape" button is: - mesh=None, delete=True - The "match shape" button is: - mesh=someMesh, delete=False - There is a possibility of a "make live" button: - live=True, delete=False + The "connect shape" button is: + mesh=None, delete=True + The "match shape" button is: + mesh=someMesh, delete=False + There is a possibility of a "make live" button: + live=True, delete=False """ pass diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 36475cde..4a4fa435 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -126,7 +126,7 @@ def split_trailing_digits(s: str) -> tuple[str, str]: return s, "" -class DCC(object): +class DCC: program = "maya" def __init__(self, simplex, stack=None): diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index aa8b9b2c..895e683f 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -214,12 +214,12 @@ class SimplexModel(AdapterModel): """ @property - def simplex(self) -> Optional[TreeRootItem]: + def simplex(self) -> TreeRootItem | None: return self._rootItem def headerData( self, section: int, orientation: Qt.Orientation, role: int - ) -> Optional[str]: + ) -> str | None: if orientation == Qt.Orientation.Horizontal: if role == Qt.ItemDataRole.DisplayRole: sects = ("Items", "Slide", "Value") @@ -285,7 +285,7 @@ class BaseProxyModel(QSortFilterProxyModel): documentation will be lacking """ - def __init__(self, model: SimplexModel, parent: Optional[QWidget] = None): + def __init__(self, model: SimplexModel, parent: QWidget | None = None): super().__init__(parent) self.setSourceModel(model) @@ -297,7 +297,7 @@ def indexFromItem(self, item: TreeItem, column: int = 0) -> QModelIndex: sourceIndex = sourceModel.indexFromItem(item, column) return self.mapFromSource(sourceIndex) - def itemFromIndex(self, index: QModelIndex) -> Optional[TreeItem]: + def itemFromIndex(self, index: QModelIndex) -> TreeItem | None: sourceModel = self.sourceModel() sIndex = self.mapToSource(index) return sourceModel.itemFromIndex(sIndex) @@ -357,7 +357,7 @@ class SimplexFilterModel(BaseProxyModel): Set the `filterString` object property to filter the model """ - def __init__(self, model: SimplexModel, parent: Optional[QWidget] = None): + def __init__(self, model: SimplexModel, parent: QWidget | None = None): super().__init__(model, parent) self.setSourceModel(model) self.filterShapes: bool = True @@ -427,7 +427,7 @@ def checkChildren(self, sourceItem: TreeItem) -> bool: class SliderFilterModel(SimplexFilterModel): """Hide single shapes under a slider""" - def __init__(self, model: SimplexModel, parent: Optional[QWidget] = None): + def __init__(self, model: SimplexModel, parent: QWidget | None = None): super().__init__(model, parent) self.requires: list[Combo] = [] self.filterRequiresAny: bool = False @@ -466,7 +466,7 @@ def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: class ComboFilterModel(SimplexFilterModel): """Filter by slider when Show Dependent Combos is checked""" - def __init__(self, model: SimplexModel, parent: Optional[QWidget] = None): + def __init__(self, model: SimplexModel, parent: QWidget | None = None): super().__init__(model, parent) self.requires: list[Slider] = [] self.filterRequiresAll: bool = False @@ -516,7 +516,7 @@ def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: class TraversalFilterModel(SimplexFilterModel): """Hide single shapes under a slider""" - def __init__(self, model: SimplexModel, parent: Optional[QWidget] = None): + def __init__(self, model: SimplexModel, parent: QWidget | None = None): super().__init__(model, parent) self.doFilter = True @@ -540,7 +540,7 @@ def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: class FalloffDataModel(AdapterModel): """A model for displaying the data of Falloff objects""" - def __init__(self, simplex: Simplex, parent: Optional[QWidget]): + def __init__(self, simplex: Simplex, parent: QWidget | None): super().__init__(simplex, parent) self.simplex = simplex @@ -551,7 +551,7 @@ def getItemAppendRow(self, item: TreeItem) -> int: return 0 def index( - self, row: int, column: int = 0, parIndex: Optional[QModelIndex] = None + self, row: int, column: int = 0, parIndex: QModelIndex | None = None ) -> QModelIndex: parIndex = QModelIndex() if parIndex is None else parIndex if row < 0: @@ -646,7 +646,7 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlag: def itemFromIndex(self, index: QModelIndex) -> TreeItem: return index.internalPointer() - def getItemRow(self, item: Falloff) -> Optional[int]: + def getItemRow(self, item: Falloff) -> int | None: try: idx = self.simplex.falloffs.index(item) except ValueError: diff --git a/src/python/simplexui/items/__init__.py b/src/python/simplexui/items/__init__.py index fc19d069..f7ac0622 100644 --- a/src/python/simplexui/items/__init__.py +++ b/src/python/simplexui/items/__init__.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from .combo import Combo, ComboPair from .falloff import Falloff, MapFalloff, PlanarFalloff from .group import Group diff --git a/src/python/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py index 93ddc4ce..dfb5f3c1 100644 --- a/src/python/simplexui/items/accessor.py +++ b/src/python/simplexui/items/accessor.py @@ -26,7 +26,7 @@ from .stack import Stack -class SimplexAccessor(object): +class SimplexAccessor: """The base object for all Simplex System object types This class provides access to the simplex system name getters/setters/unifiers, proper deepcopying, and abstract tree lookup @@ -81,9 +81,7 @@ def buildLinkedRename( self, newName: str, maxDepth: int = 5, - currentLinks: Optional[ - dict[type, dict[str, tuple[SimplexAccessor, int]]] - ] = None, + currentLinks: dict[type, dict[str, tuple[SimplexAccessor, int]]] | None = None, ) -> dict[type, dict[str, tuple[SimplexAccessor, int]]]: """For the Shape, Slider, Combo, and Traversal items, build a linked rename dictionary like {itemType: {newName: (item, maxDepth)}} recursively up to a @@ -142,7 +140,6 @@ def buildLinkedRename( class SimplexTreeAccessor(SimplexAccessor, TreeItem): def __init__(self, simplex: Simplex): - # Explicitly + # Explicitly SimplexAccessor.__init__(self, simplex) TreeItem.__init__(self, simplex) - diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index 9c2f7c86..91b6303b 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -41,7 +41,7 @@ def __init__(self, slider: Slider, value: float): super().__init__(slider.simplex) self.slider: Slider = slider self._value: float = float(value) - self.combo: Optional[Combo] = None + self.combo: Combo | None = None @property def name(self) -> str: @@ -65,10 +65,10 @@ def treeRow(self) -> int: return 0 return self.combo.pairs.index(self) - def treeParent(self) -> Optional[TreeItem]: + def treeParent(self) -> TreeItem | None: return self.combo - def treeData(self, column: int) -> Optional[Any]: + def treeData(self, column: int) -> Any | None: if column == 0: return self.name if column == 1: @@ -119,7 +119,7 @@ def __init__( pairs: list[ComboPair], prog: Progression, group: Group, - solveType: Optional[str], + solveType: str | None, ): super().__init__(simplex) @@ -130,7 +130,7 @@ def __init__( self.pairs = pairs self.prog = prog self.prog.controller = self - self._solveType: Optional[str] = solveType + self._solveType: str | None = solveType self._buildIdx = None self.expanded = {} self._enabled = True @@ -173,7 +173,7 @@ def freezeThing(self, value: Any): @classmethod def comboAlreadyExists( cls, simplex: Simplex, sliders: list[Slider], values: list[float] - ) -> Optional[Combo]: + ) -> Combo | None: """Classmethod to check whether a combo already exists with these sliders and values Parameters @@ -204,9 +204,9 @@ def createCombo( simplex: Simplex, sliders: list[Slider], values: list[float], - group: Optional[Group] = None, - shape: Optional[Shape] = None, - solveType: Optional[str] = None, + group: Group | None = None, + shape: Shape | None = None, + solveType: str | None = None, tVal: float = 1.0, ): """Classmethod to create Combo with some hard-coded defaults @@ -639,7 +639,7 @@ def treeChildCount(self) -> int: def treeChecked(self) -> bool: return self.enabled - def treeData(self, column: int) -> Optional[Any]: + def treeData(self, column: int) -> Any | None: if column == 0: return self.name return None diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index 175622ed..d104d511 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -133,17 +133,17 @@ class Falloff(SimplexAccessor): def __init__(self, name: str, simplex: Simplex, axis: str): super().__init__(simplex) with self.stack.store(self): - self._search: Optional[str] = None - self._rep: Optional[str] = None - self._weights: Optional[npt.NDArray] = None - self._verts: Optional[npt.NDArray] = None - self._thing: Optional[DCCObject] = None - self._thingRepr: Optional[str] = None + self._search: str | None = None + self._rep: str | None = None + self._weights: npt.NDArray | None = None + self._verts: npt.NDArray | None = None + self._thing: DCCObject | None = None + self._thingRepr: str | None = None self._axis: str = axis self._name: str = name self.children: list[Progression] = [] - self._buildIdx: Optional[int] = None + self._buildIdx: int | None = None self.simplex.falloffs.append(self) @property @@ -283,7 +283,7 @@ def axis(self, value: str): # TODO: Does this need to update the dcc?? @property - def verts(self) -> Optional[npt.NDArray]: + def verts(self) -> npt.NDArray | None: """Get the stored vertex values""" return self._verts @@ -299,7 +299,7 @@ def verts(self, vals: npt.NDArray): self._verts = vals @property - def weights(self) -> Optional[npt.NDArray]: + def weights(self) -> npt.NDArray | None: """Get the per-vertex weight values""" return self._weights @@ -507,7 +507,7 @@ def minVal(self, value: float): self._updateDCC() @property - def verts(self) -> Optional[npt.NDArray]: + def verts(self) -> npt.NDArray | None: """Get the stored vertex values""" return self._verts diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index 7d9b0a81..a5a6a1fe 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -64,8 +64,8 @@ def __init__( with self.stack.store(self): self._name: str = name - self.items: list[Union[Combo, Slider, Traversal]] = [] - self._buildIdx: Optional[int] = None + self.items: list[Combo | Slider | Traversal] = [] + self._buildIdx: int | None = None self.groupType: GroupType = groupType with self.insertItemManager(simplex, row=self._getInsertionRow()): @@ -108,8 +108,8 @@ def createGroup( cls, name: str, simplex: Simplex, - things: Optional[list[GroupMember]] = None, - groupType: Optional[GroupType] = None, + things: list[GroupMember] | None = None, + groupType: GroupType | None = None, ) -> Group: """Convenience method for creating a group @@ -260,7 +260,7 @@ def treeParent(self) -> TreeItem: def treeChildCount(self) -> int: return len(self.items) - def treeData(self, column: int) -> Optional[str]: + def treeData(self, column: int) -> str | None: if column == 0: return self.name return None diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index b7b1fe57..6cd59188 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -40,7 +40,7 @@ def __init__(self, simplex: Simplex, shape: Shape, value: float): super().__init__(simplex) self.shape: Shape = shape self._value: float = value - self.prog: Optional[Progression] = None + self.prog: Progression | None = None self.minValue: float = -1.0 self.maxValue: float = 1.0 if not shape.isRest and self not in self.shape.progPairs: @@ -100,7 +100,7 @@ def treeRow(self) -> int: return 0 return self.prog.pairs.index(self) - def treeParent(self) -> Optional[TreeItem]: + def treeParent(self) -> TreeItem | None: from .slider import Slider if self.prog is None: @@ -111,7 +111,7 @@ def treeParent(self) -> Optional[TreeItem]: par = par.controller return par - def treeData(self, column) -> Optional[Any]: + def treeData(self, column) -> Any | None: if column == 0: return self.name if column == 2: @@ -165,9 +165,9 @@ def __init__( self, name: str, simplex: Simplex, - pairs: Optional[list[ProgPair]] = None, + pairs: list[ProgPair] | None = None, interp: str = "spline", - falloffs: Optional[list[Falloff]] = None, + falloffs: list[Falloff] | None = None, ): super().__init__(simplex) @@ -178,7 +178,7 @@ def __init__( self._name: str = name self._interp: str = interp self.falloffs: list[Falloff] = falloffs or [] - self.controller: Optional[Union[Slider, Combo, Traversal]] = None + self.controller: Slider | Combo | Traversal | None = None self.pairs: list[ProgPair] if pairs is None: @@ -191,7 +191,7 @@ def __init__( for falloff in self.falloffs: falloff.children.append(self) - self._buildIdx: Optional[int] = None + self._buildIdx: int | None = None @property def name(self) -> str: @@ -276,7 +276,7 @@ def getInsertIndex(self, tVal: float) -> int: return i return 0 - def getShapeAtValue(self, val: float, tol=0.0001) -> Optional[Shape]: + def getShapeAtValue(self, val: float, tol=0.0001) -> Shape | None: """Return the shape at the given value Parameters @@ -430,7 +430,7 @@ def removeFalloff(self, falloff: Falloff): @stackable def createShape( - self, shapeName: Optional[str] = None, tVal: Optional[float] = None + self, shapeName: str | None = None, tVal: float | None = None ) -> ProgPair: """Create a shape and add it to a progression @@ -461,7 +461,7 @@ def createShape( return pp def newProgPair( - self, shapeName: Optional[str] = None, tVal: Optional[float] = None + self, shapeName: str | None = None, tVal: float | None = None ) -> tuple[ProgPair, int]: """Create a shape and DO NOT add it to a progression @@ -603,13 +603,13 @@ def treeRow(self) -> int: return len(self.controller.pairs) return 0 - def treeParent(self) -> Optional[TreeItem]: + def treeParent(self) -> TreeItem | None: return self.controller def treeChildCount(self): return len(self.pairs) - def treeData(self, column: int) -> Optional[Any]: + def treeData(self, column: int) -> Any | None: if column == 0: return "SHAPES" return None diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index a692c0ad..96be398e 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -54,11 +54,11 @@ class Shape(SimplexAccessor): def __init__(self, name: str, simplex: Simplex, create: bool = True): super().__init__(simplex) with self.stack.store(self): - self._thing: Optional[DCCObject] = None - self._verts: Optional[npt.NDArray] = None - self._thingRepr: Optional[str] = None + self._thing: DCCObject | None = None + self._verts: npt.NDArray | None = None + self._thingRepr: str | None = None self._name: str = name - self._buildIdx: Optional[int] = None + self._buildIdx: int | None = None simplex.shapes.append(self) self.isRest: bool = False self.progPairs: list[ProgPair] = [] @@ -76,7 +76,7 @@ def __init__(self, name: str, simplex: Simplex, create: bool = True): @classmethod def createShape( - cls, name: str, simplex: Simplex, slider: Optional[Slider] = None + cls, name: str, simplex: Simplex, slider: Slider | None = None ) -> Shape: """Convenience method for creating a new shape This will create all required parent objects to have a new shape @@ -306,7 +306,7 @@ def zeroShapes(shapes): shape.zeroShape() def connectShape( - self, mesh: Optional[DCCObject] = None, live: bool = False, delete: bool = False + self, mesh: DCCObject | None = None, live: bool = False, delete: bool = False ): """Force a shape to match a mesh The "connect shape" button is: mesh=None, delete=True @@ -371,7 +371,7 @@ def isNumberField(val: str) -> bool: return val.isdigit() @property - def verts(self) -> Optional[npt.NDArray]: + def verts(self) -> npt.NDArray | None: """Get the stored vertices""" if self._verts is None: self._verts = self.DCC.getShapeVertices(self) diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index cf1f994d..4240fbe9 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -92,7 +92,7 @@ def __init__(self, name: str = "", forceDummy=False, sliderMul=1.0): self.traversalGroups: list[Group] = [] # List of groups containing traversals self.falloffs: list[Falloff] = [] # List of contained falloff objects self.shapes: list[Shape] = [] # List of contained shape objects - self.restShape: Optional[Shape] = None # Quick access to the rest shape + self.restShape: Shape | None = None # Quick access to the rest shape self.clusterName: str = "Shape" # Name of the cluster (XSI use only) self.DCC = DummyDCC(self) if forceDummy else DCC(self) # Interface to the DCC self.stack: Stack = Stack() # Reference to the Undo stack @@ -159,7 +159,7 @@ def _initValues(self): # Alternate Constructors @classmethod def buildBaseObject( - cls, smpxPath: str, name: Optional[str] = None, forceDummy: bool = False + cls, smpxPath: str, name: str | None = None, forceDummy: bool = False ): """Build the rest object from a .smpx file @@ -225,11 +225,11 @@ def buildEmptySystem( def buildSystemFromJsonString( cls, jsString: str, - thing: Optional[DCCObject] = None, - name: Optional[str] = None, + thing: DCCObject | None = None, + name: str | None = None, forceDummy: bool = False, sliderMul: float = 1.0, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ) -> Simplex: """Build a system from a json encoded string @@ -267,11 +267,11 @@ def buildSystemFromJsonString( def buildSystemFromJson( cls, jsPath: str, - thing: Optional[DCCObject] = None, - name: Optional[str] = None, + thing: DCCObject | None = None, + name: str | None = None, forceDummy: bool = False, sliderMul: float = 1.0, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ) -> Simplex: """Build a system from .json file @@ -313,11 +313,11 @@ def buildSystemFromJson( def buildSystemFromSmpx( cls, smpxPath: str, - thing: Optional[DCCObject] = None, - name: Optional[str] = None, + thing: DCCObject | None = None, + name: str | None = None, forceDummy: bool = False, sliderMul: float = 1.0, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ) -> Simplex: """Build a system from a .smpx file SMPX files are (under-the-hood) alembic caches with each shape delta stored as a frame of animation, @@ -375,11 +375,11 @@ def buildSystemFromSmpx( def buildSystemFromFile( cls, path: str, - thing: Optional[DCCObject] = None, - name: Optional[str] = None, + thing: DCCObject | None = None, + name: str | None = None, forceDummy: bool = False, sliderMul: float = 1.0, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ) -> Simplex: """Build a system from a file @@ -436,7 +436,7 @@ def buildSystemFromMesh( name: str, forceDummy: bool = False, sliderMul: float = 1.0, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ) -> Simplex: """Build a system from the data already built in the DCC @@ -477,11 +477,11 @@ def buildSystemFromDict( cls, jsDict: dict[str, Any], thing: DCCObject, - name: Optional[str] = None, + name: str | None = None, create: bool = True, forceDummy: bool = False, sliderMul: float = 1.0, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ) -> Simplex: """Utility for building a cleared system from a dictionary @@ -518,7 +518,7 @@ def buildSystemFromDict( self.loadDefinition(jsDict, create=create, pBar=pBar) return self - def loadSmpxShapes(self, smpxPath: str, pBar: Optional[QProgressDialog] = None): + def loadSmpxShapes(self, smpxPath: str, pBar: QProgressDialog | None = None): """Load the Shapes from a .smpx file onto an already loaded system This is the "We got updated shapes from the modelers" method @@ -537,7 +537,7 @@ def loadSmpxShapes(self, smpxPath: str, pBar: Optional[QProgressDialog] = None): finally: del abcMesh, iarch - def loadSmpxPoses(self, smpxPath: str, pBar: Optional[QProgressDialog] = None): + def loadSmpxPoses(self, smpxPath: str, pBar: QProgressDialog | None = None): """Load the Poses from a .smpx file onto an already loaded system This is the "Update the joints and skin" method @@ -556,7 +556,7 @@ def loadSmpxPoses(self, smpxPath: str, pBar: Optional[QProgressDialog] = None): finally: del abcMesh, iarch - def loadSmpxFalloffs(self, abcPath: str, pBar: Optional[QProgressDialog] = None): + def loadSmpxFalloffs(self, abcPath: str, pBar: QProgressDialog | None = None): """Load the relevant data from a simplex alembic Parameters @@ -609,9 +609,7 @@ def groups(self) -> list[Group]: return self.sliderGroups + self.comboGroups + self.traversalGroups # HELPER - def comboExists( - self, sliders: list[Slider], values: list[float] - ) -> Optional[Combo]: + def comboExists(self, sliders: list[Slider], values: list[float]) -> Combo | None: """Check if a combo exists with these specific sliders and values Because combo names aren't necessarily always in the same order @@ -839,7 +837,7 @@ def loadDefinition( self, simpDict: dict[str, Any], create: bool = True, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ): """Build the structure of objects in this system based on a provided dictionary @@ -888,7 +886,7 @@ def loadV3( self, simpDict: dict[str, Any], create: bool = True, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ): """Load the version 3 simplex definition V3 is just the same as V2, except for an update Traversal definition @@ -949,7 +947,7 @@ def loadV2( self, simpDict: dict[str, Any], create: bool = True, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ): """Load the version 2 simplex definition @@ -1009,7 +1007,7 @@ def loadV1( self, simpDict: dict[str, Any], create: bool = True, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ): """Load the version 1 simplex definition @@ -1189,7 +1187,7 @@ def dump(self) -> str: """ return json.dumps(self.buildDefinition()) - def exportAbc(self, path: str, pBar: Optional[QProgressDialog] = None): + def exportAbc(self, path: str, pBar: QProgressDialog | None = None): """Export the current mesh to a .smpx formatted file Parameters @@ -1221,7 +1219,7 @@ def exportOther( dccMesh: DCCObject, world: bool = False, ensureCorrect: bool = False, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ): """Export shapes from a mesh that isn't part of the current system @@ -1267,7 +1265,7 @@ def setSlidersWeights(self, sliders: list[Slider], weights: list[float]): slider.value = weight self.DCC.setSlidersWeights(sliders, weights) - def extractRestShape(self, offset: int = 0) -> Optional[DCCObject]: + def extractRestShape(self, offset: int = 0) -> DCCObject | None: """Extract the rest shape to a mesh in the DCC Parameters @@ -1286,9 +1284,9 @@ def buildRestShape(self) -> Shape: def buildInputVectors( self, - keepSliders: Optional[set[str]] = None, - ignoreSliders: Optional[set[str]] = None, - depthCutoff: Optional[int] = None, + keepSliders: set[str] | None = None, + ignoreSliders: set[str] | None = None, + depthCutoff: int | None = None, ignoreFloaters: bool = False, ignoreTraversals: bool = False, extremes: bool = False, @@ -1560,8 +1558,8 @@ def buildSplitterList( def split( self, - sdef: Optional[SplitDefinition] = None, - pBar: Optional[QProgressDialog] = None, + sdef: SplitDefinition | None = None, + pBar: QProgressDialog | None = None, ) -> Simplex: """Return a split deepcopy of the system. The new system will be a dummy system containing all the shapes as numpy arrays which can be @@ -1733,7 +1731,7 @@ def treeChild(self, row: int) -> Group: def treeChildCount(self) -> int: return len(self.groups) - def treeData(self, column: int) -> Optional[str]: + def treeData(self, column: int) -> str | None: if column == 0: return self.name return None diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index 114f5513..a2448784 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -69,12 +69,12 @@ def __init__( with self.stack.store(self): self._name: str = name - self._thing: Optional[DCCObject] = None - self._thingRepr: Optional[str] = None + self._thing: DCCObject | None = None + self._thingRepr: str | None = None self.prog: Progression = prog self.prog.controller = self self.split: bool = False - self._buildIdx: Optional[int] = None + self._buildIdx: int | None = None self._value: float = 0.0 self._enabled: bool = True @@ -87,7 +87,6 @@ def __init__( self.group: Group = group self.group.items.append(self) - newThing = self.DCC.getSliderThing(self._name) if newThing is None: if create: @@ -115,8 +114,8 @@ def createSlider( cls, name: str, simplex: Simplex, - group: Optional[Group] = None, - shape: Optional[Shape] = None, + group: Group | None = None, + shape: Shape | None = None, tVal: float = 1.0, ) -> Slider: """Create a new slider with a name in a group. @@ -170,7 +169,7 @@ def createMultiSlider( simplex: Simplex, shapes: list[Shape], tVals: list[float], - group: Optional[Group] = None, + group: Group | None = None, ) -> Slider: """Create a new slider with a name (possibly in a custom group) with the provided shapes and t-values. @@ -459,7 +458,7 @@ def setInterps(self, sliders: list[Slider], interp: str): @stackable def createShape( - self, shapeName: Optional[str] = None, tVal: Optional[float] = None + self, shapeName: str | None = None, tVal: float | None = None ) -> ProgPair: """Create a shape and add it to a progression @@ -545,7 +544,7 @@ def extractShape( def connectShape( self, shape: Shape, - mesh: Optional[DCCObject] = None, + mesh: DCCObject | None = None, live: bool = False, delete: bool = False, ): @@ -624,7 +623,7 @@ def treeParent(self) -> TreeItem: def treeChildCount(self) -> int: return len(self.prog.pairs) - def treeData(self, column: int) -> Optional[Any]: + def treeData(self, column: int) -> Any | None: if column == 0: return self.name if column == 1: diff --git a/src/python/simplexui/items/stack.py b/src/python/simplexui/items/stack.py index 17ead486..069ea1c4 100644 --- a/src/python/simplexui/items/stack.py +++ b/src/python/simplexui/items/stack.py @@ -28,7 +28,7 @@ # UNDO STACK SETUP -class Stack(object): +class Stack: """Integrate simplex into the DCC undo stack""" def __init__(self): @@ -53,7 +53,7 @@ def push(self, key: int, value: Any): del self._stack[k] self._stack[key] = value - def getRevision(self, revision: int) -> Optional[Simplex]: + def getRevision(self, revision: int) -> Simplex | None: """Every time a change is made to the simplex definition, the revision counter is updated, and the revision/definition pair is put on the undo stack @@ -128,8 +128,6 @@ def __setitem__(self, key, value): super().__setitem__(key, value) - - def stackable(method): """A Decorator to make a method auto update the stack This decorator can only be used on methods of an object diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index ae93915f..ab0bfdff 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -49,7 +49,7 @@ def __init__(self, slider: Slider, value: float): self.minValue: float = -1.0 self.maxValue: float = 1.0 self._tickDelta: float = 0.0 - self.travPoint: Optional[TravPoint] = None + self.travPoint: TravPoint | None = None @property def name(self) -> str: @@ -103,7 +103,7 @@ def treeParent(self) -> TreeItem: raise ValueError("") return self.travPoint - def treeData(self, column: int) -> Optional[Any]: + def treeData(self, column: int) -> Any | None: if column == 0: return self.name if column == 1: @@ -124,7 +124,7 @@ def __init__(self, pairs: list[TravPair], side: TravSide): for pair in pairs: pair.travPoint = self self.side: TravSide = side - self.traversal: Optional[Traversal] = None + self.traversal: Traversal | None = None def sliders(self) -> list[Slider]: return [i.slider for i in self.pairs] @@ -146,7 +146,7 @@ def addPair(self, pair: TravPair): def removePair(self, pair: TravPair): pair.remove() - def addSlider(self, slider: Slider, val: Optional[float] = None): + def addSlider(self, slider: Slider, val: float | None = None): val = val if val is not None else slider.value val = self._wideCeiling(val) sliders = self.sliders() @@ -157,7 +157,7 @@ def addSlider(self, slider: Slider, val: Optional[float] = None): else: self.pairs[idx].value = val - def addItem(self, item: Union[Slider, Combo]): + def addItem(self, item: Slider | Combo): if isinstance(item, Slider): self.addSlider(item) elif isinstance(item, Combo): @@ -186,7 +186,7 @@ def getInputVector(self) -> list[float]: invec[self.simplex.sliders.index(cp.slider)] = cp.value return invec - def treeData(self, column: int) -> Optional[Any]: + def treeData(self, column: int) -> Any | None: if column == 0: return self.name return None @@ -199,7 +199,7 @@ def treeRow(self) -> int: return 0 return 1 - def treeParent(self) -> Optional[TreeItem]: + def treeParent(self) -> TreeItem | None: return self.traversal def treeChildCount(self) -> int: @@ -273,7 +273,7 @@ def __init__( self.endPoint: TravPoint = endPoint self.prog: Progression = prog self.prog.controller = self - self._buildIdx: Optional[int] = None + self._buildIdx: int | None = None self._enabled: bool = True self.startPoint.traversal = self self.endPoint.traversal = self @@ -290,7 +290,7 @@ def createTraversal( simplex: Simplex, startPairs: list[tuple[Slider, float]], endPairs: list[tuple[Slider, float]], - group: Optional[Group] = None, + group: Group | None = None, count: int = 4, ) -> Traversal: """Create a Traversal between two lists of pairs @@ -494,7 +494,7 @@ def nameLinks(self) -> list[bool]: @stackable def createShape( - self, shapeName: Optional[str] = None, tVal: Optional[float] = None + self, shapeName: str | None = None, tVal: float | None = None ) -> ProgPair: """Create a shape and add it to a progression @@ -700,7 +700,7 @@ def removePairs(self, pairs: list[TravPair]): @staticmethod def traversalAlreadyExists( simplex: Simplex, sliders: list[Slider], ranges: list[tuple[float, float]] - ) -> Optional[Traversal]: + ) -> Traversal | None: """In a given simplex syste, check if a traversal exists with the given sliders and ranges """ diff --git a/src/python/simplexui/items/treeItem.py b/src/python/simplexui/items/treeItem.py index d024da15..ee89a2e1 100644 --- a/src/python/simplexui/items/treeItem.py +++ b/src/python/simplexui/items/treeItem.py @@ -178,7 +178,7 @@ def getItemAppendRow(self) -> int: """Get the row to insert at to append a new item""" return 0 - def treeChild(self, row: int) -> Optional[TreeItem]: + def treeChild(self, row: int) -> TreeItem | None: """Return the child tree item at the given row if it exists""" return None @@ -186,7 +186,7 @@ def treeRow(self) -> int: """Return the row of the current item in the tree""" return 0 - def treeParent(self) -> Optional[TreeItem]: + def treeParent(self) -> TreeItem | None: """Return the parent of this TreeItem if it has one""" return None @@ -194,21 +194,21 @@ def treeChildCount(self) -> int: """Return the number of children this item has""" return 0 - def treeChecked(self) -> Optional[bool]: + def treeChecked(self) -> bool | None: """Return whether this item is checked""" return None - def treeData(self, column: int) -> Optional[Any]: + def treeData(self, column: int) -> Any | None: """Return the data for the given column""" return None - def icon(self) -> Optional[QIcon]: + def icon(self) -> QIcon | None: """Return the icon of this item, if it has one""" return None class TreeRootItem(TreeItem): - def __init__(self, observerServers: Optional[list[ObserverServer]] = None): + def __init__(self, observerServers: list[ObserverServer] | None = None): super().__init__(self) # The root just keeps track of everybody's observers if observerServers is None: @@ -233,10 +233,10 @@ class AdapterModel(QAbstractItemModel): """Model that adapts the values from the TreeItem to the AbstractItemModel interface""" def __init__( - self, rootItem: Optional[TreeRootItem] = None, parent: Optional[QObject] = None + self, rootItem: TreeRootItem | None = None, parent: QObject | None = None ): super().__init__(parent=parent) - self._rootItem: Optional[TreeRootItem] = None + self._rootItem: TreeRootItem | None = None self.observer = ObserverServer(self) if rootItem is not None: self.setRootItem(rootItem) @@ -251,7 +251,7 @@ def setRootItem(self, rootItem: TreeRootItem): finally: self.endResetModel() - def getItemRow(self, item: Optional[TreeItem]) -> Optional[int]: + def getItemRow(self, item: TreeItem | None) -> int | None: if item is None: return None return item.treeRow() @@ -262,7 +262,7 @@ def indexFromItem(self, item: TreeItem, column: int = 0) -> QModelIndex: return QModelIndex() return self.createIndex(row, column, item) - def itemFromIndex(self, index: QModelIndex) -> Optional[TreeItem]: + def itemFromIndex(self, index: QModelIndex) -> TreeItem | None: return index.internalPointer() def itemDataChanged(self, item: TreeItem): @@ -273,7 +273,7 @@ def emitDataChanged(self, index: QModelIndex): if index.isValid(): self.dataChanged.emit(index, index, []) - def getChildItem(self, parent: Optional[TreeItem], row: int) -> Optional[TreeItem]: + def getChildItem(self, parent: TreeItem | None, row: int) -> TreeItem | None: if parent is None: if row == 0: return self._rootItem @@ -281,12 +281,12 @@ def getChildItem(self, parent: Optional[TreeItem], row: int) -> Optional[TreeIte return None return parent.treeChild(row) - def getParentItem(self, item: Optional[TreeItem]) -> Optional[TreeItem]: + def getParentItem(self, item: TreeItem | None) -> TreeItem | None: if item is None: return None return item.treeParent() - def getItemRowCount(self, item: Optional[TreeItem]) -> int: + def getItemRowCount(self, item: TreeItem | None) -> int: if item is None: # Null parent means return the only root item ret = 1 @@ -340,7 +340,7 @@ def getItemData( item: TreeItem, column: int, role: Literal[Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole], - ) -> Optional[str]: ... + ) -> str | None: ... @overload def getItemData( @@ -348,7 +348,7 @@ def getItemData( item: TreeItem, column: int, role: Literal[Qt.ItemDataRole.CheckStateRole], - ) -> Optional[Qt.CheckState]: ... + ) -> Qt.CheckState | None: ... @overload def getItemData( @@ -356,7 +356,7 @@ def getItemData( item: TreeItem, column: int, role: Literal[Qt.ItemDataRole.DecorationRole], - ) -> Optional[QIcon]: ... + ) -> QIcon | None: ... @overload def getItemData( @@ -364,11 +364,9 @@ def getItemData( item: TreeItem, column: int, role: Literal[CustomRoles.UID_ROLE], - ) -> Optional[str]: ... + ) -> str | None: ... - def getItemData( - self, item: Optional[TreeItem], column: int, role: int - ) -> Optional[Any]: + def getItemData(self, item: TreeItem | None, column: int, role: int) -> Any | None: if item is None: return None @@ -390,7 +388,7 @@ def getItemData( return None def iterindices( - self, pred: Optional[Callable[[QModelIndex], bool]] = None + self, pred: Callable[[QModelIndex], bool] | None = None ) -> Iterator[QModelIndex]: """Iterate all indices of this model breadth-first""" queue = [QModelIndex()] @@ -402,8 +400,8 @@ def iterindices( queue.append(self.index(row, 0, index)) def iteritems( - self, pred: Optional[Callable[[TreeItem], bool]] = None - ) -> Iterator[Optional[TreeItem]]: + self, pred: Callable[[TreeItem], bool] | None = None + ) -> Iterator[TreeItem | None]: """Iterate all items of this model breadth-first""" # The only way I could make the typechecker happy was by # having 2 separate loops over self.iterindices diff --git a/src/python/simplexui/menu/__init__.py b/src/python/simplexui/menu/__init__.py index 7df9552e..b562785b 100644 --- a/src/python/simplexui/menu/__init__.py +++ b/src/python/simplexui/menu/__init__.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import importlib import os import pkgutil diff --git a/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py b/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py index a890f550..927d68e7 100644 --- a/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py +++ b/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from functools import partial from ...comboCheckDialog import ComboCheckDialog diff --git a/src/python/simplexui/menu/genericPlugins/exportSplit.py b/src/python/simplexui/menu/genericPlugins/exportSplit.py index 64cd3935..fb4bd2ed 100644 --- a/src/python/simplexui/menu/genericPlugins/exportSplit.py +++ b/src/python/simplexui/menu/genericPlugins/exportSplit.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from functools import partial from Qt import QtCompat @@ -48,7 +49,6 @@ def exportSplitInterface(window): ) return - path, _filter = QtCompat.QFileDialog.getSaveFileName( window, "Export Split", "", "Simplex (*.smpx)" ) @@ -56,7 +56,6 @@ def exportSplitInterface(window): if not path: return - pBar = QProgressDialog("Exporting Split smpx File", "Cancel", 0, 100, window) pBar.show() try: diff --git a/src/python/simplexui/menu/genericPlugins/showFalloffs.py b/src/python/simplexui/menu/genericPlugins/showFalloffs.py index 5b34366c..b4480d3a 100644 --- a/src/python/simplexui/menu/genericPlugins/showFalloffs.py +++ b/src/python/simplexui/menu/genericPlugins/showFalloffs.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/genericPlugins/showTraversals.py b/src/python/simplexui/menu/genericPlugins/showTraversals.py index 02f3045f..fe6e69d8 100644 --- a/src/python/simplexui/menu/genericPlugins/showTraversals.py +++ b/src/python/simplexui/menu/genericPlugins/showTraversals.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py b/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py index b8344a63..d2735782 100644 --- a/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py +++ b/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import json from ...commands.alembicCommon import buildSmpx, readSmpx diff --git a/src/python/simplexui/menu/genericPlugins/unsubdivide.py b/src/python/simplexui/menu/genericPlugins/unsubdivide.py index 1ea0059e..9b36382c 100644 --- a/src/python/simplexui/menu/genericPlugins/unsubdivide.py +++ b/src/python/simplexui/menu/genericPlugins/unsubdivide.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import os from functools import partial diff --git a/src/python/simplexui/menu/mayaPlugins/exportOther.py b/src/python/simplexui/menu/mayaPlugins/exportOther.py index 0d9083cf..17fc82b6 100644 --- a/src/python/simplexui/menu/mayaPlugins/exportOther.py +++ b/src/python/simplexui/menu/mayaPlugins/exportOther.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from functools import partial import maya.cmds as cmds diff --git a/src/python/simplexui/menu/mayaPlugins/extractProgressives.py b/src/python/simplexui/menu/mayaPlugins/extractProgressives.py index e320ab89..5c2a32f0 100644 --- a/src/python/simplexui/menu/mayaPlugins/extractProgressives.py +++ b/src/python/simplexui/menu/mayaPlugins/extractProgressives.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from functools import partial from ...interfaceModel import coerceIndexToType diff --git a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py index d4b12876..1850c0de 100644 --- a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py +++ b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from functools import partial from maya import cmds diff --git a/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py index 47bfd7ab..00dec24e 100644 --- a/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py +++ b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from functools import partial import maya.cmds as cmds diff --git a/src/python/simplexui/menu/mayaPlugins/importObjs.py b/src/python/simplexui/menu/mayaPlugins/importObjs.py index 4d5eae71..c1ca88b5 100644 --- a/src/python/simplexui/menu/mayaPlugins/importObjs.py +++ b/src/python/simplexui/menu/mayaPlugins/importObjs.py @@ -1,3 +1,4 @@ +from __future__ import annotations import os import numpy as np from functools import partial diff --git a/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py index bb8ad310..41906d69 100644 --- a/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py +++ b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from functools import partial import maya.cmds as cmds from Qt.QtGui import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py index b682376f..25fe7185 100644 --- a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py +++ b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import os from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py index 16727300..f2e488b0 100644 --- a/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py +++ b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import maya.cmds as cmds from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py index 31b190f7..93e44f8d 100644 --- a/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py +++ b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from functools import partial from Qt.QtGui import QAction from Qt.QtWidgets import QMessageBox diff --git a/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py index 5ee142e6..fc97da41 100644 --- a/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py +++ b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from functools import partial import maya.cmds as cmds diff --git a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py index c3ff2d2a..27b11619 100644 --- a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py +++ b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import maya.cmds as cmds import maya.OpenMaya as om diff --git a/src/python/simplexui/menu/mayaPlugins/tweakMix.py b/src/python/simplexui/menu/mayaPlugins/tweakMix.py index 991a59fa..d19d3f46 100644 --- a/src/python/simplexui/menu/mayaPlugins/tweakMix.py +++ b/src/python/simplexui/menu/mayaPlugins/tweakMix.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from functools import partial import maya.cmds as cmds diff --git a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py index a008b009..131c8518 100644 --- a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py +++ b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations import textwrap from functools import partial diff --git a/src/python/simplexui/travCheckDialog.py b/src/python/simplexui/travCheckDialog.py index d2c5c2e8..7aca49aa 100644 --- a/src/python/simplexui/travCheckDialog.py +++ b/src/python/simplexui/travCheckDialog.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . +from __future__ import annotations from itertools import combinations, product from .dragFilter import DragFilter diff --git a/src/python/simplexui/utils.py b/src/python/simplexui/utils.py index b2c78cb7..87ef2dc7 100755 --- a/src/python/simplexui/utils.py +++ b/src/python/simplexui/utils.py @@ -47,7 +47,7 @@ def execwid(wid): wid.exec_() -def getUiFile(fileVar: str, subFolder: str = "ui", uiName: Optional[str] = None) -> str: +def getUiFile(fileVar: str, subFolder: str = "ui", uiName: str | None = None) -> str: """Get the path to the .ui file Parameters @@ -102,7 +102,7 @@ def getNextName(name: str, currentNames: Sequence[str]) -> str: return name -def clearPathSymbols(paths: list[str], keepers: Optional[list[str]] = None): +def clearPathSymbols(paths: list[str], keepers: list[str] | None = None): """Removes path symbols from the environment. This means I can unload my tools from the current process and re-import them @@ -195,7 +195,7 @@ class singleShot(QObject): def __init__(self): super().__init__() - self._function: Optional[Callable] = None + self._function: Callable | None = None self._callScheduled: bool = False self._args = [] self._inst = None @@ -251,7 +251,7 @@ def makeUnique(seq: Sequence[T]) -> Sequence[T]: return [x for x in seq if not (x in seen or seen_add(x))] -def naturalSortKey(s: str, _nsre=re.compile("([0-9]+)")) -> list[Union[str, int]]: +def naturalSortKey(s: str, _nsre=re.compile("([0-9]+)")) -> list[str | int]: """Get a sort key that puts strings with numbers in numerical order This is accomplished by splitting the string into groups of digits, and non-digits, then converting the digit groups into integers. @@ -276,11 +276,11 @@ def getIcon(iconName: str) -> QIcon: return QIcon(path) -class Prefs(object): +class Prefs: """A wrapper for reading/writing prefs both internal and external to blur""" def __init__(self): - self._pref: Union['blurdev.prefs.Preference', QSettings] + self._pref: "blurdev.prefs.Preference" | QSettings if AT_BLUR: import blurdev.prefs @@ -288,7 +288,7 @@ def __init__(self): else: self._pref = QSettings("Blur", "Simplex3") - def restoreProperty(self, prop: str, default: Optional[str] = None) -> Any: + def restoreProperty(self, prop: str, default: str | None = None) -> Any: if isinstance(self._pref, QSettings): return self._pref.value(prop, default) else: From abe1abdb1fbc516119bf281cef6d76054297d15b Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 11 Sep 2026 16:32:45 -0700 Subject: [PATCH 39/47] Automatically update old format strings to fstrings --- src/python/simplexui/comboCheckDialog.py | 4 +- .../simplexui/commands/alembicCommon.py | 93 ++++---- .../simplexui/commands/applyCorrectives.py | 12 +- .../simplexui/commands/correctiveInterface.py | 2 +- .../simplexui/commands/expandedExport.py | 2 +- .../commands/mayaCorrectiveInterface.py | 4 +- src/python/simplexui/commands/mayatonumpy.py | 12 +- src/python/simplexui/commands/smpxBlend.py | 10 +- src/python/simplexui/commands/unsubdivide.py | 2 +- src/python/simplexui/commands/uvTransfer.py | 6 +- .../simplexui/interface/dummyInterface.py | 8 +- .../simplexui/interface/mayaInterface.py | 206 ++++++++---------- src/python/simplexui/items/combo.py | 16 +- src/python/simplexui/items/falloff.py | 20 +- src/python/simplexui/items/group.py | 2 +- src/python/simplexui/items/progression.py | 6 +- src/python/simplexui/items/shape.py | 4 +- src/python/simplexui/items/simplex.py | 20 +- src/python/simplexui/items/slider.py | 10 +- src/python/simplexui/items/traversal.py | 8 +- .../menu/genericPlugins/unsubdivide.py | 4 +- .../simplexui/menu/mayaPlugins/freezeCombo.py | 4 +- .../mayaPlugins/generateShapeIncrementals.py | 2 +- .../simplexui/menu/mayaPlugins/importObjs.py | 4 +- .../menu/mayaPlugins/makeShelfBtn.py | 6 +- .../menu/mayaPlugins/relaxToSelection.py | 2 +- .../menu/mayaPlugins/snapToNeutral.py | 4 +- .../menu/mayaPlugins/softSelectToCluster.py | 6 +- .../simplexui/menu/mayaPlugins/tweakMix.py | 2 +- .../menu/mayaPlugins/updateRestShape.py | 10 +- src/python/simplexui/simplexDialog.py | 16 +- src/python/simplexui/travCheckDialog.py | 6 +- src/python/simplexui/traversalDialog.py | 2 +- 33 files changed, 223 insertions(+), 292 deletions(-) diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index e978359a..4dbc7785 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -365,9 +365,7 @@ def _populate(self): maxPoss=maxPoss, ) - lbl = ( - "Too many possibilities. Limiting to {0}".format(maxPoss) if tooMany else "" - ) + lbl = f"Too many possibilities. Limiting to {maxPoss}" if tooMany else "" self.uiWarningLBL.setText(lbl) self.uiComboCheckLIST.clear() diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index f17ce353..7ded8187 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -55,7 +55,6 @@ TYPE_CHECKING, Optional, Union, - Type, TypeVar, overload, Any, @@ -75,10 +74,10 @@ def pbPrint( - pBar: Optional[QProgressDialog], - message: Optional[str] = None, - val: Optional[int] = None, - maxVal: Optional[int] = None, + pBar: QProgressDialog | None, + message: str | None = None, + val: int | None = None, + maxVal: int | None = None, _pbPrintLastComma=None, ): """A function that handles displaying messages in a QProgressDialog or printing to stdout @@ -109,11 +108,9 @@ def pbPrint( if message is not None: if val is not None: if maxVal is not None: - print( - message, "{0: <4} of {1: <4}\r".format(val + 1, maxVal), end=" " - ) + print(message, f"{val + 1: <4} of {maxVal: <4}\r", end=" ") else: - print(message, "{0: <4}\r".format(val + 1), end=" ") + print(message, f"{val + 1: <4}\r", end=" ") # This is the ugliest, most terrible thing I think I've ever written # Abusing the static default object to check if the last time # this function was used, there was a trailing comma @@ -126,7 +123,7 @@ def pbPrint( print(message) -def mkArray(aType: Type[T], iList: npt.NDArray) -> T: +def mkArray(aType: type[T], iList: npt.NDArray) -> T: """Makes the alembic-usable c++ typed 2-d arrays Parameters @@ -160,7 +157,7 @@ def mkArray(aType: Type[T], iList: npt.NDArray) -> T: return array -def mk1dArray(aType: Type[T], iList: npt.NDArray) -> T: +def mk1dArray(aType: type[T], iList: npt.NDArray) -> T: """Makes the alembic-usable c++ typed 1-d arrays Parameters @@ -261,7 +258,7 @@ def mkSampleUvArray(uvs: npfloat) -> V2fArray: return array -def mkUvSample(uvs: npfloat, indexes: Optional[npint] = None) -> OV2fGeomParamSample: +def mkUvSample(uvs: npfloat, indexes: npint | None = None) -> OV2fGeomParamSample: """Take an array, and make a poly mesh sample of the uvs Parameters @@ -283,9 +280,7 @@ def mkUvSample(uvs: npfloat, indexes: Optional[npint] = None) -> OV2fGeomParamSa return OV2fGeomParamSample(ary, idxs, GeometryScope.kFacevaryingScope) -def mkNormalSample( - norms: npfloat, indexes: Optional[npint] = None -) -> ON3fGeomParamSample: +def mkNormalSample(norms: npfloat, indexes: npint | None = None) -> ON3fGeomParamSample: """Take an array, and make a poly mesh sample of the normals Parameters @@ -312,9 +307,9 @@ def setAlembicSample( points: V3fArray, faceCount: IntArray, faceIndex: IntArray, - bounds: Optional[Box3d] = None, - uvs: Optional[OV2fGeomParamSample] = None, - normals: Optional[ON3fGeomParamSample] = None, + bounds: Box3d | None = None, + uvs: OV2fGeomParamSample | None = None, + normals: ON3fGeomParamSample | None = None, ): """Set an alembic sample to the output mesh with the given properties""" # Do it this way because the defaults for these arguments are some value other than None @@ -330,7 +325,7 @@ def setAlembicSample( omeshSch.set(s) -def getSampleArray(imesh: IPolyMesh, pBar: Optional[QProgressDialog] = None) -> npfloat: +def getSampleArray(imesh: IPolyMesh, pBar: QProgressDialog | None = None) -> npfloat: """Get the per-frame vertex positions for a mesh Parameters @@ -407,7 +402,7 @@ def getStaticMeshArrays(imesh: IPolyMesh) -> tuple[npint, npint]: return faces, counts -def getUvSample(imesh: IPolyMesh) -> Optional[OV2fGeomParamSample]: +def getUvSample(imesh: IPolyMesh) -> OV2fGeomParamSample | None: """Get the UV's for a mesh Parameters @@ -435,7 +430,7 @@ def getUvSample(imesh: IPolyMesh) -> Optional[OV2fGeomParamSample]: return uv -def getUvArray(imesh: IPolyMesh) -> Optional[npfloat]: +def getUvArray(imesh: IPolyMesh) -> npfloat | None: """Get the uv positions for a mesh Parameters @@ -463,7 +458,7 @@ def getUvArray(imesh: IPolyMesh) -> Optional[npfloat]: return uv -def getFlatUvFaces(imesh: IPolyMesh) -> tuple[Optional[npint], Optional[bool]]: +def getFlatUvFaces(imesh: IPolyMesh) -> tuple[npint | None, bool | None]: """Get the UV structure for a mesh if it's indexed. If un-indexed, return None This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed and can handle the data appropriately for export without keeping track of index-ness @@ -497,7 +492,7 @@ def getFlatUvFaces(imesh: IPolyMesh) -> tuple[Optional[npint], Optional[bool]]: return idxs, indexed -def getUvFaces(imesh: IPolyMesh) -> Optional[list[list[int]]]: +def getUvFaces(imesh: IPolyMesh) -> list[list[int]] | None: """Get the UV structure for a mesh if it's indexed. If un-indexed, return None This means that if we have valid UVs, but invalid uvFaces, then we're un-indexed and can handle the data appropriately for export without keeping track of index-ness @@ -573,19 +568,19 @@ def getPointCount(imesh: IPolyMesh) -> int: @overload def findAlembicObject( - obj: IObject, abcType: None = None, name: Optional[str] = None -) -> Optional[IObject]: ... + obj: IObject, abcType: None = None, name: str | None = None +) -> IObject | None: ... @overload def findAlembicObject( - obj: IObject, abcType: Type[B], name: Optional[str] = None -) -> Optional[B]: ... + obj: IObject, abcType: type[B], name: str | None = None +) -> B | None: ... def findAlembicObject( - obj: IObject, abcType: Optional[Type[B]] = None, name: Optional[str] = None -) -> Optional[Union[IObject, B]]: + obj: IObject, abcType: type[B] | None = None, name: str | None = None +) -> IObject | B | None: """Finds a single object in an alembic archive by name and/or type If only type is specified, then the first object of that type encountered will be returned @@ -606,8 +601,8 @@ def findAlembicObject( def findAllAlembicObjects( obj: IObject, - abcType: Optional[Type[_IBase]] = None, - out: Optional[list[IObject]] = None, + abcType: type[_IBase] | None = None, + out: list[IObject] | None = None, ) -> list[IObject]: """Finds all objects of a type in an alembic archive""" md = obj.getMetaData() @@ -621,7 +616,7 @@ def findAllAlembicObjects( return out -def getTypedIObject(obj: IObject) -> Optional[_IBase]: +def getTypedIObject(obj: IObject) -> _IBase | None: from alembic.AbcGeom import ( ICamera, ICurves, @@ -649,7 +644,7 @@ def getTypedIObject(obj: IObject) -> Optional[_IBase]: return None -def getMesh(infile: str) -> Optional[IPolyMesh]: +def getMesh(infile: str) -> IPolyMesh | None: """Get the first found mesh object from the alembic filepath""" iarch = IArchive(str(infile)) ipolymsh = findAlembicObject(iarch.getTop(), abcType=IPolyMesh) @@ -680,7 +675,7 @@ def writeStringProperty( numChunks = (len(value) // 65000) + 1 chunkSize = (len(value) // numChunks) + 1 for c in range(numChunks): - prop = OStringProperty(props, "{0}{1}".format(key, c)) + prop = OStringProperty(props, f"{key}{c}") prop.setValue(value[chunkSize * c : chunkSize * (c + 1)]) else: prop = OStringProperty(props, str(key)) @@ -713,7 +708,7 @@ def readStringProperty(props: ICompoundProperty, key: str) -> str: parts = [] for c in range(10): try: - prop = props.getProperty("{0}{1}".format(key, c)) + prop = props.getProperty(f"{key}{c}") except KeyError: if c == 0: raise @@ -780,17 +775,17 @@ def buildAbc( outPath: str, points: npfloat, faces: npint, - faceCounts: Optional[npint] = None, - uvs: Optional[npfloat] = None, - uvFaces: Optional[npint] = None, - normals: Optional[npfloat] = None, - normFaces: Optional[npint] = None, + faceCounts: npint | None = None, + uvs: npfloat | None = None, + uvFaces: npint | None = None, + normals: npfloat | None = None, + normFaces: npint | None = None, name: str = "polymsh", shapeSuffix: str = "Shape", transformSuffix: str = "", - propDict: Optional[dict[str, str]] = None, + propDict: dict[str, str] | None = None, ogawa: bool = True, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ): """ Build a single-mesh alembic file from all of the non-alembic raw data @@ -921,8 +916,8 @@ def getSmpxArchiveData(abcPath: str) -> tuple[IArchive, IPolyMesh, str]: def readSmpx( - path: str, pBar: Optional[QProgressDialog] = None -) -> tuple[str, npint, npfloat, npint, Optional[npfloat], Optional[npint]]: + path: str, pBar: QProgressDialog | None = None +) -> tuple[str, npint, npfloat, npint, npfloat | None, npint | None]: """Read and return the raw alembic vertex/face data in the flat alembic style Parameters @@ -964,11 +959,11 @@ def buildSmpx( faces: npint, jsString: str, name: str, - faceCounts: Optional[npint] = None, - uvs: Optional[npfloat] = None, - uvFaces: Optional[npint] = None, + faceCounts: npint | None = None, + uvs: npfloat | None = None, + uvFaces: npint | None = None, ogawa: bool = True, - pBar: Optional[QProgressDialog] = None, + pBar: QProgressDialog | None = None, ) -> None: """ Build a simplex output from raw data @@ -1016,7 +1011,7 @@ def buildSmpx( def buildAlembicArchiveData( path: str, name: str, jsString: str, ogawa: bool -) -> tuple[Optional[OArchive], Optional[OPolyMesh]]: +) -> tuple[OArchive | None, OPolyMesh | None]: """Set up an output alembic archive with a mesh ready for writing Parameters diff --git a/src/python/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py index dbf6f58b..42ca4f17 100644 --- a/src/python/simplexui/commands/applyCorrectives.py +++ b/src/python/simplexui/commands/applyCorrectives.py @@ -176,9 +176,7 @@ def _buildSolverInputs(simplex, item, value, indexBySlider): inVec[indexBySlider[pair.slider]] = pair.value * abs(value) return inVec else: - raise ValueError( - "Not a slider or combo. Got type {0}: {1}".format(type(item), item) - ) + raise ValueError(f"Not a slider or combo. Got type {type(item)}: {item}") def buildFullShapes(simplex, shapeObjs, shapes, solver, pBar=None): @@ -235,7 +233,7 @@ def buildFullShapes(simplex, shapeObjs, shapes, solver, pBar=None): pBar.setValue(i) QApplication.processEvents() else: - print("Building {0} of {1}\r".format(i + 1, len(shapeObjs)), end=" ") + print(f"Building {i + 1} of {len(shapeObjs)}\r", end=" ") item, value = shapeDict[shape] inVec = _buildSolverInputs(simplex, item, value, indexBySlider) @@ -299,11 +297,11 @@ def collapseFullShapes(simplex, allPts, ptsByShape, vecByShape, pBar=None): for shpOrderIdx, shape in enumerate(shapeOrder): if pBar is not None: pBar.setValue(shpOrderIdx) - pBar.setLabelText("Building Corrected Deltas\n{}".format(shape.name)) + pBar.setLabelText(f"Building Corrected Deltas\n{shape.name}") QApplication.processEvents() else: print( - "Collapsing {0} of {1}\r".format(shpOrderIdx + 1, len(shapeOrder)), + f"Collapsing {shpOrderIdx + 1} of {len(shapeOrder)}\r", end=" ", ) @@ -392,7 +390,7 @@ def applyCorrectives( pBar.setValue(i) QApplication.processEvents() else: - print("Correcting {0} of {1}: {2}".format(i + 1, len(shapes), shape.name)) + print(f"Correcting {i + 1} of {len(shapes)}: {shape.name}") inv = inverses[refIdx] pts = ptsByShape[shape] diff --git a/src/python/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py index 652e6dd8..f53bb167 100644 --- a/src/python/simplexui/commands/correctiveInterface.py +++ b/src/python/simplexui/commands/correctiveInterface.py @@ -247,7 +247,7 @@ def outputCorrectiveReferences( if pBar is not None: pBar.setLabelText("Writing Names") QApplication.processEvents() - nameWrite = ["{};{}".format(s.name, r) for s, r in zip(shapes, refIdxs)] + nameWrite = [f"{s.name};{r}" for s, r in zip(shapes, refIdxs)] with open(outNames, "w") as f: f.write("\n".join(nameWrite)) diff --git a/src/python/simplexui/commands/expandedExport.py b/src/python/simplexui/commands/expandedExport.py index 24f334d2..95e98617 100644 --- a/src/python/simplexui/commands/expandedExport.py +++ b/src/python/simplexui/commands/expandedExport.py @@ -228,7 +228,7 @@ def _setInputs(inVec, item, indexBySlider, value): ) return inVec raise ValueError( - "Not a Slider, Combo, or Traversal. Got type {0}: {1}".format(type(item), item) + f"Not a Slider, Combo, or Traversal. Got type {type(item)}: {item}" ) diff --git a/src/python/simplexui/commands/mayaCorrectiveInterface.py b/src/python/simplexui/commands/mayaCorrectiveInterface.py index 387182e1..ab78a1a0 100644 --- a/src/python/simplexui/commands/mayaCorrectiveInterface.py +++ b/src/python/simplexui/commands/mayaCorrectiveInterface.py @@ -112,7 +112,7 @@ def getDeformerChain(chkObj): break # Use that mesh index to get the output cnx = cmds.listConnections( - chkObj + ".input[{0}].inputGeometry".format(defIdx), + chkObj + f".input[{defIdx}].inputGeometry", destination=False, shapes=True, ) or [None] @@ -145,7 +145,7 @@ def getShiftValues(thing): dp = _getDagPath(thing) meshFn = om.MFnMesh(dp) - allVerts = "{0}.vtx[*]".format(orig) + allVerts = f"{orig}.vtx[*]" zero = _getMayaPoints(meshFn) cmds.move(1, 0, 0, allVerts, relative=1, objectSpace=1) diff --git a/src/python/simplexui/commands/mayatonumpy.py b/src/python/simplexui/commands/mayatonumpy.py index 1645e540..c9b6cd64 100644 --- a/src/python/simplexui/commands/mayatonumpy.py +++ b/src/python/simplexui/commands/mayatonumpy.py @@ -292,9 +292,7 @@ def getNumpyAttr( return mayaToNumpy(mat) else: apiTypeStr = pmo.apiTypeStr() - raise NotImplementedError( - "I don't know how to handle {0} yet".format(apiTypeStr) - ) + raise NotImplementedError(f"I don't know how to handle {apiTypeStr} yet") raise NotImplementedError("Fell all the way through") @@ -325,7 +323,7 @@ def setNumpyAttr( elif isinstance(attrName, om.MPlug): plug = attrName else: - raise ValueError("Data must be string or MPlug. Got {0}".format(type(attrName))) + raise ValueError(f"Data must be string or MPlug. Got {type(attrName)}") # First just check if the data is numeric mdh = plug.asMDataHandle() @@ -374,9 +372,7 @@ def setNumpyAttr( return else: apiTypeStr = pmo.apiTypeStr() - raise NotImplementedError( - "I don't know how to handle {0} yet".format(apiTypeStr) - ) + raise NotImplementedError(f"I don't know how to handle {apiTypeStr} yet") raise NotImplementedError("WTF? How did you get here??") @@ -396,7 +392,7 @@ def test(): # A quick test showing how to build a numpy array # containing the deltas for a shape on a blendshape node numVerts = cmds.polyEvaluate(meshName, vertex=True) - baseAttr = "{0}.it[{1}].itg[{2}].iti[6000]".format(bsName, meshIdx, bsIdx) + baseAttr = f"{bsName}.it[{meshIdx}].itg[{bsIdx}].iti[6000]" inPtAttr = baseAttr + ".inputPointsTarget" inCompAttr = baseAttr + ".inputComponentsTarget" diff --git a/src/python/simplexui/commands/smpxBlend.py b/src/python/simplexui/commands/smpxBlend.py index d7673382..d1dc98d9 100644 --- a/src/python/simplexui/commands/smpxBlend.py +++ b/src/python/simplexui/commands/smpxBlend.py @@ -359,7 +359,7 @@ def sliderBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): oNames = orderedMerge(aNames, bNames) for oIdx, oName in enumerate(oNames): - print("Copying Slider {0} of {1}: {2}".format(oIdx, len(oNames), oName)) + print(f"Copying Slider {oIdx} of {len(oNames)}: {oName}") aName, bName = mismatch["slider"].get(oName, (oName, oName)) if aName is Skip or bName is Skip: continue @@ -433,7 +433,7 @@ def comboBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): oNames = orderedMerge(aNames, bNames) for oIdx, oName in enumerate(oNames): - print("Copying Combo {0} of {1}: {2}".format(oIdx, len(oNames), oName)) + print(f"Copying Combo {oIdx} of {len(oNames)}: {oName}") aName, bName = mismatch["combo"].get(oName, (oName, oName)) if oName in mismatch: @@ -460,7 +460,7 @@ def _blendController(aItem, bItem, outSimp, blendVal, translation): elif isinstance(aCtrl, Combo): ctrl = _blendCombos(aCtrl, bCtrl, outSimp, blendVal, translation) else: - raise ValueError("Bad object type: {0} {1}".format(aCtrl, type(aCtrl))) + raise ValueError(f"Bad object type: {aCtrl} {type(aCtrl)}") return TravPair(ctrl, aItem.value, aItem.usage) @@ -471,7 +471,7 @@ def _copyController(item, outSimp, translation): elif isinstance(iCtrl, Combo): ctrl = _copyCombo(iCtrl, outSimp, translation) else: - raise ValueError("Bad object type: {0} {1}".format(iCtrl, type(iCtrl))) + raise ValueError(f"Bad object type: {iCtrl} {type(iCtrl)}") return TravPair(ctrl, item.value, item.usage) @@ -525,7 +525,7 @@ def traversalBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): oNames = orderedMerge(aNames, bNames) for oIdx, oName in enumerate(oNames): - print("Copying Traversal {0} of {1}: {2}".format(oIdx, len(oNames), oName)) + print(f"Copying Traversal {oIdx} of {len(oNames)}: {oName}") aName, bName = mismatch["traversal"].get(oName, (oName, oName)) if aName is Skip or bName is Skip: continue diff --git a/src/python/simplexui/commands/unsubdivide.py b/src/python/simplexui/commands/unsubdivide.py index ea0003f3..74f3e378 100644 --- a/src/python/simplexui/commands/unsubdivide.py +++ b/src/python/simplexui/commands/unsubdivide.py @@ -473,7 +473,7 @@ def _findOldPositionBorder( """Find the position of the un-subdivided mesh if the vertex was on the border""" nei = neighDict[vIdx][0] nei = [i for i in nei if i in borders] - assert len(nei) == 2, "Found multi border, {}".format(nei) + assert len(nei) == 2, f"Found multi border, {nei}" uVerts[vIdx] = 2 * verts[vIdx] - ((verts[nei[0]] + verts[nei[1]]) / 2) computed.add(vIdx) diff --git a/src/python/simplexui/commands/uvTransfer.py b/src/python/simplexui/commands/uvTransfer.py index 479e78c4..024ac5ff 100644 --- a/src/python/simplexui/commands/uvTransfer.py +++ b/src/python/simplexui/commands/uvTransfer.py @@ -335,7 +335,7 @@ def sweep(qPoints, uvs, tris, pBar=None): cVal = qpSIdx + mnSIdx + mxSIdx if cVal % 1283 == 0: # Just a random prime pBar.setValue(cVal) - pBar.setLabelText("Sweeping ...\n{0}/{1}".format(cVal, allVals)) + pBar.setLabelText(f"Sweeping ...\n{cVal}/{allVals}") QApplication.processEvents() if pBar.wasCanceled(): raise RuntimeError("Cancelled!") @@ -756,9 +756,7 @@ def getVertCorrelation( import time v = time.time() - print( - "Missing correspondences found. Stored in uvTransfer.MISSING[{0}]".format(v) - ) + print(f"Missing correspondences found. Stored in uvTransfer.MISSING[{v}]") MISSING[v] = missing return mvcVertDict diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index 5bd6ff29..9cc4db81 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -303,9 +303,7 @@ def loadNodes(self, simp, thing, create=True, pBar=None): if self.shapeNode is None: if not create: raise RuntimeError( - "Blendshape operator not found with creation turned off: {0}".format( - self.name - ) + f"Blendshape operator not found with creation turned off: {self.name}" ) self.shapeNode = DummyBlendshape(self.name, self.mesh, self.scene) @@ -581,7 +579,7 @@ def exportAbc( pBar.show() pBar.setMaximum(len(shapes)) spacerName = "_" * max(list(map(len, shapeNames))) - pBar.setLabelText("Exporting:\n{0}".format(spacerName)) + pBar.setLabelText(f"Exporting:\n{spacerName}") QApplication.processEvents() faces = mkSampleIntArray(self.mesh.faces) @@ -592,7 +590,7 @@ def exportAbc( for i, shape in enumerate(shapes): if pBar is not None: - pBar.setLabelText("Exporting:\n{0}".format(shape.name)) + pBar.setLabelText(f"Exporting:\n{shape.name}") pBar.setValue(i) pBar.repaint() # Required to properly show the percentage. Don't know why QApplication.processEvents() diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 4a4fa435..f59b84c4 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -234,14 +234,14 @@ def preLoad(self, simp, simpDict, create=True, pBar=None): ) bret = QMessageBox.question(pBar, "Missing Shapes", msg, btns) if not bret & QMessageBox.StandardButton.Yes: - raise RuntimeError("Missing Shapes: {}".format(toMake)) + raise RuntimeError(f"Missing Shapes: {toMake}") else: - raise RuntimeError("Missing Shapes: {}".format(toMake)) + raise RuntimeError(f"Missing Shapes: {toMake}") if pBar is not None: spacer = "_" * max(list(map(len, toMake))) pBar.setMaximum(len(toMake)) - pBar.setLabelText("Creating Empty Shape:\n{0}".format(spacer)) + pBar.setLabelText(f"Creating Empty Shape:\n{spacer}") pBar.setValue(0) QApplication.processEvents() @@ -251,7 +251,7 @@ def preLoad(self, simp, simpDict, create=True, pBar=None): for i, shapeName in enumerate(toMake): if pBar is not None: - pBar.setLabelText("Creating Empty Shape:\n{0}".format(shapeName)) + pBar.setLabelText(f"Creating Empty Shape:\n{shapeName}") pBar.setValue(i) QApplication.processEvents() @@ -261,10 +261,10 @@ def preLoad(self, simp, simpDict, create=True, pBar=None): cmds.blendShape( self.shapeNode, edit=True, target=(self.mesh, index, baseShape, 1.0) ) - weightAttr = "{0}.weight[{1}]".format(self.shapeNode, index) + weightAttr = f"{self.shapeNode}.weight[{index}]" thing = cmds.ls(weightAttr)[0] - cmds.connectAttr("{0}.weights[{1}]".format(self.op, nextIndex), thing) + cmds.connectAttr(f"{self.op}.weights[{nextIndex}]", thing) nextIndex += 1 cmds.delete(baseShape) @@ -387,11 +387,11 @@ def _createShapeNode(self, name, mesh) -> str: cmds.delete(meshToFreeze, constructionHistory=True) cmds.setAttr(meshToFreeze + ".intermediateObject", isIntermediate) - name = "{}_BS".format(name) + name = f"{name}_BS" return cmds.blendShape(mesh, name=name, frontOfChain=True)[0] def _createPoseNode(self, name) -> str: - name = "{}_BP".format(name) + name = f"{name}_BP" return cmds.createNode("blendPose", name=name) def _createSimplexNode(self, name): @@ -404,7 +404,7 @@ def _createSimplexNode(self, name): def _createControlNode(self, name, op): tfmAttrs = [".tx", ".ty", ".tz", ".rx", ".ry", ".rz", ".sx", ".sy", ".sz", ".v"] - ctrl = cmds.group(empty=True, name="{0}_CTRL".format(name)) + ctrl = cmds.group(empty=True, name=f"{name}_CTRL") for attr in tfmAttrs: cmds.setAttr(ctrl + attr, keyable=False, channelBox=False) cmds.addAttr(ctrl, longName="solver", attributeType="message") @@ -458,13 +458,13 @@ def loadNodes(self, simp, thing, create=True, pBar=None): cmds.connectAttr(f"{self.poseNode}.message", f"{self.op}.poseMsg") def getShapeThing(self, shapeName): - s = cmds.ls("{0}.{1}".format(self.shapeNode, shapeName)) + s = cmds.ls(f"{self.shapeNode}.{shapeName}") if not s: return None return s[0] def getSliderThing(self, sliderName): - things = cmds.ls("{0}.{1}".format(self.ctrl, sliderName)) + things = cmds.ls(f"{self.ctrl}.{sliderName}") if not things: return None return things[0] @@ -491,7 +491,7 @@ def buildRestAbc(cls, abcMesh, name): cmds.setAttr(abcNode + ".speed", 24) # Is this needed anymore? cmds.setAttr(abcNode + ".time", 0) - importHead, importHeadShape = cls.buildDummyMesh("{0}_SIMPLEX".format(name)) + importHead, importHeadShape = cls.buildDummyMesh(f"{name}_SIMPLEX") cmds.connectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") cmds.polyEvaluate(importHead, vertex=True) # Force a refresh @@ -579,18 +579,16 @@ def loadAbc(self, abcMesh, js, pBar=None): for i, shapeName in enumerate(shapes): if pBar is not None: pBar.setValue(i) - pBar.setLabelText("Loading:\n{0}".format(shapeName)) + pBar.setLabelText(f"Loading:\n{shapeName}") QApplication.processEvents() if pBar.wasCanceled(): return index = self.getShapeIndex(shapeDict[shapeName]) cmds.setAttr(abcNode + ".time", i) - outAttr = "{0}.worldMesh[0]".format(importHead) - tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format( - self.shapeNode, index - ) - inAttr = "{0}.inputTargetItem[6000].inputGeomTarget".format(tgn) + outAttr = f"{importHead}.worldMesh[0]" + tgn = f"{self.shapeNode}.inputTarget[0].inputTargetGroup[{index}]" + inAttr = f"{tgn}.inputTargetItem[6000].inputGeomTarget" cmds.connectAttr(outAttr, inAttr, force=True) cmds.disconnectAttr(outAttr, inAttr) @@ -612,13 +610,13 @@ def getAllShapeVertices(self, shapes, pBar=None): if pBar is not None: # find the longest name for displaying stuff sns = "_" * max(list(map(len, [s.name for s in shapes]))) - pBar.setLabelText("Getting Shape:\n{0}".format(sns)) + pBar.setLabelText(f"Getting Shape:\n{sns}") pBar.setMaximum(len(shapes)) QApplication.processEvents() for i, shape in enumerate(shapes): if pBar is not None: - pBar.setLabelText("Getting Shape:\n{0}".format(shape.name)) + pBar.setLabelText(f"Getting Shape:\n{shape.name}") pBar.setValue(i) QApplication.processEvents() @@ -630,7 +628,7 @@ def getAllShapeVertices(self, shapes, pBar=None): out = mayaToNumpy(mpts)[:, :3] else: flatverts = cmds.xform( - "{0}.vtx[*]".format(self.mesh), + f"{self.mesh}.vtx[*]", translation=1, query=1, worldSpace=False, @@ -649,7 +647,7 @@ def getShapeVertices(self, shape): cmds.setAttr(shape.thing, 1.0) if np is None: flatverts = cmds.xform( - "{0}.vtx[*]".format(self.mesh), + f"{self.mesh}.vtx[*]", translation=1, query=1, worldSpace=False, @@ -881,7 +879,7 @@ def exportAbc( pBar.show() pBar.setMaximum(len(shapes)) spacerName = "_" * max(list(map(len, shapeNames))) - pBar.setLabelText("Exporting:\n{0}".format(spacerName)) + pBar.setLabelText(f"Exporting:\n{spacerName}") QApplication.processEvents() if ensureCorrect: @@ -897,7 +895,7 @@ def exportAbc( cmds.setAttr(v, 0.0) for i, shape in enumerate(shapes): if pBar is not None: - pBar.setLabelText("Exporting:\n{0}".format(shape.name)) + pBar.setLabelText(f"Exporting:\n{shape.name}") pBar.setValue(i) QApplication.processEvents() if pBar.wasCanceled(): @@ -923,7 +921,7 @@ def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): pBar.show() pBar.setMaximum(len(shapeNames)) spacerName = "_" * max(list(map(len, shapeNames))) - pBar.setLabelText("Exporting:\n{0}".format(spacerName)) + pBar.setLabelText(f"Exporting:\n{spacerName}") QApplication.processEvents() # Get all the sliderVecs @@ -946,7 +944,7 @@ def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): shpValArray = np.zeros((len(self.simplex.shapes), len(self.simplex.shapes))) for shpIdx, shape in enumerate(self.simplex.shapes): if pBar is not None: - pBar.setLabelText("Reading Full Shapes:\n{0}".format(shape.name)) + pBar.setLabelText(f"Reading Full Shapes:\n{shape.name}") pBar.setValue(shpIdx) QApplication.processEvents() if pBar.wasCanceled(): @@ -977,7 +975,7 @@ def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): deltaShapeArray = np.zeros((len(self.simplex.shapes), len(restVerts) * 3)) for shpOrderIdx, shape in enumerate(shapeOrder): if pBar is not None: - pBar.setLabelText("Collapsing to Deltas:\n{0}".format(shape.name)) + pBar.setLabelText(f"Collapsing to Deltas:\n{shape.name}") pBar.setValue(shpOrderIdx) QApplication.processEvents() if pBar.wasCanceled(): @@ -997,7 +995,7 @@ def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): schema = abcMesh.getSchema() for shpIdx, shape in enumerate(self.simplex.shapes): if pBar is not None: - pBar.setLabelText("writing:\n{0}".format(shape.name)) + pBar.setLabelText(f"writing:\n{shape.name}") pBar.setValue(shpIdx) QApplication.processEvents() if pBar.wasCanceled(): @@ -1088,14 +1086,14 @@ def deleteSystem(self): def createShape(self, shape, live=False, offset=10): with disconnected(self.shapeNode): try: - attrs = cmds.listAttr("{0}.weight[*]".format(self.shapeNode)) + attrs = cmds.listAttr(f"{self.shapeNode}.weight[*]") except ValueError: pass # Maya throws an error if there aren't any instead of # just returning an empty list else: for attr in attrs: - cmds.setAttr("{0}.{1}".format(self.shapeNode, attr), 0.0) + cmds.setAttr(f"{self.shapeNode}.{attr}", 0.0) newShape = cmds.duplicate(self.mesh, name=shape.name)[0] cmds.delete(newShape, constructionHistory=True) @@ -1103,11 +1101,11 @@ def createShape(self, shape, live=False, offset=10): cmds.blendShape( self.shapeNode, edit=True, target=(self.mesh, index, newShape, 1.0) ) - weightAttr = "{0}.weight[{1}]".format(self.shapeNode, index) + weightAttr = f"{self.shapeNode}.weight[{index}]" thing = cmds.ls(weightAttr)[0] shapeIndex = len(shape.simplex.shapes) - 1 - cmds.connectAttr("{0}.weights[{1}]".format(self.op, shapeIndex), thing) + cmds.connectAttr(f"{self.op}.weights[{shapeIndex}]", thing) if live: cmds.xform(newShape, relative=True, translation=[offset, 0, 0]) @@ -1154,29 +1152,27 @@ def extractWithDeltaShape(self, shape, live=True, offset=10.0): cmds.setAttr(v, 0.0) # store the delta shape - delta = cmds.duplicate(self.mesh, name="{0}_Delta".format(shape.name))[0] + delta = cmds.duplicate(self.mesh, name=f"{shape.name}_Delta")[0] # Extract the shape cmds.setAttr(shape.thing, 1.0) - extracted = cmds.duplicate( - self.mesh, name="{0}_Extract".format(shape.name) - )[0] + extracted = cmds.duplicate(self.mesh, name=f"{shape.name}_Extract")[0] # Store the initial shape - init = cmds.duplicate(extracted, name="{0}_Init".format(shape.name))[0] + init = cmds.duplicate(extracted, name=f"{shape.name}_Init")[0] # clear old orig objects for item in [delta, extracted, init]: self._clearShapes(item, doOrig=True) # build the deltaObj system - bs = cmds.blendShape(delta, name="{0}_DeltaBS".format(shape.name))[0] + bs = cmds.blendShape(delta, name=f"{shape.name}_DeltaBS")[0] cmds.blendShape(bs, edit=True, target=(delta, 0, init, 1.0)) cmds.blendShape(bs, edit=True, target=(delta, 1, extracted, 1.0)) - cmds.setAttr("{0}.{1}".format(bs, init), -1.0) - cmds.setAttr("{0}.{1}".format(bs, extracted), 1.0) + cmds.setAttr(f"{bs}.{init}", -1.0) + cmds.setAttr(f"{bs}.{extracted}", 1.0) # Cleanup nodeDict = {"Delta": delta, "Init": init} @@ -1196,17 +1192,15 @@ def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0 Useful for updating progressive shapes """ with disconnected(self.shapeNode): - for attr in cmds.listAttr("{0}.weight[*]".format(self.shapeNode)): - cmds.setAttr("{0}.{1}".format(self.shapeNode, attr), 0.0) + for attr in cmds.listAttr(f"{self.shapeNode}.weight[*]"): + cmds.setAttr(f"{self.shapeNode}.{attr}", 0.0) # Pull out the rest shape. we will blend this guy to the extraction - extracted = cmds.duplicate( - self.mesh, name="{0}_Extract".format(shape.name) - )[0] + extracted = cmds.duplicate(self.mesh, name=f"{shape.name}_Extract")[0] cmds.setAttr(shape.thing, 1.0) # Store the initial shape - init = cmds.duplicate(self.mesh, name="{0}_Init".format(shape.name))[0] + init = cmds.duplicate(self.mesh, name=f"{shape.name}_Init")[0] # clear old orig objects for item in [init, extracted]: @@ -1216,19 +1210,17 @@ def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0 # build the restObj system cmds.select(clear=True) # 'cause maya - bs = cmds.blendShape(extracted, name="{0}_DeltaBS".format(shape.name))[0] + bs = cmds.blendShape(extracted, name=f"{shape.name}_DeltaBS")[0] cmds.blendShape(bs, edit=True, target=(extracted, 0, init, 1.0)) cmds.blendShape(bs, edit=True, target=(extracted, 1, deltaPar, 1.0)) - cmds.setAttr("{0}.{1}".format(bs, init), 1.0) - cmds.setAttr("{0}.{1}".format(bs, deltaPar), value) + cmds.setAttr(f"{bs}.{init}", 1.0) + cmds.setAttr(f"{bs}.{deltaPar}", value) - outCnx = "{0}.worldMesh[0]".format(delta) - inCnx = "{0}.inputTarget[0].inputTargetGroup[{1}].inputTargetItem[6000].inputGeomTarget".format( - bs, 1 - ) + outCnx = f"{delta}.worldMesh[0]" + inCnx = f"{bs}.inputTarget[0].inputTargetGroup[{1}].inputTargetItem[6000].inputGeomTarget" cmds.connectAttr(outCnx, inCnx, force=True) - cmds.aliasAttr(delta, "{0}.{1}".format(bs, deltaPar)) + cmds.aliasAttr(delta, f"{bs}.{deltaPar}") # Cleanup nodeDict = {"Init": init} @@ -1255,13 +1247,11 @@ def extractShape(self, shape, live=True, offset=10.0): Can also store its starting shape and delta data """ with disconnected(self.shapeNode): - for attr in cmds.listAttr("{0}.weight[*]".format(self.shapeNode)): - cmds.setAttr("{0}.{1}".format(self.shapeNode, attr), 0.0) + for attr in cmds.listAttr(f"{self.shapeNode}.weight[*]"): + cmds.setAttr(f"{self.shapeNode}.{attr}", 0.0) cmds.setAttr(shape.thing, 1.0) - extracted = cmds.duplicate( - self.mesh, name="{0}_Extract".format(shape.name) - )[0] + extracted = cmds.duplicate(self.mesh, name=f"{shape.name}_Extract")[0] # Shift the extracted shape to the side cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) @@ -1281,7 +1271,7 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): """ if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) - mesh = "{0}_Extract".format(attrName) + mesh = f"{attrName}_Extract" chk = cmds.ls(mesh) if not chk: @@ -1292,13 +1282,11 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): raise ValueError(msg) index = self.getShapeIndex(shape) - tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) + tgn = f"{self.shapeNode}.inputTarget[0].inputTargetGroup[{index}]" cnx = mesh + "Shape" if cmds.nodeType(mesh) == "transform" else mesh - outAttr = "{0}.worldMesh[0]".format( - cnx - ) # Make sure to check the right shape object - inAttr = "{0}.inputTargetItem[6000].inputGeomTarget".format(tgn) + outAttr = f"{cnx}.worldMesh[0]" # Make sure to check the right shape object + inAttr = f"{tgn}.inputTargetItem[6000].inputGeomTarget" if not cmds.isConnected(outAttr, inAttr): cmds.connectAttr(outAttr, inAttr, force=True) @@ -1317,20 +1305,16 @@ def extractPosedShape(self, shape): def zeroShape(self, shape): """Set the shape to be completely zeroed""" index = self.getShapeIndex(shape) - tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) - shapeInput = "{0}.inputTargetItem[6000]".format(tgn) - cmds.setAttr( - "{0}.inputPointsTarget".format(shapeInput), 0, (), type="pointArray" - ) - cmds.setAttr( - "{0}.inputComponentsTarget".format(shapeInput), 0, "", type="componentList" - ) + tgn = f"{self.shapeNode}.inputTarget[0].inputTargetGroup[{index}]" + shapeInput = f"{tgn}.inputTargetItem[6000]" + cmds.setAttr(f"{shapeInput}.inputPointsTarget", 0, (), type="pointArray") + cmds.setAttr(f"{shapeInput}.inputComponentsTarget", 0, "", type="componentList") @undoable def deleteShape(self, toDelShape): """Remove a shape from the system""" index = self.getShapeIndex(toDelShape) - tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format(self.shapeNode, index) + tgn = f"{self.shapeNode}.inputTarget[0].inputTargetGroup[{index}]" cmds.removeMultiInstance(toDelShape.thing, b=True) cmds.removeMultiInstance(tgn, b=True) cmds.aliasAttr(toDelShape.thing, remove=True) @@ -1345,13 +1329,11 @@ def _rebuildShapeConnections(self): or [] ) for i, cnx in enumerate(cnxs): - if i % 2 == 0 and cnx.startswith("{0}.weights[".format(self.op)): + if i % 2 == 0 and cnx.startswith(f"{self.op}.weights["): cmds.disconnectAttr(cnxs[i], cnxs[i + 1]) for i, shape in enumerate(self.simplex.shapes): - cmds.connectAttr( - "{0}.weights[{1}]".format(self.op, i), shape.thing, force=True - ) + cmds.connectAttr(f"{self.op}.weights[{i}]", shape.thing, force=True) @undoable def forceRebuildShapeConnections(self): @@ -1361,7 +1343,7 @@ def forceRebuildShapeConnections(self): def renameShape(self, shape, name): """Change the name of the shape""" cmds.aliasAttr(name, shape.thing) - shape.thing = "{0}.{1}".format(self.shapeNode, name) + shape.thing = f"{self.shapeNode}.{name}" @undoable def convertShapeToCorrective(self, shape): @@ -1398,8 +1380,8 @@ def createSlider(self, slider): min=slider.minValue * self.sliderMul, max=slider.maxValue * self.sliderMul, ) - thing = "{0}.{1}".format(self.ctrl, slider.name) - cmds.connectAttr(thing, "{0}.sliders[{1}]".format(self.op, index)) + thing = f"{self.ctrl}.{slider.name}" + cmds.connectAttr(thing, f"{self.op}.sliders[{index}]") return thing @undoable @@ -1418,7 +1400,7 @@ def renameSlider(self, slider, name): min=self.sliderMul * min(vals), max=self.sliderMul * max(vals), ) - newThing = "{0}.{1}".format(self.ctrl, name) + newThing = f"{self.ctrl}.{name}" slider.thing = newThing for c in cnx: cmds.connectAttr(newThing, c) @@ -1427,7 +1409,7 @@ def renameSlider(self, slider, name): def setSliderRange(self, slider): """Set the range of a slider""" vals = [v.value for v in slider.prog.pairs] - attrName = "{0}.{1}".format(self.ctrl, slider.name) + attrName = f"{self.ctrl}.{slider.name}" cmds.addAttr( attrName, edit=True, @@ -1445,11 +1427,11 @@ def deleteSlider(self, toDelSlider): self.op, plugs=True, source=True, destination=False, connections=True ) for i, cnx in enumerate(cnxs): - if cnx.startswith("{0}.sliders".format(self.op)): + if cnx.startswith(f"{self.op}.sliders"): cmds.disconnectAttr(cnxs[i + 1], cnxs[i]) for i, slider in enumerate(self.simplex.sliders): - cmds.connectAttr(slider.thing, "{0}.sliders[{1}]".format(self.op, i)) + cmds.connectAttr(slider.thing, f"{self.op}.sliders[{i}]") @undoable def addProgFalloff(self, prog, falloff): @@ -1489,7 +1471,7 @@ def updateSlidersRange(self, sliders): ) def _doesDeltaExist(self, combo, target): - dshape = "{0}_DeltaShape".format(combo.name) + dshape = f"{combo.name}_DeltaShape" if not cmds.ls(dshape): return None par = cmds.listRelatives(dshape, allParents=1) @@ -1511,7 +1493,7 @@ def _clearShapes(self, item, doOrig=False): baseName = aname.split("|")[-1] baseName, digits = split_trailing_digits(baseName) - primary = "{0}|{1}Shape{2}".format(aname, baseName, digits) + primary = f"{aname}|{baseName}Shape{digits}" origs = [] others = [] @@ -1554,7 +1536,7 @@ def _rebuildSliderConnections(self): # Reconnect by name for i, sli in enumerate(self.simplex.sliders): thing = self.getSliderThing(sli.name) - cmds.connectAttr(thing, self.op + ".sliders[{0}]".format(i)) + cmds.connectAttr(thing, self.op + f".sliders[{i}]") # Combos def _reparentDeltaShapes(self, par, nodeDict, bsNode, toDelete=None): @@ -1641,7 +1623,7 @@ def _createTravDelta(self, trav, target, tVal, doReparent=True): with disconnected(floatShapes + tShapes): # pull out the rest shape - rest = cmds.duplicate(self.mesh, name="{0}_Rest".format(trav.name))[0] + rest = cmds.duplicate(self.mesh, name=f"{trav.name}_Rest")[0] sliDict = {} for pair in trav.startPoint.pairs: @@ -1653,23 +1635,21 @@ def _createTravDelta(self, trav, target, tVal, doReparent=True): vv = start + tVal * (end - start) cmds.setAttr(sliderCnx[slider.thing], vv) - deltaObj = cmds.duplicate( - self.mesh, name="{0}_Delta".format(trav.name) - )[0] - base = cmds.duplicate(deltaObj, name="{0}_Base".format(trav.name))[0] + deltaObj = cmds.duplicate(self.mesh, name=f"{trav.name}_Delta")[0] + base = cmds.duplicate(deltaObj, name=f"{trav.name}_Base")[0] # clear out all non-primary shapes so we don't have those 'Orig1' things floating around for item in [rest, deltaObj, base]: self._clearShapes(item, doOrig=True) # Build the delta blendshape setup - bs = cmds.blendShape(deltaObj, name="{0}_DeltaBS".format(trav.name))[0] + bs = cmds.blendShape(deltaObj, name=f"{trav.name}_DeltaBS")[0] cmds.blendShape(bs, edit=True, target=(deltaObj, 0, target, 1.0)) cmds.blendShape(bs, edit=True, target=(deltaObj, 1, base, 1.0)) cmds.blendShape(bs, edit=True, target=(deltaObj, 2, rest, 1.0)) - cmds.setAttr("{0}.{1}".format(bs, target), 1.0) - cmds.setAttr("{0}.{1}".format(bs, base), 1.0) - cmds.setAttr("{0}.{1}".format(bs, rest), 1.0) + cmds.setAttr(f"{bs}.{target}", 1.0) + cmds.setAttr(f"{bs}.{base}", 1.0) + cmds.setAttr(f"{bs}.{rest}", 1.0) # Cleanup if doReparent: @@ -1711,9 +1691,7 @@ def extractTraversalShape(self, trav, shape, live=True, offset=10.0): vv = start + val * (end - start) cmds.setAttr(sliderCnx[slider.thing], vv) - extracted = cmds.duplicate( - self.mesh, name="{0}_Extract".format(shape.name) - ) + extracted = cmds.duplicate(self.mesh, name=f"{shape.name}_Extract") extracted = extracted[0] self._clearShapes(extracted) cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) @@ -1727,7 +1705,7 @@ def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False) """Connect a shape into a Traversal progression""" if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) - mesh = "{0}_Extract".format(attrName) + mesh = f"{attrName}_Extract" chk = cmds.ls(mesh) if not chk: @@ -1777,7 +1755,7 @@ def _createComboDelta(self, combo, target, tVal, doReparent=True): cmds.setAttr(a, 0.0) # pull out the rest shape - rest = cmds.duplicate(self.mesh, name="{0}_Rest".format(combo.name))[0] + rest = cmds.duplicate(self.mesh, name=f"{combo.name}_Rest")[0] # set the combo values for pair in combo.pairs: @@ -1787,21 +1765,21 @@ def _createComboDelta(self, combo, target, tVal, doReparent=True): # weightPairs = [] # self.shapeNode = None # the deformer object - deltaObj = cmds.duplicate(self.mesh, name="{0}_Delta".format(combo.name))[0] - base = cmds.duplicate(deltaObj, name="{0}_Base".format(combo.name))[0] + deltaObj = cmds.duplicate(self.mesh, name=f"{combo.name}_Delta")[0] + base = cmds.duplicate(deltaObj, name=f"{combo.name}_Base")[0] # clear out all non-primary shapes so we don't have those 'Orig1' things floating around for item in [rest, deltaObj, base]: self._clearShapes(item, doOrig=True) # Build the delta blendshape setup - bs = cmds.blendShape(deltaObj, name="{0}_DeltaBS".format(combo.name))[0] + bs = cmds.blendShape(deltaObj, name=f"{combo.name}_DeltaBS")[0] cmds.blendShape(bs, edit=True, target=(deltaObj, 0, target, 1.0)) cmds.blendShape(bs, edit=True, target=(deltaObj, 1, base, 1.0)) cmds.blendShape(bs, edit=True, target=(deltaObj, 2, rest, 1.0)) - cmds.setAttr("{0}.{1}".format(bs, target), 1.0) - cmds.setAttr("{0}.{1}".format(bs, base), 1.0) - cmds.setAttr("{0}.{1}".format(bs, rest), 1.0) + cmds.setAttr(f"{bs}.{target}", 1.0) + cmds.setAttr(f"{bs}.{base}", 1.0) + cmds.setAttr(f"{bs}.{rest}", 1.0) # Cleanup if doReparent: @@ -1830,9 +1808,7 @@ def extractComboShape(self, combo, shape, live=True, offset=10.0): for pair in combo.pairs: cmds.setAttr(sliderCnx[pair.slider.thing], pair.value * tVal) - extracted = cmds.duplicate( - self.mesh, name="{0}_Extract".format(shape.name) - )[0] + extracted = cmds.duplicate(self.mesh, name=f"{shape.name}_Extract")[0] self._clearShapes(extracted) cmds.xform(extracted, relative=True, translation=(offset, 0, 0)) @@ -1848,7 +1824,7 @@ def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): """Connect a shape into a combo progression""" if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) - mesh = "{0}_Extract".format(attrName) + mesh = f"{attrName}_Extract" chk = cmds.ls(mesh) if not chk: @@ -1874,7 +1850,7 @@ def setDisabled(cls, op): bss = list(set(cmds.listConnections(op, type="blendShape"))) helpers = [] for bs in bss: - prop = "{0}.envelope".format(bs) + prop = f"{bs}.envelope" val = cmds.getAttr(prop) cmds.setAttr(prop, 0.0) if val != 0.0: @@ -2069,9 +2045,7 @@ def primeShapes(self, combo): try: # Make sure to check for any already incoming connections index = self.getShapeIndex(shape) - tgn = "{0}.inputTarget[0].inputTargetGroup[{1}]".format( - self.shapeNode, index - ) + tgn = f"{self.shapeNode}.inputTarget[0].inputTargetGroup[{index}]" isConnected = cmds.listConnections( tgn, source=True, destination=False ) diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index 91b6303b..b5aaefcf 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -247,7 +247,7 @@ def createCombo( from .progression import ProgPair, Progression if group is None: - gname = "DEPTH_{0}".format(len(sliders)) + gname = f"DEPTH_{len(sliders)}" matches = [i for i in simplex.comboGroups if i.name == gname] if matches: group = matches[0] @@ -297,7 +297,7 @@ def buildComboName(sliders, values): # get the extreme shape and percentage-ize its name extVal = 1.0 if value > 0.0 else -1.0 - valName = "{}".format(abs(int(value * 100))) + valName = f"{abs(int(value * 100))}" valName = "n" + valName if value < 0.0 else valName shape = slider.prog.getShapeAtValue(extVal) @@ -341,15 +341,13 @@ def solveType(self, newType: str): """Set the solveType of the combo""" stNames, stVals = list(zip(*self.solveTypes)) if newType not in stVals: - raise ValueError( - "Solve Type {0} not in allowed types {1}".format(newType, stVals) - ) + raise ValueError(f"Solve Type {newType} not in allowed types {stVals}") self._solveType = newType def sliderNameLinks(self): """ """ - sliNames = ["_{0}_".format(i.slider.name) for i in self.pairs] - surr = "_{0}_".format(self.name) + sliNames = [f"_{i.slider.name}_" for i in self.pairs] + surr = f"_{self.name}_" return [sn in surr for sn in sliNames] def nameLinks(self): @@ -379,7 +377,7 @@ def getSliderIndex(self, slider): for i, p in enumerate(self.pairs): if p.slider == slider: return i - raise ValueError("Provided slider:{0} is not in the list".format(slider.name)) + raise ValueError(f"Provided slider:{slider.name} is not in the list") def isFloating(self): """Floating combos are combos that Slider values that are between 0 and 1""" @@ -581,7 +579,7 @@ def setGroup(self, grp): if not isinstance(self, grp.groupType): raise ValueError( - "All items in this group must be of type: {}".format(grp.groupType) + f"All items in this group must be of type: {grp.groupType}" ) with self.moveItemManager(self, grp): diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index d104d511..ac4ebe6e 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -334,17 +334,17 @@ def getSidedName(self, name: str, sIdx: int) -> str: replace = replaces[sIdx] nn = name - s = "{0}{1}{0}".format(sdef.SEP, search) - r = "{0}{1}{0}".format(sdef.SEP, replace) + s = f"{sdef.SEP}{search}{sdef.SEP}" + r = f"{sdef.SEP}{replace}{sdef.SEP}" nn = nn.replace(s, r) - s = "{0}{1}".format(sdef.SEP, search) # handle Postfix - r = "{0}{1}".format(sdef.SEP, replace) + s = f"{sdef.SEP}{search}" # handle Postfix + r = f"{sdef.SEP}{replace}" if nn.endswith(s): nn = r.join(nn.rsplit(s, 1)) - s = "{1}{0}".format(sdef.SEP, search) # handle Prefix - r = "{1}{0}".format(sdef.SEP, replace) + s = f"{search}{sdef.SEP}" # handle Prefix + r = f"{replace}{sdef.SEP}" if nn.startswith(s): nn = nn.replace(s, r, 1) return nn @@ -601,9 +601,7 @@ def weights(self) -> npt.NDArray: if self._weights is None: if self._verts is None: raise ValueError( - "Attempted to auto-compute weights of a procedural falloff without setting verts: {0}".format( - self.name - ) + f"Attempted to auto-compute weights of a procedural falloff without setting verts: {self.name}" ) component = self.simplex.sdef.getComponentIndex(self.axis) self._weights = np.array( @@ -708,9 +706,7 @@ def weights(self) -> npt.NDArray: if self._weights is None: raise ValueError( - "Attempted to auto-compute weights of a map falloff: {}".format( - self.name - ) + f"Attempted to auto-compute weights of a map falloff: {self.name}" ) return self._weights diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index a5a6a1fe..0cb269dc 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -240,7 +240,7 @@ def take(self, things: list[GroupMember]): if not all(isinstance(i, self.groupType) for i in things): raise ValueError( - "All items in this group must be of type: {}".format(self.groupType) + f"All items in this group must be of type: {self.groupType}" ) # do it this way instead of using set() to keep order diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index 6cd59188..673acd7c 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -228,7 +228,7 @@ def getShapeIndex(self, shape: Shape) -> int: for i, p in enumerate(self.pairs): if p.shape == shape: return i - raise ValueError("Provided shape:{0} is not in the list".format(shape.name)) + raise ValueError(f"Provided shape:{shape.name} is not in the list") def getShapes(self) -> list[Shape]: """Return the Shapes in this Progression @@ -492,9 +492,7 @@ def newProgPair( shapeName = self.controller.name else: neg = "n" if tVal < 0.0 else "" - shapeName = "{0}_{1}{2}".format( - self.controller.name, neg, int(abs(tVal) * 100) - ) + shapeName = f"{self.controller.name}_{neg}{int(abs(tVal) * 100)}" currentNames = [i.name for i in self.simplex.shapes] shapeName = getNextName(shapeName, currentNames) diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index 96be398e..decd14c7 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -68,9 +68,7 @@ def __init__(self, name: str, simplex: Simplex, create: bool = True): if create: self.thing = self.DCC.createShape(self) else: - raise RuntimeError( - "Unable to find existing shape: {0}".format(self.name) - ) + raise RuntimeError(f"Unable to find existing shape: {self.name}") else: self.thing = newThing diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 4240fbe9..6bc8bc41 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -356,9 +356,7 @@ def buildSystemFromSmpx( dccCount = DCC.vertCount(thing) if smpxCount != dccCount: raise RuntimeError( - "Point Count Mismatch. Smpx File:{0} DCC:{1}".format( - smpxCount, dccCount - ) + f"Point Count Mismatch. Smpx File:{smpxCount} DCC:{dccCount}" ) del iarch, abcMesh # release the files @@ -425,9 +423,7 @@ def buildSystemFromFile( pBar=pBar, ) else: - raise ValueError( - "The filepath provided is not a .json or .smpx: {0}".format(path) - ) + raise ValueError(f"The filepath provided is not a .json or .smpx: {path}") @classmethod def buildSystemFromMesh( @@ -1175,7 +1171,7 @@ def getRestName(self) -> str: : str The default rest shape name """ - return "Rest_{0}".format(self.name) + return f"Rest_{self.name}" def dump(self) -> str: """Dump the definition dictionary to a json string @@ -1613,15 +1609,13 @@ def split( ) if pSplit != sSplit: - msg = "Bad Prog: {0}".format(prog.name) + msg = f"Bad Prog: {prog.name}" raise ValueError( "A progression is not fully splittable\n" + msg ) if pSplit != cSplit: - msg = "Bad prog: {0}\nBad Controller:{1}".format( - prog.name, ctrl.name - ) + msg = f"Bad prog: {prog.name}\nBad Controller:{ctrl.name}" raise ValueError("A controller is not fully splittable\n" + msg) # Create the initial deepcopy @@ -1635,10 +1629,10 @@ def split( for axis, foList in foByAxis.items(): if pBar is not None: - pBar.setLabelText("Splitting On {0} axis".format(axis)) + pBar.setLabelText(f"Splitting On {axis} axis") QApplication.processEvents() else: - print("Splitting On {0} axis".format(axis)) + print(f"Splitting On {axis} axis") # Get the items to split, and the memo that ensures *only* those # items will be copied when we deepcopy diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index a2448784..d702a599 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -92,9 +92,7 @@ def __init__( if create: self.thing = simplex.DCC.createSlider(self) else: - raise RuntimeError( - "Unable to find existing shape: {0}".format(self.name) - ) + raise RuntimeError(f"Unable to find existing shape: {self.name}") else: self.thing = newThing @@ -148,7 +146,7 @@ def createSlider( if simplex.sliderGroups: group = simplex.sliderGroups[0] else: - group = Group("{0}_GROUP".format(name), simplex, Slider) + group = Group(f"{name}_GROUP", simplex, Slider) currentNames = [s.name for s in simplex.sliders] name = getNextName(name, currentNames) @@ -199,7 +197,7 @@ def createMultiSlider( if simplex.sliderGroups: group = simplex.sliderGroups[0] else: - group = Group("{0}_GROUP".format(name), simplex, Slider) + group = Group(f"{name}_GROUP", simplex, Slider) currentNames = [s.name for s in simplex.sliders] name = getNextName(name, currentNames) @@ -586,7 +584,7 @@ def setGroup(self, grp: Group): if not isinstance(self, grp.groupType): raise ValueError( - "All items in this group must be of type: {}".format(grp.groupType) + f"All items in this group must be of type: {grp.groupType}" ) with self.moveItemManager(self, grp): diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index ab0bfdff..86f7d06f 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -332,7 +332,7 @@ def createTraversal( for c in reversed(list(range(count))): val = (100 * (c + 1)) // count - pp = prog.createShape("{0}_{1}".format(name, val), val / 100.0) + pp = prog.createShape(f"{name}_{val}", val / 100.0) simplex.DCC.zeroShape(pp.shape) return trav @@ -412,7 +412,7 @@ def setGroup(self, grp: Group): if not isinstance(self, grp.groupType): raise ValueError( - "All items in this group must be of type: {}".format(grp.groupType) + f"All items in this group must be of type: {grp.groupType}" ) if self.group: @@ -476,8 +476,8 @@ def buildTraversalName(ranges: dict[Slider, tuple[float, float]]) -> str: return "Tv_" + "_".join(parts) def controllerNameLinks(self) -> list[bool]: - surr = "_{0}_".format(self.name) - return ["_{0}_".format(sli) in surr for sli in self.allSliders()] + surr = f"_{self.name}_" + return [f"_{sli}_" in surr for sli in self.allSliders()] def nameLinks(self) -> list[bool]: # In this case, these names will *NOT* have the possibility of diff --git a/src/python/simplexui/menu/genericPlugins/unsubdivide.py b/src/python/simplexui/menu/genericPlugins/unsubdivide.py index 9b36382c..de1486fa 100644 --- a/src/python/simplexui/menu/genericPlugins/unsubdivide.py +++ b/src/python/simplexui/menu/genericPlugins/unsubdivide.py @@ -54,12 +54,12 @@ def exportUnsubInterface(window): outPath = path.replace(".smpx", "_UNSUB.smpx") if path == outPath: - QMessageBox.warning(window, "Unable to rename smpx file: {}".format(path)) + QMessageBox.warning(window, f"Unable to rename smpx file: {path}") return if os.path.isfile(outPath): btns = QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel - msg = "Unsub file already exists.\n{0}\nOverwrite?".format(outPath) + msg = f"Unsub file already exists.\n{outPath}\nOverwrite?" response = QMessageBox.question(window, "File already exists", msg, btns) if not response & QMessageBox.StandardButton.Ok: return diff --git a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py index 1850c0de..6a92e550 100644 --- a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py +++ b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py @@ -98,7 +98,7 @@ def freezeCombo(combo): # Extract this fully-on shape fullGeo = cmds.duplicate( - simplex.DCC.mesh, name="{0}_Freeze".format(freezeShape.name) + simplex.DCC.mesh, name=f"{freezeShape.name}_Freeze" )[0] fullGeos.append(fullGeo) @@ -139,7 +139,7 @@ def freezeCombo(combo): # connect the output shape.thing to the basicBS # Create an empty shape. Do it like this to get the automated renaming stuff - gDup = cmds.duplicate(geo, name="{0}_DeltaCnx".format(shape.name))[0] + gDup = cmds.duplicate(geo, name=f"{shape.name}_DeltaCnx")[0] # The 4th value must be 1.0 so the blendshape auto-names cmds.blendShape(bbs, edit=True, target=(geo, idx, gDup, 1.0)) cmds.blendShape(bbs, edit=True, weight=(idx, -val)) diff --git a/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py index 00dec24e..a34a2976 100644 --- a/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py +++ b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py @@ -83,7 +83,7 @@ def generateShapeIncrementalsContext(indexes, window): percent = int(float(i) * 100 / increments) cmds.blendShape(bs, edit=True, weight=((0, val))) - nne = endObj.replace("_100_", "_{0}_".format(percent)) + nne = endObj.replace("_100_", f"_{percent}_") nn = nne.replace("_Extract", "") inc = cmds.duplicate(shapeDup, name=nne) incs.append((percent, nn, nne)) diff --git a/src/python/simplexui/menu/mayaPlugins/importObjs.py b/src/python/simplexui/menu/mayaPlugins/importObjs.py index c1ca88b5..5c42caa2 100644 --- a/src/python/simplexui/menu/mayaPlugins/importObjs.py +++ b/src/python/simplexui/menu/mayaPlugins/importObjs.py @@ -35,7 +35,7 @@ def buildMesh(simplex, mesh): def importSimpleObjs(simplex, orders, pBar): for shapeName, ctrl, shape, path in orders: pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Loading Obj :\n{0}".format(shapeName)) + pBar.setLabelText(f"Loading Obj :\n{shapeName}") QApplication.processEvents() if pBar.wasCanceled(): return @@ -62,7 +62,7 @@ def importReorderObjs(simplex, orders, pBar): for shapeName, ctrl, shape, path in orders: pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Loading Obj :\n{0}".format(shapeName)) + pBar.setLabelText(f"Loading Obj :\n{shapeName}") QApplication.processEvents() if pBar.wasCanceled(): return diff --git a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py index 25fe7185..c86ec3e1 100644 --- a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py +++ b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py @@ -21,10 +21,10 @@ from Qt.QtWidgets import QAction dn = os.path.dirname -SHELF_DEV_BUTTON = """ +SHELF_DEV_BUTTON = f""" import os, sys -path = r'{0}' +path = r'{dn(dn(dn(dn(__file__))))}' path = os.path.normcase(os.path.normpath(path)) if sys.path[0] != path: sys.path.insert(0, path) @@ -52,7 +52,7 @@ simplexui.runSimplexUI() sys.path.pop(0) -""".format(dn(dn(dn(dn(__file__))))) +""" def registerTool(window, menu): diff --git a/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py index f2e488b0..2ea5ec51 100644 --- a/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py +++ b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py @@ -55,7 +55,7 @@ def relaxToSelection(source, target): maxValue=100, defaultValue=10, ) - smoothIter = "{0}.smooth_iter".format(deltaMushRelax) + smoothIter = f"{deltaMushRelax}.smooth_iter" cmds.setAttr(smoothIter, edit=True, keyable=True) blender = cmds.blendShape(targetDup, sourceDup) diff --git a/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py index fc97da41..a25b0e16 100644 --- a/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py +++ b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py @@ -66,8 +66,6 @@ def snapShapeToNeutral(source, target): # But set the weights back to 0.0 for painting numVerts = cmds.polyEvaluate(source, vertex=1) - setter = "{0}.inputTarget[0].inputTargetGroup[0].targetWeights[0:{1}]".format( - bs, numVerts - 1 - ) + setter = f"{bs}.inputTarget[0].inputTargetGroup[0].targetWeights[0:{numVerts - 1}]" weights = [0.0] * numVerts cmds.setAttr(setter, *weights, size=numVerts) diff --git a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py index 27b11619..e1b0c6bb 100644 --- a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py +++ b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py @@ -32,7 +32,7 @@ def softSelectToClusterInterface(): sel = cmds.ls(sl=True, objectsOnly=True) if sel: name = sel[0].split("|")[-1] - softSelectToCluster(sel[0], "{0}_Soft".format(name)) + softSelectToCluster(sel[0], f"{name}_Soft") def getSoftSelectionValues(myNode, returnSimpleIndices=True): @@ -182,7 +182,7 @@ def softSelectToCluster(tfm, name): shapes = [k for k in softSelDict if k.startswith(tfm)] if not shapes: - print("No selection found on the given mesh: {0}".format(tfm)) + print(f"No selection found on the given mesh: {tfm}") return elementIndices, elementWeights = softSelDict[shapes[0]] @@ -192,7 +192,7 @@ def softSelectToCluster(tfm, name): # Build the Cluster and set the weights # Currently this part is polymesh specific clusterNode, clusterHandle = cmds.cluster(tfm, name=name) - attr = "{0}.weightList[0].weights[0:{1}]".format(clusterNode, vnum - 1) + attr = f"{clusterNode}.weightList[0].weights[0:{vnum - 1}]" cmds.setAttr(attr, *weights, size=vnum) # Reposition the cluster diff --git a/src/python/simplexui/menu/mayaPlugins/tweakMix.py b/src/python/simplexui/menu/mayaPlugins/tweakMix.py index d19d3f46..7e70b7d7 100644 --- a/src/python/simplexui/menu/mayaPlugins/tweakMix.py +++ b/src/python/simplexui/menu/mayaPlugins/tweakMix.py @@ -111,7 +111,7 @@ def tweakMix(simplex, combos, live): cmds.setAttr(tshape.thing, shapeVal) # print "setAttr", tshape.thing, shapeVal tweakMesh = cmds.duplicate( - simplex.DCC.mesh, name="{0}_Tweak".format(tshape.name) + simplex.DCC.mesh, name=f"{tshape.name}_Tweak" )[0] tweakMeshes.append(tweakMesh) cmds.setAttr(tshape.thing, 0.0) diff --git a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py index 131c8518..346cdc34 100644 --- a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py +++ b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py @@ -52,9 +52,7 @@ def updateRestShapeInterface(window): meshVerts = cmds.polyEvaluate(mesh, vertex=1) if selVerts != meshVerts: - msg = "Selected object {0} has {1} verts\nBase Object has {2} verts".format( - sel, selVerts, meshVerts - ) + msg = f"Selected object {sel} has {selVerts} verts\nBase Object has {meshVerts} verts" QMessageBox.warning(window, "Vert Mismatch", msg) return @@ -102,13 +100,13 @@ def updateRestShape(mesh, newRest, window=None): QMessageBox.warning( window, "Too Many Intermediates", - "Too Many intermediate meshes found: {0}".format(origs), + f"Too Many intermediate meshes found: {origs}", ) return orig = origs[0] - outMesh = "{0}.worldMesh[0]".format(newRest) - inMesh = "{0}.inMesh".format(orig) + outMesh = f"{newRest}.worldMesh[0]" + inMesh = f"{orig}.inMesh" cmds.connectAttr(outMesh, inMesh, force=1) cmds.refresh(force=1) diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 5f73a6b0..57e3d4ac 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -704,9 +704,9 @@ def _getAName(self, tpe, default=None, taken=(), uniqueAccept=False): unique = False default = getNextName(default, taken) while not unique: - eMsg = "Enter a name for the new {0}".format(tpe) + eMsg = f"Enter a name for the new {tpe}" newName, good = QInputDialog.getText( - self, "New {0}".format(uTpe), eMsg, text=default + self, f"New {uTpe}", eMsg, text=default ) if not good: return None @@ -937,7 +937,7 @@ def shapeConnectScene(self): # ProgressBar pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Connecting:\n{0}".format(pair.shape.name)) + pBar.setLabelText(f"Connecting:\n{pair.shape.name}") QApplication.processEvents() if pBar.wasCanceled(): return @@ -1021,7 +1021,7 @@ def shapeIndexExport(self, indexes): # ProgressBar pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Extracting:\n{0}".format(pair.shape.name)) + pBar.setLabelText(f"Extracting:\n{pair.shape.name}") QApplication.processEvents() if pBar.wasCanceled(): return @@ -1061,7 +1061,7 @@ def shapeIndexExtract(self, indexes, live=None): # ProgressBar pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Extracting:\n{0}".format(pair.shape.name)) + pBar.setLabelText(f"Extracting:\n{pair.shape.name}") QApplication.processEvents() if pBar.wasCanceled(): return extracted @@ -1098,7 +1098,7 @@ def shapeConnectIndexes(self, indexes): # ProgressBar pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Extracting:\n{0}".format(pair.shape.name)) + pBar.setLabelText(f"Extracting:\n{pair.shape.name}") QApplication.processEvents() if pBar.wasCanceled(): return @@ -1140,7 +1140,7 @@ def shapeMatchIndexes(self, indexes): # ProgressBar pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Matching:\n{0}".format(pair.shape.name)) + pBar.setLabelText(f"Matching:\n{pair.shape.name}") QApplication.processEvents() if pBar.wasCanceled(): return @@ -1343,7 +1343,7 @@ def loadFile(self, path): def _fileDialog(self, title, initPath, filters, save=True): """Convenience function for displaying File Dialogs""" - filters = ["{0} (*.{0})".format(f) for f in filters] + filters = [f"{f} (*.{f})" for f in filters] if not save: filters += ["All files (*.*)"] filters = ";;".join(filters) diff --git a/src/python/simplexui/travCheckDialog.py b/src/python/simplexui/travCheckDialog.py index 7aca49aa..ee98eabf 100644 --- a/src/python/simplexui/travCheckDialog.py +++ b/src/python/simplexui/travCheckDialog.py @@ -371,11 +371,7 @@ def _populate(self): maxPoss=self.maxPoss, ) - lbl = ( - "Too many possibilities. Limiting to {0}".format(self.maxPoss) - if tooMany - else "" - ) + lbl = f"Too many possibilities. Limiting to {self.maxPoss}" if tooMany else "" self.uiWarningLBL.setText(lbl) self.uiTravCheckTREE.clear() diff --git a/src/python/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py index 1b730ead..1b300276 100644 --- a/src/python/simplexui/traversalDialog.py +++ b/src/python/simplexui/traversalDialog.py @@ -290,7 +290,7 @@ def shapeConnectFromSelection(self): # ProgressBar pBar.setValue(pBar.value() + 1) - pBar.setLabelText("Connecting:\n{0}".format(pair.shape.name)) + pBar.setLabelText(f"Connecting:\n{pair.shape.name}") QApplication.processEvents() if pBar.wasCanceled(): return From 827bb36a09d92769be6a85261efa05c00dc7cdca Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 11 Sep 2026 16:42:53 -0700 Subject: [PATCH 40/47] Fix a bunch of U and F lint warnings --- src/python/simplexui/comboCheckDialog.py | 2 +- .../simplexui/commands/alembicCommon.py | 6 ++--- .../simplexui/commands/applyCorrectives.py | 4 ++-- src/python/simplexui/dragFilter.py | 1 - src/python/simplexui/falloffDialog.py | 5 ++--- .../simplexui/interface/mayaInterface.py | 22 +++++++++---------- src/python/simplexui/interfaceModel.py | 2 +- src/python/simplexui/items/accessor.py | 2 +- src/python/simplexui/items/combo.py | 2 +- src/python/simplexui/items/falloff.py | 2 +- src/python/simplexui/items/group.py | 2 +- src/python/simplexui/items/progression.py | 4 ++-- src/python/simplexui/items/shape.py | 2 +- src/python/simplexui/items/simplex.py | 4 ++-- src/python/simplexui/items/slider.py | 2 +- src/python/simplexui/items/stack.py | 2 +- src/python/simplexui/items/traversal.py | 2 +- src/python/simplexui/items/treeItem.py | 2 +- src/python/simplexui/utils.py | 3 +-- 19 files changed, 33 insertions(+), 38 deletions(-) diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index 4dbc7785..6709da0d 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -39,7 +39,7 @@ from .utils import getUiFile -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING if TYPE_CHECKING: from .items import Simplex diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 7ded8187..39bbe0b6 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -18,7 +18,6 @@ """Alembic files can be difficult to work with, and can be *very* slow in Python This is a library of convenience functions with the numpy speed optimizations """ - from __future__ import annotations import os @@ -53,7 +52,6 @@ from typing import ( TYPE_CHECKING, - Optional, Union, TypeVar, overload, @@ -891,7 +889,7 @@ def getSmpxArchiveData(abcPath: str) -> tuple[IArchive, IPolyMesh, str]: The json definition string """ if not os.path.isfile(str(abcPath)): - raise IOError("File does not exist: " + str(abcPath)) + raise OSError("File does not exist: " + str(abcPath)) iarch = IArchive(str(abcPath)) # because alembic hates unicode top, par, abcMesh = [None] * 3 try: @@ -1056,7 +1054,7 @@ def readFalloffData(abcPath: str) -> dict[str, npfloat]: """ if not os.path.isfile(str(abcPath)): - raise IOError("File does not exist: " + str(abcPath)) + raise OSError("File does not exist: " + str(abcPath)) iarch = IArchive(str(abcPath)) # because alembic hates unicode top, par, systemSchema, foPropPar, foProp = [None] * 5 try: diff --git a/src/python/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py index 42ca4f17..ce55ce4c 100644 --- a/src/python/simplexui/commands/applyCorrectives.py +++ b/src/python/simplexui/commands/applyCorrectives.py @@ -104,7 +104,7 @@ def loadSimplex(shapePath): """ if not os.path.isfile(str(shapePath)): - raise IOError("File does not exist: " + str(shapePath)) + raise OSError("File does not exist: " + str(shapePath)) jsString, counts, verts, faces, uvs, uvFaces = readSmpx(shapePath) @@ -140,7 +140,7 @@ def writeSimplex(inPath, outPath, newShapes, name="Face", pBar=None): """ if not os.path.isfile(str(inPath)): - raise IOError("File does not exist: " + str(inPath)) + raise OSError("File does not exist: " + str(inPath)) iarch, abcMesh, jsString = getSmpxArchiveData(inPath) faces, counts = getStaticMeshArrays(abcMesh) diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index 91eb4edd..63ed77eb 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -22,7 +22,6 @@ from enum import Enum -from typing import Optional class DragType(Enum): diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index f1ea151e..5070d8ae 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -18,7 +18,6 @@ # This module imports QT from PyQt4, PySide or PySide2 # Depending on what's available from __future__ import annotations -import struct from .interfaceModel import FalloffDataModel from .items.falloff import PlanarFalloff from Qt import QtCompat @@ -58,7 +57,7 @@ QWidget, ) from .utils import getNextName, getUiFile, Prefs -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING if TYPE_CHECKING: from .simplexDialog import SimplexDialog @@ -386,7 +385,7 @@ def loadSimplex(self): self.uiShapeFalloffCBOX.setModel(self.foModel) self._falloffMapper.setModel(self.foModel) - currentIndex = QByteArray("currentIndex".encode()) + currentIndex = QByteArray(b"currentIndex") self._falloffMapper.addMapping(self.uiFalloffTypeCBOX, 1, currentIndex) self._falloffMapper.addMapping(self.uiFalloffAxisCBOX, 2, currentIndex) diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index f59b84c4..7adc0fee 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -304,7 +304,7 @@ def _getOpNodes(self, thing: str): for sn in rawShapeNodes + rawPoseNodes: op = ( cmds.listConnections( - "{0}.{1}".format(sn, "message"), + f"{sn}.message", source=False, destination=True, type="simplex_maya", @@ -331,7 +331,7 @@ def _getShapeNodes(self, ops): for op in ops: try: sn = cmds.listConnections( - "{0}.{1}".format(op, "shapeMsg"), + f"{op}.shapeMsg", source=True, destination=False, type="blendShape", @@ -347,7 +347,7 @@ def _getPoseNodes(self, ops): for op in ops: try: sn = cmds.listConnections( - "{0}.{1}".format(op, "poseMsg"), + f"{op}.poseMsg", source=True, destination=False, type="blendPose", @@ -362,7 +362,7 @@ def _getCtrlNodes(self, ops): ctrlCnx = [] for op in ops: ccnx = cmds.listConnections( - "{0}.{1}".format(op, "ctrlMsg"), + f"{op}.ctrlMsg", source=True, destination=False, ) @@ -409,8 +409,8 @@ def _createControlNode(self, name, op): cmds.setAttr(ctrl + attr, keyable=False, channelBox=False) cmds.addAttr(ctrl, longName="solver", attributeType="message") cmds.connectAttr( - "{0}.{1}".format(ctrl, "solver"), - "{0}.{1}".format(op, "ctrlMsg"), + f"{ctrl}.solver", + f"{op}.ctrlMsg", ) return ctrl @@ -574,7 +574,7 @@ def loadAbc(self, abcMesh, js, pBar=None): pBar.setMaximum(len(shapes)) longName = max(shapes, key=len) pBar.setValue(1) - pBar.setLabelText("Loading:\n{0}".format("_" * len(longName))) + pBar.setLabelText("Loading:\n{}".format("_" * len(longName))) for i, shapeName in enumerate(shapes): if pBar is not None: @@ -1022,7 +1022,7 @@ def exportMesh(self, mesh, path): # Revision tracking def getRevision(self): try: - return cmds.getAttr("{0}.{1}".format(self.op, "revision")) + return cmds.getAttr(f"{self.op}.revision") except ValueError: # object does not exist return None @@ -1032,14 +1032,14 @@ def incrementRevision(self): value = self.getRevision() if value is None: return - cmds.setAttr("{0}.{1}".format(self.op, "revision"), value + 1) + cmds.setAttr(f"{self.op}.revision", value + 1) jsString = self.simplex.dump() self.setSimplexString(self.op, jsString) return value + 1 @undoable def setRevision(self, val): - cmds.setAttr("{0}.{1}".format(self.op, "revision"), val) + cmds.setAttr(f"{self.op}.revision", val) # System level @undoable @@ -1883,7 +1883,7 @@ def getSimplexOperatorsOnObject(cls, thing): out = [] for op in ops: shapeNode = cmds.listConnections( - "{0}.{1}".format(op, "shapeMsg"), source=True, destination=False + f"{op}.shapeMsg", source=True, destination=False ) if not shapeNode: continue diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index 895e683f..7c813bed 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -35,7 +35,7 @@ from Qt.QtWidgets import QWidget from Qt.QtCore import QModelIndex, QSortFilterProxyModel, Qt -from typing import cast, TypeVar, Optional, Any +from typing import cast, TypeVar, Any T = TypeVar('T', bound=TreeItem) diff --git a/src/python/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py index dfb5f3c1..ae5ad89b 100644 --- a/src/python/simplexui/items/accessor.py +++ b/src/python/simplexui/items/accessor.py @@ -18,7 +18,7 @@ import copy -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from .treeItem import TreeItem if TYPE_CHECKING: diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index b5aaefcf..1385ded6 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -21,7 +21,7 @@ from .dragItem import Draggable -from typing import TYPE_CHECKING, Optional, Any +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from .group import Group diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index ac4ebe6e..d7db6db1 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -23,7 +23,7 @@ from .accessor import SimplexAccessor from .stack import stackable -from typing import Optional, Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING if TYPE_CHECKING: from .simplex import Simplex, DCCObject diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index 0cb269dc..bc5f113b 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -19,7 +19,7 @@ from .stack import stackable from .treeItem import TreeItem -from typing import Optional, Union, Any, TYPE_CHECKING +from typing import Union, Any, TYPE_CHECKING if TYPE_CHECKING: from .simplex import Simplex diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index 673acd7c..e55cc29c 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -22,7 +22,7 @@ from .dragItem import Draggable -from typing import Optional, Union, Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING if TYPE_CHECKING: from .simplex import Simplex @@ -395,7 +395,7 @@ def setShapesValues(self, values: list[float]): def siblingRename( self, shape: Shape, newName: str, currentLinks: dict - ) -> dict[type, dict[str, tuple[SimplexAccessor, int]]]: + ) -> dict[type, dict[str, tuple[SimplexTreeAccessor, int]]]: # This is part of the in-progress linked naming system # get name change return {} diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index decd14c7..a7947431 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -19,7 +19,7 @@ from .accessor import SimplexAccessor from .stack import stackable from numpy import typing as npt -from typing import Optional, Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING if TYPE_CHECKING: from .slider import Slider diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 6bc8bc41..23d739a1 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -41,7 +41,7 @@ from Qt.QtGui import QColor from Qt.QtWidgets import QApplication -from typing import Optional, Any, TYPE_CHECKING, Union +from typing import Any, TYPE_CHECKING, Union if TYPE_CHECKING: @@ -1602,7 +1602,7 @@ def split( sSplit = sSplit[0] if not sSplitSame: shapes = [i.name for i in prog.getShapes()] - msg = "Bad shapes: {0}".format(", ".join(shapes)) + msg = "Bad shapes: {}".format(", ".join(shapes)) raise ValueError( "Mix of splittable and un-splittable shapes in a progression\n" + msg diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index d702a599..7cb739a5 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -27,7 +27,7 @@ from .treeItem import TreeItem from .dragItem import Draggable -from typing import Optional, Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING if TYPE_CHECKING: from .simplex import DCCObject, Simplex diff --git a/src/python/simplexui/items/stack.py b/src/python/simplexui/items/stack.py index 069ea1c4..2655fa6b 100644 --- a/src/python/simplexui/items/stack.py +++ b/src/python/simplexui/items/stack.py @@ -19,7 +19,7 @@ from collections import OrderedDict from contextlib import contextmanager from functools import wraps -from typing import Any, TYPE_CHECKING, Optional +from typing import Any, TYPE_CHECKING from ..interface import undoContext diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index 86f7d06f..71ce7cc2 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -26,7 +26,7 @@ from enum import Enum -from typing import Optional, Union, Any, TYPE_CHECKING +from typing import Any, TYPE_CHECKING if TYPE_CHECKING: from .simplex import Simplex, DCCObject diff --git a/src/python/simplexui/items/treeItem.py b/src/python/simplexui/items/treeItem.py index ee89a2e1..5ea76a32 100644 --- a/src/python/simplexui/items/treeItem.py +++ b/src/python/simplexui/items/treeItem.py @@ -49,7 +49,7 @@ """ from __future__ import annotations -from typing import Optional, Any, Generator, Callable, Iterator, Literal, overload +from typing import Any, Generator, Callable, Iterator, Literal, overload from Qt.QtGui import QIcon from Qt.QtCore import QAbstractItemModel, QModelIndex, Qt, QObject from Qt.QtWidgets import QTreeView diff --git a/src/python/simplexui/utils.py b/src/python/simplexui/utils.py index 87ef2dc7..c1cef4a9 100755 --- a/src/python/simplexui/utils.py +++ b/src/python/simplexui/utils.py @@ -22,7 +22,7 @@ import os import re import sys -from typing import Union, TypeVar, Callable, Optional, cast, Sequence, Any +from typing import TypeVar, Callable, cast, Sequence, Any from Qt import IsPySide6, IsPyQt6 @@ -280,7 +280,6 @@ class Prefs: """A wrapper for reading/writing prefs both internal and external to blur""" def __init__(self): - self._pref: "blurdev.prefs.Preference" | QSettings if AT_BLUR: import blurdev.prefs From c86173475b01e97b2f55131f0224acbcc73b51e4 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Fri, 11 Sep 2026 16:47:17 -0700 Subject: [PATCH 41/47] isort --- src/python/simplexui/__init__.py | 1 + src/python/simplexui/channelBox.py | 6 ++- src/python/simplexui/comboCheckDialog.py | 10 ++--- .../simplexui/commands/alembicCommon.py | 18 ++++---- .../simplexui/commands/applyCorrectives.py | 4 +- .../simplexui/commands/correctiveInterface.py | 4 +- .../simplexui/commands/expandedExport.py | 5 ++- src/python/simplexui/commands/hdf5Convert.py | 1 + .../commands/mayaCorrectiveInterface.py | 4 +- src/python/simplexui/commands/mayatonumpy.py | 9 ++-- src/python/simplexui/commands/poseblendlib.py | 1 + .../commands/reorderSimplexPoints.py | 5 ++- src/python/simplexui/commands/rigidAlign.py | 1 + src/python/simplexui/commands/smpxBlend.py | 1 + src/python/simplexui/commands/unsubdivide.py | 5 ++- src/python/simplexui/commands/uvTransfer.py | 1 + src/python/simplexui/dragFilter.py | 7 ++- src/python/simplexui/falloffDialog.py | 19 ++++---- src/python/simplexui/interface/__init__.py | 1 + .../simplexui/interface/dummyInterface.py | 9 ++-- .../simplexui/interface/mayaInterface.py | 9 ++-- src/python/simplexui/interfaceModel.py | 17 ++++---- src/python/simplexui/interfaceModelTrees.py | 15 ++++--- src/python/simplexui/items/__init__.py | 1 + src/python/simplexui/items/accessor.py | 4 +- src/python/simplexui/items/combo.py | 12 +++--- src/python/simplexui/items/falloff.py | 9 ++-- src/python/simplexui/items/group.py | 7 +-- src/python/simplexui/items/progression.py | 14 +++--- src/python/simplexui/items/shape.py | 11 +++-- src/python/simplexui/items/simplex.py | 11 +++-- src/python/simplexui/items/slider.py | 6 +-- src/python/simplexui/items/stack.py | 3 +- src/python/simplexui/items/traversal.py | 12 +++--- src/python/simplexui/items/treeItem.py | 12 +++--- src/python/simplexui/menu/__init__.py | 2 + .../menu/genericPlugins/_builtins.py | 4 +- .../genericPlugins/checkPossibleCombos.py | 4 +- .../menu/genericPlugins/exportSplit.py | 6 +-- .../menu/genericPlugins/showFalloffs.py | 1 + .../menu/genericPlugins/showTraversals.py | 1 + .../menu/genericPlugins/simplexUvTransfer.py | 5 ++- .../menu/genericPlugins/unsubdivide.py | 4 +- .../simplexui/menu/mayaPlugins/exportOther.py | 4 +- .../menu/mayaPlugins/extractProgressives.py | 4 +- .../simplexui/menu/mayaPlugins/freezeCombo.py | 1 + .../mayaPlugins/generateShapeIncrementals.py | 3 +- .../simplexui/menu/mayaPlugins/importObjs.py | 8 ++-- .../menu/mayaPlugins/linearizeTraversal.py | 2 + .../menu/mayaPlugins/makeShelfBtn.py | 1 + .../menu/mayaPlugins/relaxToSelection.py | 2 +- .../menu/mayaPlugins/reloadDefinition.py | 2 + .../menu/mayaPlugins/snapToNeutral.py | 2 +- .../menu/mayaPlugins/softSelectToCluster.py | 2 +- .../simplexui/menu/mayaPlugins/tweakMix.py | 3 +- .../menu/mayaPlugins/updateRestShape.py | 2 +- src/python/simplexui/simplexDialog.py | 43 ++++++++++--------- src/python/simplexui/travCheckDialog.py | 10 ++--- src/python/simplexui/traversalDialog.py | 25 +++++------ src/python/simplexui/utils.py | 7 ++- 60 files changed, 229 insertions(+), 174 deletions(-) diff --git a/src/python/simplexui/__init__.py b/src/python/simplexui/__init__.py index bb80ceff..eb6e57ed 100644 --- a/src/python/simplexui/__init__.py +++ b/src/python/simplexui/__init__.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . from __future__ import annotations + from ._version import __version__ # noqa: F401 SIMPLEX_UI = None diff --git a/src/python/simplexui/channelBox.py b/src/python/simplexui/channelBox.py index 35054e49..e7e643cc 100644 --- a/src/python/simplexui/channelBox.py +++ b/src/python/simplexui/channelBox.py @@ -21,11 +21,10 @@ """ from __future__ import annotations + import os import sys -from .items.simplex import Simplex -from .interfaceModel import Group, SimplexModel, Slider from Qt.QtCore import ( QAbstractItemModel, QEvent, @@ -39,6 +38,9 @@ from Qt.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QTextOption from Qt.QtWidgets import QApplication, QListView, QStyledItemDelegate, QTreeView +from .interfaceModel import Group, SimplexModel, Slider +from .items.simplex import Simplex + class SlideFilter(QObject): """A simplified drag filter, specialized for this purpose""" diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index 6709da0d..df808d46 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -17,9 +17,8 @@ from __future__ import annotations from itertools import combinations, product +from typing import TYPE_CHECKING -from .dragFilter import DragFilter -from .items import Combo, Slider from Qt import QtCompat from Qt.QtCore import Qt from Qt.QtGui import QBrush, QColor @@ -37,14 +36,15 @@ QWidget, ) +from .dragFilter import DragFilter +from .items import Combo, Slider from .utils import getUiFile -from typing import TYPE_CHECKING - if TYPE_CHECKING: + from Qt.QtGui import QCloseEvent + from .items import Simplex from .simplexDialog import SimplexDialog - from Qt.QtGui import QCloseEvent class TooManyPossibilitiesError(Exception): diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 39bbe0b6..a805f0c7 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -19,15 +19,18 @@ This is a library of convenience functions with the numpy speed optimizations """ from __future__ import annotations + import os +import numpy as np +import numpy.typing as npt from alembic.Abc import ( IArchive, - OArchive, - OStringProperty, + ICompoundProperty, IObject, + OArchive, OCompoundProperty, - ICompoundProperty, + OStringProperty, ) from alembic.AbcGeom import ( GeometryScope, @@ -40,9 +43,7 @@ OV2fGeomParamSample, OXform, ) -from imath import IntArray, UnsignedIntArray, V2f, V2fArray, V3fArray, Box3d -import numpy as np -import numpy.typing as npt +from imath import Box3d, IntArray, UnsignedIntArray, V2f, V2fArray, V3fArray try: from imathnumpy import arrayToNumpy @@ -52,13 +53,12 @@ from typing import ( TYPE_CHECKING, - Union, + Any, TypeVar, + Union, overload, - Any, ) - if TYPE_CHECKING: from alembic.AbcGeom import _IBase # A helper typing-only class from Qt.QtWidgets import QProgressDialog diff --git a/src/python/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py index ce55ce4c..7cc82895 100644 --- a/src/python/simplexui/commands/applyCorrectives.py +++ b/src/python/simplexui/commands/applyCorrectives.py @@ -15,13 +15,15 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + import itertools import os + import numpy as np from pysimplex import PySimplex +from Qt.QtWidgets import QApplication from ..items import Combo, Simplex, Slider -from Qt.QtWidgets import QApplication from .alembicCommon import ( buildSmpx, getSmpxArchiveData, diff --git a/src/python/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py index f53bb167..19bad1a9 100644 --- a/src/python/simplexui/commands/correctiveInterface.py +++ b/src/python/simplexui/commands/correctiveInterface.py @@ -16,8 +16,10 @@ # along with Simplex. If not, see . from __future__ import annotations -from Qt.QtWidgets import QApplication + import numpy as np +from Qt.QtWidgets import QApplication + from .mayaCorrectiveInterface import getShiftValues, resetPose, setPose dcc = "maya" diff --git a/src/python/simplexui/commands/expandedExport.py b/src/python/simplexui/commands/expandedExport.py index 95e98617..9e008573 100644 --- a/src/python/simplexui/commands/expandedExport.py +++ b/src/python/simplexui/commands/expandedExport.py @@ -16,12 +16,13 @@ # along with Simplex. If not, see . from __future__ import annotations + +import numpy as np from pysimplex import PySimplex from ..interface.mayaInterface import DCC, disconnected -from ..items import Combo, Slider, Traversal, Simplex +from ..items import Combo, Simplex, Slider, Traversal from .alembicCommon import buildSmpx -import numpy as np def _setSliders(ctrl, val, svs): diff --git a/src/python/simplexui/commands/hdf5Convert.py b/src/python/simplexui/commands/hdf5Convert.py index 2aa9c343..0bbc3750 100644 --- a/src/python/simplexui/commands/hdf5Convert.py +++ b/src/python/simplexui/commands/hdf5Convert.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . from __future__ import annotations + import json from .alembicCommon import buildSmpx, readSmpx diff --git a/src/python/simplexui/commands/mayaCorrectiveInterface.py b/src/python/simplexui/commands/mayaCorrectiveInterface.py index ab78a1a0..66be4d00 100644 --- a/src/python/simplexui/commands/mayaCorrectiveInterface.py +++ b/src/python/simplexui/commands/mayaCorrectiveInterface.py @@ -18,13 +18,13 @@ """Get the corrective deltas from a rig in Maya""" from __future__ import annotations + from ctypes import c_float +import numpy as np from maya import OpenMaya as om from maya import cmds -import numpy as np - def setPose(pvp, multiplier): """Set a percentage of a pose diff --git a/src/python/simplexui/commands/mayatonumpy.py b/src/python/simplexui/commands/mayatonumpy.py index c9b6cd64..5b39e300 100644 --- a/src/python/simplexui/commands/mayatonumpy.py +++ b/src/python/simplexui/commands/mayatonumpy.py @@ -1,8 +1,10 @@ from __future__ import annotations -from maya import OpenMaya as om + +from ctypes import c_double, c_float, c_int, c_uint +from typing import TYPE_CHECKING, TypeVar, Union + import numpy as np -from ctypes import c_float, c_double, c_int, c_uint -from typing import Union, TypeVar, TYPE_CHECKING +from maya import OpenMaya as om # fmt: off _CONVERT_DICT = { @@ -382,6 +384,7 @@ def setNumpyAttr( def test(): import time + from maya import cmds meshName = "pSphere1" diff --git a/src/python/simplexui/commands/poseblendlib.py b/src/python/simplexui/commands/poseblendlib.py index f396ff9e..107e8b70 100644 --- a/src/python/simplexui/commands/poseblendlib.py +++ b/src/python/simplexui/commands/poseblendlib.py @@ -1,4 +1,5 @@ from __future__ import annotations + import numpy as np # I could have used the Scipy rotations library, but it doeosn't deal diff --git a/src/python/simplexui/commands/reorderSimplexPoints.py b/src/python/simplexui/commands/reorderSimplexPoints.py index 0730a701..a6e0f5d2 100644 --- a/src/python/simplexui/commands/reorderSimplexPoints.py +++ b/src/python/simplexui/commands/reorderSimplexPoints.py @@ -27,10 +27,13 @@ """ from __future__ import annotations + import json -from .alembicCommon import buildSmpx, readSmpx + import numpy as np +from .alembicCommon import buildSmpx, readSmpx + def reorderSimplexPoints(sourcePath, matchPath, outPath, invertMatch=False): """Transfer shape data from the sourcePath using the numpy int array diff --git a/src/python/simplexui/commands/rigidAlign.py b/src/python/simplexui/commands/rigidAlign.py index 6b457376..488596e9 100644 --- a/src/python/simplexui/commands/rigidAlign.py +++ b/src/python/simplexui/commands/rigidAlign.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . from __future__ import annotations + import numpy as np diff --git a/src/python/simplexui/commands/smpxBlend.py b/src/python/simplexui/commands/smpxBlend.py index d1dc98d9..a50e5a0e 100644 --- a/src/python/simplexui/commands/smpxBlend.py +++ b/src/python/simplexui/commands/smpxBlend.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . from __future__ import annotations + from ..items import ( Combo, ComboPair, diff --git a/src/python/simplexui/commands/unsubdivide.py b/src/python/simplexui/commands/unsubdivide.py index 74f3e378..01940c7d 100644 --- a/src/python/simplexui/commands/unsubdivide.py +++ b/src/python/simplexui/commands/unsubdivide.py @@ -15,13 +15,14 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + import json from itertools import chain, zip_longest +import numpy as np from Qt.QtWidgets import QApplication -from .alembicCommon import buildSmpx, pbPrint, readSmpx -import numpy as np +from .alembicCommon import buildSmpx, pbPrint, readSmpx def mergeCycles(groups): diff --git a/src/python/simplexui/commands/uvTransfer.py b/src/python/simplexui/commands/uvTransfer.py index 024ac5ff..6e142e02 100644 --- a/src/python/simplexui/commands/uvTransfer.py +++ b/src/python/simplexui/commands/uvTransfer.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . from __future__ import annotations + import numpy as np INF = float("inf") diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index 63ed77eb..863ec3b5 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -16,12 +16,11 @@ # along with Simplex. If not, see . from __future__ import annotations -from Qt.QtCore import QEvent, QObject, QPoint, Qt, Signal, QRect -from Qt.QtGui import QCursor, QMouseEvent, QGuiApplication -from Qt.QtWidgets import QApplication, QWidget - from enum import Enum +from Qt.QtCore import QEvent, QObject, QPoint, QRect, Qt, Signal +from Qt.QtGui import QCursor, QGuiApplication, QMouseEvent +from Qt.QtWidgets import QApplication, QWidget class DragType(Enum): diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index 5070d8ae..e7bb1b58 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -18,8 +18,9 @@ # This module imports QT from PyQt4, PySide or PySide2 # Depending on what's available from __future__ import annotations -from .interfaceModel import FalloffDataModel -from .items.falloff import PlanarFalloff + +from typing import TYPE_CHECKING + from Qt import QtCompat from Qt.QtCore import ( QByteArray, @@ -33,13 +34,13 @@ from Qt.QtGui import ( QBrush, QColor, + QHideEvent, + QMouseEvent, QPainter, QPainterPath, + QPaintEvent, QPalette, QPen, - QPaintEvent, - QMouseEvent, - QHideEvent, QShowEvent, ) from Qt.QtWidgets import ( @@ -56,12 +57,14 @@ QVBoxLayout, QWidget, ) -from .utils import getNextName, getUiFile, Prefs -from typing import TYPE_CHECKING + +from .interfaceModel import FalloffDataModel +from .items.falloff import PlanarFalloff +from .utils import Prefs, getNextName, getUiFile if TYPE_CHECKING: - from .simplexDialog import SimplexDialog from .items import Simplex + from .simplexDialog import SimplexDialog class CurveEditWidget(QWidget): diff --git a/src/python/simplexui/interface/__init__.py b/src/python/simplexui/interface/__init__.py index 00f369a9..85696eab 100644 --- a/src/python/simplexui/interface/__init__.py +++ b/src/python/simplexui/interface/__init__.py @@ -17,6 +17,7 @@ # This file will serve as the only place where the choice of DCC will be chosen from __future__ import annotations + import os import sys diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index 9cc4db81..d2132244 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -17,16 +17,16 @@ """A placeholder interface that takes arguments and does nothing with them""" from __future__ import annotations + import copy from contextlib import contextmanager from functools import wraps -from Qt import QtCore -from Qt.QtCore import Signal - import numpy as np - from alembic.AbcGeom import OPolyMeshSchemaSample +from Qt import QtCore +from Qt.QtCore import Signal +from Qt.QtWidgets import QApplication from ..commands.alembicCommon import ( getSampleArray, @@ -36,7 +36,6 @@ mkSampleVertexPoints, mkUvSample, ) -from Qt.QtWidgets import QApplication # UNDO STACK INTEGRATION diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 7adc0fee..8edc0373 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -15,19 +15,18 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + import json import re from contextlib import contextmanager from functools import wraps from typing import TYPE_CHECKING -import numpy as np import maya.cmds as cmds import maya.OpenMaya as om +import numpy as np from alembic.AbcGeom import GeometryScope, OPolyMeshSchemaSample, OV2fGeomParamSample from imath import IntArray, UnsignedIntArray, V2fArray, V3fArray - -from ..commands.alembicCommon import mkSampleVertexPoints, buildAbc from Qt import QtCore from Qt.QtCore import Signal from Qt.QtWidgets import ( @@ -38,9 +37,11 @@ QSplashScreen, ) +from ..commands.alembicCommon import buildAbc, mkSampleVertexPoints + try: - from ..commands.numpytoimath import numpyToImath from ..commands.mayatonumpy import mayaToNumpy + from ..commands.numpytoimath import numpyToImath except ImportError: numpyToImath = None mayaToNumpy = None diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index 7c813bed..e2a35830 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -16,27 +16,26 @@ # along with Simplex. If not, see . from __future__ import annotations + import re +from typing import Any, TypeVar, cast + +from Qt.QtCore import QModelIndex, QSortFilterProxyModel, Qt +from Qt.QtWidgets import QWidget from .items import ( Combo, ComboPair, + Falloff, Group, ProgPair, Progression, - Slider, Simplex, + Slider, Traversal, TravPair, - Falloff, ) - -from .items.treeItem import TreeItem, TreeRootItem, AdapterModel - -from Qt.QtWidgets import QWidget -from Qt.QtCore import QModelIndex, QSortFilterProxyModel, Qt -from typing import cast, TypeVar, Any - +from .items.treeItem import AdapterModel, TreeItem, TreeRootItem T = TypeVar('T', bound=TreeItem) diff --git a/src/python/simplexui/interfaceModelTrees.py b/src/python/simplexui/interfaceModelTrees.py index bac3003c..6115419a 100644 --- a/src/python/simplexui/interfaceModelTrees.py +++ b/src/python/simplexui/interfaceModelTrees.py @@ -16,12 +16,7 @@ # along with Simplex. If not, see . from __future__ import annotations -from .dragFilter import DragFilter -from .utils import execmenu -from .items import Group -from .items.treeItem import CustomRoles -from .items.dragItem import Draggable -from .interfaceModel import SimplexFilterModel +from typing import cast from Qt.QtCore import ( QItemSelection, @@ -30,7 +25,13 @@ ) from Qt.QtGui import QValidator from Qt.QtWidgets import QApplication, QLineEdit, QMenu, QStyledItemDelegate, QTreeView -from typing import cast + +from .dragFilter import DragFilter +from .interfaceModel import SimplexFilterModel +from .items import Group +from .items.dragItem import Draggable +from .items.treeItem import CustomRoles +from .utils import execmenu class NameValidator(QValidator): diff --git a/src/python/simplexui/items/__init__.py b/src/python/simplexui/items/__init__.py index f7ac0622..e4c135d7 100644 --- a/src/python/simplexui/items/__init__.py +++ b/src/python/simplexui/items/__init__.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . from __future__ import annotations + from .combo import Combo, ComboPair from .falloff import Falloff, MapFalloff, PlanarFalloff from .group import Group diff --git a/src/python/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py index ae5ad89b..a67db192 100644 --- a/src/python/simplexui/items/accessor.py +++ b/src/python/simplexui/items/accessor.py @@ -15,10 +15,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations -import copy - +import copy from typing import TYPE_CHECKING + from .treeItem import TreeItem if TYPE_CHECKING: diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index 1385ded6..c51edeed 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -15,20 +15,20 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from .accessor import SimplexTreeAccessor +from .dragItem import Draggable from .stack import stackable from .treeItem import TreeItem -from .dragItem import Draggable - - -from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from .group import Group + from .progression import Progression + from .shape import Shape from .simplex import Simplex from .slider import Slider - from .shape import Shape - from .progression import Progression # Abstract Items diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index d7db6db1..4e0b4ac8 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -15,20 +15,21 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + import copy import math +from typing import TYPE_CHECKING, Any import numpy as np from numpy import typing as npt + from .accessor import SimplexAccessor from .stack import stackable -from typing import Any, TYPE_CHECKING - if TYPE_CHECKING: - from .simplex import Simplex, DCCObject - from .shape import Shape from .progression import Progression + from .shape import Shape + from .simplex import DCCObject, Simplex class SplitDefinition: diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index bc5f113b..53701b41 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -15,15 +15,16 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Union + from .accessor import SimplexTreeAccessor from .stack import stackable from .treeItem import TreeItem -from typing import Union, Any, TYPE_CHECKING - if TYPE_CHECKING: - from .simplex import Simplex from .combo import Combo + from .simplex import Simplex from .slider import Slider from .traversal import Traversal diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index e55cc29c..fd23a94c 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -15,21 +15,21 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + +from typing import TYPE_CHECKING, Any + from ..utils import getNextName from .accessor import SimplexTreeAccessor +from .dragItem import Draggable from .stack import stackable from .treeItem import TreeItem -from .dragItem import Draggable - - -from typing import Any, TYPE_CHECKING if TYPE_CHECKING: - from .simplex import Simplex - from .shape import Shape + from .combo import Combo from .falloff import Falloff + from .shape import Shape + from .simplex import Simplex from .slider import Slider - from .combo import Combo from .traversal import Traversal diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index a7947431..776225db 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -15,16 +15,19 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from numpy import typing as npt + from ..interface import DCC, undoContext from .accessor import SimplexAccessor from .stack import stackable -from numpy import typing as npt -from typing import Any, TYPE_CHECKING if TYPE_CHECKING: - from .slider import Slider - from .simplex import Simplex, DCCObject from .progression import ProgPair + from .simplex import DCCObject, Simplex + from .slider import Slider class Shape(SimplexAccessor): diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 23d739a1..9411ad5e 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -15,10 +15,15 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + import copy import itertools import json +from typing import TYPE_CHECKING, Any, Union + import numpy as np +from Qt.QtGui import QColor +from Qt.QtWidgets import QApplication from ..commands.alembicCommon import ( buildAlembicArchiveData, @@ -28,7 +33,6 @@ ) from ..interface import DCC, undoContext from ..interface.dummyInterface import DCC as DummyDCC - from .combo import Combo, ComboPair from .falloff import Falloff, SplitDefinition from .group import Group @@ -39,11 +43,6 @@ from .traversal import Traversal, TravPair from .treeItem import TreeRootItem -from Qt.QtGui import QColor -from Qt.QtWidgets import QApplication -from typing import Any, TYPE_CHECKING, Union - - if TYPE_CHECKING: from Qt.QtWidgets import QProgressDialog diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index 7cb739a5..71eaf6c0 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -15,19 +15,19 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + import itertools +from typing import TYPE_CHECKING, Any from ..interface import undoContext from ..utils import caseSplit, getNextName, makeUnique, singleShot from .accessor import SimplexTreeAccessor +from .dragItem import Draggable from .group import Group from .progression import ProgPair, Progression from .shape import Shape from .stack import stackable from .treeItem import TreeItem -from .dragItem import Draggable - -from typing import Any, TYPE_CHECKING if TYPE_CHECKING: from .simplex import DCCObject, Simplex diff --git a/src/python/simplexui/items/stack.py b/src/python/simplexui/items/stack.py index 2655fa6b..a6c8495e 100644 --- a/src/python/simplexui/items/stack.py +++ b/src/python/simplexui/items/stack.py @@ -15,11 +15,12 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + import copy from collections import OrderedDict from contextlib import contextmanager from functools import wraps -from typing import Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any from ..interface import undoContext diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index 71ce7cc2..aac3635d 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -15,22 +15,22 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING, Any + from .accessor import SimplexTreeAccessor from .combo import Combo +from .dragItem import Draggable from .group import Group from .progression import Progression from .slider import Slider from .stack import stackable from .treeItem import TreeItem -from .dragItem import Draggable - -from enum import Enum - -from typing import Any, TYPE_CHECKING if TYPE_CHECKING: - from .simplex import Simplex, DCCObject from .progression import ProgPair + from .simplex import DCCObject, Simplex class TravSide(Enum): diff --git a/src/python/simplexui/items/treeItem.py b/src/python/simplexui/items/treeItem.py index 5ea76a32..a2c4a8d9 100644 --- a/src/python/simplexui/items/treeItem.py +++ b/src/python/simplexui/items/treeItem.py @@ -49,13 +49,15 @@ """ from __future__ import annotations -from typing import Any, Generator, Callable, Iterator, Literal, overload + +import enum +import uuid +from contextlib import ExitStack, contextmanager +from typing import Any, Callable, Generator, Iterator, Literal, overload + +from Qt.QtCore import QAbstractItemModel, QModelIndex, QObject, Qt from Qt.QtGui import QIcon -from Qt.QtCore import QAbstractItemModel, QModelIndex, Qt, QObject from Qt.QtWidgets import QTreeView -from contextlib import contextmanager, ExitStack -import uuid -import enum class CustomRoles(enum.IntEnum): diff --git a/src/python/simplexui/menu/__init__.py b/src/python/simplexui/menu/__init__.py index b562785b..19e69764 100644 --- a/src/python/simplexui/menu/__init__.py +++ b/src/python/simplexui/menu/__init__.py @@ -16,12 +16,14 @@ # along with Simplex. If not, see . from __future__ import annotations + import importlib import os import pkgutil import sys from Qt.QtWidgets import QMenu + from . import genericPlugins CONTEXT = os.path.basename(sys.executable) diff --git a/src/python/simplexui/menu/genericPlugins/_builtins.py b/src/python/simplexui/menu/genericPlugins/_builtins.py index c7304d85..d584ef5b 100644 --- a/src/python/simplexui/menu/genericPlugins/_builtins.py +++ b/src/python/simplexui/menu/genericPlugins/_builtins.py @@ -16,12 +16,14 @@ # along with Simplex. If not, see . from __future__ import annotations + from functools import partial -from ...items import Combo, ComboPair, ProgPair, Progression, Slider from Qt.QtCore import Qt from Qt.QtWidgets import QCheckBox, QWidgetAction +from ...items import Combo, ComboPair, ProgPair, Progression, Slider + def registerContext(tree, clickIdx, indexes, menu): self = tree.window() diff --git a/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py b/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py index 927d68e7..da97a23a 100644 --- a/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py +++ b/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py @@ -16,11 +16,13 @@ # along with Simplex. If not, see . from __future__ import annotations + from functools import partial +from Qt.QtWidgets import QAction + from ...comboCheckDialog import ComboCheckDialog from ...items import Slider -from Qt.QtWidgets import QAction def registerTool(window, menu): diff --git a/src/python/simplexui/menu/genericPlugins/exportSplit.py b/src/python/simplexui/menu/genericPlugins/exportSplit.py index fb4bd2ed..782ef411 100644 --- a/src/python/simplexui/menu/genericPlugins/exportSplit.py +++ b/src/python/simplexui/menu/genericPlugins/exportSplit.py @@ -16,13 +16,13 @@ # along with Simplex. If not, see . from __future__ import annotations + from functools import partial +import numpy as np from Qt import QtCompat -from Qt.QtWidgets import QMessageBox, QProgressDialog from Qt.QtGui import QAction - -import numpy as np +from Qt.QtWidgets import QMessageBox, QProgressDialog def registerTool(window, menu): diff --git a/src/python/simplexui/menu/genericPlugins/showFalloffs.py b/src/python/simplexui/menu/genericPlugins/showFalloffs.py index b4480d3a..5f728416 100644 --- a/src/python/simplexui/menu/genericPlugins/showFalloffs.py +++ b/src/python/simplexui/menu/genericPlugins/showFalloffs.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . from __future__ import annotations + from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/genericPlugins/showTraversals.py b/src/python/simplexui/menu/genericPlugins/showTraversals.py index fe6e69d8..4279f7ab 100644 --- a/src/python/simplexui/menu/genericPlugins/showTraversals.py +++ b/src/python/simplexui/menu/genericPlugins/showTraversals.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . from __future__ import annotations + from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py b/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py index d2735782..9006d6d6 100644 --- a/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py +++ b/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py @@ -16,14 +16,15 @@ # along with Simplex. If not, see . from __future__ import annotations + import json +import numpy as np + from ...commands.alembicCommon import buildSmpx, readSmpx from ...commands.mesh import Mesh from ...commands.uvTransfer import applyTransfer, getVertCorrelation -import numpy as np - def simplexUvTransfer( srcSmpxPath, tarPath, outPath, srcUvPath=None, tol=0.0001, pBar=None diff --git a/src/python/simplexui/menu/genericPlugins/unsubdivide.py b/src/python/simplexui/menu/genericPlugins/unsubdivide.py index de1486fa..ae4885a2 100644 --- a/src/python/simplexui/menu/genericPlugins/unsubdivide.py +++ b/src/python/simplexui/menu/genericPlugins/unsubdivide.py @@ -16,13 +16,15 @@ # along with Simplex. If not, see . from __future__ import annotations + import os from functools import partial -from ...commands.unsubdivide import unsubdivideSimplex from Qt import QtCompat from Qt.QtWidgets import QAction, QMessageBox, QProgressDialog +from ...commands.unsubdivide import unsubdivideSimplex + try: import imathnumpy except ImportError: diff --git a/src/python/simplexui/menu/mayaPlugins/exportOther.py b/src/python/simplexui/menu/mayaPlugins/exportOther.py index 17fc82b6..d7bb2788 100644 --- a/src/python/simplexui/menu/mayaPlugins/exportOther.py +++ b/src/python/simplexui/menu/mayaPlugins/exportOther.py @@ -16,13 +16,13 @@ # along with Simplex. If not, see . from __future__ import annotations + from functools import partial import maya.cmds as cmds - from Qt import QtCompat -from Qt.QtWidgets import QMessageBox, QProgressDialog from Qt.QtGui import QAction +from Qt.QtWidgets import QMessageBox, QProgressDialog def registerTool(window, menu): diff --git a/src/python/simplexui/menu/mayaPlugins/extractProgressives.py b/src/python/simplexui/menu/mayaPlugins/extractProgressives.py index 5c2a32f0..1d15e1aa 100644 --- a/src/python/simplexui/menu/mayaPlugins/extractProgressives.py +++ b/src/python/simplexui/menu/mayaPlugins/extractProgressives.py @@ -16,11 +16,13 @@ # along with Simplex. If not, see . from __future__ import annotations + from functools import partial +from Qt.QtWidgets import QAction + from ...interfaceModel import coerceIndexToType from ...items import Slider -from Qt.QtWidgets import QAction def registerTool(window, menu): diff --git a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py index 6a92e550..9c59a007 100644 --- a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py +++ b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py @@ -14,6 +14,7 @@ # along with Simplex. If not, see . from __future__ import annotations + from functools import partial from maya import cmds diff --git a/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py index a34a2976..8f177c7e 100644 --- a/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py +++ b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py @@ -16,13 +16,14 @@ # along with Simplex. If not, see . from __future__ import annotations + from functools import partial import maya.cmds as cmds +from Qt.QtWidgets import QInputDialog, QMessageBox from ...interfaceModel import coerceIndexToType from ...items import Combo, Slider -from Qt.QtWidgets import QInputDialog, QMessageBox def registerContext(tree, clickIdx, indexes, menu): diff --git a/src/python/simplexui/menu/mayaPlugins/importObjs.py b/src/python/simplexui/menu/mayaPlugins/importObjs.py index 5c42caa2..4cc8a4b0 100644 --- a/src/python/simplexui/menu/mayaPlugins/importObjs.py +++ b/src/python/simplexui/menu/mayaPlugins/importObjs.py @@ -1,17 +1,19 @@ from __future__ import annotations + import os -import numpy as np from functools import partial + import maya.cmds as cmds -from ...items import Combo, Slider, Traversal +import numpy as np +from Qt.QtGui import QAction from Qt.QtWidgets import ( QApplication, QFileDialog, QMessageBox, QProgressDialog, ) -from Qt.QtGui import QAction +from ...items import Combo, Slider, Traversal try: from tools.MeshCrawler.commands import setAllVerts diff --git a/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py index 41906d69..914c8c26 100644 --- a/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py +++ b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py @@ -16,7 +16,9 @@ # along with Simplex. If not, see . from __future__ import annotations + from functools import partial + import maya.cmds as cmds from Qt.QtGui import QAction from Qt.QtWidgets import QMessageBox diff --git a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py index c86ec3e1..6a8f4a9e 100644 --- a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py +++ b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py @@ -16,6 +16,7 @@ # along with Simplex. If not, see . from __future__ import annotations + import os from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py index 2ea5ec51..424b6f2a 100644 --- a/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py +++ b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py @@ -16,8 +16,8 @@ # along with Simplex. If not, see . from __future__ import annotations -import maya.cmds as cmds +import maya.cmds as cmds from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py index 93e44f8d..7fc04c89 100644 --- a/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py +++ b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py @@ -16,7 +16,9 @@ # along with Simplex. If not, see . from __future__ import annotations + from functools import partial + from Qt.QtGui import QAction from Qt.QtWidgets import QMessageBox diff --git a/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py index a25b0e16..38d73953 100644 --- a/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py +++ b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py @@ -16,10 +16,10 @@ # along with Simplex. If not, see . from __future__ import annotations + from functools import partial import maya.cmds as cmds - from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py index e1b0c6bb..bc948abb 100644 --- a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py +++ b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py @@ -16,9 +16,9 @@ # along with Simplex. If not, see . from __future__ import annotations + import maya.cmds as cmds import maya.OpenMaya as om - from Qt.QtWidgets import QAction diff --git a/src/python/simplexui/menu/mayaPlugins/tweakMix.py b/src/python/simplexui/menu/mayaPlugins/tweakMix.py index 7e70b7d7..83299209 100644 --- a/src/python/simplexui/menu/mayaPlugins/tweakMix.py +++ b/src/python/simplexui/menu/mayaPlugins/tweakMix.py @@ -16,14 +16,15 @@ # along with Simplex. If not, see . from __future__ import annotations + from functools import partial import maya.cmds as cmds +from Qt.QtWidgets import QAction from ...interface.mayaInterface import disconnected from ...interfaceModel import coerceIndexToType from ...items import Combo -from Qt.QtWidgets import QAction def registerTool(window, menu): diff --git a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py index 346cdc34..59a946de 100644 --- a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py +++ b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py @@ -16,11 +16,11 @@ # along with Simplex. If not, see . from __future__ import annotations + import textwrap from functools import partial import maya.cmds as cmds - from Qt.QtGui import QAction from Qt.QtWidgets import QMessageBox diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index 57e3d4ac..e978dbb2 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with Simplex. If not, see . from __future__ import annotations + import json import os import re @@ -23,6 +24,26 @@ from contextlib import contextmanager from typing import TYPE_CHECKING +from Qt import QtCompat +from Qt.QtCore import Qt, Signal +from Qt.QtGui import QAction, QStandardItemModel +from Qt.QtWidgets import ( + QApplication, + QCheckBox, + QComboBox, + QFrame, + QGroupBox, + QInputDialog, + QLabel, + QLineEdit, + QMessageBox, + QProgressDialog, + QPushButton, + QRadioButton, + QVBoxLayout, + QWidget, +) + from .comboCheckDialog import ComboCheckDialog from .falloffDialog import FalloffDialog from .interface import DCC @@ -40,27 +61,7 @@ from .items import Combo, Group, ProgPair, Simplex, Slider, Stack from .menu import buildToolMenu, loadPlugins from .traversalDialog import TraversalDialog -from .utils import getNextName, getUiFile, makeUnique, naturalSortKey, Prefs, execwid - -from Qt import QtCompat -from Qt.QtCore import Qt, Signal -from Qt.QtGui import QStandardItemModel, QAction -from Qt.QtWidgets import ( - QApplication, - QInputDialog, - QMessageBox, - QProgressDialog, - QCheckBox, - QComboBox, - QFrame, - QGroupBox, - QLabel, - QLineEdit, - QPushButton, - QRadioButton, - QVBoxLayout, - QWidget, -) +from .utils import Prefs, execwid, getNextName, getUiFile, makeUnique, naturalSortKey if os.environ.get("SIMPLEX_AT_BLUR") == "true": # If we're at blur, use our main window subclass diff --git a/src/python/simplexui/travCheckDialog.py b/src/python/simplexui/travCheckDialog.py index ee98eabf..bea14a29 100644 --- a/src/python/simplexui/travCheckDialog.py +++ b/src/python/simplexui/travCheckDialog.py @@ -16,25 +16,25 @@ # along with Simplex. If not, see . from __future__ import annotations + from itertools import combinations, product -from .dragFilter import DragFilter -from .items import Slider, Traversal from Qt import QtCompat from Qt.QtCore import Qt from Qt.QtGui import QBrush, QColor from Qt.QtWidgets import ( - QDialog, - QTreeWidgetItem, QCheckBox, + QDialog, QGroupBox, QLabel, QPushButton, QSpinBox, QTreeWidget, + QTreeWidgetItem, ) - +from .dragFilter import DragFilter +from .items import Slider, Traversal from .utils import getUiFile diff --git a/src/python/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py index 1b300276..81d7dd2a 100644 --- a/src/python/simplexui/traversalDialog.py +++ b/src/python/simplexui/traversalDialog.py @@ -16,20 +16,11 @@ # along with Simplex. If not, see . from __future__ import annotations -import re -from .interface import DCC -from .interfaceModel import ( - TraversalFilterModel, - TraversalModel, - coerceIndexToRoots, - coerceIndexToType, -) -from .interfaceModelTrees import TraversalTree -from .items import Group, Simplex, Slider, Traversal, TravPair +import re from Qt import QtCompat -from Qt.QtGui import QStandardItemModel, QAction +from Qt.QtGui import QAction, QStandardItemModel from Qt.QtWidgets import ( QApplication, QDialog, @@ -40,8 +31,18 @@ QPushButton, QVBoxLayout, ) + +from .interface import DCC +from .interfaceModel import ( + TraversalFilterModel, + TraversalModel, + coerceIndexToRoots, + coerceIndexToType, +) +from .interfaceModelTrees import TraversalTree +from .items import Group, Simplex, Slider, Traversal, TravPair from .travCheckDialog import TraversalCheckDialog -from .utils import getUiFile, makeUnique, execwid +from .utils import execwid, getUiFile, makeUnique NAME_CHECK = re.compile(r"[A-Za-z][\w.]*") diff --git a/src/python/simplexui/utils.py b/src/python/simplexui/utils.py index c1cef4a9..d6213110 100755 --- a/src/python/simplexui/utils.py +++ b/src/python/simplexui/utils.py @@ -22,11 +22,10 @@ import os import re import sys -from typing import TypeVar, Callable, cast, Sequence, Any +from typing import Any, Callable, Sequence, TypeVar, cast - -from Qt import IsPySide6, IsPyQt6 -from Qt.QtCore import QObject, QTimer, QSettings, QPoint +from Qt import IsPyQt6, IsPySide6 +from Qt.QtCore import QObject, QPoint, QSettings, QTimer from Qt.QtGui import QIcon from Qt.QtWidgets import QMenu From 9bcfe9c82f22b05243c1b12dae1a544a39ba08cc Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Sat, 12 Sep 2026 11:48:21 -0700 Subject: [PATCH 42/47] Add a bunch of easy automatic types from pyrefly (hand-checked) --- src/python/simplexui/__init__.py | 4 +- src/python/simplexui/channelBox.py | 82 +-- src/python/simplexui/comboCheckDialog.py | 20 +- .../simplexui/commands/alembicCommon.py | 7 +- .../simplexui/commands/applyCorrectives.py | 19 +- .../simplexui/commands/correctiveInterface.py | 2 +- .../simplexui/commands/expandedExport.py | 18 +- src/python/simplexui/commands/hdf5Convert.py | 2 +- .../commands/mayaCorrectiveInterface.py | 4 +- src/python/simplexui/commands/mayatonumpy.py | 4 +- src/python/simplexui/commands/mesh.py | 68 +- .../commands/reorderSimplexPoints.py | 4 +- src/python/simplexui/commands/rigidAlign.py | 2 +- src/python/simplexui/commands/smpxBlend.py | 631 ------------------ src/python/simplexui/commands/unsubdivide.py | 14 +- src/python/simplexui/commands/uvTransfer.py | 18 +- src/python/simplexui/dragFilter.py | 14 +- src/python/simplexui/falloffDialog.py | 54 +- .../simplexui/interface/dummyInterface.py | 200 +++--- .../simplexui/interface/mayaInterface.py | 198 +++--- src/python/simplexui/interfaceModel.py | 20 +- src/python/simplexui/interfaceModelTrees.py | 57 +- src/python/simplexui/items/accessor.py | 4 +- src/python/simplexui/items/combo.py | 54 +- src/python/simplexui/items/dragItem.py | 2 +- src/python/simplexui/items/falloff.py | 47 +- src/python/simplexui/items/group.py | 10 +- src/python/simplexui/items/progression.py | 32 +- src/python/simplexui/items/shape.py | 18 +- src/python/simplexui/items/simplex.py | 40 +- src/python/simplexui/items/slider.py | 32 +- src/python/simplexui/items/stack.py | 8 +- src/python/simplexui/items/traversal.py | 42 +- src/python/simplexui/items/treeItem.py | 24 +- src/python/simplexui/menu/__init__.py | 5 +- .../menu/genericPlugins/_builtins.py | 6 +- .../genericPlugins/checkPossibleCombos.py | 6 +- .../menu/genericPlugins/exportSplit.py | 4 +- .../menu/genericPlugins/showFalloffs.py | 2 +- .../menu/genericPlugins/showTraversals.py | 2 +- .../menu/genericPlugins/simplexUvTransfer.py | 4 +- .../menu/genericPlugins/unsubdivide.py | 4 +- .../simplexui/menu/mayaPlugins/exportOther.py | 4 +- .../menu/mayaPlugins/extractProgressives.py | 8 +- .../simplexui/menu/mayaPlugins/freezeCombo.py | 8 +- .../mayaPlugins/generateShapeIncrementals.py | 4 +- .../simplexui/menu/mayaPlugins/importObjs.py | 10 +- .../menu/mayaPlugins/linearizeTraversal.py | 4 +- .../menu/mayaPlugins/makeShelfBtn.py | 4 +- .../menu/mayaPlugins/relaxToSelection.py | 6 +- .../menu/mayaPlugins/reloadDefinition.py | 6 +- .../menu/mayaPlugins/snapToNeutral.py | 6 +- .../menu/mayaPlugins/softSelectToCluster.py | 6 +- .../simplexui/menu/mayaPlugins/tweakMix.py | 10 +- .../menu/mayaPlugins/updateRestShape.py | 6 +- src/python/simplexui/simplexDialog.py | 152 ++--- src/python/simplexui/travCheckDialog.py | 24 +- src/python/simplexui/traversalDialog.py | 22 +- src/python/simplexui/utils.py | 22 +- 59 files changed, 766 insertions(+), 1324 deletions(-) delete mode 100644 src/python/simplexui/commands/smpxBlend.py diff --git a/src/python/simplexui/__init__.py b/src/python/simplexui/__init__.py index eb6e57ed..d692c2d9 100644 --- a/src/python/simplexui/__init__.py +++ b/src/python/simplexui/__init__.py @@ -23,7 +23,7 @@ SIMPLEX_UI_ROOT = None -def runSimplexUI(): +def runSimplexUI() -> None: from .interface import DISPATCH, rootWindow from .simplexDialog import SimplexDialog @@ -37,7 +37,7 @@ def runSimplexUI(): SIMPLEX_UI.show() -def tool_paths(): +def tool_paths() -> tuple[list[str], list[str]]: import os path = os.path.dirname(__file__) diff --git a/src/python/simplexui/channelBox.py b/src/python/simplexui/channelBox.py index e7e643cc..8912da38 100644 --- a/src/python/simplexui/channelBox.py +++ b/src/python/simplexui/channelBox.py @@ -36,7 +36,13 @@ Signal, ) from Qt.QtGui import QBrush, QColor, QCursor, QPainter, QPainterPath, QPen, QTextOption -from Qt.QtWidgets import QApplication, QListView, QStyledItemDelegate, QTreeView +from Qt.QtWidgets import ( + QApplication, + QListView, + QStyledItemDelegate, + QTreeView, + QWidget, +) from .interfaceModel import Group, SimplexModel, Slider from .items.simplex import Simplex @@ -51,7 +57,7 @@ class SlideFilter(QObject): slidePressed = Signal() slideReleased = Signal() - def __init__(self, parent): + def __init__(self, parent: QWidget) -> None: super().__init__(parent) self.slideCursor = Qt.CursorShape.SizeHorCursor @@ -69,21 +75,21 @@ def __init__(self, parent): self._pressed = True self._prevValue = None - def doOverrideCursor(self): + def doOverrideCursor(self) -> None: """Override the cursor""" if self._overridden: return QApplication.setOverrideCursor(self.slideCursor) self._overridden = True - def restoreOverrideCursor(self): + def restoreOverrideCursor(self) -> None: """Restore the overridden cursor""" if not self._overridden: return QApplication.restoreOverrideCursor() self._overridden = False - def eventFilter(self, obj, event): + def eventFilter(self, obj, event) -> bool: """Event filter override Parameters @@ -123,7 +129,7 @@ def eventFilter(self, obj, event): return super().eventFilter(obj, event) - def startSlide(self, obj, event): + def startSlide(self, obj, event) -> None: """Start the slide operation Parameters @@ -140,7 +146,7 @@ def startSlide(self, obj, event): self.slidePressed.emit() self.doOverrideCursor() - def doSlide(self, obj, event): + def doSlide(self, obj, event) -> None: """Do a slide tick Parameters @@ -172,7 +178,7 @@ def doSlide(self, obj, event): self.slideTick.emit(perc, offset, mul) - def myendSlide(self, obj, event): + def myendSlide(self, obj, event) -> None: """End the slide operation Parameters @@ -194,11 +200,11 @@ def myendSlide(self, obj, event): class ChannelBoxDelegate(QStyledItemDelegate): """Delegate to draw the slider items""" - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.store = {} - def paint(self, painter, opt, index): + def paint(self, painter, opt, index) -> None: """Overridden paint function""" item = index.model().itemFromIndex(index) if isinstance(item, Slider): @@ -281,7 +287,7 @@ def roundedPath(self, width, height, left=True, right=True): self.store[key] = bgPath return bgPath - def paintSlider(self, delegate, slider, painter, rect, palette): + def paintSlider(self, delegate, slider: Slider, painter, rect, palette) -> None: """Paint a slider Parameters @@ -365,13 +371,13 @@ class ChannelListModel(QAbstractItemModel): The parent of this model """ - def __init__(self, simplex, parent): + def __init__(self, simplex: Simplex, parent) -> None: super().__init__(parent) self.simplex = simplex # self.simplex.models.append(self) self.channels = [] - def setChannels(self, channels): + def setChannels(self, channels) -> None: """Set the channels to display in this model Parameters @@ -387,7 +393,7 @@ def setChannels(self, channels): self.channels = channels self.endResetModel() - def index(self, row, column=0, parIndex=None): + def index(self, row: int, column=0, parIndex=None) -> QModelIndex: if parIndex is None: parIndex = QModelIndex() @@ -397,13 +403,13 @@ def index(self, row, column=0, parIndex=None): return QModelIndex() return self.createIndex(row, column, item) - def parent(self, index): + def parent(self, index) -> QModelIndex: return QModelIndex() - def rowCount(self, parent): + def rowCount(self, parent) -> int: return len(self.channels) - def columnCount(self, parent): + def columnCount(self, parent) -> int: return 1 def data(self, index, role): @@ -420,7 +426,7 @@ def data(self, index, role): return None - def flags(self, index): + def flags(self, index) -> Qt.ItemFlag: return ( Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsEditable @@ -430,19 +436,19 @@ def flags(self, index): def itemFromIndex(self, index): return index.internalPointer() - def indexFromItem(self, item): + def indexFromItem(self, item) -> QModelIndex: try: row = self.channels.index(item) except ValueError: return QModelIndex() return self.index(row) - def typeHandled(self, item): + def typeHandled(self, item) -> bool: if isinstance(item, Group): return item.groupType == Slider return isinstance(item, Slider) - def itemDataChanged(self, item): + def itemDataChanged(self, item) -> None: if self.typeHandled(item): idx = self.indexFromItem(item) if idx.isValid(): @@ -452,14 +458,14 @@ def itemDataChanged(self, item): class ChannelList(QListView): """A list to display the chosen channels""" - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.slider = None self._nxt = 0.0 self.start = False self.residual = 0.0 - def slideStart(self): + def slideStart(self) -> None: """Handle user sliding values""" p = self.mapFromGlobal(QCursor.pos()) item = self.indexAt(p).internalPointer() @@ -467,11 +473,11 @@ def slideStart(self): self.slider = item self.start = True - def slideStop(self): + def slideStop(self) -> None: """End the user sliding values""" self.slider = None - def slideTick(self, val, offset, mul): + def slideTick(self, val, offset, mul) -> None: """Handle the ticks from the slider Filter""" if self.slider is not None: mx = self.slider.maxValue @@ -499,7 +505,7 @@ def slideTick(self, val, offset, mul): self._nxt = rn QTimer.singleShot(0, self.setval) - def setval(self): + def setval(self) -> None: """Set the value of a slider""" if self.slider is not None and self._nxt is not None: self.slider.value = self._nxt @@ -522,7 +528,7 @@ def getChildItem(self, parent, row): pass return None - def getItemRow(self, item): + def getItemRow(self, item) -> int | None: row = None if isinstance(item, Group): row = item.simplex.sliderGroups.index(item) @@ -530,23 +536,23 @@ def getItemRow(self, item): row = item.group.items.index(item) return row - def getParentItem(self, item): + def getParentItem(self, item) -> Group | None: par = None if isinstance(item, Slider): par = item.group return par - def columnCount(self, parent): + def columnCount(self, parent) -> int: return 1 - def getItemRowCount(self, item): + def getItemRowCount(self, item) -> int: if isinstance(item, Group): return len(item.items) elif item is None: return len(self.simplex.sliderGroups) return 0 - def getItemAppendRow(self, item): + def getItemAppendRow(self, item) -> int: return self.getItemRowCount(item) def getItemData(self, item, column, role): @@ -561,7 +567,7 @@ def getItemData(self, item, column, role): return "BAD" return None - def typeHandled(self, item): + def typeHandled(self, item) -> bool: if isinstance(item, Group): return item.groupType == Slider return isinstance(item, Slider) @@ -570,14 +576,14 @@ def typeHandled(self, item): class ChannelTree(QTreeView): """Display the channels in a Tree form""" - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.slider = None self._nxt = 0.0 self.start = False self.residual = 0.0 - def slideStart(self): + def slideStart(self) -> None: """Handle starting a slide drag operation""" p = self.mapFromGlobal(QCursor.pos()) item = self.indexAt(p).internalPointer() @@ -585,11 +591,11 @@ def slideStart(self): self.slider = item self.start = True - def slideStop(self): + def slideStop(self) -> None: """Handle ending a slide drag operation""" self.slider = None - def slideTick(self, val, offset, mul): + def slideTick(self, val, offset, mul) -> None: """Handle the ticks from the SliderFilter""" if self.slider is not None: mx = self.slider.maxValue @@ -617,7 +623,7 @@ def slideTick(self, val, offset, mul): self._nxt = rn QTimer.singleShot(0, self.setval) - def setval(self): + def setval(self) -> None: """Set the value of a slider""" if self.slider is not None and self._nxt is not None: self.slider.value = self._nxt @@ -740,7 +746,7 @@ def testSliderListDisplay(smpxPath): sys.exit(app.exec_()) -def testSliderTreeDisplay(smpxPath): +def testSliderTreeDisplay(smpxPath: str): """ Parameters diff --git a/src/python/simplexui/comboCheckDialog.py b/src/python/simplexui/comboCheckDialog.py index df808d46..97e05fdc 100644 --- a/src/python/simplexui/comboCheckDialog.py +++ b/src/python/simplexui/comboCheckDialog.py @@ -131,7 +131,7 @@ def buildPossibleCombos( class ComboCheckItem(QListWidgetItem): def __init__( self, pairs: list[tuple[Slider, float]], combo: Combo, *args, **kwargs - ): + ) -> None: super().__init__(*args, **kwargs) self.pairs = pairs self.combo = combo @@ -191,7 +191,7 @@ def __init__( values: dict[Slider, list[float]] | None = None, mode: str = "create", parent: SimplexDialog | None = None, - ): + ) -> None: if parent is None: raise ValueError("Parent must not be None") super().__init__(parent) @@ -237,7 +237,7 @@ def __init__( else: self._populate() - def dragTick(self, ticks: int, mul: float): + def dragTick(self, ticks: int, mul: float) -> None: """Deal with the ticks coming from the drag handler Parameters @@ -257,7 +257,7 @@ def dragTick(self, ticks: int, mul: float): item.setData(3, Qt.ItemDataRole.EditRole, val) self.uiEditTREE.viewport().update() - def setSliders(self, sliders: list[Slider]): + def setSliders(self, sliders: list[Slider]) -> None: """Set the sliders displayed in this UI Parameters @@ -302,7 +302,7 @@ def setSliders(self, sliders: list[Slider]): for col in reversed(list(range(4))): self.uiEditTREE.resizeColumnToContents(col) - def closeEvent(self, event: QCloseEvent): + def closeEvent(self, event: QCloseEvent) -> None: """Override the Qt close event""" if self.isEnabled(): self.parUI.uiSliderTREE.selectionModel().selectionChanged.disconnect( @@ -310,22 +310,22 @@ def closeEvent(self, event: QCloseEvent): ) super().closeEvent(event) - def populateWithUpdate(self): + def populateWithUpdate(self) -> None: """Populate the list from the main dialog selection""" self.setSliders(self.parUI.uiSliderTREE.getSelectedItems(typ=Slider)) self._populate() - def populateWithoutUpdate(self): + def populateWithoutUpdate(self) -> None: """Populate the list and but don't look at the main dialog""" self._populate() - def populateWithCheck(self): + def populateWithCheck(self) -> None: """Populate the list from the main dialog selection, only if the AutoUpdate checkbox is checked""" if self.uiAutoUpdateCHK.isChecked(): self.setSliders(self.parUI.uiSliderTREE.getSelectedItems(typ=Slider)) self._populate() - def _populate(self): + def _populate(self) -> None: """Populate the list widgets in the UI""" if self.parUI.simplex is None: return @@ -377,7 +377,7 @@ def _populate(self): if self.uiComboCheckLIST.count() > 0: self.uiComboCheckLIST.item(0).setSelected(True) - def createMissing(self): + def createMissing(self) -> None: """Create selected combos if they don't already exist""" simplex = self.parUI.simplex created = [] diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index a805f0c7..cb10277b 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -18,6 +18,7 @@ """Alembic files can be difficult to work with, and can be *very* slow in Python This is a library of convenience functions with the numpy speed optimizations """ + from __future__ import annotations import os @@ -77,7 +78,7 @@ def pbPrint( val: int | None = None, maxVal: int | None = None, _pbPrintLastComma=None, -): +) -> None: """A function that handles displaying messages in a QProgressDialog or printing to stdout Don't forget to call QApplication.processEvents() after using this function @@ -308,7 +309,7 @@ def setAlembicSample( bounds: Box3d | None = None, uvs: OV2fGeomParamSample | None = None, normals: ON3fGeomParamSample | None = None, -): +) -> None: """Set an alembic sample to the output mesh with the given properties""" # Do it this way because the defaults for these arguments are some value other than None kwargs = {} @@ -784,7 +785,7 @@ def buildAbc( propDict: dict[str, str] | None = None, ogawa: bool = True, pBar: QProgressDialog | None = None, -): +) -> None: """ Build a single-mesh alembic file from all of the non-alembic raw data diff --git a/src/python/simplexui/commands/applyCorrectives.py b/src/python/simplexui/commands/applyCorrectives.py index 7cc82895..8755e340 100644 --- a/src/python/simplexui/commands/applyCorrectives.py +++ b/src/python/simplexui/commands/applyCorrectives.py @@ -121,7 +121,7 @@ def loadSimplex(shapePath): return jsString, simplex, solver, verts, restPts -def writeSimplex(inPath, outPath, newShapes, name="Face", pBar=None): +def writeSimplex(inPath, outPath, newShapes, name="Face", pBar=None) -> None: """Write a simplex file with new shapes Parameters @@ -165,7 +165,7 @@ def writeSimplex(inPath, outPath, newShapes, name="Face", pBar=None): ######################################################################### -def _buildSolverInputs(simplex, item, value, indexBySlider): +def _buildSolverInputs(simplex, item, value, indexBySlider) -> list[float]: """Build an input vector for the solver that will produce a required progression value on an item """ @@ -181,7 +181,7 @@ def _buildSolverInputs(simplex, item, value, indexBySlider): raise ValueError(f"Not a slider or combo. Got type {type(item)}: {item}") -def buildFullShapes(simplex, shapeObjs, shapes, solver, pBar=None): +def buildFullShapes(simplex: Simplex, shapeObjs, shapes, solver, pBar=None): """Given shape inputs, build the full output shape from the deltas We use shapes here because a shape implies both the progression and the value of the inputs (with a little figuring) @@ -256,7 +256,7 @@ def buildFullShapes(simplex, shapeObjs, shapes, solver, pBar=None): return ptsByShape, vecByShape -def collapseFullShapes(simplex, allPts, ptsByShape, vecByShape, pBar=None): +def collapseFullShapes(simplex: Simplex, allPts, ptsByShape, vecByShape, pBar=None): """Given a set of shapes that are full-on shapes (not just deltas) Collapse them back into deltas in the simplex shape list @@ -320,7 +320,14 @@ def collapseFullShapes(simplex, allPts, ptsByShape, vecByShape, pBar=None): def applyCorrectives( - simplex, allShapePts, restPts, solver, shapes, refIdxs, references, pBar=None + simplex: Simplex, + allShapePts, + restPts, + solver, + shapes: list[Shape], + refIdxs: list[int], + references, + pBar=None, ): """Loop over the shapes and references, apply them, and return a new np.array of shape points @@ -407,7 +414,7 @@ def applyCorrectives( return newShapePts -def readAndApplyCorrectives(inPath, namePath, refPath, outPath, pBar=None): +def readAndApplyCorrectives(inPath, namePath, refPath, outPath, pBar=None) -> None: """Read the provided files, apply the correctives, then output a new file Parameters diff --git a/src/python/simplexui/commands/correctiveInterface.py b/src/python/simplexui/commands/correctiveInterface.py index 19bad1a9..ffc4473b 100644 --- a/src/python/simplexui/commands/correctiveInterface.py +++ b/src/python/simplexui/commands/correctiveInterface.py @@ -218,7 +218,7 @@ def buildCorrectiveReferences(mesh, simplex, poses, sliders, pBar=None): def outputCorrectiveReferences( outNames, outRefs, simplex, mesh, poses, sliders, pBar=None -): +) -> None: """Output the proper files for an external corrective application Parameters diff --git a/src/python/simplexui/commands/expandedExport.py b/src/python/simplexui/commands/expandedExport.py index 9e008573..690dbd5f 100644 --- a/src/python/simplexui/commands/expandedExport.py +++ b/src/python/simplexui/commands/expandedExport.py @@ -25,7 +25,7 @@ from .alembicCommon import buildSmpx -def _setSliders(ctrl, val, svs): +def _setSliders(ctrl, val, svs) -> None: slis, vals = svs.setdefault(ctrl.simplex, ([], [])) if isinstance(ctrl, Slider): @@ -44,7 +44,7 @@ def _setSliders(ctrl, val, svs): _setSliders(progCtrl, val, svs) -def setSliderGroup(ctrls, val): +def setSliderGroup(ctrls, val: float) -> None: """Set a group of controls to a given value Parameters @@ -99,7 +99,7 @@ def clientPartition(master, clients): return sliders, combos, traversals -def zeroAll(smpxs): +def zeroAll(smpxs) -> None: """Set all sliders on the given simplex systems to 0 Parameters @@ -115,7 +115,7 @@ def zeroAll(smpxs): smpx.setSlidersWeights(smpx.sliders, [0.0] * len(smpx.sliders)) -def getExpandedData(master, clients, mesh): +def getExpandedData(master, clients, mesh: str): """Get the fully expanded shape data for each slider, combo, and traversal at each of its underlying shapes @@ -202,7 +202,7 @@ def getExpandedData(master, clients, mesh): return restShape, sliderShapes, comboShapes, travShapes -def _setInputs(inVec, item, indexBySlider, value): +def _setInputs(inVec: list[float], item, indexBySlider, value) -> list[float]: """Being clever Sliders or Combos just set the value and return Traversals recursively call this function with the controllers (that only either sliders or combos) @@ -233,7 +233,7 @@ def _setInputs(inVec, item, indexBySlider, value): ) -def _buildSolverInputs(simplex, item, value, indexBySlider): +def _buildSolverInputs(simplex, item, value, indexBySlider) -> list[float]: """Build an input vector for the solver that will produce a required progression value on an item """ @@ -241,7 +241,7 @@ def _buildSolverInputs(simplex, item, value, indexBySlider): return _setInputs(inVec, item, indexBySlider, value) -def getTravDepth(trav): +def getTravDepth(trav) -> int: """Get the depth of a traversal object Parameters @@ -353,7 +353,7 @@ def parseExpandedData(smpx, restShape, sliderShapes, comboShapes, travShapes): return shapeArray -def buildShapeArray(mesh, master, clients): +def buildShapeArray(mesh: str, master: Simplex, clients): """Build the outpu shape array Parameters @@ -381,7 +381,7 @@ def buildShapeArray(mesh, master, clients): return shapeArray -def expandedExportAbc(path, mesh, master, clients=()): +def expandedExportAbc(path: str, mesh: str, master: Simplex, clients: tuple = ()) -> None: """Export the alembic by re-building the deltas from all of the full shapes This is required for delta-mushing a system, because the sum of mushed shapes is not the same as the mushed sum-of-shapes diff --git a/src/python/simplexui/commands/hdf5Convert.py b/src/python/simplexui/commands/hdf5Convert.py index 0bbc3750..a28d3699 100644 --- a/src/python/simplexui/commands/hdf5Convert.py +++ b/src/python/simplexui/commands/hdf5Convert.py @@ -22,7 +22,7 @@ from .alembicCommon import buildSmpx, readSmpx -def hdf5Convert(inPath, outPath, ogawa=False): +def hdf5Convert(inPath: str, outPath: str, ogawa=False) -> None: """Load and parse all the data from a simplex file Parameters diff --git a/src/python/simplexui/commands/mayaCorrectiveInterface.py b/src/python/simplexui/commands/mayaCorrectiveInterface.py index 66be4d00..d95d9fdf 100644 --- a/src/python/simplexui/commands/mayaCorrectiveInterface.py +++ b/src/python/simplexui/commands/mayaCorrectiveInterface.py @@ -26,7 +26,7 @@ from maya import cmds -def setPose(pvp, multiplier): +def setPose(pvp, multiplier) -> None: """Set a percentage of a pose Parameters @@ -40,7 +40,7 @@ def setPose(pvp, multiplier): cmds.setAttr(prop, val * multiplier) -def resetPose(pvp): +def resetPose(pvp) -> None: """Reset everything back to rest Parameters diff --git a/src/python/simplexui/commands/mayatonumpy.py b/src/python/simplexui/commands/mayatonumpy.py index 5b39e300..bab0bf0c 100644 --- a/src/python/simplexui/commands/mayatonumpy.py +++ b/src/python/simplexui/commands/mayatonumpy.py @@ -301,7 +301,7 @@ def getNumpyAttr( def setNumpyAttr( attrName: str | om.MPlug, value: np.ndarray | float | int | tuple[int, ...] | tuple[float, ...], -): +) -> None: """Write a numpy array directly into a maya plug This function will handle most numeric plug types. @@ -382,7 +382,7 @@ def setNumpyAttr( ################################################################################ -def test(): +def test() -> None: import time from maya import cmds diff --git a/src/python/simplexui/commands/mesh.py b/src/python/simplexui/commands/mesh.py index bb36eb33..33f7d265 100644 --- a/src/python/simplexui/commands/mesh.py +++ b/src/python/simplexui/commands/mesh.py @@ -30,6 +30,8 @@ from __future__ import annotations +from types import NotImplementedType + class Mesh: """ @@ -101,7 +103,7 @@ def __init__( uvMap=None, uvFaceMap=None, ensureWinding=False, - ): + ) -> None: self._verts = None self._faces = None self._uvs = {} @@ -169,7 +171,7 @@ def __init__( else: self.vertToFaces = [vertToFaces[i] for i in range(vertCount)] - def ensureWinding(self): + def ensureWinding(self) -> None: """Ensure the winding of the mesh after-the-fact""" if self._wound: return @@ -185,7 +187,7 @@ def ensureWinding(self): self.vertToFaces.append(self._linkPairs(wings)) @classmethod - def loadObj(cls, path, ensureWinding=True): + def loadObj(cls, path, ensureWinding: bool = True) -> Mesh: """Read a .obj file and produce a Mesh object Parameters @@ -245,7 +247,7 @@ def loadObj(cls, path, ensureWinding=True): ) @classmethod - def loadAbc(cls, path, meshName=None, ensureWinding=True): + def loadAbc(cls, path, meshName=None, ensureWinding: bool = True) -> Mesh: """Read a .abc file and produce a Mesh object Parameters @@ -302,7 +304,7 @@ def loadAbc(cls, path, meshName=None, ensureWinding=True): return cls(verts, faces, uvs=uvs, uvFaces=uvFaces, ensureWinding=ensureWinding) @classmethod - def loadPrimitive(cls, prim, channelName=None, ensureWinding=True): + def loadPrimitive(cls, prim, channelName=None, ensureWinding: bool = True) -> Mesh: """Read the vertex and face data from a cross3d primitive Parameters @@ -475,11 +477,11 @@ def adjacentVertsByEdge(self, vertIdx): """ return self.vertNeighbors[vertIdx] - def vertCount(self): + def vertCount(self) -> int: """Get the number of vertices in this mesh""" return len(self.vertArray) - def faceCount(self): + def faceCount(self) -> int: """Get the number of faces in this mesh""" return len(self.faceVertArray) @@ -507,7 +509,7 @@ def faces(self): self._faces = [Face(self, i) for i in range(len(self.faceVertArray))] return self._faces - def vertSet(self): + def vertSet(self) -> VertSet: """Get a vertex set containing the whole mesh Returns @@ -519,7 +521,7 @@ def vertSet(self): ret.update(list(range(len(self.vertArray)))) return ret - def faceSet(self): + def faceSet(self) -> FaceSet: """Get a face set containing the whole mesh Returns @@ -531,7 +533,7 @@ def faceSet(self): ret.update(list(range(len(self.faceVertArray)))) return ret - def uvs(self, channelName="default"): + def uvs(self, channelName: str = "default"): """Get all UV convenience objects Returns @@ -545,7 +547,7 @@ def uvs(self, channelName="default"): self._uvs[channelName] = [UV(self, channelName, i) for i in uvm] return self._uvs.get(channelName) - def uvFaces(self, channelName="default"): + def uvFaces(self, channelName: str = "default"): """Get all UV convenience objects Returns @@ -561,7 +563,7 @@ def uvFaces(self, channelName="default"): ] return self._uvFaces.get(channelName) - def isBorderVert(self, vertIdx): + def isBorderVert(self, vertIdx) -> bool: """Check if the given vertex index is along a border Returns @@ -576,7 +578,7 @@ def isBorderVert(self, vertIdx): return True return False - def getBorderVerts(self): + def getBorderVerts(self) -> VertSet: """Get a vertex set of the border vertices Returns @@ -590,7 +592,7 @@ def getBorderVerts(self): out.update(edge) return out - def clearCache(self): + def clearCache(self) -> None: """Clear all cached convenience classes""" self._verts = None self._faces = None @@ -616,7 +618,7 @@ class MeshComponent: __slot__ = "mesh", "index" - def __init__(self, mesh, index): + def __init__(self, mesh: Mesh, index) -> None: self.mesh = mesh self.index = index self.mesh.children.append(self) @@ -624,7 +626,7 @@ def __init__(self, mesh, index): def __int__(self): return self.index - def clear(self): + def clear(self) -> None: """Remove all reference data from this object""" self.mesh = None self.mesh.children.remove(self) @@ -694,7 +696,7 @@ def value(self): """ return self.mesh.vertArray[self.index] - def setValue(self, pos): + def setValue(self, pos) -> None: """Set the vertex position Parameters @@ -734,12 +736,12 @@ def adjacentFacesByVert(self): faces = self.mesh.faces() return [faces[i] for i in idxs] - def __eq__(self, other): + def __eq__(self, other) -> NotImplementedType | bool: if isinstance(other, Face): return set(self.verts()) == set(other.verts()) return NotImplemented - def __hash__(self): + def __hash__(self) -> int: return hash(self.verts()) def verts(self): @@ -754,7 +756,7 @@ def verts(self): verts = self.mesh.verts() return [verts[i] for i in idxs] - def uvs(self, name="default"): + def uvs(self, name: str = "default"): """Get all uvs that make up this face Returns @@ -782,7 +784,7 @@ class UV(MeshComponent): __slot__ = "mesh", "index", "name" - def __init__(self, mesh, name, index): + def __init__(self, mesh: Mesh, name: str, index) -> None: self.name = name super().__init__(mesh, index) @@ -796,7 +798,7 @@ def value(self): """ return self.mesh.uvMap[self.name][self.index] - def setValue(self, pos): + def setValue(self, pos) -> None: """Set the uv's position Parameters @@ -808,7 +810,7 @@ def setValue(self, pos): assert len(t) == 2 self.mesh.uvMap[self.name][self.index] = t - def __hash__(self): + def __hash__(self) -> int: return hash(self.name, self.index) @@ -817,16 +819,16 @@ class UVFace(MeshComponent): __slot__ = "mesh", "index", "name" - def __init__(self, mesh, name, index): + def __init__(self, mesh: Mesh, name: str, index) -> None: self.name = name super().__init__(mesh, index) - def __eq__(self, other): + def __eq__(self, other) -> NotImplementedType | bool: if isinstance(other, UVFace): return set(self.uvs()) == set(other.uvs()) return NotImplemented - def __hash__(self): + def __hash__(self) -> int: return hash(self.verts()) def verts(self): @@ -841,7 +843,7 @@ def verts(self): verts = self.mesh.verts() return [verts[i] for i in idxs] - def uvs(self, name="default"): + def uvs(self, name: str = "default"): """Get all uvs that make up this UVFace Returns @@ -885,7 +887,7 @@ def __new__(mcs, clsName, bases, dct): rnames = ["__rand__", "__ror__", "__rsub__", "__rxor__"] - def wrap_closure(name, right): + def wrap_closure(name: str, right: bool): def inner(self, *args): result = getattr(set, name)(self, *args) if not hasattr(result, "mesh"): @@ -913,7 +915,7 @@ def inner(self, *args): class MeshSet(set, metaclass=MeshSetMeta): """An set-like object that deals with geometry""" - def __init__(self, mesh, indices=None): + def __init__(self, mesh: Mesh, indices=None) -> None: idxs = [] if indices is None else [int(i) for i in indices] super().__init__(idxs) self.mesh = mesh @@ -994,7 +996,7 @@ def _partitionIslands(self, growMethod): class VertSet(MeshSet): """A set-like object that deals with vertices""" - def growByEdge(self, exclude=None, track=False): + def growByEdge(self, exclude=None, track: bool = False): """Add verts that share edges with the current set Parameters ---------- @@ -1010,7 +1012,7 @@ def growByEdge(self, exclude=None, track=False): """ return self.grow(self.mesh.adjacentVertsByEdge, exclude=exclude, track=track) - def growByFace(self, exclude=None, track=False): + def growByFace(self, exclude=None, track: bool = False): """Add verts that share faces with the current set Parameters ---------- @@ -1040,7 +1042,7 @@ def partitionIslands(self): class FaceSet(MeshSet): """A set-like object that deals with faces""" - def growByEdge(self, exclude=None, track=False): + def growByEdge(self, exclude=None, track: bool = False): """Add faces that share edges with the current set Parameters ---------- @@ -1056,7 +1058,7 @@ def growByEdge(self, exclude=None, track=False): """ return self.grow(self.mesh.adjacentFacesByEdge, exclude=exclude, track=track) - def growByVert(self, exclude=None, track=False): + def growByVert(self, exclude=None, track: bool = False): """Add faces that share verts with the current set Parameters ---------- diff --git a/src/python/simplexui/commands/reorderSimplexPoints.py b/src/python/simplexui/commands/reorderSimplexPoints.py index a6e0f5d2..ce4aa0cb 100644 --- a/src/python/simplexui/commands/reorderSimplexPoints.py +++ b/src/python/simplexui/commands/reorderSimplexPoints.py @@ -35,7 +35,9 @@ from .alembicCommon import buildSmpx, readSmpx -def reorderSimplexPoints(sourcePath, matchPath, outPath, invertMatch=False): +def reorderSimplexPoints( + sourcePath: str, matchPath: str, outPath: str, invertMatch=False +) -> None: """Transfer shape data from the sourcePath using the numpy int array at matchPath to make the final output at outPath diff --git a/src/python/simplexui/commands/rigidAlign.py b/src/python/simplexui/commands/rigidAlign.py index 488596e9..e9a10a4a 100644 --- a/src/python/simplexui/commands/rigidAlign.py +++ b/src/python/simplexui/commands/rigidAlign.py @@ -20,7 +20,7 @@ import numpy as np -def rigidAlign(P, Q, iters=10): +def rigidAlign(P, Q, iters: int = 10): """Rigidly align meshes with matching vert order by a least-squares error. Uses a variation of an algorithm by Umeyama Relevant links: diff --git a/src/python/simplexui/commands/smpxBlend.py b/src/python/simplexui/commands/smpxBlend.py deleted file mode 100644 index a50e5a0e..00000000 --- a/src/python/simplexui/commands/smpxBlend.py +++ /dev/null @@ -1,631 +0,0 @@ -# Copyright 2016, Blur Studio -# -# This file is part of Simplex. -# -# Simplex is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Simplex is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Simplex. If not, see . - -from __future__ import annotations - -from ..items import ( - Combo, - ComboPair, - Falloff, - Group, - ProgPair, - Progression, - Shape, - Simplex, - Slider, - Traversal, - TravPair, -) - - -class Skip: - """A pseudo-singleton object to signify completely skipping the un-matched shape - This will be used by running ``smpxMismatchCheck``, taking the dict, and manually - telling it to ``Skip`` certain shapes - """ - - pass - - -def smpxMismatchCheck(simpA, simpB): - """Search for mismatches between two simplex systems - - Parameters - ---------- - simpA : Simplex - A simplex system - simpB : Simplex - A simplex system - - Returns - ------- - : {str: {str: (object, object)}} - A structure of dict[objectType][objectName] - That returns an ordered pair of objects. The object is None if it doesn't - exist in (simplexA, simplexB) - """ - mmDict = {} - - # saShapeNames = {sa.name: sa for sa in simpA.shapes} - # sbShapeNames = {sb.name: sb for sb in simpB.shapes} - # saShapeNameUnique = saShapeNames.viewkeys() - sbShapeNames.viewkeys() - # sbShapeNameUnique = sbShapeNames.viewkeys() - saShapeNames.viewkeys() - # slnMatch = [(san, None) for san in saShapeNameUnique] + [(None, sbn) for sbn in sbShapeNameUnique] - # mmDict['shape'] = slnMatch - - saSliderNames = {sa.name: sa for sa in simpA.sliders} - sbSliderNames = {sb.name: sb for sb in simpB.sliders} - saSliderNameUnique = saSliderNames.keys() - sbSliderNames.keys() - sbSliderNameUnique = sbSliderNames.keys() - saSliderNames.keys() - slnMatch = {} - for san in saSliderNameUnique: - slnMatch[san] = (san, None) - for sbn in sbSliderNameUnique: - slnMatch[sbn] = (None, sbn) - mmDict["slider"] = slnMatch - - saComboNames = {sa.name: sa for sa in simpA.combos} - sbComboNames = {sb.name: sb for sb in simpB.combos} - saComboNameUnique = saComboNames.keys() - sbComboNames.keys() - sbComboNameUnique = sbComboNames.keys() - saComboNames.keys() - # TODO search for combos with mis-ordered inputs - cnMatch = {} - for can in saComboNameUnique: - cnMatch[can] = (can, None) - for cbn in sbComboNameUnique: - cnMatch[cbn] = (None, cbn) - mmDict["combo"] = cnMatch - - saTravNames = {sa.name: sa for sa in simpA.traversals} - sbTravNames = {sb.name: sb for sb in simpB.traversals} - saTravNameUnique = saTravNames.keys() - sbTravNames.keys() - sbTravNameUnique = sbTravNames.keys() - saTravNames.keys() - # TODO search for travs with mis-ordered inputs - tnMatch = {} - for tan in saTravNameUnique: - tnMatch[tan] = (tan, None) - for tbn in sbTravNameUnique: - tnMatch[tbn] = (None, tbn) - mmDict["traversal"] = tnMatch - - return mmDict - - -def orderedMerge(va, vb): - """Merge two sets, keeping some semblance of order - - Parameters - ---------- - va : set - A set - vb : set - A set - - Returns - ------- - : list - An ordered list - """ - # come up with a better way that keeps some of the input structure - return sorted(set(va) | set(vb)) - - -# Falloffs -def mergeFalloffs(simpA, simpB, outSimp, translation, nameOnly=True): - """Merge the falloffs between two simplex systems - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - A simplex system to merge - outSimp : Simplex - The output simplex that is being built - translation : {object: object} - A dictionary of input objects to output objects - nameOnly : bool - Whether to match the falloffs by name only. - Defaults to True. False is not implemented yet - """ - if not nameOnly: - raise ValueError("Not implemented yet") - aNames = [f.name for f in simpA.falloffs] - aDict = dict(zip(aNames, simpA.falloffs)) - bNames = [f.name for f in simpB.falloffs] - bDict = dict(zip(bNames, simpB.falloffs)) - oNames = orderedMerge(aNames, bNames) - for oName in oNames: - baseFo = aDict.get(oName, bDict[oName]) - if baseFo.splitType == "planar": - data = ( - baseFo.splitType, - baseFo.axis, - baseFo.maxVal, - baseFo.maxHandle, - baseFo.minHandle, - baseFo.minVal, - ) - else: - data = (baseFo.splitType, baseFo.mapName) - fo = Falloff(oName, outSimp, *data) - translation[aDict.get(oName)] = fo - translation[bDict.get(oName)] = fo - - -# Groups -def _mergeGroupSubset(aTypedGroups, bTypedGroups, outSimp, gType, translation): - asG = [g.name for g in aTypedGroups] - bsG = [g.name for g in bTypedGroups] - asGDict = dict(zip(asG, aTypedGroups)) - bsGDict = dict(zip(bsG, bTypedGroups)) - osG = orderedMerge(asG, bsG) - for groupName in osG: - newGroup = Group(groupName, outSimp, gType) - translation[asGDict.get(groupName)] = newGroup - translation[bsGDict.get(groupName)] = newGroup - - -def mergeGroups(simpA, simpB, outSimp, translation): - """Merge the groups from simpA and simpB - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - A simplex system to merge - outSimp : Simplex - The output simplex that is being built - translation : {object: object} - A dictionary of input objects to output objects - """ - _mergeGroupSubset( - simpA.sliderGroups, simpB.sliderGroups, outSimp, Slider, translation - ) - _mergeGroupSubset(simpA.comboGroups, simpB.comboGroups, outSimp, Combo, translation) - _mergeGroupSubset( - simpA.traversalGroups, simpB.traversalGroups, outSimp, Traversal, translation - ) - - -# Shapes -def _blendShape(aShape, bShape, outSimp, blendVal, translation): - if aShape in translation or bShape in translation: - return translation.get(aShape, translation[bShape]) - - newShape = Shape(aShape.name, outSimp, create=True) - - aDeltas = aShape.verts - aShape.simplex.restShape.verts - bDeltas = bShape.verts - bShape.simplex.restShape.verts - deltas = aDeltas * (1.0 - blendVal) + bDeltas * blendVal - newShape.verts = outSimp.restShape.verts + deltas - - translation[aShape] = newShape - translation[bShape] = newShape - return newShape - - -def _copyShape(shape, outSimp, translation, deltaOverride=None): - if shape in translation: - return translation[shape] - newShape = Shape(shape.name, outSimp, create=True) - - if deltaOverride is None: - deltas = shape.verts - shape.simplex.restShape.verts - newShape.verts = outSimp.restShape.verts + deltas - else: - newShape.verts = deltaOverride - - translation[shape] = newShape - return newShape - - -# Progs -def _deltaProg(shape, srcMin, srcMax, tarMin, tarMax, tVal): - srcExtDelta = tVal * (srcMax.verts - srcMin.verts) - srcShpDelta = shape.verts - srcMin.verts - srcOffset = srcShpDelta - srcExtDelta - ret = tVal * (tarMax.verts - tarMin.verts) + srcOffset - return ret - - -def _blendProg(aProg, bProg, outSimp, blendVal, translation): - aVals = [float(pp.value) for pp in aProg.pairs] - bVals = [float(pp.value) for pp in bProg.pairs] - commonVals = sorted(set(aVals) & set(bVals)) - aUniq = set(aVals) - set(commonVals) - bUniq = set(bVals) - set(commonVals) - aValDict = dict(zip(aVals, aProg.pairs)) - bValDict = dict(zip(bVals, bProg.pairs)) - - # First handle all the common values - outValDict = {} - for c in commonVals: - newShape = _blendShape( - aValDict[c].shape, bValDict[c].shape, outSimp, blendVal, translation - ) - outValDict[c] = newShape - - # Then handle any unique extremes - if 1.0 in aUniq: - outValDict[1.0] = _copyShape(aValDict[1.0].shape, outSimp, translation) - if -1.0 in aUniq: - outValDict[-1.0] = _copyShape(aValDict[-1.0].shape, outSimp, translation) - if 1.0 in bUniq: - outValDict[1.0] = _copyShape(bValDict[1.0].shape, outSimp, translation) - if -1.0 in bUniq: - outValDict[-1.0] = _copyShape(bValDict[-1.0].shape, outSimp, translation) - - # Finally handle the unique progressions as deltas off the extremes - outRest = outValDict[0.0] - aRest = aValDict[0.0].shape - for c in aUniq: - aShp = aValDict[c].shape - mm = 1.0 if c > 0.0 else -1.0 - dd = _deltaProg( - aShp, aRest, aValDict[mm].shape, outRest, outValDict[mm], mm * c - ) - outValDict[c] = _copyShape(aShp, outSimp, translation, deltaOverride=dd) - - bRest = bValDict[0.0].shape - for c in bUniq: - bShp = bValDict[c].shape - mm = 1.0 if c > 0.0 else -1.0 - dd = _deltaProg( - bShp, bRest, bValDict[mm].shape, outRest, outValDict[mm], mm * c - ) - outValDict[c] = _copyShape(bShp, outSimp, translation, deltaOverride=dd) - - outPairs = [ - ProgPair(outSimp, outValDict[val], val) for val in sorted(outValDict.keys()) - ] - prog = Progression(aProg.name, outSimp, pairs=outPairs, interp=aProg.interp) - translation[aProg] = prog - translation[bProg] = prog - return prog - - -def _copyProg(prog, outSimp, translation): - pairs = [] - for pp in prog.pairs: - newShape = _copyShape(pp.shape, outSimp, translation) - pair = ProgPair(outSimp, newShape, pp.value) - pairs.append(pair) - outProg = Progression(prog.name, outSimp, pairs=pairs, interp=prog.interp) - translation[prog] = outProg - return outProg - - -# Sliders -def _blendSliders(aSlider, bSlider, outSimp, blendVal, translation): - if aSlider in translation or bSlider in translation: - return translation.get(aSlider, translation[bSlider]) - - group = translation.get(aSlider.group, translation[bSlider.group]) - prog = _blendProg(aSlider.prog, bSlider.prog, outSimp, blendVal, translation) - outSlider = Slider(aSlider.name, outSimp, prog, group) - translation[aSlider] = outSlider - translation[bSlider] = outSlider - return outSlider - - -def _copySlider(slider, outSimp, translation): - if slider in translation: - return translation[slider] - - group = translation[slider.group] - prog = _copyProg(slider.prog, outSimp, translation) - outSlider = Slider(slider.name, outSimp, prog, group) - translation[slider] = outSlider - return outSlider - - -def sliderBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): - """Blend between the sliders of simpA and simpB - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - A simplex system to merge - outSimp : Simplex - The output simplex that is being built - blendVal : float - The 0 to 1 blend value between the simplices - translation : {object: object} - A dictionary of input objects to output objects - mismatch : dict - The crazy mismatch dict from ``smpxMismatchCheck`` - """ - aNames = [s.name for s in simpA.sliders] - aDict = dict(zip(aNames, simpA.sliders)) - bNames = [s.name for s in simpB.sliders] - bDict = dict(zip(bNames, simpB.sliders)) - oNames = orderedMerge(aNames, bNames) - - for oIdx, oName in enumerate(oNames): - print(f"Copying Slider {oIdx} of {len(oNames)}: {oName}") - aName, bName = mismatch["slider"].get(oName, (oName, oName)) - if aName is Skip or bName is Skip: - continue - elif aName is None: - _copySlider(bDict[bName], outSimp, translation) - elif bName is None: - _copySlider(aDict[aName], outSimp, translation) - else: - _blendSliders(aDict[aName], bDict[bName], outSimp, blendVal, translation) - - -# Combos -def _blendCombos(aCombo, bCombo, outSimp, blendVal, translation): - if aCombo in translation or bCombo in translation: - return translation.get(aCombo, translation[bCombo]) - - group = translation.get(aCombo.group, translation[bCombo.group]) - - cPairs = [] - for aPair, bPair in zip(aCombo.pairs, bCombo.pairs): - slider = _blendSliders( - aPair.slider, bPair.slider, outSimp, blendVal, translation - ) - cPairs.append(ComboPair(slider, aPair.value)) - - prog = _blendProg(aCombo.prog, bCombo.prog, outSimp, blendVal, translation) - outCombo = Combo(aCombo.name, outSimp, cPairs, prog, group, aCombo.solveType) - translation[aCombo] = outCombo - translation[bCombo] = outCombo - return outCombo - - -def _copyCombo(combo, outSimp, translation): - if combo in translation: - return translation[combo] - group = translation[combo.group] - - cPairs = [] - for pair in combo.pairs: - slider = _copySlider(pair.slider, outSimp, translation) - cPairs.append(ComboPair(slider, pair.value)) - prog = _copyProg(combo.prog, outSimp, translation) - - outCombo = Combo(combo.name, outSimp, cPairs, prog, group, combo.solveType) - translation[combo] = outCombo - return outCombo - - -def comboBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): - """Blend between the combos of simpA and simpB - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - A simplex system to merge - outSimp : Simplex - The output simplex that is being built - blendVal : float - The 0 to 1 blend value between the simplices - translation : {object: object} - A dictionary of input objects to output objects - mismatch : dict - The crazy mismatch dict from ``smpxMismatchCheck`` - """ - aNames = [s.name for s in simpA.combos] - aDict = dict(zip(aNames, simpA.combos)) - bNames = [s.name for s in simpB.combos] - bDict = dict(zip(bNames, simpB.combos)) - oNames = orderedMerge(aNames, bNames) - - for oIdx, oName in enumerate(oNames): - print(f"Copying Combo {oIdx} of {len(oNames)}: {oName}") - aName, bName = mismatch["combo"].get(oName, (oName, oName)) - - if oName in mismatch: - print("Getting", oName) - print("Mismatch Get", oName in mismatch, aName, bName) - - if aName is Skip or bName is Skip: - continue - elif aName is None: - _copyCombo(bDict[bName], outSimp, translation) - elif bName is None: - _copyCombo(aDict[aName], outSimp, translation) - else: - _blendCombos(aDict[aName], bDict[bName], outSimp, blendVal, translation) - - -# Traversals -def _blendController(aItem, bItem, outSimp, blendVal, translation): - aCtrl = aItem.controller - bCtrl = bItem.controller - - if isinstance(aCtrl, Slider): - ctrl = _blendSliders(aCtrl, bCtrl, outSimp, blendVal, translation) - elif isinstance(aCtrl, Combo): - ctrl = _blendCombos(aCtrl, bCtrl, outSimp, blendVal, translation) - else: - raise ValueError(f"Bad object type: {aCtrl} {type(aCtrl)}") - return TravPair(ctrl, aItem.value, aItem.usage) - - -def _copyController(item, outSimp, translation): - iCtrl = item.controller - if isinstance(iCtrl, Slider): - ctrl = _copySlider(iCtrl, outSimp, translation) - elif isinstance(iCtrl, Combo): - ctrl = _copyCombo(iCtrl, outSimp, translation) - else: - raise ValueError(f"Bad object type: {iCtrl} {type(iCtrl)}") - return TravPair(ctrl, item.value, item.usage) - - -def _blendTraversals(aTrav, bTrav, outSimp, blendVal, translation): - group = translation.get(aTrav.group, translation[bTrav.group]) - multCtrl = _blendController( - aTrav.multiplierCtrl, bTrav.multiplierCtrl, outSimp, blendVal, translation - ) - progCtrl = _blendController( - aTrav.progressCtrl, bTrav.progressCtrl, outSimp, blendVal, translation - ) - prog = _blendProg(aTrav.prog, bTrav.prog, outSimp, blendVal, translation) - outTrav = Traversal(aTrav.name, outSimp, multCtrl, progCtrl, prog, group) - translation[aTrav] = outTrav - translation[bTrav] = outTrav - return outTrav - - -def _copyTraversal(traversal, outSimp, translation): - group = translation[traversal.group] - multCtrl = _copyController(traversal.multiplierCtrl, outSimp, translation) - progCtrl = _copyController(traversal.progressCtrl, outSimp, translation) - prog = _copyProg(traversal.prog, outSimp, translation) - outTrav = Traversal(traversal.name, outSimp, multCtrl, progCtrl, prog, group) - translation[traversal] = outTrav - return outTrav - - -def traversalBlend(simpA, simpB, outSimp, blendVal, translation, mismatch): - """Blend between the traversals of simpA and simpB - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - A simplex system to merge - outSimp : Simplex - The output simplex that is being built - blendVal : float - The 0 to 1 blend value between the simplices - translation : {object: object} - A dictionary of input objects to output objects - mismatch : dict - The crazy mismatch dict from ``smpxMismatchCheck`` - """ - aNames = [s.name for s in simpA.traversals] - aDict = dict(zip(aNames, simpA.traversals)) - bNames = [s.name for s in simpB.traversals] - bDict = dict(zip(bNames, simpB.traversals)) - oNames = orderedMerge(aNames, bNames) - - for oIdx, oName in enumerate(oNames): - print(f"Copying Traversal {oIdx} of {len(oNames)}: {oName}") - aName, bName = mismatch["traversal"].get(oName, (oName, oName)) - if aName is Skip or bName is Skip: - continue - elif aName is None: - _copyTraversal(bDict[bName], outSimp, translation) - elif bName is None: - _copyTraversal(aDict[aName], outSimp, translation) - else: - _blendTraversals(aDict[aName], bDict[bName], outSimp, blendVal, translation) - - -# Simplex -def smpxBlend(simpA, simpB, blendVal=0.50, mismatchDict=None, name="Face"): - """Blend the deltas from simplexA and simplexB. Apply the output to simplexA - Equal falloffs will be combined. Others will be given suffixes - - Parameters - ---------- - simpA : Simplex - The master Simplex - simpB : Simplex - The Simplex to be merged into simpA - blendVal : float - A value between 0 and 1, where 0 is fully simpA, and 1 is fully simpB. Defaults to 0.5 - mismatchDict : dict or None - A dictionary like the one returned from ``spxMismatchCheck``. Defaults to None - name : str - The new name of the output system - - Returns - ------- - : Simplex - A new simplex system blended between the two inputs - - """ - mismatchDict = mismatchDict or {} - simpA.stack.enabled = False - simpB.stack.enabled = False - - outSimp = Simplex(name=name, forceDummy=True) - outSimp.stack.enabled = False - - dcc = outSimp.DCC - # TODO: Maybe add accessors to the simplex or DCC - dcc._faces = simpA.DCC._faces - dcc._counts = simpA.DCC._counts - dcc._uvs = simpA.DCC._uvs - dcc._falloffs = simpA.DCC._falloffs - dcc._numVerts = simpA.DCC._numVerts - - rest = Shape.buildRest(outSimp) - rest.verts = ( - simpA.restShape.verts * (1 - blendVal) + simpB.restShape.verts * blendVal - ) - - outSimp.restShape = rest - - # When walking through this smpx file, keep a "translation" dictionary - # where translation[InputObjectID] = OutputObject - translation = {} - translation[simpA.restShape] = rest - translation[simpB.restShape] = rest - - # Merge the groups. This one is easy. Merge by name - mergeGroups(simpA, simpB, outSimp, translation) - - # Merge the falloffs. We can either require them to have equal values to "merge" - # Or we can just merge by name. The nameOnly kwarg handles this. True for now - mergeFalloffs(simpA, simpB, outSimp, translation, nameOnly=True) - - # Copying one of the "big" object types should create and blend its child prog and shapes - print("Copying Sliders") - sliderBlend(simpA, simpB, outSimp, blendVal, translation, mismatchDict) - print("Copying Combos") - comboBlend(simpA, simpB, outSimp, blendVal, translation, mismatchDict) - print("Copying Traversals") - traversalBlend(simpA, simpB, outSimp, blendVal, translation, mismatchDict) - - # push the verts to the dummy dcc for later export - dcc.pushAllShapeVertices(outSimp.shapes) - return outSimp - - -if __name__ == "__main__": - pathA = r"D:\Users\tyler\Desktop\TEST\GumboA.smpx" - pathB = r"D:\Users\tyler\Desktop\TEST\GumboB.smpx" - outPath = r"D:\Users\tyler\Desktop\TEST\hoping2.smpx" - - print("Loading SimpA") - simpA = Simplex.buildSystemFromSmpx(pathA, forceDummy=True) - print("Loading SimpB") - simpB = Simplex.buildSystemFromSmpx(pathB, forceDummy=True) - - print("Matching A:B") - mismatch = smpxMismatchCheck(simpA, simpB) - outSmpx = smpxBlend(simpA, simpB, blendVal=0.5, mismatchDict=mismatch) - - print("Exporting") - print("OUT SMPX", outSmpx) - outSmpx.exportAbc(outPath) - - print("DONE") diff --git a/src/python/simplexui/commands/unsubdivide.py b/src/python/simplexui/commands/unsubdivide.py index 01940c7d..9a64d631 100644 --- a/src/python/simplexui/commands/unsubdivide.py +++ b/src/python/simplexui/commands/unsubdivide.py @@ -470,7 +470,7 @@ def _findOldPositionBorder( borders, vIdx, computed, -): +) -> None: """Find the position of the un-subdivided mesh if the vertex was on the border""" nei = neighDict[vIdx][0] nei = [i for i in nei if i in borders] @@ -490,7 +490,7 @@ def _findOldPositionSimple( uEdgeDict, vIdx, computed, -): +) -> None: """Find the position of the un-subdivided mesh if the vertex has at least 4 neighbors. Updates uVerts in-place """ @@ -856,7 +856,7 @@ def fixVerts( return uVerts -def getUVPins(faces, borders, uvFaces, uvBorders, pinBorders): +def getUVPins(faces, borders, uvFaces, uvBorders, pinBorders: bool): """Find which uvBorders are also mesh borders Parameters @@ -988,8 +988,8 @@ def unSubdivide( uvFaces, uvs, hints=None, - repositionVerts=True, - pinBorders=False, + repositionVerts: bool = True, + pinBorders: bool = False, pBar=None, ): """Given a mesh representation (faces and vertices) remove the edges added @@ -1153,7 +1153,7 @@ def _ussmpx(faces, verts, uvFaces, uvs, pBar=None): return rFaces, rVerts, rUVFaces, rUVs -def _applyShapePrefix(shapePrefix, jsString): +def _applyShapePrefix(shapePrefix, jsString: str) -> str: """Apply a prefix to the shape names. For XSI where shape names must be unique """ @@ -1177,7 +1177,7 @@ def _unflattenFaces(flatFaces, counts): return faces -def unsubdivideSimplex(inPath, outPath, shapePrefix=None, pBar=None): +def unsubdivideSimplex(inPath, outPath, shapePrefix=None, pBar=None) -> None: """Unsubdivde a .smpx file on disk Parameters diff --git a/src/python/simplexui/commands/uvTransfer.py b/src/python/simplexui/commands/uvTransfer.py index 6e142e02..45f69d2e 100644 --- a/src/python/simplexui/commands/uvTransfer.py +++ b/src/python/simplexui/commands/uvTransfer.py @@ -28,7 +28,7 @@ ######################## -def _lerp(idx, corners, p): +def _lerp(idx: int, corners, p): # If I'm here, I know that p lies on the line # between the corners at idx and idx+1. # Return the lerp value @@ -395,7 +395,7 @@ def sweep(qPoints, uvs, tris, pBar=None): ######################## -def inBox(point, mxs, mns): +def inBox(point, mxs, mns) -> bool: """Check if a point is inside the bounding box Parameters @@ -413,10 +413,10 @@ def inBox(point, mxs, mns): Whether the point is in the given bounding box """ - return np.all(point <= mxs) and np.all(point >= mns) + return bool(np.all(point <= mxs) and np.all(point >= mns)) -def _isEar(a, b, c, polygon, tol=EPS): +def _isEar(a, b, c, polygon, tol=EPS) -> bool: """Check if the points a,b,c of the polygon could be their own triangle""" signedArea = triArea(a, b, c) @@ -763,7 +763,7 @@ def getVertCorrelation( return mvcVertDict -def applyTransfer(parVerts, parFaces, correlation, outputSize): +def applyTransfer(parVerts, parFaces, correlation, outputSize: int): """Given a vertex corelation, a driver, and driven points, Apply the driver deformation to the driven. This could be for one frame, or many frames @@ -939,11 +939,11 @@ def uvTransferFiles( srcPath, tarPath, outAbcPath, - srcUvSet="default", - tarUvSet="default", - tol=0.0001, + srcUvSet: str = "default", + tarUvSet: str = "default", + tol: float = 0.0001, pBar=None, -): +) -> None: """Transfer the shape from the source to the target through uv space and write out the result diff --git a/src/python/simplexui/dragFilter.py b/src/python/simplexui/dragFilter.py index 863ec3b5..e15b11b7 100644 --- a/src/python/simplexui/dragFilter.py +++ b/src/python/simplexui/dragFilter.py @@ -87,7 +87,7 @@ class DragFilter(QObject): dragPressed: Signal = Signal() dragReleased: Signal = Signal() - def __init__(self, parent: QWidget | None): + def __init__(self, parent: QWidget | None) -> None: self._parent: QWidget | None = parent # Hold onto this to keep the qwidget type super().__init__(parent) @@ -119,7 +119,7 @@ def __init__(self, parent: QWidget | None): self._screen: QRect | None = None self._isDragging: bool = False - def doOverrideCursor(self): + def doOverrideCursor(self) -> None: """Change the cursor based on the current drag type""" if self._overridden: return @@ -133,14 +133,14 @@ def doOverrideCursor(self): self._overridden = True - def restoreOverrideCursor(self): + def restoreOverrideCursor(self) -> None: """Restore the cursor to the normal pointer""" if not self._overridden: return QApplication.restoreOverrideCursor() self._overridden = False - def doDrag(self, o: QObject, e: QMouseEvent): + def doDrag(self, o: QObject, e: QMouseEvent) -> None: """Handle a mouse drag event Parameters @@ -212,7 +212,7 @@ def doDrag(self, o: QObject, e: QMouseEvent): else: self._lastPos = None - def getCurrentScreen(self, o: QObject, global_pos: QPoint): + def getCurrentScreen(self, o: QObject, global_pos: QPoint) -> QRect: screen = QGuiApplication.screenAt(global_pos) if screen is None: if self._parent is not None: @@ -222,7 +222,7 @@ def getCurrentScreen(self, o: QObject, global_pos: QPoint): self._screen = screen.availableGeometry() return self._screen - def startDrag(self, o: QObject, e: QMouseEvent): + def startDrag(self, o: QObject, e: QMouseEvent) -> None: """Start the drag event handling Parameters @@ -264,7 +264,7 @@ def startDrag(self, o: QObject, e: QMouseEvent): ) QApplication.sendEvent(o, mouseup) - def myendDrag(self, o: QObject, e: QMouseEvent): + def myendDrag(self, o: QObject, e: QMouseEvent) -> None: """End the drag event handling. Can't call it endDrag because that's taken Parameters diff --git a/src/python/simplexui/falloffDialog.py b/src/python/simplexui/falloffDialog.py index e7bb1b58..469fd2f9 100644 --- a/src/python/simplexui/falloffDialog.py +++ b/src/python/simplexui/falloffDialog.py @@ -70,7 +70,7 @@ class CurveEditWidget(QWidget): tangentUpdated = Signal(float, float) - def __init__(self, parent: QWidget | None): + def __init__(self, parent: QWidget | None) -> None: super().__init__(parent) self.leftTan: float | None = None self.rightTan: float | None = None @@ -94,7 +94,9 @@ def __init__(self, parent: QWidget | None): self.lineColor: Qt.GlobalColor = Qt.GlobalColor.black self.limitColor: Qt.GlobalColor = Qt.GlobalColor.gray - def setTangent(self, leftTan: float | None = None, rightTan: float | None = None): + def setTangent( + self, leftTan: float | None = None, rightTan: float | None = None + ) -> None: """Set the falloff tangents, clamped 0 to 1 Parameters @@ -156,16 +158,16 @@ def mapFromCanvas(self, point: QPointF) -> QPointF: y = 1.0 - (point.y() - self.canvasMargin) / float(canvasHeight) return QPointF(x, y) - def _drawCleanLine(self, painter: QPainter, p1: QPointF, p2: QPointF): + def _drawCleanLine(self, painter: QPainter, p1: QPointF, p2: QPointF) -> None: painter.drawLine(p1 + QPointF(0.5, 0.5), p2 + QPointF(0.5, 0.5)) - def _paintBG(self, painter: QPainter): + def _paintBG(self, painter: QPainter) -> None: painter.save() painter.setBrush(self.palette().color(QPalette.ColorRole.Window)) painter.drawRect(0, 0, self.width(), self.height()) painter.restore() - def _paintLimits(self, painter: QPainter): + def _paintLimits(self, painter: QPainter) -> None: painter.save() # pen = QPen(self.limitColor) baseColor = self.palette().color(QPalette.ColorRole.Base) @@ -183,7 +185,7 @@ def _paintLimits(self, painter: QPainter): def _paintPath( self, painter: QPainter, p0: QPointF, p1: QPointF, p2: QPointF, p3: QPointF - ): + ) -> None: painter.save() path = QPainterPath() path.moveTo(p0) @@ -195,7 +197,7 @@ def _paintPath( def _paintTangents( self, painter: QPainter, p0: QPointF, p1: QPointF, p2: QPointF, p3: QPointF - ): + ) -> None: # draw the tangent lines foregroundColor = self.palette().color(QPalette.ColorRole.WindowText) pen = QPen(foregroundColor) @@ -210,7 +212,7 @@ def _paintTangents( active = i == self._activeControlPoint self.paintControlPoint(self._controlPoints[i], painter, online, active) - def paintEvent(self, e: QPaintEvent): + def paintEvent(self, e: QPaintEvent) -> None: painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) @@ -226,12 +228,12 @@ def paintEvent(self, e: QPaintEvent): self._paintPath(painter, p0, p1, p2, p3) self._paintTangents(painter, p0, p1, p2, p3) - def indexIsRealPoint(self, i: int): + def indexIsRealPoint(self, i: int) -> bool: return (i % 3) == 0 def paintControlPoint( self, point: QPointF, painter: QPainter, real: bool, active: bool - ): + ) -> None: pointSize = 4 if real: @@ -263,7 +265,7 @@ def findControlPoint(self, point: QPointF, tolerance: int = 10) -> int | None: return 2 return None - def mousePressEvent(self, e: QMouseEvent): + def mousePressEvent(self, e: QMouseEvent) -> None: if e.button() == Qt.MouseButton.LeftButton: self._activeControlPoint = self.findControlPoint(e.position()) if self._activeControlPoint is not None: @@ -271,13 +273,13 @@ def mousePressEvent(self, e: QMouseEvent): self.mousePress = e.pos() e.accept() - def mouseReleaseEvent(self, e: QMouseEvent): + def mouseReleaseEvent(self, e: QMouseEvent) -> None: if e.button() == Qt.MouseButton.LeftButton: self._activeControlPoint = None self.mouseDrag = False e.accept() - def mouseMoveEvent(self, e: QMouseEvent): + def mouseMoveEvent(self, e: QMouseEvent) -> None: if ( not self.mouseDrag and QPoint(self.mousePress - e.pos()).manhattanLength() @@ -313,7 +315,7 @@ class FalloffDialog(QDialog): uiFalloffMinHandleSPN: QDoubleSpinBox uiFalloffMinSPN: QDoubleSpinBox - def __init__(self, parent: SimplexDialog): + def __init__(self, parent: SimplexDialog) -> None: super().__init__(parent) uiPath = getUiFile(__file__) QtCompat.loadUi(uiPath, self) @@ -346,7 +348,7 @@ def __init__(self, parent: SimplexDialog): self.uiFalloffMinHandleSPN.valueChanged.connect(self.setRightTangent) self.loadSimplex() - def updateTangents(self, leftTangent: float, rightTangent: float): + def updateTangents(self, leftTangent: float, rightTangent: float) -> None: self.uiFalloffMaxHandleSPN.setValue(leftTangent) self.uiFalloffMinHandleSPN.setValue(rightTangent) @@ -358,13 +360,13 @@ def updateTangents(self, leftTangent: float, rightTangent: float): self.foModel.setData(leftTanIdx, leftTangent, role=Qt.ItemDataRole.EditRole) self.foModel.setData(rightTanIdx, rightTangent, role=Qt.ItemDataRole.EditRole) - def setLeftTangent(self, val: float): + def setLeftTangent(self, val: float) -> None: self.uiFalloffWID.setTangent(leftTan=val) - def setRightTangent(self, val: float): + def setRightTangent(self, val: float) -> None: self.uiFalloffWID.setTangent(rightTan=val) - def loadSimplex(self): + def loadSimplex(self) -> None: """Load the Simplex system from the parent UI""" system = self.parUI.simplex if system == self.simplex: @@ -401,7 +403,7 @@ def loadSimplex(self): self._falloffMapper.setCurrentIndex(0) # Falloff Settings - def newFalloff(self): + def newFalloff(self) -> None: """Create a new Falloff object""" if self.simplex is None: return @@ -422,7 +424,7 @@ def newFalloff(self): nn = getNextName(newName, foNames) PlanarFalloff.createPlanar(nn, self.simplex, "X", 1.0, 0.66, 0.33, -1.0) - def duplicateFalloff(self): + def duplicateFalloff(self) -> None: """Duplicate the selected falloff""" if self.simplex is None: return @@ -440,7 +442,7 @@ def duplicateFalloff(self): nn = getNextName(fo.name, foNames) fo.duplicate(nn) - def deleteFalloff(self): + def deleteFalloff(self) -> None: """Delete the selected falloff""" if not self.simplex or not self.simplex.falloffs: return @@ -451,7 +453,7 @@ def deleteFalloff(self): fo = self.simplex.falloffs[idx] fo.delete() - def renameFalloff(self): + def renameFalloff(self) -> None: """Rename the selected falloff""" if not self.simplex or not self.simplex.falloffs: return @@ -476,13 +478,13 @@ def renameFalloff(self): nn = getNextName(newName, foNames) fo.name = nn - def storeSettings(self): + def storeSettings(self) -> None: """Store the UI settings for this dialog""" pref = Prefs() pref.recordProperty("fogeometry", self.saveGeometry().data()) pref.save() - def loadSettings(self): + def loadSettings(self) -> None: """Load the UI settings for this dialog""" pref = Prefs() geo = pref.restoreProperty("fogeometry", None) @@ -491,12 +493,12 @@ def loadSettings(self): geo = QByteArray(geo) self.restoreGeometry(geo) - def hideEvent(self, event: QHideEvent): + def hideEvent(self, event: QHideEvent) -> None: """Override the hide event to store settings""" self.storeSettings() super().hideEvent(event) - def showEvent(self, event: QShowEvent): + def showEvent(self, event: QShowEvent) -> None: """Override the show event to restore settings""" super().showEvent(event) self.loadSettings() diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index d2132244..7c01523d 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -76,17 +76,25 @@ def stacker(self, *args, **kwargs): class DummyScene: """A dcc scene containing existing dummy objects""" - def __init__(self): + def __init__(self) -> None: self.items = {} - def get(self, tpe, name): + def get( + self, + tpe: type[DummyBlendshape] + | type[DummyFalloff] + | type[DummyMesh] + | type[DummyNode] + | type[DummySimplex], + name, + ): return self.items.get(tpe, {}).get(name) - def add(self, item): + def add(self, item) -> None: typeDict = self.items.setdefault(type(item), {}) typeDict[item.name] = item - def remove(self, item): + def remove(self, item) -> None: typeDict = self.items.setdefault(type(item), {}) typeDict.pop(item.name, None) @@ -97,7 +105,7 @@ def remove(self, item): class DummyAttr: """A generic named object attribute""" - def __init__(self, name, value, parent): + def __init__(self, name, value: float, parent) -> None: self.name = name self.value = value self.parent = parent @@ -107,7 +115,7 @@ def __init__(self, name, value, parent): class DummyNode: """A generic DCC node""" - def __init__(self, name, db=DB): + def __init__(self, name: str, db=DB) -> None: self.name = name self.attrs = {} self.ops = [] @@ -118,7 +126,7 @@ def __init__(self, name, db=DB): class DummySimplex(DummyNode): """A generic simplex node""" - def __init__(self, name, parent, db=DB): + def __init__(self, name, parent, db: DummyScene = DB) -> None: super().__init__(name, db) self.definition = "" parent.ops.append(self) @@ -127,7 +135,7 @@ def __init__(self, name, parent, db=DB): class DummyFalloff(DummyNode): """A generic simplex node""" - def __init__(self, name, parent, db=DB): + def __init__(self, name, parent: DummyScene, db=DB) -> None: super().__init__(name, db) self.weightmap = None @@ -135,7 +143,7 @@ def __init__(self, name, parent, db=DB): class DummyShape: """A generic named blendshape shape""" - def __init__(self, name, shapeNode): + def __init__(self, name, shapeNode) -> None: self.name = name self.value = 0.0 self.points = None @@ -146,7 +154,7 @@ def __init__(self, name, shapeNode): class DummyBlendshape(DummyNode): """A generic blendshape node""" - def __init__(self, name, parent, db=DB): + def __init__(self, name, parent, db: DummyScene = DB) -> None: # TODO: Just reuse the DummyAttr instead of making an equivalent super().__init__(name, db) self.shapes = {} @@ -156,7 +164,7 @@ def __init__(self, name, parent, db=DB): class DummyMesh(DummyNode): """A generic mesh""" - def __init__(self, name, db=DB): + def __init__(self, name, db=DB) -> None: super().__init__(name, db) self.importPath = "" self.faces = None @@ -169,7 +177,7 @@ def __init__(self, name, db=DB): class DCC: program = "dummy" - def __init__(self, simplex, stack=None): + def __init__(self, simplex, stack=None) -> None: self.simplex = simplex # the abstract representation of the setup self.name = simplex.name @@ -189,7 +197,7 @@ def __init__(self, simplex, stack=None): self._falloffs = {} # weightPerVert values self.sliderMul = self.simplex.sliderMul - def dummyLoad(self, other, pBar=None): + def dummyLoad(self, other, pBar=None) -> None: """Method to copy the information in a DCC to a DummyDCC Parameters @@ -231,7 +239,7 @@ def dummyLoad(self, other, pBar=None): fo.thing = DummyFalloff(fo.name, self.scene) fo.verts = restVerts - def preLoad(self, simp, simpDict, create=True, pBar=None): + def preLoad(self, simp, simpDict, create: bool = True, pBar=None) -> None: """Code to execute before loading a simplex system into a dcc Parameters @@ -252,7 +260,7 @@ def preLoad(self, simp, simpDict, create=True, pBar=None): """ return None - def postLoad(self, simp, preRet): + def postLoad(self, simp, preRet) -> None: """Code to execute after loading a simplex system into a dcc Parameters @@ -264,7 +272,7 @@ def postLoad(self, simp, preRet): """ pass - def checkForErrors(self, window): + def checkForErrors(self, window) -> None: """Check for any DCC specific errors Parameters @@ -276,7 +284,7 @@ def checkForErrors(self, window): # System IO @undoable - def loadNodes(self, simp, thing, create=True, pBar=None): + def loadNodes(self, simp, thing, create=True, pBar=None) -> None: """Load the nodes from a simplex system onto a thing Parameters @@ -320,7 +328,7 @@ def loadNodes(self, simp, thing, create=True, pBar=None): raise RuntimeError("Control object not found with creation turned off") self.ctrl = DummyNode(self.name, self.scene) - def loadConnections(self, simp, pBar=None): + def loadConnections(self, simp, pBar=None) -> None: """Load the connections that exist in a simplex system Parameters @@ -364,7 +372,7 @@ def getSliderThing(self, sliderName): @staticmethod @undoable - def buildRestAbc(abcMesh, name): + def buildRestAbc(abcMesh, name) -> DummyMesh: """Build the rest Alembic node in the dcc Parameters @@ -394,7 +402,9 @@ def buildRestAbc(abcMesh, name): @staticmethod @undoable - def buildRawTopology(name, points, faces, counts, uvs=None, uvFaces=None): + def buildRawTopology( + name, points, faces, counts, uvs=None, uvFaces=None + ) -> DummyMesh: """Build a mesh directly from raw numerical data""" # TODO: Move this guy out to the rest of the DCC's mesh = DummyMesh(name) # don't add it to a scene @@ -409,7 +419,7 @@ def buildRawTopology(name, points, faces, counts, uvs=None, uvFaces=None): return mesh @staticmethod - def vertCount(mesh): + def vertCount(mesh) -> int: """Get the vert count of the given DCC Object Parameters @@ -425,7 +435,7 @@ def vertCount(mesh): return len(mesh.verts) @undoable - def loadAbcPoses(self, abcMesh, js, pBar=None): + def loadAbcPoses(self, abcMesh, js, pBar=None) -> None: """Load the joints/skin from an alembic file onto an already-created system Parameters @@ -440,7 +450,7 @@ def loadAbcPoses(self, abcMesh, js, pBar=None): pass @undoable - def loadAbc(self, abcMesh, js, pBar=None): + def loadAbc(self, abcMesh, js, pBar=None) -> None: """Load the shapes from an alembic file onto an already-created system Parameters @@ -460,7 +470,7 @@ def loadAbc(self, abcMesh, js, pBar=None): dummyShape = self.shapeNode.shapes[name] dummyShape.points = ppos - def getAllShapeVertices(self, shapes, pBar=None): + def getAllShapeVertices(self, shapes, pBar=None) -> None: """Load all shape vertices into the simplex system for processing Parameters @@ -490,7 +500,7 @@ def getShapeVertices(self, shape): """ return shape.thing.points - def pushAllShapeVertices(self, shapes, pBar=None): + def pushAllShapeVertices(self, shapes, pBar=None) -> None: """Push the computed vertex positions for the given shapes back to the DCC Parameters @@ -504,7 +514,7 @@ def pushAllShapeVertices(self, shapes, pBar=None): for shape in shapes: self.pushShapeVertices(shape) - def pushShapeVertices(self, shape): + def pushShapeVertices(self, shape) -> None: """Push the computed vertex positions for the given shape back to the DCC Parameters ---------- @@ -513,7 +523,7 @@ def pushShapeVertices(self, shape): """ shape.thing.points = shape.verts - def loadMeshTopology(self): + def loadMeshTopology(self) -> None: """Load the mesh topology from the DCC into the simplex interface""" # Here in Dummy I either have the data already or I don't, So nothing to do pass @@ -546,7 +556,7 @@ def getMeshTopology(mesh, uvName=None): def exportAbc( self, dccMesh, abcMesh, js, world=False, ensureCorrect=False, pBar=None - ): + ) -> None: """Export a .smpx file Parameters @@ -603,7 +613,9 @@ def exportAbc( abcSample = OPolyMeshSchemaSample(verts, faces, counts) schema.set(abcSample) - def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): + def exportOtherAbc( + self, dccMesh, abcMesh, js, world: bool = False, pBar=None + ) -> None: """Export a .smpx file of a mesh other than self.mesh Parameters @@ -627,25 +639,25 @@ def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): dccMesh, abcMesh, js, world=world, ensureCorrect=False, pBar=pBar ) - def deleteObj(self, dccMesh, path): + def deleteObj(self, dccMesh, path) -> None: """Export a mesh to the given path""" pass - def exportMesh(self, dccMesh, path): + def exportMesh(self, dccMesh, path) -> None: """Export a mesh to the given path""" pass # Revision tracking - def getRevision(self): + def getRevision(self) -> int: """Get the simplex revision number""" return self._revision - def incrementRevision(self): + def incrementRevision(self) -> int: """Increment the revision number""" self._revision += 1 return self._revision - def setRevision(self, val): + def setRevision(self, val) -> None: """Manually set the revision numer Parameters @@ -657,7 +669,7 @@ def setRevision(self, val): # System level @undoable - def renameSystem(self, name): + def renameSystem(self, name) -> None: """Rename a simplex system Parameters @@ -676,7 +688,7 @@ def renameSystem(self, name): # dd.pop(oldName, None) @undoable - def deleteSystem(self): + def deleteSystem(self) -> None: """Delete the current system""" # for dd in (DB.nodes, DB.ops, DB.bss, DB.meshes): # if self.name in dd: @@ -687,7 +699,7 @@ def deleteSystem(self): # Shapes @undoable - def createShape(self, shape, live=False, offset=10): + def createShape(self, shape, live: bool = False, offset: int = 10) -> DummyShape: """Create a dcc shape Parameters @@ -704,28 +716,34 @@ def createShape(self, shape, live=False, offset=10): return newShape @undoable - def extractWithDeltaShape(self, shape, live=True, offset=10.0): + def extractWithDeltaShape( + self, shape, live: bool = True, offset: float = 10.0 + ) -> None: """Make a mesh representing a shape. Can be live or not. Also, make a shapenode that is the delta of the change being made """ pass @undoable - def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0): + def extractWithDeltaConnection( + self, shape, delta, value, live: bool = True, offset: float = 10.0 + ) -> None: """Extract a shape with a live partial delta added in. Useful for updating progressive shapes """ pass @undoable - def extractShape(self, shape, live=True, offset=10.0): + def extractShape(self, shape, live: bool = True, offset: float = 10.0) -> None: """Make a mesh representing a shape. Can be live or not. Can also store its starting shape and delta data """ pass @undoable - def connectShape(self, shape, mesh=None, live=False, delete=False): + def connectShape( + self, shape, mesh=None, live: bool = False, delete: bool = False + ) -> None: """Force a shape to match a mesh The "connect shape" button is: mesh=None, delete=True @@ -737,11 +755,11 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): pass @undoable - def extractPosedShape(self, shape): + def extractPosedShape(self, shape) -> None: pass @undoable - def zeroShape(self, shape): + def zeroShape(self, shape) -> None: """Set a shape back to rest Parameters @@ -752,7 +770,7 @@ def zeroShape(self, shape): shape.thing.points = self.getShapeVertices(self.simplex.restShape) @undoable - def deleteShape(self, toDelShape): + def deleteShape(self, toDelShape) -> None: """Delete a shape from the system Parameters @@ -762,7 +780,7 @@ def deleteShape(self, toDelShape): self.shapeNode.shapes.pop(toDelShape.name, None) @undoable - def renameShape(self, shape, name): + def renameShape(self, shape, name) -> None: """Rename a shape Parameters @@ -777,11 +795,11 @@ def renameShape(self, shape, name): self.shapeNode.shapes[name] = shape.thing @undoable - def convertShapeToCorrective(self, shape): + def convertShapeToCorrective(self, shape) -> None: pass # Falloffs - def createFalloff(self, falloff): + def createFalloff(self, falloff) -> None: """Create a per-vert falloff weightmap Parameters @@ -792,7 +810,7 @@ def createFalloff(self, falloff): fo = DummyFalloff(falloff.name, self.scene) fo.weights = np.zeros(len(self.mesh.verts)) - def duplicateFalloff(self, falloff, newFalloff): + def duplicateFalloff(self, falloff, newFalloff) -> None: """Create a new falloff from an already existing one Parameters @@ -805,7 +823,7 @@ def duplicateFalloff(self, falloff, newFalloff): fo = DummyFalloff(newFalloff.name, self.scene) fo.weights = copy.copy(falloff.thing.weights) - def deleteFalloff(self, falloff): + def deleteFalloff(self, falloff) -> None: """Delete a falloff object Parameters @@ -817,7 +835,7 @@ def deleteFalloff(self, falloff): def setFalloffData( self, falloff, splitType, axis, minVal, minHandle, maxHandle, maxVal, mapName - ): + ) -> None: """Set the data of a falloff object""" # TODO: set the per-vert falloffs pass # for eventual live splits @@ -834,7 +852,7 @@ def getFalloffThing(self, falloff): # Sliders @undoable - def createSlider(self, slider): + def createSlider(self, slider) -> DummyAttr: """Create a slider object Parameters @@ -845,7 +863,7 @@ def createSlider(self, slider): return DummyAttr(slider.name, 0.0, self.ctrl) @undoable - def renameSlider(self, slider, name): + def renameSlider(self, slider, name) -> None: """Rename a slider Parameters @@ -860,7 +878,7 @@ def renameSlider(self, slider, name): slider.thing.name = name @undoable - def setSliderRange(self, slider): + def setSliderRange(self, slider) -> None: """Set the min and max of a slider Parameters @@ -871,7 +889,7 @@ def setSliderRange(self, slider): pass @undoable - def deleteSlider(self, toDelSlider): + def deleteSlider(self, toDelSlider) -> None: """Delete a slider Parameters @@ -882,15 +900,15 @@ def deleteSlider(self, toDelSlider): self.ctrl.attrs.pop(toDelSlider.name, None) @undoable - def addProgFalloff(self, prog, falloff): + def addProgFalloff(self, prog, falloff) -> None: pass # for eventual live splits @undoable - def removeProgFalloff(self, prog, falloff): + def removeProgFalloff(self, prog, falloff) -> None: pass # for eventual live splits @undoable - def setSlidersWeights(self, sliders, weights): + def setSlidersWeights(self, sliders, weights) -> None: """Set the values for the given sliders Parameters @@ -904,7 +922,7 @@ def setSlidersWeights(self, sliders, weights): slider.thing.value = val @undoable - def setSliderWeight(self, slider, weight): + def setSliderWeight(self, slider, weight) -> None: """Set the value for a given slider Parameters @@ -917,33 +935,41 @@ def setSliderWeight(self, slider, weight): slider.thing.value = weight @undoable - def updateSlidersRange(self, sliders): + def updateSlidersRange(self, sliders) -> None: """Update the range of the given sliders""" pass @undoable - def extractTraversalShape(self, trav, shape, live=True, offset=10.0): + def extractTraversalShape( + self, trav, shape, live: bool = True, offset: float = 10.0 + ) -> None: """Extract a shape from a traversal progression""" pass @undoable - def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False): + def connectTraversalShape( + self, trav, shape, mesh=None, live: bool = True, delete: bool = False + ) -> None: """Connect a shape to a traversal progression""" pass # Combos @undoable - def extractComboShape(self, combo, shape, live=True, offset=10.0): + def extractComboShape( + self, combo, shape, live: bool = True, offset: float = 10.0 + ) -> None: """Extract a shape from a combo progression""" pass @undoable - def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): + def connectComboShape( + self, combo, shape, mesh=None, live: bool = True, delete: bool = False + ) -> None: """Connect a shape to a combo progression""" pass @staticmethod - def setDisabled(op): + def setDisabled(op) -> None: """Disable the output of any simplex systems Parameters @@ -958,7 +984,7 @@ def setDisabled(op): return None @staticmethod - def reEnable(helpers): + def reEnable(helpers) -> None: """Re-enable a simplex system Parameters @@ -969,7 +995,7 @@ def reEnable(helpers): pass @undoable - def renameCombo(self, combo, name): + def renameCombo(self, combo, name) -> None: """Set the name of a Combo Parameters @@ -1004,7 +1030,7 @@ def getSimplexOperatorsByName(name): return DB.ops.get(name) @staticmethod - def getSimplexOperatorsOnObject(thing): + def getSimplexOperatorsOnObject(thing) -> list[DummySimplex]: """Get all simplex operators controlling an object Parameters @@ -1057,7 +1083,7 @@ def getSimplexStringOnThing(thing, systemName): return None @staticmethod - def setSimplexString(op, val): + def setSimplexString(op, val) -> None: """Set the definition string on an object Parameters @@ -1070,11 +1096,11 @@ def setSimplexString(op, val): op.definition = val @staticmethod - def selectObject(thing): + def selectObject(thing) -> None: """Select an object in the DCC""" pass - def selectCtrl(self): + def selectCtrl(self) -> None: """Select the system's control object""" pass @@ -1113,20 +1139,20 @@ def getObjectName(thing): return thing.name @staticmethod - def staticUndoOpen(): + def staticUndoOpen() -> None: """Open an undo chunk without knowledge of Simplex""" pass @staticmethod - def staticUndoClose(): + def staticUndoClose() -> None: """Close an undo chunk without knowledge of Simplex""" pass - def undoOpen(self): + def undoOpen(self) -> None: """Open an undo chunk with knowledge of Simplex""" pass - def undoClose(self): + def undoClose(self) -> None: """Close an undo chunk with knowledge of Simplex""" pass @@ -1227,7 +1253,7 @@ def loadPersistentSlider(cls, thing): return cls.getObjectByName(thing) @staticmethod - def getSelectedObjects(): + def getSelectedObjects() -> list[DummyNode]: """Get the selected objects""" # Here in the dummy interface, we short-circuit this # And return a default selected object called "thing" @@ -1240,10 +1266,10 @@ def getFreezeThing(self, combo): class SliderDispatch(QtCore.QObject): valueChanged = Signal() - def __init__(self, node, parent=None): + def __init__(self, node, parent=None) -> None: super().__init__(parent) - def emitValueChanged(self, *args, **kwargs): + def emitValueChanged(self, *args, **kwargs) -> None: self.valueChanged.emit() @@ -1255,36 +1281,36 @@ class Dispatch(QtCore.QObject): undo = Signal() redo = Signal() - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) - def connectCallbacks(self): + def connectCallbacks(self) -> None: pass - def disconnectCallbacks(self): + def disconnectCallbacks(self) -> None: pass - def emitBeforeNew(self, *args, **kwargs): + def emitBeforeNew(self, *args, **kwargs) -> None: self.beforeNew.emit() - def emitAfterNew(self, *args, **kwargs): + def emitAfterNew(self, *args, **kwargs) -> None: self.afterNew.emit() - def emitBeforeOpen(self, *args, **kwargs): + def emitBeforeOpen(self, *args, **kwargs) -> None: self.beforeOpen.emit() - def emitAfterOpen(self, *args, **kwargs): + def emitAfterOpen(self, *args, **kwargs) -> None: self.afterOpen.emit() - def emitUndo(self, *args, **kwargs): + def emitUndo(self, *args, **kwargs) -> None: self.undo.emit() - def emitRedo(self, *args, **kwargs): + def emitRedo(self, *args, **kwargs) -> None: self.redo.emit() DISPATCH = Dispatch() -def rootWindow(): +def rootWindow() -> None: return None diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 8edc0373..3d22c082 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -47,12 +47,12 @@ mayaToNumpy = None if TYPE_CHECKING: - from ..items.simplex import Simplex + from ..items.simplex import Simplex, Shape # UNDO STACK INTEGRATION @contextmanager -def undoContext(inst=None): +def undoContext(inst: DCC | None = None): if inst is None: DCC.staticUndoOpen() else: @@ -103,7 +103,7 @@ def doDisconnect(targets, testCnxType=("double", "float")): return cnxs -def doReconnect(cnxs): +def doReconnect(cnxs) -> None: for tdict in cnxs.values(): for s, d in tdict.items(): if not cmds.isConnected(s, d): @@ -130,7 +130,7 @@ def split_trailing_digits(s: str) -> tuple[str, str]: class DCC: program = "maya" - def __init__(self, simplex, stack=None): + def __init__(self, simplex, stack=None) -> None: if not cmds.pluginInfo("simplex_maya", query=True, loaded=True): cmds.loadPlugin("simplex_maya") self.undoDepth: int = 0 @@ -191,7 +191,7 @@ def _checkAllShapeValidity(self, shapeNames): return missingNames, len(attrs) @classmethod - def _removeExtraShapeNodes(cls, tfm): + def _removeExtraShapeNodes(cls, tfm) -> None: shapeNodes = cmds.listRelatives(tfm, shapes=True, noIntermediate=True) if len(shapeNodes) > 1: keeper = None @@ -205,7 +205,7 @@ def _removeExtraShapeNodes(cls, tfm): if keeper is not None: cmds.delete(todel) - def preLoad(self, simp, simpDict, create=True, pBar=None): + def preLoad(self, simp, simpDict, create: bool = True, pBar=None) -> None: cmds.undoInfo(state=False) try: if pBar is not None: @@ -273,10 +273,10 @@ def preLoad(self, simp, simpDict, create=True, pBar=None): cmds.undoInfo(state=True) raise - def postLoad(self, simp, preRet): + def postLoad(self, simp, preRet) -> None: cmds.undoInfo(state=True) - def checkForErrors(self, window): + def checkForErrors(self, window) -> None: """Check for any DCC specific errors Parameters @@ -293,7 +293,7 @@ def checkForErrors(self, window): ) QMessageBox.warning(window, "Multiple Shape Nodes", "\n".join(msg)) - def _getOpNodes(self, thing: str): + def _getOpNodes(self, thing: str) -> list[str]: hist: list[str] = cmds.listHistory(thing) rawShapeNodes = cmds.ls(hist, type="blendShape") rawPoseNodes = cmds.ls(hist, type="blendPose") @@ -327,7 +327,7 @@ def _getOpNodes(self, thing: str): ) return ops - def _getShapeNodes(self, ops): + def _getShapeNodes(self, ops: list[str]): shapeNodes = [] for op in ops: try: @@ -343,7 +343,7 @@ def _getShapeNodes(self, ops): shapeNodes.append(sn[0]) return shapeNodes - def _getPoseNodes(self, ops): + def _getPoseNodes(self, ops: list[str]): poseNodes = [] for op in ops: try: @@ -359,7 +359,7 @@ def _getPoseNodes(self, ops): poseNodes.append(sn[0]) return poseNodes - def _getCtrlNodes(self, ops): + def _getCtrlNodes(self, ops: list[str]): ctrlCnx = [] for op in ops: ccnx = cmds.listConnections( @@ -417,7 +417,7 @@ def _createControlNode(self, name, op): # System IO @undoable - def loadNodes(self, simp, thing, create=True, pBar=None): + def loadNodes(self, simp, thing, create: bool = True, pBar=None) -> None: """Create a new system based on the simplex tree Build any DCC objects that are missing if create=True Raises a runtime error if missing objects are found and @@ -502,15 +502,15 @@ def buildRestAbc(cls, abcMesh, name): return importHead @classmethod - def vertCount(cls, mesh): + def vertCount(cls, mesh: str): return cmds.polyEvaluate(mesh, vertex=True) @undoable - def loadAbcPoses(self, abcMesh, js, pBar=None): + def loadAbcPoses(self, abcMesh, js, pBar=None) -> None: pass @undoable - def loadAbc(self, abcMesh, js, pBar=None): + def loadAbc(self, abcMesh, js, pBar=None) -> None: # UGH, I *REALLY* hate that this is faster # But if I want to be "pure" about it, I should just bite the bullet # and do the direct alembic manipulation in C++ @@ -596,7 +596,7 @@ def loadAbc(self, abcMesh, js, pBar=None): cmds.delete(abcNode) cmds.delete(importHead) - def getAllShapeVertices(self, shapes, pBar=None): + def getAllShapeVertices(self, shapes, pBar=None) -> None: sl = om.MSelectionList() sl.add(self.mesh) thing = om.MDagPath() @@ -659,13 +659,13 @@ def getShapeVertices(self, shape): out = self.getNumpyShape(self.mesh) return out - def pushAllShapeVertices(self, shapes, pBar=None): + def pushAllShapeVertices(self, shapes, pBar=None) -> None: # take all the verts stored on the shapes # and push them back to the DCC for shape in shapes: self.pushShapeVertices(shape) - def pushShapeVertices(self, shape): + def pushShapeVertices(self, shape) -> None: # Push the vertices for a specific shape back to the DCC pass @@ -743,11 +743,11 @@ def getMeshTopology(cls, mesh, uvName=None): return verts, faces, counts, uvs, uvFaces - def loadMeshTopology(self): + def loadMeshTopology(self) -> None: self._faces, self._counts, self._uvs = self.getAbcFaces(self.mesh) @classmethod - def getNumpyShape(cls, mesh, world=False): + def getNumpyShape(cls, mesh: str, world=False): """Get the np.array shape of the mesh connected to the smpx Parameters @@ -769,7 +769,7 @@ def getNumpyShape(cls, mesh, world=False): return ret[..., :3].copy() @classmethod - def _getMeshVertices(cls, mesh, world=False): + def _getMeshVertices(cls, mesh: str, world=False): """ """ # Get the MDagPath from the name of the mesh sl = om.MSelectionList() @@ -798,7 +798,7 @@ def _exportAbcVertices(cls, mesh, world=False): return vertices @classmethod - def getAbcFaces(cls, mesh): + def getAbcFaces(cls, mesh: str): # Get the MDagPath from the name of the mesh sl = om.MSelectionList() sl.add(mesh) @@ -860,8 +860,14 @@ def getAbcFaces(cls, mesh): return abcFaceIndices, abcFaceCounts, uv def exportAbc( - self, dccMesh, abcMesh, js, world=False, ensureCorrect=False, pBar=None - ): + self, + dccMesh, + abcMesh, + js, + world: bool = False, + ensureCorrect: bool = False, + pBar=None, + ) -> None: # export the data to alembic if dccMesh is None: dccMesh = self.mesh @@ -913,7 +919,9 @@ def exportAbc( if ensureCorrect: cmds.setAttr(self.shapeNode + ".envelope", envelope) - def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): + def exportOtherAbc( + self, dccMesh, abcMesh, js, world: bool = False, pBar=None + ) -> None: shapeNames = js["shapes"] if js["encodingVersion"] > 1: shapeNames = [i["name"] for i in shapeNames] @@ -1009,11 +1017,11 @@ def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): abcSample = OPolyMeshSchemaSample(shpVerts, faces, counts) schema.set(abcSample) - def deleteObj(self, thing): + def deleteObj(self, thing) -> None: """Delete the given object""" cmds.delete(thing) - def exportMesh(self, mesh, path): + def exportMesh(self, mesh, path) -> None: """Export a mesh to the given path""" faces, counts, uvs = self.getAbcFaces(mesh) shape = self.getNumpyShape(mesh) @@ -1039,12 +1047,12 @@ def incrementRevision(self): return value + 1 @undoable - def setRevision(self, val): + def setRevision(self, val) -> None: cmds.setAttr(f"{self.op}.revision", val) # System level @undoable - def renameSystem(self, name): + def renameSystem(self, name) -> None: if ( self.mesh is None or self.ctrl is None @@ -1073,7 +1081,7 @@ def renameSystem(self, name): self.name = name @undoable - def deleteSystem(self): + def deleteSystem(self) -> None: cmds.delete(self.ctrl) cmds.delete(self.shapeNode) cmds.delete(self.op) @@ -1084,7 +1092,7 @@ def deleteSystem(self): # Shapes @undoable - def createShape(self, shape, live=False, offset=10): + def createShape(self, shape, live: bool = False, offset: int = 10): with disconnected(self.shapeNode): try: attrs = cmds.listAttr(f"{self.shapeNode}.weight[*]") @@ -1115,7 +1123,7 @@ def createShape(self, shape, live=False, offset=10): return thing - def _firstAvailableIndex(self): + def _firstAvailableIndex(self) -> int: aliases = cmds.aliasAttr(self.shapeNode, query=True) idxs = set() if not aliases: @@ -1132,7 +1140,7 @@ def _firstAvailableIndex(self): # there should be no way to get here, but just in case: return len(idxs) + 1 - def getShapeIndex(self, shape): + def getShapeIndex(self, shape: Shape) -> int: aName = cmds.attributeName(shape.thing) aliases = cmds.aliasAttr(self.shapeNode, query=True) idx = aliases.index(aName) @@ -1143,7 +1151,7 @@ def getShapeIndex(self, shape): return int(matches[-1].strip("[]")) @undoable - def extractWithDeltaShape(self, shape, live=True, offset=10.0): + def extractWithDeltaShape(self, shape, live: bool = True, offset: float = 10.0): """Make a mesh representing a shape. Can be live or not. Also, make a shapenode that is the delta of the change being made """ @@ -1188,7 +1196,9 @@ def extractWithDeltaShape(self, shape, live=True, offset=10.0): return extracted, repDict["Delta"] @undoable - def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0): + def extractWithDeltaConnection( + self, shape, delta, value, live: bool = True, offset: float = 10.0 + ): """Extract a shape with a live partial delta added in. Useful for updating progressive shapes """ @@ -1243,7 +1253,7 @@ def extractWithDeltaConnection(self, shape, delta, value, live=True, offset=10.0 return extracted @undoable - def extractShape(self, shape, live=True, offset=10.0): + def extractShape(self, shape, live: bool = True, offset: float = 10.0): """Make a mesh representing a shape. Can be live or not. Can also store its starting shape and delta data """ @@ -1261,7 +1271,7 @@ def extractShape(self, shape, live=True, offset=10.0): return extracted @undoable - def connectShape(self, shape, mesh=None, live=False, delete=False): + def connectShape(self, shape, mesh=None, live=False, delete=False) -> None: """Force a shape to match a mesh The "connect shape" button is: mesh=None, delete=True @@ -1299,11 +1309,11 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): cmds.delete(mesh) @undoable - def extractPosedShape(self, shape): + def extractPosedShape(self, shape) -> None: pass @undoable - def zeroShape(self, shape): + def zeroShape(self, shape) -> None: """Set the shape to be completely zeroed""" index = self.getShapeIndex(shape) tgn = f"{self.shapeNode}.inputTarget[0].inputTargetGroup[{index}]" @@ -1312,7 +1322,7 @@ def zeroShape(self, shape): cmds.setAttr(f"{shapeInput}.inputComponentsTarget", 0, "", type="componentList") @undoable - def deleteShape(self, toDelShape): + def deleteShape(self, toDelShape) -> None: """Remove a shape from the system""" index = self.getShapeIndex(toDelShape) tgn = f"{self.shapeNode}.inputTarget[0].inputTargetGroup[{index}]" @@ -1321,7 +1331,7 @@ def deleteShape(self, toDelShape): cmds.aliasAttr(toDelShape.thing, remove=True) self._rebuildShapeConnections() - def _rebuildShapeConnections(self): + def _rebuildShapeConnections(self) -> None: # Rebuild the shape connections in the proper order cnxs = ( cmds.listConnections( @@ -1337,32 +1347,32 @@ def _rebuildShapeConnections(self): cmds.connectAttr(f"{self.op}.weights[{i}]", shape.thing, force=True) @undoable - def forceRebuildShapeConnections(self): + def forceRebuildShapeConnections(self) -> None: self._rebuildShapeConnections() @undoable - def renameShape(self, shape, name): + def renameShape(self, shape, name) -> None: """Change the name of the shape""" cmds.aliasAttr(name, shape.thing) shape.thing = f"{self.shapeNode}.{name}" @undoable - def convertShapeToCorrective(self, shape): + def convertShapeToCorrective(self, shape) -> None: pass # Falloffs - def createFalloff(self, name): + def createFalloff(self, name) -> None: pass # for eventual live splits - def duplicateFalloff(self, falloff, newFalloff, newName): + def duplicateFalloff(self, falloff, newFalloff, newName) -> None: pass # for eventual live splits - def deleteFalloff(self, falloff): + def deleteFalloff(self, falloff) -> None: pass # for eventual live splits def setFalloffData( self, falloff, splitType, axis, minVal, minHandle, maxHandle, maxVal, mapName - ): + ) -> None: pass # for eventual live splits def getFalloffThing(self, falloff): @@ -1371,7 +1381,7 @@ def getFalloffThing(self, falloff): # Sliders @undoable - def createSlider(self, slider): + def createSlider(self, slider) -> str: index = slider.simplex.sliders.index(slider) cmds.addAttr( self.ctrl, @@ -1386,7 +1396,7 @@ def createSlider(self, slider): return thing @undoable - def renameSlider(self, slider, name): + def renameSlider(self, slider, name) -> None: """Set the name of a slider""" vals = [v.value for v in slider.prog.pairs] cnx = cmds.listConnections( @@ -1407,7 +1417,7 @@ def renameSlider(self, slider, name): cmds.connectAttr(newThing, c) @undoable - def setSliderRange(self, slider): + def setSliderRange(self, slider) -> None: """Set the range of a slider""" vals = [v.value for v in slider.prog.pairs] attrName = f"{self.ctrl}.{slider.name}" @@ -1419,7 +1429,7 @@ def setSliderRange(self, slider): ) @undoable - def deleteSlider(self, toDelSlider): + def deleteSlider(self, toDelSlider) -> None: cmds.deleteAttr(toDelSlider.thing) # Rebuild the slider connections in the proper order @@ -1435,15 +1445,15 @@ def deleteSlider(self, toDelSlider): cmds.connectAttr(slider.thing, f"{self.op}.sliders[{i}]") @undoable - def addProgFalloff(self, prog, falloff): + def addProgFalloff(self, prog, falloff) -> None: pass # for eventual live splits @undoable - def removeProgFalloff(self, prog, falloff): + def removeProgFalloff(self, prog, falloff) -> None: pass # for eventual live splits @undoable - def setSlidersWeights(self, sliders, weights): + def setSlidersWeights(self, sliders, weights) -> None: """Set the weight of a slider. This does not change the definition""" for slider, weight in zip(sliders, weights): try: @@ -1453,7 +1463,7 @@ def setSlidersWeights(self, sliders, weights): pass @undoable - def setSliderWeight(self, slider, weight): + def setSliderWeight(self, slider, weight) -> None: try: cmds.setAttr(slider.thing, weight) except RuntimeError: @@ -1461,7 +1471,7 @@ def setSliderWeight(self, slider, weight): pass @undoable - def updateSlidersRange(self, sliders): + def updateSlidersRange(self, sliders) -> None: for slider in sliders: vals = [v.value for v in slider.prog.pairs] cmds.addAttr( @@ -1488,7 +1498,7 @@ def _doesDeltaExist(self, combo, target): return None return par + "|" + dshape - def _clearShapes(self, item, doOrig=False): + def _clearShapes(self, item, doOrig=False) -> None: aname = cmds.ls(item, long=1)[0] shapes = cmds.ls(cmds.listRelatives(item, shapes=1), long=1) baseName = aname.split("|")[-1] @@ -1519,10 +1529,10 @@ def _clearShapes(self, item, doOrig=False): cmds.delete(origs[1:]) @undoable - def forceRebuildSliderConnections(self): + def forceRebuildSliderConnections(self) -> None: self._rebuildSliderConnections() - def _rebuildSliderConnections(self): + def _rebuildSliderConnections(self) -> None: # disconnect all outputs from the ctrl rcnx = cmds.listConnections( self.ctrl, source=False, plugs=True, connections=True @@ -1660,7 +1670,9 @@ def _createTravDelta(self, trav, target, tVal, doReparent=True): return deltaObj @undoable - def extractTraversalShape(self, trav, shape, live=True, offset=10.0): + def extractTraversalShape( + self, trav, shape, live: bool = True, offset: float = 10.0 + ): """Extract a shape from a Traversal progression""" floatShapes = self.simplex.getFloatingShapes() floatShapes = [i.thing for i in floatShapes] @@ -1702,7 +1714,9 @@ def extractTraversalShape(self, trav, shape, live=True, offset=10.0): return extracted @undoable - def connectTraversalShape(self, trav, shape, mesh=None, live=True, delete=False): + def connectTraversalShape( + self, trav, shape, mesh=None, live=True, delete=False + ) -> None: """Connect a shape into a Traversal progression""" if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) @@ -1790,7 +1804,7 @@ def _createComboDelta(self, combo, target, tVal, doReparent=True): return deltaObj @undoable - def extractComboShape(self, combo, shape, live=True, offset=10.0): + def extractComboShape(self, combo, shape, live: bool = True, offset: float = 10.0): """Extract a shape from a combo progression""" floatShapes = self.simplex.getFloatingShapes() floatShapes = [i.thing for i in floatShapes] @@ -1821,7 +1835,9 @@ def extractComboShape(self, combo, shape, live=True, offset=10.0): return extracted @undoable - def connectComboShape(self, combo, shape, mesh=None, live=True, delete=False): + def connectComboShape( + self, combo, shape, mesh=None, live=True, delete=False + ) -> None: """Connect a shape into a combo progression""" if mesh is None: attrName = cmds.attributeName(shape.thing, long=True) @@ -1859,12 +1875,12 @@ def setDisabled(cls, op): return helpers @classmethod - def reEnable(cls, helpers): + def reEnable(cls, helpers) -> None: for prop, val in helpers: cmds.setAttr(prop, val) @undoable - def renameCombo(self, combo, name): + def renameCombo(self, combo, name) -> None: """Set the name of a Combo""" pass @@ -1911,15 +1927,15 @@ def getSimplexStringOnThing(cls, thing, systemName): return None @classmethod - def setSimplexString(cls, op, val): + def setSimplexString(cls, op: str, val: str): return cmds.setAttr(op + ".definition", val, type="string") @classmethod - def selectObject(cls, thing): + def selectObject(cls, thing: str) -> None: """Select an object in the DCC""" cmds.select([thing]) - def selectCtrl(self): + def selectCtrl(self) -> None: """Select the system's control object""" if self.ctrl: self.selectObject(self.ctrl) @@ -1938,19 +1954,19 @@ def getObjectName(cls, thing): return thing @classmethod - def staticUndoOpen(cls): + def staticUndoOpen(cls) -> None: cmds.undoInfo(chunkName="SimplexOperation", openChunk=True) @classmethod - def staticUndoClose(cls): + def staticUndoClose(cls) -> None: cmds.undoInfo(closeChunk=True) - def undoOpen(self): + def undoOpen(self) -> None: if self.undoDepth == 0: self.staticUndoOpen() self.undoDepth += 1 - def undoClose(self): + def undoClose(self) -> None: self.undoDepth -= 1 if self.undoDepth == 0: self.staticUndoClose() @@ -2022,7 +2038,7 @@ def _getDeformerChain(cls, chkObj): return memo # Freezing stuff - def primeShapes(self, combo): + def primeShapes(self, combo) -> None: """Make sure the upstream shapes of this combo are primed and ready. Priming here means the deltas are stored and available on the blendshape node """ @@ -2095,21 +2111,21 @@ def getFreezeThing(self, combo): class SliderDispatch(QtCore.QObject): valueChanged = Signal() - def __init__(self, node, parent=None): + def __init__(self, node, parent=None) -> None: super().__init__(parent) mObject = getMObject(node) self.callbackID = om.MNodeMessage.addAttributeChangedCallback( mObject, self.emitValueChanged ) - def emitValueChanged(self, *args, **kwargs): + def emitValueChanged(self, *args, **kwargs) -> None: self.valueChanged.emit() - def disconnectCallbacks(self): + def disconnectCallbacks(self) -> None: om.MMessage.removeCallback(self.callbackID) self.callbackID = None - def __del__(self): + def __del__(self) -> None: self.disconnectCallbacks() @@ -2121,12 +2137,12 @@ class Dispatch(QtCore.QObject): undo = Signal() redo = Signal() - def __init__(self, parent=None): + def __init__(self, parent=None) -> None: super().__init__(parent) self.callbackIDs = [] self.connectCallbacks() - def connectCallbacks(self): + def connectCallbacks(self) -> None: if self.callbackIDs: self.disconnectCallbacks() @@ -2155,30 +2171,30 @@ def connectCallbacks(self): om.MEventMessage.addEventCallback("Redo", self.emitRedo) ) - def disconnectCallbacks(self): + def disconnectCallbacks(self) -> None: for i in self.callbackIDs: om.MMessage.removeCallback(i) self.callbackIDs = [] - def emitBeforeNew(self, *args, **kwargs): + def emitBeforeNew(self, *args, **kwargs) -> None: self.beforeNew.emit() - def emitAfterNew(self, *args, **kwargs): + def emitAfterNew(self, *args, **kwargs) -> None: self.afterNew.emit() - def emitBeforeOpen(self, *args, **kwargs): + def emitBeforeOpen(self, *args, **kwargs) -> None: self.beforeOpen.emit() - def emitAfterOpen(self, *args, **kwargs): + def emitAfterOpen(self, *args, **kwargs) -> None: self.afterOpen.emit() - def emitUndo(self, *args, **kwargs): + def emitUndo(self, *args, **kwargs) -> None: self.undo.emit() - def emitRedo(self, *args, **kwargs): + def emitRedo(self, *args, **kwargs) -> None: self.redo.emit() - def __del__(self): + def __del__(self) -> None: self.disconnectCallbacks() @@ -2264,7 +2280,7 @@ def getMObject(name): """ -def buildDeleterScriptJob(): +def buildDeleterScriptJob() -> None: dcbName = "SimplexDeleterCallback" if not cmds.ls(dcbName): cmds.scriptNode( @@ -2275,7 +2291,7 @@ def buildDeleterScriptJob(): ) -def simplexDelCB(_node, dgMod, clientData): +def simplexDelCB(_node, dgMod, clientData) -> None: _xNode, dName = clientData dNode = getMObject(dName) if dNode and not dNode.isNull(): diff --git a/src/python/simplexui/interfaceModel.py b/src/python/simplexui/interfaceModel.py index e2a35830..52c6c526 100644 --- a/src/python/simplexui/interfaceModel.py +++ b/src/python/simplexui/interfaceModel.py @@ -284,7 +284,7 @@ class BaseProxyModel(QSortFilterProxyModel): documentation will be lacking """ - def __init__(self, model: SimplexModel, parent: QWidget | None = None): + def __init__(self, model: SimplexModel, parent: QWidget | None = None) -> None: super().__init__(parent) self.setSourceModel(model) @@ -301,13 +301,13 @@ def itemFromIndex(self, index: QModelIndex) -> TreeItem | None: sIndex = self.mapToSource(index) return sourceModel.itemFromIndex(sIndex) - def invalidate(self): + def invalidate(self) -> None: source = self.sourceModel() if isinstance(source, QSortFilterProxyModel): source.invalidate() super().invalidate() - def invalidateFilter(self): + def invalidateFilter(self) -> None: source = self.sourceModel() if isinstance(source, QSortFilterProxyModel): source.invalidateFilter() @@ -356,7 +356,7 @@ class SimplexFilterModel(BaseProxyModel): Set the `filterString` object property to filter the model """ - def __init__(self, model: SimplexModel, parent: QWidget | None = None): + def __init__(self, model: SimplexModel, parent: QWidget | None = None) -> None: super().__init__(model, parent) self.setSourceModel(model) self.filterShapes: bool = True @@ -369,7 +369,7 @@ def filterString(self) -> str: return " ".join(self._filterString) @filterString.setter - def filterString(self, val: str): + def filterString(self, val: str) -> None: self._filterString = val.split() self._filterReg = [] @@ -426,7 +426,7 @@ def checkChildren(self, sourceItem: TreeItem) -> bool: class SliderFilterModel(SimplexFilterModel): """Hide single shapes under a slider""" - def __init__(self, model: SimplexModel, parent: QWidget | None = None): + def __init__(self, model: SimplexModel, parent: QWidget | None = None) -> None: super().__init__(model, parent) self.requires: list[Combo] = [] self.filterRequiresAny: bool = False @@ -465,7 +465,7 @@ def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: class ComboFilterModel(SimplexFilterModel): """Filter by slider when Show Dependent Combos is checked""" - def __init__(self, model: SimplexModel, parent: QWidget | None = None): + def __init__(self, model: SimplexModel, parent: QWidget | None = None) -> None: super().__init__(model, parent) self.requires: list[Slider] = [] self.filterRequiresAll: bool = False @@ -515,7 +515,7 @@ def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: class TraversalFilterModel(SimplexFilterModel): """Hide single shapes under a slider""" - def __init__(self, model: SimplexModel, parent: QWidget | None = None): + def __init__(self, model: SimplexModel, parent: QWidget | None = None) -> None: super().__init__(model, parent) self.doFilter = True @@ -539,7 +539,7 @@ def filterAcceptsRow(self, sourceRow: int, sourceParent: QModelIndex) -> bool: class FalloffDataModel(AdapterModel): """A model for displaying the data of Falloff objects""" - def __init__(self, simplex: Simplex, parent: QWidget | None): + def __init__(self, simplex: Simplex, parent: QWidget | None) -> None: super().__init__(simplex, parent) self.simplex = simplex @@ -566,7 +566,7 @@ def index( def parent(self, index: QModelIndex) -> QModelIndex: return QModelIndex() - def rowCount(self, parent): + def rowCount(self, parent) -> int: try: return len(self.simplex.falloffs) except AttributeError: diff --git a/src/python/simplexui/interfaceModelTrees.py b/src/python/simplexui/interfaceModelTrees.py index 6115419a..18c18656 100644 --- a/src/python/simplexui/interfaceModelTrees.py +++ b/src/python/simplexui/interfaceModelTrees.py @@ -21,10 +21,11 @@ from Qt.QtCore import ( QItemSelection, QItemSelectionModel, + QModelIndex, Qt, ) from Qt.QtGui import QValidator -from Qt.QtWidgets import QApplication, QLineEdit, QMenu, QStyledItemDelegate, QTreeView +from Qt.QtWidgets import QApplication, QLineEdit, QMenu, QStyledItemDelegate, QTreeView, QWidget from .dragFilter import DragFilter from .interfaceModel import SimplexFilterModel @@ -35,7 +36,7 @@ class NameValidator(QValidator): - def __init__(self, parent=None): + def __init__(self, parent: QLineEdit | None = None) -> None: super().__init__(parent) def validate(self, input_str: str, pos: int) -> tuple[QValidator.State, str, int]: @@ -49,7 +50,7 @@ def validate(self, input_str: str, pos: int) -> tuple[QValidator.State, str, int class SimplexNameDelegate(QStyledItemDelegate): """An QStyledItemDelegate subclass that implements a Regex validator""" - def createEditor(self, parent, option, index): + def createEditor(self, parent, option, index) -> QLineEdit: editor = QLineEdit(parent) validator = NameValidator(editor) editor.setValidator(validator) @@ -59,7 +60,7 @@ def createEditor(self, parent, option, index): class SimplexTree(QTreeView): """Abstract base tree displaying Simplex objects""" - def __init__(self, parent): + def __init__(self, parent: QWidget) -> None: super().__init__(parent) self.expandModifier = Qt.KeyboardModifier.ControlModifier @@ -84,13 +85,13 @@ def __init__(self, parent): self.setColumnWidth(1, 50) self.setColumnWidth(2, 20) - def setModel(self, model: SimplexFilterModel): + def setModel(self, model: SimplexFilterModel) -> None: super().setModel(model) def model(self) -> SimplexFilterModel: return cast(SimplexFilterModel, super().model()) - def setPlugins(self, plugins): + def setPlugins(self, plugins) -> None: """Set the right-click menu plugins for the tree Parameters @@ -100,7 +101,7 @@ def setPlugins(self, plugins): """ self._plugins = plugins - def unifySelection(self): + def unifySelection(self) -> None: """Handle selection across multiple Trees. The other tree's selectionChanged signal will be connected to this And it will clear the selection on this tree if no modifiers are being held @@ -117,7 +118,7 @@ def unifySelection(self): selModel.blockSignals(False) self.viewport().update() - def hideRedundant(self, check): + def hideRedundant(self, check) -> None: """Update the filter model to show/hide single shapes Parameters @@ -129,7 +130,7 @@ def hideRedundant(self, check): model.filterShapes = check model.invalidateFilter() - def stringFilter(self, filterString): + def stringFilter(self, filterString) -> None: """Update the filter model with a filter string Parameters @@ -141,7 +142,7 @@ def stringFilter(self, filterString): model.filterString = str(filterString) model.invalidateFilter() - def isolate(self, sliderNames): + def isolate(self, sliderNames) -> None: """Update the filter model with a whitelist of names Parameters @@ -153,18 +154,18 @@ def isolate(self, sliderNames): model.isolateList = sliderNames model.invalidateFilter() - def isolateSelected(self): + def isolateSelected(self) -> None: """Isolate the selected items""" items = self.getSelectedItems() isoList = [i.name for i in items] self.isolate(isoList) - def exitIsolate(self): + def exitIsolate(self) -> None: """Remove all items from isolation""" self.isolate([]) # Tree expansion/collapse code - def expandTree(self, index): + def expandTree(self, index) -> None: """Expand all items under index Parameters @@ -174,7 +175,7 @@ def expandTree(self, index): """ self.toggleTree(index, True) - def collapseTree(self, index): + def collapseTree(self, index) -> None: """Collapse all items under index Parameters @@ -184,7 +185,7 @@ def collapseTree(self, index): """ self.toggleTree(index, False) - def resizeColumns(self): + def resizeColumns(self) -> None: '''Resize all columns to their contents "smartly"''' model = self.model() for i in range(model.columnCount() - 1): @@ -194,7 +195,7 @@ def resizeColumns(self): self.setColumnWidth(i, max(oldcw, newcw, 30)) self.setColumnWidth(model.columnCount() - 1, 5) - def toggleTree(self, index, expand): + def toggleTree(self, index, expand: bool) -> None: """Recursively expand or collapse an entire sub-tree of an index. If certain modifiers are held, then only a partial sub-tree will be expanded @@ -235,7 +236,7 @@ def toggleTree(self, index, expand): if expand: self.resizeColumns() - def expandToItem(self, item): + def expandToItem(self, item) -> None: """Make sure that all parents leading to `item` are expanded Parameters @@ -247,7 +248,7 @@ def expandToItem(self, item): index = model.indexFromItem(item) self.expandToIndex(index) - def expandToIndex(self, index): + def expandToIndex(self, index: QModelIndex) -> None: """Make sure that all parents leading to `index` are expanded Parameters @@ -260,7 +261,7 @@ def expandToIndex(self, index): index = index.parent() self.resizeColumns() - def scrollToItem(self, item): + def scrollToItem(self, item) -> None: """Ensure that the item is scrolled to in the tree Parameters @@ -272,7 +273,7 @@ def scrollToItem(self, item): index = model.indexFromItem(item) self.scrollToIndex(index) - def scrollToIndex(self, index): + def scrollToIndex(self, index: QModelIndex) -> None: """Ensure that the index is scrolled to in the tree Parameters @@ -284,7 +285,7 @@ def scrollToIndex(self, index): self.expandToIndex(index) self.scrollTo(index) - def storeExpansion(self): + def storeExpansion(self) -> None: """Store the expansion state of the tree for the undo stack""" expanded_uids = set() model = self.model() @@ -294,7 +295,7 @@ def storeExpansion(self): expanded_uids.add(uid) self._expansions = expanded_uids - def setItemExpansion(self): + def setItemExpansion(self) -> None: """Load the stored expansions onto the tree""" model = self.model() for index in model.iterindices( @@ -302,7 +303,7 @@ def setItemExpansion(self): ): self.setExpanded(index, True) - def dragTick(self, ticks, mul): + def dragTick(self, ticks, mul) -> None: """Deal with the ticks coming from the drag handler Parameters @@ -322,12 +323,12 @@ def dragTick(self, ticks, mul): self.viewport().update() # Menus and Actions - def connectMenus(self): + def connectMenus(self) -> None: """Setup the QT signal/slot connections to the context menus""" self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self.openMenu) - def openMenu(self, pos): + def openMenu(self, pos) -> None: """Handle getting the data to show the context menu Parameters @@ -341,7 +342,7 @@ def openMenu(self, pos): return self.showContextMenu(clickIdx, selIdxs, self.viewport().mapToGlobal(pos)) - def showContextMenu(self, clickIdx, indexes, pos): + def showContextMenu(self, clickIdx: QModelIndex, indexes, pos) -> None: """Handle showing the context menu items from the plugins Parameters @@ -384,7 +385,7 @@ def getSelectedItems(self, typ=None): items = [i for i in items if isinstance(i, typ)] return items - def getSelectedIndexes(self, filtered=False): + def getSelectedIndexes(self, filtered: bool = False): """Get selected indexes for either the filtered or unfiltered models Parameters @@ -408,7 +409,7 @@ def getSelectedIndexes(self, filtered=False): indexes = [model.mapToSource(i) for i in selIdxs] return indexes - def setItemSelection(self, items): + def setItemSelection(self, items) -> None: """Set the selection based on a list of items Parameters diff --git a/src/python/simplexui/items/accessor.py b/src/python/simplexui/items/accessor.py index a67db192..1ffff98f 100644 --- a/src/python/simplexui/items/accessor.py +++ b/src/python/simplexui/items/accessor.py @@ -32,7 +32,7 @@ class SimplexAccessor: name getters/setters/unifiers, proper deepcopying, and abstract tree lookup """ - def __init__(self, simplex: Simplex): + def __init__(self, simplex: Simplex) -> None: self.simplex: Simplex = simplex self._name: str = "" self._splitApplied = set() @@ -139,7 +139,7 @@ def buildLinkedRename( class SimplexTreeAccessor(SimplexAccessor, TreeItem): - def __init__(self, simplex: Simplex): + def __init__(self, simplex: Simplex) -> None: # Explicitly SimplexAccessor.__init__(self, simplex) TreeItem.__init__(self, simplex) diff --git a/src/python/simplexui/items/combo.py b/src/python/simplexui/items/combo.py index c51edeed..9a0331be 100644 --- a/src/python/simplexui/items/combo.py +++ b/src/python/simplexui/items/combo.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from .group import Group - from .progression import Progression + from .progression import Progression, ProgPair from .shape import Shape from .simplex import Simplex from .slider import Slider @@ -37,7 +37,7 @@ class ComboPair(SimplexTreeAccessor, Draggable): classDepth: int = 6 - def __init__(self, slider: Slider, value: float): + def __init__(self, slider: Slider, value: float) -> None: super().__init__(slider.simplex) self.slider: Slider = slider self._value: float = float(value) @@ -53,7 +53,7 @@ def value(self) -> float: @value.setter @stackable - def value(self, val: float): + def value(self, val: float) -> None: self._value = val def buildDefinition(self, simpDict: dict, legacy: bool) -> tuple[int, float]: @@ -120,7 +120,7 @@ def __init__( prog: Progression, group: Group, solveType: str | None, - ): + ) -> None: super().__init__(simplex) if group.groupType is not type(self): @@ -150,7 +150,7 @@ def enabled(self) -> bool: @enabled.setter @stackable - def enabled(self, value: bool): + def enabled(self, value: bool) -> None: """Set whether this Combo is evaluated in the solver""" self._enabled = value @@ -167,7 +167,7 @@ def freezeThing(self) -> Any: return self._freezeThing @freezeThing.setter - def freezeThing(self, value: Any): + def freezeThing(self, value: Any) -> None: self._freezeThing = value @classmethod @@ -208,7 +208,7 @@ def createCombo( shape: Shape | None = None, solveType: str | None = None, tVal: float = 1.0, - ): + ) -> Combo: """Classmethod to create Combo with some hard-coded defaults Parameters @@ -318,33 +318,33 @@ def buildComboName(sliders, values): return "_".join(parts) @property - def name(self): + def name(self) -> str: """Get the name of a combo""" return self._name @name.setter @stackable - def name(self, value): + def name(self, value) -> None: """Set the name of a combo""" self._name = value self.prog.name = value self.DCC.renameCombo(self, value) @property - def solveType(self): + def solveType(self) -> str | None: """Get the solveType of the combo""" return self._solveType @solveType.setter @stackable - def solveType(self, newType: str): + def solveType(self, newType: str) -> None: """Set the solveType of the combo""" stNames, stVals = list(zip(*self.solveTypes)) if newType not in stVals: raise ValueError(f"Solve Type {newType} not in allowed types {stVals}") self._solveType = newType - def sliderNameLinks(self): + def sliderNameLinks(self) -> list[bool]: """ """ sliNames = [f"_{i.slider.name}_" for i in self.pairs] surr = f"_{self.name}_" @@ -373,20 +373,20 @@ def nameLinks(self): shapeNames.append(x) return [i == self.name for i in shapeNames] - def getSliderIndex(self, slider): + def getSliderIndex(self, slider) -> int: for i, p in enumerate(self.pairs): if p.slider == slider: return i raise ValueError(f"Provided slider:{slider.name} is not in the list") - def isFloating(self): + def isFloating(self) -> bool: """Floating combos are combos that Slider values that are between 0 and 1""" for pair in self.pairs: if abs(pair.value) != 1.0: return True return False - def getSliders(self): + def getSliders(self) -> list[Slider]: """ """ return [i.slider for i in self.pairs] @@ -445,7 +445,7 @@ def buildDefinition(self, simpDict, legacy): simpDict.setdefault("combos", []).append(x) return self._buildIdx - def clearBuildIndex(self): + def clearBuildIndex(self) -> None: """Clear the build index of this object The buildIndex is stored when building a definition dictionary @@ -455,7 +455,7 @@ def clearBuildIndex(self): self.prog.clearBuildIndex() self.group.clearBuildIndex() - def extractShape(self, shape, live=True, offset=10.0): + def extractShape(self, shape, live: bool = True, offset: float = 10.0): """Extract a shape from a combo progression Parameters @@ -474,7 +474,9 @@ def extractShape(self, shape, live=True, offset=10.0): """ return self.DCC.extractComboShape(self, shape, live, offset) - def connectShape(self, shape, mesh=None, live=False, delete=False): + def connectShape( + self, shape, mesh=None, live: bool = False, delete: bool = False + ) -> None: """Connect a shape into a combo progression Parameters @@ -492,7 +494,7 @@ def connectShape(self, shape, mesh=None, live=False, delete=False): self.DCC.connectComboShape(self, shape, mesh, live, delete) @stackable - def delete(self): + def delete(self) -> None: """Delete this combo and any shapes it contains""" if self not in self.group.items: return # Can happen when deleting multiple groups @@ -508,7 +510,7 @@ def delete(self): self.DCC.deleteShape(pp.shape) @stackable - def setInterpolation(self, interp): + def setInterpolation(self, interp) -> None: """Set the interpolation of a combo Parameters @@ -519,7 +521,7 @@ def setInterpolation(self, interp): self.prog.interp = interp @stackable - def setComboValue(self, slider, value): + def setComboValue(self, slider, value) -> None: """Set the Slider/value pairs for a combo Parameters @@ -534,7 +536,7 @@ def setComboValue(self, slider, value): pair.value = value @stackable - def appendComboValue(self, slider, value): + def appendComboValue(self, slider, value) -> None: """Append a Slider/value pair for a combo Parameters @@ -550,7 +552,7 @@ def appendComboValue(self, slider, value): cp.combo = self @stackable - def deleteComboPair(self, comboPair): + def deleteComboPair(self, comboPair) -> None: """Delete a Slider/value pair for a combo Parameters @@ -566,7 +568,7 @@ def deleteComboPair(self, comboPair): comboPair.combo = None @stackable - def setGroup(self, grp): + def setGroup(self, grp) -> None: """Set the group for this Combo Parameters @@ -589,7 +591,7 @@ def setGroup(self, grp): self.group = grp @stackable - def createShape(self, shapeName=None, tVal=None): + def createShape(self, shapeName=None, tVal=None) -> ProgPair: """Create a shape and add it to a progression Parameters @@ -607,7 +609,7 @@ def createShape(self, shapeName=None, tVal=None): self.prog.pairs.insert(idx, pp) return pp - def getInputVector(self): + def getInputVector(self) -> list[float]: """Get the input to the Solver that would fully activate this Combo Returns diff --git a/src/python/simplexui/items/dragItem.py b/src/python/simplexui/items/dragItem.py index 369f7f38..6c585018 100644 --- a/src/python/simplexui/items/dragItem.py +++ b/src/python/simplexui/items/dragItem.py @@ -27,7 +27,7 @@ class Draggable: maxValue: float = 1.0 minValue: float = 0.0 - def valueTick(self, ticks: int, mul: float): + def valueTick(self, ticks: int, mul: float) -> None: """Change the value of the current object by some number of ticks with some given multiplier. This is the interface for the MMB drag diff --git a/src/python/simplexui/items/falloff.py b/src/python/simplexui/items/falloff.py index 4e0b4ac8..abfe24f0 100644 --- a/src/python/simplexui/items/falloff.py +++ b/src/python/simplexui/items/falloff.py @@ -130,8 +130,9 @@ class Falloff(SimplexAccessor): The data used to build this falloff. You should use one of the classmethod like Falloff.createPlanar or Falloff.createMap instead """ + splitType = None - def __init__(self, name: str, simplex: Simplex, axis: str): + def __init__(self, name: str, simplex: Simplex, axis: str) -> None: super().__init__(simplex) with self.stack.store(self): self._search: str | None = None @@ -154,7 +155,7 @@ def name(self) -> str: @name.setter @stackable - def name(self, value: str): + def name(self, value: str) -> None: """Set the name of a Falloff""" self._name = value @@ -233,7 +234,7 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: """ raise NotImplementedError("Can't build definition on an untyped falloff") - def clearBuildIndex(self): + def clearBuildIndex(self) -> None: """Clear the build index of this object The buildIndex is stored when building a definition dictionary @@ -264,7 +265,7 @@ def duplicate(self, newName: str) -> Falloff: return nf @stackable - def delete(self): + def delete(self) -> None: """Delete the Falloff""" fIdx = self.simplex.falloffs.index(self) for child in self.children: @@ -279,7 +280,7 @@ def axis(self) -> str: @axis.setter @stackable - def axis(self, value: str): + def axis(self, value: str) -> None: self._axis = value # TODO: Does this need to update the dcc?? @@ -289,7 +290,7 @@ def verts(self) -> npt.NDArray | None: return self._verts @verts.setter - def verts(self, vals: npt.NDArray): + def verts(self, vals: npt.NDArray) -> None: """Input the vertices into this falloff and compute the weights Parameters @@ -305,7 +306,7 @@ def weights(self) -> npt.NDArray | None: return self._weights @weights.setter - def weights(self, val: npt.ArrayLike): + def weights(self, val: npt.ArrayLike) -> None: """Set the per-vertex weight values Parameters @@ -366,7 +367,7 @@ def canRename(self, item: SimplexAccessor) -> bool: nn = self.getSidedName(item.name, 0) return nn != item.name - def splitRename(self, item: SimplexAccessor, sIdx: int): + def splitRename(self, item: SimplexAccessor, sIdx: int) -> None: """Actually run the rename for a particular item Parameters @@ -384,7 +385,7 @@ def splitRename(self, item: SimplexAccessor, sIdx: int): if isinstance(item, (Shape, Slider, Combo, Traversal)): item.name = self.getSidedName(item.name, sIdx) - def applyFalloff(self, shape: Shape, sIdx: int): + def applyFalloff(self, shape: Shape, sIdx: int) -> None: """Apply the falloff to the vertices of a shape Parameters @@ -428,7 +429,7 @@ def __init__( maxHandle: float, minHandle: float, minVal: float, - ): + ) -> None: self._maxVal = maxVal self._maxHandle = maxHandle self._minHandle = minHandle @@ -444,7 +445,7 @@ def setPlanarData( minHandle: float, maxHandle: float, maxVal: float, - ): + ) -> None: """Set the type/data for a planar Falloff Parameters @@ -473,7 +474,7 @@ def maxVal(self) -> float: @maxVal.setter @stackable - def maxVal(self, value: float): + def maxVal(self, value: float) -> None: self._maxVal = value self._updateDCC() @@ -483,7 +484,7 @@ def maxHandle(self) -> float: @maxHandle.setter @stackable - def maxHandle(self, value: float): + def maxHandle(self, value: float) -> None: self._maxHandle = value self._updateDCC() @@ -493,7 +494,7 @@ def minHandle(self) -> float: @minHandle.setter @stackable - def minHandle(self, value: float): + def minHandle(self, value: float) -> None: self._minHandle = value self._updateDCC() @@ -503,7 +504,7 @@ def minVal(self) -> float: @minVal.setter @stackable - def minVal(self, value: float): + def minVal(self, value: float) -> None: self._minVal = value self._updateDCC() @@ -513,7 +514,7 @@ def verts(self) -> npt.NDArray | None: return self._verts @verts.setter - def verts(self, vals: npt.NDArray): + def verts(self, vals: npt.NDArray) -> None: """Input the vertices into this falloff and compute the weights Parameters @@ -525,7 +526,7 @@ def verts(self, vals: npt.NDArray): self._weights = None self._verts = vals - def _updateDCC(self): + def _updateDCC(self) -> None: """ """ # TODO: Separate Map and Planar falloff data self.DCC.setFalloffData( @@ -612,7 +613,7 @@ def weights(self) -> npt.NDArray: return self._weights @weights.setter - def weights(self, val: npt.ArrayLike): + def weights(self, val: npt.ArrayLike) -> None: """Set the per-vertex weight values Parameters @@ -660,12 +661,12 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: class MapFalloff(Falloff): splitType: str = "map" - def __init__(self, name: str, simplex: Simplex, axis: str, mapName: str): + def __init__(self, name: str, simplex: Simplex, axis: str, mapName: str) -> None: self._mapName = mapName super().__init__(name, simplex, axis) @stackable - def setMapData(self, axis: str, mapName: str): + def setMapData(self, axis: str, mapName: str) -> None: """Set the type/data for a map Falloff Parameters @@ -683,11 +684,11 @@ def mapName(self) -> str: @mapName.setter @stackable - def mapName(self, value: str): + def mapName(self, value: str) -> None: self._mapName = value self._updateDCC() - def _updateDCC(self): + def _updateDCC(self) -> None: """ """ # TODO: Separate Map and Planar falloff data self.DCC.setFalloffData( @@ -712,7 +713,7 @@ def weights(self) -> npt.NDArray: return self._weights @weights.setter - def weights(self, val: npt.ArrayLike): + def weights(self, val: npt.ArrayLike) -> None: """Set the per-vertex weight values Parameters diff --git a/src/python/simplexui/items/group.py b/src/python/simplexui/items/group.py index 53701b41..a3c4bf5d 100644 --- a/src/python/simplexui/items/group.py +++ b/src/python/simplexui/items/group.py @@ -57,7 +57,7 @@ def __init__( name: str, simplex: Simplex, groupType: GroupType, - ): + ) -> None: super().__init__(simplex) from .combo import Combo from .slider import Slider @@ -100,7 +100,7 @@ def name(self) -> str: @name.setter @stackable - def name(self, value: str): + def name(self, value: str) -> None: """Set the name of the Group""" self._name = value @@ -193,7 +193,7 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: simpDict.setdefault("groups", []).append(x) return self._buildIdx - def clearBuildIndex(self): + def clearBuildIndex(self) -> None: """Clear the build index of this object The buildIndex is stored when building a definition dictionary @@ -202,7 +202,7 @@ def clearBuildIndex(self): self._buildIdx = None @stackable - def delete(self): + def delete(self) -> None: """Delete a group. Any objects in this group will be deleted""" from .combo import Combo from .slider import Slider @@ -228,7 +228,7 @@ def delete(self): gList.remove(self) @stackable - def take(self, things: list[GroupMember]): + def take(self, things: list[GroupMember]) -> None: """Remove some items from their current groups and put them in this one Parameters diff --git a/src/python/simplexui/items/progression.py b/src/python/simplexui/items/progression.py index fd23a94c..88361cff 100644 --- a/src/python/simplexui/items/progression.py +++ b/src/python/simplexui/items/progression.py @@ -36,7 +36,7 @@ class ProgPair(SimplexTreeAccessor, Draggable): classDepth: int = 9 - def __init__(self, simplex: Simplex, shape: Shape, value: float): + def __init__(self, simplex: Simplex, shape: Shape, value: float) -> None: super().__init__(simplex) self.shape: Shape = shape self._value: float = value @@ -51,7 +51,7 @@ def name(self) -> str: return self.shape.name @name.setter - def name(self, value: str): + def name(self, value: str) -> None: self.shape.name = value def buildDefinition( @@ -69,7 +69,7 @@ def value(self) -> float: @value.setter @stackable - def value(self, val: float): + def value(self, val: float) -> None: from .slider import Slider self._value = val @@ -82,7 +82,7 @@ def value(self, val: float): self.prog.controller.setRange() @stackable - def delete(self): + def delete(self) -> None: if self.prog is None: return with self.removeItemManager(self): @@ -168,7 +168,7 @@ def __init__( pairs: list[ProgPair] | None = None, interp: str = "spline", falloffs: list[Falloff] | None = None, - ): + ) -> None: super().__init__(simplex) if self.simplex.restShape is None: @@ -198,7 +198,7 @@ def name(self) -> str: return self._name @name.setter - def name(self, value: str): + def name(self, value: str) -> None: self._name = value @property @@ -208,7 +208,7 @@ def interp(self) -> str: @interp.setter @stackable - def interp(self, value: str): + def interp(self, value: str) -> None: """Set the interp for this Progression""" self._interp = value @@ -276,7 +276,7 @@ def getInsertIndex(self, tVal: float) -> int: return i return 0 - def getShapeAtValue(self, val: float, tol=0.0001) -> Shape | None: + def getShapeAtValue(self, val: float, tol: float = 0.0001) -> Shape | None: """Return the shape at the given value Parameters @@ -349,7 +349,7 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: simpDict.setdefault("progressions", []).append(x) return self._buildIdx - def clearBuildIndex(self): + def clearBuildIndex(self) -> None: """Clear the build index of this object The buildIndex is stored when building a definition dictionary @@ -362,7 +362,7 @@ def clearBuildIndex(self): fo.clearBuildIndex() @stackable - def moveShapeToProgression(self, shapePair: ProgPair): + def moveShapeToProgression(self, shapePair: ProgPair) -> None: """Remove the shapePair from its current progression and set it in a new progression Parameters @@ -377,7 +377,7 @@ def moveShapeToProgression(self, shapePair: ProgPair): shapePair.prog = self @stackable - def setShapesValues(self, values: list[float]): + def setShapesValues(self, values: list[float]) -> None: """Set all the Shape's values Parameters @@ -401,7 +401,7 @@ def siblingRename( return {} @stackable - def addFalloff(self, falloff: Falloff): + def addFalloff(self, falloff: Falloff) -> None: """Add a falloff to a slider's falloff list Parameters @@ -415,7 +415,7 @@ def addFalloff(self, falloff: Falloff): self.DCC.addProgFalloff(self, falloff) @stackable - def removeFalloff(self, falloff: Falloff): + def removeFalloff(self, falloff: Falloff) -> None: """Remove a falloff from a slider's falloff list Parameters @@ -527,7 +527,7 @@ def guessNextTVal(self) -> float: return 1.0 @stackable - def deleteShape(self, shape: Shape): + def deleteShape(self, shape: Shape) -> None: """Delete a shape from the system and the DCC Parameters @@ -550,7 +550,7 @@ def deleteShape(self, shape: Shape): self.DCC.deleteShape(shape) @stackable - def delete(self): + def delete(self) -> None: """Delete the Progression and all its Shapes""" with self.removeItemManager(self): for pp in self.pairs[:]: @@ -604,7 +604,7 @@ def treeRow(self) -> int: def treeParent(self) -> TreeItem | None: return self.controller - def treeChildCount(self): + def treeChildCount(self) -> int: return len(self.pairs) def treeData(self, column: int) -> Any | None: diff --git a/src/python/simplexui/items/shape.py b/src/python/simplexui/items/shape.py index 776225db..d19fcc46 100644 --- a/src/python/simplexui/items/shape.py +++ b/src/python/simplexui/items/shape.py @@ -54,7 +54,7 @@ class Shape(SimplexAccessor): classDepth: int = 10 - def __init__(self, name: str, simplex: Simplex, create: bool = True): + def __init__(self, name: str, simplex: Simplex, create: bool = True) -> None: super().__init__(simplex) with self.stack.store(self): self._thing: DCCObject | None = None @@ -141,7 +141,7 @@ def name(self) -> str: @name.setter @stackable - def name(self, value: str): + def name(self, value: str) -> None: """Set the Shape's name""" if value == self._name: return @@ -235,7 +235,7 @@ def thing(self) -> DCCObject: return self._thing @thing.setter - def thing(self, value: DCCObject): + def thing(self, value: DCCObject) -> None: """Set the stored reference to the DCC object""" self._thing = value self._thingRepr = self.DCC.getPersistentShape(value) @@ -281,7 +281,7 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: simpDict.setdefault("shapes", []).append(x) return self._buildIdx - def clearBuildIndex(self): + def clearBuildIndex(self) -> None: """Clear the build index of this object The buildIndex is stored when building a definition dictionary @@ -289,12 +289,12 @@ def clearBuildIndex(self): """ self._buildIdx = None - def zeroShape(self): + def zeroShape(self) -> None: """Set the shape to be equal to the rest shape""" self.DCC.zeroShape(self) @staticmethod - def zeroShapes(shapes): + def zeroShapes(shapes) -> None: """Set the shapes to be equal to the rest shape Parameters @@ -308,7 +308,7 @@ def zeroShapes(shapes): def connectShape( self, mesh: DCCObject | None = None, live: bool = False, delete: bool = False - ): + ) -> None: """Force a shape to match a mesh The "connect shape" button is: mesh=None, delete=True The "match shape" button is: mesh=someMesh, delete=False @@ -331,7 +331,7 @@ def connectShapes( meshes: list[DCCObject], live: bool = False, delete: bool = False, - ): + ) -> None: """Connect multiple meshes to multiple Shapes Parameters @@ -379,6 +379,6 @@ def verts(self) -> npt.NDArray | None: return self._verts @verts.setter - def verts(self, value: npt.NDArray): + def verts(self, value: npt.NDArray) -> None: """Set the stored vertices""" self._verts = value diff --git a/src/python/simplexui/items/simplex.py b/src/python/simplexui/items/simplex.py index 9411ad5e..5e51951d 100644 --- a/src/python/simplexui/items/simplex.py +++ b/src/python/simplexui/items/simplex.py @@ -65,7 +65,7 @@ class Simplex(TreeRootItem): classDepth = 0 - def __init__(self, name: str = "", forceDummy=False, sliderMul=1.0): + def __init__(self, name: str = "", forceDummy=False, sliderMul=1.0) -> None: """Constructor Parameters @@ -140,7 +140,7 @@ def __deepcopy__(self, memo: dict[int, Any]) -> Simplex: setattr(result, k, copy.deepcopy(v, memo)) return result - def _initValues(self): + def _initValues(self) -> None: """Re-initialize the variables to that of an empy system""" self.sliderMul = 1.0 self._name = "" # The name of the system @@ -513,7 +513,9 @@ def buildSystemFromDict( self.loadDefinition(jsDict, create=create, pBar=pBar) return self - def loadSmpxShapes(self, smpxPath: str, pBar: QProgressDialog | None = None): + def loadSmpxShapes( + self, smpxPath: str, pBar: QProgressDialog | None = None + ) -> None: """Load the Shapes from a .smpx file onto an already loaded system This is the "We got updated shapes from the modelers" method @@ -532,7 +534,7 @@ def loadSmpxShapes(self, smpxPath: str, pBar: QProgressDialog | None = None): finally: del abcMesh, iarch - def loadSmpxPoses(self, smpxPath: str, pBar: QProgressDialog | None = None): + def loadSmpxPoses(self, smpxPath: str, pBar: QProgressDialog | None = None) -> None: """Load the Poses from a .smpx file onto an already loaded system This is the "Update the joints and skin" method @@ -551,7 +553,9 @@ def loadSmpxPoses(self, smpxPath: str, pBar: QProgressDialog | None = None): finally: del abcMesh, iarch - def loadSmpxFalloffs(self, abcPath: str, pBar: QProgressDialog | None = None): + def loadSmpxFalloffs( + self, abcPath: str, pBar: QProgressDialog | None = None + ) -> None: """Load the relevant data from a simplex alembic Parameters @@ -576,7 +580,7 @@ def name(self) -> str: @name.setter @stackable - def name(self, value: str): + def name(self, value: str) -> None: """Set the system name""" if value == self._name: return @@ -628,7 +632,7 @@ def comboExists(self, sliders: list[Slider], values: list[float]) -> Combo | Non return None # DESTRUCTOR - def deleteSystem(self): + def deleteSystem(self) -> None: """Delete an existing system from the DCC""" # Store the models as temp so the model doesn't go crazy with the signals with self.resetManager(): @@ -730,7 +734,7 @@ def getDownstreamCombos(self, slider: Slider) -> list[Combo]: downstream = list(set(downstream)) return downstream - def deleteDownstream(self, item: Slider): + def deleteDownstream(self, item: Slider) -> None: """Delete all items from the system that depend on the given item Parameters @@ -745,7 +749,7 @@ def deleteDownstream(self, item: Slider): c.delete() # USER METHODS - def setLegacy(self, legacy: bool): + def setLegacy(self, legacy: bool) -> None: """Set whether to use the legacy .json format Parameters @@ -833,7 +837,7 @@ def loadDefinition( simpDict: dict[str, Any], create: bool = True, pBar: QProgressDialog | None = None, - ): + ) -> None: """Build the structure of objects in this system based on a provided dictionary @@ -882,7 +886,7 @@ def loadV3( simpDict: dict[str, Any], create: bool = True, pBar: QProgressDialog | None = None, - ): + ) -> None: """Load the version 3 simplex definition V3 is just the same as V2, except for an update Traversal definition @@ -943,7 +947,7 @@ def loadV2( simpDict: dict[str, Any], create: bool = True, pBar: QProgressDialog | None = None, - ): + ) -> None: """Load the version 2 simplex definition Parameters @@ -1003,7 +1007,7 @@ def loadV1( simpDict: dict[str, Any], create: bool = True, pBar: QProgressDialog | None = None, - ): + ) -> None: """Load the version 1 simplex definition Parameters @@ -1125,7 +1129,7 @@ def loadV1( finally: self.DCC.postLoad(self, preRet) - def storeExtras(self, simpDict: dict[str, Any]): + def storeExtras(self, simpDict: dict[str, Any]) -> None: """Store any unknown keys when dumping, just in case they're important elsewhere Parameters @@ -1152,7 +1156,7 @@ def storeExtras(self, simpDict: dict[str, Any]): del sd[ktn] self._extras = sd - def loadJSON(self, jsString: str): + def loadJSON(self, jsString: str) -> None: """Convenience method to load a JSON string definition Parameters @@ -1182,7 +1186,7 @@ def dump(self) -> str: """ return json.dumps(self.buildDefinition()) - def exportAbc(self, path: str, pBar: QProgressDialog | None = None): + def exportAbc(self, path: str, pBar: QProgressDialog | None = None) -> None: """Export the current mesh to a .smpx formatted file Parameters @@ -1215,7 +1219,7 @@ def exportOther( world: bool = False, ensureCorrect: bool = False, pBar: QProgressDialog | None = None, - ): + ) -> None: """Export shapes from a mesh that isn't part of the current system The export process for shapes differs from DCC to DCC. @@ -1245,7 +1249,7 @@ def exportOther( finally: del arch, abcMesh - def setSlidersWeights(self, sliders: list[Slider], weights: list[float]): + def setSlidersWeights(self, sliders: list[Slider], weights: list[float]) -> None: """Set the weights of multiple sliders as one method Parameters diff --git a/src/python/simplexui/items/slider.py b/src/python/simplexui/items/slider.py index 71eaf6c0..2eb53e45 100644 --- a/src/python/simplexui/items/slider.py +++ b/src/python/simplexui/items/slider.py @@ -61,7 +61,7 @@ def __init__( prog: Progression, group: Group, create: bool = True, - ): + ) -> None: if group.groupType is not type(self): raise ValueError("Cannot add this slider to a combo group") @@ -103,7 +103,7 @@ def enabled(self) -> bool: @enabled.setter @stackable - def enabled(self, value: bool): + def enabled(self, value: bool) -> None: """Get whether this Slider is evaluated in the solver""" self._enabled = value @@ -216,7 +216,7 @@ def name(self) -> str: @name.setter @stackable - def name(self, value: str): + def name(self, value: str) -> None: """Set the name of a Slider""" self._name = value self.prog.name = value @@ -301,7 +301,7 @@ def thing(self) -> DCCObject: return self._thing @thing.setter - def thing(self, value: DCCObject): + def thing(self, value: DCCObject) -> None: self._thing = value self._thingRepr = self.DCC.getPersistentSlider(value) @@ -311,19 +311,19 @@ def value(self) -> float: return self._value @value.setter - def value(self, val: float): + def value(self, val: float) -> None: """Set the current value for this Slider""" self._value = val # singleShot consolidates all self._setAllSliders(self) # type: ignore @singleShot() - def _setAllSliders(self, *sliders: Slider): + def _setAllSliders(self, *sliders: Slider) -> None: with undoContext(self.DCC): for slider in sliders: self.DCC.setSliderWeight(slider, slider.value) - def updateValue(self): + def updateValue(self) -> None: pass @classmethod @@ -388,7 +388,7 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: simpDict.setdefault("sliders", []).append(x) return self._buildIdx - def clearBuildIndex(self): + def clearBuildIndex(self) -> None: """Clear the build index of this object The buildIndex is stored when building a definition dictionary @@ -398,7 +398,7 @@ def clearBuildIndex(self): self.prog.clearBuildIndex() self.group.clearBuildIndex() - def setRange(self): + def setRange(self) -> None: """Set the range of this Slider based on the progPair values""" values = [i.value for i in self.prog.pairs] self.minValue = min(values) @@ -406,7 +406,7 @@ def setRange(self): self.DCC.setSliderRange(self) @stackable - def delete(self): + def delete(self) -> None: """Delete a slider, any shapes it contains, and all downstream Combos and Traversals""" self.simplex.deleteDownstream(self) with self.removeItemManager(self): @@ -424,7 +424,7 @@ def delete(self): self.DCC.deleteSlider(self) @stackable - def setInterpolation(self, interp: str): + def setInterpolation(self, interp: str) -> None: """Set the interpolation of a single Slider Parameters @@ -435,7 +435,7 @@ def setInterpolation(self, interp: str): self.prog.interp = interp @stackable - def setInterps(self, sliders: list[Slider], interp: str): + def setInterps(self, sliders: list[Slider], interp: str) -> None: """Set the interpolation of multiple Sliders Parameters @@ -483,7 +483,7 @@ def createShape( def extractProgressive( self, live: bool = True, offset: float = 10.0, separation: float = 5.0 - ): + ) -> None: """Extract all of the Shapes in this Slider's Progression Parameters @@ -545,7 +545,7 @@ def connectShape( mesh: DCCObject | None = None, live: bool = False, delete: bool = False, - ): + ) -> None: """Connect a Shape that is controlled by a Slider to a DCC mesh This is on Slider (vs being on Shape) because live connections are handled @@ -566,12 +566,12 @@ def connectShape( """ self.DCC.connectShape(shape, mesh, live, delete) - def updateRange(self): + def updateRange(self) -> None: """Update the range in the DCC for this Slider""" self.DCC.updateSlidersRange([self]) @stackable - def setGroup(self, grp: Group): + def setGroup(self, grp: Group) -> None: """Set the Group for this Slider Parameters diff --git a/src/python/simplexui/items/stack.py b/src/python/simplexui/items/stack.py index a6c8495e..02742bc7 100644 --- a/src/python/simplexui/items/stack.py +++ b/src/python/simplexui/items/stack.py @@ -32,14 +32,14 @@ class Stack: """Integrate simplex into the DCC undo stack""" - def __init__(self): + def __init__(self) -> None: # Technically not needed, but I depend on the ordering behavior self._stack: OrderedDict[int, Any] = OrderedDict() self.depth: int = 0 self.currentRevision: int = 0 self.enabled: bool = True - def push(self, key: int, value: Any): + def push(self, key: int, value: Any) -> None: """Push the memento value onto the stack for the given revision key""" gt = [] # when setting a new key, remove all keys from @@ -79,7 +79,7 @@ def getRevision(self, revision: int) -> Simplex | None: return data return None - def purge(self): + def purge(self) -> None: """Clear the undo stack. This should be done on new-file""" self._stack = OrderedDict() self.depth = 0 @@ -124,7 +124,7 @@ def store(self, wrapObj: Any): class Memo(dict): - def __setitem__(self, key, value): + def __setitem__(self, key, value) -> None: print("SETTING", key, value) super().__setitem__(key, value) diff --git a/src/python/simplexui/items/traversal.py b/src/python/simplexui/items/traversal.py index aac3635d..34409570 100644 --- a/src/python/simplexui/items/traversal.py +++ b/src/python/simplexui/items/traversal.py @@ -41,7 +41,7 @@ class TravSide(Enum): class TravPair(SimplexTreeAccessor, Draggable): classDepth: int = 4 - def __init__(self, slider: Slider, value: float): + def __init__(self, slider: Slider, value: float) -> None: simplex = slider.simplex super().__init__(simplex) self.slider: Slider = slider @@ -61,7 +61,7 @@ def value(self) -> float: @value.setter @stackable - def value(self, val: float): + def value(self, val: float) -> None: self._value = val def buildDefinition( @@ -71,20 +71,20 @@ def buildDefinition( return sIdx, self.value @stackable - def remove(self): + def remove(self) -> None: with self.removeItemManager(self): if self.travPoint is not None: self.travPoint.pairs.remove(self) self.travPoint = None @stackable - def delete(self): + def delete(self) -> None: if self.travPoint is not None: if self.travPoint.traversal is not None: self.travPoint.traversal.removePairs([self]) @staticmethod - def removeAll(pairs: list[TravPair]): + def removeAll(pairs: list[TravPair]) -> None: points = [i.travPoint for i in pairs if i.travPoint is not None] travs = list({pp.traversal for pp in points}) for trav in travs: @@ -114,7 +114,7 @@ def treeData(self, column: int) -> Any | None: class TravPoint(SimplexTreeAccessor): classDepth: int = 3 - def __init__(self, pairs: list[TravPair], side: TravSide): + def __init__(self, pairs: list[TravPair], side: TravSide) -> None: if not pairs: raise ValueError("Pairs must be provided for a TravPoint") simplex = pairs[0].slider.simplex @@ -130,7 +130,7 @@ def sliders(self) -> list[Slider]: return [i.slider for i in self.pairs] @staticmethod - def _wideCeiling(val: float, eps: float = 0.001): + def _wideCeiling(val: float, eps: float = 0.001) -> float: if val > eps: return 1.0 elif val < -eps: @@ -138,15 +138,15 @@ def _wideCeiling(val: float, eps: float = 0.001): return 0.0 @stackable - def addPair(self, pair: TravPair): + def addPair(self, pair: TravPair) -> None: with self.insertItemManager(self): self.pairs.append(pair) pair.travPoint = self - def removePair(self, pair: TravPair): + def removePair(self, pair: TravPair) -> None: pair.remove() - def addSlider(self, slider: Slider, val: float | None = None): + def addSlider(self, slider: Slider, val: float | None = None) -> None: val = val if val is not None else slider.value val = self._wideCeiling(val) sliders = self.sliders() @@ -157,7 +157,7 @@ def addSlider(self, slider: Slider, val: float | None = None): else: self.pairs[idx].value = val - def addItem(self, item: Slider | Combo): + def addItem(self, item: Slider | Combo) -> None: if isinstance(item, Slider): self.addSlider(item) elif isinstance(item, Combo): @@ -261,7 +261,7 @@ def __init__( endPoint: TravPoint, prog: Progression, group: Group, - ): + ) -> None: super().__init__(simplex) with self.stack.store(self): if group.groupType is not type(self): @@ -343,7 +343,7 @@ def enabled(self) -> bool: @enabled.setter @stackable - def enabled(self, value: bool): + def enabled(self, value: bool) -> None: """Set whether this Traversal is evaluated in the solver""" self._enabled = value @@ -354,7 +354,7 @@ def name(self) -> str: @name.setter @stackable - def name(self, value: str): + def name(self, value: str) -> None: """Set the name of a Traversal""" self._name = value self.prog.name = value @@ -399,7 +399,7 @@ def ranges(self) -> dict[Slider, tuple[float, float]]: return rangeDict @stackable - def setGroup(self, grp: Group): + def setGroup(self, grp: Group) -> None: """Set the Group for this Slider Parameters @@ -630,7 +630,7 @@ def buildDefinition(self, simpDict: dict[str, Any], legacy: bool) -> int: simpDict.setdefault("traversals", []).append(x) return self._buildIdx - def clearBuildIndex(self): + def clearBuildIndex(self) -> None: """Clear the build index of this object The buildIndex is stored when building a definition dictionary @@ -641,7 +641,7 @@ def clearBuildIndex(self): self.group.clearBuildIndex() @stackable - def delete(self): + def delete(self) -> None: """Delete a traversal and any shapes it contains""" with self.removeItemManager(self): g = self.group @@ -657,11 +657,11 @@ def delete(self): self.simplex.shapes.remove(pp.shape) self.DCC.deleteShape(pp.shape) - def extractShape(self, shape, live=True, offset=10.0) -> DCCObject: + def extractShape(self, shape, live: bool = True, offset: float = 10.0) -> DCCObject: """Extract a shape from a Traversal progression""" return self.DCC.extractTraversalShape(self, shape, live, offset) - def addSlider(self, slider: Slider): + def addSlider(self, slider: Slider) -> None: """Add a slider to both the startPoint and endPoint of this Traversal Parameters @@ -672,7 +672,7 @@ def addSlider(self, slider: Slider): self.startPoint.addSlider(slider, val=0.0) self.endPoint.addSlider(slider) - def removePairs(self, pairs: list[TravPair]): + def removePairs(self, pairs: list[TravPair]) -> None: """Remove the given pairs from both the startPoint and endPoint of this Traversal Parameters @@ -753,7 +753,7 @@ def getInputVector(self, value: float) -> list[float]: for pair in self.endPoint.pairs: fullEnd[indexBySlider[pair.slider]] = pair.value - def _lerp(s, e, v): + def _lerp(s: float, e: float, v: float) -> float: return s * (1 - v) + e * v return [_lerp(fs, fe, value) for fs, fe in zip(fullStart, fullEnd)] diff --git a/src/python/simplexui/items/treeItem.py b/src/python/simplexui/items/treeItem.py index a2c4a8d9..e7c272b8 100644 --- a/src/python/simplexui/items/treeItem.py +++ b/src/python/simplexui/items/treeItem.py @@ -68,7 +68,7 @@ class CustomRoles(enum.IntEnum): class ObserverServer: # it's just fun to say! - def __init__(self, model: AdapterModel): + def __init__(self, model: AdapterModel) -> None: self.model: AdapterModel = model def valueObserver(self, item: TreeItem) -> None: @@ -130,7 +130,7 @@ def resetManager(self) -> SimpleGenerator: class TreeItem: classDepth = -1 - def __init__(self, root: TreeRootItem): + def __init__(self, root: TreeRootItem) -> None: self.root: TreeRootItem = root self.uid: str = uuid.uuid4().hex # Unique identifier for tree expansion @@ -138,7 +138,7 @@ def __init__(self, root: TreeRootItem): def observers(self) -> list[ObserverServer]: return self.root.observerServers() - def notifyChanged(self): + def notifyChanged(self) -> None: """Let the observers know that this item has changed""" for ob in self.observers: if ob.valueObserver is not None: @@ -210,17 +210,17 @@ def icon(self) -> QIcon | None: class TreeRootItem(TreeItem): - def __init__(self, observerServers: list[ObserverServer] | None = None): + def __init__(self, observerServers: list[ObserverServer] | None = None) -> None: super().__init__(self) # The root just keeps track of everybody's observers if observerServers is None: observerServers = [] self._observerServers: list[ObserverServer] = observerServers - def addObserver(self, observerServer: ObserverServer): + def addObserver(self, observerServer: ObserverServer) -> None: self._observerServers.append(observerServer) - def removeObserver(self, observerServer: ObserverServer): + def removeObserver(self, observerServer: ObserverServer) -> None: self._observerServers.remove(observerServer) def observerServers(self) -> list[ObserverServer]: @@ -236,14 +236,14 @@ class AdapterModel(QAbstractItemModel): def __init__( self, rootItem: TreeRootItem | None = None, parent: QObject | None = None - ): + ) -> None: super().__init__(parent=parent) self._rootItem: TreeRootItem | None = None self.observer = ObserverServer(self) if rootItem is not None: self.setRootItem(rootItem) - def setRootItem(self, rootItem: TreeRootItem): + def setRootItem(self, rootItem: TreeRootItem) -> None: self.beginResetModel() try: if self._rootItem is not None: @@ -267,11 +267,11 @@ def indexFromItem(self, item: TreeItem, column: int = 0) -> QModelIndex: def itemFromIndex(self, index: QModelIndex) -> TreeItem | None: return index.internalPointer() - def itemDataChanged(self, item: TreeItem): + def itemDataChanged(self, item: TreeItem) -> None: idx = self.indexFromItem(item) self.emitDataChanged(idx) - def emitDataChanged(self, index: QModelIndex): + def emitDataChanged(self, index: QModelIndex) -> None: if index.isValid(): self.dataChanged.emit(index, index, []) @@ -412,7 +412,7 @@ def iteritems( yield self.itemFromIndex(index) return - def itempred(x): + def itempred(x) -> bool: item = self.itemFromIndex(x) return False if item is None else pred(item) @@ -432,7 +432,7 @@ def save_view_expansion_state(tree_view: QTreeView) -> set[str]: return expanded_uids -def restore_view_expansion_state(tree_view: QTreeView, expanded_uids: set[str]): +def restore_view_expansion_state(tree_view: QTreeView, expanded_uids: set[str]) -> None: model = tree_view.model() assert isinstance(model, AdapterModel) for index in model.iterindices( diff --git a/src/python/simplexui/menu/__init__.py b/src/python/simplexui/menu/__init__.py index 19e69764..e471d01a 100644 --- a/src/python/simplexui/menu/__init__.py +++ b/src/python/simplexui/menu/__init__.py @@ -25,6 +25,7 @@ from Qt.QtWidgets import QMenu from . import genericPlugins +from types import ModuleType CONTEXT = os.path.basename(sys.executable) if CONTEXT == "maya.exe": @@ -42,7 +43,7 @@ def _iter_namespace(ns_pkg): # Registration class -def loadPlugins(): +def loadPlugins() -> tuple[list[ModuleType], list[ModuleType]]: toolModules = [] contextModules = [] imod = sorted([i[1] for i in _iter_namespace(genericPlugins)]) @@ -65,7 +66,7 @@ def buildToolMenu(window, modules): return menu -def buildRightClickMenu(tree, indexes, modules): +def buildRightClickMenu(tree, indexes, modules) -> QMenu: menu = QMenu() for m in modules: m.registerContext(tree, indexes, menu) diff --git a/src/python/simplexui/menu/genericPlugins/_builtins.py b/src/python/simplexui/menu/genericPlugins/_builtins.py index d584ef5b..2de80dc7 100644 --- a/src/python/simplexui/menu/genericPlugins/_builtins.py +++ b/src/python/simplexui/menu/genericPlugins/_builtins.py @@ -25,7 +25,7 @@ from ...items import Combo, ComboPair, ProgPair, Progression, Slider -def registerContext(tree, clickIdx, indexes, menu): +def registerContext(tree, clickIdx, indexes, menu) -> None: self = tree.window() if tree == self.uiComboTREE: registerComboTree(self, clickIdx, indexes, menu) @@ -33,7 +33,7 @@ def registerContext(tree, clickIdx, indexes, menu): registerSliderTree(self, clickIdx, indexes, menu) -def registerSliderTree(window, clickIdx, indexes, menu): +def registerSliderTree(window, clickIdx, indexes, menu) -> None: self = window # live = self.uiLiveShapeConnectionACT.isChecked() items = [i.model().itemFromIndex(i) for i in indexes] @@ -175,7 +175,7 @@ def registerSliderTree(window, clickIdx, indexes, menu): menu.addSeparator() -def registerComboTree(window, clickIdx, indexes, menu): +def registerComboTree(window, clickIdx, indexes, menu) -> None: self = window # live = self.uiLiveShapeConnectionACT.isChecked() items = [i.model().itemFromIndex(i) for i in indexes] diff --git a/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py b/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py index da97a23a..d385cd13 100644 --- a/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py +++ b/src/python/simplexui/menu/genericPlugins/checkPossibleCombos.py @@ -25,7 +25,7 @@ from ...items import Slider -def registerTool(window, menu): +def registerTool(window, menu) -> None: checkPossibleCombosACT = QAction("Check Possible Combos ...", window) menu.addAction(checkPossibleCombosACT) checkPossibleCombosACT.triggered.connect( @@ -33,7 +33,7 @@ def registerTool(window, menu): ) -def registerContext(tree, clickIdx, indexes, menu): +def registerContext(tree, clickIdx, indexes, menu) -> None: window = tree.window() checkPossibleCombosACT = QAction("Check Possible Combos ...", tree) menu.addAction(checkPossibleCombosACT) @@ -42,7 +42,7 @@ def registerContext(tree, clickIdx, indexes, menu): ) -def checkPossibleCombosInterface(window): +def checkPossibleCombosInterface(window) -> None: sliders = window.uiSliderTREE.getSelectedItems(typ=Slider) ccd = ComboCheckDialog(sliders, parent=window) ccd.show() diff --git a/src/python/simplexui/menu/genericPlugins/exportSplit.py b/src/python/simplexui/menu/genericPlugins/exportSplit.py index 782ef411..8d44c6fd 100644 --- a/src/python/simplexui/menu/genericPlugins/exportSplit.py +++ b/src/python/simplexui/menu/genericPlugins/exportSplit.py @@ -25,14 +25,14 @@ from Qt.QtWidgets import QMessageBox, QProgressDialog -def registerTool(window, menu): +def registerTool(window, menu) -> None: if np is not None: exportSplitACT = QAction("Export Split", window) menu.addAction(exportSplitACT) exportSplitACT.triggered.connect(partial(exportSplitInterface, window)) -def exportSplitInterface(window): +def exportSplitInterface(window) -> None: if np is None: QMessageBox.warning( window, diff --git a/src/python/simplexui/menu/genericPlugins/showFalloffs.py b/src/python/simplexui/menu/genericPlugins/showFalloffs.py index 5f728416..48df9644 100644 --- a/src/python/simplexui/menu/genericPlugins/showFalloffs.py +++ b/src/python/simplexui/menu/genericPlugins/showFalloffs.py @@ -20,7 +20,7 @@ from Qt.QtWidgets import QAction -def registerTool(window, menu): +def registerTool(window, menu) -> None: editFalloffsACT = QAction("Edit Falloffs ...", window) menu.addAction(editFalloffsACT) editFalloffsACT.triggered.connect(window.showFalloffDialog) diff --git a/src/python/simplexui/menu/genericPlugins/showTraversals.py b/src/python/simplexui/menu/genericPlugins/showTraversals.py index 4279f7ab..9e83e6fa 100644 --- a/src/python/simplexui/menu/genericPlugins/showTraversals.py +++ b/src/python/simplexui/menu/genericPlugins/showTraversals.py @@ -20,7 +20,7 @@ from Qt.QtWidgets import QAction -def registerTool(window, menu): +def registerTool(window, menu) -> None: showTraversalsACT = QAction("Show Traversals ...", window) menu.addAction(showTraversalsACT) showTraversalsACT.triggered.connect(window.showTraversalDialog) diff --git a/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py b/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py index 9006d6d6..4e31f489 100644 --- a/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py +++ b/src/python/simplexui/menu/genericPlugins/simplexUvTransfer.py @@ -27,8 +27,8 @@ def simplexUvTransfer( - srcSmpxPath, tarPath, outPath, srcUvPath=None, tol=0.0001, pBar=None -): + srcSmpxPath, tarPath, outPath, srcUvPath=None, tol: float = 0.0001, pBar=None +) -> None: """Transfer a simplex system onto a mesh through UV space Parameters diff --git a/src/python/simplexui/menu/genericPlugins/unsubdivide.py b/src/python/simplexui/menu/genericPlugins/unsubdivide.py index ae4885a2..ad9102e0 100644 --- a/src/python/simplexui/menu/genericPlugins/unsubdivide.py +++ b/src/python/simplexui/menu/genericPlugins/unsubdivide.py @@ -31,14 +31,14 @@ imathnumpy = None -def registerTool(window, menu): +def registerTool(window, menu) -> None: if imathnumpy is not None: exportUnsubACT = QAction("Un Subdivide Smpx ...", window) menu.addAction(exportUnsubACT) exportUnsubACT.triggered.connect(partial(exportUnsubInterface, window)) -def exportUnsubInterface(window): +def exportUnsubInterface(window) -> None: if imathnumpy is None: QMessageBox.warning( window, diff --git a/src/python/simplexui/menu/mayaPlugins/exportOther.py b/src/python/simplexui/menu/mayaPlugins/exportOther.py index d7bb2788..704093f1 100644 --- a/src/python/simplexui/menu/mayaPlugins/exportOther.py +++ b/src/python/simplexui/menu/mayaPlugins/exportOther.py @@ -25,13 +25,13 @@ from Qt.QtWidgets import QMessageBox, QProgressDialog -def registerTool(window, menu): +def registerTool(window, menu) -> None: exportOtherACT = QAction("Export Other", window) menu.addAction(exportOtherACT) exportOtherACT.triggered.connect(partial(exportOtherInterface, window)) -def exportOtherInterface(window): +def exportOtherInterface(window) -> None: if window.simplex is None: QMessageBox.warning( window, diff --git a/src/python/simplexui/menu/mayaPlugins/extractProgressives.py b/src/python/simplexui/menu/mayaPlugins/extractProgressives.py index 1d15e1aa..d9e38243 100644 --- a/src/python/simplexui/menu/mayaPlugins/extractProgressives.py +++ b/src/python/simplexui/menu/mayaPlugins/extractProgressives.py @@ -25,7 +25,7 @@ from ...items import Slider -def registerTool(window, menu): +def registerTool(window, menu) -> None: extractProgressivesACT = QAction("Extract Progressive", window) menu.addAction(extractProgressivesACT) extractProgressivesACT.triggered.connect( @@ -33,7 +33,7 @@ def registerTool(window, menu): ) -def registerContext(tree, clickIdx, indexes, menu): +def registerContext(tree, clickIdx, indexes, menu) -> bool: window = tree.window() live = window.uiLiveShapeConnectionACT.isChecked() sliders = coerceIndexToType(indexes, Slider) @@ -51,14 +51,14 @@ def registerContext(tree, clickIdx, indexes, menu): return False -def extractProgressivesContext(indexes, live): +def extractProgressivesContext(indexes, live) -> None: sliders = [idx.model().itemFromIndex(idx) for idx in indexes] sliders = list(set(sliders)) for sli in sliders: sli.extractProgressive(live=live) -def extractProgressivesInterface(window): +def extractProgressivesInterface(window) -> None: live = window.uiLiveShapeConnectionACT.isChecked() indexes = window.uiSliderTREE.getSelectedIndexes() indexes = coerceIndexToType(indexes, Slider) diff --git a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py index 9c59a007..369db23d 100644 --- a/src/python/simplexui/menu/mayaPlugins/freezeCombo.py +++ b/src/python/simplexui/menu/mayaPlugins/freezeCombo.py @@ -25,7 +25,7 @@ # UI stuff -def registerContext(tree, clickIdx, indexes, menu): +def registerContext(tree, clickIdx, indexes, menu) -> bool: # The basicBlendshape deformer is included in simplex_maya if not cmds.pluginInfo("simplex_maya", query=True, loaded=True): try: @@ -45,7 +45,7 @@ def registerContext(tree, clickIdx, indexes, menu): return False -def freezeCombosContext(combos, tree, doFreeze): +def freezeCombosContext(combos, tree, doFreeze) -> None: if doFreeze: for combo in combos: if not combo.frozen: @@ -58,7 +58,7 @@ def freezeCombosContext(combos, tree, doFreeze): tree.update() -def freezeCombo(combo): +def freezeCombo(combo) -> None: """Freeze a combo so you can change the upstream combos and shapes without affecting the result that you sculpted for the given combo @@ -184,7 +184,7 @@ def freezeCombo(combo): combo.freezeThing = helpers -def unfreezeCombo(combo): +def unfreezeCombo(combo) -> None: if combo.freezeThing: cmds.delete(combo.freezeThing) combo.freezeThing = [] diff --git a/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py index 8f177c7e..beb29eed 100644 --- a/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py +++ b/src/python/simplexui/menu/mayaPlugins/generateShapeIncrementals.py @@ -26,7 +26,7 @@ from ...items import Combo, Slider -def registerContext(tree, clickIdx, indexes, menu): +def registerContext(tree, clickIdx, indexes, menu) -> bool: window = tree.window() sliders = coerceIndexToType(indexes, Slider) combos = coerceIndexToType(indexes, Combo) @@ -47,7 +47,7 @@ def registerContext(tree, clickIdx, indexes, menu): return True -def generateShapeIncrementalsContext(indexes, window): +def generateShapeIncrementalsContext(indexes, window) -> None: idx = indexes[0] # Only on the click index slider = idx.model().itemFromIndex(idx) if len(slider.prog.pairs) > 2: diff --git a/src/python/simplexui/menu/mayaPlugins/importObjs.py b/src/python/simplexui/menu/mayaPlugins/importObjs.py index 4cc8a4b0..cfc5e4bb 100644 --- a/src/python/simplexui/menu/mayaPlugins/importObjs.py +++ b/src/python/simplexui/menu/mayaPlugins/importObjs.py @@ -34,7 +34,7 @@ def buildMesh(simplex, mesh): return Mesh(topo[0], tuple(faces)) -def importSimpleObjs(simplex, orders, pBar): +def importSimpleObjs(simplex, orders, pBar) -> None: for shapeName, ctrl, shape, path in orders: pBar.setValue(pBar.value() + 1) pBar.setLabelText(f"Loading Obj :\n{shapeName}") @@ -56,7 +56,7 @@ def importSimpleObjs(simplex, orders, pBar): ) -def importReorderObjs(simplex, orders, pBar): +def importReorderObjs(simplex, orders, pBar) -> None: reoMesh = simplex.DCC.extractShape(simplex.restShape, live=False) orderMesh = buildMesh(simplex, reoMesh) @@ -92,7 +92,7 @@ def importReorderObjs(simplex, orders, pBar): cmds.delete(reoMesh) -def importObjList(simplex, paths, pBar, reorder=True): +def importObjList(simplex, paths: list[str], pBar, reorder=True) -> None: """Import all given .obj files Parameters @@ -161,13 +161,13 @@ def importObjList(simplex, paths, pBar, reorder=True): pBar.close() -def registerTool(window, menu): +def registerTool(window, menu) -> None: importObjsACT = QAction("Import Obj Folder", window) menu.addAction(importObjsACT) importObjsACT.triggered.connect(partial(importObjsInterface, window)) -def importObjsInterface(window): +def importObjsInterface(window) -> None: if window.simplex is None: QMessageBox.warning( window, diff --git a/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py index 914c8c26..e5083390 100644 --- a/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py +++ b/src/python/simplexui/menu/mayaPlugins/linearizeTraversal.py @@ -24,13 +24,13 @@ from Qt.QtWidgets import QMessageBox -def registerTool(window, menu): +def registerTool(window, menu) -> None: lineTravACT = QAction("Linearize Traversal", window) menu.addAction(lineTravACT) lineTravACT.triggered.connect(partial(lineTrav, window)) -def lineTrav(window): +def lineTrav(window) -> None: if window.simplex is None: QMessageBox.warning( window, diff --git a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py index 6a8f4a9e..2a320d75 100644 --- a/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py +++ b/src/python/simplexui/menu/mayaPlugins/makeShelfBtn.py @@ -56,12 +56,12 @@ """ -def registerTool(window, menu): +def registerTool(window, menu) -> None: makeShelfBtnACT = QAction("Make Shelf Button", window) menu.addAction(makeShelfBtnACT) makeShelfBtnACT.triggered.connect(makeShelfButton) -def makeShelfButton(): +def makeShelfButton() -> None: pass # TODO: Actually, ya know, Add the button to the shelf diff --git a/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py index 424b6f2a..8b012817 100644 --- a/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py +++ b/src/python/simplexui/menu/mayaPlugins/relaxToSelection.py @@ -21,19 +21,19 @@ from Qt.QtWidgets import QAction -def registerTool(window, menu): +def registerTool(window, menu) -> None: relaxToSelectionACT = QAction("Relax To Selection", window) menu.addAction(relaxToSelectionACT) relaxToSelectionACT.triggered.connect(relaxToSelectionInterface) -def relaxToSelectionInterface(): +def relaxToSelectionInterface() -> None: sel = cmds.ls(sl=True) if len(sel) >= 2: relaxToSelection(sel[0], sel[1]) -def relaxToSelection(source, target): +def relaxToSelection(source, target) -> None: """ Transfer high-frequency sculpts (like wrinkles) from one shape to another diff --git a/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py index 7fc04c89..73b87a2f 100644 --- a/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py +++ b/src/python/simplexui/menu/mayaPlugins/reloadDefinition.py @@ -23,13 +23,13 @@ from Qt.QtWidgets import QMessageBox -def registerTool(window, menu): +def registerTool(window, menu) -> None: reloadDefinitionACT = QAction("Reload Definition", window) menu.addAction(reloadDefinitionACT) reloadDefinitionACT.triggered.connect(partial(reloadDefinitionInterface, window)) -def reloadDefinitionInterface(window): +def reloadDefinitionInterface(window) -> None: if window.simplex is None: QMessageBox.warning( window, @@ -40,5 +40,5 @@ def reloadDefinitionInterface(window): reloadDefinition(window.simplex) -def reloadDefinition(simplex): +def reloadDefinition(simplex) -> None: simplex.DCC.setSimplexString(simplex.DCC.op, simplex.dump()) diff --git a/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py index 38d73953..7f80f39c 100644 --- a/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py +++ b/src/python/simplexui/menu/mayaPlugins/snapToNeutral.py @@ -23,7 +23,7 @@ from Qt.QtWidgets import QAction -def registerTool(window, menu): +def registerTool(window, menu) -> None: snapShapeToNeutralACT = QAction("Snap Shape To Neutral", window) menu.addAction(snapShapeToNeutralACT) snapShapeToNeutralACT.triggered.connect( @@ -31,7 +31,7 @@ def registerTool(window, menu): ) -def snapShapeToNeutralInterface(window): +def snapShapeToNeutralInterface(window) -> None: sel = cmds.ls(sl=True) if len(sel) >= 2: snapShapeToNeutral(sel[0], sel[1]) @@ -41,7 +41,7 @@ def snapShapeToNeutralInterface(window): cmds.delete(rest) -def snapShapeToNeutral(source, target): +def snapShapeToNeutral(source, target) -> None: """ Take a mesh, and find the closest location on the target head, and snap to that Then set up a blendShape so the artist can "paint" in the snapping behavior diff --git a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py index bc948abb..34d0fd59 100644 --- a/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py +++ b/src/python/simplexui/menu/mayaPlugins/softSelectToCluster.py @@ -22,13 +22,13 @@ from Qt.QtWidgets import QAction -def registerTool(window, menu): +def registerTool(window, menu) -> None: softSelectToClusterACT = QAction("Soft Select To Cluster", window) menu.addAction(softSelectToClusterACT) softSelectToClusterACT.triggered.connect(softSelectToClusterInterface) -def softSelectToClusterInterface(): +def softSelectToClusterInterface() -> None: sel = cmds.ls(sl=True, objectsOnly=True) if sel: name = sel[0].split("|")[-1] @@ -168,7 +168,7 @@ def getSoftSelectionValues(myNode, returnSimpleIndices=True): return toReturn -def softSelectToCluster(tfm, name): +def softSelectToCluster(tfm, name: str) -> None: # Get the manipulator position for the selection cmds.setToolTo("Move") currentMoveMode = cmds.manipMoveContext("Move", query=True, mode=True) diff --git a/src/python/simplexui/menu/mayaPlugins/tweakMix.py b/src/python/simplexui/menu/mayaPlugins/tweakMix.py index 83299209..47eed133 100644 --- a/src/python/simplexui/menu/mayaPlugins/tweakMix.py +++ b/src/python/simplexui/menu/mayaPlugins/tweakMix.py @@ -27,13 +27,13 @@ from ...items import Combo -def registerTool(window, menu): +def registerTool(window, menu) -> None: tweakMixACT = QAction("Tweak Mix", window) menu.addAction(tweakMixACT) tweakMixACT.triggered.connect(partial(tweakMixInterface, window)) -def tweakMixInterface(window): +def tweakMixInterface(window) -> None: if not window.simplex: return live = window.uiLiveShapeConnectionACT.isChecked() @@ -47,7 +47,7 @@ def tweakMixInterface(window): tweakMix(window.simplex, combos, live) -def registerContext(tree, clickIdx, indexes, menu): +def registerContext(tree, clickIdx, indexes, menu) -> bool: window = tree.window() live = window.uiLiveShapeConnectionACT.isChecked() indexes = coerceIndexToType(indexes, Combo) @@ -59,13 +59,13 @@ def registerContext(tree, clickIdx, indexes, menu): return False -def tweakMixContext(window, indexes, live): +def tweakMixContext(window, indexes, live) -> None: combos = [idx.model().itemFromIndex(idx) for idx in indexes] combos = list(set(combos)) tweakMix(window.simplex, combos, live) -def tweakMix(simplex, combos, live): +def tweakMix(simplex, combos, live) -> None: # first extract the rest shape non-live restGeo = simplex.extractRestShape() diff --git a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py index 59a946de..bc4ddb5b 100644 --- a/src/python/simplexui/menu/mayaPlugins/updateRestShape.py +++ b/src/python/simplexui/menu/mayaPlugins/updateRestShape.py @@ -25,13 +25,13 @@ from Qt.QtWidgets import QMessageBox -def registerTool(window, menu): +def registerTool(window, menu) -> None: updateRestShapeACT = QAction("Update Rest Shape", window) menu.addAction(updateRestShapeACT) updateRestShapeACT.triggered.connect(partial(updateRestShapeInterface, window)) -def updateRestShapeInterface(window): +def updateRestShapeInterface(window) -> None: if window.simplex is None: QMessageBox.warning( window, @@ -78,7 +78,7 @@ def updateRestShapeInterface(window): updateRestShape(mesh, sel, window=window) -def updateRestShape(mesh, newRest, window=None): +def updateRestShape(mesh, newRest, window=None) -> None: allShapes = cmds.listRelatives(mesh, children=1, shapes=1) or [] noInter = cmds.listRelatives(mesh, children=1, shapes=1, noIntermediate=1) or [] hist = cmds.listHistory(mesh) diff --git a/src/python/simplexui/simplexDialog.py b/src/python/simplexui/simplexDialog.py index e978dbb2..a44a0b18 100644 --- a/src/python/simplexui/simplexDialog.py +++ b/src/python/simplexui/simplexDialog.py @@ -85,7 +85,7 @@ @contextmanager -def signalsBlocked(item): +def signalsBlocked(item: QObject): """Context manager to block the qt signals on an item""" item.blockSignals(True) try: @@ -178,7 +178,7 @@ class SimplexDialog(Window): simplexLoaded = Signal() openedDialogs: list[weakref.ref[SimplexDialog]] = [] - def __init__(self, parent=None, dispatch=None): + def __init__(self, parent=None, dispatch=None) -> None: super().__init__(parent) uiPath = getUiFile(__file__) @@ -262,7 +262,7 @@ def __init__(self, parent=None, dispatch=None): type(self).openedDialogs.append(weakref.ref(self)) @classmethod - def lastOpenedDialog(cls): + def lastOpenedDialog(cls) -> SimplexDialog | None: """Returns the last currently opened dialog that still exists""" for dlgRef in reversed(cls.openedDialogs): dlg = dlgRef() @@ -270,7 +270,7 @@ def lastOpenedDialog(cls): return dlg return None - def showTraversalDialog(self): + def showTraversalDialog(self) -> None: """Display the traversal dialog""" if self.simplex is None: return @@ -278,7 +278,7 @@ def showTraversalDialog(self): self.travDialog.show() self.travDialog.setGeometry(30, 30, 400, 400) - def showFalloffDialog(self): + def showFalloffDialog(self) -> None: """Display the Falloff Dialog""" if self.simplex is None: return @@ -289,40 +289,40 @@ def showFalloffDialog(self): if x < 0 or y < 0: self.falloffDialog.move(max(x, 0), max(y, 0)) - def dragStart(self): + def dragStart(self) -> None: """Slot for handling the start of a MMB Drag event""" if self.simplex is not None: self.simplex.DCC.undoOpen() - def dragStop(self): + def dragStop(self) -> None: """Slot for handling the end of a MMB Drag event""" if self.simplex is not None: self.simplex.DCC.undoClose() - def storeSettings(self): + def storeSettings(self) -> None: """Store the state of the UI for the next run""" pref = Prefs() pref.recordProperty("geometry", self.saveGeometry()) pref.save() - def loadSettings(self): + def loadSettings(self) -> None: """Load the state of the UI from a previous run""" pref = Prefs() geo = pref.restoreProperty("geometry", None) if geo is not None: self.restoreGeometry(geo) - def closeEvent(self, event): + def closeEvent(self, event) -> None: """Handle the close event""" self.storeSettings() self.deleteLater() # Undo/Redo - def newScene(self): + def newScene(self) -> None: """Call this before a new scene is created. Usually called from the stack""" self.clearSelectedObject() - def handleUndo(self): + def handleUndo(self) -> None: """Call this after an undo/redo action. Usually called from the stack""" if self.simplex is None: return @@ -334,7 +334,7 @@ def handleUndo(self): self.uiSliderTREE.setItemExpansion() self.uiComboTREE.setItemExpansion() - def currentSystemChanged(self, idx): + def currentSystemChanged(self, idx: int) -> None: """Slot called when the current system changes""" if idx == -1: self.setSystem(None) @@ -354,7 +354,7 @@ def currentSystemChanged(self, idx): self.setSystem(system) pBar.close() - def setSystem(self, system): + def setSystem(self, system: Simplex | None) -> None: """Set the system on this UI Parameters @@ -432,7 +432,7 @@ def setSystem(self, system): self.simplexLoaded.emit() # UI Setup - def _makeConnections(self): + def _makeConnections(self) -> None: """Make all the ui connections""" # Setup Trees! self.uiSliderTREE.setColumnWidth(1, 50) @@ -541,14 +541,14 @@ def getCurrentObject(self): return self._currentObject # Setup Trees! - def sliderStringFilter(self): + def sliderStringFilter(self) -> None: """Set the filter for the slider tree""" filterString = str(self.uiSliderFilterLINE.text()) sliderModel = self.uiSliderTREE.model() sliderModel.filterString = str(filterString) sliderModel.invalidateFilter() - def comboStringFilter(self): + def comboStringFilter(self) -> None: """Set the filter for the combo tree""" filterString = str(self.uiComboFilterLINE.text()) comboModel = self.uiComboTREE.model() @@ -556,7 +556,7 @@ def comboStringFilter(self): comboModel.invalidateFilter() # selection setup - def unifySliderSelection(self): + def unifySliderSelection(self) -> None: """Clear the selection of the combo tree when an item on the slider tree is selected """ @@ -572,7 +572,7 @@ def unifySliderSelection(self): comboSelModel.clearSelection() self.uiComboTREE.viewport().update() - def unifyComboSelection(self): + def unifyComboSelection(self) -> None: """Clear the selection of the slider tree when an item on the combo tree is selected """ @@ -588,14 +588,14 @@ def unifyComboSelection(self): self.uiSliderTREE.viewport().update() # dependency setup - def setLockComboRequirement(self): + def setLockComboRequirement(self) -> None: """Refresh the combo selection filter with the new filterLock state""" comboModel = self.uiComboTREE.model() if not comboModel: return self.populateComboRequirements() - def populateComboRequirements(self): + def populateComboRequirements(self) -> None: """Let the combo tree know the requirements from the slider tree""" items = self.uiSliderTREE.getSelectedItems(Slider) comboModel = self.uiComboTREE.model() @@ -609,7 +609,7 @@ def populateComboRequirements(self): ): comboModel.invalidateFilter() - def enableComboRequirements(self): + def enableComboRequirements(self) -> None: """Set the requirements for the combo filter model""" comboModel = self.uiComboTREE.model() if not comboModel: @@ -622,7 +622,7 @@ def enableComboRequirements(self): ) comboModel.invalidateFilter() - def populateSliderRequirements(self): + def populateSliderRequirements(self) -> None: """Let the slider tree know the requirements from the combo tree""" items = self.uiComboTREE.getSelectedItems(Combo) sliderModel = self.uiSliderTREE.model() @@ -632,14 +632,14 @@ def populateSliderRequirements(self): if sliderModel.filterRequiresAny or sliderModel.filterRequiresAll: sliderModel.invalidateFilter() - def setLockSliderRequirements(self): + def setLockSliderRequirements(self) -> None: """Refresh the combo selection filter with the new filterLock state""" sliderModel = self.uiSliderTREE.model() if not sliderModel: return self.populateSliderRequirements() - def enableSliderRequirements(self): + def enableSliderRequirements(self) -> None: sliderModel = self.uiSliderTREE.model() if not sliderModel: return @@ -654,7 +654,7 @@ def enableSliderRequirements(self): sliderModel.invalidateFilter() # Bottom Left Corner Buttons - def zeroAllSliders(self): + def zeroAllSliders(self) -> None: """Slot to Zero all Sliders in Slider UI panel""" if self.simplex is None: return @@ -663,7 +663,7 @@ def zeroAllSliders(self): self.simplex.setSlidersWeights(sliders, weights) self.uiSliderTREE.repaint() - def zeroSelectedSliders(self): + def zeroSelectedSliders(self) -> None: """Slot to Zero the selected sliders in the UI panel""" if self.simplex is None: return @@ -672,13 +672,13 @@ def zeroSelectedSliders(self): self.simplex.setSlidersWeights(items, values) self.uiSliderTREE.repaint() - def selectCtrl(self): + def selectCtrl(self) -> None: """Select the Control object in the DCC""" if self.simplex is None: return self.simplex.DCC.selectCtrl() - def autoSetSliders(self): + def autoSetSliders(self) -> None: """Automatically set any selected sliders to 1.0""" if self.simplex is None: return @@ -694,7 +694,7 @@ def autoSetSliders(self): self.simplex.setSlidersWeights(sliders, weights) self.uiSliderTREE.repaint() - def _getAName(self, tpe, default=None, taken=(), uniqueAccept=False): + def _getAName(self, tpe: str, default=None, taken=(), uniqueAccept=False) -> str | None: """uniqueAccept forces the user to provide and accept a unique name If the user enters a non-unique name, the name is uniquified, and the dialog is re-shown with the unique name as the default suggestion @@ -728,7 +728,7 @@ def _getAName(self, tpe, default=None, taken=(), uniqueAccept=False): return default # Top Left Corner Buttons - def newSliderGroup(self): + def newSliderGroup(self) -> None: """Slot to Create a new slider group""" if self.simplex is None: return @@ -739,7 +739,7 @@ def newSliderGroup(self): Group.createGroup(str(newName), self.simplex, groupType=Slider) - def newSlider(self): + def newSlider(self) -> None: """Slot to create a new slider""" if self.simplex is None: return @@ -754,7 +754,7 @@ def newSlider(self): Slider.createSlider(str(newName), self.simplex, group=group) - def newSliderShape(self): + def newSliderShape(self) -> None: """Slot to create a new Shape in a Slider's progression""" pars = self.uiSliderTREE.getSelectedItems(Slider) if not pars: @@ -763,7 +763,7 @@ def newSliderShape(self): parItem.createShape() self.uiSliderTREE.model().invalidateFilter() - def sliderTreeDelete(self): + def sliderTreeDelete(self) -> None: """Delete some objects in the slider tree""" idxs = self.uiSliderTREE.getSelectedIndexes() roots = coerceIndexToRoots(idxs) @@ -783,7 +783,7 @@ def sliderTreeDelete(self): self.uiSliderTREE.model().invalidateFilter() # Top Right Corner Buttons - def comboTreeDelete(self): + def comboTreeDelete(self) -> None: """Delete some objects in the Combo tree""" idxs = self.uiComboTREE.getSelectedIndexes() roots = coerceIndexToRoots(idxs) @@ -802,7 +802,7 @@ def comboTreeDelete(self): r.delete() self.uiComboTREE.model().invalidateFilter() - def _newCombo(self, sliders, values): + def _newCombo(self, sliders, values) -> None: if len(sliders) < 2: message = "A combo must use at least 2 sliders" QMessageBox.warning(self, "Warning", message) @@ -812,7 +812,7 @@ def _newCombo(self, sliders, values): ccd.move(self.pos()) execwid(ccd) - def newActiveCombo(self): + def newActiveCombo(self) -> None: """Create a combo based on the UI Sliders that are currently nonzero""" if self.simplex is None: return @@ -824,7 +824,7 @@ def newActiveCombo(self): values[s] = [s.value] self._newCombo(sliders, values) - def newSelectedCombo(self): + def newSelectedCombo(self) -> None: """Create a combo based on the currently selected UI sliders""" if self.simplex is None: return @@ -832,7 +832,7 @@ def newSelectedCombo(self): values = {s: [1.0] for s in sliders} self._newCombo(sliders, values) - def newComboShape(self): + def newComboShape(self) -> None: """Create a new shape in the Combo's progression""" parIdxs = self.uiComboTREE.getSelectedIndexes() pars = coerceIndexToParentType(parIdxs, Combo) @@ -843,7 +843,7 @@ def newComboShape(self): parItem.createShape() self.uiComboTREE.model().invalidateFilter() - def newComboGroup(self): + def newComboGroup(self) -> None: """Create a new group for organizing combos""" if self.simplex is None: return @@ -855,7 +855,7 @@ def newComboGroup(self): Group.createGroup(str(newName), self.simplex, groupType=Combo) # Bottom right corner buttons - def setSliderVals(self): + def setSliderVals(self) -> None: """Set all slider values to those stored in the currently selected Combos""" if self.simplex is None: return @@ -873,7 +873,7 @@ def setSliderVals(self): self.simplex.setSlidersWeights(sliders, values) self.uiSliderTREE.repaint() - def selectSliders(self): + def selectSliders(self) -> None: """Select the sliders that are contained in the currently selected Combos""" combos = self.uiComboTREE.getSelectedItems(Combo) sliders = [] @@ -882,7 +882,7 @@ def selectSliders(self): sliders.append(pair.slider) self.uiSliderTREE.setItemSelection(sliders) - def autoSetComboSliders(self): + def autoSetComboSliders(self) -> None: """Automatically set the DCC Slider values to activate the currently selected Combos""" if self.simplex is None: return @@ -903,7 +903,7 @@ def autoSetComboSliders(self): self.uiSliderTREE.repaint() # Extraction/connection - def shapeConnectScene(self): + def shapeConnectScene(self) -> None: """Connect any selected meshes into the system based on the name""" if self.simplex is None: return @@ -961,13 +961,13 @@ def shapeExtract(self): comboIdxs = self.uiComboTREE.getSelectedIndexes() return self.shapeIndexExtract(sliderIdxs + comboIdxs) - def shapeExport(self): + def shapeExport(self) -> None: """Export meshes""" sliderIdxs = self.uiSliderTREE.getSelectedIndexes() comboIdxs = self.uiComboTREE.getSelectedIndexes() return self.shapeIndexExport(sliderIdxs + comboIdxs) - def shapeIndexExport(self, indexes): + def shapeIndexExport(self, indexes) -> None: """Export meshes Parameters @@ -1070,13 +1070,13 @@ def shapeIndexExtract(self, indexes, live=None): pBar.close() return extracted - def shapeConnect(self): + def shapeConnect(self) -> None: """Match any selected Shapes to DCC meshes based on their names, then delete the Meshes""" sliderIdxs = self.uiSliderTREE.getSelectedIndexes() comboIdxs = self.uiComboTREE.getSelectedIndexes() self.shapeConnectIndexes(sliderIdxs + comboIdxs) - def shapeConnectIndexes(self, indexes): + def shapeConnectIndexes(self, indexes) -> None: """Match the provided shapes to DCC meshes based on their names, then delete the Meshes Parameters @@ -1106,13 +1106,13 @@ def shapeConnectIndexes(self, indexes): pBar.close() - def shapeMatch(self): + def shapeMatch(self) -> None: """Match any selected Shapes to A selected DCC mesh""" sliderIdxs = self.uiSliderTREE.getSelectedIndexes() comboIdxs = self.uiComboTREE.getSelectedIndexes() self.shapeMatchIndexes(sliderIdxs + comboIdxs) - def shapeMatchIndexes(self, indexes): + def shapeMatchIndexes(self, indexes) -> None: """Match any provided shapes to A selected DCC mesh Parameters @@ -1148,13 +1148,13 @@ def shapeMatchIndexes(self, indexes): pBar.close() - def shapeClear(self): + def shapeClear(self) -> None: """Match all selected shapes to the rest""" sliderIdxs = self.uiSliderTREE.getSelectedIndexes() comboIdxs = self.uiComboTREE.getSelectedIndexes() self.shapeClearIndexes(sliderIdxs + comboIdxs) - def shapeClearIndexes(self, indexes): + def shapeClearIndexes(self, indexes) -> None: """Match all provided shapes to the rest Parameters @@ -1170,7 +1170,7 @@ def shapeClearIndexes(self, indexes): pair.shape.zeroShape() # System level - def loadObject(self, thing): + def loadObject(self, thing) -> None: """Load a DCC mesh into the UI Parameters @@ -1198,7 +1198,7 @@ def loadObject(self, thing): name = d["systemName"] self.uiCurrentSystemCBOX.addItem(name, (self._currentObject, name)) - def currentObjectChanged(self): + def currentObjectChanged(self) -> None: """Slot called when the current DCC object is changed""" name = str(self.uiCurrentObjectTXT.text()) if self._currentObjectName == name: @@ -1212,7 +1212,7 @@ def currentObjectChanged(self): self.loadObject(newObject) - def getSelectedObject(self): + def getSelectedObject(self) -> None: """Load the first selected DCC object into the UI""" sel = DCC.getSelectedObjects() if not sel: @@ -1222,7 +1222,7 @@ def getSelectedObject(self): return self.loadObject(newObj) - def clearSelectedObject(self): + def clearSelectedObject(self) -> None: """Unload the current DCC object from the UI""" self.uiClearSelectedObjectBTN.hide() self.uiCurrentSystemCBOX.clear() @@ -1232,7 +1232,7 @@ def clearSelectedObject(self): self.setSystem(None) # Clear the current system - def newSystem(self): + def newSystem(self) -> None: """Create a new system on the current DCC Object""" if self._currentObject is None: QMessageBox.warning(self, "Warning", "Must have a current object selection") @@ -1252,7 +1252,7 @@ def newSystem(self): ) self.setSystem(newSystem) - def renameSystem(self): + def renameSystem(self) -> None: """Rename the current Simplex system""" if self.simplex is None: return @@ -1271,14 +1271,14 @@ def renameSystem(self): self.currentSystemChanged(idx) - def setSimplexLegacy(self): + def setSimplexLegacy(self) -> None: """Slot to toggle the legacy behavior of the current Simplex system""" if self.simplex is not None: self.simplex.setLegacy(self.uiLegacyJsonACT.isChecked()) self.simplex.DCC.incrementRevision() # File Menu - def importSystemFromFile(self): + def importSystemFromFile(self) -> None: """Open a File Dialog to load a simplex system from a file. Systems can be in either .smpx, or .json formats """ @@ -1299,7 +1299,7 @@ def importSystemFromFile(self): self.loadFile(path) - def loadFile(self, path): + def loadFile(self, path) -> None: pBar = QProgressDialog("Loading Shapes", "Cancel", 0, 100, self) pBar.show() QApplication.processEvents() @@ -1342,7 +1342,7 @@ def loadFile(self, path): pBar.close() self.simplex.DCC.checkForErrors(self) - def _fileDialog(self, title, initPath, filters, save=True): + def _fileDialog(self, title: str, initPath: str, filters: list[str], save=True): """Convenience function for displaying File Dialogs""" filters = [f"{f} (*.{f})" for f in filters] if not save: @@ -1366,7 +1366,7 @@ def _fileDialog(self, title, initPath, filters, save=True): return path - def exportSystemTemplate(self): + def exportSystemTemplate(self) -> None: """Open a file dialog and export a system to the chosen path""" if self._currentObject is None: QMessageBox.warning(self, "Warning", "Must have a current object selection") @@ -1399,7 +1399,7 @@ def exportSystemTemplate(self): f.write(dump) # Slider Settings - def setSelectedSliderGroups(self, group): + def setSelectedSliderGroups(self, group) -> None: """Set the group for the selected Sliders Parameters @@ -1413,7 +1413,7 @@ def setSelectedSliderGroups(self, group): group.take(sliders) self.uiSliderTREE.viewport().update() - def setSelectedSliderFalloff(self, falloff, state): + def setSelectedSliderFalloff(self, falloff, state) -> None: """Set the Falloffs for the selected Sliders Parameters @@ -1433,7 +1433,7 @@ def setSelectedSliderFalloff(self, falloff, state): s.prog.removeFalloff(falloff) self.uiSliderTREE.viewport().update() - def setSelectedSliderInterp(self, interp): + def setSelectedSliderInterp(self, interp) -> None: """Set the interpolation for the selected sliders Parameters @@ -1447,7 +1447,7 @@ def setSelectedSliderInterp(self, interp): s.prog.interp = interp # Combo Settings - def setSelectedComboGroups(self, group): + def setSelectedComboGroups(self, group) -> None: """Set the group for the selected Combos Parameters @@ -1461,7 +1461,7 @@ def setSelectedComboGroups(self, group): group.take(combos) self.uiComboTREE.viewport().update() - def setSelectedComboSolveType(self, stVal): + def setSelectedComboSolveType(self, stVal) -> None: """Set the solve type for the selected combos Parameters @@ -1475,7 +1475,7 @@ def setSelectedComboSolveType(self, stVal): c.solveType = stVal # Edit Menu - def hideRedundant(self): + def hideRedundant(self) -> None: """Hide redundant items from the Slider and Combo trees based on a user preference""" if self.simplex is None: return @@ -1488,7 +1488,7 @@ def hideRedundant(self): sliderModel.doFilter = check sliderModel.invalidateFilter() - def setSliderRange(self): + def setSliderRange(self) -> None: """Double the range for the sliders *IN THE DCC ONLY* based on a user preference""" self._sliderMul = 2.0 if self.uiDoubleSliderRangeACT.isChecked() else 1.0 if self.simplex is None: @@ -1497,36 +1497,36 @@ def setSliderRange(self): self.simplex.DCC.setSlidersRange(self.simplex.sliders) # Isolation - def isSliderIsolate(self): + def isSliderIsolate(self) -> bool: """Check if the slider tree is currently isolated""" model = self.uiSliderTREE.model() if model: return bool(model.isolateList) return False - def sliderIsolateSelected(self): + def sliderIsolateSelected(self) -> None: """Isolate the selected Sliders in the Slider Tree""" self.uiSliderTREE.isolateSelected() self.uiSliderExitIsolateBTN.show() - def sliderTreeExitIsolate(self): + def sliderTreeExitIsolate(self) -> None: """Disable isolation mode in the Slider Tree""" self.uiSliderTREE.exitIsolate() self.uiSliderExitIsolateBTN.hide() - def isComboIsolate(self): + def isComboIsolate(self) -> bool: """Check if the combo tree is currently isolated""" model = self.uiComboTREE.model() if model: return bool(model.isolateList) return False - def comboIsolateSelected(self): + def comboIsolateSelected(self) -> None: """Isolate the selected Combos in the Combo Tree""" self.uiComboTREE.isolateSelected() self.uiComboExitIsolateBTN.show() - def comboTreeExitIsolate(self): + def comboTreeExitIsolate(self) -> None: """Disable isolation mode in the Combo Tree""" self.uiComboTREE.exitIsolate() self.uiComboExitIsolateBTN.hide() diff --git a/src/python/simplexui/travCheckDialog.py b/src/python/simplexui/travCheckDialog.py index bea14a29..52fd6b72 100644 --- a/src/python/simplexui/travCheckDialog.py +++ b/src/python/simplexui/travCheckDialog.py @@ -47,7 +47,7 @@ class TooManyPossibilitiesError(Exception): def buildPossibleTraversals( - simplex, sliders, minDepth, maxDepth, lockDict=None, maxPoss=100 + simplex, sliders, minDepth: int, maxDepth: int, lockDict=None, maxPoss=100 ): """Build a list of possible traversals @@ -129,7 +129,7 @@ def buildPossibleTraversals( class TravCheckItem(QTreeWidgetItem): - def __init__(self, pairs, trav, *args, **kwargs): + def __init__(self, pairs, trav, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.pairs = pairs self.trav = trav @@ -201,10 +201,10 @@ def __init__( sliders, values=None, dynamics=None, - mode="create", + mode: str = "create", parent=None, grandparent=None, - ): + ) -> None: # Store the Parent UI rather than relying on Qt's .parent() # Could cause crashes otherwise if parent is None or grandparent is None: @@ -257,7 +257,7 @@ def __init__( else: self._populate() - def dragTick(self, ticks, mul): + def dragTick(self, ticks, mul) -> None: """Deal with the ticks coming from the drag handler Parameters @@ -281,7 +281,7 @@ def dragTick(self, ticks, mul): item.setData(3, Qt.ItemDataRole.EditRole, val) self.uiEditTREE.viewport().update() - def setSliders(self, val): + def setSliders(self, val) -> None: """Set the sliders displayed in this UI Parameters @@ -318,29 +318,29 @@ def setSliders(self, val): for col in reversed(list(range(4))): self.uiEditTREE.resizeColumnToContents(col) - def closeEvent(self, event): + def closeEvent(self, event) -> None: """Override the Qt close event""" self.gparUI.uiSliderTREE.selectionModel().selectionChanged.disconnect( self.populateWithCheck ) super().closeEvent(event) - def populateWithUpdate(self): + def populateWithUpdate(self) -> None: """Populate the list from the main dialog selection""" self.setSliders(self.gparUI.uiSliderTREE.getSelectedItems(typ=Slider)) self._populate() - def populateWithoutUpdate(self): + def populateWithoutUpdate(self) -> None: """Populate the list and but don't look at the main dialog""" self._populate() - def populateWithCheck(self): + def populateWithCheck(self) -> None: """Populate the list from the main dialog selection, only if the AutoUpdate checkbox is checked""" if self.uiAutoUpdateCHK.isChecked(): self.setSliders(self.gparUI.uiSliderTREE.getSelectedItems(typ=Slider)) self._populate() - def _populate(self): + def _populate(self) -> None: """Populate the list widgets in the UI""" minDepth = self.uiMinLimitSPIN.value() maxDepth = self.uiMaxLimitSPIN.value() @@ -385,7 +385,7 @@ def _populate(self): if self.uiTravCheckTREE.topLevelItemCount() > 0: self.uiTravCheckTREE.topLevelItem(0).setSelected(True) - def createMissing(self): + def createMissing(self) -> None: """Create selected traversals if they don't already exist""" simplex = self.parUI.simplex created = [] diff --git a/src/python/simplexui/traversalDialog.py b/src/python/simplexui/traversalDialog.py index 81d7dd2a..360d906d 100644 --- a/src/python/simplexui/traversalDialog.py +++ b/src/python/simplexui/traversalDialog.py @@ -81,7 +81,7 @@ class TraversalDialog(QDialog): uiSetWorkingDirectoryACT: QAction uiSplitShapePSDACT: QAction - def __init__(self, parent): + def __init__(self, parent) -> None: super().__init__(parent) uiPath = getUiFile(__file__) @@ -114,7 +114,7 @@ def __init__(self, parent): self.loadSimplex() - def hideRedundant(self): + def hideRedundant(self) -> None: """Hide Redundant items in the ui based on the checkbox""" if self.simplex is None: return @@ -123,17 +123,17 @@ def hideRedundant(self): travModel.doFilter = check travModel.invalidateFilter() - def dragStart(self): + def dragStart(self) -> None: """Slot for handling the start of a MMB Drag event""" if self.simplex is not None: self.simplex.DCC.undoOpen() - def dragStop(self): + def dragStop(self) -> None: """Slot for handling the end of a MMB Drag event""" if self.simplex is not None: self.simplex.DCC.undoClose() - def loadSimplex(self): + def loadSimplex(self) -> None: """Load the simplex system from the parent dialog""" system = self.parUI.simplex if system is None: @@ -173,7 +173,7 @@ def loadSimplex(self): travProxModel = TraversalFilterModel(travModel) self.uiTraversalTREE.setModel(travProxModel) - def deleteTrav(self): + def deleteTrav(self) -> None: """Delete the selected traversals""" idxs = self.uiTraversalTREE.getSelectedIndexes() roots = coerceIndexToRoots(idxs) @@ -198,7 +198,7 @@ def deleteTrav(self): self.uiTraversalTREE.model().invalidateFilter() - def newGroup(self): + def newGroup(self) -> None: """Create a new group for organizing the traversals""" if self.simplex is None: return @@ -215,7 +215,7 @@ def newGroup(self): items = self.uiTraversalTREE.getSelectedItems(Slider) Group.createGroup(str(newName), self.simplex, items) - def newShape(self): + def newShape(self) -> None: """Add a new shape to the traversal's Progression""" pars = self.uiTraversalTREE.getSelectedIndexes() if not pars: @@ -230,7 +230,7 @@ def shapeExtract(self): indexes = self.uiTraversalTREE.getSelectedIndexes() return self.parUI.shapeIndexExtract(indexes) - def newTrav(self): + def newTrav(self) -> None: """Create a new traversal based on the selection in the main UI""" sliders = self.parUI.uiSliderTREE.getSelectedItems(Slider) if len(sliders) < 2: @@ -244,7 +244,7 @@ def newTrav(self): tcd.move(self.pos()) execwid(tcd) - def addSlider(self): + def addSlider(self) -> None: """Add a slider to the traversal's definition""" # add the slider to both the start and end travs = self.uiTraversalTREE.getSelectedItems(Traversal) @@ -256,7 +256,7 @@ def addSlider(self): for slider in sliders: travs[-1].addSlider(slider) - def shapeConnectFromSelection(self): + def shapeConnectFromSelection(self) -> None: """Connect a shape into the traversal based on the DCC scene selection""" if self.simplex is None: return diff --git a/src/python/simplexui/utils.py b/src/python/simplexui/utils.py index d6213110..d79e1cc0 100755 --- a/src/python/simplexui/utils.py +++ b/src/python/simplexui/utils.py @@ -32,14 +32,14 @@ AT_BLUR = os.environ.get("SIMPLEX_AT_BLUR") == "true" -def execmenu(act: QMenu, pos: QPoint): +def execmenu(act: QMenu, pos: QPoint) -> None: if IsPySide6 or IsPyQt6: act.exec(pos) else: act.exec_() -def execwid(wid): +def execwid(wid) -> None: if IsPySide6 or IsPyQt6: wid.exec() else: @@ -101,7 +101,7 @@ def getNextName(name: str, currentNames: Sequence[str]) -> str: return name -def clearPathSymbols(paths: list[str], keepers: list[str] | None = None): +def clearPathSymbols(paths: list[str], keepers: list[str] | None = None) -> None: """Removes path symbols from the environment. This means I can unload my tools from the current process and re-import them @@ -192,7 +192,7 @@ class singleShot(QObject): a snappy user interface """ - def __init__(self): + def __init__(self) -> None: super().__init__() self._function: Callable | None = None self._callScheduled: bool = False @@ -202,7 +202,7 @@ def __init__(self): def __call__(self, function: F) -> F: self._function = function - def newFunction(inst, *args): + def newFunction(inst, *args) -> None: self._args.extend(args) self._args = makeUnique(self._args) if not self._callScheduled: @@ -217,7 +217,7 @@ def newFunction(inst, *args): # and autocomplete features for the caller return cast(F, newFunction) - def callback(self): + def callback(self) -> None: """Calls the decorated function and resets singleShot for the next group of calls""" self._callScheduled = False # self._args needs to be cleared before we call self._function @@ -250,7 +250,9 @@ def makeUnique(seq: Sequence[T]) -> Sequence[T]: return [x for x in seq if not (x in seen or seen_add(x))] -def naturalSortKey(s: str, _nsre=re.compile("([0-9]+)")) -> list[str | int]: +def naturalSortKey( + s: str, _nsre: re.Pattern[str] = re.compile("([0-9]+)") +) -> list[str | int]: """Get a sort key that puts strings with numbers in numerical order This is accomplished by splitting the string into groups of digits, and non-digits, then converting the digit groups into integers. @@ -278,7 +280,7 @@ def getIcon(iconName: str) -> QIcon: class Prefs: """A wrapper for reading/writing prefs both internal and external to blur""" - def __init__(self): + def __init__(self) -> None: if AT_BLUR: import blurdev.prefs @@ -292,13 +294,13 @@ def restoreProperty(self, prop: str, default: str | None = None) -> Any: else: return self._pref.restoreProperty(prop, default) - def recordProperty(self, prop: str, val: Any): + def recordProperty(self, prop: str, val: Any) -> None: if isinstance(self._pref, QSettings): self._pref.setValue(prop, val) else: self._pref.recordProperty(prop, val) - def save(self): + def save(self) -> None: if isinstance(self._pref, QSettings): self._pref.sync() else: From 1216d927dc4a9d6b3cf5d3da006a7c40d55b4e54 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 15 Sep 2026 16:02:31 -0700 Subject: [PATCH 43/47] Fix the export speed issue --- .../simplexui/commands/alembicCommon.py | 25 ++++++++++++++++++ .../simplexui/interface/dummyInterface.py | 7 ++--- .../simplexui/interface/mayaInterface.py | 26 +++---------------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index cb10277b..8fff6edb 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -324,6 +324,31 @@ def setAlembicSample( omeshSch.set(s) +def getSampleArrayIndex( + imesh: IPolyMesh, index: int = 0, pBar: QProgressDialog | None = None +) -> npfloat: + """Get the per-frame vertex positions for a mesh + + Parameters + ---------- + imesh : IPolyMesh + The input alembic mesh object + index: int + The sample index to retrieve + + Returns + ------- + : np.array or list + The per-frame vertex positions + """ + meshSchema = imesh.getSchema() + posProp = meshSchema.getPositionsProperty() + s = posProp.samples[index] + if arrayToNumpy is not None: + return arrayToNumpy(s).copy() + return np.array([list(s.x), list(s.y), list(s.z)]).T + + def getSampleArray(imesh: IPolyMesh, pBar: QProgressDialog | None = None) -> npfloat: """Get the per-frame vertex positions for a mesh diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index 7c01523d..119cdaa8 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -30,6 +30,7 @@ from ..commands.alembicCommon import ( getSampleArray, + getSampleArrayIndex, getStaticMeshData, getUvSample, mkSampleIntArray, @@ -383,11 +384,7 @@ def buildRestAbc(abcMesh, name) -> DummyMesh: The name of the object to create """ mesh = DummyMesh(name) # don't add it to a scene - - sa = getSampleArray(abcMesh) - if len(sa.shape) == 3: - sa = sa[0] - mesh.verts = sa + mesh.verts = getSampleArrayIndex(abcMesh, index=0) faces, counts = getStaticMeshData(abcMesh) uvs = getUvSample(abcMesh) diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 3d22c082..3d3fe9d3 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -38,13 +38,8 @@ ) from ..commands.alembicCommon import buildAbc, mkSampleVertexPoints +from ..commands.mayatonumpy import mayaToNumpy -try: - from ..commands.mayatonumpy import mayaToNumpy - from ..commands.numpytoimath import numpyToImath -except ImportError: - numpyToImath = None - mayaToNumpy = None if TYPE_CHECKING: from ..items.simplex import Simplex, Shape @@ -622,20 +617,9 @@ def getAllShapeVertices(self, shapes, pBar=None) -> None: QApplication.processEvents() cmds.setAttr(shape.thing, 1.0) - - if np is not None and mayaToNumpy is not None: - mpts = om.MPointArray() - meshFn.getPoints(mpts) - out = mayaToNumpy(mpts)[:, :3] - else: - flatverts = cmds.xform( - f"{self.mesh}.vtx[*]", - translation=1, - query=1, - worldSpace=False, - ) - args = [iter(flatverts)] * 3 - out = list(zip(*args)) + mpts = om.MPointArray() + meshFn.getPoints(mpts) + out = mayaToNumpy(mpts)[:, :3] cmds.setAttr(shape.thing, 0.0) shape.verts = out @@ -762,8 +746,6 @@ def getNumpyShape(cls, mesh: str, world=False): : np.array The point positions of the mesh """ - if np is None or mayaToNumpy is None: - raise RuntimeError("Can't do numpy stuff if its not importable") vts = cls._getMeshVertices(mesh, world=world) ret = mayaToNumpy(vts) return ret[..., :3].copy() From 339237364f53d719d4263b8ab14ec36d58250ea4 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 15 Sep 2026 17:44:00 -0700 Subject: [PATCH 44/47] Add a bunch of alembic stuff, including the walker base class and types to go with it --- .../simplexui/commands/alembicCommon.py | 43 +++ .../simplexui/commands/alembic_walker.py | 299 ++++++++++++++++++ .../commands/replaceDefinitionJson.py | 74 +++++ .../simplexui/interface/dummyInterface.py | 2 +- .../simplexui/interface/mayaInterface.py | 88 +++--- .../simplexui/menu/mayaPlugins/importObjs.py | 4 +- typings/alembic/Abc.pyi | 66 +++- 7 files changed, 512 insertions(+), 64 deletions(-) create mode 100644 src/python/simplexui/commands/alembic_walker.py create mode 100644 src/python/simplexui/commands/replaceDefinitionJson.py diff --git a/src/python/simplexui/commands/alembicCommon.py b/src/python/simplexui/commands/alembicCommon.py index 8fff6edb..479fdbbb 100644 --- a/src/python/simplexui/commands/alembicCommon.py +++ b/src/python/simplexui/commands/alembicCommon.py @@ -1103,3 +1103,46 @@ def readFalloffData(abcPath: str) -> dict[str, npfloat]: iarch, top, par, systemSchema, foPropPar, foProp = [None] * 6 return foDict + + +def getIArchive(filepath: str | list[str]) -> IArchive: + """Get the IArchive from the given path, but check if the file exists + first and raise an appropriate error if it doesn't + + Parameters + ---------- + abcPath : str + Path to the .smpx file + + Returns + ------- + IArchive : + The opened IArchive + """ + if not filepath: + raise ValueError("Invalid Filepath: {0}".format(filepath)) + + # Alembic 1.7+ allows for opening multiple caches at the same time + # since I use that, I will just *always* use a list for consistency + if isinstance(filepath, str): + filepath = [filepath] + + if not isinstance(filepath, (list, tuple)): + raise TypeError("Invalid Filepath Type: {0}".format(filepath)) + + # Make sure all the paths are stringified so alembic doesn't complain + # Alembic crashes with unicode filepaths in py2 + filepath = [str(i) for i in filepath] + + # Check for file existence + for fp in filepath: + if not os.path.exists(str(fp)): + raise OSError("Filepath does not exist: {0}".format(fp)) + + try: + return IArchive(filepath) + except Exception as e: + # Make sure to include the filepath list in any other errors that alembic throws + errArgs = list(e.args) + errArgs[0] = f"{errArgs[0]}: {filepath}" + raise type(e)(*errArgs) from e diff --git a/src/python/simplexui/commands/alembic_walker.py b/src/python/simplexui/commands/alembic_walker.py new file mode 100644 index 00000000..4329ee6f --- /dev/null +++ b/src/python/simplexui/commands/alembic_walker.py @@ -0,0 +1,299 @@ +from __future__ import annotations +from typing import cast, Iterator + +from .alembicCommon import getIArchive + +from alembic.Abc import ( + IArchive, + OArchive, + IObject, + OObject, + IProperty, + OProperty, + OCompoundProperty, + OScalarProperty, + OArrayProperty, + ICompoundProperty, +) + + +class AlembicWalker(object): + """This class recursively walks an alembic hierarchy, copying + all objects and properties along the way. + + It's written as a class to allow for overriding methods used in the + copying process. To add extra decision making and archive mutation + + copy_alembic Takes input and output filepaths and kicks off the copy process + copy_hierarchy takes input and output alembic objects and copies the hierarchy + from the input to the output + copy_object_properties copies the property data from one object to another. + copy_property_data copies the the data from input to output + + make_obj_walk is the recursive generator function that walks the object hierarchy + depth-first, and creates the empty output objects with the correct type metadata + that copy_object_properties will fill + make_prop_walk is the recursive generator function that walks the property hierarchy + depth-first, and creates the empty output properties of the correct type that + copy_property_data fills + """ + + @classmethod + def make_prop_walk( + cls, + iPar: ICompoundProperty, + oPar: OCompoundProperty, + depth: int = 0, + name: list[str] | None = None, + debug: bool = False, + *args, + **kwargs, + ) -> Iterator[tuple[IProperty, OProperty, int, list[str]]]: + """Generator for depth-first iteration and duplication of the + properties of an alembic object. Every input property is recreated with the + same name and type as an output object, then yielded from the method + + You would override this method if you needed to add or remove certain properties + from the objects when processing the input archive + + Arguments: + iPar (ICompoundProperty): The parent input property whose sub-properties we will + iterate over + oPar (OCompoundProperty): The output property that will act as the container + for all the newly created properties + depth (int): The current depth of the hierarchy + name (list): The full path-name of the current property. This is the name + of all parent container properties as a list + debug (bool): Whether to print the debug messages + args/kwargs : Additional arguments that will get passed down the stack + This saves you from having to override all methods to pass a single + argument to the bottom of the call stack + + Yields: + iProperty: An input property to copy sub-properties and values from + oProperty: An output property to copy sub-properties and values to + int: The current depth of the objects in the hierarchy + list: The current path-name of the properties to be copied + """ + name = name or [] + num = iPar.getNumProperties() + pfx = " " * depth + for i in range(num): + iProp = iPar.getProperty(i) + nextName = name + [iProp.getName()] + + if iProp.isCompound(): + if debug: + print(pfx, iProp.getName()) + oProp = OCompoundProperty(oPar, iProp.getName(), iProp.getMetaData()) + elif iProp.isArray(): + if debug: + print(pfx, iProp.getName()) + oProp = OArrayProperty( + oPar, iProp.getName(), iProp.getDataType(), iProp.getMetaData() + ) + else: + if debug: + print(pfx, iProp.getName()) + oProp = OScalarProperty( + oPar, iProp.getName(), iProp.getDataType(), iProp.getMetaData() + ) + + yield iProp, oProp, depth, nextName + if iProp.isCompound(): + iProp = cast(ICompoundProperty, iProp) + oProp = cast(OCompoundProperty, oProp) + + for ip, op, d, nn in cls.make_prop_walk( + iProp, oProp, depth + 1, nextName, *args, debug=debug, **kwargs + ): + yield ip, op, d, nn + + @classmethod + def make_obj_walk( + cls, + iPar: IObject, + oPar: OObject, + depth: int = 0, + debug: bool = False, + *args, + **kwargs, + ) -> Iterator[tuple[IObject, OObject, int]]: + """Generator for depth-first iteration and duplication over the + objects in an alembic file. Every input object is recreated with the same + name and metadata as an output object, then yielded from the method + + You would override this method if you needed to change the type of an + object, change the output hierarchy, or skip certain objects when + processing the input archive + + Arguments: + iPar (iObject): The parent input object whose children we will + iterate over + oPar (oObject): The output object that will act as the parent + for all the newly created duplicates of the iPar's children + depth (int): The current depth of the hierarchy + debug (bool): Whether to print the debug messages + args/kwargs : Additional arguments that will get passed down the stack + This saves you from having to override all methods to pass a single + argument to the bottom of the call stack + + Yields: + iObject: An object to copy properties from + oObject: An object to copy properties to + int: The current depth of the objects in the hierarchy + """ + num = iPar.getNumChildren() + pfx = " " * depth + for i in range(num): + iChild = iPar.getChild(i) + if debug: + print(pfx, iChild.getName()) + oChild = OObject(oPar, iChild.getName(), iChild.getMetaData()) + + yield iChild, oChild, depth + for ic, oc, d in cls.make_obj_walk( + iChild, oChild, depth + 1, *args, debug=debug, **kwargs + ): + yield ic, oc, d + + @classmethod + def copy_time_sampling( + cls, inProp: IProperty, outProp: OProperty, *args, **kwargs + ) -> None: + """Choose the output timeSampling corresponding to the input one + + Arguments: + inProp (IProperty): The property to copy the time sampling from + outProp (OProperty): The property to copy the time sampling to + args/kwargs : Additional arguments that will get passed down the stack + This saves you from having to override all methods to pass a single + argument to the bottom of the call stack + """ + iSampling = inProp.getTimeSampling() + outProp.setTimeSampling(iSampling) + + @classmethod + def copy_property_data( + cls, + inProp: IProperty, + outProp: OProperty, + name: list[str], + objDepth: int, + propDepth: int, + *args, + **kwargs, + ) -> None: + """Copy the data from an input property to an output property + + Override this method if you need to change the data that gets stored + on a specific property + + Arguments: + inProp (iProperty): The input property to copy data from + outProp (oProperty): The output property to copy data to + name (list): The full path-name of the property being copied + objDepth (int): The depth in the *object* hierarchy that the parent + object of this property is + propDepth (int): The depth of the *property* hierarchy that this + property is + """ + if not inProp.isCompound(): + cls.copy_time_sampling(inProp, outProp, *args, **kwargs) + for s in inProp.samples: + outProp.setValue(s) + + @classmethod + def copy_object_properties( + cls, + inObj: IObject, + outObj: OObject, + objDepth: int, + debug: bool = False, + *args, + **kwargs, + ) -> None: + """Copy an input object and all of its properties to an output object + + Override this method if you need to change the structure of the properties on an object + + Arguments: + inObj (iObject): The input object whose properties to copy from + outObj (oObject): The output object to recieve the copied properties + objDepth (int): The depth in the hierarchy these objects are + debug (bool): Whether to print debug messages + args/kwargs : Additional arguments that will get passed down the stack + This saves you from having to override all methods to pass a single + argument to the bottom of the call stack + """ + + iProps = inObj.getProperties() + oProps = outObj.getProperties() + for ip, op, propDepth, name in cls.make_prop_walk( + iProps, oProps, *args, debug=debug, **kwargs + ): + cls.copy_property_data(ip, op, name, objDepth, propDepth, *args, **kwargs) + + @classmethod + def copy_hierarchy( + cls, inPar: IObject, outPar: OObject, debug: bool = False, *args, **kwargs + ) -> None: + """Start off the copying of an object hierarchy + + Arguments: + inPar (iObject): The top-level input object to copy the hierarchy from + outPar (oObject): The top-level output object to copy the hierarchy to + debug (bool): Whether to print debug messages + args/kwargs : Additional arguments that will get passed down the stack + This saves you from having to override all methods to pass a single + argument to the bottom of the call stack + """ + for iChild, oChild, depth in cls.make_obj_walk( + inPar, outPar, *args, debug=debug, **kwargs + ): + cls.copy_object_properties( + iChild, oChild, depth, *args, debug=debug, **kwargs + ) + + @classmethod + def copy_archive_time_samplings( + cls, iArch: IArchive, oArch: OArchive, debug: bool = False, *args, **kwargs + ) -> None: + """Copy the time samplings from the iArch to the oArch + + Arguments: + iArch (IArchive): The input archive object + oArch (OArchive): The output archive object + debug (bool): Whether to print debug messages + args/kwargs : Additional arguments that will get passed down the stack + This saves you from having to override all methods to pass a single + argument to the bottom of the call stack + """ + for idx in range(iArch.getNumTimeSamplings()): + ts = iArch.getTimeSampling(idx) + oArch.addTimeSampling(ts) + + @classmethod + def copy_alembic( + cls, inPath: str, outPath: str, debug: bool = False, *args, **kwargs + ): + """Start off the copying of an alembic archive + + Override this method if you need to get extra data about the + archives before starting the copy process + + Arguments: + inPath (str): The path to the input archive to copy + outPath (str): The path where to create a the copied archive + debug (bool): Whether to print debug messages + args/kwargs : Additional arguments that will get passed down the stack + This saves you from having to override all methods to pass a single + argument to the bottom of the call stack + """ + iArch = getIArchive(inPath) + oArch = OArchive(str(outPath)) + cls.copy_archive_time_samplings(iArch, oArch, *args, debug=debug, **kwargs) + + iTop = iArch.getTop() + oTop = oArch.getTop() + cls.copy_hierarchy(iTop, oTop, *args, debug=debug, **kwargs) diff --git a/src/python/simplexui/commands/replaceDefinitionJson.py b/src/python/simplexui/commands/replaceDefinitionJson.py new file mode 100644 index 00000000..11063933 --- /dev/null +++ b/src/python/simplexui/commands/replaceDefinitionJson.py @@ -0,0 +1,74 @@ +# Copyright 2016, Blur Studio +# +# This file is part of Simplex. +# +# Simplex is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Simplex is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Simplex. If not, see . + + +from .alembic_walker import AlembicWalker + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from alembic.Abc import IProperty, OProperty + + +class ReplaceSmpxAttr(AlembicWalker): + @classmethod + def copy_property_data( + cls, + inProp: IProperty, + outProp: OProperty, + name: list[str], + objDepth: int, + propDepth: int, + *args, + **kwargs, + ) -> None: + """Copy the data from an input property to an output property + + Override this method if you need to change the data that gets stored + on a specific property + + Arguments: + inProp (iProperty): The input property to copy data from + outProp (oProperty): The output property to copy data to + name (list): The full path-name of the property being copied + objDepth (int): The depth in the *object* hierarchy that the parent + object of this property is + propDepth (int): The depth of the *property* hierarchy that this + property is + """ + + if name[-1] == 'simplex' and 'newjs' in kwargs: + if not inProp.isCompound(): + AlembicWalker.copy_time_sampling(inProp, outProp, *args, **kwargs) + for _ in inProp.samples: + outProp.setValue(kwargs['newjs']) + else: + AlembicWalker.copy_property_data( + inProp, outProp, name, objDepth, propDepth, *args, **kwargs + ) + + +def replaceDefinitionJson(inSmpxPath: str, outSmpxPath: str, newJsonString: str): + """Given a simplex file, make a copy of that file with the new json definition string + This is good for doing manual renames and edits of the json + + Args: + inSmpxPath (str): The filepath to the existing simplex + outSmpxPath (str): The filepath save the new simplex + newJsonString (str): The json string that will replace the old one + """ + ReplaceSmpxAttr.copy_alembic(inSmpxPath, outSmpxPath, newjs=newJsonString) diff --git a/src/python/simplexui/interface/dummyInterface.py b/src/python/simplexui/interface/dummyInterface.py index 119cdaa8..6342e65f 100644 --- a/src/python/simplexui/interface/dummyInterface.py +++ b/src/python/simplexui/interface/dummyInterface.py @@ -407,7 +407,7 @@ def buildRawTopology( mesh = DummyMesh(name) # don't add it to a scene aryType = list if np is None else np.array - mesh.points = aryType(points) + mesh.verts = aryType(points) mesh.faces = aryType(faces) mesh.counts = aryType(counts) if uvs is not None and uvFaces is not None: diff --git a/src/python/simplexui/interface/mayaInterface.py b/src/python/simplexui/interface/mayaInterface.py index 3d3fe9d3..2d1b9ebe 100644 --- a/src/python/simplexui/interface/mayaInterface.py +++ b/src/python/simplexui/interface/mayaInterface.py @@ -487,7 +487,7 @@ def buildRestAbc(cls, abcMesh, name): cmds.setAttr(abcNode + ".speed", 24) # Is this needed anymore? cmds.setAttr(abcNode + ".time", 0) - importHead, importHeadShape = cls.buildDummyMesh(f"{name}_SIMPLEX") + importHead, importHeadShape = cls.buildDummyMesh("{0}_SIMPLEX".format(name)) cmds.connectAttr(abcNode + ".outPolyMesh[0]", importHeadShape + ".inMesh") cmds.polyEvaluate(importHead, vertex=True) # Force a refresh @@ -630,17 +630,7 @@ def getShapeVertices(self, shape): for v in shapeCnx.values(): cmds.setAttr(v, 0.0) cmds.setAttr(shape.thing, 1.0) - if np is None: - flatverts = cmds.xform( - f"{self.mesh}.vtx[*]", - translation=1, - query=1, - worldSpace=False, - ) - args = [iter(flatverts)] * 3 - out = list(zip(*args)) - else: - out = self.getNumpyShape(self.mesh) + out = self.getNumpyShape(self.mesh) return out def pushAllShapeVertices(self, shapes, pBar=None) -> None: @@ -769,14 +759,10 @@ def _getMeshVertices(cls, mesh: str, world=False): @classmethod def _exportAbcVertices(cls, mesh, world=False): - if np is None or numpyToImath is None: - vts = cls._getMeshVertices(mesh, world=world) - vertices = V3fArray(vts.length()) - for i in range(vts.length()): - vertices[i] = (vts[i].x, vts[i].y, vts[i].z) - else: - vts = cls.getNumpyShape(mesh, world=world) - vertices = mkSampleVertexPoints(vts) + vts = cls._getMeshVertices(mesh, world=world) + vertices = V3fArray(vts.length()) + for i in range(vts.length()): + vertices[i] = (vts[i].x, vts[i].y, vts[i].z) return vertices @classmethod @@ -901,9 +887,8 @@ def exportAbc( if ensureCorrect: cmds.setAttr(self.shapeNode + ".envelope", envelope) - def exportOtherAbc( - self, dccMesh, abcMesh, js, world: bool = False, pBar=None - ) -> None: + def exportOtherAbc(self, dccMesh, abcMesh, js, world=False, pBar=None): + """ """ shapeNames = js["shapes"] if js["encodingVersion"] > 1: shapeNames = [i["name"] for i in shapeNames] @@ -912,7 +897,7 @@ def exportOtherAbc( pBar.show() pBar.setMaximum(len(shapeNames)) spacerName = "_" * max(list(map(len, shapeNames))) - pBar.setLabelText(f"Exporting:\n{spacerName}") + pBar.setLabelText("Exporting:\n{0}".format(spacerName)) QApplication.processEvents() # Get all the sliderVecs @@ -935,7 +920,7 @@ def exportOtherAbc( shpValArray = np.zeros((len(self.simplex.shapes), len(self.simplex.shapes))) for shpIdx, shape in enumerate(self.simplex.shapes): if pBar is not None: - pBar.setLabelText(f"Reading Full Shapes:\n{shape.name}") + pBar.setLabelText("Reading Full Shapes:\n{0}".format(shape.name)) pBar.setValue(shpIdx) QApplication.processEvents() if pBar.wasCanceled(): @@ -954,19 +939,43 @@ def exportOtherAbc( ary[np.isclose(ary, 0.0)] = 0.0 shpValArray[shpIdx] = ary + deltaShapeArray = self._collapseDeltas( + self.simplex, restVerts, shpValArray, fullShapes, pBar=pBar + ) + + # Finally write the outputs + faces, counts, uvs = self.getAbcFaces(dccMesh) + schema = abcMesh.getSchema() + for shpIdx, shape in enumerate(self.simplex.shapes): + if pBar is not None: + pBar.setLabelText("writing:\n{0}".format(shape.name)) + pBar.setValue(shpIdx) + QApplication.processEvents() + if pBar.wasCanceled(): + raise RuntimeError("Cancelled!") + shpVerts = restVerts + deltaShapeArray[shpIdx] + shpVerts = mkSampleVertexPoints(shpVerts) + if uvs is not None: + abcSample = OPolyMeshSchemaSample(shpVerts, faces, counts, uvs) + else: + abcSample = OPolyMeshSchemaSample(shpVerts, faces, counts) + schema.set(abcSample) + + @classmethod + def _collapseDeltas(cls, smpx, restVerts, shpValArray, fullShapes, pBar=None): # Figure out what order to build the deltas # so that the deltas exist when I try to combine them - ctrlOrder = self.simplex.controllersByDepth() + ctrlOrder = smpx.controllersByDepth() shapeOrder = [pp.shape for ctrl in ctrlOrder for pp in ctrl.prog.pairs] shapeOrder = [i for i in shapeOrder if not i.isRest] # Incrementally Build the numpy array of delta shapes # build deltaShapeArray as a 2d array because numpy is like 10x faster on 2d arrays - indexByShape = {v: k for k, v in enumerate(self.simplex.shapes)} - deltaShapeArray = np.zeros((len(self.simplex.shapes), len(restVerts) * 3)) + indexByShape = {v: k for k, v in enumerate(smpx.shapes)} + deltaShapeArray = np.zeros((len(smpx.shapes), len(restVerts) * 3)) for shpOrderIdx, shape in enumerate(shapeOrder): if pBar is not None: - pBar.setLabelText(f"Collapsing to Deltas:\n{shape.name}") + pBar.setLabelText("Collapsing to Deltas:\n{0}".format(shape.name)) pBar.setValue(shpOrderIdx) QApplication.processEvents() if pBar.wasCanceled(): @@ -979,25 +988,8 @@ def exportOtherAbc( ).flatten() # And move that 2d array back into 3d - deltaShapeArray = deltaShapeArray.reshape((len(self.simplex.shapes), -1, 3)) - - # Finally write the outputs - faces, counts, uvs = self.getAbcFaces(dccMesh) - schema = abcMesh.getSchema() - for shpIdx, shape in enumerate(self.simplex.shapes): - if pBar is not None: - pBar.setLabelText(f"writing:\n{shape.name}") - pBar.setValue(shpIdx) - QApplication.processEvents() - if pBar.wasCanceled(): - raise RuntimeError("Cancelled!") - shpVerts = restVerts + deltaShapeArray[shpIdx] - shpVerts = mkSampleVertexPoints(shpVerts) - if uvs is not None: - abcSample = OPolyMeshSchemaSample(shpVerts, faces, counts, uvs) - else: - abcSample = OPolyMeshSchemaSample(shpVerts, faces, counts) - schema.set(abcSample) + deltaShapeArray = deltaShapeArray.reshape((len(smpx.shapes), -1, 3)) + return deltaShapeArray def deleteObj(self, thing) -> None: """Delete the given object""" diff --git a/src/python/simplexui/menu/mayaPlugins/importObjs.py b/src/python/simplexui/menu/mayaPlugins/importObjs.py index cfc5e4bb..ab34fbca 100644 --- a/src/python/simplexui/menu/mayaPlugins/importObjs.py +++ b/src/python/simplexui/menu/mayaPlugins/importObjs.py @@ -157,8 +157,8 @@ def importObjList(simplex, paths: list[str], pBar, reorder=True) -> None: importReorderObjs(simplex, importOrder, pBar) else: importSimpleObjs(simplex, importOrder, pBar) - - pBar.close() + if pBar is not None: + pBar.close() def registerTool(window, menu) -> None: diff --git a/typings/alembic/Abc.pyi b/typings/alembic/Abc.pyi index 67e00301..3997efc0 100644 --- a/typings/alembic/Abc.pyi +++ b/typings/alembic/Abc.pyi @@ -1,49 +1,89 @@ -from typing import Any, List +from typing import Any from imath import Box3d, V3fArray, IntArray, V2fArray, UnsignedIntArray class IObject: - children: List['IObject'] + children: list['IObject'] def getName(self) -> str: ... - def getParent(self) -> 'IObject': ... + def getParent(self) -> IObject: ... def getMetaData(self) -> Any: ... + def getProperties(self) -> ICompoundProperty: ... + def getNumChildren(self) -> int: ... + def getChild(self, index: int) -> IObject: ... class OObject: - pass + def __init__(self, parent: OObject, name: str, metaData: Any = None) -> None: ... + def getProperties(self) -> OCompoundProperty: ... class IArchive: - def __init__(self, path: str) -> None: ... + def __init__(self, path: str | list[str]) -> None: ... def getTop(self) -> IObject: ... + def getNumTimeSamplings(self) -> int: ... + def getTimeSampling(self, idx: int) -> Any: ... class OArchive: def __init__(self, path: str, ogawa: bool = ...) -> None: ... def getTop(self) -> OObject: ... + def addTimeSampling(self, ts: Any) -> None: ... + +class OProperty: + def setTimeSampling(self, timeSampling: Any) -> None: ... + def setValue(self, value: Any) -> None: ... class IProperty: + def getName(self) -> str: ... def getValue(self) -> Any: ... + def getDataType(self) -> Any: ... + def isCompound(self) -> bool: ... + def isArray(self) -> bool: ... + def getMetaData(self) -> Any: ... + def getTimeSampling(self) -> Any: ... + samples: list[Any] -class ICompoundProperty: +class ICompoundProperty(IProperty): def valid(self) -> bool: ... def getProperty(self, str) -> IProperty: ... + def getNumProperties(self) -> int: ... -class OCompoundProperty: ... +class OCompoundProperty(OProperty): + def __init__( + self, parent: OCompoundProperty, name: str, metaData: Any = None + ) -> None: ... -class OStringProperty: +class OStringProperty(OProperty): def __init__(self, props: OCompoundProperty, key: str) -> None: ... def setValue(self, value: str) -> None: ... -class OBox3dProperty: +class OScalarProperty(OProperty): + def __init__( + self, + parent: OCompoundProperty, + name: str, + dataType: Any = None, + metaData: Any = None, + ) -> None: ... + +class OArrayProperty(OProperty): + def __init__( + self, + parent: OCompoundProperty, + name: str, + dataType: Any = None, + metaData: Any = None, + ) -> None: ... + +class OBox3dProperty(OProperty): def setValue(self, value: Box3d) -> None: ... -class IP3fArrayProperty: +class IP3fArrayProperty(IProperty): samples: list[V3fArray] -class IInt32ArrayProperty: +class IInt32ArrayProperty(IProperty): samples: list[IntArray] -class IV2fArrayProperty: +class IV2fArrayProperty(IProperty): def getValue(self) -> V2fArray: ... -class IUInt32ArrayProperty: +class IUInt32ArrayProperty(IProperty): def getValue(self) -> UnsignedIntArray: ... class IV2fGeomParam: From d5a8d35fe071e16922a663a998ebb80f96473846 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 15 Sep 2026 23:14:54 -0700 Subject: [PATCH 45/47] Disable numpy linking by default --- subprojects/maya/meson.options | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subprojects/maya/meson.options b/subprojects/maya/meson.options index 22b6a942..daea4313 100644 --- a/subprojects/maya/meson.options +++ b/subprojects/maya/meson.options @@ -49,7 +49,7 @@ option( option( 'maya_link_numpy', type : 'feature', - value : 'auto', + value : 'disabled', description : 'Provide maya-numpy dependency (Requires maya_install_path to exist)', yield : true, ) From 52f6fcf36b72a0f672e1576e601404be2da4b0ae Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 15 Sep 2026 23:17:52 -0700 Subject: [PATCH 46/47] More build --- subprojects/maya/meson.build | 2 +- subprojects/maya/meson.options | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/subprojects/maya/meson.build b/subprojects/maya/meson.build index 2703b6bd..ac5035ba 100644 --- a/subprojects/maya/meson.build +++ b/subprojects/maya/meson.build @@ -215,7 +215,7 @@ if check_alembic or check_imath if check_imath imath_lib = cmplr.find_library('Imath-3_2-maya', dirs : alembic_lib_dir, required: false) if not imath_lib.found() - imath_lib = cmplr.find_library('Imath', dirs : alembic_lib_dir) + imath_lib = cmplr.find_library('Imath', dirs : alembic_lib_dir, required: false) endif imath_inc = include_directories(alembic_base / 'include' / 'Imath') maya_imath_dep = declare_dependency(dependencies: [imath_lib], include_directories: imath_inc) diff --git a/subprojects/maya/meson.options b/subprojects/maya/meson.options index daea4313..01e057cf 100644 --- a/subprojects/maya/meson.options +++ b/subprojects/maya/meson.options @@ -33,7 +33,7 @@ option( option( 'maya_link_qt', type : 'feature', - value : 'auto', + value : 'disabled', description : 'Provide maya-qt dependency linking to Maya\'s Qt libraries', yield : true, ) @@ -57,7 +57,7 @@ option( option( 'maya_link_alembic', type : 'feature', - value : 'auto', + value : 'disabled', description : 'Provide maya-alembic dependency (Requires maya_devkit_base)', yield : true, ) @@ -65,7 +65,7 @@ option( option( 'maya_link_imath', type : 'feature', - value : 'auto', + value : 'disabled', description : 'Provide maya-imath dependency (Requires maya_devkit_base)', yield : true, ) @@ -73,7 +73,7 @@ option( option( 'maya_link_ufe', type : 'feature', - value : 'auto', + value : 'disabled', description : 'Provide maya-ufe dependency (Requires maya_devkit_base)', yield : true, ) From 963a098bebdb21990789f26d0864c56eac4e6827 Mon Sep 17 00:00:00 2001 From: Tyler Fox Date: Tue, 15 Sep 2026 23:25:48 -0700 Subject: [PATCH 47/47] No macos 13 --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9ecd044b..f2ff8120 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,6 +35,10 @@ jobs: maya: 2022 - os: macos-latest maya: 2023 + - os: macos-13 + maya: 2022 + - os: macos-13 + maya: 2023 - os: macos-13 maya: 2024 - os: macos-13