Streamingle_URP/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.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

189 lines
6.7 KiB
C#

using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using System.Collections.Generic;
[CustomEditor(typeof(StreamingleFacialReceiver))]
public class StreamingleFacialReceiverEditor : Editor
{
private const string UxmlPath = "Assets/External/StreamingleFacial/Editor/UXML/StreamingleFacialReceiverEditor.uxml";
private const string UssPath = "Assets/External/StreamingleFacial/Editor/UXML/StreamingleFacialReceiverEditor.uss";
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private StreamingleFacialReceiver receiver;
private VisualElement portButtonsContainer;
private Label activePortValue;
private VisualElement statusContainer;
private VisualElement filteringFields;
public override VisualElement CreateInspectorGUI()
{
receiver = (StreamingleFacialReceiver)target;
var root = new VisualElement();
// Load stylesheets
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
if (commonUss != null) root.styleSheets.Add(commonUss);
var uss = AssetDatabase.LoadAssetAtPath<StyleSheet>(UssPath);
if (uss != null) root.styleSheets.Add(uss);
// Load UXML
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
if (uxml != null) uxml.CloneTree(root);
// Cache references
statusContainer = root.Q("statusContainer");
activePortValue = root.Q<Label>("activePortValue");
portButtonsContainer = root.Q("portButtonsContainer");
filteringFields = root.Q("filteringFields");
// Auto-find button
var autoFindBtn = root.Q<Button>("autoFindBtn");
var autoFindResult = root.Q<Label>("autoFindResult");
if (autoFindBtn != null)
autoFindBtn.clicked += () => AutoFindARKitMeshes(autoFindResult);
// Build dynamic port buttons
RebuildPortButtons();
// Track enableFiltering for conditional visibility
var enableFilteringProp = serializedObject.FindProperty("enableFiltering");
UpdateFilteringVisibility(enableFilteringProp.boolValue);
root.TrackPropertyValue(enableFilteringProp, prop =>
{
UpdateFilteringVisibility(prop.boolValue);
});
// Track availablePorts and activePortIndex changes to rebuild port buttons
var portsProp = serializedObject.FindProperty("availablePorts");
root.TrackPropertyValue(portsProp, _ => RebuildPortButtons());
var activeIndexProp = serializedObject.FindProperty("activePortIndex");
root.TrackPropertyValue(activeIndexProp, _ => RebuildPortButtons());
// Play mode status polling
root.schedule.Execute(UpdatePlayModeState).Every(200);
return root;
}
private void UpdateFilteringVisibility(bool enabled)
{
if (filteringFields == null) return;
filteringFields.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
}
private void RebuildPortButtons()
{
if (portButtonsContainer == null || receiver == null) return;
portButtonsContainer.Clear();
// Update active port label
if (activePortValue != null)
activePortValue.text = receiver.LOCAL_PORT.ToString();
if (receiver.availablePorts == null || receiver.availablePorts.Length == 0) return;
for (int i = 0; i < receiver.availablePorts.Length; i++)
{
int idx = i;
var btn = new Button(() => OnPortButtonClicked(idx))
{
text = receiver.availablePorts[i].ToString()
};
btn.AddToClassList("facial-port-btn");
if (i == receiver.activePortIndex)
btn.AddToClassList("facial-port-btn--active");
portButtonsContainer.Add(btn);
}
}
private void OnPortButtonClicked(int index)
{
if (Application.isPlaying)
{
receiver.SwitchToPort(index);
}
else
{
Undo.RecordObject(target, "Switch Port");
receiver.activePortIndex = index;
EditorUtility.SetDirty(target);
}
serializedObject.Update();
RebuildPortButtons();
}
// ARKit 블렌드셰이프 이름 (감지용 최소 세트)
private static readonly HashSet<string> ARKitNames = new HashSet<string>(System.StringComparer.OrdinalIgnoreCase)
{
"EyeBlinkLeft", "EyeBlinkRight", "JawOpen", "MouthClose",
"MouthSmileLeft", "MouthSmileRight", "BrowDownLeft", "BrowDownRight",
"EyeWideLeft", "EyeWideRight", "MouthFunnel", "MouthPucker",
"CheekPuff", "TongueOut", "NoseSneerLeft", "NoseSneerRight",
// _L/_R 변형
"eyeBlink_L", "eyeBlink_R", "jawOpen", "mouthClose",
"mouthSmile_L", "mouthSmile_R", "browDown_L", "browDown_R",
};
private const int MinARKitMatchCount = 5;
private void AutoFindARKitMeshes(Label resultLabel)
{
if (receiver == null) return;
var allSMRs = receiver.GetComponentsInChildren<SkinnedMeshRenderer>(true);
var found = new List<SkinnedMeshRenderer>();
foreach (var smr in allSMRs)
{
if (smr == null || smr.sharedMesh == null) continue;
int matchCount = 0;
for (int i = 0; i < smr.sharedMesh.blendShapeCount; i++)
{
string name = smr.sharedMesh.GetBlendShapeName(i);
if (ARKitNames.Contains(name))
{
matchCount++;
if (matchCount >= MinARKitMatchCount) break;
}
}
if (matchCount >= MinARKitMatchCount)
found.Add(smr);
}
if (found.Count == 0)
{
if (resultLabel != null)
resultLabel.text = "ARKit 블렌드셰이프를 가진 메쉬를 찾지 못했습니다.";
return;
}
Undo.RecordObject(target, "Auto Find ARKit Meshes");
receiver.faceMeshRenderers = found.ToArray();
EditorUtility.SetDirty(target);
serializedObject.Update();
if (resultLabel != null)
resultLabel.text = $"{found.Count}개 메쉬 등록 완료";
}
private void UpdatePlayModeState()
{
if (statusContainer == null) return;
bool isPlaying = Application.isPlaying;
if (isPlaying && !statusContainer.ClassListContains("facial-status-container--visible"))
statusContainer.AddToClassList("facial-status-container--visible");
else if (!isPlaying && statusContainer.ClassListContains("facial-status-container--visible"))
statusContainer.RemoveFromClassList("facial-status-container--visible");
}
}