diff --git a/cmake/Dependencies.cmake b/cmake/Dependencies.cmake index 138c8ee..7ffb1d5 100644 --- a/cmake/Dependencies.cmake +++ b/cmake/Dependencies.cmake @@ -101,7 +101,8 @@ add_library(imgui STATIC ${imgui_SOURCE_DIR}/imgui_demo.cpp ${imgui_SOURCE_DIR}/backends/imgui_impl_sdl3.cpp ${imgui_SOURCE_DIR}/backends/imgui_impl_vulkan.cpp - ${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp) + ${imgui_SOURCE_DIR}/backends/imgui_impl_opengl3.cpp + ${imgui_SOURCE_DIR}/backends/imgui_impl_sdlrenderer3.cpp) # (the crash screen) target_include_directories(imgui SYSTEM PUBLIC ${imgui_SOURCE_DIR} ${imgui_SOURCE_DIR}/backends) target_compile_definitions(imgui PUBLIC IMGUI_IMPL_VULKAN_USE_VOLK) target_link_libraries(imgui PUBLIC volk SDL3::SDL3) diff --git a/docs/content.js b/docs/content.js index 4b95b1c..d05ae59 100644 --- a/docs/content.js +++ b/docs/content.js @@ -46,12 +46,18 @@ window.FRY = { ], shots: [ - { img: "img/model.webp", title: "Model", text: "Starter models, a folder of parts, a shape drawn onto a cube's face, rendered with sky and shadows." }, - { img: "img/paint.webp", title: "Paint", text: "A picture pasted on as a sticker: it bends over the cube's edge like a real one." }, - { img: "img/print.webp", title: "Print", text: "The printer's plate, sizes in millimetres, checks and overhangs before you slice." }, - { img: "img/phone.webp", title: "Phone", text: "The phone layout: big buttons along the bottom, panels when you want them, fingers for the view." }, - { img: "img/combine.webp", title: "Combine", text: "A cylinder taken out of a cube, copies in a row in a folder, a plane made thick." }, - { img: "img/crash.webp", title: "Even the crash screen", text: "If something does get burnt, your work comes back from the last autosave." }, + { img: "img/model.webp", title: "Model", text: "Starter models with materials (wood, glossy glass, ceramic), a folder for what's on the table, rendered with sky and shadows while you work." }, + { img: "img/sculpt.webp", title: "Sculpt", text: "A ball pushed into a head with the brushes: a nose raised, eyes dug in, a smile pinched, ears grabbed out. Tab in the Paint tab." }, + { img: "img/materials.webp", title: "Materials", text: "Gold, chrome, wood, rubber, copper, ceramic, steel: picked in Properties, shown in the view, exported with the model." }, + { img: "img/paint.webp", title: "Paint", text: "The logo pasted onto a box as a sticker: drag its corners to fit, Enter to lock it on. The Picture panel shows where it lands." }, + { img: "img/layout.webp", title: "Layout", text: "The box's surface laid out flat: move, turn and size the frames, and the paint follows." }, + { img: "img/render.webp", title: "Render", text: "Sun, sky and ground, brightness, soft shadows and glow, and F12 for a picture of just the view." }, + { img: "img/print.webp", title: "Print", text: "Two gears and an arch on a Prusa's plate: sizes in millimetres, checked to be closed, the arch's overhang flagged, weight and filament." }, + { img: "img/combine.webp", title: "Combine", text: "A hole taken out of a box, pegs as copies in a folder, a plane made thick: everything stays closed." }, + { img: "img/scripts.webp", title: "Scripts", text: "A little town from a dozen lines of Lua: random houses, each a folder of walls and roof. F5 runs it, Ctrl+Z takes it back." }, + { img: "img/light.webp", title: "Light", text: "The light appearance, warm paper instead of graphite. Or let it follow your desktop (View > Appearance)." }, + { img: "img/phone.webp", title: "Phone", text: "The phone layout: big buttons along the bottom, Shift and Ctrl that stay down, panels when you want them." }, + { img: "img/crash.webp", title: "Even the crash screen", text: "If something does get burnt, it says so plainly, keeps a report for you, and your work comes back from the last autosave." }, ], scripting: { diff --git a/docs/img/combine.webp b/docs/img/combine.webp index 2e66415..b2efe4b 100644 Binary files a/docs/img/combine.webp and b/docs/img/combine.webp differ diff --git a/docs/img/crash.webp b/docs/img/crash.webp index 4fc98d2..29e848f 100644 Binary files a/docs/img/crash.webp and b/docs/img/crash.webp differ diff --git a/docs/img/layout.webp b/docs/img/layout.webp new file mode 100644 index 0000000..787e72c Binary files /dev/null and b/docs/img/layout.webp differ diff --git a/docs/img/light.webp b/docs/img/light.webp new file mode 100644 index 0000000..acac722 Binary files /dev/null and b/docs/img/light.webp differ diff --git a/docs/img/materials.webp b/docs/img/materials.webp new file mode 100644 index 0000000..3332c96 Binary files /dev/null and b/docs/img/materials.webp differ diff --git a/docs/img/model.webp b/docs/img/model.webp index 829b27e..289af1a 100644 Binary files a/docs/img/model.webp and b/docs/img/model.webp differ diff --git a/docs/img/paint.webp b/docs/img/paint.webp index b8b63e8..a05b4b0 100644 Binary files a/docs/img/paint.webp and b/docs/img/paint.webp differ diff --git a/docs/img/phone.webp b/docs/img/phone.webp index 178a897..ae87ff8 100644 Binary files a/docs/img/phone.webp and b/docs/img/phone.webp differ diff --git a/docs/img/print.webp b/docs/img/print.webp index b23e10d..27c455b 100644 Binary files a/docs/img/print.webp and b/docs/img/print.webp differ diff --git a/docs/img/render.webp b/docs/img/render.webp new file mode 100644 index 0000000..5c20de6 Binary files /dev/null and b/docs/img/render.webp differ diff --git a/docs/img/scripts.webp b/docs/img/scripts.webp new file mode 100644 index 0000000..97db006 Binary files /dev/null and b/docs/img/scripts.webp differ diff --git a/docs/img/sculpt.webp b/docs/img/sculpt.webp new file mode 100644 index 0000000..9ae6a6c Binary files /dev/null and b/docs/img/sculpt.webp differ diff --git a/docs/index.html b/docs/index.html index 706f8b1..df74152 100644 --- a/docs/index.html +++ b/docs/index.html @@ -94,7 +94,7 @@
How it looks -

