lib/gui/src/paint/cpu/geometry.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2
3 const gui = @import("../../root.zig");
4 const paint_root = @import("../root.zig");
5
6 const Command = paint_root.command.Command;
7 const Rect = gui.layout.Rect;
8
9 pub const Bounds = struct {
10 x0: u32,
11 y0: u32,
12 x1: u32,
13 y1: u32,
14 };
15
16 pub const Region = struct {
17 x: u32 = 0,
18 y: u32 = 0,
19 width: u32 = 0,
20 height: u32 = 0,
21
22 pub fn full(width: u32, height: u32) Region {
23 return .{ .width = width, .height = height };
24 }
25
26 pub fn pixelCount(self: Region) usize {
27 return @as(usize, self.width) * @as(usize, self.height);
28 }
29
30 pub fn clamped(self: Region, width: u32, height: u32) Region {
31 if (self.x >= width or self.y >= height) return .{ .x = self.x, .y = self.y };
32 return .{
33 .x = self.x,
34 .y = self.y,
35 .width = @min(self.width, width - self.x),
36 .height = @min(self.height, height - self.y),
37 };
38 }
39 };
40
41 pub fn clippedBounds(width: u32, height: u32, paint: Command, region: Region) ?Bounds {
42 const bounds = paint_root.command.bounds(paint);
43 const region_rect = regionRect(region);
44 const x0 = floorClamp(@max(@max(bounds.x, paint.clip.x), region_rect.x), width);
45 const y0 = floorClamp(@max(@max(bounds.y, paint.clip.y), region_rect.y), height);
46 const x1 = ceilClamp(@min(@min(bounds.x + bounds.width, paint.clip.x + paint.clip.width), region_rect.x + region_rect.width), width);
47 const y1 = ceilClamp(@min(@min(bounds.y + bounds.height, paint.clip.y + paint.clip.height), region_rect.y + region_rect.height), height);
48 if (x0 >= x1 or y0 >= y1) return null;
49 return .{ .x0 = x0, .y0 = y0, .x1 = x1, .y1 = y1 };
50 }
51
52 pub fn regionRect(region: Region) Rect {
53 return .{
54 .x = @floatFromInt(region.x),
55 .y = @floatFromInt(region.y),
56 .width = @floatFromInt(region.width),
57 .height = @floatFromInt(region.height),
58 };
59 }
60
61 pub fn floorClamp(value: f32, limit: u32) u32 {
62 if (std.math.isNan(value) or value <= 0) return 0;
63 if (!std.math.isFinite(value)) return limit;
64 const floored = @floor(value);
65 if (floored >= @as(f32, @floatFromInt(limit))) return limit;
66 return @intFromFloat(floored);
67 }
68
69 pub fn ceilClamp(value: f32, limit: u32) u32 {
70 if (std.math.isNan(value) or value <= 0) return 0;
71 if (!std.math.isFinite(value)) return limit;
72 const ceiled = @ceil(value);
73 if (ceiled >= @as(f32, @floatFromInt(limit))) return limit;
74 return @intFromFloat(ceiled);
75 }
76
77 pub fn inside(rect: Rect, x: f32, y: f32) bool {
78 return x >= rect.x and y >= rect.y and x < rect.x + rect.width and y < rect.y + rect.height;
79 }
80
81 pub fn insideRounded(rect: Rect, radius: f32, x: f32, y: f32) bool {
82 if (!inside(rect, x, y)) return false;
83 const clamped = clampedRadius(rect, radius);
84 if (clamped <= 0) return true;
85 const inner_x0 = rect.x + clamped;
86 const inner_y0 = rect.y + clamped;
87 const inner_x1 = rect.x + rect.width - clamped;
88 const inner_y1 = rect.y + rect.height - clamped;
89 const cx = @max(inner_x0, @min(x, inner_x1));
90 const cy = @max(inner_y0, @min(y, inner_y1));
91 const dx = x - cx;
92 const dy = y - cy;
93 return dx * dx + dy * dy <= clamped * clamped;
94 }
95
96 pub fn roundedRectOutsideDistance(rect: Rect, radius: f32, x: f32, y: f32) f32 {
97 const clamped = clampedRadius(rect, radius);
98 const inner_x0 = rect.x + clamped;
99 const inner_y0 = rect.y + clamped;
100 const inner_x1 = rect.x + rect.width - clamped;
101 const inner_y1 = rect.y + rect.height - clamped;
102 const cx = @max(inner_x0, @min(x, inner_x1));
103 const cy = @max(inner_y0, @min(y, inner_y1));
104 const dx = x - cx;
105 const dy = y - cy;
106 return @max(@sqrt(dx * dx + dy * dy) - clamped, 0);
107 }
108
109 pub fn clampedRadius(rect: Rect, radius: f32) f32 {
110 return @min(@max(radius, 0), @min(rect.width, rect.height) * 0.5);
111 }
112
113 pub fn inset(rect: Rect, value: f32) Rect {
114 return .{
115 .x = rect.x + value,
116 .y = rect.y + value,
117 .width = @max(rect.width - value * 2, 0),
118 .height = @max(rect.height - value * 2, 0),
119 };
120 }
121
122 pub fn finiteRects(rect: Rect, clip: Rect) bool {
123 return !std.math.isNan(rect.x) and !std.math.isNan(rect.y) and
124 !std.math.isNan(rect.width) and !std.math.isNan(rect.height) and
125 !std.math.isNan(clip.x) and !std.math.isNan(clip.y) and
126 !std.math.isNan(clip.width) and !std.math.isNan(clip.height);
127 }
128
129 test "floorClamp and ceilClamp bound non-finite and negative values" {
130 try std.testing.expectEqual(@as(u32, 0), floorClamp(-3.5, 10));
131 try std.testing.expectEqual(@as(u32, 0), floorClamp(std.math.nan(f32), 10));
132 try std.testing.expectEqual(@as(u32, 10), floorClamp(400, 10));
133 try std.testing.expectEqual(@as(u32, 4), ceilClamp(3.5, 10));
134 try std.testing.expectEqual(@as(u32, 0), ceilClamp(std.math.inf(f32) * -1, 10));
135 try std.testing.expectEqual(@as(u32, 10), ceilClamp(std.math.inf(f32), 10));
136 try std.testing.expectEqual(@as(u32, 10), floorClamp(std.math.inf(f32), 10));
137 try std.testing.expectEqual(@as(u32, 0), floorClamp(std.math.inf(f32) * -1, 10));
138 }
139
140 test "clippedBounds culls positive-infinite geometry instead of leaking outside the region" {
141 const inf = std.math.inf(f32);
142 const region = Region{ .x = 3, .y = 2, .width = 4, .height = 4 };
143 const command = Command{
144 .kind = .fill,
145 .rect = .{ .x = inf, .y = 1, .width = 5, .height = 5 },
146 .clip = .{ .x = 0, .y = 0, .width = 10, .height = 8 },
147 .color = .{ .r = 1, .g = 2, .b = 3, .a = 255 },
148 };
149 try std.testing.expectEqual(@as(?Bounds, null), clippedBounds(10, 8, command, region));
150
151 const clipped_away = Command{
152 .kind = .fill,
153 .rect = .{ .x = 1, .y = 1, .width = 5, .height = 5 },
154 .clip = .{ .x = inf, .y = 0, .width = 10, .height = 8 },
155 .color = .{ .r = 1, .g = 2, .b = 3, .a = 255 },
156 };
157 try std.testing.expectEqual(@as(?Bounds, null), clippedBounds(10, 8, clipped_away, region));
158 }
159
160 test "insideRounded keeps straight bands and rejects corners" {
161 const rect = Rect{ .x = 0, .y = 0, .width = 10, .height = 10 };
162 try std.testing.expect(insideRounded(rect, 3, 5, 0.1));
163 try std.testing.expect(!insideRounded(rect, 3, 0.2, 0.2));
164 try std.testing.expect(insideRounded(rect, 3, 3, 3));
165 }