From 4bf92356afaebfc14c855b152f7cf99d2d93e001 Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Tue, 3 Mar 2026 19:07:53 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D1=8B=D0=B9=20=D0=BA?= =?UTF-8?q?=D1=80=D1=83=D1=82=D0=BE=D0=B9=20=D0=BA=D1=80=D1=83=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/Property.zig | 2 + src/models/shape/ellipse.zig | 1 + src/render/cpu/draw.zig | 2 +- src/render/cpu/ellipse.zig | 109 ++++++++++++++++++++--------------- src/ui/canvas_view.zig | 8 +++ 5 files changed, 75 insertions(+), 47 deletions(-) diff --git a/src/models/Property.zig b/src/models/Property.zig index eaf28fe..e3b2fd0 100644 --- a/src/models/Property.zig +++ b/src/models/Property.zig @@ -15,6 +15,8 @@ pub const Data = union(enum) { size: Size_f, radii: Radii_f, + /// Процент дуги эллипса: 100 — полный эллипс, 50 — полуэллипс (0..100). + arc_percent: f32, end_point: Point2_f, points: std.ArrayList(Point2_f), diff --git a/src/models/shape/ellipse.zig b/src/models/shape/ellipse.zig index 055319d..d18dcf1 100644 --- a/src/models/shape/ellipse.zig +++ b/src/models/shape/ellipse.zig @@ -8,6 +8,7 @@ const shape_mod = @import("shape.zig"); /// Свойства фигуры по умолчанию. pub const default_shape_properties = [_]Property{ .{ .data = .{ .radii = .{ .x = 50, .y = 50 } } }, + .{ .data = .{ .arc_percent = 100.0 } }, }; /// Теги обязательных свойств. diff --git a/src/render/cpu/draw.zig b/src/render/cpu/draw.zig index 88750b0..f1a1f9b 100644 --- a/src/render/cpu/draw.zig +++ b/src/render/cpu/draw.zig @@ -30,7 +30,7 @@ fn drawObject( switch (obj.shape) { .line => line.draw(ctx, obj), - .ellipse => ellipse.draw(ctx, obj), + .ellipse => try ellipse.draw(ctx, obj, allocator), .broken => try broken.draw(ctx, obj, allocator), .arc => arc.draw(ctx, obj), } diff --git a/src/render/cpu/ellipse.zig b/src/render/cpu/ellipse.zig index 7fbcf7e..b2bd0c5 100644 --- a/src/render/cpu/ellipse.zig +++ b/src/render/cpu/ellipse.zig @@ -3,21 +3,32 @@ 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; -/// Эллипс с центром (0,0) и полуосями radii (обводка с учётом thickness). -pub fn draw(ctx: *DrawContext, obj: *const Object) void { +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); + }; - // Полуширина обводки в нормализованных единицах (d = (x/rx)² + (y/ry)², граница при d=1). + 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); @@ -25,18 +36,25 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void { const d_inner_sq = inner * inner; const d_outer_sq = outer * outer; - const corners = [_]struct { x: f32, y: f32 }{ - .{ .x = -rx, .y = -ry }, - .{ .x = rx, .y = -ry }, - .{ .x = rx, .y = ry }, - .{ .x = -rx, .y = ry }, + 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 }, }; - const w0 = ctx.localToWorld(corners[0].x, corners[0].y); - const b0 = ctx.worldToBufferF(w0.x, w0.y); - var min_bx: f32 = b0.x; - var min_by: f32 = b0.y; - var max_bx: f32 = b0.x; - var max_by: f32 = b0.y; + 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); @@ -47,50 +65,49 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void { } 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 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; - // Один раз считаем аффин buffer -> local, чтобы в цикле не вызывать cos/sin и лишние функции. - const t = &ctx.transform; - const ctx_sx = if (ctx.scale_x != 0) ctx.scale_x else 1.0; - const ctx_sy = if (ctx.scale_y != 0) ctx.scale_y else 1.0; - const inv_ctx_sx = 1.0 / ctx_sx; - const inv_ctx_sy = 1.0 / ctx_sy; - const vx = @as(f32, @floatFromInt(ctx.visible_rect.x)); - const vy = @as(f32, @floatFromInt(ctx.visible_rect.y)); - const t_sx = if (t.scale.scale_x != 0) t.scale.scale_x else 1.0; - const t_sy = if (t.scale.scale_y != 0) t.scale.scale_y else 1.0; - const ca = std.math.cos(-t.angle); - const sa = std.math.sin(-t.angle); - const dx_off = vx * inv_ctx_sx - t.position.x; - const dy_off = vy * inv_ctx_sy - t.position.y; - const loc_x_off = (dx_off * ca - dy_off * sa) / t_sx; - const loc_y_off = (dx_off * sa + dy_off * ca) / t_sy; - const m00 = inv_ctx_sx * ca / t_sx; - const m01 = -inv_ctx_sy * sa / t_sx; - const m10 = inv_ctx_sx * sa / t_sy; - const m11 = inv_ctx_sy * ca / t_sy; 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; - const row_loc_x_off = buf_y * m01 + loc_x_off; - const row_loc_y_off = buf_y * m11 + loc_y_off; var bx: i32 = x0; while (bx < x1) : (bx += 1) { const buf_x = @as(f32, @floatFromInt(bx)) + 0.5; - const loc_x = buf_x * m00 + row_loc_x_off; - const loc_y = buf_x * m10 + row_loc_y_off; - const nx = loc_x * inv_rx; - const ny = loc_y * inv_ry; + 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 and d <= d_outer_sq) { - ctx.blendPixelAtBuffer(bx, by, stroke); + 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); } diff --git a/src/ui/canvas_view.zig b/src/ui/canvas_view.zig index 5472ae6..f8e7646 100644 --- a/src/ui/canvas_view.zig +++ b/src/ui/canvas_view.zig @@ -532,6 +532,13 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope canvas.requestRedraw(); } }, + .arc_percent => |pct| { + var next = pct; + if (dvui.sliderEntry(@src(), "{d:0.0}%", .{ .value = &next, .min = 0.0, .max = 100.0, .interval = 1.0 }, .{ .expand = .horizontal })) { + obj.setProperty(canvas.allocator, .{ .data = .{ .arc_percent = next } }) catch {}; + canvas.requestRedraw(); + } + }, .end_point => |pt| { var next = pt; var changed = false; @@ -745,6 +752,7 @@ fn propertyLabel(tag: std.meta.Tag(PropertyData)) []const u8 { .locked => "Locked", .size => "Size", .radii => "Radii", + .arc_percent => "Arc %", .end_point => "End point", .points => "Points", .fill_rgba => "Fill color",