Skip to content

Commit 0085f31

Browse files
hchokiclaude
andcommitted
Add eye-blink check to the wizard FX Check step
The wizard's FX Check step previously guarded only gesture-driven facial expression transitions. Extend it to also detect and guard eye-blink layers (EyeTrackingActive > 0.5), matching the standalone FX Gesture Checker window. - Surface detected blink layers with confidence badges, per-layer guard toggles, and detection reasons; auto-skip an entry only when it has neither gesture nor blink layers. - Fold blink selection into the existing Apply Fixes flow so gesture and blink guards are applied to the same copied FX controller in one pass; status now reports transition, layer, and blink guard counts. - Pre-expand gesture and blink layers after analysis so results are visible without a click. Introduce FXGestureCheckerUI as the single home for FX result rendering (gesture layers, transitions, blink section, confidence styles), which the wizard step renders through. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d0ee917 commit 0085f31

3 files changed

Lines changed: 393 additions & 135 deletions

File tree

Packages/net.pawlygon.unitytools/Editor/AvatarSetupWizard.cs

Lines changed: 123 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ public class AvatarSetupWizard : EditorWindow
4545
private bool patcherHubImportedThisSession;
4646
private GUIStyle stepStyle;
4747
private GUIStyle currentStepStyle;
48-
private GUIStyle fxLayerHeaderStyle;
49-
private GUIStyle fxGuardedLabelStyle;
5048
private GUIStyle helpBoxPadding10_8;
5149
private GUIStyle helpBoxPadding8_6;
5250
private GUIStyle helpBoxPadding10_6;
@@ -55,6 +53,8 @@ public class AvatarSetupWizard : EditorWindow
5553
private GUIStyle boldLabel13;
5654
private bool fxCheckAnalyzed;
5755
private readonly HashSet<int> fxExpandedLayers = new HashSet<int>();
56+
private readonly HashSet<int> fxExpandedBlinkLayers = new HashSet<int>();
57+
private bool fxShowAllBlinkLayers;
5858

