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

364 lines
13 KiB
C#

using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using Unity.Cinemachine;
[CustomEditor(typeof(CameraManager))]
public class CameraManagerEditor : Editor
{
private const string UxmlPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/CameraManagerEditor.uxml";
private const string UssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/CameraManagerEditor.uss";
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private VisualElement presetsContainer;
private Label presetsTitleLabel;
private CameraManager manager;
public override VisualElement CreateInspectorGUI()
{
manager = (CameraManager)target;
var root = new VisualElement();
// Stylesheets
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);
// UXML template
var uxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UxmlPath);
if (uxml != null) uxml.CloneTree(root);
// Conditional UI logic
SetupZoomValidation(root);
SetupFOVValidation(root);
SetupRotationTargetLogic(root);
SetupBlendTransitionLogic(root);
// Preset list (dynamic)
BuildPresetsSection(root);
// Play mode active state polling
root.schedule.Execute(UpdatePlayModeState).Every(200);
// Undo/redo
Undo.undoRedoPerformed += OnUndoRedo;
root.RegisterCallback<DetachFromPanelEvent>(_ => Undo.undoRedoPerformed -= OnUndoRedo);
return root;
}
private void OnUndoRedo()
{
if (manager == null) return;
RebuildPresetList();
}
#region Validation
private void SetupZoomValidation(VisualElement root)
{
var warning = root.Q<HelpBox>("zoomWarning");
if (warning == null) return;
var minProp = serializedObject.FindProperty("minZoomDistance");
var maxProp = serializedObject.FindProperty("maxZoomDistance");
void UpdateVisibility(SerializedProperty _)
{
serializedObject.Update();
bool show = serializedObject.FindProperty("minZoomDistance").floatValue >=
serializedObject.FindProperty("maxZoomDistance").floatValue;
warning.style.display = show ? DisplayStyle.Flex : DisplayStyle.None;
}
root.TrackPropertyValue(minProp, UpdateVisibility);
root.TrackPropertyValue(maxProp, UpdateVisibility);
UpdateVisibility(null);
}
private void SetupFOVValidation(VisualElement root)
{
var warning = root.Q<HelpBox>("fovWarning");
if (warning == null) return;
var minProp = serializedObject.FindProperty("minFOV");
var maxProp = serializedObject.FindProperty("maxFOV");
void UpdateVisibility(SerializedProperty _)
{
serializedObject.Update();
bool show = serializedObject.FindProperty("minFOV").floatValue >=
serializedObject.FindProperty("maxFOV").floatValue;
warning.style.display = show ? DisplayStyle.Flex : DisplayStyle.None;
}
root.TrackPropertyValue(minProp, UpdateVisibility);
root.TrackPropertyValue(maxProp, UpdateVisibility);
UpdateVisibility(null);
}
private void SetupRotationTargetLogic(VisualElement root)
{
var targetField = root.Q<PropertyField>("manualRotationTargetField");
var info = root.Q<HelpBox>("rotationTargetInfo");
if (targetField == null || info == null) return;
var useHeadProp = serializedObject.FindProperty("useAvatarHeadAsTarget");
void UpdateState(SerializedProperty _)
{
serializedObject.Update();
bool useHead = serializedObject.FindProperty("useAvatarHeadAsTarget").boolValue;
targetField.SetEnabled(!useHead);
info.style.display = useHead ? DisplayStyle.Flex : DisplayStyle.None;
}
root.TrackPropertyValue(useHeadProp, UpdateState);
UpdateState(null);
}
private void SetupBlendTransitionLogic(VisualElement root)
{
var settingsGroup = root.Q("blendSettingsGroup");
var modeInfo = root.Q<HelpBox>("blendModeInfo");
var rendererWarning = root.Q<HelpBox>("blendRendererWarning");
if (settingsGroup == null || modeInfo == null || rendererWarning == null) return;
var useBlendProp = serializedObject.FindProperty("useBlendTransition");
var useRealtimeProp = serializedObject.FindProperty("useRealtimeBlend");
void UpdateState(SerializedProperty _)
{
serializedObject.Update();
bool useBlend = serializedObject.FindProperty("useBlendTransition").boolValue;
bool useRealtime = serializedObject.FindProperty("useRealtimeBlend").boolValue;
settingsGroup.SetEnabled(useBlend);
rendererWarning.style.display = useBlend ? DisplayStyle.Flex : DisplayStyle.None;
if (useBlend)
{
modeInfo.style.display = DisplayStyle.Flex;
modeInfo.text = useRealtime
? "실시간 모드: 블렌딩 중 두 카메라 모두 실시간으로 렌더링됩니다.\n성능 비용이 증가하지만 이전 카메라도 움직입니다."
: "스냅샷 모드: 이전 화면을 캡처한 후 블렌딩합니다.\n성능 효율적이지만 이전 화면이 정지합니다.";
}
else
{
modeInfo.style.display = DisplayStyle.None;
}
}
root.TrackPropertyValue(useBlendProp, UpdateState);
root.TrackPropertyValue(useRealtimeProp, UpdateState);
UpdateState(null);
}
#endregion
#region Preset List
private void BuildPresetsSection(VisualElement root)
{
var section = root.Q("presetsSection");
if (section == null) return;
// Header
var header = new VisualElement();
header.AddToClassList("presets-header");
presetsTitleLabel = new Label($"Camera Presets ({manager.cameraPresets.Count})");
presetsTitleLabel.AddToClassList("presets-title");
header.Add(presetsTitleLabel);
var addBtn = new Button(AddPreset) { text = "+ 프리셋 추가" };
addBtn.AddToClassList("preset-add-btn");
header.Add(addBtn);
section.Add(header);
// Container
presetsContainer = new VisualElement();
section.Add(presetsContainer);
RebuildPresetList();
}
private void RebuildPresetList()
{
if (presetsContainer == null || manager == null) return;
presetsContainer.Clear();
if (presetsTitleLabel != null)
presetsTitleLabel.text = $"Camera Presets ({manager.cameraPresets.Count})";
if (manager.cameraPresets.Count == 0)
{
var empty = new Label("카메라 프리셋이 없습니다. '+ 프리셋 추가' 버튼을 눌러 추가하세요.");
empty.AddToClassList("preset-empty");
presetsContainer.Add(empty);
return;
}
for (int i = 0; i < manager.cameraPresets.Count; i++)
{
presetsContainer.Add(CreatePresetItem(i));
}
}
private VisualElement CreatePresetItem(int index)
{
var preset = manager.cameraPresets[index];
bool isActive = Application.isPlaying && manager.CurrentPreset == preset;
var item = new VisualElement();
item.AddToClassList("preset-item");
if (isActive) item.AddToClassList("preset-item--active");
// --- Header row ---
var headerRow = new VisualElement();
headerRow.AddToClassList("preset-item-header");
var indexLabel = new Label($"{index + 1}");
indexLabel.AddToClassList("preset-index");
headerRow.Add(indexLabel);
var nameField = new TextField();
nameField.value = preset.presetName;
nameField.AddToClassList("preset-name-field");
int idx = index;
nameField.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(target, "Rename Camera Preset");
manager.cameraPresets[idx].presetName = evt.newValue;
EditorUtility.SetDirty(target);
});
headerRow.Add(nameField);
if (isActive)
{
var activeLabel = new Label("[Active]");
activeLabel.AddToClassList("preset-active-label");
headerRow.Add(activeLabel);
}
// Up button
var upBtn = new Button(() => SwapPresets(index, index - 1)) { text = "\u25B2" };
upBtn.AddToClassList("preset-reorder-btn");
upBtn.SetEnabled(index > 0);
headerRow.Add(upBtn);
// Down button
var downBtn = new Button(() => SwapPresets(index, index + 1)) { text = "\u25BC" };
downBtn.AddToClassList("preset-reorder-btn");
downBtn.SetEnabled(index < manager.cameraPresets.Count - 1);
headerRow.Add(downBtn);
// Delete button
var deleteBtn = new Button(() => DeletePreset(index)) { text = "X" };
deleteBtn.AddToClassList("preset-delete-btn");
headerRow.Add(deleteBtn);
item.Add(headerRow);
// --- Property fields ---
var fields = new VisualElement();
fields.AddToClassList("preset-fields");
var cameraField = new ObjectField("Virtual Camera")
{
objectType = typeof(CinemachineCamera),
allowSceneObjects = true,
value = preset.virtualCamera
};
int ci = index;
cameraField.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(target, "Change Camera Preset Camera");
manager.cameraPresets[ci].virtualCamera = evt.newValue as CinemachineCamera;
EditorUtility.SetDirty(target);
});
fields.Add(cameraField);
var mouseToggle = new Toggle("Allow Mouse Control")
{
tooltip = "이 카메라에서 마우스 조작(회전, 팬, 줌)을 허용할지 여부",
value = preset.allowMouseControl
};
int mi = index;
mouseToggle.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(target, "Toggle Mouse Control");
manager.cameraPresets[mi].allowMouseControl = evt.newValue;
EditorUtility.SetDirty(target);
});
fields.Add(mouseToggle);
item.Add(fields);
return item;
}
private void AddPreset()
{
var newCamera = FindFirstObjectByType<CinemachineCamera>();
if (newCamera != null)
{
Undo.RecordObject(target, "Add Camera Preset");
manager.cameraPresets.Add(new CameraManager.CameraPreset(newCamera));
EditorUtility.SetDirty(target);
RebuildPresetList();
}
else
{
EditorUtility.DisplayDialog("알림", "Scene에 CinemachineCamera가 없습니다.", "확인");
}
}
private void DeletePreset(int index)
{
var preset = manager.cameraPresets[index];
if (EditorUtility.DisplayDialog("프리셋 삭제",
$"프리셋 '{preset.presetName}'을(를) 삭제하시겠습니까?", "삭제", "취소"))
{
Undo.RecordObject(target, "Delete Camera Preset");
manager.cameraPresets.RemoveAt(index);
EditorUtility.SetDirty(target);
RebuildPresetList();
}
}
private void SwapPresets(int a, int b)
{
Undo.RecordObject(target, "Reorder Camera Presets");
(manager.cameraPresets[a], manager.cameraPresets[b]) = (manager.cameraPresets[b], manager.cameraPresets[a]);
EditorUtility.SetDirty(target);
RebuildPresetList();
}
#endregion
#region Play Mode State
private void UpdatePlayModeState()
{
if (presetsContainer == null || manager == null) return;
if (!Application.isPlaying) return;
for (int i = 0; i < presetsContainer.childCount && i < manager.cameraPresets.Count; i++)
{
var item = presetsContainer[i];
bool isActive = manager.CurrentPreset == manager.cameraPresets[i];
if (isActive && !item.ClassListContains("preset-item--active"))
item.AddToClassList("preset-item--active");
else if (!isActive && item.ClassListContains("preset-item--active"))
item.RemoveFromClassList("preset-item--active");
}
}
#endregion
}