Заливка и замкнутая фигура

This commit is contained in:
2026-03-03 14:21:55 +03:00
parent e5b8e6735d
commit 32cffb757d
5 changed files with 181 additions and 1 deletions

View File

@@ -18,6 +18,11 @@ pub const Data = union(enum) {
end_point: Point2_f,
points: std.ArrayList(Point2_f),
/// Замкнутый контур (для ломаной: отрезок последняя–первая точка + заливка).
closed: bool,
/// Включена ли заливка.
filled: bool,
/// Цвет заливки, 0xRRGGBBAA.
fill_rgba: u32,

View File

@@ -25,6 +25,9 @@ pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object)
var points = std.ArrayList(Point2_f).empty;
try points.appendSlice(allocator, &default_points);
try obj.properties.append(allocator, .{ .data = .{ .points = points } });
try obj.properties.append(allocator, .{ .data = .{ .closed = false } });
try obj.properties.append(allocator, .{ .data = .{ .filled = true } });
try obj.properties.append(allocator, .{ .data = .{ .fill_rgba = obj.getProperty(.stroke_rgba).?.stroke_rgba } });
}
/// Локальные границы: AABB по точкам.

View File

@@ -7,9 +7,10 @@ const Color = @import("dvui").Color;
const Object = Document.Object;
const default_stroke: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 255 };
const default_fill: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 0 };
const default_thickness: f32 = 2.0;
/// Ломаная по точкам, обводка stroke_rgba
/// Ломаная по точкам, обводка stroke_rgba. При closed — отрезок последняя–первая точка и заливка fill_rgba.
pub fn draw(
ctx: *DrawContext,
obj: *const Object,
@@ -20,6 +21,9 @@ pub fn draw(
if (pts.len < 2) return;
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 closed = if (obj.getProperty(.closed)) |c| c.closed else false;
const filled = if (obj.getProperty(.filled)) |f| f.filled else true;
const fill_color = if (obj.getProperty(.fill_rgba)) |f| pipeline.rgbaToPma(f.fill_rgba) else default_fill;
const buffer = try allocator.alloc(Color.PMA, ctx.buf_width * ctx.buf_height);
@memset(buffer, .{ .r = 0, .g = 0, .b = 0, .a = 0 });
@@ -29,10 +33,24 @@ pub fn draw(
copy_ctx.pixels = buffer;
copy_ctx.replace_mode = true;
const do_fill = closed and filled;
if (do_fill) {
copy_ctx.startFill(allocator) catch return;
}
var i: usize = 0;
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);
}
if (closed and pts.len >= 2) {
const last = pts.len - 1;
line.drawLine(&copy_ctx, pts[last].x, pts[last].y, pts[0].x, pts[0].y, stroke, thickness);
}
if (do_fill) {
copy_ctx.stopFill(allocator, fill_color);
}
ctx.compositeDrawerContext(&copy_ctx, copy_ctx.transform.opacity);
}

View File

