user 41270a34f5 Refactor: 전체 에디터 UXML 전환 + 대시보드/런타임 UI + 한글화 + NanumGothic 폰트
- 모든 컨트롤러 에디터를 IMGUI → UI Toolkit(UXML/USS)으로 전환
  (Camera, Item, Event, Avatar, System, StreamDeck, OptiTrack, Facial)
- StreamingleCommon.uss 공통 테마 + 개별 에디터 USS 스타일시트
- SystemController 서브매니저 분리 (OptiTrack, Facial, Recording, Screenshot 등)
- 런타임 컨트롤 패널 (ESC 토글, 좌측 오버레이, 150% 스케일)
- 웹 대시보드 서버 (StreamingleDashboardServer) + 리타게팅 통합
- 설정 도구(StreamingleControllerSetupTool) UXML 재작성 + 원클릭 설정
- SimplePoseTransfer UXML 에디터 추가
- 전체 UXML 한글화 + NanumGothic 폰트 적용
- Streamingle.Debug → Streamingle.Debugging 네임스페이스 변경 (Debug.Log 충돌 해결)
- 불필요 코드 제거 (rawkey.cs, RetargetingHTTPServer, OptitrackSkeletonAnimator 등)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 02:51:43 +09:00

341 lines
12 KiB
C#

