17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-10-23 14:55:15 +02:00

feat(editor): Help > Tutorial instead of First steps: seven lessons (looking around, laying track, curves, joining, switches, selecting and fixing, saving and driving), each with a looping clip from lang/editor_tutorial/<lesson>.gif played from the start on a click, the mouse buttons and keys drawn as pictures beside what they do, ticks that fill in when the user does it, a button that turns the right tool on, Back / Next and a dot per lesson that turns green when it is done

This commit is contained in:
maj00r
2026-10-15 17:00:40 +02:00
parent 6d0423b58a
commit fc3f391e19
7 changed files with 663 additions and 30 deletions

View File

@@ -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"

View File

@@ -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<std::size_t>(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<input> inputs;
std::vector<goal> goals;
trial action;
};
static std::vector<lesson> 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<std::size_t>(Goal.step)]; });
};
m_tutorial_page = std::clamp(m_tutorial_page, 0, static_cast<int>(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<int>(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<ImTextureID>(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<std::size_t>(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<int>(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<int>(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)
{

View File

@@ -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<bool, static_cast<std::size_t>(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);

View File

@@ -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)

119
editor/editorClips.cpp Normal file
View File

@@ -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 <algorithm>
#include <cmath>
#include <fstream>
#include <iterator>
#include <thread>
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<clip::frames>()};
m_frames = frames;
std::thread([frames, Path]() {
stbi_set_flip_vertically_on_load_thread(0);
std::ifstream file(Path, std::ios::binary);
std::vector<unsigned char> const bytes{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
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<int>(bytes.size()), &delays, &width, &height, &count, &components, 4)};
if (rgba != nullptr && width > 0 && height > 0 && count > 0)
{
frames->rgba.assign(rgba, rgba + static_cast<std::size_t>(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<long long>(std::floor(Seconds * 1000.0)) % total};
int index{0};
while (index + 1 < static_cast<int>(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<std::size_t>(frames.width) * frames.height * 4 * index, frames.width, frames.height);
m_shown = index;
return m_texture;
}
} // namespace editor_clips

56
editor/editorClips.hpp Normal file
View File

@@ -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 <atomic>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
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<std::uint8_t> rgba;
std::vector<int> delays;
std::atomic<bool> done{false};
};
std::string m_path;
std::shared_ptr<frames> m_frames;
std::uint64_t m_texture{0};
int m_shown{-1};
};
} // namespace editor_clips

View File

@@ -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)"