Streamingle_URP/Assets/External/StreamingleFacial/Editor/StreamingleFacialReceiverEditor.cs

194 lines
6.9 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 emaFields;
private VisualElement euroFields;
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");
emaFields = root.Q("emaFields");
euroFields = root.Q("euroFields");
// 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 filterMode for conditional visibility of EMA/Euro fields
var filterModeProp = serializedObject.FindProperty("filterMode");
UpdateFilterModeVisibility(filterModeProp.enumValueIndex);
root.TrackPropertyValue(filterModeProp, prop =>
{
UpdateFilterModeVisibility(prop.enumValueIndex);
});
// 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 UpdateFilterModeVisibility(int modeIndex)
{
// FilterMode: 0=None, 1=EMA, 2=OneEuro
if (emaFields != null)
emaFields.style.display = modeIndex == 1 ? DisplayStyle.Flex : DisplayStyle.None;
if (euroFields != null)
euroFields.style.display = modeIndex == 2 ? 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");
}
}