using System.Collections.Generic; using System.IO; using System.Linq; using Chameleon.Editor.WebAssets; using UnityEditor; using UnityEngine; namespace Chameleon.Editor.PolyHaven { /// /// Editor window: browse/paste a Poly Haven asset, fetch its maps, pick resolution + /// maps, then download and auto-build a URP/Lit material under Assets/Poly Haven/{Name}/. /// public sealed class PolyHavenImporterWindow : EditorWindow { private const string RootFolder = "Assets/Poly Haven"; private const string DefaultRes = "2k"; private string _urlInput = "https://polyhaven.com/a/"; private PolyHavenInfo _info; private PolyHavenFiles _files; private Texture2D _thumbnail; private string _selectedRes = DefaultRes; private readonly Dictionary _mapEnabled = new Dictionary(); private bool _busy; private string _status = ""; private Vector2 _scroll; // ---- Browser ---------------------------------------------------------- // Catalog cached for the whole session (survives window close, cleared on domain reload). private static List s_catalog; private AssetBrowserGrid _grid; private bool _browseFoldout = true; private bool _autoLoadTried; private bool _catalogLoading; [MenuItem("Tools/Poly Haven Importer")] public static void Open() { var w = GetWindow("Poly Haven Importer"); w.minSize = new Vector2(440, 480); } private void OnEnable() { _grid = new AssetBrowserGrid(Repaint); // Catalog may already be cached from a previous window instance this session. if (s_catalog != null) { _autoLoadTried = true; _grid.SetCatalog(s_catalog); } } private void OnGUI() { _scroll = EditorGUILayout.BeginScrollView(_scroll); EditorGUILayout.LabelField("Poly Haven Importer", EditorStyles.boldLabel); EditorGUILayout.HelpBox( "Pick a texture on polyhaven.com, paste its URL here (e.g. .../a/broken_brick_wall), " + "Fetch, choose resolution + maps, then Download. A URP/Lit material is built automatically.", MessageType.Info); EditorGUILayout.Space(); DrawBrowser(); EditorGUILayout.Space(); using (new EditorGUI.DisabledScope(_busy)) { EditorGUILayout.LabelField("Asset URL or slug", EditorStyles.miniBoldLabel); using (new EditorGUILayout.HorizontalScope()) { _urlInput = EditorGUILayout.TextField(_urlInput); if (GUILayout.Button("Fetch", GUILayout.Width(70))) Fetch(); } } if (_info != null) DrawAssetSection(); EditorGUILayout.Space(); if (!string.IsNullOrEmpty(_status)) EditorGUILayout.HelpBox(_status, _busy ? MessageType.None : MessageType.Info); EditorGUILayout.EndScrollView(); } private void DrawAssetSection() { EditorGUILayout.Space(); using (new EditorGUILayout.HorizontalScope()) { if (_thumbnail) { var r = GUILayoutUtility.GetRect(96, 96, GUILayout.Width(96), GUILayout.Height(96)); GUI.DrawTexture(r, _thumbnail, ScaleMode.ScaleToFit); } using (new EditorGUILayout.VerticalScope()) { EditorGUILayout.LabelField(_info.Name, EditorStyles.boldLabel); EditorGUILayout.LabelField($"Slug: {_info.Slug}", EditorStyles.miniLabel); if (_info.Categories != null && _info.Categories.Length > 0) EditorGUILayout.LabelField($"Categories: {string.Join(", ", _info.Categories)}", EditorStyles.miniLabel); if (_info.Type != 1) EditorGUILayout.HelpBox("This asset is not a texture — only textures are supported.", MessageType.Warning); } } if (_files == null || _files.Resolutions.Count == 0) return; EditorGUILayout.Space(); // Resolution popup. var resArray = _files.Resolutions.ToArray(); int resIdx = Mathf.Max(0, System.Array.IndexOf(resArray, _selectedRes)); resIdx = EditorGUILayout.Popup("Resolution", resIdx, resArray.Select(r => r.ToUpperInvariant()).ToArray()); _selectedRes = resArray[resIdx]; EditorGUILayout.Space(); EditorGUILayout.LabelField("Maps", EditorStyles.miniBoldLabel); foreach (var meta in PbrMapInfo.All) { if (!_files.Has(meta.Type)) continue; bool cur = _mapEnabled.TryGetValue(meta.Type, out var v) ? v : meta.DefaultEnabled; _mapEnabled[meta.Type] = EditorGUILayout.ToggleLeft(meta.Label, cur); } EditorGUILayout.Space(); bool canDownload = !_busy && _info.Type == 1 && AnyMapSelected(); using (new EditorGUI.DisabledScope(!canDownload)) { if (GUILayout.Button("Download / Import", GUILayout.Height(30))) Download(); } } private bool AnyMapSelected() { foreach (var meta in PbrMapInfo.All) if (_files.Has(meta.Type) && _mapEnabled.TryGetValue(meta.Type, out var v) && v) return true; // Fall back to defaults if the user hasn't toggled anything yet. foreach (var meta in PbrMapInfo.All) if (_files.Has(meta.Type) && meta.DefaultEnabled && !_mapEnabled.ContainsKey(meta.Type)) return true; return false; } // ---- Browser ----------------------------------------------------------- private void DrawBrowser() { _browseFoldout = EditorGUILayout.Foldout(_browseFoldout, "Browse catalog", true, EditorStyles.foldoutHeader); if (!_browseFoldout) return; using (new EditorGUI.DisabledScope(_busy)) { if (!_grid.HasCatalog) { // Auto-load once when the browser is first opened; still offer a manual retry. if (!_catalogLoading && !_autoLoadTried) { _autoLoadTried = true; LoadCatalog(); } using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField(_catalogLoading ? "Loading texture catalog…" : "Catalog not loaded."); using (new EditorGUI.DisabledScope(_catalogLoading)) if (GUILayout.Button("Load catalog", GUILayout.Width(110))) LoadCatalog(); } return; } _grid.Draw(SelectAsset, LoadCatalog, "textures"); } } private void SelectAsset(AssetBrowserGrid.Entry a) { _urlInput = "https://polyhaven.com/a/" + a.Key; GUI.FocusControl(null); Fetch(); // url filled -> fetch, then everything as before } private void LoadCatalog() { _catalogLoading = true; _status = "Loading Poly Haven texture catalog…"; Repaint(); PolyHavenApi.FetchAssets("textures", list => { s_catalog = list; _catalogLoading = false; _grid.SetCatalog(list); _status = $"Catalog loaded: {list.Count} textures."; Repaint(); }, err => { _catalogLoading = false; _status = "Catalog error: " + err; Debug.LogError("[PolyHaven] " + err); Repaint(); }); } // ---- Fetch ------------------------------------------------------------- private void Fetch() { string slug = PolyHavenApi.ExtractSlug(_urlInput); if (string.IsNullOrEmpty(slug)) { EditorUtility.DisplayDialog("Poly Haven", "Could not read a slug from the URL.", "OK"); return; } _busy = true; _status = $"Fetching '{slug}'…"; _info = null; _files = null; _thumbnail = null; _mapEnabled.Clear(); Repaint(); PolyHavenApi.FetchInfo(slug, info => { _info = info; Repaint(); EditorWebRequest.GetTexture(info.ThumbnailUrl, tex => { _thumbnail = tex; Repaint(); }, _ => { }); PolyHavenApi.FetchFiles(slug, files => { _files = files; if (files.Resolutions.Contains(DefaultRes)) _selectedRes = DefaultRes; else if (files.Resolutions.Count > 0) _selectedRes = files.Resolutions[Mathf.Min(1, files.Resolutions.Count - 1)]; _busy = false; _status = info.Type == 1 ? $"Ready: {files.Maps.Count} map type(s), {files.Resolutions.Count} resolution(s)." : "Fetched, but this asset is not a texture."; Repaint(); }, err => Fail(err)); }, err => Fail(err)); } // ---- Download ---------------------------------------------------------- private void Download() { // Resolve which maps to fetch. var wanted = new List(); foreach (var meta in PbrMapInfo.All) { if (!_files.Has(meta.Type)) continue; bool on = _mapEnabled.TryGetValue(meta.Type, out var v) ? v : meta.DefaultEnabled; if (on) wanted.Add(meta.Type); } // ARM fallback: if the asset packs AO/Rough/Metal only into ARM (no dedicated // map for a channel), pull ARM so the builder can unpack the missing channels. if (_files.Has(PbrMapType.Arm) && !wanted.Contains(PbrMapType.Arm)) { bool channelMissing = !_files.Has(PbrMapType.Metal) || !_files.Has(PbrMapType.Rough) || !_files.Has(PbrMapType.AO); if (channelMissing) wanted.Add(PbrMapType.Arm); } if (wanted.Count == 0) return; string displayName = _info.Name; string folder = $"{RootFolder}/{ImportUtil.SanitizeFileName(displayName)}"; folder = ImportUtil.ResolveFolderCollision(folder, "Poly Haven"); if (folder == null) return; ImportUtil.EnsureFolder(folder); int maxSize = ImportUtil.ResolutionToMaxSize(_selectedRes); _busy = true; _status = $"Downloading {wanted.Count} map(s) at {_selectedRes.ToUpperInvariant()}…"; Repaint(); var results = new List(); var failures = new List(); int remaining = wanted.Count; AssetDatabase.StartAssetEditing(); void FinishOne() { remaining--; if (remaining > 0) return; AssetDatabase.StopAssetEditing(); AssetDatabase.Refresh(); if (results.Count == 0) { _busy = false; _status = "Download failed — no maps saved."; if (failures.Count > 0) Debug.LogError("[PolyHaven] " + string.Join("; ", failures)); Repaint(); return; } PbrMaterialBuilder.Build(folder, displayName, _info.Slug, maxSize, results); _busy = false; _status = $"Done: {results.Count} map(s) imported, material built in {folder}."; if (failures.Count > 0) _status += $" ({failures.Count} map(s) failed — see console.)"; Repaint(); } foreach (var type in wanted) { if (!_files.TryGetWithFallback(type, _selectedRes, out var file, out var usedRes)) { failures.Add($"{type}: no file"); FinishOne(); continue; } if (usedRes != _selectedRes) Debug.LogWarning($"[PolyHaven] {type}: {_selectedRes} unavailable, using {usedRes}."); var meta = PbrMapInfo.Get(type); string fileName = $"{_info.Slug}_{meta.Suffix}.{file.Format}"; string destPath = $"{folder}/{fileName}"; EditorWebRequest.DownloadFile(file.Url, Path.GetFullPath(destPath), onDone: () => { results.Add(new PbrMaterialBuilder.Downloaded { Type = type, AssetPath = destPath }); FinishOne(); }, onError: err => { failures.Add($"{type}: {err}"); FinishOne(); }); } } // ---- Helpers ----------------------------------------------------------- private void Fail(string message) { _busy = false; _status = "Error: " + message; Debug.LogError("[PolyHaven] " + message); Repaint(); } } }