5959
private enum WizardStep
6060
{
@@ -2588,10 +2588,6 @@ private void EnsureStyles()
25882588
? new Color(0.8f, 0.92f, 1f)
25892589
: new Color(0.1f, 0.35f, 0.7f);
25902590

2591-
fxLayerHeaderStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 12 };
2592-
fxGuardedLabelStyle = new GUIStyle(EditorStyles.miniLabel) { fontStyle = FontStyle.Italic };
2593-
fxGuardedLabelStyle.normal.textColor = new Color(0.3f, 0.75f, 0.3f);
2594-
25952591
helpBoxPadding10_8 = new GUIStyle(EditorStyles.helpBox) { padding = new RectOffset(10, 10, 8, 8) };
25962592
helpBoxPadding8_6 = new GUIStyle(EditorStyles.helpBox) { padding = new RectOffset(8, 8, 6, 6) };
25972593
helpBoxPadding10_6 = new GUIStyle(EditorStyles.helpBox) { padding = new RectOffset(10, 10, 6, 6) };
@@ -2675,7 +2671,7 @@ private void DrawFXCheckStep()
26752671
{
26762672
PawlygonEditorUI.DrawSection(
26772673
"FX Gesture Check",
2678-
"Analyze and guard gesture-based facial expression transitions in the FX controller.",
2674+
"Analyze and guard gesture-based facial expression transitions and eye-blink layers in the FX controller.",
26792675
() =>
26802676
{
26812677
// Check VRChat SDK availability
@@ -2715,18 +2711,36 @@ private void DrawFXCheckStep()
27152711
EditorGUILayout.HelpBox(entry.fxAnalysisResult.StatusMessage,
27162712
entry.fxAnalysisResult.StatusMessageType);
27172713
}
2718-
else if (entry.fxAnalysisResult.Layers == null ||
2719-
entry.fxAnalysisResult.Layers.Count == 0)
2720-
{
2721-
EditorGUILayout.HelpBox(
2722-
"No gesture-based facial expression transitions found.",
2723-
MessageType.Info);
2724-
}
27252714
else
27262715
{
2727-
DrawFXResultsForEntry(entry);
2728-
EditorGUILayout.Space(SectionSpacing);
2729-
DrawFXApplySectionForEntry(entry);
2716+
bool hasGestureLayers = entry.fxAnalysisResult.Layers != null &&
2717+
entry.fxAnalysisResult.Layers.Count > 0;
2718+
bool hasBlinkLayers = entry.fxAnalysisResult.BlinkLayers != null &&
2719+
entry.fxAnalysisResult.BlinkLayers.Count > 0;
2720+
2721+
if (!hasGestureLayers && !hasBlinkLayers)
2722+
{
2723+
EditorGUILayout.HelpBox(
2724+
"No gesture-based facial expression transitions or blink layers found.",
2725+
MessageType.Info);
2726+
}
2727+
else
2728+
{
2729+
if (hasGestureLayers)
2730+
{
2731+
DrawFXResultsForEntry(entry);
2732+
}
2733+
2734+
if (hasBlinkLayers)
2735+
{
2736+
EditorGUILayout.Space(SectionSpacing);
2737+
FXGestureCheckerUI.DrawBlinkSection(
2738+
entry.fxAnalysisResult.BlinkLayers, fxExpandedBlinkLayers, ref fxShowAllBlinkLayers);
2739+
}
2740+
2741+
EditorGUILayout.Space(SectionSpacing);
2742+
DrawFXApplySectionForEntry(entry);
2743+
}
27302744
}
27312745

27322746
// Navigation buttons
@@ -2789,10 +2803,27 @@ private void AnalyzeAllEntries()
27892803
if (entry.fxAnalysisResult.FXController != null)
27902804
entry.originalFxControllerPath = AssetDatabase.GetAssetPath(entry.fxAnalysisResult.FXController);
27912805

2792-
// Auto-skip entries with no actionable results
2793-
if (!entry.fxAnalysisResult.Success ||
2794-
entry.fxAnalysisResult.Layers == null ||
2795-
entry.fxAnalysisResult.Layers.Count == 0)
2806+
// Pre-expand gesture layers so their transitions are visible without a click.
2807+
if (entry.fxAnalysisResult.Layers != null)
2808+
{
2809+
foreach (FXGestureCheckerCore.LayerAnalysis l in entry.fxAnalysisResult.Layers)
2810+
fxExpandedLayers.Add(l.LayerIndex);
2811+
}
2812+
2813+
// Sort detected blink layers by confidence (most likely first) and pre-expand them.
2814+
if (entry.fxAnalysisResult.BlinkLayers != null && entry.fxAnalysisResult.BlinkLayers.Count > 0)
2815+
{
2816+
entry.fxAnalysisResult.BlinkLayers.Sort((a, b) => b.ConfidenceScore.CompareTo(a.ConfidenceScore));
2817+
foreach (FXGestureCheckerCore.BlinkLayerAnalysis bl in entry.fxAnalysisResult.BlinkLayers)
2818+
fxExpandedBlinkLayers.Add(bl.LayerIndex);
2819+
}
2820+
2821+
// Auto-skip entries with no actionable results (no gesture layers and no blink layers)
2822+
bool hasGestureLayers = entry.fxAnalysisResult.Layers != null &&
2823+
entry.fxAnalysisResult.Layers.Count > 0;
2824+
bool hasBlinkLayers = entry.fxAnalysisResult.BlinkLayers != null &&
2825+
entry.fxAnalysisResult.BlinkLayers.Count > 0;
2826+
if (!entry.fxAnalysisResult.Success || (!hasGestureLayers && !hasBlinkLayers))
27962827
{
27972828
entry.fxCheckComplete = true;
27982829
}
@@ -2834,110 +2865,29 @@ private void DrawFXResultsForEntry(AvatarEntry entry)
28342865
MessageType.Info);
28352866
EditorGUILayout.Space(4f);
28362867

2837-
// Per-layer foldouts
2838-
for (int i = 0; i < result.Layers.Count; i++)
2839-
{
2840-
DrawFXLayerAnalysis(result.Layers[i]);
2841-
EditorGUILayout.Space(4f);
2842-
}
2843-
}
2844-
2845-
private void DrawFXLayerAnalysis(FXGestureCheckerCore.LayerAnalysis layer)
2846-
{
2847-
using (new EditorGUILayout.VerticalScope(PawlygonEditorUI.SectionStyle))
2848-
{
2849-
bool isExpanded = fxExpandedLayers.Contains(layer.LayerIndex);
2850-
string gestureCount = $"{layer.GestureTransitions.Count} gesture transition{(layer.GestureTransitions.Count != 1 ? "s" : "")}";
2851-
2852-
using (new EditorGUILayout.HorizontalScope())
2853-
{
2854-
bool newExpanded = EditorGUILayout.Foldout(isExpanded, "", true);
2855-
EditorGUILayout.LabelField($"Layer: {layer.LayerName}", fxLayerHeaderStyle);
2856-
GUILayout.FlexibleSpace();
2857-
EditorGUILayout.LabelField($"({gestureCount})", EditorStyles.miniLabel, GUILayout.Width(150f));
2858-
2859-
if (newExpanded != isExpanded)
2860-
{
2861-
if (newExpanded) fxExpandedLayers.Add(layer.LayerIndex);
2862-
else fxExpandedLayers.Remove(layer.LayerIndex);
2863-
}
2864-
}
2865-
2866-
if (!isExpanded) return;
2867-
2868-
EditorGUI.indentLevel++;
2869-
2870-
EditorGUILayout.Space(4f);
2871-
if (layer.AlreadyHasLayerGuard)
2872-
{
2873-
using (new EditorGUILayout.HorizontalScope())
2874-
{
2875-
using (new EditorGUI.DisabledScope(true))
2876-
{
2877-
EditorGUILayout.ToggleLeft("Disable entire layer when FacialExpressionsDisabled", true);
2878-
}
2879-
EditorGUILayout.LabelField("[Applied]", fxGuardedLabelStyle, GUILayout.Width(60f));
2880-
}
2881-
}
2882-
else
2883-
{
2884-
layer.SelectedForLayerDisable = EditorGUILayout.ToggleLeft(
2885-
"Disable entire layer when FacialExpressionsDisabled",
2886-
layer.SelectedForLayerDisable);
2887-
}
2888-
2889-
EditorGUILayout.Space(4f);
2890-
PawlygonEditorUI.DrawSeparator();
2891-
EditorGUILayout.Space(4f);
2892-
2893-
foreach (FXGestureCheckerCore.TransitionAnalysis transition in layer.GestureTransitions)
2894-
{
2895-
DrawFXTransitionRow(transition);
2896-
}
2897-
2898-
EditorGUI.indentLevel--;
2899-
}
2900-
}
2901-
2902-
private void DrawFXTransitionRow(FXGestureCheckerCore.TransitionAnalysis transition)
2903-
{
2904-
string gestureName = FXGestureCheckerCore.GetGestureName(transition.GestureValue);
2905-
string label = $"{transition.SourceName} -> {transition.DestinationName} ({transition.GestureParameter}={gestureName})";
2906-
2907-
using (new EditorGUILayout.HorizontalScope())
2908-
{
2909-
if (transition.HasDisabledGuard)
2910-
{
2911-
using (new EditorGUI.DisabledScope(true))
2912-
{
2913-
EditorGUILayout.ToggleLeft(label, true);
2914-
}
2915-
EditorGUILayout.LabelField("[Applied]", fxGuardedLabelStyle, GUILayout.Width(60f));
2916-
}
2917-
else
2918-
{
2919-
transition.SelectedForFix = EditorGUILayout.ToggleLeft(label, transition.SelectedForFix);
2920-
}
2921-
}
2868+
FXGestureCheckerUI.DrawGestureLayers(result.Layers, fxExpandedLayers);
29222869
}
29232870

