467 lines
14 KiB
C#
467 lines
14 KiB
C#
using UnityEngine;
|
||
using UnityEditor;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System;
|
||
|
||
/// <summary>
|
||
/// 모션 캡쳐 파일 네이밍 편의성 툴
|
||
/// - 선택한 파일들에 일괄적으로 접미사/접두사 추가
|
||
/// - 번호 매기기, 날짜 스탬프 등 다양한 네이밍 패턴 지원
|
||
/// </summary>
|
||
public class MotionCaptureFileRenamer : EditorWindow
|
||
{
|
||
#region Nested Classes
|
||
[Serializable]
|
||
public class NamingPreset
|
||
{
|
||
public string presetName = "New Preset";
|
||
public NamingMode mode = NamingMode.Suffix;
|
||
public string customText = "";
|
||
public int startNumber = 1;
|
||
public int numberPadding = 3;
|
||
public string separator = "_";
|
||
}
|
||
|
||
public enum NamingMode
|
||
{
|
||
Suffix, // 파일명 뒤에 추가
|
||
Prefix, // 파일명 앞에 추가
|
||
Replace, // 전체 이름 변경
|
||
Sequential // 순차 번호
|
||
}
|
||
#endregion
|
||
|
||
#region Fields
|
||
private List<UnityEngine.Object> selectedFiles = new List<UnityEngine.Object>();
|
||
private Vector2 scrollPosition;
|
||
private Vector2 fileListScrollPosition;
|
||
private Vector2 previewScrollPosition;
|
||
|
||
// Naming settings
|
||
private NamingMode namingMode = NamingMode.Suffix;
|
||
private string customText = "_ok";
|
||
private int startNumber = 1;
|
||
private int numberPadding = 3;
|
||
private string separator = "_";
|
||
|
||
// Presets
|
||
private List<NamingPreset> presets = new List<NamingPreset>();
|
||
private int selectedPresetIndex = -1;
|
||
private bool showPresetSection = false;
|
||
private string newPresetName = "My Preset";
|
||
|
||
// UI Style
|
||
private GUIStyle headerStyle;
|
||
private GUIStyle boxStyle;
|
||
private Color successColor = new Color(0.5f, 1f, 0.5f, 0.3f);
|
||
private Color warningColor = new Color(1f, 1f, 0.5f, 0.3f);
|
||
private Color errorColor = new Color(1f, 0.5f, 0.5f, 0.3f);
|
||
#endregion
|
||
|
||
#region Window Initialization
|
||
[MenuItem("Tools/Motion Capture Tools/File Renamer")]
|
||
public static void ShowWindow()
|
||
{
|
||
var window = GetWindow<MotionCaptureFileRenamer>("MoCap File Renamer");
|
||
window.minSize = new Vector2(450, 500);
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
LoadPresets();
|
||
RefreshSelectedFiles();
|
||
Selection.selectionChanged += OnSelectionChanged;
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
SavePresets();
|
||
Selection.selectionChanged -= OnSelectionChanged;
|
||
}
|
||
|
||
private void OnSelectionChanged()
|
||
{
|
||
RefreshSelectedFiles();
|
||
Repaint();
|
||
}
|
||
|
||
private void InitializeStyles()
|
||
{
|
||
if (headerStyle == null)
|
||
{
|
||
headerStyle = new GUIStyle(EditorStyles.boldLabel)
|
||
{
|
||
fontSize = 14,
|
||
alignment = TextAnchor.MiddleLeft
|
||
};
|
||
}
|
||
|
||
if (boxStyle == null)
|
||
{
|
||
boxStyle = new GUIStyle(EditorStyles.helpBox)
|
||
{
|
||
padding = new RectOffset(10, 10, 10, 10)
|
||
};
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region GUI
|
||
private void OnGUI()
|
||
{
|
||
InitializeStyles();
|
||
|
||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||
|
||
DrawHeader();
|
||
DrawFileSelectionSection();
|
||
DrawNamingOptionsSection();
|
||
DrawPresetsSection();
|
||
DrawActionButtons();
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
private void DrawHeader()
|
||
{
|
||
EditorGUILayout.Space(5);
|
||
EditorGUILayout.LabelField("🎬 MoCap File Renamer", headerStyle);
|
||
EditorGUILayout.Space(5);
|
||
}
|
||
|
||
private void DrawFileSelectionSection()
|
||
{
|
||
EditorGUILayout.BeginVertical(boxStyle);
|
||
|
||
// 선택된 파일 수 표시
|
||
GUIStyle countStyle = new GUIStyle(EditorStyles.boldLabel);
|
||
countStyle.fontSize = 13;
|
||
|
||
if (selectedFiles.Count > 0)
|
||
{
|
||
countStyle.normal.textColor = new Color(0.3f, 0.8f, 0.3f);
|
||
EditorGUILayout.LabelField($"📁 {selectedFiles.Count} files selected", countStyle);
|
||
}
|
||
else
|
||
{
|
||
countStyle.normal.textColor = Color.gray;
|
||
EditorGUILayout.LabelField($"📁 No files selected", countStyle);
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
EditorGUILayout.Space(5);
|
||
}
|
||
|
||
private void DrawNamingOptionsSection()
|
||
{
|
||
EditorGUILayout.BeginVertical(boxStyle);
|
||
EditorGUILayout.LabelField("⚙️ Naming Options", EditorStyles.boldLabel);
|
||
EditorGUILayout.Space(5);
|
||
|
||
// Naming mode
|
||
namingMode = (NamingMode)EditorGUILayout.EnumPopup("Mode", namingMode);
|
||
|
||
switch (namingMode)
|
||
{
|
||
case NamingMode.Suffix:
|
||
customText = EditorGUILayout.TextField("Suffix", customText);
|
||
EditorGUILayout.BeginHorizontal();
|
||
if (GUILayout.Button("_ok")) customText = "_ok";
|
||
if (GUILayout.Button("_final")) customText = "_final";
|
||
if (GUILayout.Button("_test")) customText = "_test";
|
||
EditorGUILayout.EndHorizontal();
|
||
break;
|
||
case NamingMode.Prefix:
|
||
customText = EditorGUILayout.TextField("Prefix", customText);
|
||
EditorGUILayout.BeginHorizontal();
|
||
if (GUILayout.Button("Take001")) customText = "Take001";
|
||
if (GUILayout.Button("MoCap")) customText = "MoCap";
|
||
if (GUILayout.Button("Test")) customText = "Test";
|
||
EditorGUILayout.EndHorizontal();
|
||
break;
|
||
case NamingMode.Replace:
|
||
customText = EditorGUILayout.TextField("New Name", customText);
|
||
startNumber = EditorGUILayout.IntField("Start Number", startNumber);
|
||
numberPadding = EditorGUILayout.IntSlider("Padding", numberPadding, 1, 5);
|
||
break;
|
||
case NamingMode.Sequential:
|
||
customText = EditorGUILayout.TextField("Base Name", customText);
|
||
startNumber = EditorGUILayout.IntField("Start Number", startNumber);
|
||
numberPadding = EditorGUILayout.IntSlider("Padding", numberPadding, 1, 5);
|
||
EditorGUILayout.LabelField($"Ex: {customText}{startNumber.ToString().PadLeft(numberPadding, '0')}", EditorStyles.miniLabel);
|
||
break;
|
||
}
|
||
|
||
EditorGUILayout.EndVertical();
|
||
EditorGUILayout.Space(5);
|
||
}
|
||
|
||
|
||
private void DrawPresetsSection()
|
||
{
|
||
showPresetSection = EditorGUILayout.Foldout(showPresetSection, "💾 Presets", true, EditorStyles.foldoutHeader);
|
||
|
||
if (showPresetSection)
|
||
{
|
||
EditorGUILayout.BeginVertical(boxStyle);
|
||
|
||
if (presets.Count > 0)
|
||
{
|
||
for (int i = 0; i < presets.Count; i++)
|
||
{
|
||
EditorGUILayout.BeginHorizontal();
|
||
|
||
if (GUILayout.Button(presets[i].presetName, GUILayout.Height(22)))
|
||
{
|
||
LoadPreset(i);
|
||
}
|
||
|
||
if (GUILayout.Button("×", GUILayout.Width(25), GUILayout.Height(22)))
|
||
{
|
||
presets.RemoveAt(i);
|
||
SavePresets();
|
||
}
|
||
|
||
EditorGUILayout.EndHorizontal();
|
||
}
|
||
|
||
EditorGUILayout.Space(3);
|
||
}
|
||
|
||
EditorGUILayout.BeginHorizontal();
|
||
newPresetName = EditorGUILayout.TextField(newPresetName);
|
||
if (GUILayout.Button("Save", GUILayout.Width(60)))
|
||
{
|
||
SaveCurrentAsPreset();
|
||
}
|
||
EditorGUILayout.EndHorizontal();
|
||
|
||
EditorGUILayout.EndVertical();
|
||
}
|
||
|
||
EditorGUILayout.Space(5);
|
||
}
|
||
|
||
private void DrawActionButtons()
|
||
{
|
||
GUI.enabled = selectedFiles.Count > 0;
|
||
|
||
GUI.backgroundColor = new Color(0.3f, 0.8f, 0.3f);
|
||
if (GUILayout.Button("✓ Apply Rename", GUILayout.Height(50)))
|
||
{
|
||
ApplyRename();
|
||
}
|
||
GUI.backgroundColor = Color.white;
|
||
|
||
GUI.enabled = true;
|
||
}
|
||
#endregion
|
||
|
||
#region File Selection
|
||
private void RefreshSelectedFiles()
|
||
{
|
||
selectedFiles.Clear();
|
||
|
||
var selected = Selection.objects;
|
||
foreach (var obj in selected)
|
||
{
|
||
string path = AssetDatabase.GetAssetPath(obj);
|
||
if (!string.IsNullOrEmpty(path) && !AssetDatabase.IsValidFolder(path))
|
||
{
|
||
selectedFiles.Add(obj);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void HandleDragAndDrop(Rect dropArea)
|
||
{
|
||
Event evt = Event.current;
|
||
|
||
if (dropArea.Contains(evt.mousePosition))
|
||
{
|
||
if (evt.type == EventType.DragUpdated)
|
||
{
|
||
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
||
evt.Use();
|
||
}
|
||
else if (evt.type == EventType.DragPerform)
|
||
{
|
||
DragAndDrop.AcceptDrag();
|
||
|
||
foreach (var obj in DragAndDrop.objectReferences)
|
||
{
|
||
string path = AssetDatabase.GetAssetPath(obj);
|
||
if (!string.IsNullOrEmpty(path) && !AssetDatabase.IsValidFolder(path))
|
||
{
|
||
if (!selectedFiles.Contains(obj))
|
||
{
|
||
selectedFiles.Add(obj);
|
||
}
|
||
}
|
||
}
|
||
|
||
evt.Use();
|
||
}
|
||
}
|
||
}
|
||
#endregion
|
||
|
||
#region Naming Logic
|
||
private string GenerateNewName(string originalName, int index)
|
||
{
|
||
string baseName = originalName;
|
||
string result = "";
|
||
|
||
switch (namingMode)
|
||
{
|
||
case NamingMode.Suffix:
|
||
result = baseName + separator + customText;
|
||
break;
|
||
|
||
case NamingMode.Prefix:
|
||
result = customText + separator + baseName;
|
||
break;
|
||
|
||
case NamingMode.Replace:
|
||
string number = (startNumber + index).ToString().PadLeft(numberPadding, '0');
|
||
result = customText + separator + number;
|
||
break;
|
||
|
||
case NamingMode.Sequential:
|
||
string seqNumber = (startNumber + index).ToString().PadLeft(numberPadding, '0');
|
||
result = customText + seqNumber;
|
||
break;
|
||
}
|
||
|
||
// Remove invalid characters
|
||
result = string.Join("_", result.Split(Path.GetInvalidFileNameChars()));
|
||
|
||
return result;
|
||
}
|
||
|
||
private void ApplyRename()
|
||
{
|
||
if (selectedFiles.Count == 0) return;
|
||
|
||
int successCount = 0;
|
||
int failCount = 0;
|
||
|
||
AssetDatabase.StartAssetEditing();
|
||
|
||
try
|
||
{
|
||
for (int i = 0; i < selectedFiles.Count; i++)
|
||
{
|
||
var file = selectedFiles[i];
|
||
if (file == null) continue;
|
||
|
||
string assetPath = AssetDatabase.GetAssetPath(file);
|
||
string directory = Path.GetDirectoryName(assetPath);
|
||
string extension = Path.GetExtension(assetPath);
|
||
string oldName = file.name;
|
||
string newName = GenerateNewName(oldName, i);
|
||
string newPath = Path.Combine(directory, newName + extension);
|
||
|
||
// Check if file already exists
|
||
if (File.Exists(newPath) && assetPath != newPath)
|
||
{
|
||
Debug.LogWarning($"Skipped (exists): {oldName}");
|
||
failCount++;
|
||
continue;
|
||
}
|
||
|
||
// Rename asset
|
||
string error = AssetDatabase.RenameAsset(assetPath, newName);
|
||
|
||
if (string.IsNullOrEmpty(error))
|
||
{
|
||
Debug.Log($"✓ {oldName} → {newName}");
|
||
successCount++;
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError($"✗ {oldName}: {error}");
|
||
failCount++;
|
||
}
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
AssetDatabase.StopAssetEditing();
|
||
AssetDatabase.SaveAssets();
|
||
AssetDatabase.Refresh();
|
||
}
|
||
|
||
Debug.Log($"<b>Rename Complete:</b> {successCount} success, {failCount} failed");
|
||
|
||
// Clear selection
|
||
selectedFiles.Clear();
|
||
}
|
||
#endregion
|
||
|
||
#region Presets
|
||
private void SaveCurrentAsPreset()
|
||
{
|
||
var preset = new NamingPreset
|
||
{
|
||
presetName = newPresetName,
|
||
mode = namingMode,
|
||
customText = customText,
|
||
startNumber = startNumber,
|
||
numberPadding = numberPadding,
|
||
separator = separator
|
||
};
|
||
|
||
presets.Add(preset);
|
||
SavePresets();
|
||
|
||
Debug.Log($"Preset saved: {newPresetName}");
|
||
}
|
||
|
||
private void LoadPreset(int index)
|
||
{
|
||
if (index < 0 || index >= presets.Count) return;
|
||
|
||
var preset = presets[index];
|
||
namingMode = preset.mode;
|
||
customText = preset.customText;
|
||
startNumber = preset.startNumber;
|
||
numberPadding = preset.numberPadding;
|
||
separator = preset.separator;
|
||
|
||
Debug.Log($"Preset loaded: {preset.presetName}");
|
||
}
|
||
|
||
private void SavePresets()
|
||
{
|
||
string json = JsonUtility.ToJson(new PresetList { presets = presets }, true);
|
||
EditorPrefs.SetString("MotionCaptureFileRenamer_Presets", json);
|
||
}
|
||
|
||
private void LoadPresets()
|
||
{
|
||
string json = EditorPrefs.GetString("MotionCaptureFileRenamer_Presets", "");
|
||
if (!string.IsNullOrEmpty(json))
|
||
{
|
||
try
|
||
{
|
||
var presetList = JsonUtility.FromJson<PresetList>(json);
|
||
presets = presetList.presets ?? new List<NamingPreset>();
|
||
}
|
||
catch
|
||
{
|
||
presets = new List<NamingPreset>();
|
||
}
|
||
}
|
||
}
|
||
|
||
[Serializable]
|
||
private class PresetList
|
||
{
|
||
public List<NamingPreset> presets = new List<NamingPreset>();
|
||
}
|
||
#endregion
|
||
}
|