Five tabs: what you're doing, not where it's hidden.

+

What you're doing, not where it's hidden.

diff --git a/engine/crash.cpp b/engine/crash.cpp index 4cfcfb8..e95563e 100644 --- a/engine/crash.cpp +++ b/engine/crash.cpp @@ -2,6 +2,13 @@ #include #include +#include +#include + +#include +#include +#include +#include #include @@ -190,6 +197,9 @@ Busy::~Busy() { } // ── The crash screen: SDL's 2D renderer and its built-in 8×8 font, scaled up ── +// The crash screen: its own small program (this one, started again), drawn with SDL's own +// renderer rather than Vulkan or OpenGL (what crashed may have been the graphics driver), in the +// app's look: the same fonts, colours and keys. int runReporter(int argc, char** argv) { if (argc < 3 || std::strcmp(argv[1], "-crashreport") != 0) return -1; std::string report; @@ -208,173 +218,200 @@ int runReporter(int argc, char** argv) { for (std::string l; std::getline(ss, l);) lines.push_back(l); } const std::string headline = lines.empty() ? "Fry3DX crashed." : lines[0]; + std::string details; // (the report without its headline) + for (size_t i = 1; i < lines.size(); ++i) details += lines[i] + "\n"; + while (!details.empty() && details.front() == '\n') details.erase(0, 1); if (!SDL_Init(SDL_INIT_VIDEO)) return 1; SDL_Window* win = nullptr; SDL_Renderer* ren = nullptr; - if (!SDL_CreateWindowAndRenderer("Fry3DX - something got burnt", 1000, 620, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY, &win, &ren)) return 1; + if (!SDL_CreateWindowAndRenderer("Fry3DX - something got burnt", 940, 640, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY, &win, &ren)) return 1; SDL_SetRenderVSync(ren, 1); - SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND); - auto text = [&](float x, float y, float scale, const std::string& s, SDL_Color c) { - SDL_SetRenderScale(ren, scale, scale); - SDL_SetRenderDrawColor(ren, c.r, c.g, c.b, c.a); - SDL_RenderDebugText(ren, x / scale, y / scale, s.c_str()); - SDL_SetRenderScale(ren, 1, 1); - }; - // Shapes for the picture: an ellipse (filled), a box. - auto ellipse = [&](float cx, float cy, float rx, float ry, SDL_FColor c) { - std::vector v; - const int n = 48; - for (int i = 0; i < n; ++i) { - const float a0 = 6.2831853f * i / n, a1 = 6.2831853f * (i + 1) / n; - v.push_back({{cx, cy}, c, {}}); - v.push_back({{cx + std::cos(a0) * rx, cy + std::sin(a0) * ry}, c, {}}); - v.push_back({{cx + std::cos(a1) * rx, cy + std::sin(a1) * ry}, c, {}}); + const char* base = SDL_GetBasePath(); + const std::string exeDir = base ? base : "./"; + fs::mountGameDir(exeDir + "engine"); // (the fonts) + fs::mountGameDir(exeDir + "Fry3DX"); // (the logo) + + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGui::GetIO().IniFilename = nullptr; + ImGui_ImplSDL3_InitForSDLRenderer(win, ren); + ImGui_ImplSDLRenderer3_Init(ren); + ui::init(); + const float density = SDL_GetWindowDisplayScale(win); + ImGui::GetStyle().ScaleAllSizes(density); + ImGui::GetStyle().FontScaleDpi = density; + + SDL_Texture* logo = nullptr; + if (auto png = fs::read("branding/logo.png")) { + int w = 0, h = 0, n = 0; + if (stbi_uc* px = stbi_load_from_memory(png->data(), int(png->size()), &w, &h, &n, 4)) { + if ((logo = SDL_CreateTexture(ren, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_STATIC, w, h))) { + SDL_UpdateTexture(logo, nullptr, px, w * 4); + SDL_SetTextureBlendMode(logo, SDL_BLENDMODE_BLEND); + } + stbi_image_free(px); } - SDL_RenderGeometry(ren, nullptr, v.data(), int(v.size()), nullptr, 0); - }; - auto quad = [&](SDL_FPoint a, SDL_FPoint b, SDL_FPoint c, SDL_FPoint d, SDL_FColor col) { - const SDL_Vertex v[6] = {{a, col, {}}, {b, col, {}}, {c, col, {}}, {a, col, {}}, {c, col, {}}, {d, col, {}}}; - SDL_RenderGeometry(ren, nullptr, v, 6, nullptr, 0); - }; - auto rgb = [](int r, int g, int b, float a = 1.0f) { return SDL_FColor{r / 255.0f, g / 255.0f, b / 255.0f, a}; }; - // Word wrap for the 8-pixel font. - auto wrap = [](const std::string& s, size_t cols) { - std::vector out; - std::string line; - std::stringstream ss(s); - for (std::string w; ss >> w;) { - if (!line.empty() && line.size() + 1 + w.size() > cols) out.push_back(line), line.clear(); - line += (line.empty() ? "" : " ") + w; - } - if (!line.empty()) out.push_back(line); - return out; - }; - const SDL_Color ink{236, 232, 226, 255}, dim{150, 146, 140, 255}, orange{255, 140, 60, 255}; - int scroll = 0, result = 0; + } + + using ui::Role; + auto col = [](Role r, float a = 1.0f) { return ImGui::ColorConvertU32ToFloat4(ui::color(r, a)); }; std::string toast; double toastUntil = 0; + int frame = 0, choice = -1; for (bool running = true; running;) { - int w = 0, h = 0; - SDL_GetRenderOutputSize(ren, &w, &h); - const float s = std::min(h / 620.0f, w / 1000.0f); - const double t = SDL_GetTicks() / 1000.0; - struct Button { const char* label; float x; } buttons[] = {{"OPEN IT AGAIN", 60 * s}, {"COPY REPORT", 290 * s}, {"REPORT ON GITHUB", 520 * s}, {"CLOSE", 750 * s}}; - const float by = h - 90 * s, bw = 210 * s, bh = 50 * s; - float mx = 0, my = 0; - SDL_GetMouseState(&mx, &my); - float px = 1, py = 1; // window → pixels - { - int ww, wh; - SDL_GetWindowSize(win, &ww, &wh); - px = float(w) / ww, py = float(h) / wh; - } - mx *= px, my *= py; - int clicked = -1; for (SDL_Event e; SDL_PollEvent(&e);) { + ImGui_ImplSDL3_ProcessEvent(&e); if (e.type == SDL_EVENT_QUIT) running = false; - if (e.type == SDL_EVENT_KEY_DOWN) { - if (e.key.key == SDLK_ESCAPE || e.key.key == SDLK_Q) running = false; - if (e.key.key == SDLK_R) clicked = 0; - if (e.key.key == SDLK_C) clicked = 1; - } - if (e.type == SDL_EVENT_MOUSE_WHEEL) scroll = std::max(0, scroll - int(e.wheel.y * 3)); - if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) - for (int i = 0; i < 4; ++i) - if (mx >= buttons[i].x && mx < buttons[i].x + bw && my >= by && my < by + bh) clicked = i; } - if (clicked == 0) { // opened again as it was started (it offers the autosave back) - std::vector a; - std::string exe = argv[0]; - a.push_back(exe.data()); - for (std::string& r : restart) a.push_back(r.data()); - a.push_back(nullptr); - SDL_DestroyRenderer(ren); - SDL_DestroyWindow(win); - SDL_Quit(); -#ifdef _WIN32 - _spawnv(_P_NOWAIT, exe.c_str(), a.data()); - return 0; -#else - execv(std::filesystem::read_symlink("/proc/self/exe").c_str(), a.data()); - return 1; -#endif - } - if (clicked == 1 || clicked == 2) { - SDL_SetClipboardText(report.c_str()); - toast = clicked == 1 ? "Copied: paste it into a bug report." : "Copied: paste it into the new issue."; - toastUntil = t + 3.0; - if (clicked == 2) SDL_OpenURL("https://github.com/Nexoniarz/Fry3DX/issues/new"); - } - if (clicked == 3) running = false; + ImGui_ImplSDLRenderer3_NewFrame(); + ImGui_ImplSDL3_NewFrame(); + ui::beginFrame(1080.0f); // (sizes as they are: the window's density is in FontScaleDpi) + ImGui::NewFrame(); + const double t = SDL_GetTicks() / 1000.0; + const float u = density; // (one point) - SDL_SetRenderDrawColor(ren, 22, 22, 25, 255); - SDL_RenderClear(ren); - // The picture, top right: a pan, a cube on it burnt black, smoke going up. + const ImGuiViewport* vp = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(vp->Pos); + ImGui::SetNextWindowSize(vp->Size); + ImGui::PushStyleColor(ImGuiCol_WindowBg, col(Role::Background)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(36 * u, 44 * u)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); + ImGui::Begin("crash", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings); + ImDrawList* dl = ImGui::GetWindowDrawList(); + + // The logo, gone dark at the edges, with smoke curling up off it. + const float size = 92 * u; + const ImVec2 at = ImGui::GetCursorScreenPos(); + if (logo) dl->AddImage(ImTextureRef(ImTextureID(logo)), at, ImVec2(at.x + size, at.y + size)); + for (int i = 0; i < 7; ++i) { + const double life = std::fmod(t * 0.35 + i / 7.0, 1.0); + const float x = at.x + size * (0.45f + 0.18f * float(std::sin(life * 5.0 + i * 1.7))), y = at.y + size * 0.3f - float(life) * 52 * u; + dl->AddCircleFilled(ImVec2(x, y), (4 + float(life) * 12) * u, ui::color(Role::TextTertiary, 0.28f * float(1.0 - life)), 24); + } + ImGui::Dummy(ImVec2(size, size)); + ImGui::SameLine(0, 24 * u); + ImGui::BeginGroup(); + ImGui::Dummy(ImVec2(0, 4 * u)); + ImGui::PushStyleColor(ImGuiCol_Text, col(Role::AccentText)); + ImGui::PushFont(ui::font(ui::Font::Bold), 13.0f); + ImGui::TextUnformatted("FRY3DX"); + ImGui::PopFont(); + ImGui::PopStyleColor(); + ImGui::PushFont(ui::font(ui::Font::Title), 34.0f); + ImGui::TextUnformatted("Something got burnt."); + ImGui::PopFont(); + ImGui::PushStyleColor(ImGuiCol_Text, col(Role::TextSecondary)); + ImGui::PushTextWrapPos(ImGui::GetContentRegionMax().x); + ImGui::TextUnformatted(headline.c_str()); + ImGui::PopTextWrapPos(); + ImGui::PopStyleColor(); + ImGui::EndGroup(); + ImGui::Dummy(ImVec2(0, 14 * u)); + + // The good news, on a card of its own. { - const float cx = w - 230 * s, cy = 170 * s; - quad({cx + 80 * s, cy - 6 * s}, {cx + 200 * s, cy - 30 * s}, {cx + 204 * s, cy - 16 * s}, {cx + 84 * s, cy + 8 * s}, rgb(58, 58, 64)); // handle - ellipse(cx, cy + 10 * s, 105 * s, 34 * s, rgb(34, 34, 38)); - ellipse(cx, cy, 105 * s, 34 * s, rgb(70, 70, 78)); - ellipse(cx, cy + 2 * s, 88 * s, 26 * s, rgb(28, 28, 32)); - for (int i = 0; i < 3; ++i) { // flames under it - const float f = float(std::sin(t * 7 + i * 2.1) * 0.5 + 0.5); - const float fx = cx + (i - 1) * 50 * s; - ellipse(fx, cy + 46 * s, (14 + 4 * f) * s, (12 + 6 * f) * s, rgb(255, 74, 28, 0.85f)); - ellipse(fx, cy + 48 * s, 7 * s, (6 + 4 * f) * s, rgb(255, 194, 74, 0.9f)); - } - const float k = 26 * s; // the cube (seen from a corner), charred - quad({cx, cy - 2 * k}, {cx + k, cy - 1.5f * k}, {cx, cy - k}, {cx - k, cy - 1.5f * k}, rgb(64, 52, 46)); - quad({cx - k, cy - 1.5f * k}, {cx, cy - k}, {cx, cy}, {cx - k, cy - 0.5f * k}, rgb(40, 32, 28)); - quad({cx, cy - k}, {cx + k, cy - 1.5f * k}, {cx + k, cy - 0.5f * k}, {cx, cy}, rgb(26, 21, 19)); - for (int i = 0; i < 6; ++i) { // smoke, rising and fading - const double life = std::fmod(t * 0.45 + i / 6.0, 1.0); - const float sx = cx + float(std::sin(life * 6 + i) * 18 * s), sy = cy - 2.2f * k - float(life * 80 * s); - ellipse(sx, sy, float(10 + life * 22) * s, float(8 + life * 16) * s, rgb(150, 150, 158, float(0.35 * (1 - life)))); - } + const ImVec2 a = ImGui::GetCursorScreenPos(); + const float w = ImGui::GetContentRegionAvail().x, h = ImGui::GetTextLineHeight() + 26 * u; + dl->AddRectFilled(a, ImVec2(a.x + w, a.y + h), ui::color(Role::Success, 0.14f), 8 * u); + dl->AddRect(a, ImVec2(a.x + w, a.y + h), ui::color(Role::Success, 0.4f), 8 * u); + dl->AddCircleFilled(ImVec2(a.x + 18 * u, a.y + h / 2), 4 * u, ui::color(Role::Success)); + ImGui::SetCursorScreenPos(ImVec2(a.x + 32 * u, a.y + 13 * u)); + ImGui::PushStyleColor(ImGuiCol_Text, col(Role::SuccessText)); + ImGui::TextUnformatted("Your work isn't lost: open it again and Fry3DX offers back the last autosave."); + ImGui::PopStyleColor(); + ImGui::SetCursorScreenPos(ImVec2(a.x, a.y + h + 18 * u)); } - text(60 * s, 60 * s, 4 * s, "Fry3DX", orange); - text(60 * s, 110 * s, 2.5f * s, "Something got burnt.", ink); - float y = 160 * s; - for (const std::string& l : wrap(headline, size_t((w - 400 * s) / (16 * s)))) - text(60 * s, y, 2 * s, l, ink), y += 24 * s; - y += 6 * s; - for (const std::string& l : wrap("Your work isn't lost: Open it again and Fry3DX offers back the last autosave.", size_t((w - 400 * s) / (12 * s)))) - text(60 * s, y, 1.5f * s, l, {130, 210, 120, 255}), y += 18 * s; - text(60 * s, y + 4 * s, 1.25f * s, "Report saved to " + std::string(argv[2]), dim); - y += 30 * s; - // The report's tail, in a panel, scrollable. - const SDL_FRect panel{50 * s, y - 8 * s, w - 100 * s, by - 20 * s - (y - 8 * s)}; - SDL_SetRenderDrawColor(ren, 15, 15, 17, 255); - SDL_RenderFillRect(ren, &panel); - SDL_SetRenderDrawColor(ren, 255, 120, 40, 255); - const SDL_FRect edge{panel.x, panel.y, 3 * s, panel.h}; - SDL_RenderFillRect(ren, &edge); - const int visible = std::max(1, int((panel.h - 10 * s) / (13 * s))); - const int start = std::clamp(int(lines.size()) - visible - scroll, 1, std::max(1, int(lines.size()) - 1)); - for (int i = start; i < int(lines.size()) && y < by - 30 * s; ++i, y += 13 * s) - text(64 * s, y, 1.25f * s, lines[size_t(i)].substr(0, size_t((w - 140 * s) / (10 * s))), {165, 160, 152, 255}); - for (int i = 0; i < 4; ++i) { - const bool hover = mx >= buttons[i].x && mx < buttons[i].x + bw && my >= by && my < by + bh; - SDL_FRect r{buttons[i].x, by, bw, bh}; - if (i == 0 || hover) SDL_SetRenderDrawColor(ren, hover ? 255 : 220, hover ? 140 : 100, hover ? 60 : 30, 255); - else SDL_SetRenderDrawColor(ren, 46, 46, 52, 255); - SDL_RenderFillRect(ren, &r); - const float n = float(std::strlen(buttons[i].label)), ts = std::min(1.75f * s, (bw - 24 * s) / (n * 8)); // (fits its button) - text(buttons[i].x + (bw - n * 8 * ts) / 2, by + bh / 2 - 4 * ts, ts, buttons[i].label, ink); + + // The report: what was going on when it happened, for a bug report. + ImGui::PushStyleColor(ImGuiCol_Text, col(Role::TextTertiary)); + ImGui::PushTextWrapPos(0.0f); + ImGui::Text("The report, saved to %s", argv[2]); + ImGui::PopTextWrapPos(); + ImGui::PopStyleColor(); + const float buttonsH = ImGui::GetFrameHeight() + 26 * u + ImGui::GetTextLineHeight(); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(14 * u, 12 * u)); + ImGui::BeginChild("report", ImVec2(0, std::max(80 * u, ImGui::GetContentRegionAvail().y - buttonsH)), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_AlwaysUseWindowPadding, + ImGuiWindowFlags_HorizontalScrollbar); + ImGui::PushFont(ui::font(ui::Font::Mono), 13.0f); + ImGui::PushStyleColor(ImGuiCol_Text, col(Role::TextSecondary)); + ImGui::TextUnformatted(details.c_str()); + ImGui::PopStyleColor(); + ImGui::PopFont(); + if (frame == 1) ImGui::SetScrollHereY(1.0f); // (the newest lines: the end) + ImGui::EndChild(); + ImGui::PopStyleVar(); + + ImGui::Dummy(ImVec2(0, 10 * u)); + const ImVec2 button(170 * u, ImGui::GetFrameHeight() + 10 * u); + ImGui::PushStyleColor(ImGuiCol_Button, col(Role::Accent)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, col(Role::AccentHover)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, col(Role::AccentActive)); + ImGui::PushStyleColor(ImGuiCol_Text, col(Role::OnAccent)); + if (ImGui::Button("Open it again", button)) choice = 0; + ImGui::PopStyleColor(4); + ImGui::SameLine(0, 10 * u); + if (ImGui::Button("Copy the report", button)) choice = 1; + ImGui::SameLine(0, 10 * u); + if (ImGui::Button("Report it on GitHub", button)) choice = 2; + ImGui::SameLine(0, 10 * u); + if (ImGui::Button("Close", ImVec2(110 * u, button.y))) running = false; + if (!ImGui::GetIO().WantTextInput) { + if (ImGui::IsKeyPressed(ImGuiKey_R, false)) choice = 0; + if (ImGui::IsKeyPressed(ImGuiKey_C, false)) choice = 1; + if (ImGui::IsKeyPressed(ImGuiKey_Escape, false) || ImGui::IsKeyPressed(ImGuiKey_Q, false)) running = false; } - text(60 * s, by + bh + 12 * s, 1.25f * s, "R open again C copy Esc close", dim); - if (t < toastUntil) text(60 * s, by - 16 * s, 1.5f * s, toast, orange); - if (const char* shot = std::getenv("KEEL_CRASHSHOT")) { // for checking the screen: one frame to a file, then quit + ImGui::PushStyleColor(ImGuiCol_Text, col(t < toastUntil ? Role::AccentText : Role::TextTertiary)); + ImGui::TextUnformatted(t < toastUntil ? toast.c_str() : "R open it again C copy Esc close"); + ImGui::PopStyleColor(); + ImGui::End(); + ImGui::PopStyleVar(2); + ImGui::PopStyleColor(); + + if (choice == 1 || choice == 2) { + SDL_SetClipboardText(report.c_str()); + toast = choice == 1 ? "Copied: paste it into a bug report." : "Copied: paste it into the new issue."; + toastUntil = t + 3.0; + if (choice == 2) SDL_OpenURL("https://github.com/Nexoniarz/Fry3DX/issues/new"); + choice = -1; + } + + ImGui::Render(); + const ImVec4 bg = col(Role::Background); + SDL_SetRenderDrawColorFloat(ren, bg.x, bg.y, bg.z, 1.0f); + SDL_RenderClear(ren); + ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), ren); + if (const char* shot = std::getenv("KEEL_CRASHSHOT"); shot && frame == 4) { // for checking the screen: one frame to a file, then quit if (SDL_Surface* px = SDL_RenderReadPixels(ren, nullptr)) SDL_SaveBMP(px, shot), SDL_DestroySurface(px); running = false; } SDL_RenderPresent(ren); + ++frame; + if (choice == 0) break; } + ImGui_ImplSDLRenderer3_Shutdown(); + ImGui_ImplSDL3_Shutdown(); + ImGui::DestroyContext(); + if (logo) SDL_DestroyTexture(logo); SDL_DestroyRenderer(ren); SDL_DestroyWindow(win); SDL_Quit(); - return result; + if (choice == 0) { // opened again as it was started (it offers the autosave back) + const std::string exe = sys::executablePath(); + std::vector a; + std::string self = argv[0]; + a.push_back(self.data()); + for (std::string& r : restart) a.push_back(r.data()); + a.push_back(nullptr); +#ifdef _WIN32 + _spawnv(_P_NOWAIT, exe.c_str(), a.data()); + return 0; +#else + execv(exe.c_str(), a.data()); + return 1; +#endif + } + return 0; } } // namespace keel::crash diff --git a/src/app.cpp b/src/app.cpp index 9eab7bf..df7ee08 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -541,6 +541,7 @@ public: static keel::Cmd autosaveNow("fry_autosavenow", [](const keel::Args&) { self->autosave(true); }, "Fry3DX: autosave now"); static keel::Cmd closeAsk("fry_close", [](const keel::Args&) { if (self->allowQuit()) keel::execute("quit"); }, "Fry3DX: as the window's close button"); static keel::Cmd recover("fry_recover", [](const keel::Args&) { self->findAutosaves(), self->showRecover_ = !self->recover_.empty(); }, "Fry3DX: show the autosaves to recover"); + static keel::Cmd scriptPanel("fry_scriptpanel", [](const keel::Args& a) { self->showScript_ = a.size() < 2 || a[1] != "0"; }, "Fry3DX: show (1) or hide (0) the Script panel"); static keel::Cmd about("fry_about", [](const keel::Args&) { self->showAbout_ = true; }, "Fry3DX: show the About window"); keel::execute("r_autoexposure 0; r_ssr 0; r_volumetric 0; r_shafts 0; r_dynres 0; r_renderscale 1; fps_max 144"); #if defined(__ANDROID__) // a phone: none of the heavy parts (and its GPU's driver is kept to well-trodden ground) @@ -5836,6 +5837,16 @@ private: void paintProperties(float w) { Object* o = selected_.empty() ? nullptr : find(active_); + if (sculpting_) { // (its look, while you shape it: what it's made of shows how the light runs over it) + if (!o) return ImGui::TextWrapped("Brush over an object to sculpt it.\n\nA shape with few points is made finer first, keeping its shape. Tab goes back to painting colours."); + ImGui::TextColored(accentText(), "%s", o->name.c_str()); + ImGui::TextDisabled("%zu points, %zu faces", o->mesh.verts.size(), o->mesh.faces.size()); + ImGui::SeparatorText("Look"); + materialPicker(*o); + undoable([&] { return ImGui::ColorEdit3("Colour", &o->color.x); }); + if (ImGui::Checkbox("Smooth shading", &o->smooth)) pushUndo(), touched(o->id); + return; + } if (!o) { ImGui::TextWrapped("Click an object to paint on it.\n\nIts surface gets laid out flat on a picture (the Picture panel), which you can paint on too, " "or put pictures onto: copy one in any app and press Ctrl+V."); @@ -5955,7 +5966,8 @@ private: if (o && ImGui::Button("Lay it out (and start its picture)")) readyToPaint(o->id); return ImGui::End(); } - ImGui::TextWrapped(o ? "Paint on it (or press Start painting it in Properties) and its picture shows here." : "Click an object to paint it: its picture shows here."); + ImGui::TextWrapped(sculpting_ ? "Sculpting changes its shape, not its picture. Tab goes back to painting colours." + : o ? "Paint on it (or press Start painting it in Properties) and its picture shows here." : "Click an object to paint it: its picture shows here."); return ImGui::End(); } uploadPicture(*o, !paint_.active); @@ -6303,6 +6315,9 @@ private: } else if (tab_ == Tab::Print && printTool_ != PrintTool::None) { ImGui::TextDisabled(printTool_ == PrintTool::Measure ? "Measure: click two points on the model (corners catch the mouse); a third click starts again · M: done" : "Lay flat: click the face it should stand on: it turns so that face is down, on the bed · L: done"); + } else if (tab_ == Tab::Paint && sculpting_) { + for (const SculptToolInfo& t : kSculptTools) + if (t.brush == sculptBrush_) ImGui::TextDisabled("%s: %s · Shift: smooth X: both sides Tab: colour Right drag: orbit F1: all keys", t.name, t.hint); } else if (tab_ == Tab::Paint) { for (const PaintToolInfo& t : kPaintTools) if (t.tool == paintTool_) ImGui::TextDisabled("%s: %s · Right drag: orbit Wheel: zoom (in the Picture panel too) F1: all keys", t.name, t.hint);