Skip to content

Instantly share code, notes, and snippets.

@Pakmanv
Created May 2, 2025 18:24
Show Gist options
  • Select an option

  • Save Pakmanv/2f8a67f0a64519431a650edd0143da3a to your computer and use it in GitHub Desktop.

Select an option

Save Pakmanv/2f8a67f0a64519431a650edd0143da3a to your computer and use it in GitHub Desktop.
Vtx Snappy VV 1.4
# -*- coding: utf-8 -*-
from maya import cmds
import maya.api.OpenMaya as om
import math
print("Imported maya.cmds, maya.api.OpenMaya")
try:
from PySide2 import QtWidgets, QtCore, QtGui
print("Imported PySide2")
except ImportError:
cmds.error("PySide2 not found. Ensure you're running this in Maya 2017 or later with PySide2 available.")
raise
class VVvertsnap(QtWidgets.QDialog):
def __init__(self, parent=None):
maya_main_window = None
for obj in QtWidgets.QApplication.topLevelWidgets():
print(f"Found top-level widget: {obj.objectName()}")
if obj.objectName() == "MayaWindow":
maya_main_window = obj
break
if not maya_main_window:
print("Maya main window not found, using no parent.")
super(VVvertsnap, self).__init__(parent=maya_main_window or None)
self.setWindowTitle("Vtx Snappy VV 1.4")
self.setMinimumSize(300, 680)
self.setStyleSheet("QDialog:focus { border: none; }")
self.rebuild_after_creation = False
self.live_projected_cv = False
self.offset_mode = "noOffset"
self.offset_value = 0.0
self.tolerance_value = 10.0
self.cleanup_tween_value = 0.5
self.use_tolerance_tweening = False
self.symmetry_tolerance_value = 0.001
self.use_stored_target = False
self.use_conform_list = False
self.stored_target = None
self.stored_mesh_list = []
self.last_vertices = []
self.last_targets = []
self.created_curve = None
self.selection_callback = None
self.list_height = 200
print("Initializing Vtx Snappy VV UI...")
self.create_ui()
self.setup_selection_callback()
# Connect buttons and controls
self.snap_btn.clicked.connect(self.snap_vertices)
self.snap_btn.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.snap_btn.customContextMenuRequested.connect(self.show_snap_context_menu)
self.match_mesh_btn.clicked.connect(self.match_mesh)
self.match_mesh_btn.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.match_mesh_btn.customContextMenuRequested.connect(self.show_match_mesh_context_menu)
self.cv_project_btn.clicked.connect(self.cv_project)
self.cv_project_btn.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.cv_project_btn.customContextMenuRequested.connect(self.show_cv_context_menu)
self.rebuild_curve_btn.clicked.connect(self.rebuild_curve)
self.edges_to_curve_btn.clicked.connect(self.edges_to_curve)
self.cleanup_edge_flow_btn.clicked.connect(self.cleanup_edge_flow)
self.check_symmetry_btn.clicked.connect(self.check_symmetry)
self.compare_verts_btn.clicked.connect(self.compare_verts)
self.compare_verts_btn.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.compare_verts_btn.customContextMenuRequested.connect(self.show_compare_verts_context_menu)
self.store_mesh_btn.clicked.connect(self.store_mesh_in_list)
self.revert_mesh_btn.clicked.connect(self.revert_to_stored_mesh)
self.revert_mesh_btn.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.revert_mesh_btn.customContextMenuRequested.connect(self.show_revert_context_menu)
self.offset_dropdown.currentTextChanged.connect(self.update_offset_mode)
self.offset_slider.valueChanged.connect(self.update_offset_value)
self.offset_field.valueChanged.connect(self.update_offset_field)
self.tolerance_slider.valueChanged.connect(self.update_tolerance_value)
self.tolerance_field.valueChanged.connect(self.update_tolerance_field)
self.cleanup_tween_cb.stateChanged.connect(self.update_tolerance_tweening)
self.cleanup_tween_slider.valueChanged.connect(self.update_cleanup_tween_value)
self.cleanup_tween_field.valueChanged.connect(self.update_cleanup_tween_field)
self.symmetry_tolerance_field.valueChanged.connect(self.update_symmetry_tolerance_field)
self.use_stored_target_cb.stateChanged.connect(self.update_use_stored_target)
self.stored_mesh_list_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.stored_mesh_list_widget.customContextMenuRequested.connect(self.show_mesh_list_context_menu)
self.sort_dropdown.currentTextChanged.connect(self.sort_conform_list)
print("Button and control connections established.")
def create_ui(self):
layout = QtWidgets.QVBoxLayout()
layout.setSpacing(10)
# Snap Tolerance and Snap Vertices (same row)
snap_layout = QtWidgets.QHBoxLayout()
tolerance_layout = QtWidgets.QVBoxLayout()
tolerance_label = QtWidgets.QLabel("Snap Tolerance (world units):")
tolerance_controls_layout = QtWidgets.QHBoxLayout()
self.tolerance_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.tolerance_slider.setRange(0, 10000)
self.tolerance_slider.setValue(100)
self.tolerance_slider.setToolTip("Adjust snap tolerance (0 to 1000 units) for snapping vertices or CVs.")
self.tolerance_field = QtWidgets.QDoubleSpinBox()
self.tolerance_field.setRange(0.0, 1000.0)
self.tolerance_field.setValue(10.0)
self.tolerance_field.setSingleStep(0.1)
self.tolerance_field.setToolTip("Enter precise snap tolerance (0 to 1000 units) for snapping vertices or CVs.")
tolerance_controls_layout.addWidget(self.tolerance_slider)
tolerance_controls_layout.addWidget(self.tolerance_field)
tolerance_layout.addWidget(tolerance_label)
tolerance_layout.addLayout(tolerance_controls_layout)
self.snap_btn = QtWidgets.QPushButton("Snap Vertices")
self.snap_btn.setStyleSheet("background-color: #FFC7A1; color: black; padding: 5px;")
self.snap_btn.setToolTip("Snap source mesh vertices to closest target mesh vertices within tolerance. Select target mesh first, then source mesh.")
snap_layout.addLayout(tolerance_layout)
snap_layout.addWidget(self.snap_btn)
layout.addLayout(snap_layout)
# Separator
snap_match_spacer = QtWidgets.QFrame()
snap_match_spacer.setFrameShape(QtWidgets.QFrame.HLine)
snap_match_spacer.setFrameShadow(QtWidgets.QFrame.Sunken)
layout.addWidget(snap_match_spacer)
# Match Mesh note
match_mesh_label = QtWidgets.QLabel("Match Mesh: Copies mesh without history, ideal for corrective shapes.")
match_mesh_label.setStyleSheet("color: gray;")
layout.addWidget(match_mesh_label)
# Match Mesh
copy_layout = QtWidgets.QHBoxLayout()
self.match_mesh_btn = QtWidgets.QPushButton("Match Mesh")
self.match_mesh_btn.setStyleSheet("background-color: #A1C7FF; color: black; padding: 5px;")
self.match_mesh_btn.setToolTip("Match all vertex positions of the source mesh(es) to the target mesh in the selected space. Right-click to store target in memory.")
self.space_dropdown = QtWidgets.QComboBox()
self.space_dropdown.addItems(["World Space", "Local Space"])
self.space_dropdown.setCurrentText("Local Space")
self.space_dropdown.setToolTip("Choose whether to match vertex positions in world or local space.")
self.use_stored_target_cb = QtWidgets.QCheckBox("Use Stored Target")
self.use_stored_target_cb.setToolTip("When checked, matches selected mesh(es) to the stored target mesh if available, requiring only source mesh(es) selected.")
copy_layout.addWidget(self.match_mesh_btn)
copy_layout.addWidget(self.space_dropdown)
copy_layout.addWidget(self.use_stored_target_cb)
layout.addLayout(copy_layout)
# Separator
spacer = QtWidgets.QFrame()
spacer.setFrameShape(QtWidgets.QFrame.HLine)
spacer.setFrameShadow(QtWidgets.QFrame.Sunken)
layout.addWidget(spacer)
# Curve rebuild
rebuild_layout = QtWidgets.QHBoxLayout()
degree_label = QtWidgets.QLabel("Curve Degree:")
self.degree_dropdown = QtWidgets.QComboBox()
self.degree_dropdown.addItems(["1 Linear", "2", "3 Cubic", "5", "7"])
self.degree_dropdown.setCurrentText("3 Cubic")
self.degree_dropdown.setToolTip("Degree for creating and rebuilding NURBS curves.")
spans_label = QtWidgets.QLabel("Spans/CVs:")
self.spans_field = QtWidgets.QSpinBox()
self.spans_field.setRange(1, 100)
self.spans_field.setValue(4)
self.spans_field.setToolTip("Number of spans (or CVs) for rebuilt curves.")
self.rebuild_curve_btn = QtWidgets.QPushButton("Rebuild Curve")
self.rebuild_curve_btn.setToolTip("Select a NURBS curve and rebuild it with specified degree and spans/CVs.")
rebuild_layout.addWidget(degree_label)
rebuild_layout.addWidget(self.degree_dropdown)
rebuild_layout.addWidget(spans_label)
rebuild_layout.addWidget(self.spans_field)
rebuild_layout.addWidget(self.rebuild_curve_btn)
layout.addLayout(rebuild_layout)
# Edges to Curve
self.edges_to_curve_btn = QtWidgets.QPushButton("Edges to Curve")
self.edges_to_curve_btn.setStyleSheet("background-color: #FFFACD; color: black; padding: 5px;")
self.edges_to_curve_btn.setToolTip("Select edges on a polygonal mesh to convert to a NURBS curve.")
layout.addWidget(self.edges_to_curve_btn)
# Separator
spacer_cv = QtWidgets.QFrame()
spacer_cv.setFrameShape(QtWidgets.QFrame.HLine)
spacer_cv.setFrameShadow(QtWidgets.QFrame.Sunken)
layout.addWidget(spacer_cv)
# CV Project
cv_project_label = QtWidgets.QLabel("CV Project: Select vertices or CVs, then a target mesh, NURBS surface, or curve.")
cv_project_label.setStyleSheet("color: gray;")
layout.addWidget(cv_project_label)
self.cv_project_btn = QtWidgets.QPushButton("CV Project")
self.cv_project_btn.setStyleSheet("background-color: #A3E4D7; color: black; padding: 5px;")
self.cv_project_btn.setToolTip("Project selected vertices or CVs to the nearest point on the target. Right-click to toggle live projection.")
layout.addWidget(self.cv_project_btn)
# Offset Options
offset_frame = QtWidgets.QFrame()
offset_layout = QtWidgets.QVBoxLayout()
offset_layout.setSpacing(5)
offset_label = QtWidgets.QLabel("Offset Options:")
self.offset_dropdown = QtWidgets.QComboBox()
self.offset_dropdown.addItems(["No offset", "distance from surface", "travel percentage"])
self.offset_dropdown.setToolTip("Select offset mode for CV Project.")
offset_layout.addWidget(offset_label)
offset_layout.addWidget(self.offset_dropdown)
offset_value_layout = QtWidgets.QHBoxLayout()
offset_value_label = QtWidgets.QLabel("Offset Value:")
self.offset_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.offset_slider.setRange(-1000, 1000)
self.offset_slider.setValue(0)
self.offset_slider.setToolTip("Adjust offset distance or percentage.")
self.offset_slider.setEnabled(False)
self.offset_field = QtWidgets.QDoubleSpinBox()
self.offset_field.setRange(-100.0, 100.0)
self.offset_field.setValue(0.0)
self.offset_field.setSingleStep(0.01)
self.offset_field.setToolTip("Enter precise offset distance or percentage.")
self.offset_field.setEnabled(False)
offset_value_layout.addWidget(offset_value_label)
offset_value_layout.addWidget(self.offset_slider)
offset_value_layout.addWidget(self.offset_field)
offset_layout.addLayout(offset_value_layout)
offset_frame.setLayout(offset_layout)
layout.addWidget(offset_frame)
# Separator
spacer_cleanup = QtWidgets.QFrame()
spacer_cleanup.setFrameShape(QtWidgets.QFrame.HLine)
spacer_cleanup.setFrameShadow(QtWidgets.QFrame.Sunken)
layout.addWidget(spacer_cleanup)
# Cleanup Edge Flow
self.cleanup_edge_flow_btn = QtWidgets.QPushButton("Cleanup Edge Flow")
self.cleanup_edge_flow_btn.setStyleSheet("background-color: #D8B4FE; color: black; padding: 5px;")
self.cleanup_edge_flow_btn.setToolTip("Select edges to clean up edge flow.")
layout.addWidget(self.cleanup_edge_flow_btn)
# Cleanup Tween
cleanup_tween_layout = QtWidgets.QHBoxLayout()
self.cleanup_tween_cb = QtWidgets.QCheckBox("Use Tolerance")
self.cleanup_tween_cb.setToolTip("Enable to use tween value for CV count interpolation.")
self.cleanup_tween_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.cleanup_tween_slider.setRange(0, 50)
self.cleanup_tween_slider.setValue(25)
self.cleanup_tween_slider.setToolTip("Adjust tween value (0 to 0.5).")
self.cleanup_tween_slider.setEnabled(False)
self.cleanup_tween_field = QtWidgets.QDoubleSpinBox()
self.cleanup_tween_field.setRange(0.0, 0.5)
self.cleanup_tween_field.setValue(0.25)
self.cleanup_tween_field.setSingleStep(0.01)
self.cleanup_tween_field.setToolTip("Enter precise tween value (0 to 0.5).")
self.cleanup_tween_field.setEnabled(False)
cleanup_tween_layout.addWidget(self.cleanup_tween_cb)
cleanup_tween_layout.addWidget(self.cleanup_tween_slider)
cleanup_tween_layout.addWidget(self.cleanup_tween_field)
layout.addLayout(cleanup_tween_layout)
# Separator
spacer_new_features = QtWidgets.QFrame()
spacer_new_features.setFrameShape(QtWidgets.QFrame.HLine)
spacer_new_features.setFrameShadow(QtWidgets.QFrame.Sunken)
layout.addWidget(spacer_new_features)
# Symmetry and Compare Tolerance
symmetry_tolerance_layout = QtWidgets.QHBoxLayout()
symmetry_tolerance_layout.setSpacing(4)
symmetry_tolerance_label = QtWidgets.QLabel("Symmetry/Compare Tolerance:")
self.symmetry_tolerance_field = QtWidgets.QDoubleSpinBox()
self.symmetry_tolerance_field.setDecimals(4)
self.symmetry_tolerance_field.setRange(0.0, 10.0)
self.symmetry_tolerance_field.setValue(0.001)
self.symmetry_tolerance_field.setSingleStep(0.00001)
self.symmetry_tolerance_field.setToolTip("Enter tolerance for Check Symmetry and Compare Verts (in local space units).")
symmetry_tolerance_layout.addWidget(symmetry_tolerance_label)
symmetry_tolerance_layout.addWidget(self.symmetry_tolerance_field)
layout.addLayout(symmetry_tolerance_layout)
# Check Symmetry
check_symmetry_layout = QtWidgets.QHBoxLayout()
check_symmetry_layout.setSpacing(4)
self.check_symmetry_btn = QtWidgets.QPushButton("Check Symmetry")
self.check_symmetry_btn.setToolTip("Check symmetry in local space across the chosen axis for selected mesh(es).")
self.symmetry_axis_dropdown = QtWidgets.QComboBox()
self.symmetry_axis_dropdown.addItems(["X", "Y", "Z"])
self.symmetry_axis_dropdown.setToolTip("Select axis for symmetry check.")
check_symmetry_layout.addWidget(self.check_symmetry_btn)
check_symmetry_layout.addWidget(self.symmetry_axis_dropdown)
layout.addLayout(check_symmetry_layout)
# Compare Verts
self.compare_verts_btn = QtWidgets.QPushButton("Compare Verts")
self.compare_verts_btn.setToolTip("Select two or more meshes to compare vertices, or use Conform Mesh List with right-click option.")
layout.addWidget(self.compare_verts_btn)
# Separator
spacer_revert = QtWidgets.QFrame()
spacer_revert.setFrameShape(QtWidgets.QFrame.HLine)
spacer_revert.setFrameShadow(QtWidgets.QFrame.Sunken)
layout.addWidget(spacer_revert)
# Conform Mesh List Section
conform_label_layout = QtWidgets.QHBoxLayout()
conform_label = QtWidgets.QLabel("Conform Mesh List:")
conform_label.setStyleSheet("color: gray;")
self.sort_dropdown = QtWidgets.QComboBox()
self.sort_dropdown.addItems(["Alphabetical A-Z", "Alphabetical Z-A", "Time"])
self.sort_dropdown.setCurrentText("Time")
self.sort_dropdown.setToolTip("Sort the Conform Mesh List by name or order of addition.")
conform_label_layout.addWidget(conform_label)
conform_label_layout.addStretch()
conform_label_layout.addWidget(self.sort_dropdown)
layout.addLayout(conform_label_layout)
# Revert Mesh List with Scroll Area
self.stored_mesh_list_widget = QtWidgets.QListWidget()
self.stored_mesh_list_widget.setSelectionMode(QtWidgets.QListWidget.MultiSelection)
self.stored_mesh_list_widget.setToolTip("Stored mesh references for reversion. Select multiple meshes with Ctrl+click; single-click to select one. Right-click to remove, rename, or add/remove tag.")
self.stored_mesh_list_widget.itemClicked.connect(self.handle_item_clicked)
scroll_area = QtWidgets.QScrollArea()
scroll_area.setWidget(self.stored_mesh_list_widget)
scroll_area.setWidgetResizable(True)
scroll_area.setMinimumHeight(self.list_height)
scroll_area.setMaximumHeight(self.list_height)
layout.addWidget(scroll_area)
# Store and Revert Buttons
revert_buttons_layout = QtWidgets.QHBoxLayout()
revert_buttons_layout.setSpacing(4)
self.store_mesh_btn = QtWidgets.QPushButton("Store Mesh in List")
self.store_mesh_btn.setToolTip("Store a reference to the selected mesh(es) for reversion.")
self.revert_mesh_btn = QtWidgets.QPushButton("Revert")
self.revert_mesh_btn.setToolTip("Revert selected mesh(es) or vertices to the current state of selected reference mesh(es).")
revert_buttons_layout.addWidget(self.store_mesh_btn)
revert_buttons_layout.addWidget(self.revert_mesh_btn)
layout.addLayout(revert_buttons_layout)
# History Label
self.history_label = QtWidgets.QLabel("No actions performed.")
self.history_label.setStyleSheet("font-size: 12px; color: gray;")
self.history_label.setAlignment(QtCore.Qt.AlignLeft)
layout.addWidget(self.history_label)
# Credit
credit_label = QtWidgets.QLabel("Tool made by Vypac Voeur and should not be distributed")
credit_label.setStyleSheet("font-size: 10px; color: gray; margin-top: 10px;")
credit_label.setAlignment(QtCore.Qt.AlignLeft)
layout.addWidget(credit_label)
self.setLayout(layout)
print("Vtx Snappy VV UI created successfully.")
def handle_item_clicked(self, item):
modifiers = QtWidgets.QApplication.keyboardModifiers()
print(f"Item clicked: {item.text()}, Modifiers: Ctrl={bool(modifiers & QtCore.Qt.ControlModifier)}, Shift={bool(modifiers & QtCore.Qt.ShiftModifier)}")
if not (modifiers & QtCore.Qt.ControlModifier or modifiers & QtCore.Qt.ShiftModifier):
self.stored_mesh_list_widget.clearSelection()
item.setSelected(True)
print(f"Selected only: {item.text()}")
def show_mesh_list_context_menu(self, pos):
menu = QtWidgets.QMenu(self)
selected_items = self.stored_mesh_list_widget.selectedItems()
if selected_items:
remove_action = menu.addAction("Remove Mesh")
remove_action.setToolTip("Remove the selected mesh(es) from the list.")
remove_action.triggered.connect(self.remove_mesh_from_list)
rename_action = menu.addAction("Rename Mesh")
rename_action.setToolTip("Change the display name of the selected mesh in the list.")
rename_action.triggered.connect(self.rename_mesh_in_list)
add_tag_action = menu.addAction("Add Custom Tag")
add_tag_action.setToolTip("Add a custom tag prefix to the selected mesh in the list.")
add_tag_action.triggered.connect(self.add_custom_tag_to_mesh)
remove_tag_action = menu.addAction("Remove Tag")
remove_tag_action.setToolTip("Remove the custom tag prefix from the selected mesh in the list.")
remove_tag_action.triggered.connect(self.remove_tag_from_mesh)
menu.addSeparator()
extend_action = menu.addAction("Extend List")
extend_action.setToolTip("Increase the height of the mesh list.")
extend_action.triggered.connect(self.extend_list_height)
shorten_action = menu.addAction("Shorten List")
shorten_action.setToolTip("Decrease the height of the mesh list.")
shorten_action.triggered.connect(self.shorten_list_height)
print("Showing mesh list context menu")
menu.exec_(self.stored_mesh_list_widget.mapToGlobal(pos))
def add_custom_tag_to_mesh(self):
selected_items = self.stored_mesh_list_widget.selectedItems()
if not selected_items:
cmds.warning("No mesh selected to add tag.")
print("Add Custom Tag aborted: No mesh selected.")
return
tag_name, ok = QtWidgets.QInputDialog.getText(self, "Add Custom Tag", "Enter tag name:", text="TAG")
if not ok or not tag_name:
cmds.warning("No tag name provided.")
print("Add Custom Tag aborted: No tag name provided.")
self.history_label.setText("Add Tag failed: No tag name.")
return
try:
for item in selected_items:
index = self.stored_mesh_list_widget.row(item)
if index < len(self.stored_mesh_list):
current_name = item.text()
# Remove existing tag if present
if current_name.startswith("["):
current_name = current_name.split("]", 1)[-1]
new_name = f"[{tag_name}]{current_name}"
self.stored_mesh_list[index]["name"] = new_name
item.setText(new_name)
cmds.warning(f"Added tag [{tag_name}] to {current_name}.")
print(f"Added tag: {new_name}")
self.history_label.setText(f"Added tag [{tag_name}] to {len(selected_items)} mesh(es).")
self.stored_mesh_list_widget.repaint()
except Exception as e:
cmds.warning(f"Error adding custom tag: {str(e)}")
print(f"Error in add_custom_tag_to_mesh: {e}")
self.history_label.setText("Add Tag failed: Error occurred.")
def remove_tag_from_mesh(self):
selected_items = self.stored_mesh_list_widget.selectedItems()
if not selected_items:
cmds.warning("No mesh selected to remove tag.")
print("Remove Tag aborted: No mesh selected.")
return
try:
for item in selected_items:
index = self.stored_mesh_list_widget.row(item)
if index < len(self.stored_mesh_list):
current_name = item.text()
if current_name.startswith("["):
new_name = current_name.split("]", 1)[-1]
self.stored_mesh_list[index]["name"] = new_name
item.setText(new_name)
cmds.warning(f"Removed tag from {current_name}.")
print(f"Removed tag: {new_name}")
else:
print(f"Skipping {current_name}: No tag to remove.")
self.history_label.setText(f"Removed tag from {len(selected_items)} mesh(es).")
self.stored_mesh_list_widget.repaint()
except Exception as e:
cmds.warning(f"Error removing tag: {str(e)}")
print(f"Error in remove_tag_from_mesh: {e}")
self.history_label.setText("Remove Tag failed: Error occurred.")
def sort_conform_list(self, sort_mode):
try:
if sort_mode == "Alphabetical A-Z":
self.stored_mesh_list.sort(key=lambda x: x["name"].lower())
elif sort_mode == "Alphabetical Z-A":
self.stored_mesh_list.sort(key=lambda x: x["name"].lower(), reverse=True)
else: # Time
self.stored_mesh_list.sort(key=lambda x: x.get("time_added", 0))
self.stored_mesh_list_widget.clear()
for mesh_data in self.stored_mesh_list:
item = QtWidgets.QListWidgetItem(mesh_data["name"])
self.stored_mesh_list_widget.addItem(item)
mesh_data["item"] = item
cmds.warning(f"Conform Mesh List sorted by {sort_mode}.")
self.history_label.setText(f"Sorted list by {sort_mode}.")
print(f"Sorted Conform Mesh List by {sort_mode}")
except Exception as e:
cmds.warning(f"Error sorting list: {str(e)}")
print(f"Error in sort_conform_list: {e}")
self.history_label.setText("Sort failed: Error occurred.")
def store_mesh_in_list(self):
sel = cmds.ls(sl=True, o=True)
if not sel:
cmds.warning("No meshes selected to store in list.")
print("Store Mesh in List aborted: No meshes selected.")
self.history_label.setText("Store Mesh failed: No meshes selected.")
return
try:
cmds.undoInfo(openChunk=True, chunkName="storeMeshInList")
stored_count = 0
existing_names = [mesh_data["name"].lstrip("[") for mesh_data in self.stored_mesh_list]
import time
for mesh in sel:
if mesh in existing_names:
cmds.warning(f"Mesh {mesh} already stored in list. Skipping.")
print(f"Skipping {mesh}: Already stored in list.")
continue
shapes = cmds.listRelatives(mesh, shapes=True, fullPath=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning(f"Skipping {mesh}: Not a polygonal mesh.")
print(f"Skipping {mesh}: Not a polygonal mesh.")
continue
verts = cmds.ls(f"{mesh}.vtx[*]", fl=True)
if not verts:
cmds.warning(f"No vertices found in {mesh}.")
print(f"No vertices in {mesh}.")
continue
vertex_indices = [int(v.split("[")[-1].split("]")[0]) for v in verts]
mesh_data = {
"name": mesh,
"vertex_count": len(verts),
"vertex_indices": vertex_indices,
"item": None,
"time_added": time.time()
}
self.stored_mesh_list.append(mesh_data)
item = QtWidgets.QListWidgetItem(mesh)
self.stored_mesh_list_widget.addItem(item)
mesh_data["item"] = item
stored_count += 1
print(f"Stored reference to mesh {mesh} with {len(verts)} vertices in list.")
if stored_count > 0:
cmds.warning(f"Stored {stored_count} mesh reference(s) in list.")
self.history_label.setText(f"Stored {stored_count} mesh(es) in list.")
else:
cmds.warning("No valid meshes stored in list.")
print("Store Mesh in List aborted: No valid meshes stored.")
self.history_label.setText("Store Mesh failed: No valid meshes.")
self.stored_mesh_list_widget.repaint()
except Exception as e:
cmds.warning(f"Error storing meshes in list: {str(e)}")
print(f"Error in store_mesh_in_list: {e}")
self.history_label.setText("Store Mesh failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def remove_mesh_from_list(self):
selected_items = self.stored_mesh_list_widget.selectedItems()
if not selected_items:
cmds.warning("No mesh selected to remove.")
print("Remove Mesh aborted: No mesh selected.")
return
try:
for item in selected_items:
index = self.stored_mesh_list_widget.row(item)
if index < len(self.stored_mesh_list):
mesh_name = self.stored_mesh_list[index]["name"]
self.stored_mesh_list.pop(index)
self.stored_mesh_list_widget.takeItem(index)
cmds.warning(f"Removed mesh {mesh_name} from list.")
print(f"Removed mesh {mesh_name} from list.")
else:
cmds.warning("Invalid mesh index in list.")
print("Remove Mesh aborted: Invalid index.")
self.history_label.setText(f"Removed {len(selected_items)} mesh(es) from list.")
self.stored_mesh_list_widget.repaint()
except Exception as e:
cmds.warning(f"Error removing mesh: {str(e)}")
print(f"Error in remove_mesh_from_list: {e}")
self.history_label.setText("Remove Mesh failed: Error occurred.")
def rename_mesh_in_list(self):
selected_items = self.stored_mesh_list_widget.selectedItems()
if not selected_items:
cmds.warning("No mesh selected to rename.")
print("Rename Mesh aborted: No mesh selected.")
return
item = selected_items[0]
current_name = item.text()
tag = ""
if current_name.startswith("["):
tag = current_name[:current_name.find("]") + 1]
current_name = current_name.lstrip(tag)
new_name, ok = QtWidgets.QInputDialog.getText(self, "Rename Mesh", "Enter new display name:", text=current_name)
if ok and new_name:
try:
index = self.stored_mesh_list_widget.row(item)
if index < len(self.stored_mesh_list):
new_display_name = f"{tag}{new_name}" if tag else new_name
self.stored_mesh_list[index]["name"] = new_display_name
item.setText(new_display_name)
cmds.warning(f"Renamed mesh to {new_display_name} in list.")
print(f"Renamed mesh to {new_display_name} in list.")
self.history_label.setText(f"Renamed mesh to {new_display_name}.")
else:
cmds.warning("Invalid mesh index in list.")
print("Rename Mesh aborted: Invalid index.")
self.stored_mesh_list_widget.repaint()
except Exception as e:
cmds.warning(f"Error renaming mesh: {str(e)}")
print(f"Error in rename_mesh_in_list: {e}")
self.history_label.setText("Rename Mesh failed: Error occurred.")
def extend_list_height(self):
current_height = self.stored_mesh_list_widget.parent().height()
new_height = min(current_height + 50, 500)
self.stored_mesh_list_widget.parent().setMinimumHeight(new_height)
self.stored_mesh_list_widget.parent().setMaximumHeight(new_height)
cmds.warning(f"List height set to {new_height} pixels.")
print(f"List height extended to {new_height} pixels.")
self.history_label.setText(f"Extended list height to {new_height}px.")
def shorten_list_height(self):
current_height = self.stored_mesh_list_widget.parent().height()
new_height = max(current_height - 50, 100)
self.stored_mesh_list_widget.parent().setMinimumHeight(new_height)
self.stored_mesh_list_widget.parent().setMaximumHeight(new_height)
cmds.warning(f"List height set to {new_height} pixels.")
print(f"List height shortened to {new_height} pixels.")
self.history_label.setText(f"Shortened list height to {new_height}px.")
def show_revert_context_menu(self, pos):
menu = QtWidgets.QMenu(self)
# Placeholder for future revert options
menu.exec_(self.revert_mesh_btn.mapToGlobal(pos))
def show_compare_verts_context_menu(self, pos):
menu = QtWidgets.QMenu(self)
use_conform_action = menu.addAction("Use Conform List")
use_conform_action.setCheckable(True)
use_conform_action.setChecked(self.use_conform_list)
use_conform_action.setToolTip("Toggle to use the first selected mesh in the Conform Mesh List as the reference for Compare Verts.")
use_conform_action.triggered.connect(self.toggle_use_conform_list)
menu.exec_(self.compare_verts_btn.mapToGlobal(pos))
def toggle_use_conform_list(self):
self.use_conform_list = not self.use_conform_list
cmds.warning(f"Use Conform List for Compare Verts: {'Enabled' if self.use_conform_list else 'Disabled'}")
self.history_label.setText(f"Use Conform List: {'Enabled' if self.use_conform_list else 'Disabled'}.")
print(f"Use Conform List: {'Enabled' if self.use_conform_list else 'Disabled'}")
def revert_to_stored_mesh(self):
sel = cmds.ls(sl=True, fl=True)
if not sel:
cmds.warning("Select at least one mesh or vertices to revert.")
print("Revert aborted: No selection.")
self.history_label.setText("Revert failed: No selection.")
return
selected_items = self.stored_mesh_list_widget.selectedItems()
if not selected_items:
cmds.warning("Select at least one stored mesh from the list to revert to.")
print("Revert aborted: No stored mesh selected.")
self.history_label.setText("Revert failed: No stored mesh selected.")
return
try:
cmds.undoInfo(openChunk=True, chunkName="revertToStoredMesh")
selected_meshes = [s for s in cmds.ls(sl=True, o=True) if cmds.listRelatives(s, shapes=True, fullPath=True) and cmds.objectType(cmds.listRelatives(s, shapes=True, fullPath=True)[0]) == "mesh"]
selected_vertices = [s for s in sel if ".vtx[" in s]
space = om.MSpace.kObject if self.space_dropdown.currentText() == "Local Space" else om.MSpace.kWorld
total_reverted_items = []
# Estimate operation time based on vertex count
total_vertices = sum(len(cmds.ls(f"{m}.vtx[*]", fl=True) or []) for m in selected_meshes) + len(selected_vertices)
use_progress = total_vertices > 10000 # Rough threshold for >1s operation
progress = None
if use_progress:
progress = QtWidgets.QProgressDialog("Reverting to stored mesh...", "Cancel", 0, 100, self)
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setMinimumDuration(0)
progress.setValue(0)
QtWidgets.QApplication.processEvents()
item_count = len(selected_items)
for idx, item in enumerate(selected_items):
if use_progress:
progress.setValue(int((idx / item_count) * 100))
if progress.wasCanceled():
cmds.warning("Revert operation canceled by user.")
self.history_label.setText("Revert canceled.")
return
QtWidgets.QApplication.processEvents()
target_name = item.text()
if target_name.startswith("["):
target_name = target_name.split("]", 1)[-1]
target_data = next((m for m in self.stored_mesh_list if m["name"].endswith(target_name)), None)
if not target_data:
cmds.warning(f"Skipping {target_name}: Not found in stored list")
print(f"Skipping {target_name}: Not found in stored list")
continue
target_vertex_count = target_data["vertex_count"]
target_indices = target_data["vertex_indices"]
if not cmds.objExists(target_name):
cmds.warning(f"Skipping {target_name}: Mesh does not exist")
print(f"Skipping {target_name}: Mesh does not exist")
continue
target_verts = cmds.ls(f"{target_name}.vtx[*]", fl=True)
if len(target_verts) != target_vertex_count:
cmds.warning(f"Skipping {target_name}: Vertex count mismatch: Current ({len(target_verts)}) vs Stored ({target_vertex_count})")
print(f"Skipping {target_name}: Vertex count mismatch")
continue
target_positions = [cmds.pointPosition(v, local=True) for v in target_verts]
print(f"Queried current state of {target_name} with {target_vertex_count} vertices")
reverted_items = []
if selected_vertices:
for vert in selected_vertices:
mesh = vert.split(".vtx[")[0]
vert_index_str = vert.split("[")[-1].split("]")[0]
try:
vert_index = int(vert_index_str)
except ValueError:
cmds.warning(f"Skipping {vert}: Invalid vertex index")
print(f"Skipping {vert}: Invalid vertex index")
continue
shapes = cmds.listRelatives(mesh, shapes=True, fullPath=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning(f"Skipping {vert}: Not part of a polygonal mesh")
print(f"Skipping {vert}: Not part of a polygonal mesh")
continue
if mesh != target_name:
print(f"Mesh {mesh} does not match referenced mesh {target_name}. Proceeding with vertex count check")
if vert_index in target_indices:
target_idx = target_indices.index(vert_index)
target_pos = target_positions[target_idx]
cmds.xform(vert, t=target_pos, ws=(space == om.MSpace.kWorld))
reverted_items.append(vert)
total_reverted_items.append(vert)
print(f"Reverted vertex {vert} to position {target_pos} from {target_name}")
else:
cmds.warning(f"Skipping {vert}: Vertex index {vert_index} not in referenced mesh {target_name}")
print(f"Skipping {vert}: Vertex index not in referenced mesh")
else:
for mesh in selected_meshes:
shapes = cmds.listRelatives(mesh, shapes=True, fullPath=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning(f"Skipping {mesh}: Not a polygonal mesh")
print(f"Skipping {mesh}: Not a polygonal mesh")
continue
if mesh != target_name:
print(f"Mesh {mesh} does not match referenced mesh {target_name}. Proceeding with vertex count check")
mesh_list = om.MSelectionList()
mesh_list.add(mesh)
mesh_dag = mesh_list.getDagPath(0)
mesh_mfn = om.MFnMesh(mesh_dag)
mesh_points = mesh_mfn.getPoints(space)
mesh_vertex_count = len(mesh_points)
if mesh_vertex_count != target_vertex_count:
cmds.warning(f"Skipping {mesh}: Vertex count mismatch: Mesh ({mesh_vertex_count}) vs Referenced ({target_vertex_count})")
print(f"Skipping {mesh}: Vertex count mismatch")
continue
target_points = [om.MPoint(p[0], p[1], p[2]) for p in target_positions]
mesh_mfn.setPoints(target_points, space)
reverted_items.append(mesh)
total_reverted_items.append(mesh)
print(f"Reverted mesh {mesh} to current state of {target_name}")
if reverted_items:
cmds.warning(f"Reverted {len(reverted_items)} item(s) to current state of {target_name}")
print(f"Reverted {len(reverted_items)} item(s) to current state of {target_name}")
if use_progress:
progress.setValue(100)
progress.deleteLater()
if not total_reverted_items:
cmds.warning("No meshes or vertices reverted due to validation errors.")
print("Revert aborted: No valid items reverted")
self.history_label.setText("Revert failed: No valid items.")
return
cmds.select(total_reverted_items, r=True)
cmds.warning(f"Reverted {len(total_reverted_items)} item(s) to selected reference mesh(es)")
self.history_label.setText(f"Reverted {len(total_reverted_items)} item(s).")
print(f"Revert complete: Reverted {len(total_reverted_items)} items")
except Exception as e:
cmds.warning(f"Error reverting to referenced mesh: {str(e)}")
print(f"Error in revert_to_stored_mesh: {e}")
self.history_label.setText("Revert failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def show_snap_context_menu(self, pos):
menu = QtWidgets.QMenu(self)
match_action = menu.addAction("Match Position")
match_action.setToolTip("Align source mesh's transform to target mesh.")
match_action.triggered.connect(self.match_position)
move_origin_action = menu.addAction("Move to Origin")
move_origin_action.setToolTip("Move selected objects to world origin.")
move_origin_action.triggered.connect(self.move_to_origin)
menu.exec_(self.snap_btn.mapToGlobal(pos))
def show_match_mesh_context_menu(self, pos):
menu = QtWidgets.QMenu(self)
store_target_action = menu.addAction("Register Target")
store_target_action.setToolTip("Store first selected mesh as target for Match Mesh.")
store_target_action.triggered.connect(self.store_target_mesh)
menu.exec_(self.match_mesh_btn.mapToGlobal(pos))
def store_target_mesh(self):
sel = cmds.ls(sl=True, o=True)
if not sel:
cmds.warning("No mesh selected to store as target.")
print("Store Target aborted: No mesh selected.")
self.history_label.setText("Store Target failed: No mesh selected.")
return
mesh = sel[0]
shapes = cmds.listRelatives(mesh, shapes=True, fullPath=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning("Selected object must be a polygonal mesh.")
print("Store Target aborted: Not a polygonal mesh.")
self.history_label.setText("Store Target failed: Not a mesh.")
return
self.stored_target = mesh
self.use_stored_target_cb.setChecked(True)
self.use_stored_target = True
cmds.warning(f"Stored target mesh: {mesh}")
self.history_label.setText(f"Stored target mesh: {mesh}")
print(f"Stored target mesh: {mesh}")
def match_position(self):
sel = cmds.ls(sl=True, o=True)
if len(sel) != 2:
cmds.warning("Select exactly two objects: target first, then source.")
print("Match Position aborted: Need exactly two selected objects.")
self.history_label.setText("Match Position failed: Need two objects.")
return
target, source = sel
shapes = cmds.listRelatives(target, shapes=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning("Target must be a polygonal mesh.")
print("Match Position aborted: Target is not a polygonal mesh.")
self.history_label.setText("Match Position failed: Target not a mesh.")
return
shapes = cmds.listRelatives(source, shapes=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning("Source must be a polygonal mesh.")
print("Match Position aborted: Source is not a polygonal mesh.")
self.history_label.setText("Match Position failed: Source not a mesh.")
return
try:
cmds.undoInfo(openChunk=True, chunkName="matchPosition")
target_trans = cmds.xform(target, q=True, t=True, ws=True)
target_rot = cmds.xform(target, q=True, ro=True, ws=True)
target_scale = cmds.xform(target, q=True, s=True, ws=True)
cmds.xform(source, t=target_trans, ws=True)
cmds.xform(source, ro=target_rot, ws=True)
cmds.xform(source, s=target_scale, ws=True)
print(f"Matched source {source} to target {target}")
cmds.warning("Match Position complete.")
self.history_label.setText(f"Matched {source} to {target}.")
except Exception as e:
cmds.warning(f"Error during Match Position: {str(e)}")
print(f"Error in match_position: {e}")
self.history_label.setText("Match Position failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def move_to_origin(self):
sel = cmds.ls(sl=True, o=True)
if not sel:
cmds.warning("No objects selected to move to origin.")
print("Move to Origin aborted: No objects selected.")
self.history_label.setText("Move to Origin failed: No objects.")
return
try:
cmds.undoInfo(openChunk=True, chunkName="moveToOrigin")
for obj in sel:
cmds.xform(obj, t=[0, 0, 0], ws=True)
print(f"Moved {obj} to world origin")
cmds.warning(f"Moved {len(sel)} object(s) to origin.")
self.history_label.setText(f"Moved {len(sel)} object(s) to origin.")
except Exception as e:
cmds.warning(f"Error moving to origin: {str(e)}")
print(f"Error in move_to_origin: {e}")
self.history_label.setText("Move to Origin failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def get_closest_vertex(self, source_pos, target_obj, tolerance):
try:
sel_list = om.MSelectionList()
sel_list.add(target_obj)
dag_path = sel_list.getDagPath(0)
mesh_fn = om.MFnMesh(dag_path)
point = om.MPoint(source_pos[0], source_pos[1], source_pos[2])
vertex_iterator = om.MItMeshVertex(dag_path)
min_dist = float('inf')
closest_vertex_pos = None
while not vertex_iterator.isDone():
vertex_pos = vertex_iterator.position(om.MSpace.kWorld)
dist = (point - vertex_pos).length()
if dist < min_dist:
min_dist = dist
closest_vertex_pos = [vertex_pos.x, vertex_pos.y, vertex_pos.z]
vertex_iterator.next()
if min_dist <= tolerance:
print(f"Found closest vertex at {closest_vertex_pos}, distance: {min_dist}")
return closest_vertex_pos
else:
print(f"No target vertex within tolerance {tolerance} (distance: {min_dist})")
return None
except Exception as e:
cmds.warning(f"Error finding closest vertex: {str(e)}")
print(f"Error in get_closest_vertex: {e}")
return None
def snap_vertices(self):
sel = cmds.ls(sl=True, o=True)
if len(sel) != 2:
cmds.warning("Select exactly two objects: target first, then source.")
print("Snap Vertices aborted: Need exactly two selected objects.")
self.history_label.setText("Snap Vertices failed: Need two objects.")
return
target, source = sel
shapes = cmds.listRelatives(target, shapes=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning("Target must be a polygonal mesh.")
print("Snap Vertices aborted: Target is not a polygonal mesh.")
self.history_label.setText("Snap Vertices failed: Target not a mesh.")
return
shapes = cmds.listRelatives(source, shapes=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning("Source must be a polygonal mesh.")
print("Snap Vertices aborted: Source is not a polygonal mesh.")
self.history_label.setText("Snap Vertices failed: Source not a mesh.")
return
try:
cmds.undoInfo(openChunk=True, chunkName="snapVertices")
tolerance = self.tolerance_value
print(f"Using snap tolerance: {tolerance} world units")
source_verts = cmds.ls(f"{source}.vtx[*]", fl=True)
print(f"Processing {len(source_verts)} vertices...")
snapped_count = 0
for vert in source_verts:
source_pos = cmds.pointPosition(vert, world=True)
target_pos = self.get_closest_vertex(source_pos, target, tolerance)
if target_pos:
cmds.xform(vert, t=target_pos, ws=True)
snapped_count += 1
print(f"Snapped vertex {vert} to {target_pos}")
else:
print(f"Skipping vertex {vert}: No target vertex within tolerance")
cmds.select(source)
cmds.warning(f"Vertex snapping complete! Snapped {snapped_count}/{len(source_verts)} vertices.")
self.history_label.setText(f"Snapped {snapped_count}/{len(source_verts)} vertices.")
print(f"Snapping complete: {snapped_count}/{len(source_verts)} vertices snapped.")
except Exception as e:
cmds.warning(f"Error snapping vertices: {str(e)}")
print(f"Error in snap_vertices: {e}")
self.history_label.setText("Snap Vertices failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def setup_selection_callback(self):
try:
self.selection_callback = om.MEventMessage.addEventCallback("SelectionChanged", self.on_selection_changed)
print("Selection change callback registered.")
except Exception as e:
print(f"Failed to register selection callback: {e}")
def on_selection_changed(self, *args):
if self.live_projected_cv:
selection = cmds.ls(sl=True, fl=True)
vertices = [s for s in selection if ".vtx[" in s or ".cv[" in s]
targets = [s for s in selection if ".vtx[" not in s and ".cv[" not in s]
if not vertices or not targets:
self.last_vertices = []
self.last_targets = []
print("Live Projected (CV Project) skipped: Invalid selection.")
else:
self.last_vertices = vertices
self.last_targets = targets
self.cv_project()
print("Live Projected (CV Project) updated.")
def closeEvent(self, event):
if self.selection_callback:
try:
om.MMessage.removeCallback(self.selection_callback)
print("Selection callback removed.")
except:
pass
super(VVvertsnap, self).closeEvent(event)
def show_cv_context_menu(self, pos):
menu = QtWidgets.QMenu(self)
live_action = menu.addAction("Toggle Live Projection")
live_action.setCheckable(True)
live_action.setChecked(self.live_projected_cv)
live_action.triggered.connect(self.toggle_live_projected_cv)
menu.exec_(self.cv_project_btn.mapToGlobal(pos))
def toggle_live_projected_cv(self):
self.live_projected_cv = not self.live_projected_cv
if self.live_projected_cv:
self.cv_project_btn.setStyleSheet("background-color: #70C4B4; color: black; padding: 5px;")
selection = cmds.ls(sl=True, fl=True)
vertices = [s for s in selection if ".vtx[" in s or ".cv[" in s]
targets = [s for s in selection if ".vtx[" not in s and ".cv[" not in s]
if vertices and targets:
self.last_vertices = vertices
self.last_targets = targets
self.cv_project()
print("Live Projected (CV Project) enabled and triggered.")
else:
print("Live Projected (CV Project) enabled but no valid selection.")
else:
self.cv_project_btn.setStyleSheet("background-color: #A3E4D7; color: black; padding: 5px;")
print("Live Projected (CV Project) disabled.")
self.history_label.setText(f"Live Projection {'enabled' if self.live_projected_cv else 'disabled'}.")
def update_offset_mode(self, mode_text):
mode_map = {
"No offset": "noOffset",
"distance from surface": "offsetMode0",
"travel percentage": "offsetMode1"
}
self.offset_mode = mode_map.get(mode_text, "noOffset")
if self.offset_mode == "offsetMode1":
self.offset_slider.setRange(0, 100)
self.offset_field.setRange(0.0, 100.0)
self.offset_slider.setValue(100)
self.offset_field.setValue(100.0)
else:
self.offset_slider.setRange(-1000, 1000)
self.offset_field.setRange(-100.0, 100.0)
self.offset_slider.setValue(0)
self.offset_field.setValue(0.0)
self.offset_slider.setEnabled(self.offset_mode != "noOffset")
self.offset_field.setEnabled(self.offset_mode != "noOffset")
print(f"Offset mode set to: {self.offset_mode}")
self.history_label.setText(f"Offset mode set to: {mode_text}.")
if self.live_projected_cv and self.last_vertices and self.last_targets:
self.cv_project()
def update_offset_value(self, value):
if self.offset_mode == "offsetMode1":
self.offset_value = value / 100.0
self.offset_field.setValue(value)
else:
self.offset_value = value / 10.0
self.offset_field.setValue(self.offset_value)
print(f"Offset value set to: {self.offset_value}")
self.history_label.setText(f"Offset value set to: {self.offset_value}.")
if self.live_projected_cv and self.last_vertices and self.last_targets:
self.cv_project()
def update_offset_field(self, value):
if self.offset_mode == "offsetMode1":
self.offset_value = value / 100.0
self.offset_slider.setValue(int(value))
else:
self.offset_value = value
self.offset_slider.setValue(int(value * 10))
print(f"Offset value set to: {self.offset_value}")
self.history_label.setText(f"Offset value set to: {self.offset_value}.")
if self.live_projected_cv and self.last_vertices and self.last_targets:
self.cv_project()
def update_tolerance_value(self, value):
self.tolerance_value = value / 10.0
self.tolerance_field.setValue(self.tolerance_value)
print(f"Tolerance value set to: {self.tolerance_value}")
self.history_label.setText(f"Tolerance set to: {self.tolerance_value}.")
if self.live_projected_cv and self.last_vertices and self.last_targets:
self.cv_project()
def update_tolerance_field(self, value):
self.tolerance_value = value
self.tolerance_slider.setValue(int(value * 10))
print(f"Tolerance value set to: {self.tolerance_value}")
self.history_label.setText(f"Tolerance set to: {self.tolerance_value}.")
if self.live_projected_cv and self.last_vertices and self.last_targets:
self.cv_project()
def update_tolerance_tweening(self, state):
self.use_tolerance_tweening = state == QtCore.Qt.Checked
self.cleanup_tween_slider.setEnabled(self.use_tolerance_tweening)
self.cleanup_tween_field.setEnabled(self.use_tolerance_tweening)
print(f"Use Tolerance: {'Enabled' if self.use_tolerance_tweening else 'Disabled'}")
self.history_label.setText(f"Use Tolerance: {'Enabled' if self.use_tolerance_tweening else 'Disabled'}.")
def update_cleanup_tween_value(self, value):
self.cleanup_tween_value = value / 100.0
self.cleanup_tween_field.setValue(self.cleanup_tween_value)
print(f"Cleanup Tween value set to: {self.cleanup_tween_value}")
self.history_label.setText(f"Cleanup Tween set to: {self.cleanup_tween_value}.")
def update_cleanup_tween_field(self, value):
self.cleanup_tween_value = value
self.cleanup_tween_slider.setValue(int(value * 100))
print(f"Cleanup Tween value set to: {self.cleanup_tween_value}")
self.history_label.setText(f"Cleanup Tween set to: {self.cleanup_tween_value}.")
def update_symmetry_tolerance_field(self, value):
self.symmetry_tolerance_value = value
print(f"Symmetry/Compare Tolerance set to: {self.symmetry_tolerance_value}")
self.history_label.setText(f"Symmetry Tolerance set to: {self.symmetry_tolerance_value}.")
def update_use_stored_target(self, state):
self.use_stored_target = state == QtCore.Qt.Checked
print(f"Use Stored Target: {'Enabled' if self.use_stored_target else 'Disabled'}")
self.history_label.setText(f"Use Stored Target: {'Enabled' if self.use_stored_target else 'Disabled'}.")
def rebuild_curve(self):
sel = cmds.ls(selection=True, fl=True)
if not sel:
cmds.warning("Select a NURBS curve to rebuild.")
self.history_label.setText("Rebuild Curve failed: No curve selected.")
return
shapes = cmds.listRelatives(sel[0], shapes=True, fullPath=True)
if not shapes or cmds.objectType(shapes[0]) != "nurbsCurve":
cmds.warning("Selected object is not a NURBS curve.")
self.history_label.setText("Rebuild Curve failed: Not a curve.")
return
try:
cmds.undoInfo(openChunk=True, chunkName="rebuildCurve")
curve = sel[0]
degree = int(self.degree_dropdown.currentText().split()[0])
spans = self.spans_field.value()
if spans < degree:
spans = degree
self.spans_field.setValue(spans)
cmds.rebuildCurve(curve, ch=False, rpo=True, rt=0, end=1, kr=0, kcp=0, kep=1, kt=0, s=spans, d=degree)
cmds.warning(f"Curve {curve} rebuilt with degree {degree}, {spans} spans/CVs.")
self.history_label.setText(f"Rebuilt curve: {curve}.")
print(f"Rebuilt curve: {curve}")
except Exception as e:
cmds.warning(f"Error rebuilding curve: {str(e)}")
self.history_label.setText("Rebuild Curve failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def edges_to_curve(self):
sel = cmds.ls(selection=True, fl=True)
if not sel or not any(".e[" in s for s in sel):
cmds.warning("Select edges on a polygonal mesh.")
self.history_label.setText("Edges to Curve failed: No edges selected.")
return
mesh = sel[0].split(".e[")[0]
shapes = cmds.listRelatives(mesh, shapes=True, fullPath=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning(f"Selected edges do not belong to a polygonal mesh.")
self.history_label.setText("Edges to Curve failed: Not a mesh.")
return
try:
cmds.undoInfo(openChunk=True, chunkName="edgesToCurve")
degree = int(self.degree_dropdown.currentText().split()[0])
curve = cmds.polyToCurve(form=2, degree=degree, name="edgeCurve_#")[0]
curve_shapes = cmds.listRelatives(curve, shapes=True, fullPath=True)
if cmds.getAttr(f"{curve_shapes[0]}.form") != 0:
cmds.openCloseCurve(curve, ch=False, ps=0, rpo=True)
cmds.select(curve)
cmds.warning(f"Edges converted to curve: {curve}.")
self.history_label.setText(f"Created curve: {curve}.")
print(f"Created curve: {curve}")
except Exception as e:
cmds.warning(f"Error converting edges to curve: {str(e)}")
self.history_label.setText("Edges to Curve failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def cleanup_edge_flow(self):
sel = cmds.ls(selection=True, fl=True)
if not sel:
cmds.warning("Please select edges on a polygonal mesh.")
print("Cleanup Edge Flow aborted: No edges selected.")
self.history_label.setText("Cleanup Edge Flow failed: No edges selected.")
return
edges = [s for s in sel if ".e[" in s]
if not edges:
cmds.warning("No edges selected. Please select edge components.")
print("Cleanup Edge Flow aborted: No edge components selected.")
self.history_label.setText("Cleanup Edge Flow failed: No edges selected.")
return
mesh = edges[0].split(".e[")[0]
shapes = cmds.listRelatives(mesh, shapes=True, fullPath=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning(f"Selected edges do not belong to a polygonal mesh.")
print(f"Cleanup Edge Flow aborted: {mesh} is not a polygonal mesh.")
self.history_label.setText("Cleanup Edge Flow failed: Not a mesh.")
return
try:
cmds.undoInfo(openChunk=True, chunkName="cleanupEdgeFlow")
cmds.select(edges, r=True)
cmds.ConvertSelectionToVertices()
vertices = cmds.ls(sl=True, fl=True)
if not vertices or not all(".vtx[" in v for v in vertices):
cmds.warning("Failed to convert edges to vertices.")
print("Cleanup Edge Flow aborted: No vertices from edge conversion.")
self.history_label.setText("Cleanup Edge Flow failed: No vertices.")
return
print(f"Converted {len(edges)} edges to {len(vertices)} vertices")
initial_positions = {v: cmds.pointPosition(v, world=True) for v in vertices}
print(f"Initial vertex positions captured")
cmds.select(edges, r=True)
curve = cmds.polyToCurve(form=2, degree=3, name="edgeFlowCurve_#")[0]
self.created_curve = curve
print(f"Created curve from edges: {curve}")
curve_shapes = cmds.listRelatives(curve, shapes=True, fullPath=True)
if not curve_shapes:
raise RuntimeError(f"Curve {curve} has no shape node.")
curve_form = cmds.getAttr(f"{curve_shapes[0]}.form")
if curve_form != 0:
print(f"Curve {curve} is closed, forcing open.")
cmds.openCloseCurve(curve, ch=False, ps=0, rpo=True)
print(f"Forced curve {curve} to open.")
degree = 3
num_edges = len(edges)
if self.use_tolerance_tweening:
max_cvs = max(degree + 1, num_edges)
min_cvs = max(degree + 1, math.ceil(num_edges / 30.0))
t = self.cleanup_tween_value
initial_cvs = int(max_cvs - t * (max_cvs - min_cvs))
final_cvs = initial_cvs * 3
print(f"Use Tolerance enabled: initial_cvs={initial_cvs}, final_cvs={final_cvs}")
else:
initial_cvs = 5
final_cvs = 15
print(f"Use Tolerance disabled: initial_cvs={initial_cvs}, final_cvs={final_cvs}")
initial_spans = initial_cvs - degree
if initial_spans < 1:
initial_spans = 1
initial_cvs = initial_spans + degree
print(f"Adjusted initial_cvs to {initial_cvs}")
cmds.rebuildCurve(curve, ch=False, rpo=True, rt=0, end=1, kr=0, kcp=0, kep=1, kt=0, s=initial_spans, d=degree)
print(f"Initial rebuild: {curve} with {initial_cvs} CVs")
final_spans = final_cvs - degree
if final_spans < 1:
final_spans = 1
final_cvs = final_spans + degree
print(f"Adjusted final_cvs to {final_cvs}")
cmds.rebuildCurve(curve, ch=False, rpo=True, rt=0, end=1, kr=0, kcp=0, kep=1, kt=0, s=final_spans, d=degree)
print(f"Final rebuild: {curve} with {final_cvs} CVs")
if self.use_tolerance_tweening:
cmds.delete(curve, ch=True)
print(f"Deleted history on curve: {curve}")
cmds.select(clear=True)
for vertex in vertices:
cmds.select(vertex, add=True)
cmds.select(curve, add=True)
print(f"Selected for CV Project: {vertices} and curve: {curve}")
self.cv_project()
snapped_positions = {v: cmds.pointPosition(v, world=True) for v in vertices}
snapped_count = 0
for v in vertices:
initial = initial_positions[v]
snapped = snapped_positions[v]
distance = self.calculate_distance(initial, snapped)
if distance > 1e-6:
snapped_count += 1
print(f"Vertex {v} snapped: distance={distance}")
else:
print(f"Vertex {v} did not move")
if snapped_count == 0:
cmds.warning("No vertices snapped. Check tolerance or curve alignment.")
print("CV Project failed to snap vertices.")
self.history_label.setText("Cleanup Edge Flow failed: No vertices snapped.")
return
cmds.delete(curve)
print(f"Deleted curve: {curve}")
self.created_curve = None
cmds.select(vertices, r=True)
cmds.warning(f"Cleanup Edge Flow complete! Snapped {snapped_count}/{len(vertices)} vertices.")
self.history_label.setText(f"Snapped {snapped_count}/{len(vertices)} vertices in edge flow.")
print(f"Cleanup Edge Flow complete: {snapped_count} vertices snapped.")
except Exception as e:
cmds.warning(f"Error in Cleanup Edge Flow: {str(e)}")
print(f"Error in Cleanup Edge Flow: {e}")
self.history_label.setText("Cleanup Edge Flow failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def calculate_distance(self, p1, p2):
return ((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2 + (p2[2] - p1[2])**2)**0.5
def normalize_vector(self, vec):
length = (vec[0]**2 + vec[1]**2 + vec[2]**2)**0.5
return [vec[0]/length, vec[1]/length, vec[2]/length] if length > 0 else [0, 0, 0]
def get_closest_point_xyz(self, input_xyz, target_obj):
try:
null_group = cmds.group(em=True, n="tempNull_#")
cmds.xform(null_group, ws=True, t=input_xyz)
cmds.select(target_obj, null_group, r=True)
geo_const = cmds.geometryConstraint(weight=1)[0]
location = cmds.xform(geo_const, q=True, t=True, ws=True)
cmds.delete(null_group)
return location
except Exception as e:
print(f"Error in get_closest_point_xyz: {e}")
return None
def get_closest_surface_point(self, source_pos, target_obj, tolerance, is_curve=False):
temp_curve = None
temp_curve_copy = None
loft_surface = None
try:
shapes = cmds.listRelatives(target_obj, shapes=True, fullPath=True)
if not shapes:
return None
target_shape = shapes[0]
target_type = cmds.objectType(target_shape)
if is_curve or target_type == "nurbsCurve":
degree = int(self.degree_dropdown.currentText().split()[0])
spans = self.spans_field.value()
if spans < degree:
spans = degree
temp_curve = cmds.duplicate(target_obj, n="tempCurve_#")[0]
cmds.rebuildCurve(temp_curve, ch=False, rpo=True, rt=0, end=1, kr=0, kcp=0, kep=1, kt=0, s=spans, d=degree)
temp_curve_copy = cmds.duplicate(temp_curve, n="tempCurveCopy_#")[0]
cmds.xform(temp_curve_copy, t=[0, 0.01, 0], ws=True)
loft_surface = cmds.loft(temp_curve, temp_curve_copy, n="tempLoftSurface_#", ch=False, u=True, ss=1)[0]
loft_shapes = cmds.listRelatives(loft_surface, shapes=True, fullPath=True)
target_shape = loft_shapes[0]
print(f"Created loft surface: {loft_surface}")
closest_pos = self.get_closest_point_xyz(source_pos, target_obj)
if not closest_pos:
return None
closest_point_node = cmds.createNode("closestPointOnSurface", n="tempClosestPoint_#")
cmds.connectAttr(f"{target_shape}.worldSpace[0]", f"{closest_point_node}.inputSurface")
cmds.setAttr(f"{closest_point_node}.inPosition", *source_pos)
u_value = cmds.getAttr(f"{closest_point_node}.parameterU")
v_value = cmds.getAttr(f"{closest_point_node}.parameterV")
cmds.delete(closest_point_node)
u_range = cmds.getAttr(f"{target_shape}.minMaxRangeU")[0]
v_range = cmds.getAttr(f"{target_shape}.minMaxRangeV")[0]
if not (u_range[0] <= u_value <= u_range[1] and v_range[0] <= v_value <= v_range[1]):
return None
distance = self.calculate_distance(source_pos, closest_pos)
if distance > tolerance:
return None
return closest_pos
except Exception as e:
print(f"Error in get_closest_surface_point: {e}")
return None
finally:
if any([temp_curve, temp_curve_copy, loft_surface]):
try:
cmds.delete([x for x in [temp_curve, temp_curve_copy, loft_surface] if x])
except:
pass
def get_closest_mesh_point(self, source_pos, target_obj, tolerance):
try:
sel_list = om.MSelectionList()
sel_list.add(target_obj)
dag_path = sel_list.getDagPath(0)
mesh_fn = om.MFnMesh(dag_path)
point = om.MPoint(source_pos[0], source_pos[1], source_pos[2])
closest_point, _ = mesh_fn.getClosestPoint(point, om.MSpace.kWorld)
closest_pos = [closest_point.x, closest_point.y, closest_point.z]
distance = (point - closest_point).length()
if distance > tolerance:
return None
return closest_pos
except Exception as e:
print(f"Error in get_closest_mesh_point: {e}")
return None
def match_mesh(self):
sel = cmds.ls(sl=True, o=True)
if self.use_stored_target and self.stored_target:
if not sel:
cmds.warning("Select at least one source mesh.")
print("Match Mesh aborted: No source meshes selected.")
self.history_label.setText("Match Mesh failed: No source meshes.")
return
target = self.stored_target
sources = sel
else:
if len(sel) < 2:
cmds.warning("Select at least two meshes: target first, then source(s).")
print("Match Mesh aborted: Need at least two selected meshes.")
self.history_label.setText("Match Mesh failed: Need two meshes.")
return
target = sel[0]
sources = sel[1:]
shapes = cmds.listRelatives(target, shapes=True, fullPath=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning("Target must be a polygonal mesh.")
print("Match Mesh aborted: Target is not a polygonal mesh.")
self.history_label.setText("Match Mesh failed: Target not a mesh.")
return
try:
cmds.undoInfo(openChunk=True, chunkName="matchMesh")
space = om.MSpace.kWorld if self.space_dropdown.currentText() == "World Space" else om.MSpace.kObject
sel_list = om.MSelectionList()
sel_list.add(target)
target_dag = sel_list.getDagPath(0)
target_mfn = om.MFnMesh(target_dag)
target_points = target_mfn.getPoints(space)
target_vertex_count = len(target_points)
print(f"Target mesh: {target} with {target_vertex_count} vertices")
matched_meshes = []
for source in sources:
shapes = cmds.listRelatives(source, shapes=True, fullPath=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning(f"Skipping {source}: Not a polygonal mesh.")
print(f"Skipping {source}: Not a polygonal mesh.")
continue
sel_list.clear()
sel_list.add(source)
source_dag = sel_list.getDagPath(0)
source_mfn = om.MFnMesh(source_dag)
source_points = source_mfn.getPoints(space)
source_vertex_count = len(source_points)
if source_vertex_count != target_vertex_count:
cmds.warning(f"Vertex count mismatch for {source}: Source ({source_vertex_count}) vs Target ({target_vertex_count}).")
print(f"Skipping {source}: Vertex count mismatch.")
continue
source_mfn.setPoints(target_points, space)
matched_meshes.append(source)
print(f"Matched source {source} to target {target}")
if not matched_meshes:
cmds.warning("No meshes matched due to validation errors.")
print("Match Mesh aborted: No valid meshes matched.")
self.history_label.setText("Match Mesh failed: No valid meshes.")
return
cmds.select(matched_meshes, r=True)
cmds.warning(f"Matched {len(matched_meshes)} mesh(es) to target {target}.")
self.history_label.setText(f"Matched {len(matched_meshes)} mesh(es) to {target}.")
print(f"Match Mesh complete: {len(matched_meshes)} meshes matched.")
except Exception as e:
cmds.warning(f"Error matching meshes: {str(e)}")
print(f"Error in match_mesh: {e}")
self.history_label.setText("Match Mesh failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def cv_project(self):
selection = cmds.ls(sl=True, fl=True)
vertices = [s for s in selection if ".vtx[" in s or ".cv[" in s]
targets = [s for s in selection if ".vtx[" not in s and ".cv[" not in s]
if not vertices or not targets:
cmds.warning("Select vertices or CVs and a target (mesh, surface, or curve).")
print("CV Project aborted: Invalid selection.")
self.history_label.setText("CV Project failed: Invalid selection.")
return
if len(targets) > 1:
cmds.warning("Multiple targets selected. Using the first one.")
print("CV Project: Multiple targets detected, using first target.")
targets = [targets[0]]
target = targets[0]
shapes = cmds.listRelatives(target, shapes=True, fullPath=True)
if not shapes:
cmds.warning("Target has no valid shape.")
print("CV Project aborted: No valid target shape.")
self.history_label.setText("CV Project failed: No target shape.")
return
target_type = cmds.objectType(shapes[0])
is_curve = target_type == "nurbsCurve"
is_surface = target_type in ["nurbsSurface", "nurbsCurve"]
is_mesh = target_type == "mesh"
try:
cmds.undoInfo(openChunk=True, chunkName="cvProject")
tolerance = self.tolerance_value
projected_count = 0
for vertex in vertices:
source_pos = cmds.pointPosition(vertex, world=True)
closest_pos = None
if is_mesh:
closest_pos = self.get_closest_mesh_point(source_pos, target, tolerance)
elif is_surface or is_curve:
closest_pos = self.get_closest_surface_point(source_pos, target, tolerance, is_curve)
else:
cmds.warning(f"Unsupported target type for {target}: {target_type}")
print(f"Skipping {vertex}: Unsupported target type.")
continue
if closest_pos:
if self.offset_mode == "offsetMode0" and self.offset_value != 0.0:
normal = self.get_surface_normal(target, closest_pos) if is_surface else [0, 0, 1]
closest_pos = [
closest_pos[0] + normal[0] * self.offset_value,
closest_pos[1] + normal[1] * self.offset_value,
closest_pos[2] + normal[2] * self.offset_value
]
elif self.offset_mode == "offsetMode1" and is_curve:
closest_pos = self.adjust_curve_position(target, closest_pos, self.offset_value)
cmds.xform(vertex, t=closest_pos, ws=True)
projected_count += 1
print(f"Projected {vertex} to {closest_pos}")
else:
print(f"Skipping {vertex}: No valid projection point within tolerance.")
cmds.select(vertices, r=True)
cmds.warning(f"CV Project complete! Projected {projected_count}/{len(vertices)} vertices.")
self.history_label.setText(f"Projected {projected_count}/{len(vertices)} vertices.")
print(f"CV Project complete: {projected_count}/{len(vertices)} vertices projected.")
except Exception as e:
cmds.warning(f"Error projecting vertices: {str(e)}")
print(f"Error in cv_project: {e}")
self.history_label.setText("CV Project failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def get_surface_normal(self, target, point):
try:
closest_point_node = cmds.createNode("closestPointOnSurface", n="tempClosestPoint_#")
shapes = cmds.listRelatives(target, shapes=True, fullPath=True)
cmds.connectAttr(f"{shapes[0]}.worldSpace[0]", f"{closest_point_node}.inputSurface")
cmds.setAttr(f"{closest_point_node}.inPosition", *point)
normal = cmds.getAttr(f"{closest_point_node}.normal")[0]
cmds.delete(closest_point_node)
return self.normalize_vector(normal)
except:
return [0, 0, 1]
def adjust_curve_position(self, curve, point, percentage):
try:
closest_point_node = cmds.createNode("pointOnCurveInfo", n="tempPointOnCurve_#")
shapes = cmds.listRelatives(curve, shapes=True, fullPath=True)
cmds.connectAttr(f"{shapes[0]}.worldSpace[0]", f"{closest_point_node}.inputCurve")
cmds.setAttr(f"{closest_point_node}.position", *point)
param = cmds.getAttr(f"{closest_point_node}.parameter")
param += percentage
cmds.setAttr(f"{closest_point_node}.parameter", param)
new_pos = cmds.getAttr(f"{closest_point_node}.position")[0]
cmds.delete(closest_point_node)
return new_pos
except:
return point
def check_symmetry(self):
sel = cmds.ls(sl=True, o=True)
if not sel:
cmds.warning("Select at least one polygonal mesh.")
print("Check Symmetry aborted: No meshes selected.")
self.history_label.setText("Check Symmetry failed: No meshes selected.")
return
try:
cmds.undoInfo(openChunk=True, chunkName="checkSymmetry")
tolerance = self.symmetry_tolerance_value
axis = self.symmetry_axis_dropdown.currentText().lower()
axis_index = {"x": 0, "y": 1, "z": 2}[axis]
print(f"Checking symmetry across {axis}-axis with tolerance {tolerance}")
# Estimate operation time based on total vertex count
total_vertices = sum(len(cmds.ls(f"{m}.vtx[*]", fl=True) or []) for m in sel)
use_progress = total_vertices > 10000 # Rough threshold for >1s operation
progress = None
if use_progress:
progress = QtWidgets.QProgressDialog("Checking symmetry...", "Cancel", 0, len(sel), self)
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setMinimumDuration(0)
progress.setValue(0)
QtWidgets.QApplication.processEvents()
asymmetric_vertices = []
total_asymmetric = 0
centerline_tolerance = 1e-6
for idx, mesh in enumerate(sel):
if use_progress:
progress.setValue(idx)
if progress.wasCanceled():
cmds.warning("Symmetry check canceled by user.")
self.history_label.setText("Check Symmetry canceled.")
return
QtWidgets.QApplication.processEvents()
shapes = cmds.listRelatives(mesh, shapes=True, fullPath=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning(f"Skipping {mesh}: Not a polygonal mesh.")
print(f"Skipping {mesh}: Not a polygonal mesh.")
continue
sel_list = om.MSelectionList()
sel_list.add(mesh)
dag_path = sel_list.getDagPath(0)
mesh_fn = om.MFnMesh(dag_path)
points = mesh_fn.getPoints(om.MSpace.kObject)
vertex_count = len(points)
mesh_asymmetric = []
for i in range(vertex_count):
pos = points[i]
if abs(pos[axis_index]) < centerline_tolerance:
print(f"Skipping vertex {i} in {mesh}: On centerline (pos[{axis}]={pos[axis_index]})")
continue
mirror_pos = [pos[0], pos[1], pos[2]]
mirror_pos[axis_index] = -mirror_pos[axis_index]
mirror_point = om.MPoint(*mirror_pos)
found_match = False
for j in range(vertex_count):
if i == j:
continue
other_pos = points[j]
if (mirror_point - other_pos).length() < tolerance:
found_match = True
break
if not found_match:
mesh_asymmetric.append(f"{mesh}.vtx[{i}]")
print(f"Vertex {i} in {mesh} at {pos} has no symmetric match.")
if mesh_asymmetric:
asymmetric_vertices.extend(mesh_asymmetric)
total_asymmetric += len(mesh_asymmetric)
print(f"Mesh {mesh}: Found {len(mesh_asymmetric)} asymmetric vertices")
else:
print(f"Mesh {mesh}: Symmetric within tolerance")
if use_progress:
progress.setValue(len(sel))
progress.deleteLater()
if asymmetric_vertices:
cmds.select(asymmetric_vertices, r=True)
cmds.warning(f"Found {total_asymmetric} asymmetric vertices across {len(sel)} mesh(es).")
self.history_label.setText(f"Found {total_asymmetric} asymmetric vertices.")
print(f"Total asymmetric vertices: {total_asymmetric}")
else:
cmds.select(clear=True)
cmds.warning("All meshes are symmetric within tolerance.")
self.history_label.setText("All meshes symmetric.")
print("All meshes symmetric within tolerance.")
except Exception as e:
cmds.warning(f"Error checking symmetry: {str(e)}")
print(f"Error in check_symmetry: {e}")
self.history_label.setText("Check Symmetry failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def compare_verts(self):
if self.use_conform_list:
selected_items = self.stored_mesh_list_widget.selectedItems()
if not selected_items:
cmds.warning("Select at least one mesh from the Conform Mesh List.")
print("Compare Verts aborted: No mesh selected from list.")
self.history_label.setText("Compare Verts failed: No list mesh selected.")
return
reference_item = selected_items[0]
reference_name = reference_item.text()
if reference_name.startswith("["):
reference_name = reference_name.split("]", 1)[-1]
sel = cmds.ls(sl=True, o=True)
if not sel:
cmds.warning("Select at least one additional mesh to compare.")
print("Compare Verts aborted: No additional meshes selected.")
self.history_label.setText("Compare Verts failed: No additional meshes.")
return
meshes = [reference_name] + sel
else:
meshes = cmds.ls(sl=True, o=True)
if len(meshes) < 2:
cmds.warning("Select at least two polygonal meshes to compare vertices.")
print("Compare Verts aborted: Need at least two selected meshes.")
self.history_label.setText("Compare Verts failed: Need two meshes.")
return
try:
cmds.undoInfo(openChunk=True, chunkName="compareVerts")
tolerance = self.symmetry_tolerance_value
space = om.MSpace.kObject if self.space_dropdown.currentText() == "Local Space" else om.MSpace.kWorld
print(f"Comparing vertices in {'local' if space == om.MSpace.kObject else 'world'} space with tolerance {tolerance}")
# Collect vertex data for all meshes
mesh_data = []
total_vertices = 0
for mesh in meshes:
if not cmds.objExists(mesh):
cmds.warning(f"Skipping {mesh}: Mesh does not exist.")
print(f"Skipping {mesh}: Mesh does not exist.")
continue
shapes = cmds.listRelatives(mesh, shapes=True, fullPath=True)
if not shapes or cmds.objectType(shapes[0]) != "mesh":
cmds.warning(f"Skipping {mesh}: Not a polygonal mesh.")
print(f"Skipping {mesh}: Not a polygonal mesh.")
continue
sel_list = om.MSelectionList()
sel_list.add(mesh)
dag_path = sel_list.getDagPath(0)
mesh_fn = om.MFnMesh(dag_path)
points = mesh_fn.getPoints(space)
vertex_count = len(points)
total_vertices += vertex_count
mesh_data.append({"name": mesh, "points": points, "vertex_count": vertex_count})
if len(mesh_data) < 2:
cmds.warning("Not enough valid meshes to compare.")
print("Compare Verts aborted: Insufficient valid meshes.")
self.history_label.setText("Compare Verts failed: Insufficient meshes.")
return
# Find reference vertex count
vertex_counts = [data["vertex_count"] for data in mesh_data]
if len(set(vertex_counts)) > 1:
cmds.warning("Vertex count mismatch between meshes.")
print("Compare Verts aborted: Vertex count mismatch.")
self.history_label.setText("Compare Verts failed: Vertex count mismatch.")
return
vertex_count = vertex_counts[0]
# Estimate operation time
use_progress = total_vertices > 10000 # Rough threshold for >1s operation
progress = None
if use_progress:
progress = QtWidgets.QProgressDialog("Comparing vertices...", "Cancel", 0, vertex_count, self)
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setMinimumDuration(0)
progress.setValue(0)
QtWidgets.QApplication.processEvents()
# Compare vertices across meshes
differing_vertices = []
reference_pos = mesh_data[0]["points"]
for i in range(vertex_count):
if use_progress:
progress.setValue(i)
if progress.wasCanceled():
cmds.warning("Vertex comparison canceled by user.")
self.history_label.setText("Compare Verts canceled.")
return
QtWidgets.QApplication.processEvents()
ref_pos = reference_pos[i]
for j in range(1, len(mesh_data)):
other_pos = mesh_data[j]["points"][i]
if (ref_pos - other_pos).length() > tolerance:
differing_vertices.append(f"{mesh_data[j]['name']}.vtx[{i}]")
if use_progress:
progress.setValue(vertex_count)
progress.deleteLater()
if differing_vertices:
cmds.select(differing_vertices, r=True)
cmds.warning(f"Found {len(differing_vertices)} vertices differing by more than {tolerance} units.")
self.history_label.setText(f"{'Compared using list: ' if self.use_conform_list else ''}Found {len(differing_vertices)} differing vertices.")
print(f"Differing vertices: {differing_vertices}")
else:
cmds.select(clear=True)
cmds.warning("All vertices match within tolerance.")
self.history_label.setText(f"{'Compared using list: ' if self.use_conform_list else ''}All vertices match.")
print("All vertices match within tolerance.")
except Exception as e:
cmds.warning(f"Error comparing vertices: {str(e)}")
print(f"Error in compare_verts: {e}")
self.history_label.setText("Compare Verts failed: Error occurred.")
finally:
cmds.undoInfo(closeChunk=True)
def show_vvvertsnap():
try:
if cmds.window("vertsnappyWin", exists=True):
cmds.deleteUI("vertsnappyWin", window=True)
print("Creating Vtx Snappy VV window...")
window = VVvertsnap()
window.setObjectName("vertsnappyWin")
window.show()
print("Vtx Snappy VV window shown.")
except Exception as e:
cmds.warning(f"Failed to launch Vtx Snappy VV: {e}")
print(f"Error launching Vtx Snappy VV: {e}")
if __name__ == "__main__":
print("Starting Vtx Snappy VV 1.4...")
show_vvvertsnap()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment