commit 7baece90bca4b2fba5f7ca6379e307033db57149 Author: jeffreytsai1004 Date: Sun Nov 23 20:50:30 2025 +0800 Update diff --git a/2023/README.md b/2023/README.md new file mode 100644 index 0000000..06c65ed --- /dev/null +++ b/2023/README.md @@ -0,0 +1,47 @@ +# Nexus Maya 2023 Plugin + +## 目录结构 + +- **shelves/** - 工具架文件 (.mel 格式) + - shelf_Nexus_Modeling.mel - 建模工具架 + - shelf_Nexus_Rigging.mel - 绑定工具架 + - shelf_Nexus_Animation.mel - 动画工具架 + +- **scripts/** - Python/MEL 脚本 + - userSetup.py - Maya 启动时自动执行 + - nexus_test.py - 测试脚本 + - modeling_tools/ - 建模工具包 + - rigging_tools/ - 绑定工具包 + - animation_tools/ - 动画工具包 + +- **plug-ins/** - Maya 插件文件 (.py 或 .mll) + - nexus_plugin.py - Nexus 插件 + +- **icons/** - 工具图标 + - 工具架按钮使用的图标文件 + +## 使用方法 + +1. 在 NexusLauncher 的 config.json 中配置: + ```json + "maya_plugin_path": "E:/Zoroot/Dev/NexusLauncher/template/plugins/Nexus" + ``` + +2. 启动 Maya 2023,系统将自动: + - 加载 Nexus 工具架(Nexus_Modeling, Nexus_Rigging, Nexus_Animation) + - 执行 userSetup.py + - 设置环境变量 + +3. 测试: + - 检查是否出现 Nexus 工具架 + - 点击测试按钮 + - 应出现确认对话框 + +## 环境变量 + +启动时自动设置: +- MAYA_SHELF_PATH - 指向 shelves 目录 +- MAYA_SCRIPT_PATH - 指向 scripts 目录 +- PYTHONPATH - 指向 scripts 目录 +- MAYA_PLUG_IN_PATH - 指向 plug-ins 目录 +- XBMLANGPATH - 指向 icons 目录 diff --git a/2023/RELOAD_SHELF.py b/2023/RELOAD_SHELF.py new file mode 100644 index 0000000..81677ec --- /dev/null +++ b/2023/RELOAD_SHELF.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Nexus Shelf Reload Script +Use this script to reload Nexus shelves without restarting Maya +""" + +import maya.cmds as cmds +import maya.mel as mel +import os + +SHELF_NAMES = ["Nexus_Modeling", "Nexus_Rigging", "Nexus_Animation"] + +def reload_nexus_shelves(): + """Reload all Nexus shelves""" + print("=" * 60) + print("[Nexus] Reloading Nexus shelves...") + print("=" * 60) + + # Get shelf path + shelf_paths = os.environ.get('MAYA_SHELF_PATH', '') + + if not shelf_paths: + print("[Nexus] MAYA_SHELF_PATH not set") + try: + script_dir = os.path.dirname(os.path.abspath(__file__)) + shelf_paths = os.path.join(script_dir, "shelves") + print(f"[Nexus] Using script directory: {shelf_paths}") + except: + print("[Nexus] Could not determine shelf path") + return + + path_separator = ';' if os.name == 'nt' else ':' + shelf_path_list = shelf_paths.split(path_separator) + + for shelf_name in SHELF_NAMES: + # Find shelf file + shelf_file_found = None + for shelf_path in shelf_path_list: + shelf_path = shelf_path.strip() + if not shelf_path: + continue + + shelf_file = os.path.join(shelf_path, f"shelf_{shelf_name}.mel") + shelf_file = shelf_file.replace("\\", "/") + + if os.path.exists(shelf_file): + shelf_file_found = shelf_file + break + + if not shelf_file_found: + print(f"[Nexus] Could not find shelf_{shelf_name}.mel") + continue + + # Delete old shelf if exists + if cmds.shelfLayout(shelf_name, exists=True): + print(f"[Nexus] Deleting old shelf: {shelf_name}") + try: + cmds.deleteUI(shelf_name, layout=True) + except Exception as e: + print(f"[Nexus] Warning: Could not delete old shelf: {e}") + + # Load shelf using proper MEL method + print(f"[Nexus] Loading shelf: {shelf_name}") + try: + # Disable auto-save + mel.eval('optionVar -intValue "saveLastLoadedShelf" 0;') + + # Create shelf layout + mel.eval(f''' + global string $gShelfTopLevel; + if (`shelfLayout -exists {shelf_name}`) {{ + deleteUI -layout {shelf_name}; + }} + setParent $gShelfTopLevel; + shelfLayout -cellWidth 35 -cellHeight 34 {shelf_name}; + ''') + print(f"[Nexus] ✓ Created shelf layout: {shelf_name}") + + # Set parent and execute shelf script + mel.eval(f'setParent {shelf_name};') + mel.eval(f'source "{shelf_file_found}";') + mel.eval(f'shelf_{shelf_name}();') + print(f"[Nexus] ✓ Executed shelf script: shelf_{shelf_name}()") + + # Verify shelf + if cmds.shelfLayout(shelf_name, exists=True): + buttons = cmds.shelfLayout(shelf_name, query=True, childArray=True) or [] + if buttons: + print(f"[Nexus] ✓ Shelf loaded with {len(buttons)} button(s): {shelf_name}") + else: + print(f"[Nexus] ⚠ Shelf created but no buttons: {shelf_name}") + + # Remove auto-saved config + try: + maya_version = cmds.about(version=True).split()[0] + maya_app_dir = os.environ.get('MAYA_APP_DIR', '') + if maya_app_dir: + shelf_config = os.path.join(maya_app_dir, maya_version, "prefs", "shelves", f"shelf_{shelf_name}.mel") + if os.path.exists(shelf_config): + os.remove(shelf_config) + print(f"[Nexus] ✓ Removed auto-saved config: {shelf_name}") + except Exception as e: + print(f"[Nexus] Warning: {e}") + else: + print(f"[Nexus] ✗ Shelf layout not created: {shelf_name}") + + except Exception as e: + print(f"[Nexus] Error loading shelf {shelf_name}: {e}") + import traceback + traceback.print_exc() + continue + + print("=" * 60) + print("[Nexus] Shelf reload complete!") + print("=" * 60) + + +if __name__ == "__main__": + reload_nexus_shelves() diff --git a/2023/icons/README.md b/2023/icons/README.md new file mode 100644 index 0000000..438368a --- /dev/null +++ b/2023/icons/README.md @@ -0,0 +1,24 @@ +# Nexus Icons Directory + +存放工具架按钮使用的图标文件。 + +## 图标格式 + +- 支持格式:PNG, XPM, BMP +- 推荐格式:PNG(支持透明) +- 推荐尺寸:32x32 或 64x64 像素 + +## 命名规范 + +- 使用小写字母和下划线 +- 例如:`modeling_tool.png`, `rigging_helper.png` + +## 使用方法 + +在 MEL 工具架文件中引用: +```mel +-image "your_icon.png" +-image1 "your_icon.png" +``` + +Maya 会自动在 XBMLANGPATH 环境变量指定的目录中查找图标文件。 diff --git a/2023/icons/batchextrusion.png b/2023/icons/batchextrusion.png new file mode 100644 index 0000000..e74527d Binary files /dev/null and b/2023/icons/batchextrusion.png differ diff --git a/2023/plug-ins/nexus_plugin.py b/2023/plug-ins/nexus_plugin.py new file mode 100644 index 0000000..d85d6e6 --- /dev/null +++ b/2023/plug-ins/nexus_plugin.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Nexus Plugin +Maya Python API 2.0 plugin +""" + +import sys +import maya.api.OpenMaya as om + +def maya_useNewAPI(): + """Tell Maya to use Python API 2.0""" + pass + + +class NexusCmd(om.MPxCommand): + """Nexus command""" + + kPluginCmdName = "nexusCmd" + + def __init__(self): + om.MPxCommand.__init__(self) + + def doIt(self, args): + """Execute the command""" + print("[Nexus] Nexus Plugin command executed!") + om.MGlobal.displayInfo("Nexus Plugin is working!") + + +def cmdCreator(): + """Create command instance""" + return NexusCmd() + + +def initializePlugin(mobject): + """Initialize plugin""" + mplugin = om.MFnPlugin(mobject, "Nexus", "1.0", "Any") + try: + mplugin.registerCommand( + NexusCmd.kPluginCmdName, + cmdCreator + ) + print("[Nexus] Plugin initialized: nexus_plugin.py") + except: + sys.stderr.write(f"Failed to register command: {NexusCmd.kPluginCmdName}\n") + raise + + +def uninitializePlugin(mobject): + """Uninitialize plugin""" + mplugin = om.MFnPlugin(mobject) + try: + mplugin.deregisterCommand(NexusCmd.kPluginCmdName) + print("[Nexus] Plugin uninitialized: nexus_plugin.py") + except: + sys.stderr.write(f"Failed to deregister command: {NexusCmd.kPluginCmdName}\n") + raise diff --git a/2023/scripts/animation_tools/__init__.py b/2023/scripts/animation_tools/__init__.py new file mode 100644 index 0000000..b553fc0 --- /dev/null +++ b/2023/scripts/animation_tools/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Nexus Animation Tools Package +Animation utilities and helpers +""" + +__all__ = [] diff --git a/2023/scripts/modeling_tools/__init__.py b/2023/scripts/modeling_tools/__init__.py new file mode 100644 index 0000000..cb40630 --- /dev/null +++ b/2023/scripts/modeling_tools/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Nexus Modeling Tools Package +General modeling utilities +""" + +from .batchextrusion import show_batch_extrusion_ui + +__all__ = [ + 'show_batch_extrusion_ui' +] diff --git a/2023/scripts/modeling_tools/__pycache__/__init__.cpython-39.pyc b/2023/scripts/modeling_tools/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..dd05daf Binary files /dev/null and b/2023/scripts/modeling_tools/__pycache__/__init__.cpython-39.pyc differ diff --git a/2023/scripts/modeling_tools/__pycache__/batchextrusion.cpython-39.pyc b/2023/scripts/modeling_tools/__pycache__/batchextrusion.cpython-39.pyc new file mode 100644 index 0000000..1ad3e46 Binary files /dev/null and b/2023/scripts/modeling_tools/__pycache__/batchextrusion.cpython-39.pyc differ diff --git a/2023/scripts/modeling_tools/batchextrusion.py b/2023/scripts/modeling_tools/batchextrusion.py new file mode 100644 index 0000000..350c0c7 --- /dev/null +++ b/2023/scripts/modeling_tools/batchextrusion.py @@ -0,0 +1,761 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# @Site : CGNICO Games +# @Author : Jeffrey Tsai + +""" +Maya Batch Extrusion Shell Mesh Tool + +Features: +1. Copy selected model to a specified number of layers +2. Extrude each layer, automatically deleting original layers to prevent overlap +3. Set vertex colors increasing from the inside out +4. Support model merging +""" + +import maya.cmds as cmds +import maya.mel as mel + +# UI Style configurations + +MESSAGE_BUTTON_STYLE = """ + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #D97706, stop:1 #B45309); + color: white; + border: 1px solid #92400E; + border-radius: 8px; + padding: 10px 16px; + font-weight: 600; + font-size: 12px; + min-width: 100px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #F59E0B, stop:1 #D97706); + border-color: #D97706; + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #B45309, stop:1 #92400E); + border-color: #78350F; + } +""" + +SUCCESS_BUTTON_STYLE = """ + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #3B82F6, stop:1 #2563EB); + color: white; + border: none; + border-radius: 10px; + padding: 12px 20px; + font-weight: 600; + font-size: 13px; + min-width: 110px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #60A5FA, stop:1 #3B82F6); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #2563EB, stop:1 #1D4ED8); + transform: translateY(0px); + } +""" + +WARNING_BUTTON_STYLE = """ + QPushButton { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #F59E0B, stop:1 #D97706); + color: white; + border: none; + border-radius: 10px; + padding: 12px 20px; + font-weight: 600; + font-size: 13px; + min-width: 110px; + } + QPushButton:hover { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #FBBF24, stop:1 #F59E0B); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3); + } + QPushButton:pressed { + background: qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #D97706, stop:1 #B45309); + transform: translateY(0px); + } +""" + +class BatchExtrusion: + def __init__(self): + self.window_name = "BatchExtrusionWindow" + self.layers = 7 # Default number of layers + self.thickness = 0.1 # Default thickness + self.min_color = [0.0, 0.0, 0.0] # Inner layer color (black) + self.max_color = [1.0, 1.0, 1.0] # Outer layer color (white) + self.merge_layers = False # Whether to merge all layers + self.original_objects = [] # Original object list + self.generated_objects = [] # Generated object list + self.is_updating = False # Prevent recursive updates + self.button_info = [] # Store button information for delayed style application + self.qt_available = self.check_qt_availability() # Check Qt availability + + def check_qt_availability(self): + """Check Qt and related module availability""" + try: + maya_version = int(cmds.about(version=True)) + print(f"Maya version: {maya_version}") + + # Try PySide6 first (Maya 2022+) + try: + from PySide6 import QtWidgets, QtCore + import shiboken6 as shiboken + print("Using PySide6 (Maya 2022+)") + return "PySide6" + except ImportError: + try: + from PySide2 import QtWidgets, QtCore + import shiboken2 as shiboken + print("Using PySide2 (Maya 2020-2021)") + return "PySide2" + except ImportError: + print("PySide module is not available, using Maya native style") + return None + + except Exception as e: + print(f"Qt availability check failed: {str(e)}") + return None + + def create_ui(self): + """Create user interface""" + # If the window already exists, delete it + if cmds.window(self.window_name, exists=True): + cmds.deleteUI(self.window_name) + # Reset window preferences to avoid obscuring buttons due to old window dimensions + if cmds.windowPref(self.window_name, exists=True): + cmds.windowPref(self.window_name, remove=True) + + # Create window + cmds.window(self.window_name, title="Batch Extrusion Shell Mesh", widthHeight=(420, 600), sizeable=True) + + # Create main layout (using scroll layout to prevent small window obscuring bottom buttons) + scroll = cmds.scrollLayout(horizontalScrollBarThickness=0, verticalScrollBarThickness=12) + main_layout = cmds.columnLayout(adjustableColumn=True, rowSpacing=10, columnOffset=('both', 10)) + + # Title + cmds.text(label="Batch Extrusion Shell Mesh", font="boldLabelFont", height=30) + cmds.separator(height=10) + + # Number of layers control + cmds.text(label="Layers:", align="left", font="boldLabelFont") + self.layers_slider = cmds.intSliderGrp( + label="Layers: ", + field=True, + minValue=1, + maxValue=20, + value=self.layers, + step=1, + changeCommand=self.on_layers_changed + ) + + # Thickness control + cmds.text(label="Thickness:", align="left", font="boldLabelFont") + self.thickness_slider = cmds.floatSliderGrp( + label="Thickness: ", + field=True, + minValue=0.000001, + maxValue=10.0, + value=self.thickness, + precision=4, + step=0.01, + changeCommand=self.on_thickness_changed + ) + + # Vertex color control + cmds.text(label="Vertex Color:", align="left", font="boldLabelFont") + self.min_color_field = cmds.colorSliderGrp( + label="Inner layer: ", + rgb=self.min_color, + changeCommand=self.on_color_changed + ) + self.max_color_field = cmds.colorSliderGrp( + label="Outer layer: ", + rgb=self.max_color, + changeCommand=self.on_color_changed + ) + + # Merge options + cmds.text(label="Merge:", align="left", font="boldLabelFont") + self.merge_checkbox = cmds.checkBox( + label="Merge All Extruded Layers", + value=self.merge_layers, + changeCommand=self.on_merge_changed + ) + + # Preview area + cmds.text(label="Vertex Color Setting Preview:", align="left", font="boldLabelFont") + self.preview_text = cmds.scrollField( + editable=False, + wordWrap=True, + height=100, + backgroundColor=[0.2, 0.2, 0.2] + ) + + cmds.separator(height=10) + + # Operation buttons + self.create_styled_button("Extrude Shell Mesh", self.select_base_objects, SUCCESS_BUTTON_STYLE) + + self.create_styled_button("Clear All Extruded Layers", self.clear_all_layers, WARNING_BUTTON_STYLE) + + # Delay style application to ensure Qt controls are fully created + cmds.evalDeferred(self.apply_delayed_styles) + + # Show window + cmds.showWindow(self.window_name) + + # Initial preview + self.preview_colors() + + def create_styled_button(self, label, command, style): + """Create styled button""" + # Set base color based on style type + base_color = [0.4, 0.4, 0.4] # Default gray + if "SUCCESS" in style: + base_color = [0.2, 0.5, 0.8] # Blue + elif "WARNING" in style: + base_color = [0.8, 0.6, 0.2] # Orange + elif "MESSAGE" in style: + base_color = [0.7, 0.4, 0.1] # Deep orange + + # Create Maya button, set base color first + button_name = cmds.button( + label=label, + command=command, + height=35, + backgroundColor=base_color + ) + + # Store button info for delayed style application + self.button_info.append({ + 'name': button_name, + 'label': label, + 'style': style + }) + + # Immediately try to apply Qt style + self.apply_style_to_button(button_name, label, style) + + return button_name + + def apply_style_to_button(self, button_name, label, style): + """Apply style to a single button""" + # If Qt is not available, return False + if not self.qt_available: + print(f"Qt is not available, skipping style application: {label}") + return False + + try: + # Import corresponding modules based on detected Qt version + if self.qt_available == "PySide6": + from PySide6 import QtWidgets, QtCore + import shiboken6 as shiboken + elif self.qt_available == "PySide2": + from PySide2 import QtWidgets, QtCore + import shiboken2 as shiboken + else: + return False + + import maya.OpenMayaUI as omui + + # Get the Qt object of the button + button_ptr = omui.MQtUtil.findControl(button_name) + if button_ptr: + qt_button = shiboken.wrapInstance(int(button_ptr), QtWidgets.QPushButton) + if qt_button: + qt_button.setStyleSheet(style) + print(f"✓ Successfully applied Qt style: {label}") + return True + else: + print(f"✗ Failed to get Qt button object: {label}") + else: + print(f"✗ Failed to find button control: {label}") + + except Exception as e: + print(f"✗ Qt style application failed ({label}): {str(e)}") + + return False + + def apply_delayed_styles(self): + """Delay style application to all buttons""" + print("=== Starting delayed style application ===") + success_count = 0 + for button_info in self.button_info: + if cmds.control(button_info['name'], exists=True): + if self.apply_style_to_button( + button_info['name'], + button_info['label'], + button_info['style'] + ): + success_count += 1 + else: + print(f"✗ Button control does not exist: {button_info['label']}") + + print(f"=== Style application completed: {success_count}/{len(self.button_info)} successful ===") + + def show_dialog(self, title, message): + """Show styled confirmation dialog""" + dialog_name = "StyledConfirmDialog" + + # If the dialog already exists, delete it + if cmds.window(dialog_name, exists=True): + cmds.deleteUI(dialog_name) + + # Create dialog window + cmds.window(dialog_name, title=title, widthHeight=(300, 120), sizeable=False) + + # Main layout + main_layout = cmds.columnLayout(adjustableColumn=True, rowSpacing=10, columnOffset=('both', 15)) + + # Message text + cmds.text(label=message, align="center", wordWrap=True, height=40) + + cmds.separator(height=5) + + # Confirm button + self.create_styled_button("Yes", lambda *args: self.close_dialog(dialog_name), MESSAGE_BUTTON_STYLE) + + # Show dialog + cmds.showWindow(dialog_name) + + def close_dialog(self, dialog_name): + """Close dialog""" + if cmds.window(dialog_name, exists=True): + cmds.deleteUI(dialog_name) + + def calculate_vertex_colors(self, total_layers): + """Calculate vertex colors for each layer""" + if total_layers <= 1: + return [[1.0, 1.0, 1.0]] + + colors = [] + for i in range(total_layers): + # Calculate interpolation factor (0.0 to 1.0) + factor = i / (total_layers - 1) if total_layers > 1 else 0.0 + + # Interpolate between minimum and maximum colors + r = self.min_color[0] + (self.max_color[0] - self.min_color[0]) * factor + g = self.min_color[1] + (self.max_color[1] - self.min_color[1]) * factor + b = self.min_color[2] + (self.max_color[2] - self.min_color[2]) * factor + + colors.append([r, g, b]) + + return colors + + def on_layers_changed(self, *args): + """Callback function when the number of layers changes - Real-time update""" + if self.is_updating: + return + + self.layers = cmds.intSliderGrp(self.layers_slider, query=True, value=True) + self.preview_colors() + + # Update logic + if self.original_objects: # If there are base models + if self.merge_layers: + print("The models have been merged, please manually clean up and re-extrude!") + else: + print(f"Number of layers updated to {self.layers}, regenerating shell layers...") + # Clean up existing layers + self.clear_generated_objects() + # Regenerate layers + self.generate_layers() + + def on_thickness_changed(self, *args): + """Callback function when thickness changes - Real-time update""" + if self.is_updating: + return + + self.thickness = cmds.floatSliderGrp(self.thickness_slider, query=True, value=True) + self.preview_colors() + + # Update logic + if self.original_objects and self.generated_objects: # If there are base models and generated layers + if self.merge_layers: + print("The models have been merged, please manually clean up and re-extrude!") + else: + print(f"Thickness updated to {self.thickness}, updating thickness for all layers...") + self.update_thickness() + + def on_merge_changed(self, *args): + """Callback function when merge option changes""" + if self.is_updating: + return + self.merge_layers = cmds.checkBox(self.merge_checkbox, query=True, value=True) + self.preview_colors() + + def on_color_changed(self, *args): + """Callback function when colors change - Real-time update""" + if self.is_updating: + return + + self.min_color = cmds.colorSliderGrp(self.min_color_field, query=True, rgb=True) + self.max_color = cmds.colorSliderGrp(self.max_color_field, query=True, rgb=True) + self.preview_colors() + + # Update logic + if self.original_objects and self.generated_objects: # If there are base models and generated layers + if self.merge_layers: + print("The models have been merged, please manually clean up and re-extrude!") + else: + print("Colors updated, updating vertex colors for all layers...") + self.update_colors_for_all_layers() + + def preview_colors(self, *args): + """Preview color distribution""" + layers = self.layers + total_layers = layers + 1 # Include original layer + colors = self.calculate_vertex_colors(total_layers) + + preview_text = f"Colors preview ({total_layers} layers):\n\n" + + for i, color in enumerate(colors): + r, g, b = color + if i == 0: + layer_name = "Original layer" + else: + layer_name = f"Layer {i}" + + preview_text += f"{layer_name}: RGB({r:.2f}, {g:.2f}, {b:.2f})\n" + + cmds.scrollField(self.preview_text, edit=True, text=preview_text) + + def select_base_objects(self, *args): + """Select base objects and start extrusion""" + selected = cmds.ls(selection=True, type='transform') + if not selected: + cmds.warning("Please select the models to process first") + return + + # Clean up existing layers + self.clear_generated_objects() + + # Save original objects + self.original_objects = selected[:] + + # Generate layers immediately + self.generate_layers() + + self.show_dialog("Completed", f"Processed {len(selected)} objects, generated {self.layers} shell layers") + + def clear_all_layers(self, *args): + """Clear all generated layers""" + # Check if merge option is checked + if self.merge_layers: + print("The models have been merged, please manually clean up and re-extrude!") + return + self.clear_generated_objects() + self.show_dialog("Completed", "All generated layers have been cleared") + + def clear_generated_objects(self): + """Clear generated objects""" + for obj in self.generated_objects: + if cmds.objExists(obj): + cmds.delete(obj) + self.generated_objects = [] + + def generate_layers(self): + """Generate shell layers""" + if not self.original_objects: + print("Error: No original objects selected") + return + + try: + print(f"=== Starting generation of {self.layers} shell layers ===") + + created_objects = [] + + for obj_index, original_obj in enumerate(self.original_objects): + print(f"Processing object {obj_index + 1}: {original_obj}") + + # Collect all layer objects (including original object) + all_layers = [original_obj] + + # Create layers one by one (always from the original object) + for layer_num in range(1, self.layers + 1): + print(f" Creating layer {layer_num}...") + + # Key: Always copy from the original object, not from the previous layer + new_layer = self.extrude_layer_correct(original_obj, layer_num, self.thickness) + if new_layer: + all_layers.append(new_layer) + created_objects.append(new_layer) + print(f" ✓ Layer {layer_num} completed") + else: + print(f" ✗ Layer {layer_num} failed, stopping") + break + + # Set vertex colors + print(f" Setting vertex colors for {len(all_layers)} layers...") + self.apply_colors(all_layers) + + # Save results + self.generated_objects = created_objects + print(f"=== Completed! Created {len(created_objects)} layers ===") + + # Check if merge option is checked + if self.merge_layers and len(created_objects) > 0: + print("=== Starting simple merge ===") + self.simple_merge_objects() + else: + print("=== Skipping merge (not checked or no objects) ===") + + except Exception as e: + print(f"Generation failed: {str(e)}") + # Cleanup + for obj in created_objects: + if cmds.objExists(obj): + try: + cmds.delete(obj) + except: + pass + + def simple_merge_objects(self): + """Simple merge method to avoid complex operations""" + try: + all_objects = self.original_objects + self.generated_objects + print(f"Simple merge {len(all_objects)} objects") + + if len(all_objects) > 1: + # Only do basic merge + existing_objects = [obj for obj in all_objects if cmds.objExists(obj)] + + if len(existing_objects) > 1: + cmds.select(existing_objects, replace=True) + merged = cmds.polyUnite(existing_objects, name="BatchExtruded_Simple")[0] + print(f"Simple merge completed: {merged}") + + # Update object list + self.original_objects = [merged] + self.generated_objects = [] + else: + print("Not enough objects to merge") + else: + print("Not enough objects to merge") + + except Exception as e: + print(f"Simple merge failed: {str(e)}") + + def extrude_layer_correct(self, obj, layer_index, thickness): + """Follow the correct Maya workflow: duplicate -> extrude -> delete by inverse selection""" + duplicated = None + try: + print(f" === Correct workflow for layer {layer_index} ===") + + # 1. Verify object + if not cmds.objExists(obj): + print(f" Error: Object {obj} does not exist") + return None + + # 2. Copy model + duplicated = cmds.duplicate(obj, name=f"Layer_{layer_index}")[0] + print(f" Copy completed: {duplicated}") + + # 3. Switch to face mode and select all faces + print(f" Switching to face mode and selecting all faces...") + + # Switch to component mode + cmds.selectMode(component=True) + cmds.selectType(facet=True) + + # Select all faces directly (more reliable method) + try: + cmds.select(f"{duplicated}.f[*]", replace=True) + selected_faces = cmds.ls(selection=True, flatten=True) + print(f" Selected faces count: {len(selected_faces)}") + + if len(selected_faces) == 0: + # Backup method 1: Select object first then convert + print(f" Direct selection failed, trying conversion method...") + cmds.selectMode(object=True) + cmds.select(duplicated, replace=True) + cmds.selectMode(component=True) + cmds.selectType(facet=True) + + # Use mel command to convert + try: + mel.eval("ConvertSelectionToFaces;") + selected_faces = cmds.ls(selection=True, flatten=True) + print(f" Selected faces count: {len(selected_faces)}") + except Exception as mel_error: + print(f" MEL conversion failed: {str(mel_error)}") + + # Backup method 2: Use polyEvaluate to get face count, then select + try: + face_count = cmds.polyEvaluate(duplicated, face=True) + if face_count > 0: + face_range = f"{duplicated}.f[0:{face_count-1}]" + cmds.select(face_range, replace=True) + selected_faces = cmds.ls(selection=True, flatten=True) + print(f" Selected faces count: {len(selected_faces)}") + except Exception as backup_error: + print(f" Backup method also failed: {str(backup_error)}") + + except Exception as select_error: + print(f" Face selection failed: {str(select_error)}") + raise select_error + + # Verify face selection success + if len(selected_faces) == 0: + print(f" Error: No faces selected") + return None + + print(f" Selected {len(selected_faces)} faces, starting extrusion...") + + # Execute extrusion (thickness=0) + extrude_result = cmds.polyExtrudeFacet( + constructionHistory=True, + keepFacesTogether=True, + divisions=1, + twist=0, + taper=1, + off=0, + thickness=0, + smoothingAngle=30 + ) + print(f" Extrusion command completed: {extrude_result}") + + # Set cumulative thickness (each layer's thickness is cumulative) + if extrude_result: + extrude_node = extrude_result[0] + # Cumulative thickness = base thickness * layer number + cumulative_thickness = thickness * layer_index + cmds.setAttr(f"{extrude_node}.thickness", cumulative_thickness) + print(f" Set cumulative thickness: {cumulative_thickness} (Layer {layer_index})") + + # 4. Invert selection and delete internal faces + print(f" Inverting selection and deleting internal faces...") + + # After extrusion, the original faces are still selected + # We need to keep these original faces and delete the newly generated faces + original_selected_faces = cmds.ls(selection=True, flatten=True) + print(f" Original selected faces count: {len(original_selected_faces)}") + + # Get all faces after extrusion + all_faces_after_extrude = cmds.ls(f"{duplicated}.f[*]", flatten=True) + print(f" Total faces after extrusion: {len(all_faces_after_extrude)}") + + # Calculate newly generated faces (faces to delete) + if len(all_faces_after_extrude) > len(original_selected_faces): + # New faces = All faces - Original faces + new_faces = [face for face in all_faces_after_extrude if face not in original_selected_faces] + + if new_faces: + print(f" Deleting {len(new_faces)} newly generated faces") + cmds.select(new_faces, replace=True) + cmds.delete() + print(f" Deleted successfully, keeping original faces as shell") + else: + print(f" No newly generated faces found") + else: + print(f" No faces added, skipping deletion") + + # 5. Return to object mode + cmds.selectMode(object=True) + cmds.select(clear=True) + + print(f" === Layer {layer_index} completed ===") + return duplicated + + except Exception as e: + print(f" Layer {layer_index} failed: {str(e)}") + if duplicated and cmds.objExists(duplicated): + try: + cmds.delete(duplicated) + except: + pass + return None + + def apply_colors(self, layer_objects): + """Simple vertex color application""" + try: + total_layers = len(layer_objects) + colors = self.calculate_vertex_colors(total_layers) + + print(f" Setting colors for {total_layers} layers...") + + for i, obj in enumerate(layer_objects): + if cmds.objExists(obj) and i < len(colors): + color = colors[i] + print(f" Layer {i}: {obj} -> RGB{color}") + + # Set vertex colors + try: + # Select all vertices + cmds.select(f"{obj}.vtx[*]", replace=True) + + # Apply color + cmds.polyColorPerVertex(rgb=color, colorDisplayOption=True) + + # Enable vertex color display + cmds.setAttr(f"{obj}.displayColors", 1) + + except Exception as color_error: + print(f" Setting vertex colors failed: {str(color_error)}") + + # Clear selection + cmds.select(clear=True) + print(f" Vertex colors set successfully") + except Exception as e: + print(f" Vertex color application failed: {str(e)}") + + def update_thickness(self): + """Update thickness for all layers""" + try: + print(f"=== Updating thickness to {self.thickness} ===") + + for layer_index, obj in enumerate(self.generated_objects, 1): + if cmds.objExists(obj): + print(f" Updating layer {layer_index}: {obj}") + + # Find extrude node + history = cmds.listHistory(obj, pruneDagObjects=True) + extrude_nodes = [node for node in history if cmds.nodeType(node) == 'polyExtrudeFace'] + + if extrude_nodes: + extrude_node = extrude_nodes[0] # Take the latest extrude node + # Set cumulative thickness + cumulative_thickness = self.thickness * layer_index + cmds.setAttr(f"{extrude_node}.thickness", cumulative_thickness) + print(f" ✓ Updated thickness: {cumulative_thickness}") + else: + print(f" ✗ No extrude node found") + + print(f"=== Thickness updated successfully ===") + + except Exception as e: + print(f"Thickness update failed: {str(e)}") + + def update_colors_for_all_layers(self): + """Update colors for all layers""" + try: + print(f"=== Updating colors for all layers ===") + + # Collect all layer objects (including original objects) + all_layers = self.original_objects + self.generated_objects + + # Reapply colors + self.apply_colors(all_layers) + + print(f"=== Colors updated successfully ===") + + except Exception as e: + print(f"Colors update failed: {str(e)}") + +# Create and display UI function +def show_batch_extrusion_ui(): + """Show batch extrusion UI""" + batch_extrusion = BatchExtrusion() + batch_extrusion.create_ui() + +# If run this script directly +if __name__ == "__main__": + show_batch_extrusion_ui() diff --git a/2023/scripts/nexus_test.py b/2023/scripts/nexus_test.py new file mode 100644 index 0000000..8a8eb10 --- /dev/null +++ b/2023/scripts/nexus_test.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Nexus Test Script +Simple test to verify the Nexus plugin system is working +""" + +import maya.cmds as cmds + + +def run_test(): + """Run a simple test""" + print("[Nexus Test] Running test...") + + # Show confirmation dialog + result = cmds.confirmDialog( + title='Nexus Test', + message='Nexus Plugin System is working correctly!', + button=['OK'], + defaultButton='OK', + cancelButton='OK', + dismissString='OK' + ) + + print(f"[Nexus Test] Test completed: {result}") + return result + + +if __name__ == "__main__": + run_test() diff --git a/2023/scripts/rigging_tools/__init__.py b/2023/scripts/rigging_tools/__init__.py new file mode 100644 index 0000000..a987049 --- /dev/null +++ b/2023/scripts/rigging_tools/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Nexus Rigging Tools Package +Character rigging utilities +""" + +__all__ = [] diff --git a/2023/scripts/userSetup.py b/2023/scripts/userSetup.py new file mode 100644 index 0000000..5d41610 --- /dev/null +++ b/2023/scripts/userSetup.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Nexus Maya 2023 - User Setup Script +Automatically executed when Maya starts +""" + +import maya.cmds as cmds +import maya.mel as mel +import maya.utils +import os +import sys + +SHELF_NAMES = ["Nexus_Modeling", "Nexus_Rigging", "Nexus_Animation"] + +def load_nexus_shelves(): + """Load all Nexus shelves (force refresh)""" + try: + shelf_paths = os.environ.get('MAYA_SHELF_PATH', '') + + if not shelf_paths: + print("[Nexus] MAYA_SHELF_PATH not set, trying alternative method...") + try: + script_dir = os.path.dirname(os.path.abspath(__file__)) + shelf_paths = os.path.join(os.path.dirname(script_dir), "shelves") + except: + print("[Nexus] Could not determine shelf path, skipping shelf load") + return + + path_separator = ';' if os.name == 'nt' else ':' + shelf_path_list = shelf_paths.split(path_separator) + + for shelf_name in SHELF_NAMES: + shelf_file_found = None + for shelf_path in shelf_path_list: + shelf_path = shelf_path.strip() + if not shelf_path: + continue + + shelf_file = os.path.join(shelf_path, f"shelf_{shelf_name}.mel") + shelf_file = shelf_file.replace("\\", "/") + + if os.path.exists(shelf_file): + shelf_file_found = shelf_file + print(f"[Nexus] Found shelf file: {shelf_file}") + break + + if not shelf_file_found: + print(f"[Nexus] Could not find shelf_{shelf_name}.mel") + continue + + # Delete old shelf if exists + if cmds.shelfLayout(shelf_name, exists=True): + print(f"[Nexus] Deleting old shelf: {shelf_name}") + try: + cmds.deleteUI(shelf_name, layout=True) + except Exception as e: + print(f"[Nexus] Warning: Could not delete old shelf: {e}") + + # Load shelf using proper MEL method + print(f"[Nexus] Loading shelf: {shelf_name}") + try: + # Disable auto-save + mel.eval('optionVar -intValue "saveLastLoadedShelf" 0;') + + # Create shelf layout + mel.eval(f''' + global string $gShelfTopLevel; + if (`shelfLayout -exists {shelf_name}`) {{ + deleteUI -layout {shelf_name}; + }} + setParent $gShelfTopLevel; + shelfLayout -cellWidth 35 -cellHeight 34 {shelf_name}; + ''') + print(f"[Nexus] ✓ Created shelf layout: {shelf_name}") + + # Set parent and execute shelf script + mel.eval(f'setParent {shelf_name};') + mel.eval(f'source "{shelf_file_found}";') + mel.eval(f'shelf_{shelf_name}();') + print(f"[Nexus] ✓ Executed shelf script: shelf_{shelf_name}()") + + # Verify shelf + if cmds.shelfLayout(shelf_name, exists=True): + buttons = cmds.shelfLayout(shelf_name, query=True, childArray=True) or [] + if buttons: + print(f"[Nexus] ✓ Shelf loaded with {len(buttons)} button(s): {shelf_name}") + else: + print(f"[Nexus] ⚠ Shelf created but no buttons: {shelf_name}") + + # Remove auto-saved config + try: + maya_version = cmds.about(version=True).split()[0] + maya_app_dir = os.environ.get('MAYA_APP_DIR', '') + if maya_app_dir: + shelf_config = os.path.join(maya_app_dir, maya_version, "prefs", "shelves", f"shelf_{shelf_name}.mel") + if os.path.exists(shelf_config): + os.remove(shelf_config) + print(f"[Nexus] ✓ Removed auto-saved config: {shelf_name}") + except Exception as e: + print(f"[Nexus] Warning: {e}") + else: + print(f"[Nexus] ✗ Shelf layout not created: {shelf_name}") + + except Exception as e: + print(f"[Nexus] Error loading shelf {shelf_name}: {e}") + import traceback + traceback.print_exc() + continue + + except Exception as e: + print(f"[Nexus] Error in load_nexus_shelves: {e}") + import traceback + traceback.print_exc() + + +def load_nexus_plugins(): + """Load Nexus plugins""" + try: + plugin_paths = os.environ.get('MAYA_PLUG_IN_PATH', '') + + if not plugin_paths: + print("[Nexus] MAYA_PLUG_IN_PATH not set") + return + + path_separator = ';' if os.name == 'nt' else ':' + plugin_path_list = plugin_paths.split(path_separator) + + plugins_to_load = ["nexus_plugin.py"] + + for plugin_name in plugins_to_load: + plugin_found = False + for plugin_path in plugin_path_list: + plugin_path = plugin_path.strip() + if not plugin_path: + continue + + plugin_file = os.path.join(plugin_path, plugin_name) + if os.path.exists(plugin_file): + plugin_found = True + break + + if not plugin_found: + print(f"[Nexus] Plugin not found: {plugin_name}") + continue + + # Load plugin + try: + if not cmds.pluginInfo(plugin_name, query=True, loaded=True): + cmds.loadPlugin(plugin_name) + print(f"[Nexus] ✓ Plugin loaded: {plugin_name}") + else: + print(f"[Nexus] Plugin already loaded: {plugin_name}") + except Exception as e: + print(f"[Nexus] Error loading plugin {plugin_name}: {e}") + + except Exception as e: + print(f"[Nexus] Error in load_nexus_plugins: {e}") + import traceback + traceback.print_exc() + + +# Deferred execution +def initialize_nexus(): + """Initialize Nexus plugin system""" + print("=" * 80) + print("[Nexus] Initializing Nexus Maya 2023 Plugin System...") + print("=" * 80) + + load_nexus_shelves() + load_nexus_plugins() + + print("=" * 80) + print("[Nexus] Nexus Plugin System Initialized!") + print("=" * 80) + + +# Execute after Maya is fully loaded +maya.utils.executeDeferred(initialize_nexus) + + +# Cleanup on exit +def cleanup_on_exit(): + """Cleanup when Maya exits""" + print("[Nexus] Cleaning up...") + + # Unload plugins + plugins_to_unload = ["nexus_plugin.py"] + for plugin_name in plugins_to_unload: + if cmds.pluginInfo(plugin_name, query=True, loaded=True): + try: + cmds.unloadPlugin(plugin_name) + print(f"[Nexus] Plugin unloaded: {plugin_name}") + except: + pass + + +import atexit +atexit.register(cleanup_on_exit) diff --git a/2023/shelves/shelf_Nexus_Animation.mel b/2023/shelves/shelf_Nexus_Animation.mel new file mode 100644 index 0000000..a7ff9a1 --- /dev/null +++ b/2023/shelves/shelf_Nexus_Animation.mel @@ -0,0 +1,43 @@ +global proc shelf_Nexus_Animation () { + global string $gBuffStr; + global string $gBuffStr0; + global string $gBuffStr1; + + + shelfButton + -enableCommandRepeat 1 + -flexibleWidthType 3 + -flexibleWidthValue 32 + -enable 1 + -width 35 + -height 34 + -manage 1 + -visible 1 + -preventOverride 0 + -annotation "Nexus Test - Verify plugin system" + -enableBackground 0 + -backgroundColor 0 0 0 + -highlightColor 0.321569 0.521569 0.65098 + -align "center" + -label "Test" + -labelOffset 0 + -rotation 0 + -flipX 0 + -flipY 0 + -useAlpha 1 + -font "plainLabelFont" + -imageOverlayLabel "Test" + -overlayLabelColor 0.8 0.8 0.8 + -overlayLabelBackColor 0 0 0 0.5 + -image "commandButton.png" + -image1 "commandButton.png" + -style "iconOnly" + -marginWidth 0 + -marginHeight 1 + -command "import nexus_test\nnexus_test.run_test()" + -sourceType "python" + -commandRepeatable 1 + -flat 1 + ; + +} diff --git a/2023/shelves/shelf_Nexus_Modeling.mel b/2023/shelves/shelf_Nexus_Modeling.mel new file mode 100644 index 0000000..590ff7f --- /dev/null +++ b/2023/shelves/shelf_Nexus_Modeling.mel @@ -0,0 +1,43 @@ +global proc shelf_Nexus_Modeling () { + global string $gBuffStr; + global string $gBuffStr0; + global string $gBuffStr1; + + + shelfButton + -enableCommandRepeat 1 + -flexibleWidthType 3 + -flexibleWidthValue 32 + -enable 1 + -width 35 + -height 34 + -manage 1 + -visible 1 + -preventOverride 0 + -annotation "Batch Extrusion - Shell mesh tool with vertex colors" + -enableBackground 0 + -backgroundColor 0 0 0 + -highlightColor 0.321569 0.521569 0.65098 + -align "center" + -label "BatchExt" + -labelOffset 0 + -rotation 0 + -flipX 0 + -flipY 0 + -useAlpha 1 + -font "plainLabelFont" + -imageOverlayLabel "Extrude" + -overlayLabelColor 0.8 0.8 0.8 + -overlayLabelBackColor 0 0 0 0.5 + -image "batchextrusion.png" + -image1 "batchextrusion.png" + -style "iconOnly" + -marginWidth 0 + -marginHeight 1 + -command "import modeling_tools\nmodeling_tools.show_batch_extrusion_ui()" + -sourceType "python" + -commandRepeatable 1 + -flat 1 + ; + +} diff --git a/2023/shelves/shelf_Nexus_Rigging.mel b/2023/shelves/shelf_Nexus_Rigging.mel new file mode 100644 index 0000000..778a59f --- /dev/null +++ b/2023/shelves/shelf_Nexus_Rigging.mel @@ -0,0 +1,43 @@ +global proc shelf_Nexus_Rigging () { + global string $gBuffStr; + global string $gBuffStr0; + global string $gBuffStr1; + + + shelfButton + -enableCommandRepeat 1 + -flexibleWidthType 3 + -flexibleWidthValue 32 + -enable 1 + -width 35 + -height 34 + -manage 1 + -visible 1 + -preventOverride 0 + -annotation "Nexus Test - Verify plugin system" + -enableBackground 0 + -backgroundColor 0 0 0 + -highlightColor 0.321569 0.521569 0.65098 + -align "center" + -label "Test" + -labelOffset 0 + -rotation 0 + -flipX 0 + -flipY 0 + -useAlpha 1 + -font "plainLabelFont" + -imageOverlayLabel "Test" + -overlayLabelColor 0.8 0.8 0.8 + -overlayLabelBackColor 0 0 0 0.5 + -image "commandButton.png" + -image1 "commandButton.png" + -style "iconOnly" + -marginWidth 0 + -marginHeight 1 + -command "import nexus_test\nnexus_test.run_test()" + -sourceType "python" + -commandRepeatable 1 + -flat 1 + ; + +} diff --git a/CleanCache.bat b/CleanCache.bat new file mode 100644 index 0000000..c73e079 --- /dev/null +++ b/CleanCache.bat @@ -0,0 +1,37 @@ +@echo off +echo ======================================== +echo NexusLauncher Cache Cleaner +echo ======================================== +echo. + +@REM echo Close NexusLauncher if it is running... +@REM taskkill /f /im pythonw.exe + +echo [1/4] Cleaning all __pycache__ folders... +for /d /r %%d in (__pycache__) do @if exist "%%d" ( + echo Deleting: %%d + rd /s /q "%%d" +) +echo. + +echo [2/4] Cleaning root cache... +if exist "__pycache__" rd /s /q "__pycache__" +echo. + +echo [3/4] Cleaning module caches... +if exist "config\__pycache__" rd /s /q "config\__pycache__" +if exist "ui\__pycache__" rd /s /q "ui\__pycache__" +if exist "ui\task\__pycache__" rd /s /q "ui\task\__pycache__" +echo. + +echo [4/4] Cleaning .pyc files... +del /s /q *.pyc 2>nul +echo. + +@REM echo Clear old config file +@REM if exist config.json del /f config.json + +echo ======================================== +echo Cache cleaned successfully! +echo ======================================== +pause \ No newline at end of file