Improves asset loading and scene management
Adds scene management system with GameObjects and Components. Improves asset loading by scanning assets in a background thread and queuing textures for upload. Also displays a progress bar in the file browser for texture loading. Adds basic component system with TagComponent to identify GameObjects by a user defined name.
This commit is contained in:
parent
3ecfa15e4a
commit
9641de9b2c
@ -100,6 +100,13 @@ add_library(Core STATIC
|
||||
src/core/systems/assets/Texture2D.cpp
|
||||
src/core/systems/assets/Texture2D.h
|
||||
src/core/systems/Shader.cpp
|
||||
src/core/systems/Scene/Scene.cpp
|
||||
src/core/systems/Scene/Scene.h
|
||||
src/core/systems/Scene/GameObject.cpp
|
||||
src/core/systems/Scene/GameObject.h
|
||||
src/core/systems/Scene/Components/TagComponent.cpp
|
||||
src/core/systems/Scene/Components/TagComponent.h
|
||||
src/core/systems/Scene/Components/Component.h
|
||||
)
|
||||
|
||||
target_include_directories(Core PUBLIC src/core)
|
||||
|
@ -16,6 +16,10 @@ namespace OX
|
||||
std::unordered_map<std::string, std::shared_ptr<Asset> > AssetManager::s_LoadedAssets;
|
||||
std::unordered_map<std::string, AssetManager::AssetMetadata> AssetManager::s_MetadataMap;
|
||||
|
||||
std::atomic<size_t> OX::AssetManager::s_TotalTexturesToLoad{0};
|
||||
std::atomic<size_t> OX::AssetManager::s_LoadedTexturesCount{0};
|
||||
|
||||
|
||||
// Map from an actual file path to virtual ID
|
||||
std::unordered_map<std::string, std::string> AssetManager::s_PathToID;
|
||||
std::mutex AssetManager::s_AssetMutex;
|
||||
@ -39,13 +43,13 @@ namespace OX
|
||||
s_FileTree->name = "res://";
|
||||
s_FileTree->path = "res://";
|
||||
s_FileTree->isDirectory = true;
|
||||
|
||||
s_TotalTexturesToLoad.store(0);
|
||||
s_LoadedTexturesCount.store(0);
|
||||
s_ScanThread = std::thread(BackgroundScan);
|
||||
}
|
||||
|
||||
void AssetManager::Shutdown()
|
||||
{
|
||||
|
||||
s_Scanning = false;
|
||||
|
||||
if (s_ScanThread.joinable()) {
|
||||
@ -62,56 +66,76 @@ namespace OX
|
||||
s_FileTree->name = "res://";
|
||||
s_FileTree->path = "res://";
|
||||
s_FileTree->isDirectory = true;
|
||||
|
||||
s_TotalTexturesToLoad.store(0);
|
||||
s_LoadedTexturesCount.store(0);
|
||||
s_ScanThread = std::thread(BackgroundScan);
|
||||
}
|
||||
|
||||
void AssetManager::BackgroundScan()
|
||||
{
|
||||
while (s_Scanning) {
|
||||
for (auto &entry: fs::recursive_directory_iterator(s_ProjectRoot)) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
if (!s_Scanning) break; // Note: This is needed or shutdown takes ages.
|
||||
size_t totalTextures = 0;
|
||||
for (auto &entry: fs::recursive_directory_iterator(s_ProjectRoot)) {
|
||||
if (!s_Scanning) break;
|
||||
if (!entry.is_directory()) {
|
||||
if (DetectAssetType(entry.path()) == "texture2D")
|
||||
++totalTextures;
|
||||
}
|
||||
}
|
||||
s_TotalTexturesToLoad.store(totalTextures, std::memory_order_relaxed);
|
||||
s_LoadedTexturesCount.store(0, std::memory_order_relaxed);
|
||||
|
||||
|
||||
while (s_Scanning) {
|
||||
std::vector<fs::directory_entry> allEntries;
|
||||
allEntries.reserve(1024);
|
||||
for (auto &entry: fs::recursive_directory_iterator(s_ProjectRoot)) {
|
||||
if (!s_Scanning) break;
|
||||
allEntries.push_back(entry);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, AssetMetadata> > newMetadata;
|
||||
newMetadata.reserve(256);
|
||||
|
||||
for (auto &entry: allEntries) {
|
||||
if (!s_Scanning) break;
|
||||
|
||||
const auto &path = entry.path();
|
||||
std::string resPath = MakeVirtualPath(path);
|
||||
|
||||
if (entry.is_directory()) {
|
||||
// you can also guard directories if you like:
|
||||
// std::lock_guard g(s_AssetMutex);
|
||||
// if (s_MetadataMap.count(resPath)) continue;
|
||||
AddToTree(resPath, true);
|
||||
} else {
|
||||
{
|
||||
// quick check: have we already seen this asset?
|
||||
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||
if (s_MetadataMap.find(resPath) != s_MetadataMap.end())
|
||||
continue;
|
||||
// reserve a spot so other threads know it's in-flight
|
||||
s_MetadataMap[resPath] = {"", ""};
|
||||
}
|
||||
continue;
|
||||
} {
|
||||
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||
if (s_MetadataMap.count(resPath))
|
||||
continue;
|
||||
s_MetadataMap.emplace(resPath, AssetMetadata{"", ""});
|
||||
}
|
||||
AddToTree(resPath, false);
|
||||
|
||||
AddToTree(resPath, false);
|
||||
if (DetectAssetType(path) == "texture2D") {
|
||||
int w, h, ch;
|
||||
stbi_set_flip_vertically_on_load(1);
|
||||
unsigned char *data = stbi_load(path.string().c_str(), &w, &h, &ch, 0);
|
||||
if (!data) continue;
|
||||
|
||||
auto type = DetectAssetType(path);
|
||||
if (type == "texture2D") {
|
||||
int w, h, ch;
|
||||
stbi_set_flip_vertically_on_load(1);
|
||||
unsigned char *data = stbi_load(
|
||||
path.string().c_str(), &w, &h, &ch, 0
|
||||
);
|
||||
if (data) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||
// now fill in the real metadata
|
||||
s_MetadataMap[resPath] = {type, path.string()};
|
||||
}
|
||||
std::lock_guard<std::mutex> qlock(s_QueueMutex);
|
||||
s_TextureQueue.push({resPath, w, h, ch, data});
|
||||
}
|
||||
newMetadata.emplace_back(resPath, AssetMetadata{"texture2D", path.string()}); {
|
||||
std::lock_guard<std::mutex> qlock(s_QueueMutex);
|
||||
s_TextureQueue.push({resPath, w, h, ch, data});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!newMetadata.empty()) {
|
||||
std::lock_guard<std::mutex> lock(s_AssetMutex);
|
||||
for (auto &kv: newMetadata)
|
||||
s_MetadataMap[kv.first] = kv.second;
|
||||
}
|
||||
|
||||
if (s_Scanning)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
s_Scanning = false;
|
||||
@ -124,6 +148,8 @@ namespace OX
|
||||
while (!s_TextureQueue.empty()) {
|
||||
auto pending = s_TextureQueue.front();
|
||||
s_TextureQueue.pop();
|
||||
s_LoadedTexturesCount.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
|
||||
GLuint texID;
|
||||
glGenTextures(1, &texID);
|
||||
|
@ -12,66 +12,85 @@
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
namespace OX {
|
||||
namespace OX
|
||||
{
|
||||
class Asset;
|
||||
|
||||
struct ResourceTreeNode {
|
||||
struct ResourceTreeNode
|
||||
{
|
||||
std::string name;
|
||||
std::string path;
|
||||
bool isDirectory = false;
|
||||
std::vector<std::shared_ptr<ResourceTreeNode>> children;
|
||||
std::vector<std::shared_ptr<ResourceTreeNode> > children;
|
||||
};
|
||||
|
||||
class AssetManager {
|
||||
class AssetManager
|
||||
{
|
||||
public:
|
||||
// -- Lifecycle --
|
||||
static void Init(const std::string& projectRoot);
|
||||
static void Init(const std::string &projectRoot);
|
||||
|
||||
static void Shutdown();
|
||||
|
||||
static void Tick();
|
||||
|
||||
static void Rescan();
|
||||
|
||||
|
||||
static size_t GetTotalTexturesToLoad() { return s_TotalTexturesToLoad.load(); }
|
||||
static size_t GetLoadedTexturesCount() { return s_LoadedTexturesCount.load(); }
|
||||
|
||||
// -- Queries --
|
||||
// Lookup by virtual path ("res://…") or real FS path.
|
||||
static std::shared_ptr<Asset> Get(const std::string& path);
|
||||
static std::shared_ptr<Asset> Get(const std::string &path);
|
||||
|
||||
static std::shared_ptr<ResourceTreeNode> GetFileTree();
|
||||
static void SaveAssetPack(const std::string& outputPath);
|
||||
|
||||
static std::mutex s_TreeMutex;
|
||||
static void SaveAssetPack(const std::string &outputPath);
|
||||
|
||||
static std::mutex s_TreeMutex;
|
||||
|
||||
private:
|
||||
struct AssetMetadata {
|
||||
struct AssetMetadata
|
||||
{
|
||||
std::string type;
|
||||
std::string absolutePath;
|
||||
};
|
||||
struct PendingTexture {
|
||||
|
||||
struct PendingTexture
|
||||
{
|
||||
std::string id;
|
||||
int width, height, channels;
|
||||
unsigned char* data;
|
||||
unsigned char *data;
|
||||
};
|
||||
|
||||
static void BackgroundScan();
|
||||
static void AddToTree(const std::string& virtualPath, bool isDir);
|
||||
static std::string DetectAssetType(const std::filesystem::path& path);
|
||||
static std::string MakeVirtualPath(const std::filesystem::path& full);
|
||||
|
||||
static void AddToTree(const std::string &virtualPath, bool isDir);
|
||||
|
||||
static std::string DetectAssetType(const std::filesystem::path &path);
|
||||
|
||||
static std::string MakeVirtualPath(const std::filesystem::path &full);
|
||||
|
||||
// loaded assets & metadata
|
||||
static std::unordered_map<std::string, std::string> s_PathToID;
|
||||
static std::unordered_map<std::string, std::shared_ptr<Asset>> s_LoadedAssets;
|
||||
static std::unordered_map<std::string, AssetMetadata> s_MetadataMap;
|
||||
static std::mutex s_AssetMutex;
|
||||
static std::unordered_map<std::string, std::shared_ptr<Asset> > s_LoadedAssets;
|
||||
static std::unordered_map<std::string, AssetMetadata> s_MetadataMap;
|
||||
static std::mutex s_AssetMutex;
|
||||
|
||||
// directory tree
|
||||
static std::shared_ptr<ResourceTreeNode> s_FileTree;
|
||||
|
||||
// background scan
|
||||
static std::filesystem::path s_ProjectRoot;
|
||||
static std::atomic<bool> s_Scanning;
|
||||
static std::thread s_ScanThread;
|
||||
static std::atomic<bool> s_Scanning;
|
||||
static std::thread s_ScanThread;
|
||||
|
||||
// texture upload queue
|
||||
static std::queue<PendingTexture> s_TextureQueue;
|
||||
static std::mutex s_QueueMutex;
|
||||
static std::mutex s_QueueMutex;
|
||||
|
||||
static std::atomic<size_t> s_TotalTexturesToLoad;
|
||||
static std::atomic<size_t> s_LoadedTexturesCount;
|
||||
};
|
||||
}
|
||||
|
36
src/core/systems/Scene/Components/Component.h
Normal file
36
src/core/systems/Scene/Components/Component.h
Normal file
@ -0,0 +1,36 @@
|
||||
//
|
||||
// Created by spenc on 5/22/2025.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
namespace OX
|
||||
{
|
||||
class GameObject;
|
||||
|
||||
class Component
|
||||
{
|
||||
public:
|
||||
virtual ~Component() = default;
|
||||
|
||||
/// Called when this component is attached to a GameObject.
|
||||
virtual void OnAttach(GameObject *owner) { m_owner = owner; }
|
||||
|
||||
/// Called just before this component is detached or destroyed.
|
||||
virtual void OnDetach() { m_owner = nullptr; }
|
||||
|
||||
/// Returns the GameObject this component is attached to (or nullptr).
|
||||
[[nodiscard]] GameObject *GetOwner() const { return m_owner; }
|
||||
|
||||
/// Serialize this component’s state into a YAML node.
|
||||
[[nodiscard]] virtual YAML::Node Serialize() const = 0;
|
||||
|
||||
/// Restore this component’s state from a YAML node.
|
||||
virtual void Deserialize(const YAML::Node &node) = 0;
|
||||
|
||||
protected:
|
||||
GameObject *m_owner = nullptr;
|
||||
};
|
||||
};
|
23
src/core/systems/Scene/Components/TagComponent.cpp
Normal file
23
src/core/systems/Scene/Components/TagComponent.cpp
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// Created by spenc on 5/22/2025.
|
||||
//
|
||||
|
||||
// src/Components/TagComponent.cpp
|
||||
#include "TagComponent.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
YAML::Node TagComponent::Serialize() const
|
||||
{
|
||||
YAML::Node node;
|
||||
node["type"] = "TagComponent";
|
||||
node["name"] = name;
|
||||
return node;
|
||||
}
|
||||
|
||||
void TagComponent::Deserialize(const YAML::Node &node)
|
||||
{
|
||||
if (node["name"])
|
||||
name = node["name"].as<std::string>();
|
||||
}
|
||||
} // namespace OX
|
30
src/core/systems/Scene/Components/TagComponent.h
Normal file
30
src/core/systems/Scene/Components/TagComponent.h
Normal file
@ -0,0 +1,30 @@
|
||||
// src/h/Components/TagComponent.h
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <yaml-cpp/yaml.h>
|
||||
#include "Component.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
class TagComponent : public Component
|
||||
{
|
||||
public:
|
||||
TagComponent() = default;
|
||||
|
||||
explicit TagComponent(std::string tag)
|
||||
: name(std::move(tag))
|
||||
{
|
||||
}
|
||||
|
||||
/// The tag string you’ll look up by
|
||||
std::string name;
|
||||
|
||||
/// Serialize into YAML: writes { type: "TagComponent", name: "<your tag>" }
|
||||
[[nodiscard]] YAML::Node Serialize() const override;
|
||||
|
||||
/// Deserialize from YAML: reads `.name` from node["name"]
|
||||
void Deserialize(const YAML::Node &node) override;
|
||||
};
|
||||
} // namespace OX
|
88
src/core/systems/Scene/GameObject.cpp
Normal file
88
src/core/systems/Scene/GameObject.cpp
Normal file
@ -0,0 +1,88 @@
|
||||
// src/GameObject.cpp
|
||||
#include "GameObject.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
GameObject::GameObject(const std::string &name)
|
||||
: m_name(name)
|
||||
{
|
||||
}
|
||||
|
||||
GameObject::~GameObject() = default;
|
||||
|
||||
void GameObject::setName(const std::string &name) { m_name = name; }
|
||||
const std::string &GameObject::name() const { return m_name; }
|
||||
|
||||
GameObject *GameObject::parent() const { return m_parent; }
|
||||
|
||||
void GameObject::addChild(std::unique_ptr<GameObject> child)
|
||||
{
|
||||
child->m_parent = this;
|
||||
m_children.push_back(std::move(child));
|
||||
}
|
||||
|
||||
std::unique_ptr<GameObject> GameObject::removeChild(GameObject *child)
|
||||
{
|
||||
auto it = std::find_if(m_children.begin(), m_children.end(),
|
||||
[child](const std::unique_ptr<GameObject> &u) { return u.get() == child; });
|
||||
if (it == m_children.end()) return nullptr;
|
||||
auto out = std::move(*it);
|
||||
out->m_parent = nullptr;
|
||||
m_children.erase(it);
|
||||
return out;
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<GameObject> > &GameObject::children() const
|
||||
{
|
||||
return m_children;
|
||||
}
|
||||
|
||||
void GameObject::addComponent(std::unique_ptr<Component> comp)
|
||||
{
|
||||
comp->OnAttach(this);
|
||||
m_components.push_back(std::move(comp));
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<Component> > &GameObject::components() const
|
||||
{
|
||||
return m_components;
|
||||
}
|
||||
|
||||
YAML::Node GameObject::Serialize() const
|
||||
{
|
||||
YAML::Node node;
|
||||
node["name"] = m_name;
|
||||
|
||||
for (auto &comp: m_components)
|
||||
node["components"].push_back(comp->Serialize());
|
||||
|
||||
for (auto &child: m_children)
|
||||
node["children"].push_back(child->Serialize());
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
void GameObject::Deserialize(const YAML::Node &node)
|
||||
{
|
||||
if (node["name"])
|
||||
m_name = node["name"].as<std::string>();
|
||||
|
||||
if (node["components"]) {
|
||||
for (auto &cnode: node["components"]) {
|
||||
// e.g.:
|
||||
// std::string type = cnode["type"].as<std::string>();
|
||||
// auto comp = ComponentFactory::Create(type);
|
||||
// comp->Deserialize(cnode);
|
||||
// addComponent(std::move(comp));
|
||||
}
|
||||
}
|
||||
|
||||
if (node["children"]) {
|
||||
for (auto &cnode: node["children"]) {
|
||||
auto child = std::make_unique<GameObject>();
|
||||
child->Deserialize(cnode);
|
||||
addChild(std::move(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace OX
|
82
src/core/systems/Scene/GameObject.h
Normal file
82
src/core/systems/Scene/GameObject.h
Normal file
@ -0,0 +1,82 @@
|
||||
// src/h/GameObject.h
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <yaml-cpp/yaml.h>
|
||||
#include "Components/Component.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
class GameObject
|
||||
{
|
||||
public:
|
||||
explicit GameObject(const std::string &name = "");
|
||||
|
||||
~GameObject();
|
||||
|
||||
// Name
|
||||
void setName(const std::string &name);
|
||||
|
||||
const std::string &name() const;
|
||||
|
||||
// Hierarchy
|
||||
GameObject *parent() const;
|
||||
|
||||
void addChild(std::unique_ptr<GameObject> child);
|
||||
|
||||
std::unique_ptr<GameObject> removeChild(GameObject *child);
|
||||
|
||||
const std::vector<std::unique_ptr<GameObject> > &children() const;
|
||||
|
||||
// Components
|
||||
void addComponent(std::unique_ptr<Component> comp);
|
||||
|
||||
template<typename T>
|
||||
T *getComponent() const;
|
||||
|
||||
template<typename T>
|
||||
std::unique_ptr<Component> removeComponent();
|
||||
|
||||
const std::vector<std::unique_ptr<Component> > &components() const;
|
||||
|
||||
// Serialization
|
||||
YAML::Node Serialize() const;
|
||||
|
||||
void Deserialize(const YAML::Node &node);
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
GameObject *m_parent = nullptr;
|
||||
std::vector<std::unique_ptr<GameObject> > m_children;
|
||||
std::vector<std::unique_ptr<Component> > m_components;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
T *GameObject::getComponent() const
|
||||
{
|
||||
for (auto &c: m_components) {
|
||||
if (auto ptr = dynamic_cast<T *>(c.get()))
|
||||
return ptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::unique_ptr<Component> GameObject::removeComponent()
|
||||
{
|
||||
auto it = std::find_if(m_components.begin(), m_components.end(),
|
||||
[](const std::unique_ptr<Component> &c)
|
||||
{
|
||||
return dynamic_cast<T *>(c.get()) != nullptr;
|
||||
});
|
||||
if (it == m_components.end())
|
||||
return nullptr;
|
||||
|
||||
auto out = std::move(*it);
|
||||
out->OnDetach();
|
||||
m_components.erase(it);
|
||||
return out;
|
||||
}
|
||||
} // namespace OX
|
60
src/core/systems/Scene/Scene.cpp
Normal file
60
src/core/systems/Scene/Scene.cpp
Normal file
@ -0,0 +1,60 @@
|
||||
// src/Scene.cpp
|
||||
#include "Scene.h"
|
||||
#include <algorithm> // for std::find_if
|
||||
|
||||
namespace OX
|
||||
{
|
||||
GameObject *Scene::createRootObject(const std::string &name)
|
||||
{
|
||||
auto go = std::make_unique<GameObject>(name);
|
||||
GameObject *ptr = go.get();
|
||||
registerByTag(ptr);
|
||||
m_roots.push_back(std::move(go));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
bool Scene::removeRootObject(GameObject *go)
|
||||
{
|
||||
auto it = std::find_if(
|
||||
m_roots.begin(), m_roots.end(),
|
||||
[go](const std::unique_ptr<GameObject> &u) { return u.get() == go; }
|
||||
);
|
||||
if (it == m_roots.end())
|
||||
return false;
|
||||
|
||||
unregisterByTag(go);
|
||||
m_roots.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<GameObject> > &Scene::roots() const
|
||||
{
|
||||
return m_roots;
|
||||
}
|
||||
|
||||
GameObject *Scene::findByTag(const std::string &tag) const
|
||||
{
|
||||
auto it = m_tagIndex.find(tag);
|
||||
return (it != m_tagIndex.end()) ? it->second : nullptr;
|
||||
}
|
||||
|
||||
void Scene::registerByTag(GameObject *go)
|
||||
{
|
||||
if (auto tc = go->getComponent<TagComponent>()) {
|
||||
m_tagIndex[tc->name] = go;
|
||||
}
|
||||
for (auto &child: go->children()) {
|
||||
registerByTag(child.get());
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::unregisterByTag(GameObject *go)
|
||||
{
|
||||
if (auto tc = go->getComponent<TagComponent>()) {
|
||||
m_tagIndex.erase(tc->name);
|
||||
}
|
||||
for (auto &child: go->children()) {
|
||||
unregisterByTag(child.get());
|
||||
}
|
||||
}
|
||||
} // namespace OX
|
44
src/core/systems/Scene/Scene.h
Normal file
44
src/core/systems/Scene/Scene.h
Normal file
@ -0,0 +1,44 @@
|
||||
// src/h/Scene.h
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
#include "GameObject.h"
|
||||
#include "Components/TagComponent.h"
|
||||
#include "systems/MACROS.h"
|
||||
|
||||
namespace OX
|
||||
{
|
||||
class Scene
|
||||
{
|
||||
public:
|
||||
Scene() = default;
|
||||
|
||||
~Scene() = default;
|
||||
|
||||
/// Create and own a new root GameObject. Returns the raw pointer.
|
||||
GameObject *createRootObject(const std::string &name = "");
|
||||
|
||||
/// Remove (and destroy) a root object by pointer.
|
||||
bool removeRootObject(GameObject *go);
|
||||
|
||||
/// Access all root objects.
|
||||
const std::vector<std::unique_ptr<GameObject> > &roots() const;
|
||||
|
||||
/// O(1)
|
||||
GameObject *findByTag(const std::string &tag) const;
|
||||
|
||||
/// Must be called whenever a GameObject’s tag is set or changed.
|
||||
void registerByTag(GameObject *go);
|
||||
|
||||
/// Must be called before a GameObject is destroyed or its tag changes.
|
||||
void unregisterByTag(GameObject *go);
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<GameObject> > m_roots;
|
||||
std::unordered_map<std::string, GameObject *> m_tagIndex;
|
||||
};
|
||||
} // namespace OX
|
@ -44,7 +44,6 @@ namespace OX
|
||||
|
||||
fileBrowser = std::make_shared<FileBrowser>();
|
||||
primaryViewport = std::make_shared<Viewport>();
|
||||
|
||||
}
|
||||
|
||||
void Editor::Update(Core &core)
|
||||
@ -153,8 +152,6 @@ namespace OX
|
||||
|
||||
// --- Render ImGui onto FBO 0 ---
|
||||
{
|
||||
|
||||
|
||||
ImGui::EndFrame();
|
||||
ImGui::Render();
|
||||
ImGui::UpdatePlatformWindows();
|
||||
@ -164,11 +161,8 @@ namespace OX
|
||||
|
||||
void Editor::Shutdown(Core &core)
|
||||
{
|
||||
|
||||
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplGlfw_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
|
||||
}
|
||||
} // OX
|
||||
|
@ -5,40 +5,60 @@
|
||||
|
||||
namespace OX
|
||||
{
|
||||
FileBrowser::FileBrowser() = default;
|
||||
FileBrowser::FileBrowser()
|
||||
: _lastProgress(0.0f)
|
||||
{
|
||||
}
|
||||
|
||||
FileBrowser::~FileBrowser() = default;
|
||||
|
||||
// Call this every frame to report your current loading progress (0.0 → 1.0).
|
||||
// Passing in 0 resets it and hides the bar.
|
||||
void FileBrowser::SetProgress(const float p)
|
||||
{
|
||||
if (p <= 0.0f) {
|
||||
_lastProgress = 0.0f;
|
||||
} else {
|
||||
_lastProgress = std::max(_lastProgress, p);
|
||||
}
|
||||
}
|
||||
|
||||
void FileBrowser::SetFilter(const std::string &f) { _filter = f; }
|
||||
void FileBrowser::SetFileSelectedCallback(FileSelectedCallback cb) { _onFileSelected = std::move(cb); }
|
||||
|
||||
void FileBrowser::Draw(const char *title)
|
||||
{
|
||||
OX_PROFILE_FUNCTION();
|
||||
|
||||
|
||||
ImGui::Begin(title);
|
||||
|
||||
// --- toolbar now contains back button, inline path, filter & view toggle all on one row ---
|
||||
DrawToolbar();
|
||||
|
||||
// main content
|
||||
// ——— main content ———
|
||||
auto root = AssetManager::GetFileTree();
|
||||
std::function<std::shared_ptr<ResourceTreeNode>(std::shared_ptr<ResourceTreeNode>, const std::string &)> find =
|
||||
[&](auto node, const std::string &path)-> std::shared_ptr<ResourceTreeNode>
|
||||
[&](auto node, const std::string &path) -> std::shared_ptr<ResourceTreeNode>
|
||||
{
|
||||
if (node->path == path) return node;
|
||||
for (auto &c: node->children) {
|
||||
if (c->isDirectory) {
|
||||
if (auto f = find(c, path)) return f;
|
||||
}
|
||||
}
|
||||
if (node->path == path)
|
||||
return node;
|
||||
for (auto &c: node->children)
|
||||
if (c->isDirectory)
|
||||
if (auto f = find(c, path))
|
||||
return f;
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
auto node = find(root, _currentPath);
|
||||
if (!node) {
|
||||
ImGui::TextColored(ImGui::GetStyle().Colors[ImGuiCol_TextDisabled],
|
||||
"Path not found: %s", _currentPath.c_str());
|
||||
} else if (_gridMode) {
|
||||
// ——— Grid view with nicer spacing ———
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(_cfg.padding, _cfg.padding));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(_cfg.padding, _cfg.padding));
|
||||
DrawGridView(node);
|
||||
ImGui::PopStyleVar(2);
|
||||
} else {
|
||||
DrawListView(node);
|
||||
}
|
||||
@ -49,7 +69,6 @@ namespace OX
|
||||
// ——— Combined toolbar & inline path ———
|
||||
void FileBrowser::DrawToolbar()
|
||||
{
|
||||
// back button
|
||||
if (_currentPath != "res://") {
|
||||
if (ImGui::Button("Back")) {
|
||||
auto pos = _currentPath.find_last_of('/');
|
||||
@ -57,31 +76,34 @@ namespace OX
|
||||
? _currentPath.substr(0, pos)
|
||||
: "res://";
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
|
||||
// inline, non-interactive path text
|
||||
ImGui::Text("%s", _currentPath.c_str());
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// filter input takes all remaining space
|
||||
float avail = ImGui::GetContentRegionAvail().x;
|
||||
ImGui::PushItemWidth(avail * 0.5f);
|
||||
//ImGui::InputTextWithHint("##filter", "Filter files...", _filter.c_str(), ImGuiInputTextFlags_AutoSelectAll);
|
||||
|
||||
|
||||
//ImGui::InputTextWithHint("##filter", "Filter files...", &_filter);
|
||||
ImGui::PopItemWidth();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
// grid/list toggle
|
||||
if (_gridMode) {
|
||||
if (ImGui::Button("List View")) _gridMode = false;
|
||||
} else {
|
||||
if (ImGui::Button("Grid View")) _gridMode = true;
|
||||
}
|
||||
|
||||
size_t total = AssetManager::GetTotalTexturesToLoad();
|
||||
size_t loaded = AssetManager::GetLoadedTexturesCount();
|
||||
|
||||
if (total > 0 && loaded < total) {
|
||||
float progress = float(loaded) / float(total);
|
||||
ImGui::SameLine();
|
||||
ImGui::ProgressBar(progress, ImVec2(-1, 0));
|
||||
}
|
||||
|
||||
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
@ -89,39 +111,44 @@ namespace OX
|
||||
void FileBrowser::DrawGridView(const std::shared_ptr<ResourceTreeNode> &node)
|
||||
{
|
||||
OX_PROFILE_FUNCTION();
|
||||
|
||||
const float cellW = _cfg.thumbnailSize + _cfg.padding * 2;
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
int cols = std::max(1, int(avail.x / cellW));
|
||||
|
||||
ImGui::BeginChild("GridRegion", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysUseWindowPadding);
|
||||
// Add a little padding around the child region
|
||||
ImGui::BeginChild("GridRegion",
|
||||
ImVec2(0, 0),
|
||||
false,
|
||||
ImGuiWindowFlags_AlwaysUseWindowPadding);
|
||||
|
||||
ImGui::Columns(cols, nullptr, false);
|
||||
|
||||
std::vector<std::shared_ptr<ResourceTreeNode> > children; {
|
||||
std::lock_guard<std::mutex> lock(AssetManager::s_TreeMutex); // assume you add this mutex
|
||||
std::lock_guard<std::mutex> lock(AssetManager::s_TreeMutex);
|
||||
children = node->children;
|
||||
}
|
||||
for (auto &c: children) {
|
||||
if (!c) return;
|
||||
|
||||
for (auto &c: children) {
|
||||
if (!c) continue;
|
||||
if (!_filter.empty() && !PassesFilter(c->name)) continue;
|
||||
|
||||
ImGui::PushID(c->path.c_str());
|
||||
ImGui::BeginGroup();
|
||||
|
||||
// invisible button as hit target
|
||||
ImVec2 btnSize(cellW, _cfg.thumbnailSize + _cfg.labelHeight + _cfg.padding);
|
||||
ImGui::InvisibleButton("cell", btnSize);
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
bool clicked = ImGui::IsItemClicked();
|
||||
|
||||
// background rect
|
||||
// background
|
||||
ImVec2 mn = ImGui::GetItemRectMin();
|
||||
ImVec2 mx = ImGui::GetItemRectMax();
|
||||
ImU32 bg = hovered ? _cfg.highlightColor : _cfg.bgColor;
|
||||
ImGui::GetWindowDrawList()
|
||||
->AddRectFilled(mn, mx, bg, 4.0f);
|
||||
|
||||
// thumbnail or icon
|
||||
// thumbnail
|
||||
ImGui::SetCursorScreenPos({mn.x + _cfg.padding, mn.y + _cfg.padding});
|
||||
if (c->isDirectory) {
|
||||
ImGui::Image(GetIconTexture(*c),
|
||||
@ -146,11 +173,10 @@ namespace OX
|
||||
|
||||
// click handling
|
||||
if (clicked) {
|
||||
if (c->isDirectory) {
|
||||
if (c->isDirectory)
|
||||
_currentPath = c->path;
|
||||
} else if (_onFileSelected) {
|
||||
else if (_onFileSelected)
|
||||
_onFileSelected(c->path);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndGroup();
|
||||
@ -171,11 +197,10 @@ namespace OX
|
||||
if (!_filter.empty() && !PassesFilter(c->name)) continue;
|
||||
if (ImGui::Selectable(c->name.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick)) {
|
||||
if (ImGui::IsMouseDoubleClicked(0)) {
|
||||
if (c->isDirectory) {
|
||||
if (c->isDirectory)
|
||||
_currentPath = c->path;
|
||||
} else if (_onFileSelected) {
|
||||
else if (_onFileSelected)
|
||||
_onFileSelected(c->path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -185,8 +210,7 @@ namespace OX
|
||||
|
||||
bool FileBrowser::PassesFilter(const std::string &name) const
|
||||
{
|
||||
return _filter.empty() ||
|
||||
(name.find(_filter) != std::string::npos);
|
||||
return _filter.empty() || (name.find(_filter) != std::string::npos);
|
||||
}
|
||||
|
||||
ImTextureID FileBrowser::GetIconTexture(const ResourceTreeNode &node)
|
||||
|
@ -43,6 +43,8 @@ namespace OX {
|
||||
/// Called whenever the user clicks on a file (not directory)
|
||||
void SetFileSelectedCallback(FileSelectedCallback cb);
|
||||
|
||||
|
||||
|
||||
/// Access to tweak colors/sizes
|
||||
FileBrowserConfig& Config() { return _cfg; }
|
||||
|
||||
@ -52,7 +54,8 @@ namespace OX {
|
||||
void DrawBreadcrumbs();
|
||||
void DrawGridView(const std::shared_ptr<ResourceTreeNode>& node);
|
||||
void DrawListView(const std::shared_ptr<ResourceTreeNode>& node);
|
||||
bool PassesFilter(const std::string& name) const;
|
||||
[[nodiscard]] bool PassesFilter(const std::string& name) const;
|
||||
void SetProgress(float p);
|
||||
ImTextureID GetIconTexture(const ResourceTreeNode&); // stub for folder/file icons
|
||||
|
||||
// state
|
||||
@ -61,5 +64,8 @@ namespace OX {
|
||||
std::string _filter;
|
||||
bool _gridMode = true;
|
||||
FileSelectedCallback _onFileSelected;
|
||||
float _lastProgress;
|
||||
size_t _maxQueueSize;
|
||||
|
||||
};
|
||||
} // namespace OX
|
||||
|
Loading…
Reference in New Issue
Block a user