From 0114db1f488834c9fa0897f667977728858fa3aa Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Wed, 25 Feb 2026 00:57:55 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=82=D0=BE=D0=BB=D1=89=D0=B8=D0=BD=D1=83=20=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=D0=B8=D0=B9,=20=D1=83=D0=BD=D0=B8=D1=84=D0=B8?= =?UTF-8?q?=D1=86=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BB=20=D0=B3=D0=B5=D0=BE?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D1=80=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переименовал основные геометрические модели (Point, Size, Rect, Scale, Radii), явно разделив их на типы с плавающей точкой (_f) и целочисленные (_i). Обновил использование этих типов во всем проекте для улучшения типобезопасности и ясности. Ввел новое свойство thickness для объектов и реализовал его применение при отрисовке линий и ломаных. Добавил Point2_i для целочисленных координат буфера в конвейере отрисовки. --- src/Canvas.zig | 28 ++++++++++++++-------------- src/WindowContext.zig | 2 +- src/models/Document.zig | 6 +++--- src/models/Object.zig | 2 +- src/models/Property.zig | 20 ++++++++++---------- src/models/basic_models.zig | 24 +++++++++++++++--------- src/models/random_document.zig | 26 ++++++++++++++------------ src/models/shape/arc.zig | 4 ++-- src/models/shape/broken.zig | 10 +++++----- src/models/shape/ellipse.zig | 4 ++-- src/models/shape/line.zig | 4 ++-- src/models/shape/shape.zig | 2 +- src/render/CpuRenderEngine.zig | 12 ++++++------ src/render/RenderEngine.zig | 4 ++-- src/render/cpu/broken.zig | 5 +++-- src/render/cpu/draw.zig | 19 ++++++++++--------- src/render/cpu/line.zig | 22 ++++++++++------------ src/render/cpu/pipeline.zig | 32 +++++++++++++++++++++----------- src/ui/canvas_view.zig | 4 ++-- 19 files changed, 124 insertions(+), 106 deletions(-) diff --git a/src/Canvas.zig b/src/Canvas.zig index 5b49a76..17722ed 100644 --- a/src/Canvas.zig +++ b/src/Canvas.zig @@ -4,9 +4,9 @@ const dvui = @import("dvui"); const Document = @import("models/Document.zig"); const RenderEngine = @import("render/RenderEngine.zig").RenderEngine; 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 Rect_i = basic_models.Rect_i; +const Size_i = basic_models.Size_i; +const Point2_f = @import("models/basic_models.zig").Point2_f; const Color = dvui.Color; const Canvas = @This(); @@ -22,11 +22,11 @@ scroll: dvui.ScrollInfo = .{ }, native_scaling: bool = true, redraw_throttle_ms: u32 = 50, -_visible_rect: ?ImageRect = null, +_visible_rect: ?Rect_i = null, _zoom: f32 = 1, _redraw_pending: bool = false, _last_redraw_time_ms: i64 = 0, -cursor_document_point: ?Point2 = null, +cursor_document_point: ?Point2_f = null, /// true — рисовать документ (render), false — пример (gradient/squares). draw_document: bool = true, @@ -48,7 +48,7 @@ pub fn deinit(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 }; + const vis: Rect_i = self._visible_rect orelse Rect_i{ .x = 0, .y = 0, .w = 0, .h = 0 }; if (vis.w == 0 or vis.h == 0) { if (self.texture) |tex| { @@ -58,7 +58,7 @@ fn redraw(self: *Canvas) !void { return; } - const canvas_size: ImageSize = .{ .w = full.w, .h = full.h }; + const canvas_size: Size_i = .{ .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 @@ -106,17 +106,17 @@ pub fn processPendingRedraw(self: *Canvas) !void { try self.redraw(); } -pub fn getZoomedImageSize(self: Canvas) ImageRect { +pub fn getZoomedImageSize(self: Canvas) Rect_i { const doc = self.document; return .{ .x = @intFromFloat(self.pos.x), .y = @intFromFloat(self.pos.y), - .w = @intFromFloat(doc.size.width * self._zoom), - .h = @intFromFloat(doc.size.height * self._zoom), + .w = @intFromFloat(doc.size.w * self._zoom), + .h = @intFromFloat(doc.size.h * self._zoom), }; } -pub fn contentPointToDocument(self: Canvas, content_point: dvui.Point, natural_scale: f32) ?Point2 { +pub fn contentPointToDocument(self: Canvas, content_point: dvui.Point, natural_scale: f32) ?Point2_f { const img = self.getZoomedImageSize(); const left_n = @as(f32, @floatFromInt(img.x)) / natural_scale; const top_n = @as(f32, @floatFromInt(img.y)) / natural_scale; @@ -129,7 +129,7 @@ pub fn contentPointToDocument(self: Canvas, content_point: dvui.Point, natural_s const px_x = content_point.x * natural_scale - @as(f32, @floatFromInt(img.x)); const px_y = content_point.y * natural_scale - @as(f32, @floatFromInt(img.y)); - return Point2{ + return Point2_f{ .x = px_x / self._zoom, .y = px_y / self._zoom, }; @@ -148,7 +148,7 @@ pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset: return false; } -fn computeVisibleImageRect(self: Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) ImageRect { +fn computeVisibleImageRect(self: Canvas, viewport: dvui.Rect, scroll_offset: dvui.Point) Rect_i { const image_rect = self.getZoomedImageSize(); const img_w: u32 = image_rect.w; @@ -163,7 +163,7 @@ fn computeVisibleImageRect(self: Canvas, viewport: dvui.Rect, scroll_offset: dvu const vis_x: u32 = @intCast(std.math.clamp(raw_x, 0, @as(i64, img_w) - @as(i64, vis_w))); const vis_y: u32 = @intCast(std.math.clamp(raw_y, 0, @as(i64, img_h) - @as(i64, vis_h))); - return ImageRect{ + return Rect_i{ .x = vis_x, .y = vis_y, .w = vis_w, diff --git a/src/WindowContext.zig b/src/WindowContext.zig index 7748376..6fb6733 100644 --- a/src/WindowContext.zig +++ b/src/WindowContext.zig @@ -14,7 +14,7 @@ pub const OpenDocument = struct { canvas: Canvas, pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void { - const default_size = basic_models.Size{ .width = 800, .height = 600 }; + const default_size = basic_models.Size_f{ .w = 800, .h = 600 }; self.document = Document.init(allocator, default_size); self.cpu_render = CpuRenderEngine.init(allocator, .Squares); self.canvas = Canvas.init(allocator, &self.document, (&self.cpu_render).renderEngine()); diff --git a/src/models/Document.zig b/src/models/Document.zig index 781ba07..733fe2f 100644 --- a/src/models/Document.zig +++ b/src/models/Document.zig @@ -1,16 +1,16 @@ const std = @import("std"); const basic_models = @import("basic_models.zig"); -const Size = basic_models.Size; +const Size_f = basic_models.Size_f; const Document = @This(); pub const Object = @import("Object.zig"); const shape = @import("shape/shape.zig"); -size: Size, +size: Size_f, allocator: std.mem.Allocator, objects: std.ArrayList(Object), -pub fn init(allocator: std.mem.Allocator, size: Size) Document { +pub fn init(allocator: std.mem.Allocator, size: Size_f) Document { return .{ .size = size, .allocator = allocator, diff --git a/src/models/Object.zig b/src/models/Object.zig index a6b9b13..51d6657 100644 --- a/src/models/Object.zig +++ b/src/models/Object.zig @@ -18,6 +18,7 @@ const default_common_data = [_]PropertyData{ .{ .opacity = 1.0 }, .{ .locked = false }, .{ .stroke_rgba = 0x000000FF }, + .{ .thickness = 2.0 }, }; pub const defaultCommonProperties: [default_common_data.len]Property = blk: { @@ -47,7 +48,6 @@ pub fn setProperty(self: *Object, allocator: std.mem.Allocator, prop: Property) return; } } - std.debug.print("Property not found: {s}\n", .{@tagName(prop.data)}); return error.PropertyNotFound; } diff --git a/src/models/Property.zig b/src/models/Property.zig index fdf6716..eb30471 100644 --- a/src/models/Property.zig +++ b/src/models/Property.zig @@ -1,23 +1,23 @@ const std = @import("std"); const basic_models = @import("basic_models.zig"); -const Point2 = basic_models.Point2; -const Scale2 = basic_models.Scale2; -const Size = basic_models.Size; -const Radii = basic_models.Radii; +const Point2_f = basic_models.Point2_f; +const Scale2_f = basic_models.Scale2_f; +const Size_f = basic_models.Size_f; +const Radii_f = basic_models.Radii_f; pub const Data = union(enum) { - position: Point2, + position: Point2_f, angle: f32, - scale: Scale2, + scale: Scale2_f, visible: bool, opacity: f32, locked: bool, - size: Size, - radii: Radii, - end_point: Point2, + size: Size_f, + radii: Radii_f, + end_point: Point2_f, - points: std.ArrayList(Point2), + points: std.ArrayList(Point2_f), fill_rgba: u32, stroke_rgba: u32, diff --git a/src/models/basic_models.zig b/src/models/basic_models.zig index fcd20da..92e753a 100644 --- a/src/models/basic_models.zig +++ b/src/models/basic_models.zig @@ -1,37 +1,43 @@ -pub const ImageRect = struct { +pub const Rect_i = struct { x: u32, y: u32, w: u32, h: u32, }; -pub const ImageSize = struct { +pub const Size_i = struct { w: u32, h: u32, }; -pub const Size = struct { - width: f32, - height: f32, +pub const Size_f = struct { + w: f32, + h: f32, }; -pub const Point2 = struct { +pub const Point2_f = struct { x: f32 = 0, y: f32 = 0, }; -pub const Radii = struct { +/// Целочисленная точка (например, координаты в буфере пикселей). +pub const Point2_i = struct { + x: i32 = 0, + y: i32 = 0, +}; + +pub const Radii_f = struct { x: f32, y: f32, }; -pub const Scale2 = struct { +pub const Scale2_f = struct { scale_x: f32 = 1, scale_y: f32 = 1, }; /// Прямоугольник в координатах документа (f32), например локальные границы объекта. -pub const Rect = struct { +pub const Rect_f = struct { x: f32 = 0, y: f32 = 0, w: f32 = 0, diff --git a/src/models/random_document.zig b/src/models/random_document.zig index 9219bf7..2a3e1f6 100644 --- a/src/models/random_document.zig +++ b/src/models/random_document.zig @@ -3,10 +3,10 @@ const Document = @import("Document.zig"); const Object = Document.Object; const shape = @import("shape/shape.zig"); const basic_models = @import("basic_models.zig"); -const Size = basic_models.Size; -const Point2 = basic_models.Point2; -const Scale2 = basic_models.Scale2; -const Radii = basic_models.Radii; +const Size_f = basic_models.Size_f; +const Point2_f = basic_models.Point2_f; +const Scale2_f = basic_models.Scale2_f; +const Radii_f = basic_models.Radii_f; fn randFloat(rng: std.Random, min: f32, max: f32) f32 { return min + (max - min) * rng.float(f32); @@ -26,10 +26,10 @@ fn randomShapeKind(rng: std.Random) Object.ShapeKind { } /// Случайно заполняет все доступные свойства объекта; позиция — в пределах документа. -fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size, obj: *Object, rng: std.Random) !void { +fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size_f, obj: *Object, rng: std.Random) !void { const margin: f32 = 8; - const max_x = @max(0, doc_size.width - margin); - const max_y = @max(0, doc_size.height - margin); + const max_x = @max(0, doc_size.w - margin); + const max_y = @max(0, doc_size.h - margin); try obj.setProperty(allocator, .{ .data = .{ .position = .{ @@ -44,17 +44,19 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size .scale_y = randFloat(rng, 0.25, 2.0), }, } }); - try obj.setProperty(allocator, .{ .data = .{ .visible = rng.boolean() } }); + try obj.setProperty(allocator, .{ .data = .{ .visible = true } }); try obj.setProperty(allocator, .{ .data = .{ .opacity = randFloat(rng, 0.3, 1.0) } }); try obj.setProperty(allocator, .{ .data = .{ .locked = rng.boolean() } }); const stroke = randRgba(rng); try obj.setProperty(allocator, .{ .data = .{ .stroke_rgba = stroke } }); obj.setProperty(allocator, .{ .data = .{ .fill_rgba = randRgba(rng) } }) catch {}; + const thickness = randFloat(rng, max_x * 0.01, max_x * 0.1); + try obj.setProperty(allocator, .{ .data = .{ .thickness = thickness } }); switch (obj.shape) { .line => { - const len = randFloat(rng, 20, @min(doc_size.width, doc_size.height) * 0.5); + const len = randFloat(rng, 20, @min(doc_size.w, doc_size.h) * 0.5); const angle = randFloat(rng, 0, 2 * std.math.pi); try obj.setProperty(allocator, .{ .data = .{ .end_point = .{ @@ -64,7 +66,7 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size } }); }, .ellipse => { - const max_r = @min(120, @min(doc_size.width / 4, doc_size.height / 4)); + const max_r = @min(120, @min(doc_size.w / 4, doc_size.h / 4)); try obj.setProperty(allocator, .{ .data = .{ .radii = .{ .x = randFloat(rng, 8, @max(8, max_r)), @@ -73,7 +75,7 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size } }); }, .broken => { - var points = std.ArrayList(Point2).empty; + var points = std.ArrayList(Point2_f).empty; const n = rng.intRangeLessThan(usize, 2, 9); var x: f32 = 0; var y: f32 = 0; @@ -96,7 +98,7 @@ pub fn addRandomShapes(doc: *Document, rng: std.Random) !void { var total_count: usize = 0; const allocator = doc.allocator; - const n_root = rng.intRangeLessThan(usize, 1, 5); + const n_root = rng.intRangeLessThan(usize, 6, 15); for (0..n_root) |_| { if (total_count >= max_total) break; var obj = try shape.createObject(allocator, randomShapeKind(rng)); diff --git a/src/models/shape/arc.zig b/src/models/shape/arc.zig index ef5a793..12bc5a6 100644 --- a/src/models/shape/arc.zig +++ b/src/models/shape/arc.zig @@ -1,7 +1,7 @@ const std = @import("std"); const Object = @import("../Object.zig"); const PropertyData = @import("../Property.zig").Data; -const Rect = @import("../basic_models.zig").Rect; +const Rect_f = @import("../basic_models.zig").Rect_f; const shape_mod = @import("shape.zig"); /// Теги обязательных свойств (заглушка: arc пока не реализован). @@ -17,7 +17,7 @@ pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) } /// Локальные границы дуги (заглушка: пока не реализовано). -pub fn getLocalBounds(obj: *const Object) !Rect { +pub fn getLocalBounds(obj: *const Object) !Rect_f { try shape_mod.ensure(obj, .arc); return error.ArcNotImplemented; } diff --git a/src/models/shape/broken.zig b/src/models/shape/broken.zig index a82cce5..983a4f8 100644 --- a/src/models/shape/broken.zig +++ b/src/models/shape/broken.zig @@ -2,12 +2,12 @@ const std = @import("std"); const Object = @import("../Object.zig"); const Property = @import("../Property.zig").Property; const PropertyData = @import("../Property.zig").Data; -const Point2 = @import("../basic_models.zig").Point2; -const Rect = @import("../basic_models.zig").Rect; +const Point2_f = @import("../basic_models.zig").Point2_f; +const Rect_f = @import("../basic_models.zig").Rect_f; const shape_mod = @import("shape.zig"); /// Точки ломаной по умолчанию (для создания). -pub const default_points = [_]Point2{ +pub const default_points = [_]Point2_f{ .{ .x = 0, .y = 0 }, .{ .x = 80, .y = 0 }, .{ .x = 80, .y = 60 }, @@ -22,13 +22,13 @@ pub fn getRequiredTags() []const std.meta.Tag(PropertyData) { /// Добавляет к объекту свойства по умолчанию для ломаной (points из default_points). pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) !void { - var points = std.ArrayList(Point2).empty; + var points = std.ArrayList(Point2_f).empty; try points.appendSlice(allocator, &default_points); try obj.properties.append(allocator, .{ .data = .{ .points = points } }); } /// Локальные границы ломаной: AABB по всем точкам. -pub fn getLocalBounds(obj: *const Object) !Rect { +pub fn getLocalBounds(obj: *const Object) !Rect_f { try shape_mod.ensure(obj, .broken); const p = obj.getProperty(.points).?; if (p.points.items.len == 0) return error.EmptyPoints; diff --git a/src/models/shape/ellipse.zig b/src/models/shape/ellipse.zig index 23293df..61a2daf 100644 --- a/src/models/shape/ellipse.zig +++ b/src/models/shape/ellipse.zig @@ -2,7 +2,7 @@ const std = @import("std"); const Object = @import("../Object.zig"); const Property = @import("../Property.zig").Property; const PropertyData = @import("../Property.zig").Data; -const Rect = @import("../basic_models.zig").Rect; +const Rect_f = @import("../basic_models.zig").Rect_f; const shape_mod = @import("shape.zig"); /// Свойства фигуры по умолчанию (для создания и проверки типа). Теги для ensure выводятся отсюда. @@ -21,7 +21,7 @@ pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) } /// Локальные границы эллипса: [-radii.x, -radii.y] .. [radii.x, radii.y]. -pub fn getLocalBounds(obj: *const Object) !Rect { +pub fn getLocalBounds(obj: *const Object) !Rect_f { try shape_mod.ensure(obj, .ellipse); const r = obj.getProperty(.radii).?; return .{ diff --git a/src/models/shape/line.zig b/src/models/shape/line.zig index 649aa4d..c3ebf0d 100644 --- a/src/models/shape/line.zig +++ b/src/models/shape/line.zig @@ -2,7 +2,7 @@ const std = @import("std"); const Object = @import("../Object.zig"); const Property = @import("../Property.zig").Property; const PropertyData = @import("../Property.zig").Data; -const Rect = @import("../basic_models.zig").Rect; +const Rect_f = @import("../basic_models.zig").Rect_f; const shape_mod = @import("shape.zig"); /// Свойства фигуры по умолчанию (для создания и проверки типа). Теги для ensure выводятся отсюда. @@ -21,7 +21,7 @@ pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) } /// Локальные границы линии: от (0,0) до end_point. -pub fn getLocalBounds(obj: *const Object) !Rect { +pub fn getLocalBounds(obj: *const Object) !Rect_f { try shape_mod.ensure(obj, .line); const ep = obj.getProperty(.end_point).?; const min_x = @min(0, ep.end_point.x); diff --git a/src/models/shape/shape.zig b/src/models/shape/shape.zig index 0c38ab4..3bb50b1 100644 --- a/src/models/shape/shape.zig +++ b/src/models/shape/shape.zig @@ -9,7 +9,7 @@ const ellipse = @import("ellipse.zig"); const broken = @import("broken.zig"); const arc = @import("arc.zig"); -pub const Rect = basic_models.Rect; +pub const Rect = basic_models.Rectf; /// Создаёт объект с общими свойствами по умолчанию и специфичными для типа фигуры. pub fn createObject(allocator: std.mem.Allocator, shape_kind: Object.ShapeKind) !Object { diff --git a/src/render/CpuRenderEngine.zig b/src/render/CpuRenderEngine.zig index 97e11fe..a195c6e 100644 --- a/src/render/CpuRenderEngine.zig +++ b/src/render/CpuRenderEngine.zig @@ -5,8 +5,8 @@ const RenderEngine = @import("RenderEngine.zig").RenderEngine; const Document = @import("../models/Document.zig"); const basic_models = @import("../models/basic_models.zig"); const cpu_draw = @import("cpu/draw.zig"); -const ImageSize = basic_models.ImageSize; -const ImageRect = basic_models.ImageRect; +const Size_i = basic_models.Size_i; +const Rect_i = basic_models.Rect_i; const Allocator = std.mem.Allocator; const Color = dvui.Color; @@ -35,7 +35,7 @@ pub fn exampleReset(self: *CpuRenderEngine) void { self.gradient_end = Color.PMA{ .r = random.int(u8), .g = random.int(u8), .b = random.int(u8), .a = 255 }; } -fn renderGradient(self: CpuRenderEngine, pixels: []Color.PMA, width: u32, height: u32, full_w: u32, full_h: u32, visible_rect: ImageRect) void { +fn renderGradient(self: CpuRenderEngine, pixels: []Color.PMA, width: u32, height: u32, full_w: u32, full_h: u32, visible_rect: Rect_i) void { var y: u32 = 0; while (y < height) : (y += 1) { var x: u32 = 0; @@ -61,7 +61,7 @@ fn renderGradient(self: CpuRenderEngine, pixels: []Color.PMA, width: u32, height } } -fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: ImageSize, visible_rect: ImageRect) void { +fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: Size_i, visible_rect: Rect_i) void { _ = self; const colors = [_]Color.PMA{ @@ -152,7 +152,7 @@ fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: ImageS } } -pub fn example(self: CpuRenderEngine, canvas_size: ImageSize, visible_rect: ImageRect) !?dvui.Texture { +pub fn example(self: CpuRenderEngine, canvas_size: Size_i, visible_rect: Rect_i) !?dvui.Texture { const full_w = canvas_size.w; const full_h = canvas_size.h; @@ -175,7 +175,7 @@ pub fn renderEngine(self: *CpuRenderEngine) RenderEngine { } /// Растеризует документ: фон + рекурсивная отрисовка фигур через конвейер (трансформ, прозрачность, наложение). -pub fn renderDocument(self: *CpuRenderEngine, document: *const Document, canvas_size: ImageSize, visible_rect: ImageRect) !?dvui.Texture { +pub fn renderDocument(self: *CpuRenderEngine, document: *const Document, canvas_size: Size_i, visible_rect: Rect_i) !?dvui.Texture { const width = visible_rect.w; const height = visible_rect.h; const pixels = try self._allocator.alloc(Color.PMA, @as(usize, width) * height); diff --git a/src/render/RenderEngine.zig b/src/render/RenderEngine.zig index e1f5bf5..5bfc29f 100644 --- a/src/render/RenderEngine.zig +++ b/src/render/RenderEngine.zig @@ -12,14 +12,14 @@ pub const RenderEngine = union(enum) { } } - pub fn example(self: RenderEngine, canvas_size: basic_models.ImageSize, visible_rect: basic_models.ImageRect) !?dvui.Texture { + pub fn example(self: RenderEngine, canvas_size: basic_models.Size_i, visible_rect: basic_models.Rect_i) !?dvui.Texture { return switch (self) { .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 { + pub fn render(self: RenderEngine, document: *const Document, canvas_size: basic_models.Size_i, visible_rect: basic_models.Rect_i) !?dvui.Texture { return switch (self) { .cpu => |cpu_r| cpu_r.renderDocument(document, canvas_size, visible_rect), }; diff --git a/src/render/cpu/broken.zig b/src/render/cpu/broken.zig index 174e9f3..70491cb 100644 --- a/src/render/cpu/broken.zig +++ b/src/render/cpu/broken.zig @@ -7,6 +7,7 @@ const Color = @import("dvui").Color; 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 { @@ -14,9 +15,9 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void { 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; 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); + line.drawLine(ctx, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, stroke, thickness); } } diff --git a/src/render/cpu/draw.zig b/src/render/cpu/draw.zig index 93e4351..7b2a506 100644 --- a/src/render/cpu/draw.zig +++ b/src/render/cpu/draw.zig @@ -6,17 +6,17 @@ const ellipse = @import("ellipse.zig"); const broken = @import("broken.zig"); const arc = @import("arc.zig"); const basic_models = @import("../../models/basic_models.zig"); -const ImageRect = basic_models.ImageRect; -const ImageSize = basic_models.ImageSize; +const Rect_i = basic_models.Rect_i; +const Size_i = basic_models.Size_i; const Object = Document.Object; const DrawContext = pipeline.DrawContext; const Transform = pipeline.Transform; fn getLocalTransform(obj: *const Object) Transform { - const pos = if (obj.getProperty(.position)) |p| p.position else basic_models.Point2{ .x = 0, .y = 0 }; + const pos = if (obj.getProperty(.position)) |p| p.position else basic_models.Point2_f{ .x = 0, .y = 0 }; const angle = if (obj.getProperty(.angle)) |p| p.angle else 0; - const scale = if (obj.getProperty(.scale)) |p| p.scale else basic_models.Scale2{ .scale_x = 1, .scale_y = 1 }; + const scale = if (obj.getProperty(.scale)) |p| p.scale else basic_models.Scale2_f{ .scale_x = 1, .scale_y = 1 }; const opacity = if (obj.getProperty(.opacity)) |p| p.opacity else 1.0; return .{ .position = pos, @@ -53,12 +53,12 @@ pub fn drawDocument( pixels: []@import("dvui").Color.PMA, buf_width: u32, buf_height: u32, - visible_rect: ImageRect, + visible_rect: Rect_i, document: *const Document, - canvas_size: ImageSize, + canvas_size: Size_i, ) void { - 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; + 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; var ctx = DrawContext{ .pixels = pixels, @@ -68,7 +68,8 @@ pub fn drawDocument( .scale_x = scale_x, .scale_y = scale_y, }; - + // вывести visible_rect + std.debug.print("visible_rect: {{ x: {}, y: {}, w: {}, h: {} }}\n", .{ visible_rect.x, visible_rect.y, visible_rect.w, visible_rect.h }); const identity = Transform{}; for (document.objects.items) |*obj| { drawObject(&ctx, obj, identity); diff --git a/src/render/cpu/line.zig b/src/render/cpu/line.zig index 0d9dc4c..ce8d528 100644 --- a/src/render/cpu/line.zig +++ b/src/render/cpu/line.zig @@ -1,4 +1,3 @@ -const std = @import("std"); const Document = @import("../../models/Document.zig"); const pipeline = @import("pipeline.zig"); const DrawContext = pipeline.DrawContext; @@ -6,6 +5,7 @@ const Color = @import("dvui").Color; 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) до end_point. Растеризация в координатах буфера (без пробелов при зуме). pub fn draw(ctx: *DrawContext, obj: *const Object) void { @@ -13,25 +13,21 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void { const end_x = ep_prop.end_point.x; const end_y = ep_prop.end_point.y; const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke; - - drawLine(ctx, 0, 0, end_x, end_y, stroke); + const thickness = if (obj.getProperty(.thickness)) |t| t.thickness else default_thickness; + drawLine(ctx, 0, 0, end_x, end_y, stroke, thickness); } /// Линия по локальным координатам фигуры: переводит концы в буфер и рисует в пикселях буфера. -pub fn drawLine(ctx: *DrawContext, x0: f32, y0: f32, x1: f32, y1: f32, color: Color.PMA) void { +pub fn drawLine(ctx: *DrawContext, x0: f32, y0: f32, x1: f32, y1: f32, color: Color.PMA, thickness: f32) void { const w0 = ctx.localToWorld(x0, y0); const w1 = ctx.localToWorld(x1, y1); - const b0 = ctx.worldToBufferF(w0.x, w0.y); - const b1 = ctx.worldToBufferF(w1.x, w1.y); - const bx0: i32 = @intFromFloat(std.math.round(b0.x)); - const by0: i32 = @intFromFloat(std.math.round(b0.y)); - const bx1: i32 = @intFromFloat(std.math.round(b1.x)); - const by1: i32 = @intFromFloat(std.math.round(b1.y)); - drawLineInBuffer(ctx, bx0, by0, bx1, by1, color); + const b0 = ctx.worldToBuffer(w0.x, w0.y); + const b1 = ctx.worldToBuffer(w1.x, w1.y); + drawLineInBuffer(ctx, b0.x, b0.y, b1.x, b1.y, color, thickness); } /// Брезенхем в координатах буфера; пиксели вне [0, buf_width) x [0, buf_height) пропускаются. -pub fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, color: Color.PMA) void { +fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, color: Color.PMA, thickness: f32) void { const bw: i32 = @intCast(ctx.buf_width); const bh: i32 = @intCast(ctx.buf_height); const dx: i32 = @intCast(@abs(bx1 - bx0)); @@ -42,6 +38,8 @@ pub fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i3 var x = bx0; var y = by0; + _ = thickness; + while (true) { if (x >= 0 and x < bw and y >= 0 and y < bh) { ctx.blendPixelAtBuffer(@intCast(x), @intCast(y), color); diff --git a/src/render/cpu/pipeline.zig b/src/render/cpu/pipeline.zig index 99eb17c..ba282f2 100644 --- a/src/render/cpu/pipeline.zig +++ b/src/render/cpu/pipeline.zig @@ -1,16 +1,17 @@ const std = @import("std"); const dvui = @import("dvui"); const basic_models = @import("../../models/basic_models.zig"); -const Point2 = basic_models.Point2; -const Scale2 = basic_models.Scale2; -const ImageRect = basic_models.ImageRect; +const Point2_f = basic_models.Point2_f; +const Point2_i = basic_models.Point2_i; +const Scale2_f = basic_models.Scale2_f; +const Rect_i = basic_models.Rect_i; const Color = dvui.Color; /// Трансформ объекта в мировых координатах документа (позиция, угол, масштаб, непрозрачность). pub const Transform = struct { - position: Point2 = .{}, + position: Point2_f = .{}, angle: f32 = 0, - scale: Scale2 = .{}, + scale: Scale2_f = .{}, opacity: f32 = 1.0, /// Композиция: мировой трансформ = parent * local (local в пространстве родителя). @@ -41,7 +42,7 @@ pub const DrawContext = struct { pixels: []Color.PMA, buf_width: u32, buf_height: u32, - visible_rect: ImageRect, + visible_rect: Rect_i, scale_x: f32, scale_y: f32, transform: Transform = .{}, @@ -51,7 +52,7 @@ pub const DrawContext = struct { } /// Локальные координаты фигуры -> мировые (документ). - pub fn localToWorld(self: *const DrawContext, local_x: f32, local_y: f32) Point2 { + pub fn localToWorld(self: *const DrawContext, local_x: f32, local_y: f32) Point2_f { const t = &self.transform; const cos_a = std.math.cos(t.angle); const sin_a = std.math.sin(t.angle); @@ -61,8 +62,8 @@ pub const DrawContext = struct { }; } - /// Мировые координаты документа -> координаты в буфере (могут быть вне [0, buf_w] x [0, buf_h]). - pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2 { + /// Мировые координаты документа -> координаты в буфере (float; могут быть вне [0, buf_w] x [0, buf_h]). + pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2_f { const canvas_x = wx * self.scale_x; const canvas_y = wy * self.scale_y; const vx = @as(f32, @floatFromInt(self.visible_rect.x)); @@ -73,8 +74,17 @@ pub const DrawContext = struct { }; } + /// Мировые координаты документа -> целочисленные координаты в буфере (округление до ближайшего пикселя). + pub fn worldToBuffer(self: *const DrawContext, wx: f32, wy: f32) Point2_i { + const b = self.worldToBufferF(wx, wy); + return .{ + .x = @intFromFloat(std.math.round(b.x)), + .y = @intFromFloat(std.math.round(b.y)), + }; + } + /// Координаты буфера -> мировые (документ). scale_x/scale_y не должны быть 0. - pub fn bufferToWorld(self: *const DrawContext, buf_x: f32, buf_y: f32) Point2 { + pub fn bufferToWorld(self: *const DrawContext, buf_x: f32, buf_y: f32) Point2_f { const vx = @as(f32, @floatFromInt(self.visible_rect.x)); const vy = @as(f32, @floatFromInt(self.visible_rect.y)); const canvas_x = buf_x + vx; @@ -88,7 +98,7 @@ pub const DrawContext = struct { } /// Мировые координаты -> локальные фигуры (обратное к localToWorld). - pub fn worldToLocal(self: *const DrawContext, wx: f32, wy: f32) Point2 { + pub fn worldToLocal(self: *const DrawContext, wx: f32, wy: f32) Point2_f { const t = &self.transform; const dx = wx - t.position.x; const dy = wy - t.position.y; diff --git a/src/ui/canvas_view.zig b/src/ui/canvas_view.zig index 3dbb391..ea60149 100644 --- a/src/ui/canvas_view.zig +++ b/src/ui/canvas_view.zig @@ -2,7 +2,7 @@ const std = @import("std"); const dvui = @import("dvui"); const dvui_ext = @import("dvui_ext.zig"); const Canvas = @import("../Canvas.zig"); -const ImageRect = @import("../models/basic_models.zig").ImageRect; +const Rect_i = @import("../models/basic_models.zig").Rect_i; pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void { var textured = dvui_ext.texturedBox(content_rect_scale, dvui.Rect.all(20)); @@ -67,7 +67,7 @@ fn drawCanvasContent(canvas: *Canvas, scroll: anytype) void { ); { if (canvas.texture) |tex| { - const vis = canvas._visible_rect orelse ImageRect{ .x = 0, .y = 0, .w = 0, .h = 0 }; + const vis = canvas._visible_rect orelse Rect_i{ .x = 0, .y = 0, .w = 0, .h = 0 }; const left = @as(f32, @floatFromInt(img_size.x + vis.x)) / natural_scale; const top = @as(f32, @floatFromInt(img_size.y + vis.y)) / natural_scale;