feat: Добавил толщину линий, унифицировал геометрию

Переименовал основные геометрические модели (Point, Size, Rect, Scale, Radii), явно разделив их на типы с плавающей точкой (_f) и целочисленные (_i). Обновил использование этих типов во всем проекте для улучшения типобезопасности и ясности.

Ввел новое свойство thickness для объектов и реализовал его применение при отрисовке линий и ломаных. Добавил Point2_i для целочисленных координат буфера в конвейере отрисовки.
This commit is contained in:
2026-02-25 00:57:55 +03:00
parent 1d995995e7
commit 0114db1f48
19 changed files with 124 additions and 106 deletions

View File

@@ -4,9 +4,9 @@ const dvui = @import("dvui");
const Document = @import("models/Document.zig"); const Document = @import("models/Document.zig");
const RenderEngine = @import("render/RenderEngine.zig").RenderEngine; const RenderEngine = @import("render/RenderEngine.zig").RenderEngine;
const basic_models = @import("models/basic_models.zig"); const basic_models = @import("models/basic_models.zig");
const ImageRect = basic_models.ImageRect; const Rect_i = basic_models.Rect_i;
const ImageSize = basic_models.ImageSize; const Size_i = basic_models.Size_i;
const Point2 = @import("models/basic_models.zig").Point2; const Point2_f = @import("models/basic_models.zig").Point2_f;
const Color = dvui.Color; const Color = dvui.Color;
const Canvas = @This(); const Canvas = @This();
@@ -22,11 +22,11 @@ scroll: dvui.ScrollInfo = .{
}, },
native_scaling: bool = true, native_scaling: bool = true,
redraw_throttle_ms: u32 = 50, redraw_throttle_ms: u32 = 50,
_visible_rect: ?ImageRect = null, _visible_rect: ?Rect_i = null,
_zoom: f32 = 1, _zoom: f32 = 1,
_redraw_pending: bool = false, _redraw_pending: bool = false,
_last_redraw_time_ms: i64 = 0, _last_redraw_time_ms: i64 = 0,
cursor_document_point: ?Point2 = null, cursor_document_point: ?Point2_f = null,
/// true — рисовать документ (render), false — пример (gradient/squares). /// true — рисовать документ (render), false — пример (gradient/squares).
draw_document: bool = true, draw_document: bool = true,
@@ -48,7 +48,7 @@ pub fn deinit(self: *Canvas) void {
fn redraw(self: *Canvas) !void { fn redraw(self: *Canvas) !void {
const full = self.getZoomedImageSize(); 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 (vis.w == 0 or vis.h == 0) {
if (self.texture) |tex| { if (self.texture) |tex| {
@@ -58,7 +58,7 @@ fn redraw(self: *Canvas) !void {
return; 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) const new_texture = if (self.draw_document)
self.render_engine.render(self.document, canvas_size, vis) catch null self.render_engine.render(self.document, canvas_size, vis) catch null
else else
@@ -106,17 +106,17 @@ pub fn processPendingRedraw(self: *Canvas) !void {
try self.redraw(); try self.redraw();
} }
pub fn getZoomedImageSize(self: Canvas) ImageRect { pub fn getZoomedImageSize(self: Canvas) Rect_i {
const doc = self.document; const doc = self.document;
return .{ return .{
.x = @intFromFloat(self.pos.x), .x = @intFromFloat(self.pos.x),
.y = @intFromFloat(self.pos.y), .y = @intFromFloat(self.pos.y),
.w = @intFromFloat(doc.size.width * self._zoom), .w = @intFromFloat(doc.size.w * self._zoom),
.h = @intFromFloat(doc.size.height * 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 img = self.getZoomedImageSize();
const left_n = @as(f32, @floatFromInt(img.x)) / natural_scale; const left_n = @as(f32, @floatFromInt(img.x)) / natural_scale;
const top_n = @as(f32, @floatFromInt(img.y)) / 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_x = content_point.x * natural_scale - @as(f32, @floatFromInt(img.x));
const px_y = content_point.y * natural_scale - @as(f32, @floatFromInt(img.y)); const px_y = content_point.y * natural_scale - @as(f32, @floatFromInt(img.y));
return Point2{ return Point2_f{
.x = px_x / self._zoom, .x = px_x / self._zoom,
.y = px_y / self._zoom, .y = px_y / self._zoom,
}; };
@@ -148,7 +148,7 @@ pub fn updateVisibleImageRect(self: *Canvas, viewport: dvui.Rect, scroll_offset:
return false; 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 image_rect = self.getZoomedImageSize();
const img_w: u32 = image_rect.w; 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_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))); 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, .x = vis_x,
.y = vis_y, .y = vis_y,
.w = vis_w, .w = vis_w,

View File

@@ -14,7 +14,7 @@ pub const OpenDocument = struct {
canvas: Canvas, canvas: Canvas,
pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void { 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.document = Document.init(allocator, default_size);
self.cpu_render = CpuRenderEngine.init(allocator, .Squares); self.cpu_render = CpuRenderEngine.init(allocator, .Squares);
self.canvas = Canvas.init(allocator, &self.document, (&self.cpu_render).renderEngine()); self.canvas = Canvas.init(allocator, &self.document, (&self.cpu_render).renderEngine());

View File

@@ -1,16 +1,16 @@
const std = @import("std"); const std = @import("std");
const basic_models = @import("basic_models.zig"); const basic_models = @import("basic_models.zig");
const Size = basic_models.Size; const Size_f = basic_models.Size_f;
const Document = @This(); const Document = @This();
pub const Object = @import("Object.zig"); pub const Object = @import("Object.zig");
const shape = @import("shape/shape.zig"); const shape = @import("shape/shape.zig");
size: Size, size: Size_f,
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
objects: std.ArrayList(Object), 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 .{ return .{
.size = size, .size = size,
.allocator = allocator, .allocator = allocator,

View File

@@ -18,6 +18,7 @@ const default_common_data = [_]PropertyData{
.{ .opacity = 1.0 }, .{ .opacity = 1.0 },
.{ .locked = false }, .{ .locked = false },
.{ .stroke_rgba = 0x000000FF }, .{ .stroke_rgba = 0x000000FF },
.{ .thickness = 2.0 },
}; };
pub const defaultCommonProperties: [default_common_data.len]Property = blk: { 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; return;
} }
} }
std.debug.print("Property not found: {s}\n", .{@tagName(prop.data)});
return error.PropertyNotFound; return error.PropertyNotFound;
} }

View File

@@ -1,23 +1,23 @@
const std = @import("std"); const std = @import("std");
const basic_models = @import("basic_models.zig"); const basic_models = @import("basic_models.zig");
const Point2 = basic_models.Point2; const Point2_f = basic_models.Point2_f;
const Scale2 = basic_models.Scale2; const Scale2_f = basic_models.Scale2_f;
const Size = basic_models.Size; const Size_f = basic_models.Size_f;
const Radii = basic_models.Radii; const Radii_f = basic_models.Radii_f;
pub const Data = union(enum) { pub const Data = union(enum) {
position: Point2, position: Point2_f,
angle: f32, angle: f32,
scale: Scale2, scale: Scale2_f,
visible: bool, visible: bool,
opacity: f32, opacity: f32,
locked: bool, locked: bool,
size: Size, size: Size_f,
radii: Radii, radii: Radii_f,
end_point: Point2, end_point: Point2_f,
points: std.ArrayList(Point2), points: std.ArrayList(Point2_f),
fill_rgba: u32, fill_rgba: u32,
stroke_rgba: u32, stroke_rgba: u32,

View File

@@ -1,37 +1,43 @@
pub const ImageRect = struct { pub const Rect_i = struct {
x: u32, x: u32,
y: u32, y: u32,
w: u32, w: u32,
h: u32, h: u32,
}; };
pub const ImageSize = struct { pub const Size_i = struct {
w: u32, w: u32,
h: u32, h: u32,
}; };
pub const Size = struct { pub const Size_f = struct {
width: f32, w: f32,
height: f32, h: f32,
}; };
pub const Point2 = struct { pub const Point2_f = struct {
x: f32 = 0, x: f32 = 0,
y: 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, x: f32,
y: f32, y: f32,
}; };
pub const Scale2 = struct { pub const Scale2_f = struct {
scale_x: f32 = 1, scale_x: f32 = 1,
scale_y: f32 = 1, scale_y: f32 = 1,
}; };
/// Прямоугольник в координатах документа (f32), например локальные границы объекта. /// Прямоугольник в координатах документа (f32), например локальные границы объекта.
pub const Rect = struct { pub const Rect_f = struct {
x: f32 = 0, x: f32 = 0,
y: f32 = 0, y: f32 = 0,
w: f32 = 0, w: f32 = 0,

View File

@@ -3,10 +3,10 @@ const Document = @import("Document.zig");
const Object = Document.Object; const Object = Document.Object;
const shape = @import("shape/shape.zig"); const shape = @import("shape/shape.zig");
const basic_models = @import("basic_models.zig"); const basic_models = @import("basic_models.zig");
const Size = basic_models.Size; const Size_f = basic_models.Size_f;
const Point2 = basic_models.Point2; const Point2_f = basic_models.Point2_f;
const Scale2 = basic_models.Scale2; const Scale2_f = basic_models.Scale2_f;
const Radii = basic_models.Radii; const Radii_f = basic_models.Radii_f;
fn randFloat(rng: std.Random, min: f32, max: f32) f32 { fn randFloat(rng: std.Random, min: f32, max: f32) f32 {
return min + (max - min) * rng.float(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 margin: f32 = 8;
const max_x = @max(0, doc_size.width - margin); const max_x = @max(0, doc_size.w - margin);
const max_y = @max(0, doc_size.height - margin); const max_y = @max(0, doc_size.h - margin);
try obj.setProperty(allocator, .{ .data = .{ try obj.setProperty(allocator, .{ .data = .{
.position = .{ .position = .{
@@ -44,17 +44,19 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size
.scale_y = randFloat(rng, 0.25, 2.0), .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 = .{ .opacity = randFloat(rng, 0.3, 1.0) } });
try obj.setProperty(allocator, .{ .data = .{ .locked = rng.boolean() } }); try obj.setProperty(allocator, .{ .data = .{ .locked = rng.boolean() } });
const stroke = randRgba(rng); const stroke = randRgba(rng);
try obj.setProperty(allocator, .{ .data = .{ .stroke_rgba = stroke } }); try obj.setProperty(allocator, .{ .data = .{ .stroke_rgba = stroke } });
obj.setProperty(allocator, .{ .data = .{ .fill_rgba = randRgba(rng) } }) catch {}; 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) { switch (obj.shape) {
.line => { .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); const angle = randFloat(rng, 0, 2 * std.math.pi);
try obj.setProperty(allocator, .{ .data = .{ try obj.setProperty(allocator, .{ .data = .{
.end_point = .{ .end_point = .{
@@ -64,7 +66,7 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size
} }); } });
}, },
.ellipse => { .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 = .{ try obj.setProperty(allocator, .{ .data = .{
.radii = .{ .radii = .{
.x = randFloat(rng, 8, @max(8, max_r)), .x = randFloat(rng, 8, @max(8, max_r)),
@@ -73,7 +75,7 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size
} }); } });
}, },
.broken => { .broken => {
var points = std.ArrayList(Point2).empty; var points = std.ArrayList(Point2_f).empty;
const n = rng.intRangeLessThan(usize, 2, 9); const n = rng.intRangeLessThan(usize, 2, 9);
var x: f32 = 0; var x: f32 = 0;
var y: f32 = 0; var y: f32 = 0;
@@ -96,7 +98,7 @@ pub fn addRandomShapes(doc: *Document, rng: std.Random) !void {
var total_count: usize = 0; var total_count: usize = 0;
const allocator = doc.allocator; const allocator = doc.allocator;
const n_root = rng.intRangeLessThan(usize, 1, 5); const n_root = rng.intRangeLessThan(usize, 6, 15);
for (0..n_root) |_| { for (0..n_root) |_| {
if (total_count >= max_total) break; if (total_count >= max_total) break;
var obj = try shape.createObject(allocator, randomShapeKind(rng)); var obj = try shape.createObject(allocator, randomShapeKind(rng));

View File

@@ -1,7 +1,7 @@
const std = @import("std"); const std = @import("std");
const Object = @import("../Object.zig"); const Object = @import("../Object.zig");
const PropertyData = @import("../Property.zig").Data; 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"); const shape_mod = @import("shape.zig");
/// Теги обязательных свойств (заглушка: arc пока не реализован). /// Теги обязательных свойств (заглушка: 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); try shape_mod.ensure(obj, .arc);
return error.ArcNotImplemented; return error.ArcNotImplemented;
} }

View File

@@ -2,12 +2,12 @@ const std = @import("std");
const Object = @import("../Object.zig"); const Object = @import("../Object.zig");
const Property = @import("../Property.zig").Property; const Property = @import("../Property.zig").Property;
const PropertyData = @import("../Property.zig").Data; const PropertyData = @import("../Property.zig").Data;
const Point2 = @import("../basic_models.zig").Point2; const Point2_f = @import("../basic_models.zig").Point2_f;
const Rect = @import("../basic_models.zig").Rect; const Rect_f = @import("../basic_models.zig").Rect_f;
const shape_mod = @import("shape.zig"); const shape_mod = @import("shape.zig");
/// Точки ломаной по умолчанию (для создания). /// Точки ломаной по умолчанию (для создания).
pub const default_points = [_]Point2{ pub const default_points = [_]Point2_f{
.{ .x = 0, .y = 0 }, .{ .x = 0, .y = 0 },
.{ .x = 80, .y = 0 }, .{ .x = 80, .y = 0 },
.{ .x = 80, .y = 60 }, .{ .x = 80, .y = 60 },
@@ -22,13 +22,13 @@ pub fn getRequiredTags() []const std.meta.Tag(PropertyData) {
/// Добавляет к объекту свойства по умолчанию для ломаной (points из default_points). /// Добавляет к объекту свойства по умолчанию для ломаной (points из default_points).
pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) !void { 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 points.appendSlice(allocator, &default_points);
try obj.properties.append(allocator, .{ .data = .{ .points = points } }); try obj.properties.append(allocator, .{ .data = .{ .points = points } });
} }
/// Локальные границы ломаной: AABB по всем точкам. /// Локальные границы ломаной: AABB по всем точкам.
pub fn getLocalBounds(obj: *const Object) !Rect { pub fn getLocalBounds(obj: *const Object) !Rect_f {
try shape_mod.ensure(obj, .broken); try shape_mod.ensure(obj, .broken);
const p = obj.getProperty(.points).?; const p = obj.getProperty(.points).?;
if (p.points.items.len == 0) return error.EmptyPoints; if (p.points.items.len == 0) return error.EmptyPoints;

View File

@@ -2,7 +2,7 @@ const std = @import("std");
const Object = @import("../Object.zig"); const Object = @import("../Object.zig");
const Property = @import("../Property.zig").Property; const Property = @import("../Property.zig").Property;
const PropertyData = @import("../Property.zig").Data; 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"); const shape_mod = @import("shape.zig");
/// Свойства фигуры по умолчанию (для создания и проверки типа). Теги для ensure выводятся отсюда. /// Свойства фигуры по умолчанию (для создания и проверки типа). Теги для ensure выводятся отсюда.
@@ -21,7 +21,7 @@ pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object)
} }
/// Локальные границы эллипса: [-radii.x, -radii.y] .. [radii.x, radii.y]. /// Локальные границы эллипса: [-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); try shape_mod.ensure(obj, .ellipse);
const r = obj.getProperty(.radii).?; const r = obj.getProperty(.radii).?;
return .{ return .{

View File

@@ -2,7 +2,7 @@ const std = @import("std");
const Object = @import("../Object.zig"); const Object = @import("../Object.zig");
const Property = @import("../Property.zig").Property; const Property = @import("../Property.zig").Property;
const PropertyData = @import("../Property.zig").Data; 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"); const shape_mod = @import("shape.zig");
/// Свойства фигуры по умолчанию (для создания и проверки типа). Теги для ensure выводятся отсюда. /// Свойства фигуры по умолчанию (для создания и проверки типа). Теги для ensure выводятся отсюда.
@@ -21,7 +21,7 @@ pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object)
} }
/// Локальные границы линии: от (0,0) до end_point. /// Локальные границы линии: от (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); try shape_mod.ensure(obj, .line);
const ep = obj.getProperty(.end_point).?; const ep = obj.getProperty(.end_point).?;
const min_x = @min(0, ep.end_point.x); const min_x = @min(0, ep.end_point.x);

View File

@@ -9,7 +9,7 @@ const ellipse = @import("ellipse.zig");
const broken = @import("broken.zig"); const broken = @import("broken.zig");
const arc = @import("arc.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 { pub fn createObject(allocator: std.mem.Allocator, shape_kind: Object.ShapeKind) !Object {

View File

@@ -5,8 +5,8 @@ const RenderEngine = @import("RenderEngine.zig").RenderEngine;
const Document = @import("../models/Document.zig"); const Document = @import("../models/Document.zig");
const basic_models = @import("../models/basic_models.zig"); const basic_models = @import("../models/basic_models.zig");
const cpu_draw = @import("cpu/draw.zig"); const cpu_draw = @import("cpu/draw.zig");
const ImageSize = basic_models.ImageSize; const Size_i = basic_models.Size_i;
const ImageRect = basic_models.ImageRect; const Rect_i = basic_models.Rect_i;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Color = dvui.Color; 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 }; 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; var y: u32 = 0;
while (y < height) : (y += 1) { while (y < height) : (y += 1) {
var x: u32 = 0; 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; _ = self;
const colors = [_]Color.PMA{ 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_w = canvas_size.w;
const full_h = canvas_size.h; 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 width = visible_rect.w;
const height = visible_rect.h; const height = visible_rect.h;
const pixels = try self._allocator.alloc(Color.PMA, @as(usize, width) * height); const pixels = try self._allocator.alloc(Color.PMA, @as(usize, width) * height);

View File

@@ -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) { return switch (self) {
.cpu => |cpu_r| cpu_r.example(canvas_size, visible_rect), .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) { return switch (self) {
.cpu => |cpu_r| cpu_r.renderDocument(document, canvas_size, visible_rect), .cpu => |cpu_r| cpu_r.renderDocument(document, canvas_size, visible_rect),
}; };

View File

@@ -7,6 +7,7 @@ const Color = @import("dvui").Color;
const Object = Document.Object; const Object = Document.Object;
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 }; const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
const default_thickness: f32 = 2.0;
/// Рисует ломаную по точкам в локальных координатах. Обводка по stroke_rgba. /// Рисует ломаную по точкам в локальных координатах. Обводка по stroke_rgba.
pub fn draw(ctx: *DrawContext, obj: *const Object) void { 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; const pts = p_prop.points.items;
if (pts.len < 2) return; if (pts.len < 2) return;
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke; 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; var i: usize = 0;
while (i + 1 < pts.len) : (i += 1) { 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);
} }
} }

View File

@@ -6,17 +6,17 @@ const ellipse = @import("ellipse.zig");
const broken = @import("broken.zig"); const broken = @import("broken.zig");
const arc = @import("arc.zig"); const arc = @import("arc.zig");
const basic_models = @import("../../models/basic_models.zig"); const basic_models = @import("../../models/basic_models.zig");
const ImageRect = basic_models.ImageRect; const Rect_i = basic_models.Rect_i;
const ImageSize = basic_models.ImageSize; const Size_i = basic_models.Size_i;
const Object = Document.Object; const Object = Document.Object;
const DrawContext = pipeline.DrawContext; const DrawContext = pipeline.DrawContext;
const Transform = pipeline.Transform; const Transform = pipeline.Transform;
fn getLocalTransform(obj: *const Object) 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 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; const opacity = if (obj.getProperty(.opacity)) |p| p.opacity else 1.0;
return .{ return .{
.position = pos, .position = pos,
@@ -53,12 +53,12 @@ pub fn drawDocument(
pixels: []@import("dvui").Color.PMA, pixels: []@import("dvui").Color.PMA,
buf_width: u32, buf_width: u32,
buf_height: u32, buf_height: u32,
visible_rect: ImageRect, visible_rect: Rect_i,
document: *const Document, document: *const Document,
canvas_size: ImageSize, canvas_size: Size_i,
) void { ) void {
const scale_x: f32 = if (document.size.width > 0) @as(f32, @floatFromInt(canvas_size.w)) / document.size.width 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.height > 0) @as(f32, @floatFromInt(canvas_size.h)) / document.size.height 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{ var ctx = DrawContext{
.pixels = pixels, .pixels = pixels,
@@ -68,7 +68,8 @@ pub fn drawDocument(
.scale_x = scale_x, .scale_x = scale_x,
.scale_y = scale_y, .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{}; const identity = Transform{};
for (document.objects.items) |*obj| { for (document.objects.items) |*obj| {
drawObject(&ctx, obj, identity); drawObject(&ctx, obj, identity);

View File

@@ -1,4 +1,3 @@
const std = @import("std");
const Document = @import("../../models/Document.zig"); const Document = @import("../../models/Document.zig");
const pipeline = @import("pipeline.zig"); const pipeline = @import("pipeline.zig");
const DrawContext = pipeline.DrawContext; const DrawContext = pipeline.DrawContext;
@@ -6,6 +5,7 @@ const Color = @import("dvui").Color;
const Object = Document.Object; const Object = Document.Object;
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 }; const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
const default_thickness: f32 = 2.0;
/// Рисует линию в локальных координатах: от (0,0) до end_point. Растеризация в координатах буфера (без пробелов при зуме). /// Рисует линию в локальных координатах: от (0,0) до end_point. Растеризация в координатах буфера (без пробелов при зуме).
pub fn draw(ctx: *DrawContext, obj: *const Object) void { 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_x = ep_prop.end_point.x;
const end_y = ep_prop.end_point.y; const end_y = ep_prop.end_point.y;
const stroke = if (obj.getProperty(.stroke_rgba)) |s| pipeline.rgbaToPma(s.stroke_rgba) else default_stroke; 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;
drawLine(ctx, 0, 0, end_x, end_y, stroke); 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 w0 = ctx.localToWorld(x0, y0);
const w1 = ctx.localToWorld(x1, y1); const w1 = ctx.localToWorld(x1, y1);
const b0 = ctx.worldToBufferF(w0.x, w0.y); const b0 = ctx.worldToBuffer(w0.x, w0.y);
const b1 = ctx.worldToBufferF(w1.x, w1.y); const b1 = ctx.worldToBuffer(w1.x, w1.y);
const bx0: i32 = @intFromFloat(std.math.round(b0.x)); drawLineInBuffer(ctx, b0.x, b0.y, b1.x, b1.y, color, thickness);
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);
} }
/// Брезенхем в координатах буфера; пиксели вне [0, buf_width) x [0, buf_height) пропускаются. /// Брезенхем в координатах буфера; пиксели вне [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 bw: i32 = @intCast(ctx.buf_width);
const bh: i32 = @intCast(ctx.buf_height); const bh: i32 = @intCast(ctx.buf_height);
const dx: i32 = @intCast(@abs(bx1 - bx0)); 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 x = bx0;
var y = by0; var y = by0;
_ = thickness;
while (true) { while (true) {
if (x >= 0 and x < bw and y >= 0 and y < bh) { if (x >= 0 and x < bw and y >= 0 and y < bh) {
ctx.blendPixelAtBuffer(@intCast(x), @intCast(y), color); ctx.blendPixelAtBuffer(@intCast(x), @intCast(y), color);

View File

@@ -1,16 +1,17 @@
const std = @import("std"); const std = @import("std");
const dvui = @import("dvui"); const dvui = @import("dvui");
const basic_models = @import("../../models/basic_models.zig"); const basic_models = @import("../../models/basic_models.zig");
const Point2 = basic_models.Point2; const Point2_f = basic_models.Point2_f;
const Scale2 = basic_models.Scale2; const Point2_i = basic_models.Point2_i;
const ImageRect = basic_models.ImageRect; const Scale2_f = basic_models.Scale2_f;
const Rect_i = basic_models.Rect_i;
const Color = dvui.Color; const Color = dvui.Color;
/// Трансформ объекта в мировых координатах документа (позиция, угол, масштаб, непрозрачность). /// Трансформ объекта в мировых координатах документа (позиция, угол, масштаб, непрозрачность).
pub const Transform = struct { pub const Transform = struct {
position: Point2 = .{}, position: Point2_f = .{},
angle: f32 = 0, angle: f32 = 0,
scale: Scale2 = .{}, scale: Scale2_f = .{},
opacity: f32 = 1.0, opacity: f32 = 1.0,
/// Композиция: мировой трансформ = parent * local (local в пространстве родителя). /// Композиция: мировой трансформ = parent * local (local в пространстве родителя).
@@ -41,7 +42,7 @@ pub const DrawContext = struct {
pixels: []Color.PMA, pixels: []Color.PMA,
buf_width: u32, buf_width: u32,
buf_height: u32, buf_height: u32,
visible_rect: ImageRect, visible_rect: Rect_i,
scale_x: f32, scale_x: f32,
scale_y: f32, scale_y: f32,
transform: Transform = .{}, 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 t = &self.transform;
const cos_a = std.math.cos(t.angle); const cos_a = std.math.cos(t.angle);
const sin_a = std.math.sin(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]). /// Мировые координаты документа -> координаты в буфере (float; могут быть вне [0, buf_w] x [0, buf_h]).
pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2 { pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2_f {
const canvas_x = wx * self.scale_x; const canvas_x = wx * self.scale_x;
const canvas_y = wy * self.scale_y; const canvas_y = wy * self.scale_y;
const vx = @as(f32, @floatFromInt(self.visible_rect.x)); 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. /// Координаты буфера -> мировые (документ). 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 vx = @as(f32, @floatFromInt(self.visible_rect.x));
const vy = @as(f32, @floatFromInt(self.visible_rect.y)); const vy = @as(f32, @floatFromInt(self.visible_rect.y));
const canvas_x = buf_x + vx; const canvas_x = buf_x + vx;
@@ -88,7 +98,7 @@ pub const DrawContext = struct {
} }
/// Мировые координаты -> локальные фигуры (обратное к localToWorld). /// Мировые координаты -> локальные фигуры (обратное к 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 t = &self.transform;
const dx = wx - t.position.x; const dx = wx - t.position.x;
const dy = wy - t.position.y; const dy = wy - t.position.y;

View File

@@ -2,7 +2,7 @@ const std = @import("std");
const dvui = @import("dvui"); const dvui = @import("dvui");
const dvui_ext = @import("dvui_ext.zig"); const dvui_ext = @import("dvui_ext.zig");
const Canvas = @import("../Canvas.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 { pub fn canvasView(canvas: *Canvas, content_rect_scale: dvui.RectScale) void {
var textured = dvui_ext.texturedBox(content_rect_scale, dvui.Rect.all(20)); 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| { 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 left = @as(f32, @floatFromInt(img_size.x + vis.x)) / natural_scale;
const top = @as(f32, @floatFromInt(img_size.y + vis.y)) / natural_scale; const top = @as(f32, @floatFromInt(img_size.y + vis.y)) / natural_scale;