[Из песочницы] Стрелочные часы на CMake

0f9b59429cb041cca2fe1f101321981c.pngКогда я устроился на новую работу, пришлось в ускоренном темпе осваивать новые для меня технологии, которые используются в данной компании. Одной из таких технологий стала система сборки cmake, с которой мне раньше не приходилось сталкиваться.

Эта система имеет свой встроенный язык для написания сборочных скриптов. Этот самый язык меня и заинтересовал. Вскоре я выяснил, что в нем есть возможность вычисления математических выражений, запись и чтение из файлов, запуск внешних процессов и другие интересные возможности, что навело меня на мысль воспользоваться этим языком в качестве основного ЯП и написать на нем что-нибудь осязаемое. Речь пойдет о том, как я писал стрелочные часы на языке cmake 2.8.Честно говоря, сначала мне в голову пришла идея проверить cmake на возможность ввода-вывода из стандартных потоков. Хотелось научиться считывать нажатые клавиши, ну или, на худой конец, события мыши, что позволило бы сделать какую-нибудь интерактивную программу, написать, к примеру, тетрис. С выводом оказалось все довольно просто:

file (WRITE /dev/stdout «blabla») А вот считывать стандартный поток cmake напрочь отказывался, читать напрямую из ивентов (/dev/input/event4 или /dev/input/mice) также не удавалось. Так что идея сделать тетрис была отброшена и я решил поиграться с выводом, всё-таки способность выводить напрямую в stdout меня привлекала больше, чем стандартная команда message ().Я решил, раз уж могу писать напрямую в stdout, то надо попробовать писать туда escape-последовательности. Это бы дало богатые возможности: цветной вывод, перемещения курсора, очистка экрана и другие. К счастью, в cmake оказалась возможность вывода непечатаемых символов — это операция ASCII функции string, так я написал функцию очистки экрана:

string (ASCII 27 ESCAPE) function (clrscr) file (WRITE /dev/stdout »${ESCAPE}[2J») endfunction (clrscr) Раз уж escape-коды заработали, то я решил следующим шагом научиться выводить текст в произвольных координатах: function (textXY X Y MSG) file (WRITE /dev/stdout »${ESCAPE}[${Y};${X}H${MSG}») endfunction (textXY) Ну и следующим логичным продолжением этого родилась мысль написать функцию рисования линии. Здесь уже пришлось столкнуться с первыми трудностями: cmake вычисляет выражение записанное в строку и результат его целочисленный; функции в cmake не возвращают значение; хочется иметь универсальный алгоритм рисования линии, не зависящий от расположения концов относительно друг друга; Решать эти трудности я начал с конца. Во-первых, был придуман алгоритм рисования линии: Найти разницу координат концов Dx=x2-x1 и Dy=y2-y1 с учетом минуса (он будет нужен для направления); Найти максимальную по модулю дельту Dmax = max (abs (Dx), abs (Dy)); Пробежать циклом i = 0…Dmax, на каждом шаге вычисляя текущие координаты по формулам: x = x1 + i * Dx / Dmax y = y1 + i * Dy / Dmax Во-вторых, нужны были функции поиска максимума и абсолютного значения. Так как функции в cmake значения не возвращают, то пришлось воспользоваться макросами. В макросы можно подставлять как переменные, так и значения. Мне показалось, что переменные везде подставлять красивее, но макрос получается слишком «волосатым», так что в дальнейшем я стал использовать подстановку переменной только для результата.Код макросов macro (max a b m) if (${a} LESS ${${b}}) set (${m} ${${b}}) else (${a} LESS ${${b}}) set (${m} ${${a}}) endif (${a} LESS ${${b}}) endmacro (max)

macro (abs a res) if (${a} LESS 0) string (LENGTH ${a} len) math (EXPR l1 »${len} — 1») string (SUBSTRING ${a} 1 ${l1} ${res}) else (${a} LESS 0) set (${res} ${a}) endif (${a} LESS 0) endmacro (abs) Для поиска абсолютного значения используется тот факт, что cmake оперирует строками и просто «откусывается» минус, если он есть.Когда макросы были готовы, то при попытке вычислять выражения для координат, используя команду

math (EXPR ) мною были осознаны интересные нюансы, связанные с тем, что cmake оперирует строками, поэтому, например, выражение »${a} + ${b}», в случае, когда b отрицательное — вычисляться не будет (т.к. может получится что-то вроде 5 + -6, а такое выражение не валидно). Этот нюанс удалось обойти хитрым правилом — везде, где в формуле может встретиться отрицательное значение переменной, добавлять к ней ведущий 0 и брать все это в скобки:»${a} + (0${b})». Итоговая функция рисования линии получилась такой: Код функции line (x1 y1×2 y2 chr) function (line x1 y1×2 y2 chr) math (EXPR Dx »${x2} — ${x1}») abs (${Dx} aDx) math (EXPR Dy »${y2} — ${y1}») abs (${Dy} aDy) max (aDx aDy Dmax) set (i 0) while (i LESS ${Dmax}) math (EXPR cx »${x1} + ${i} * (0${Dx}) / ${Dmax}») math (EXPR cy »${y1} + ${i} * (0${Dy}) / ${Dmax}») textXY (${cx} ${cy} ${chr}) math (EXPR i »${i} + 1») endwhile (i LESS ${Dmax}) endfunction (line) После тестирования функции рисования линии и появилась идея куда-то ее применить (например, «запилить» стрелочные часы). До этого я вообще не знал, что интересного можно со всем этим сделать. Оказалось, практически все готово, осталось нарисовать циферблат, получить время из системы, вычислить необходимые углы, нарисовать 3 линии под нужными углами (часовая, минутная и секундная стрелки) и часы будут готовы. Не хватало еще 2-х функций: синуса и косинуса, для рисования окружности и рисования линии под заданным углом.Дело осложнилось тем, что синус и косинус имеют значения в интервале [0;1], а cmake оперирует только целочисленными значениями, так что решено было использовать коэффициент 1000: находить синус и косинус умноженный на 1000, а в выражении, где они применяются делить все на этот коэффициент.

Для реализации тригонометрических функций применяется их разложение в ряд Маклорена. И снова трудности:

Не хочется использовать слишком высокие степени и факториалы в ряде Маклорена; При использовании 2–3 первых членов ряда хорошие приближения получаются только в интервале [-pi/2; pi/2]. Мне же хотелось иметь ОДЗ хотя бы в интервале [-pi; 2*pi], для этого было решено угол в радианах переводить в правую полуплоскость, делая поправку на знак функции. Технически тут геометрический смысл и формулы приведения, поэтому сильно не «разжевываю». Итоговый код тригонометрических функций получился довольно «страшненьким»: Код синуса и косинуса set (PI1000 3142) set (PI500 1571) set (_PI500 -1571) set (_2PI1000 6283)

macro (m_rad1000_4sin x res) math (EXPR rad1000 »(0${x}) * ${PI1000} / 180») if (rad1000 GREATER ${PI1000}) math (EXPR rad1000_ »${PI1000} — ${rad1000}») else (rad1000 GREATER ${PI1000}) set (rad1000_ ${rad1000}) endif (rad1000 GREATER ${PI1000}) if (rad1000_ GREATER ${PI500}) math (EXPR rad1000__ »${PI1000} — ${rad1000_}») else (rad1000_ GREATER ${PI500}) if (rad1000_ LESS ${_PI500}) abs (${rad1000_} abs_rad1000_) math (EXPR rad1000__ »${abs_rad1000_} — ${PI1000}») else (rad1000_ LESS ${_PI500}) set (rad1000__ ${rad1000_}) endif (rad1000_ LESS ${_PI500}) endif (rad1000_ GREATER ${PI500}) set (${res} ${rad1000__}) endmacro (m_rad1000_4sin)

macro (m_rad1000_4cos x res) math (EXPR rad1000 »(0${x}) * ${PI1000} / 180») if (rad1000 GREATER ${PI1000}) math (EXPR rad1000_ »${rad1000} — ${_2PI1000}») else (rad1000 GREATER ${PI1000}) set (rad1000_ ${rad1000}) endif (rad1000 GREATER ${PI1000}) set (${res} ${rad1000_}) endmacro (m_rad1000_4cos)

macro (sin1000 x res) m_rad1000_4sin (${x} r1000) math (EXPR ${res} »0${r1000} — (0${r1000}) * (0${r1000}) / 1000 * (0${r1000}) / 1000 / 6 + (0${r1000}) * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 / 120») endmacro (sin1000)

macro (cos1000 x res) m_rad1000_4cos (${x} r1000) unset (sign) if (r1000 GREATER ${PI500}) math (EXPR r1000_ »${PI1000} — ${r1000}») set (r1000 ${r1000_}) set (sign »0-») endif (r1000 GREATER ${PI500}) if (r1000 LESS ${_PI500}) math (EXPR r1000_ »${PI1000} + (0${r1000})») set (r1000 ${r1000_}) set (sign »0-») endif (r1000 LESS ${_PI500}) math (EXPR ${res} »${sign}(1000 — (0${r1000}) * (0${r1000}) / 1000 / 2 + (0${r1000}) * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 / 24 — (0${r1000}) * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 * (0${r1000}) / 1000 / 720)») endmacro (cos1000) После этого остальное уже было делом техники — нарисовать 12 чисел по кругу, крутиться в цикле и спрашивать у системы время; когда оно изменилось, стирать старые стрелки и рисовать новые под нужными углами. Время получаем через запуск внешнего процесса: execute_process (COMMAND «date» »+%H%M%S» OUTPUT_VARIABLE time) выделить подстроки из time и вычислить углы — в рамках школьной математики.Полный код можно посмотреть на гитхабе.Тестировалось на cmake version 2.8.12.2, Ubuntu 12.04, 14.04.

© Habrahabr.ru