From 05f5481a429c821fd0e946f8efc9612ccc18b2e2 Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Thu, 26 Feb 2026 13:48:42 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=B1=D1=83=D1=84?= =?UTF-8?q?=D0=B5=D1=80=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D1=80=D0=B8?= =?UTF-8?q?=D1=81=D0=BE=D0=B2=D0=BA=D0=B8=20broken=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/render/CpuRenderEngine.zig | 2 +- src/render/cpu/broken.zig | 21 ++++++++++++++++++--- src/render/cpu/draw.zig | 17 ++++++++++++----- src/render/cpu/pipeline.zig | 30 ++++++++++++++++++++++++++++-- 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/render/CpuRenderEngine.zig b/src/render/CpuRenderEngine.zig index 5ba537c..ff9fedd 100644 --- a/src/render/CpuRenderEngine.zig +++ b/src/render/CpuRenderEngine.zig @@ -182,7 +182,7 @@ pub fn renderDocument(self: *CpuRenderEngine, document: *const Document, canvas_ defer self._allocator.free(pixels); for (pixels) |*p| p.* = .{ .r = 255, .g = 255, .b = 255, .a = 255 }; - cpu_draw.drawDocument(pixels, width, height, visible_rect, document, canvas_size); + try cpu_draw.drawDocument(pixels, width, height, visible_rect, document, canvas_size, self._allocator); return try dvui.textureCreate(pixels, width, height, .nearest); } diff --git a/src/render/cpu/broken.zig b/src/render/cpu/broken.zig index d2661db..9ce4feb 100644 --- a/src/render/cpu/broken.zig +++ b/src/render/cpu/broken.zig @@ -9,15 +9,30 @@ const Object = Document.Object; const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 }; const default_thickness: f32 = 2.0; -/// Ломаная по точкам, обводка stroke_rgba. -pub fn draw(ctx: *DrawContext, obj: *const Object) void { +/// Ломаная по точкам, обводка stroke_rgba +pub fn draw( + ctx: *DrawContext, + obj: *const Object, + allocator: std.mem.Allocator, +) !void { const p_prop = obj.getProperty(.points) orelse return; const pts = p_prop.points.items; if (pts.len < 2) 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 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 copy_ctx = ctx.*; + copy_ctx.pixels = buffer; + copy_ctx.replace_mode = true; + var i: usize = 0; while (i + 1 < pts.len) : (i += 1) { - line.drawLine(ctx, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, stroke, thickness); + line.drawLine(©_ctx, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, stroke, thickness); } + + ctx.compositeDrawerContext(©_ctx, copy_ctx.transform.opacity); } diff --git a/src/render/cpu/draw.zig b/src/render/cpu/draw.zig index bac0845..21389a3 100644 --- a/src/render/cpu/draw.zig +++ b/src/render/cpu/draw.zig @@ -30,7 +30,12 @@ fn isVisible(obj: *const Object) bool { return if (obj.getProperty(.visible)) |p| p.visible else true; } -fn drawObject(ctx: *DrawContext, obj: *const Object, parent_transform: Transform) void { +fn drawObject( + ctx: *DrawContext, + obj: *const Object, + parent_transform: Transform, + allocator: std.mem.Allocator, +) !void { if (!isVisible(obj)) return; const local = getLocalTransform(obj); const world = Transform.compose(parent_transform, local); @@ -39,16 +44,17 @@ fn drawObject(ctx: *DrawContext, obj: *const Object, parent_transform: Transform switch (obj.shape) { .line => line.draw(ctx, obj), .ellipse => ellipse.draw(ctx, obj), - .broken => broken.draw(ctx, obj), + .broken => try broken.draw(ctx, obj, allocator), .arc => arc.draw(ctx, obj), } for (obj.children.items) |*child| { - drawObject(ctx, child, world); + try drawObject(ctx, child, world, allocator); } } /// Рекурсивно рисует документ в буфер (объекты и потомки по порядку). +/// allocator опционален; если передан, ломаные рисуются через слой (без двойного наложения при alpha < 1). pub fn drawDocument( pixels: []@import("dvui").Color.PMA, buf_width: u32, @@ -56,7 +62,8 @@ pub fn drawDocument( visible_rect: Rect_i, document: *const Document, canvas_size: Size_i, -) void { + allocator: std.mem.Allocator, +) !void { const scale_x: f32 = if (document.size.w > 0) @as(f32, @floatFromInt(canvas_size.w)) / document.size.w else 0; const scale_y: f32 = if (document.size.h > 0) @as(f32, @floatFromInt(canvas_size.h)) / document.size.h else 0; @@ -70,6 +77,6 @@ pub fn drawDocument( }; const identity = Transform{}; for (document.objects.items) |*obj| { - drawObject(&ctx, obj, identity); + try drawObject(&ctx, obj, identity, allocator); } } diff --git a/src/render/cpu/pipeline.zig b/src/render/cpu/pipeline.zig index 71a8fd7..5dcb79e 100644 --- a/src/render/cpu/pipeline.zig +++ b/src/render/cpu/pipeline.zig @@ -45,6 +45,8 @@ pub const DrawContext = struct { scale_x: f32, scale_y: f32, transform: Transform = .{}, + /// Если true, blendPixelAtBuffer перезаписывает пиксель без бленда + replace_mode: bool = false, pub fn setTransform(self: *DrawContext, t: Transform) void { self.transform = t; @@ -111,12 +113,16 @@ pub const DrawContext = struct { }; } - /// Смешивает цвет в пикселе буфера с учётом opacity трансформа. + /// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель. pub fn blendPixelAtBuffer(self: *DrawContext, bx: u32, by: u32, color: Color.PMA) void { if (bx >= self.buf_width or by >= self.buf_height) return; - const t = &self.transform; const idx = by * self.buf_width + bx; const dst = &self.pixels[idx]; + if (self.replace_mode) { + dst.* = color; + return; + } + const t = &self.transform; const a = @as(f32, @floatFromInt(color.a)) / 255.0 * t.opacity; const src_r = @as(f32, @floatFromInt(color.r)) * t.opacity; const src_g = @as(f32, @floatFromInt(color.g)) * t.opacity; @@ -128,6 +134,26 @@ pub const DrawContext = struct { dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255)); } + /// Накладывает буфер другого контекста на этот с заданной прозрачностью (один бленд на пиксель). Размеры буферов должны совпадать. + pub fn compositeDrawerContext(self: *DrawContext, other: *const DrawContext, opacity: f32) void { + if (self.buf_width != other.buf_width or self.buf_height != other.buf_height) return; + const n = self.buf_width * self.buf_height; + for (0..n) |i| { + const src = other.pixels[i]; + if (src.a == 0) continue; + const dst = &self.pixels[i]; + const a = @as(f32, @floatFromInt(src.a)) / 255.0 * opacity; + const src_r = @as(f32, @floatFromInt(src.r)) * opacity; + const src_g = @as(f32, @floatFromInt(src.g)) * opacity; + const src_b = @as(f32, @floatFromInt(src.b)) * opacity; + const inv_a = 1.0 - a; + dst.r = @intFromFloat(std.math.clamp(src_r + inv_a * @as(f32, @floatFromInt(dst.r)), 0, 255)); + dst.g = @intFromFloat(std.math.clamp(src_g + inv_a * @as(f32, @floatFromInt(dst.g)), 0, 255)); + dst.b = @intFromFloat(std.math.clamp(src_b + inv_a * @as(f32, @floatFromInt(dst.b)), 0, 255)); + dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255)); + } + } + /// Пиксель в локальных координатах (трансформ + PMA). pub fn blendPixelLocal(self: *DrawContext, local_x: f32, local_y: f32, color: Color.PMA) void { const w = self.localToWorld(local_x, local_y);