📜 ⬆️ ⬇️

Arrow hours on CMake



When I got a new job, I had to at an accelerated pace learn new technologies for me that are used in this company. One of these technologies 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 has the ability to calculate mathematical expressions, write and read from files, run external processes and other interesting features, which made me think of using this language as the main PL and write something tangible on it. It will be a question of how I wrote an analog clock in cmake 2.8.

To be honest, at first I had the idea to check cmake for the possibility of I / O from standard streams. I wanted to learn how to read keystrokes, or, at worst, mouse events, which would make it possible to create some kind of interactive program, to write, for example, Tetris. The conclusion turned out to be quite simple:
  file (WRITE / dev / stdout "blabla") 
But I completely refused to read the standard stream, I could not read directly from events (/ dev / input / event4 or / dev / input / mice). So the idea to make Tetris was discarded and I decided to play around with the output, yet the ability to output directly to stdout attracted me more than the standard message () command.
')
I decided, since I can write directly to stdout, then I have to try writing escape sequences there . It would give rich possibilities: color output, cursor movement, screen cleaning and others. Fortunately, cmake was able to output non-printable characters — this is an ASCII operation of the string function, so I wrote the screen cleaning function:
 string (ASCII 27 ESCAPE)
 function (clrscr)
     file (WRITE / dev / stdout "$ {ESCAPE} [2J")
 endfunction (clrscr)

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

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

I began to solve these difficulties from the end. First, the line drawing algorithm was invented:
  1. Find the difference of 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 modulo 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, the functions of searching for the maximum and absolute value were needed. Since the 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 to substitute variables more beautifully everywhere, 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 search for an absolute value, we use the fact that cmake operates with strings and simply “bites off” a minus, if there is one.

When the macros were ready, when trying to calculate expressions for coordinates using the command
  math (EXPR <result> <expression>) 
I recognized the interesting nuances associated with the fact that cmake operates on strings, so, for example, the expression "$ {a} + $ {b}", in the case when b is negative, will not be calculated (as it may happen that something like 5 + -6, and such an expression is not valid). We managed to get around this nuance by a tricky rule — wherever a negative value of a variable can occur in a formula, add a leading 0 to it and put all this in brackets: "$ {a} + (0 $ {b})". The final line drawing function is:

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, an idea appeared to apply it somewhere (for example, “gash” the analogue clock). Before that, I did not know at all that something interesting could be done with all this. It turned out that almost everything is ready, it remains to draw the dial, get 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. Two more functions were missing: sine and cosine, for drawing a circle and drawing a line at a given angle.

The matter was complicated by the fact that sine and cosine have values ​​in the interval [0; 1], and cmake operates only with integer values, so it was decided to use a factor of 1000: find the sine and cosine times 1000, and in the expression where they are used to divide all by this factor.

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

I also wanted to have a TLD at least in the interval [-pi; 2 * pi], for this it was decided to convert the angle in radians to the right half-plane, making an amendment to the sign of the function. Technically, the geometrical meaning and formulas of the cast are here , so I don’t chew much. The final code of trigonometric functions turned out to be rather “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 matter of technique - to draw 12 numbers in a circle, spin in a loop and ask the system for time; when it has changed, erase the old arrows and draw new ones at the right angles. Time is obtained by running an external process:
  execute_process (COMMAND "date" "+% H% M% S" OUTPUT_VARIABLE time) 
select substrings from time and calculate the angles - in the framework of school mathematics.

The full code can be viewed on the githaba .
It was tested on cmake version 2.8.12.2, Ubuntu 12.04, 14.04.

Source: https://habr.com/ru/post/253813/


All Articles