Add ambientCG importer and extract shared web-asset import framework

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>
This commit is contained in:
adminandClaude Fable 5 committed 2026-07-16 15:27:02 +02:00
1 parent 9b35ee2a97
commit fb4098b9f8
21 files changed
+1205 -417

No files matched your search

@@ -0,0 +1,172 @@
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);
}
}
}
}