Streamingle_URP/Assets/Scripts/Editor/TimelineTools/TimelineBindingTransferWindow.cs
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

261 lines
10 KiB
C#

using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace Streamingle.Editor
{
public class TimelineBindingTransferWindow : EditorWindow
{
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private ObjectField sourceField;
private ObjectField targetField;
private Toggle matchByPathToggle;
private Toggle requireTypeToggle;
private Toggle showPreviewToggle;
private VisualElement previewContainer;
private Button applyBtn;
private PlayableDirector sourceDirector;
private PlayableDirector targetDirector;
private struct TrackMap
{
public TrackAsset sourceTrack;
public TrackAsset targetTrack;
public Object boundObject;
}
private List<TrackMap> previewMappings = new List<TrackMap>();
[MenuItem("Tools/Timeline Tools/타임라인 바인딩 복사")]
public static void ShowWindow()
{
GetWindow<TimelineBindingTransferWindow>("타임라인 바인딩 복사");
}
public void CreateGUI()
{
var root = rootVisualElement;
root.AddToClassList("tool-root");
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
if (commonUss != null) root.styleSheets.Add(commonUss);
root.Add(new Label("타임라인 바인딩 복사") { name = "title" });
root.Q<Label>("title").AddToClassList("tool-title");
sourceField = new ObjectField("소스 디렉터") { objectType = typeof(PlayableDirector), allowSceneObjects = true };
sourceField.RegisterValueChangedCallback(evt => sourceDirector = evt.newValue as PlayableDirector);
root.Add(sourceField);
targetField = new ObjectField("타겟 디렉터") { objectType = typeof(PlayableDirector), allowSceneObjects = true };
targetField.RegisterValueChangedCallback(evt => targetDirector = evt.newValue as PlayableDirector);
root.Add(targetField);
matchByPathToggle = new Toggle("트랙 계층 경로로 매칭 (권장)") { value = true };
root.Add(matchByPathToggle);
requireTypeToggle = new Toggle("트랙 타입이 동일할 때만 매칭") { value = true };
root.Add(requireTypeToggle);
showPreviewToggle = new Toggle("미리보기 표시") { value = true };
showPreviewToggle.RegisterValueChangedCallback(evt =>
previewContainer.style.display = evt.newValue ? DisplayStyle.Flex : DisplayStyle.None);
root.Add(showPreviewToggle);
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 8 } };
var previewBtn = new Button(BuildPreviewMapping) { text = "매핑 미리보기 갱신" };
previewBtn.style.flexGrow = 1;
previewBtn.style.marginRight = 4;
btnRow.Add(previewBtn);
applyBtn = new Button(ApplyMappings) { text = "바인딩 복사 실행" };
applyBtn.style.flexGrow = 1;
applyBtn.AddToClassList("btn-primary");
btnRow.Add(applyBtn);
root.Add(btnRow);
// 미리보기 영역
previewContainer = new VisualElement { style = { marginTop = 8 } };
previewContainer.Add(new Label("매핑 미리보기") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } });
var previewScroll = new ScrollView { name = "preview-scroll", style = { minHeight = 150 } };
previewScroll.Add(new HelpBox("미리보기가 비어 있습니다. '매핑 미리보기 갱신'을 눌러 생성하세요.", HelpBoxMessageType.Info));
previewContainer.Add(previewScroll);
root.Add(previewContainer);
root.schedule.Execute(() =>
{
bool canBuild = CanBuildMapping();
previewBtn.SetEnabled(canBuild);
applyBtn.SetEnabled(previewMappings.Count > 0);
}).Every(200);
}
private bool CanBuildMapping()
{
if (sourceDirector == null || targetDirector == null) return false;
var srcAsset = sourceDirector.playableAsset as TimelineAsset;
var dstAsset = targetDirector.playableAsset as TimelineAsset;
return srcAsset != null && dstAsset != null;
}
private void BuildPreviewMapping()
{
previewMappings.Clear();
var srcAsset = sourceDirector.playableAsset as TimelineAsset;
var dstAsset = targetDirector.playableAsset as TimelineAsset;
if (srcAsset == null || dstAsset == null)
{
EditorUtility.DisplayDialog("오류", "소스/타겟 디렉터에 유효한 TimelineAsset이 필요합니다.", "확인");
return;
}
var sourceTracks = GetAllOutputTracks(srcAsset).ToList();
var targetTracks = GetAllOutputTracks(dstAsset).ToList();
Dictionary<string, List<TrackAsset>> nameToTargetTracks = new Dictionary<string, List<TrackAsset>>();
Dictionary<string, TrackAsset> pathToTargetTrack = new Dictionary<string, TrackAsset>();
foreach (var t in targetTracks)
{
string nameKey = t.name;
if (!nameToTargetTracks.TryGetValue(nameKey, out var list))
{
list = new List<TrackAsset>();
nameToTargetTracks[nameKey] = list;
}
list.Add(t);
string pathKey = GetTrackPath(t);
if (!pathToTargetTrack.ContainsKey(pathKey)) pathToTargetTrack.Add(pathKey, t);
}
foreach (var s in sourceTracks)
{
var bound = sourceDirector.GetGenericBinding(s);
if (bound == null) continue;
TrackAsset matched = null;
if (matchByPathToggle.value)
{
pathToTargetTrack.TryGetValue(GetTrackPath(s), out matched);
}
else
{
if (nameToTargetTracks.TryGetValue(s.name, out var candidates))
{
matched = requireTypeToggle.value
? candidates.FirstOrDefault(c => c.GetType() == s.GetType())
: candidates.FirstOrDefault();
}
}
if (matched == null) continue;
if (requireTypeToggle.value && matched.GetType() != s.GetType()) continue;
previewMappings.Add(new TrackMap
{
sourceTrack = s,
targetTrack = matched,
boundObject = bound as Object
});
}
RebuildPreviewUI();
}
private void RebuildPreviewUI()
{
var scroll = rootVisualElement.Q<ScrollView>("preview-scroll");
if (scroll == null) return;
scroll.Clear();
if (previewMappings.Count == 0)
{
scroll.Add(new HelpBox("매칭된 트랙이 없습니다.", HelpBoxMessageType.Warning));
return;
}
foreach (var m in previewMappings)
{
var box = new VisualElement();
box.style.backgroundColor = new Color(0, 0, 0, 0.1f);
box.style.borderTopLeftRadius = box.style.borderTopRightRadius =
box.style.borderBottomLeftRadius = box.style.borderBottomRightRadius = 4;
box.style.paddingTop = box.style.paddingBottom = box.style.paddingLeft = box.style.paddingRight = 4;
box.style.marginBottom = 2;
box.Add(new Label($"소스: {GetTrackDisplay(m.sourceTrack)}") { style = { fontSize = 11 } });
box.Add(new Label($"타겟: {GetTrackDisplay(m.targetTrack)}") { style = { fontSize = 11 } });
var objField = new ObjectField("바인딩 객체") { value = m.boundObject, objectType = typeof(Object) };
objField.SetEnabled(false);
box.Add(objField);
scroll.Add(box);
}
}
private void ApplyMappings()
{
if (targetDirector == null)
{
EditorUtility.DisplayDialog("오류", "타겟 디렉터가 필요합니다.", "확인");
return;
}
int applied = 0;
Undo.RecordObject(targetDirector, "Timeline Binding Transfer");
foreach (var map in previewMappings)
{
if (map.targetTrack == null || map.boundObject == null) continue;
targetDirector.SetGenericBinding(map.targetTrack, map.boundObject);
applied++;
}
EditorUtility.SetDirty(targetDirector);
var go = targetDirector.gameObject;
if (go != null && go.scene.IsValid())
{
UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(go.scene);
}
EditorUtility.DisplayDialog("완료", $"바인딩 복사 완료: {applied}개 적용", "확인");
}
private static IEnumerable<TrackAsset> GetAllOutputTracks(TimelineAsset asset)
{
return asset.GetOutputTracks();
}
private static string GetTrackPath(TrackAsset track)
{
List<string> names = new List<string>();
var current = track;
while (current != null)
{
names.Add(current.name);
current = current.parent as TrackAsset;
}
names.Reverse();
return string.Join("/", names);
}
private static string GetTrackDisplay(TrackAsset track)
{
if (track == null) return "(없음)";
return $"{GetTrackPath(track)} <{track.GetType().Name}>";
}
}
}