user 4a49ecd772 Refactor: 배경/프랍 브라우저 IMGUI→UI Toolkit 전환 + USS 리디자인
- BackgroundSceneLoaderWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField)
- PropBrowserWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField)
- StreamingleCommon.uss: 브라우저 공통 스타일 추가 (그리드/리스트/뷰토글/액션바/상태바)
- excludeFromWeb 상태 새로고침 시 보존 수정
- 삭제된 배경 리소스 정리

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 01:55:48 +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($"카메라 프리셋 ({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 = $"카메라 프리셋 ({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("가상 카메라")
{
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("마우스 조작 허용")
{
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 카메라 프리셋");
(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
}