29242871
private void DrawFXApplySectionForEntry(AvatarEntry entry)
29252872
{
29262873
FXGestureCheckerCore.AnalysisResult result = entry.fxAnalysisResult;
2927-
List<FXGestureCheckerCore.LayerAnalysis> layers = result.Layers;
2874+
List<FXGestureCheckerCore.LayerAnalysis> layers =
2875+
result.Layers ?? new List<FXGestureCheckerCore.LayerAnalysis>();
2876+
List<FXGestureCheckerCore.BlinkLayerAnalysis> blinkLayers =
2877+
result.BlinkLayers ?? new List<FXGestureCheckerCore.BlinkLayerAnalysis>();
29282878

2929-
bool anySelected = layers.Any(l =>
2930-
l.SelectedForLayerDisable ||
2931-
l.GestureTransitions.Any(t => t.SelectedForFix));
2879+
bool anySelected =
2880+
layers.Any(l => l.SelectedForLayerDisable || l.GestureTransitions.Any(t => t.SelectedForFix)) ||
2881+
blinkLayers.Any(b => b.SelectedForGuard);
29322882

2933-
bool allGuarded = layers.All(l =>
2934-
l.AlreadyHasLayerGuard &&
2935-
l.GestureTransitions.All(t => t.HasDisabledGuard));
2883+
bool allGuarded =
2884+
layers.All(l => l.AlreadyHasLayerGuard && l.GestureTransitions.All(t => t.HasDisabledGuard)) &&
2885+
blinkLayers.All(b => b.AlreadyHasBlinkGuard);
29362886

29372887
if (allGuarded)
29382888
{
29392889
EditorGUILayout.HelpBox(
2940-
"All gesture transitions and layers are already guarded.",
2890+
"All gesture transitions, layers, and blink layers are already guarded.",
29412891
MessageType.Info);
29422892
entry.fxCheckComplete = true;
29432893
return;
@@ -2947,9 +2897,15 @@ private void DrawFXApplySectionForEntry(AvatarEntry entry)
29472897
{
29482898
GUILayout.FlexibleSpace();
29492899
if (GUILayout.Button("Select All Unguarded", GUILayout.Height(26f), GUILayout.Width(160f)))
2900+
{
29502901
FXGestureCheckerCore.SelectAllUnguarded(layers);
2902+
FXGestureCheckerCore.SelectAllUnguardedBlink(blinkLayers);
2903+
}
29512904
if (GUILayout.Button("Deselect All", GUILayout.Height(26f), GUILayout.Width(120f)))
2905+
{
29522906
FXGestureCheckerCore.DeselectAll(layers);
2907+
FXGestureCheckerCore.DeselectAllBlink(blinkLayers);
2908+
}
29532909
}
29542910

29552911
EditorGUILayout.Space(4f);
@@ -2965,7 +2921,7 @@ private void DrawFXApplySectionForEntry(AvatarEntry entry)
29652921
if (!anySelected)
29662922
{
29672923
EditorGUILayout.HelpBox(
2968-
"Select transitions or layers to apply the FacialExpressionsDisabled guard.",
2924+
"Select gesture transitions, layers, or blink layers to apply their guards.",
29692925
MessageType.Info);
29702926
}
29712927
}
@@ -2978,15 +2934,28 @@ private void ApplyFXFixesForEntry(AvatarEntry entry)
29782934
// Capture user selections before re-analysis resets them
29792935
var selectedTransitionKeys = new HashSet<string>();
29802936
var selectedLayerIndices = new HashSet<int>();
2981-
foreach (FXGestureCheckerCore.LayerAnalysis layer in result.Layers)
2937+
if (result.Layers != null)
2938+
{
2939+
foreach (FXGestureCheckerCore.LayerAnalysis layer in result.Layers)
2940+
{
2941+
if (layer.SelectedForLayerDisable)
2942+
selectedLayerIndices.Add(layer.LayerIndex);
2943+
foreach (FXGestureCheckerCore.TransitionAnalysis t in layer.GestureTransitions)
2944+
{
2945+
if (t.SelectedForFix)
2946+
selectedTransitionKeys.Add(
2947+
$"{layer.LayerIndex}:{t.SourceName}->{t.DestinationName}:{t.GestureParameter}");
2948+
}
2949+
}
2950+
}
2951+
2952+
var selectedBlinkIndices = new HashSet<int>();
2953+
if (result.BlinkLayers != null)
29822954
{
2983-
if (layer.SelectedForLayerDisable)
2984-
selectedLayerIndices.Add(layer.LayerIndex);
2985-
foreach (FXGestureCheckerCore.TransitionAnalysis t in layer.GestureTransitions)
2955+
foreach (FXGestureCheckerCore.BlinkLayerAnalysis bl in result.BlinkLayers)
29862956
{
2987-
if (t.SelectedForFix)
2988-
selectedTransitionKeys.Add(
2989-
$"{layer.LayerIndex}:{t.SourceName}->{t.DestinationName}:{t.GestureParameter}");
2957+
if (bl.SelectedForGuard)
2958+
selectedBlinkIndices.Add(bl.LayerIndex);
29902959
}
29912960
}
29922961

@@ -3001,29 +2970,46 @@ private void ApplyFXFixesForEntry(AvatarEntry entry)
30012970
}
30022971
entry.copiedFxControllerPath = AssetDatabase.GetAssetPath(copy);
30032972

