├── .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