Files
Zivro/src/render/cpu/line.zig
2026-03-04 02:00:40 +03:00

193 lines
7.2 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 end_point = obj.getProperty(.end_point) orelse return;
const end_x = end_point.x;
const end_y = end_point.y;
const stroke = if (obj.getProperty(.stroke_rgba)) |stroke_rgba| pipeline.rgbaToPma(stroke_rgba) else default_stroke;
const thickness = obj.getProperty(.thickness) orelse default_thickness;
drawLine(ctx, 0, 0, end_x, end_y, stroke, thickness, false);
}
/// Рисует отрезок по локальным концам (перевод в буфер внутри).
/// draw_when_outside: если true, участки линии за экраном тоже рисуются (толщиной 1 px); в буфере — обычная толщина.
pub fn drawLine(ctx: *DrawContext, x0: f32, y0: f32, x1: f32, y1: f32, color: Color.PMA, thickness: f32, draw_when_outside: bool) 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 = @as(u32, @intFromFloat(std.math.round(thickness * scale)));
if (thickness_px > 0)
drawLineInBuffer(ctx, b0.x, b0.y, b1.x, b1.y, color, thickness_px, draw_when_outside);
}
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;
}
/// LiangBarsky отсечение отрезка (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, draw_when_outside: bool) 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;
const vertical_based = desired / @max(sin_theta, eps);
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);
const thickness_corrected_i: i32 = @as(i32, @intCast(thickness_corrected));
var p0 = Point2_i{ .x = bx0, .y = by0 };
var p1 = Point2_i{ .x = bx1, .y = by1 };
// Отсечение только когда не рисуем вне viewport: иначе линия идёт целиком, толщина 1 px снаружи.
if (!draw_when_outside) {
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;
const buf_w_i: i32 = @intCast(ctx.buf_width);
const buf_h_i: i32 = @intCast(ctx.buf_height);
while (true) {
// Внутри viewport — полная толщина; снаружи при draw_when_outside — только 1 пиксель.
const in_viewport = x0 >= -thickness_corrected_i and x0 < buf_w_i + thickness_corrected_i and y0 >= -thickness_corrected_i and y0 < buf_h_i + thickness_corrected_i;
const effective_half: i32 = if (draw_when_outside and !in_viewport) 0 else half_thickness;
var thick: i32 = -effective_half;
while (thick <= effective_half) {
const x = if (use_vertical) x0 + thick else x0;
const y = if (use_vertical) y0 else y0 + thick;
ctx.blendPixelAtBuffer(x, 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;
}
}
}