using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization.Json; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Cysharp.Threading.Tasks; using UnityEditor; using UnityEditor.AnimatedValues; using UnityEngine; using UnityEngine.UI; using VketCloudGUITools.Runtime; using VketCloudGUITools.Serialization; namespace VketCloudGUITools.Editor { internal class VCEditorCanvasImporter : EditorWindow { private const float Inch2Meter = 0.0254f; private const string prefix = "VketCloudGUITools_Importer_"; private const string dataPathKey = prefix + "DataPath"; private const string recentDataPathListKey = prefix + "RecentDataPathList"; private const string assetOutputPathKey = prefix + "AssetOutputPath"; private const string dotPerSettingKey = prefix + "DotPerSetting"; private const string toggleDotPerModeKey = prefix + "ToggleDotPerMode"; [System.Serializable] public class DotPerSettingSavedata { public DotPerMode dotPerMode; public float value; public DotPerSettingSavedata(DotPerMode dotPerMode, float value) { this.dotPerMode = dotPerMode; this.value = value; } public DotPerSettingSavedata(VCEditorCanvasImporter importer) { SaveSetting(importer); } public void SaveSetting(VCEditorCanvasImporter importer) { dotPerMode = importer.dotPerMode; switch (dotPerMode) { case DotPerMode.DPI: value = importer.dpi; break; case DotPerMode.DPM: value = importer.dpm; break; case DotPerMode.LocalScale: value = importer.localScale; break; } } public void LoadSetting(VCEditorCanvasImporter impoter) { impoter.dotPerMode = dotPerMode; switch (dotPerMode) { case DotPerMode.DPI: impoter.dpi = value; break; case DotPerMode.DPM: impoter.dpm = value; break; case DotPerMode.LocalScale: impoter.localScale = value; break; } } public string ToToggleLabel() { return $"{dotPerMode} : {value}"; } } enum ScaleMode { WorldSpace, VRView, DesktopScreen, } public enum DotPerMode { [InspectorName("DPI ( Dots Per Inch )")] DPI, [InspectorName("DPM ( Dots Per Meter )")] DPM, [InspectorName("Local Scale( inverse DPM )")] LocalScale, } enum DPITemplate { [InspectorName("85インチモニター/8K")] Inch85Monitor_8K, [InspectorName("85インチモニター/4K")] Inch85Monitor_4K, [InspectorName("75インチモニター/8K")] Inch75Monitor_8K, [InspectorName("75インチモニター/4K")] Inch75Monitor_4K, [InspectorName("24インチモニター/8K")] Inch24Monitor_8K, [InspectorName("24インチモニター/DCI 4K")] Inch24Monitor_DCI4K, [InspectorName("24インチモニター/4K")] Inch24Monitor_4K, [InspectorName("24インチモニター/WQHD")] Inch24Monitor_WQHD, [InspectorName("24インチモニター/WUXGA")] Inch24Monitor_WUXGA, [InspectorName("24インチモニター/Full HD")] Inch24Monitor_FullHD, [InspectorName("24インチモニター/UXGA")] Inch24Monitor_UXGA, [InspectorName("24インチモニター/SXGA")] Inch24Monitor_SXGA, [InspectorName("24インチモニター/XGA")] Inch24Monitor_XGA, [InspectorName("24インチモニター/SVGA")] Inch24Monitor_SVGA, [InspectorName("24インチモニター/VGA")] Inch24Monitor_VGA, [InspectorName("iPad Pro/12.9''")] iPadPro_12_9, [InspectorName("iPad Pro/11''")] iPadPro_11, [InspectorName("iPhone/13 & 12 Max")] iPhone13_or_12_Max, [InspectorName("iPhone/11 (Max & XS Max)")] iPhone11_Max_or_XSMax, [InspectorName("Simple/DPI = 1000")] Simple_DPI1000, [InspectorName("Simple/DPI = 500")] Simple_DPI500, [InspectorName("Simple/DPI = 200")] Simple_DPI200, [InspectorName("Simple/DPI = 100")] Simple_DPI100, [InspectorName("Simple/DPI = 50")] Simple_DP50, [InspectorName("Simple/DPI = 1024")] Simple_DPI1024, [InspectorName("Simple/DPI = 512")] Simple_DPI512, [InspectorName("Simple/DPI = 256")] Simple_DPI256, [InspectorName("Simple/DPI = 128")] Simple_DPI128, [InspectorName("Simple/DPI = 64")] Simple_DPI64, } private AnimBool toggleDotPerMode = new AnimBool(); [SerializeField] private string dataPath = string.Empty; [SerializeField] private List recentDataPathList = new List(); [SerializeField] private string assetOutputPath = string.Empty; [SerializeField] private ScaleMode scaleMode = ScaleMode.DesktopScreen; [SerializeField] private DotPerMode dotPerMode = DotPerMode.DPI; [SerializeField] private DPITemplate dpiTemplate = DPITemplate.iPhone13_or_12_Max; [SerializeField] private float dpi = 256; [SerializeField] private float dpm = 1f; [SerializeField] private float localScale = 1f; [SerializeField] private Transform parentTransform; [SerializeField] private bool advancedOptions = false; [MenuItem("VketCloudGUITools/GUI Importer")] private static void Create() { GetWindow("VketCloud GUI Importer"); } private void OnEnable() { dataPath = EditorPrefs.GetString(dataPathKey); EditorJsonUtility.FromJsonOverwrite(EditorPrefs.GetString(recentDataPathListKey), recentDataPathList); assetOutputPath = EditorPrefs.GetString(assetOutputPathKey); var dotPerSetting = new DotPerSettingSavedata(DotPerMode.DPM, 256f); EditorJsonUtility.FromJsonOverwrite(EditorPrefs.GetString(dotPerSettingKey), dotPerSetting); dotPerSetting.LoadSetting(this); toggleDotPerMode.value = EditorPrefs.GetBool(toggleDotPerModeKey, false); toggleDotPerMode.valueChanged.AddListener(Repaint); } private void OnDisable() { EditorPrefs.SetString(dataPathKey, dataPath); EditorPrefs.SetString(recentDataPathListKey, EditorJsonUtility.ToJson(recentDataPathList)); EditorPrefs.SetString(assetOutputPathKey, assetOutputPath); EditorPrefs.SetString(dotPerSettingKey, EditorJsonUtility.ToJson(new DotPerSettingSavedata(this))); EditorPrefs.SetBool(toggleDotPerModeKey, toggleDotPerMode.target); toggleDotPerMode.valueChanged.RemoveAllListeners(); } private void OnGUI() { // Import Path using (new EditorGUILayout.HorizontalScope()) { dataPath = EditorGUILayout.TextField("ImportCanvas Path", dataPath); if (GUILayout.Button("Select File", GUILayout.ExpandWidth(false))) { ButtonDataPathSelectFilePress(); } if (GUILayout.Button("Select Recent", GUILayout.ExpandWidth(false))) { ButtonDataPathSelectRecentPress(); } } // Import Path Error Messasges if (!System.IO.File.Exists(dataPath)) { EditorGUILayout.HelpBox("File Not Found", MessageType.Warning); } if (System.IO.Path.GetExtension(dataPath) != ".json") { EditorGUILayout.HelpBox("File Extention is Not .josn", MessageType.Warning); } // Advanced Options advancedOptions = EditorGUILayout.BeginFoldoutHeaderGroup(advancedOptions, "Advanced Options"); if (advancedOptions) { EditorGUI.indentLevel++; // Asset Output Path using (new EditorGUILayout.HorizontalScope()) { assetOutputPath = EditorGUILayout.TextField("Assets Output Path", assetOutputPath); if (GUILayout.Button("Select File", GUILayout.ExpandWidth(false))) { var newAssetOutputPath = EditorUtility.OpenFolderPanel("Import Asset Output", assetOutputPath, string.Empty); if (IsDataPath(newAssetOutputPath)) { newAssetOutputPath = "Assets" + StlipDataPath(newAssetOutputPath); } // CancelするとEmptyが返る。OKしたときのみ反映する。 if (!string.IsNullOrEmpty(newAssetOutputPath)) { assetOutputPath = newAssetOutputPath; } } } // Scale Mode { scaleMode = (ScaleMode)EditorGUILayout.EnumPopup("Scale Mode", scaleMode); EditorGUILayout.HelpBox(GetScaleModeTooltip(scaleMode), MessageType.None); } EditorGUILayout.Space(); toggleDotPerMode.target = EditorGUILayout.Foldout(toggleDotPerMode.target, new DotPerSettingSavedata(this).ToToggleLabel()); using (var toggleScope = new EditorGUILayout.FadeGroupScope(toggleDotPerMode.faded)) { if (toggleScope.visible) { DrawDotPerSettings(); if (toggleDotPerMode.isAnimating) Repaint(); } } EditorGUILayout.Space(); parentTransform = (Transform)EditorGUILayout.ObjectField("Parent", parentTransform, typeof(Transform), allowSceneObjects: true); EditorGUI.indentLevel--; } EditorGUILayout.EndFoldoutHeaderGroup(); if (GUILayout.Button("Import")) { _ = ImportButtonPressAsync(); } if (importProgress != null) { string debugText = importProgress.ToDebugInfo(); GUILayout.TextArea(debugText); } } private void ButtonDataPathSelectRecentPress() { GenericMenu gm = new GenericMenu(); foreach (var resentDataPath in recentDataPathList) { gm.AddItem(new GUIContent(resentDataPath.Replace('/', '\\')), resentDataPath == dataPath, () => OnSelectRecentFilePath(resentDataPath)); } if (recentDataPathList.Count == 0) { gm.AddDisabledItem(new GUIContent("No File Imported.")); } gm.AddSeparator(""); gm.AddItem(new GUIContent("Options.../Clear/Clear All"), false, () => recentDataPathList.Clear()); gm.AddItem(new GUIContent("Options.../Clear/Clear Deleted File Path"), false, () => recentDataPathList.RemoveAll(path => !System.IO.File.Exists(path))); gm.ShowAsContext(); } private void ButtonDataPathSelectFilePress() { string directoryPath = string.Empty; try { directoryPath = System.IO.Path.GetDirectoryName(dataPath); } catch // GetDirectoryName throws exeption, when path is invalid. { directoryPath = dataPath; } var newDataPath = EditorUtility.OpenFilePanel("Import Canvas JSON", directoryPath, "json"); // CancelするとEmptyが返る。OKしたときのみ反映する。 if (!string.IsNullOrEmpty(newDataPath)) { dataPath = newDataPath; } } private async UniTask ImportButtonPressAsync() { try { var canvasGameObject = await DoImport(); var canvas = canvasGameObject.GetComponent(); // RenderMode設定 if (scaleMode == ScaleMode.DesktopScreen) { canvas.renderMode = RenderMode.ScreenSpaceOverlay; } else if (scaleMode == ScaleMode.VRView || scaleMode == ScaleMode.WorldSpace) { canvas.renderMode = RenderMode.WorldSpace; } // DPI設定 if (scaleMode == ScaleMode.VRView || scaleMode == ScaleMode.WorldSpace) { canvas.transform.localScale = Vector3.one / (dpi / Inch2Meter); } canvas.transform.SetParent(parentTransform, worldPositionStays: true); canvas.transform.localPosition = Vector3.zero; canvas.transform.localRotation = Quaternion.identity; Debug.Log("Canvas Import Success."); } catch (Exception e) { Debug.LogError("Canvas Import Error."); Debug.LogException(e); } // 最近インポートしたファイル名の一覧に追加 if (!recentDataPathList.Contains(dataPath)) { recentDataPathList.Add(dataPath); } // もっとも古いファイル名を一覧から削除 while (recentDataPathList.Count > 10) { recentDataPathList.RemoveAt(1); } } /// /// DPI設定機能グループを描画 /// private void DrawDotPerSettings() { using (new EditorGUILayout.HorizontalScope(GUI.skin.box)) { using (new EditorGUI.DisabledGroupScope(scaleMode == ScaleMode.DesktopScreen)) { using (new EditorGUILayout.VerticalScope()) { dotPerMode = (DotPerMode)EditorGUILayout.EnumPopup("Dot Per Mode", dotPerMode); EditorGUILayout.HelpBox(GetDotPerModeTooltip(dotPerMode), MessageType.None); // 現在のDPI種類から、他のDPI設定を逆算 switch (dotPerMode) { case DotPerMode.DPI: dpm = dpi / Inch2Meter; localScale = 1f / dpm; break; case DotPerMode.DPM: dpi = dpm * Inch2Meter; localScale = 1f / dpm; break; case DotPerMode.LocalScale: dpm = 1f / localScale; dpi = dpm * Inch2Meter; break; } } EditorGUILayout.Space(); // 各種DPI系を表示 using (new EditorGUILayout.VerticalScope()) { // テンプレートからDPIを選択 EditorGUI.BeginChangeCheck(); dpiTemplate = (DPITemplate)EditorGUILayout.EnumPopup("DPI Template", dpiTemplate); if (EditorGUI.EndChangeCheck()) { dpi = GetDPI(dpiTemplate); dpm = dpi / Inch2Meter; localScale = 1f / dpm; } EditorGUILayout.Space(); EditorGUI.BeginDisabledGroup(dotPerMode != DotPerMode.DPI); dpi = EditorGUILayout.FloatField("DPI (Dot Per Inch)", dpi); EditorGUI.EndDisabledGroup(); EditorGUI.BeginDisabledGroup(dotPerMode != DotPerMode.DPM); dpm = EditorGUILayout.FloatField("DPM (Dot Per Meter)", dpm); EditorGUI.EndDisabledGroup(); EditorGUI.BeginDisabledGroup(dotPerMode != DotPerMode.LocalScale); localScale = EditorGUILayout.FloatField("Local Scale", localScale); EditorGUI.EndDisabledGroup(); } } } } private void OnSelectRecentFilePath(string selectedDataPath) { dataPath = selectedDataPath; } ImportProgress importProgress = null; private async UniTask DoImport() { // Editorなので処理落ちを気にしない設定に var settings = VCGUICanvasImporterEditorSettings.EditorDefaults(dataPath, assetOutputPath); var path = dataPath; if (string.IsNullOrEmpty(path)) { return null; } string jsonData = string.Empty; using (var fs = new FileStream(dataPath, FileMode.Open)) { using (var sr = new StreamReader(fs)) { jsonData = sr.ReadToEnd(); } } var canvas = await VCCanvasImporter.ImportAsync(jsonData, parentTransform, settings, newProgress => importProgress = newProgress); return canvas.gameObject; } static string GetScaleModeTooltip(ScaleMode scaleMode) { switch (scaleMode) { case ScaleMode.WorldSpace: return "ワールドに配置されるUI用です。\nスケールはDPIを見て、現実のメートル換算で変換されます。"; case ScaleMode.VRView: return "[未実装] VRユーザーの視界に追従するUI用です。\nVRユーザーの身長に応じて、スケールが補正されます。"; case ScaleMode.DesktopScreen: return "Desktopユーザーに追従するUI用です。\nワールドとは関係なく、画面に覆いかぶさります。"; } return "this cant be happening!"; } static string GetDotPerModeTooltip(DotPerMode scaleMode) { switch (scaleMode) { case DotPerMode.DPI: return "DPI (Dot Per Inch)\nほとんどの現実のモニターで使われる、1インチ当たりのドット数の基準値です。\n利点:リアルモニターのカタログスペックを入力するだけで良い。\n   1インチはおよそ「指1本の太さ」なので、指1本あたり何ドットか体感的にわかる。\n   注意:ガタイの良いアメリカ成人男性基準\n欠点:Unityはメートル法なので、出力される倍率は直観的でない。"; case DotPerMode.DPM: return "DPM (Dot Per Meter)\n1メートルあたりのドット数の基準値です。\n利点:Unityはメートル法なので、直観的な数値で扱える。\n欠点:DPIから再計算する必要がある。"; case DotPerMode.LocalScale: return "Local Scale\n単純にUnityのTransformに入れるScaleの値を指定します。\n利点:見たままの数値が入る。\n欠点:解像度としては大小が逆で比較が難しい。"; } return "this cant be happening!"; } // Monitor DPI source // https://www.ipentec.com/document/hardware-display-dot-pitch // https://www.apollomaniacs.com/ipod/ifamily_screensize.htm static int GetDPI(DPITemplate type) { switch (type) { case DPITemplate.Inch85Monitor_8K: return 103; case DPITemplate.Inch85Monitor_4K: return 60; case DPITemplate.Inch75Monitor_8K: return 117; case DPITemplate.Inch75Monitor_4K: return 59; case DPITemplate.Inch24Monitor_8K: return 367; case DPITemplate.Inch24Monitor_DCI4K: return 192; case DPITemplate.Inch24Monitor_4K: return 183; case DPITemplate.Inch24Monitor_WQHD: return 122; case DPITemplate.Inch24Monitor_WUXGA: return 94; case DPITemplate.Inch24Monitor_FullHD: return 91; case DPITemplate.Inch24Monitor_UXGA: return 83; case DPITemplate.Inch24Monitor_SXGA: return 68; case DPITemplate.Inch24Monitor_XGA: return 53; case DPITemplate.Inch24Monitor_SVGA: return 41; case DPITemplate.Inch24Monitor_VGA: return 33; case DPITemplate.iPadPro_12_9: return 264; case DPITemplate.iPadPro_11: return 264; case DPITemplate.iPhone13_or_12_Max: return 264; case DPITemplate.iPhone11_Max_or_XSMax: return 458; case DPITemplate.Simple_DPI1000: return 1000; case DPITemplate.Simple_DPI500: return 500; case DPITemplate.Simple_DPI200: return 200; case DPITemplate.Simple_DPI100: return 100; case DPITemplate.Simple_DP50: return 50; case DPITemplate.Simple_DPI1024: return 1024; case DPITemplate.Simple_DPI512: return 512; case DPITemplate.Simple_DPI256: return 256; case DPITemplate.Simple_DPI128: return 128; case DPITemplate.Simple_DPI64: return 64; default: return -1; } } public static bool IsDataPath(string path) { return path.StartsWith(Application.dataPath); } public static string StlipDataPath(string path) { Debug.Log(path); Debug.Log(Application.dataPath); return path.Remove(0, Application.dataPath.Length); } } }