Added scriptable window support to Modularity, Yey!

This commit is contained in:
Anemunt
2025-12-18 17:40:01 -05:00
parent 655d4cce49
commit ff4baceaa5
11 changed files with 5753 additions and 5447 deletions

View File

@@ -0,0 +1,54 @@
// Minimal sample showing how to expose a custom editor tab from a script binary.
// Build via the engines “Compile Script” action. If compiling manually:
// Linux: g++ -std=c++20 -fPIC -O2 -I../src -I../include -c EditorWindowSample.cpp -o ../Cache/ScriptBin/EditorWindowSample.o
// g++ -shared ../Cache/ScriptBin/EditorWindowSample.o -o ../Cache/ScriptBin/EditorWindowSample.so -ldl -lpthread
// Windows: cl /nologo /std:c++20 /EHsc /MD /O2 /I ..\src /I ..\include /c EditorWindowSample.cpp /Fo ..\Cache\ScriptBin\EditorWindowSample.obj
// link /nologo /DLL ..\Cache\ScriptBin\EditorWindowSample.obj /OUT:..\Cache\ScriptBin\EditorWindowSample.dll User32.lib Advapi32.lib
#include "ScriptRuntime.h"
#include "SceneObject.h"
#include "ThirdParty/imgui/imgui.h"
#include <string>
namespace {
bool toggle = false;
float sliderValue = 0.5f;
char note[128] = "Hello from script!";
void drawContent(ScriptContext& ctx) {
ImGui::TextUnformatted("EditorWindowSample");
ImGui::Separator();
ImGui::Checkbox("Toggle", &toggle);
ImGui::SliderFloat("Value", &sliderValue, 0.0f, 1.0f, "%.2f");
ImGui::InputText("Note", note, sizeof(note));
if (ImGui::Button("Log Message")) {
ctx.AddConsoleMessage(std::string("Script tab says: ") + note);
}
if (ctx.object) {
ImGui::Separator();
ImGui::TextDisabled("Selected object: %s (id=%d)", ctx.object->name.c_str(), ctx.object->id);
if (ImGui::Button("Nudge +Y")) {
auto pos = ctx.object->position;
pos.y += 0.25f;
ctx.SetPosition(pos);
ctx.MarkDirty();
}
} else {
ImGui::TextDisabled("Select an object to enable actions");
}
}
} // namespace
extern "C" void RenderEditorWindow(ScriptContext& ctx) {
// Called every frame while the scripted window is open. Use ImGui freely here.
drawContent(ctx);
}
extern "C" void ExitRenderEditorWindow(ScriptContext& ctx) {
// Called once when the user closes the tab from View -> Scripted Windows.
// Good place to persist settings or emit a final log.
(void)ctx;
}

View File

@@ -0,0 +1,98 @@
#include "Engine.h"
#include "ModelLoader.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <cstdlib>
#include <cfloat>
#include <cmath>
#include <functional>
#include <sstream>
#include <unordered_set>
#include <optional>
#include <future>
#include <chrono>
#include <future>
#ifdef _WIN32
#include <shlobj.h>
#endif
void Engine::renderEnvironmentWindow() {
if (!showEnvironmentWindow) return;
ImGui::Begin("Environment", &showEnvironmentWindow);
Skybox* skybox = renderer.getSkybox();
if (skybox) {
float tod = skybox->getTimeOfDay();
ImGui::TextDisabled("Day / Night Cycle");
ImGui::SetNextItemWidth(-1);
if (ImGui::SliderFloat("##EnvDayNight", &tod, 0.0f, 1.0f, "%.2f")) {
skybox->setTimeOfDay(tod);
projectManager.currentProject.hasUnsavedChanges = true;
}
static char skyVertBuf[256] = {};
static char skyFragBuf[256] = {};
if (skyVertBuf[0] == '\0') std::snprintf(skyVertBuf, sizeof(skyVertBuf), "%s", skybox->getVertPath().c_str());
if (skyFragBuf[0] == '\0') std::snprintf(skyFragBuf, sizeof(skyFragBuf), "%s", skybox->getFragPath().c_str());
ImGui::Separator();
ImGui::Text("Skybox Shader");
ImGui::SetNextItemWidth(-1);
if (ImGui::InputText("##SkyVert", skyVertBuf, sizeof(skyVertBuf))) {}
ImGui::SetNextItemWidth(-1);
if (ImGui::InputText("##SkyFrag", skyFragBuf, sizeof(skyFragBuf))) {}
bool selectionIsShader = false;
if (!fileBrowser.selectedFile.empty() && fs::exists(fileBrowser.selectedFile)) {
selectionIsShader = fileBrowser.getFileCategory(fs::directory_entry(fileBrowser.selectedFile)) == FileCategory::Shader;
}
ImGui::BeginDisabled(!selectionIsShader);
if (ImGui::Button("Use Selection as Vert")) {
std::snprintf(skyVertBuf, sizeof(skyVertBuf), "%s", fileBrowser.selectedFile.string().c_str());
}
ImGui::SameLine();
if (ImGui::Button("Use Selection as Frag")) {
std::snprintf(skyFragBuf, sizeof(skyFragBuf), "%s", fileBrowser.selectedFile.string().c_str());
}
ImGui::EndDisabled();
if (ImGui::Button("Reload Skybox Shader")) {
skybox->setShaderPaths(skyVertBuf, skyFragBuf);
}
} else {
ImGui::TextDisabled("Skybox not available");
}
ImGui::Separator();
ImGui::Text("Global Ambient");
glm::vec3 ambient = renderer.getAmbientColor();
if (ImGui::ColorEdit3("##AmbientColor", &ambient.x, ImGuiColorEditFlags_DisplayRGB)) {
renderer.setAmbientColor(ambient);
projectManager.currentProject.hasUnsavedChanges = true;
}
ImGui::End();
}
void Engine::renderCameraWindow() {
if (!showCameraWindow) return;
ImGui::Begin("Camera", &showCameraWindow);
ImGui::TextDisabled("Movement");
ImGui::SetNextItemWidth(-1);
if (ImGui::DragFloat("Base Speed", &camera.moveSpeed, 0.1f, 0.1f, 100.0f, "%.2f")) {
camera.moveSpeed = std::max(0.01f, camera.moveSpeed);
}
ImGui::SetNextItemWidth(-1);
if (ImGui::DragFloat("Sprint Speed", &camera.sprintSpeed, 0.1f, 0.1f, 200.0f, "%.2f")) {
camera.sprintSpeed = std::max(camera.moveSpeed, camera.sprintSpeed);
}
ImGui::Checkbox("Smooth Movement", &camera.smoothMovement);
ImGui::BeginDisabled(!camera.smoothMovement);
ImGui::SetNextItemWidth(-1);
ImGui::DragFloat("Acceleration", &camera.acceleration, 0.1f, 0.1f, 100.0f, "%.2f");
ImGui::EndDisabled();
ImGui::End();
}

View File

@@ -0,0 +1,788 @@
#include "Engine.h"
#include "ModelLoader.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <cstdlib>
#include <cfloat>
#include <cmath>
#include <functional>
#include <sstream>
#include <unordered_set>
#include <optional>
#include <future>
#include <chrono>
#include <future>
#ifdef _WIN32
#include <shlobj.h>
#endif
namespace FileIcons {
// Draw a folder icon
void DrawFolderIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float w = size;
float h = size * 0.75f;
float tabW = w * 0.4f;
float tabH = h * 0.15f;
// Folder body
drawList->AddRectFilled(
ImVec2(pos.x, pos.y + tabH),
ImVec2(pos.x + w, pos.y + h),
color, 3.0f
);
// Folder tab
drawList->AddRectFilled(
ImVec2(pos.x, pos.y),
ImVec2(pos.x + tabW, pos.y + tabH + 2),
color, 2.0f
);
}
// Draw a scene/document icon
void DrawSceneIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float w = size * 0.8f;
float h = size;
float cornerSize = w * 0.25f;
// Main document body
ImVec2 p1 = ImVec2(pos.x, pos.y);
ImVec2 p2 = ImVec2(pos.x + w - cornerSize, pos.y);
ImVec2 p3 = ImVec2(pos.x + w, pos.y + cornerSize);
ImVec2 p4 = ImVec2(pos.x + w, pos.y + h);
ImVec2 p5 = ImVec2(pos.x, pos.y + h);
drawList->AddQuadFilled(p1, ImVec2(pos.x + w - cornerSize, pos.y), ImVec2(pos.x + w - cornerSize, pos.y + h), p5, color);
drawList->AddTriangleFilled(p2, p3, ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), color);
drawList->AddRectFilled(ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), p4, color);
// Corner fold
drawList->AddTriangleFilled(p2, ImVec2(pos.x + w - cornerSize, pos.y + cornerSize), p3,
IM_COL32(255, 255, 255, 60));
// Scene icon indicator (play triangle)
float cx = pos.x + w * 0.5f;
float cy = pos.y + h * 0.55f;
float triSize = size * 0.25f;
drawList->AddTriangleFilled(
ImVec2(cx - triSize * 0.4f, cy - triSize * 0.5f),
ImVec2(cx - triSize * 0.4f, cy + triSize * 0.5f),
ImVec2(cx + triSize * 0.5f, cy),
IM_COL32(255, 255, 255, 180)
);
}
// Draw a 3D model icon (cube wireframe)
void DrawModelIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float s = size * 0.8f;
float offset = size * 0.1f;
float depth = s * 0.3f;
// Front face
ImVec2 f1 = ImVec2(pos.x + offset, pos.y + offset + depth);
ImVec2 f2 = ImVec2(pos.x + offset + s, pos.y + offset + depth);
ImVec2 f3 = ImVec2(pos.x + offset + s, pos.y + offset + s);
ImVec2 f4 = ImVec2(pos.x + offset, pos.y + offset + s);
// Back face
ImVec2 b1 = ImVec2(f1.x + depth, f1.y - depth);
ImVec2 b2 = ImVec2(f2.x + depth, f2.y - depth);
ImVec2 b3 = ImVec2(f3.x + depth, f3.y - depth);
// Fill front face
drawList->AddQuadFilled(f1, f2, f3, f4, color);
// Fill top face
drawList->AddQuadFilled(f1, f2, b2, b1, IM_COL32(
(color & 0xFF) * 0.7f,
((color >> 8) & 0xFF) * 0.7f,
((color >> 16) & 0xFF) * 0.7f,
(color >> 24) & 0xFF
));
// Fill right face
drawList->AddQuadFilled(f2, b2, b3, f3, IM_COL32(
(color & 0xFF) * 0.5f,
((color >> 8) & 0xFF) * 0.5f,
((color >> 16) & 0xFF) * 0.5f,
(color >> 24) & 0xFF
));
// Edges
ImU32 edgeColor = IM_COL32(255, 255, 255, 100);
drawList->AddLine(f1, f2, edgeColor, 1.0f);
drawList->AddLine(f2, f3, edgeColor, 1.0f);
drawList->AddLine(f3, f4, edgeColor, 1.0f);
drawList->AddLine(f4, f1, edgeColor, 1.0f);
drawList->AddLine(f1, b1, edgeColor, 1.0f);
drawList->AddLine(f2, b2, edgeColor, 1.0f);
drawList->AddLine(b1, b2, edgeColor, 1.0f);
drawList->AddLine(f3, b3, edgeColor, 1.0f);
drawList->AddLine(b2, b3, edgeColor, 1.0f);
}
// Draw a texture/image icon
void DrawTextureIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float padding = size * 0.1f;
ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding);
ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding);
// Frame
drawList->AddRectFilled(tl, br, color, 2.0f);
// Mountain landscape
float midY = pos.y + size * 0.6f;
drawList->AddTriangleFilled(
ImVec2(pos.x + size * 0.2f, br.y - padding),
ImVec2(pos.x + size * 0.45f, midY),
ImVec2(pos.x + size * 0.7f, br.y - padding),
IM_COL32(60, 60, 60, 255)
);
drawList->AddTriangleFilled(
ImVec2(pos.x + size * 0.5f, br.y - padding),
ImVec2(pos.x + size * 0.7f, midY + size * 0.1f),
ImVec2(pos.x + size * 0.9f, br.y - padding),
IM_COL32(80, 80, 80, 255)
);
// Sun
float sunR = size * 0.1f;
drawList->AddCircleFilled(ImVec2(pos.x + size * 0.75f, pos.y + size * 0.35f), sunR, IM_COL32(255, 220, 100, 255));
}
// Draw a shader icon (code brackets)
void DrawShaderIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float padding = size * 0.15f;
ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding);
ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding);
// Background
drawList->AddRectFilled(tl, br, color, 3.0f);
// Code lines
ImU32 lineColor = IM_COL32(255, 255, 255, 180);
float lineY = pos.y + size * 0.35f;
float lineH = size * 0.08f;
float lineSpacing = size * 0.15f;
drawList->AddRectFilled(ImVec2(pos.x + size * 0.25f, lineY), ImVec2(pos.x + size * 0.7f, lineY + lineH), lineColor);
lineY += lineSpacing;
drawList->AddRectFilled(ImVec2(pos.x + size * 0.3f, lineY), ImVec2(pos.x + size * 0.8f, lineY + lineH), lineColor);
lineY += lineSpacing;
drawList->AddRectFilled(ImVec2(pos.x + size * 0.25f, lineY), ImVec2(pos.x + size * 0.55f, lineY + lineH), lineColor);
}
// Draw an audio icon (speaker/waveform)
void DrawAudioIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
// Speaker body
float spkW = size * 0.25f;
float spkH = size * 0.3f;
float cx = pos.x + size * 0.35f;
float cy = pos.y + size * 0.5f;
drawList->AddRectFilled(
ImVec2(cx - spkW * 0.5f, cy - spkH * 0.5f),
ImVec2(cx + spkW * 0.5f, cy + spkH * 0.5f),
color
);
// Speaker cone
drawList->AddTriangleFilled(
ImVec2(cx + spkW * 0.5f, cy - spkH * 0.5f),
ImVec2(cx + spkW * 0.5f, cy + spkH * 0.5f),
ImVec2(cx + spkW * 1.2f, cy + spkH),
color
);
drawList->AddTriangleFilled(
ImVec2(cx + spkW * 0.5f, cy - spkH * 0.5f),
ImVec2(cx + spkW * 1.2f, cy - spkH),
ImVec2(cx + spkW * 1.2f, cy + spkH),
color
);
// Sound waves
ImU32 waveColor = IM_COL32(255, 255, 255, 150);
float waveX = cx + spkW * 1.5f;
drawList->AddBezierQuadratic(
ImVec2(waveX, cy - size * 0.15f),
ImVec2(waveX + size * 0.1f, cy),
ImVec2(waveX, cy + size * 0.15f),
waveColor, 2.0f
);
waveX += size * 0.12f;
drawList->AddBezierQuadratic(
ImVec2(waveX, cy - size * 0.22f),
ImVec2(waveX + size * 0.12f, cy),
ImVec2(waveX, cy + size * 0.22f),
waveColor, 2.0f
);
}
// Draw a generic file icon
void DrawFileIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float w = size * 0.7f;
float h = size * 0.9f;
float offsetX = (size - w) * 0.5f;
float offsetY = (size - h) * 0.5f;
float cornerSize = w * 0.25f;
ImVec2 p1 = ImVec2(pos.x + offsetX, pos.y + offsetY);
ImVec2 p2 = ImVec2(pos.x + offsetX + w - cornerSize, pos.y + offsetY);
ImVec2 p3 = ImVec2(pos.x + offsetX + w, pos.y + offsetY + cornerSize);
ImVec2 p4 = ImVec2(pos.x + offsetX + w, pos.y + offsetY + h);
ImVec2 p5 = ImVec2(pos.x + offsetX, pos.y + offsetY + h);
// Main body
drawList->AddQuadFilled(p1, p2, ImVec2(p2.x, p4.y), p5, color);
drawList->AddTriangleFilled(p2, p3, ImVec2(p2.x, p3.y), color);
drawList->AddRectFilled(ImVec2(p2.x, p3.y), p4, color);
// Corner fold
drawList->AddTriangleFilled(p2, ImVec2(p2.x, p3.y), p3, IM_COL32(255, 255, 255, 50));
}
// Draw a script/code icon
void DrawScriptIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
float padding = size * 0.12f;
ImVec2 tl = ImVec2(pos.x + padding, pos.y + padding);
ImVec2 br = ImVec2(pos.x + size - padding, pos.y + size - padding);
// Background
drawList->AddRectFilled(tl, br, color, 3.0f);
// Brackets < >
ImU32 bracketColor = IM_COL32(255, 255, 255, 200);
float cx = pos.x + size * 0.5f;
float cy = pos.y + size * 0.5f;
float bSize = size * 0.2f;
// Left bracket <
drawList->AddLine(ImVec2(cx - bSize * 0.5f, cy - bSize), ImVec2(cx - bSize * 1.5f, cy), bracketColor, 2.5f);
drawList->AddLine(ImVec2(cx - bSize * 1.5f, cy), ImVec2(cx - bSize * 0.5f, cy + bSize), bracketColor, 2.5f);
// Right bracket >
drawList->AddLine(ImVec2(cx + bSize * 0.5f, cy - bSize), ImVec2(cx + bSize * 1.5f, cy), bracketColor, 2.5f);
drawList->AddLine(ImVec2(cx + bSize * 1.5f, cy), ImVec2(cx + bSize * 0.5f, cy + bSize), bracketColor, 2.5f);
}
// Draw a text icon
void DrawTextIcon(ImDrawList* drawList, ImVec2 pos, float size, ImU32 color) {
DrawFileIcon(drawList, pos, size, color);
// Text lines
ImU32 lineColor = IM_COL32(255, 255, 255, 150);
float startX = pos.x + size * 0.25f;
float endX = pos.x + size * 0.65f;
float lineY = pos.y + size * 0.4f;
float lineH = size * 0.06f;
float spacing = size * 0.12f;
for (int i = 0; i < 3; i++) {
float w = (i == 1) ? (endX - startX) * 0.7f : (endX - startX);
drawList->AddRectFilled(ImVec2(startX, lineY), ImVec2(startX + w, lineY + lineH), lineColor);
lineY += spacing;
}
}
void DrawIcon(ImDrawList* drawList, FileCategory category, ImVec2 pos, float size, ImU32 color) {
switch (category) {
case FileCategory::Folder: DrawFolderIcon(drawList, pos, size, color); break;
case FileCategory::Scene: DrawSceneIcon(drawList, pos, size, color); break;
case FileCategory::Model: DrawModelIcon(drawList, pos, size, color); break;
case FileCategory::Material:DrawShaderIcon(drawList, pos, size, color); break;
case FileCategory::Texture: DrawTextureIcon(drawList, pos, size, color); break;
case FileCategory::Shader: DrawShaderIcon(drawList, pos, size, color); break;
case FileCategory::Script: DrawScriptIcon(drawList, pos, size, color); break;
case FileCategory::Audio: DrawAudioIcon(drawList, pos, size, color); break;
case FileCategory::Text: DrawTextIcon(drawList, pos, size, color); break;
default: DrawFileIcon(drawList, pos, size, color); break;
}
}
}
void Engine::renderFileBrowserPanel() {
ImGui::Begin("Project", &showFileBrowser);
ImGuiStyle& style = ImGui::GetStyle();
ImVec4 toolbarBg = style.Colors[ImGuiCol_MenuBarBg];
toolbarBg.x = std::min(toolbarBg.x + 0.02f, 1.0f);
toolbarBg.y = std::min(toolbarBg.y + 0.02f, 1.0f);
toolbarBg.z = std::min(toolbarBg.z + 0.02f, 1.0f);
if (fileBrowser.needsRefresh) {
fileBrowser.refresh();
}
// Get colors for categories
auto getCategoryColor = [](FileCategory cat) -> ImU32 {
switch (cat) {
case FileCategory::Folder: return IM_COL32(255, 200, 80, 255); // Yellow/orange
case FileCategory::Scene: return IM_COL32(100, 180, 255, 255); // Blue
case FileCategory::Model: return IM_COL32(100, 220, 140, 255); // Green
case FileCategory::Material:return IM_COL32(220, 200, 120, 255); // Gold
case FileCategory::Texture: return IM_COL32(220, 130, 220, 255); // Purple/pink
case FileCategory::Shader: return IM_COL32(255, 140, 90, 255); // Orange
case FileCategory::Script: return IM_COL32(130, 200, 255, 255); // Light blue
case FileCategory::Audio: return IM_COL32(255, 180, 100, 255); // Warm orange
case FileCategory::Text: return IM_COL32(180, 180, 180, 255); // Gray
default: return IM_COL32(150, 150, 150, 255); // Dark gray
}
};
ImGui::PushStyleColor(ImGuiCol_ChildBg, toolbarBg);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 3.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(5.0f, 3.0f));
ImGui::BeginChild("ProjectToolbar", ImVec2(0, 44), true, ImGuiWindowFlags_NoScrollbar);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 3.0f));
bool canGoBack = fileBrowser.historyIndex > 0;
bool canGoForward = fileBrowser.historyIndex < (int)fileBrowser.pathHistory.size() - 1;
bool canGoUp = fileBrowser.currentPath != fileBrowser.projectRoot &&
fileBrowser.currentPath.has_parent_path();
ImGui::BeginDisabled(!canGoBack);
ImGui::Button("<##Back", ImVec2(26, 0));
if (ImGui::IsItemActivated()) fileBrowser.navigateBack();
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Back");
ImGui::SameLine();
ImGui::BeginDisabled(!canGoForward);
ImGui::Button(">##Forward", ImVec2(26, 0));
if (ImGui::IsItemActivated()) fileBrowser.navigateForward();
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Forward");
ImGui::SameLine();
ImGui::BeginDisabled(!canGoUp);
ImGui::Button("^##Up", ImVec2(26, 0));
if (ImGui::IsItemActivated()) fileBrowser.navigateUp();
ImGui::EndDisabled();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("Up one folder");
ImGui::PopStyleVar();
ImGui::SameLine();
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.3f, 0.3f, 0.5f));
fs::path relativePath;
if (fileBrowser.projectRoot.empty()) {
relativePath = fileBrowser.currentPath.filename();
} else {
try {
relativePath = fs::relative(fileBrowser.currentPath, fileBrowser.projectRoot);
} catch (...) {
relativePath = fileBrowser.currentPath.filename();
}
}
std::vector<fs::path> pathParts;
fs::path accumulated = fileBrowser.projectRoot;
pathParts.push_back(fileBrowser.projectRoot);
for (const auto& part : relativePath) {
if (part != ".") {
accumulated /= part;
pathParts.push_back(accumulated);
}
}
struct Breadcrumb {
std::string label;
fs::path target;
};
std::vector<Breadcrumb> crumbs;
if (pathParts.size() <= 4) {
for (size_t i = 0; i < pathParts.size(); ++i) {
std::string name = (i == 0) ? "Project" : pathParts[i].filename().string();
crumbs.push_back({name, pathParts[i]});
}
} else {
crumbs.push_back({"Project", pathParts.front()});
crumbs.push_back({"..", pathParts[pathParts.size() - 3]});
crumbs.push_back({pathParts[pathParts.size() - 2].filename().string(), pathParts[pathParts.size() - 2]});
crumbs.push_back({pathParts.back().filename().string(), pathParts.back()});
}
for (size_t i = 0; i < crumbs.size(); i++) {
ImGui::PushID(static_cast<int>(i));
if (ImGui::SmallButton(crumbs[i].label.c_str())) {
fileBrowser.navigateTo(crumbs[i].target);
}
ImGui::PopID();
if (i < crumbs.size() - 1) {
ImGui::SameLine(0, 2);
ImGui::TextDisabled("/");
ImGui::SameLine(0, 2);
}
}
ImGui::PopStyleColor(2);
ImGui::SameLine();
ImGui::SetNextItemWidth(140);
if (ImGui::InputTextWithHint("##Search", "Search...", fileBrowserSearch, sizeof(fileBrowserSearch))) {
fileBrowser.searchFilter = fileBrowserSearch;
fileBrowser.needsRefresh = true;
}
ImGui::SameLine();
bool isGridMode = fileBrowser.viewMode == FileBrowserViewMode::Grid;
if (isGridMode) {
ImGui::TextDisabled("Size");
ImGui::SameLine();
ImGui::SetNextItemWidth(90);
ImGui::SliderFloat("##IconScale", &fileBrowserIconScale, 0.6f, 2.0f, "%.1fx");
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Icon Size: %.1fx", fileBrowserIconScale);
ImGui::SameLine();
}
if (ImGui::Button(isGridMode ? "Grid" : "List", ImVec2(54, 0))) {
fileBrowser.viewMode = isGridMode ? FileBrowserViewMode::List : FileBrowserViewMode::Grid;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip(isGridMode ? "Switch to List View" : "Switch to Grid View");
ImGui::SameLine();
if (ImGui::Button("Refresh", ImVec2(68, 0))) {
fileBrowser.needsRefresh = true;
}
ImGui::SameLine();
if (ImGui::Button("New Mat", ImVec2(78, 0))) {
fs::path target = fileBrowser.currentPath / "NewMaterial.mat";
int counter = 1;
while (fs::exists(target)) {
target = fileBrowser.currentPath / ("NewMaterial" + std::to_string(counter++) + ".mat");
}
SceneObject temp("Material", ObjectType::Cube, -1);
temp.materialPath = target.string();
saveMaterialToFile(temp);
fileBrowser.needsRefresh = true;
}
ImGui::EndChild();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
ImGui::Spacing();
// === FILE CONTENT AREA ===
ImVec4 contentBg = style.Colors[ImGuiCol_WindowBg];
contentBg.x = std::min(contentBg.x + 0.01f, 1.0f);
contentBg.y = std::min(contentBg.y + 0.01f, 1.0f);
contentBg.z = std::min(contentBg.z + 0.01f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_ChildBg, contentBg);
ImGui::BeginChild("FileContent", ImVec2(0, 0), true);
ImDrawList* drawList = ImGui::GetWindowDrawList();
if (fileBrowser.viewMode == FileBrowserViewMode::Grid) {
float baseIconSize = 64.0f;
float iconSize = baseIconSize * fileBrowserIconScale;
float padding = 8.0f * fileBrowserIconScale;
float textHeight = 32.0f; // Space for filename text
float cellWidth = iconSize + padding * 2;
float cellHeight = iconSize + padding * 2 + textHeight;
float windowWidth = ImGui::GetContentRegionAvail().x;
int columns = std::max(1, (int)((windowWidth + padding) / (cellWidth + padding)));
// Use a table for consistent grid layout
if (ImGui::BeginTable("FileGrid", columns, ImGuiTableFlags_NoPadInnerX)) {
for (int i = 0; i < (int)fileBrowser.entries.size(); i++) {
const auto& entry = fileBrowser.entries[i];
std::string filename = entry.path().filename().string();
FileCategory category = fileBrowser.getFileCategory(entry);
bool isSelected = fileBrowser.selectedFile == entry.path();
ImGui::TableNextColumn();
ImGui::PushID(i);
// Cell content area
ImVec2 cellStart = ImGui::GetCursorScreenPos();
ImVec2 cellEnd(cellStart.x + cellWidth, cellStart.y + cellHeight);
// Invisible button for the entire cell
if (ImGui::InvisibleButton("##cell", ImVec2(cellWidth, cellHeight))) {
fileBrowser.selectedFile = entry.path();
}
bool hovered = ImGui::IsItemHovered();
bool doubleClicked = hovered && ImGui::IsMouseDoubleClicked(0);
// Draw background
ImU32 bgColor = isSelected ? IM_COL32(70, 110, 160, 200) :
(hovered ? IM_COL32(60, 65, 75, 180) : IM_COL32(0, 0, 0, 0));
if (bgColor != IM_COL32(0, 0, 0, 0)) {
drawList->AddRectFilled(cellStart, cellEnd, bgColor, 6.0f);
}
// Draw border on selection
if (isSelected) {
drawList->AddRect(cellStart, cellEnd, IM_COL32(100, 150, 220, 255), 6.0f, 0, 2.0f);
}
// Draw icon centered in cell
ImVec2 iconPos(
cellStart.x + (cellWidth - iconSize) * 0.5f,
cellStart.y + padding
);
FileIcons::DrawIcon(drawList, category, iconPos, iconSize, getCategoryColor(category));
// Draw filename below icon (centered, with wrapping)
std::string displayName = filename;
float maxTextWidth = cellWidth - 4;
// Truncate if too long
ImVec2 textSize = ImGui::CalcTextSize(displayName.c_str());
if (textSize.x > maxTextWidth) {
while (displayName.length() > 3) {
displayName.pop_back();
if (ImGui::CalcTextSize((displayName + "...").c_str()).x <= maxTextWidth) {
break;
}
}
displayName += "...";
textSize = ImGui::CalcTextSize(displayName.c_str());
}
ImVec2 textPos(
cellStart.x + (cellWidth - textSize.x) * 0.5f,
cellStart.y + padding + iconSize + 4
);
// Text with subtle shadow for readability
drawList->AddText(ImVec2(textPos.x + 1, textPos.y + 1), IM_COL32(0, 0, 0, 100), displayName.c_str());
drawList->AddText(textPos, IM_COL32(230, 230, 230, 255), displayName.c_str());
// Handle double click
if (doubleClicked) {
if (entry.is_directory()) {
fileBrowser.navigateTo(entry.path());
} else if (fileBrowser.isModelFile(entry)) {
bool isObj = fileBrowser.isOBJFile(entry);
std::string defaultName = entry.path().stem().string();
if (isObj) {
pendingOBJPath = entry.path().string();
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
showImportOBJDialog = true;
} else {
pendingModelPath = entry.path().string();
strncpy(importModelName, defaultName.c_str(), sizeof(importModelName) - 1);
showImportModelDialog = true;
}
} else if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
if (SceneObject* sel = getSelectedObject()) {
sel->materialPath = entry.path().string();
loadMaterialFromFile(*sel);
}
} else if (fileBrowser.isSceneFile(entry)) {
std::string sceneName = entry.path().stem().string();
loadScene(sceneName);
logToConsole("Loaded scene: " + sceneName);
}
}
// Context menu
if (ImGui::BeginPopupContextItem("FileContextMenu")) {
if (ImGui::MenuItem("Open")) {
if (entry.is_directory()) {
fileBrowser.navigateTo(entry.path());
} else if (fileBrowser.isSceneFile(entry)) {
std::string sceneName = entry.path().stem().string();
loadScene(sceneName);
}
}
if (fileBrowser.isModelFile(entry)) {
bool isObj = fileBrowser.isOBJFile(entry);
if (ImGui::MenuItem("Import to Scene")) {
std::string defaultName = entry.path().stem().string();
if (isObj) {
pendingOBJPath = entry.path().string();
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
showImportOBJDialog = true;
} else {
pendingModelPath = entry.path().string();
strncpy(importModelName, defaultName.c_str(), sizeof(importModelName) - 1);
showImportModelDialog = true;
}
}
if (ImGui::MenuItem("Quick Import")) {
if (isObj) {
importOBJToScene(entry.path().string(), "");
} else {
importModelToScene(entry.path().string(), "");
}
}
if (ImGui::MenuItem("Convert to Raw Mesh")) {
convertModelToRawMesh(entry.path().string());
}
}
if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
if (ImGui::MenuItem("Apply to Selected")) {
if (SceneObject* sel = getSelectedObject()) {
sel->materialPath = entry.path().string();
loadMaterialFromFile(*sel);
}
}
}
if (fileBrowser.getFileCategory(entry) == FileCategory::Script) {
if (ImGui::MenuItem("Compile Script")) {
compileScriptFile(entry.path());
}
}
ImGui::Separator();
if (ImGui::MenuItem("Show in Explorer")) {
#ifdef _WIN32
std::string cmd = "explorer \"" + entry.path().parent_path().string() + "\"";
system(cmd.c_str());
#elif __linux__
std::string cmd = "xdg-open \"" + entry.path().parent_path().string() + "\"";
system(cmd.c_str());
#endif
}
ImGui::EndPopup();
}
ImGui::PopID();
}
ImGui::EndTable();
}
} else {
// List View
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 2));
for (int i = 0; i < (int)fileBrowser.entries.size(); i++) {
const auto& entry = fileBrowser.entries[i];
std::string filename = entry.path().filename().string();
FileCategory category = fileBrowser.getFileCategory(entry);
bool isSelected = fileBrowser.selectedFile == entry.path();
ImGui::PushID(i);
// Selectable row
if (ImGui::Selectable("##row", isSelected, ImGuiSelectableFlags_AllowDoubleClick, ImVec2(0, 20))) {
fileBrowser.selectedFile = entry.path();
if (ImGui::IsMouseDoubleClicked(0)) {
if (entry.is_directory()) {
fileBrowser.navigateTo(entry.path());
} else if (fileBrowser.isModelFile(entry)) {
bool isObj = fileBrowser.isOBJFile(entry);
std::string defaultName = entry.path().stem().string();
if (isObj) {
pendingOBJPath = entry.path().string();
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
showImportOBJDialog = true;
} else {
pendingModelPath = entry.path().string();
strncpy(importModelName, defaultName.c_str(), sizeof(importModelName) - 1);
showImportModelDialog = true;
}
} else if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
if (SceneObject* sel = getSelectedObject()) {
sel->materialPath = entry.path().string();
loadMaterialFromFile(*sel);
}
} else if (fileBrowser.isSceneFile(entry)) {
std::string sceneName = entry.path().stem().string();
loadScene(sceneName);
logToConsole("Loaded scene: " + sceneName);
}
}
}
// Context menu
if (ImGui::BeginPopupContextItem("FileContextMenu")) {
if (ImGui::MenuItem("Open")) {
if (entry.is_directory()) {
fileBrowser.navigateTo(entry.path());
} else if (fileBrowser.isSceneFile(entry)) {
std::string sceneName = entry.path().stem().string();
loadScene(sceneName);
}
}
if (fileBrowser.isModelFile(entry)) {
bool isObj = fileBrowser.isOBJFile(entry);
std::string ext = entry.path().extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
bool isRaw = ext == ".rmesh";
if (ImGui::MenuItem("Import to Scene")) {
std::string defaultName = entry.path().stem().string();
if (isObj) {
pendingOBJPath = entry.path().string();
strncpy(importOBJName, defaultName.c_str(), sizeof(importOBJName) - 1);
showImportOBJDialog = true;
} else {
pendingModelPath = entry.path().string();
strncpy(importModelName, defaultName.c_str(), sizeof(importModelName) - 1);
showImportModelDialog = true;
}
}
if (ImGui::MenuItem("Quick Import")) {
if (isObj) {
importOBJToScene(entry.path().string(), "");
} else {
importModelToScene(entry.path().string(), "");
}
}
if (!isRaw && ImGui::MenuItem("Convert to Raw Mesh")) {
convertModelToRawMesh(entry.path().string());
}
}
if (fileBrowser.getFileCategory(entry) == FileCategory::Material) {
if (ImGui::MenuItem("Apply to Selected")) {
if (SceneObject* sel = getSelectedObject()) {
sel->materialPath = entry.path().string();
loadMaterialFromFile(*sel);
}
}
}
if (fileBrowser.getFileCategory(entry) == FileCategory::Script) {
if (ImGui::MenuItem("Compile Script")) {
compileScriptFile(entry.path());
}
}
ImGui::Separator();
if (ImGui::MenuItem("Show in Explorer")) {
#ifdef _WIN32
std::string cmd = "explorer \"" + entry.path().parent_path().string() + "\"";
system(cmd.c_str());
#elif __linux__
std::string cmd = "xdg-open \"" + entry.path().parent_path().string() + "\"";
system(cmd.c_str());
#endif
}
ImGui::EndPopup();
}
// Draw icon inline
ImGui::SameLine(4);
ImVec2 iconPos = ImGui::GetCursorScreenPos();
iconPos.y -= 2;
FileIcons::DrawIcon(drawList, category, iconPos, 16, getCategoryColor(category));
ImGui::SameLine(26);
// Color-coded filename
ImVec4 textColor;
switch (category) {
case FileCategory::Folder: textColor = ImVec4(1.0f, 0.85f, 0.4f, 1.0f); break;
case FileCategory::Scene: textColor = ImVec4(0.5f, 0.75f, 1.0f, 1.0f); break;
case FileCategory::Model: textColor = ImVec4(0.5f, 0.9f, 0.6f, 1.0f); break;
case FileCategory::Material:textColor = ImVec4(0.95f, 0.8f, 0.45f, 1.0f); break;
case FileCategory::Texture: textColor = ImVec4(0.9f, 0.6f, 0.9f, 1.0f); break;
default: textColor = ImVec4(0.85f, 0.85f, 0.85f, 1.0f); break;
}
ImGui::TextColored(textColor, "%s", filename.c_str());
ImGui::PopID();
}
ImGui::PopStyleVar();
}
ImGui::EndChild();
ImGui::PopStyleColor();
ImGui::End();
}

