├── .gitignore ├── Documentation~ ├── Manager.png └── Storyboard.png ├── Editor.meta ├── Editor ├── ModalNode.cs ├── ModalNode.cs.meta ├── ModalNodeEditor.cs ├── ModalNodeEditor.cs.meta ├── PageNode.cs ├── PageNode.cs.meta ├── PageNodeEditor.cs ├── PageNodeEditor.cs.meta ├── PrefabScreenshotUtility.cs ├── PrefabScreenshotUtility.cs.meta ├── StoryboardManagerData.cs ├── StoryboardManagerData.cs.meta ├── StoryboardManagerWindow.cs ├── StoryboardManagerWindow.cs.meta ├── StoryboardTreeView.cs ├── StoryboardTreeView.cs.meta ├── StoryboardTreeViewItem.cs ├── StoryboardTreeViewItem.cs.meta ├── UIStoryboardGraph.cs ├── UIStoryboardGraph.cs.meta ├── UIStoryboardGraphEditor.cs ├── UIStoryboardGraphEditor.cs.meta ├── UIStoryboardSettingsProvider.cs ├── UIStoryboardSettingsProvider.cs.meta ├── com.kwanjoong.unityuistoryboard.Editor.asmdef └── com.kwanjoong.unityuistoryboard.Editor.asmdef.meta ├── LICENSE.md ├── LICENSE.md.meta ├── README.md ├── README.md.meta ├── Runtime.meta ├── Runtime ├── UIStoryboardSettings.cs ├── UIStoryboardSettings.cs.meta ├── UIStoryboardSettingsAsset.cs ├── UIStoryboardSettingsAsset.cs.meta ├── com.kwanjoong.unityuistoryboard.asmdef └── com.kwanjoong.unityuistoryboard.asmdef.meta ├── package.json └── package.json.meta /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Mm]emoryCaptures/ 12 | 13 | # Never ignore Asset meta data 14 | !/[Aa]ssets/**/*.meta 15 | 16 | # Uncomment this line if you wish to ignore the asset store tools plugin 17 | # /[Aa]ssets/AssetStoreTools* 18 | 19 | # TextMesh Pro files 20 | [Aa]ssets/TextMesh*Pro/ 21 | 22 | # Autogenerated Jetbrains Rider plugin 23 | [Aa]ssets/Plugins/Editor/JetBrains* 24 | 25 | # Visual Studio cache directory 26 | .vs/ 27 | 28 | # Gradle cache directory 29 | .gradle/ 30 | 31 | # Autogenerated VS/MD/Consulo solution and project files 32 | ExportedObj/ 33 | .consulo/ 34 | *.csproj 35 | *.unityproj 36 | *.sln 37 | *.suo 38 | *.tmp 39 | *.user 40 | *.userprefs 41 | *.pidb 42 | *.booproj 43 | *.svd 44 | *.pdb 45 | *.mdb 46 | *.opendb 47 | *.VC.db 48 | 49 | # Unity3D generated meta files 50 | *.pidb.meta 51 | *.pdb.meta 52 | *.mdb.meta 53 | 54 | # Unity3D generated file on crash reports 55 | sysinfo.txt 56 | 57 | # Builds 58 | *.apk 59 | *.unitypackage 60 | 61 | # Crashlytics generated file 62 | crashlytics-build.properties 63 | 64 | # MacOS files 65 | .DS_Store 66 | -------------------------------------------------------------------------------- /Documentation~/Manager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwan3854/UnityUIStoryboard/0f3b06f60bbe9c05d009c1693436a6a6a0dbee0d/Documentation~/Manager.png -------------------------------------------------------------------------------- /Documentation~/Storyboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kwan3854/UnityUIStoryboard/0f3b06f60bbe9c05d009c1693436a6a6a0dbee0d/Documentation~/Storyboard.png -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2ff6f32a544594be49e1c64bff6994c1 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/ModalNode.cs: -------------------------------------------------------------------------------- 1 | using ScreenSystem.Modal; 2 | using ScreenSystem.Page; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using XNode; 6 | 7 | namespace com.kwanjoong.unityuistoryboard.Editor 8 | { 9 | /// 10 | /// Node that references: 11 | /// 1) View Prefab (root has ModalViewBase + LifetimeScope) 12 | /// 2) Lifecycle (LifecycleModalBase), Model, Builder(IModalBuilder) as MonoScript 13 | /// 3) Cached Thumbnail (Texture2D) for the prefab's rendered image 14 | /// 15 | public class ModalNode : Node 16 | { 17 | // Ports 18 | [Input(typeConstraint = TypeConstraint.Inherited)] 19 | public PageViewBase pageViewIn; 20 | [Input(typeConstraint = TypeConstraint.Inherited)] 21 | public ModalViewBase modalViewIn; 22 | [Output(typeConstraint = TypeConstraint.Inherited)] 23 | public ModalViewBase modalViewOut; 24 | 25 | // -- Prefab -- 26 | [SerializeField] 27 | private GameObject viewPrefab; 28 | 29 | // -- Scripts (MonoScript) -- 30 | [SerializeField] private MonoScript lifecycleScript; // => LifecycleModalBase 31 | [SerializeField] private MonoScript modelScript; 32 | [SerializeField] private MonoScript builderScript; // => IModalBuilder 33 | 34 | // // -- Cached thumbnail 35 | // [SerializeField, HideInInspector] 36 | // private Texture2D cachedThumbnail; 37 | [SerializeField, HideInInspector] 38 | private byte[] thumbnailData; 39 | 40 | // Memo field 41 | [SerializeField] private string memo; 42 | 43 | private Texture2D _cachedThumbnail; 44 | 45 | protected override void Init() 46 | { 47 | base.Init(); 48 | } 49 | 50 | public override object GetValue(NodePort port) 51 | { 52 | return null; 53 | } 54 | 55 | // Properties for editor usage 56 | public GameObject ViewPrefab => viewPrefab; 57 | public MonoScript LifecycleScript => lifecycleScript; 58 | public MonoScript ModelScript => modelScript; 59 | public MonoScript BuilderScript => builderScript; 60 | public string Memo { get => memo; set => memo = value; } 61 | 62 | // Called by editor script to store the newly captured screenshot 63 | public void SetCachedThumbnail(Texture2D tex) 64 | { 65 | if (tex == null) 66 | { 67 | return; 68 | } 69 | 70 | thumbnailData = tex.EncodeToPNG(); 71 | _cachedThumbnail = tex; 72 | } 73 | 74 | public Texture2D GetCachedThumbnail() 75 | { 76 | if (thumbnailData == null) 77 | { 78 | return null; 79 | } 80 | 81 | if (_cachedThumbnail != null) 82 | { 83 | return _cachedThumbnail; 84 | } 85 | 86 | var tex = new Texture2D(2, 2); 87 | tex.LoadImage(thumbnailData); 88 | _cachedThumbnail = tex; 89 | return tex; 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /Editor/ModalNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f80cfa19c83eb40d7a1ea1a4ea3f7261 -------------------------------------------------------------------------------- /Editor/ModalNodeEditor.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using System; 3 | using ScreenSystem.Modal; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using VContainer.Unity; 7 | using XNodeEditor; 8 | 9 | namespace com.kwanjoong.unityuistoryboard.Editor 10 | { 11 | [CustomNodeEditor(typeof(ModalNode))] 12 | public class ModalNodeEditor : NodeEditor 13 | { 14 | public override int GetWidth() 15 | { 16 | return 700; 17 | } 18 | 19 | public override void OnBodyGUI() 20 | { 21 | serializedObject.Update(); 22 | 23 | // Ports 24 | NodeEditorGUILayout.PortField(target.GetInputPort("pageViewIn"), GUILayout.MinWidth(30)); 25 | NodeEditorGUILayout.PortField(target.GetInputPort("modalViewIn"), GUILayout.MinWidth(30)); 26 | NodeEditorGUILayout.PortField(target.GetOutputPort("modalViewOut"), GUILayout.MinWidth(30)); 27 | 28 | EditorGUILayout.Space(); 29 | 30 | EditorGUILayout.BeginHorizontal(); 31 | { 32 | // [Left: Thumbnail] 33 | Texture2D cachedTex = (target as ModalNode)?.GetCachedThumbnail(); 34 | 35 | float thumbWidth = 320f; 36 | float thumbHeight = 400f; 37 | 38 | EditorGUILayout.BeginVertical(GUILayout.Width(thumbWidth)); 39 | { 40 | Rect thumbRect = GUILayoutUtility.GetRect(thumbWidth, thumbHeight, GUILayout.ExpandWidth(false)); 41 | EditorGUI.DrawRect(thumbRect, Color.clear); 42 | 43 | if (cachedTex != null) 44 | { 45 | EditorGUI.DrawPreviewTexture( 46 | thumbRect, 47 | cachedTex, 48 | null, 49 | ScaleMode.ScaleToFit 50 | ); 51 | } 52 | 53 | if (GUILayout.Button("Update Thumbnail")) 54 | { 55 | var node = (ModalNode)target; 56 | CapturePrefabThumbnail(node); 57 | } 58 | } 59 | EditorGUILayout.EndVertical(); 60 | 61 | // [Right: Properties] 62 | EditorGUILayout.BeginVertical(); 63 | { 64 | // View Prefab 65 | var prefabProp = serializedObject.FindProperty("viewPrefab"); 66 | DrawObjectFieldWithOpenButton( 67 | prefabProp, 68 | "View Prefab", 69 | () => 70 | { 71 | GameObject p = prefabProp.objectReferenceValue as GameObject; 72 | if (p != null) 73 | { 74 | EditorGUIUtility.PingObject(p); 75 | // or AssetDatabase.OpenAsset(p); 76 | } 77 | } 78 | ); 79 | 80 | // If prefab != null => check info 81 | GameObject prefab = prefabProp.objectReferenceValue as GameObject; 82 | if (prefab != null) 83 | { 84 | DrawModalViewAndScopeInfo(prefab); 85 | } 86 | 87 | EditorGUILayout.Space(); 88 | 89 | // Lifecycle => LifecycleModalBase 90 | var lifecycleProp = serializedObject.FindProperty("lifecycleScript"); 91 | DrawMonoScriptWithOpenButton( 92 | lifecycleProp, 93 | "Lifecycle", 94 | scriptClass => { 95 | bool isValid = typeof(LifecycleModalBase).IsAssignableFrom(scriptClass) 96 | && !scriptClass.IsAbstract; 97 | return isValid; 98 | } 99 | ); 100 | 101 | // Model => any class (non-abstract) 102 | var modelProp = serializedObject.FindProperty("modelScript"); 103 | DrawMonoScriptWithOpenButton( 104 | modelProp, 105 | "Model", 106 | scriptClass => !scriptClass.IsAbstract 107 | ); 108 | 109 | // Builder => IModalBuilder 110 | var builderProp = serializedObject.FindProperty("builderScript"); 111 | DrawMonoScriptWithOpenButton( 112 | builderProp, 113 | "Builder", 114 | scriptClass => { 115 | bool isValid = typeof(IModalBuilder).IsAssignableFrom(scriptClass) 116 | && !scriptClass.IsAbstract; 117 | return isValid; 118 | } 119 | ); 120 | 121 | // Memo 122 | EditorGUILayout.LabelField("Memo"); 123 | var memoProp = serializedObject.FindProperty("memo"); 124 | if (memoProp != null) 125 | { 126 | memoProp.stringValue = EditorGUILayout.TextArea( 127 | memoProp.stringValue, 128 | GUILayout.MinHeight(60), 129 | GUILayout.MaxHeight(230), 130 | GUILayout.MaxWidth(300), 131 | GUILayout.ExpandWidth(true), 132 | GUILayout.ExpandHeight(false) 133 | ); 134 | } 135 | } 136 | EditorGUILayout.EndVertical(); 137 | } 138 | EditorGUILayout.EndHorizontal(); 139 | 140 | serializedObject.ApplyModifiedProperties(); 141 | } 142 | 143 | // Capture 144 | private void CapturePrefabThumbnail(ModalNode node) 145 | { 146 | var prefab = node.ViewPrefab; 147 | if (!prefab) 148 | { 149 | EditorUtility.DisplayDialog("Capture Thumbnail", "No Prefab assigned!", "OK"); 150 | return; 151 | } 152 | 153 | var settings = UIStoryboardSettingsAsset.Instance; 154 | int width = settings.CanvasReferenceWidth; 155 | int height = settings.CanvasReferenceHeight; 156 | Texture2D screenshot = PrefabScreenshotUtility.TakeScreenshot(prefab, width, height); 157 | if (screenshot != null) 158 | { 159 | node.SetCachedThumbnail(screenshot); 160 | AssetDatabase.SaveAssets(); 161 | } 162 | else 163 | { 164 | EditorUtility.DisplayDialog("Capture Thumbnail", "Failed to capture screenshot.", "OK"); 165 | } 166 | } 167 | 168 | // DrawObjectFieldWithOpenButton / DrawMonoScriptWithOpenButton => same logic as PageNodeEditor 169 | private void DrawObjectFieldWithOpenButton(SerializedProperty prop, string label, Action openAction) 170 | { 171 | EditorGUILayout.BeginHorizontal(); 172 | { 173 | EditorGUILayout.PropertyField(prop, new GUIContent(label), GUILayout.MinWidth(100)); 174 | 175 | bool hasObject = (prop.objectReferenceValue != null); 176 | bool oldEnabled = GUI.enabled; 177 | GUI.enabled = hasObject; 178 | 179 | if (GUILayout.Button("Open", GUILayout.Width(50))) 180 | { 181 | openAction?.Invoke(); 182 | } 183 | GUI.enabled = oldEnabled; 184 | } 185 | EditorGUILayout.EndHorizontal(); 186 | } 187 | 188 | private void DrawMonoScriptWithOpenButton( 189 | SerializedProperty prop, 190 | string label, 191 | Func validator 192 | ) 193 | { 194 | EditorGUILayout.BeginHorizontal(); 195 | { 196 | EditorGUILayout.PropertyField(prop, new GUIContent(label), GUILayout.MinWidth(100)); 197 | 198 | MonoScript script = prop.objectReferenceValue as MonoScript; 199 | bool isValid = false; 200 | if (script != null) 201 | { 202 | Type scriptClass = script.GetClass(); 203 | if (scriptClass != null) isValid = validator.Invoke(scriptClass); 204 | } 205 | 206 | bool oldEnabled = GUI.enabled; 207 | GUI.enabled = (script != null && isValid); 208 | 209 | if (GUILayout.Button("Open", GUILayout.Width(50))) 210 | { 211 | AssetDatabase.OpenAsset(script); 212 | } 213 | 214 | GUI.enabled = oldEnabled; 215 | } 216 | EditorGUILayout.EndHorizontal(); 217 | 218 | // Validation msg 219 | if (prop.objectReferenceValue != null) 220 | { 221 | MonoScript ms = prop.objectReferenceValue as MonoScript; 222 | if (ms != null) 223 | { 224 | Type cls = ms.GetClass(); 225 | if (cls == null || !validator.Invoke(cls)) 226 | { 227 | EditorGUILayout.HelpBox( 228 | $"Selected script is not a valid {label} or is abstract.", 229 | MessageType.Warning 230 | ); 231 | } 232 | } 233 | } 234 | } 235 | 236 | private void DrawModalViewAndScopeInfo(GameObject prefab) 237 | { 238 | string path = AssetDatabase.GetAssetPath(prefab); 239 | if (string.IsNullOrEmpty(path)) 240 | { 241 | EditorGUILayout.HelpBox("Prefab is not an asset? Cannot load contents.", MessageType.Warning); 242 | return; 243 | } 244 | 245 | var prefabRoot = PrefabUtility.LoadPrefabContents(path); 246 | if (prefabRoot == null) 247 | { 248 | EditorGUILayout.HelpBox("Cannot load prefab contents.", MessageType.Error); 249 | return; 250 | } 251 | 252 | // ModalViewBase instead of PageViewBase 253 | var modalView = prefabRoot.GetComponent(); 254 | var scope = prefabRoot.GetComponent(); 255 | 256 | // ModalView 257 | if (!modalView) 258 | { 259 | EditorGUILayout.HelpBox("No ModalViewBase found on root GameObject.", MessageType.Warning); 260 | } 261 | else 262 | { 263 | EditorGUILayout.BeginHorizontal(); 264 | { 265 | EditorGUILayout.LabelField($"ModalViewBase: {modalView.GetType().Name}"); 266 | if (GUILayout.Button("Open", GUILayout.Width(50))) 267 | { 268 | OpenMonoScript(modalView); 269 | } 270 | } 271 | EditorGUILayout.EndHorizontal(); 272 | } 273 | 274 | // LifetimeScope same 275 | if (!scope) 276 | { 277 | EditorGUILayout.HelpBox("No LifetimeScope found on root GameObject.", MessageType.Warning); 278 | } 279 | else 280 | { 281 | EditorGUILayout.BeginHorizontal(); 282 | { 283 | EditorGUILayout.LabelField($"LifetimeScope: {scope.GetType().Name}"); 284 | if (GUILayout.Button("Open", GUILayout.Width(50))) 285 | { 286 | OpenMonoScript(scope); 287 | } 288 | } 289 | EditorGUILayout.EndHorizontal(); 290 | } 291 | 292 | PrefabUtility.UnloadPrefabContents(prefabRoot); 293 | } 294 | 295 | private void OpenMonoScript(Component component) 296 | { 297 | if (component == null) return; 298 | var mono = component as MonoBehaviour; 299 | if (!mono) mono = component.GetComponent(); 300 | if (mono) 301 | { 302 | var script = MonoScript.FromMonoBehaviour(mono); 303 | if (script != null) AssetDatabase.OpenAsset(script); 304 | } 305 | else 306 | { 307 | EditorUtility.DisplayDialog("Open Code", "Could not open script (not a MonoBehaviour?).", "OK"); 308 | } 309 | } 310 | } 311 | } 312 | #endif -------------------------------------------------------------------------------- /Editor/ModalNodeEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e02993f5a82f49e08de40295552f88b4 3 | timeCreated: 1736343529 -------------------------------------------------------------------------------- /Editor/PageNode.cs: -------------------------------------------------------------------------------- 1 | using ScreenSystem.Modal; 2 | using ScreenSystem.Page; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using VContainer.Unity; 6 | using XNode; 7 | 8 | namespace com.kwanjoong.unityuistoryboard.Editor 9 | { 10 | /// 11 | /// Node that references: 12 | /// 1) View Prefab (root has PageViewBase + LifetimeScope) 13 | /// 2) Lifecycle, Model, Builder as MonoScript 14 | /// 3) Cached Thumbnail (Texture2D) for the prefab's rendered image 15 | /// 16 | public class PageNode : Node 17 | { 18 | [Input(typeConstraint = TypeConstraint.Inherited)] 19 | public PageViewBase pageViewIn; 20 | [Input(typeConstraint = TypeConstraint.Inherited)] 21 | public ModalViewBase modalViewIn; 22 | [Output(typeConstraint = TypeConstraint.Inherited)] 23 | public PageViewBase pageViewOut; 24 | 25 | // -- Prefab -- 26 | [SerializeField] private GameObject viewPrefab; 27 | 28 | // -- Scripts (MonoScript) -- 29 | [SerializeField] private MonoScript lifecycleScript; 30 | [SerializeField] private MonoScript modelScript; 31 | [SerializeField] private MonoScript builderScript; 32 | 33 | // -- Cached thumbnail from "Update Thumbnail" button -- 34 | [SerializeField, HideInInspector] 35 | private byte[] thumbnailData; 36 | 37 | private Texture2D _cachedThumbnail; 38 | 39 | [SerializeField] private string memo; 40 | 41 | protected override void Init() 42 | { 43 | base.Init(); 44 | } 45 | 46 | public override object GetValue(NodePort port) 47 | { 48 | return null; 49 | } 50 | 51 | // Public getters if needed by NodeEditor 52 | public GameObject ViewPrefab => viewPrefab; 53 | public MonoScript LifecycleScript => lifecycleScript; 54 | public MonoScript ModelScript => modelScript; 55 | public MonoScript BuilderScript => builderScript; 56 | 57 | // Called by editor script to store the newly captured screenshot 58 | public void SetCachedThumbnail(Texture2D tex) 59 | { 60 | if (tex == null) 61 | { 62 | return; 63 | } 64 | 65 | thumbnailData = tex.EncodeToPNG(); 66 | _cachedThumbnail = tex; 67 | } 68 | 69 | public Texture2D GetCachedThumbnail() 70 | { 71 | if (thumbnailData == null) 72 | { 73 | return null; 74 | } 75 | 76 | if (_cachedThumbnail != null) 77 | { 78 | return _cachedThumbnail; 79 | } 80 | 81 | var tex = new Texture2D(2, 2); 82 | tex.LoadImage(thumbnailData); 83 | _cachedThumbnail = tex; 84 | return tex; 85 | } 86 | 87 | public string Memo {get => memo; set => memo = value;} 88 | } 89 | } -------------------------------------------------------------------------------- /Editor/PageNode.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e91921c149518462ca89e3ae04e9efec -------------------------------------------------------------------------------- /Editor/PageNodeEditor.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using System; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using XNodeEditor; 6 | using ScreenSystem.Page; 7 | using VContainer.Unity; 8 | 9 | namespace com.kwanjoong.unityuistoryboard.Editor 10 | { 11 | [CustomNodeEditor(typeof(PageNode))] 12 | public class PageNodeEditor : NodeEditor 13 | { 14 | public override int GetWidth() 15 | { 16 | return 700; 17 | } 18 | 19 | public override Color GetTint() 20 | { 21 | return new Color(0.25f, 0.25f, 0.25f, 1f); 22 | } 23 | 24 | public override void OnBodyGUI() 25 | { 26 | serializedObject.Update(); 27 | 28 | // Ports 29 | NodeEditorGUILayout.PortField(target.GetInputPort("pageViewIn"), GUILayout.MinWidth(30)); 30 | NodeEditorGUILayout.PortField(target.GetInputPort("modalViewIn"), GUILayout.MinWidth(30)); 31 | NodeEditorGUILayout.PortField(target.GetOutputPort("pageViewOut"), GUILayout.MinWidth(30)); 32 | 33 | EditorGUILayout.Space(); 34 | 35 | EditorGUILayout.BeginHorizontal(); 36 | { 37 | // --------------------------------------------- 38 | // [Left: Thumbnail] 39 | // --------------------------------------------- 40 | Texture2D cachedTex = (target as PageNode)?.GetCachedThumbnail(); 41 | 42 | float thumbWidth = 320f; 43 | float thumbHeight = 400f; 44 | 45 | 46 | EditorGUILayout.BeginVertical(GUILayout.Width(thumbWidth)); 47 | { 48 | Rect thumbRect = GUILayoutUtility.GetRect(thumbWidth, thumbHeight, GUILayout.ExpandWidth(false)); 49 | EditorGUI.DrawRect(thumbRect, Color.clear); 50 | 51 | if (cachedTex != null) 52 | { 53 | EditorGUI.DrawPreviewTexture( 54 | thumbRect, 55 | cachedTex, 56 | null, 57 | ScaleMode.ScaleToFit 58 | ); 59 | } 60 | 61 | // Thumbnail Button 62 | if (GUILayout.Button("Update Thumbnail")) 63 | { 64 | var node = (PageNode)target; 65 | CapturePrefabThumbnail(node); 66 | } 67 | } 68 | EditorGUILayout.EndVertical(); 69 | 70 | 71 | 72 | // --------------------------------------------- 73 | // [Right: Properties] 74 | // --------------------------------------------- 75 | EditorGUILayout.BeginVertical(); 76 | { 77 | // ---------------------------- 78 | // View Prefab 79 | // ---------------------------- 80 | var prefabProp = serializedObject.FindProperty("viewPrefab"); 81 | DrawObjectFieldWithOpenButton( 82 | prefabProp, 83 | "View Prefab", 84 | () => { 85 | // 만약 Prefab을 열기 => 애셋 열기 86 | GameObject p = prefabProp.objectReferenceValue as GameObject; 87 | if (p != null) 88 | { 89 | // ping or open in project 90 | EditorGUIUtility.PingObject(p); 91 | // optionally: AssetDatabase.OpenAsset(p); 92 | } 93 | } 94 | ); 95 | 96 | // Prefab Info (Includes "LifetimeScope" open button) 97 | GameObject prefab = prefabProp.objectReferenceValue as GameObject; 98 | if (prefab != null) 99 | { 100 | DrawPageViewAndScopeInfo(prefab); 101 | } 102 | 103 | EditorGUILayout.Space(); 104 | 105 | // ---------------------------- 106 | // Lifecycle 107 | // ---------------------------- 108 | var lifecycleProp = serializedObject.FindProperty("lifecycleScript"); 109 | DrawMonoScriptWithOpenButton( 110 | lifecycleProp, 111 | "Lifecycle", 112 | scriptClass => { 113 | bool isValid = typeof(LifecyclePageBase).IsAssignableFrom(scriptClass) 114 | && !scriptClass.IsAbstract; 115 | return isValid; 116 | } 117 | ); 118 | 119 | // ---------------------------- 120 | // Model 121 | // ---------------------------- 122 | var modelProp = serializedObject.FindProperty("modelScript"); 123 | DrawMonoScriptWithOpenButton( 124 | modelProp, 125 | "Model", 126 | scriptClass => !scriptClass.IsAbstract 127 | ); 128 | 129 | // ---------------------------- 130 | // Builder + memo 131 | // ---------------------------- 132 | var builderProp = serializedObject.FindProperty("builderScript"); 133 | DrawMonoScriptWithOpenButton( 134 | builderProp, 135 | "Builder", 136 | scriptClass => { 137 | bool isValid = typeof(IPageBuilder).IsAssignableFrom(scriptClass) 138 | && !scriptClass.IsAbstract; 139 | return isValid; 140 | } 141 | ); 142 | 143 | // === Memo for builder === 144 | EditorGUILayout.LabelField("Memo"); 145 | var memoProp = serializedObject.FindProperty("memo"); 146 | if (memoProp != null) 147 | { 148 | memoProp.stringValue = EditorGUILayout.TextArea( 149 | memoProp.stringValue, 150 | GUILayout.MinHeight(60), 151 | GUILayout.MaxHeight(230), 152 | GUILayout.MaxWidth(300), 153 | GUILayout.ExpandWidth(true), 154 | GUILayout.ExpandHeight(false) 155 | ); 156 | } 157 | } 158 | EditorGUILayout.EndVertical(); 159 | } 160 | EditorGUILayout.EndHorizontal(); 161 | 162 | serializedObject.ApplyModifiedProperties(); 163 | } 164 | 165 | #region Capture Thumbnail 166 | private void CapturePrefabThumbnail(PageNode node) 167 | { 168 | var prefab = node.ViewPrefab; 169 | if (!prefab) 170 | { 171 | EditorUtility.DisplayDialog("Capture Thumbnail", "No Prefab assigned!", "OK"); 172 | return; 173 | } 174 | 175 | var settings = UIStoryboardSettingsAsset.Instance; 176 | int width = settings.CanvasReferenceWidth; 177 | int height = settings.CanvasReferenceHeight; 178 | Texture2D screenshot = PrefabScreenshotUtility.TakeScreenshot(prefab, width, height); 179 | if (screenshot != null) 180 | { 181 | node.SetCachedThumbnail(screenshot); 182 | AssetDatabase.SaveAssets(); 183 | } 184 | else 185 | { 186 | EditorUtility.DisplayDialog("Capture Thumbnail", "Failed to capture screenshot.", "OK"); 187 | } 188 | } 189 | #endregion 190 | 191 | #region Helper UI: DrawObjectFieldWithOpenButton 192 | /// 193 | /// ObjectField + Open Button 194 | /// 195 | private void DrawObjectFieldWithOpenButton(SerializedProperty prop, string label, Action openAction) 196 | { 197 | EditorGUILayout.BeginHorizontal(); 198 | { 199 | EditorGUILayout.PropertyField(prop, new GUIContent(label), GUILayout.MinWidth(100)); 200 | 201 | bool hasObject = (prop.objectReferenceValue != null); 202 | bool oldEnabled = GUI.enabled; 203 | GUI.enabled = hasObject; 204 | 205 | if (GUILayout.Button("Open", GUILayout.Width(50))) 206 | { 207 | openAction?.Invoke(); 208 | } 209 | GUI.enabled = oldEnabled; 210 | } 211 | EditorGUILayout.EndHorizontal(); 212 | } 213 | #endregion 214 | 215 | #region Helper UI: DrawMonoScriptWithOpenButton 216 | private void DrawMonoScriptWithOpenButton( 217 | SerializedProperty prop, 218 | string label, 219 | Func validator 220 | ) 221 | { 222 | EditorGUILayout.BeginHorizontal(); 223 | { 224 | EditorGUILayout.PropertyField(prop, new GUIContent(label), GUILayout.MinWidth(100)); 225 | 226 | MonoScript script = prop.objectReferenceValue as MonoScript; 227 | bool isValid = false; 228 | if (script != null) 229 | { 230 | Type scriptClass = script.GetClass(); 231 | if (scriptClass != null) isValid = validator.Invoke(scriptClass); 232 | } 233 | 234 | bool oldEnabled = GUI.enabled; 235 | GUI.enabled = (script != null && isValid); 236 | 237 | if (GUILayout.Button("Open", GUILayout.Width(50))) 238 | { 239 | // Open MonoScript 240 | AssetDatabase.OpenAsset(script); 241 | } 242 | 243 | GUI.enabled = oldEnabled; 244 | } 245 | EditorGUILayout.EndHorizontal(); 246 | 247 | // Validation Msg 248 | if (prop.objectReferenceValue != null) 249 | { 250 | MonoScript ms = prop.objectReferenceValue as MonoScript; 251 | if (ms != null) 252 | { 253 | Type cls = ms.GetClass(); 254 | if (cls == null || !validator.Invoke(cls)) 255 | { 256 | EditorGUILayout.HelpBox( 257 | $"Selected script is not a valid {label} or is abstract.", 258 | MessageType.Warning 259 | ); 260 | } 261 | } 262 | } 263 | } 264 | #endregion 265 | 266 | #region Prefab Info (View & LifetimeScope) 267 | private void DrawPageViewAndScopeInfo(GameObject prefab) 268 | { 269 | string path = AssetDatabase.GetAssetPath(prefab); 270 | if (string.IsNullOrEmpty(path)) 271 | { 272 | EditorGUILayout.HelpBox("Prefab is not an asset? Cannot load contents.", MessageType.Warning); 273 | return; 274 | } 275 | 276 | var prefabRoot = PrefabUtility.LoadPrefabContents(path); 277 | if (prefabRoot == null) 278 | { 279 | EditorGUILayout.HelpBox("Cannot load prefab contents.", MessageType.Error); 280 | return; 281 | } 282 | 283 | var pageView = prefabRoot.GetComponent(); 284 | var scope = prefabRoot.GetComponent(); 285 | 286 | // PageView 287 | if (!pageView) 288 | { 289 | EditorGUILayout.HelpBox("No PageViewBase found on root GameObject.", MessageType.Warning); 290 | } 291 | else 292 | { 293 | // 한 줄 + [Open] 294 | EditorGUILayout.BeginHorizontal(); 295 | { 296 | EditorGUILayout.LabelField($"PageViewBase: {pageView.GetType().Name}"); 297 | if (GUILayout.Button("Open", GUILayout.Width(50))) 298 | { 299 | OpenMonoScript(pageView); 300 | } 301 | } 302 | EditorGUILayout.EndHorizontal(); 303 | } 304 | 305 | // LifetimeScope 306 | if (!scope) 307 | { 308 | EditorGUILayout.HelpBox("No LifetimeScope found on root GameObject.", MessageType.Warning); 309 | } 310 | else 311 | { 312 | EditorGUILayout.BeginHorizontal(); 313 | { 314 | EditorGUILayout.LabelField($"LifetimeScope: {scope.GetType().Name}"); 315 | if (GUILayout.Button("Open", GUILayout.Width(50))) 316 | { 317 | OpenMonoScript(scope); 318 | } 319 | } 320 | EditorGUILayout.EndHorizontal(); 321 | } 322 | 323 | PrefabUtility.UnloadPrefabContents(prefabRoot); 324 | } 325 | 326 | private void OpenMonoScript(Component component) 327 | { 328 | if (component == null) return; 329 | var mono = component as MonoBehaviour; 330 | if (!mono) 331 | { 332 | mono = component.GetComponent(); 333 | } 334 | if (mono) 335 | { 336 | var script = MonoScript.FromMonoBehaviour(mono); 337 | if (script != null) AssetDatabase.OpenAsset(script); 338 | } 339 | else 340 | { 341 | EditorUtility.DisplayDialog("Open Code", 342 | "Could not open script (not a MonoBehaviour?).", 343 | "OK"); 344 | } 345 | } 346 | #endregion 347 | } 348 | } 349 | #endif -------------------------------------------------------------------------------- /Editor/PageNodeEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6d21c128cff7443f3a6a455a818c4153 -------------------------------------------------------------------------------- /Editor/PrefabScreenshotUtility.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using UnityEngine; 3 | using UnityEditor; 4 | using UnityEngine.UI; // for Canvas, CanvasScaler 5 | using System.Collections.Generic; 6 | 7 | namespace com.kwanjoong.unityuistoryboard.Editor 8 | { 9 | public static class PrefabScreenshotUtility 10 | { 11 | /// 12 | /// Uses PreviewRenderUtility to instantiate a uGUI prefab in ScreenSpaceCamera mode. 13 | /// Automatically adjusts an orthographic camera so the entire UI is visible. 14 | /// Returns the rendered Texture2D. 15 | /// 16 | public static Texture2D TakeScreenshot(GameObject prefab, int width, int height) 17 | { 18 | if (prefab == null) 19 | { 20 | Debug.LogError("[CapturePrefabWithPreviewRenderUtility] No prefab provided."); 21 | return null; 22 | } 23 | 24 | // 1) Create PreviewRenderUtility & configure camera 25 | var preview = new PreviewRenderUtility(); 26 | preview.camera.backgroundColor = Color.clear; 27 | preview.camera.clearFlags = CameraClearFlags.SolidColor; 28 | preview.camera.cameraType = CameraType.Game; 29 | preview.camera.farClipPlane = 1000f; 30 | preview.camera.nearClipPlane = 0.1f; 31 | // We will switch to orthographic below, after we find the bounding box 32 | preview.camera.orthographic = false; 33 | preview.camera.transform.position = new Vector3(0, 0, -10f); 34 | preview.camera.transform.LookAt(Vector3.zero); 35 | 36 | // 2) BeginStaticPreview with a rectangle (this defines the render area) 37 | var previewRect = new Rect(0, 0, width, height); 38 | preview.BeginStaticPreview(previewRect); 39 | 40 | // 3) Instantiate prefab via PreviewRenderUtility 41 | var instance = preview.InstantiatePrefabInScene(prefab); 42 | if (!instance) 43 | { 44 | Debug.LogError("[CapturePrefabWithPreviewRenderUtility] Failed to instantiate prefab in preview scene."); 45 | preview.EndStaticPreview(); 46 | preview.Cleanup(); 47 | return null; 48 | } 49 | 50 | // 4) Setup Canvas 51 | var canvas = instance.GetComponent(); 52 | if (canvas == null) canvas = instance.AddComponent(); 53 | canvas.renderMode = RenderMode.ScreenSpaceCamera; 54 | 55 | canvas.worldCamera = preview.camera; 56 | 57 | var scaler = instance.GetComponent(); 58 | if (scaler == null) scaler = instance.AddComponent(); 59 | scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; 60 | scaler.referenceResolution = new Vector2(width, height); 61 | 62 | // Force positioning 63 | instance.transform.position = Vector3.zero; 64 | 65 | // 5) Force UI layout 66 | Canvas.ForceUpdateCanvases(); 67 | 68 | // 6) Calculate bounding box of all UI elements -> set Orthographic camera so entire UI fits 69 | Bounds bounds = CalculateUIBounds(instance); 70 | // Switch to orthographic 71 | preview.camera.orthographic = true; 72 | 73 | // bounding box extents 74 | float xSize = bounds.size.x; 75 | float ySize = bounds.size.y; 76 | float halfMax = Mathf.Max(xSize, ySize) * 0.5f; 77 | // Add small padding factor so UI isn't right at the edge 78 | float paddingFactor = 1.1f; 79 | halfMax *= paddingFactor; 80 | 81 | preview.camera.orthographicSize = halfMax; 82 | 83 | // Position camera to center on bounding box 84 | // boundingBox center is 'bounds.center' 85 | // We'll place the camera at (center.x, center.y, someZ) so the UI is centered 86 | // (Make sure we offset by z so we don't clip through the UI) 87 | Vector3 center = bounds.center; 88 | Vector3 camPos = new Vector3(center.x, center.y, -10f); 89 | preview.camera.transform.position = camPos; 90 | preview.camera.transform.LookAt(center); 91 | 92 | // Force update again 93 | Canvas.ForceUpdateCanvases(); 94 | 95 | // 7) Render 96 | preview.Render(); 97 | 98 | // 8) EndStaticPreview to get the final texture 99 | var texture = preview.EndStaticPreview(); 100 | 101 | // 9) Cleanup 102 | preview.camera.targetTexture = null; 103 | preview.Cleanup(); 104 | 105 | return texture; 106 | } 107 | 108 | /// 109 | /// Calculate the bounding box of all RectTransforms in the prefab instance. 110 | /// We'll gather their world corners and encapsulate them into a Bounds. 111 | /// 112 | private static Bounds CalculateUIBounds(GameObject root) 113 | { 114 | var rectTransforms = root.GetComponentsInChildren(true); 115 | 116 | bool first = true; 117 | Bounds bounds = new Bounds(); 118 | 119 | foreach (var rt in rectTransforms) 120 | { 121 | var corners = new Vector3[4]; 122 | rt.GetWorldCorners(corners); 123 | // Encapsulate all 4 corners 124 | foreach (var c in corners) 125 | { 126 | if (first) 127 | { 128 | bounds = new Bounds(c, Vector3.zero); 129 | first = false; 130 | } 131 | else 132 | { 133 | bounds.Encapsulate(c); 134 | } 135 | } 136 | } 137 | return bounds; 138 | } 139 | } 140 | } 141 | #endif -------------------------------------------------------------------------------- /Editor/PrefabScreenshotUtility.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 80e9d45142bbb42d195badf2905d3d97 -------------------------------------------------------------------------------- /Editor/StoryboardManagerData.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using System; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | namespace com.kwanjoong.unityuistoryboard.Editor 7 | { 8 | [Serializable] 9 | public class StoryboardManagerData : ScriptableObject 10 | { 11 | [Serializable] 12 | public class TreeNodeData 13 | { 14 | public int id; 15 | public bool isFolder; 16 | public string name; 17 | public string assetPath; 18 | public int parentId; 19 | public List childrenIds = new List(); 20 | } 21 | 22 | public List nodes = new List(); 23 | public int nextId = 1; 24 | } 25 | } 26 | #endif -------------------------------------------------------------------------------- /Editor/StoryboardManagerData.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: fe1398cb62064858927eda0c3e2413d1 3 | timeCreated: 1736404747 -------------------------------------------------------------------------------- /Editor/StoryboardManagerWindow.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using com.kwanjoong.unityuistoryboard.Editor; 7 | using UnityEditor; 8 | using UnityEditor.IMGUI.Controls; 9 | using UnityEngine; 10 | using XNodeEditor; 11 | 12 | 13 | namespace com.kwanjoong.unityuistoryboard.Editor 14 | { 15 | public class UIStoryboardManagerWindow : EditorWindow 16 | { 17 | private TreeViewState _treeViewState; 18 | private StoryboardTreeView _treeView; 19 | private StoryboardManagerData _managerData; 20 | 21 | private string _newFolderName = "NewFolder"; 22 | private string _newStoryboardName = "NewStoryboard"; 23 | private const string ManagerAssetPath = "Assets/UIStoryboard/StoryboardManagerData.asset"; 24 | 25 | [MenuItem("Window/UI Storyboard/Storyboard Manager")] 26 | public static void Open() 27 | { 28 | var wnd = GetWindow(); 29 | wnd.titleContent = new GUIContent("UI Storyboard Manager"); 30 | wnd.Show(); 31 | } 32 | 33 | private void OnEnable() 34 | { 35 | LoadOrCreateManagerData(); 36 | InitializeTreeView(); 37 | } 38 | 39 | private void LoadOrCreateManagerData() 40 | { 41 | _managerData = AssetDatabase.LoadAssetAtPath(ManagerAssetPath); 42 | } 43 | 44 | private void InitializeTreeView() 45 | { 46 | if (_treeViewState == null) 47 | _treeViewState = new TreeViewState(); 48 | 49 | _treeView = new StoryboardTreeView(_treeViewState) 50 | { 51 | OnGetAllStoryboardAssets = FindAllStoryboardAssets, 52 | OnDoubleClickStoryboard = (item) => 53 | { 54 | // Open the storyboard asset in new window 55 | var asset = AssetDatabase.LoadAssetAtPath(item.AssetPath); 56 | if (asset != null) 57 | { 58 | var window = NodeEditorWindow.Open(asset); 59 | window.titleContent = new GUIContent(item.StoryboardName); 60 | } 61 | } 62 | }; 63 | 64 | if (_managerData != null) 65 | { 66 | _treeView.LoadFromManagerData(_managerData); 67 | } 68 | 69 | _treeView.Reload(); 70 | } 71 | 72 | private void OnGUI() 73 | { 74 | if (_managerData == null) 75 | { 76 | EditorGUILayout.HelpBox("Manager Data not found. Create it first.", MessageType.Warning); 77 | if (GUILayout.Button("Create Manager Data")) 78 | { 79 | CreateManagerData(); 80 | InitializeTreeView(); 81 | } 82 | 83 | return; 84 | } 85 | 86 | DrawToolbar(); 87 | DrawTreeView(); 88 | } 89 | 90 | private void DrawToolbar() 91 | { 92 | EditorGUILayout.BeginHorizontal("box"); 93 | { 94 | _newFolderName = EditorGUILayout.TextField("Folder", _newFolderName); 95 | if (GUILayout.Button("Create Folder")) 96 | { 97 | var item = _treeView.CreateFolder(_newFolderName); 98 | if (item != null) 99 | SaveManagerData(); 100 | } 101 | } 102 | EditorGUILayout.EndHorizontal(); 103 | 104 | EditorGUILayout.BeginHorizontal("box"); 105 | { 106 | _newStoryboardName = EditorGUILayout.TextField("Storyboard", _newStoryboardName); 107 | if (GUILayout.Button("Create Storyboard")) 108 | { 109 | string basePath = "Assets/UIStoryboard/" + _newStoryboardName + ".asset"; 110 | var uniquePath = AssetDatabase.GenerateUniqueAssetPath(basePath); 111 | 112 | var nodeGraph = ScriptableObject.CreateInstance(); 113 | AssetDatabase.CreateAsset(nodeGraph, uniquePath); 114 | AssetDatabase.SaveAssets(); 115 | 116 | var item = _treeView.CreateStoryboard(_newStoryboardName, uniquePath); 117 | if (item != null) 118 | SaveManagerData(); 119 | } 120 | } 121 | EditorGUILayout.EndHorizontal(); 122 | 123 | if (GUILayout.Button("Refresh External Storyboards")) 124 | { 125 | _treeView.RefreshAllStoryboards(_treeView.OnGetAllStoryboardAssets); 126 | SaveManagerData(); 127 | } 128 | } 129 | 130 | private void DrawTreeView() 131 | { 132 | var rect = GUILayoutUtility.GetRect(0, 100000, 0, 100000); 133 | _treeView.OnGUI(rect); 134 | } 135 | 136 | private void CreateManagerData() 137 | { 138 | var directory = Path.GetDirectoryName(ManagerAssetPath); 139 | if (!Directory.Exists(directory)) 140 | Directory.CreateDirectory(directory); 141 | 142 | _managerData = ScriptableObject.CreateInstance(); 143 | AssetDatabase.CreateAsset(_managerData, ManagerAssetPath); 144 | AssetDatabase.SaveAssets(); 145 | } 146 | 147 | public void SaveManagerData() 148 | { 149 | if (_managerData != null && _treeView != null) 150 | { 151 | _treeView.SaveToManagerData(_managerData); 152 | EditorUtility.SetDirty(_managerData); 153 | AssetDatabase.SaveAssets(); 154 | } 155 | } 156 | 157 | private List FindAllStoryboardAssets() 158 | { 159 | var guids = AssetDatabase.FindAssets("t:UIStoryboardGraph"); 160 | return guids.Select(AssetDatabase.GUIDToAssetPath).ToList(); 161 | } 162 | } 163 | } 164 | #endif -------------------------------------------------------------------------------- /Editor/StoryboardManagerWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 7fc839f7b82ff47d98aaef3f2e5a0129 -------------------------------------------------------------------------------- /Editor/StoryboardTreeView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using UnityEditor; 6 | using UnityEditor.IMGUI.Controls; 7 | using UnityEngine; 8 | using XNodeEditor; 9 | 10 | namespace com.kwanjoong.unityuistoryboard.Editor 11 | { 12 | public class StoryboardTreeView : TreeView 13 | { 14 | public Func> OnGetAllStoryboardAssets; 15 | public Action OnDoubleClickStoryboard; 16 | 17 | private int _currentId = 1; 18 | private Dictionary _items = new Dictionary(); 19 | private StoryboardTreeViewItem _root; 20 | 21 | public StoryboardTreeView(TreeViewState state) : base(state) 22 | { 23 | showAlternatingRowBackgrounds = true; 24 | showBorder = true; 25 | rowHeight = EditorGUIUtility.singleLineHeight * 1.2f; 26 | 27 | _root = new StoryboardTreeViewItem 28 | { 29 | id = 0, 30 | depth = -1, 31 | displayName = "Root", 32 | IsFolder = true, 33 | FolderName = "Root", 34 | children = new List() 35 | }; 36 | } 37 | 38 | public void LoadFromManagerData(StoryboardManagerData data) 39 | { 40 | _items.Clear(); 41 | _currentId = data.nextId; 42 | 43 | // First pass: Create all items 44 | foreach (var node in data.nodes) 45 | { 46 | var item = new StoryboardTreeViewItem 47 | { 48 | id = node.id, 49 | IsFolder = node.isFolder, 50 | displayName = node.name, 51 | depth = 0 // Will be set correctly later 52 | }; 53 | 54 | if (node.isFolder) 55 | { 56 | item.FolderName = node.name; 57 | item.children = new List(); 58 | } 59 | else 60 | { 61 | item.StoryboardName = node.name; 62 | item.AssetPath = node.assetPath; 63 | } 64 | 65 | _items[node.id] = item; 66 | } 67 | 68 | // Second pass: Set up parent-child relationships 69 | foreach (var node in data.nodes) 70 | { 71 | var item = _items[node.id]; 72 | if (node.parentId == 0) 73 | { 74 | item.parent = _root; 75 | if (!_root.children.Contains(item)) 76 | _root.children.Add(item); 77 | } 78 | else if (_items.ContainsKey(node.parentId)) 79 | { 80 | var parent = _items[node.parentId]; 81 | item.parent = parent; 82 | if (parent.children == null) 83 | parent.children = new List(); 84 | if (!parent.children.Contains(item)) 85 | parent.children.Add(item); 86 | } 87 | } 88 | 89 | // Update depths 90 | UpdateDepthsRecursive(_root, -1); 91 | 92 | Reload(); 93 | } 94 | 95 | private void UpdateDepthsRecursive(TreeViewItem item, int depth) 96 | { 97 | item.depth = depth; 98 | if (item.children != null) 99 | { 100 | foreach (var child in item.children) 101 | { 102 | UpdateDepthsRecursive(child, depth + 1); 103 | } 104 | } 105 | } 106 | 107 | public void SaveToManagerData(StoryboardManagerData data) 108 | { 109 | data.nodes.Clear(); 110 | data.nextId = _currentId; 111 | 112 | foreach (var item in _items.Values) 113 | { 114 | var parentItem = item.parent as StoryboardTreeViewItem; 115 | var childrenIds = new List(); 116 | 117 | if (item.children != null) 118 | { 119 | foreach (var child in item.children) 120 | { 121 | if (child is StoryboardTreeViewItem paletteChild) 122 | { 123 | childrenIds.Add(paletteChild.id); 124 | } 125 | } 126 | } 127 | 128 | var node = new StoryboardManagerData.TreeNodeData 129 | { 130 | id = item.id, 131 | isFolder = item.IsFolder, 132 | name = item.IsFolder ? item.FolderName : item.StoryboardName, 133 | assetPath = item.IsFolder ? "" : item.AssetPath, 134 | parentId = parentItem == _root ? 0 : parentItem?.id ?? 0, 135 | childrenIds = childrenIds 136 | }; 137 | 138 | data.nodes.Add(node); 139 | } 140 | } 141 | 142 | 143 | protected override TreeViewItem BuildRoot() 144 | { 145 | return _root; 146 | } 147 | 148 | private void RebuildTreeRecursive(StoryboardTreeViewItem parent) 149 | { 150 | var childItems = _items.Values.Where(item => item.parent == parent).ToList(); 151 | 152 | parent.children = new List(); 153 | foreach (var child in childItems) 154 | { 155 | parent.children.Add(child); 156 | if (child.IsFolder) 157 | { 158 | RebuildTreeRecursive(child); 159 | } 160 | } 161 | } 162 | 163 | protected override void RowGUI(RowGUIArgs args) 164 | { 165 | var item = args.item as StoryboardTreeViewItem; 166 | if (item == null) return; 167 | 168 | // inline rename 169 | if (item.RenameMode) 170 | { 171 | float indent = GetContentIndent(item); 172 | var rowRect = args.rowRect; 173 | rowRect.xMin += indent; 174 | 175 | if (item.IsFolder) 176 | { 177 | item.FolderName = EditorGUI.TextField(rowRect, item.FolderName); 178 | // finalize on Enter 179 | if (Event.current.type == EventType.KeyUp && Event.current.keyCode == KeyCode.Return) 180 | { 181 | FinalizeRename(item); 182 | } 183 | } 184 | else 185 | { 186 | item.StoryboardName = EditorGUI.TextField(rowRect, item.StoryboardName); 187 | // finalize on Enter 188 | if (Event.current.type == EventType.KeyUp && Event.current.keyCode == KeyCode.Return) 189 | { 190 | FinalizeRename(item); 191 | } 192 | } 193 | } 194 | else 195 | { 196 | // default label 197 | base.RowGUI(args); 198 | } 199 | } 200 | 201 | private void OpenStoryboardWindow(StoryboardTreeViewItem item) 202 | { 203 | if (item.IsFolder) return; 204 | 205 | // 스토리보드 에셋 로드 206 | var storyboardAsset = AssetDatabase.LoadAssetAtPath(item.AssetPath); 207 | if (storyboardAsset != null) 208 | { 209 | // XNodeEditorWindow를 열고 특정 그래프 로드 210 | var editorWindow = NodeEditorWindow.Open(storyboardAsset); 211 | editorWindow.titleContent = new GUIContent(item.StoryboardName); 212 | } 213 | } 214 | 215 | 216 | private void FinalizeRename(StoryboardTreeViewItem item) 217 | { 218 | if (item.IsFolder) 219 | { 220 | // check duplication among siblings 221 | if (HasFolderNameDuplicate(item)) 222 | { 223 | EditorUtility.DisplayDialog("Error", 224 | $"Folder '{item.FolderName}' already exists in this parent!", 225 | "OK"); 226 | // revert or do something 227 | item.FolderName = "Folder" + item.id; 228 | } 229 | } 230 | else 231 | { 232 | // rename .asset 233 | if (!string.IsNullOrEmpty(item.AssetPath)) 234 | { 235 | string oldPath = item.AssetPath; 236 | var dir = Path.GetDirectoryName(oldPath); 237 | var newName = item.StoryboardName; 238 | var newPath = Path.Combine(dir, newName + ".asset"); 239 | newPath = AssetDatabase.GenerateUniqueAssetPath(newPath); 240 | AssetDatabase.RenameAsset(oldPath, Path.GetFileNameWithoutExtension(newPath)); 241 | AssetDatabase.SaveAssets(); 242 | AssetDatabase.Refresh(); 243 | item.AssetPath = newPath; 244 | 245 | } 246 | } 247 | 248 | item.RenameMode = false; 249 | item.displayName = item.IsFolder ? item.FolderName : item.StoryboardName; 250 | 251 | if (EditorWindow.GetWindow() is UIStoryboardManagerWindow window) 252 | { 253 | window.SaveManagerData(); 254 | } 255 | 256 | Reload(); 257 | } 258 | 259 | private bool HasFolderNameDuplicate(StoryboardTreeViewItem folder) 260 | { 261 | if (folder.parent == null) return false; 262 | var siblings = folder.parent.children; 263 | foreach (var s in siblings) 264 | { 265 | if (s is StoryboardTreeViewItem p 266 | && p.IsFolder 267 | && p != folder 268 | && p.FolderName.Equals(folder.FolderName, StringComparison.OrdinalIgnoreCase)) 269 | { 270 | return true; 271 | } 272 | } 273 | 274 | return false; 275 | } 276 | 277 | #region DragAndDrop 278 | 279 | protected override bool CanStartDrag(CanStartDragArgs args) 280 | { 281 | return true; 282 | } 283 | 284 | protected override void SetupDragAndDrop(SetupDragAndDropArgs args) 285 | { 286 | var dragged = new List(); 287 | foreach (var id in args.draggedItemIDs) 288 | { 289 | if (_items.ContainsKey(id)) 290 | dragged.Add(_items[id]); 291 | } 292 | 293 | if (dragged.Count == 0) return; 294 | 295 | DragAndDrop.PrepareStartDrag(); 296 | DragAndDrop.SetGenericData("PaletteTreeViewDrag", dragged); 297 | DragAndDrop.objectReferences = new UnityEngine.Object[0]; 298 | DragAndDrop.StartDrag(dragged.Count > 1 ? "" : dragged[0].DisplayName); 299 | } 300 | 301 | protected override DragAndDropVisualMode HandleDragAndDrop(DragAndDropArgs args) 302 | { 303 | if (!args.performDrop) 304 | return DragAndDropVisualMode.Move; 305 | 306 | var dragged = DragAndDrop.GetGenericData("PaletteTreeViewDrag") as List; 307 | if (dragged == null || dragged.Count == 0) 308 | return DragAndDropVisualMode.None; 309 | 310 | var parentItem = args.parentItem as StoryboardTreeViewItem; 311 | var insertIdx = args.insertAtIndex; 312 | 313 | switch (args.dragAndDropPosition) 314 | { 315 | case DragAndDropPosition.BetweenItems: 316 | if (parentItem == null) 317 | parentItem = rootItem as StoryboardTreeViewItem; 318 | 319 | foreach (var d in dragged) 320 | { 321 | if (d.parent != null && d.parent.children != null) 322 | d.parent.children.Remove(d); 323 | 324 | d.parent = parentItem; 325 | d.depth = parentItem.depth + 1; 326 | } 327 | 328 | if (parentItem.children == null) 329 | parentItem.children = new List(); 330 | 331 | // Handle the case when dropping at the end of the list 332 | if (insertIdx < 0 || insertIdx > parentItem.children.Count) 333 | insertIdx = parentItem.children.Count; 334 | 335 | foreach (var d in dragged) 336 | { 337 | parentItem.children.Insert(insertIdx, d); 338 | insertIdx++; 339 | } 340 | 341 | break; 342 | 343 | case DragAndDropPosition.UponItem: 344 | if (parentItem != null && parentItem.IsFolder) 345 | { 346 | foreach (var d in dragged) 347 | { 348 | if (d.parent != null && d.parent.children != null) 349 | d.parent.children.Remove(d); 350 | 351 | d.parent = parentItem; 352 | d.depth = parentItem.depth + 1; 353 | 354 | if (parentItem.children == null) 355 | parentItem.children = new List(); 356 | parentItem.children.Add(d); 357 | } 358 | } 359 | 360 | break; 361 | 362 | case DragAndDropPosition.OutsideItems: 363 | var root = rootItem as StoryboardTreeViewItem; 364 | foreach (var d in dragged) 365 | { 366 | if (d.parent != null && d.parent.children != null) 367 | d.parent.children.Remove(d); 368 | 369 | d.parent = root; 370 | d.depth = root.depth + 1; 371 | 372 | if (root.children == null) 373 | root.children = new List(); 374 | root.children.Add(d); 375 | } 376 | 377 | break; 378 | } 379 | 380 | // Save the updated hierarchy 381 | if (EditorWindow.GetWindow() is UIStoryboardManagerWindow window) 382 | { 383 | window.SaveManagerData(); 384 | } 385 | 386 | Reload(); 387 | return DragAndDropVisualMode.Move; 388 | } 389 | 390 | #endregion 391 | 392 | #region Click 393 | 394 | protected override void DoubleClickedItem(int id) 395 | { 396 | if (_items.TryGetValue(id, out var item)) 397 | { 398 | if (!item.IsFolder) 399 | { 400 | OnDoubleClickStoryboard?.Invoke(item); 401 | } 402 | } 403 | } 404 | 405 | #endregion 406 | 407 | #region Context 408 | 409 | protected override void ContextClickedItem(int id) 410 | { 411 | if (!_items.ContainsKey(id)) return; 412 | var item = _items[id]; 413 | var menu = new GenericMenu(); 414 | 415 | if (!item.RenameMode) 416 | { 417 | menu.AddItem(new GUIContent("Rename"), false, () => 418 | { 419 | item.RenameMode = true; 420 | Reload(); 421 | }); 422 | } 423 | else 424 | { 425 | menu.AddDisabledItem(new GUIContent("Rename")); 426 | } 427 | 428 | if (!item.IsFolder) 429 | { 430 | menu.AddSeparator(""); 431 | AddMoveToFolderMenuItems(menu, item); 432 | } 433 | 434 | menu.AddSeparator(""); 435 | AddDeleteMenuItem(menu, item); 436 | 437 | menu.ShowAsContext(); 438 | } 439 | 440 | private void AddMoveToFolderMenuItems(GenericMenu menu, StoryboardTreeViewItem item) 441 | { 442 | var folders = _items.Values.Where(x => x.IsFolder && x != item).ToList(); 443 | foreach (var folder in folders) 444 | { 445 | menu.AddItem(new GUIContent($"Move to/{folder.DisplayName}"), false, 446 | () => { MoveItemToFolder(item, folder); }); 447 | } 448 | } 449 | 450 | private void AddDeleteMenuItem(GenericMenu menu, StoryboardTreeViewItem item) 451 | { 452 | menu.AddItem(new GUIContent("Delete"), false, () => 453 | { 454 | if (EditorUtility.DisplayDialog("Delete?", 455 | $"Really delete '{item.DisplayName}'?", 456 | "Yes", "No")) 457 | { 458 | DeleteItem(item); 459 | } 460 | }); 461 | } 462 | 463 | private void MoveItemToFolder(StoryboardTreeViewItem item, StoryboardTreeViewItem targetFolder) 464 | { 465 | if (item.parent != null && item.parent.children != null) 466 | { 467 | item.parent.children.Remove(item); 468 | } 469 | 470 | item.parent = targetFolder; 471 | item.depth = targetFolder.depth + 1; 472 | 473 | if (targetFolder.children == null) 474 | { 475 | targetFolder.children = new List(); 476 | } 477 | 478 | targetFolder.children.Add(item); 479 | 480 | if (EditorWindow.GetWindow() is UIStoryboardManagerWindow window) 481 | { 482 | window.SaveManagerData(); 483 | } 484 | 485 | Reload(); 486 | } 487 | 488 | private void DeleteItem(StoryboardTreeViewItem item) 489 | { 490 | if (!item.IsFolder && !string.IsNullOrEmpty(item.AssetPath)) 491 | { 492 | AssetDatabase.DeleteAsset(item.AssetPath); 493 | AssetDatabase.SaveAssets(); 494 | } 495 | 496 | if (item.parent != null && item.parent.children != null) 497 | { 498 | item.parent.children.Remove(item); 499 | } 500 | 501 | _items.Remove(item.id); 502 | 503 | Reload(); 504 | } 505 | 506 | #endregion 507 | 508 | #region CRUD 509 | 510 | /// 511 | /// Create a folder item at (optional) parent. 512 | /// Checks duplication among parent's children. 513 | /// 514 | public StoryboardTreeViewItem CreateFolder(string name, StoryboardTreeViewItem parent = null) 515 | { 516 | if (parent == null) parent = _root; 517 | 518 | // 중복 체크 519 | if (_items.Values.Any(x => x.parent == parent && x.IsFolder && 520 | x.FolderName.Equals(name, StringComparison.OrdinalIgnoreCase))) 521 | { 522 | EditorUtility.DisplayDialog("Error", $"Folder '{name}' already exists!", "OK"); 523 | return null; 524 | } 525 | 526 | var folder = new StoryboardTreeViewItem 527 | { 528 | id = _currentId++, 529 | IsFolder = true, 530 | FolderName = name, 531 | displayName = name, 532 | parent = parent, 533 | depth = parent.depth + 1, 534 | children = new List() 535 | }; 536 | 537 | _items[folder.id] = folder; 538 | 539 | // 부모의 children 리스트 초기화 및 추가 540 | if (parent.children == null) 541 | { 542 | parent.children = new List(); 543 | } 544 | 545 | parent.children.Add(folder); 546 | 547 | Reload(); 548 | return folder; 549 | } 550 | 551 | 552 | /// 553 | /// Create a storyboard item at (optional) parent, referencing an assetPath. 554 | /// 555 | public StoryboardTreeViewItem CreateStoryboard(string name, string assetPath, StoryboardTreeViewItem parent = null) 556 | { 557 | if (parent == null) parent = _root; 558 | 559 | var sb = new StoryboardTreeViewItem 560 | { 561 | id = _currentId++, 562 | IsFolder = false, 563 | StoryboardName = name, 564 | AssetPath = assetPath, 565 | displayName = name, 566 | parent = parent, 567 | depth = parent.depth + 1, 568 | children = new List() 569 | }; 570 | 571 | _items[sb.id] = sb; 572 | 573 | // 부모의 children 리스트 초기화 및 추가 574 | if (parent.children == null) 575 | { 576 | parent.children = new List(); 577 | } 578 | 579 | parent.children.Add(sb); 580 | 581 | Reload(); 582 | return sb; 583 | } 584 | 585 | /// 586 | /// Clear all items (folders + storyboards). 587 | /// 588 | public void ClearAll() 589 | { 590 | _items.Clear(); 591 | rootItem.children?.Clear(); 592 | Reload(); 593 | } 594 | 595 | 596 | /// 597 | /// Refresh external storyboard assets by calling a user-provided function 598 | /// that returns a list of .asset paths. 599 | /// Add any not already in the tree. 600 | /// 601 | public void RefreshAllStoryboards(Func> getAllAssets) 602 | { 603 | if (getAllAssets == null) return; 604 | var allPaths = getAllAssets(); 605 | int added = 0; 606 | foreach (var p in allPaths) 607 | { 608 | bool found = _items.Values.Any(x => !x.IsFolder && x.AssetPath == p); 609 | if (!found) 610 | { 611 | var fileName = Path.GetFileNameWithoutExtension(p); 612 | CreateStoryboard(fileName, p, rootItem as StoryboardTreeViewItem); 613 | added++; 614 | } 615 | } 616 | 617 | if (added > 0) 618 | { 619 | Debug.Log($"Refreshed. Found {added} new storyboards from external assets."); 620 | } 621 | else 622 | { 623 | Debug.Log("No new storyboards found outside the manager."); 624 | } 625 | } 626 | 627 | #endregion 628 | } 629 | } -------------------------------------------------------------------------------- /Editor/StoryboardTreeView.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3ba20b0f1c324730894dbc9047403ce8 3 | timeCreated: 1736404853 -------------------------------------------------------------------------------- /Editor/StoryboardTreeViewItem.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor.IMGUI.Controls; 2 | 3 | namespace com.kwanjoong.unityuistoryboard.Editor 4 | { 5 | public class StoryboardTreeViewItem : TreeViewItem 6 | { 7 | public bool IsFolder; 8 | public bool RenameMode; // Toggle for inline rename 9 | 10 | public string FolderName; // if IsFolder = true 11 | public string StoryboardName; // if !IsFolder 12 | public string AssetPath; // if !IsFolder => .asset path 13 | 14 | public string DisplayName => IsFolder ? FolderName : StoryboardName; 15 | } 16 | } -------------------------------------------------------------------------------- /Editor/StoryboardTreeViewItem.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 10b43733890940039371dddffe418648 3 | timeCreated: 1736404845 -------------------------------------------------------------------------------- /Editor/UIStoryboardGraph.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using XNode; 3 | 4 | namespace com.kwanjoong.unityuistoryboard.Editor 5 | { 6 | [CreateAssetMenu] 7 | public class UIStoryboardGraph : NodeGraph { 8 | 9 | // TODO: Create PageNode, ModalNode context menu 10 | // [ContextMenu("Create PageNode")] 11 | // [ContextMenu("Create ModalNode")] 12 | } 13 | } -------------------------------------------------------------------------------- /Editor/UIStoryboardGraph.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e53c3629090e14839b8d1ecbb7b81533 -------------------------------------------------------------------------------- /Editor/UIStoryboardGraphEditor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using XNodeEditor; 3 | 4 | namespace com.kwanjoong.unityuistoryboard.Editor 5 | { 6 | [CustomNodeGraphEditor(typeof(UIStoryboardGraph))] 7 | public class UIStoryboardGraphEditor : NodeGraphEditor 8 | { 9 | public override string GetNodeMenuName(Type type) 10 | { 11 | // Only show PageNode and ModalNode in the context menu 12 | if (type.Name == nameof(PageNode)) 13 | return "Create/PageNode"; 14 | if (type.Name == nameof(ModalNode)) 15 | return "Create/ModalNode"; 16 | return null; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Editor/UIStoryboardGraphEditor.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 99d199ec88cb14120a051da4afec1491 -------------------------------------------------------------------------------- /Editor/UIStoryboardSettingsProvider.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using UnityEditor; 3 | using UnityEngine; 4 | 5 | namespace com.kwanjoong.unityuistoryboard.Editor 6 | { 7 | /// 8 | /// A custom Project Settings page for "UI Storyboard". 9 | /// If no asset exists: "Create Settings Asset" only. 10 | /// If exists: Show fields (Project Name, Root Path, Addressable Folder), plus "Initialize Project Structure" button. 11 | /// PreloadedAssets registration is ALWAYS done automatically, no toggle shown. 12 | /// Remove Settings functionality is not provided (user cannot delete). 13 | /// 14 | public class UIStoryboardSettingsProvider : SettingsProvider 15 | { 16 | private SerializedObject _serializedObject; 17 | private UIStoryboardSettingsAsset _settingsAsset; 18 | 19 | /// 20 | /// Needed constructor for SettingsProvider. 21 | /// 22 | private UIStoryboardSettingsProvider(string path, SettingsScope scope) 23 | : base(path, scope) 24 | { 25 | } 26 | 27 | /// 28 | /// Registers "Project/UI Storyboard" in Project Settings. 29 | /// 30 | [SettingsProvider] 31 | public static SettingsProvider CreateUIStoryboardSettingsProvider() 32 | { 33 | return new UIStoryboardSettingsProvider("Project/UI Storyboard", SettingsScope.Project) 34 | { 35 | label = "UI Storyboard" 36 | }; 37 | } 38 | 39 | public override void OnActivate(string searchContext, UnityEngine.UIElements.VisualElement rootElement) 40 | { 41 | base.OnActivate(searchContext, rootElement); 42 | RefreshSettings(); 43 | } 44 | 45 | public override void OnGUI(string searchContext) 46 | { 47 | if (_settingsAsset == null) 48 | { 49 | // Asset doesn't exist -> show create button 50 | EditorGUILayout.HelpBox("No UIStoryboardSettingsAsset found in the project.", MessageType.Info); 51 | 52 | if (GUILayout.Button("Create Settings Asset")) 53 | { 54 | var created = UIStoryboardSettings.CreateAsset(); 55 | if (created != null) 56 | { 57 | RefreshSettings(); 58 | } 59 | } 60 | } 61 | else 62 | { 63 | // Asset exists -> show fields + "Initialize" button 64 | if (_serializedObject == null) 65 | _serializedObject = new SerializedObject(_settingsAsset); 66 | 67 | _serializedObject.Update(); 68 | 69 | EditorGUILayout.LabelField("UI Storyboard Settings", EditorStyles.boldLabel); 70 | 71 | EditorGUILayout.PropertyField(_serializedObject.FindProperty("projectName"), 72 | new GUIContent("Project Name")); 73 | EditorGUILayout.PropertyField(_serializedObject.FindProperty("projectRootPath"), 74 | new GUIContent("Project Root Path")); 75 | EditorGUILayout.PropertyField(_serializedObject.FindProperty("addressableRootFolderName"), 76 | new GUIContent("Addressable Root Folder")); 77 | 78 | EditorGUILayout.Space(); 79 | if (GUILayout.Button("Initialize Project Structure")) 80 | { 81 | UIStoryboardSettings.CreateProjectStructure(_settingsAsset); 82 | } 83 | 84 | EditorGUILayout.PropertyField(_serializedObject.FindProperty("canvasReferenceWidth"), 85 | new GUIContent("Canvas Reference Width")); 86 | EditorGUILayout.PropertyField(_serializedObject.FindProperty("canvasReferenceHeight"), 87 | new GUIContent("Canvas Reference Height")); 88 | 89 | _serializedObject.ApplyModifiedProperties(); 90 | } 91 | } 92 | 93 | private void RefreshSettings() 94 | { 95 | _settingsAsset = UIStoryboardSettings.LoadAsset(); 96 | _serializedObject = _settingsAsset ? new SerializedObject(_settingsAsset) : null; 97 | } 98 | } 99 | } 100 | #endif -------------------------------------------------------------------------------- /Editor/UIStoryboardSettingsProvider.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e346612b297854dcb8ef1cdd321d2544 -------------------------------------------------------------------------------- /Editor/com.kwanjoong.unityuistoryboard.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.kwanjoong.unityuistoryboard.Editor", 3 | "rootNamespace": "com.kwanjoong.unityuistoryboard", 4 | "references": [ 5 | "com.kwanjoong.unityuistoryboard", 6 | "XNode", 7 | "XNodeEditor", 8 | "ScreenSystem", 9 | "UnityScreenNavigator", 10 | "VContainer" 11 | ], 12 | "includePlatforms": [ 13 | "Editor" 14 | ], 15 | "excludePlatforms": [], 16 | "allowUnsafeCode": false, 17 | "overrideReferences": false, 18 | "precompiledReferences": [], 19 | "autoReferenced": true, 20 | "defineConstraints": [], 21 | "versionDefines": [], 22 | "noEngineReferences": false 23 | } -------------------------------------------------------------------------------- /Editor/com.kwanjoong.unityuistoryboard.Editor.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c6da04d50592646d29df25a711dac3d7 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2025 KwanJoong Lee 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: a0b59e09aa0b441368bc4a0cc6a76dab 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unity UI Storyboard 2 | 3 | **Unity UI Storyboard** is a powerful collaboration and organization tool designed for creating and managing complex UI structures in Unity. It simplifies the process of designing, building, and maintaining structured UI systems, ensuring seamless collaboration between team members such as planners, UI/UX designers, and programmers. 4 | 5 | ![Manager.png](Documentation~/Manager.png) 6 | ![Storyboard.png](Documentation~/Storyboard.png) 7 | 8 | ## Why Use Unity UI Storyboard? 9 | 10 | When working on games or apps with intricate UI structures, the biggest challenges are: 11 | - Defining a clear and consistent architecture. 12 | - Establishing and maintaining design rules. 13 | - Facilitating effective communication across multiple disciplines. 14 | 15 | Unity UI Storyboard addresses these challenges by providing a **well-structured project template** to kickstart your UI development. It helps maintain order throughout the project lifecycle, allowing teams to focus on their core tasks without constantly revisiting basic structural decisions. 16 | 17 | --- 18 | 19 | ## Design Philosophy 20 | 21 | Unity UI Storyboard is built to support collaborative workflows and structured development. Here's what it enables: 22 | 23 | ### Key Features: 24 | - Designed with **practical Clean Code principles**, promoting readability and maintainability. 25 | - Incorporates **Dependency Injection**, enabling flexible and decoupled component management. 26 | - Supports **view-level testing**, ensuring that each UI element can be independently verified and debugged. 27 | - Provides a structure that simplifies applying these principles, helping teams implement clean, testable, and scalable UI designs effortlessly. 28 | 29 | ### What You Can Do: 30 | - **Planners** can conceptualize and annotate UI prototypes as storyboards. 31 | - **Designers** can craft visual layouts based on these prototypes. 32 | - **Programmers** can implement the necessary logic based on the structured views. 33 | - All team members can easily **review, verify, and refine** the UI design together. 34 | 35 | ### What It Doesn't Do: 36 | - It does not automatically generate working UI elements by simply connecting nodes or creating layouts without code. Coding and implementation are still essential parts of the process. 37 | 38 | --- 39 | 40 | For further documentation and usage guides, refer to the official [documentation](https://kwanjoong-dev.gitbook.io/unity-ui-storyboard). 41 | 42 | --- -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4a14c9e7eb77d424991bc295b49af477 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Runtime.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 2b458d361657346a6b179445a7352c00 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Runtime/UIStoryboardSettings.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR 2 | using UnityEditor; 3 | #endif 4 | using System; 5 | using System.IO; 6 | using System.Linq; 7 | using UnityEngine; 8 | using Object = UnityEngine.Object; 9 | 10 | namespace com.kwanjoong.unityuistoryboard 11 | { 12 | /// 13 | /// Static helper class that: 14 | /// - Loads/Creates the UIStoryboardSettingsAsset via AssetDatabase 15 | /// - Always registers it in Preloaded Assets 16 | /// - Creates the project structure (folders + asmdef) on demand 17 | /// 18 | public static class UIStoryboardSettings 19 | { 20 | #if UNITY_EDITOR 21 | 22 | #region Folder Names 23 | private const string CoreFolder = "Core"; 24 | private const string UIFolder = "UI"; 25 | private const string LifetimeScopeFolder = "LifetimeScope"; 26 | private const string ModelFolder = "Model"; 27 | private const string PresentationFolder = "Presentation"; 28 | private const string ViewFolder = "View"; 29 | private const string GatewayFolder = "Gateway"; 30 | private const string RepositoryFolder = "Repository"; 31 | private const string UseCaseFolder = "UseCase"; 32 | private const string OutGameFolder = "OutGame"; 33 | private const string RuntimeFolder = "Runtime"; 34 | private const string BuilderFolder = "Builder"; 35 | private const string PresenterFolder = "Presenter"; 36 | #endregion 37 | 38 | #region Reference Names 39 | private const string UniTaskRef = "UniTask"; 40 | private const string UniTaskLinqRef = "UniTask.Linq"; 41 | private const string UniTaskTextMeshProRef = "UniTask.TextMeshPro"; 42 | private const string VContainerRef = "VContainer"; 43 | private const string MessagePipeRef = "MessagePipe"; 44 | private const string MessagePipeVContainerRef = "MessagePipe.VContainer"; 45 | private const string UnityScreenNavigatorRef = "UnityScreenNavigator"; 46 | private const string ScreenSystemRef = "ScreenSystem"; 47 | private const string TextMeshProRef = "Unity.TextMeshPro"; 48 | 49 | private const string PresentationRef = "OutGame.Runtime.UI.Presentation"; 50 | private const string UILifetimeScopeRef = "OutGame.Runtime.UI.LifetimeScope"; 51 | private const string ViewRef = "OutGame.Runtime.UI.View"; 52 | private const string ModelRef = "OutGame.Runtime.UI.Model"; 53 | 54 | private const string UseCaseRef = "OutGame.Runtime.Core.UseCase"; 55 | private const string GatewayRef = "OutGame.Runtime.Core.Gateway"; 56 | private const string RepositoryRef = "OutGame.Runtime.Core.Repository"; 57 | private const string CoreLifetimeScopeRef = "OutGame.Runtime.Core.LifetimeScope"; 58 | #endregion 59 | 60 | #region Core References 61 | private static readonly string[] CoreGatewayRefs = { UniTaskRef, UniTaskLinqRef, VContainerRef }; 62 | private static readonly string[] CoreLifetimeScopeRefs = { VContainerRef, MessagePipeRef, MessagePipeVContainerRef, 63 | UnityScreenNavigatorRef, ScreenSystemRef, PresentationRef, UseCaseRef, GatewayRef, RepositoryRef }; 64 | private static readonly string[] RepositoryRefs = { VContainerRef, UniTaskRef, UniTaskLinqRef, GatewayRef }; 65 | private static readonly string[] UseCaseRefs = { VContainerRef, UniTaskRef, UniTaskLinqRef, RepositoryRef }; 66 | #endregion 67 | 68 | #region UI References 69 | private static readonly string[] UILifetimeScopeRefs = 70 | { 71 | UnityScreenNavigatorRef, ScreenSystemRef, VContainerRef, MessagePipeRef, MessagePipeVContainerRef, ViewRef, 72 | PresentationRef, UseCaseRef 73 | }; 74 | private static readonly string[] ModelRefs = { UniTaskRef, UniTaskLinqRef }; 75 | private static readonly string[] PresentationRefs = 76 | { 77 | UniTaskRef, UniTaskLinqRef, UnityScreenNavigatorRef, ScreenSystemRef, VContainerRef, ViewRef, ModelRef, 78 | UseCaseRef 79 | }; 80 | private static readonly string[] ViewRefs = 81 | { 82 | UniTaskRef, UniTaskLinqRef, UniTaskTextMeshProRef, ScreenSystemRef, TextMeshProRef, UnityScreenNavigatorRef, 83 | ModelRef 84 | }; 85 | #endregion 86 | 87 | 88 | /// 89 | /// Loads the first found UIStoryboardSettingsAsset from the project, or null if none exist. 90 | /// 91 | public static UIStoryboardSettingsAsset LoadAsset() 92 | { 93 | return LoadFromAssetDatabase(); 94 | } 95 | 96 | /// 97 | /// Opens a SaveFilePanelInProject to create a new settings asset, then automatically registers it in PreloadedAssets. 98 | /// If an asset already exists, throws an exception. 99 | /// 100 | public static UIStoryboardSettingsAsset CreateAsset() 101 | { 102 | var existing = LoadAsset(); 103 | if (existing != null) 104 | { 105 | var path = AssetDatabase.GetAssetPath(existing); 106 | throw new InvalidOperationException( 107 | $"{nameof(UIStoryboardSettingsAsset)} already exists at '{path}'."); 108 | } 109 | 110 | // Prompt user for the asset save location 111 | var assetPath = EditorUtility.SaveFilePanelInProject( 112 | "Save UIStoryboardSettingsAsset", 113 | nameof(UIStoryboardSettingsAsset), 114 | "asset", 115 | "Select where to save the UI Storyboard Settings asset.", 116 | "Assets" 117 | ); 118 | 119 | if (string.IsNullOrEmpty(assetPath)) 120 | return null; // user canceled 121 | 122 | return CreateAssetAtPath(assetPath); 123 | } 124 | 125 | /// 126 | /// Creates the asset at a given path, then automatically registers it in PreloadedAssets. 127 | /// 128 | private static UIStoryboardSettingsAsset CreateAssetAtPath(string assetPath) 129 | { 130 | if (string.IsNullOrEmpty(assetPath)) 131 | throw new ArgumentNullException(nameof(assetPath)); 132 | 133 | var instance = ScriptableObject.CreateInstance(); 134 | AssetDatabase.CreateAsset(instance, assetPath); 135 | AssetDatabase.SaveAssets(); 136 | 137 | // Always register in PreloadedAssets (no toggle). 138 | RegisterToPreloadedAssets(); 139 | 140 | return instance; 141 | } 142 | 143 | /// 144 | /// Registers the settings asset in Preloaded Assets to ensure it's loaded at runtime. 145 | /// 146 | private static void RegisterToPreloadedAssets() 147 | { 148 | var asset = LoadAsset(); 149 | if (asset == null) 150 | return; 151 | 152 | var preloaded = PlayerSettings.GetPreloadedAssets().ToList(); 153 | if (!preloaded.Contains(asset)) 154 | { 155 | preloaded.Add(asset); 156 | PlayerSettings.SetPreloadedAssets(preloaded.ToArray()); 157 | AssetDatabase.SaveAssets(); 158 | } 159 | } 160 | 161 | /// 162 | /// Creates folder + asmdef structure based on the user's settings (ProjectName, RootPath, etc.). 163 | /// 164 | public static void CreateProjectStructure(UIStoryboardSettingsAsset settings) 165 | { 166 | if (settings == null) 167 | { 168 | Debug.LogError("[UIStoryboard] Settings asset is null. Cannot create structure."); 169 | return; 170 | } 171 | 172 | string rootPath = Path.Combine(settings.ProjectRootPath, settings.ProjectName); 173 | CreateFolderIfNotExist(rootPath); 174 | 175 | // Example subfolders: OutGame/Runtime/Core, OutGame/Runtime/UI, etc. 176 | // (Adjust as needed) 177 | string outGamePath = Path.Combine(rootPath, OutGameFolder); 178 | CreateFolderIfNotExist(outGamePath); 179 | 180 | string runtimePath = Path.Combine(outGamePath, RuntimeFolder); 181 | CreateFolderIfNotExist(runtimePath); 182 | 183 | // Core 184 | string corePath = Path.Combine(runtimePath, CoreFolder); 185 | CreateFolderIfNotExist(corePath); 186 | CreateFolderIfNotExist(Path.Combine(corePath, GatewayFolder)); 187 | CreateFolderIfNotExist(Path.Combine(corePath, LifetimeScopeFolder)); 188 | CreateFolderIfNotExist(Path.Combine(corePath, RepositoryFolder)); 189 | CreateFolderIfNotExist(Path.Combine(corePath, UseCaseFolder)); 190 | 191 | // UI 192 | string uiPath = Path.Combine(runtimePath, UIFolder); 193 | CreateFolderIfNotExist(uiPath); 194 | CreateFolderIfNotExist(Path.Combine(uiPath, LifetimeScopeFolder)); 195 | CreateFolderIfNotExist(Path.Combine(uiPath, ModelFolder)); 196 | CreateFolderIfNotExist(Path.Combine(uiPath, PresentationFolder)); 197 | 198 | string presentationPath = Path.Combine(uiPath, PresentationFolder); 199 | CreateFolderIfNotExist(presentationPath); 200 | CreateFolderIfNotExist(Path.Combine(presentationPath, BuilderFolder)); 201 | CreateFolderIfNotExist(Path.Combine(presentationPath, PresenterFolder)); 202 | 203 | // Addressable folder 204 | if (!string.IsNullOrEmpty(settings.AddressableRootFolderName)) 205 | { 206 | string addrPath = Path.Combine(settings.ProjectRootPath, settings.AddressableRootFolderName); 207 | CreateFolderIfNotExist(addrPath); 208 | } 209 | 210 | // Create asmdef 211 | CreateAsmdefFiles(corePath, uiPath); 212 | 213 | AssetDatabase.Refresh(); 214 | Debug.Log("[UIStoryboard] Project structure initialization complete!"); 215 | } 216 | 217 | /// 218 | /// Example logic to create .asmdef in each folder. 219 | /// References are placeholders; adapt to your real dependencies. 220 | /// 221 | private static void CreateAsmdefFiles(string corePath, string uiPath) 222 | { 223 | // UI 224 | CreateAsmdef(Path.Combine(uiPath, LifetimeScopeFolder), UILifetimeScopeRef, UILifetimeScopeRefs); 225 | CreateAsmdef(Path.Combine(uiPath, ModelFolder), ModelRef, ModelRefs); 226 | CreateAsmdef(Path.Combine(uiPath, PresentationFolder), PresentationRef, PresentationRefs); 227 | CreateAsmdef(Path.Combine(uiPath, ViewFolder), ViewRef, ViewRefs); 228 | 229 | // Core 230 | CreateAsmdef(Path.Combine(corePath, GatewayFolder), GatewayRef, CoreGatewayRefs); 231 | CreateAsmdef(Path.Combine(corePath, LifetimeScopeFolder), CoreLifetimeScopeRef, CoreLifetimeScopeRefs); 232 | CreateAsmdef(Path.Combine(corePath, RepositoryFolder), RepositoryRef, RepositoryRefs); 233 | CreateAsmdef(Path.Combine(corePath, UseCaseFolder), UseCaseRef, UseCaseRefs); 234 | } 235 | 236 | private static void CreateAsmdef(string folderPath, string assemblyName, string[] references) 237 | { 238 | if (!Directory.Exists(folderPath)) 239 | { 240 | Directory.CreateDirectory(folderPath); 241 | } 242 | 243 | string asmdefPath = Path.Combine(folderPath, assemblyName + ".asmdef"); 244 | if (File.Exists(asmdefPath)) 245 | { 246 | Debug.Log($"[UIStoryboard] Asmdef already exists: {asmdefPath}"); 247 | return; 248 | } 249 | 250 | // Minimal .asmdef JSON 251 | var data = new AsmdefData 252 | { 253 | name = assemblyName, 254 | references = references ?? new string[0], 255 | autoReferenced = true 256 | }; 257 | 258 | string json = JsonUtility.ToJson(data, true); 259 | File.WriteAllText(asmdefPath, json); 260 | Debug.Log($"[UIStoryboard] Created asmdef: {asmdefPath}"); 261 | } 262 | 263 | private static void CreateFolderIfNotExist(string path) 264 | { 265 | if (!Directory.Exists(path)) 266 | { 267 | Directory.CreateDirectory(path); 268 | Debug.Log($"[UIStoryboard] Created folder: {path}"); 269 | } 270 | } 271 | 272 | [Serializable] 273 | private class AsmdefData 274 | { 275 | public string name; 276 | public string[] references; 277 | public bool autoReferenced; 278 | } 279 | 280 | /// 281 | /// Generic method to find a single asset of type T in the project. 282 | /// 283 | private static T LoadFromAssetDatabase() where T : Object 284 | { 285 | var guids = AssetDatabase.FindAssets($"t:{typeof(T).Name}"); 286 | foreach (var guid in guids) 287 | { 288 | var path = AssetDatabase.GUIDToAssetPath(guid); 289 | var loaded = AssetDatabase.LoadAssetAtPath(path); 290 | if (loaded != null) 291 | return loaded; 292 | } 293 | return null; 294 | } 295 | #endif 296 | } 297 | } -------------------------------------------------------------------------------- /Runtime/UIStoryboardSettings.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 00635e470bc834cba8bcf3fdaa4d6a68 -------------------------------------------------------------------------------- /Runtime/UIStoryboardSettingsAsset.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEngine; 3 | 4 | namespace com.kwanjoong.unityuistoryboard 5 | { 6 | /// 7 | /// Main ScriptableObject for UI Storyboard settings (used by both Editor and Runtime). 8 | /// User sets "ProjectName", "ProjectRootPath", and "AddressableRootFolderName" in Project Settings. 9 | /// 10 | [Serializable] 11 | public sealed class UIStoryboardSettingsAsset : ScriptableObject 12 | { 13 | [SerializeField] private string projectName = "ProjectName"; 14 | [SerializeField] private string projectRootPath = "Assets"; 15 | [SerializeField] private string addressableRootFolderName = "Prefabs"; 16 | 17 | [SerializeField] private int canvasReferenceWidth = 1080; 18 | [SerializeField] private int canvasReferenceHeight = 1920; 19 | 20 | /// 21 | /// The top-level folder name (e.g. "SampleProject"). 22 | /// 23 | public string ProjectName 24 | { 25 | get => projectName; 26 | set => projectName = value; 27 | } 28 | 29 | /// 30 | /// Usually "Assets". 31 | /// 32 | public string ProjectRootPath 33 | { 34 | get => projectRootPath; 35 | set => projectRootPath = value; 36 | } 37 | 38 | /// 39 | /// The folder name for Addressable assets (e.g. "Prefabs"). 40 | /// 41 | public string AddressableRootFolderName 42 | { 43 | get => addressableRootFolderName; 44 | set => addressableRootFolderName = value; 45 | } 46 | 47 | /// 48 | /// The reference resolution width for uGUI CanvasScaler. 49 | /// 50 | public int CanvasReferenceWidth 51 | { 52 | get => canvasReferenceWidth; 53 | set => canvasReferenceWidth = value; 54 | } 55 | 56 | /// 57 | /// The reference resolution height for uGUI CanvasScaler. 58 | /// 59 | public int CanvasReferenceHeight 60 | { 61 | get => canvasReferenceHeight; 62 | set => canvasReferenceHeight = value; 63 | } 64 | 65 | // -------------------------------------------------------------------------------- 66 | // Runtime/Editor Singleton Access 67 | // -------------------------------------------------------------------------------- 68 | 69 | private static UIStoryboardSettingsAsset _instance; 70 | 71 | public static UIStoryboardSettingsAsset Instance 72 | { 73 | get 74 | { 75 | #if UNITY_EDITOR 76 | if (_instance == null) 77 | _instance = UIStoryboardSettings.LoadAsset(); 78 | #else 79 | // In a built player or at runtime, if not preloaded, fallback to a new in-memory instance. 80 | if (_instance == null) 81 | Debug.LogError("UIStoryboardSettings scriptable object not found in preloaded assets."); 82 | #endif 83 | return _instance; 84 | } 85 | } 86 | 87 | private void OnEnable() 88 | { 89 | #if !UNITY_EDITOR 90 | // If this asset is in PreloadedAssets at runtime, ensure the static instance points here. 91 | _instance = this; 92 | #endif 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Runtime/UIStoryboardSettingsAsset.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 32d3aa930fc2a4ebc917daa94fd7d3f0 -------------------------------------------------------------------------------- /Runtime/com.kwanjoong.unityuistoryboard.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.kwanjoong.unityuistoryboard", 3 | "rootNamespace": "com.kwanjoong.unityuistoryboard", 4 | "references": [], 5 | "includePlatforms": [], 6 | "excludePlatforms": [], 7 | "allowUnsafeCode": false, 8 | "overrideReferences": false, 9 | "precompiledReferences": [], 10 | "autoReferenced": true, 11 | "defineConstraints": [], 12 | "versionDefines": [], 13 | "noEngineReferences": false 14 | } -------------------------------------------------------------------------------- /Runtime/com.kwanjoong.unityuistoryboard.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e76c8b37765af4504b3dcec8edc06b3b 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.kwanjoong.unityuistoryboard", 3 | "version": "0.1.1", 4 | "displayName": "Unity UI Storyboard", 5 | "description": "A Unity package for structured UI development.", 6 | "unity": "2022.3", 7 | "documentationUrl": "https://kwanjoong-dev.gitbook.io/unity-ui-storyboard", 8 | "keywords": [ 9 | "ugui", 10 | "storyboard", 11 | "Clean Architecture", 12 | "Simple Sclean Architecture", 13 | "Onion Architecture" 14 | ], 15 | "author": { 16 | "name": "Kwanjoong Lee", 17 | "url": "https://github.com/kwan3854" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/kwan3854/UnityUIStoryboard.git" 22 | }, 23 | "dependencies": { 24 | "com.cysharp.unitask": "2.5.0", 25 | "jp.hadashikick.vcontainer": "1.6.0", 26 | "com.harumak.unityscreennavigator": "1.7.0", 27 | "com.kwanjoong.screensystem": "0.1.0", 28 | "com.cysharp.messagepipe": "1.8.0", 29 | "com.cysharp.messagepipe.vcontainer": "1.8.0", 30 | "com.harumak.upalette": "2.5.0", 31 | "com.github.siccity.xnode": "1.8.0" 32 | }, 33 | "license": "MIT" 34 | } -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 68a64239c71714c10973838807766d59 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | --------------------------------------------------------------------------------