Files
Zivro/src/render/cpu/line.zig

189 lines
6.9 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 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;
}
/// 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) 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;
}
}
}