View File

@@ -0,0 +1,741 @@
#include "Engine.h"
#include "ModelLoader.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <cstdlib>
#include <cfloat>
#include <cmath>
#include <functional>
#include <sstream>
#include <unordered_set>
#include <optional>
#include <future>
#include <chrono>
#include <future>
#ifdef _WIN32
#include <shlobj.h>
#endif
namespace ImGui {
// Animated progress bar that keeps circles moving while work happens in the background.
bool BufferingBar(const char* label, float value, const ImVec2& size_arg, const ImU32& bg_col, const ImU32& fg_col) {
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return false;
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
ImVec2 pos = window->DC.CursorPos;
ImVec2 size = size_arg;
size.x -= style.FramePadding.x * 2;
const ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y));
ItemSize(bb, style.FramePadding.y);
if (!ItemAdd(bb, id))
return false;
const float circleStart = size.x * 0.7f;
const float circleEnd = size.x;
const float circleWidth = circleEnd - circleStart;
window->DrawList->AddRectFilled(bb.Min, ImVec2(pos.x + circleStart, bb.Max.y), bg_col);
window->DrawList->AddRectFilled(bb.Min, ImVec2(pos.x + circleStart * value, bb.Max.y), fg_col);
const float t = g.Time;
const float r = size.y / 2;
const float speed = 1.5f;
const float a = speed * 0;
const float b = speed * 0.333f;
const float c = speed * 0.666f;
const float o1 = (circleWidth + r) * (t + a - speed * (int)((t + a) / speed)) / speed;
const float o2 = (circleWidth + r) * (t + b - speed * (int)((t + b) / speed)) / speed;
const float o3 = (circleWidth + r) * (t + c - speed * (int)((t + c) / speed)) / speed;
window->DrawList->AddCircleFilled(ImVec2(pos.x + circleEnd - o1, bb.Min.y + r), r, bg_col);
window->DrawList->AddCircleFilled(ImVec2(pos.x + circleEnd - o2, bb.Min.y + r), r, bg_col);
window->DrawList->AddCircleFilled(ImVec2(pos.x + circleEnd - o3, bb.Min.y + r), r, bg_col);
return true;
}
// Simple loading spinner for indeterminate tasks.
bool Spinner(const char* label, float radius, int thickness, const ImU32& color) {
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return false;
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
ImVec2 pos = window->DC.CursorPos;
ImVec2 size((radius) * 2, (radius + style.FramePadding.y) * 2);
const ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y));
ItemSize(bb, style.FramePadding.y);
if (!ItemAdd(bb, id))
return false;
window->DrawList->PathClear();
int num_segments = 30;
int start = abs(ImSin(g.Time * 1.8f) * (num_segments - 5));
const float a_min = IM_PI * 2.0f * ((float)start) / (float)num_segments;
const float a_max = IM_PI * 2.0f * ((float)num_segments - 3) / (float)num_segments;
const ImVec2 centre = ImVec2(pos.x + radius, pos.y + radius + style.FramePadding.y);
for (int i = 0; i < num_segments; i++) {
const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min);
window->DrawList->PathLineTo(ImVec2(centre.x + ImCos(a + g.Time * 8) * radius,
centre.y + ImSin(a + g.Time * 8) * radius));
}
window->DrawList->PathStroke(color, false, thickness);
return true;
}
} // namespace ImGui
namespace {
struct PackageTaskResult {
bool success = false;
std::string message;
};
struct PackageTaskState {
bool active = false;
float startTime = 0.0f;
std::string label;
std::future<PackageTaskResult> future;
};
} // namespace
void Engine::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");
#else
system("xdg-open 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
{
float availWidth = ImGui::GetContentRegionAvail().x;
for (size_t i = 0; i < projectManager.recentProjects.size(); ++i)
{
const auto& rp = projectManager.recentProjects[i];
ImGui::PushID(static_cast<int>(i));
char label[512];
std::snprintf(label, sizeof(label), "%s\n%s",
rp.name.c_str(), rp.path.c_str());
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.20f, 0.30f, 0.45f, 0.40f));
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(0.25f, 0.38f, 0.55f, 0.70f));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(0.20f, 0.35f, 0.60f, 0.90f));
bool selected = ImGui::Selectable(
label,
false,
ImGuiSelectableFlags_AllowDoubleClick,
ImVec2(availWidth, 48.0f)
);
ImGui::PopStyleColor(3);
// Dummy to extend window bounds properly
ImGui::Dummy(ImVec2(0, 0));
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 0.6.8");
ImGui::EndChild();
}
ImGui::End();
ImGui::PopStyleColor(2);
ImGui::PopStyleVar(3);
if (projectManager.showNewProjectDialog)
renderNewProjectDialog();
if (projectManager.showOpenProjectDialog)
renderOpenProjectDialog();
}
void Engine::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();
if (ImGui::Button("Choose Pipeline Mode")) {
}
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 Engine::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 {
OpenProjectPath(projectManager.openProjectPath);
if (!projectManager.errorMessage.empty()) {
// Error handled in OpenProjectPath
} else {
projectManager.showOpenProjectDialog = false;
}
}
}
}
ImGui::End();
}
void Engine::renderProjectBrowserPanel() {
ImVec4 headerCol = ImVec4(0.20f, 0.27f, 0.36f, 1.0f);
ImVec4 headerColActive = ImVec4(0.24f, 0.34f, 0.46f, 1.0f);
ImGui::PushStyleColor(ImGuiCol_Header, headerCol);
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, headerColActive);
ImGui::PushStyleColor(ImGuiCol_HeaderActive, headerColActive);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8.0f, 5.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6.0f, 4.0f));
ImGui::Begin("Project Settings", &showProjectBrowser);
if (!projectManager.currentProject.isLoaded) {
ImGui::TextDisabled("No project loaded");
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(3);
return;
}
ImGui::TextColored(ImVec4(0.4f, 0.7f, 0.95f, 1.0f), "%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();
static int selectedTab = 0;
const char* tabs[] = { "Scenes", "Packages", "Assets" };
ImGui::BeginChild("SettingsNav", ImVec2(180, 0), true);
for (int i = 0; i < 3; ++i) {
if (ImGui::Selectable(tabs[i], selectedTab == i, 0, ImVec2(0, 32))) {
selectedTab = i;
}
}
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("SettingsBody", ImVec2(0, 0), false);
if (selectedTab == 0) {
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");
}
} else if (selectedTab == 1) {
static PackageTaskState packageTask;
static std::string packageStatus;
static char gitUrlBuf[256] = "";
static char gitNameBuf[128] = "";
static char gitIncludeBuf[128] = "include";
auto pollPackageTask = [&]() {
if (!packageTask.active) return;
if (!packageTask.future.valid()) {
packageTask.active = false;
return;
}
auto state = packageTask.future.wait_for(std::chrono::milliseconds(0));
if (state == std::future_status::ready) {
PackageTaskResult result = packageTask.future.get();
packageStatus = result.message;
packageTask.active = false;
packageTask.future = std::future<PackageTaskResult>();
}
};
auto startPackageTask = [&](const char* label, std::function<PackageTaskResult()> fn) {
if (packageTask.active) return;
packageTask.active = true;
packageTask.label = label;
packageTask.startTime = static_cast<float>(ImGui::GetTime());
packageTask.future = std::async(std::launch::async, std::move(fn));
};
pollPackageTask();
if (!packageStatus.empty()) {
ImGui::TextWrapped("%s", packageStatus.c_str());
}
if (packageTask.active) {
const ImU32 col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
const ImU32 bg = ImGui::GetColorU32(ImGuiCol_Button);
float elapsed = static_cast<float>(ImGui::GetTime()) - packageTask.startTime;
float phase = std::fmod(elapsed * 0.25f, 1.0f);
ImGui::Separator();
ImGui::Text("%s", packageTask.label.c_str());
ImGui::BufferingBar("##pkg_buffer", phase, ImVec2(ImGui::GetContentRegionAvail().x, 6.0f), bg, col);
ImGui::Spinner("##pkg_spinner", 10.0f, 4, col);
}
ImGui::BeginDisabled(packageTask.active);
ImGui::TextDisabled("Add package from Git");
ImGui::InputTextWithHint("URL", "https://github.com/user/repo.git", gitUrlBuf, sizeof(gitUrlBuf));
ImGui::InputTextWithHint("Name (optional)", "use repo name", gitNameBuf, sizeof(gitNameBuf));
ImGui::InputTextWithHint("Include dir", "include", gitIncludeBuf, sizeof(gitIncludeBuf));
if (ImGui::Button("Add as submodule")) {
std::string url = gitUrlBuf;
std::string name = gitNameBuf;
std::string include = gitIncludeBuf;
startPackageTask("Installing package...", [this, url, name, include]() {
PackageTaskResult result;
std::string newId;
if (packageManager.installGitPackage(url, name, include, newId)) {
result.success = true;
result.message = "Installed package: " + newId;
} else {
result.message = packageManager.getLastError();
}
return result;
});
}
ImGui::EndDisabled();
ImGui::Separator();
ImGui::TextDisabled("Installed packages");
if (packageTask.active) {
ImGui::TextDisabled("Working...");
} else {
const auto& registry = packageManager.getRegistry();
const auto& installedIds = packageManager.getInstalled();
if (installedIds.empty()) {
ImGui::TextDisabled("None installed");
} else {
for (const auto& id : installedIds) {
const PackageInfo* pkg = nullptr;
for (const auto& p : registry) {
if (p.id == id) { pkg = &p; break; }
}
if (!pkg) continue;
ImGui::PushID(pkg->id.c_str());
ImGui::Separator();
ImGui::Text("%s", pkg->name.c_str());
ImGui::TextDisabled("%s", pkg->description.c_str());
if (!pkg->external) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.4f, 0.7f, 0.95f, 1.0f), "[bundled]");
ImGui::PopID();
continue;
}
ImGui::TextDisabled("Path: %s", pkg->localPath.string().c_str());
ImGui::TextDisabled("Git: %s", pkg->gitUrl.c_str());
if (ImGui::Button("Check updates")) {
std::string id = pkg->id;
startPackageTask("Checking package status...", [this, id]() {
PackageTaskResult result;
std::string status;
if (packageManager.checkGitStatus(id, status)) {
result.success = true;
result.message = status;
} else {
result.message = packageManager.getLastError();
}
return result;
});
}
ImGui::SameLine();
if (ImGui::Button("Update")) {
std::string id = pkg->id;
std::string name = pkg->name;
startPackageTask("Updating package...", [this, id, name]() {
PackageTaskResult result;
std::string log;
if (packageManager.updateGitPackage(id, log)) {
result.success = true;
result.message = "Updated " + name + "\n" + log;
} else {
result.message = packageManager.getLastError();
}
return result;
});
}
ImGui::SameLine();
if (ImGui::Button("Uninstall")) {
std::string id = pkg->id;
std::string name = pkg->name;
startPackageTask("Removing package...", [this, id, name]() {
PackageTaskResult result;
if (packageManager.remove(id)) {
result.success = true;
result.message = "Removed " + name;
} else {
result.message = packageManager.getLastError();
}
return result;
});
}
ImGui::PopID();
}
}
}
} else if (selectedTab == 2) {
ImGui::TextDisabled("Loaded OBJ Meshes");
const auto& meshesObj = g_objLoader.getAllMeshes();
if (meshesObj.empty()) {
ImGui::TextDisabled("No meshes loaded");
ImGui::TextDisabled("Import .obj files from File Browser");
} else {
for (size_t i = 0; i < meshesObj.size(); i++) {
const auto& mesh = meshesObj[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();
}
}
}
ImGui::Separator();
ImGui::TextDisabled("Loaded Models (Assimp)");
const auto& meshesAssimp = getModelLoader().getAllMeshes();
if (meshesAssimp.empty()) {
ImGui::TextDisabled("No models loaded");
ImGui::TextDisabled("Import FBX/GLTF/other supported models from File Browser");
} else {
for (size_t i = 0; i < meshesAssimp.size(); i++) {
const auto& mesh = meshesAssimp[i];
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_SpanAvailWidth |
ImGuiTreeNodeFlags_NoTreePushOnOpen;
ImGui::TreeNodeEx((void*)(intptr_t)(10000 + i), flags, "[A] %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();
}
}
}
}
ImGui::EndChild();
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::PopStyleColor(3);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@
#include "ModelLoader.h"
#include <iostream>
#include <fstream>
#include <unordered_set>
#include <unordered_map>
namespace {
struct MaterialFileData {
@@ -394,6 +396,7 @@ void Engine::run() {
if (showProjectBrowser) renderProjectBrowserPanel();
}
renderScriptEditorWindows();
renderViewport();
if (showGameViewport) renderGameViewportWindow();
renderDialogs();
@@ -926,6 +929,8 @@ void Engine::OpenProjectPath(const std::string& path) {
fileBrowser.setProjectRoot(projectManager.currentProject.projectPath);
fileBrowser.currentPath = projectManager.currentProject.projectPath;
fileBrowser.needsRefresh = true;
scriptEditorWindowsDirty = true;
scriptEditorWindows.clear();
showLauncher = false;
addConsoleMessage("Opened project: " + projectManager.currentProject.name, ConsoleMessageType::Info);
} else {
@@ -970,6 +975,8 @@ void Engine::createNewProject(const char* name, const char* location) {
fileBrowser.setProjectRoot(projectManager.currentProject.projectPath);
fileBrowser.currentPath = projectManager.currentProject.projectPath;
fileBrowser.needsRefresh = true;
scriptEditorWindowsDirty = true;
scriptEditorWindows.clear();
showLauncher = false;
firstFrame = true;
@@ -1362,6 +1369,119 @@ void Engine::compileScriptFile(const fs::path& scriptPath) {
addConsoleMessage("Compiled script -> " + commands.binaryPath.string(), ConsoleMessageType::Success);
if (!output.compileLog.empty()) addConsoleMessage(output.compileLog, ConsoleMessageType::Info);
if (!output.linkLog.empty()) addConsoleMessage(output.linkLog, ConsoleMessageType::Info);
// Update any ScriptComponents that point to this source with the freshly built binary path.
std::string compiledSource = fs::absolute(scriptPath).lexically_normal().string();
for (auto& obj : sceneObjects) {
for (auto& sc : obj.scripts) {
std::error_code ec;
fs::path scAbs = fs::absolute(sc.path, ec);
std::string scPathNorm = (ec ? fs::path(sc.path) : scAbs).lexically_normal().string();
if (scPathNorm == compiledSource) {
sc.lastBinaryPath = commands.binaryPath.string();
}
}
}
// Refresh scripted editor window registry in case new tabs were added by this build.
scriptEditorWindowsDirty = true;
refreshScriptEditorWindows();
}
void Engine::refreshScriptEditorWindows() {
if (!scriptEditorWindowsDirty) return;
scriptEditorWindowsDirty = false;
if (!projectManager.currentProject.isLoaded) {
scriptEditorWindows.clear();
return;
}
std::unordered_map<std::string, bool> previousState;
for (const auto& entry : scriptEditorWindows) {
previousState[entry.binaryPath.lexically_normal().string()] = entry.open;
}
std::unordered_set<std::string> seen;
std::vector<ScriptEditorWindowEntry> updated;
auto tryAddEntry = [&](const fs::path& binaryPath) {
if (binaryPath.empty() || !fs::exists(binaryPath)) return;
std::string key = binaryPath.lexically_normal().string();
if (!seen.insert(key).second) return;
if (!scriptRuntime.hasEditorWindow(binaryPath)) return;
ScriptEditorWindowEntry entry;
entry.binaryPath = binaryPath;
entry.label = binaryPath.stem().string();
if (entry.label.empty()) entry.label = "ScriptWindow";
auto it = previousState.find(key);
entry.open = (it != previousState.end()) ? it->second : false;
updated.push_back(std::move(entry));
};
for (const auto& obj : sceneObjects) {
for (const auto& sc : obj.scripts) {
fs::path binaryPath;
if (!sc.lastBinaryPath.empty()) {
binaryPath = fs::path(sc.lastBinaryPath);
}
if (binaryPath.empty() || !fs::exists(binaryPath)) {
binaryPath = resolveScriptBinary(sc.path);
}
tryAddEntry(binaryPath);
}
}
// Also scan the configured script output directory for standalone editor tabs.
fs::path configPath = projectManager.currentProject.scriptsConfigPath;
if (configPath.empty()) {
configPath = projectManager.currentProject.projectPath / "Scripts.modu";
}
ScriptBuildConfig config;
std::string error;
if (scriptCompiler.loadConfig(configPath, config, error)) {
fs::path outDir = config.outDir;
if (!outDir.is_absolute()) {
outDir = projectManager.currentProject.projectPath / outDir;
}
std::error_code ec;
if (fs::exists(outDir, ec)) {
for (auto it = fs::recursive_directory_iterator(outDir, ec);
it != fs::recursive_directory_iterator(); ++it) {
if (it->is_directory()) continue;
auto ext = it->path().extension().string();
if (ext == ".so" || ext == ".dll" || ext == ".dylib") {
tryAddEntry(it->path());
}
}
}
}
scriptEditorWindows.swap(updated);
}
void Engine::renderScriptEditorWindows() {
if (scriptEditorWindows.empty()) return;
ScriptContext ctx;
ctx.engine = this;
ctx.object = getSelectedObject();
ctx.script = nullptr;
for (auto& entry : scriptEditorWindows) {
if (!entry.open) continue;
std::string title = entry.label + "###" + entry.binaryPath.string();
if (ImGui::Begin(title.c_str(), &entry.open)) {
scriptRuntime.callEditorWindow(entry.binaryPath, ctx);
}
ImGui::End();
if (!entry.open) {
scriptRuntime.callExitEditorWindow(entry.binaryPath, ctx);
}
}
}
void Engine::setupImGui() {

View File

@@ -78,6 +78,13 @@ private:
bool showSaveSceneAsDialog = false;
char newSceneName[128] = "";
char saveSceneAsName[128] = "";
struct ScriptEditorWindowEntry {
fs::path binaryPath;
std::string label;
bool open = false;
};
std::vector<ScriptEditorWindowEntry> scriptEditorWindows;
bool scriptEditorWindowsDirty = true;
bool rendererInitialized = false;
bool showImportOBJDialog = false;
@@ -159,6 +166,8 @@ private:
void renderGameViewportWindow();
void renderDialogs();
void renderProjectBrowserPanel();
void renderScriptEditorWindows();
void refreshScriptEditorWindows();
Camera makeCameraFromObject(const SceneObject& obj) const;
void compileScriptFile(const fs::path& scriptPath);
void updateScripts(float delta);

File diff suppressed because it is too large Load Diff

View File

@@ -385,6 +385,8 @@ ScriptRuntime::Module* ScriptRuntime::getModule(const fs::path& binaryPath) {
mod.testEditor = reinterpret_cast<TestEditorFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_TestEditor"));
mod.update = reinterpret_cast<UpdateFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_Update"));
mod.tickUpdate = reinterpret_cast<TickUpdateFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "Script_TickUpdate"));
mod.editorRender = reinterpret_cast<EditorRenderFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "RenderEditorWindow"));
mod.editorExit = reinterpret_cast<EditorExitFn>(GetProcAddress(static_cast<HMODULE>(mod.handle), "ExitRenderEditorWindow"));
#else
mod.handle = dlopen(binaryPath.string().c_str(), RTLD_NOW);
if (!mod.handle) {
@@ -398,11 +400,13 @@ ScriptRuntime::Module* ScriptRuntime::getModule(const fs::path& binaryPath) {
mod.testEditor = reinterpret_cast<TestEditorFn>(dlsym(mod.handle, "Script_TestEditor"));
mod.update = reinterpret_cast<UpdateFn>(dlsym(mod.handle, "Script_Update"));
mod.tickUpdate = reinterpret_cast<TickUpdateFn>(dlsym(mod.handle, "Script_TickUpdate"));
mod.editorRender = reinterpret_cast<EditorRenderFn>(dlsym(mod.handle, "RenderEditorWindow"));
mod.editorExit = reinterpret_cast<EditorExitFn>(dlsym(mod.handle, "ExitRenderEditorWindow"));
#if !defined(_WIN32)
{
const char* err = dlerror();
if (err && !mod.inspector && !mod.begin && !mod.spec && !mod.testEditor
&& !mod.update && !mod.tickUpdate) {
&& !mod.update && !mod.tickUpdate && !mod.editorRender && !mod.editorExit) {
lastError = err;
}
}
@@ -410,7 +414,7 @@ ScriptRuntime::Module* ScriptRuntime::getModule(const fs::path& binaryPath) {
#endif
if (!mod.inspector && !mod.begin && !mod.spec && !mod.testEditor
&& !mod.update && !mod.tickUpdate) {
&& !mod.update && !mod.tickUpdate && !mod.editorRender && !mod.editorExit) {
#if defined(_WIN32)
FreeLibrary(static_cast<HMODULE>(mod.handle));
#else
@@ -469,3 +473,22 @@ void ScriptRuntime::unloadAll() {
}
loaded.clear();
}
bool ScriptRuntime::hasEditorWindow(const fs::path& binaryPath) {
Module* mod = getModule(binaryPath);
return mod && mod->editorRender;
}
void ScriptRuntime::callEditorWindow(const fs::path& binaryPath, ScriptContext& ctx) {
Module* mod = getModule(binaryPath);
if (mod && mod->editorRender) {
mod->editorRender(ctx);
}
}
void ScriptRuntime::callExitEditorWindow(const fs::path& binaryPath, ScriptContext& ctx) {
Module* mod = getModule(binaryPath);
if (mod && mod->editorExit) {
mod->editorExit(ctx);
}
}

View File

@@ -78,6 +78,8 @@ public:
using UpdateFn = void(*)(ScriptContext&, float);
using TickUpdateFn = void(*)(ScriptContext&, float);
using InspectorFn = void(*)(ScriptContext&);
using EditorRenderFn = void(*)(ScriptContext&);
using EditorExitFn = void(*)(ScriptContext&);
using IEnumFn = void(*)(ScriptContext&, float);
InspectorFn getInspector(const fs::path& binaryPath);
@@ -85,6 +87,10 @@ public:
bool runSpec, bool runTest);
void unloadAll();
const std::string& getLastError() const { return lastError; }
// Editor extension hooks: load RenderEditorWindow/ExitRenderEditorWindow from a script binary.
bool hasEditorWindow(const fs::path& binaryPath);
void callEditorWindow(const fs::path& binaryPath, ScriptContext& ctx);
void callExitEditorWindow(const fs::path& binaryPath, ScriptContext& ctx);
private:
struct Module {
@@ -95,6 +101,8 @@ private:
TestEditorFn testEditor = nullptr;
UpdateFn update = nullptr;
TickUpdateFn tickUpdate = nullptr;
EditorRenderFn editorRender = nullptr;
EditorExitFn editorExit = nullptr;
std::unordered_set<int> beginCalledObjects;
};
Module* getModule(const fs::path& binaryPath);