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 }