using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using System.Collections.Generic;
[CustomEditor(typeof(AvatarOutfitController))]
public class AvatarOutfitControllerEditor : Editor
{
private const string UxmlPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/AvatarOutfitControllerEditor.uxml";
private const string UssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/AvatarOutfitControllerEditor.uss";
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private VisualElement avatarsContainer;
private Label avatarsTitleLabel;
private AvatarOutfitController controller;
public override VisualElement CreateInspectorGUI()
{
controller = (AvatarOutfitController)target;
var root = new VisualElement();
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
if (commonUss != null) root.styleSheets.Add(commonUss);
if (uss != null) root.styleSheets.Add(uss);
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
if (uxml != null) uxml.CloneTree(root);
BuildAvatarsSection(root);
root.schedule.Execute(UpdatePlayModeState).Every(200);
Undo.undoRedoPerformed += OnUndoRedo;
root.RegisterCallback<DetachFromPanelEvent>(_ => Undo.undoRedoPerformed -= OnUndoRedo);
return root;
}
private void OnUndoRedo()
{
if (controller == null) return;
serializedObject.Update();
RebuildAvatarList();
}
#region Avatars List
private void BuildAvatarsSection(VisualElement root)
{
var section = root.Q("avatarsSection");
if (section == null) return;
var header = new VisualElement();
header.AddToClassList("list-header");
avatarsTitleLabel = new Label($"Avatars ({controller.avatars.Count})");
avatarsTitleLabel.AddToClassList("list-title");
header.Add(avatarsTitleLabel);
var addBtn = new Button(AddAvatar) { text = "+ 아바타 추가" };
addBtn.AddToClassList("list-add-btn");
header.Add(addBtn);
section.Add(header);
avatarsContainer = new VisualElement();
section.Add(avatarsContainer);
RebuildAvatarList();
}
private void RebuildAvatarList()
{
if (avatarsContainer == null || controller == null) return;
avatarsContainer.Clear();
if (avatarsTitleLabel != null)
avatarsTitleLabel.text = $"Avatars ({controller.avatars.Count})";
if (controller.avatars.Count == 0)
{
var empty = new Label("아바타가 없습니다. '+ 아바타 추가' 버튼을 눌러 추가하세요.");
empty.AddToClassList("list-empty");
avatarsContainer.Add(empty);
return;
}
for (int i = 0; i < controller.avatars.Count; i++)
{
avatarsContainer.Add(CreateAvatarElement(i));
}
}
private VisualElement CreateAvatarElement(int avatarIndex)
{
var avatar = controller.avatars[avatarIndex];
bool isCurrent = Application.isPlaying && controller.CurrentAvatar == avatar;
var item = new VisualElement();
item.AddToClassList("list-item");
item.AddToClassList("avatar-item");
if (isCurrent) item.AddToClassList("list-item--active");
// Header row
var headerRow = new VisualElement();
headerRow.AddToClassList("list-item-header");
var indexLabel = new Label($"{avatarIndex + 1}");
indexLabel.AddToClassList("list-index");
headerRow.Add(indexLabel);
var nameField = new TextField();
nameField.value = avatar.avatarName;
nameField.AddToClassList("list-name-field");
int ai = avatarIndex;
nameField.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(target, "Rename Avatar");
controller.avatars[ai].avatarName = evt.newValue;
EditorUtility.SetDirty(target);
});
headerRow.Add(nameField);
if (isCurrent)
{
var activeLabel = new Label("[Current]");
activeLabel.AddToClassList("list-active-label");
headerRow.Add(activeLabel);
}
// Reorder buttons
var upBtn = new Button(() => SwapAvatars(ai, ai - 1)) { text = "\u25B2" };
upBtn.AddToClassList("list-reorder-btn");
upBtn.SetEnabled(avatarIndex > 0);
headerRow.Add(upBtn);
var downBtn = new Button(() => SwapAvatars(ai, ai + 1)) { text = "\u25BC" };
downBtn.AddToClassList("list-reorder-btn");
downBtn.SetEnabled(avatarIndex < controller.avatars.Count - 1);
headerRow.Add(downBtn);
var deleteBtn = new Button(() => DeleteAvatar(ai)) { text = "X" };
deleteBtn.AddToClassList("list-delete-btn");
headerRow.Add(deleteBtn);
item.Add(headerRow);
// Avatar fields
var fields = new VisualElement();
fields.AddToClassList("list-fields");
var listProp = serializedObject.FindProperty("avatars");
var avatarProp = listProp.GetArrayElementAtIndex(avatarIndex);
var avatarObjField = new PropertyField(avatarProp.FindPropertyRelative("avatarObject"), "Avatar Object");
fields.Add(avatarObjField);
// Outfits sub-list
int outfitCount = avatar.outfits?.Length ?? 0;
var outfitsFoldout = new Foldout { text = $"Outfits ({outfitCount})", value = true };
outfitsFoldout.AddToClassList("outfit-foldout");
var outfitsContainer = new VisualElement();
outfitsContainer.AddToClassList("outfits-container");
if (avatar.outfits != null && avatar.outfits.Length > 0)
{
var outfitsProp = avatarProp.FindPropertyRelative("outfits");
for (int oi = 0; oi < avatar.outfits.Length; oi++)
{
outfitsContainer.Add(CreateOutfitElement(avatarIndex, oi, outfitsProp.GetArrayElementAtIndex(oi)));
}
}
else
{
var emptyOutfit = new Label("의상이 없습니다.");
emptyOutfit.AddToClassList("list-empty");
outfitsContainer.Add(emptyOutfit);
}
var addOutfitBtn = new Button(() => AddOutfit(ai)) { text = "+ 의상 추가" };
addOutfitBtn.AddToClassList("list-add-btn");
addOutfitBtn.style.marginTop = 4;
outfitsContainer.Add(addOutfitBtn);
outfitsFoldout.Add(outfitsContainer);
fields.Add(outfitsFoldout);
item.Add(fields);
return item;
}
private VisualElement CreateOutfitElement(int avatarIndex, int outfitIndex, SerializedProperty outfitProp)
{
var avatar = controller.avatars[avatarIndex];
var outfit = avatar.outfits[outfitIndex];
bool isCurrentOutfit = avatar.CurrentOutfitIndex == outfitIndex;
var outfitItem = new VisualElement();
outfitItem.AddToClassList("outfit-item");
if (isCurrentOutfit) outfitItem.AddToClassList("outfit-item--current");
// Outfit header
var outfitHeader = new VisualElement();
outfitHeader.AddToClassList("outfit-header");
var outfitIdx = new Label($"{outfitIndex + 1}");
outfitIdx.AddToClassList("list-index");
outfitHeader.Add(outfitIdx);
var outfitNameField = new TextField();
outfitNameField.value = outfit.outfitName;
outfitNameField.AddToClassList("list-name-field");
int ai = avatarIndex, oi = outfitIndex;
outfitNameField.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(target, "Rename Outfit");
controller.avatars[ai].outfits[oi].outfitName = evt.newValue;
EditorUtility.SetDirty(target);
});
outfitHeader.Add(outfitNameField);
if (isCurrentOutfit)
{
var currentLabel = new Label("[Equipped]");
currentLabel.AddToClassList("outfit-current-label");
outfitHeader.Add(currentLabel);
}
var deleteOutfitBtn = new Button(() => DeleteOutfit(ai, oi)) { text = "X" };
deleteOutfitBtn.AddToClassList("list-delete-btn");
outfitHeader.Add(deleteOutfitBtn);
outfitItem.Add(outfitHeader);
// Outfit fields
var outfitFields = new VisualElement();
outfitFields.AddToClassList("outfit-fields");
var clothingField = new PropertyField(outfitProp.FindPropertyRelative("clothingObjects"), "Clothing");
outfitFields.Add(clothingField);
var hideField = new PropertyField(outfitProp.FindPropertyRelative("hideObjects"), "Hide Objects");
outfitFields.Add(hideField);
outfitItem.Add(outfitFields);
return outfitItem;
}
#endregion
#region Avatar Actions
private void AddAvatar()
{
Undo.RecordObject(target, "Add Avatar");
controller.avatars.Add(new AvatarOutfitController.AvatarData("New Avatar"));
EditorUtility.SetDirty(target);
serializedObject.Update();
RebuildAvatarList();
}
private void DeleteAvatar(int index)
{
var avatar = controller.avatars[index];
if (EditorUtility.DisplayDialog("아바타 삭제",
$"아바타 '{avatar.avatarName}'을(를) 삭제하시겠습니까?", "삭제", "취소"))
{
Undo.RecordObject(target, "Delete Avatar");
controller.avatars.RemoveAt(index);
EditorUtility.SetDirty(target);
serializedObject.Update();
RebuildAvatarList();
}
}
private void SwapAvatars(int a, int b)
{
Undo.RecordObject(target, "Reorder Avatars");
(controller.avatars[a], controller.avatars[b]) = (controller.avatars[b], controller.avatars[a]);
EditorUtility.SetDirty(target);
serializedObject.Update();
RebuildAvatarList();
}
private void AddOutfit(int avatarIndex)
{
Undo.RecordObject(target, "Add Outfit");
var avatar = controller.avatars[avatarIndex];
var outfits = avatar.outfits != null
? new List<AvatarOutfitController.OutfitData>(avatar.outfits)
: new List<AvatarOutfitController.OutfitData>();
outfits.Add(new AvatarOutfitController.OutfitData { outfitName = "New Outfit" });
avatar.outfits = outfits.ToArray();
EditorUtility.SetDirty(target);
serializedObject.Update();
RebuildAvatarList();
}
private void DeleteOutfit(int avatarIndex, int outfitIndex)
{
var outfit = controller.avatars[avatarIndex].outfits[outfitIndex];
if (EditorUtility.DisplayDialog("의상 삭제",
$"의상 '{outfit.outfitName}'을(를) 삭제하시겠습니까?", "삭제", "취소"))
{
Undo.RecordObject(target, "Delete Outfit");
var outfits = new List<AvatarOutfitController.OutfitData>(controller.avatars[avatarIndex].outfits);
outfits.RemoveAt(outfitIndex);
controller.avatars[avatarIndex].outfits = outfits.ToArray();
EditorUtility.SetDirty(target);
serializedObject.Update();
RebuildAvatarList();
}
}
#endregion
#region Play Mode State
private void UpdatePlayModeState()
{
if (avatarsContainer == null || controller == null) return;
if (!Application.isPlaying) return;
for (int i = 0; i < avatarsContainer.childCount && i < controller.avatars.Count; i++)
{
var item = avatarsContainer[i];
bool isCurrent = controller.CurrentAvatar == controller.avatars[i];
if (isCurrent && !item.ClassListContains("list-item--active"))
item.AddToClassList("list-item--active");
else if (!isCurrent && item.ClassListContains("list-item--active"))
item.RemoveFromClassList("list-item--active");
}
}
#endregion
}