From 40b3b0ad105cbc9ff1335a282705cb893fdd5cbb Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Thu, 26 Feb 2026 18:16:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D1=82=D0=BE=D0=BB=D1=89=D0=B8=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/models/shape/line.zig | 2 +- src/render/cpu/line.zig | 193 ++++++++++++++++++++++++++++---------- 2 files changed, 145 insertions(+), 50 deletions(-) diff --git a/src/models/shape/line.zig b/src/models/shape/line.zig index 9c3323f..7166b04 100644 --- a/src/models/shape/line.zig +++ b/src/models/shape/line.zig @@ -7,7 +7,7 @@ const shape_mod = @import("shape.zig"); /// Свойства фигуры по умолчанию. pub const default_shape_properties = [_]Property{ - .{ .data = .{ .end_point = .{ .x = 100, .y = 0 } } }, + .{ .data = .{ .end_point = .{ .x = 100, .y = 200 } } }, }; /// Теги обязательных свойств. diff --git a/src/render/cpu/line.zig b/src/render/cpu/line.zig index 4a9a4c5..086f81b 100644 --- a/src/render/cpu/line.zig +++ b/src/render/cpu/line.zig @@ -3,6 +3,8 @@ const Document = @import("../../models/Document.zig"); const pipeline = @import("pipeline.zig"); const DrawContext = pipeline.DrawContext; const Color = @import("dvui").Color; +const base_models = @import("../../models/basic_models.zig"); +const Point2_i = base_models.Point2_i; const Object = Document.Object; const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 }; @@ -30,6 +32,88 @@ pub fn drawLine(ctx: *DrawContext, x0: f32, y0: f32, x1: f32, y1: f32, color: Co drawLineInBuffer(ctx, b0.x, b0.y, b1.x, b1.y, color, thickness_px); } +inline fn clip(p: f32, q: f32, t0: *f32, t1: *f32) bool { + if (p == 0) { + return q >= 0; + } + const r = q / p; + if (p < 0) { + if (r > t1.*) return false; + if (r > t0.*) t0.* = r; + } else { + if (r < t0.*) return false; + if (r < t1.*) t1.* = r; + } + return true; +} + +/// Liang–Barsky отсечение отрезка (x0,y0)-(x1,y1) прямоугольником [left,right]x[top,bottom]. +/// Координаты концов модифицируются по месту. Возвращает false, если отрезок целиком вне прямоугольника. +fn liangBarskyClip( + x0: *i32, + y0: *i32, + x1: *i32, + y1: *i32, + left: i32, + top: i32, + right: i32, + bottom: i32, +) bool { + const fx0: f32 = @floatFromInt(x0.*); + const fy0: f32 = @floatFromInt(y0.*); + const fx1: f32 = @floatFromInt(x1.*); + const fy1: f32 = @floatFromInt(y1.*); + + const dx = fx1 - fx0; + const dy = fy1 - fy0; + + var t0: f32 = 0.0; + var t1: f32 = 1.0; + + const fl: f32 = @floatFromInt(left); + const ft: f32 = @floatFromInt(top); + const fr: f32 = @floatFromInt(right); + const fb: f32 = @floatFromInt(bottom); + + if (!clip(-dx, fx0 - fl, &t0, &t1)) return false; // x >= left + if (!clip(dx, fr - fx0, &t0, &t1)) return false; // x <= right + if (!clip(-dy, fy0 - ft, &t0, &t1)) return false; // y >= top + if (!clip(dy, fb - fy0, &t0, &t1)) return false; // y <= bottom + + const nx0 = fx0 + dx * t0; + const ny0 = fy0 + dy * t0; + const nx1 = fx0 + dx * t1; + const ny1 = fy0 + dy * t1; + + x0.* = @intFromFloat(std.math.round(nx0)); + y0.* = @intFromFloat(std.math.round(ny0)); + x1.* = @intFromFloat(std.math.round(nx1)); + y1.* = @intFromFloat(std.math.round(ny1)); + + return true; +} + +/// Отсекает отрезок буфером ctx (0..buf_width-1, 0..buf_height-1). +fn clipLineToBuffer(ctx: *DrawContext, a: *Point2_i, b: *Point2_i, thickness: i32) bool { + var x0 = a.x; + var y0 = a.y; + var x1 = b.x; + var y1 = b.y; + + const left: i32 = -thickness; + const top: i32 = -thickness; + const right: i32 = @as(i32, @intCast(ctx.buf_width - 1)) + thickness; + const bottom: i32 = @as(i32, @intCast(ctx.buf_height - 1)) + thickness; + + if (!liangBarskyClip(&x0, &y0, &x1, &y1, left, top, right, bottom)) { + return false; + } + + a.* = .{ .x = x0, .y = y0 }; + b.* = .{ .x = x1, .y = y1 }; + return true; +} + /// Точка (px, py) лежит строго внутри круга с центром (cx, cy) и радиусом в квадрате r_sq (граница не включается). fn insideCircle(px: i32, py: i32, cx: i32, cy: i32, r_sq: i32) bool { const dx = px - cx; @@ -57,62 +141,73 @@ fn fillCircleAtBuffer(ctx: *DrawContext, cx: i32, cy: i32, radius: i32, color: C } fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, color: Color.PMA, thickness_px: u32) void { - const bw = ctx.buf_width; - const bh = ctx.buf_height; - const dx: i32 = @intCast(@abs(bx1 - bx0)); - const dy: i32 = @intCast(@abs(by1 - by0)); - const sx: i32 = if (bx0 < bx1) 1 else -1; - const sy: i32 = if (by0 < by1) 1 else -1; - var err = dx - dy; - var x = bx0; - var y = by0; - const half: i32 = @intCast(thickness_px / 2); - const half_sq = half * half; - const thickness_i: i32 = @intCast(thickness_px); + // Коррекция толщины в зависимости от угла линии. + var thickness_corrected: u32 = thickness_px; + var use_vertical: bool = undefined; + const dx_f: f32 = @floatFromInt(bx1 - bx0); + const dy_f: f32 = @floatFromInt(by1 - by0); + const len: f32 = @sqrt(dx_f * dx_f + dy_f * dy_f); + if (len > 0) { + const cos_theta = @abs(dx_f) / len; + const sin_theta = @abs(dy_f) / len; + const desired: f32 = @floatFromInt(thickness_px); + const eps: f32 = 1e-3; - // Брезенхем + штамп по доминирующей оси: горизонтальная линия — вертикальный штамп, вертикальная — горизонтальный. - const more_horizontal = dx >= dy; + // Если будем рисовать «вертикальными» полосами (смещение по X), + // перпендикулярное смещение на 1 пиксель X равно |sin(theta)|. + const vertical_based = desired / @max(sin_theta, eps); + + // Если будем рисовать «горизонтальными» полосами (смещение по Y), + // перпендикулярное смещение на 1 пиксель Y равно |cos(theta)|. + const horizontal_based = desired / @max(cos_theta, eps); + + // Предпочитаем тот вариант, где проекция больше (меньше разброс по пикселям). + use_vertical = sin_theta >= cos_theta; + const corrected_f = if (use_vertical) vertical_based else horizontal_based; + + thickness_corrected = @max(@as(u32, 1), @as(u32, @intFromFloat(std.math.round(corrected_f)))); + } + const half_thickness: i32 = @intCast(thickness_corrected / 2); + + var p0 = Point2_i{ .x = bx0, .y = by0 }; + var p1 = Point2_i{ .x = bx1, .y = by1 }; + + // Отсечение отрезка буфером. Если он целиком вне — рисовать нечего. + if (!clipLineToBuffer(ctx, &p0, &p1, @as(i32, @intCast(thickness_corrected)))) return; + + var x0 = p0.x; + var y0 = p0.y; + const ex = p1.x; + const ey = p1.y; + + const dx: i32 = @intCast(@abs(ex - x0)); + const sx: i32 = if (x0 < ex) 1 else -1; + const dy_abs: i32 = @intCast(@abs(ey - y0)); + const dy: i32 = -dy_abs; + const sy: i32 = if (y0 < ey) 1 else -1; + + var err: i32 = dx + dy; while (true) { - const near_start = @abs(x - bx0) + @abs(y - by0) <= thickness_i; - const near_end = @abs(x - bx1) + @abs(y - by1) <= thickness_i; - - if (more_horizontal) { - var yo: i32 = -half; - while (yo <= half) : (yo += 1) { - const py = y + yo; - if (near_start and insideCircle(x, py, bx0, by0, half_sq)) continue; - if (near_end and insideCircle(x, py, bx1, by1, half_sq)) continue; - if (x >= 0 and x < @as(i32, @intCast(bw)) and py >= 0 and py < @as(i32, @intCast(bh))) { - ctx.blendPixelAtBuffer(@intCast(x), @intCast(py), color); - } - } - } else { - var xo: i32 = -half; - while (xo <= half) : (xo += 1) { - const px = x + xo; - if (near_start and insideCircle(px, y, bx0, by0, half_sq)) continue; - if (near_end and insideCircle(px, y, bx1, by1, half_sq)) continue; - if (px >= 0 and px < @as(i32, @intCast(bw)) and y >= 0 and y < @as(i32, @intCast(bh))) { - ctx.blendPixelAtBuffer(@intCast(px), @intCast(y), color); - } - } + var thick: i32 = -half_thickness; + while (thick <= half_thickness) { + const x = if (use_vertical) x0 + thick else x0; + const y = if (use_vertical) y0 else y0 + thick; + if (x >= 0 and y >= 0) + ctx.blendPixelAtBuffer(@intCast(x), @intCast(y), color); + thick += 1; } - if (x == bx1 and y == by1) break; - const e2 = 2 * err; - if (e2 >= -dy) { - err -= dy; - x += sx; + + if (x0 == ex and y0 == ey) break; + + const e2: i32 = 2 * err; + if (e2 >= dy) { + err += dy; + x0 += sx; } if (e2 <= dx) { err += dx; - y += sy; + y0 += sy; } } - - // Скруглённые концы: круги в крайних точках. - if (half > 0) { - fillCircleAtBuffer(ctx, bx0, by0, half, color); - fillCircleAtBuffer(ctx, bx1, by1, half, color); - } }