- PVSM.RU - https://www.pvsm.ru -

Когда я устроился на новую работу, пришлось в ускоренном темпе осваивать новые для меня технологии, которые используются в данной компании. Одной из таких технологий стала система сборки cmake [1], с которой мне раньше не приходилось сталкиваться.
Эта система имеет свой встроенный язык для написания сборочных скриптов. Этот самый язык меня и заинтересовал. Вскоре я выяснил, что в нем есть возможность вычисления математических выражений, запись и чтение из файлов, запуск внешних процессов и другие интересные возможности, что навело меня на мысль воспользоваться этим языком в качестве основного ЯП и написать на нем что-нибудь осязаемое. Речь пойдет о том, как я писал стрелочные часы на языке cmake 2.8.
Честно говоря, сначала мне в голову пришла идея проверить cmake на возможность ввода-вывода из стандартных потоков. Хотелось научиться считывать нажатые клавиши, ну или, на худой конец, события мыши, что позволило бы сделать какую-нибудь интерактивную программу, написать, к примеру, тетрис. С выводом оказалось все довольно просто:
file(WRITE /dev/stdout "blabla")
А вот считывать стандартный поток cmake напрочь отказывался, читать напрямую из ивентов (/dev/input/event4 или /dev/input/mice) также не удавалось. Так что идея сделать тетрис была отброшена и я решил поиграться с выводом, всё-таки способность выводить напрямую в stdout меня привлекала больше, чем стандартная команда message().
Я решил, раз уж могу писать напрямую в stdout, то надо попробовать писать туда escape-последовательности [2]. Это бы дало богатые возможности: цветной вывод, перемещения курсора, очистка экрана и другие. К счастью, в 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)
Ну и следующим логичным продолжением этого родилась мысль написать функцию рисования линии. Здесь уже пришлось столкнуться с первыми трудностями:
Решать эти трудности я начал с конца. Во-первых, был придуман алгоритм рисования линии:
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 <result> <expression>)
мною были осознаны интересные нюансы, связанные с тем, что cmake оперирует строками, поэтому, например, выражение "${a} + ${b}", в случае, когда b отрицательное — вычисляться не будет (т.к. может получится что-то вроде 5 + -6, а такое выражение не валидно). Этот нюанс удалось обойти хитрым правилом — везде, где в формуле может встретиться отрицательное значение переменной, добавлять к ней ведущий 0 и брать все это в скобки: "${a} + (0${b})". Итоговая функция рисования линии получилась такой:
function(line x1 y1 x2 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, а в выражении, где они применяются делить все на этот коэффициент.
Для реализации тригонометрических функций применяется их разложение в ряд Маклорена [3]. И снова трудности:
Мне же хотелось иметь ОДЗ хотя бы в интервале [-pi; 2*pi], для этого было решено угол в радианах переводить в правую полуплоскость, делая поправку на знак функции. Технически тут геометрический смысл и формулы приведения [4], поэтому сильно не «разжевываю». Итоговый код тригонометрических функций получился довольно «страшненьким»:
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 и вычислить углы — в рамках школьной математики.
Полный код можно посмотреть на гитхабе [5].
Тестировалось на cmake version 2.8.12.2, Ubuntu 12.04, 14.04.
Автор: arjunarus
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/86683
Ссылки в тексте:
[1] cmake: http://www.cmake.org/
[2] escape-последовательности: https://en.wikipedia.org/wiki/ANSI_escape_code
[3] ряд Маклорена: https://ru.wikipedia.org/wiki/%D0%A0%D1%8F%D0%B4_%D0%A2%D0%B5%D0%B9%D0%BB%D0%BE%D1%80%D0%B0#.D0.A0.D1.8F.D0.B4.D1.8B_.D0.9C.D0.B0.D0.BA.D0.BB.D0.BE.D1.80.D0.B5.D0.BD.D0.B0_.D0.BD.D0.B5.D0.BA.D0.BE.D1.82.D0.BE.D1.80.D1.8B.D1.85_.D1.84.D1.83.D0.BD.D0.BA.D1.86.D0.B8.D0.B9
[4] геометрический смысл и формулы приведения: https://ru.wikipedia.org/wiki/%D0%A2%D1%80%D0%B8%D0%B3%D0%BE%D0%BD%D0%BE%D0%BC%D0%B5%D1%82%D1%80%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8#.D0.93.D0.B5.D0.BE.D0.BC.D0.B5.D1.82.D1.80.D0.B8.D1.87.D0.B5.D1.81.D0.BA.D0.BE.D0.B5_.D0.BE.D0.BF.D1.80.D0.B5.D0.B4.D0.B5.D0.BB.D0.B5.D0.BD.D0.B8.D0.B5
[5] гитхабе: https://github.com/Arjunarus/CMakeExp
[6] Источник: http://habrahabr.ru/post/253813/
Нажмите здесь для печати.