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 }