Some checks failed
Build C++ Project with Fedora Container and Create Release / build (push) Has been cancelled
3333 lines
124 KiB
C++
3333 lines
124 KiB
C++
#include <iostream>
|
|
#include <algorithm>
|
|
#include <stdexcept>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <ctime>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <glad/glad.h>
|
|
#include "ThirdParty/imgui/imgui.h"
|
|
#include "ThirdParty/imgui/imgui_internal.h"
|
|
#include "ThirdParty/imgui/backends/imgui_impl_glfw.h"
|
|
#include "ThirdParty/imgui/backends/imgui_impl_opengl3.h"
|
|
#include "ThirdParty/ImGuizmo/ImGuizmo.h"
|
|
#include "ThirdParty/glm/gtc/matrix_transform.hpp"
|
|
#include "ThirdParty/glm/gtc/type_ptr.hpp"
|
|
#define TINYOBJLOADER_IMPLEMENTATION
|
|
#include "../include/ThirdParty/tiny_obj_loader.h"
|
|
#include "../include/Window/Window.h"
|
|
#include "../include/Shaders/Shader.h"
|
|
#include "../include/Textures/Texture.h"
|
|
#include "../include/Skybox/Skybox.h"
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#include <shlobj.h>
|
|
#endif
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
constexpr float SENSITIVITY = 0.1f;
|
|
constexpr float CAMERA_SPEED = 2.5f;
|
|
constexpr float FOV = 45.0f;
|
|
constexpr float NEAR_PLANE = 0.1f;
|
|
constexpr float FAR_PLANE = 100.0f;
|
|
constexpr float PI = 3.14159265359f;
|
|
|
|
// Replace the existing float vertices[] array (lines ~50-100) with this full 8-float version (pos + normal + texcoord)
|
|
float vertices[] = {
|
|
// Back face (z = -0.5f)
|
|
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
|
|
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
|
|
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
|
|
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
|
|
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
|
|
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
|
|
|
|
// Front face (z = 0.5f)
|
|
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
|
|
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,
|
|
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
|
|
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
|
|
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f,
|
|
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
|
|
|
|
// Left face (x = -0.5f)
|
|
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
|
|
-0.5f, 0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
|
|
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
|
|
-0.5f, -0.5f, -0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
|
|
-0.5f, -0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
|
|
-0.5f, 0.5f, 0.5f, -1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
|
|
|
|
// Right face (x = 0.5f)
|
|
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
|
|
0.5f, 0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f,
|
|
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
|
|
0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f,
|
|
0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f,
|
|
0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
|
|
|
|
// Bottom face (y = -0.5f)
|
|
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
|
|
0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 1.0f,
|
|
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
|
|
0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f,
|
|
-0.5f, -0.5f, 0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 0.0f,
|
|
-0.5f, -0.5f, -0.5f, 0.0f, -1.0f, 0.0f, 0.0f, 1.0f,
|
|
|
|
// Top face (y = 0.5f)
|
|
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,
|
|
0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f,
|
|
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
|
|
0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f,
|
|
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f,
|
|
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f
|
|
};
|
|
|
|
enum class ObjectType {
|
|
Cube,
|
|
Sphere,
|
|
Capsule,
|
|
OBJMesh // New type for loaded OBJ models
|
|
};
|
|
|
|
enum class ConsoleMessageType {
|
|
Info,
|
|
Warning,
|
|
Error,
|
|
Success
|
|
};
|
|
|
|
class SceneObject {
|
|
public:
|
|
std::string name;
|
|
ObjectType type;
|
|
glm::vec3 position;
|
|
glm::vec3 rotation;
|
|
glm::vec3 scale;
|
|
int id;
|
|
int parentId = -1;
|
|
std::vector<int> childIds;
|
|
bool isExpanded = true;
|
|
std::string meshPath; // Path to OBJ file (for OBJMesh type)
|
|
int meshId = -1; // Index into loaded meshes cache
|
|
|
|
SceneObject(const std::string& name, ObjectType type, int id)
|
|
: name(name), type(type), position(0.0f), rotation(0.0f), scale(1.0f), id(id) {}
|
|
};
|
|
|
|
class FileBrowser {
|
|
public:
|
|
fs::path currentPath;
|
|
fs::path selectedFile;
|
|
std::vector<fs::directory_entry> entries;
|
|
bool needsRefresh = true;
|
|
|
|
FileBrowser() {
|
|
currentPath = fs::current_path();
|
|
}
|
|
|
|
void refresh() {
|
|
entries.clear();
|
|
try {
|
|
for (const auto& entry : fs::directory_iterator(currentPath)) {
|
|
entries.push_back(entry);
|
|
}
|
|
std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
|
|
if (a.is_directory() != b.is_directory()) {
|
|
return a.is_directory() > b.is_directory();
|
|
}
|
|
return a.path().filename().string() < b.path().filename().string();
|
|
});
|
|
} catch (...) {
|
|
}
|
|
needsRefresh = false;
|
|
}
|
|
|
|
void navigateUp() {
|
|
if (currentPath.has_parent_path() && currentPath != currentPath.root_path()) {
|
|
currentPath = currentPath.parent_path();
|
|
needsRefresh = true;
|
|
}
|
|
}
|
|
|
|
void navigateTo(const fs::path& path) {
|
|
if (fs::is_directory(path)) {
|
|
currentPath = path;
|
|
needsRefresh = true;
|
|
}
|
|
}
|
|
|
|
const char* getFileIcon(const fs::directory_entry& entry) const {
|
|
if (entry.is_directory()) return "[D]";
|
|
|
|
std::string ext = entry.path().extension().string();
|
|
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
|
|
|
if (ext == ".cpp" || ext == ".c" || ext == ".h" || ext == ".hpp") return "[C]";
|
|
if (ext == ".glsl" || ext == ".vert" || ext == ".frag") return "[S]";
|
|
if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".bmp") return "[I]";
|
|
if (ext == ".obj" || ext == ".fbx" || ext == ".gltf") return "[M]";
|
|
if (ext == ".txt" || ext == ".md") return "[T]";
|
|
return "[F]";
|
|
}
|
|
|
|
bool isOBJFile(const fs::directory_entry& entry) const {
|
|
if (entry.is_directory()) return false;
|
|
std::string ext = entry.path().extension().string();
|
|
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
|
return ext == ".obj";
|
|
}
|
|
};
|
|
|
|
// Replace the entire generateSphere() function (around line ~200) with this version that adds normals (now 8 floats per vertex)
|
|
std::vector<float> generateSphere(int segments = 32, int rings = 16) {
|
|
std::vector<float> vertices;
|
|
|
|
for (int ring = 0; ring <= rings; ring++) {
|
|
float theta = ring * PI / rings;
|
|
float sinTheta = sin(theta);
|
|
float cosTheta = cos(theta);
|
|
|
|
for (int seg = 0; seg <= segments; seg++) {
|
|
float phi = seg * 2.0f * PI / segments;
|
|
float sinPhi = sin(phi);
|
|
float cosPhi = cos(phi);
|
|
|
|
float x = cosPhi * sinTheta;
|
|
float y = cosTheta;
|
|
float z = sinPhi * sinTheta;
|
|
|
|
// Position
|
|
vertices.push_back(x * 0.5f);
|
|
vertices.push_back(y * 0.5f);
|
|
vertices.push_back(z * 0.5f);
|
|
|
|
// Normal (same as position for unit sphere)
|
|
vertices.push_back(x);
|
|
vertices.push_back(y);
|
|
vertices.push_back(z);
|
|
|
|
// Texcoord
|
|
vertices.push_back((float)seg / segments);
|
|
vertices.push_back((float)ring / rings);
|
|
}
|
|
}
|
|
|
|
std::vector<float> triangulated;
|
|
int stride = segments + 1;
|
|
for (int ring = 0; ring < rings; ring++) {
|
|
for (int seg = 0; seg < segments; seg++) {
|
|
int current = ring * stride + seg;
|
|
int next = current + stride;
|
|
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[current * 8 + i]);
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
|
|
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(next + 1) * 8 + i]);
|
|
}
|
|
}
|
|
|
|
return triangulated;
|
|
}
|
|
|
|
// Replace the entire generateCapsule() function (around line ~250) with this version that adds normals (now 8 floats per vertex)
|
|
std::vector<float> generateCapsule(int segments = 16, int rings = 8) {
|
|
std::vector<float> vertices;
|
|
float cylinderHeight = 0.5f;
|
|
float radius = 0.25f;
|
|
|
|
// Top hemisphere
|
|
for (int ring = 0; ring <= rings / 2; ring++) {
|
|
float theta = ring * PI / rings;
|
|
float sinTheta = sin(theta);
|
|
float cosTheta = cos(theta);
|
|
|
|
for (int seg = 0; seg <= segments; seg++) {
|
|
float phi = seg * 2.0f * PI / segments;
|
|
float sinPhi = sin(phi);
|
|
float cosPhi = cos(phi);
|
|
|
|
float x = cosPhi * sinTheta * radius;
|
|
float y = cosTheta * radius + cylinderHeight;
|
|
float z = sinPhi * sinTheta * radius;
|
|
|
|
// Position
|
|
vertices.push_back(x);
|
|
vertices.push_back(y);
|
|
vertices.push_back(z);
|
|
|
|
// Normal (normalized direction from center)
|
|
glm::vec3 normal = glm::normalize(glm::vec3(x, y - cylinderHeight, z));
|
|
vertices.push_back(normal.x);
|
|
vertices.push_back(normal.y);
|
|
vertices.push_back(normal.z);
|
|
|
|
// Texcoord
|
|
vertices.push_back((float)seg / segments);
|
|
vertices.push_back((float)ring / (rings / 2));
|
|
}
|
|
}
|
|
|
|
// Cylinder body (top and bottom rings)
|
|
for (int i = 0; i <= 1; i++) {
|
|
float y = i == 0 ? cylinderHeight : -cylinderHeight;
|
|
for (int seg = 0; seg <= segments; seg++) {
|
|
float phi = seg * 2.0f * PI / segments;
|
|
float x = cos(phi) * radius;
|
|
float z = sin(phi) * radius;
|
|
|
|
// Position
|
|
vertices.push_back(x);
|
|
vertices.push_back(y);
|
|
vertices.push_back(z);
|
|
|
|
// Normal (outward for cylinder)
|
|
glm::vec3 normal = glm::normalize(glm::vec3(x, 0.0f, z));
|
|
vertices.push_back(normal.x);
|
|
vertices.push_back(normal.y);
|
|
vertices.push_back(normal.z);
|
|
|
|
// Texcoord
|
|
vertices.push_back((float)seg / segments);
|
|
vertices.push_back(0.5f);
|
|
}
|
|
}
|
|
|
|
// Bottom hemisphere
|
|
for (int ring = rings / 2; ring <= rings; ring++) {
|
|
float theta = ring * PI / rings;
|
|
float sinTheta = sin(theta);
|
|
float cosTheta = cos(theta);
|
|
|
|
for (int seg = 0; seg <= segments; seg++) {
|
|
float phi = seg * 2.0f * PI / segments;
|
|
float sinPhi = sin(phi);
|
|
float cosPhi = cos(phi);
|
|
|
|
float x = cosPhi * sinTheta * radius;
|
|
float y = cosTheta * radius - cylinderHeight;
|
|
float z = sinPhi * sinTheta * radius;
|
|
|
|
// Position
|
|
vertices.push_back(x);
|
|
vertices.push_back(y);
|
|
vertices.push_back(z);
|
|
|
|
// Normal
|
|
glm::vec3 normal = glm::normalize(glm::vec3(x, y + cylinderHeight, z));
|
|
vertices.push_back(normal.x);
|
|
vertices.push_back(normal.y);
|
|
vertices.push_back(normal.z);
|
|
|
|
// Texcoord
|
|
vertices.push_back((float)seg / segments);
|
|
vertices.push_back((float)ring / rings);
|
|
}
|
|
}
|
|
|
|
std::vector<float> triangulated;
|
|
int stride = segments + 1;
|
|
int totalRings = rings + 3; // hemispheres + 3 for cylinder rings? Wait, adjust if needed
|
|
|
|
for (int ring = 0; ring < totalRings - 1; ring++) {
|
|
for (int seg = 0; seg < segments; seg++) {
|
|
int current = ring * stride + seg;
|
|
int next = current + stride;
|
|
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[current * 8 + i]);
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
|
|
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(current + 1) * 8 + i]);
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[next * 8 + i]);
|
|
for (int i = 0; i < 8; i++) triangulated.push_back(vertices[(next + 1) * 8 + i]);
|
|
}
|
|
}
|
|
|
|
return triangulated;
|
|
}
|
|
|
|
class Mesh {
|
|
private:
|
|
unsigned int VAO, VBO;
|
|
int vertexCount;
|
|
|
|
public:
|
|
Mesh(const float* vertexData, size_t dataSizeBytes) {
|
|
vertexCount = dataSizeBytes / (8 * sizeof(float));
|
|
|
|
glGenVertexArrays(1, &VAO);
|
|
glGenBuffers(1, &VBO);
|
|
|
|
glBindVertexArray(VAO);
|
|
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
|
glBufferData(GL_ARRAY_BUFFER, dataSizeBytes, vertexData, GL_STATIC_DRAW);
|
|
|
|
// 0: Position
|
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
|
|
glEnableVertexAttribArray(0);
|
|
|
|
// 1: Normal
|
|
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
|
|
glEnableVertexAttribArray(1);
|
|
|
|
// 2: TexCoord
|
|
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
|
|
glEnableVertexAttribArray(2);
|
|
|
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
glBindVertexArray(0);
|
|
}
|
|
|
|
~Mesh() {
|
|
glDeleteVertexArrays(1, &VAO);
|
|
glDeleteBuffers(1, &VBO);
|
|
}
|
|
|
|
void draw() const {
|
|
glBindVertexArray(VAO);
|
|
glDrawArrays(GL_TRIANGLES, 0, vertexCount);
|
|
glBindVertexArray(0);
|
|
}
|
|
|
|
int getVertexCount() const { return vertexCount; }
|
|
};
|
|
|
|
class OBJLoader {
|
|
public:
|
|
struct LoadedMesh {
|
|
std::string path;
|
|
std::unique_ptr<Mesh> mesh;
|
|
std::string name;
|
|
int vertexCount = 0;
|
|
int faceCount = 0;
|
|
bool hasNormals = false;
|
|
bool hasTexCoords = false;
|
|
};
|
|
|
|
private:
|
|
std::vector<LoadedMesh> loadedMeshes;
|
|
|
|
public:
|
|
// Load an OBJ file and return index into cache, or -1 on failure
|
|
int loadOBJ(const std::string& filepath, std::string& errorMsg) {
|
|
// Check if already loaded
|
|
for (size_t i = 0; i < loadedMeshes.size(); i++) {
|
|
if (loadedMeshes[i].path == filepath) {
|
|
return static_cast<int>(i);
|
|
}
|
|
}
|
|
|
|
tinyobj::attrib_t attrib;
|
|
std::vector<tinyobj::shape_t> shapes;
|
|
std::vector<tinyobj::material_t> materials;
|
|
std::string warn, err;
|
|
|
|
std::string baseDir = fs::path(filepath).parent_path().string();
|
|
if (!baseDir.empty()) baseDir += "/";
|
|
|
|
bool ret = tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err,
|
|
filepath.c_str(), baseDir.c_str());
|
|
|
|
if (!warn.empty()) {
|
|
errorMsg += "Warning: " + warn + "\n";
|
|
}
|
|
|
|
if (!err.empty()) {
|
|
errorMsg += "Error: " + err + "\n";
|
|
}
|
|
|
|
if (!ret || shapes.empty()) {
|
|
errorMsg += "Failed to load OBJ file: " + filepath;
|
|
return -1;
|
|
}
|
|
|
|
// Convert to our vertex format (pos + uv)
|
|
// Convert to our vertex format (pos + normal + uv)
|
|
std::vector<float> vertices;
|
|
bool hasNormalsInFile = !attrib.normals.empty();
|
|
|
|
size_t totalIndices = 0;
|
|
int faceCount = 0;
|
|
for (const auto& shape : shapes) {
|
|
totalIndices += shape.mesh.indices.size();
|
|
faceCount += static_cast<int>(shape.mesh.num_face_vertices.size());
|
|
}
|
|
vertices.reserve(totalIndices * 8 / 3); // rough estimate for triangulated verts
|
|
|
|
for (const auto& shape : shapes) {
|
|
size_t indexOffset = 0;
|
|
for (size_t f = 0; f < shape.mesh.num_face_vertices.size(); f++) {
|
|
int fv = shape.mesh.num_face_vertices[f];
|
|
|
|
// Gather vertices for the face
|
|
struct TempVertex {
|
|
glm::vec3 pos;
|
|
glm::vec2 uv;
|
|
glm::vec3 normal;
|
|
bool hasNormal = false;
|
|
};
|
|
std::vector<TempVertex> faceVerts;
|
|
|
|
for (int v = 0; v < fv; v++) {
|
|
tinyobj::index_t idx = shape.mesh.indices[indexOffset + v];
|
|
|
|
TempVertex tv;
|
|
tv.pos.x = attrib.vertices[3 * size_t(idx.vertex_index) + 0];
|
|
tv.pos.y = attrib.vertices[3 * size_t(idx.vertex_index) + 1];
|
|
tv.pos.z = attrib.vertices[3 * size_t(idx.vertex_index) + 2];
|
|
|
|
if (idx.texcoord_index >= 0 && !attrib.texcoords.empty()) {
|
|
tv.uv.x = attrib.texcoords[2 * size_t(idx.texcoord_index) + 0];
|
|
tv.uv.y = attrib.texcoords[2 * size_t(idx.texcoord_index) + 1];
|
|
} else {
|
|
tv.uv = glm::vec2(0.0f);
|
|
}
|
|
|
|
if (idx.normal_index >= 0 && hasNormalsInFile) {
|
|
tv.normal.x = attrib.normals[3 * size_t(idx.normal_index) + 0];
|
|
tv.normal.y = attrib.normals[3 * size_t(idx.normal_index) + 1];
|
|
tv.normal.z = attrib.normals[3 * size_t(idx.normal_index) + 2];
|
|
tv.hasNormal = true;
|
|
}
|
|
|
|
faceVerts.push_back(tv);
|
|
}
|
|
|
|
// Compute face normal if no normals in file
|
|
if (!hasNormalsInFile && fv >= 3) {
|
|
glm::vec3 v0 = faceVerts[0].pos;
|
|
glm::vec3 v1 = faceVerts[1].pos;
|
|
glm::vec3 v2 = faceVerts[2].pos;
|
|
glm::vec3 faceNormal = glm::normalize(glm::cross(v1 - v0, v2 - v0));
|
|
|
|
for (auto& tv : faceVerts) {
|
|
tv.normal = faceNormal;
|
|
tv.hasNormal = true;
|
|
}
|
|
}
|
|
|
|
// Triangulate fan
|
|
for (int v = 1; v < fv - 1; v++) {
|
|
const TempVertex* tri[3] = { &faceVerts[0], &faceVerts[v], &faceVerts[v+1] };
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
// Pos
|
|
vertices.push_back(tri[i]->pos.x);
|
|
vertices.push_back(tri[i]->pos.y);
|
|
vertices.push_back(tri[i]->pos.z);
|
|
|
|
// Normal
|
|
vertices.push_back(tri[i]->normal.x);
|
|
vertices.push_back(tri[i]->normal.y);
|
|
vertices.push_back(tri[i]->normal.z);
|
|
|
|
// UV
|
|
vertices.push_back(tri[i]->uv.x);
|
|
vertices.push_back(tri[i]->uv.y);
|
|
}
|
|
}
|
|
|
|
indexOffset += fv;
|
|
}
|
|
}
|
|
|
|
if (vertices.empty()) {
|
|
errorMsg += "No vertices found in OBJ file";
|
|
return -1;
|
|
}
|
|
|
|
// Create mesh
|
|
LoadedMesh loaded;
|
|
loaded.path = filepath;
|
|
loaded.name = fs::path(filepath).stem().string();
|
|
loaded.mesh = std::make_unique<Mesh>(vertices.data(), vertices.size() * sizeof(float));
|
|
loaded.vertexCount = static_cast<int>(vertices.size() / 8);
|
|
loaded.faceCount = faceCount;
|
|
loaded.hasNormals = hasNormalsInFile;
|
|
loaded.hasTexCoords = !attrib.texcoords.empty();
|
|
|
|
loadedMeshes.push_back(std::move(loaded));
|
|
return static_cast<int>(loadedMeshes.size() - 1);
|
|
}
|
|
|
|
// Get mesh by index
|
|
Mesh* getMesh(int index) {
|
|
if (index < 0 || index >= static_cast<int>(loadedMeshes.size())) {
|
|
return nullptr;
|
|
}
|
|
return loadedMeshes[index].mesh.get();
|
|
}
|
|
|
|
// Get mesh info
|
|
const LoadedMesh* getMeshInfo(int index) const {
|
|
if (index < 0 || index >= static_cast<int>(loadedMeshes.size())) {
|
|
return nullptr;
|
|
}
|
|
return &loadedMeshes[index];
|
|
}
|
|
|
|
// Get all loaded meshes (for UI display)
|
|
const std::vector<LoadedMesh>& getAllMeshes() const {
|
|
return loadedMeshes;
|
|
}
|
|
|
|
// Clear all loaded meshes
|
|
void clear() {
|
|
loadedMeshes.clear();
|
|
}
|
|
|
|
size_t getMeshCount() const { return loadedMeshes.size(); }
|
|
};
|
|
|
|
// Global OBJ loader instance
|
|
OBJLoader g_objLoader;
|
|
|
|
class Camera {
|
|
public:
|
|
glm::vec3 position = glm::vec3(0.0f, 0.0f, 3.0f);
|
|
glm::vec3 front = glm::vec3(0.0f, 0.0f, -1.0f);
|
|
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
|
|
glm::vec3 velocity = glm::vec3(0.0f);
|
|
float yaw = -90.0f;
|
|
float pitch = 0.0f;
|
|
float speed = CAMERA_SPEED;
|
|
float lastX = 400.0f, lastY = 300.0f;
|
|
bool firstMouse = true;
|
|
|
|
void processMouse(double xpos, double ypos) {
|
|
if (ImGuizmo::IsUsing() || ImGuizmo::IsOver()) {
|
|
// Don't process camera movement when gizmo is active
|
|
return;
|
|
}
|
|
if (firstMouse) {
|
|
lastX = xpos;
|
|
lastY = ypos;
|
|
firstMouse = false;
|
|
}
|
|
|
|
float xoffset = (xpos - lastX) * SENSITIVITY;
|
|
float yoffset = (lastY - ypos) * SENSITIVITY;
|
|
lastX = xpos;
|
|
lastY = ypos;
|
|
|
|
yaw += xoffset;
|
|
pitch += yoffset;
|
|
|
|
if (pitch > 89.0f) pitch = 89.0f;
|
|
if (pitch < -89.0f) pitch = -89.0f;
|
|
|
|
glm::vec3 direction;
|
|
direction.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
|
|
direction.y = sin(glm::radians(pitch));
|
|
direction.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
|
|
front = glm::normalize(direction);
|
|
}
|
|
|
|
void processKeyboard(float deltaTime, GLFWwindow* window) {
|
|
const float CAMERA_SPEED = 5.0f;
|
|
const float SPRINT_SPEED = 10.0f;
|
|
const float ACCELERATION = 15.0f;
|
|
|
|
float currentSpeed = CAMERA_SPEED;
|
|
if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) {
|
|
currentSpeed = SPRINT_SPEED;
|
|
}
|
|
|
|
glm::vec3 desiredDir(0.0f);
|
|
bool isMoving = false;
|
|
|
|
if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
|
|
desiredDir += front;
|
|
isMoving = true;
|
|
}
|
|
if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
|
|
desiredDir -= front;
|
|
isMoving = true;
|
|
}
|
|
if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) {
|
|
desiredDir -= glm::normalize(glm::cross(front, up));
|
|
isMoving = true;
|
|
}
|
|
if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) {
|
|
desiredDir += glm::normalize(glm::cross(front, up));
|
|
isMoving = true;
|
|
}
|
|
if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) {
|
|
desiredDir -= up;
|
|
isMoving = true;
|
|
}
|
|
if (glfwGetKey(window, GLFW_KEY_E) == GLFW_PRESS) {
|
|
desiredDir += up;
|
|
isMoving = true;
|
|
}
|
|
if (glfwGetKey(window, GLFW_KEY_1) == GLFW_PRESS) {
|
|
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
|
|
}
|
|
if (glfwGetKey(window, GLFW_KEY_2) == GLFW_PRESS) {
|
|
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
|
}
|
|
|
|
glm::vec3 targetVelocity(0.0f);
|
|
if (isMoving) {
|
|
float length = glm::length(desiredDir);
|
|
if (length > 0.0001f) {
|
|
desiredDir = desiredDir / length;
|
|
targetVelocity = desiredDir * currentSpeed;
|
|
} else {
|
|
targetVelocity = glm::vec3(0.0f);
|
|
}
|
|
}
|
|
|
|
float smoothFactor = 1.0f - std::exp(-ACCELERATION * deltaTime);
|
|
velocity = glm::mix(velocity, targetVelocity, smoothFactor);
|
|
|
|
position += velocity * deltaTime;
|
|
}
|
|
|
|
glm::mat4 getViewMatrix() const {
|
|
return glm::lookAt(position, position + front, up);
|
|
}
|
|
};
|
|
|
|
class ViewportController {
|
|
private:
|
|
bool viewportFocused = false;
|
|
bool manualUnfocus = false;
|
|
|
|
public:
|
|
void updateFocusFromImGui(bool windowFocused) {
|
|
if (!windowFocused && viewportFocused && !manualUnfocus) {
|
|
viewportFocused = false;
|
|
}
|
|
}
|
|
|
|
void setFocused(bool focused) {
|
|
viewportFocused = focused;
|
|
}
|
|
|
|
bool isViewportFocused() const {
|
|
return viewportFocused;
|
|
}
|
|
|
|
void clearManualUnfocus() {
|
|
manualUnfocus = false;
|
|
}
|
|
|
|
void update(GLFWwindow* window, bool& cursorLocked) {
|
|
if (viewportFocused && glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
|
|
viewportFocused = false;
|
|
manualUnfocus = true;
|
|
cursorLocked = false;
|
|
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
|
|
}
|
|
}
|
|
};
|
|
|
|
class Renderer {
|
|
private:
|
|
unsigned int framebuffer = 0, viewportTexture = 0, rbo = 0;
|
|
int currentWidth = 800, currentHeight = 600;
|
|
Shader* shader = nullptr;
|
|
Texture* texture1 = nullptr;
|
|
Texture* texture2 = nullptr;
|
|
Mesh* cubeMesh = nullptr;
|
|
Mesh* sphereMesh = nullptr;
|
|
Mesh* capsuleMesh = nullptr;
|
|
Skybox* skybox = nullptr;
|
|
|
|
public:
|
|
Renderer() = default;
|
|
~Renderer() {
|
|
delete shader;
|
|
delete texture1;
|
|
delete texture2;
|
|
delete cubeMesh;
|
|
delete sphereMesh;
|
|
delete capsuleMesh;
|
|
delete skybox;
|
|
if (framebuffer) glDeleteFramebuffers(1, &framebuffer);
|
|
if (viewportTexture) glDeleteTextures(1, &viewportTexture);
|
|
if (rbo) glDeleteRenderbuffers(1, &rbo);
|
|
}
|
|
|
|
void initialize() {
|
|
shader = new Shader("Resources/Shaders/vert.glsl", "Resources/Shaders/frag.glsl");
|
|
if (shader->ID == 0) {
|
|
std::cerr << "Shader compilation failed!\n";
|
|
delete shader;
|
|
shader = nullptr;
|
|
throw std::runtime_error("Shader error");
|
|
}
|
|
|
|
texture1 = new Texture("Resources/Textures/container.jpg");
|
|
texture2 = new Texture("Resources/Textures/awesomeface.png");
|
|
|
|
cubeMesh = new Mesh(vertices, sizeof(vertices));
|
|
|
|
auto sphereVerts = generateSphere();
|
|
sphereMesh = new Mesh(sphereVerts.data(), sphereVerts.size() * sizeof(float));
|
|
|
|
auto capsuleVerts = generateCapsule();
|
|
capsuleMesh = new Mesh(capsuleVerts.data(), capsuleVerts.size() * sizeof(float));
|
|
|
|
skybox = new Skybox();
|
|
|
|
setupFBO();
|
|
glEnable(GL_DEPTH_TEST);
|
|
}
|
|
|
|
void resize(int w, int h) {
|
|
if (w <= 0 || h <= 0 || (w == currentWidth && h == currentHeight)) return;
|
|
|
|
std::cout << "RESIZE TRIGGERED: " << w << "x" << h << std::endl;
|
|
|
|
GLint currentFB;
|
|
glGetIntegerv(GL_FRAMEBUFFER_BINDING, ¤tFB);
|
|
std::cout << "Framebuffer bound during resize: " << currentFB << " (should be " << framebuffer << ")" << std::endl;
|
|
|
|
currentWidth = w;
|
|
currentHeight = h;
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
|
|
|
glBindTexture(GL_TEXTURE_2D, viewportTexture);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, currentWidth, currentHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
|
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, currentWidth, currentHeight);
|
|
|
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
std::cerr << "Framebuffer incomplete after resize!\n";
|
|
}
|
|
|
|
std::cout << "Resize complete" << std::endl;
|
|
}
|
|
|
|
int getWidth() const { return currentWidth; }
|
|
int getHeight() const { return currentHeight; }
|
|
|
|
void beginRender(const glm::mat4& view, const glm::mat4& proj) {
|
|
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
|
glViewport(0, 0, currentWidth, currentHeight);
|
|
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
|
|
shader->use();
|
|
shader->setMat4("view", view);
|
|
shader->setMat4("projection", proj);
|
|
texture1->Bind(GL_TEXTURE0);
|
|
texture2->Bind(GL_TEXTURE1);
|
|
shader->setInt("texture1", 0);
|
|
shader->setInt("texture2", 1);
|
|
}
|
|
|
|
void renderSkybox(const glm::mat4& view, const glm::mat4& proj) {
|
|
if (skybox) {
|
|
GLint currentFB;
|
|
glGetIntegerv(GL_FRAMEBUFFER_BINDING, ¤tFB);
|
|
|
|
glDepthFunc(GL_LEQUAL);
|
|
skybox->draw(glm::value_ptr(view), glm::value_ptr(proj));
|
|
glDepthFunc(GL_LESS);
|
|
|
|
GLint afterFB;
|
|
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &afterFB);
|
|
|
|
if (currentFB != afterFB) {
|
|
std::cerr << "WARNING: Framebuffer changed during skybox render! "
|
|
<< currentFB << " -> " << afterFB << std::endl;
|
|
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
|
}
|
|
|
|
shader->use();
|
|
shader->setMat4("view", view);
|
|
shader->setMat4("projection", proj);
|
|
shader->use();
|
|
}
|
|
}
|
|
|
|
Skybox* getSkybox() { return skybox; }
|
|
|
|
void renderObject(const SceneObject& obj) {
|
|
glm::mat4 model = glm::mat4(1.0f);
|
|
model = glm::translate(model, obj.position);
|
|
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1, 0, 0));
|
|
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0, 1, 0));
|
|
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0, 0, 1));
|
|
model = glm::scale(model, obj.scale);
|
|
|
|
shader->setMat4("model", model);
|
|
|
|
switch (obj.type) {
|
|
case ObjectType::Cube:
|
|
cubeMesh->draw();
|
|
break;
|
|
case ObjectType::Sphere:
|
|
sphereMesh->draw();
|
|
break;
|
|
case ObjectType::Capsule:
|
|
capsuleMesh->draw();
|
|
break;
|
|
case ObjectType::OBJMesh:
|
|
// Draw loaded OBJ mesh
|
|
if (obj.meshId >= 0) {
|
|
Mesh* objMesh = g_objLoader.getMesh(obj.meshId);
|
|
if (objMesh) {
|
|
objMesh->draw();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void renderScene(const Camera& camera, const std::vector<SceneObject>& sceneObjects) {
|
|
shader->use();
|
|
shader->setMat4("view", camera.getViewMatrix());
|
|
shader->setMat4("projection", glm::perspective(glm::radians(FOV), (float)currentWidth / (float)currentHeight, NEAR_PLANE, FAR_PLANE));
|
|
shader->setVec3("lightPos", glm::vec3(4.0f, 6.0f, 4.0f)); // Slightly higher and farther
|
|
shader->setVec3("lightColor", glm::vec3(1.0f, 1.0f, 1.0f));
|
|
shader->setFloat("ambientStrength", 0.25f);
|
|
shader->setFloat("specularStrength", 0.8f);
|
|
shader->setFloat("shininess", 64.0f);
|
|
shader->setFloat("mixAmount", 0.3f);
|
|
|
|
texture1->Bind(0);
|
|
texture2->Bind(1);
|
|
|
|
for (const auto& obj : sceneObjects) {
|
|
glm::mat4 model = glm::mat4(1.0f);
|
|
model = glm::translate(model, obj.position);
|
|
model = glm::rotate(model, glm::radians(obj.rotation.x), glm::vec3(1.0f, 0.0f, 0.0f));
|
|
model = glm::rotate(model, glm::radians(obj.rotation.y), glm::vec3(0.0f, 1.0f, 0.0f));
|
|
model = glm::rotate(model, glm::radians(obj.rotation.z), glm::vec3(0.0f, 0.0f, 1.0f));
|
|
model = glm::scale(model, obj.scale);
|
|
|
|
shader->setMat4("model", model);
|
|
|
|
Mesh* meshToDraw = nullptr;
|
|
if (obj.type == ObjectType::Cube) meshToDraw = cubeMesh;
|
|
else if (obj.type == ObjectType::Sphere) meshToDraw = sphereMesh;
|
|
else if (obj.type == ObjectType::Capsule) meshToDraw = capsuleMesh;
|
|
else if (obj.type == ObjectType::OBJMesh && obj.meshId != -1) {
|
|
meshToDraw = g_objLoader.getMesh(obj.meshId);
|
|
}
|
|
|
|
if (meshToDraw) {
|
|
meshToDraw->draw();
|
|
}
|
|
}
|
|
|
|
// Skybox last
|
|
if (skybox) {
|
|
glm::mat4 view = camera.getViewMatrix();
|
|
glm::mat4 proj = glm::perspective(glm::radians(FOV),
|
|
(float)currentWidth / currentHeight,
|
|
NEAR_PLANE, FAR_PLANE);
|
|
|
|
skybox->draw(glm::value_ptr(view), glm::value_ptr(proj));
|
|
}
|
|
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
void endRender() {
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
|
|
unsigned int getViewportTexture() const { return viewportTexture; }
|
|
|
|
private:
|
|
void setupFBO() {
|
|
glGenFramebuffers(1, &framebuffer);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
|
|
|
glGenTextures(1, &viewportTexture);
|
|
glBindTexture(GL_TEXTURE_2D, viewportTexture);
|
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, currentWidth, currentHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, viewportTexture, 0);
|
|
|
|
glGenRenderbuffers(1, &rbo);
|
|
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
|
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, currentWidth, currentHeight);
|
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
|
|
|
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
|
std::cerr << "Framebuffer setup failed!\n";
|
|
}
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
}
|
|
};
|
|
|
|
struct RecentProject {
|
|
std::string name;
|
|
std::string path;
|
|
std::string lastOpened;
|
|
};
|
|
|
|
class Project {
|
|
public:
|
|
std::string name;
|
|
fs::path projectPath;
|
|
fs::path scenesPath;
|
|
fs::path assetsPath;
|
|
fs::path scriptsPath;
|
|
std::string currentSceneName;
|
|
bool isLoaded = false;
|
|
bool hasUnsavedChanges = false;
|
|
|
|
Project() = default;
|
|
|
|
Project(const std::string& projectName, const fs::path& basePath)
|
|
: name(projectName) {
|
|
projectPath = basePath / projectName;
|
|
scenesPath = projectPath / "Scenes";
|
|
assetsPath = projectPath / "Assets";
|
|
scriptsPath = projectPath / "Scripts";
|
|
}
|
|
|
|
bool create() {
|
|
try {
|
|
fs::create_directories(projectPath);
|
|
fs::create_directories(scenesPath);
|
|
fs::create_directories(assetsPath);
|
|
fs::create_directories(assetsPath / "Textures");
|
|
fs::create_directories(assetsPath / "Models");
|
|
fs::create_directories(assetsPath / "Shaders");
|
|
fs::create_directories(scriptsPath);
|
|
|
|
saveProjectFile();
|
|
|
|
currentSceneName = "Main";
|
|
isLoaded = true;
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Failed to create project: " << e.what() << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool load(const fs::path& projectFilePath) {
|
|
try {
|
|
projectPath = projectFilePath.parent_path();
|
|
scenesPath = projectPath / "Scenes";
|
|
assetsPath = projectPath / "Assets";
|
|
scriptsPath = projectPath / "Scripts";
|
|
|
|
std::ifstream file(projectFilePath);
|
|
if (!file.is_open()) return false;
|
|
|
|
std::string line;
|
|
while (std::getline(file, line)) {
|
|
if (line.find("name=") == 0) {
|
|
name = line.substr(5);
|
|
} else if (line.find("lastScene=") == 0) {
|
|
currentSceneName = line.substr(10);
|
|
}
|
|
}
|
|
file.close();
|
|
|
|
if (currentSceneName.empty()) {
|
|
currentSceneName = "Main";
|
|
}
|
|
|
|
isLoaded = true;
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Failed to load project: " << e.what() << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void saveProjectFile() const {
|
|
std::ofstream file(projectPath / "project.modu");
|
|
file << "name=" << name << "\n";
|
|
file << "lastScene=" << currentSceneName << "\n";
|
|
file.close();
|
|
}
|
|
|
|
std::vector<std::string> getSceneList() const {
|
|
std::vector<std::string> scenes;
|
|
try {
|
|
for (const auto& entry : fs::directory_iterator(scenesPath)) {
|
|
if (entry.path().extension() == ".scene") {
|
|
scenes.push_back(entry.path().stem().string());
|
|
}
|
|
}
|
|
} catch (...) {}
|
|
return scenes;
|
|
}
|
|
|
|
fs::path getSceneFilePath(const std::string& sceneName) const {
|
|
return scenesPath / (sceneName + ".scene");
|
|
}
|
|
};
|
|
|
|
class ProjectManager {
|
|
public:
|
|
std::vector<RecentProject> recentProjects;
|
|
fs::path appDataPath;
|
|
char newProjectName[128] = "";
|
|
char newProjectLocation[512] = "";
|
|
char openProjectPath[512] = "";
|
|
bool showNewProjectDialog = false;
|
|
bool showOpenProjectDialog = false;
|
|
std::string errorMessage;
|
|
Project currentProject;
|
|
|
|
ProjectManager() {
|
|
#ifdef _WIN32
|
|
const char* appdata = std::getenv("APPDATA");
|
|
if (appdata) {
|
|
appDataPath = fs::path(appdata) / ".Modularity";
|
|
} else {
|
|
appDataPath = fs::current_path() / "AppData";
|
|
}
|
|
#else
|
|
const char* home = std::getenv("HOME");
|
|
if (home) {
|
|
appDataPath = fs::path(home) / ".Modularity";
|
|
} else {
|
|
appDataPath = fs::current_path() / ".Modularity";
|
|
}
|
|
#endif
|
|
|
|
fs::create_directories(appDataPath);
|
|
loadRecentProjects();
|
|
|
|
std::string defaultPath = (fs::current_path() / "Projects").string();
|
|
strncpy(newProjectLocation, defaultPath.c_str(), sizeof(newProjectLocation) - 1);
|
|
}
|
|
|
|
void loadRecentProjects() {
|
|
recentProjects.clear();
|
|
fs::path recentFile = appDataPath / "recent_projects.txt";
|
|
|
|
if (!fs::exists(recentFile)) return;
|
|
|
|
std::ifstream file(recentFile);
|
|
std::string line;
|
|
while (std::getline(file, line)) {
|
|
if (line.empty()) continue;
|
|
|
|
RecentProject rp;
|
|
size_t pos1 = line.find('|');
|
|
size_t pos2 = line.find('|', pos1 + 1);
|
|
|
|
if (pos1 != std::string::npos && pos2 != std::string::npos) {
|
|
rp.name = line.substr(0, pos1);
|
|
rp.path = line.substr(pos1 + 1, pos2 - pos1 - 1);
|
|
rp.lastOpened = line.substr(pos2 + 1);
|
|
|
|
if (fs::exists(rp.path)) {
|
|
recentProjects.push_back(rp);
|
|
}
|
|
}
|
|
}
|
|
file.close();
|
|
}
|
|
|
|
void saveRecentProjects() {
|
|
fs::path recentFile = appDataPath / "recent_projects.txt";
|
|
std::ofstream file(recentFile);
|
|
|
|
for (const auto& rp : recentProjects) {
|
|
file << rp.name << "|" << rp.path << "|" << rp.lastOpened << "\n";
|
|
}
|
|
file.close();
|
|
}
|
|
|
|
void addToRecentProjects(const std::string& name, const std::string& path) {
|
|
recentProjects.erase(
|
|
std::remove_if(recentProjects.begin(), recentProjects.end(),
|
|
[&path](const RecentProject& rp) { return rp.path == path; }),
|
|
recentProjects.end()
|
|
);
|
|
|
|
std::time_t now = std::time(nullptr);
|
|
char timeStr[64];
|
|
std::strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M", std::localtime(&now));
|
|
|
|
RecentProject rp;
|
|
rp.name = name;
|
|
rp.path = path;
|
|
rp.lastOpened = timeStr;
|
|
recentProjects.insert(recentProjects.begin(), rp);
|
|
|
|
if (recentProjects.size() > 10) {
|
|
recentProjects.resize(10);
|
|
}
|
|
|
|
saveRecentProjects();
|
|
}
|
|
|
|
bool loadProject(const std::string& projectPath) {
|
|
fs::path path(projectPath);
|
|
if (fs::is_directory(path)) {
|
|
path = path / "project.modu";
|
|
}
|
|
|
|
if (!fs::exists(path)) {
|
|
errorMessage = "Project file not found.";
|
|
return false;
|
|
}
|
|
|
|
Project loadedProject;
|
|
if (loadedProject.load(path)) {
|
|
currentProject = loadedProject;
|
|
addToRecentProjects(currentProject.name, path.string());
|
|
return true;
|
|
} else {
|
|
errorMessage = "Failed to load project.";
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
class SceneSerializer {
|
|
public:
|
|
static bool saveScene(const fs::path& filePath,
|
|
const std::vector<SceneObject>& objects,
|
|
int nextId) {
|
|
try {
|
|
std::ofstream file(filePath);
|
|
if (!file.is_open()) return false;
|
|
|
|
file << "# Scene File\n";
|
|
file << "version=2\n"; // Bumped version for new format
|
|
file << "nextId=" << nextId << "\n";
|
|
file << "objectCount=" << objects.size() << "\n";
|
|
file << "\n";
|
|
|
|
for (const auto& obj : objects) {
|
|
file << "[Object]\n";
|
|
file << "id=" << obj.id << "\n";
|
|
file << "name=" << obj.name << "\n";
|
|
file << "type=" << static_cast<int>(obj.type) << "\n";
|
|
file << "parentId=" << obj.parentId << "\n";
|
|
file << "position=" << obj.position.x << "," << obj.position.y << "," << obj.position.z << "\n";
|
|
file << "rotation=" << obj.rotation.x << "," << obj.rotation.y << "," << obj.rotation.z << "\n";
|
|
file << "scale=" << obj.scale.x << "," << obj.scale.y << "," << obj.scale.z << "\n";
|
|
|
|
// Save mesh path for OBJ meshes
|
|
if (obj.type == ObjectType::OBJMesh && !obj.meshPath.empty()) {
|
|
file << "meshPath=" << obj.meshPath << "\n";
|
|
}
|
|
|
|
file << "children=";
|
|
for (size_t i = 0; i < obj.childIds.size(); i++) {
|
|
if (i > 0) file << ",";
|
|
file << obj.childIds[i];
|
|
}
|
|
file << "\n\n";
|
|
}
|
|
|
|
file.close();
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Failed to save scene: " << e.what() << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool loadScene(const fs::path& filePath,
|
|
std::vector<SceneObject>& objects,
|
|
int& nextId) {
|
|
try {
|
|
std::ifstream file(filePath);
|
|
if (!file.is_open()) return false;
|
|
|
|
objects.clear();
|
|
std::string line;
|
|
SceneObject* currentObj = nullptr;
|
|
|
|
while (std::getline(file, line)) {
|
|
line.erase(0, line.find_first_not_of(" \t\r\n"));
|
|
line.erase(line.find_last_not_of(" \t\r\n") + 1);
|
|
|
|
if (line.empty() || line[0] == '#') continue;
|
|
|
|
if (line == "[Object]") {
|
|
objects.push_back(SceneObject("", ObjectType::Cube, 0));
|
|
currentObj = &objects.back();
|
|
continue;
|
|
}
|
|
|
|
size_t eqPos = line.find('=');
|
|
if (eqPos == std::string::npos) continue;
|
|
|
|
std::string key = line.substr(0, eqPos);
|
|
std::string value = line.substr(eqPos + 1);
|
|
|
|
if (key == "nextId") {
|
|
nextId = std::stoi(value);
|
|
} else if (currentObj) {
|
|
if (key == "id") {
|
|
currentObj->id = std::stoi(value);
|
|
} else if (key == "name") {
|
|
currentObj->name = value;
|
|
} else if (key == "type") {
|
|
currentObj->type = static_cast<ObjectType>(std::stoi(value));
|
|
} else if (key == "parentId") {
|
|
currentObj->parentId = std::stoi(value);
|
|
} else if (key == "position") {
|
|
sscanf(value.c_str(), "%f,%f,%f",
|
|
¤tObj->position.x,
|
|
¤tObj->position.y,
|
|
¤tObj->position.z);
|
|
} else if (key == "rotation") {
|
|
sscanf(value.c_str(), "%f,%f,%f",
|
|
¤tObj->rotation.x,
|
|
¤tObj->rotation.y,
|
|
¤tObj->rotation.z);
|
|
} else if (key == "scale") {
|
|
sscanf(value.c_str(), "%f,%f,%f",
|
|
¤tObj->scale.x,
|
|
¤tObj->scale.y,
|
|
¤tObj->scale.z);
|
|
} else if (key == "meshPath") {
|
|
currentObj->meshPath = value;
|
|
// Reload the mesh
|
|
if (!value.empty() && currentObj->type == ObjectType::OBJMesh) {
|
|
std::string err;
|
|
currentObj->meshId = g_objLoader.loadOBJ(value, err);
|
|
}
|
|
} else if (key == "children" && !value.empty()) {
|
|
std::stringstream ss(value);
|
|
std::string item;
|
|
while (std::getline(ss, item, ',')) {
|
|
if (!item.empty()) {
|
|
currentObj->childIds.push_back(std::stoi(item));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
file.close();
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "Failed to load scene: " << e.what() << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
void window_size_callback(GLFWwindow* window, int width, int height) {
|
|
glViewport(0, 0, width, height);
|
|
}
|
|
|
|
class Engine;
|
|
|
|
class Engine {
|
|
private:
|
|
Window window;
|
|
GLFWwindow* editorWindow = nullptr;
|
|
Renderer renderer;
|
|
Camera camera;
|
|
ViewportController viewportController;
|
|
float deltaTime = 0.0f;
|
|
float lastFrame = 0.0f;
|
|
bool cursorLocked = false;
|
|
int viewportWidth = 800;
|
|
int viewportHeight = 600;
|
|
|
|
std::vector<SceneObject> sceneObjects;
|
|
int selectedObjectId = -1;
|
|
int nextObjectId = 0;
|
|
|
|
SceneObject* getSelectedObject() {
|
|
if (selectedObjectId == -1) return nullptr;
|
|
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
|
|
[this](const SceneObject& obj) { return obj.id == selectedObjectId; });
|
|
return (it != sceneObjects.end()) ? &(*it) : nullptr;
|
|
}
|
|
|
|
static void DecomposeMatrix(const glm::mat4& matrix, glm::vec3& pos, glm::vec3& rot, glm::vec3& scale) {
|
|
// Extract translation
|
|
pos = glm::vec3(matrix[3]);
|
|
|
|
// Extract scale
|
|
scale.x = glm::length(glm::vec3(matrix[0]));
|
|
scale.y = glm::length(glm::vec3(matrix[1]));
|
|
scale.z = glm::length(glm::vec3(matrix[2]));
|
|
|
|
// Extract rotation (convert to Euler angles)
|
|
glm::mat3 rotMat(matrix);
|
|
if (scale.x != 0.0f) rotMat[0] /= scale.x;
|
|
if (scale.y != 0.0f) rotMat[1] /= scale.y;
|
|
if (scale.z != 0.0f) rotMat[2] /= scale.z;
|
|
|
|
rot = glm::eulerAngles(glm::quat_cast(rotMat));
|
|
}
|
|
|
|
// Gizmo state
|
|
ImGuizmo::OPERATION mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
|
|
ImGuizmo::MODE mCurrentGizmoMode = ImGuizmo::LOCAL;
|
|
bool useSnap = false;
|
|
float snapValue[3] = { 0.5f, 0.5f, 0.5f };
|
|
float rotationSnapValue = 15.0f;
|
|
|
|
FileBrowser fileBrowser;
|
|
bool viewportFullscreen = false;
|
|
bool showHierarchy = true;
|
|
bool showInspector = true;
|
|
bool showFileBrowser = true;
|
|
bool showConsole = true;
|
|
bool showProjectBrowser = true;
|
|
bool firstFrame = true;
|
|
std::vector<std::string> consoleLog;
|
|
int draggedObjectId = -1;
|
|
|
|
ProjectManager projectManager;
|
|
bool showLauncher = true;
|
|
bool showNewSceneDialog = false;
|
|
bool showSaveSceneAsDialog = false;
|
|
char newSceneName[128] = "";
|
|
char saveSceneAsName[128] = "";
|
|
bool rendererInitialized = false;
|
|
|
|
bool showImportOBJDialog = false;
|
|
std::string pendingOBJPath;
|
|
char importOBJName[128] = "";
|
|
|
|
public:
|
|
Engine() = default;
|
|
|
|
bool init() {
|
|
editorWindow = window.makeWindow();
|
|
if (!editorWindow) return false;
|
|
|
|
glfwSetWindowUserPointer(editorWindow, this);
|
|
glfwSetWindowSizeCallback(editorWindow, window_size_callback);
|
|
|
|
auto mouse_cb = [](GLFWwindow* window, double xpos, double ypos) {
|
|
auto* engine = static_cast<Engine*>(glfwGetWindowUserPointer(window));
|
|
if (!engine) return;
|
|
|
|
int cursorMode = glfwGetInputMode(window, GLFW_CURSOR);
|
|
if (!engine->viewportController.isViewportFocused() || cursorMode != GLFW_CURSOR_DISABLED) {
|
|
return;
|
|
}
|
|
|
|
engine->camera.processMouse(xpos, ypos);
|
|
};
|
|
glfwSetCursorPosCallback(editorWindow, mouse_cb);
|
|
|
|
setupImGui();
|
|
logToConsole("Engine initialized - Waiting for project selection");
|
|
return true;
|
|
}
|
|
|
|
|
|
bool initRenderer() {
|
|
if (rendererInitialized) return true;
|
|
|
|
try {
|
|
renderer.initialize();
|
|
rendererInitialized = true;
|
|
return true;
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void run() {
|
|
while (!glfwWindowShouldClose(editorWindow)) {
|
|
if (glfwGetWindowAttrib(editorWindow, GLFW_ICONIFIED)) {
|
|
ImGui_ImplGlfw_Sleep(10);
|
|
continue;
|
|
}
|
|
|
|
float currentFrame = glfwGetTime();
|
|
deltaTime = currentFrame - lastFrame;
|
|
lastFrame = currentFrame;
|
|
|
|
deltaTime = std::min(deltaTime, 1.0f / 30.0f);
|
|
|
|
glfwPollEvents();
|
|
|
|
if (!showLauncher) {
|
|
handleKeyboardShortcuts();
|
|
}
|
|
|
|
viewportController.update(editorWindow, cursorLocked);
|
|
|
|
if (!viewportController.isViewportFocused() && cursorLocked) {
|
|
cursorLocked = false;
|
|
glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
|
|
camera.firstMouse = true;
|
|
}
|
|
|
|
if (viewportController.isViewportFocused() && cursorLocked) {
|
|
camera.processKeyboard(deltaTime, editorWindow);
|
|
}
|
|
|
|
|
|
if (!showLauncher && projectManager.currentProject.isLoaded && rendererInitialized) {
|
|
glm::mat4 view = camera.getViewMatrix();
|
|
float aspect = static_cast<float>(viewportWidth) / static_cast<float>(viewportHeight);
|
|
if (aspect <= 0.0f) aspect = 1.0f;
|
|
glm::mat4 proj = glm::perspective(glm::radians(FOV), aspect, NEAR_PLANE, FAR_PLANE);
|
|
|
|
renderer.beginRender(view, proj);
|
|
|
|
for (const auto& obj : sceneObjects) {
|
|
renderer.renderObject(obj);
|
|
}
|
|
|
|
renderer.renderSkybox(view, proj);
|
|
|
|
renderer.endRender();
|
|
}
|
|
|
|
ImGui_ImplOpenGL3_NewFrame();
|
|
ImGui_ImplGlfw_NewFrame();
|
|
ImGui::NewFrame();
|
|
|
|
if (showLauncher) {
|
|
renderLauncher();
|
|
} else {
|
|
setupDockspace();
|
|
renderMainMenuBar();
|
|
|
|
if (!viewportFullscreen) {
|
|
if (showHierarchy) renderHierarchyPanel();
|
|
if (showInspector) renderInspectorPanel();
|
|
if (showFileBrowser) renderFileBrowserPanel();
|
|
if (showConsole) renderConsolePanel();
|
|
if (showProjectBrowser) renderProjectBrowserPanel();
|
|
}
|
|
|
|
renderViewport();
|
|
renderDialogs();
|
|
}
|
|
|
|
int displayW, displayH;
|
|
glfwGetFramebufferSize(editorWindow, &displayW, &displayH);
|
|
glViewport(0, 0, displayW, displayH);
|
|
glClearColor(0.1f, 0.1f, 0.12f, 1.00f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
ImGui::Render();
|
|
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
|
GLFWwindow* backup_current_context = glfwGetCurrentContext();
|
|
ImGui::UpdatePlatformWindows();
|
|
ImGui::RenderPlatformWindowsDefault();
|
|
glfwMakeContextCurrent(backup_current_context);
|
|
}
|
|
|
|
glfwSwapBuffers(editorWindow);
|
|
firstFrame = false;
|
|
}
|
|
}
|
|
|
|
void shutdown() {
|
|
if (projectManager.currentProject.isLoaded && projectManager.currentProject.hasUnsavedChanges) {
|
|
saveCurrentScene();
|
|
}
|
|
|
|
ImGui_ImplOpenGL3_Shutdown();
|
|
ImGui_ImplGlfw_Shutdown();
|
|
ImGui::DestroyContext();
|
|
glfwTerminate();
|
|
}
|
|
|
|
private:
|
|
void importOBJToScene(const std::string& filepath, const std::string& objectName) {
|
|
std::string errorMsg;
|
|
int meshId = g_objLoader.loadOBJ(filepath, errorMsg);
|
|
|
|
if (meshId < 0) {
|
|
addConsoleMessage("Failed to load OBJ: " + errorMsg, ConsoleMessageType::Error);
|
|
return;
|
|
}
|
|
|
|
// Create scene object
|
|
int id = nextObjectId++;
|
|
std::string name = objectName.empty() ? fs::path(filepath).stem().string() : objectName;
|
|
|
|
SceneObject obj(name, ObjectType::OBJMesh, id);
|
|
obj.meshPath = filepath;
|
|
obj.meshId = meshId;
|
|
|
|
sceneObjects.push_back(obj);
|
|
selectedObjectId = id;
|
|
|
|
if (projectManager.currentProject.isLoaded) {
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
}
|
|
|
|
const auto* meshInfo = g_objLoader.getMeshInfo(meshId);
|
|
if (meshInfo) {
|
|
addConsoleMessage("Imported OBJ: " + name + " (" +
|
|
std::to_string(meshInfo->vertexCount) + " vertices, " +
|
|
std::to_string(meshInfo->faceCount) + " faces)",
|
|
ConsoleMessageType::Success);
|
|
} else {
|
|
addConsoleMessage("Imported OBJ: " + name, ConsoleMessageType::Success);
|
|
}
|
|
}
|
|
|
|
void handleKeyboardShortcuts() {
|
|
static bool f11Pressed = false;
|
|
if (glfwGetKey(editorWindow, GLFW_KEY_F11) == GLFW_PRESS && !f11Pressed) {
|
|
viewportFullscreen = !viewportFullscreen;
|
|
f11Pressed = true;
|
|
}
|
|
if (glfwGetKey(editorWindow, GLFW_KEY_F11) == GLFW_RELEASE) {
|
|
f11Pressed = false;
|
|
}
|
|
|
|
static bool ctrlSPressed = false;
|
|
bool ctrlDown = glfwGetKey(editorWindow, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS ||
|
|
glfwGetKey(editorWindow, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS;
|
|
|
|
if (ctrlDown && glfwGetKey(editorWindow, GLFW_KEY_S) == GLFW_PRESS && !ctrlSPressed) {
|
|
if (projectManager.currentProject.isLoaded) {
|
|
saveCurrentScene();
|
|
}
|
|
ctrlSPressed = true;
|
|
}
|
|
if (glfwGetKey(editorWindow, GLFW_KEY_S) == GLFW_RELEASE) {
|
|
ctrlSPressed = false;
|
|
}
|
|
|
|
static bool ctrlNPressed = false;
|
|
if (ctrlDown && glfwGetKey(editorWindow, GLFW_KEY_N) == GLFW_PRESS && !ctrlNPressed) {
|
|
if (projectManager.currentProject.isLoaded) {
|
|
showNewSceneDialog = true;
|
|
memset(newSceneName, 0, sizeof(newSceneName));
|
|
}
|
|
ctrlNPressed = true;
|
|
}
|
|
if (glfwGetKey(editorWindow, GLFW_KEY_N) == GLFW_RELEASE) {
|
|
ctrlNPressed = false;
|
|
}
|
|
|
|
// Gizmo operation hotkeys
|
|
if (ImGui::IsKeyPressed(ImGuiKey_Q)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
|
|
if (ImGui::IsKeyPressed(ImGuiKey_W)) mCurrentGizmoOperation = ImGuizmo::ROTATE;
|
|
if (ImGui::IsKeyPressed(ImGuiKey_E)) mCurrentGizmoOperation = ImGuizmo::SCALE;
|
|
if (ImGui::IsKeyPressed(ImGuiKey_R)) mCurrentGizmoOperation = ImGuizmo::UNIVERSAL;
|
|
|
|
// Local / World toggle
|
|
if (ImGui::IsKeyPressed(ImGuiKey_Z)) {
|
|
mCurrentGizmoMode = (mCurrentGizmoMode == ImGuizmo::LOCAL) ? ImGuizmo::WORLD : ImGuizmo::LOCAL;
|
|
}
|
|
|
|
// Snap toggle
|
|
if (ImGui::IsKeyPressed(ImGuiKey_LeftCtrl)) useSnap = !useSnap;
|
|
}
|
|
|
|
void OpenProjectPath(const std::string& path) {
|
|
if (projectManager.loadProject(path)) {
|
|
if (!initRenderer()) {
|
|
addConsoleMessage("Error: Failed to initialize renderer!", ConsoleMessageType::Error);
|
|
} else {
|
|
showLauncher = false;
|
|
loadRecentScenes();
|
|
addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info);
|
|
}
|
|
} else {
|
|
addConsoleMessage("Error opening project: " + projectManager.errorMessage, ConsoleMessageType::Error);
|
|
}
|
|
}
|
|
|
|
void renderLauncher() {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
ImVec2 displaySize = io.DisplaySize;
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.09f, 1.0f));
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
|
ImGui::SetNextWindowSize(displaySize);
|
|
|
|
ImGuiWindowFlags flags =
|
|
ImGuiWindowFlags_NoTitleBar |
|
|
ImGuiWindowFlags_NoResize |
|
|
ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoCollapse |
|
|
ImGuiWindowFlags_NoDocking |
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
|
|
if (ImGui::Begin("Launcher", nullptr, flags))
|
|
{
|
|
float leftPanelWidth = 280.0f;
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.06f, 0.06f, 0.07f, 1.0f));
|
|
ImGui::BeginChild("LauncherLeft", ImVec2(leftPanelWidth, 0), true);
|
|
ImGui::PopStyleColor();
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImVec4(0.45f, 0.72f, 0.95f, 1.0f), "MODULARITY");
|
|
ImGui::TextDisabled("Game Engine");
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "GET STARTED");
|
|
ImGui::Spacing();
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.18f, 0.38f, 0.55f, 1.0f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.24f, 0.48f, 0.68f, 1.0f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.20f, 0.42f, 0.60f, 1.0f));
|
|
|
|
if (ImGui::Button("New Project", ImVec2(-1, 36.0f)))
|
|
{
|
|
projectManager.showNewProjectDialog = true;
|
|
projectManager.errorMessage.clear();
|
|
std::memset(projectManager.newProjectName, 0, sizeof(projectManager.newProjectName));
|
|
|
|
#ifdef _WIN32
|
|
char documentsPath[MAX_PATH];
|
|
SHGetFolderPathA(NULL, CSIDL_MYDOCUMENTS, NULL, 0, documentsPath);
|
|
std::strcpy(projectManager.newProjectLocation, documentsPath);
|
|
std::strcat(projectManager.newProjectLocation, "\\ModularityProjects");
|
|
#else
|
|
const char* home = std::getenv("HOME");
|
|
if (home)
|
|
{
|
|
std::strcpy(projectManager.newProjectLocation, home);
|
|
std::strcat(projectManager.newProjectLocation, "/ModularityProjects");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
if (ImGui::Button("Open Project", ImVec2(-1, 36.0f)))
|
|
{
|
|
projectManager.showOpenProjectDialog = true;
|
|
projectManager.errorMessage.clear();
|
|
}
|
|
|
|
ImGui::PopStyleColor(3);
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "QUICK ACTIONS");
|
|
ImGui::Spacing();
|
|
|
|
if (ImGui::Button("Documentation", ImVec2(-1, 30.0f)))
|
|
{
|
|
#ifdef _WIN32
|
|
system("start https://github.com");
|
|
#endif
|
|
}
|
|
|
|
if (ImGui::Button("Exit", ImVec2(-1, 30.0f)))
|
|
{
|
|
glfwSetWindowShouldClose(editorWindow, GLFW_TRUE);
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
|
|
ImGui::SameLine();
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.10f, 0.10f, 0.11f, 1.0f));
|
|
ImGui::BeginChild("LauncherRight", ImVec2(0, 0), true);
|
|
ImGui::PopStyleColor();
|
|
|
|
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.78f, 1.0f), "RECENT PROJECTS");
|
|
ImGui::Spacing();
|
|
|
|
if (projectManager.recentProjects.empty())
|
|
{
|
|
ImGui::Spacing();
|
|
ImGui::TextDisabled("No recent projects");
|
|
ImGui::TextDisabled("Create a new project to get started!");
|
|
}
|
|
else
|
|
{
|
|
for (size_t i = 0; i < projectManager.recentProjects.size(); ++i)
|
|
{
|
|
const auto& rp = projectManager.recentProjects[i];
|
|
ImGui::PushID(static_cast<int>(i));
|
|
|
|
char label[512];
|
|
std::snprintf(label, sizeof(label), "%s\n%s",
|
|
rp.name.c_str(), rp.path.c_str());
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.20f, 0.30f, 0.45f, 0.40f));
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.25f, 0.38f, 0.55f, 0.70f));
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.20f, 0.35f, 0.60f, 0.90f));
|
|
|
|
bool selected = ImGui::Selectable(
|
|
label,
|
|
false,
|
|
ImGuiSelectableFlags_AllowDoubleClick,
|
|
ImVec2(0.0f, 48.0f)
|
|
);
|
|
|
|
ImGui::PopStyleColor(3);
|
|
|
|
if (selected || ImGui::IsItemClicked(ImGuiMouseButton_Left))
|
|
{
|
|
OpenProjectPath(rp.path);
|
|
}
|
|
|
|
if (ImGui::BeginPopupContextItem("RecentProjectContext"))
|
|
{
|
|
if (ImGui::MenuItem("Open"))
|
|
{
|
|
OpenProjectPath(rp.path);
|
|
}
|
|
|
|
if (ImGui::MenuItem("Remove from Recent"))
|
|
{
|
|
projectManager.recentProjects.erase(
|
|
projectManager.recentProjects.begin() + i
|
|
);
|
|
projectManager.saveRecentProjects();
|
|
ImGui::EndPopup();
|
|
ImGui::PopID();
|
|
break;
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::PopID();
|
|
ImGui::Spacing();
|
|
}
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::TextDisabled("Modularity Engine - Version 1.0.1");
|
|
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
ImGui::End();
|
|
ImGui::PopStyleColor(2);
|
|
ImGui::PopStyleVar(3);
|
|
|
|
if (projectManager.showNewProjectDialog)
|
|
renderNewProjectDialog();
|
|
if (projectManager.showOpenProjectDialog)
|
|
renderOpenProjectDialog();
|
|
}
|
|
|
|
void renderNewProjectDialog() {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
|
|
|
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
|
ImGui::SetNextWindowSize(ImVec2(500, 250), ImGuiCond_Appearing);
|
|
|
|
if (ImGui::Begin("New Project", &projectManager.showNewProjectDialog,
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
|
|
|
|
ImGui::Text("Project Name:");
|
|
ImGui::SetNextItemWidth(-1);
|
|
ImGui::InputText("##ProjectName", projectManager.newProjectName,
|
|
sizeof(projectManager.newProjectName));
|
|
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Location:");
|
|
ImGui::SetNextItemWidth(-70);
|
|
ImGui::InputText("##Location", projectManager.newProjectLocation,
|
|
sizeof(projectManager.newProjectLocation));
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Browse")) {
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
if (strlen(projectManager.newProjectName) > 0) {
|
|
fs::path previewPath = fs::path(projectManager.newProjectLocation) /
|
|
projectManager.newProjectName;
|
|
ImGui::TextDisabled("Project will be created at:");
|
|
ImGui::TextWrapped("%s", previewPath.string().c_str());
|
|
}
|
|
|
|
if (!projectManager.errorMessage.empty()) {
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s",
|
|
projectManager.errorMessage.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
float buttonWidth = 100;
|
|
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
|
|
projectManager.showNewProjectDialog = false;
|
|
memset(projectManager.newProjectName, 0, sizeof(projectManager.newProjectName));
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.3f, 1.0f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.6f, 0.4f, 1.0f));
|
|
if (ImGui::Button("Create", ImVec2(buttonWidth, 0))) {
|
|
if (strlen(projectManager.newProjectName) == 0) {
|
|
projectManager.errorMessage = "Please enter a project name";
|
|
} else if (strlen(projectManager.newProjectLocation) == 0) {
|
|
projectManager.errorMessage = "Please specify a location";
|
|
} else {
|
|
createNewProject(projectManager.newProjectName,
|
|
projectManager.newProjectLocation);
|
|
projectManager.showNewProjectDialog = false;
|
|
}
|
|
}
|
|
ImGui::PopStyleColor(2);
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void renderOpenProjectDialog() {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
|
|
|
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
|
ImGui::SetNextWindowSize(ImVec2(500, 180), ImGuiCond_Appearing);
|
|
|
|
if (ImGui::Begin("Open Project", &projectManager.showOpenProjectDialog,
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
|
|
|
|
ImGui::Text("Project File Path (.modu):");
|
|
ImGui::SetNextItemWidth(-70);
|
|
ImGui::InputText("##OpenPath", projectManager.openProjectPath,
|
|
sizeof(projectManager.openProjectPath));
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Browse")) {
|
|
}
|
|
|
|
ImGui::TextDisabled("Select a project.modu file");
|
|
|
|
if (!projectManager.errorMessage.empty()) {
|
|
ImGui::Spacing();
|
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s",
|
|
projectManager.errorMessage.c_str());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
float buttonWidth = 100;
|
|
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
|
|
projectManager.showOpenProjectDialog = false;
|
|
memset(projectManager.openProjectPath, 0, sizeof(projectManager.openProjectPath));
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
|
|
if (ImGui::Button("Open", ImVec2(buttonWidth, 0))) {
|
|
if (strlen(projectManager.openProjectPath) == 0) {
|
|
projectManager.errorMessage = "Please enter a project path";
|
|
} else {
|
|
if (projectManager.loadProject(projectManager.openProjectPath)) {
|
|
if (!initRenderer()) {
|
|
addConsoleMessage("Error: Failed to initialize renderer!", ConsoleMessageType::Error);
|
|
} else {
|
|
showLauncher = false;
|
|
loadRecentScenes();
|
|
addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info);
|
|
}
|
|
} else {
|
|
addConsoleMessage("Error opening project: " + projectManager.errorMessage, ConsoleMessageType::Error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
void createNewProject(const char* name, const char* location) {
|
|
fs::path basePath(location);
|
|
fs::create_directories(basePath);
|
|
|
|
Project newProject(name, basePath);
|
|
if (newProject.create()) {
|
|
projectManager.currentProject = newProject;
|
|
projectManager.addToRecentProjects(name,
|
|
(newProject.projectPath / "project.modu").string());
|
|
|
|
if (!initRenderer()) {
|
|
logToConsole("Error: Failed to initialize renderer!");
|
|
return;
|
|
}
|
|
|
|
sceneObjects.clear();
|
|
selectedObjectId = -1;
|
|
nextObjectId = 0;
|
|
|
|
addObject(ObjectType::Cube, "Cube");
|
|
|
|
fileBrowser.currentPath = projectManager.currentProject.assetsPath;
|
|
fileBrowser.needsRefresh = true;
|
|
|
|
showLauncher = false;
|
|
firstFrame = true;
|
|
|
|
addConsoleMessage("Created new project: " + std::string(name), ConsoleMessageType::Success);
|
|
addConsoleMessage("Project location: " + newProject.projectPath.string(), ConsoleMessageType::Info);
|
|
|
|
saveCurrentScene();
|
|
} else {
|
|
projectManager.errorMessage = "Failed to create project directory";
|
|
}
|
|
}
|
|
|
|
void loadRecentScenes() {
|
|
sceneObjects.clear();
|
|
selectedObjectId = -1;
|
|
nextObjectId = 0;
|
|
|
|
fs::path scenePath = projectManager.currentProject.getSceneFilePath(projectManager.currentProject.currentSceneName);
|
|
if (fs::exists(scenePath)) {
|
|
if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId)) {
|
|
addConsoleMessage("Loaded scene: " + projectManager.currentProject.currentSceneName, ConsoleMessageType::Success);
|
|
} else {
|
|
addConsoleMessage("Warning: Failed to load scene, starting fresh", ConsoleMessageType::Warning);
|
|
addObject(ObjectType::Cube, "Cube");
|
|
}
|
|
} else {
|
|
addConsoleMessage("Default scene not found, starting with a new scene.", ConsoleMessageType::Info);
|
|
addObject(ObjectType::Cube, "Cube");
|
|
}
|
|
|
|
fileBrowser.currentPath = projectManager.currentProject.assetsPath;
|
|
fileBrowser.needsRefresh = true;
|
|
}
|
|
|
|
void saveCurrentScene() {
|
|
if (!projectManager.currentProject.isLoaded) return;
|
|
|
|
fs::path scenePath = projectManager.currentProject.getSceneFilePath(projectManager.currentProject.currentSceneName);
|
|
if (SceneSerializer::saveScene(scenePath, sceneObjects, nextObjectId)) {
|
|
projectManager.currentProject.hasUnsavedChanges = false;
|
|
projectManager.currentProject.saveProjectFile();
|
|
addConsoleMessage("Saved scene: " + projectManager.currentProject.currentSceneName, ConsoleMessageType::Success);
|
|
} else {
|
|
addConsoleMessage("Error: Failed to save scene!", ConsoleMessageType::Error);
|
|
}
|
|
}
|
|
|
|
void loadScene(const std::string& sceneName) {
|
|
if (!projectManager.currentProject.isLoaded) return;
|
|
|
|
if (projectManager.currentProject.hasUnsavedChanges) {
|
|
saveCurrentScene();
|
|
}
|
|
|
|
fs::path scenePath = projectManager.currentProject.getSceneFilePath(sceneName);
|
|
if (SceneSerializer::loadScene(scenePath, sceneObjects, nextObjectId)) {
|
|
projectManager.currentProject.currentSceneName = sceneName;
|
|
projectManager.currentProject.hasUnsavedChanges = false;
|
|
projectManager.currentProject.saveProjectFile();
|
|
selectedObjectId = -1;
|
|
addConsoleMessage("Loaded scene: " + sceneName, ConsoleMessageType::Success);
|
|
} else {
|
|
addConsoleMessage("Error: Failed to load scene: " + sceneName, ConsoleMessageType::Error);
|
|
}
|
|
}
|
|
|
|
void createNewScene(const std::string& sceneName) {
|
|
if (!projectManager.currentProject.isLoaded || sceneName.empty()) return;
|
|
|
|
if (projectManager.currentProject.hasUnsavedChanges) {
|
|
saveCurrentScene();
|
|
}
|
|
|
|
sceneObjects.clear();
|
|
selectedObjectId = -1;
|
|
nextObjectId = 0;
|
|
|
|
projectManager.currentProject.currentSceneName = sceneName;
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
|
|
addObject(ObjectType::Cube, "Cube");
|
|
|
|
saveCurrentScene();
|
|
|
|
addConsoleMessage("Created new scene: " + sceneName, ConsoleMessageType::Success);
|
|
}
|
|
|
|
void renderDialogs() {
|
|
if (showNewSceneDialog) {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
|
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
|
ImGui::SetNextWindowSize(ImVec2(350, 130), ImGuiCond_Appearing);
|
|
|
|
if (ImGui::Begin("New Scene", &showNewSceneDialog,
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
|
|
ImGui::Text("Scene Name:");
|
|
ImGui::SetNextItemWidth(-1);
|
|
ImGui::InputText("##NewSceneName", newSceneName, sizeof(newSceneName));
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
float buttonWidth = 80;
|
|
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
|
|
showNewSceneDialog = false;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Create", ImVec2(buttonWidth, 0))) {
|
|
if (strlen(newSceneName) > 0) {
|
|
createNewScene(newSceneName);
|
|
showNewSceneDialog = false;
|
|
memset(newSceneName, 0, sizeof(newSceneName));
|
|
}
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
if (showSaveSceneAsDialog) {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
|
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
|
ImGui::SetNextWindowSize(ImVec2(350, 130), ImGuiCond_Appearing);
|
|
|
|
if (ImGui::Begin("Save Scene As", &showSaveSceneAsDialog,
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
|
|
ImGui::Text("Scene Name:");
|
|
ImGui::SetNextItemWidth(-1);
|
|
ImGui::InputText("##SaveSceneAsName", saveSceneAsName, sizeof(saveSceneAsName));
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
float buttonWidth = 80;
|
|
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
|
|
showSaveSceneAsDialog = false;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Save", ImVec2(buttonWidth, 0))) {
|
|
if (strlen(saveSceneAsName) > 0) {
|
|
projectManager.currentProject.currentSceneName = saveSceneAsName;
|
|
saveCurrentScene();
|
|
showSaveSceneAsDialog = false;
|
|
memset(saveSceneAsName, 0, sizeof(saveSceneAsName));
|
|
}
|
|
}
|
|
}
|
|
ImGui::End();
|
|
}
|
|
|
|
// OBJ Import dialog
|
|
if (showImportOBJDialog) {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
ImVec2 center = ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f);
|
|
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
|
ImGui::SetNextWindowSize(ImVec2(400, 160), ImGuiCond_Appearing);
|
|
|
|
if (ImGui::Begin("Import OBJ Model", &showImportOBJDialog,
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoDocking)) {
|
|
ImGui::Text("File: %s", fs::path(pendingOBJPath).filename().string().c_str());
|
|
ImGui::TextDisabled("%s", pendingOBJPath.c_str());
|
|
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Object Name:");
|
|
ImGui::SetNextItemWidth(-1);
|
|
ImGui::InputText("##ImportOBJName", importOBJName, sizeof(importOBJName));
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
float buttonWidth = 80;
|
|
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - buttonWidth * 2 - 20);
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(buttonWidth, 0))) {
|
|
showImportOBJDialog = false;
|
|
pendingOBJPath.clear();
|
|
}
|
|
ImGui::SameLine();
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.3f, 1.0f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.6f, 0.4f, 1.0f));
|
|
if (ImGui::Button("Import", ImVec2(buttonWidth, 0))) {
|
|
importOBJToScene(pendingOBJPath, importOBJName);
|
|
showImportOBJDialog = false;
|
|
pendingOBJPath.clear();
|
|
memset(importOBJName, 0, sizeof(importOBJName));
|
|
}
|
|
ImGui::PopStyleColor(2);
|
|
}
|
|
ImGui::End();
|
|
}
|
|
}
|
|
|
|
void renderProjectBrowserPanel() {
|
|
ImGui::Begin("Project", &showProjectBrowser);
|
|
|
|
if (!projectManager.currentProject.isLoaded) {
|
|
ImGui::TextDisabled("No project loaded");
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.7f, 0.95f, 1.0f), "[P] %s", projectManager.currentProject.name.c_str());
|
|
if (projectManager.currentProject.hasUnsavedChanges) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "*");
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::CollapsingHeader("Scenes", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
if (ImGui::Button("+ New Scene")) {
|
|
showNewSceneDialog = true;
|
|
memset(newSceneName, 0, sizeof(newSceneName));
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
auto scenes = projectManager.currentProject.getSceneList();
|
|
for (const auto& scene : scenes) {
|
|
bool isCurrentScene = (scene == projectManager.currentProject.currentSceneName);
|
|
|
|
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
|
|
ImGuiTreeNodeFlags_SpanAvailWidth |
|
|
ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
|
if (isCurrentScene) flags |= ImGuiTreeNodeFlags_Selected;
|
|
|
|
ImGui::TreeNodeEx(scene.c_str(), flags, "[S] %s", scene.c_str());
|
|
|
|
if (ImGui::IsItemClicked() && !isCurrentScene) {
|
|
loadScene(scene);
|
|
}
|
|
|
|
if (ImGui::BeginPopupContextItem()) {
|
|
if (ImGui::MenuItem("Load") && !isCurrentScene) {
|
|
loadScene(scene);
|
|
}
|
|
if (ImGui::MenuItem("Duplicate")) {
|
|
addConsoleMessage("Scene duplication not yet implemented.", ConsoleMessageType::Info);
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Delete") && !isCurrentScene) {
|
|
fs::remove(projectManager.currentProject.getSceneFilePath(scene));
|
|
addConsoleMessage("Deleted scene: " + scene, ConsoleMessageType::Info);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
if (scenes.empty()) {
|
|
ImGui::TextDisabled("No scenes yet");
|
|
}
|
|
}
|
|
|
|
if (ImGui::CollapsingHeader("Loaded Meshes")) {
|
|
const auto& meshes = g_objLoader.getAllMeshes();
|
|
if (meshes.empty()) {
|
|
ImGui::TextDisabled("No meshes loaded");
|
|
ImGui::TextDisabled("Import .obj files from File Browser");
|
|
} else {
|
|
for (size_t i = 0; i < meshes.size(); i++) {
|
|
const auto& mesh = meshes[i];
|
|
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
|
|
ImGuiTreeNodeFlags_SpanAvailWidth |
|
|
ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
|
|
|
ImGui::TreeNodeEx((void*)(intptr_t)i, flags, "[M] %s", mesh.name.c_str());
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::BeginTooltip();
|
|
ImGui::Text("Vertices: %d", mesh.vertexCount);
|
|
ImGui::Text("Faces: %d", mesh.faceCount);
|
|
ImGui::Text("Has Normals: %s", mesh.hasNormals ? "Yes" : "No");
|
|
ImGui::Text("Has UVs: %s", mesh.hasTexCoords ? "Yes" : "No");
|
|
ImGui::TextDisabled("%s", mesh.path.c_str());
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
// Right-click to add instance to scene
|
|
if (ImGui::BeginPopupContextItem()) {
|
|
if (ImGui::MenuItem("Add to Scene")) {
|
|
int id = nextObjectId++;
|
|
SceneObject obj(mesh.name, ObjectType::OBJMesh, id);
|
|
obj.meshPath = mesh.path;
|
|
obj.meshId = static_cast<int>(i);
|
|
sceneObjects.push_back(obj);
|
|
selectedObjectId = id;
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
addConsoleMessage("Added mesh instance: " + mesh.name, ConsoleMessageType::Info);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ImGui::CollapsingHeader("Folders", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
ImGuiTreeNodeFlags folderFlags = ImGuiTreeNodeFlags_Leaf |
|
|
ImGuiTreeNodeFlags_SpanAvailWidth |
|
|
ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
|
|
|
ImGui::TreeNodeEx("Assets", folderFlags, "[D] Assets");
|
|
if (ImGui::IsItemClicked()) {
|
|
fileBrowser.currentPath = projectManager.currentProject.assetsPath;
|
|
fileBrowser.needsRefresh = true;
|
|
}
|
|
|
|
ImGui::TreeNodeEx("Scripts", folderFlags, "[D] Scripts");
|
|
if (ImGui::IsItemClicked()) {
|
|
fileBrowser.currentPath = projectManager.currentProject.scriptsPath;
|
|
fileBrowser.needsRefresh = true;
|
|
}
|
|
|
|
ImGui::TreeNodeEx("Scenes", folderFlags, "[D] Scenes");
|
|
if (ImGui::IsItemClicked()) {
|
|
fileBrowser.currentPath = projectManager.currentProject.scenesPath;
|
|
fileBrowser.needsRefresh = true;
|
|
}
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
void addConsoleMessage(const std::string& message, ConsoleMessageType type) {
|
|
std::string prefix;
|
|
switch (type) {
|
|
case ConsoleMessageType::Info: prefix = "Info: "; break;
|
|
case ConsoleMessageType::Warning: prefix = "Warning: "; break;
|
|
case ConsoleMessageType::Error: prefix = "Error: "; break;
|
|
case ConsoleMessageType::Success: prefix = "Success: "; break;
|
|
}
|
|
|
|
auto now = std::chrono::system_clock::now();
|
|
auto time = std::chrono::system_clock::to_time_t(now);
|
|
std::string timeStr = std::ctime(&time);
|
|
timeStr = timeStr.substr(11, 8);
|
|
|
|
consoleLog.push_back("[" + timeStr + "] " + prefix + " " + message);
|
|
if (consoleLog.size() > 1000) {
|
|
consoleLog.erase(consoleLog.begin());
|
|
}
|
|
}
|
|
|
|
void logToConsole(const std::string& message) {
|
|
addConsoleMessage(message, ConsoleMessageType::Info);
|
|
}
|
|
|
|
void applyModernTheme() {
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
ImVec4* colors = style.Colors;
|
|
|
|
colors[ImGuiCol_WindowBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
|
|
colors[ImGuiCol_ChildBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
|
|
colors[ImGuiCol_PopupBg] = ImVec4(0.12f, 0.12f, 0.14f, 0.98f);
|
|
|
|
colors[ImGuiCol_Header] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f);
|
|
colors[ImGuiCol_HeaderHovered] = ImVec4(0.28f, 0.28f, 0.32f, 1.00f);
|
|
colors[ImGuiCol_HeaderActive] = ImVec4(0.24f, 0.24f, 0.28f, 1.00f);
|
|
|
|
colors[ImGuiCol_Button] = ImVec4(0.22f, 0.22f, 0.26f, 1.00f);
|
|
colors[ImGuiCol_ButtonHovered] = ImVec4(0.30f, 0.30f, 0.36f, 1.00f);
|
|
colors[ImGuiCol_ButtonActive] = ImVec4(0.26f, 0.26f, 0.30f, 1.00f);
|
|
|
|
colors[ImGuiCol_FrameBg] = ImVec4(0.14f, 0.14f, 0.16f, 1.00f);
|
|
colors[ImGuiCol_FrameBgHovered] = ImVec4(0.18f, 0.18f, 0.22f, 1.00f);
|
|
colors[ImGuiCol_FrameBgActive] = ImVec4(0.22f, 0.22f, 0.26f, 1.00f);
|
|
|
|
colors[ImGuiCol_Tab] = ImVec4(0.14f, 0.14f, 0.16f, 1.00f);
|
|
colors[ImGuiCol_TabHovered] = ImVec4(0.28f, 0.28f, 0.32f, 1.00f);
|
|
colors[ImGuiCol_TabActive] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f);
|
|
colors[ImGuiCol_TabUnfocused] = ImVec4(0.12f, 0.12f, 0.14f, 1.00f);
|
|
colors[ImGuiCol_TabUnfocusedActive] = ImVec4(0.16f, 0.16f, 0.18f, 1.00f);
|
|
|
|
colors[ImGuiCol_TitleBg] = ImVec4(0.08f, 0.08f, 0.10f, 1.00f);
|
|
colors[ImGuiCol_TitleBgActive] = ImVec4(0.12f, 0.12f, 0.14f, 1.00f);
|
|
colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.08f, 0.08f, 0.10f, 0.75f);
|
|
|
|
colors[ImGuiCol_ScrollbarBg] = ImVec4(0.10f, 0.10f, 0.12f, 1.00f);
|
|
colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.24f, 0.24f, 0.28f, 1.00f);
|
|
colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.30f, 0.30f, 0.34f, 1.00f);
|
|
colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.36f, 0.36f, 0.40f, 1.00f);
|
|
|
|
colors[ImGuiCol_Separator] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f);
|
|
colors[ImGuiCol_SeparatorHovered] = ImVec4(0.30f, 0.55f, 0.75f, 1.00f);
|
|
colors[ImGuiCol_SeparatorActive] = ImVec4(0.30f, 0.55f, 0.75f, 1.00f);
|
|
|
|
colors[ImGuiCol_ResizeGrip] = ImVec4(0.24f, 0.24f, 0.28f, 0.25f);
|
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.30f, 0.55f, 0.75f, 0.67f);
|
|
colors[ImGuiCol_ResizeGripActive] = ImVec4(0.30f, 0.55f, 0.75f, 0.95f);
|
|
|
|
colors[ImGuiCol_DockingPreview] = ImVec4(0.30f, 0.55f, 0.75f, 0.70f);
|
|
colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.08f, 0.08f, 0.10f, 1.00f);
|
|
|
|
colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.92f, 1.00f);
|
|
colors[ImGuiCol_TextDisabled] = ImVec4(0.50f, 0.50f, 0.52f, 1.00f);
|
|
|
|
colors[ImGuiCol_Border] = ImVec4(0.20f, 0.20f, 0.24f, 1.00f);
|
|
colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f);
|
|
|
|
colors[ImGuiCol_MenuBarBg] = ImVec4(0.12f, 0.12f, 0.14f, 1.00f);
|
|
|
|
colors[ImGuiCol_CheckMark] = ImVec4(0.40f, 0.70f, 0.95f, 1.00f);
|
|
colors[ImGuiCol_SliderGrab] = ImVec4(0.40f, 0.70f, 0.95f, 1.00f);
|
|
colors[ImGuiCol_SliderGrabActive] = ImVec4(0.50f, 0.80f, 1.00f, 1.00f);
|
|
|
|
style.WindowRounding = 4.0f;
|
|
style.FrameRounding = 3.0f;
|
|
style.PopupRounding = 4.0f;
|
|
style.ScrollbarRounding = 6.0f;
|
|
style.GrabRounding = 3.0f;
|
|
style.TabRounding = 4.0f;
|
|
style.WindowBorderSize = 1.0f;
|
|
style.FrameBorderSize = 0.0f;
|
|
style.PopupBorderSize = 1.0f;
|
|
style.WindowPadding = ImVec2(10, 10);
|
|
style.FramePadding = ImVec2(6, 4);
|
|
style.ItemSpacing = ImVec2(8, 6);
|
|
style.ItemInnerSpacing = ImVec2(6, 4);
|
|
style.IndentSpacing = 20.0f;
|
|
style.ScrollbarSize = 14.0f;
|
|
style.GrabMinSize = 12.0f;
|
|
}
|
|
|
|
void setupDockspace() {
|
|
ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking;
|
|
|
|
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
|
ImGui::SetNextWindowPos(viewport->WorkPos);
|
|
ImGui::SetNextWindowSize(viewport->WorkSize);
|
|
ImGui::SetNextWindowViewport(viewport->ID);
|
|
|
|
window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
|
|
window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus;
|
|
window_flags |= ImGuiWindowFlags_NoBackground;
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
|
|
|
ImGui::Begin("DockSpace", nullptr, window_flags);
|
|
ImGui::PopStyleVar(3);
|
|
|
|
ImGuiID dockspace_id = ImGui::GetID("MainDockSpace");
|
|
ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_PassthruCentralNode);
|
|
|
|
if (firstFrame) {
|
|
ImGui::DockBuilderRemoveNode(dockspace_id);
|
|
ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
|
|
ImGui::DockBuilderSetNodeSize(dockspace_id, viewport->Size);
|
|
|
|
ImGuiID dock_main_id = dockspace_id;
|
|
ImGuiID dock_left = ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Left, 0.2f, nullptr, &dock_main_id);
|
|
ImGuiID dock_right = ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Right, 0.25f, nullptr, &dock_main_id);
|
|
ImGuiID dock_bottom = ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Down, 0.25f, nullptr, &dock_main_id);
|
|
|
|
ImGui::DockBuilderDockWindow("Hierarchy", dock_left);
|
|
ImGui::DockBuilderDockWindow("Project", dock_left);
|
|
ImGui::DockBuilderDockWindow("File Browser", dock_left);
|
|
ImGui::DockBuilderDockWindow("Viewport", dock_main_id);
|
|
ImGui::DockBuilderDockWindow("Inspector", dock_right);
|
|
ImGui::DockBuilderDockWindow("Console", dock_bottom);
|
|
|
|
ImGui::DockBuilderFinish(dockspace_id);
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
void renderMainMenuBar() {
|
|
if (ImGui::BeginMainMenuBar()) {
|
|
if (ImGui::BeginMenu("File")) {
|
|
if (ImGui::MenuItem("New Scene", "Ctrl+N")) {
|
|
showNewSceneDialog = true;
|
|
memset(newSceneName, 0, sizeof(newSceneName));
|
|
}
|
|
if (ImGui::MenuItem("Save Scene", "Ctrl+S")) {
|
|
saveCurrentScene();
|
|
}
|
|
if (ImGui::MenuItem("Save Scene As...")) {
|
|
showSaveSceneAsDialog = true;
|
|
strncpy(saveSceneAsName, projectManager.currentProject.currentSceneName.c_str(),
|
|
sizeof(saveSceneAsName) - 1);
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Close Project")) {
|
|
if (projectManager.currentProject.hasUnsavedChanges) {
|
|
saveCurrentScene();
|
|
}
|
|
projectManager.currentProject = Project();
|
|
sceneObjects.clear();
|
|
selectedObjectId = -1;
|
|
showLauncher = true;
|
|
addConsoleMessage("Closed project", ConsoleMessageType::Info);
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Exit", "Alt+F4")) {
|
|
glfwSetWindowShouldClose(editorWindow, true);
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::BeginMenu("Edit")) {
|
|
if (ImGui::MenuItem("Undo", "Ctrl+Z")) {}
|
|
if (ImGui::MenuItem("Redo", "Ctrl+Y")) {}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Duplicate", "Ctrl+D") && selectedObjectId != -1) {
|
|
duplicateSelected();
|
|
}
|
|
if (ImGui::MenuItem("Delete", "Delete") && selectedObjectId != -1) {
|
|
deleteSelected();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::BeginMenu("GameObject")) {
|
|
if (ImGui::BeginMenu("3D Object")) {
|
|
if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube");
|
|
if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere");
|
|
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
|
|
ImGui::Separator();
|
|
// Import OBJ from menu
|
|
if (ImGui::MenuItem("Import OBJ...")) {
|
|
addConsoleMessage("Use File Browser to import .obj files (double-click or right-click)", ConsoleMessageType::Info);
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
// So uh, why did i have to change one line in here???
|
|
if (ImGui::BeginMenu("Window")) {
|
|
ImGui::MenuItem("Hierarchy", nullptr, &showHierarchy);
|
|
ImGui::MenuItem("Inspector", nullptr, &showInspector);
|
|
ImGui::MenuItem("File Browser", nullptr, &showFileBrowser);
|
|
ImGui::MenuItem("Project", nullptr, &showProjectBrowser);
|
|
ImGui::MenuItem("Console", nullptr, &showConsole);
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Fullscreen Viewport", "F11", &viewportFullscreen)) {}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
if (ImGui::BeginMenu("Help")) {
|
|
if (ImGui::MenuItem("About")) {
|
|
addConsoleMessage("Modularity - V1.0.1", ConsoleMessageType::Info);
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetWindowWidth() - 100);
|
|
ImGui::TextColored(ImVec4(0.5f, 0.8f, 0.5f, 1.0f), "FPS: %.1f", 1.0f / deltaTime);
|
|
|
|
ImGui::EndMainMenuBar();
|
|
}
|
|
}
|
|
|
|
void renderHierarchyPanel() {
|
|
ImGui::Begin("Hierarchy", &showHierarchy);
|
|
|
|
// -------------------------------
|
|
// TOP TOOLBAR (row with + button)
|
|
// -------------------------------
|
|
{
|
|
// Child helps align things more cleanly
|
|
ImGui::BeginChild("HierarchyToolbar", ImVec2(0, 28), false);
|
|
|
|
// "+" button top-left
|
|
if (ImGui::Button("+", ImVec2(24, 24))) {
|
|
ImGui::OpenPopup("CreateMenu");
|
|
}
|
|
|
|
// Popup create menu
|
|
if (ImGui::BeginPopup("CreateMenu")) {
|
|
if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube");
|
|
if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere");
|
|
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
// -------------------------------
|
|
// SEARCH BAR
|
|
// -------------------------------
|
|
static char searchBuffer[128] = "";
|
|
ImGui::SetNextItemWidth(-1);
|
|
ImGui::InputTextWithHint("##search", "Search objects...", searchBuffer, sizeof(searchBuffer));
|
|
|
|
ImGui::Separator();
|
|
|
|
// -------------------------------
|
|
// HIERARCHY TREE
|
|
// -------------------------------
|
|
ImGui::BeginChild("SceneTree", ImVec2(0, 0), false);
|
|
|
|
std::string filter = searchBuffer;
|
|
std::transform(filter.begin(), filter.end(), filter.begin(), ::tolower);
|
|
|
|
for (size_t i = 0; i < sceneObjects.size(); i++) {
|
|
if (sceneObjects[i].parentId != -1)
|
|
continue;
|
|
|
|
renderObjectNode(sceneObjects[i], filter);
|
|
}
|
|
|
|
// Right-click empty space
|
|
if (ImGui::BeginPopupContextWindow("HierarchyBackground",
|
|
ImGuiPopupFlags_MouseButtonRight |
|
|
ImGuiPopupFlags_NoOpenOverItems))
|
|
{
|
|
if (ImGui::BeginMenu("Create")) {
|
|
if (ImGui::MenuItem("Cube")) addObject(ObjectType::Cube, "Cube");
|
|
if (ImGui::MenuItem("Sphere")) addObject(ObjectType::Sphere, "Sphere");
|
|
if (ImGui::MenuItem("Capsule")) addObject(ObjectType::Capsule, "Capsule");
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
void renderObjectNode(SceneObject& obj, const std::string& filter) {
|
|
std::string nameLower = obj.name;
|
|
std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower);
|
|
|
|
if (!filter.empty() && nameLower.find(filter) == std::string::npos) {
|
|
return;
|
|
}
|
|
|
|
bool hasChildren = !obj.childIds.empty();
|
|
bool isSelected = (selectedObjectId == obj.id);
|
|
|
|
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth;
|
|
if (isSelected) flags |= ImGuiTreeNodeFlags_Selected;
|
|
if (!hasChildren) flags |= ImGuiTreeNodeFlags_Leaf;
|
|
|
|
const char* icon = "";
|
|
switch (obj.type) {
|
|
case ObjectType::Cube: icon = "[#]"; break;
|
|
case ObjectType::Sphere: icon = "(O)"; break;
|
|
case ObjectType::Capsule: icon = "[|]"; break;
|
|
case ObjectType::OBJMesh: icon = "[M]"; break; // OBJ mesh icon
|
|
}
|
|
|
|
bool nodeOpen = ImGui::TreeNodeEx((void*)(intptr_t)obj.id, flags, "%s %s", icon, obj.name.c_str());
|
|
|
|
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
|
|
selectedObjectId = obj.id;
|
|
}
|
|
|
|
if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) {
|
|
ImGui::SetDragDropPayload("SCENE_OBJECT", &obj.id, sizeof(int));
|
|
ImGui::Text("Moving: %s", obj.name.c_str());
|
|
ImGui::EndDragDropSource();
|
|
}
|
|
|
|
if (ImGui::BeginDragDropTarget()) {
|
|
if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("SCENE_OBJECT")) {
|
|
int draggedId = *(const int*)payload->Data;
|
|
if (draggedId != obj.id) {
|
|
setParent(draggedId, obj.id);
|
|
}
|
|
}
|
|
ImGui::EndDragDropTarget();
|
|
}
|
|
|
|
if (ImGui::BeginPopupContextItem()) {
|
|
if (ImGui::MenuItem("Duplicate")) {
|
|
selectedObjectId = obj.id;
|
|
duplicateSelected();
|
|
}
|
|
if (ImGui::MenuItem("Delete")) {
|
|
selectedObjectId = obj.id;
|
|
deleteSelected();
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Clear Parent") && obj.parentId != -1) {
|
|
setParent(obj.id, -1);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (nodeOpen) {
|
|
for (int childId : obj.childIds) {
|
|
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
|
|
[childId](const SceneObject& o) { return o.id == childId; });
|
|
if (it != sceneObjects.end()) {
|
|
renderObjectNode(*it, filter);
|
|
}
|
|
}
|
|
ImGui::TreePop();
|
|
}
|
|
}
|
|
|
|
void renderFileBrowserPanel() {
|
|
ImGui::Begin("File Browser", &showFileBrowser);
|
|
|
|
if (fileBrowser.needsRefresh) {
|
|
fileBrowser.refresh();
|
|
}
|
|
|
|
if (ImGui::Button("<")) {
|
|
fileBrowser.navigateUp();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Refresh")) {
|
|
fileBrowser.needsRefresh = true;
|
|
}
|
|
ImGui::SameLine();
|
|
|
|
std::string pathStr = fileBrowser.currentPath.string();
|
|
ImGui::TextWrapped("%s", pathStr.c_str());
|
|
|
|
ImGui::Separator();
|
|
|
|
ImGui::BeginChild("FileList", ImVec2(0, 0), false);
|
|
|
|
for (const auto& entry : fileBrowser.entries) {
|
|
const char* icon = fileBrowser.getFileIcon(entry);
|
|
std::string filename = entry.path().filename().string();
|
|
|
|
bool isSelected = (fileBrowser.selectedFile == entry.path());
|
|
bool isOBJ = fileBrowser.isOBJFile(entry);
|
|
|
|
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_NoTreePushOnOpen;
|
|
if (isSelected) flags |= ImGuiTreeNodeFlags_Selected;
|
|
|
|
// Highlight OBJ files
|
|
if (isOBJ) {
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 0.4f, 1.0f));
|
|
}
|
|
|
|
ImGui::TreeNodeEx(filename.c_str(), flags, "%s %s", icon, filename.c_str());
|
|
|
|
if (isOBJ) {
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
if (ImGui::IsItemClicked()) {
|
|
fileBrowser.selectedFile = entry.path();
|
|
}
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
|
if (entry.is_directory()) {
|
|
fileBrowser.navigateTo(entry.path());
|
|
} else if (isOBJ) {
|
|
// Double-click OBJ to import
|
|
pendingOBJPath = entry.path().string();
|
|
std::string defaultName = entry.path().stem().string();
|
|
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
|
|
showImportOBJDialog = true;
|
|
} else {
|
|
logToConsole("Selected file: " + filename);
|
|
}
|
|
}
|
|
|
|
if (ImGui::BeginPopupContextItem()) {
|
|
if (ImGui::MenuItem("Open")) {
|
|
if (entry.is_directory()) {
|
|
fileBrowser.navigateTo(entry.path());
|
|
}
|
|
}
|
|
// Add Import option for OBJ files
|
|
if (isOBJ) {
|
|
if (ImGui::MenuItem("Import to Scene")) {
|
|
pendingOBJPath = entry.path().string();
|
|
std::string defaultName = entry.path().stem().string();
|
|
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
|
|
showImportOBJDialog = true;
|
|
}
|
|
if (ImGui::MenuItem("Quick Import")) {
|
|
importOBJToScene(entry.path().string(), "");
|
|
}
|
|
}
|
|
if (ImGui::MenuItem("Show in Explorer")) {
|
|
#ifdef _WIN32
|
|
std::string cmd = "explorer \"" + entry.path().parent_path().string() + "\"";
|
|
system(cmd.c_str());
|
|
#endif
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
ImGui::End();
|
|
}
|
|
|
|
void renderInspectorPanel() {
|
|
ImGui::Begin("Inspector", &showInspector);
|
|
|
|
if (selectedObjectId == -1) {
|
|
ImGui::TextDisabled("No object selected");
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
|
|
[this](const SceneObject& obj) { return obj.id == selectedObjectId; });
|
|
|
|
if (it == sceneObjects.end()) {
|
|
ImGui::TextDisabled("Object not found");
|
|
ImGui::End();
|
|
return;
|
|
}
|
|
|
|
SceneObject& obj = *it;
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.2f, 0.4f, 0.6f, 1.0f));
|
|
|
|
if (ImGui::CollapsingHeader("Object Info", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
char nameBuffer[128];
|
|
strncpy(nameBuffer, obj.name.c_str(), sizeof(nameBuffer));
|
|
nameBuffer[sizeof(nameBuffer) - 1] = '\0';
|
|
|
|
ImGui::Text("Name:");
|
|
ImGui::SameLine();
|
|
ImGui::SetNextItemWidth(-1);
|
|
if (ImGui::InputText("##Name", nameBuffer, sizeof(nameBuffer))) {
|
|
obj.name = nameBuffer;
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
}
|
|
|
|
ImGui::Text("Type:");
|
|
ImGui::SameLine();
|
|
const char* typeNames[] = { "Cube", "Sphere", "Capsule", "OBJ Mesh" };
|
|
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "%s", typeNames[(int)obj.type]);
|
|
|
|
ImGui::Text("ID:");
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("%d", obj.id);
|
|
}
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
ImGui::Spacing();
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.4f, 0.5f, 0.3f, 1.0f));
|
|
|
|
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
ImGui::Indent(10.0f);
|
|
|
|
ImGui::Text("Position");
|
|
ImGui::PushItemWidth(-1);
|
|
if (ImGui::DragFloat3("##Position", &obj.position.x, 0.1f)) {
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
}
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Rotation");
|
|
ImGui::PushItemWidth(-1);
|
|
if (ImGui::DragFloat3("##Rotation", &obj.rotation.x, 1.0f, -360.0f, 360.0f)) {
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
}
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Scale");
|
|
ImGui::PushItemWidth(-1);
|
|
if (ImGui::DragFloat3("##Scale", &obj.scale.x, 0.05f, 0.01f, 100.0f)) {
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
}
|
|
ImGui::PopItemWidth();
|
|
|
|
ImGui::Spacing();
|
|
|
|
if (ImGui::Button("Reset Transform", ImVec2(-1, 0))) {
|
|
obj.position = glm::vec3(0.0f);
|
|
obj.rotation = glm::vec3(0.0f);
|
|
obj.scale = glm::vec3(1.0f);
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
}
|
|
|
|
ImGui::Unindent(10.0f);
|
|
}
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
// OBJ Mesh info section
|
|
if (obj.type == ObjectType::OBJMesh) {
|
|
ImGui::Spacing();
|
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.3f, 0.5f, 0.4f, 1.0f));
|
|
|
|
if (ImGui::CollapsingHeader("Mesh Info", ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
ImGui::Indent(10.0f);
|
|
|
|
const auto* meshInfo = g_objLoader.getMeshInfo(obj.meshId);
|
|
if (meshInfo) {
|
|
ImGui::Text("Source File:");
|
|
ImGui::TextDisabled("%s", fs::path(meshInfo->path).filename().string().c_str());
|
|
|
|
ImGui::Spacing();
|
|
|
|
ImGui::Text("Vertices: %d", meshInfo->vertexCount);
|
|
ImGui::Text("Faces: %d", meshInfo->faceCount);
|
|
ImGui::Text("Has Normals: %s", meshInfo->hasNormals ? "Yes" : "No");
|
|
ImGui::Text("Has UVs: %s", meshInfo->hasTexCoords ? "Yes" : "No");
|
|
|
|
ImGui::Spacing();
|
|
|
|
if (ImGui::Button("Reload Mesh", ImVec2(-1, 0))) {
|
|
std::string errMsg;
|
|
int newId = g_objLoader.loadOBJ(obj.meshPath, errMsg);
|
|
if (newId >= 0) {
|
|
obj.meshId = newId;
|
|
addConsoleMessage("Reloaded mesh: " + obj.name, ConsoleMessageType::Success);
|
|
} else {
|
|
addConsoleMessage("Failed to reload: " + errMsg, ConsoleMessageType::Error);
|
|
}
|
|
}
|
|
} else {
|
|
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "Mesh data not found!");
|
|
ImGui::TextDisabled("Path: %s", obj.meshPath.c_str());
|
|
|
|
if (ImGui::Button("Try Reload", ImVec2(-1, 0))) {
|
|
std::string errMsg;
|
|
obj.meshId = g_objLoader.loadOBJ(obj.meshPath, errMsg);
|
|
if (obj.meshId >= 0) {
|
|
addConsoleMessage("Mesh reloaded successfully", ConsoleMessageType::Success);
|
|
} else {
|
|
addConsoleMessage("Reload failed: " + errMsg, ConsoleMessageType::Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::Unindent(10.0f);
|
|
}
|
|
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
|
|
if (renderer.getSkybox()) {
|
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.5f, 0.3f, 0.5f, 1.0f));
|
|
|
|
if (ImGui::CollapsingHeader("Environment")) {
|
|
ImGui::Indent(10.0f);
|
|
|
|
float timeOfDay = renderer.getSkybox()->getTimeOfDay();
|
|
|
|
ImGui::Text("Time of Day");
|
|
ImGui::PushItemWidth(-1);
|
|
if (ImGui::SliderFloat("##TimeOfDay", &timeOfDay, 0.0f, 1.0f, "%.2f")) {
|
|
renderer.getSkybox()->setTimeOfDay(timeOfDay);
|
|
}
|
|
ImGui::PopItemWidth();
|
|
|
|
if (ImGui::Button("Night", ImVec2(60, 0))) renderer.getSkybox()->setTimeOfDay(0.0f);
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Sunrise", ImVec2(60, 0))) renderer.getSkybox()->setTimeOfDay(0.25f);
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Day", ImVec2(60, 0))) renderer.getSkybox()->setTimeOfDay(0.5f);
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Sunset", ImVec2(60, 0))) renderer.getSkybox()->setTimeOfDay(0.75f);
|
|
|
|
ImGui::Unindent(10.0f);
|
|
}
|
|
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
if (ImGui::Button("Duplicate", ImVec2(ImGui::GetContentRegionAvail().x * 0.5f - 4, 0))) {
|
|
duplicateSelected();
|
|
}
|
|
ImGui::SameLine();
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.2f, 0.2f, 1.0f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.3f, 0.3f, 1.0f));
|
|
if (ImGui::Button("Delete", ImVec2(-1, 0))) {
|
|
deleteSelected();
|
|
}
|
|
ImGui::PopStyleColor(2);
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
void renderConsolePanel() {
|
|
ImGui::Begin("Console", &showConsole);
|
|
|
|
if (ImGui::Button("Clear")) {
|
|
consoleLog.clear();
|
|
}
|
|
ImGui::SameLine();
|
|
static bool autoScroll = true;
|
|
ImGui::Checkbox("Auto-scroll", &autoScroll);
|
|
|
|
ImGui::Separator();
|
|
|
|
ImGui::BeginChild("LogArea", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar);
|
|
|
|
for (const auto& log : consoleLog) {
|
|
ImVec4 color = ImVec4(0.8f, 0.8f, 0.8f, 1.0f);
|
|
if (log.find("error") != std::string::npos || log.find("Error") != std::string::npos || log.find("[ERROR]") != std::string::npos) {
|
|
color = ImVec4(1.0f, 0.4f, 0.4f, 1.0f);
|
|
} else if (log.find("warning") != std::string::npos || log.find("Warning") != std::string::npos || log.find("[WARN]") != std::string::npos) {
|
|
color = ImVec4(1.0f, 0.8f, 0.4f, 1.0f);
|
|
} else if (log.find("success") != std::string::npos || log.find("Success") != std::string::npos || log.find("[SUCCESS]") != std::string::npos) {
|
|
color = ImVec4(0.4f, 1.0f, 0.4f, 1.0f);
|
|
}
|
|
ImGui::TextColored(color, "%s", log.c_str());
|
|
}
|
|
|
|
if (autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
|
|
ImGui::SetScrollHereY(1.0f);
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
ImGui::End();
|
|
}
|
|
|
|
void renderViewport() {
|
|
ImGuiWindowFlags viewportFlags = ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoScrollbar;
|
|
|
|
if (viewportFullscreen) {
|
|
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
|
ImGui::SetNextWindowPos(viewport->WorkPos);
|
|
ImGui::SetNextWindowSize(viewport->WorkSize);
|
|
viewportFlags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking;
|
|
}
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
|
ImGui::Begin("Viewport", nullptr, viewportFlags);
|
|
ImGui::PopStyleVar();
|
|
|
|
ImVec2 fullAvail = ImGui::GetContentRegionAvail();
|
|
|
|
const float toolbarHeight = 20.0f;
|
|
ImVec2 imageSize = fullAvail;
|
|
imageSize.y = ImMax(1.0f, imageSize.y - toolbarHeight);
|
|
|
|
if (imageSize.x > 0 && imageSize.y > 0) {
|
|
viewportWidth = static_cast<int>(imageSize.x);
|
|
viewportHeight = static_cast<int>(imageSize.y);
|
|
if (rendererInitialized) {
|
|
renderer.resize(viewportWidth, viewportHeight);
|
|
}
|
|
}
|
|
|
|
// We'll forward this outside the block for camera focus
|
|
bool mouseOverViewportImage = false;
|
|
|
|
if (rendererInitialized) {
|
|
// RENDER SCENE TO TEXTURE
|
|
glm::mat4 proj = glm::perspective(
|
|
glm::radians(FOV),
|
|
(float)viewportWidth / (float)viewportHeight,
|
|
NEAR_PLANE, FAR_PLANE
|
|
);
|
|
|
|
glm::mat4 view = camera.getViewMatrix();
|
|
|
|
renderer.beginRender(view, proj);
|
|
renderer.renderScene(camera, sceneObjects);
|
|
unsigned int tex = renderer.getViewportTexture();
|
|
|
|
// DRAW THE VIEWPORT IMAGE (only top region, below we keep space for toolbar)
|
|
ImGui::Image((void*)(intptr_t)tex, imageSize, ImVec2(0, 1), ImVec2(1, 0));
|
|
|
|
// Get the exact rect of the image we just drew
|
|
ImVec2 imageMin = ImGui::GetItemRectMin();
|
|
ImVec2 imageMax = ImGui::GetItemRectMax();
|
|
mouseOverViewportImage = ImGui::IsItemHovered();
|
|
|
|
// GIZMO (Please work...)
|
|
SceneObject* selectedObj = getSelectedObject();
|
|
if (selectedObj) {
|
|
ImGuizmo::BeginFrame();
|
|
ImGuizmo::Enable(true);
|
|
ImGuizmo::SetOrthographic(false);
|
|
ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList());
|
|
|
|
ImGuizmo::SetRect(
|
|
imageMin.x,
|
|
imageMin.y,
|
|
imageMax.x - imageMin.x,
|
|
imageMax.y - imageMin.y
|
|
);
|
|
|
|
// Build model matrix from your SceneObject
|
|
glm::mat4 modelMatrix(1.0f);
|
|
modelMatrix = glm::translate(modelMatrix, selectedObj->position);
|
|
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.x), glm::vec3(1, 0, 0));
|
|
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.y), glm::vec3(0, 1, 0));
|
|
modelMatrix = glm::rotate(modelMatrix, glm::radians(selectedObj->rotation.z), glm::vec3(0, 0, 1));
|
|
modelMatrix = glm::scale(modelMatrix, selectedObj->scale);
|
|
|
|
float* snapPtr = nullptr;
|
|
float snapRot[3] = { rotationSnapValue, rotationSnapValue, rotationSnapValue };
|
|
|
|
if (useSnap) {
|
|
if (mCurrentGizmoOperation == ImGuizmo::ROTATE) {
|
|
snapPtr = snapRot;
|
|
} else {
|
|
snapPtr = snapValue;
|
|
}
|
|
}
|
|
|
|
ImGuizmo::Manipulate(
|
|
glm::value_ptr(view),
|
|
glm::value_ptr(proj),
|
|
mCurrentGizmoOperation,
|
|
mCurrentGizmoMode,
|
|
glm::value_ptr(modelMatrix),
|
|
nullptr,
|
|
snapPtr
|
|
);
|
|
|
|
if (ImGuizmo::IsUsing()) {
|
|
// Use ImGuizmo's own decompose helper to be safe
|
|
float t[3], r[3], s[3];
|
|
ImGuizmo::DecomposeMatrixToComponents(glm::value_ptr(modelMatrix), t, r, s);
|
|
|
|
selectedObj->position = glm::vec3(t[0], t[1], t[2]);
|
|
// r[] is already in degrees
|
|
selectedObj->rotation = glm::vec3(r[0], r[1], r[2]);
|
|
selectedObj->scale = glm::vec3(s[0], s[1], s[2]);
|
|
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
}
|
|
}
|
|
|
|
// Place it just under the image
|
|
ImGui::SetCursorPos(ImVec2(20, imageSize.y + 20));
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.95f);
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.12f, 0.12f, 0.14f, 0.9f));
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 0.6f));
|
|
|
|
// I hate how long this took to actually work. :sob:
|
|
if (ImGui::RadioButton("Move", mCurrentGizmoOperation == ImGuizmo::TRANSLATE)) mCurrentGizmoOperation = ImGuizmo::TRANSLATE;
|
|
ImGui::SameLine();
|
|
if (ImGui::RadioButton("Rotate", mCurrentGizmoOperation == ImGuizmo::ROTATE)) mCurrentGizmoOperation = ImGuizmo::ROTATE;
|
|
ImGui::SameLine();
|
|
if (ImGui::RadioButton("Scale", mCurrentGizmoOperation == ImGuizmo::SCALE)) mCurrentGizmoOperation = ImGuizmo::SCALE;
|
|
ImGui::SameLine();
|
|
if (ImGui::RadioButton("Uni", mCurrentGizmoOperation == ImGuizmo::UNIVERSAL)) mCurrentGizmoOperation = ImGuizmo::UNIVERSAL;
|
|
|
|
ImGui::SameLine();
|
|
if (ImGui::RadioButton("Local", mCurrentGizmoMode == ImGuizmo::LOCAL)) mCurrentGizmoMode = ImGuizmo::LOCAL;
|
|
ImGui::SameLine();
|
|
if (ImGui::RadioButton("World", mCurrentGizmoMode == ImGuizmo::WORLD)) mCurrentGizmoMode = ImGuizmo::WORLD;
|
|
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("Snap", &useSnap);
|
|
|
|
if (useSnap) {
|
|
ImGui::SameLine();
|
|
switch (mCurrentGizmoOperation) {
|
|
case ImGuizmo::TRANSLATE:
|
|
case ImGuizmo::SCALE:
|
|
ImGui::SetNextItemWidth(100);
|
|
ImGui::DragFloat3("##snap", snapValue, 0.1f, 0.01f, 10.0f);
|
|
break;
|
|
case ImGuizmo::ROTATE:
|
|
ImGui::SetNextItemWidth(80);
|
|
ImGui::DragFloat("Angle", &rotationSnapValue, 1.0f, 0.0f, 90.0f);
|
|
break;
|
|
}
|
|
}
|
|
|
|
ImGui::PopStyleColor(2);
|
|
ImGui::PopStyleVar();
|
|
|
|
// CAMERA FOCUS CLICK (only when not dragging gizmo and ONLY over the image)
|
|
if (mouseOverViewportImage &&
|
|
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
|
!ImGuizmo::IsUsing())
|
|
{
|
|
viewportController.setFocused(true);
|
|
cursorLocked = true;
|
|
glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
|
|
camera.firstMouse = true;
|
|
}
|
|
|
|
// ESC to release cursor
|
|
if (glfwGetKey(editorWindow, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
|
|
glfwSetInputMode(editorWindow, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
|
|
viewportController.setFocused(false);
|
|
cursorLocked = false;
|
|
camera.firstMouse = true;
|
|
}
|
|
}
|
|
|
|
// OVERLAY HINT TEXT (top-left over the image)
|
|
ImGui::SetCursorPos(ImVec2(10, 30));
|
|
ImGui::TextColored(
|
|
ImVec4(1, 1, 1, 0.7f),
|
|
"WASD: Move | QE: Up/Down | Shift: Sprint | ESC: Release | F11: Fullscreen"
|
|
);
|
|
|
|
if (viewportController.isViewportFocused()) {
|
|
ImGui::SetCursorPos(ImVec2(10, 50));
|
|
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Camera Active");
|
|
}
|
|
|
|
bool windowFocused = ImGui::IsWindowFocused();
|
|
viewportController.updateFocusFromImGui(windowFocused);
|
|
|
|
ImGui::End();
|
|
}
|
|
|
|
void addObject(ObjectType type, const std::string& baseName) {
|
|
int id = nextObjectId++;
|
|
std::string name = baseName + " " + std::to_string(id);
|
|
sceneObjects.push_back(SceneObject(name, type, id));
|
|
selectedObjectId = id;
|
|
if (projectManager.currentProject.isLoaded) {
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
}
|
|
logToConsole("Created: " + name);
|
|
}
|
|
|
|
void duplicateSelected() {
|
|
auto it = std::find_if(sceneObjects.begin(), sceneObjects.end(),
|
|
[this](const SceneObject& obj) { return obj.id == selectedObjectId; });
|
|
|
|
if (it != sceneObjects.end()) {
|
|
int id = nextObjectId++;
|
|
SceneObject newObj(it->name + " (Copy)", it->type, id);
|
|
newObj.position = it->position + glm::vec3(1.0f, 0.0f, 0.0f);
|
|
newObj.rotation = it->rotation;
|
|
newObj.scale = it->scale;
|
|
// Copy mesh data for OBJ meshes
|
|
newObj.meshPath = it->meshPath;
|
|
newObj.meshId = it->meshId;
|
|
|
|
sceneObjects.push_back(newObj);
|
|
selectedObjectId = id;
|
|
if (projectManager.currentProject.isLoaded) {
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
}
|
|
logToConsole("Duplicated: " + newObj.name);
|
|
}
|
|
}
|
|
|
|
void deleteSelected() {
|
|
auto it = std::remove_if(sceneObjects.begin(), sceneObjects.end(),
|
|
[this](const SceneObject& obj) { return obj.id == selectedObjectId; });
|
|
|
|
if (it != sceneObjects.end()) {
|
|
logToConsole("Deleted object");
|
|
sceneObjects.erase(it, sceneObjects.end());
|
|
selectedObjectId = -1;
|
|
if (projectManager.currentProject.isLoaded) {
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void setParent(int childId, int parentId) {
|
|
auto childIt = std::find_if(sceneObjects.begin(), sceneObjects.end(),
|
|
[childId](const SceneObject& obj) { return obj.id == childId; });
|
|
|
|
if (childIt == sceneObjects.end()) return;
|
|
|
|
if (childIt->parentId != -1) {
|
|
auto oldParentIt = std::find_if(sceneObjects.begin(), sceneObjects.end(),
|
|
[&childIt](const SceneObject& obj) { return obj.id == childIt->parentId; });
|
|
if (oldParentIt != sceneObjects.end()) {
|
|
auto& children = oldParentIt->childIds;
|
|
children.erase(std::remove(children.begin(), children.end(), childId), children.end());
|
|
}
|
|
}
|
|
|
|
childIt->parentId = parentId;
|
|
|
|
if (parentId != -1) {
|
|
auto newParentIt = std::find_if(sceneObjects.begin(), sceneObjects.end(),
|
|
[parentId](const SceneObject& obj) { return obj.id == parentId; });
|
|
if (newParentIt != sceneObjects.end()) {
|
|
newParentIt->childIds.push_back(childId);
|
|
}
|
|
}
|
|
|
|
if (projectManager.currentProject.isLoaded) {
|
|
projectManager.currentProject.hasUnsavedChanges = true;
|
|
}
|
|
logToConsole("Reparented object");
|
|
}
|
|
|
|
void setupImGui() {
|
|
float mainScale = ImGui_ImplGlfw_GetContentScaleForMonitor(glfwGetPrimaryMonitor());
|
|
IMGUI_CHECKVERSION();
|
|
ImGui::CreateContext();
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
|
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
|
|
|
applyModernTheme();
|
|
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
style.ScaleAllSizes(mainScale);
|
|
style.FontScaleDpi = mainScale;
|
|
|
|
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
|
style.WindowRounding = 0.0f;
|
|
style.Colors[ImGuiCol_WindowBg].w = 1.0f;
|
|
}
|
|
|
|
ImGui_ImplGlfw_InitForOpenGL(editorWindow, true);
|
|
if (!ImGui_ImplOpenGL3_Init("#version 330")) {
|
|
std::cerr << "ImGui OpenGL3 init failed!\n";
|
|
throw std::runtime_error("ImGui error");
|
|
}
|
|
}
|
|
};
|
|
|
|
int main() {
|
|
Engine engine;
|
|
if (!engine.init()) {
|
|
std::cerr << "Engine init failed!\n";
|
|
return -1;
|
|
}
|
|
|
|
engine.run();
|
|
engine.shutdown();
|
|
return 0;
|
|
}
|