const std = @import("std"); const Document = @import("../../models/Document.zig"); const pipeline = @import("pipeline.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_thickness: f32 = 2.0; const std_math = std.math; /// Эллипс с центром (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 = blk: { const p = obj.getProperty(.arc_percent); break :blk std.math.clamp(if (p) |v| v.arc_percent else 100.0, 0.0, 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 = undefined; var min_by: f32 = undefined; var max_bx: f32 = undefined; var max_by: f32 = undefined; { const w0 = ctx.localToWorld(corners[0].x, corners[0].y); const b0 = ctx.worldToBufferF(w0.x, w0.y); min_bx = b0.x; min_by = b0.y; max_bx = b0.x; max_by = b0.y; } for (corners[1..]) |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; const inv_rx = 1.0 / rx; const inv_ry = 1.0 / ry; const arc_full = arc_percent >= 100.0; const t_start = std_math.pi / 2.0; const t_end_raw = t_start - 2.0 * std_math.pi * arc_percent / 100.0; const t_end = if (t_end_raw <= -std_math.pi) t_end_raw + 2.0 * std_math.pi else t_end_raw; 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_full) { const t_pt = std_math.atan2(ny, nx); const in_arc = if (t_end <= t_start) (t_pt >= t_end and t_pt <= t_start) else (t_pt >= t_end or t_pt <= t_start); if (!in_arc) continue; } stroke_ctx.blendPixelAtBuffer(bx, by, stroke); } } ctx.compositeDrawerContext(&stroke_ctx, t.opacity); }