Skip to content

Commit 23134ee

Browse files
committed
Terminal/Primitive
1 parent e5f34f1 commit 23134ee

5 files changed

Lines changed: 355 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
- [`Utility`](modules/Math/Utility.mpp) - Floating-point comparison with epsilon tolerance
6868

6969
### 🔠 String
70+
- [`Encoding`](modules/String/Encoding.mpp) - UTF-8 and UTF-32 conversion and character display width utilities
7071
- [`Hash`](modules/String/Hash.mpp) - Hashing utilities for strings
7172

7273
### 🌐 Networking
@@ -76,6 +77,7 @@
7677
### 💻 Terminal
7778
- [`Canvas`](modules/Terminal/Canvas.mpp) - Terminal-based 2D drawing surface
7879
- [`Cursor`](modules/Terminal/Cursor.mpp) - Terminal cursor manipulation
80+
- [`Primitive`](modules/Terminal/Primitive.mpp) - Basic drawing primitives for terminal (lines, rectangles, circles, ellipses)
7981
- [`ProgressBar`](modules/Terminal/ProgressBar.mpp) - Dynamically updating terminal progress bar
8082
- [`RawTerminal`](modules/Terminal/RawTerminal.mpp) - Raw input handling (no buffering or echo)
8183
- [`Size`](modules/Terminal/Size.mpp) - Terminal size utilities

modules/Terminal/Primitive.mpp

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
export module CppUtils.Terminal.Primitive;
2+
3+
import std;
4+
import CppUtils.Terminal.Area;
5+
import CppUtils.Terminal.CharAttributes;
6+
import CppUtils.Container.Size;
7+
8+
export namespace CppUtils::Terminal
9+
{
10+
inline auto drawLine(WritableAreaView& view, const Container::Size2& start, const Container::Size2& end, const CharAttributes& c) -> void
11+
{
12+
// https://fr.wikipedia.org/wiki/Algorithme_de_trac%C3%A9_de_segment_de_Bresenham
13+
auto x0 = static_cast<int>(start.x());
14+
auto y0 = static_cast<int>(start.y());
15+
const auto x1 = static_cast<int>(end.x());
16+
const auto y1 = static_cast<int>(end.y());
17+
18+
const auto deltaX = std::abs(x1 - x0);
19+
const auto deltaY = -std::abs(y1 - y0);
20+
const auto stepX = x0 < x1 ? 1 : -1;
21+
const auto stepY = y0 < y1 ? 1 : -1;
22+
auto drift = deltaX + deltaY;
23+
24+
while (true)
25+
{
26+
view.setChar({static_cast<std::size_t>(x0), static_cast<std::size_t>(y0)}, c);
27+
if (x0 == x1 and y0 == y1)
28+
break;
29+
auto doubleDrift = 2 * drift;
30+
if (doubleDrift >= deltaY)
31+
{
32+
drift += deltaY;
33+
x0 += stepX;
34+
}
35+
if (doubleDrift <= deltaX)
36+
{
37+
drift += deltaX;
38+
y0 += stepY;
39+
}
40+
}
41+
}
42+
43+
inline auto drawRectangle(WritableAreaView& view, const Container::Size2& topLeft, const Container::Size2& size, bool outline, const CharAttributes& c) -> void
44+
{
45+
if (size.width() == 0 or size.height() == 0)
46+
return;
47+
48+
const auto x = topLeft.x();
49+
const auto y = topLeft.y();
50+
const auto width = size.width();
51+
const auto height = size.height();
52+
53+
if (outline)
54+
{
55+
const auto topRight = Container::Size2{x + width - 1, y};
56+
const auto bottomLeft = Container::Size2{x, y + height - 1};
57+
const auto bottomRight = Container::Size2{x + width - 1, y + height - 1};
58+
59+
drawLine(view, topLeft, topRight, c);
60+
drawLine(view, bottomLeft, bottomRight, c);
61+
drawLine(view, topLeft, bottomLeft, c);
62+
drawLine(view, topRight, bottomRight, c);
63+
}
64+
else
65+
for (auto currentY = y; currentY < y + height; ++currentY)
66+
for (auto currentX = x; currentX < x + width; ++currentX)
67+
view.setChar({currentX, currentY}, c);
68+
}
69+
70+
inline auto drawCircle(WritableAreaView& view, const Container::Size2& center, std::size_t radius, bool outline, const CharAttributes& c) -> void
71+
{
72+
// https://fr.wikipedia.org/wiki/Algorithme_de_trac%C3%A9_d%27arc_de_cercle_de_Bresenham
73+
auto centerX = static_cast<int>(center.x());
74+
auto centerY = static_cast<int>(center.y());
75+
auto signedRadius = static_cast<int>(radius);
76+
77+
auto x = 0;
78+
auto y = signedRadius;
79+
auto drift = 3 - 2 * signedRadius;
80+
81+
auto drawPoint = [&](int px, int py) {
82+
if (px >= 0 and py >= 0)
83+
view.setChar({static_cast<std::size_t>(px), static_cast<std::size_t>(py)}, c);
84+
};
85+
86+
while (y >= x)
87+
{
88+
if (outline)
89+
{
90+
drawPoint(centerX + x, centerY + y);
91+
drawPoint(centerX - x, centerY + y);
92+
drawPoint(centerX + x, centerY - y);
93+
drawPoint(centerX - x, centerY - y);
94+
drawPoint(centerX + y, centerY + x);
95+
drawPoint(centerX - y, centerY + x);
96+
drawPoint(centerX + y, centerY - x);
97+
drawPoint(centerX - y, centerY - x);
98+
}
99+
else
100+
{
101+
for (auto i = centerX - x; i <= centerX + x; ++i)
102+
{
103+
drawPoint(i, centerY + y);
104+
drawPoint(i, centerY - y);
105+
}
106+
for (auto i = centerX - y; i <= centerX + y; ++i)
107+
{
108+
drawPoint(i, centerY + x);
109+
drawPoint(i, centerY - x);
110+
}
111+
}
112+
113+
x++;
114+
if (drift > 0)
115+
{
116+
y--;
117+
drift = drift + 4 * (x - y) + 10;
118+
}
119+
else
120+
drift = drift + 4 * x + 6;
121+
}
122+
}
123+
124+
inline auto drawEllipse(WritableAreaView& view, const Container::Size2& p1, const Container::Size2& p2, bool outline, const CharAttributes& c) -> void
125+
{
126+
// https://fr.wikipedia.org/wiki/Algorithme_de_trac%C3%A9_d%27arc_de_cercle_de_Bresenham
127+
auto x0 = static_cast<int>(p1.x());
128+
auto y0 = static_cast<int>(p1.y());
129+
auto x1 = static_cast<int>(p2.x());
130+
auto y1 = static_cast<int>(p2.y());
131+
132+
if (x0 > x1)
133+
std::swap(x0, x1);
134+
if (y0 > y1)
135+
std::swap(y0, y1);
136+
137+
auto width = std::abs(x1 - x0);
138+
auto height = std::abs(y1 - y0);
139+
auto isHeightOdd = height & 1;
140+
141+
auto deltaX = 4 * (1.0 - width) * height * height;
142+
auto deltaY = 4.0 * (isHeightOdd + 1) * width * width;
143+
auto drift = deltaX + deltaY + isHeightOdd * width * width;
144+
auto doubleDrift = 0.0;
145+
146+
y0 += (height + 1) / 2;
147+
y1 = y0 - isHeightOdd;
148+
width *= 8 * width;
149+
isHeightOdd = 8 * height * height;
150+
151+
auto drawPoint = [&](int px, int py) {
152+
if (px >= 0 and py >= 0)
153+
view.setChar({static_cast<std::size_t>(px), static_cast<std::size_t>(py)}, c);
154+
};
155+
156+
do
157+
{
158+
if (outline)
159+
{
160+
drawPoint(x1, y0);
161+
drawPoint(x0, y0);
162+
drawPoint(x0, y1);
163+
drawPoint(x1, y1);
164+
}
165+
else
166+
for (auto x = x0; x <= x1; ++x)
167+
{
168+
drawPoint(x, y0);
169+
drawPoint(x, y1);
170+
}
171+
172+
doubleDrift = 2 * drift;
173+
if (doubleDrift <= deltaY)
174+
{
175+
++y0;
176+
--y1;
177+
drift += deltaY += width;
178+
}
179+
if (doubleDrift >= deltaX || 2 * drift > deltaY)
180+
{
181+
++x0;
182+
--x1;
183+
drift += deltaX += isHeightOdd;
184+
}
185+
}
186+
while (x0 <= x1);
187+
188+
while (y0 - y1 < height)
189+
{
190+
if (outline)
191+
{
192+
drawPoint(x0 - 1, y0);
193+
drawPoint(x1 + 1, y0++);
194+
drawPoint(x0 - 1, y1);
195+
drawPoint(x1 + 1, y1--);
196+
}
197+
else
198+
{
199+
for (auto x = x0 - 1; x <= x1 + 1; ++x)
200+
{
201+
drawPoint(x, y0);
202+
drawPoint(x, y1);
203+
}
204+
++y0;
205+
--y1;
206+
}
207+
}
208+
}
209+
}

modules/Terminal/Terminal.mpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export import CppUtils.Terminal.Cursor;
88
export import CppUtils.Terminal.Handle;
99
export import CppUtils.Terminal.Layout;
1010
export import CppUtils.Terminal.ProgressBar;
11+
export import CppUtils.Terminal.Primitive;
1112
export import CppUtils.Terminal.RawTerminal;
1213
export import CppUtils.Terminal.Size;
1314
export import CppUtils.Terminal.Spinner;

