feat: Añadir filledTriangle (rasterización scanline)

Nueva primitiva de dibujo para gráficos 3D:
- FilledTriangleCommand en command.zig
- Algoritmo scanline en software.zig
- Ordena vértices por Y, interpola bordes
- Soporta clipping correctamente

Base para logos 3D sólidos y widgets avanzados.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
R.Eugenio 2025-12-30 00:51:46 +01:00
parent ae600f4341
commit de56496803
2 changed files with 135 additions and 0 deletions

View file

@ -33,6 +33,9 @@ pub const DrawCommand = union(enum) {
/// Draw a gradient-filled rectangle (fancy mode) /// Draw a gradient-filled rectangle (fancy mode)
gradient: GradientCommand, gradient: GradientCommand,
/// Draw a filled triangle (for 3D graphics)
filled_triangle: FilledTriangleCommand,
/// Begin clipping to a rectangle /// Begin clipping to a rectangle
clip: ClipCommand, clip: ClipCommand,
@ -154,6 +157,21 @@ pub const GradientCommand = struct {
radius: u8 = 0, radius: u8 = 0,
}; };
/// Draw a filled triangle (for 3D graphics, logos, etc.)
pub const FilledTriangleCommand = struct {
/// First vertex
x1: i32,
y1: i32,
/// Second vertex
x2: i32,
y2: i32,
/// Third vertex
x3: i32,
y3: i32,
/// Fill color
color: Style.Color,
};
/// Begin clipping to a rectangle /// Begin clipping to a rectangle
pub const ClipCommand = struct { pub const ClipCommand = struct {
x: i32, x: i32,
@ -224,6 +242,19 @@ pub fn clipEnd() DrawCommand {
return .clip_end; return .clip_end;
} }
/// Create a filled triangle command
pub fn filledTriangle(x1: i32, y1: i32, x2: i32, y2: i32, x3: i32, y3: i32, color: Style.Color) DrawCommand {
return .{ .filled_triangle = .{
.x1 = x1,
.y1 = y1,
.x2 = x2,
.y2 = y2,
.x3 = x3,
.y3 = y3,
.color = color,
} };
}
/// Create a rounded rect command (fancy mode) /// Create a rounded rect command (fancy mode)
pub fn roundedRect(x: i32, y: i32, w: u32, h: u32, color: Style.Color, radius: u8) DrawCommand { pub fn roundedRect(x: i32, y: i32, w: u32, h: u32, color: Style.Color, radius: u8) DrawCommand {
return .{ .rounded_rect = .{ return .{ .rounded_rect = .{

View file

@ -93,6 +93,7 @@ pub const SoftwareRenderer = struct {
.rounded_rect_outline => |r| self.drawRoundedRectOutline(r), .rounded_rect_outline => |r| self.drawRoundedRectOutline(r),
.shadow => |s| self.drawShadow(s), .shadow => |s| self.drawShadow(s),
.gradient => |g| self.drawGradient(g), .gradient => |g| self.drawGradient(g),
.filled_triangle => |tri| self.drawFilledTriangle(tri),
.clip => |c| self.pushClip(c), .clip => |c| self.pushClip(c),
.clip_end => self.popClip(), .clip_end => self.popClip(),
.nop => {}, .nop => {},
@ -477,6 +478,97 @@ pub const SoftwareRenderer = struct {
} }
} }
/// Draw a filled triangle using scanline algorithm
fn drawFilledTriangle(self: *Self, tri: Command.FilledTriangleCommand) void {
const clip = self.getClip();
// Sort vertices by Y coordinate (p0.y <= p1.y <= p2.y)
var p0 = [2]i32{ tri.x1, tri.y1 };
var p1 = [2]i32{ tri.x2, tri.y2 };
var p2 = [2]i32{ tri.x3, tri.y3 };
// Bubble sort by Y
if (p0[1] > p1[1]) {
const tmp = p0;
p0 = p1;
p1 = tmp;
}
if (p1[1] > p2[1]) {
const tmp = p1;
p1 = p2;
p2 = tmp;
}
if (p0[1] > p1[1]) {
const tmp = p0;
p0 = p1;
p1 = tmp;
}
// Early exit if triangle is degenerate (all same Y)
if (p0[1] == p2[1]) return;
// Calculate inverse slopes for edge interpolation
const total_height = p2[1] - p0[1];
const top_height = p1[1] - p0[1];
const bottom_height = p2[1] - p1[1];
// Draw scanlines from top to bottom
var y = p0[1];
while (y <= p2[1]) : (y += 1) {
// Skip if outside clip region
if (y < clip.y or y >= clip.y + @as(i32, @intCast(clip.h))) {
continue;
}
// Calculate X coordinates for this scanline
var x_left: i32 = undefined;
var x_right: i32 = undefined;
// Progress along the long edge (p0 to p2)
const t_long: f32 = @as(f32, @floatFromInt(y - p0[1])) / @as(f32, @floatFromInt(total_height));
const x_long = p0[0] + @as(i32, @intFromFloat(@as(f32, @floatFromInt(p2[0] - p0[0])) * t_long));
// Progress along the short edges
var x_short: i32 = undefined;
if (y < p1[1]) {
// Upper half: interpolate p0 to p1
if (top_height == 0) {
x_short = p0[0];
} else {
const t_short: f32 = @as(f32, @floatFromInt(y - p0[1])) / @as(f32, @floatFromInt(top_height));
x_short = p0[0] + @as(i32, @intFromFloat(@as(f32, @floatFromInt(p1[0] - p0[0])) * t_short));
}
} else {
// Lower half: interpolate p1 to p2
if (bottom_height == 0) {
x_short = p1[0];
} else {
const t_short: f32 = @as(f32, @floatFromInt(y - p1[1])) / @as(f32, @floatFromInt(bottom_height));
x_short = p1[0] + @as(i32, @intFromFloat(@as(f32, @floatFromInt(p2[0] - p1[0])) * t_short));
}
}
// Ensure left < right
if (x_long < x_short) {
x_left = x_long;
x_right = x_short;
} else {
x_left = x_short;
x_right = x_long;
}
// Clip X to clip region
if (x_left < clip.x) x_left = clip.x;
if (x_right >= clip.x + @as(i32, @intCast(clip.w))) x_right = clip.x + @as(i32, @intCast(clip.w)) - 1;
// Draw horizontal line
if (x_left <= x_right) {
const w: u32 = @intCast(x_right - x_left + 1);
self.framebuffer.fillRect(x_left, y, w, 1, tri.color);
}
}
}
fn pushClip(self: *Self, c: Command.ClipCommand) void { fn pushClip(self: *Self, c: Command.ClipCommand) void {
if (self.clip_depth >= self.clip_stack.len) return; if (self.clip_depth >= self.clip_stack.len) return;
@ -543,6 +635,18 @@ fn commandBounds(cmd: DrawCommand) ?Rect {
); );
}, },
.gradient => |g| Rect.init(g.x, g.y, g.w, g.h), .gradient => |g| Rect.init(g.x, g.y, g.w, g.h),
.filled_triangle => |tri| blk: {
const min_x = @min(@min(tri.x1, tri.x2), tri.x3);
const min_y = @min(@min(tri.y1, tri.y2), tri.y3);
const max_x = @max(@max(tri.x1, tri.x2), tri.x3);
const max_y = @max(@max(tri.y1, tri.y2), tri.y3);
break :blk Rect.init(
min_x,
min_y,
@intCast(@max(1, max_x - min_x + 1)),
@intCast(@max(1, max_y - min_y + 1)),
);
},
.clip, .clip_end, .nop => null, .clip, .clip_end, .nop => null,
}; };
} }