lib/zen/src/diagram/ascii/canvas.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 pub const Canvas = struct {
4 width: usize,
5 height: usize,
6 cells: []u8,
7
8 pub fn init(cells: []u8, width: usize, height: usize) Canvas {
9 std.debug.assert(cells.len == width * height);
10 @memset(cells, ' ');
11 return .{
12 .width = width,
13 .height = height,
14 .cells = cells,
15 };
16 }
17
18 pub fn set(self: *Canvas, x: usize, y: usize, value: u8) void {
19 if (x >= self.width or y >= self.height) return;
20 self.cells[y * self.width + x] = value;
21 }
22
23 pub fn fillRect(
24 self: *Canvas,
25 x0: usize,
26 y0: usize,
27 x1: usize,
28 y1: usize,
29 value: u8,
30 ) void {
31 const left = @min(x0, x1);
32 const right = @max(x0, x1);
33 const top = @min(y0, y1);
34 const bottom = @max(y0, y1);
35 var y = top;
36 while (y <= bottom) : (y += 1) {
37 var x = left;
38 while (x <= right) : (x += 1) self.set(x, y, value);
39 }
40 }
41
42 pub fn writeText(self: *Canvas, x: usize, y: usize, value: []const u8) void {
43 if (y >= self.height) return;
44 var index: usize = 0;
45 while (index < value.len and x + index < self.width) : (index += 1) {
46 const byte = value[index];
47 self.set(x + index, y, if (byte >= 32 and byte < 127) byte else '?');
48 }
49 }
50
51 pub fn writeCentered(
52 self: *Canvas,
53 center: usize,
54 y: usize,
55 value: []const u8,
56 ) void {
57 if (value.len == 0 or y >= self.height) return;
58 const start = if (center > value.len / 2) center - value.len / 2 else 0;
59 self.writeText(start, y, value);
60 }
61
62 pub fn drawLine(
63 self: *Canvas,
64 x0: usize,
65 y0: usize,
66 x1: usize,
67 y1: usize,
68 value: u8,
69 ) void {
70 const steps = @max(distance(x0, x1), distance(y0, y1));
71 if (steps == 0) {
72 self.set(x0, y0, value);
73 return;
74 }
75 var step_index: usize = 0;
76 while (step_index <= steps) : (step_index += 1) {
77 const t = @as(f64, @floatFromInt(step_index)) /
78 @as(f64, @floatFromInt(steps));
79 const x = roundClamp(
80 lerp(@floatFromInt(x0), @floatFromInt(x1), t),
81 0,
82 self.width - 1,
83 );
84 const y = roundClamp(
85 lerp(@floatFromInt(y0), @floatFromInt(y1), t),
86 0,
87 self.height - 1,
88 );
89 self.set(x, y, value);
90 }
91 }
92
93 pub fn drawBox(self: *Canvas, x0: usize, y0: usize, x1: usize, y1: usize) void {
94 const left = @min(x0, x1);
95 const right = @max(x0, x1);
96 const top = @min(y0, y1);
97 const bottom = @max(y0, y1);
98 if (right <= left or bottom <= top) return;
99 var x = left;
100 while (x <= right) : (x += 1) {
101 self.set(x, top, '-');
102 self.set(x, bottom, '-');
103 }
104 var y = top;
105 while (y <= bottom) : (y += 1) {
106 self.set(left, y, '|');
107 self.set(right, y, '|');
108 }
109 self.set(left, top, '+');
110 self.set(right, top, '+');
111 self.set(left, bottom, '+');
112 self.set(right, bottom, '+');
113 }
114
115 pub fn writeOutput(self: *Canvas, output: []u8) []u8 {
116 std.debug.assert(output.len >= (self.width + 1) * self.height);
117 var output_index: usize = 0;
118 var y: usize = 0;
119 while (y < self.height) : (y += 1) {
120 var end = self.width;
121 while (end > 0 and self.cells[y * self.width + end - 1] == ' ') : (end -= 1) {}
122 @memcpy(
123 output[output_index..][0..end],
124 self.cells[y * self.width .. y * self.width + end],
125 );
126 output_index += end;
127 output[output_index] = '\n';
128 output_index += 1;
129 }
130 return output[0..output_index];
131 }
132 };
133
134 pub fn distance(a: usize, b: usize) usize {
135 return if (a > b) a - b else b - a;
136 }
137
138 pub fn roundClamp(value: f64, min: usize, max: usize) usize {
139 const min_float: f64 = @floatFromInt(min);
140 const max_float: f64 = @floatFromInt(max);
141 return @intFromFloat(@round(@max(min_float, @min(max_float, value))));
142 }
143
144 fn lerp(a: f64, b: f64, t: f64) f64 {
145 return a + (b - a) * t;
146 }
147
148 test "ASCII canvas compacts rows to exact output bytes" {
149 var cells: [12]u8 = undefined;
150 var output: [15]u8 = undefined;
151 var canvas = Canvas.init(&cells, 4, 3);
152 canvas.writeText(0, 0, "AB");
153 canvas.set(3, 1, 'X');
154 try std.testing.expectEqualStrings("AB\n X\n\n", canvas.writeOutput(&output));
155 }