17
0
mirror of https://github.com/MaSzyna-EU07/maszyna.git synced 2026-11-01 02:19:05 +01:00

feat(editor): icons over the 3d view: the tools of the field of work along its left edge (the edit modes of the surroundings, the track tools, the road tools with the lanes, the terrain tools with the orthophoto), the modes of the gizmo at its top, the axes of the world as the camera sees them in its top right corner, a click on one looking along it, with the top view under them; the icons drawn by editor/icons/editor_icons.py into a png which CMake writes out as an array built into the executable; the gizmo modes and the top view left out of the toolbar; Polish translation

This commit is contained in:
2026-10-09 11:54:47 +02:00
parent f2e73812d9
commit 645a54a739
9 changed files with 1049 additions and 23 deletions

176
editor/editorIcons.cpp Normal file
View File

@@ -0,0 +1,176 @@
/*
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/editorIcons.hpp"
#include "rendering/renderer.h"
#include "utilities/Globals.h"
#include "stb/stb_image.h"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <thread>
#include <vector>
namespace editor_icons
{
namespace
{
// editor_icons.png, written out by CMake
unsigned char const png[] = {
#include "editor_icons_png.inc"
};
int const cell{64}; // pixels of an icon in the png
int const columns{8};
int const rows{(static_cast<int>(icon::count) + columns - 1) / columns};
// the coverage of the icons, a byte a pixel, as they're in the png. empty if it couldn't be read
std::vector<std::uint8_t> const &coverage()
{
static std::vector<std::uint8_t> const pixels = []() {
std::vector<std::uint8_t> result;
// read in a thread of its own: the texture loader turns the images upside down for the threads which don't say otherwise,
// and what a thread says stays with it
std::thread reader([&result]() {
stbi_set_flip_vertically_on_load_thread(0);
int width{0}, height{0}, components{0};
auto *rgba{stbi_load_from_memory(png, static_cast<int>(sizeof(png)), &width, &height, &components, 4)};
if (rgba == nullptr)
return;
if (width == columns * cell && height >= rows * cell)
{
result.resize(static_cast<std::size_t>(width) * height);
for (std::size_t index = 0; index < result.size(); ++index)
result[index] = rgba[index * 4 + 3];
}
stbi_image_free(rgba);
});
reader.join();
return result;
}();
return pixels;
}
// the icons scaled down to the size they're drawn at, each with a pixel of empty space around it, so that the filtering
// doesn't pick up a neighbour
struct atlas
{
std::uint64_t texture{0};
int size{0}; // pixels of an icon
bool failed{false};
};
atlas g_atlas;
int stride(int const Size)
{
return Size + 2;
}
// part of the source pixel Pixel the target pixel Target of the size Scale covers
float overlap(int const Target, float const Scale, int const Pixel)
{
auto const start{std::max(Target * Scale, static_cast<float>(Pixel))};
auto const end{std::min((Target + 1) * Scale, static_cast<float>(Pixel + 1))};
return std::max(0.0f, end - start);
}
std::vector<std::uint8_t> scaled(int const Size)
{
auto const &source{coverage()};
auto const width{columns * stride(Size)};
auto const height{rows * stride(Size)};
std::vector<std::uint8_t> rgba(static_cast<std::size_t>(width) * height * 4, 255);
for (std::size_t index = 3; index < rgba.size(); index += 4)
rgba[index] = 0;
// each target pixel the average of the source pixels under it, the ones under its edges by the part they're under it
auto const scale{static_cast<float>(cell) / Size};
std::vector<float> across(static_cast<std::size_t>(Size) * cell);
for (int index = 0; index < static_cast<int>(icon::count); ++index)
{
auto const sourcex{(index % columns) * cell};
auto const sourcey{(index / columns) * cell};
for (int y = 0; y < cell; ++y)
{
for (int x = 0; x < Size; ++x)
{
float sum{0.0f};
for (int pixel = static_cast<int>(x * scale); pixel < std::min(cell, static_cast<int>(std::ceil((x + 1) * scale))); ++pixel)
sum += overlap(x, scale, pixel) * source[static_cast<std::size_t>(sourcey + y) * columns * cell + sourcex + pixel];
across[static_cast<std::size_t>(y) * Size + x] = sum / scale;
}
}
auto const targetx{(index % columns) * stride(Size) + 1};
auto const targety{(index / columns) * stride(Size) + 1};
for (int y = 0; y < Size; ++y)
{
for (int x = 0; x < Size; ++x)
{
float sum{0.0f};
for (int pixel = static_cast<int>(y * scale); pixel < std::min(cell, static_cast<int>(std::ceil((y + 1) * scale))); ++pixel)
sum += overlap(y, scale, pixel) * across[static_cast<std::size_t>(pixel) * Size + x];
rgba[(static_cast<std::size_t>(targety + y) * width + targetx + x) * 4 + 3] = static_cast<std::uint8_t>(std::clamp(sum / scale + 0.5f, 0.0f, 255.0f));
}
}
}
return rgba;
}
// the texture of the icons at the size, made again when the size changes
bool prepare(int const Size)
{
if (g_atlas.failed)
return false;
if (g_atlas.texture != 0 && g_atlas.size == Size)
return true;
auto *renderer{GfxRenderer ? GfxRenderer->GetImguiRenderer() : nullptr};
if (renderer == nullptr || coverage().empty())
{
g_atlas.failed = true;
return false;
}
if (g_atlas.texture != 0)
renderer->Release_Image(g_atlas.texture);
auto const rgba{scaled(Size)};
g_atlas.texture = renderer->Create_Image(rgba.data(), columns * stride(Size), rows * stride(Size));
g_atlas.size = Size;
// a renderer which can't show them gets no more requests
g_atlas.failed = (g_atlas.texture == 0);
return false == g_atlas.failed;
}
} // namespace
float size()
{
return std::round(20.0f * std::max(1.0f, Global.ui_scale));
}
bool draw(ImDrawList *List, icon const Icon, ImVec2 const &Center, ImU32 const Color)
{
// all at the one size, the texture is made for it
auto const pixels{static_cast<int>(size())};
if (List == nullptr || Icon >= icon::count || false == prepare(pixels))
return false;
auto const index{static_cast<int>(Icon)};
auto const width{static_cast<float>(columns * stride(pixels))};
auto const height{static_cast<float>(rows * stride(pixels))};
ImVec2 const uv0{((index % columns) * stride(pixels) + 1) / width, ((index / columns) * stride(pixels) + 1) / height};
ImVec2 const uv1{uv0.x + pixels / width, uv0.y + pixels / height};
// on whole pixels, the icon was scaled for them
ImVec2 const min{std::round(Center.x - pixels * 0.5f), std::round(Center.y - pixels * 0.5f)};
List->AddImage(static_cast<ImTextureID>(g_atlas.texture), min, ImVec2(min.x + pixels, min.y + pixels), uv0, uv1, Color);
return true;
}
} // namespace editor_icons

61
editor/editorIcons.hpp Normal file
View File

@@ -0,0 +1,61 @@
/*
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 "imgui/imgui.h"
// icons of the scenery editor. they're drawn by editor/icons/editor_icons.py into editor_icons.png, which CMake writes out
// as the bytes of an array built into the executable, so they're there without any file of the simulator
namespace editor_icons
{
// in the order of the cells of editor_icons.png
enum class icon
{
select,
insert,
brush,
area_fill,
copy_to_bank,
translate,
rotate,
scale,
local_space,
ortho,
perspective,
sculpt,
smooth,
chunks,
orthophoto,
road,
place,
lanes,
lay_track,
turnout,
straight,
curve,
signal,
objects,
vehicle,
profile,
speed,
joints,
infra,
gauge,
turntable,
count
};
// pixels of the side of an icon, at the scale of the user interface
float size();
// draws the icon in the colour given, centred on the point. returns: false if the renderer can't show images
bool draw(ImDrawList *List, icon const Icon, ImVec2 const &Center, ImU32 const Color);
} // namespace editor_icons

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,364 @@
#!/usr/bin/env python3
# draws the icons of the scenery editor into editor_icons.png, next to this script: white shapes on a transparent ground,
# 64 x 64 pixels each, 8 in a row, in the order of editor_icons::icon (editor/editorIcons.hpp).
# the editor tints them as it draws them; the png is built into the executable by CMake, see CMakeLists.txt.
# needs Pillow: python3 editor_icons.py
import math
import os
from PIL import Image, ImageDraw
CELL = 64
SUPER = 4 # drawn this many times bigger, then scaled down for smooth edges
S = CELL * SUPER
W = 18 # width of the lines, at the bigger size
COLUMNS = 8
def p(x, y):
# coordinates given on a 64 x 64 grid
return (x * SUPER, y * SUPER)
def line(d, points, width=W):
pts = [p(*q) for q in points]
d.line(pts, fill=255, width=width, joint="curve")
for q in (pts[0], pts[-1]):
r = width / 2
d.ellipse((q[0] - r, q[1] - r, q[0] + r, q[1] + r), fill=255)
def poly(d, points, outline=False, width=W):
pts = [p(*q) for q in points]
if outline:
line(d, points + [points[0]], width)
else:
d.polygon(pts, fill=255)
def circle(d, x, y, r, outline=False, width=W):
box = (p(x - r, y - r), p(x + r, y + r))
if outline:
d.ellipse(box, outline=255, width=width)
else:
d.ellipse(box, fill=255)
def rect(d, x0, y0, x1, y1, outline=False, width=W, radius=0):
box = (p(x0, y0), p(x1, y1))
if outline:
d.rounded_rectangle(box, radius=radius * SUPER, outline=255, width=width)
else:
d.rounded_rectangle(box, radius=radius * SUPER, fill=255)
def arrowhead(d, x, y, angle, size=9):
# filled head pointing at angle (degrees, 0 = right, 90 = down)
a = math.radians(angle)
tip = (x + math.cos(a) * size * 0.6, y + math.sin(a) * size * 0.6)
left = (x + math.cos(a + 2.5) * size, y + math.sin(a + 2.5) * size)
right = (x + math.cos(a - 2.5) * size, y + math.sin(a - 2.5) * size)
poly(d, [tip, left, right])
def arc(d, x, y, r, start, end, width=W):
box = (p(x - r, y - r), p(x + r, y + r))
d.arc(box, start, end, fill=255, width=width)
def dashed(d, a, b, dash=6, gap=4, width=W):
length = math.hypot(b[0] - a[0], b[1] - a[1])
if length == 0:
return
ux, uy = (b[0] - a[0]) / length, (b[1] - a[1]) / length
t = 0.0
while t < length:
e = min(t + dash, length)
line(d, [(a[0] + ux * t, a[1] + uy * t), (a[0] + ux * e, a[1] + uy * e)], width)
t = e + gap
# the icons, in the order of editor_icons::icon
def select(d):
poly(d, [(16, 8), (16, 50), (26, 40), (33, 56), (40, 53), (33, 37), (47, 37)])
def insert(d):
# a box, with a plus over its corner
poly(d, [(8, 26), (28, 16), (48, 26), (28, 36)], outline=True)
line(d, [(8, 26), (8, 46), (28, 56), (48, 46), (48, 26)])
line(d, [(28, 36), (28, 56)])
circle(d, 48, 14, 11)
def brush(d):
line(d, [(52, 8), (30, 32)], W + 4)
poly(d, [(26, 30), (34, 38), (24, 52), (8, 56), (12, 42)])
for x, y in ((44, 40), (52, 30), (50, 50)):
circle(d, x, y, 3)
def area_fill(d):
outline = [(8, 18), (34, 8), (56, 24), (50, 54), (16, 52)]
for i in range(len(outline)):
dashed(d, outline[i], outline[(i + 1) % len(outline)], 6, 5, W - 4)
for x, y in ((24, 38), (38, 30), (36, 44)):
poly(d, [(x, y - 9), (x + 6, y + 4), (x - 6, y + 4)])
def copy_to_bank(d):
rect(d, 18, 6, 46, 26, outline=True, radius=3)
line(d, [(32, 26), (32, 40)])
arrowhead(d, 32, 42, 90, 12)
line(d, [(8, 40), (8, 56), (56, 56), (56, 40)])
def translate(d):
line(d, [(32, 10), (32, 54)])
line(d, [(10, 32), (54, 32)])
arrowhead(d, 32, 8, 270, 13)
arrowhead(d, 32, 56, 90, 13)
arrowhead(d, 8, 32, 180, 13)
arrowhead(d, 56, 32, 0, 13)
def rotate(d):
# turning clockwise, the gap at the top
arc(d, 32, 34, 21, 300, 225)
a = math.radians(225)
arrowhead(d, 32 + math.cos(a) * 21, 34 + math.sin(a) * 21, 315, 15)
circle(d, 32, 34, 4)
def scale(d):
rect(d, 8, 30, 34, 56, radius=2)
for a, b in (((8, 8), (56, 8)), ((56, 8), (56, 56)), ((8, 8), (8, 22)), ((42, 56), (56, 56))):
dashed(d, a, b, 6, 5, W - 6)
line(d, [(30, 34), (48, 16)])
arrowhead(d, 50, 14, 315, 14)
def local_space(d):
# the axes turned with the model
o = (22, 42)
for angle, length in ((-20, 34), (-110, 30), (150, 18)):
a = math.radians(angle)
e = (o[0] + math.cos(a) * length, o[1] + math.sin(a) * length)
line(d, [o, e], W - 2)
arrowhead(d, e[0], e[1], angle, 11)
circle(d, o[0], o[1], 5)
def ortho(d):
# the plan seen from over it: a grid without perspective
rect(d, 8, 8, 56, 56, outline=True, width=W - 2)
for t in (24, 40):
line(d, [(t, 8), (t, 56)], W - 8)
line(d, [(8, t), (56, t)], W - 8)
def perspective(d):
# the grid going away
poly(d, [(20, 14), (44, 14), (58, 54), (6, 54)], outline=True, width=W - 2)
line(d, [(28, 14), (24, 54)], W - 8)
line(d, [(36, 14), (40, 54)], W - 8)
line(d, [(15, 30), (49, 30)], W - 8)
def sculpt(d):
poly(d, [(4, 56), (22, 26), (32, 38), (42, 22), (60, 56)])
line(d, [(50, 18), (50, 4)], W - 4)
arrowhead(d, 50, 3, 270, 11)
def smooth(d):
pts = [(x, 40 - 12 * math.sin((x - 6) / 52 * math.pi)) for x in range(6, 59, 2)]
line(d, pts)
line(d, [(6, 54), (58, 54)], W - 6)
circle(d, 32, 28, 14, outline=True, width=W - 8)
def chunks(d):
for x, y in ((6, 6), (33, 6), (6, 33)):
rect(d, x, y, x + 25, y + 25, radius=3)
rect(d, 33, 33, 58, 58, outline=True, width=W - 6, radius=3)
line(d, [(45.5, 39), (45.5, 52)], W - 8)
line(d, [(39, 45.5), (52, 45.5)], W - 8)
def orthophoto(d):
rect(d, 6, 10, 58, 54, outline=True, radius=4)
poly(d, [(10, 50), (26, 28), (36, 40), (42, 34), (54, 50)])
circle(d, 44, 21, 5)
def road(d):
line(d, [(24, 8), (8, 56)])
line(d, [(40, 8), (56, 56)])
dashed(d, (32, 8), (32, 56), 8, 6, W - 4)
def place(d):
# a pin standing on the place
poly(d, [(32, 58), (18, 34), (46, 34)])
circle(d, 32, 26, 15)
def lanes(d):
for x in (14, 50):
line(d, [(x, 6), (x, 58)], W - 4)
dashed(d, (32, 6), (32, 58), 7, 6, W - 6)
line(d, [(23, 52), (23, 22)], W - 6)
arrowhead(d, 23, 18, 270, 10)
line(d, [(41, 12), (41, 42)], W - 6)
arrowhead(d, 41, 46, 90, 10)
def lay_track(d):
# two rails on sleepers, going up to the right
for t in range(0, 6):
x, y = 10 + t * 9, 54 - t * 9
line(d, [(x - 8, y - 8), (x + 8, y + 8)], W - 8)
line(d, [(4, 50), (50, 4)], W - 4)
line(d, [(14, 60), (60, 14)], W - 4)
def switch(d):
line(d, [(18, 60), (18, 4)], W - 2)
line(d, [(34, 60), (34, 4)], W - 2)
pts = [(18 + 30 * (1 - math.cos(t / 20 * math.pi / 2)), 52 - t * 2.4) for t in range(0, 21)]
line(d, pts, W - 2)
pts = [(34 + 22 * (1 - math.cos(t / 20 * math.pi / 2)), 60 - t * 2.4) for t in range(0, 21)]
line(d, pts, W - 2)
def straight(d):
line(d, [(12, 52), (52, 12)])
rect(d, 4, 44, 20, 60, radius=2)
rect(d, 44, 4, 60, 20, radius=2)
def curve(d):
arc(d, 56, 56, 44, 180, 270)
dashed(d, (56, 56), (25, 25), 5, 4, W - 8)
circle(d, 12, 56, 6)
circle(d, 56, 12, 6)
circle(d, 25, 25, 6)
def signal(d):
line(d, [(32, 34), (32, 60)], W - 2)
line(d, [(20, 60), (44, 60)], W - 2)
rect(d, 20, 4, 44, 38, radius=10)
def objects(d):
# a post by the track with a board on it
line(d, [(24, 30), (24, 60)], W - 2)
rect(d, 10, 6, 38, 30, radius=3)
line(d, [(42, 60), (60, 60)], W - 6)
line(d, [(48, 52), (60, 52)], W - 6)
def vehicle(d):
poly(d, [(4, 22), (50, 22), (60, 32), (60, 46), (4, 46)])
circle(d, 16, 52, 6)
circle(d, 46, 52, 6)
line(d, [(4, 58), (60, 58)], W - 8)
def profile(d):
pts = [(6, 46), (22, 30), (38, 36), (58, 14)]
line(d, pts)
for q in pts:
circle(d, q[0], q[1], 5)
line(d, [(6, 58), (58, 58)], W - 8)
def speed(d):
arc(d, 32, 40, 24, 180, 360)
for a in (200, 240, 280, 320):
r = math.radians(a)
line(d, [(32 + math.cos(r) * 15, 40 + math.sin(r) * 15), (32 + math.cos(r) * 20, 40 + math.sin(r) * 20)], W - 8)
line(d, [(32, 40), (46, 24)], W - 2)
circle(d, 32, 40, 6)
def joints(d):
line(d, [(4, 26), (26, 26)])
line(d, [(38, 26), (60, 26)])
line(d, [(4, 40), (26, 40)])
line(d, [(38, 44), (60, 44)])
rect(d, 28, 14, 36, 54, outline=True, width=W - 10, radius=2)
def infra(d):
# a catenary mast with its arm and the wire hanging from it
line(d, [(14, 60), (14, 6)], W + 2)
line(d, [(14, 12), (54, 12)], W - 4)
line(d, [(14, 26), (46, 12)], W - 8)
line(d, [(44, 12), (44, 28)], W - 8)
pts = [(x, 30 + 0.012 * (x - 36) ** 2) for x in range(14, 61, 2)]
line(d, pts, W - 6)
def gauge(d):
# the outline of the structure gauge over the rails
line(d, [(12, 52), (12, 22), (22, 8), (42, 8), (52, 22), (52, 52)], W - 4)
line(d, [(4, 56), (60, 56)], W - 6)
for x in (24, 40):
rect(d, x - 3, 48, x + 3, 56)
def turntable(d):
# the pit with its bridge, and the tracks leading out of it
circle(d, 32, 32, 20, outline=True, width=W - 4)
line(d, [(18, 46), (46, 18)], W)
for a in (-90, -30, 30):
r = math.radians(a)
line(d, [(32 + math.cos(r) * 24, 32 + math.sin(r) * 24), (32 + math.cos(r) * 31, 32 + math.sin(r) * 31)], W - 4)
ICONS = [select, insert, brush, area_fill, copy_to_bank, translate, rotate, scale,
local_space, ortho, perspective, sculpt, smooth, chunks, orthophoto, road,
place, lanes, lay_track, switch, straight, curve, signal, objects,
vehicle, profile, speed, joints, infra, gauge, turntable]
def cutouts(name, d):
# the parts drawn in the colour of the ground: the plus of the insert, the hole of the pin, the lights of the signal
if name == "insert":
line(d, [(48, 8), (48, 20)], W - 8)
line(d, [(42, 14), (54, 14)], W - 8)
elif name == "place":
circle(d, 32, 26, 6)
elif name == "signal":
circle(d, 32, 14, 5)
circle(d, 32, 28, 5)
elif name == "vehicle":
rect(d, 10, 27, 20, 35)
rect(d, 26, 27, 36, 35)
rect(d, 42, 27, 50, 35)
def main():
rows = (len(ICONS) + COLUMNS - 1) // COLUMNS
atlas = Image.new("RGBA", (COLUMNS * CELL, rows * CELL), (255, 255, 255, 0))
for index, draw in enumerate(ICONS):
mask = Image.new("L", (S, S), 0)
d = ImageDraw.Draw(mask)
draw(d)
holes = Image.new("L", (S, S), 0)
cutouts(draw.__name__, ImageDraw.Draw(holes))
mask.paste(0, (0, 0), holes)
mask = mask.resize((CELL, CELL), Image.LANCZOS)
icon = Image.new("RGBA", (CELL, CELL), (255, 255, 255, 0))
icon.putalpha(mask)
atlas.paste(icon, ((index % COLUMNS) * CELL, (index // COLUMNS) * CELL))
atlas.save(os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor_icons.png"), optimize=True)
if __name__ == "__main__":
main()