@@ -75,6 +75,7 @@ pub const DrawContext = struct {
transform: Transform = .{},
/// Если true, blendPixelAtBuffer перезаписывает пиксель без бленда
replace_mode: bool = false,
_fill_canvas: ?*FillCanvas = null,
pub fn setTransform(self: *DrawContext, t: Transform) void {
self.transform = t;
@@ -132,8 +133,10 @@ pub const DrawContext = struct {
}
/// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель.
/// Если активен fill canvas, каждый записанный пиксель помечается как граница для заливки.
pub fn blendPixelAtBuffer(self: *DrawContext, bx: u32, by: u32, color: Color.PMA) void {
if (bx >= self.buf_width or by >= self.buf_height) return;
if (self._fill_canvas) |fc| fc.setBorder(bx, by);
const idx = by * self.buf_width + bx;
const dst = &self.pixels[idx];
if (self.replace_mode) {
@@ -183,6 +186,23 @@ pub const DrawContext = struct {
if (bx < 0 or bx >= vw or by < 0 or by >= vh) return;
self.blendPixelAtBuffer(@intCast(bx), @intCast(by), color);
}
/// Начинает сбор границ для заливки: создаёт FillCanvas и при последующих вызовах blendPixelAtBuffer помечает пиксели как границу.
pub fn startFill(self: *DrawContext, allocator: std.mem.Allocator) !void {
const fc = try FillCanvas.init(allocator, self.buf_width, self.buf_height);
const ptr = try allocator.create(FillCanvas);
ptr.* = fc;
self._fill_canvas = ptr;
}
/// Рисует заливку по собранным границам цветом color, освобождает FillCanvas и сбрасывает режим.
pub fn stopFill(self: *DrawContext, allocator: std.mem.Allocator, color: Color.PMA) void {
const fc = self._fill_canvas orelse return;
self._fill_canvas = null;
fc.fillColor(self, color);
fc.deinit(allocator);
allocator.destroy(fc);
}
};
/// Конвертирует u32 0xRRGGBBAA в Color.PMA.
@@ -200,3 +220,110 @@ pub fn rgbaToPma(rgba: u32) Color.PMA {
.a = a,
};
}
/// Контекст для заполнения фигур цветом.
const FillCanvas = struct {
/// Все пиксели холста в виде одного непрерывного массива.
_pixels: []PixelType,
/// Массив строк, каждая строка содержит свой срез пикселей и границы.
rows: []Row,
buf_width: u32,
buf_height: u32,
const PixelType = enum(u8) {
Empty, // Пустой пиксель
Border, // Пиксель границы
Fill, // Пиксель закрашен
};
const Row = struct {
pixels: []PixelType,
first_border_x: ?u32 = null,
last_border_x: ?u32 = null,
};
pub fn init(allocator: std.mem.Allocator, width: u32, height: u32) !FillCanvas {
const pixels = try allocator.alloc(PixelType, width * height);
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 .{
._pixels = pixels,
.rows = rows,
.buf_width = width,
.buf_height = height,
};
}
pub fn deinit(self: *FillCanvas, allocator: std.mem.Allocator) void {
allocator.free(self.rows);
allocator.free(self._pixels);
}
pub fn setBorder(self: *FillCanvas, x: u32, y: u32) void {
if (x >= self.buf_width or y >= self.buf_height) return;
const row = &self.rows[y];
row.pixels[x] = .Border;
if (row.first_border_x) |first| {
if (x < first) {
row.first_border_x = x;
}
} else {
row.first_border_x = x;
}
if (row.last_border_x) |last| {
if (x > last) {
row.last_border_x = x;
}
} else {
row.last_border_x = x;
}
}
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 = 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(x, y, color);
}
}
}
}
};

View File

@@ -392,6 +392,17 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
);
{
const tag = std.meta.activeTag(prop.data);
// Скрываем строку с цветом заливки, если заполнение выключено.
if (tag == .fill_rgba) {
const filled_prop = obj.getProperty(.filled);
const filled = if (filled_prop) |p| p.filled else false;
if (!filled) {
row.deinit();
return;
}
}
dvui.labelNoFmt(@src(), propertyLabel(tag), .{}, .{});
switch (prop.data) {
@@ -694,6 +705,20 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
}
}
},
.closed => |v| {
var next = v;
if (dvui.checkbox(@src(), &next, "Closed", .{})) {
obj.setProperty(canvas.allocator, .{ .data = .{ .closed = next } }) catch {};
canvas.requestRedraw();
}
},
.filled => |v| {
var next = v;
if (dvui.checkbox(@src(), &next, "Filled", .{})) {
obj.setProperty(canvas.allocator, .{ .data = .{ .filled = next } }) catch {};
canvas.requestRedraw();
}
},
}
}
row.deinit();
@@ -731,6 +756,8 @@ fn propertyLabel(tag: std.meta.Tag(PropertyData)) []const u8 {
.fill_rgba => "Fill color",
.stroke_rgba => "Stroke color",
.thickness => "Thickness",
.closed => "Closed",
.filled => "Filled",
};
}