font/sprite: rework `yQuads` and friends for better alignment with `draw_block` (#7488)

This improves "outer edge" alignment of octants and other elements drawn
using `yQuads` and friends with blocks drawn with `draw_block` -- this
should guarantee alignment along a continuous edge, but may result in a
1px overlap of opposing edges (such as a top half block followed by a
bottom half block with an odd cell height, they will both have the
center row filled).

This is very necessary since several block elements are needed to
complete the set of octants, since dedicated octant characters aren't
included when they would be redundant.

Fixes #7479 

<details>
<summary><b><code>Box.ppm</code> diff</b></summary>


![image](https://github.com/user-attachments/assets/aea667fd-446f-4b60-a220-cdd636093d05)

</details>

> [!NOTE]
> In the future I think we should have a unified single source of truth
for grid positions (divisions of the cell) to ensure this type of thing
can't happen with other characters, and also it would make a lot of the
code cleaner. For now this works though.
pull/7498/head
Mitchell Hashimoto 2025-05-30 19:29:45 -07:00 committed by GitHub
commit 6e69893f29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 55 additions and 35 deletions

View File

@ -2488,10 +2488,10 @@ fn draw_sextant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
if (sex.tl) self.rect(canvas, 0, 0, x_halfs[0], y_thirds[0]);
if (sex.tr) self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_thirds[0]);
if (sex.ml) self.rect(canvas, 0, y_thirds[0], x_halfs[0], y_thirds[1]);
if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[0], self.metrics.cell_width, y_thirds[1]);
if (sex.bl) self.rect(canvas, 0, y_thirds[1], x_halfs[0], self.metrics.cell_height);
if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[1], self.metrics.cell_width, self.metrics.cell_height);
if (sex.ml) self.rect(canvas, 0, y_thirds[1], x_halfs[0], y_thirds[2]);
if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[1], self.metrics.cell_width, y_thirds[2]);
if (sex.bl) self.rect(canvas, 0, y_thirds[3], x_halfs[0], self.metrics.cell_height);
if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[3], self.metrics.cell_width, self.metrics.cell_height);
}
fn draw_octant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
@ -2545,42 +2545,58 @@ fn draw_octant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void {
const oct = octants[cp - octant_min];
if (oct.@"1") self.rect(canvas, 0, 0, x_halfs[0], y_quads[0]);
if (oct.@"2") self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_quads[0]);
if (oct.@"3") self.rect(canvas, 0, y_quads[0], x_halfs[0], y_quads[1]);
if (oct.@"4") self.rect(canvas, x_halfs[1], y_quads[0], self.metrics.cell_width, y_quads[1]);
if (oct.@"5") self.rect(canvas, 0, y_quads[1], x_halfs[0], y_quads[2]);
if (oct.@"6") self.rect(canvas, x_halfs[1], y_quads[1], self.metrics.cell_width, y_quads[2]);
if (oct.@"7") self.rect(canvas, 0, y_quads[2], x_halfs[0], self.metrics.cell_height);
if (oct.@"8") self.rect(canvas, x_halfs[1], y_quads[2], self.metrics.cell_width, self.metrics.cell_height);
if (oct.@"3") self.rect(canvas, 0, y_quads[1], x_halfs[0], y_quads[2]);
if (oct.@"4") self.rect(canvas, x_halfs[1], y_quads[1], self.metrics.cell_width, y_quads[2]);
if (oct.@"5") self.rect(canvas, 0, y_quads[3], x_halfs[0], y_quads[4]);
if (oct.@"6") self.rect(canvas, x_halfs[1], y_quads[3], self.metrics.cell_width, y_quads[4]);
if (oct.@"7") self.rect(canvas, 0, y_quads[5], x_halfs[0], self.metrics.cell_height);
if (oct.@"8") self.rect(canvas, x_halfs[1], y_quads[5], self.metrics.cell_width, self.metrics.cell_height);
}
/// xHalfs[0] should be used as the right edge of a left-aligned half.
/// xHalfs[1] should be used as the left edge of a right-aligned half.
fn xHalfs(self: Box) [2]u32 {
const float_width: f64 = @floatFromInt(self.metrics.cell_width);
const half_width: u32 = @intFromFloat(@round(0.5 * float_width));
return .{ half_width, self.metrics.cell_width - half_width };
}
/// Use these values as such:
/// yThirds[0] bottom edge of the first third.
/// yThirds[1] top edge of the second third.
/// yThirds[2] bottom edge of the second third.
/// yThirds[3] top edge of the final third.
fn yThirds(self: Box) [4]u32 {
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
const one_third_height: u32 = @intFromFloat(@round(one_third * float_height));
const two_thirds_height: u32 = @intFromFloat(@round(two_thirds * float_height));
return .{
@as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2))),
@as(u32, @intFromFloat(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2)),
one_third_height,
self.metrics.cell_height - two_thirds_height,
two_thirds_height,
self.metrics.cell_height - one_third_height,
};
}
fn yThirds(self: Box) [2]u32 {
return switch (@mod(self.metrics.cell_height, 3)) {
0 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 },
1 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 + 1 },
2 => .{ self.metrics.cell_height / 3 + 1, 2 * self.metrics.cell_height / 3 },
else => unreachable,
};
}
// assume octants might be striped across multiple rows of cells. to maximize
// distance between excess pixellines, we want (1) an arbitrary region (there
// will be a pattern of 1'-3-1'-3-1'-3 no matter what), (2) discontiguous
// regions (0 and 2 or 1 and 3), and (3) an arbitrary three regions (there will
// be a pattern of 3-1-3-1-3-1 no matter what).
fn yQuads(self: Box) [3]u32 {
return switch (@mod(self.metrics.cell_height, 4)) {
0 => .{ self.metrics.cell_height / 4, 2 * self.metrics.cell_height / 4, 3 * self.metrics.cell_height / 4 },
1 => .{ self.metrics.cell_height / 4, 2 * self.metrics.cell_height / 4 + 1, 3 * self.metrics.cell_height / 4 },
2 => .{ self.metrics.cell_height / 4 + 1, 2 * self.metrics.cell_height / 4, 3 * self.metrics.cell_height / 4 + 1 },
3 => .{ self.metrics.cell_height / 4 + 1, 2 * self.metrics.cell_height / 4 + 1, 3 * self.metrics.cell_height / 4 },
else => unreachable,
/// Use these values as such:
/// yQuads[0] bottom edge of first quarter.
/// yQuads[1] top edge of second quarter.
/// yQuads[2] bottom edge of second quarter.
/// yQuads[3] top edge of third quarter.
/// yQuads[4] bottom edge of third quarter
/// yQuads[5] top edge of fourth quarter.
fn yQuads(self: Box) [6]u32 {
const float_height: f64 = @floatFromInt(self.metrics.cell_height);
const quarter_height: u32 = @intFromFloat(@round(0.25 * float_height));
const half_height: u32 = @intFromFloat(@round(0.50 * float_height));
const three_quarters_height: u32 = @intFromFloat(@round(0.75 * float_height));
return .{
quarter_height,
self.metrics.cell_height - three_quarters_height,
half_height,
self.metrics.cell_height - half_height,
three_quarters_height,
self.metrics.cell_height - quarter_height,
};
}
@ -2591,8 +2607,12 @@ fn draw_smooth_mosaic(
) !void {
const y_thirds = self.yThirds();
const top: f64 = 0.0;
const upper: f64 = @floatFromInt(y_thirds[0]);
const lower: f64 = @floatFromInt(y_thirds[1]);
// We average the edge positions for the y_thirds boundaries here
// rather than having to deal with varying alignments depending on
// the surrounding pieces. The most this will be off by is half of
// a pixel, so hopefully it's not noticeable.
const upper: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[0])) + @as(f64, @floatFromInt(y_thirds[1])));
const lower: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[2])) + @as(f64, @floatFromInt(y_thirds[3])));
const bottom: f64 = @floatFromInt(self.metrics.cell_height);
const left: f64 = 0.0;
const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2);

Binary file not shown.