445311ee
feat(font): glyph atlas with row-based packing
a73x 2026-04-08 06:20
diff --git a/src/font.zig b/src/font.zig index cd75d12..ff94d0a 100644 --- a/src/font.zig +++ b/src/font.zig @@ -140,6 +140,93 @@ pub const Face = struct { } }; pub const GlyphUV = struct { u0: f32, v0: f32, u1: f32, v1: f32, width: u32, height: u32, bearing_x: i32, bearing_y: i32, advance_x: i32, }; pub const Atlas = struct { alloc: std.mem.Allocator, width: u32, height: u32, pixels: []u8, // R8 cursor_x: u32, cursor_y: u32, row_height: u32, cache: std.AutoHashMap(u21, GlyphUV), dirty: bool, pub fn init(alloc: std.mem.Allocator, width: u32, height: u32) !Atlas { const pixels = try alloc.alloc(u8, @as(usize, width) * @as(usize, height)); @memset(pixels, 0); return .{ .alloc = alloc, .width = width, .height = height, .pixels = pixels, .cursor_x = 0, .cursor_y = 0, .row_height = 0, .cache = std.AutoHashMap(u21, GlyphUV).init(alloc), .dirty = true, }; } pub fn deinit(self: *Atlas) void { self.alloc.free(self.pixels); self.cache.deinit(); } pub fn getOrInsert(self: *Atlas, face: *Face, codepoint: u21) !GlyphUV { if (self.cache.get(codepoint)) |uv| return uv; const glyph = try face.rasterize(codepoint); defer face.freeGlyph(glyph); if (self.cursor_x + glyph.width > self.width) { self.cursor_x = 0; self.cursor_y += self.row_height; self.row_height = 0; } if (self.cursor_y + glyph.height > self.height) { return error.AtlasFull; } var y: u32 = 0; while (y < glyph.height) : (y += 1) { const src = glyph.bitmap.ptr + @as(usize, y) * glyph.width; const dst = self.pixels.ptr + (@as(usize, self.cursor_y + y) * self.width) + self.cursor_x; @memcpy(dst[0..glyph.width], src[0..glyph.width]); } const uv = GlyphUV{ .u0 = @as(f32, @floatFromInt(self.cursor_x)) / @as(f32, @floatFromInt(self.width)), .v0 = @as(f32, @floatFromInt(self.cursor_y)) / @as(f32, @floatFromInt(self.height)), .u1 = @as(f32, @floatFromInt(self.cursor_x + glyph.width)) / @as(f32, @floatFromInt(self.width)), .v1 = @as(f32, @floatFromInt(self.cursor_y + glyph.height)) / @as(f32, @floatFromInt(self.height)), .width = glyph.width, .height = glyph.height, .bearing_x = glyph.bearing_x, .bearing_y = glyph.bearing_y, .advance_x = glyph.advance_x, }; try self.cache.put(codepoint, uv); self.cursor_x += glyph.width; if (glyph.height > self.row_height) self.row_height = glyph.height; self.dirty = true; return uv; } }; test "lookupMonospace returns a valid font path" { var lookup = try lookupMonospace(std.testing.allocator); defer lookup.deinit(std.testing.allocator); @@ -162,3 +249,24 @@ test "Face rasterizes glyph 'M'" { try std.testing.expect(glyph.height > 0); try std.testing.expect(glyph.bitmap.len == @as(usize, glyph.width) * @as(usize, glyph.height)); } test "Atlas packs multiple glyphs and returns UVs" { var lookup = try lookupMonospace(std.testing.allocator); defer lookup.deinit(std.testing.allocator); var face = try Face.init(std.testing.allocator, lookup.path, lookup.index, 14); defer face.deinit(); var atlas = try Atlas.init(std.testing.allocator, 512, 512); defer atlas.deinit(); const uv_m = try atlas.getOrInsert(&face, 'M'); const uv_a = try atlas.getOrInsert(&face, 'a'); try std.testing.expect(uv_m.u0 >= 0.0 and uv_m.u1 <= 1.0); try std.testing.expect(uv_a.u0 >= 0.0 and uv_a.u1 <= 1.0); const uv_m2 = try atlas.getOrInsert(&face, 'M'); try std.testing.expectEqual(uv_m.u0, uv_m2.u0); try std.testing.expectEqual(uv_m.v0, uv_m2.v0); }