diff --git a/CMakeLists.txt b/CMakeLists.txt index c8c4324c..57531796 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,6 +189,7 @@ set(SOURCES "editor/editorOrthophoto.cpp" "editor/editorPreviews.cpp" "editor/editorIcons.cpp" +"editor/editorClips.cpp" "application/editormode.cpp" "application/editormodetrack.cpp" "application/editormodetrackgizmo.cpp" diff --git a/application/editormode.cpp b/application/editormode.cpp index 509f273d..58c55f8e 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -4614,9 +4614,9 @@ void editor_mode::render_help_menu() ImGui::MenuItem(STR_C("Keyboard shortcuts"), "F1", &m_shortcuts_open); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", STR_C("The keys of the editor, for each field of work")); - ImGui::MenuItem(STR_C("First steps"), nullptr, &m_first_steps_open); + ImGui::MenuItem(STR_C("Tutorial"), nullptr, &m_first_steps_open); if (ImGui::IsItemHovered()) - ImGui::SetTooltip("%s", STR_C("Laying the first track, step by step")); + ImGui::SetTooltip("%s", STR_C("Seven short lessons with clips: the camera, laying track, curves, joining, switches, fixing, saving")); ImGui::EndMenu(); } @@ -4625,46 +4625,305 @@ void editor_mode::first_step_done(first_step const Step) m_first_steps_done[static_cast(Step)] = true; } +namespace +{ + +ImU32 const kTutorialLit{IM_COL32(90, 175, 255, 255)}; + +float tutorial_mouse(ImDrawList &List, ImVec2 const At, float const Height, std::string const &Part) +{ + auto const width{std::round(Height * 0.68f)}; + auto const line{ImGui::GetColorU32(ImGuiCol_Text)}; + auto const thickness{std::max(1.0f, Height * 0.07f)}; + auto const rounding{width * 0.5f}; + auto const split{At.y + std::round(Height * 0.45f)}; + auto const middle{At.x + width * 0.5f}; + ImVec2 const high{At.x + width, At.y + Height}; + if (Part == "LMB") + List.AddRectFilled(At, ImVec2(middle, split), kTutorialLit, rounding, ImDrawFlags_RoundCornersTopLeft); + if (Part == "RMB") + List.AddRectFilled(ImVec2(middle, At.y), ImVec2(high.x, split), kTutorialLit, rounding, ImDrawFlags_RoundCornersTopRight); + List.AddRect(At, high, line, rounding, 0, thickness); + List.AddLine(ImVec2(At.x, split), ImVec2(high.x, split), line, thickness); + List.AddLine(ImVec2(middle, At.y), ImVec2(middle, split), line, thickness); + auto const wheel{std::max(3.0f, std::round(width * 0.24f))}; + ImVec2 const wheel_low{middle - wheel * 0.5f, At.y + Height * 0.1f}; + ImVec2 const wheel_high{middle + wheel * 0.5f, At.y + Height * 0.36f}; + List.AddRectFilled(wheel_low, wheel_high, (Part == "MMB" || Part == "wheel") ? kTutorialLit : ImGui::GetColorU32(ImGuiCol_WindowBg), wheel * 0.5f); + List.AddRect(wheel_low, wheel_high, line, wheel * 0.5f, 0, thickness * 0.8f); + auto right{high.x}; + auto const arrow{Height * 0.16f}; + if (Part == "wheel") + { + auto const x{right + arrow * 1.6f}; + List.AddTriangleFilled(ImVec2(x - arrow, At.y + arrow * 1.8f), ImVec2(x + arrow, At.y + arrow * 1.8f), ImVec2(x, At.y + arrow * 0.3f), kTutorialLit); + List.AddTriangleFilled(ImVec2(x - arrow, high.y - arrow * 1.8f), ImVec2(x + arrow, high.y - arrow * 1.8f), ImVec2(x, high.y - arrow * 0.3f), kTutorialLit); + right = x + arrow; + } + if (Part == "move") + { + auto const reach{Height * 0.42f}; + ImVec2 const centre{right + arrow * 0.8f + reach, At.y + Height * 0.5f}; + List.AddLine(ImVec2(centre.x - reach + arrow, centre.y), ImVec2(centre.x + reach - arrow, centre.y), kTutorialLit, thickness); + List.AddLine(ImVec2(centre.x, centre.y - reach + arrow), ImVec2(centre.x, centre.y + reach - arrow), kTutorialLit, thickness); + List.AddTriangleFilled(ImVec2(centre.x - reach, centre.y), ImVec2(centre.x - reach + arrow * 1.4f, centre.y - arrow), ImVec2(centre.x - reach + arrow * 1.4f, centre.y + arrow), kTutorialLit); + List.AddTriangleFilled(ImVec2(centre.x + reach, centre.y), ImVec2(centre.x + reach - arrow * 1.4f, centre.y + arrow), ImVec2(centre.x + reach - arrow * 1.4f, centre.y - arrow), kTutorialLit); + List.AddTriangleFilled(ImVec2(centre.x, centre.y - reach), ImVec2(centre.x + arrow, centre.y - reach + arrow * 1.4f), ImVec2(centre.x - arrow, centre.y - reach + arrow * 1.4f), kTutorialLit); + List.AddTriangleFilled(ImVec2(centre.x, centre.y + reach), ImVec2(centre.x - arrow, centre.y + reach - arrow * 1.4f), ImVec2(centre.x + arrow, centre.y + reach - arrow * 1.4f), kTutorialLit); + right = centre.x + reach; + } + return right - At.x; +} + +float tutorial_key(ImDrawList &List, ImVec2 const At, float const Height, std::string const &Text) +{ + auto const size{ImGui::CalcTextSize(Text.c_str())}; + auto const width{std::max(std::round(size.x + Height * 0.6f), Height)}; + auto const rounding{Height * 0.2f}; + ImVec2 const high{At.x + width, At.y + Height}; + List.AddRectFilled(At, high, ImGui::GetColorU32(ImGuiCol_FrameBg), rounding); + List.AddRect(At, high, ImGui::GetColorU32(ImGuiCol_TextDisabled), rounding, 0, std::max(1.0f, Height * 0.06f)); + List.AddText(ImVec2(std::round(At.x + (width - size.x) * 0.5f), std::round(At.y + (Height - size.y) * 0.5f)), ImGui::GetColorU32(ImGuiCol_Text), Text.c_str()); + return width; +} + +void tutorial_input(char const *Keys, char const *Text, float const Column) +{ + auto &list{*ImGui::GetWindowDrawList()}; + auto const at{ImGui::GetCursorScreenPos()}; + auto const height{std::round(ImGui::GetTextLineHeight() * 1.45f)}; + auto const gap{std::round(height * 0.2f)}; + float x{0.0f}; + std::istringstream words{Keys}; + for (std::string word; words >> word;) + { + ImVec2 const place{at.x + x, at.y}; + if (word == "+") + { + auto const size{ImGui::CalcTextSize("+")}; + list.AddText(ImVec2(place.x, std::round(at.y + (height - size.y) * 0.5f)), ImGui::GetColorU32(ImGuiCol_TextDisabled), "+"); + x += size.x + gap; + } + else if (word == "LMB" || word == "RMB" || word == "MMB" || word == "wheel" || word == "move") + x += tutorial_mouse(list, place, height, word) + gap; + else + x += tutorial_key(list, place, height, word) + gap; + } + ImGui::Dummy(ImVec2(std::max(x, Column), height)); + ImGui::SameLine(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + std::round((height - ImGui::GetTextLineHeight()) * 0.5f)); + ImGui::TextWrapped("%s", STR_C(Text)); +} + +} // namespace + void editor_mode::render_first_steps() { if (false == m_first_steps_open) + { + if (false == m_tutorial_clip.path().empty()) + m_tutorial_clip.close(); return; + } if (ui()->mode() == nodebank_panel::TRACK) first_step_done(first_step::tracks); if (m_lay.active) first_step_done(first_step::lay); - static char const *const steps[]{ - "F3: the Tracks field of work", - "L: lay track", - "Click on the ground, then once more: a straight. Each next click lays a piece and goes on from its end, Esc ends", - "While laying on: Ctrl+wheel changes the radius, Shift+wheel the shape (arc then straight, straight then arc, arc, straight). The plain wheel zooms", - "Joining: press on a free end (magenta ring) and drag onto another free end", - "T: switch. Click a straight, then press on it and drag: along sets the direction, sideways the side", - "Ctrl+Z takes a step back, Backspace while laying takes back the last piece", - "Ctrl+S saves the scenery", - "Drive it: Trainset in the toolbar, pick a locomotive from the list, LMB on the track, then Place and drive"}; - ImGui::SetNextWindowSize(ImVec2(440.0f * Global.ui_scale, 0.0f), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(8.0f, ImGui::GetIO().DisplaySize.y - 8.0f), ImGuiCond_FirstUseEver, ImVec2(0.0f, 1.0f)); - ImGui::SetNextWindowBgAlpha(1.0f); - if (ImGui::Begin((std::string(STR_C("First steps")) + "###firststeps").c_str(), &m_first_steps_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) + if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::path && selected_track() != nullptr) + first_step_done(first_step::select); + enum class trial { none, top, lay, turnout, select }; + struct input { - ImGui::PushTextWrapPos(420.0f * Global.ui_scale); - ImGui::TextDisabled("%s", STR_C("The ticks fill in by themselves, the window doesn't stop anything")); - for (std::size_t i = 0; i < m_first_steps_done.size(); ++i) + char const *keys; + char const *text; + }; + struct goal + { + first_step step; + char const *text; + }; + struct lesson + { + char const *title; + char const *clip; + std::vector inputs; + std::vector goals; + trial action; + }; + static std::vector const lessons{ + {"Looking around", + "camera", + {{"RMB + move", "Hold the right button and move the mouse: look around"}, + {"RMB + W S A D", "With the right button held: fly forwards, back and to the sides; E and Q: up and down"}, + {"O", "The top view, and back to 3D"}, + {"wheel", "In the top view: zoom in and out where the cursor points"}, + {"MMB + move", "In the top view: hold the wheel and drag to move the map"}, + {"F", "Fly to what is selected"}}, + {{first_step::look, "Look around with the right button held"}, {first_step::top, "Switch to the top view"}}, + trial::top}, + {"Laying track", + "lay", + {{"F3", "The Tracks field of work"}, + {"L", "The lay tool"}, + {"LMB", "A click on the ground: the first point; the next click: a straight up to it"}, + {"LMB", "Each next click lays a piece on from the end of the last one"}, + {"Backspace", "Takes the last piece back"}, + {"Esc", "Ends laying"}}, + {{first_step::tracks, "Open the Tracks field of work"}, {first_step::lay, "Turn the lay tool on"}, {first_step::laid, "Lay a track"}}, + trial::lay}, + {"Curves", + "curves", + {{"Ctrl + wheel", "The radius of the curve, while laying on from an end"}, + {"Shift + wheel", "The shape: arc then straight, straight then arc, arc, straight"}, + {"Alt", "Held: the direction turns in steps of 15 degrees"}, + {"0 1 2 3", "Type the length of the next piece, Enter lays it"}, + {"wheel", "Alone it zooms, as everywhere"}}, + {{first_step::wheel, "Change the radius with Ctrl+wheel"}, {first_step::shape, "Change the shape with Shift+wheel"}}, + trial::lay}, + {"Joining tracks", + "join", + {{"LMB + move", "Press on a free end (a magenta ring) and drag: a new track goes out of it"}, + {"LMB", "Let go on another free end: a curve joins the two tracks"}, + {"Ctrl + wheel", "While over the other end: the radius of the joining curve"}, + {"Shift + wheel", "While over the other end: another way to join"}}, + {{first_step::join, "Join two free ends"}}, + trial::lay}, + {"Switches", + "switch", + {{"T", "The switch tool; the type of the switch is chosen in its list"}, + {"LMB", "Click a straight track: the switch goes into it"}, + {"LMB + move", "Press on it and drag: along sets the direction, sideways the side"}, + {"Esc", "Back to selecting"}}, + {{first_step::turnout, "Put a switch into a track"}}, + trial::turnout}, + {"Selecting and fixing", + "edit", + {{"Esc", "The select tool"}, + {"LMB", "Click a track: it is selected, with its points"}, + {"LMB + move", "Drag a point: the end or the curve follows"}, + {"Q W E", "Move, turn or scale with the gizmo"}, + {"RMB", "A click on a track: what can be done with it"}, + {"Del", "Deletes the selected"}, + {"Ctrl + Z", "Takes the last change back; Ctrl+Y brings it back again"}}, + {{first_step::select, "Select a track"}, {first_step::undo, "Take a change back"}}, + trial::select}, + {"Saving and driving", + nullptr, + {{"Ctrl + S", "Saves the scenery"}, {"LMB", "Trainset in the toolbar: pick a locomotive from the list, click the track, then Place and drive"}}, + {{first_step::save, "Save the scenery"}, {first_step::drive, "Drive on the new track"}}, + trial::none}, + }; + auto const done = [&](lesson const &Lesson) { + return std::all_of(Lesson.goals.begin(), Lesson.goals.end(), [&](goal const &Goal) { return m_first_steps_done[static_cast(Goal.step)]; }); + }; + m_tutorial_page = std::clamp(m_tutorial_page, 0, static_cast(lessons.size()) - 1); + auto const &lesson{lessons[m_tutorial_page]}; + if (lesson.clip == nullptr) + m_tutorial_clip.close(); + else if (auto const path{std::string{"lang/editor_tutorial/"} + lesson.clip + ".gif"}; path != m_tutorial_clip.path()) + { + m_tutorial_clip.open(path); + m_tutorial_since = ImGui::GetTime(); + } + auto const scale{Global.ui_scale}; + auto const width{520.0f * scale}; + ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowBgAlpha(1.0f); + if (ImGui::Begin((std::string(STR_C("Tutorial")) + "###tutorial").c_str(), &m_first_steps_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + width); + ImGui::TextDisabled(STR_C("Lesson %d of %d"), m_tutorial_page + 1, static_cast(lessons.size())); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.55f, 0.8f, 1.0f, 1.0f), "%s", STR_C(lesson.title)); + if (lesson.clip != nullptr && false == m_tutorial_clip.missing()) { - bool const done{m_first_steps_done[i]}; - ImGui::TextColored(done ? ImVec4(0.4f, 0.9f, 0.4f, 1.0f) : ImGui::GetStyleColorVec4(ImGuiCol_Text), "%s", done ? "[x]" : "[ ]"); - ImGui::SameLine(); - if (done) - ImGui::TextDisabled("%s", STR_C(steps[i])); + auto const height{width * 320.0f / 520.0f}; + if (auto const texture{m_tutorial_clip.texture(ImGui::GetTime() - m_tutorial_since)}; texture != 0) + { + ImGui::Image(static_cast(texture), ImVec2(width, width * m_tutorial_clip.height() / std::max(1, m_tutorial_clip.width()))); + if (ImGui::IsItemClicked()) + m_tutorial_since = ImGui::GetTime(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Click: play it from the start")); + } else - ImGui::TextWrapped("%s", STR_C(steps[i])); + { + auto const at{ImGui::GetCursorScreenPos()}; + ImGui::GetWindowDrawList()->AddRectFilled(at, ImVec2(at.x + width, at.y + height), ImGui::GetColorU32(ImGuiCol_FrameBg)); + ImGui::Dummy(ImVec2(width, height)); + } } - ImGui::Separator(); - ImGui::TextDisabled("%s", STR_C("RMB held + W S A D: fly the camera, F: fly to the selected, F1: all the keys")); - if (ui()->mode() == nodebank_panel::TRACK && false == GfxRenderer->Mouse_Hit()) + ImGui::SeparatorText(STR_C("Mouse and keys")); + for (auto const &entry : lesson.inputs) + tutorial_input(entry.keys, entry.text, 150.0f * scale); + ImGui::SeparatorText(STR_C("Try it")); + for (auto const &entry : lesson.goals) + { + bool const reached{m_first_steps_done[static_cast(entry.step)]}; + ImGui::TextColored(reached ? ImVec4(0.4f, 0.9f, 0.4f, 1.0f) : ImGui::GetStyleColorVec4(ImGuiCol_Text), "%s", reached ? "[x]" : "[ ]"); + ImGui::SameLine(); + if (reached) + ImGui::TextDisabled("%s", STR_C(entry.text)); + else + ImGui::TextWrapped("%s", STR_C(entry.text)); + } + if (m_tutorial_page == 1 && ui()->mode() == nodebank_panel::TRACK && false == GfxRenderer->Mouse_Hit()) ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", STR_C("No ground under the cursor: track goes onto the level 0 plane, drawn as a grid. Terrain: F5")); + switch (lesson.action) + { + case trial::top: + if (ImGui::Button(Global.EditorOrtho ? STR_C("3D view (O)") : STR_C("Top view, orthographic (O)"))) + toggle_ortho(); + break; + case trial::lay: + case trial::turnout: + case trial::select: + { + auto const tab{lesson.action == trial::lay ? track_tab::lay : lesson.action == trial::turnout ? track_tab::turnout : track_tab::path}; + auto const on{ui()->mode() == nodebank_panel::TRACK && m_track_tab == tab}; + ImGui::BeginDisabled(on); + if (ImGui::Button(lesson.action == trial::lay ? STR_C("Lay track (L)") : lesson.action == trial::turnout ? STR_C("The switch tool (T)") : STR_C("The select tool (Esc)"))) + { + choose_work_area(work_area::tracks); + show_track_tab(tab); + } + ImGui::EndDisabled(); + break; + } + default: break; + } ImGui::PopTextWrapPos(); + ImGui::Separator(); + ImGui::BeginDisabled(m_tutorial_page == 0); + if (ImGui::Button(STR_C("< Back"))) + --m_tutorial_page; + ImGui::EndDisabled(); + ImGui::SameLine(); + auto const dot{ImGui::GetFrameHeight()}; + for (int index = 0; index < static_cast(lessons.size()); ++index) + { + ImGui::PushID(index); + auto const at{ImGui::GetCursorScreenPos()}; + if (ImGui::InvisibleButton("##lesson", ImVec2(dot * 0.8f, dot))) + m_tutorial_page = index; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C(lessons[index].title)); + ImVec2 const centre{at.x + dot * 0.4f, at.y + dot * 0.5f}; + auto const colour{done(lessons[index]) ? IM_COL32(100, 230, 100, 255) : ImGui::GetColorU32(ImGuiCol_TextDisabled)}; + if (index == m_tutorial_page) + ImGui::GetWindowDrawList()->AddCircle(centre, dot * 0.3f, kTutorialLit, 0, 2.0f); + ImGui::GetWindowDrawList()->AddCircleFilled(centre, dot * 0.17f, colour); + ImGui::PopID(); + ImGui::SameLine(0.0f, 0.0f); + } + ImGui::SameLine(); + if (m_tutorial_page + 1 < static_cast(lessons.size())) + { + if (ImGui::Button(STR_C("Next >"))) + ++m_tutorial_page; + } + else if (ImGui::Button(STR_C("Close"))) + m_first_steps_open = false; bool show{EditorSettings.first_steps()}; if (ImGui::Checkbox(STR_C("Show when the editor opens"), &show)) { @@ -5089,6 +5348,7 @@ void editor_mode::toggle_ortho() Global.EditorOrtho = !Global.EditorOrtho; if (Global.EditorOrtho) { + first_step_done(first_step::top); m_ortho_pitch = Camera.Angle.x; Camera.Angle.x = -M_PI_2; Camera.Angle.z = 0.0; @@ -5979,6 +6239,7 @@ void editor_mode::on_event_poll() if (flying) { m_input.poll(); + first_step_done(first_step::look); } else if (m_camera_flying) { diff --git a/application/editormode.h b/application/editormode.h index a3f3a873..2a81a03d 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -29,6 +29,7 @@ http://mozilla.org/MPL/2.0/. #include "editor/editorProfile.hpp" #include "editor/editorInfra.hpp" #include "editor/editorGauge.hpp" +#include "editor/editorClips.hpp" #include "application/scriptedinput.h" #include "world/Sweep.h" @@ -254,11 +255,14 @@ class editor_mode : public application_mode, private editor_track::observer void render_help_menu(); void render_shortcuts_window(); bool m_shortcuts_open{false}; - enum class first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, count }; + enum class first_step { tracks, lay, laid, wheel, join, turnout, undo, save, drive, look, top, select, shape, count }; void render_first_steps(); void first_step_done(first_step const Step); bool m_first_steps_open{false}; std::array(first_step::count)> m_first_steps_done{}; + int m_tutorial_page{0}; + double m_tutorial_since{0.0}; + editor_clips::clip m_tutorial_clip; // deletes what's selected: a road piece or point, a signal, an include of a scenery template, a model, tracks (Del) void delete_selected(); void delete_model(TAnimModel *Model, bool const Joined); diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp index 225507fd..c200fcf9 100644 --- a/application/editormodetrack.cpp +++ b/application/editormodetrack.cpp @@ -4883,7 +4883,7 @@ bool editor_mode::track_wheel(double const Offset) return *radius; return radii[0]; }; - first_step_done(first_step::wheel); + first_step_done(io.KeyCtrl ? first_step::wheel : first_step::shape); if (io.KeyCtrl) { if (false == joined) diff --git a/editor/editorClips.cpp b/editor/editorClips.cpp new file mode 100644 index 00000000..a1ef212c --- /dev/null +++ b/editor/editorClips.cpp @@ -0,0 +1,119 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorClips.hpp" + +#include "rendering/renderer.h" +#include "stb/stb_image.h" + +#include +#include +#include +#include +#include + +namespace editor_clips +{ + +clip::~clip() +{ + close(); +} + +void clip::open(std::string const &Path) +{ + if (Path == m_path && m_frames) + return; + close(); + m_path = Path; + auto frames{std::make_shared()}; + m_frames = frames; + std::thread([frames, Path]() { + stbi_set_flip_vertically_on_load_thread(0); + std::ifstream file(Path, std::ios::binary); + std::vector const bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + if (false == bytes.empty()) + { + int *delays{nullptr}; + int width{0}, height{0}, count{0}, components{0}; + auto *rgba{stbi_load_gif_from_memory(bytes.data(), static_cast(bytes.size()), &delays, &width, &height, &count, &components, 4)}; + if (rgba != nullptr && width > 0 && height > 0 && count > 0) + { + frames->rgba.assign(rgba, rgba + static_cast(width) * height * 4 * count); + frames->delays.resize(count); + for (int index = 0; index < count; ++index) + frames->delays[index] = std::max(delays != nullptr ? delays[index] : 100, 20); + frames->width = width; + frames->height = height; + } + if (rgba != nullptr) + stbi_image_free(rgba); + if (delays != nullptr) + stbi_image_free(delays); + } + frames->done = true; + }).detach(); +} + +void clip::close() +{ + if (m_texture != 0 && GfxRenderer && GfxRenderer->GetImguiRenderer() != nullptr) + GfxRenderer->GetImguiRenderer()->Release_Image(m_texture); + m_texture = 0; + m_shown = -1; + m_frames.reset(); + m_path.clear(); +} + +bool clip::loading() const +{ + return m_frames && false == m_frames->done; +} + +bool clip::missing() const +{ + return m_frames == nullptr || (m_frames->done && m_frames->delays.empty()); +} + +int clip::width() const +{ + return m_frames && m_frames->done ? m_frames->width : 0; +} + +int clip::height() const +{ + return m_frames && m_frames->done ? m_frames->height : 0; +} + +std::uint64_t clip::texture(double const Seconds) +{ + if (loading() || missing()) + return 0; + auto const &frames{*m_frames}; + long long total{0}; + for (auto const delay : frames.delays) + total += delay; + auto time{static_cast(std::floor(Seconds * 1000.0)) % total}; + int index{0}; + while (index + 1 < static_cast(frames.delays.size()) && time >= frames.delays[index]) + time -= frames.delays[index++]; + if (index == m_shown && m_texture != 0) + return m_texture; + auto *renderer{GfxRenderer ? GfxRenderer->GetImguiRenderer() : nullptr}; + if (renderer == nullptr) + return 0; + if (m_texture != 0) + renderer->Release_Image(m_texture); + m_texture = renderer->Create_Image(frames.rgba.data() + static_cast(frames.width) * frames.height * 4 * index, frames.width, frames.height); + m_shown = index; + return m_texture; +} + +} // namespace editor_clips diff --git a/editor/editorClips.hpp b/editor/editorClips.hpp new file mode 100644 index 00000000..e3a972bc --- /dev/null +++ b/editor/editorClips.hpp @@ -0,0 +1,56 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +namespace editor_clips +{ + +class clip +{ + public: + clip() = default; + ~clip(); + clip(clip const &) = delete; + clip &operator=(clip const &) = delete; + + void open(std::string const &Path); + void close(); + std::uint64_t texture(double const Seconds); + bool loading() const; + bool missing() const; + int width() const; + int height() const; + std::string const &path() const + { + return m_path; + } + + private: + struct frames + { + int width{0}; + int height{0}; + std::vector rgba; + std::vector delays; + std::atomic done{false}; + }; + std::string m_path; + std::shared_ptr m_frames; + std::uint64_t m_texture{0}; + int m_shown{-1}; +}; + +} // namespace editor_clips diff --git a/lang/editor_pl.po b/lang/editor_pl.po index 7e47b164..e67d736a 100644 --- a/lang/editor_pl.po +++ b/lang/editor_pl.po @@ -6204,3 +6204,195 @@ msgstr "połączenie tam" msgid "where a double crossover meets" msgstr "gdzie spotyka się podwójne przejście" +msgid "Tutorial" +msgstr "Samouczek" + +msgid "Seven short lessons with clips: the camera, laying track, curves, joining, switches, fixing, saving" +msgstr "Siedem krótkich lekcji z filmikami: kamera, układanie toru, łuki, łączenie, rozjazdy, poprawki, zapis" + +msgid "Lesson %d of %d" +msgstr "Lekcja %d z %d" + +msgid "Click: play it from the start" +msgstr "Klik: od początku" + +msgid "Mouse and keys" +msgstr "Mysz i klawisze" + +msgid "Try it" +msgstr "Spróbuj" + +msgid "< Back" +msgstr "< Wstecz" + +msgid "Next >" +msgstr "Dalej >" + +msgid "Close" +msgstr "Zamknij" + +msgid "Looking around" +msgstr "Rozglądanie się" + +msgid "Hold the right button and move the mouse: look around" +msgstr "Trzymaj prawy przycisk i ruszaj myszą: rozglądasz się" + +msgid "With the right button held: fly forwards, back and to the sides; E and Q: up and down" +msgstr "Z wciśniętym prawym przyciskiem: lot do przodu, do tyłu i na boki; E i Q: w górę i w dół" + +msgid "The top view, and back to 3D" +msgstr "Widok z góry i z powrotem 3D" + +msgid "In the top view: zoom in and out where the cursor points" +msgstr "W widoku z góry: przybliża i oddala tam, gdzie wskazuje kursor" + +msgid "In the top view: hold the wheel and drag to move the map" +msgstr "W widoku z góry: wciśnij kółko i przeciągnij, żeby przesunąć mapę" + +msgid "Fly to what is selected" +msgstr "Lot do zaznaczonego" + +msgid "Look around with the right button held" +msgstr "Rozejrzyj się z wciśniętym prawym przyciskiem" + +msgid "Switch to the top view" +msgstr "Przełącz na widok z góry" + +msgid "Laying track" +msgstr "Układanie toru" + +msgid "The Tracks field of work" +msgstr "Dział Tory" + +msgid "The lay tool" +msgstr "Narzędzie Układanie" + +msgid "A click on the ground: the first point; the next click: a straight up to it" +msgstr "Klik na ziemi: pierwszy punkt; następny klik: prosta do niego" + +msgid "Each next click lays a piece on from the end of the last one" +msgstr "Każdy kolejny klik układa kawałek dalej, od końca poprzedniego" + +msgid "Takes the last piece back" +msgstr "Cofa ostatni kawałek" + +msgid "Ends laying" +msgstr "Kończy układanie" + +msgid "Open the Tracks field of work" +msgstr "Otwórz dział Tory" + +msgid "Turn the lay tool on" +msgstr "Włącz układanie" + +msgid "Lay a track" +msgstr "Ułóż tor" + +msgid "Curves" +msgstr "Łuki" + +msgid "The radius of the curve, while laying on from an end" +msgstr "Promień łuku, gdy układasz dalej od końca toru" + +msgid "The shape: arc then straight, straight then arc, arc, straight" +msgstr "Kształt: łuk i prosta, prosta i łuk, sam łuk, prosta" + +msgid "Held: the direction turns in steps of 15 degrees" +msgstr "Trzymany: kierunek zmienia się co 15 stopni" + +msgid "Type the length of the next piece, Enter lays it" +msgstr "Wpisz długość następnego kawałka, Enter go układa" + +msgid "Alone it zooms, as everywhere" +msgstr "Samo kółko przybliża, jak wszędzie" + +msgid "Change the radius with Ctrl+wheel" +msgstr "Zmień promień: Ctrl+kółko" + +msgid "Change the shape with Shift+wheel" +msgstr "Zmień kształt: Shift+kółko" + +msgid "Joining tracks" +msgstr "Łączenie torów" + +msgid "Press on a free end (a magenta ring) and drag: a new track goes out of it" +msgstr "Wciśnij na wolnym końcu (różowe kółko) i przeciągnij: wychodzi z niego nowy tor" + +msgid "Let go on another free end: a curve joins the two tracks" +msgstr "Puść na innym wolnym końcu: łuk połączy oba tory" + +msgid "While over the other end: the radius of the joining curve" +msgstr "Nad drugim końcem: promień łuku, który je łączy" + +msgid "While over the other end: another way to join" +msgstr "Nad drugim końcem: inny sposób połączenia" + +msgid "Join two free ends" +msgstr "Połącz dwa wolne końce" + +msgid "The switch tool; the type of the switch is chosen in its list" +msgstr "Narzędzie Rozjazd; typ rozjazdu wybierasz z jego listy" + +msgid "Click a straight track: the switch goes into it" +msgstr "Kliknij prosty tor: w niego wejdzie rozjazd" + +msgid "Press on it and drag: along sets the direction, sideways the side" +msgstr "Wciśnij na nim i przeciągnij: wzdłuż ustala kierunek, w bok stronę" + +msgid "Back to selecting" +msgstr "Powrót do zaznaczania" + +msgid "Put a switch into a track" +msgstr "Wstaw rozjazd w tor" + +msgid "Selecting and fixing" +msgstr "Zaznaczanie i poprawki" + +msgid "The select tool" +msgstr "Narzędzie Zaznacz" + +msgid "Click a track: it is selected, with its points" +msgstr "Kliknij tor: zostaje zaznaczony, widać jego punkty" + +msgid "Drag a point: the end or the curve follows" +msgstr "Przeciągnij punkt: koniec albo łuk idzie za nim" + +msgid "Move, turn or scale with the gizmo" +msgstr "Przesuwanie, obracanie, skalowanie gizmem" + +msgid "A click on a track: what can be done with it" +msgstr "Klik na torze: co można z nim zrobić" + +msgid "Deletes the selected" +msgstr "Usuwa zaznaczone" + +msgid "Takes the last change back; Ctrl+Y brings it back again" +msgstr "Cofa ostatnią zmianę; Ctrl+Y ją przywraca" + +msgid "Select a track" +msgstr "Zaznacz tor" + +msgid "Take a change back" +msgstr "Cofnij zmianę" + +msgid "Saving and driving" +msgstr "Zapis i jazda" + +msgid "Saves the scenery" +msgstr "Zapisuje scenerię" + +msgid "Trainset in the toolbar: pick a locomotive from the list, click the track, then Place and drive" +msgstr "Skład na pasku narzędzi: wybierz lokomotywę z listy, kliknij tor, potem Postaw i jedź" + +msgid "Drive on the new track" +msgstr "Przejedź się po nowym torze" + +msgid "Lay track (L)" +msgstr "Układaj tor (L)" + +msgid "The switch tool (T)" +msgstr "Narzędzie Rozjazd (T)" + +msgid "The select tool (Esc)" +msgstr "Zaznaczanie (Esc)" +