#!/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()