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

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