diff --git a/CHANGELOG.md b/CHANGELOG.md index ea2dbaa..e6ccc6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/src/core/command.zig b/src/core/command.zig index 19fe585..6ab2cce 100644 --- a/src/core/command.zig +++ b/src/core/command.zig @@ -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 = .{ diff --git a/src/render/software.zig b/src/render/software.zig index ed10d0f..0fb59f2 100644 --- a/src/render/software.zig +++ b/src/render/software.zig @@ -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, }; }