Watch on CMake



When I got a new job, I had to quickly master the new technologies for me that are used in this company. One such technology was the cmake build system , which I had never encountered before.

This system has its own built-in language for writing assembly scripts. This very language interested me. Soon, I found out that it had the ability to calculate mathematical expressions, write and read from files, start external processes and other interesting features, which led me to think of using this language as the main language and writing something tangible on it. It will be about how I wrote the clock in cmake 2.8.

Honestly, at first I came up with the idea of ​​checking cmake for the possibility of input-output from standard streams. I wanted to learn how to read the pressed keys, or, at worst, mouse events, which would make it possible to make some kind of interactive program, write, for example, Tetris. The conclusion turned out to be quite simple:
file (WRITE / dev / stdout "blabla")
But cmake completely refused to read the standard stream, it also failed to read directly from the events (/ dev / input / event4 or / dev / input / mice). So the idea of ​​making tetris was discarded and I decided to play around with the output, nevertheless, the ability to output directly to stdout attracted me more than the standard message () command.

I decided, since I can write directly to stdout, I should try to write escape sequences there . This would give rich opportunities: color output, cursor movements, screen cleaning, and others. Fortunately, cmake turned out to be able to output non-printable characters - this is an ASCII operation of the string function, so I wrote a screen cleaning function:
string (ASCII 27 ESCAPE)
function (clrscr)
    file (WRITE / dev / stdout "$ {ESCAPE} [2J")
endfunction (clrscr)

Since the escape codes worked, then I decided to learn the next step in displaying text in arbitrary coordinates:
function (textXY XY MSG)
    file (WRITE / dev / stdout "$ {ESCAPE} [$ {Y}; $ {X} H $ {MSG}")
endfunction (textXY)

Well, the next logical continuation of this was the idea to write a line drawing function. Here already had to face the first difficulties:
  1. cmake evaluates the expression written to the string and its result is integer;
  2. functions in cmake do not return a value;
  3. I want to have a universal line drawing algorithm that does not depend on the location of the ends relative to each other;

I began to solve these difficulties from the end. First, a line drawing algorithm was invented:
  1. Find the difference in the coordinates of the ends Dx = x2-x1 and Dy = y2-y1 taking into account the minus (it will be needed for the direction);
  2. Find the maximum modulus delta Dmax = max (abs (Dx), abs (Dy));
  3. Run through the cycle i = 0..Dmax, at each step calculating the current coordinates using the formulas:
    	x = x1 + i * Dx / Dmax
    	y = y1 + i * Dy / Dmax

Secondly, we needed the functions of finding the maximum and absolute value. Since functions in cmake do not return values, I had to use macros. In macros, you can substitute both variables and values. It seemed to me that substituting variables everywhere is prettier, but the macro turns out to be too “hairy”, so in the future I began to use variable substitution only for the result.

Macro code
macro (max abm)
	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)


To find the absolute value, the fact is used that cmake operates with strings and simply “bites off” minus, if any.

When the macros were ready, when trying to calculate expressions for coordinates using the command
math (EXPR )
I realized the interesting nuances associated with the fact that cmake operates with strings, therefore, for example, the expression "$ {a} + $ {b}", in the case when b is negative, will not be calculated (because it may turn out that something like 5 + -6, but such an expression is not valid). We managed to circumvent this nuance by a tricky rule - wherever a negative value of a variable can be found in a formula, add a leading 0 to it and take all this in brackets: "$ {a} + (0 $ {b})". The resulting line drawing function is as follows:

Function code line (x1 y1 x2 y2 chr)
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)


After testing the line drawing function, the idea came up to apply it somewhere (for example, “gash” the clock). Before that, I did not know at all that you could do something interesting with all this. It turned out that almost everything is ready, it remains to draw the dial, get the time from the system, calculate the necessary angles, draw 3 lines at the right angles (hour, minute and second hands) and the clock will be ready. There were still 2 more functions missing: sine and cosine, for drawing a circle and drawing a line at a given angle.

The matter was complicated by the fact that the sine and cosine values ​​are in the range [0; 1], and cmake operates only with integer values, so it was decided to use the coefficient 1000: find the sine and cosine times 1000, and divide everything in the expression where they are applied at this ratio.

To implement trigonometric functions, their expansion in the Maclaurin series is applied . And again difficulties:
  1. I do not want to use too high degrees and factorials in the Maclaurin series;
  2. When using the first 2-3 members of the series, good approximations are obtained only in the interval [-pi / 2; pi / 2].

I wanted to have an ODZ at least in the interval [-pi; 2 * pi], for this it was decided to translate the angle in radians into the right half-plane, making correction for the sign of the function. Technically, there is a geometric meaning and reduction formulas , so I don’t chew much. The resulting code of trigonometric functions is pretty ugly:

Sine and cosine code
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)


After that, the rest was already a technical matter - draw 12 numbers in a circle, spin in a cycle and ask the system for time; when it has changed, erase the old arrows and draw new ones at the right angles. We get time through the launch of an external process:
 execute_process (COMMAND "date" "+% H% M% S" OUTPUT_VARIABLE time)
select substrings from time and calculate angles - in the framework of school mathematics.

The full code can be viewed on the github .
Tested on cmake version 2.8.12.2, Ubuntu 12.04, 14.04.

Also popular now: