Универсальная растровая заливка
This commit is contained in:
@@ -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(©_ctx, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, stroke, thickness);
|
line.drawLine(©_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(©_ctx, pts[last].x, pts[last].y, pts[0].x, pts[0].y, stroke, thickness);
|
line.drawLine(©_ctx, pts[last].x, pts[last].y, pts[0].x, pts[0].y, stroke, thickness, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (do_fill) {
|
if (do_fill) {
|
||||||
|
|||||||
@@ -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 (!clipLineToBuffer(ctx, &p0, &p1, @as(i32, @intCast(thickness_corrected)))) return;
|
if (!draw_when_outside) {
|
||||||
|
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);
|
||||||
|
|||||||
@@ -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;
|
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;
|
||||||
|
|
||||||
if (row.first_border_x) |first| {
|
const buf_w_i: i32 = @intCast(self.buf_width);
|
||||||
if (x < first) {
|
const buf_h_i: i32 = @intCast(self.buf_height);
|
||||||
row.first_border_x = @intCast(x);
|
|
||||||
}
|
// Ключи один раз по (y, x) — по строкам x уже будут отсортированы.
|
||||||
} else {
|
var keys_buf = std.ArrayList(Point2_i).empty;
|
||||||
row.first_border_x = @intCast(x);
|
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 {
|
||||||
if (row.last_border_x) |last| {
|
fn lessThan(_: void, a: Point2_i, b: Point2_i) bool {
|
||||||
if (x > last) {
|
if (a.y != b.y) return a.y < b.y;
|
||||||
row.last_border_x = @intCast(x);
|
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;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
row.last_border_x = @intCast(x);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fillColor(self: *FillCanvas, draw_ctx: *DrawContext, color: Color.PMA) void {
|
/// По строкам: рёбра (подряд идущие x) → сегменты между ними. Семена — середины чётных сегментов (при чётном числе границ).
|
||||||
// В каждой строке пройтись от первого до последнего x с учетом границ:
|
fn findFillSeeds(
|
||||||
var y: u32 = 0;
|
self: *const FillCanvas,
|
||||||
while (y < self.buf_height) : (y += 1) {
|
keys: []const Point2_i,
|
||||||
const row = &self.rows[y];
|
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;
|
||||||
if (row.first_border_x == null or row.last_border_x == null) continue;
|
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;
|
||||||
|
|
||||||
const first = row.first_border_x.?;
|
segments.clearRetainingCapacity();
|
||||||
const last = row.last_border_x.?;
|
var run_end_x: i32 = row[0].x;
|
||||||
|
for (row[1..]) |p| {
|
||||||
var on_border = true;
|
if (p.x != run_end_x + 1) {
|
||||||
var segment_index: usize = 0; // индекс "промежутка" (счет с 0)
|
try segments.append(allocator, .{ .left = run_end_x + 1, .right = p.x - 1 });
|
||||||
var x: u32 = @intCast(first);
|
run_end_x = p.x;
|
||||||
while (x <= last) : (x += 1) {
|
|
||||||
if (row.pixels[x] == .Border) {
|
|
||||||
on_border = true;
|
|
||||||
} else {
|
} else {
|
||||||
if (on_border)
|
run_end_x = p.x;
|
||||||
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);
|
if ((segments.items.len + 1) % 2 != 0) continue;
|
||||||
|
|
||||||
|
for (segments.items, 0..) |seg, gi| {
|
||||||
|
if (gi % 2 != 0 or seg.left > seg.right) continue;
|
||||||
|
const left = @max(seg.left, 0);
|
||||||
|
const right = @min(seg.right, buf_w_i - 1);
|
||||||
|
if (left <= right) {
|
||||||
|
try list.append(allocator, .{ .x = left + @divTrunc(right - left, 2), .y = y });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user