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:
parent
de56496803
commit
d8f04f85bc
3 changed files with 121 additions and 0 deletions
22
CHANGELOG.md
22
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.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.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-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
|
- **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
|
- **Cursor**: Velocidad de parpadeo aumentada (500ms→300ms) para mejor feedback durante edición
|
||||||
→ Archivos: `context.zig`, `text_input.zig`, `autocomplete.zig`
|
→ 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`
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ pub const DrawCommand = union(enum) {
|
||||||
/// Draw a filled triangle (for 3D graphics)
|
/// Draw a filled triangle (for 3D graphics)
|
||||||
filled_triangle: FilledTriangleCommand,
|
filled_triangle: FilledTriangleCommand,
|
||||||
|
|
||||||
|
/// Draw a filled circle (for organic shapes, icons)
|
||||||
|
filled_circle: FilledCircleCommand,
|
||||||
|
|
||||||
/// Begin clipping to a rectangle
|
/// Begin clipping to a rectangle
|
||||||
clip: ClipCommand,
|
clip: ClipCommand,
|
||||||
|
|
||||||
|
|
@ -172,6 +175,19 @@ pub const FilledTriangleCommand = struct {
|
||||||
color: Style.Color,
|
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
|
/// Begin clipping to a rectangle
|
||||||
pub const ClipCommand = struct {
|
pub const ClipCommand = struct {
|
||||||
x: i32,
|
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)
|
/// 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 = .{
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ pub const SoftwareRenderer = struct {
|
||||||
.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),
|
.filled_triangle => |tri| self.drawFilledTriangle(tri),
|
||||||
|
.filled_circle => |cir| self.drawFilledCircle(cir),
|
||||||
.clip => |c| self.pushClip(c),
|
.clip => |c| self.pushClip(c),
|
||||||
.clip_end => self.popClip(),
|
.clip_end => self.popClip(),
|
||||||
.nop => {},
|
.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 {
|
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;
|
||||||
|
|
||||||
|
|
@ -647,6 +709,15 @@ fn commandBounds(cmd: DrawCommand) ?Rect {
|
||||||
@intCast(@max(1, max_y - min_y + 1)),
|
@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,
|
.clip, .clip_end, .nop => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue