Универсальная растровая заливка

This commit is contained in:
2026-03-03 18:30:24 +03:00
parent 5b1b3a8c5e
commit b1177265ea
3 changed files with 117 additions and 88 deletions

View File

@@ -41,11 +41,11 @@ pub fn draw(
var i: usize = 0; var i: usize = 0;
while (i + 1 < pts.len) : (i += 1) { while (i + 1 < pts.len) : (i += 1) {
line.drawLine(&copy_ctx, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, stroke, thickness); line.drawLine(&copy_ctx, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, stroke, thickness, true);
} }
if (closed and pts.len >= 2) { if (closed and pts.len >= 2) {
const last = pts.len - 1; const last = pts.len - 1;
line.drawLine(&copy_ctx, pts[last].x, pts[last].y, pts[0].x, pts[0].y, stroke, thickness); line.drawLine(&copy_ctx, pts[last].x, pts[last].y, pts[0].x, pts[0].y, stroke, thickness, true);
} }
if (do_fill) { if (do_fill) {

View File

@@ -17,11 +17,12 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void {
const end_y = ep_prop.end_point.y; 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 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; const thickness = if (obj.getProperty(.thickness)) |t| t.thickness else default_thickness;
drawLine(ctx, 0, 0, end_x, end_y, stroke, thickness); drawLine(ctx, 0, 0, end_x, end_y, stroke, thickness, false);
} }
/// Рисует отрезок по локальным концам (перевод в буфер внутри). /// Рисует отрезок по локальным концам (перевод в буфер внутри).
pub fn drawLine(ctx: *DrawContext, x0: f32, y0: f32, x1: f32, y1: f32, color: Color.PMA, thickness: f32) void { /// 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 w0 = ctx.localToWorld(x0, y0);
const w1 = ctx.localToWorld(x1, y1); const w1 = ctx.localToWorld(x1, y1);
const b0 = ctx.worldToBuffer(w0.x, w0.y); const b0 = ctx.worldToBuffer(w0.x, w0.y);
@@ -30,7 +31,7 @@ pub fn drawLine(ctx: *DrawContext, x0: f32, y0: f32, x1: f32, y1: f32, color: Co
const scale = @sqrt(t.scale.scale_x * ctx.scale_x * t.scale.scale_y * ctx.scale_y); 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))); const thickness_px: u32 = @as(u32, @intFromFloat(std.math.round(thickness * scale)));
if (thickness_px > 0) if (thickness_px > 0)
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, draw_when_outside);
} }
inline fn clip(p: f32, q: f32, t0: *f32, t1: *f32) bool { inline fn clip(p: f32, q: f32, t0: *f32, t1: *f32) bool {
@@ -115,7 +116,7 @@ fn clipLineToBuffer(ctx: *DrawContext, a: *Point2_i, b: *Point2_i, thickness: i3
return true; return true;
} }
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, draw_when_outside: bool) void {
// Коррекция толщины в зависимости от угла линии. // Коррекция толщины в зависимости от угла линии.
var thickness_corrected: u32 = thickness_px; var thickness_corrected: u32 = thickness_px;
var use_vertical: bool = undefined; var use_vertical: bool = undefined;
@@ -137,12 +138,15 @@ fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, c
thickness_corrected = @max(@as(u32, 1), @as(u32, @intFromFloat(std.math.round(corrected_f)))); thickness_corrected = @max(@as(u32, 1), @as(u32, @intFromFloat(std.math.round(corrected_f))));
} }
const half_thickness: i32 = @intCast(thickness_corrected / 2); 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 p0 = Point2_i{ .x = bx0, .y = by0 };
var p1 = Point2_i{ .x = bx1, .y = by1 }; 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; if (!clipLineToBuffer(ctx, &p0, &p1, @as(i32, @intCast(thickness_corrected)))) return;
}
var x0 = p0.x; var x0 = p0.x;
var y0 = p0.y; var y0 = p0.y;
@@ -156,10 +160,16 @@ fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, c
const sy: i32 = if (y0 < ey) 1 else -1; const sy: i32 = if (y0 < ey) 1 else -1;
var err: i32 = dx + dy; 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) { while (true) {
var thick: i32 = -half_thickness; // Внутри viewport — полная толщина; снаружи при draw_when_outside — только 1 пиксель.
while (thick <= half_thickness) { 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 x = if (use_vertical) x0 + thick else x0;
const y = if (use_vertical) y0 else y0 + thick; const y = if (use_vertical) y0 else y0 + thick;
ctx.blendPixelAtBuffer(x, y, color); ctx.blendPixelAtBuffer(x, y, color);

View File

@@ -202,8 +202,8 @@ pub const DrawContext = struct {
pub fn stopFill(self: *DrawContext, allocator: std.mem.Allocator, color: Color.PMA) void { pub fn stopFill(self: *DrawContext, allocator: std.mem.Allocator, color: Color.PMA) void {
const fc = self._fill_canvas orelse return; const fc = self._fill_canvas orelse return;
self._fill_canvas = null; self._fill_canvas = null;
fc.fillColor(self, color); fc.fillColor(self, allocator, color);
fc.deinit(allocator); fc.deinit();
allocator.destroy(fc); allocator.destroy(fc);
} }
}; };
@@ -224,109 +224,128 @@ pub fn rgbaToPma(rgba: u32) Color.PMA {
}; };
} }
/// Контекст для заполнения фигур цветом. /// Контекст для заполнения фигур цветом. Границы хранятся в set — по x и y можно добавлять произвольные точки.
const FillCanvas = struct { const FillCanvas = struct {
/// Все пиксели холста в виде одного непрерывного массива. /// Множество пикселей границы (x, y) — без ограничения по размеру буфера.
_pixels: []PixelType, border_set: std.AutoHashMap(Point2_i, void),
/// Массив строк, каждая строка содержит свой срез пикселей и границы.
rows: []Row,
buf_width: u32, buf_width: u32,
buf_height: u32, buf_height: u32,
const PixelType = enum(u8) {
Empty, // Пустой пиксель
Border, // Пиксель границы
Fill, // Пиксель закрашен
};
const Row = struct {
pixels: []PixelType,
first_border_x: ?i32 = null,
last_border_x: ?i32 = null,
};
pub fn init(allocator: std.mem.Allocator, width: u32, height: u32) !FillCanvas { pub fn init(allocator: std.mem.Allocator, width: u32, height: u32) !FillCanvas {
const pixels = try allocator.alloc(PixelType, width * height); const border_set = std.AutoHashMap(Point2_i, void).init(allocator);
errdefer allocator.free(pixels);
@memset(pixels, .Empty);
const rows = try allocator.alloc(Row, height);
errdefer allocator.free(rows);
var y: u32 = 0;
while (y < height) : (y += 1) {
const start = y * width;
rows[y] = .{
.pixels = pixels[start .. start + width],
.first_border_x = null,
.last_border_x = null,
};
}
return .{ return .{
._pixels = pixels, .border_set = border_set,
.rows = rows,
.buf_width = width, .buf_width = width,
.buf_height = height, .buf_height = height,
}; };
} }
pub fn deinit(self: *FillCanvas, allocator: std.mem.Allocator) void { pub fn deinit(self: *FillCanvas) void {
allocator.free(self.rows); self.border_set.deinit();
allocator.free(self._pixels);
} }
/// Добавляет точку границы; координаты x, y могут быть любыми (условно бесконечное поле).
pub fn setBorder(self: *FillCanvas, x: i32, y: i32) void { pub fn setBorder(self: *FillCanvas, x: i32, y: i32) void {
if (x < 0 or y < 0 or x >= self.buf_width or y >= self.buf_height) return; self.border_set.put(.{ .x = x, .y = y }, {}) catch {};
const row = &self.rows[@intCast(y)];
row.pixels[@intCast(x)] = .Border;
if (row.first_border_x) |first| {
if (x < first) {
row.first_border_x = @intCast(x);
} }
/// Заливка четырёхсвязным стековым алгоритмом от первой найденной внутренней точки.
pub fn fillColor(self: *FillCanvas, draw_ctx: *DrawContext, allocator: std.mem.Allocator, color: Color.PMA) void {
const n = self.border_set.count();
if (n == 0) return;
const buf_w_i: i32 = @intCast(self.buf_width);
const buf_h_i: i32 = @intCast(self.buf_height);
// Ключи один раз по (y, x) — по строкам x уже будут отсортированы.
var keys_buf = std.ArrayList(Point2_i).empty;
defer keys_buf.deinit(allocator);
keys_buf.ensureTotalCapacity(allocator, n) catch return;
var iter = self.border_set.keyIterator();
while (iter.next()) |k| {
keys_buf.appendAssumeCapacity(k.*);
}
std.mem.sort(Point2_i, keys_buf.items, {}, struct {
fn lessThan(_: void, a: Point2_i, b: Point2_i) bool {
if (a.y != b.y) return a.y < b.y;
return a.x < b.x;
}
}.lessThan);
// Семена: по строкам находим сегменты (пары x), пересекаем с окном буфера, берём середину сегмента.
var seeds = findFillSeeds(self, keys_buf.items, buf_w_i, buf_h_i, allocator) catch return;
defer seeds.deinit(allocator);
var stack = std.ArrayList(Point2_i).empty;
defer stack.deinit(allocator);
var filled = std.AutoHashMap(Point2_i, void).init(allocator);
defer filled.deinit();
for (seeds.items) |s| {
if (self.border_set.contains(s)) continue;
if (filled.contains(s)) continue;
stack.clearRetainingCapacity();
stack.append(allocator, s) catch return;
while (stack.pop()) |cell| {
if (self.border_set.contains(cell)) continue;
const gop = filled.getOrPut(cell) catch return;
if (gop.found_existing) continue;
if (cell.x >= 0 and cell.x < buf_w_i and cell.y >= 0 and cell.y < buf_h_i) {
draw_ctx.blendPixelAtBuffer(cell.x, cell.y, color);
}
if (cell.x > 0) stack.append(allocator, .{ .x = cell.x - 1, .y = cell.y }) catch return;
if (cell.x < buf_w_i - 1) stack.append(allocator, .{ .x = cell.x + 1, .y = cell.y }) catch return;
if (cell.y > 0) stack.append(allocator, .{ .x = cell.x, .y = cell.y - 1 }) catch return;
if (cell.y < buf_h_i - 1) stack.append(allocator, .{ .x = cell.x, .y = cell.y + 1 }) catch return;
}
}
}
/// По строкам: рёбра (подряд идущие x) → сегменты между ними. Семена — середины чётных сегментов (при чётном числе границ).
fn findFillSeeds(
self: *const FillCanvas,
keys: []const Point2_i,
buf_w_i: i32,
buf_h_i: i32,
allocator: std.mem.Allocator,
) !std.ArrayList(Point2_i) {
_ = self;
var list = std.ArrayList(Point2_i).empty;
errdefer list.deinit(allocator);
var segments = std.ArrayList(struct { left: i32, right: i32 }).empty;
defer segments.deinit(allocator);
var i: usize = 0;
while (i < keys.len) {
const y = keys[i].y;
const row_start = i;
while (i < keys.len and keys[i].y == y) : (i += 1) {}
const row = keys[row_start..i];
if (row.len < 2 or y < 0 or y >= buf_h_i) continue;
segments.clearRetainingCapacity();
var run_end_x: i32 = row[0].x;
for (row[1..]) |p| {
if (p.x != run_end_x + 1) {
try segments.append(allocator, .{ .left = run_end_x + 1, .right = p.x - 1 });
run_end_x = p.x;
} else { } else {
row.first_border_x = @intCast(x); run_end_x = p.x;
} }
}
// Семена только при чётном числе границ
if ((segments.items.len + 1) % 2 != 0) continue;
if (row.last_border_x) |last| { for (segments.items, 0..) |seg, gi| {
if (x > last) { if (gi % 2 != 0 or seg.left > seg.right) continue;
row.last_border_x = @intCast(x); const left = @max(seg.left, 0);
} const right = @min(seg.right, buf_w_i - 1);
} else { if (left <= right) {
row.last_border_x = @intCast(x); try list.append(allocator, .{ .x = left + @divTrunc(right - left, 2), .y = y });
}
}
pub fn fillColor(self: *FillCanvas, draw_ctx: *DrawContext, color: Color.PMA) void {
// В каждой строке пройтись от первого до последнего x с учетом границ:
var y: u32 = 0;
while (y < self.buf_height) : (y += 1) {
const row = &self.rows[y];
// Если нет хотя бы одной границы — пропустить строку
if (row.first_border_x == null or row.last_border_x == null) continue;
const first = row.first_border_x.?;
const last = row.last_border_x.?;
var on_border = true;
var segment_index: usize = 0; // индекс "промежутка" (счет с 0)
var x: u32 = @intCast(first);
while (x <= last) : (x += 1) {
if (row.pixels[x] == .Border) {
on_border = true;
} else {
if (on_border)
segment_index += 1;
on_border = false;
}
if (!on_border and row.pixels[x] == .Empty and segment_index % 2 == 1) {
row.pixels[x] = .Fill;
draw_ctx.blendPixelAtBuffer(@intCast(x), @intCast(y), color);
} }
} }
} }
return list;
} }
}; };