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 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,

View File

@@ -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());

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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));

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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 .{

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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);

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) {
.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),
};

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;