#include #include #include #include #include #include #include #include #include #include #include #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 #include #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 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 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 generateSphere(int segments = 32, int rings = 16) { std::vector 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 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 generateCapsule(int segments = 16, int rings = 8) { std::vector 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 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; std::string name; int vertexCount = 0; int faceCount = 0; bool hasNormals = false; bool hasTexCoords = false; }; private: std::vector 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(i); } } tinyobj::attrib_t attrib; std::vector shapes; std::vector 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 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(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 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(vertices.data(), vertices.size() * sizeof(float)); loaded.vertexCount = static_cast(vertices.size() / 8); loaded.faceCount = faceCount; loaded.hasNormals = hasNormalsInFile; loaded.hasTexCoords = !attrib.texcoords.empty(); loadedMeshes.push_back(std::move(loaded)); return static_cast(loadedMeshes.size() - 1); } // Get mesh by index Mesh* getMesh(int index) { if (index < 0 || index >= static_cast(loadedMeshes.size())) { return nullptr; } return loadedMeshes[index].mesh.get(); } // Get mesh info const LoadedMesh* getMeshInfo(int index) const { if (index < 0 || index >= static_cast(loadedMeshes.size())) { return nullptr; } return &loadedMeshes[index]; } // Get all loaded meshes (for UI display) const std::vector& 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& 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 getSceneList() const { std::vector 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 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& 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(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& 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(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 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 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(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(viewportWidth) / static_cast(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(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(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(imageSize.x); viewportHeight = static_cast(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; }