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