tests/Terminal/Primitive.mpp

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
export module CppUtils.UnitTests.Terminal.Primitive;
2+
3+
import std;
4+
import CppUtils;
5+
6+
namespace CppUtils::UnitTest::Terminal::Primitive
7+
{
8+
auto _ = TestSuite{"Terminal/Canvas/Primitive", {"UnitTest", "Terminal/Canvas"}, [](auto& suite) {
9+
using namespace std::literals;
10+
11+
suite.addTest("Draw Line", [&] {
12+
auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{10, 10}};
13+
auto view = CppUtils::Terminal::WritableAreaView{canvas, canvas.getViewport()};
14+
15+
CppUtils::Terminal::drawLine(view, {0, 0}, {9, 0}, CppUtils::Terminal::CharAttributes{'-'});
16+
CppUtils::Terminal::drawLine(view, {0, 0}, {0, 9}, CppUtils::Terminal::CharAttributes{'|'});
17+
CppUtils::Terminal::drawLine(view, {0, 0}, {9, 9}, CppUtils::Terminal::CharAttributes{'\\'});
18+
canvas.print();
19+
20+
suite.expectEqual(static_cast<std::uint32_t>(canvas.getChar({5, 0}).character), static_cast<std::uint32_t>(U'-'));
21+
suite.expectEqual(static_cast<std::uint32_t>(canvas.getChar({0, 5}).character), static_cast<std::uint32_t>(U'|'));
22+
suite.expectEqual(static_cast<std::uint32_t>(canvas.getChar({5, 5}).character), static_cast<std::uint32_t>(U'\\'));
23+
});
24+
25+
suite.addTest("Draw Rectangle Outline", [&] {
26+
auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{20, 10}};
27+
auto view = CppUtils::Terminal::WritableAreaView{canvas, canvas.getViewport()};
28+
29+
CppUtils::Terminal::drawRectangle(view, {1, 1}, {18, 8}, true, CppUtils::Terminal::CharAttributes{'#'});
30+
canvas.print();
31+
32+
suite.expectEqual(canvas.getChar({1, 1}).character, U'#');
33+
suite.expectEqual(canvas.getChar({18, 1}).character, U'#');
34+
suite.expectEqual(canvas.getChar({1, 8}).character, U'#');
35+
suite.expectEqual(canvas.getChar({18, 8}).character, U'#');
36+
37+
suite.expectEqual(canvas.getChar({10, 1}).character, U'#');
38+
suite.expectEqual(canvas.getChar({10, 5}).character, U' ');
39+
});
40+
41+
suite.addTest("Draw Rectangle Filled", [&] {
42+
auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{20, 10}};
43+
auto view = CppUtils::Terminal::WritableAreaView{canvas, canvas.getViewport()};
44+
45+
CppUtils::Terminal::drawRectangle(view, {1, 1}, {18, 8}, false, CppUtils::Terminal::CharAttributes{'#'});
46+
canvas.print();
47+
48+
suite.expectEqual(canvas.getChar({10, 0}).character, U' ');
49+
for (auto y = 1uz; y <= 8uz; ++y)
50+
suite.expectEqual(canvas.getChar({10, y}).character, U'#');
51+
suite.expectEqual(canvas.getChar({10, 9}).character, U' ');
52+
53+
suite.expectEqual(canvas.getChar({0, 5}).character, U' ');
54+
for (auto x = 1uz; x <= 18uz; ++x)
55+
suite.expectEqual(canvas.getChar({x, 5}).character, U'#');
56+
suite.expectEqual(canvas.getChar({19, 5}).character, U' ');
57+
});
58+
59+
suite.addTest("Draw Circle Outline", [&] {
60+
auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{11, 11}};
61+
auto view = CppUtils::Terminal::WritableAreaView{canvas, canvas.getViewport()};
62+
63+
CppUtils::Terminal::drawCircle(view, {5, 5}, 4, true, CppUtils::Terminal::CharAttributes{'O'});
64+
canvas.print();
65+
66+
suite.expectEqual(canvas.getChar({5, 0}).character, U' ');
67+
suite.expectEqual(canvas.getChar({5, 1}).character, U'O');
68+
for (auto y = 2uz; y <= 8uz; ++y)
69+
suite.expectEqual(canvas.getChar({5, y}).character, U' ');
70+
suite.expectEqual(canvas.getChar({5, 9}).character, U'O');
71+
suite.expectEqual(canvas.getChar({5, 10}).character, U' ');
72+
73+
suite.expectEqual(canvas.getChar({0, 5}).character, U' ');
74+
suite.expectEqual(canvas.getChar({1, 5}).character, U'O');
75+
for (auto x = 2uz; x <= 8uz; ++x)
76+
suite.expectEqual(canvas.getChar({x, 5}).character, U' ');
77+
suite.expectEqual(canvas.getChar({9, 5}).character, U'O');
78+
suite.expectEqual(canvas.getChar({10, 5}).character, U' ');
79+
80+
suite.expectEqual(canvas.getChar({0, 0}).character, U' ');
81+
suite.expectEqual(canvas.getChar({10, 10}).character, U' ');
82+
});
83+
84+
suite.addTest("Draw Circle Filled", [&] {
85+
auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{11, 11}};
86+
auto view = CppUtils::Terminal::WritableAreaView{canvas, canvas.getViewport()};
87+
88+
CppUtils::Terminal::drawCircle(view, {5, 5}, 4, false, CppUtils::Terminal::CharAttributes{'#'});
89+
canvas.print();
90+
91+
suite.expectEqual(canvas.getChar({5, 0}).character, U' ');
92+
for (auto y = 1uz; y <= 9uz; ++y)
93+
suite.expectEqual(canvas.getChar({5, y}).character, U'#');
94+
suite.expectEqual(canvas.getChar({5, 10}).character, U' ');
95+
96+
suite.expectEqual(canvas.getChar({0, 5}).character, U' ');
97+
for (auto x = 1uz; x <= 9uz; ++x)
98+
suite.expectEqual(canvas.getChar({x, 5}).character, U'#');
99+
suite.expectEqual(canvas.getChar({10, 5}).character, U' ');
100+
});
101+
102+
suite.addTest("Draw Ellipse Outline", [&] {
103+
auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{20, 10}};
104+
auto view = CppUtils::Terminal::WritableAreaView{canvas, canvas.getViewport()};
105+
106+
CppUtils::Terminal::drawEllipse(view, {1, 1}, {18, 8}, true, CppUtils::Terminal::CharAttributes{'O'});
107+
canvas.print();
108+
109+
suite.expectEqual(canvas.getChar({10, 0}).character, U' ');
110+
suite.expectEqual(canvas.getChar({10, 1}).character, U'O');
111+
for (auto y = 2uz; y <= 7uz; ++y)
112+
suite.expectEqual(canvas.getChar({10, y}).character, U' ');
113+
suite.expectEqual(canvas.getChar({10, 8}).character, U'O');
114+
suite.expectEqual(canvas.getChar({10, 9}).character, U' ');
115+
116+
suite.expectEqual(canvas.getChar({0, 5}).character, U' ');
117+
suite.expectEqual(canvas.getChar({1, 5}).character, U'O');
118+
for (auto x = 2uz; x <= 17uz; ++x)
119+
suite.expectEqual(canvas.getChar({x, 5}).character, U' ');
120+
suite.expectEqual(canvas.getChar({18, 5}).character, U'O');
121+
suite.expectEqual(canvas.getChar({19, 5}).character, U' ');
122+
});
123+
124+
suite.addTest("Draw Ellipse Filled", [&] {
125+
auto canvas = CppUtils::Terminal::Canvas{CppUtils::Container::Size2{20, 10}};
126+
auto view = CppUtils::Terminal::WritableAreaView{canvas, canvas.getViewport()};
127+
128+
CppUtils::Terminal::drawEllipse(view, {1, 1}, {18, 8}, false, CppUtils::Terminal::CharAttributes{'#'});
129+
canvas.print();
130+
131+
suite.expectEqual(canvas.getChar({10, 0}).character, U' ');
132+
for (auto y = 1uz; y <= 8uz; ++y)
133+
suite.expectEqual(canvas.getChar({10, y}).character, U'#');
134+
suite.expectEqual(canvas.getChar({10, 9}).character, U' ');
135+
136+
suite.expectEqual(canvas.getChar({0, 5}).character, U' ');
137+
for (auto x = 1uz; x <= 18uz; ++x)
138+
suite.expectEqual(canvas.getChar({x, 5}).character, U'#');
139+
suite.expectEqual(canvas.getChar({19, 5}).character, U' ');
140+
});
141+
}};
142+
}

tests/UnitTests.mpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export import CppUtils.UnitTests.String.Encoding;
3636
export import CppUtils.UnitTests.String.Utility;
3737
export import CppUtils.UnitTests.System.Error;
3838
export import CppUtils.UnitTests.Terminal.Canvas;
39+
export import CppUtils.UnitTests.Terminal.Primitive;
3940
export import CppUtils.UnitTests.Terminal;
4041
export import CppUtils.UnitTests.Thread.AsyncEventDispatcher;
4142
export import CppUtils.UnitTests.Thread.ScheduledEventDispatcher;

0 commit comments

Comments
 (0)