3004-
// 2. Re-analyze on the copy so TransitionRef references point to the new asset
2973+
// 2. Re-analyze on the copy so the analysis references point to the new asset
30052974
FXGestureCheckerCore.AnalysisResult copyResult = FXGestureCheckerCore.Analyze(copy);
3006-
if (!copyResult.Success || copyResult.Layers == null)
2975+
if (!copyResult.Success)
30072976
{
30082977
statusMessage = "Failed to analyze copied FX controller.";
30092978
return;
30102979
}
30112980

3012-
// Restore user selections on re-analyzed layers
3013-
foreach (FXGestureCheckerCore.LayerAnalysis layer in copyResult.Layers)
2981+
// Restore user selections on re-analyzed gesture layers
2982+
if (copyResult.Layers != null)
30142983
{
3015-
if (selectedLayerIndices.Contains(layer.LayerIndex))
3016-
layer.SelectedForLayerDisable = true;
3017-
foreach (FXGestureCheckerCore.TransitionAnalysis t in layer.GestureTransitions)
2984+
foreach (FXGestureCheckerCore.LayerAnalysis layer in copyResult.Layers)
30182985
{
3019-
string key = $"{layer.LayerIndex}:{t.SourceName}->{t.DestinationName}:{t.GestureParameter}";
3020-
if (selectedTransitionKeys.Contains(key))
3021-
t.SelectedForFix = true;
2986+
if (selectedLayerIndices.Contains(layer.LayerIndex))
2987+
layer.SelectedForLayerDisable = true;
2988+
foreach (FXGestureCheckerCore.TransitionAnalysis t in layer.GestureTransitions)
2989+
{
2990+
string key = $"{layer.LayerIndex}:{t.SourceName}->{t.DestinationName}:{t.GestureParameter}";
2991+
if (selectedTransitionKeys.Contains(key))
2992+
t.SelectedForFix = true;
2993+
}
30222994
}
30232995
}
30242996

