762 lines
30 KiB
Python
762 lines
30 KiB
Python
#!/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()
|