diff --git a/.gitignore b/.gitignore index f261df5..9568818 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /build* /cmake-build* /out +/dump diff --git a/CMakeLists.txt b/CMakeLists.txt index cf86f54..6fd5850 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ FetchContent_MakeAvailable(rmlui) # Find other dependencies via vcpkg find_package(glfw3 CONFIG REQUIRED) find_package(freetype CONFIG REQUIRED) +find_package(PNG REQUIRED) # Get the RmlUi source directory for include paths FetchContent_GetProperties(rmlui) @@ -37,7 +38,7 @@ add_executable(mosis-designer ${rmlui_SOURCE_DIR}/Backends/RmlUi_Renderer_GL3.cpp ) target_link_libraries(mosis-designer PUBLIC - RmlUi::RmlUi RmlUi::Lua glfw freetype + RmlUi::RmlUi RmlUi::Lua glfw freetype PNG::PNG ) target_include_directories(mosis-designer PUBLIC ${RMLUI_SOURCE_DIR} diff --git a/main.cpp b/main.cpp index 2a466f8..7499e6a 100644 --- a/main.cpp +++ b/main.cpp @@ -3,11 +3,17 @@ #include #include #include +#include +#include +#include #include #include #include #include #include +#include +#include +#include void load_fonts(const std::filesystem::path& dir) { @@ -20,6 +26,124 @@ void load_fonts(const std::filesystem::path& dir) } } +void DumpElementTree(Rml::Element* element, std::ofstream& out, int depth = 0) +{ + if (!element) return; + + std::string indent(depth * 2, ' '); + std::string id = element->GetId().empty() ? "" : "#" + element->GetId(); + + out << indent << element->GetTagName() << id + << " @ (" << element->GetAbsoluteLeft() << ", " << element->GetAbsoluteTop() << ") " + << element->GetOffsetWidth() << "x" << element->GetOffsetHeight() << "\n"; + + for (int i = 0; i < element->GetNumChildren(); i++) + DumpElementTree(element->GetChild(i), out, depth + 1); +} + +// Offscreen framebuffer for screenshot capture +struct OffscreenFBO { + GLuint fbo = 0; + GLuint texture = 0; + GLuint depth_rbo = 0; + int width = 0; + int height = 0; + + bool create(int w, int h) + { + width = w; + height = h; + + // Create framebuffer + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + + // Create color texture + glGenTextures(1, &texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + 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, texture, 0); + + // Create depth/stencil renderbuffer + glGenRenderbuffers(1, &depth_rbo); + glBindRenderbuffer(GL_RENDERBUFFER, depth_rbo); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, depth_rbo); + + // Check completeness + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + std::println("Failed to create offscreen framebuffer"); + destroy(); + return false; + } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + return true; + } + + void destroy() + { + if (texture) glDeleteTextures(1, &texture); + if (depth_rbo) glDeleteRenderbuffers(1, &depth_rbo); + if (fbo) glDeleteFramebuffers(1, &fbo); + fbo = texture = depth_rbo = 0; + } + + void bind() + { + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glViewport(0, 0, width, height); + } + + std::vector readPixels() + { + std::vector pixels(width * height * 3); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE, pixels.data()); + return pixels; + } +}; + +void SavePNG(const std::filesystem::path& path, const std::vector& pixels, int width, int height) +{ + // Flip vertically (OpenGL origin is bottom-left) + std::vector flipped(width * height * 3); + for (int y = 0; y < height; y++) + { + std::memcpy(&flipped[y * width * 3], + &pixels[(height - 1 - y) * width * 3], + width * 3); + } + + FILE* fp = fopen(path.string().c_str(), "wb"); + if (!fp) + { + std::println("Failed to open {} for writing", path.string()); + return; + } + + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + png_infop info = png_create_info_struct(png); + png_init_io(png, fp); + png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + png_write_info(png, info); + + std::vector rows(height); + for (int y = 0; y < height; y++) + rows[y] = const_cast(&flipped[y * width * 3]); + png_write_image(png, rows.data()); + png_write_end(png, nullptr); + + png_destroy_write_struct(&png, &info); + fclose(fp); + + std::println("Screenshot saved to: {}", path.string()); +} + int main(const int argc, const char* argv[]) { constexpr int window_width = 540; @@ -27,11 +151,12 @@ int main(const int argc, const char* argv[]) if (argc < 2) { - std::println("Usage: mosis-designer file.rml"); + std::println("Usage: mosis-designer file.rml [--dump]"); return EXIT_FAILURE; } const std::filesystem::path file = argv[1]; + const bool dump_mode = (argc >= 3 && std::string(argv[2]) == "--dump"); if (!std::filesystem::exists(file)) { std::println("File does not exist"); @@ -65,6 +190,67 @@ int main(const int argc, const char* argv[]) document->Show(); } + // Dump mode: render and capture screenshot + if (dump_mode) + { + // Create offscreen FBO at window dimensions + OffscreenFBO fbo; + if (!fbo.create(window_width, window_height)) + { + std::println("Failed to create FBO"); + Rml::Shutdown(); + Backend::Shutdown(); + return EXIT_FAILURE; + } + + // First render to default framebuffer to initialize everything + Backend::ProcessEvents(context); + context->Update(); + Backend::BeginFrame(); + context->Render(); + Backend::PresentFrame(); + + // Create dump folder if it doesn't exist + auto dump_folder = std::filesystem::absolute("dump"); + std::filesystem::create_directories(dump_folder); + + // Write element dump + std::ofstream dump_file((dump_folder / "element_dump.txt").string()); + if (dump_file) + { + dump_file << "Window: " << window_width << "x" << window_height << "\n"; + dump_file << "\n=== Element Layout ===\n"; + DumpElementTree(document, dump_file); + dump_file.close(); + } + + // Bind FBO and set up OpenGL state for rendering + fbo.bind(); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + + // Enable blending for proper text rendering + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // Render to FBO + context->Update(); + context->Render(); + + // Read pixels + auto pixels = fbo.readPixels(); + fbo.destroy(); + + SavePNG(dump_folder / "screenshot.png", pixels, window_width, window_height); + + // Restore default framebuffer + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + Rml::Shutdown(); + Backend::Shutdown(); + return EXIT_SUCCESS; + } + const HANDLE hNotif = FindFirstChangeNotification(assets_path.c_str(), TRUE, FILE_NOTIFY_CHANGE_LAST_WRITE); diff --git a/vcpkg.json b/vcpkg.json index 44bc68e..e65a3d5 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -4,6 +4,7 @@ "dependencies": [ "glfw3", "freetype", - "lua" + "lua", + "libpng" ] } \ No newline at end of file