diff --git a/src/models/Property.zig b/src/models/Property.zig index dace7db..eaf28fe 100644 --- a/src/models/Property.zig +++ b/src/models/Property.zig @@ -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, diff --git a/src/models/shape/broken.zig b/src/models/shape/broken.zig index 382e162..a3aaac5 100644 --- a/src/models/shape/broken.zig +++ b/src/models/shape/broken.zig @@ -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 по точкам. diff --git a/src/render/cpu/broken.zig b/src/render/cpu/broken.zig index 9ce4feb..1b6564c 100644 --- a/src/render/cpu/broken.zig +++ b/src/render/cpu/broken.zig @@ -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(©_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); } diff --git a/src/render/cpu/pipeline.zig b/src/render/cpu/pipeline.zig index 73a933b..129d757 100644 --- a/src/render/cpu/pipeline.zig +++ b/src/render/cpu/pipeline.zig @@ -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); + } + } + } + } +}; diff --git a/src/ui/canvas_view.zig b/src/ui/canvas_view.zig index 0e0bf25..39095b5 100644 --- a/src/ui/canvas_view.zig +++ b/src/ui/canvas_view.zig @@ -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", }; }