user 4a49ecd772 Refactor: 배경/프랍 브라우저 IMGUI→UI Toolkit 전환 + USS 리디자인
- BackgroundSceneLoaderWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField)
- PropBrowserWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField)
- StreamingleCommon.uss: 브라우저 공통 스타일 추가 (그리드/리스트/뷰토글/액션바/상태바)
- excludeFromWeb 상태 새로고침 시 보존 수정
- 삭제된 배경 리소스 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 01:55:48 +09:00

249 lines
9.3 KiB
C#

using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using System.IO;
using System.Collections.Generic;
using System.Linq;
public class StreamingleAvatarExporter : EditorWindow
{
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private ObjectField targetField;
private VisualElement meshListContainer;
private Button exportBtn;
private GameObject targetAvatar;
private Dictionary<SkinnedMeshRenderer, string> meshTags = new Dictionary<SkinnedMeshRenderer, string>();
private string[] tagOptions = { "None", "Clothes", "Hair", "Face", "Body", "Other" };
[MenuItem("Tools/Streamingle/아바타 내보내기")]
public static void ShowWindow()
{
GetWindow<StreamingleAvatarExporter>("Streamingle 내보내기");
}
public void CreateGUI()
{
var root = rootVisualElement;
root.AddToClassList("tool-root");
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
if (commonUss != null) root.styleSheets.Add(commonUss);
root.Add(new Label("Streamingle 아바타 내보내기") { name = "title" });
root.Q<Label>("title").AddToClassList("tool-title");
targetField = new ObjectField("아바타 오브젝트") { objectType = typeof(GameObject), allowSceneObjects = true };
targetField.RegisterValueChangedCallback(evt =>
{
var newTarget = evt.newValue as GameObject;
if (newTarget != targetAvatar)
{
targetAvatar = newTarget;
if (targetAvatar != null)
InitializeMeshList();
else
meshTags.Clear();
RebuildMeshList();
}
});
root.Add(targetField);
root.Add(new Label("메쉬 태그 설정") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 8, marginBottom = 4 } });
var meshScroll = new ScrollView { style = { flexGrow = 1, minHeight = 100 } };
meshListContainer = new VisualElement();
meshScroll.Add(meshListContainer);
root.Add(meshScroll);
exportBtn = new Button(ExportAvatar) { text = "아바타 내보내기" };
exportBtn.style.height = 30;
exportBtn.style.marginTop = 8;
exportBtn.AddToClassList("btn-primary");
root.Add(exportBtn);
root.schedule.Execute(() => exportBtn.SetEnabled(targetAvatar != null)).Every(200);
}
private void InitializeMeshList()
{
meshTags.Clear();
var renderers = targetAvatar.GetComponentsInChildren<SkinnedMeshRenderer>(true);
foreach (var renderer in renderers)
meshTags[renderer] = "None";
}
private void RebuildMeshList()
{
if (meshListContainer == null) return;
meshListContainer.Clear();
if (meshTags.Count == 0)
{
meshListContainer.Add(new Label("아바타를 선택하면 메쉬 목록이 표시됩니다.")
{
style = { color = new Color(0.6f, 0.6f, 0.6f), unityFontStyleAndWeight = FontStyle.Italic }
});
return;
}
foreach (var pair in meshTags.ToList())
{
var renderer = pair.Key;
if (renderer == null) continue;
var box = new VisualElement();
box.style.backgroundColor = new Color(0, 0, 0, 0.1f);
box.style.borderTopLeftRadius = box.style.borderTopRightRadius =
box.style.borderBottomLeftRadius = box.style.borderBottomRightRadius = 4;
box.style.paddingTop = box.style.paddingBottom = box.style.paddingLeft = box.style.paddingRight = 6;
box.style.marginBottom = 4;
box.Add(new Label($"메쉬: {renderer.name}") { style = { unityFontStyleAndWeight = FontStyle.Bold, fontSize = 12 } });
var infoRow = new VisualElement { style = { flexDirection = FlexDirection.Row } };
infoRow.Add(new Label($"버텍스: {renderer.sharedMesh.vertexCount}") { style = { fontSize = 11, minWidth = 120 } });
infoRow.Add(new Label($"블렌드쉐입: {renderer.sharedMesh.blendShapeCount}") { style = { fontSize = 11 } });
box.Add(infoRow);
int currentIndex = System.Array.IndexOf(tagOptions, pair.Value);
var tagField = new PopupField<string>("태그", tagOptions.ToList(), currentIndex);
var capturedRenderer = renderer;
tagField.RegisterValueChangedCallback(evt => meshTags[capturedRenderer] = evt.newValue);
box.Add(tagField);
meshListContainer.Add(box);
}
}
private void ExportAvatar()
{
if (targetAvatar == null)
{
EditorUtility.DisplayDialog("오류", "아바타를 먼저 선택해주세요.", "확인");
return;
}
string fullpath = EditorUtility.SaveFilePanel("Streamingle 아바타 내보내기", ".", targetAvatar.name, "streamingle");
if (string.IsNullOrEmpty(fullpath)) return;
string filename = Path.GetFileName(fullpath);
string prefabPath = "Assets/StreamingleAvatarTemp.prefab";
try
{
if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(targetAvatar)))
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(targetAvatar), ImportAssetOptions.ForceUpdate);
string tempPath = Path.Combine(Application.dataPath, "../Temp/AssetBundles");
if (Directory.Exists(tempPath)) Directory.Delete(tempPath, true);
Directory.CreateDirectory(tempPath);
bool succeededPack = false;
GameObject prefabAsset = PrefabUtility.SaveAsPrefabAsset(targetAvatar, prefabPath, out succeededPack);
if (!succeededPack || prefabAsset == null) throw new System.Exception("프리팹 생성 실패");
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
System.Threading.Thread.Sleep(1000);
AssetBundleBuild bundleBuild = new AssetBundleBuild
{
assetBundleName = filename,
assetNames = new string[] { prefabPath }
};
BuildPipeline.BuildAssetBundles(tempPath,
new AssetBundleBuild[] { bundleBuild },
BuildAssetBundleOptions.None,
EditorUserBuildSettings.activeBuildTarget);
string bundlePath = Path.Combine(tempPath, filename);
if (File.Exists(bundlePath))
{
if (File.Exists(fullpath)) File.Delete(fullpath);
File.Copy(bundlePath, fullpath, true);
}
else
{
throw new System.Exception($"AssetBundle 파일을 찾을 수 없습니다: {bundlePath}");
}
EditorUtility.DisplayDialog("완료", "아바타 내보내기 완료!", "확인");
}
catch (System.Exception e)
{
Debug.LogError($"내보내기 실패: {e.Message}");
EditorUtility.DisplayDialog("오류", "아바타 내보내기 실패! 콘솔을 확인해주세요.", "확인");
}
finally
{
AssetDatabase.DeleteAsset(prefabPath);
if (File.Exists(prefabPath)) File.Delete(prefabPath);
}
}
}
public class TagSetupWindow : EditorWindow
{
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private GameObject targetAvatar;
private ScrollView scrollView;
private string[] availableTags = { "Clothes", "Hair", "Face", "Body", "Other" };
public void Initialize(GameObject avatar)
{
targetAvatar = avatar;
titleContent = new GUIContent("태그 설정");
}
public void CreateGUI()
{
var root = rootVisualElement;
root.AddToClassList("tool-root");
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
if (commonUss != null) root.styleSheets.Add(commonUss);
root.Add(new Label("아바타 파츠 태그 설정") { name = "title" });
root.Q<Label>("title").AddToClassList("tool-title");
scrollView = new ScrollView();
root.Add(scrollView);
RebuildTagList();
}
private void RebuildTagList()
{
if (scrollView == null || targetAvatar == null) return;
scrollView.Clear();
Transform[] children = targetAvatar.GetComponentsInChildren<Transform>(true);
foreach (Transform child in children)
{
if (child == targetAvatar.transform) continue;
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 2 } };
row.Add(new Label(child.name) { style = { flexGrow = 1 } });
string currentTag = child.tag;
int tagIndex = System.Array.IndexOf(availableTags, currentTag);
if (tagIndex < 0) tagIndex = 0;
var popup = new PopupField<string>(availableTags.ToList(), tagIndex);
popup.style.width = 100;
var capturedChild = child;
popup.RegisterValueChangedCallback(evt =>
{
capturedChild.tag = evt.newValue;
EditorUtility.SetDirty(capturedChild.gameObject);
});
row.Add(popup);
scrollView.Add(row);
}
}
}