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:
parent
ae600f4341
commit
de56496803
2 changed files with 135 additions and 0 deletions
|
|
@ -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 = .{
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue