Правильная толщина
This commit is contained in:
@@ -7,7 +7,7 @@ const shape_mod = @import("shape.zig");
|
|||||||
|
|
||||||
/// Свойства фигуры по умолчанию.
|
/// Свойства фигуры по умолчанию.
|
||||||
pub const default_shape_properties = [_]Property{
|
pub const default_shape_properties = [_]Property{
|
||||||
.{ .data = .{ .end_point = .{ .x = 100, .y = 0 } } },
|
.{ .data = .{ .end_point = .{ .x = 100, .y = 200 } } },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Теги обязательных свойств.
|
/// Теги обязательных свойств.
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ const Document = @import("../../models/Document.zig");
|
|||||||
const pipeline = @import("pipeline.zig");
|
const pipeline = @import("pipeline.zig");
|
||||||
const DrawContext = pipeline.DrawContext;
|
const DrawContext = pipeline.DrawContext;
|
||||||
const Color = @import("dvui").Color;
|
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 Object = Document.Object;
|
||||||
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
|
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);
|
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 (граница не включается).
|
/// Точка (px, py) лежит строго внутри круга с центром (cx, cy) и радиусом в квадрате r_sq (граница не включается).
|
||||||
fn insideCircle(px: i32, py: i32, cx: i32, cy: i32, r_sq: i32) bool {
|
fn insideCircle(px: i32, py: i32, cx: i32, cy: i32, r_sq: i32) bool {
|
||||||
const dx = px - cx;
|
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 {
|
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;
|
var thickness_corrected: u32 = thickness_px;
|
||||||
const dx: i32 = @intCast(@abs(bx1 - bx0));
|
var use_vertical: bool = undefined;
|
||||||
const dy: i32 = @intCast(@abs(by1 - by0));
|
const dx_f: f32 = @floatFromInt(bx1 - bx0);
|
||||||
const sx: i32 = if (bx0 < bx1) 1 else -1;
|
const dy_f: f32 = @floatFromInt(by1 - by0);
|
||||||
const sy: i32 = if (by0 < by1) 1 else -1;
|
const len: f32 = @sqrt(dx_f * dx_f + dy_f * dy_f);
|
||||||
var err = dx - dy;
|
if (len > 0) {
|
||||||
var x = bx0;
|
const cos_theta = @abs(dx_f) / len;
|
||||||
var y = by0;
|
const sin_theta = @abs(dy_f) / len;
|
||||||
const half: i32 = @intCast(thickness_px / 2);
|
const desired: f32 = @floatFromInt(thickness_px);
|
||||||
const half_sq = half * half;
|
const eps: f32 = 1e-3;
|
||||||
const thickness_i: i32 = @intCast(thickness_px);
|
|
||||||
|
|
||||||
// Брезенхем + штамп по доминирующей оси: горизонтальная линия — вертикальный штамп, вертикальная — горизонтальный.
|
// Если будем рисовать «вертикальными» полосами (смещение по X),
|
||||||
const more_horizontal = dx >= dy;
|
// перпендикулярное смещение на 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) {
|
while (true) {
|
||||||
const near_start = @abs(x - bx0) + @abs(y - by0) <= thickness_i;
|
var thick: i32 = -half_thickness;
|
||||||
const near_end = @abs(x - bx1) + @abs(y - by1) <= thickness_i;
|
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 (more_horizontal) {
|
if (x0 == ex and y0 == ey) break;
|
||||||
var yo: i32 = -half;
|
|
||||||
while (yo <= half) : (yo += 1) {
|
const e2: i32 = 2 * err;
|
||||||
const py = y + yo;
|
if (e2 >= dy) {
|
||||||
if (near_start and insideCircle(x, py, bx0, by0, half_sq)) continue;
|
err += dy;
|
||||||
if (near_end and insideCircle(x, py, bx1, by1, half_sq)) continue;
|
x0 += sx;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (x == bx1 and y == by1) break;
|
|
||||||
const e2 = 2 * err;
|
|
||||||
if (e2 >= -dy) {
|
|
||||||
err -= dy;
|
|
||||||
x += sx;
|
|
||||||
}
|
}
|
||||||
if (e2 <= dx) {
|
if (e2 <= dx) {
|
||||||
err += dx;
|
err += dx;
|
||||||
y += sy;
|
y0 += sy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скруглённые концы: круги в крайних точках.
|
|
||||||
if (half > 0) {
|
|
||||||
fillCircleAtBuffer(ctx, bx0, by0, half, color);
|
|
||||||
fillCircleAtBuffer(ctx, bx1, by1, half, color);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user