From 540b8645f2b32dd80b3edab44af88530dabc96b2 Mon Sep 17 00:00:00 2001 From: "qsxft258@gmail.com" Date: Thu, 16 Oct 2025 20:39:12 +0900 Subject: [PATCH] =?UTF-8?q?ADD=20:=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=ED=88=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Editor/MotionCaptureFileRenamer.cs | 691 ++++++++++++++++++ 1 file changed, 691 insertions(+) create mode 100644 Assets/Scripts/Editor/MotionCaptureFileRenamer.cs diff --git a/Assets/Scripts/Editor/MotionCaptureFileRenamer.cs b/Assets/Scripts/Editor/MotionCaptureFileRenamer.cs new file mode 100644 index 00000000..c76973d7 --- /dev/null +++ b/Assets/Scripts/Editor/MotionCaptureFileRenamer.cs @@ -0,0 +1,691 @@ +using UnityEngine; +using UnityEditor; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System; + +/// +/// 모션 캡쳐 파일 네이밍 편의성 툴 +/// - 선택한 파일들에 일괄적으로 접미사/접두사 추가 +/// - 번호 매기기, 날짜 스탬프 등 다양한 네이밍 패턴 지원 +/// +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 bool includeDate = false; + public bool includeTime = false; + public string dateFormat = "yyyyMMdd"; + public string timeFormat = "HHmmss"; + public string separator = "_"; + } + + public enum NamingMode + { + Suffix, // 파일명 뒤에 추가 + Prefix, // 파일명 앞에 추가 + Replace, // 전체 이름 변경 + Sequential // 순차 번호 + } + #endregion + + #region Fields + private List selectedFiles = new List(); + private Vector2 scrollPosition; + + // Naming settings + private NamingMode namingMode = NamingMode.Suffix; + private string customText = "_ok"; + private int startNumber = 1; + private int numberPadding = 3; + private bool includeDate = false; + private bool includeTime = false; + private string dateFormat = "yyyyMMdd"; + private string timeFormat = "HHmmss"; + private string separator = "_"; + + // Preview + private bool showPreview = true; + private Dictionary previewNames = new Dictionary(); + + // Presets + private List presets = new List(); + 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/File Renamer")] + public static void ShowWindow() + { + var window = GetWindow("MoCap File Renamer"); + window.minSize = new Vector2(450, 500); + } + + private void OnEnable() + { + LoadPresets(); + RefreshSelectedFiles(); + } + + private void OnDisable() + { + SavePresets(); + } + + 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(); + DrawPreviewSection(); + DrawPresetsSection(); + DrawActionButtons(); + + EditorGUILayout.EndScrollView(); + } + + private void DrawHeader() + { + EditorGUILayout.Space(10); + EditorGUILayout.LabelField("Motion Capture File Renamer", headerStyle); + EditorGUILayout.LabelField("모션 캡쳐 파일 일괄 네이밍 도구", EditorStyles.miniLabel); + EditorGUILayout.Space(10); + EditorGUILayout.HelpBox("프로젝트 뷰에서 파일을 선택하거나 아래 영역에 드래그 앤 드롭하세요.", MessageType.Info); + EditorGUILayout.Space(5); + } + + private void DrawFileSelectionSection() + { + EditorGUILayout.BeginVertical(boxStyle); + EditorGUILayout.LabelField("📁 File Selection", EditorStyles.boldLabel); + EditorGUILayout.Space(5); + + // Drag and drop area + Rect dropArea = GUILayoutUtility.GetRect(0f, 50f, GUILayout.ExpandWidth(true)); + GUI.Box(dropArea, "Drag & Drop Files Here\n또는 'Load Selected Files' 버튼 클릭", EditorStyles.helpBox); + + HandleDragAndDrop(dropArea); + + EditorGUILayout.Space(5); + + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Load Selected Files", GUILayout.Height(30))) + { + RefreshSelectedFiles(); + } + if (GUILayout.Button("Clear", GUILayout.Width(80), GUILayout.Height(30))) + { + selectedFiles.Clear(); + previewNames.Clear(); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(5); + EditorGUILayout.LabelField($"Selected Files: {selectedFiles.Count}", EditorStyles.miniLabel); + + if (selectedFiles.Count > 0) + { + EditorGUILayout.Space(5); + EditorGUILayout.LabelField("Current Files:", EditorStyles.miniBoldLabel); + + var rect = EditorGUILayout.BeginVertical(GUILayout.MaxHeight(100)); + scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.MaxHeight(100)); + + for (int i = 0; i < selectedFiles.Count; i++) + { + if (selectedFiles[i] != null) + { + EditorGUILayout.LabelField($"{i + 1}. {selectedFiles[i].name}", EditorStyles.miniLabel); + } + } + + EditorGUILayout.EndScrollView(); + EditorGUILayout.EndVertical(); + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(10); + } + + private void DrawNamingOptionsSection() + { + EditorGUILayout.BeginVertical(boxStyle); + EditorGUILayout.LabelField("⚙️ Naming Options", EditorStyles.boldLabel); + EditorGUILayout.Space(5); + + // Naming mode + namingMode = (NamingMode)EditorGUILayout.EnumPopup("Naming Mode", namingMode); + EditorGUILayout.Space(5); + + switch (namingMode) + { + case NamingMode.Suffix: + DrawSuffixOptions(); + break; + case NamingMode.Prefix: + DrawPrefixOptions(); + break; + case NamingMode.Replace: + DrawReplaceOptions(); + break; + case NamingMode.Sequential: + DrawSequentialOptions(); + break; + } + + EditorGUILayout.Space(5); + + // Common options + EditorGUILayout.LabelField("Additional Options", EditorStyles.miniBoldLabel); + separator = EditorGUILayout.TextField("Separator", separator); + includeDate = EditorGUILayout.Toggle("Include Date", includeDate); + if (includeDate) + { + EditorGUI.indentLevel++; + dateFormat = EditorGUILayout.TextField("Date Format", dateFormat); + EditorGUILayout.LabelField($"Example: {DateTime.Now.ToString(dateFormat)}", EditorStyles.miniLabel); + EditorGUI.indentLevel--; + } + + includeTime = EditorGUILayout.Toggle("Include Time", includeTime); + if (includeTime) + { + EditorGUI.indentLevel++; + timeFormat = EditorGUILayout.TextField("Time Format", timeFormat); + EditorGUILayout.LabelField($"Example: {DateTime.Now.ToString(timeFormat)}", EditorStyles.miniLabel); + EditorGUI.indentLevel--; + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(10); + } + + private void DrawSuffixOptions() + { + EditorGUILayout.HelpBox("파일명 뒤에 텍스트를 추가합니다.\n예: Animation.anim → Animation_ok.anim", MessageType.None); + customText = EditorGUILayout.TextField("Suffix Text", customText); + + EditorGUILayout.Space(5); + EditorGUILayout.LabelField("Quick Presets:", EditorStyles.miniBoldLabel); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("_ok")) customText = "_ok"; + if (GUILayout.Button("_final")) customText = "_final"; + if (GUILayout.Button("_test")) customText = "_test"; + if (GUILayout.Button("_backup")) customText = "_backup"; + EditorGUILayout.EndHorizontal(); + } + + private void DrawPrefixOptions() + { + EditorGUILayout.HelpBox("파일명 앞에 텍스트를 추가합니다.\n예: Animation.anim → Take001_Animation.anim", MessageType.None); + customText = EditorGUILayout.TextField("Prefix Text", customText); + + EditorGUILayout.Space(5); + EditorGUILayout.LabelField("Quick Presets:", EditorStyles.miniBoldLabel); + EditorGUILayout.BeginHorizontal(); + if (GUILayout.Button("Take001")) customText = "Take001"; + if (GUILayout.Button("MoCap")) customText = "MoCap"; + if (GUILayout.Button("Test")) customText = "Test"; + if (GUILayout.Button("WIP")) customText = "WIP"; + EditorGUILayout.EndHorizontal(); + } + + private void DrawReplaceOptions() + { + EditorGUILayout.HelpBox("파일명을 완전히 새로운 이름으로 변경합니다.\n(확장자는 유지됩니다)", MessageType.Warning); + customText = EditorGUILayout.TextField("New Base Name", customText); + + EditorGUILayout.Space(5); + bool autoNumber = EditorGUILayout.Toggle("Auto Number", true); + if (autoNumber) + { + EditorGUI.indentLevel++; + startNumber = EditorGUILayout.IntField("Start Number", startNumber); + numberPadding = EditorGUILayout.IntSlider("Number Padding", numberPadding, 1, 5); + EditorGUI.indentLevel--; + } + } + + private void DrawSequentialOptions() + { + EditorGUILayout.HelpBox("선택한 파일들에 순차적으로 번호를 부여합니다.\n예: Take001, Take002, Take003...", MessageType.None); + + customText = EditorGUILayout.TextField("Base Name", customText); + startNumber = EditorGUILayout.IntField("Start Number", startNumber); + numberPadding = EditorGUILayout.IntSlider("Number Padding", numberPadding, 1, 5); + + EditorGUILayout.Space(5); + EditorGUILayout.LabelField($"Example: {customText}{startNumber.ToString().PadLeft(numberPadding, '0')}", EditorStyles.miniLabel); + } + + private void DrawPreviewSection() + { + showPreview = EditorGUILayout.Foldout(showPreview, "🔍 Preview", true, EditorStyles.foldoutHeader); + + if (showPreview) + { + EditorGUILayout.BeginVertical(boxStyle); + + if (selectedFiles.Count == 0) + { + EditorGUILayout.HelpBox("파일을 먼저 선택하세요.", MessageType.Info); + } + else + { + if (GUILayout.Button("Generate Preview", GUILayout.Height(25))) + { + GeneratePreview(); + } + + if (previewNames.Count > 0) + { + EditorGUILayout.Space(5); + EditorGUILayout.LabelField("변경 예정:", EditorStyles.miniBoldLabel); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + Vector2 previewScroll = EditorGUILayout.BeginScrollView(scrollPosition, GUILayout.MaxHeight(150)); + + foreach (var kvp in previewNames) + { + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField(kvp.Key, GUILayout.Width(150)); + EditorGUILayout.LabelField("→", GUILayout.Width(20)); + EditorGUILayout.LabelField(kvp.Value, EditorStyles.boldLabel); + EditorGUILayout.EndHorizontal(); + } + + EditorGUILayout.EndScrollView(); + EditorGUILayout.EndVertical(); + } + } + + EditorGUILayout.EndVertical(); + } + + EditorGUILayout.Space(10); + } + + private void DrawPresetsSection() + { + showPresetSection = EditorGUILayout.Foldout(showPresetSection, "💾 Presets", true, EditorStyles.foldoutHeader); + + if (showPresetSection) + { + EditorGUILayout.BeginVertical(boxStyle); + + EditorGUILayout.BeginHorizontal(); + newPresetName = EditorGUILayout.TextField("Preset Name", newPresetName); + if (GUILayout.Button("Save Current", GUILayout.Width(100))) + { + SaveCurrentAsPreset(); + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(5); + + if (presets.Count > 0) + { + EditorGUILayout.LabelField("Saved Presets:", EditorStyles.miniBoldLabel); + + for (int i = 0; i < presets.Count; i++) + { + EditorGUILayout.BeginHorizontal(); + + if (GUILayout.Button(presets[i].presetName, GUILayout.Height(25))) + { + LoadPreset(i); + } + + if (GUILayout.Button("×", GUILayout.Width(25), GUILayout.Height(25))) + { + if (EditorUtility.DisplayDialog("Delete Preset", + $"'{presets[i].presetName}' 프리셋을 삭제하시겠습니까?", "Delete", "Cancel")) + { + presets.RemoveAt(i); + SavePresets(); + } + } + + EditorGUILayout.EndHorizontal(); + } + } + else + { + EditorGUILayout.HelpBox("저장된 프리셋이 없습니다.", MessageType.Info); + } + + EditorGUILayout.EndVertical(); + } + + EditorGUILayout.Space(10); + } + + private void DrawActionButtons() + { + EditorGUILayout.BeginVertical(boxStyle); + + GUI.enabled = selectedFiles.Count > 0; + + EditorGUILayout.BeginHorizontal(); + + if (GUILayout.Button("Apply Rename", GUILayout.Height(40))) + { + if (EditorUtility.DisplayDialog("Confirm Rename", + $"{selectedFiles.Count}개의 파일 이름을 변경하시겠습니까?\n\n이 작업은 Undo로 되돌릴 수 있습니다.", + "Rename", "Cancel")) + { + ApplyRename(); + } + } + + EditorGUILayout.EndHorizontal(); + + GUI.enabled = true; + + EditorGUILayout.EndVertical(); + } + #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); + } + } + + previewNames.Clear(); + Debug.Log($"Loaded {selectedFiles.Count} files"); + } + + 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 void GeneratePreview() + { + previewNames.Clear(); + + for (int i = 0; i < selectedFiles.Count; i++) + { + var file = selectedFiles[i]; + if (file == null) continue; + + string originalName = file.name; + string newName = GenerateNewName(originalName, i); + + previewNames[originalName] = newName; + } + } + + private string GenerateNewName(string originalName, int index) + { + string baseName = originalName; + string result = ""; + + // Build timestamp if needed + string timestamp = ""; + if (includeDate) + { + timestamp += DateTime.Now.ToString(dateFormat); + } + if (includeTime) + { + if (!string.IsNullOrEmpty(timestamp)) + timestamp += separator; + timestamp += DateTime.Now.ToString(timeFormat); + } + + switch (namingMode) + { + case NamingMode.Suffix: + result = baseName + separator + customText; + if (!string.IsNullOrEmpty(timestamp)) + result += separator + timestamp; + break; + + case NamingMode.Prefix: + result = customText + separator + baseName; + if (!string.IsNullOrEmpty(timestamp)) + result += separator + timestamp; + break; + + case NamingMode.Replace: + string number = (startNumber + index).ToString().PadLeft(numberPadding, '0'); + result = customText + separator + number; + if (!string.IsNullOrEmpty(timestamp)) + result += separator + timestamp; + break; + + case NamingMode.Sequential: + string seqNumber = (startNumber + index).ToString().PadLeft(numberPadding, '0'); + result = customText + seqNumber; + if (!string.IsNullOrEmpty(timestamp)) + result += separator + timestamp; + break; + } + + // Remove invalid characters + result = string.Join("_", result.Split(Path.GetInvalidFileNameChars())); + + return result; + } + + private void ApplyRename() + { + if (selectedFiles.Count == 0) + { + EditorUtility.DisplayDialog("Error", "선택된 파일이 없습니다.", "OK"); + 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($"File already exists: {newPath}. Skipping {oldName}."); + failCount++; + continue; + } + + // Rename asset + string error = AssetDatabase.RenameAsset(assetPath, newName); + + if (string.IsNullOrEmpty(error)) + { + Debug.Log($"Renamed: {oldName} → {newName}"); + successCount++; + } + else + { + Debug.LogError($"Failed to rename {oldName}: {error}"); + failCount++; + } + } + } + finally + { + AssetDatabase.StopAssetEditing(); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + + string message = $"Rename completed!\n\nSuccess: {successCount}\nFailed: {failCount}"; + EditorUtility.DisplayDialog("Rename Result", message, "OK"); + + // Clear selection and preview + selectedFiles.Clear(); + previewNames.Clear(); + } + #endregion + + #region Presets + private void SaveCurrentAsPreset() + { + var preset = new NamingPreset + { + presetName = newPresetName, + mode = namingMode, + customText = customText, + startNumber = startNumber, + numberPadding = numberPadding, + includeDate = includeDate, + includeTime = includeTime, + dateFormat = dateFormat, + timeFormat = timeFormat, + separator = separator + }; + + presets.Add(preset); + SavePresets(); + + Debug.Log($"Preset saved: {newPresetName}"); + EditorUtility.DisplayDialog("Success", $"프리셋 '{newPresetName}'이(가) 저장되었습니다.", "OK"); + } + + 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; + includeDate = preset.includeDate; + includeTime = preset.includeTime; + dateFormat = preset.dateFormat; + timeFormat = preset.timeFormat; + 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(json); + presets = presetList.presets ?? new List(); + } + catch + { + presets = new List(); + } + } + } + + [Serializable] + private class PresetList + { + public List presets = new List(); + } + #endregion +}