// // AssetItemView.cpp:资产条目绘制单元实现。 // #include "AssetItemView.h" #include "AssetKind.h" #include "EditorColors.h" #include "ThumbnailCache.h" #include "panels/AssetBrowserPanel.h" // kPrefabDragPayload / kModelDragPayload #include "panels/ControllerEditorPanel.h" // kControllerDragPayload #include "panels/MaterialEditorPanel.h" // kMaterialDragPayload / kTextureDragPayload #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr float kBoxRounding = 3.0f; // 类别色压暗背板 + 描边(p0/p1 来自上一个 item rect) void DrawBoxChrome(const ImVec2& p0, const ImVec2& p1, const ImVec4& accent) { ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddRectFilled(p0, p1, ImGui::GetColorU32(ImVec4(accent.x * 0.25f, accent.y * 0.25f, accent.z * 0.25f, 1.0f)), kBoxRounding); dl->AddRect(p0, p1, ImGui::GetColorU32(accent), kBoxRounding); } // 名字按可用宽度截断(UTF-8 边界安全) std::string TruncateToWidth(const std::string& s, float maxW) { if (ImGui::CalcTextSize(s.c_str()).x <= maxW) return s; std::string out = s; const char* ell = ".."; while (!out.empty() && ImGui::CalcTextSize((out + ell).c_str()).x > maxW) { out.pop_back(); while (!out.empty() && (static_cast(out.back()) & 0xC0) == 0x80) { out.pop_back(); // 退到 UTF-8 起始字节 } } return out + ell; } // 已尝试按需加载的材质资产路径:加载失败不再每帧重试(资产缓存接管 // 成功路径,重复调用是缓存命中) std::set& MaterialLoadTried() { static std::set s; return s; } // 材质加载闸门:大目录(上百个材质)逐个同步加载(.material 解析 + // GPU 纹理上传——Texture 构造内含 submit + fence 阻塞等待,单个材质 // 可含多张大贴图)会顶爆单帧 → "卡成 PPT"。闸门 = 每帧至多 1 个 + // 按上次实测耗时冷却(1 帧≈16ms;重载 ≈ 冷却相应帧数),把加载占用 // 压到 ≤ ~50% 帧预算,剩余帧时间还给渲染 struct MaterialLoadGate { bool Allow() { const int frame = ImGui::GetFrameCount(); return frame != m_LastLoadFrame && frame >= m_CooldownUntil; } void Charge(double elapsedMs) { const int frame = ImGui::GetFrameCount(); m_LastLoadFrame = frame; const int cool = static_cast(elapsedMs / 16.0); m_CooldownUntil = frame + 1 + std::min(cool, 30); } private: int m_LastLoadFrame = -1; int m_CooldownUntil = 0; }; MaterialLoadGate& MaterialLoad() { static MaterialLoadGate s_Gate; return s_Gate; } // 已烘焙材质缩略图对应的 (serial, editVersion)。资产重载(新 serial) // 或参数编辑(editVersion++)后与当前不符 = 缩略图过期,须作废重烘 //(否则浏览器永远停在旧图)——记录时机 = 发起烘焙请求那一帧 std::unordered_map>& BakedIdentity() { static std::unordered_map> s; return s; } // 材质球缩略图全部走编辑器缩略图缓存:引擎把预览球渲染一次后 GPU→CPU // 回读,App 注入缓存(与贴图缩略图同一条出图路径),浏览器不再占用 // 引擎预览 tile 池——大目录滚动/材质编辑器预览互不饿死 void DrawCachedThumb(const ThumbView& v, float size, const ImVec2& p0) { float fw = size, fh = size; if (v.w > 0 && v.h > 0) { if (v.w >= v.h) fh = size * static_cast(v.h) / v.w; else fw = size * static_cast(v.w) / v.h; } const ImVec2 fp0(p0.x + (size - fw) * 0.5f, p0.y + (size - fh) * 0.5f); ImGui::GetWindowDrawList()->AddImage( v.tex, fp0, ImVec2(fp0.x + fw, fp0.y + fh)); } // 缩略图/占位块:一个命中项 + 绘制层。 // interactive=true → InvisibleButton(带 ID,可承载拖拽源/点击;Dummy // 无 ID,在其上 BeginDragDropSource 会断言);false → Dummy(行内小图 // 纯展示,不抢行 Selectable 的点击)。 // 出图时**不铺任何背板**——透明像素会被类别色染色;背板只在未出图时 // 作为占位(类别色压暗 + 短标签)。仅可见项才请求缩略图,离屏项不入 // 解码队列(大目录不再一次性排队,出图延迟可控) void DrawThumbBox(const EditorContext& ctx, const std::filesystem::path& path, float size, EditorAsset::AssetKind kind, bool interactive) { const bool visible = ImGui::IsRectVisible(ImVec2(size, size)); if (interactive) { ImGui::InvisibleButton("##thumb", ImVec2(size, size)); } else { ImGui::Dummy(ImVec2(size, size)); } const ImVec2 p0 = ImGui::GetItemRectMin(); const ImVec2 p1 = ImGui::GetItemRectMax(); const ImVec4 accent = EditorColors::AssetColor(kind); bool drawn = false; if (visible && kind == EditorAsset::AssetKind::Material && ctx.assets) { // 材质资产缩略图 = 引擎烘焙的静态材质球(GPU→CPU 回读 → 缩略图 // 缓存;与贴图缩略图同一条出图路径) auto& cache = EditorThumbnailCache::Get(); const std::string assetPath = path.generic_string(); // 当前材质身份:已加载才有;未加载时无法判新旧 Engine::Material* curMat = nullptr; if (ctx.assets->IsMaterialLoaded(assetPath)) { Engine::MaterialAsset* a = ctx.assets->FindMaterialAsset(assetPath); curMat = a ? a->GetMaterial() : nullptr; } const ThumbView v = cache.Peek(assetPath); if (v.state == ThumbView::State::Ready) { // 失效检测:缓存缩略图对应的材质身份 vs 当前身份——资产 // 重载(新 serial,换贴图槽)/ 参数编辑(editVersion++)后 // 不符 = 过期,作废缓存让下一帧重烘(否则浏览器永远旧图) bool stale = false; if (curMat) { auto it = BakedIdentity().find(assetPath); if (it != BakedIdentity().end() && (it->second.first != curMat->GetSerial() || it->second.second != curMat->GetEditVersion())) { stale = true; } } if (!stale) { DrawCachedThumb(v, size, p0); drawn = true; } else { cache.Invalidate(assetPath); BakedIdentity().erase(assetPath); } } else if (v.state != ThumbView::State::Pending && ctx.renderer) { if (curMat) { // 未烘焙/已失效:请求烘焙并登记当前身份(key 幂等 // keepalive,回读完成即进缓存) ctx.renderer->RequestMaterialPreviewBake( curMat, cache.BakeKeyForPath(assetPath)); BakedIdentity()[assetPath] = { curMat->GetSerial(), curMat->GetEditVersion()}; } else if (!MaterialLoadTried().count(assetPath) && MaterialLoad().Allow()) { // 按需加载材质(闸门限流,避免大目录同帧卡顿) MaterialLoadTried().insert(assetPath); const auto t0 = std::chrono::steady_clock::now(); ctx.assets->LoadMaterialAsset(assetPath); const double ms = std::chrono::duration(std::chrono::steady_clock::now() - t0) .count(); MaterialLoad().Charge(ms); } } } if (!drawn && visible && EditorAsset::IsThumbnailable(path)) { const ThumbView v = EditorThumbnailCache::Get().Request(path.generic_string()); if (v.state == ThumbView::State::Ready) { DrawCachedThumb(v, size, p0); drawn = true; } } if (!drawn) { // 占位:类别色压暗背板 + 描边;大框叠加短标签 DrawBoxChrome(p0, p1, accent); if (size >= 28.0f) { const char* label = EditorAsset::KindShortLabel(kind); const ImVec2 ts = ImGui::CalcTextSize(label); ImGui::GetWindowDrawList()->AddText( ImVec2(p0.x + (size - ts.x) * 0.5f, p0.y + (size - ts.y) * 0.5f), ImGui::GetColorU32(accent), label); } } } // 文件夹命中项(黄色)+ 短标签 void DrawFolderBox(float size, bool interactive) { if (interactive) { ImGui::InvisibleButton("##dir", ImVec2(size, size)); } else { ImGui::Dummy(ImVec2(size, size)); } const ImVec2 p0 = ImGui::GetItemRectMin(); const ImVec2 p1 = ImGui::GetItemRectMax(); DrawBoxChrome(p0, p1, EditorColors::kFolder); if (size >= 28.0f) { const ImVec2 ts = ImGui::CalcTextSize("DIR"); ImGui::GetWindowDrawList()->AddText( ImVec2(p0.x + (size - ts.x) * 0.5f, p0.y + (size - ts.y) * 0.5f), ImGui::GetColorU32(EditorColors::kFolder), "DIR"); } } } // namespace namespace EditorAssetView { void DrawDragSource(const std::filesystem::path& path, const std::string& name) { const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path); const char* payload = nullptr; switch (kind) { case EditorAsset::AssetKind::Material: payload = kMaterialDragPayload; break; case EditorAsset::AssetKind::AnimClip: payload = kControllerDragPayload; break; case EditorAsset::AssetKind::Prefab: payload = kPrefabDragPayload; break; case EditorAsset::AssetKind::Model: payload = kModelDragPayload; break; case EditorAsset::AssetKind::Texture: payload = kTextureDragPayload; break; default: return; } if (ImGui::BeginDragDropSource()) { const std::string assetPath = path.generic_string(); ImGui::SetDragDropPayload(payload, assetPath.c_str(), assetPath.size() + 1); ImGui::TextUnformatted(name.c_str()); ImGui::EndDragDropSource(); } } void DrawTooltip(const EditorContext& ctx, const std::filesystem::path& path) { namespace fs = std::filesystem; const std::string name = path.filename().string(); const std::string assetPath = path.generic_string(); const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path); ImGui::BeginTooltip(); ImGui::TextUnformatted(name.c_str()); ImGui::Separator(); ImGui::Text("type: %s", EditorAsset::KindLabel(kind)); std::error_code ec; const auto sz = fs::file_size(path, ec); if (!ec) { char szBuf[32]; if (sz >= 1024ull * 1024ull) { std::snprintf(szBuf, sizeof(szBuf), "%.1f MB", static_cast(sz) / (1024.0 * 1024.0)); } else { std::snprintf(szBuf, sizeof(szBuf), "%.1f KB", static_cast(sz) / 1024.0); } ImGui::Text("size: %s", szBuf); } const auto ftime = fs::last_write_time(path, ec); if (!ec) { // file_clock → system_clock 近似转换(C++17 无官方转换) const auto sctp = std::chrono::time_point_cast< std::chrono::system_clock::duration>( ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now()); const std::time_t tt = std::chrono::system_clock::to_time_t(sctp); char tmBuf[32]; std::strftime(tmBuf, sizeof(tmBuf), "%Y-%m-%d %H:%M", std::localtime(&tt)); ImGui::Text("modified: %s", tmBuf); } if (ctx.assets) { if (kind == EditorAsset::AssetKind::Model || kind == EditorAsset::AssetKind::Texture) { const bool loaded = (kind == EditorAsset::AssetKind::Model) ? ctx.assets->IsModelLoaded(assetPath) : ctx.assets->IsEnvironmentLoaded(assetPath); ImGui::Text("state: %s", loaded ? "loaded" : "not loaded"); } else if (kind == EditorAsset::AssetKind::Prefab) { ImGui::TextDisabled( "template — drag into scene / double-click"); } if (kind == EditorAsset::AssetKind::Model) { if (const Engine::Model* m = ctx.assets->GetModelByPath(assetPath); m != nullptr) { ImGui::Text("meshes: %u, bones: %u, clips: %u", static_cast(m->GetMeshes().size()), m->GetBoneCount(), static_cast(m->GetClips().size())); } } } ImGui::TextDisabled("%s", assetPath.c_str()); ImGui::EndTooltip(); } ItemResult DrawRow(const EditorContext& ctx, const std::filesystem::path& path, bool selected) { ItemResult res; const std::string name = path.filename().string(); const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path); ImGui::PushID(name.c_str()); // 内联小缩略图 / 类别色占位块(非交互:行点击归 Selectable) DrawThumbBox(ctx, path, ImGui::GetTextLineHeight(), kind, false); ImGui::SameLine(); // 名称(类别色提亮)+ 类别色 hover/选中高亮 ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind)); ImGui::PushStyleColor(ImGuiCol_Header, EditorColors::AssetHighlightColor(kind)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, EditorColors::AssetHighlightColor(kind)); ImGui::PushStyleColor(ImGuiCol_HeaderActive, EditorColors::AssetHighlightColor(kind)); res.clicked = ImGui::Selectable(name.c_str(), selected, ImGuiSelectableFlags_AllowDoubleClick); ImGui::PopStyleColor(4); if (res.clicked && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { res.doubleClicked = true; } DrawDragSource(path, name); if (ImGui::IsItemHovered()) { DrawTooltip(ctx, path); } ImGui::PopID(); return res; } ItemResult DrawTile(const EditorContext& ctx, const std::filesystem::path& path, float tileSize, bool selected) { ItemResult res; const std::string name = path.filename().string(); const EditorAsset::AssetKind kind = EditorAsset::KindOfPath(path); const ImVec4 accent = EditorColors::AssetColor(kind); ImGui::PushID(name.c_str()); ImGui::BeginGroup(); DrawThumbBox(ctx, path, tileSize, kind, true); // 选中/hover 描边(叠在缩略图上) const ImVec2 p0 = ImGui::GetItemRectMin(); const ImVec2 p1 = ImGui::GetItemRectMax(); const bool hovered = ImGui::IsItemHovered(); if (selected || hovered) { ImGui::GetWindowDrawList()->AddRect( p0, p1, ImGui::GetColorU32(accent), kBoxRounding, 0, selected ? 2.0f : 1.0f); } res.clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { res.doubleClicked = true; } DrawDragSource(path, name); if (ImGui::IsItemHovered()) { DrawTooltip(ctx, path); } ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::AssetNameColor(kind)); ImGui::TextUnformatted(TruncateToWidth(name, tileSize).c_str()); ImGui::PopStyleColor(); ImGui::EndGroup(); ImGui::PopID(); return res; } bool DrawFolderRow(const std::filesystem::path& dir) { const std::string dname = dir.filename().string(); ImGui::PushID(dname.c_str()); DrawFolderBox(ImGui::GetTextLineHeight(), false); ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder); const bool clicked = ImGui::Selectable(dname.c_str()); ImGui::PopStyleColor(); ImGui::PopID(); return clicked; } bool DrawFolderTile(const std::filesystem::path& dir, float tileSize, bool selected) { const std::string dname = dir.filename().string(); ImGui::PushID(dname.c_str()); ImGui::BeginGroup(); DrawFolderBox(tileSize, true); const ImVec2 p0 = ImGui::GetItemRectMin(); const ImVec2 p1 = ImGui::GetItemRectMax(); if (selected || ImGui::IsItemHovered()) { ImGui::GetWindowDrawList()->AddRect( p0, p1, ImGui::GetColorU32(EditorColors::kFolder), kBoxRounding, 0, selected ? 2.0f : 1.0f); } const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); ImGui::PushStyleColor(ImGuiCol_Text, EditorColors::kFolder); ImGui::TextUnformatted(TruncateToWidth(dname, tileSize).c_str()); ImGui::PopStyleColor(); ImGui::EndGroup(); ImGui::PopID(); return clicked; } } // namespace EditorAssetView