From ef768e9fe73e9652510ac25847b64ecf9508ec51 Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Tue, 24 Feb 2026 20:51:40 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D1=8B=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BF=D1=8B=D1=82=D0=BA=D0=B8=20=D1=80=D0=B8=D1=81=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Canvas.zig | 20 +++++--- src/render/CpuRenderEngine.zig | 94 ++++++++++++++++++++++++++++++++++ src/render/RenderEngine.zig | 8 +++ src/ui/left_panel.zig | 17 +++--- 4 files changed, 127 insertions(+), 12 deletions(-) diff --git a/src/Canvas.zig b/src/Canvas.zig index e636a6b..08f19f9 100644 --- a/src/Canvas.zig +++ b/src/Canvas.zig @@ -3,7 +3,9 @@ const builtin = @import("builtin"); const dvui = @import("dvui"); const Document = @import("models/Document.zig"); const RenderEngine = @import("render/RenderEngine.zig").RenderEngine; -const ImageRect = @import("models/basic_models.zig").ImageRect; +const basic_models = @import("models/basic_models.zig"); +const ImageRect = basic_models.ImageRect; +const ImageSize = basic_models.ImageSize; const Point2 = @import("models/basic_models.zig").Point2; const Color = dvui.Color; @@ -25,6 +27,8 @@ _zoom: f32 = 1, _redraw_pending: bool = false, _last_redraw_time_ms: i64 = 0, cursor_document_point: ?Point2 = null, +/// true — рисовать документ (render), false — пример (gradient/squares). +draw_document: bool = true, pub fn init(allocator: std.mem.Allocator, document: *Document, engine: RenderEngine) Canvas { return .{ @@ -41,7 +45,7 @@ pub fn deinit(self: *Canvas) void { } } -pub fn redrawExample(self: *Canvas) !void { +fn redraw(self: *Canvas) !void { const full = self.getZoomedImageSize(); const vis: ImageRect = self._visible_rect orelse ImageRect{ .x = 0, .y = 0, .w = 0, .h = 0 }; @@ -54,7 +58,11 @@ pub fn redrawExample(self: *Canvas) !void { return; } - const new_texture = self.render_engine.example(.{ .w = full.w, .h = full.h }, vis) catch null; + const canvas_size: ImageSize = .{ .w = full.w, .h = full.h }; + const new_texture = if (self.draw_document) + self.render_engine.render(self.document, canvas_size, vis) catch null + else + self.render_engine.example(canvas_size, vis) catch null; if (new_texture) |tex| { if (self.texture) |old_tex| { @@ -68,7 +76,7 @@ pub fn redrawExample(self: *Canvas) !void { pub fn exampleReset(self: *Canvas) !void { self.render_engine.exampleReset(); - try self.redrawExample(); + try self.redraw(); } pub fn setZoom(self: *Canvas, value: f32) void { @@ -88,14 +96,14 @@ pub fn processPendingRedraw(self: *Canvas) !void { if (!self._redraw_pending) return; if (self.redraw_throttle_ms == 0) { self._redraw_pending = false; - try self.redrawExample(); + try self.redraw(); return; } const now_ms = std.time.milliTimestamp(); const elapsed: i64 = if (self._last_redraw_time_ms == 0) self.redraw_throttle_ms else now_ms - self._last_redraw_time_ms; if (elapsed < @as(i64, @intCast(self.redraw_throttle_ms))) return; self._redraw_pending = false; - try self.redrawExample(); + try self.redraw(); } pub fn getZoomedImageSize(self: Canvas) ImageRect { diff --git a/src/render/CpuRenderEngine.zig b/src/render/CpuRenderEngine.zig index 6f0c416..3d14df5 100644 --- a/src/render/CpuRenderEngine.zig +++ b/src/render/CpuRenderEngine.zig @@ -2,6 +2,8 @@ const std = @import("std"); const builtin = @import("builtin"); const dvui = @import("dvui"); const RenderEngine = @import("RenderEngine.zig").RenderEngine; +const Document = @import("../models/Document.zig"); +const shape = @import("../models/shape/shape.zig"); const basic_models = @import("../models/basic_models.zig"); const ImageSize = basic_models.ImageSize; const ImageRect = basic_models.ImageRect; @@ -171,3 +173,95 @@ pub fn example(self: CpuRenderEngine, canvas_size: ImageSize, visible_rect: Imag pub fn renderEngine(self: *CpuRenderEngine) RenderEngine { return .{ .cpu = self }; } + +/// Растеризует документ: фон + контуры AABB всех объектов по getLocalBounds и position. +pub fn renderDocument(self: *CpuRenderEngine, document: *const Document, canvas_size: ImageSize, visible_rect: ImageRect) !?dvui.Texture { + const width = visible_rect.w; + const height = visible_rect.h; + const pixels = try self._allocator.alloc(Color.PMA, @as(usize, width) * height); + defer self._allocator.free(pixels); + + const scale_x: f32 = if (document.size.width > 0) @as(f32, @floatFromInt(canvas_size.w)) / document.size.width else 0; + const scale_y: f32 = if (document.size.height > 0) @as(f32, @floatFromInt(canvas_size.h)) / document.size.height else 0; + + // Фон + for (pixels) |*p| p.* = .{ .r = 255, .g = 255, .b = 255, .a = 255 }; + + const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 }; + for (document.objects.items) |*obj| { + const local_bounds = shape.getLocalBounds(obj) catch continue; + const pos = if (obj.getProperty(.position)) |p| p.position else basic_models.Point2{ .x = 0, .y = 0 }; + const world_x = local_bounds.x + pos.x; + const world_y = local_bounds.y + pos.y; + const px_min_x: i32 = @intFromFloat(world_x * scale_x); + const px_min_y: i32 = @intFromFloat(world_y * scale_y); + const px_max_x: i32 = @intFromFloat((world_x + local_bounds.w) * scale_x); + const px_max_y: i32 = @intFromFloat((world_y + local_bounds.h) * scale_y); + + const stroke = if (obj.getProperty(.stroke_rgba)) |s| rgbaToPma(s.stroke_rgba) else default_stroke; + drawRectOutline(pixels, width, height, visible_rect, px_min_x, px_min_y, px_max_x, px_max_y, stroke); + } + + return try dvui.textureCreate(pixels, width, height, .nearest); +} + +fn rgbaToPma(rgba: u32) Color.PMA { + const r: u8 = @intCast((rgba >> 0) & 0xFF); + const g: u8 = @intCast((rgba >> 8) & 0xFF); + const b: u8 = @intCast((rgba >> 16) & 0xFF); + const a: u8 = @intCast((rgba >> 24) & 0xFF); + if (a == 0) return .{ .r = 0, .g = 0, .b = 0, .a = 0 }; + const af: f32 = @as(f32, @floatFromInt(a)) / 255.0; + return .{ + .r = @intFromFloat(@as(f32, @floatFromInt(r)) * af), + .g = @intFromFloat(@as(f32, @floatFromInt(g)) * af), + .b = @intFromFloat(@as(f32, @floatFromInt(b)) * af), + .a = a, + }; +} + +fn drawRectOutline( + pixels: []Color.PMA, + buf_width: u32, + buf_height: u32, + visible_rect: ImageRect, + px_min_x: i32, + px_min_y: i32, + px_max_x: i32, + px_max_y: i32, + color: Color.PMA, +) void { + _ = buf_height; + const vx: i32 = @intCast(visible_rect.x); + const vy: i32 = @intCast(visible_rect.y); + const vw: i32 = @intCast(visible_rect.w); + const vh: i32 = @intCast(visible_rect.h); + + const x0 = std.math.clamp(px_min_x, vx, vx + vw - 1); + const x1 = std.math.clamp(px_max_x, vx, vx + vw - 1); + const y0 = std.math.clamp(px_min_y, vy, vy + vh - 1); + const y1 = std.math.clamp(px_max_y, vy, vy + vh - 1); + + const y_edges = [_]i32{ y0, y1 }; + for (y_edges) |cy| { + if (cy < vy or cy >= vy + vh) continue; + const by: u32 = @intCast(cy - vy); + var cx = x0; + while (cx <= x1) : (cx += 1) { + if (cx < vx or cx >= vx + vw) continue; + const bx: u32 = @intCast(cx - vx); + pixels[by * buf_width + bx] = color; + } + } + const x_edges = [_]i32{ x0, x1 }; + for (x_edges) |cx| { + if (cx < vx or cx >= vx + vw) continue; + const bx: u32 = @intCast(cx - vx); + var cy = y0; + while (cy <= y1) : (cy += 1) { + if (cy < vy or cy >= vy + vh) continue; + const by: u32 = @intCast(cy - vy); + pixels[by * buf_width + bx] = color; + } + } +} diff --git a/src/render/RenderEngine.zig b/src/render/RenderEngine.zig index f9362c8..e1f5bf5 100644 --- a/src/render/RenderEngine.zig +++ b/src/render/RenderEngine.zig @@ -1,5 +1,6 @@ const dvui = @import("dvui"); const CpuRenderEngine = @import("CpuRenderEngine.zig"); +const Document = @import("../models/Document.zig"); const basic_models = @import("../models/basic_models.zig"); pub const RenderEngine = union(enum) { @@ -16,4 +17,11 @@ pub const RenderEngine = union(enum) { .cpu => |cpu_r| cpu_r.example(canvas_size, visible_rect), }; } + + /// Растеризует документ в текстуру (размер и видимая область в пикселях холста). + pub fn render(self: RenderEngine, document: *const Document, canvas_size: basic_models.ImageSize, visible_rect: basic_models.ImageRect) !?dvui.Texture { + return switch (self) { + .cpu => |cpu_r| cpu_r.renderDocument(document, canvas_size, visible_rect), + }; + } }; diff --git a/src/ui/left_panel.zig b/src/ui/left_panel.zig index 2f2770a..63329d1 100644 --- a/src/ui/left_panel.zig +++ b/src/ui/left_panel.zig @@ -112,13 +112,18 @@ pub fn leftPanel(ctx: *WindowContext) void { if (active_doc) |doc| { const canvas = &doc.canvas; if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {} - if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) { - if (doc.cpu_render.type == .Gradient) { - doc.cpu_render.type = .Squares; - } else { - doc.cpu_render.type = .Gradient; + if (dvui.checkbox(@src(), &canvas.draw_document, "Draw document", .{})) { + canvas.requestRedraw(); + } + if (!canvas.draw_document) { + if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) { + if (doc.cpu_render.type == .Gradient) { + doc.cpu_render.type = .Squares; + } else { + doc.cpu_render.type = .Gradient; + } + canvas.requestRedraw(); } - canvas.redrawExample() catch {}; } } else { dvui.label(@src(), "No document", .{}, .{});