New Tools/ambientCG Importer window (catalog browser, ZIP download with selective map extraction, URP/Lit material build). Generic pieces moved from the PolyHaven tool into Editor/WebAssets (EditorWebRequest, PbrMapType, PbrMaterialBuilder, ImportUtil, AssetBrowserGrid); PolyHaven API/window now delegate to the shared code, behavior unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
173 lines
6.3 KiB
C#
173 lines
6.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace Chameleon.Editor.WebAssets
|
|
{
|
|
/// <summary>
|
|
/// Reusable catalog browser for importer windows: search field + responsive
|
|
/// thumbnail grid with lazy, throttled thumbnail loading. The host window owns
|
|
/// the catalog data and reacts to cell clicks via the onSelect callback.
|
|
/// </summary>
|
|
public sealed class AssetBrowserGrid
|
|
{
|
|
public sealed class Entry
|
|
{
|
|
public string Key; // slug / asset id
|
|
public string Name; // display name (cell label)
|
|
public string ThumbnailUrl;
|
|
public string Search; // pre-lowered haystack for filtering
|
|
}
|
|
|
|
private const int MaxResults = 150;
|
|
private const int MaxThumbInFlight = 6;
|
|
|
|
private readonly Action _repaint;
|
|
|
|
private List<Entry> _catalog;
|
|
private readonly List<Entry> _filtered = new List<Entry>();
|
|
private string _searchQuery = "";
|
|
private Vector2 _gridScroll;
|
|
|
|
private readonly Dictionary<string, Texture2D> _thumbs = new Dictionary<string, Texture2D>();
|
|
private readonly HashSet<string> _thumbRequested = new HashSet<string>();
|
|
private int _thumbsInFlight;
|
|
|
|
public AssetBrowserGrid(Action repaint)
|
|
{
|
|
_repaint = repaint;
|
|
}
|
|
|
|
public bool HasCatalog => _catalog != null;
|
|
|
|
public void SetCatalog(List<Entry> catalog)
|
|
{
|
|
_catalog = catalog;
|
|
ApplyFilter();
|
|
}
|
|
|
|
/// <summary>Draws search field, match count and the grid. Call only when HasCatalog.</summary>
|
|
public void Draw(Action<Entry> onSelect, Action reloadCatalog, string itemNoun, float gridHeight = 320f)
|
|
{
|
|
using (new EditorGUILayout.HorizontalScope())
|
|
{
|
|
EditorGUI.BeginChangeCheck();
|
|
_searchQuery = EditorGUILayout.TextField("Search", _searchQuery);
|
|
if (EditorGUI.EndChangeCheck())
|
|
ApplyFilter();
|
|
if (reloadCatalog != null && GUILayout.Button("↻", GUILayout.Width(26)))
|
|
reloadCatalog();
|
|
}
|
|
|
|
int shown = Mathf.Min(_filtered.Count, MaxResults);
|
|
string cap = _filtered.Count > MaxResults ? $", showing first {MaxResults}" : "";
|
|
EditorGUILayout.LabelField(
|
|
$"{_filtered.Count} match(es){cap} of {_catalog.Count} {itemNoun}.",
|
|
EditorStyles.miniLabel);
|
|
|
|
DrawGrid(shown, onSelect, gridHeight);
|
|
}
|
|
|
|
private void DrawGrid(int count, Action<Entry> onSelect, float gridHeight)
|
|
{
|
|
const float cell = 104f;
|
|
const float pad = 6f;
|
|
float viewW = EditorGUIUtility.currentViewWidth - 24f;
|
|
int cols = Mathf.Max(1, Mathf.FloorToInt((viewW + pad) / (cell + pad)));
|
|
|
|
_gridScroll = EditorGUILayout.BeginScrollView(_gridScroll, GUILayout.Height(gridHeight));
|
|
|
|
int rows = Mathf.CeilToInt(count / (float)cols);
|
|
for (int row = 0; row < rows; row++)
|
|
{
|
|
using (new EditorGUILayout.HorizontalScope())
|
|
{
|
|
for (int c = 0; c < cols; c++)
|
|
{
|
|
int i = row * cols + c;
|
|
if (i >= count)
|
|
{
|
|
GUILayout.FlexibleSpace();
|
|
break;
|
|
}
|
|
DrawCell(_filtered[i], cell, onSelect);
|
|
}
|
|
}
|
|
}
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
}
|
|
|
|
private void DrawCell(Entry a, float size, Action<Entry> onSelect)
|
|
{
|
|
using (new EditorGUILayout.VerticalScope(GUILayout.Width(size)))
|
|
{
|
|
var thumbRect = GUILayoutUtility.GetRect(size, size - 22f,
|
|
GUILayout.Width(size), GUILayout.Height(size - 22f));
|
|
|
|
var tex = GetThumb(a);
|
|
if (tex)
|
|
GUI.DrawTexture(thumbRect, tex, ScaleMode.ScaleToFit);
|
|
else
|
|
EditorGUI.DrawRect(thumbRect, new Color(0f, 0f, 0f, 0.15f));
|
|
|
|
// Transparent hit target over the thumbnail.
|
|
if (GUI.Button(thumbRect, new GUIContent("", a.Name), GUIStyle.none))
|
|
onSelect?.Invoke(a);
|
|
|
|
EditorGUILayout.LabelField(new GUIContent(a.Name, a.Name),
|
|
EditorStyles.miniLabel, GUILayout.Width(size));
|
|
}
|
|
}
|
|
|
|
/// <summary>Lazy, throttled thumbnail loader — only visible cells request, capped in-flight.</summary>
|
|
private Texture2D GetThumb(Entry a)
|
|
{
|
|
if (_thumbs.TryGetValue(a.Key, out var tex))
|
|
{
|
|
if (tex)
|
|
return tex;
|
|
// Cached texture was destroyed (e.g. play-mode cleanup) — purge and re-request.
|
|
_thumbs.Remove(a.Key);
|
|
_thumbRequested.Remove(a.Key);
|
|
}
|
|
|
|
// Don't add to _thumbRequested until a slot is free, so it retries next repaint.
|
|
if (!_thumbRequested.Contains(a.Key) && _thumbsInFlight < MaxThumbInFlight)
|
|
{
|
|
_thumbRequested.Add(a.Key);
|
|
_thumbsInFlight++;
|
|
EditorWebRequest.GetTexture(a.ThumbnailUrl,
|
|
t => { _thumbs[a.Key] = t; _thumbsInFlight--; _repaint?.Invoke(); },
|
|
_ => { _thumbsInFlight--; _repaint?.Invoke(); }); // no retry-spam on error
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void ApplyFilter()
|
|
{
|
|
_filtered.Clear();
|
|
if (_catalog == null)
|
|
return;
|
|
|
|
var q = _searchQuery?.Trim().ToLowerInvariant();
|
|
if (string.IsNullOrEmpty(q))
|
|
{
|
|
_filtered.AddRange(_catalog);
|
|
return;
|
|
}
|
|
|
|
var terms = q.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);
|
|
foreach (var a in _catalog)
|
|
{
|
|
bool all = true;
|
|
foreach (var t in terms)
|
|
if (a.Search.IndexOf(t, StringComparison.Ordinal) < 0) { all = false; break; }
|
|
if (all)
|
|
_filtered.Add(a);
|
|
}
|
|
}
|
|
}
|
|
}
|