Files
Zivro/src/render/cpu/ellipse.zig

117 lines
5.1 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const std = @import("std");
const std_math = std.math;
const Document = @import("../../models/Document.zig");
const pipeline = @import("pipeline.zig");
const line = @import("line.zig");
const DrawContext = pipeline.DrawContext;
const Color = @import("dvui").Color;
const basic_models = @import("../../models/basic_models.zig");
const Point2_f = basic_models.Point2_f;
const Object = Document.Object;
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
const default_fill: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 0 };
const default_thickness: f32 = 2.0;
/// Эллипс с центром (0,0) и полуосями radii. Обводка — полоса расстояния до контура (чёткая линия, не круги).
/// arc_percent: 100 — полный эллипс, иначе одна дуга; обход в коде от (0,ry) по квадрантам (визуально может казаться от низа против часовой из‑за экранной Y).
/// Отрисовка в отдельный буфер и один composite, чтобы при alpha<255 пиксели не накладывались несколько раз.
pub fn draw(ctx: *DrawContext, obj: *const Object, allocator: std.mem.Allocator) !void {
const r_prop = obj.getProperty(.radii) orelse return;
const rx = r_prop.radii.x;
const ry = r_prop.radii.y;
if (rx <= 0 or ry <= 0) return;
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke;
const thickness = if (obj.getProperty(.thickness)) |t| t.thickness else default_thickness;
const arc_percent = std_math.clamp(if (obj.getProperty(.arc_percent)) |p| p.arc_percent else 100.0, 0.0, 100.0);
const closed = if (obj.getProperty(.closed)) |c| c.closed else true;
const filled = if (obj.getProperty(.filled)) |f| f.filled else false;
const fill_color = if (obj.getProperty(.fill_rgba)) |f| pipeline.rgbaToPma(f.fill_rgba) else default_fill;
const do_fill = filled and (closed or arc_percent >= 100.0);
const t = &ctx.transform;
const min_r = @min(rx, ry);
const half_norm = thickness / (2.0 * min_r);
const inner = @max(0.0, 1.0 - half_norm);
const outer = 1.0 + half_norm;
const d_inner_sq = inner * inner;
const d_outer_sq = outer * outer;
const margin = 1.0 + half_norm;
const corners = [_]Point2_f{
.{ .x = -rx * margin, .y = -ry * margin },
.{ .x = rx * margin, .y = -ry * margin },
.{ .x = rx * margin, .y = ry * margin },
.{ .x = -rx * margin, .y = ry * margin },
};
var min_bx: f32 = std_math.inf(f32);
var min_by: f32 = std_math.inf(f32);
var max_bx: f32 = -std_math.inf(f32);
var max_by: f32 = -std_math.inf(f32);
for (corners) |c| {
const w = ctx.localToWorld(c.x, c.y);
const b = ctx.worldToBufferF(w.x, w.y);
min_bx = @min(min_bx, b.x);
min_by = @min(min_by, b.y);
max_bx = @max(max_bx, b.x);
max_by = @max(max_by, b.y);
}
const buf_w: i32 = @intCast(ctx.buf_width);
const buf_h: i32 = @intCast(ctx.buf_height);
const x0: i32 = @max(0, @as(i32, @intFromFloat(std_math.floor(min_bx))));
const y0: i32 = @max(0, @as(i32, @intFromFloat(std_math.floor(min_by))));
const x1: i32 = @min(buf_w, @as(i32, @intFromFloat(std_math.ceil(max_bx))) + 1);
const y1: i32 = @min(buf_h, @as(i32, @intFromFloat(std_math.ceil(max_by))) + 1);
const buffer = try allocator.alloc(Color.PMA, ctx.buf_width * ctx.buf_height);
@memset(buffer, .{ .r = 0, .g = 0, .b = 0, .a = 0 });
defer allocator.free(buffer);
var stroke_ctx = ctx.*;
stroke_ctx.pixels = buffer;
stroke_ctx.replace_mode = true;
if (do_fill) {
stroke_ctx.startFill(allocator) catch return;
}
const inv_rx = 1.0 / rx;
const inv_ry = 1.0 / ry;
const arc_len = 2.0 * std_math.pi * arc_percent / 100.0;
var by: i32 = y0;
while (by < y1) : (by += 1) {
const buf_y = @as(f32, @floatFromInt(by)) + 0.5;
var bx: i32 = x0;
while (bx < x1) : (bx += 1) {
const buf_x = @as(f32, @floatFromInt(bx)) + 0.5;
const w = stroke_ctx.bufferToWorld(buf_x, buf_y);
const loc = stroke_ctx.worldToLocal(w.x, w.y);
const nx = loc.x * inv_rx;
const ny = loc.y * inv_ry;
const d = nx * nx + ny * ny;
if (d < d_inner_sq or d > d_outer_sq) continue;
if (arc_percent < 100.0) {
var diff = std_math.pi / 2.0 - std_math.atan2(ny, nx);
if (diff < 0) diff += 2.0 * std_math.pi;
if (diff > arc_len) continue;
}
stroke_ctx.blendPixelAtBuffer(bx, by, stroke);
}
}
if (closed and arc_percent < 100.0) {
const end_x = rx * std_math.sin(arc_len);
const end_y = ry * std_math.cos(arc_len);
line.drawLine(&stroke_ctx, 0, 0, 0, ry, stroke, thickness, false);
line.drawLine(&stroke_ctx, 0, 0, end_x, end_y, stroke, thickness, false);
}
if (do_fill) {
stroke_ctx.stopFill(allocator, fill_color);
}
ctx.compositeDrawerContext(&stroke_ctx, t.opacity);
}