- BackgroundSceneLoaderWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField) - PropBrowserWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField) - StreamingleCommon.uss: 브라우저 공통 스타일 추가 (그리드/리스트/뷰토글/액션바/상태바) - excludeFromWeb 상태 새로고침 시 보존 수정 - 삭제된 배경 리소스 정리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
364 lines
13 KiB
C#
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
|
|
}
|