Streamingle_URP/Assets/Scripts/Editor/StreamingleTools/StreamingleControllerSetupToolAdvanced.cs
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

428 lines
15 KiB
C#

using System;
using System.Text;
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
namespace Streamingle.Editor
{
public class StreamingleControllerSetupToolAdvanced : EditorWindow
{
private const string UxmlPath = "Assets/Scripts/Editor/StreamingleTools/StreamingleControllerSetupTool.uxml";
private struct ControllerDef
{
public string label;
public string propertyName;
public Type componentType;
public string gameObjectName;
}
private ControllerDef[] controllerDefs;
private Component[] existingComponents;
private Toggle[] createToggles;
private VisualElement statusList;
private VisualElement createTogglesContainer;
private Toggle toggleCreateParent;
private TextField fieldParentName;
private Toggle toggleAutoConnect;
private Toggle toggleMoveExisting;
private Button btnOneClick;
private Button btnCreate;
private Button btnConnect;
private Label resultText;
private readonly StringBuilder logBuilder = new();
[MenuItem("Tools/Streamingle/고급 컨트롤러 설정 도구")]
public static void ShowWindow()
{
var wnd = GetWindow<StreamingleControllerSetupToolAdvanced>("Streamingle 설정");
wnd.minSize = new Vector2(360, 380);
}
private void InitControllerDefs()
{
controllerDefs = new ControllerDef[]
{
new() { label = "StreamDeck 서버 매니저", propertyName = null, componentType = typeof(StreamDeckServerManager), gameObjectName = "StreamDeck 서버 매니저" },
new() { label = "시스템 컨트롤러", propertyName = "systemController", componentType = typeof(SystemController), gameObjectName = "시스템 컨트롤러" },
new() { label = "카메라 매니저", propertyName = "cameraManager", componentType = typeof(CameraManager), gameObjectName = "카메라 매니저" },
new() { label = "아이템 컨트롤러", propertyName = "itemController", componentType = typeof(ItemController), gameObjectName = "아이템 컨트롤러" },
new() { label = "이벤트 컨트롤러", propertyName = "eventController", componentType = typeof(EventController), gameObjectName = "이벤트 컨트롤러" },
new() { label = "아바타 의상 컨트롤러", propertyName = "avatarOutfitController", componentType = typeof(AvatarOutfitController), gameObjectName = "아바타 의상 컨트롤러" },
};
existingComponents = new Component[controllerDefs.Length];
createToggles = new Toggle[controllerDefs.Length];
}
private void CreateGUI()
{
InitControllerDefs();
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
if (uxml != null) uxml.CloneTree(rootVisualElement);
statusList = rootVisualElement.Q("status-list");
createTogglesContainer = rootVisualElement.Q("create-toggles");
toggleCreateParent = rootVisualElement.Q<Toggle>("toggle-create-parent");
fieldParentName = rootVisualElement.Q<TextField>("field-parent-name");
toggleAutoConnect = rootVisualElement.Q<Toggle>("toggle-auto-connect");
toggleMoveExisting = rootVisualElement.Q<Toggle>("toggle-move-existing");
btnOneClick = rootVisualElement.Q<Button>("btn-one-click");
btnCreate = rootVisualElement.Q<Button>("btn-create");
btnConnect = rootVisualElement.Q<Button>("btn-connect");
resultText = rootVisualElement.Q<Label>("result-text");
toggleCreateParent?.RegisterValueChangedCallback(evt =>
{
if (fieldParentName != null)
fieldParentName.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None;
});
btnOneClick?.RegisterCallback<ClickEvent>(_ => OneClickSetup());
btnCreate?.RegisterCallback<ClickEvent>(_ => CreateControllers());
btnConnect?.RegisterCallback<ClickEvent>(_ => ConnectExistingControllers());
rootVisualElement.Q<Button>("btn-refresh")?.RegisterCallback<ClickEvent>(_ => RefreshAll());
rootVisualElement.Q<Button>("btn-move")?.RegisterCallback<ClickEvent>(_ => MoveExistingControllersToParent());
BuildCreateToggles();
RefreshAll();
}
private void OnFocus()
{
if (statusList != null) RefreshAll();
}
#region Status
private void RefreshAll()
{
FindExistingControllers();
RebuildStatusList();
UpdateCreateToggles();
UpdateButtonStates();
}
private void FindExistingControllers()
{
if (controllerDefs == null) return;
for (int i = 0; i < controllerDefs.Length; i++)
existingComponents[i] = FindFirstObjectByType(controllerDefs[i].componentType) as Component;
}
private void RebuildStatusList()
{
if (statusList == null) return;
statusList.Clear();
int foundCount = 0;
for (int i = 0; i < controllerDefs.Length; i++)
{
var def = controllerDefs[i];
var comp = existingComponents[i];
bool found = comp != null;
if (found) foundCount++;
var row = new VisualElement();
row.AddToClassList("status-row");
var dot = new VisualElement();
dot.AddToClassList("status-dot");
dot.AddToClassList(found ? "status-dot--found" : "status-dot--missing");
row.Add(dot);
var nameLabel = new Label(def.label);
nameLabel.AddToClassList("status-name");
row.Add(nameLabel);
var locLabel = new Label(found ? GetParentInfo(comp.transform) : "미발견");
locLabel.AddToClassList("status-location");
row.Add(locLabel);
if (found)
{
var selectBtn = new Button(() =>
{
Selection.activeGameObject = comp.gameObject;
EditorGUIUtility.PingObject(comp.gameObject);
}) { text = "선택" };
selectBtn.AddToClassList("status-select-btn");
row.Add(selectBtn);
}
statusList.Add(row);
}
var summary = new VisualElement();
summary.AddToClassList("summary-line");
summary.style.marginTop = 4;
summary.style.paddingTop = 4;
summary.style.borderTopWidth = 1;
summary.style.borderTopColor = new Color(1, 1, 1, 0.04f);
var countLabel = new Label($"{foundCount}/{controllerDefs.Length}");
countLabel.AddToClassList("summary-count");
summary.Add(countLabel);
var summaryText = new Label("개 발견됨");
summaryText.AddToClassList("summary-label");
summary.Add(summaryText);
statusList.Add(summary);
}
private string GetParentInfo(Transform t)
{
return t.parent == null ? "(루트)" : $"({t.parent.name})";
}
#endregion
#region Create Toggles
private void BuildCreateToggles()
{
if (createTogglesContainer == null) return;
for (int i = 0; i < controllerDefs.Length; i++)
{
var row = new VisualElement();
row.AddToClassList("controller-toggle-row");
var toggle = new Toggle(controllerDefs[i].label) { value = true };
createToggles[i] = toggle;
row.Add(toggle);
var badge = new Label("존재");
badge.AddToClassList("controller-exists-badge");
badge.style.display = DisplayStyle.None;
badge.name = $"badge-{i}";
row.Add(badge);
createTogglesContainer.Add(row);
}
}
private void UpdateCreateToggles()
{
for (int i = 0; i < controllerDefs.Length; i++)
{
bool exists = existingComponents[i] != null;
var toggle = createToggles[i];
if (toggle == null) continue;
toggle.SetEnabled(!exists);
if (exists) toggle.value = false;
var badge = createTogglesContainer.Q<Label>($"badge-{i}");
if (badge != null)
badge.style.display = exists ? DisplayStyle.Flex : DisplayStyle.None;
}
}
private void UpdateButtonStates()
{
bool hasStreamDeck = existingComponents[0] != null;
if (btnConnect != null) btnConnect.SetEnabled(hasStreamDeck);
bool anyMissing = false;
for (int i = 0; i < existingComponents.Length; i++)
{
if (existingComponents[i] == null) { anyMissing = true; break; }
}
if (btnOneClick != null) btnOneClick.SetEnabled(anyMissing || !hasStreamDeck);
}
#endregion
#region Actions
private void OneClickSetup()
{
logBuilder.Clear();
Log("=== 원클릭 설정 시작 ===");
FindExistingControllers();
for (int i = 0; i < controllerDefs.Length; i++)
{
if (existingComponents[i] == null)
createToggles[i].value = true;
}
CreateControllers();
ConnectExistingControllers();
if (toggleMoveExisting != null && toggleMoveExisting.value)
MoveExistingControllersToParent();
Log("=== 설정 완료 ===");
FlushLog();
RefreshAll();
}
private void CreateControllers()
{
logBuilder.Clear();
GameObject parent = null;
bool useParent = toggleCreateParent?.value ?? true;
string parentName = fieldParentName?.value ?? "Streamingle 컨트롤러들";
if (useParent)
{
parent = GameObject.Find(parentName);
if (parent == null)
{
parent = new GameObject(parentName);
Undo.RegisterCreatedObjectUndo(parent, "Create Streamingle Parent");
Log($"+ 부모 오브젝트 생성: {parentName}");
}
}
int created = 0;
for (int i = 0; i < controllerDefs.Length; i++)
{
if (existingComponents[i] != null) continue;
if (createToggles[i] == null || !createToggles[i].value) continue;
var def = controllerDefs[i];
var go = new GameObject(def.gameObjectName);
Undo.RegisterCreatedObjectUndo(go, $"Create {def.label}");
if (parent != null)
go.transform.SetParent(parent.transform);
go.AddComponent(def.componentType);
existingComponents[i] = go.GetComponent(def.componentType);
Log($"+ 생성: {def.label}");
created++;
}
if (created == 0)
Log("생성할 항목이 없습니다.");
else
Log($"{created}개 컨트롤러 생성됨.");
if (created > 0 && (toggleAutoConnect?.value ?? true))
ConnectExistingControllers();
if (created > 0 && parent != null && (toggleMoveExisting?.value ?? true))
MoveExistingControllersToParent(parent);
FlushLog();
RefreshAll();
if (parent != null)
{
Selection.activeGameObject = parent;
EditorGUIUtility.PingObject(parent);
}
}
private void ConnectExistingControllers()
{
FindExistingControllers();
var manager = existingComponents[0] as StreamDeckServerManager;
if (manager == null)
{
Log("! StreamDeck 서버 매니저를 찾을 수 없습니다.");
FlushLog();
return;
}
var so = new SerializedObject(manager);
so.Update();
int connected = 0;
for (int i = 1; i < controllerDefs.Length; i++)
{
var def = controllerDefs[i];
var comp = existingComponents[i];
if (comp == null || def.propertyName == null) continue;
var prop = so.FindProperty(def.propertyName);
if (prop != null && prop.objectReferenceValue != comp)
{
prop.objectReferenceValue = comp;
connected++;
Log($" -> 연결: {def.label}");
}
}
so.ApplyModifiedProperties();
if (connected == 0)
Log("모든 컨트롤러가 이미 연결되어 있습니다.");
else
Log($"{connected}개 컨트롤러를 매니저에 연결했습니다.");
FlushLog();
}
private void MoveExistingControllersToParent(GameObject parent = null)
{
FindExistingControllers();
string parentName = fieldParentName?.value ?? "Streamingle 컨트롤러들";
if (parent == null)
{
parent = GameObject.Find(parentName);
if (parent == null)
{
parent = new GameObject(parentName);
Undo.RegisterCreatedObjectUndo(parent, "Create Streamingle Parent");
Log($"+ 부모 오브젝트 생성: {parentName}");
}
}
int moved = 0;
for (int i = 0; i < controllerDefs.Length; i++)
{
var comp = existingComponents[i];
if (comp == null) continue;
if (comp.transform.parent == parent.transform) continue;
Undo.SetTransformParent(comp.transform, parent.transform, $"Move {controllerDefs[i].label}");
moved++;
Log($" -> 이동: {controllerDefs[i].label}");
}
if (moved == 0)
Log("이동할 컨트롤러가 없습니다.");
else
Log($"{moved}개 컨트롤러를 {parentName} 하위로 이동.");
FlushLog();
RefreshAll();
Selection.activeGameObject = parent;
EditorGUIUtility.PingObject(parent);
}
#endregion
#region Logging
private void Log(string msg)
{
logBuilder.AppendLine(msg);
}
private void FlushLog()
{
if (resultText != null)
resultText.text = logBuilder.ToString().TrimEnd();
Debug.Log($"[Streamingle Setup] {logBuilder}");
}
#endregion
}
}