Заливка и замкнутая фигура
This commit is contained in:
@@ -18,6 +18,11 @@ pub const Data = union(enum) {
|
|||||||
end_point: Point2_f,
|
end_point: Point2_f,
|
||||||
|
|
||||||
points: std.ArrayList(Point2_f),
|
points: std.ArrayList(Point2_f),
|
||||||
|
/// Замкнутый контур (для ломаной: отрезок последняя–первая точка + заливка).
|
||||||
|
closed: bool,
|
||||||
|
|
||||||
|
/// Включена ли заливка.
|
||||||
|
filled: bool,
|
||||||
|
|
||||||
/// Цвет заливки, 0xRRGGBBAA.
|
/// Цвет заливки, 0xRRGGBBAA.
|
||||||
fill_rgba: u32,
|
fill_rgba: u32,
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object)
|
|||||||
var points = std.ArrayList(Point2_f).empty;
|
var points = std.ArrayList(Point2_f).empty;
|
||||||
try points.appendSlice(allocator, &default_points);
|
try points.appendSlice(allocator, &default_points);
|
||||||
try obj.properties.append(allocator, .{ .data = .{ .points = 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 по точкам.
|
/// Локальные границы: AABB по точкам.
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ const Color = @import("dvui").Color;
|
|||||||
|
|
||||||
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 };
|
||||||
|
const default_fill: Color.PMA = .{ .r = 0, .g = 0, .b = 0, .a = 0 };
|
||||||
const default_thickness: f32 = 2.0;
|
const default_thickness: f32 = 2.0;
|
||||||
|
|
||||||
/// Ломаная по точкам, обводка stroke_rgba
|
/// Ломаная по точкам, обводка stroke_rgba. При closed — отрезок последняя–первая точка и заливка fill_rgba.
|
||||||
pub fn draw(
|
pub fn draw(
|
||||||
ctx: *DrawContext,
|
ctx: *DrawContext,
|
||||||
obj: *const Object,
|
obj: *const Object,
|
||||||
@@ -20,6 +21,9 @@ pub fn draw(
|
|||||||
if (pts.len < 2) return;
|
if (pts.len < 2) return;
|
||||||
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;
|
||||||
|
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);
|
const buffer = try allocator.alloc(Color.PMA, ctx.buf_width * ctx.buf_height);
|
||||||
@memset(buffer, .{ .r = 0, .g = 0, .b = 0, .a = 0 });
|
@memset(buffer, .{ .r = 0, .g = 0, .b = 0, .a = 0 });
|
||||||
@@ -29,10 +33,24 @@ pub fn draw(
|
|||||||
copy_ctx.pixels = buffer;
|
copy_ctx.pixels = buffer;
|
||||||
copy_ctx.replace_mode = true;
|
copy_ctx.replace_mode = true;
|
||||||
|
|
||||||
|
const do_fill = closed and filled;
|
||||||
|
|
||||||
|
if (do_fill) {
|
||||||
|
copy_ctx.startFill(allocator) catch return;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
if (closed and pts.len >= 2) {
|
||||||
|
const last = pts.len - 1;
|
||||||
|
line.drawLine(©_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(©_ctx, copy_ctx.transform.opacity);
|
ctx.compositeDrawerContext(©_ctx, copy_ctx.transform.opacity);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ pub const DrawContext = struct {
|
|||||||
transform: Transform = .{},
|
transform: Transform = .{},
|
||||||
/// Если true, blendPixelAtBuffer перезаписывает пиксель без бленда
|
/// Если true, blendPixelAtBuffer перезаписывает пиксель без бленда
|
||||||
replace_mode: bool = false,
|
replace_mode: bool = false,
|
||||||
|
_fill_canvas: ?*FillCanvas = null,
|
||||||
|
|
||||||
pub fn setTransform(self: *DrawContext, t: Transform) void {
|
pub fn setTransform(self: *DrawContext, t: Transform) void {
|
||||||
self.transform = t;
|
self.transform = t;
|
||||||
@@ -132,8 +133,10 @@ pub const DrawContext = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель.
|
/// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель.
|
||||||
|
/// Если активен fill canvas, каждый записанный пиксель помечается как граница для заливки.
|
||||||
pub fn blendPixelAtBuffer(self: *DrawContext, bx: u32, by: u32, color: Color.PMA) void {
|
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 (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 idx = by * self.buf_width + bx;
|
||||||
const dst = &self.pixels[idx];
|
const dst = &self.pixels[idx];
|
||||||
if (self.replace_mode) {
|
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;
|
if (bx < 0 or bx >= vw or by < 0 or by >= vh) return;
|
||||||
self.blendPixelAtBuffer(@intCast(bx), @intCast(by), color);
|
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.
|
/// Конвертирует u32 0xRRGGBBAA в Color.PMA.
|
||||||
@@ -200,3 +220,110 @@ pub fn rgbaToPma(rgba: u32) Color.PMA {
|
|||||||
.a = a,
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -392,6 +392,17 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
|
|||||||
);
|
);
|
||||||
{
|
{
|
||||||
const tag = std.meta.activeTag(prop.data);
|
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), .{}, .{});
|
dvui.labelNoFmt(@src(), propertyLabel(tag), .{}, .{});
|
||||||
|
|
||||||
switch (prop.data) {
|
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();
|
row.deinit();
|
||||||
@@ -731,6 +756,8 @@ fn propertyLabel(tag: std.meta.Tag(PropertyData)) []const u8 {
|
|||||||
.fill_rgba => "Fill color",
|
.fill_rgba => "Fill color",
|
||||||
.stroke_rgba => "Stroke color",
|
.stroke_rgba => "Stroke color",
|
||||||
.thickness => "Thickness",
|
.thickness => "Thickness",
|
||||||
|
.closed => "Closed",
|
||||||
|
.filled => "Filled",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user