952 lines
38 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|