From 290b618c3d6c1fb8f956b4076030f65ca3d22e41 Mon Sep 17 00:00:00 2001 From: herizoran Date: Mon, 10 Feb 2025 16:05:49 +0300 Subject: [PATCH 01/12] feat: add custom splitter --- dwpicker/designer/qsplitter.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 dwpicker/designer/qsplitter.py diff --git a/dwpicker/designer/qsplitter.py b/dwpicker/designer/qsplitter.py new file mode 100644 index 0000000..019d470 --- /dev/null +++ b/dwpicker/designer/qsplitter.py @@ -0,0 +1,34 @@ +from dwpicker.pyside import QtWidgets, QtCore + + +class CustomSplitterHandle(QtWidgets.QSplitterHandle): + def __init__(self, orientation, parent): + super(CustomSplitterHandle, self).__init__(orientation, parent) + + self.split_button = QtWidgets.QPushButton("▶", self) + self.split_button.setCursor(QtCore.Qt.ArrowCursor) + self.split_button.setFixedSize(30, 30) + self.split_button.clicked.connect(self.toggle_left_widget) + self.splitter = parent + + def resizeEvent(self, event): + super(CustomSplitterHandle, self).resizeEvent(event) + if self.orientation() == QtCore.Qt.Horizontal: + self.split_button.move( + (self.width() - self.split_button.width()) // 2, + (self.height() - self.split_button.height()) // 2) + + def toggle_left_widget(self): + """Collapse or expand the left widget""" + sizes = self.splitter.sizes() + if sizes[0] > 0: # If left widget is visible + self.splitter.setSizes([0, sizes[1]]) + self.split_button.setText("▶") + else: + self.splitter.setSizes([sizes[1] // 2, sizes[1] // 2]) + self.split_button.setText("◀") + + +class CustomSplitter(QtWidgets.QSplitter): + def createHandle(self): + return CustomSplitterHandle(self.orientation(), self) From 0c9d82050ea4f1533f2134aa6c9ac6cb764999b8 Mon Sep 17 00:00:00 2001 From: herizoran Date: Mon, 10 Feb 2025 16:06:44 +0300 Subject: [PATCH 02/12] feat: add viewport widget --- dwpicker/designer/viewportwidget.py | 342 ++++++++++++++++++++++++++++ dwpicker/icons/camera.png | Bin 0 -> 1222 bytes dwpicker/icons/fieldChart.png | Bin 0 -> 354 bytes dwpicker/icons/grid.png | Bin 0 -> 278 bytes dwpicker/icons/lock.png | Bin 0 -> 468 bytes dwpicker/icons/resolutionGate.png | Bin 0 -> 686 bytes dwpicker/icons/snapshot.png | Bin 0 -> 674 bytes 7 files changed, 342 insertions(+) create mode 100644 dwpicker/designer/viewportwidget.py create mode 100644 dwpicker/icons/camera.png create mode 100644 dwpicker/icons/fieldChart.png create mode 100644 dwpicker/icons/grid.png create mode 100644 dwpicker/icons/lock.png create mode 100644 dwpicker/icons/resolutionGate.png create mode 100644 dwpicker/icons/snapshot.png diff --git a/dwpicker/designer/viewportwidget.py b/dwpicker/designer/viewportwidget.py new file mode 100644 index 0000000..e604379 --- /dev/null +++ b/dwpicker/designer/viewportwidget.py @@ -0,0 +1,342 @@ +import os +import sys +import uuid + +import maya.OpenMayaUI as omui +from maya import cmds +from maya import mel + +from dwpicker.capture import snap +from dwpicker.pyside import QtWidgets, QtCore, QtGui +from dwpicker.pyside import shiboken2 +from dwpicker.qtutils import icon +from dwpicker.templates import BACKGROUND + +if sys.version_info[0] == 3: + long = int + +IMAGE_SIZE_PRESETS = { + "Default": {"width": 960, "height": 540}, + "1080x1080": {"width": 1080, "height": 1080}, + "720x1000": {"width": 720, "height": 1000}, + "720x1280": {"width": 720, "height": 1280}, + "1080x1350": {"width": 1080, "height": 1350}, + "1080x1920": {"width": 1080, "height": 1920}, + "1280x720": {"width": 1280, "height": 720}, + "1920x1080": {"width": 1920, "height": 1080}, + "Custom": {"width": None, "height": None} +} + + +class ViewportWidget(QtWidgets.QWidget): + def __init__(self, editor, parent=None): + super(ViewportWidget, self).__init__(parent) + + self.editor = editor + + self.setObjectName("ViewportWidget") + self.resize(320, 620) + + self.main_layout = QtWidgets.QVBoxLayout(self) + self.main_layout.setContentsMargins(5, 0, 0, 0) + self.main_layout.setObjectName("ViewportPickerLayout") + + layout = omui.MQtUtil.fullName(long(shiboken2.getCppPointer( + self.main_layout)[0])) + cmds.setParent(layout) + panel_layout_name = cmds.paneLayout() + + ptr = omui.MQtUtil.findControl(panel_layout_name) + self.panel_layout = shiboken2.wrapInstance(long(ptr), + QtWidgets.QWidget) + + cameras = cmds.ls(type="camera") + cameras = [cmds.listRelatives(cam, parent=True)[0] for cam in + cameras] + + picker_model_name = "PickerModelPanel" + str(uuid.uuid4())[:4] + self.camera_name = "front" + + if not self.camera_name in cameras: + camera_ = cmds.camera(n=self.camera_name)[0] + cmds.rename(camera_, self.camera_name) + + self.model_panel_name = cmds.modelPanel(picker_model_name, mbv=False) + cmds.modelEditor(self.model_panel_name, + edit=True, + displayAppearance='smoothShaded', + cam=self.camera_name, + gr=True) + + ptr = omui.MQtUtil.findControl(self.model_panel_name) + self.model_panel_widget = shiboken2.wrapInstance(long(ptr), + QtWidgets.QWidget) + + self.cam_label = QtWidgets.QLabel("Cam") + + camera_combo_box = QtWidgets.QComboBox() + camera_combo_box.setToolTip("Cameras") + camera_combo_box.addItems(cameras) + camera_combo_box.currentTextChanged.connect( + lambda: self.update_camera_viewport(camera_combo_box, + picker_model_name)) + + self.lock_toggle = QtWidgets.QAction(icon('lock.png'), '', self) + self.lock_toggle.setToolTip("Toggle lock camera") + self.lock_toggle.triggered.connect( + lambda: toggle_camera_settings(picker_model_name, + self.camera_name, "lock_camera")) + + self.camera_toggle = QtWidgets.QAction(icon('camera.png'), '', self) + self.camera_toggle.setToolTip("Toggle orthographic view") + self.camera_toggle.triggered.connect( + lambda: toggle_camera_view(self.camera_name)) + + self.grid_toggle = QtWidgets.QAction(icon('grid.png'), '', self) + self.grid_toggle.setToolTip("Toggle grid view") + self.grid_toggle.triggered.connect( + lambda: toggle_grid_view(picker_model_name)) + + self.field_toggle = QtWidgets.QAction(icon('fieldChart.png'), '', self) + self.field_toggle.setToolTip("Toggle field chart") + self.field_toggle.triggered.connect( + lambda: toggle_camera_settings(picker_model_name, + self.camera_name, "field_chart")) + + self.resolution_toggle = QtWidgets.QAction(icon('resolutionGate.png'), + '', self) + self.resolution_toggle.setToolTip("Toggle resolution") + self.resolution_toggle.triggered.connect( + lambda: toggle_camera_settings(picker_model_name, + self.camera_name, "resolution")) + + image_size_combo_box = QtWidgets.QComboBox(self) + image_size_combo_box.setToolTip("Image sizes") + image_size_combo_box.setMaximumWidth(85) + for resolution in IMAGE_SIZE_PRESETS: + image_size_combo_box.addItem(resolution) + image_size_combo_box.currentTextChanged.connect( + lambda: self.update_resolution_settings(image_size_combo_box, + IMAGE_SIZE_PRESETS)) + + self.width_input = QtWidgets.QLineEdit() + self.width_input.setMaximumWidth(35) + self.width_input.setValidator(QtGui.QIntValidator(self)) + self.width_input.setVisible(False) + self.height_input = QtWidgets.QLineEdit() + self.height_input.setMaximumWidth(35) + self.height_input.setValidator(QtGui.QIntValidator(self)) + self.height_input.setVisible(False) + self.width_input.editingFinished.connect( + lambda: self.update_resolution_settings(image_size_combo_box, + IMAGE_SIZE_PRESETS, + self.width_input, + self.height_input)) + self.height_input.editingFinished.connect( + lambda: self.update_resolution_settings(image_size_combo_box, + IMAGE_SIZE_PRESETS, + self.width_input, + self.height_input)) + + self.snapshot = QtWidgets.QAction(icon('snapshot.png'), '', self) + self.snapshot.setToolTip("Capture snapshot") + self.snapshot.triggered.connect( + lambda: capture_snapshot(self, camera_combo_box)) + + self.toolbar = QtWidgets.QToolBar() + self.toolbar.setIconSize(QtCore.QSize(14, 14)) + toolbar_layout = self.toolbar.layout() + toolbar_layout.setSpacing(0) + self.toolbar.addWidget(self.cam_label) + self.toolbar.addWidget(camera_combo_box) + self.toolbar.addAction(self.lock_toggle) + self.toolbar.addAction(self.camera_toggle) + self.toolbar.addAction(self.grid_toggle) + self.toolbar.addAction(self.field_toggle) + self.toolbar.addAction(self.resolution_toggle) + self.toolbar.addWidget(image_size_combo_box) + self.width_action = self.toolbar.addWidget(self.width_input) + self.height_action = self.toolbar.addWidget(self.height_input) + self.toolbar.addAction(self.snapshot) + + self.main_layout.setSpacing(0) + self.main_layout.addWidget(self.toolbar) + self.main_layout.addWidget(self.panel_layout) + + def showEvent(self, event): + super(ViewportWidget, self).showEvent(event) + self.model_panel_widget.repaint() + + def add_snapshot_image(self, file=None): + if os.path.exists(file): + self.editor.create_shape(BACKGROUND, before=True, image=True, + filepath=file) + + def update_camera_viewport(self, combo_box, panel): + """ + Update the camera in the active panel when a new camera is selected from the combo box. + """ + active_camera = combo_box.currentText() + cmds.modelPanel(panel, edit=True, camera=active_camera) + self.camera_name = active_camera + + def update_resolution_settings(self, combobox, image_size, + custom_width=None, + custom_height=None): + resolution = combobox.currentText() + + if resolution == "Custom": + self.width_action.setVisible(True) + self.height_action.setVisible(True) + else: + self.width_action.setVisible(False) + self.height_action.setVisible(False) + + width = image_size[resolution]["width"] + height = image_size[resolution]["height"] + + if not (width and height): + if not (custom_width and custom_height): + return + if not custom_width.text(): + return + if not custom_height.text(): + return + + width = int(custom_width.text()) + height = int(custom_height.text()) + + try: + device_aspect_ratio = round(width / height, 3) + except ZeroDivisionError: + raise ZeroDivisionError("Height cannot be Zero") + + cmds.setAttr("defaultResolution.width", width) + cmds.setAttr("defaultResolution.height", height) + cmds.setAttr("defaultResolution.deviceAspectRatio", + device_aspect_ratio) + cmds.setAttr("defaultResolution.pixelAspect", 1) + + +class NotificationWidget(QtWidgets.QLabel): + def __init__(self, parent=None, message="Notification", duration=2000): + super(NotificationWidget, self).__init__(parent) + self.setText(message) + self.setStyleSheet(""" + background-color: grey; + color: white; + border-radius: 4px; + font-size: 14px; + font-weight: bold; + """) + self.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.setFixedSize(140, 30) + + parent_rect = parent.rect() + self.move( + (parent_rect.width() - self.width()) // 2, + 140 + ) + + self.timer = QtCore.QTimer(self) + self.timer.setSingleShot(True) + self.timer.timeout.connect(self.deleteLater) + + self.show() + self.timer.start(duration) + + @staticmethod + def show_notification(parent, message="Notification", duration=2000): + NotificationWidget(parent, message, duration) + + +def ui_delete_callback(): + panels = cmds.getPanel(type="modelPanel") + for panel in panels: + if panel.startswith('PickerModelPanel') and cmds.modelPanel( + panel, + exists=True): + cmds.deleteUI(panel, panel=True) + + +def toggle_grid_view(panel): + current_grid_state = cmds.modelEditor(panel, query=True, grid=True) + set_state = not current_grid_state + cmds.modelEditor(panel, edit=True, grid=set_state) + + +def toggle_camera_view(camera_name): + """ + Adjusts the camera view based on its type (perspective or orthographic). + + Args: + camera_name (str): The name of the camera to adjust. + """ + is_perspective = not cmds.camera(camera_name, query=True, + orthographic=True) + up_dir = cmds.camera(camera_name, query=True, worldUp=True) + + camera_view = {"o": True} if is_perspective else {"p": True} + cmds.viewPlace(camera_name, up=(up_dir[0], up_dir[1], up_dir[2]), + **camera_view) + + +def toggle_camera_settings(panel, camera_name="persp", option="resolution"): + """ + Toggles the camera settings based on the option specified. + + - 'resolution' toggles the camera resolution (displayResolution and overscan). + - 'field_chart' toggles the field chart display. + """ + + if option == "lock_camera": + mel.eval("changeCameraLockStatus" + " %s;" % panel) + + if option == "resolution": + display_resolution = cmds.camera(camera_name, query=True, + displayResolution=True) + overscan_value = cmds.camera(camera_name, query=True, overscan=True) + + if display_resolution and overscan_value == 1.3: + cmds.camera(camera_name, edit=True, displayFilmGate=False, + displayResolution=False, overscan=1.0) + return + cmds.camera(camera_name, edit=True, displayFilmGate=False, + displayResolution=True, overscan=1.3) + + elif option == "field_chart": + field_chart_display = cmds.camera(camera_name, query=True, + displayFieldChart=True) + if field_chart_display: + cmds.camera(camera_name, edit=True, displayFieldChart=False) + return + cmds.camera(camera_name, edit=True, displayFieldChart=True) + + +def capture_snapshot(cls, combo_box): + """ + snapshot args: + off_screen: boolean (Process in the background when True) + show_ornaments: boolean (Hide Axis and camera names,... when False) + """ + active_camera = combo_box.currentText() + folder_path = QtWidgets.QFileDialog.getExistingDirectory(None, + "Select Directory", + "") + if not folder_path: + return + + filename = os.path.join(folder_path, str(uuid.uuid4())) + + snap(active_camera, + off_screen=True, + filename=filename, + frame_padding=0, + show_ornaments=False, + # clipboard=True, + maintain_aspect_ratio=True, + camera_options={"displayFieldChart": False}) + + NotificationWidget.show_notification(cls, "Snapshot done!") + + cls.add_snapshot_image(filename + ".0.png") diff --git a/dwpicker/icons/camera.png b/dwpicker/icons/camera.png new file mode 100644 index 0000000000000000000000000000000000000000..a3296e75d461bab16d6e84b80be747252ebbcd1d GIT binary patch literal 1222 zcmaJ>YiJx*6rN^FuxS?3TCow)VG=`ZGduUrYj=jM+kLhx-I#6{-1wt(9(Q+#WFDEB zxRYpT67#4D`cF`+6#c2zDupVDRv{pQMM01th=?enph!W43h~ZnH}*&9z|6h(-1D9D zo!2}+K6WzN^FR+l5YghPTp6!@;d@Uv{yuzbTfs}epRf97?V8_ITtuXGTSa8iRO+aV z6n%F33p7d)kyWEo^{b`RvSynxCDe%pri0N0F`5b-MVmxEsiL}JC8<9@x zN}%LqQNuVj=c2Q7V-;;~Qj>Hl^%yxC$k>31e1#0mDa(_CB-OGj<9#S*D6*yEPbR4y zQq|HpnYCR+3New^AcSOGia{a4vBC(+0w^#*Vj!Ralp&B=K(-$Wdvo=gT+S8RzVInY zHGJQZ8K&86#+qErcIyn3B#8km!?HBipuJhkR|4Ad209jU$kSZI@eSJ|LyL-P&-h6S zGu;WnbV{XN!^7ix4Ou0UF4SttTd z)PSmW?CgavNO?X3`9d1-X$bRTCYP2988Hvi0?Q|$(7_fh&sQuBb$lDxcN?3(D^||B zNbzmAV%t-lE*NjvzU?(^hst^349t4;KGPJ@o9siwEC$_14zb){Cpl z%bN?m%DzJ@D=Wt`nM~r!Gq?Y~^4s!(dryduZ)|MbzqYo{e%7QS>Ma6Kis&W9d&-cc_|(UnV;>~ zQx`sI{_^E3y;skDoEaQEe*EpO#%pi=2qQ;c_+~Na>Z?a0(>=prG4>R}ETiutkJyP5M#{{h=cj`;up literal 0 HcmV?d00001 diff --git a/dwpicker/icons/fieldChart.png b/dwpicker/icons/fieldChart.png new file mode 100644 index 0000000000000000000000000000000000000000..e8c6c37657ce92e7fb15baeb02b5321feddbfa35 GIT binary patch literal 354 zcmV-o0iFJdP)WDBvd*&i_eiG_b;v0B;5vR^X45V1@Z zA(crJ$ea5h_4)%Y7fGuThMBCSxJ`KPol7Q}nQM$OMnskXwyJsvA+pFMyMtc=TmT#c ziL9%LBt}(te30k)hqZPRLSz9b$um$~($D4t=uIC)=oZE=V`jJ?} zTK0H1&@|2J*?O~PG@#=W==c%poBoC_)<%@Y-imGP!^jTdIJOZCq|vTqh5K3pI54TdW4Dje^M#f z;gzJ?e$@M{fPn0eHx6n$|Fbo?=_h7?R9Vl}o&8nR#P8i7u?4PmeQ!$?zA+>TFWMkJ zAx3An%FWllax(wa3f7bbeB3L%JICGOqo4wdf?LCe$1-Jek_7+$){674^YgAd&+or; Xhr$e%knI^jKQVZ^`njxgN@xNAf@fwc literal 0 HcmV?d00001 diff --git a/dwpicker/icons/lock.png b/dwpicker/icons/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..7f2bc364ec42c1f9732143878b95e43d7929593d GIT binary patch literal 468 zcmV;_0W1EAP)>>yf1hI)} z#6ko$+JqFTlFmjuNv(fiVa|Ei6)Q;^&Bn;c1TKLjo85bNRWk6vosVbU!#u-fTCHQY zh&ZiI1FzM0MX&N$o@+JPfZJ+E{h_{BUlx5(f2xsEy46(iMPNsL8WCsq&*w4q8So2u zQfu*uxC`{un@6oJ@#M9$wG{7myOY4CI^VBHeF%K-bUJ5;22kg7?(KH_RKK2-(uDd2 zcu)tx2Jrm2KHvrLacBVG+aP(WcYv>T0Box>0|3lqZyf-=Apm;+9zaA~RPO=zi*|sO zO21#N6z`wDI;UPZs`&N%CsTKuP}%$HfdIf#Q^hL~ONZsQ+%!P0aRSi#Un2OVURJND zpGOXmQaZQiODUZnIY3liMddX#S^(hDo-coo;|{D=e5>O)fwjsBtd9d=Ga{~K0bApU zV0S>B04)E#5dd$S1_0h1b^_BQ0Jw1=K!LafJXRO#RM~TCf5G3TPe4{(0B>ag0000< KMNUMnLSTZi9Le?o literal 0 HcmV?d00001 diff --git a/dwpicker/icons/resolutionGate.png b/dwpicker/icons/resolutionGate.png new file mode 100644 index 0000000000000000000000000000000000000000..21282b8be1cdda97d654f88206e6d16c47d10793 GIT binary patch literal 686 zcmV;f0#W^mP);wd1gw|I{6B7&Y2ggr{m z{(=z1Lk)lyn5?J}4N3LRk!3Z@=i z{Y`aMO;3`R^2xMxtB?PA~_$}KL`i*dG+!3Tw(-viS2YR zkrxC&RWf0W*`vl0pB(_70aOC{HGs)Lz9AgQlj0M|6^RkZBeI9=rP;5Fa@H8LiR7?m zU`%9T1u!1SZ%9sgndFRcAWw-;;6E-g0{fWmF|794L>19EAvs6#cqE?tRRb~T$KC?~ z{GX1XUazlB({yXU-Z{6b)oN`oTGMPcw>jrlFByQfc1JjfNS-G7QQPO1fDt`_gMPx1 ze3fO{F3}4h0MB)}y#sK#6aeSk3S-QE08eyrrQL3Cmaz%oBFPgvg3oG@7bNci7?mLe z;JWIk0T>-VGXUO}0?=qQHim;j$LRqbq5Jf$ACtVS&+h@8(|}h=K2`lA@1s5M@;qO^ zD1ch6w#E+%0AB{Tz6AHi2^?HPSOFNOG5M^FSnzhrW6n6}oQEGWYJB#)4+1Gv^# zgh%z5$`xTqeQkJ3&sqxeW?vgxE!vl9n(p*%Nb;HbcT2ZnNLiNc?)7>byeykqYq$L7 z^s{wW%D_nqsMF~T$)GB-4L(ML4EPL0zmmDAmr^W)Yk4*5j|rE-uh~S@pVQd%9WRv` U%_0Kdxc~qF07*qoM6N<$g6sz?y8r+H literal 0 HcmV?d00001 diff --git a/dwpicker/icons/snapshot.png b/dwpicker/icons/snapshot.png new file mode 100644 index 0000000000000000000000000000000000000000..7b5da1886912f3e24a74eac7d290e342e798ac33 GIT binary patch literal 674 zcmV;T0$u%yP)RS1KR{xoh$y5GYz%^e zoiM2gM^e8K`pcqV^|M;Vw2@R2qyzx&Y zuZhS?9q;vet4O{n3}E}mcah{w3iC93lXpwM1+30oXzEQO=bc)p=nU z?y3X;;Ffc=wq)_Sq4PSO&ibkVcfAM7*Af6%T-XMyhTyel^Q0(%h#b;6#+Va@0E{um zvYbUxw9VNZOgpHoeLy}uu&I|x}{9Cmk=-=;ol?}j0m0! Date: Mon, 10 Feb 2025 16:07:22 +0300 Subject: [PATCH 03/12] feat: add capture module --- dwpicker/capture.py | 873 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 873 insertions(+) create mode 100644 dwpicker/capture.py diff --git a/dwpicker/capture.py b/dwpicker/capture.py new file mode 100644 index 0000000..1ff3544 --- /dev/null +++ b/dwpicker/capture.py @@ -0,0 +1,873 @@ +"""Maya Capture + +Playblasting with independent viewport, camera and display options + +Code from https://github.com/abstractfactory/maya-capture +Thanks for this ! +""" + +import re +import sys +import contextlib +from collections import defaultdict + +from maya import cmds +from maya import mel + +try: + from PySide6 import QtGui, QtWidgets +except ImportError: + try: + from PySide2 import QtGui, QtWidgets + except ImportError: + from PySide import QtGui + QtWidgets = QtGui + +version_info = (2, 6, 0) + +__version__ = "%s.%s.%s" % version_info +__license__ = "MIT" + + +def capture(camera=None, + width=None, + height=None, + filename=None, + start_frame=None, + end_frame=None, + frame=None, + format='qt', + compression='H.264', + quality=100, + off_screen=False, + viewer=True, + show_ornaments=True, + sound=None, + isolate=None, + maintain_aspect_ratio=True, + overwrite=False, + frame_padding=4, + raw_frame_numbers=False, + use_camera_sequencer=False, + camera_options=None, + display_options=None, + viewport_options=None, + viewport2_options=None, + complete_filename=None): + """Playblast in an independent panel + + Arguments: + camera (str, optional): Name of camera, defaults to "persp" + width (int, optional): Width of output in pixels + height (int, optional): Height of output in pixels + filename (str, optional): Name of output file. If + none is specified, no files are saved. + start_frame (float, optional): Defaults to current start frame. + end_frame (float, optional): Defaults to current end frame. + frame (float or tuple, optional): A single frame or list of frames. + Use this to capture a single frame or an arbitrary sequence of + frames. + format (str, optional): Name of format, defaults to "qt". + compression (str, optional): Name of compression, defaults to "H.264" + quality (int, optional): The quality of the output, defaults to 100 + off_screen (bool, optional): Whether or not to playblast off screen + viewer (bool, optional): Display results in native player + show_ornaments (bool, optional): Whether or not model view ornaments + (e.g. axis icon, grid and HUD) should be displayed. + sound (str, optional): Specify the sound node to be used during + playblast. When None (default) no sound will be used. + isolate (list): List of nodes to isolate upon capturing + maintain_aspect_ratio (bool, optional): Modify height in order to + maintain aspect ratio. + overwrite (bool, optional): Whether or not to overwrite if file + already exists. If disabled and file exists and error will be + raised. + frame_padding (bool, optional): Number of zeros used to pad file name + for image sequences. + raw_frame_numbers (bool, optional): Whether or not to use the exact + frame numbers from the scene or capture to a sequence starting at + zero. Defaults to False. When set to True `viewer` can't be used + and will be forced to False. + use_camera_sequencer (bool, optional): Whether or not to playblast + using the camera sequencer. Defaults to False. When set to True the + value of `camera` will be ignored and the cameras from the + sequencer will be used instead. Additionally, the `start_frame` and + `end_frame` values will be in sequence time instead of scene frame + numbers. + camera_options (dict, optional): Supplied camera options, + using `CameraOptions` + display_options (dict, optional): Supplied display + options, using `DisplayOptions` + viewport_options (dict, optional): Supplied viewport + options, using `ViewportOptions` + viewport2_options (dict, optional): Supplied display + options, using `Viewport2Options` + complete_filename (str, optional): Exact name of output file. Use this + to override the output of `filename` so it excludes frame padding. + + Example: + >>> # Launch default capture + >>> capture() + >>> # Launch capture with custom viewport settings + >>> capture('persp', 800, 600, + ... viewport_options={ + ... "displayAppearance": "wireframe", + ... "grid": False, + ... "polymeshes": True, + ... }, + ... camera_options={ + ... "displayResolution": True + ... } + ... ) + + + """ + + camera = camera or "persp" + + # Ensure camera exists + if not cmds.objExists(camera): + raise RuntimeError("Camera does not exist: {0}".format(camera)) + + width = width or cmds.getAttr("defaultResolution.width") + height = height or cmds.getAttr("defaultResolution.height") + if maintain_aspect_ratio: + ratio = cmds.getAttr("defaultResolution.deviceAspectRatio") + height = round(width / ratio) + + # Set frame range if no custom frame range specified + if use_camera_sequencer: + # Get frames from the Camera Sequencer + sequencer = cmds.sequenceManager(query=True, writableSequencer=True) + + if start_frame is None: + start_frame = cmds.getAttr(sequencer + ".minFrame") + if end_frame is None: + end_frame = cmds.getAttr(sequencer + ".maxFrame") + else: + # Get frames from the timeline + if start_frame is None: + start_frame = cmds.playbackOptions(minTime=True, query=True) + if end_frame is None: + end_frame = cmds.playbackOptions(maxTime=True, query=True) + + # (#74) Bugfix: `maya.cmds.playblast` will raise an error when playblasting + # with `rawFrameNumbers` set to True but no explicit `frames` provided. + # Since we always know what frames will be included we can provide it + # explicitly + if raw_frame_numbers and frame is None: + frame = range(int(start_frame), int(end_frame) + 1) + + # We need to wrap `completeFilename`, otherwise even when None is provided + # it will use filename as the exact name. Only when lacking as argument + # does it function correctly. + playblast_kwargs = dict() + if complete_filename: + playblast_kwargs['completeFilename'] = complete_filename + if frame is not None: + playblast_kwargs['frame'] = frame + if sound is not None: + playblast_kwargs['sound'] = sound + + # We need to raise an error when the user gives a custom frame range with + # negative frames in combination with raw frame numbers. This will result + # in a minimal integer frame number : filename.-2147483648.png for any + # negative rendered frame + if frame and raw_frame_numbers: + check = frame if isinstance(frame, (list, tuple, range)) else [frame] + if any(f < 0 for f in check): + raise RuntimeError("Negative frames are not supported with " + "raw frame numbers and explicit frame numbers") + + # (#21) Bugfix: `maya.cmds.playblast` suffers from undo bug where it + # always sets the currentTime to frame 1. By setting currentTime before + # the playblast call it'll undo correctly. + cmds.currentTime(cmds.currentTime(query=True)) + + padding = 10 # Extend panel to accommodate for OS window manager + with _independent_panel(width=width + padding, + height=height + padding, + off_screen=off_screen) as panel: + cmds.setFocus(panel) + + with _disabled_inview_messages(),\ + _maintain_camera(panel, camera),\ + _applied_viewport_options(viewport_options, panel),\ + _applied_camera_options(camera_options, panel, use_camera_sequencer),\ + _applied_display_options(display_options),\ + _applied_viewport2_options(viewport2_options),\ + _isolated_nodes(isolate, panel),\ + _maintained_time(): + + output = cmds.playblast( + compression=compression, + format=format, + percent=100, + quality=quality, + viewer=viewer, + startTime=start_frame, + endTime=end_frame, + offScreen=off_screen, + showOrnaments=show_ornaments, + forceOverwrite=overwrite, + filename=filename, + widthHeight=[width, height], + rawFrameNumbers=raw_frame_numbers, + framePadding=frame_padding, + sequenceTime=use_camera_sequencer, + **playblast_kwargs) + + return output + + +def snap(*args, **kwargs): + """Single frame playblast in an independent panel. + + The arguments of `capture` are all valid here as well, except for + `start_frame` and `end_frame`. + + Arguments: + frame (float, optional): The frame to snap. If not provided current + frame is used. + clipboard (bool, optional): Whether to add the output image to the + global clipboard. This allows to easily paste the snapped image + into another application, eg. into Photoshop. + + Keywords: + See `capture`. + + """ + + # capture single frame + frame = kwargs.pop('frame', cmds.currentTime(q=1)) + kwargs['start_frame'] = frame + kwargs['end_frame'] = frame + kwargs['frame'] = frame + + if not isinstance(frame, (int, float)): + raise TypeError("frame must be a single frame (integer or float). " + "Use `capture()` for sequences.") + + # override capture defaults + format = kwargs.pop('format', "image") + compression = kwargs.pop('compression', "png") + viewer = kwargs.pop('viewer', False) + raw_frame_numbers = kwargs.pop('raw_frame_numbers', True) + kwargs['compression'] = compression + kwargs['format'] = format + kwargs['viewer'] = viewer + kwargs['raw_frame_numbers'] = raw_frame_numbers + + # pop snap only keyword arguments + clipboard = kwargs.pop('clipboard', False) + + # perform capture + output = capture(*args, **kwargs) + + def replace(m): + """Substitute # with frame number""" + return str(int(frame)).zfill(len(m.group())) + + output = re.sub("#+", replace, output) + + # add image to clipboard + if clipboard: + _image_to_clipboard(output) + + return output + + +CameraOptions = { + "displayGateMask": False, + "displayResolution": False, + "displayFilmGate": False, + "displayFieldChart": False, + "displaySafeAction": False, + "displaySafeTitle": False, + "displayFilmPivot": False, + "displayFilmOrigin": False, + "overscan": 1.0, + "depthOfField": False, +} + +DisplayOptions = { + "displayGradient": True, + "background": (0.631, 0.631, 0.631), + "backgroundTop": (0.535, 0.617, 0.702), + "backgroundBottom": (0.052, 0.052, 0.052), +} + +# These display options require a different command to be queried and set +_DisplayOptionsRGB = set(["background", "backgroundTop", "backgroundBottom"]) + +ViewportOptions = { + # renderer + "rendererName": "vp2Renderer", + "fogging": False, + "fogMode": "linear", + "fogDensity": 1, + "fogStart": 1, + "fogEnd": 1, + "fogColor": (0, 0, 0, 0), + "shadows": False, + "displayTextures": True, + "displayLights": "default", + "useDefaultMaterial": False, + "wireframeOnShaded": False, + "displayAppearance": 'smoothShaded', + "selectionHiliteDisplay": False, + "headsUpDisplay": True, + # object display + "imagePlane": True, + "nurbsCurves": False, + "nurbsSurfaces": False, + "polymeshes": True, + "subdivSurfaces": False, + "planes": True, + "cameras": False, + "controlVertices": True, + "lights": False, + "grid": False, + "hulls": True, + "joints": False, + "ikHandles": False, + "deformers": False, + "dynamics": False, + "fluids": False, + "hairSystems": False, + "follicles": False, + "nCloths": False, + "nParticles": False, + "nRigids": False, + "dynamicConstraints": False, + "locators": False, + "manipulators": False, + "dimensions": False, + "handles": False, + "pivots": False, + "textures": False, + "strokes": False +} + +Viewport2Options = { + "consolidateWorld": True, + "enableTextureMaxRes": False, + "bumpBakeResolution": 64, + "colorBakeResolution": 64, + "floatingPointRTEnable": True, + "floatingPointRTFormat": 1, + "gammaCorrectionEnable": False, + "gammaValue": 2.2, + "lineAAEnable": False, + "maxHardwareLights": 8, + "motionBlurEnable": False, + "motionBlurSampleCount": 8, + "motionBlurShutterOpenFraction": 0.2, + "motionBlurType": 0, + "multiSampleCount": 8, + "multiSampleEnable": False, + "singleSidedLighting": False, + "ssaoEnable": False, + "ssaoAmount": 1.0, + "ssaoFilterRadius": 16, + "ssaoRadius": 16, + "ssaoSamples": 16, + "textureMaxResolution": 4096, + "threadDGEvaluation": False, + "transparencyAlgorithm": 1, + "transparencyQuality": 0.33, + "useMaximumHardwareLights": True, + "vertexAnimationCache": 0 +} + + +def apply_view(panel, **options): + """Apply options to panel""" + + camera = cmds.modelPanel(panel, camera=True, query=True) + + # Display options + display_options = options.get("display_options", {}) + for key, value in display_options.items(): + if key in _DisplayOptionsRGB: + cmds.displayRGBColor(key, *value) + else: + cmds.displayPref(**{key: value}) + + # Camera options + camera_options = options.get("camera_options", {}) + for key, value in camera_options.items(): + cmds.setAttr("{0}.{1}".format(camera, key), value) + + # Viewport options + viewport_options = options.get("viewport_options", {}) + for key, value in viewport_options.items(): + cmds.modelEditor(panel, edit=True, **{key: value}) + + viewport2_options = options.get("viewport2_options", {}) + for key, value in viewport2_options.items(): + attr = "hardwareRenderingGlobals.{0}".format(key) + cmds.setAttr(attr, value) + + +def parse_active_panel(): + """Parse the active modelPanel. + + Raises + RuntimeError: When no active modelPanel an error is raised. + + Returns: + str: Name of modelPanel + + """ + + panel = cmds.getPanel(withFocus=True) + + # This happens when last focus was on panel + # that got deleted (e.g. `capture()` then `parse_active_view()`) + if not panel or "modelPanel" not in panel: + raise RuntimeError("No active model panel found") + + return panel + + +def parse_active_view(): + """Parse the current settings from the active view""" + panel = parse_active_panel() + return parse_view(panel) + + +def parse_view(panel): + """Parse the scene, panel and camera for their current settings + + Example: + >>> parse_view("modelPanel1") + + Arguments: + panel (str): Name of modelPanel + + """ + + camera = cmds.modelPanel(panel, query=True, camera=True) + + # Display options + display_options = {} + for key in DisplayOptions: + if key in _DisplayOptionsRGB: + display_options[key] = cmds.displayRGBColor(key, query=True) + else: + display_options[key] = cmds.displayPref(query=True, **{key: True}) + + # Camera options + camera_options = {} + for key in CameraOptions: + camera_options[key] = cmds.getAttr("{0}.{1}".format(camera, key)) + + # Viewport options + viewport_options = {} + + # capture plugin display filters first to ensure we never override + # built-in arguments if ever possible a plugin has similarly named + # plugin display filters (which it shouldn't!) + plugins = cmds.pluginDisplayFilter(query=True, listFilters=True) + for plugin in plugins: + plugin = str(plugin) # unicode->str for simplicity of the dict + state = cmds.modelEditor(panel, query=True, queryPluginObjects=plugin) + viewport_options[plugin] = state + + for key in ViewportOptions: + viewport_options[key] = cmds.modelEditor( + panel, query=True, **{key: True}) + + viewport2_options = {} + for key in Viewport2Options.keys(): + attr = "hardwareRenderingGlobals.{0}".format(key) + try: + viewport2_options[key] = cmds.getAttr(attr) + except ValueError: + continue + + return { + "camera": camera, + "display_options": display_options, + "camera_options": camera_options, + "viewport_options": viewport_options, + "viewport2_options": viewport2_options + } + + +def parse_active_scene(): + """Parse active scene for arguments for capture() + + *Resolution taken from render settings. + + """ + + time_control = mel.eval("$gPlayBackSlider = $gPlayBackSlider") + + return { + "start_frame": cmds.playbackOptions(minTime=True, query=True), + "end_frame": cmds.playbackOptions(maxTime=True, query=True), + "width": cmds.getAttr("defaultResolution.width"), + "height": cmds.getAttr("defaultResolution.height"), + "compression": cmds.optionVar(query="playblastCompression"), + "filename": (cmds.optionVar(query="playblastFile") + if cmds.optionVar(query="playblastSaveToFile") else None), + "format": cmds.optionVar(query="playblastFormat"), + "off_screen": (True if cmds.optionVar(query="playblastOffscreen") + else False), + "show_ornaments": (True if cmds.optionVar(query="playblastShowOrnaments") + else False), + "quality": cmds.optionVar(query="playblastQuality"), + "sound": cmds.timeControl(time_control, q=True, sound=True) or None + } + + +def apply_scene(**options): + """Apply options from scene + + Example: + >>> apply_scene({"start_frame": 1009}) + + Arguments: + options (dict): Scene options + + """ + + if "start_frame" in options: + cmds.playbackOptions(minTime=options["start_frame"]) + + if "end_frame" in options: + cmds.playbackOptions(maxTime=options["end_frame"]) + + if "width" in options: + cmds.setAttr("defaultResolution.width", options["width"]) + + if "height" in options: + cmds.setAttr("defaultResolution.height", options["height"]) + + if "compression" in options: + cmds.optionVar( + stringValue=["playblastCompression", options["compression"]]) + + if "filename" in options: + cmds.optionVar( + stringValue=["playblastFile", options["filename"]]) + + if "format" in options: + cmds.optionVar( + stringValue=["playblastFormat", options["format"]]) + + if "off_screen" in options: + cmds.optionVar( + intValue=["playblastFormat", options["off_screen"]]) + + if "show_ornaments" in options: + cmds.optionVar( + intValue=["show_ornaments", options["show_ornaments"]]) + + if "quality" in options: + cmds.optionVar( + floatValue=["playblastQuality", options["quality"]]) + + +@contextlib.contextmanager +def _applied_view(panel, **options): + """Apply options to panel""" + + original = parse_view(panel) + apply_view(panel, **options) + + try: + yield + finally: + apply_view(panel, **original) + + +@contextlib.contextmanager +def _independent_panel(width, height, off_screen=False): + """Create capture-window context without decorations + + Arguments: + width (int): Width of panel + height (int): Height of panel + + Example: + >>> with _independent_panel(800, 600): + ... cmds.capture() + + """ + + # center panel on screen + screen_width, screen_height = _get_screen_size() + topLeft = [int((screen_height-height)/2.0), + int((screen_width-width)/2.0)] + + window = cmds.window(width=width, + height=height, + topLeftCorner=topLeft, + menuBarVisible=False, + titleBar=False, + visible=not off_screen) + cmds.paneLayout() + panel = cmds.modelPanel(menuBarVisible=False, + label='CapturePanel') + + # Hide icons under panel menus + bar_layout = cmds.modelPanel(panel, q=True, barLayout=True) + cmds.frameLayout(bar_layout, edit=True, collapse=True) + + if not off_screen: + cmds.showWindow(window) + + # Set the modelEditor of the modelPanel as the active view so it takes + # the playback focus. Does seem redundant with the `refresh` added in. + editor = cmds.modelPanel(panel, query=True, modelEditor=True) + cmds.modelEditor(editor, edit=True, activeView=True) + + # Force a draw refresh of Maya so it keeps focus on the new panel + # This focus is required to force preview playback in the independent panel + cmds.refresh(force=True) + + try: + yield panel + finally: + # Delete the panel to fix memory leak (about 5 mb per capture) + cmds.deleteUI(panel, panel=True) + cmds.deleteUI(window) + + +@contextlib.contextmanager +def _applied_camera_options(options, panel, use_camera_sequencer=False): + """Context manager for applying `options` to the cameras used""" + + if use_camera_sequencer: + cameras = [ + cmds.shot(shot, query=True, currentCamera=True) + for shot in cmds.sequenceManager(listShots=True) + if not cmds.shot(shot, query=True, mute=True) + ] + else: + cameras = [cmds.modelPanel(panel, query=True, camera=True)] + + options = dict(CameraOptions, **(options or {})) + + old_options = defaultdict(dict) + for camera in cameras: + cam_options = options.copy() + for opt in cam_options: + try: + old_options[camera][opt] = cmds.getAttr(camera + "." + opt) + except: + sys.stderr.write("Could not get camera attribute " + "for capture: %s.%s" % (camera, opt)) + cam_options.pop(opt) + + for opt, value in cam_options.items(): + cmds.setAttr(camera + "." + opt, value) + + try: + yield + finally: + for camera, orig_options in old_options.items(): + if orig_options: + for opt, value in orig_options.items(): + cmds.setAttr(camera + "." + opt, value) + + +@contextlib.contextmanager +def _applied_display_options(options): + """Context manager for setting background color display options.""" + + options = dict(DisplayOptions, **(options or {})) + + colors = ['background', 'backgroundTop', 'backgroundBottom'] + preferences = ['displayGradient'] + + # Store current settings + original = {} + for color in colors: + original[color] = cmds.displayRGBColor(color, query=True) or [] + + for preference in preferences: + original[preference] = cmds.displayPref( + query=True, **{preference: True}) + + # Apply settings + for color in colors: + value = options[color] + cmds.displayRGBColor(color, *value) + + for preference in preferences: + value = options[preference] + cmds.displayPref(**{preference: value}) + + try: + yield + + finally: + # Restore original settings + for color in colors: + cmds.displayRGBColor(color, *original[color]) + for preference in preferences: + cmds.displayPref(**{preference: original[preference]}) + + +@contextlib.contextmanager +def _applied_viewport_options(options, panel): + """Context manager for applying `options` to `panel`""" + + options = dict(ViewportOptions, **(options or {})) + + # separate the plugin display filter options since they need to + # be set differently (see #55) + plugins = cmds.pluginDisplayFilter(query=True, listFilters=True) + plugin_options = dict() + for plugin in plugins: + if plugin in options: + plugin_options[plugin] = options.pop(plugin) + + # default options + cmds.modelEditor(panel, edit=True, **options) + + # plugin display filter options + for plugin, state in plugin_options.items(): + cmds.modelEditor(panel, edit=True, pluginObjects=(plugin, state)) + + yield + + +@contextlib.contextmanager +def _applied_viewport2_options(options): + """Context manager for setting viewport 2.0 options. + + These options are applied by setting attributes on the + "hardwareRenderingGlobals" node. + + """ + + options = dict(Viewport2Options, **(options or {})) + + # Store current settings + original = {} + for opt in options.copy(): + try: + original[opt] = cmds.getAttr("hardwareRenderingGlobals." + opt) + except ValueError: + options.pop(opt) + + # Apply settings + for opt, value in options.items(): + cmds.setAttr("hardwareRenderingGlobals." + opt, value) + + try: + yield + finally: + # Restore previous settings + for opt, value in original.items(): + cmds.setAttr("hardwareRenderingGlobals." + opt, value) + + +@contextlib.contextmanager +def _isolated_nodes(nodes, panel): + """Context manager for isolating `nodes` in `panel`""" + + if nodes is not None: + cmds.isolateSelect(panel, state=True) + for obj in nodes: + cmds.isolateSelect(panel, addDagObject=obj) + yield + + +@contextlib.contextmanager +def _maintained_time(): + """Context manager for preserving (resetting) the time after the context""" + + current_time = cmds.currentTime(query=1) + try: + yield + finally: + cmds.currentTime(current_time) + + +@contextlib.contextmanager +def _maintain_camera(panel, camera): + state = {} + + if not _in_standalone(): + cmds.lookThru(panel, camera) + else: + state = dict((camera, cmds.getAttr(camera + ".rnd")) + for camera in cmds.ls(type="camera")) + cmds.setAttr(camera + ".rnd", True) + + try: + yield + finally: + for camera, renderable in state.items(): + cmds.setAttr(camera + ".rnd", renderable) + + +@contextlib.contextmanager +def _disabled_inview_messages(): + """Disable in-view help messages during the context""" + original = cmds.optionVar(q="inViewMessageEnable") + cmds.optionVar(iv=("inViewMessageEnable", 0)) + try: + yield + finally: + cmds.optionVar(iv=("inViewMessageEnable", original)) + + +def _image_to_clipboard(path): + """Copies the image at path to the system's global clipboard.""" + if _in_standalone(): + raise Exception("Cannot copy to clipboard from Maya Standalone") + + image = QtGui.QImage(path) + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setImage(image, mode=QtGui.QClipboard.Clipboard) + + +def _get_screen_size(): + """Return available screen size without space occupied by taskbar""" + if _in_standalone(): + return [0, 0] + + try: + rect = QtWidgets.QDesktopWidget().screenGeometry(-1) + except AttributeError: + # in Qt6 it is a different call + rect = QtWidgets.QApplication.primaryScreen().availableGeometry() + + return [rect.width(), rect.height()] + + +def _in_standalone(): + return not hasattr(cmds, "about") or cmds.about(batch=True) + + +# -------------------------------- +# +# Apply version specific settings +# +# -------------------------------- + +version = mel.eval("getApplicationVersionAsFloat") +if version > 2015: + Viewport2Options.update({ + "hwFogAlpha": 1.0, + "hwFogFalloff": 0, + "hwFogDensity": 0.1, + "hwFogEnable": False, + "holdOutDetailMode": 1, + "hwFogEnd": 100.0, + "holdOutMode": True, + "hwFogColorR": 0.5, + "hwFogColorG": 0.5, + "hwFogColorB": 0.5, + "hwFogStart": 0.0, + }) + ViewportOptions.update({ + "motionTrails": False + }) From 75b8646aff9d5232180ccd3023f9a38b6506df4a Mon Sep 17 00:00:00 2001 From: herizoran Date: Mon, 10 Feb 2025 16:08:12 +0300 Subject: [PATCH 04/12] feat: add viewport integration --- dwpicker/designer/editor.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/dwpicker/designer/editor.py b/dwpicker/designer/editor.py index a16e847..6da1816 100644 --- a/dwpicker/designer/editor.py +++ b/dwpicker/designer/editor.py @@ -26,6 +26,8 @@ from dwpicker.designer.display import DisplayOptions from dwpicker.designer.menu import MenuWidget from dwpicker.designer.attributes import AttributeEditor +from dwpicker.designer.qsplitter import CustomSplitter +from dwpicker.designer.viewportwidget import ViewportWidget DIRECTION_OFFSETS = { @@ -39,6 +41,9 @@ def __init__(self, document, parent=None): title = "Picker editor - " + document.data['general']['name'] self.setWindowTitle(title) + self.splitter_layout = CustomSplitter(QtCore.Qt.Horizontal) + self.splitter_layout.setObjectName("SplitterLayout") + self.document = document self.document.shapes_changed.connect(self.update) self.document.general_option_changed.connect(self.generals_modified) @@ -47,6 +52,8 @@ def __init__(self, document, parent=None): self.display_options = DisplayOptions() + self.viewport_widget = ViewportWidget(self) + self.shape_canvas = ShapeEditCanvas( self.document, self.display_options) self.shape_canvas.callContextMenu.connect(self.call_context_menu) @@ -115,13 +122,18 @@ def __init__(self, document, parent=None): self.attribute_editor.panelDoubleClicked.connect( self.shape_canvas.select_panel_shapes) + self.splitter_layout.addWidget(self.viewport_widget) + self.splitter_layout.addWidget(self.shape_canvas) + self.splitter_layout.setSizes([0, 1]) + self.hlayout = QtWidgets.QHBoxLayout() self.hlayout.setSizeConstraint(QtWidgets.QLayout.SetMaximumSize) self.hlayout.setContentsMargins(0, 0, 0, 0) - self.hlayout.addWidget(self.shape_canvas) + self.hlayout.addWidget(self.splitter_layout) self.hlayout.addWidget(self.attribute_editor) self.vlayout = QtWidgets.QVBoxLayout(self) + self.vlayout.setObjectName("VerticalLayout") self.vlayout.setContentsMargins(0, 0, 0, 0) self.vlayout.setSpacing(0) self.vlayout.addWidget(self.menu) @@ -284,13 +296,16 @@ def create_library_shape(self, path): def create_shape( self, template, before=False, position=None, targets=None, - image=False): + image=False,filepath=None): options = deepcopy(template) panel = self.shape_canvas.display_options.current_panel options['panel'] = max((panel, 0)) if image: - filename = get_image_path(self, "Select background image.") + if filepath: + filename = filepath + else: + filename = get_image_path(self, "Select background image.") if filename: filename = format_path(filename) options['image.path'] = filename From d98ac50c1a310d4274bd33ac59e197490249fc76 Mon Sep 17 00:00:00 2001 From: herizoran Date: Mon, 10 Feb 2025 16:08:30 +0300 Subject: [PATCH 05/12] feat: add delete ui callback --- dwpicker/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dwpicker/main.py b/dwpicker/main.py index b6ff293..33c0d4f 100644 --- a/dwpicker/main.py +++ b/dwpicker/main.py @@ -40,6 +40,7 @@ load_local_picker_data, store_local_picker_data, clean_stray_picker_holder_nodes) from dwpicker.templates import PICKER, BACKGROUND +from dwpicker.designer.viewportwidget import ui_delete_callback ABOUT = """\ @@ -237,6 +238,7 @@ def show(self, *args, **kwargs): self.register_callbacks() def close_event(self): + ui_delete_callback() self.preferences_window.close() def list_scene_namespaces(self): From e50e10b5699e3e7b88a960424052399d6e99cb0b Mon Sep 17 00:00:00 2001 From: herizoran Date: Tue, 11 Feb 2025 10:51:31 +0300 Subject: [PATCH 06/12] feat: add saving image handling --- dwpicker/designer/viewportwidget.py | 39 ++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/dwpicker/designer/viewportwidget.py b/dwpicker/designer/viewportwidget.py index e604379..c5bb0f9 100644 --- a/dwpicker/designer/viewportwidget.py +++ b/dwpicker/designer/viewportwidget.py @@ -7,6 +7,9 @@ from maya import mel from dwpicker.capture import snap +from dwpicker.optionvar import (OVERRIDE_PROD_PICKER_DIRECTORY_ENV, + CUSTOM_PROD_PICKER_DIRECTORY, + LAST_IMAGE_DIRECTORY_USED, save_optionvar) from dwpicker.pyside import QtWidgets, QtCore, QtGui from dwpicker.pyside import shiboken2 from dwpicker.qtutils import icon @@ -250,6 +253,35 @@ def show_notification(parent, message="Notification", duration=2000): NotificationWidget(parent, message, duration) +def get_custom_picker_directory(): + dir_env_overridden = cmds.optionVar( + query=OVERRIDE_PROD_PICKER_DIRECTORY_ENV) + if not dir_env_overridden: + return None + + dir_path = cmds.optionVar(query=CUSTOM_PROD_PICKER_DIRECTORY) + if not os.path.isdir(dir_path): + return None + + return dir_path + + +def get_filename(): + folder_path = get_custom_picker_directory() + if not folder_path: + folder_path = QtWidgets.QFileDialog.getExistingDirectory(None, + "Select Directory", + "") + if folder_path: + save_optionvar(LAST_IMAGE_DIRECTORY_USED, folder_path) + + if not folder_path: + folder_path = cmds.optionVar(query=LAST_IMAGE_DIRECTORY_USED) + filename = os.path.join(folder_path, "dwpicker-" + str(uuid.uuid4())) + + return filename + + def ui_delete_callback(): panels = cmds.getPanel(type="modelPanel") for panel in panels: @@ -320,13 +352,8 @@ def capture_snapshot(cls, combo_box): show_ornaments: boolean (Hide Axis and camera names,... when False) """ active_camera = combo_box.currentText() - folder_path = QtWidgets.QFileDialog.getExistingDirectory(None, - "Select Directory", - "") - if not folder_path: - return - filename = os.path.join(folder_path, str(uuid.uuid4())) + filename = get_filename() snap(active_camera, off_screen=True, From 8e79be7b360d77b6ef67d1b820367379a3b00218 Mon Sep 17 00:00:00 2001 From: herizoran Date: Tue, 25 Feb 2025 17:12:42 +0300 Subject: [PATCH 07/12] chore: move viewport toggle on the toolbar --- dwpicker/designer/editor.py | 5 +++++ dwpicker/designer/menu.py | 6 ++++++ dwpicker/designer/qsplitter.py | 22 +++++++--------------- dwpicker/icons/3d_axis.png | Bin 0 -> 563 bytes 4 files changed, 18 insertions(+), 15 deletions(-) create mode 100644 dwpicker/icons/3d_axis.png diff --git a/dwpicker/designer/editor.py b/dwpicker/designer/editor.py index 6da1816..989da7e 100644 --- a/dwpicker/designer/editor.py +++ b/dwpicker/designer/editor.py @@ -74,6 +74,7 @@ def __init__(self, document, parent=None): self.menu.snapValuesChanged.connect(self.snap_value_changed) self.menu.buttonLibraryRequested.connect(self.call_library) self.menu.useSnapToggled.connect(self.use_snap) + self.menu.viewportToggled.connect(self.toggle_viewport) method = self.shape_canvas.set_lock_background_shape self.menu.lockBackgroundShapeToggled.connect(method) self.menu.undoRequested.connect(self.document.undo) @@ -139,6 +140,10 @@ def __init__(self, document, parent=None): self.vlayout.addWidget(self.menu) self.vlayout.addLayout(self.hlayout) + def toggle_viewport(self): + if self.splitter_layout.splitter_handle: + self.splitter_layout.splitter_handle.toggle_left_widget() + def call_library(self, point): self.shape_library_menu.move(point) self.shape_library_menu.show() diff --git a/dwpicker/designer/menu.py b/dwpicker/designer/menu.py index 22fdfcf..2daaa3e 100644 --- a/dwpicker/designer/menu.py +++ b/dwpicker/designer/menu.py @@ -33,6 +33,7 @@ class MenuWidget(QtWidgets.QWidget): symmetryRequested = QtCore.Signal(bool) undoRequested = QtCore.Signal() useSnapToggled = QtCore.Signal(bool) + viewportToggled = QtCore.Signal() def __init__(self, display_options, parent=None): super(MenuWidget, self).__init__(parent=parent) @@ -89,6 +90,10 @@ def __init__(self, display_options, parent=None): self.hierarchy.setChecked(state) self.hierarchy.toggled.connect(self.toggle_hierarchy_display) + self.viewport = QtWidgets.QAction(icon('3d_axis.png'), '', self) + self.viewport.triggered.connect(self.viewportToggled.emit) + self.viewport.setToolTip('Toggle viewport') + self.snap = QtWidgets.QAction(icon('snap.png'), '', self) self.snap.setToolTip('Snap grid enable') self.snap.setCheckable(True) @@ -202,6 +207,7 @@ def __init__(self, display_options, parent=None): self.toolbar.addAction(self.lock_bg) self.toolbar.addAction(self.isolate) self.toolbar.addAction(self.hierarchy) + self.toolbar.addAction(self.viewport) self.toolbar.addSeparator() self.toolbar.addAction(self.snap) self.toolbar.addWidget(self.snapx) diff --git a/dwpicker/designer/qsplitter.py b/dwpicker/designer/qsplitter.py index 019d470..168f561 100644 --- a/dwpicker/designer/qsplitter.py +++ b/dwpicker/designer/qsplitter.py @@ -4,31 +4,23 @@ class CustomSplitterHandle(QtWidgets.QSplitterHandle): def __init__(self, orientation, parent): super(CustomSplitterHandle, self).__init__(orientation, parent) - - self.split_button = QtWidgets.QPushButton("▶", self) - self.split_button.setCursor(QtCore.Qt.ArrowCursor) - self.split_button.setFixedSize(30, 30) - self.split_button.clicked.connect(self.toggle_left_widget) self.splitter = parent - def resizeEvent(self, event): - super(CustomSplitterHandle, self).resizeEvent(event) - if self.orientation() == QtCore.Qt.Horizontal: - self.split_button.move( - (self.width() - self.split_button.width()) // 2, - (self.height() - self.split_button.height()) // 2) - def toggle_left_widget(self): """Collapse or expand the left widget""" sizes = self.splitter.sizes() if sizes[0] > 0: # If left widget is visible self.splitter.setSizes([0, sizes[1]]) - self.split_button.setText("▶") else: self.splitter.setSizes([sizes[1] // 2, sizes[1] // 2]) - self.split_button.setText("◀") class CustomSplitter(QtWidgets.QSplitter): + def __init__(self, orientation, parent=None): + super(CustomSplitter, self).__init__(orientation, parent) + self.splitter_handle = None + def createHandle(self): - return CustomSplitterHandle(self.orientation(), self) + handle = CustomSplitterHandle(self.orientation(), self) + self.splitter_handle = handle + return handle diff --git a/dwpicker/icons/3d_axis.png b/dwpicker/icons/3d_axis.png new file mode 100644 index 0000000000000000000000000000000000000000..180fd53afa2c9c6f25717c5e5cebee6948263780 GIT binary patch literal 563 zcmV-30?hr1P)i0qEHw*|Q`dByDRyziHJ zGxOG%p@!-aD`0~J8{Ki(z-01$ay)nd!6^y0!!6un!6z16aD=CY0fdSQDGpiiixn|P zn9njlk!QUx0INse@z6aNteO-=Pgvfw0B)O@{8pg z0fJH!foVF3vDNrWYt184drWX=aOVB|XAE_FbqSJ&(& Date: Tue, 25 Feb 2025 18:58:26 +0300 Subject: [PATCH 08/12] chore: refactor image folder path --- dwpicker/designer/viewportwidget.py | 58 ++++++++++++++--------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/dwpicker/designer/viewportwidget.py b/dwpicker/designer/viewportwidget.py index c5bb0f9..a2000ca 100644 --- a/dwpicker/designer/viewportwidget.py +++ b/dwpicker/designer/viewportwidget.py @@ -253,35 +253,6 @@ def show_notification(parent, message="Notification", duration=2000): NotificationWidget(parent, message, duration) -def get_custom_picker_directory(): - dir_env_overridden = cmds.optionVar( - query=OVERRIDE_PROD_PICKER_DIRECTORY_ENV) - if not dir_env_overridden: - return None - - dir_path = cmds.optionVar(query=CUSTOM_PROD_PICKER_DIRECTORY) - if not os.path.isdir(dir_path): - return None - - return dir_path - - -def get_filename(): - folder_path = get_custom_picker_directory() - if not folder_path: - folder_path = QtWidgets.QFileDialog.getExistingDirectory(None, - "Select Directory", - "") - if folder_path: - save_optionvar(LAST_IMAGE_DIRECTORY_USED, folder_path) - - if not folder_path: - folder_path = cmds.optionVar(query=LAST_IMAGE_DIRECTORY_USED) - filename = os.path.join(folder_path, "dwpicker-" + str(uuid.uuid4())) - - return filename - - def ui_delete_callback(): panels = cmds.getPanel(type="modelPanel") for panel in panels: @@ -345,6 +316,35 @@ def toggle_camera_settings(panel, camera_name="persp", option="resolution"): cmds.camera(camera_name, edit=True, displayFieldChart=True) +def get_custom_picker_directory(): + dir_env_overridden = cmds.optionVar( + query=OVERRIDE_PROD_PICKER_DIRECTORY_ENV) + if not dir_env_overridden: + return None + + dir_path = cmds.optionVar(query=CUSTOM_PROD_PICKER_DIRECTORY) + if not os.path.isdir(dir_path): + return None + + return dir_path + + +def get_filename(): + folder_path = get_custom_picker_directory() + if not folder_path: + folder_path = QtWidgets.QFileDialog.getExistingDirectory(None, + "Select Directory", + "") + if folder_path: + save_optionvar(LAST_IMAGE_DIRECTORY_USED, folder_path) + else: + folder_path = cmds.optionVar(query=LAST_IMAGE_DIRECTORY_USED) + + filename = os.path.join(folder_path, "dwpicker-" + str(uuid.uuid4())) + + return filename + + def capture_snapshot(cls, combo_box): """ snapshot args: From dfecaf6201aa601361941c0a2e9c7ad9316c10f8 Mon Sep 17 00:00:00 2001 From: herizoran Date: Tue, 25 Mar 2025 00:26:47 +0300 Subject: [PATCH 09/12] chore: refactoring --- dwpicker/designer/editor.py | 2 +- dwpicker/designer/viewportwidget.py | 137 ++++++++++++---------------- dwpicker/path.py | 21 ++++- 3 files changed, 79 insertions(+), 81 deletions(-) diff --git a/dwpicker/designer/editor.py b/dwpicker/designer/editor.py index 989da7e..de12e47 100644 --- a/dwpicker/designer/editor.py +++ b/dwpicker/designer/editor.py @@ -301,7 +301,7 @@ def create_library_shape(self, path): def create_shape( self, template, before=False, position=None, targets=None, - image=False,filepath=None): + image=False, filepath=None): options = deepcopy(template) panel = self.shape_canvas.display_options.current_panel diff --git a/dwpicker/designer/viewportwidget.py b/dwpicker/designer/viewportwidget.py index a2000ca..6cb3020 100644 --- a/dwpicker/designer/viewportwidget.py +++ b/dwpicker/designer/viewportwidget.py @@ -7,9 +7,8 @@ from maya import mel from dwpicker.capture import snap -from dwpicker.optionvar import (OVERRIDE_PROD_PICKER_DIRECTORY_ENV, - CUSTOM_PROD_PICKER_DIRECTORY, - LAST_IMAGE_DIRECTORY_USED, save_optionvar) + +from dwpicker.path import get_filename from dwpicker.pyside import QtWidgets, QtCore, QtGui from dwpicker.pyside import shiboken2 from dwpicker.qtutils import icon @@ -64,12 +63,13 @@ def __init__(self, editor, parent=None): camera_ = cmds.camera(n=self.camera_name)[0] cmds.rename(camera_, self.camera_name) - self.model_panel_name = cmds.modelPanel(picker_model_name, mbv=False) + self.model_panel_name = cmds.modelPanel( + picker_model_name, menuBarVisible=False) cmds.modelEditor(self.model_panel_name, - edit=True, - displayAppearance='smoothShaded', - cam=self.camera_name, - gr=True) + edit=True, + displayAppearance='smoothShaded', + camera=self.camera_name, + grid=True) ptr = omui.MQtUtil.findControl(self.model_panel_name) self.model_panel_widget = shiboken2.wrapInstance(long(ptr), @@ -144,7 +144,7 @@ def __init__(self, editor, parent=None): self.snapshot = QtWidgets.QAction(icon('snapshot.png'), '', self) self.snapshot.setToolTip("Capture snapshot") self.snapshot.triggered.connect( - lambda: capture_snapshot(self, camera_combo_box)) + lambda: self.capture_snapshot(camera_combo_box)) self.toolbar = QtWidgets.QToolBar() self.toolbar.setIconSize(QtCore.QSize(14, 14)) @@ -168,12 +168,12 @@ def __init__(self, editor, parent=None): def showEvent(self, event): super(ViewportWidget, self).showEvent(event) - self.model_panel_widget.repaint() + self.model_panel_widget.update() def add_snapshot_image(self, file=None): if os.path.exists(file): - self.editor.create_shape(BACKGROUND, before=True, image=True, - filepath=file) + self.editor.create_shape( + BACKGROUND, before=True, image=True, filepath=file) def update_camera_viewport(self, combo_box, panel): """ @@ -183,9 +183,8 @@ def update_camera_viewport(self, combo_box, panel): cmds.modelPanel(panel, edit=True, camera=active_camera) self.camera_name = active_camera - def update_resolution_settings(self, combobox, image_size, - custom_width=None, - custom_height=None): + def update_resolution_settings( + self, combobox, image_size, custom_width=None, custom_height=None): resolution = combobox.currentText() if resolution == "Custom": @@ -220,6 +219,29 @@ def update_resolution_settings(self, combobox, image_size, device_aspect_ratio) cmds.setAttr("defaultResolution.pixelAspect", 1) + def capture_snapshot(self, combo_box): + """ + snapshot args: + off_screen: boolean (Process in the background when True) + show_ornaments: boolean (Hide Axis and camera names,... when False) + """ + active_camera = combo_box.currentText() + + filename = get_filename() + + snap(active_camera, + off_screen=True, + filename=filename, + frame_padding=0, + show_ornaments=False, + # clipboard=True, + maintain_aspect_ratio=True, + camera_options={"displayFieldChart": False}) + + # NotificationWidget.show_notification(cls, "Snapshot done!") + + self.add_snapshot_image(filename + ".0.png") + class NotificationWidget(QtWidgets.QLabel): def __init__(self, parent=None, message="Notification", duration=2000): @@ -249,7 +271,7 @@ def __init__(self, parent=None, message="Notification", duration=2000): self.timer.start(duration) @staticmethod - def show_notification(parent, message="Notification", duration=2000): + def show_notification(parent, message="", duration=2000): NotificationWidget(parent, message, duration) @@ -279,91 +301,48 @@ def toggle_camera_view(camera_name): orthographic=True) up_dir = cmds.camera(camera_name, query=True, worldUp=True) - camera_view = {"o": True} if is_perspective else {"p": True} - cmds.viewPlace(camera_name, up=(up_dir[0], up_dir[1], up_dir[2]), - **camera_view) + camera_view = {"ortho": True} if is_perspective else {"perspective": True} + cmds.viewPlace( + camera_name, up=(up_dir[0], up_dir[1], up_dir[2]), **camera_view) def toggle_camera_settings(panel, camera_name="persp", option="resolution"): """ Toggles the camera settings based on the option specified. - - 'resolution' toggles the camera resolution (displayResolution and overscan). - - 'field_chart' toggles the field chart display. + Args: + panel (str): The panel's name used on the viewport. + camera_name (str): The name of the camera. Defaults to "persp". + option (str): The setting to toggle. Options include: + - 'lock_camera': Locks or unlocks the camera. + - 'resolution': Toggles the camera's resolution settings (displayResolution and overscan). + - 'field_chart': Toggles the display of the field chart. """ if option == "lock_camera": mel.eval("changeCameraLockStatus" + " %s;" % panel) if option == "resolution": - display_resolution = cmds.camera(camera_name, query=True, - displayResolution=True) + display_resolution = cmds.camera( + camera_name, query=True, displayResolution=True) overscan_value = cmds.camera(camera_name, query=True, overscan=True) if display_resolution and overscan_value == 1.3: - cmds.camera(camera_name, edit=True, displayFilmGate=False, - displayResolution=False, overscan=1.0) + cmds.camera( + camera_name, edit=True, displayFilmGate=False, + displayResolution=False, overscan=1.0) return - cmds.camera(camera_name, edit=True, displayFilmGate=False, - displayResolution=True, overscan=1.3) + cmds.camera( + camera_name, edit=True, displayFilmGate=False, + displayResolution=True, overscan=1.3) elif option == "field_chart": - field_chart_display = cmds.camera(camera_name, query=True, - displayFieldChart=True) + field_chart_display = cmds.camera( + camera_name, query=True, displayFieldChart=True) if field_chart_display: cmds.camera(camera_name, edit=True, displayFieldChart=False) return cmds.camera(camera_name, edit=True, displayFieldChart=True) -def get_custom_picker_directory(): - dir_env_overridden = cmds.optionVar( - query=OVERRIDE_PROD_PICKER_DIRECTORY_ENV) - if not dir_env_overridden: - return None - - dir_path = cmds.optionVar(query=CUSTOM_PROD_PICKER_DIRECTORY) - if not os.path.isdir(dir_path): - return None - - return dir_path - - -def get_filename(): - folder_path = get_custom_picker_directory() - if not folder_path: - folder_path = QtWidgets.QFileDialog.getExistingDirectory(None, - "Select Directory", - "") - if folder_path: - save_optionvar(LAST_IMAGE_DIRECTORY_USED, folder_path) - else: - folder_path = cmds.optionVar(query=LAST_IMAGE_DIRECTORY_USED) - - filename = os.path.join(folder_path, "dwpicker-" + str(uuid.uuid4())) - - return filename - - -def capture_snapshot(cls, combo_box): - """ - snapshot args: - off_screen: boolean (Process in the background when True) - show_ornaments: boolean (Hide Axis and camera names,... when False) - """ - active_camera = combo_box.currentText() - - filename = get_filename() - - snap(active_camera, - off_screen=True, - filename=filename, - frame_padding=0, - show_ornaments=False, - # clipboard=True, - maintain_aspect_ratio=True, - camera_options={"displayFieldChart": False}) - - NotificationWidget.show_notification(cls, "Snapshot done!") - cls.add_snapshot_image(filename + ".0.png") diff --git a/dwpicker/path.py b/dwpicker/path.py index e7ff6f2..86a36db 100644 --- a/dwpicker/path.py +++ b/dwpicker/path.py @@ -1,11 +1,15 @@ import os +import uuid from maya import cmds +from dwpicker.pyside import QtWidgets from dwpicker.optionvar import ( AUTO_COLLAPSE_IMG_PATH_FROM_ENV, CUSTOM_PROD_PICKER_DIRECTORY, LAST_IMPORT_DIRECTORY, LAST_IMAGE_DIRECTORY_USED, LAST_OPEN_DIRECTORY, - OVERRIDE_PROD_PICKER_DIRECTORY_ENV, USE_PROD_PICKER_DIR_AS_DEFAULT) + OVERRIDE_PROD_PICKER_DIRECTORY_ENV, USE_PROD_PICKER_DIR_AS_DEFAULT, + save_optionvar) + def unix_path(path, isroot=False): @@ -78,3 +82,18 @@ def get_image_directory(): if directory: return directory return cmds.optionVar(query=LAST_IMAGE_DIRECTORY_USED) + +def get_filename(): + folder_path = get_picker_project_directory() + if not folder_path: + folder_path = QtWidgets.QFileDialog.getExistingDirectory(None, + "Select Directory", + "") + if folder_path: + save_optionvar(LAST_IMAGE_DIRECTORY_USED, folder_path) + else: + folder_path = cmds.optionVar(query=LAST_IMAGE_DIRECTORY_USED) + + filename = os.path.join(folder_path, "dwpicker-" + str(uuid.uuid4())) + + return filename \ No newline at end of file From 492632fa579be315a0f24b8d506a70faf6ee1393 Mon Sep 17 00:00:00 2001 From: herizoran Date: Tue, 25 Mar 2025 17:52:51 +0300 Subject: [PATCH 10/12] chore: remove passing the class editor from the viewport class --- dwpicker/designer/editor.py | 6 +++++- dwpicker/designer/viewportwidget.py | 20 ++++---------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/dwpicker/designer/editor.py b/dwpicker/designer/editor.py index de12e47..bf9d1eb 100644 --- a/dwpicker/designer/editor.py +++ b/dwpicker/designer/editor.py @@ -52,7 +52,8 @@ def __init__(self, document, parent=None): self.display_options = DisplayOptions() - self.viewport_widget = ViewportWidget(self) + self.viewport_widget = ViewportWidget() + self.viewport_widget.addSnapshotRequested.connect(self.capture_snapshot) self.shape_canvas = ShapeEditCanvas( self.document, self.display_options) @@ -144,6 +145,9 @@ def toggle_viewport(self): if self.splitter_layout.splitter_handle: self.splitter_layout.splitter_handle.toggle_left_widget() + def capture_snapshot(self, file=None): + self.create_shape(BACKGROUND, before=True, image=True, filepath=file) + def call_library(self, point): self.shape_library_menu.move(point) self.shape_library_menu.show() diff --git a/dwpicker/designer/viewportwidget.py b/dwpicker/designer/viewportwidget.py index 6cb3020..55dcf44 100644 --- a/dwpicker/designer/viewportwidget.py +++ b/dwpicker/designer/viewportwidget.py @@ -1,4 +1,3 @@ -import os import sys import uuid @@ -12,7 +11,6 @@ from dwpicker.pyside import QtWidgets, QtCore, QtGui from dwpicker.pyside import shiboken2 from dwpicker.qtutils import icon -from dwpicker.templates import BACKGROUND if sys.version_info[0] == 3: long = int @@ -31,10 +29,10 @@ class ViewportWidget(QtWidgets.QWidget): - def __init__(self, editor, parent=None): - super(ViewportWidget, self).__init__(parent) + addSnapshotRequested = QtCore.Signal(str) - self.editor = editor + def __init__(self, parent=None): + super(ViewportWidget, self).__init__(parent) self.setObjectName("ViewportWidget") self.resize(320, 620) @@ -170,11 +168,6 @@ def showEvent(self, event): super(ViewportWidget, self).showEvent(event) self.model_panel_widget.update() - def add_snapshot_image(self, file=None): - if os.path.exists(file): - self.editor.create_shape( - BACKGROUND, before=True, image=True, filepath=file) - def update_camera_viewport(self, combo_box, panel): """ Update the camera in the active panel when a new camera is selected from the combo box. @@ -220,11 +213,6 @@ def update_resolution_settings( cmds.setAttr("defaultResolution.pixelAspect", 1) def capture_snapshot(self, combo_box): - """ - snapshot args: - off_screen: boolean (Process in the background when True) - show_ornaments: boolean (Hide Axis and camera names,... when False) - """ active_camera = combo_box.currentText() filename = get_filename() @@ -240,7 +228,7 @@ def capture_snapshot(self, combo_box): # NotificationWidget.show_notification(cls, "Snapshot done!") - self.add_snapshot_image(filename + ".0.png") + self.addSnapshotRequested.emit(filename + ".0.png") class NotificationWidget(QtWidgets.QLabel): From 0ee270312143d1e310d54f83c7a8369da3b23610 Mon Sep 17 00:00:00 2001 From: herizoran Date: Tue, 25 Mar 2025 19:14:26 +0300 Subject: [PATCH 11/12] chore: remove custom splitter, add normal splitter --- dwpicker/designer/editor.py | 11 +++++++---- dwpicker/designer/qsplitter.py | 26 -------------------------- 2 files changed, 7 insertions(+), 30 deletions(-) delete mode 100644 dwpicker/designer/qsplitter.py diff --git a/dwpicker/designer/editor.py b/dwpicker/designer/editor.py index bf9d1eb..d8f849f 100644 --- a/dwpicker/designer/editor.py +++ b/dwpicker/designer/editor.py @@ -26,7 +26,6 @@ from dwpicker.designer.display import DisplayOptions from dwpicker.designer.menu import MenuWidget from dwpicker.designer.attributes import AttributeEditor -from dwpicker.designer.qsplitter import CustomSplitter from dwpicker.designer.viewportwidget import ViewportWidget @@ -41,7 +40,7 @@ def __init__(self, document, parent=None): title = "Picker editor - " + document.data['general']['name'] self.setWindowTitle(title) - self.splitter_layout = CustomSplitter(QtCore.Qt.Horizontal) + self.splitter_layout = QtWidgets.QSplitter() self.splitter_layout.setObjectName("SplitterLayout") self.document = document @@ -142,8 +141,12 @@ def __init__(self, document, parent=None): self.vlayout.addLayout(self.hlayout) def toggle_viewport(self): - if self.splitter_layout.splitter_handle: - self.splitter_layout.splitter_handle.toggle_left_widget() + """Collapse or expand the left widget""" + sizes = self.splitter_layout.sizes() + if sizes[0] > 0: # If left widget is visible + self.splitter_layout.setSizes([0, sizes[1]]) + else: + self.splitter_layout.setSizes([sizes[1] // 2, sizes[1] // 2]) def capture_snapshot(self, file=None): self.create_shape(BACKGROUND, before=True, image=True, filepath=file) diff --git a/dwpicker/designer/qsplitter.py b/dwpicker/designer/qsplitter.py deleted file mode 100644 index 168f561..0000000 --- a/dwpicker/designer/qsplitter.py +++ /dev/null @@ -1,26 +0,0 @@ -from dwpicker.pyside import QtWidgets, QtCore - - -class CustomSplitterHandle(QtWidgets.QSplitterHandle): - def __init__(self, orientation, parent): - super(CustomSplitterHandle, self).__init__(orientation, parent) - self.splitter = parent - - def toggle_left_widget(self): - """Collapse or expand the left widget""" - sizes = self.splitter.sizes() - if sizes[0] > 0: # If left widget is visible - self.splitter.setSizes([0, sizes[1]]) - else: - self.splitter.setSizes([sizes[1] // 2, sizes[1] // 2]) - - -class CustomSplitter(QtWidgets.QSplitter): - def __init__(self, orientation, parent=None): - super(CustomSplitter, self).__init__(orientation, parent) - self.splitter_handle = None - - def createHandle(self): - handle = CustomSplitterHandle(self.orientation(), self) - self.splitter_handle = handle - return handle From 63c8252850df4b6c42ace8889ee5e1b89c018751 Mon Sep 17 00:00:00 2001 From: herizoran Date: Tue, 25 Mar 2025 22:20:25 +0300 Subject: [PATCH 12/12] chore: change notification method to instance method --- dwpicker/designer/viewportwidget.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/dwpicker/designer/viewportwidget.py b/dwpicker/designer/viewportwidget.py index 55dcf44..0662c41 100644 --- a/dwpicker/designer/viewportwidget.py +++ b/dwpicker/designer/viewportwidget.py @@ -34,6 +34,8 @@ class ViewportWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(ViewportWidget, self).__init__(parent) + self.notification = None + self.setObjectName("ViewportWidget") self.resize(320, 620) @@ -226,15 +228,18 @@ def capture_snapshot(self, combo_box): maintain_aspect_ratio=True, camera_options={"displayFieldChart": False}) - # NotificationWidget.show_notification(cls, "Snapshot done!") + # Instantiating the NotificationWidget during initialization can result in + # an incorrect notification position, as the parent widget is not + # fully created yet. + self.notification = NotificationWidget(self) + self.notification.show_notification("Snapshot done!") self.addSnapshotRequested.emit(filename + ".0.png") class NotificationWidget(QtWidgets.QLabel): - def __init__(self, parent=None, message="Notification", duration=2000): + def __init__(self, parent=None): super(NotificationWidget, self).__init__(parent) - self.setText(message) self.setStyleSheet(""" background-color: grey; color: white; @@ -246,22 +251,17 @@ def __init__(self, parent=None, message="Notification", duration=2000): self.setFixedSize(140, 30) parent_rect = parent.rect() - self.move( - (parent_rect.width() - self.width()) // 2, - 140 - ) + self.move((parent_rect.width() - self.width()) // 2, 140) self.timer = QtCore.QTimer(self) self.timer.setSingleShot(True) self.timer.timeout.connect(self.deleteLater) + def show_notification(self, message="", duration=2000): + self.setText(message) self.show() self.timer.start(duration) - @staticmethod - def show_notification(parent, message="", duration=2000): - NotificationWidget(parent, message, duration) - def ui_delete_callback(): panels = cmds.getPanel(type="modelPanel")