streamingle-unity-utilities/CameraAI~/Editor/AICameraGeneratorWindow.UI.cs
2026-08-03 04:42:44 +09:00

952 lines
38 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.UIElements;
namespace Streamingle.Editor
{
public sealed partial class AICameraGeneratorWindow
{
private static readonly string[] GeneratorUxmlPaths =
{
"Packages/com.mingle.cw-ai/Editor/UI/AICameraGeneratorWindow.uxml",
"Assets/Scripts/Editor/TimelineTools/UI/AICameraGeneratorWindow.uxml"
};
private static readonly string[] GeneratorUssPaths =
{
"Packages/com.mingle.cw-ai/Editor/UI/AICameraGeneratorWindow.uss",
"Assets/Scripts/Editor/TimelineTools/UI/AICameraGeneratorWindow.uss"
};
private VisualElement _generatorUiRoot;
private ScrollView _mainScroll;
private VisualElement _readinessCard;
private Label _readinessTitle;
private Label _readinessDetail;
private DropdownField _sourceDirectorDropdown;
private HelpBox _sourceSummaryBox;
private DropdownField _cutRhythmDropdown;
private Toggle _freshCandidateToggle;
private HelpBox _freshCandidateWarning;
private Button _generateFullButton;
private VisualElement _refinementCard;
private VisualElement _selectedShotPickerRow;
private HelpBox _selectedShotInfo;
private DropdownField _selectedShotDropdown;
private Foldout _shotOptionsFoldout;
private DropdownField _shotSizeDropdown;
private DropdownField _motionDropdown;
private DropdownField _compositionDropdown;
private Slider _motionIntensitySlider;
private FloatField _distanceField;
private Slider _bodyFollowSlider;
private Button _resetShotOptionsButton;
private Button _regenerateShotButton;
private Button _removeResultButton;
private VisualElement _finalCard;
private Foldout _feedbackFoldout;
private TextField _editorStyleIdField;
private Button _recordCorrectionButton;
private Button _saveFinalSceneButton;
private Foldout _advancedFoldout;
private VisualElement _advancedConfigurationHost;
private IMGUIContainer _advancedConfigurationGui;
private Button _extractDatasetButton;
private Button _reapplyFullButton;
private Button _reapplyShotButton;
private Button _buildCacheButton;
private Button _refreshDiagnosticsButton;
private Button _clearLogButton;
private TextField _logField;
private VisualElement _statusFooter;
private Label _statusMessage;
private Label _generationStageLabel;
private ProgressBar _generationProgress;
private Label _generationTimingLabel;
private HelpBox _appliedSeedBox;
private Button _statusLogButton;
private Button _cancelGenerationButton;
private IVisualElementScheduledItem _generatorUiRefreshItem;
private PlayableDirector[] _uiSourceDirectors =
Array.Empty<PlayableDirector>();
private PlayableDirector _uiRenderedSourceDirector;
private string _uiShotSignature = string.Empty;
private string _uiRenderedLog = string.Empty;
private bool _uiSourceReady;
private bool _uiLibraryReady;
private string _uiLibraryIssue = string.Empty;
private double _nextSourceValidationTime;
private double _nextLibraryValidationTime;
public void CreateGUI()
{
_generatorUiRefreshItem?.Pause();
var root = rootVisualElement;
root.Clear();
try
{
var uxmlPath = ResolveUiAssetPathForTests(false);
var visualTree = string.IsNullOrWhiteSpace(uxmlPath)
? null
: AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath);
if (visualTree == null)
{
throw new FileNotFoundException(
"AI 카메라 생성 UXML을 찾을 수 없습니다.",
uxmlPath);
}
visualTree.CloneTree(root);
var ussPath = ResolveUiAssetPathForTests(true);
var styleSheet = string.IsNullOrWhiteSpace(ussPath)
? null
: AssetDatabase.LoadAssetAtPath<StyleSheet>(ussPath);
if (styleSheet != null && !root.styleSheets.Contains(styleSheet))
{
root.styleSheets.Add(styleSheet);
}
CacheGeneratorUiElements();
BuildAdvancedConfigurationUi();
RegisterGeneratorUiCallbacks();
RefreshGeneratorUi(true);
if (styleSheet == null)
{
_generatorUiRoot.Insert(
0,
new HelpBox(
"스타일 파일을 찾지 못해 기본 Unity 스타일로 표시합니다.",
HelpBoxMessageType.Warning));
}
_generatorUiRefreshItem = root.schedule
.Execute(() => RefreshGeneratorUi(false))
.Every(200);
}
catch (Exception exception)
{
BuildGeneratorUiFallback(exception);
}
}
internal static string ResolveUiAssetPathForTests(bool styleSheet)
{
var candidates = styleSheet
? GeneratorUssPaths
: GeneratorUxmlPaths;
foreach (var candidate in candidates)
{
var asset = styleSheet
? (UnityEngine.Object)AssetDatabase.LoadAssetAtPath<StyleSheet>(
candidate)
: AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(candidate);
if (asset != null)
{
return candidate;
}
}
return string.Empty;
}
private void CacheGeneratorUiElements()
{
_generatorUiRoot = RequireUi<VisualElement>("camera-generator-root");
_mainScroll = RequireUi<ScrollView>("main-scroll");
_readinessCard = RequireUi<VisualElement>("readiness-card");
_readinessTitle = RequireUi<Label>("readiness-title");
_readinessDetail = RequireUi<Label>("readiness-detail");
_sourceDirectorDropdown =
RequireUi<DropdownField>("source-director-dropdown");
_sourceSummaryBox = RequireUi<HelpBox>("source-summary-box");
_cutRhythmDropdown =
RequireUi<DropdownField>("cut-rhythm-dropdown");
_freshCandidateToggle =
RequireUi<Toggle>("fresh-candidate-toggle");
_freshCandidateWarning =
RequireUi<HelpBox>("fresh-candidate-warning");
_generateFullButton = RequireUi<Button>("generate-full-button");
_refinementCard = RequireUi<VisualElement>("refinement-card");
_selectedShotPickerRow =
RequireUi<VisualElement>("selected-shot-picker-row");
_selectedShotInfo = RequireUi<HelpBox>("selected-shot-info");
_selectedShotDropdown =
RequireUi<DropdownField>("selected-shot-dropdown");
_shotOptionsFoldout = RequireUi<Foldout>("shot-options-foldout");
_shotSizeDropdown = RequireUi<DropdownField>("shot-size-dropdown");
_motionDropdown = RequireUi<DropdownField>("motion-dropdown");
_compositionDropdown =
RequireUi<DropdownField>("composition-dropdown");
_motionIntensitySlider =
RequireUi<Slider>("motion-intensity-slider");
_distanceField = RequireUi<FloatField>("distance-field");
_bodyFollowSlider = RequireUi<Slider>("body-follow-slider");
_resetShotOptionsButton =
RequireUi<Button>("reset-shot-options-button");
_regenerateShotButton =
RequireUi<Button>("regenerate-shot-button");
_removeResultButton = RequireUi<Button>("remove-result-button");
_finalCard = RequireUi<VisualElement>("final-card");
_feedbackFoldout = RequireUi<Foldout>("feedback-foldout");
_editorStyleIdField = RequireUi<TextField>("editor-style-id-field");
_recordCorrectionButton =
RequireUi<Button>("record-correction-button");
_saveFinalSceneButton = RequireUi<Button>("save-final-scene-button");
_advancedFoldout = RequireUi<Foldout>("advanced-foldout");
_advancedConfigurationHost =
RequireUi<VisualElement>("advanced-configuration-host");
_extractDatasetButton = RequireUi<Button>("extract-dataset-button");
_reapplyFullButton = RequireUi<Button>("reapply-full-button");
_reapplyShotButton = RequireUi<Button>("reapply-shot-button");
_buildCacheButton = RequireUi<Button>("build-cache-button");
_refreshDiagnosticsButton =
RequireUi<Button>("refresh-diagnostics-button");
_clearLogButton = RequireUi<Button>("clear-log-button");
_logField = RequireUi<TextField>("log-field");
_statusFooter = RequireUi<VisualElement>("status-footer");
_statusMessage = RequireUi<Label>("status-message");
_generationStageLabel =
RequireUi<Label>("generation-stage-label");
_generationProgress = RequireUi<ProgressBar>("generation-progress");
_generationTimingLabel =
RequireUi<Label>("generation-timing-label");
_appliedSeedBox = RequireUi<HelpBox>("applied-seed-box");
_statusLogButton = RequireUi<Button>("status-log-button");
_cancelGenerationButton =
RequireUi<Button>("cancel-generation-button");
}
private T RequireUi<T>(string name) where T : VisualElement
{
var element = rootVisualElement.Q<T>(name);
if (element == null)
{
throw new InvalidDataException(
$"AICameraGeneratorWindow.uxml에 '{name}' 요소가 없습니다.");
}
return element;
}
private void BuildAdvancedConfigurationUi()
{
_advancedConfigurationHost.Clear();
_advancedConfigurationGui = new IMGUIContainer(
DrawAdvancedConfigurationForUi);
_advancedConfigurationGui.AddToClassList("advanced-imgui");
_advancedConfigurationHost.Add(_advancedConfigurationGui);
_logField.multiline = true;
_logField.isReadOnly = true;
}
private void RegisterGeneratorUiCallbacks()
{
_sourceDirectorDropdown.RegisterValueChangedCallback(evt =>
{
var selectedIndex =
_sourceDirectorDropdown.choices.IndexOf(evt.newValue) - 1;
_sourceDirector = selectedIndex >= 0 &&
selectedIndex < _uiSourceDirectors.Length
? _uiSourceDirectors[selectedIndex]
: null;
RefreshShotNames();
RefreshGeneratorUi(true);
});
_cutRhythmDropdown.RegisterValueChangedCallback(evt =>
{
var selectedIndex =
_cutRhythmDropdown.choices.IndexOf(evt.newValue);
if (selectedIndex >= 0)
{
_cutRhythm = (CutRhythmPreference)selectedIndex;
}
});
_freshCandidateToggle.RegisterValueChangedCallback(evt =>
{
_forceFreshCandidatePool = evt.newValue;
RefreshGeneratorUi(false);
});
_generateFullButton.clicked += () =>
{
ExportGenerateAndImport();
RefreshGeneratorUi(true);
};
_selectedShotDropdown.RegisterValueChangedCallback(evt =>
{
var selectedIndex =
_selectedShotDropdown.choices.IndexOf(evt.newValue);
if (selectedIndex >= 0 && selectedIndex < _shotNames.Length)
{
_selectedShotIndex = selectedIndex;
}
});
_shotOptionsFoldout.RegisterValueChangedCallback(evt =>
_showDirectionSettings = evt.newValue);
_shotSizeDropdown.RegisterValueChangedCallback(evt =>
{
var selectedIndex = _shotSizeDropdown.choices.IndexOf(evt.newValue);
if (selectedIndex >= 0)
{
_shotSize = (ShotSizePreference)selectedIndex;
}
});
_motionDropdown.RegisterValueChangedCallback(evt =>
{
var selectedIndex = _motionDropdown.choices.IndexOf(evt.newValue);
if (selectedIndex >= 0)
{
_motion = (MotionPreference)selectedIndex;
}
});
_compositionDropdown.RegisterValueChangedCallback(evt =>
{
var selectedIndex =
_compositionDropdown.choices.IndexOf(evt.newValue);
if (selectedIndex >= 0)
{
_composition = (CompositionPreference)selectedIndex;
}
});
_motionIntensitySlider.RegisterValueChangedCallback(evt =>
_motionIntensity = evt.newValue);
_distanceField.RegisterValueChangedCallback(evt =>
_distanceMeters = Mathf.Max(0f, evt.newValue));
_bodyFollowSlider.RegisterValueChangedCallback(evt =>
_bodyFollowStrength = evt.newValue);
_resetShotOptionsButton.clicked += ResetSelectedShotOptions;
_regenerateShotButton.clicked += () =>
{
StartGeneration(ProcessCompletionAction.ReplaceSelectedShot);
RefreshGeneratorUi(false);
};
_removeResultButton.clicked += () =>
{
RunEditorAction(
"AI 카메라 결과를 제거하는 중...",
delegate
{
var result = RemoveScopedPreview();
RefreshShotNames();
return result;
});
RefreshGeneratorUi(true);
};
_editorStyleIdField.RegisterValueChangedCallback(evt =>
_editorStyleId = evt.newValue);
_recordCorrectionButton.clicked += () =>
{
RunEditorAction(
"컷 수정 기록을 저장하는 중...",
CaptureCutCorrectionSnapshot);
RefreshGeneratorUi(false);
};
_saveFinalSceneButton.clicked += SaveActiveSceneCopy;
_feedbackFoldout.SetValueWithoutNotify(false);
_advancedFoldout.SetValueWithoutNotify(_showAdvancedSettings);
_advancedFoldout.RegisterValueChangedCallback(evt =>
{
_showAdvancedSettings = evt.newValue;
if (evt.newValue)
{
_advancedConfigurationGui?.MarkDirtyRepaint();
RefreshGeneratorUi(true);
}
});
_extractDatasetButton.clicked += ExtractDatasetFromUi;
_reapplyFullButton.clicked += () =>
{
ImportFullResult(_generatedFolder);
RefreshGeneratorUi(true);
};
_reapplyShotButton.clicked += () =>
{
ImportSelectedShot(_generatedFolder, _selectedShotIndex);
RefreshGeneratorUi(true);
};
_buildCacheButton.clicked += () =>
{
StartPreparationCacheBuild();
RefreshGeneratorUi(false);
};
_refreshDiagnosticsButton.clicked += () =>
{
RefreshDatasetSongs(true);
ValidateOrAutoSelectSourceDirector();
RefreshShotNames();
RefreshGeneratorUi(true);
};
_clearLogButton.clicked += () =>
{
_log = string.Empty;
_uiRenderedLog = string.Empty;
_logField.SetValueWithoutNotify("아직 로그가 없습니다.");
};
_statusLogButton.clicked += () =>
{
_showAdvancedSettings = true;
_advancedFoldout.SetValueWithoutNotify(true);
RefreshGeneratorUi(true);
_mainScroll.schedule.Execute(() =>
_mainScroll.ScrollTo(_advancedFoldout));
};
_cancelGenerationButton.clicked += CancelGeneration;
_generatorUiRoot.RegisterCallback<GeometryChangedEvent>(evt =>
SetClassState(
_generatorUiRoot,
"narrow-layout",
evt.newRect.width > 0f && evt.newRect.width < 680f));
}
private void ResetSelectedShotOptions()
{
_shotSize = ShotSizePreference.Auto;
_motion = MotionPreference.Auto;
_composition = CompositionPreference.Auto;
_motionIntensity = 1f;
_distanceMeters = 0f;
_bodyFollowStrength = 0.18f;
RefreshGeneratorUi(false);
}
private void ExtractDatasetFromUi()
{
RunEditorAction(
"Timeline 데이터를 추출하는 중...",
delegate
{
var output = TimelineCameraDatasetExporter
.ExportAll60FpsForCli();
ConfigureExportedDataset(output, _songId.Trim());
return $"추출 완료: {output}\n" +
$"곡 {_datasetSongNames.Length:N0}개를 찾았습니다.";
});
RefreshGeneratorUi(true);
}
private void DrawAdvancedConfigurationForUi()
{
EditorGUILayout.LabelField(
"경로와 생성 세부 설정",
EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"일반 생성에는 변경할 필요가 없습니다. Worker 경로, 입력 ID, " +
"Seed와 키 단순화를 직접 조정할 때만 사용하세요.",
MessageType.Info);
using (new EditorGUI.DisabledScope(IsProcessRunning))
{
DrawPathSettings();
EditorGUILayout.Space(10f);
DrawTargetSettings();
}
}
private void RefreshGeneratorUi(bool refreshLists)
{
if (_generatorUiRoot == null || _generatorUiRoot.panel == null)
{
return;
}
if (refreshLists || _uiRenderedSourceDirector != _sourceDirector)
{
RefreshSourceDirectorDropdown();
_nextSourceValidationTime = 0d;
}
var shotSignature = string.Join("\u001f", _shotNames);
if (refreshLists || !string.Equals(
shotSignature,
_uiShotSignature,
StringComparison.Ordinal))
{
RefreshShotDropdown();
_uiShotSignature = shotSignature;
}
RefreshSourceSummary(refreshLists);
RefreshGenerationLibraryStatus(refreshLists);
RefreshReadiness();
RefreshGenerationControls();
RefreshRefinementControls();
RefreshStatusFooter();
RefreshAdvancedControls();
}
private void RefreshSourceDirectorDropdown()
{
_uiSourceDirectors = GetAvailableSourceDirectors();
if (_sourceDirector != null &&
!_uiSourceDirectors.Contains(_sourceDirector))
{
_sourceDirector = null;
}
if (_sourceDirector == null && _uiSourceDirectors.Length == 1)
{
_sourceDirector = _uiSourceDirectors[0];
RefreshShotNames();
}
var choices = new List<string>();
if (_uiSourceDirectors.Length == 0)
{
choices.Add("현재 씬에 사용할 Timeline이 없습니다");
}
else
{
choices.Add("Timeline을 선택하세요");
choices.AddRange(_uiSourceDirectors.Select(GetSourceDirectorLabel));
}
_sourceDirectorDropdown.choices = choices;
var currentIndex = Array.IndexOf(_uiSourceDirectors, _sourceDirector);
var choiceIndex = _uiSourceDirectors.Length == 0
? 0
: Mathf.Clamp(currentIndex + 1, 0, choices.Count - 1);
_sourceDirectorDropdown.SetValueWithoutNotify(choices[choiceIndex]);
_uiRenderedSourceDirector = _sourceDirector;
}
private void RefreshSourceSummary(bool force)
{
var now = EditorApplication.timeSinceStartup;
if (!force && now < _nextSourceValidationTime)
{
return;
}
_nextSourceValidationTime = now + 1d;
if (_sourceDirector == null)
{
_uiSourceReady = false;
_sourceSummaryBox.text =
"모션과 음원이 들어 있는 원본 Timeline을 선택하세요.";
_sourceSummaryBox.messageType = HelpBoxMessageType.Warning;
return;
}
try
{
var summary = TimelineCameraDatasetExporter
.GetGenerationInputSummaryForEditor(_sourceDirector);
_uiSourceReady = summary.isValid;
if (!summary.isValid)
{
_sourceSummaryBox.text = string.IsNullOrWhiteSpace(summary.error)
? "캐릭터 모션과 음원을 확인할 수 없습니다."
: summary.error;
_sourceSummaryBox.messageType = HelpBoxMessageType.Error;
return;
}
_sourceSummaryBox.text =
$"캐릭터 {summary.characterName}\n" +
$"음원 {summary.audioClipName} · " +
$"{summary.duration:F1}초";
_sourceSummaryBox.messageType = HelpBoxMessageType.Info;
}
catch (Exception exception)
{
_uiSourceReady = false;
_sourceSummaryBox.text = exception.Message;
_sourceSummaryBox.messageType = HelpBoxMessageType.Error;
}
}
private void RefreshReadiness()
{
var running = IsProcessRunning;
var ready = _uiSourceReady && _uiLibraryReady;
SetClassState(_readinessCard, "readiness--ready", !running && ready);
SetClassState(_readinessCard, "readiness--running", running);
SetClassState(
_readinessCard,
"readiness--warning",
!running && !ready);
if (running)
{
_readinessTitle.text = "카메라를 생성하고 있습니다";
_readinessDetail.text =
"현재 단계와 예상 남은 시간은 아래 상태 영역에서 확인할 수 있습니다.";
}
else if (!_uiLibraryReady)
{
_readinessTitle.text = "생성 도구 설정이 필요합니다";
_readinessDetail.text = _uiLibraryIssue;
}
else if (_uiSourceReady)
{
_readinessTitle.text = "Timeline 입력 확인 완료";
_readinessDetail.text =
"모션과 음원이 확인되었습니다. 전체 생성 버튼을 눌러 시작하세요.";
}
else if (_sourceDirector == null)
{
_readinessTitle.text = "Timeline 선택이 필요합니다";
_readinessDetail.text =
"현재 씬에서 모션과 음원이 있는 Timeline을 선택하세요.";
}
else
{
_readinessTitle.text = "입력 구성을 확인해주세요";
_readinessDetail.text =
"선택한 Timeline에서 캐릭터 모션 또는 음원을 찾지 못했습니다.";
}
}
private void RefreshGenerationLibraryStatus(bool force)
{
var now = EditorApplication.timeSinceStartup;
if (!force && now < _nextLibraryValidationTime)
{
return;
}
_nextLibraryValidationTime = now + 1d;
var repaired = TryAutoRepairGenerationLibraryRoot();
var root = string.IsNullOrWhiteSpace(_cwAiRoot)
? string.Empty
: Path.GetFullPath(_cwAiRoot);
if (string.IsNullOrWhiteSpace(root) || !Directory.Exists(root))
{
_uiLibraryReady = false;
_uiLibraryIssue =
"참조 데이터 폴더를 찾지 못했습니다. 고급·진단에서 경로를 확인하세요.";
return;
}
if (!Directory.Exists(Path.Combine(root, "DatasetExports")) ||
!File.Exists(Path.Combine(
root,
"reports",
"training_index.json")) ||
!File.Exists(Path.Combine(
root,
"models",
"cut_ranker_v2.json")))
{
_uiLibraryReady = false;
_uiLibraryIssue =
"참조 데이터가 완전하지 않습니다. 고급·진단에서 CW-AI 루트를 확인하세요.";
return;
}
if (string.IsNullOrWhiteSpace(_cliExecutable) ||
!File.Exists(_cliExecutable))
{
_cliExecutable = ResolveCliExecutable(true);
}
if (string.IsNullOrWhiteSpace(_cliExecutable) ||
!File.Exists(_cliExecutable))
{
_uiLibraryReady = false;
_uiLibraryIssue =
"CWCameraWorker를 찾지 못했습니다. 고급·진단에서 실행 파일을 확인하세요.";
return;
}
_uiLibraryReady = true;
_uiLibraryIssue = string.Empty;
if (repaired &&
!string.IsNullOrWhiteSpace(_status) &&
_status.IndexOf(
"training_index",
StringComparison.OrdinalIgnoreCase) >= 0)
{
_status =
"참조 데이터 경로를 자동 복구했습니다. 전체 생성을 다시 실행하세요.";
}
}
private void RefreshGenerationControls()
{
var running = IsProcessRunning;
_sourceDirectorDropdown.SetEnabled(
!running && _uiSourceDirectors.Length > 0);
_cutRhythmDropdown.choices =
new List<string>(SimpleCutRhythmNames);
_cutRhythmDropdown.SetValueWithoutNotify(
SimpleCutRhythmNames[Mathf.Clamp(
(int)_cutRhythm,
0,
SimpleCutRhythmNames.Length - 1)]);
_cutRhythmDropdown.SetEnabled(!running);
_freshCandidateToggle.SetValueWithoutNotify(_forceFreshCandidatePool);
_freshCandidateToggle.SetEnabled(!running);
SetDisplayed(_freshCandidateWarning, _forceFreshCandidatePool);
_generateFullButton.text = running
? "카메라 생성 중..."
: FullAutonomousGenerationButtonLabel;
_generateFullButton.SetEnabled(
!running && _uiSourceReady && _uiLibraryReady);
}
private void RefreshShotDropdown()
{
var choices = _shotNames.Length > 0
? new List<string>(_shotNames)
: new List<string> { "가져온 AI 카메라가 없습니다" };
_selectedShotDropdown.choices = choices;
_selectedShotIndex = _shotNames.Length == 0
? 0
: Mathf.Clamp(_selectedShotIndex, 0, _shotNames.Length - 1);
_selectedShotDropdown.SetValueWithoutNotify(
choices[Mathf.Clamp(_selectedShotIndex, 0, choices.Count - 1)]);
}
private void RefreshRefinementControls()
{
var hasShots = _shotNames.Length > 0;
var running = IsProcessRunning;
_editorStyleIdField.SetValueWithoutNotify(_editorStyleId);
_recordCorrectionButton.SetEnabled(
hasShots &&
!running &&
!string.IsNullOrWhiteSpace(_editorStyleId) &&
_sourceDirector != null);
_saveFinalSceneButton.SetEnabled(hasShots && !running);
SetDisplayed(_refinementCard, hasShots);
SetDisplayed(_finalCard, hasShots);
SetDisplayed(_feedbackFoldout, hasShots);
if (!hasShots)
{
return;
}
var selectedFromTimeline =
TrySyncSelectedShotFromEditor(out var selectedCameraName);
_selectedShotIndex = Mathf.Clamp(
_selectedShotIndex,
0,
_shotNames.Length - 1);
_selectedShotDropdown.SetValueWithoutNotify(
_shotNames[_selectedShotIndex]);
SetDisplayed(_selectedShotPickerRow, !selectedFromTimeline);
_selectedShotInfo.text = selectedFromTimeline
? $"Timeline/Hierarchy 선택 {selectedCameraName} · " +
$"카메라 {_selectedShotIndex:D3}"
: "Timeline의 AI 카메라를 선택하면 자동으로 연결됩니다. " +
"아래 목록에서 직접 선택할 수도 있습니다.";
_selectedShotInfo.messageType = selectedFromTimeline
? HelpBoxMessageType.Info
: HelpBoxMessageType.None;
_shotOptionsFoldout.SetValueWithoutNotify(_showDirectionSettings);
_shotOptionsFoldout.SetEnabled(!running);
_shotSizeDropdown.choices = new List<string>(SimpleShotSizeNames);
_shotSizeDropdown.SetValueWithoutNotify(
SimpleShotSizeNames[Mathf.Clamp(
(int)_shotSize,
0,
SimpleShotSizeNames.Length - 1)]);
_motionDropdown.choices = new List<string>(SimpleMotionNames);
_motionDropdown.SetValueWithoutNotify(
SimpleMotionNames[Mathf.Clamp(
(int)_motion,
0,
SimpleMotionNames.Length - 1)]);
_compositionDropdown.choices =
new List<string>(SimpleCompositionNames);
_compositionDropdown.SetValueWithoutNotify(
SimpleCompositionNames[Mathf.Clamp(
(int)_composition,
0,
SimpleCompositionNames.Length - 1)]);
_motionIntensitySlider.SetValueWithoutNotify(_motionIntensity);
_distanceField.SetValueWithoutNotify(_distanceMeters);
_bodyFollowSlider.SetValueWithoutNotify(_bodyFollowStrength);
_regenerateShotButton.text = selectedFromTimeline
? "선택한 카메라 재생성"
: "목록의 카메라 재생성";
_regenerateShotButton.tooltip = selectedFromTimeline
? SelectedCameraRegenerationButtonLabel
: ListedCameraRegenerationButtonLabel;
_regenerateShotButton.SetEnabled(!running);
_removeResultButton.SetEnabled(!running);
}
private void RefreshStatusFooter()
{
var running = IsProcessRunning;
var status = string.IsNullOrWhiteSpace(_status)
? "준비됨"
: _status.Trim();
_statusMessage.text = status;
var isError = status.IndexOf(
"실패",
StringComparison.OrdinalIgnoreCase) >= 0 ||
status.IndexOf(
"오류",
StringComparison.OrdinalIgnoreCase) >= 0 ||
status.IndexOf(
"찾을 수 없",
StringComparison.OrdinalIgnoreCase) >= 0;
SetClassState(_statusFooter, "status--running", running);
SetClassState(_statusFooter, "status--error", !running && isError);
SetClassState(_statusFooter, "status--ready", !running && !isError);
SetDisplayed(_statusLogButton, !running && isError);
if (_generationOperationActive)
{
var stageText = _generationStageIndex > 0
? $"{_generationStageIndex}/{_generationStageCount}단계 · " +
_generationStage
: _generationStage;
_generationStageLabel.text = stageText;
SetDisplayed(_generationStageLabel, true);
SetDisplayed(
_generationProgress,
_generationStageProgress >= 0f);
if (_generationStageProgress >= 0f)
{
_generationProgress.value = Mathf.Clamp01(
_generationStageProgress);
_generationProgress.title = stageText;
}
_generationTimingLabel.text = BuildRunningTimingText();
SetDisplayed(_generationTimingLabel, true);
}
else
{
SetDisplayed(_generationStageLabel, false);
SetDisplayed(_generationProgress, false);
var recentDuration = _lastCompletedDurationSeconds > 0d
? _lastCompletedDurationSeconds
: EditorPrefs.GetFloat(
RuntimeHistoryKey("LastSeconds"),
-1f);
_generationTimingLabel.text = recentDuration > 0d
? "최근 생성 " + FormatObservedDuration(recentDuration)
: string.Empty;
SetDisplayed(
_generationTimingLabel,
!isError && recentDuration > 0d);
}
SetDisplayed(_cancelGenerationButton, running);
_cancelGenerationButton.SetEnabled(
running && _cliRunner?.CancellationRequested != true);
SetDisplayed(_appliedSeedBox, _hasLastAppliedSeed);
if (_hasLastAppliedSeed)
{
_appliedSeedBox.text = BuildAppliedSeedSummary(
_lastAppliedSeed,
_seed,
_lastAppliedSeedWasSelectedShot,
_lastAppliedSeedShotIndex);
}
}
private void RefreshAdvancedControls()
{
_advancedFoldout.SetValueWithoutNotify(_showAdvancedSettings);
_extractDatasetButton.SetEnabled(!IsProcessRunning);
_reapplyFullButton.SetEnabled(!IsProcessRunning);
_reapplyShotButton.SetEnabled(
!IsProcessRunning && _shotNames.Length > 0);
_buildCacheButton.SetEnabled(
!IsProcessRunning && _uiLibraryReady);
_refreshDiagnosticsButton.SetEnabled(!IsProcessRunning);
if (_showAdvancedSettings)
{
_advancedConfigurationGui?.MarkDirtyRepaint();
var logText = string.IsNullOrWhiteSpace(_log)
? "아직 로그가 없습니다."
: _log;
if (!string.Equals(
logText,
_uiRenderedLog,
StringComparison.Ordinal))
{
_uiRenderedLog = logText;
_logField.SetValueWithoutNotify(logText);
}
}
}
private void BuildGeneratorUiFallback(Exception exception)
{
var root = rootVisualElement;
root.Clear();
var helpBox = new HelpBox(
"새 UI를 불러오지 못해 호환 화면으로 전환했습니다.\n" +
exception.Message,
HelpBoxMessageType.Error);
helpBox.style.marginLeft = 8f;
helpBox.style.marginRight = 8f;
helpBox.style.marginTop = 8f;
root.Add(helpBox);
var legacyContainer = new IMGUIContainer(DrawLegacyGui);
legacyContainer.style.flexGrow = 1f;
root.Add(legacyContainer);
UnityEngine.Debug.LogException(exception);
}
private void OnSelectionChange()
{
RefreshGeneratorUi(false);
}
private void OnHierarchyChange()
{
if (_generatorUiRoot == null)
{
return;
}
ValidateOrAutoSelectSourceDirector();
RefreshShotNames();
RefreshGeneratorUi(true);
}
private static void SetDisplayed(VisualElement element, bool displayed)
{
if (element != null)
{
element.style.display = displayed
? DisplayStyle.Flex
: DisplayStyle.None;
}
}
private static void SetClassState(
VisualElement element,
string className,
bool enabled)
{
if (enabled)
{
element.AddToClassList(className);
}
else
{
element.RemoveFromClassList(className);
}
}
}
}