3025-
// 3. Apply fixes on the copy
3026-
var (tFixes, lFixes) = FXGestureCheckerCore.ApplySelectedFixes(copy, copyResult.Layers);
2997+
// Restore user selections on re-analyzed blink layers
2998+
if (copyResult.BlinkLayers != null)
2999+
{
3000+
foreach (FXGestureCheckerCore.BlinkLayerAnalysis bl in copyResult.BlinkLayers)
3001+
{
3002+
if (selectedBlinkIndices.Contains(bl.LayerIndex))
3003+
bl.SelectedForGuard = true;
3004+
}
3005+
}
3006+
3007+
// 3. Apply gesture and blink guards on the copy
3008+
var (tFixes, lFixes) = FXGestureCheckerCore.ApplySelectedFixes(
3009+
copy, copyResult.Layers ?? new List<FXGestureCheckerCore.LayerAnalysis>());
3010+
int bFixes = copyResult.BlinkLayers != null
3011+
? FXGestureCheckerCore.ApplySelectedBlinkGuards(copy, copyResult.BlinkLayers)
3012+
: 0;
30273013

30283014
// 4. Assign copy to descriptor — must reopen prefab to get fresh descriptor
30293015
GameObject prefabRoot = null;
@@ -3062,7 +3048,7 @@ private void ApplyFXFixesForEntry(AvatarEntry entry)
30623048
}
30633049

30643050
entry.fxCheckComplete = true;
3065-
statusMessage = $"Applied {tFixes} transition guard(s) and {lFixes} layer guard(s).";
3051+
statusMessage = $"Applied {tFixes} transition guard(s), {lFixes} layer guard(s), and {bFixes} blink guard(s).";
30663052

30673053
// Propagate to other entries sharing the same original FX controller
30683054
PropagateSharedFXFixes(entry, copy);
@@ -3139,6 +3125,8 @@ private void ResetWizard()
31393125
importLoadAttempts = 0;
31403126
fxCheckAnalyzed = false;
31413127
fxExpandedLayers.Clear();
3128+
fxExpandedBlinkLayers.Clear();
3129+
fxShowAllBlinkLayers = false;
31423130
currentStep = WizardStep.Setup;
31433131
}
31443132
}

0 commit comments

Comments
 (0)