import maya.cmds as cmds if cmds.about(version=True) == "2025": from PySide6 import QtWidgets, QtGui import shiboken6 as shiboken else: from PySide2 import QtWidgets, QtGui import shiboken2 as shiboken import maya.OpenMayaUI as omui from enum import Enum import random import os import pathlib from datetime import datetime import time def get_scaling_ratio(): screen = QtGui.QGuiApplication.primaryScreen() return screen.logicalDotsPerInch() / 96.0 scaling_ratio = get_scaling_ratio() def maya_main_window(): main_window_pointer = omui.MQtUtil.mainWindow() return shiboken.wrapInstance(int(main_window_pointer), QtWidgets.QWidget) class OptionType(Enum): Unknown = 1 Bool = 2 Int = 3 Float = 4 Option = 5 File = 6 class Option: option_type = OptionType.Unknown option_name = None default = None setting = None widget = None def __init__(self, option_name, option_type, default, setting, meta=None): self.option_name = option_name self.option_type = option_type self.default = default self.setting = setting self.meta = meta def make_widget(self, form_layout): if self.option_type == OptionType.Bool: self.widget = QtWidgets.QCheckBox() self.widget.setChecked(self.default) form_layout.addRow(self.option_name + ":", self.widget) elif self.option_type == OptionType.Int: self.widget = QtWidgets.QSpinBox() self.widget.setFixedWidth(70 * scaling_ratio) self.widget.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) if self.meta: if "min" in self.meta: self.widget.setMinimum(self.meta["min"]) if "max" in self.meta: self.widget.setMaximum(self.meta["max"]) self.widget.setValue(self.default) form_layout.addRow(self.option_name + ":", self.widget) elif self.option_type == OptionType.Float: self.widget = QtWidgets.QDoubleSpinBox() self.widget.setDecimals(4) self.widget.setSingleStep(0.01) self.widget.setFixedWidth(70 * scaling_ratio) self.widget.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) if self.meta: if "min" in self.meta: self.widget.setMinimum(self.meta["min"]) if "max" in self.meta: self.widget.setMaximum(self.meta["max"]) self.widget.setValue(self.default) form_layout.addRow(self.option_name + ":", self.widget) elif self.option_type == OptionType.Option: self.widget = QtWidgets.QComboBox() if self.meta: if "options" in self.meta: for option in self.meta["options"]: self.widget.addItem(option[1], option[0]) form_layout.addRow(self.option_name + ":", self.widget) index = self.widget.findData(self.default) self.widget.setCurrentIndex(index) elif self.option_type == OptionType.File: self.widget = QtWidgets.QLineEdit() widget_layout = QtWidgets.QHBoxLayout() widget_layout.setContentsMargins(0, 0, 0, 0) browse_button = QtWidgets.QPushButton("Browse") def browse_action(): path = self.widget.text() file_filter = "" if self.meta: if "fileFilter" in self.meta: file_filter = self.meta["fileFilter"] result = cmds.fileDialog2(dir=path, fileFilter=file_filter, dialogStyle=2) if result: self.widget.setText(result[0]) browse_button.clicked.connect(browse_action) widget_layout.addWidget(self.widget) widget_layout.addWidget(browse_button) form_layout.addRow(self.option_name + ":", widget_layout) self.widget.setText(self.default) def load(self): if not self.widget: return if self.option_type == OptionType.Bool: if cmds.optionVar(exists=self.setting): value = bool(cmds.optionVar(q=self.setting)) self.widget.setChecked(value) elif self.option_type == OptionType.Int: if cmds.optionVar(exists=self.setting): value = cmds.optionVar(q=self.setting) self.widget.setValue(value) elif self.option_type == OptionType.Float: if cmds.optionVar(exists=self.setting): value = cmds.optionVar(q=self.setting) self.widget.setValue(value) elif self.option_type == OptionType.Option: if cmds.optionVar(exists=self.setting): value = cmds.optionVar(q=self.setting) index = self.widget.findData(value) self.widget.setCurrentIndex(index) elif self.option_type == OptionType.File: if cmds.optionVar(exists=self.setting): value = cmds.optionVar(q=self.setting) self.widget.setText(value) def save(self): if not self.widget: return if self.option_type == OptionType.Bool: if self.widget.isChecked(): cmds.optionVar(iv=(self.setting, 1)) else: cmds.optionVar(iv=(self.setting, 0)) elif self.option_type == OptionType.Int: cmds.optionVar(iv=(self.setting, self.widget.value())) elif self.option_type == OptionType.Float: cmds.optionVar(fv=(self.setting, self.widget.value())) elif self.option_type == OptionType.Option: cmds.optionVar(iv=(self.setting, self.widget.currentData())) elif self.option_type == OptionType.File: cmds.optionVar(sv=(self.setting, self.widget.text())) def reset(self): if not self.widget: return if self.option_type == OptionType.Bool: self.widget.setChecked(self.default) if self.default: cmds.optionVar(iv=(self.setting, 1)) else: cmds.optionVar(iv=(self.setting, 0)) elif self.option_type == OptionType.Int: self.widget.setValue(self.default) cmds.optionVar(iv=(self.setting, self.default)) elif self.option_type == OptionType.Float: self.widget.setValue(self.default) cmds.optionVar(fv=(self.setting, self.default)) elif self.option_type == OptionType.Option: index = self.widget.findData(self.default) self.widget.setCurrentIndex(index) cmds.optionVar(iv=(self.setting, self.default)) elif self.option_type == OptionType.File: self.widget.setText(self.default) cmds.optionVar(sv=(self.setting, self.default)) class OptionsDialogType(Enum): Create = 1 Export = 2 class OptionsDialog(QtWidgets.QDialog): def __init__(self, options_dialog_type, title, command, options, parent=None): super(OptionsDialog, self).__init__(parent) self.setWindowTitle(title) self.options_dialog_type = options_dialog_type self.command = command self.options = options layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) self.setLayout(layout) menu_bar = QtWidgets.QMenuBar() edit_menu = menu_bar.addMenu("Edit") edit_menu.addAction("Save Settings", self.save) edit_menu.addAction("Reset Settings", self.reset) if self.options_dialog_type == OptionsDialogType.Export: edit_menu.addSeparator() edit_menu.addAction("Clear Log", self.clear_log) layout.addWidget(menu_bar) form_layout = QtWidgets.QFormLayout() form_layout_margins = 8 * scaling_ratio form_layout.setContentsMargins( form_layout_margins, form_layout_margins, form_layout_margins, form_layout_margins) for option in self.options: option.make_widget(form_layout) layout.addLayout(form_layout) if options_dialog_type == OptionsDialogType.Create: layout.addStretch() buttons_layout = QtWidgets.QHBoxLayout() layout.addLayout(buttons_layout) buttons_layout_margins = 4 * scaling_ratio buttons_layout.setContentsMargins( buttons_layout_margins, buttons_layout_margins, buttons_layout_margins, buttons_layout_margins) buttons_layout.setSpacing(4) if options_dialog_type == OptionsDialogType.Create: create_button = QtWidgets.QPushButton("Create") apply_button = QtWidgets.QPushButton("Apply") close_button = QtWidgets.QPushButton("Close") create_button.clicked.connect(self.create) apply_button.clicked.connect(self.apply) close_button.clicked.connect(self.close) buttons_layout.addWidget(create_button) buttons_layout.addWidget(apply_button) buttons_layout.addWidget(close_button) elif options_dialog_type == OptionsDialogType.Export: self.log = QtWidgets.QTextEdit() #self.log.setFixedHeight(80) self.log.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) self.log.setReadOnly(True) form_layout.addRow("Log:", self.log) export_button = QtWidgets.QPushButton("Export") close_button = QtWidgets.QPushButton("Close") export_button.clicked.connect(self.apply) close_button.clicked.connect(self.close) buttons_layout.addStretch() buttons_layout.addWidget(export_button) buttons_layout.addWidget(close_button) self.load() self.show() def load(self): for option in self.options: option.load() def save(self): for option in self.options: option.save() def reset(self): for option in self.options: option.reset() def create(self): self.save() self.command() self.close() def apply(self): self.save() if self.options_dialog_type == OptionsDialogType.Export: start_time = time.time() start_date = datetime.now() self.log.moveCursor(QtGui.QTextCursor.End) self.log.append("Export started " + str(start_date)) try: self.command() end_time = time.time() end_date = datetime.now() self.log.moveCursor(QtGui.QTextCursor.End) self.log.append("Export ended " + str(end_date)) self.log.append("Export took %.4f seconds" % (end_time - start_time)) self.log.append("Done") except Exception as e: self.log.moveCursor(QtGui.QTextCursor.End) self.log.append("%s" % e) self.log.append("Failure") else: self.command() def activate(self): self.load() self.show() self.activateWindow() def clear_log(self): self.log.clear() def get_value(setting): if cmds.optionVar(exists=setting[1]): return cmds.optionVar(q=setting[1]) return setting[0] create_ahoge_window = None create_ahoge_randomize_seed = [True, "ahoge_randomize_seed"] create_ahoge_swap_uv = [True, "ahoge_randomize_swap_uv"] create_ahoge_segments = [16, "ahoge_segments"] create_ahoge_number_of_curves = [10, "ahoge_number_of_curves"] create_ahoge_width = [0.01, "ahoge_width"] def create_ahoge(): cmds.undoInfo(chunkName="create_ahoge", openChunk=True) try: randomize_seed = get_value(create_ahoge_randomize_seed) swap_uv = get_value(create_ahoge_swap_uv) segments = get_value(create_ahoge_segments) number_of_curves = get_value(create_ahoge_number_of_curves) width = get_value(create_ahoge_width) surfaces = cmds.ls(sl=True, dag=True, type="nurbsSurface") ahoge_shape = cmds.createNode("ahogeShape") if randomize_seed: cmds.setAttr(ahoge_shape + ".seed", random.randint(0, 10000)) cmds.setAttr(ahoge_shape + ".numSegments", segments) cmds.setAttr(ahoge_shape + ".numCurves", number_of_curves) cmds.setAttr(ahoge_shape + ".width", width) for index, surface in enumerate(surfaces): if swap_uv and cmds.getAttr(surface + ".fu") == 0 and cmds.getAttr(surface + ".fv") == 2: cmds.warning(surface + " swapping UV") cmds.reverseSurface(surface, d=3, ch=1, rpo=1) cmds.reverseSurface(surface, d=0, ch=1, rpo=1) cmds.connectAttr(surface + ".worldSpace[0]", ahoge_shape + ".inputSurfaces[" + str(index) + "]") finally: cmds.undoInfo(closeChunk=True) def create_ahoge_options(): global create_ahoge_window if not create_ahoge_window: title = "Create Ahoge Options" options = [] options.append(Option("Randomize Seed", OptionType.Bool, *create_ahoge_randomize_seed)) options.append(Option("Swap UV", OptionType.Bool, *create_ahoge_swap_uv)) options.append(Option("Segments", OptionType.Int, *create_ahoge_segments, {"min": 1, "max": 1000000})) options.append(Option("Number of Curves", OptionType.Int, *create_ahoge_number_of_curves, {"min": 1, "max": 1000000})) options.append(Option("Width", OptionType.Float, *create_ahoge_width, {"min": 0.0001, "max": 1000000.0})) create_ahoge_window = OptionsDialog(OptionsDialogType.Create, title, create_ahoge, options, maya_main_window()) else: create_ahoge_window.activate() create_nurbopus_window = None create_nurbopus_method = [1, "ahoge_nurbopus_method"] create_nurbopus_shape = [0, "ahoge_nurbopus_shape"] create_nurbopus_useg = [4, "ahoge_nurbopus_useg"] create_nurbopus_vseg = [8, "ahoge_nurbopus_vseg"] def create_nurbopus(): cmds.undoInfo(chunkName="create_nurbopus", openChunk=True) try: method = get_value(create_nurbopus_method) shape = get_value(create_nurbopus_shape) useg = get_value(create_nurbopus_useg) vseg = get_value(create_nurbopus_vseg) curves = cmds.ls(sl=True, dag=True, type="nurbsCurve") if not curves: cmds.error("No curves selected") meshes = cmds.ls(sl=True, dag=True, type="mesh") nurbopus = cmds.createNode("nurbopusNode") cmds.setAttr(nurbopus + ".method", method) cmds.setAttr(nurbopus + ".shape", shape) cmds.setAttr(nurbopus + ".uSegments", useg) cmds.setAttr(nurbopus + ".vSegments", vseg) for index, curve in enumerate(curves): cmds.connectAttr(curve + ".worldSpace[0]", nurbopus + ".inputCurves[" + str(index) + "]") surface = cmds.createNode("nurbsSurface") cmds.connectAttr(nurbopus + ".outputSurfaces[" + str(index) + "]", surface + ".create") cmds.hyperShade(surface, assign="initialShadingGroup") if meshes: cmds.connectAttr(meshes[0] + ".worldMesh[0]", nurbopus + ".inputMesh") cmds.select(nurbopus) finally: cmds.undoInfo(closeChunk=True) def create_nurbopus_options(): global create_nurbopus_window if not create_nurbopus_window: title = "Create Nurbopus Options" options = [] options.append(Option("Method", OptionType.Option, *create_nurbopus_method, {"options": [[0, "Up Vector"], [1, "Untwist"]]})) options.append(Option("Shape", OptionType.Option, *create_nurbopus_shape, {"options": [[0, "circle"], [1, "line"]]})) options.append(Option("U Segments", OptionType.Int, *create_nurbopus_useg, {"min": 1, "max": 1000000})) options.append(Option("V Segments", OptionType.Int, *create_nurbopus_vseg, {"min": 1, "max": 1000000})) create_nurbopus_window = OptionsDialog(OptionsDialogType.Create, title, create_nurbopus, options, maya_main_window()) else: create_nurbopus_window.activate() export_alembic_window = None abc_path = pathlib.Path(os.environ["MAYA_APP_DIR"]) abc_path = abc_path / "projects" / "default" / "data" / "test.abc" export_alembic_path = [str(abc_path), "ahoge_export_alembic_path"] export_alembic_mode = [0, "ahoge_export_alembic_mode"] export_alembic_time_range = [0, "ahoge_export_alembic_time_range"] export_alembic_start = [1, "ahoge_export_alembic_start"] export_alembic_end = [24, "ahoge_export_alembic_end"] export_alembic_relative_sample = [False, "ahoge_export_alembic_relative_sample"] export_alembic_low = [-0.2, "ahoge_export_alembic_low"] export_alembic_high = [0.2, "ahoge_export_alembic_high"] def export_alembic(): alembic_mode = get_value(export_alembic_mode) alembic_time_range = get_value(export_alembic_time_range) alembic_path = get_value(export_alembic_path) start = get_value(export_alembic_start) end = get_value(export_alembic_end) relative_sample = get_value(export_alembic_relative_sample) low = get_value(export_alembic_low) high = get_value(export_alembic_high) export_mode="default" if alembic_mode == 1: export_mode = "unreal" elif alembic_mode == 2: export_mode = "marmoset" extra = "" time_range = "current_frame" if alembic_time_range == 1: time_range = "time_slider" elif alembic_time_range == 2: time_range = "start_end" extra += " -startEnd %d %d" % (start, end) if relative_sample: extra += " -relativeSample true -lowHigh %f %f" % (low, high) print("ahogeCmd -e \"%s\" -exportMode \"%s\" -timeRange \"%s\"%s;" % (alembic_path.replace("\\", "/"), export_mode, time_range, extra)) cmds.ahogeCmd(e=alembic_path, exportMode=export_mode, timeRange=time_range, startEnd=(start, end), relativeSample=relative_sample, lowHigh=(low, high)) def export_alembic_options(): global export_alembic_window if not export_alembic_window: title = "Ahoge Export: Alembic" options = [] options.append(Option("Path", OptionType.File, *export_alembic_path, {"fileFilter": "*.abc"})) options.append(Option("Mode", OptionType.Option, *export_alembic_mode, {"options": [[0, "Default"], [1, "Unreal"], [2, "Marmoset"]]})) options.append(Option("Time Range", OptionType.Option, *export_alembic_time_range, {"options": [[0, "Current Frame"], [1, "Time Slider"], [2, "Start/End"]]})) options.append(Option("Start Frame", OptionType.Int, *export_alembic_start, {"min": -1000000, "max": 1000000})) options.append(Option("End Frame", OptionType.Int, *export_alembic_end, {"min": -1000000, "max": 1000000})) options.append(Option("Relative Sample", OptionType.Bool, *export_alembic_relative_sample)) options.append(Option("Low", OptionType.Float, *export_alembic_low, {"min": -1.0, "max": 0.0})) options.append(Option("High", OptionType.Float, *export_alembic_high, {"min": 0.0, "max": 1.0})) export_alembic_window = OptionsDialog(OptionsDialogType.Export, title, export_alembic, options, maya_main_window()) else: export_alembic_window.activate()