diff --git a/src/core/command.zig b/src/core/command.zig index 5d912b2..19fe585 100644 --- a/src/core/command.zig +++ b/src/core/command.zig @@ -33,6 +33,9 @@ pub const DrawCommand = union(enum) { /// Draw a gradient-filled rectangle (fancy mode) gradient: GradientCommand, + /// Draw a filled triangle (for 3D graphics) + filled_triangle: FilledTriangleCommand, + /// Begin clipping to a rectangle clip: ClipCommand, @@ -154,6 +157,21 @@ pub const GradientCommand = struct { 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 pub const ClipCommand = struct { x: i32, @@ -224,6 +242,19 @@ pub fn clipEnd() DrawCommand { 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) pub fn roundedRect(x: i32, y: i32, w: u32, h: u32, color: Style.Color, radius: u8) DrawCommand { return .{ .rounded_rect = .{ diff --git a/src/render/software.zig b/src/render/software.zig index b442f54..ed10d0f 100644 --- a/src/render/software.zig +++ b/src/render/software.zig @@ -93,6 +93,7 @@ pub const SoftwareRenderer = struct { .rounded_rect_outline => |r| self.drawRoundedRectOutline(r), .shadow => |s| self.drawShadow(s), .gradient => |g| self.drawGradient(g), + .filled_triangle => |tri| self.drawFilledTriangle(tri), .clip => |c| self.pushClip(c), .clip_end => self.popClip(), .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 { 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), + .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, }; }