const std = @import("std"); 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 }; const default_thickness: f32 = 2.0; /// Линия от (0,0) до end_point. pub fn draw(ctx: *DrawContext, obj: *const Object) void { const ep_prop = obj.getProperty(.end_point) orelse return; 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; 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, thickness: f32) void { const w0 = ctx.localToWorld(x0, y0); const w1 = ctx.localToWorld(x1, y1); const b0 = ctx.worldToBuffer(w0.x, w0.y); const b1 = ctx.worldToBuffer(w1.x, w1.y); const t = &ctx.transform; const scale = @sqrt(t.scale.scale_x * ctx.scale_x * t.scale.scale_y * ctx.scale_y); const thickness_px: u32 = @max(@as(u32, 1), @as(u32, @intFromFloat(std.math.round(thickness * scale)))); 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; } fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, color: Color.PMA, thickness_px: u32) void { // Коррекция толщины в зависимости от угла линии. 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; // Если будем рисовать «вертикальными» полосами (смещение по 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) { 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 (x0 == ex and y0 == ey) break; const e2: i32 = 2 * err; if (e2 >= dy) { err += dy; x0 += sx; } if (e2 <= dx) { err += dx; y0 += sy; } } }