lib/zen/src/diagram/ascii/plan.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const diagram = @import("../root.zig");
4 const model = @import("model.zig");
5
6 /// Maximum scratch and output regions admitted by ASCII render storage.
7 pub const Limits = struct {
8 /// Maximum distinct categorical bound slots.
9 max_categories: usize,
10 /// Maximum stacked-series total slots.
11 max_stack_totals: usize,
12 /// Maximum width times height canvas bytes.
13 max_canvas_bytes: usize,
14 /// Maximum compacted row bytes, including line endings.
15 max_output_bytes: usize,
16 };
17
18 /// Exact, clamped storage demand for one document and option set.
19 pub const Plan = struct {
20 /// Admitted width after clamping.
21 width: usize,
22 /// Admitted height after clamping.
23 height: usize,
24 /// Categorical bound slots required by the document.
25 categories: usize,
26 /// Stacked-series total slots required by the document.
27 stack_totals: usize,
28 /// Exact canvas bytes required.
29 canvas_bytes: usize,
30 /// Maximum output bytes required before row compaction.
31 output_bytes: usize,
32
33 /// Measures document scratch and clamps dimensions without allocating.
34 pub fn inspect(
35 document: *const diagram.Document,
36 options: model.Options,
37 ) error{CapacityOverflow}!Plan {
38 const width = clamp(options.width, 24, 200);
39 const height = clamp(options.height, 8, 80);
40 const scratch = diagram.bounds.ScratchPlan.inspect(document);
41 return .{
42 .width = width,
43 .height = height,
44 .categories = scratch.categories,
45 .stack_totals = scratch.stack_totals,
46 .canvas_bytes = try multiplied(width, height),
47 .output_bytes = try multiplied(try added(width, 1), height),
48 };
49 }
50
51 /// Returns the smallest limits that admit this render.
52 pub fn exactLimits(self: Plan) Limits {
53 return .{
54 .max_categories = self.categories,
55 .max_stack_totals = self.stack_totals,
56 .max_canvas_bytes = self.canvas_bytes,
57 .max_output_bytes = self.output_bytes,
58 };
59 }
60
61 /// Rejects when any measured region exceeds supplied limits.
62 pub fn require(self: Plan, limits: Limits) model.Exhaustion!void {
63 if (self.categories > limits.max_categories) {
64 return error.AsciiCategoryCapacityExceeded;
65 }
66 if (self.stack_totals > limits.max_stack_totals) {
67 return error.AsciiStackTotalCapacityExceeded;
68 }
69 if (self.canvas_bytes > limits.max_canvas_bytes) {
70 return error.AsciiCanvasByteCapacityExceeded;
71 }
72 if (self.output_bytes > limits.max_output_bytes) {
73 return error.AsciiOutputByteCapacityExceeded;
74 }
75 }
76 };
77
78 fn added(left: usize, right: usize) error{CapacityOverflow}!usize {
79 return std.math.add(usize, left, right) catch error.CapacityOverflow;
80 }
81
82 fn multiplied(left: usize, right: usize) error{CapacityOverflow}!usize {
83 return std.math.mul(usize, left, right) catch error.CapacityOverflow;
84 }
85
86 fn clamp(value: usize, min: usize, max: usize) usize {
87 return @max(min, @min(max, value));
88 }
89
90 test "ASCII render plan exposes exact clamped regions" {
91 var document = diagram.Document{ .allocator = std.testing.allocator };
92 const plan = try Plan.inspect(&document, .{ .width = 1, .height = std.math.maxInt(usize) });
93 try std.testing.expectEqual(@as(usize, 24), plan.width);
94 try std.testing.expectEqual(@as(usize, 80), plan.height);
95 try std.testing.expectEqual(@as(usize, 1_920), plan.canvas_bytes);
96 try std.testing.expectEqual(@as(usize, 2_000), plan.output_bytes);
97 try std.testing.expectEqual(plan.exactLimits(), Limits{
98 .max_categories = 0,
99 .max_stack_totals = 0,
100 .max_canvas_bytes = 1_920,
101 .max_output_bytes = 2_000,
102 });
103 }
104
105 test "ASCII render plan rejects every resource limit" {
106 const plan = Plan{
107 .width = 24,
108 .height = 8,
109 .categories = 1,
110 .stack_totals = 1,
111 .canvas_bytes = 192,
112 .output_bytes = 200,
113 };
114 const exact = plan.exactLimits();
115 try plan.require(exact);
116 try std.testing.expectError(error.AsciiCategoryCapacityExceeded, plan.require(.{
117 .max_categories = 0,
118 .max_stack_totals = exact.max_stack_totals,
119 .max_canvas_bytes = exact.max_canvas_bytes,
120 .max_output_bytes = exact.max_output_bytes,
121 }));
122 try std.testing.expectError(error.AsciiStackTotalCapacityExceeded, plan.require(.{
123 .max_categories = exact.max_categories,
124 .max_stack_totals = 0,
125 .max_canvas_bytes = exact.max_canvas_bytes,
126 .max_output_bytes = exact.max_output_bytes,
127 }));
128 try std.testing.expectError(error.AsciiCanvasByteCapacityExceeded, plan.require(.{
129 .max_categories = exact.max_categories,
130 .max_stack_totals = exact.max_stack_totals,
131 .max_canvas_bytes = exact.max_canvas_bytes - 1,
132 .max_output_bytes = exact.max_output_bytes,
133 }));
134 try std.testing.expectError(error.AsciiOutputByteCapacityExceeded, plan.require(.{
135 .max_categories = exact.max_categories,
136 .max_stack_totals = exact.max_stack_totals,
137 .max_canvas_bytes = exact.max_canvas_bytes,
138 .max_output_bytes = exact.max_output_bytes - 1,
139 }));
140 }