feat: FilledCircle primitive (v0.24.0)

- Add FilledCircleCommand in command.zig
- Implement drawFilledCircle using Midpoint Circle Algorithm (Bresenham)
- Integer-only arithmetic (efficient, no sqrt/trig)
- Scanline filling with horizontal symmetry
- Add commandBounds for dirty region optimization
- Update CHANGELOG with v0.23.0 (FilledTriangle) and v0.24.0 (FilledCircle)

🤖 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 13:29:57 +01:00
parent de56496803
commit d8f04f85bc
3 changed files with 121 additions and 0 deletions

View file

@ -42,6 +42,8 @@
| 2025-12-19 | v0.22.0 | ⭐ AutoComplete: focus system integration, getTextInput(), first_frame guard |
| 2025-12-19 | v0.22.1 | ⭐ Text Metrics: ctx.measureText/measureTextToCursor para fuentes TTF de ancho variable |
| 2025-12-19 | v0.22.2 | Cursor blink rate: 500ms→300ms (más responsive durante edición) |
| 2025-12-30 | v0.23.0 | ⭐ FilledTriangle primitive: scanline rasterizer for 3D graphics |
| 2025-12-30 | v0.24.0 | ⭐ FilledCircle primitive: Midpoint Circle Algorithm (Bresenham) |
---
@ -64,3 +66,23 @@ Widget de tabla avanzada con schema, CRUD, sorting, lookup, multi-select, search
- **Text Metrics**: Nuevo sistema ctx.measureText() para posicionamiento correcto del cursor con fuentes TTF
- **Cursor**: Velocidad de parpadeo aumentada (500ms→300ms) para mejor feedback durante edición
→ Archivos: `context.zig`, `text_input.zig`, `autocomplete.zig`
### v0.23.0-v0.24.0 - Primitivas Gráficas 2D (2025-12-30)
Nuevas primitivas para gráficos 2D y mascotas animadas:
- **FilledTriangle** (v0.23.0):
- Rasterización por scanlines con interpolación de bordes
- Soporte para backface culling y Z-sorting (3D)
- Clipping integrado con sistema de clip rects
- Uso: logos 3D, iconos, formas geométricas
- **FilledCircle** (v0.24.0):
- Algoritmo Midpoint Circle (Bresenham)
- Solo aritmética entera (eficiente, sin sqrt/trig)
- Relleno por scanlines horizontales simétricos
- Uso: mascotas, avatares, UI orgánica, gráficos
**Aplicación:** Mascota "Zcat" en zsimifactu (aparece tras 15s de inactividad)
→ Archivos: `core/command.zig`, `render/software.zig`
→ Doc: `zsimifactu/docs/PLAN_CIRCULOS_Y_ZCAT_2025-12-30.md`

View file

@ -36,6 +36,9 @@ pub const DrawCommand = union(enum) {
/// Draw a filled triangle (for 3D graphics)
filled_triangle: FilledTriangleCommand,
/// Draw a filled circle (for organic shapes, icons)
filled_circle: FilledCircleCommand,
/// Begin clipping to a rectangle
clip: ClipCommand,
@ -172,6 +175,19 @@ pub const FilledTriangleCommand = struct {
color: Style.Color,
};
/// Draw a filled circle (for organic shapes, mascots, icons)
/// Uses Midpoint Circle Algorithm for efficient rasterization.
pub const FilledCircleCommand = struct {
/// Center X coordinate
cx: i32,
/// Center Y coordinate
cy: i32,
/// Radius in pixels
radius: u16,
/// Fill color
color: Style.Color,
};
/// Begin clipping to a rectangle
pub const ClipCommand = struct {
x: i32,
@ -255,6 +271,18 @@ pub fn filledTriangle(x1: i32, y1: i32, x2: i32, y2: i32, x3: i32, y3: i32, colo
} };
}
/// Create a filled circle command
/// Uses Midpoint Circle Algorithm for efficient scanline rasterization.
/// Perfect for organic shapes, mascots, icons, and UI elements.
pub fn filledCircle(cx: i32, cy: i32, radius: u16, color: Style.Color) DrawCommand {
return .{ .filled_circle = .{
.cx = cx,
.cy = cy,
.radius = radius,
.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 = .{

View file

@ -94,6 +94,7 @@ pub const SoftwareRenderer = struct {
.shadow => |s| self.drawShadow(s),
.gradient => |g| self.drawGradient(g),
.filled_triangle => |tri| self.drawFilledTriangle(tri),
.filled_circle => |cir| self.drawFilledCircle(cir),
.clip => |c| self.pushClip(c),
.clip_end => self.popClip(),
.nop => {},
@ -569,6 +570,67 @@ pub const SoftwareRenderer = struct {
}
}
/// Draw a filled circle using Midpoint Circle Algorithm (Bresenham)
/// Efficient: uses only integer arithmetic (no sqrt, no trig)
/// Fills by drawing horizontal scanlines between symmetric octants
fn drawFilledCircle(self: *Self, cir: Command.FilledCircleCommand) void {
const clip = self.getClip();
const cx = cir.cx;
const cy = cir.cy;
const radius: i32 = @intCast(cir.radius);
if (radius <= 0) return;
// Midpoint Circle Algorithm
var x: i32 = 0;
var y: i32 = radius;
var d: i32 = 3 - 2 * radius;
// Helper to draw a horizontal line with clipping
const drawHLine = struct {
fn draw(fb: *Framebuffer, clip_rect: Rect, y_pos: i32, x_start: i32, x_end: i32, color: Color) void {
// Skip if outside vertical clip
if (y_pos < clip_rect.y or y_pos >= clip_rect.y + @as(i32, @intCast(clip_rect.h))) return;
// Clip horizontally
var x1 = x_start;
var x2 = x_end;
if (x1 > x2) {
const tmp = x1;
x1 = x2;
x2 = tmp;
}
if (x1 < clip_rect.x) x1 = clip_rect.x;
if (x2 >= clip_rect.x + @as(i32, @intCast(clip_rect.w))) x2 = clip_rect.x + @as(i32, @intCast(clip_rect.w)) - 1;
if (x1 <= x2) {
const w: u32 = @intCast(x2 - x1 + 1);
fb.fillRect(x1, y_pos, w, 1, color);
}
}
}.draw;
while (y >= x) {
// Draw horizontal lines for all 8 octants (4 lines cover all)
// Top and bottom (wide)
drawHLine(self.framebuffer, clip, cy - y, cx - x, cx + x, cir.color);
drawHLine(self.framebuffer, clip, cy + y, cx - x, cx + x, cir.color);
// Middle (tall)
drawHLine(self.framebuffer, clip, cy - x, cx - y, cx + y, cir.color);
drawHLine(self.framebuffer, clip, cy + x, cx - y, cx + y, cir.color);
// Update decision variable
if (d < 0) {
d = d + 4 * x + 6;
} else {
d = d + 4 * (x - y) + 10;
y -= 1;
}
x += 1;
}
}
fn pushClip(self: *Self, c: Command.ClipCommand) void {
if (self.clip_depth >= self.clip_stack.len) return;
@ -647,6 +709,15 @@ fn commandBounds(cmd: DrawCommand) ?Rect {
@intCast(@max(1, max_y - min_y + 1)),
);
},
.filled_circle => |cir| blk: {
const r: i32 = @intCast(cir.radius);
break :blk Rect.init(
cir.cx - r,
cir.cy - r,
@intCast(r * 2 + 1),
@intCast(r * 2 + 1),
);
},
.clip, .clip_end, .nop => null,
};
}