diff --git a/src/render/cpu/broken.zig b/src/render/cpu/broken.zig index 1b6564c..dcbc2fa 100644 --- a/src/render/cpu/broken.zig +++ b/src/render/cpu/broken.zig @@ -41,11 +41,11 @@ pub fn draw( 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); + 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) { 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) { diff --git a/src/render/cpu/line.zig b/src/render/cpu/line.zig index 5860eaa..0715482 100644 --- a/src/render/cpu/line.zig +++ b/src/render/cpu/line.zig @@ -17,11 +17,12 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void { 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 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 w1 = ctx.localToWorld(x1, y1); 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 thickness_px: u32 = @as(u32, @intFromFloat(std.math.round(thickness * scale))); 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 { @@ -115,7 +116,7 @@ fn clipLineToBuffer(ctx: *DrawContext, a: *Point2_i, b: *Point2_i, thickness: i3 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 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)))); } 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 p1 = Point2_i{ .x = bx1, .y = by1 }; - // Отсечение отрезка буфером. Если он целиком вне — рисовать нечего. - if (!clipLineToBuffer(ctx, &p0, &p1, @as(i32, @intCast(thickness_corrected)))) return; + // Отсечение только когда не рисуем вне viewport: иначе линия идёт целиком, толщина 1 px снаружи. + if (!draw_when_outside) { + if (!clipLineToBuffer(ctx, &p0, &p1, @as(i32, @intCast(thickness_corrected)))) return; + } var x0 = p0.x; 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; 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) { - var thick: i32 = -half_thickness; - while (thick <= half_thickness) { + // Внутри viewport — полная толщина; снаружи при draw_when_outside — только 1 пиксель. + 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 y = if (use_vertical) y0 else y0 + thick; ctx.blendPixelAtBuffer(x, y, color); diff --git a/src/render/cpu/pipeline.zig b/src/render/cpu/pipeline.zig index c00c138..6713857 100644 --- a/src/render/cpu/pipeline.zig +++ b/src/render/cpu/pipeline.zig @@ -202,8 +202,8 @@ pub const DrawContext = struct { 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); + fc.fillColor(self, allocator, color); + fc.deinit(); allocator.destroy(fc); } }; @@ -224,109 +224,128 @@ pub fn rgbaToPma(rgba: u32) Color.PMA { }; } -/// Контекст для заполнения фигур цветом. +/// Контекст для заполнения фигур цветом. Границы хранятся в set — по x и y можно добавлять произвольные точки. const FillCanvas = struct { - /// Все пиксели холста в виде одного непрерывного массива. - _pixels: []PixelType, - /// Массив строк, каждая строка содержит свой срез пикселей и границы. - rows: []Row, + /// Множество пикселей границы (x, y) — без ограничения по размеру буфера. + border_set: std.AutoHashMap(Point2_i, void), buf_width: 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 { - 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, - }; - } - + const border_set = std.AutoHashMap(Point2_i, void).init(allocator); return .{ - ._pixels = pixels, - .rows = rows, + .border_set = border_set, .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 deinit(self: *FillCanvas) void { + self.border_set.deinit(); } + /// Добавляет точку границы; координаты x, y могут быть любыми (условно бесконечное поле). 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| { - if (x < first) { - row.first_border_x = @intCast(x); - } - } else { - row.first_border_x = @intCast(x); + 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.*); } - - if (row.last_border_x) |last| { - if (x > last) { - row.last_border_x = @intCast(x); + 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; } - } else { - row.last_border_x = @intCast(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]; + /// По строкам: рёбра (подряд идущие 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); - // Если нет хотя бы одной границы — пропустить строку - if (row.first_border_x == null or row.last_border_x == null) continue; + 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; - 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; + 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 { - if (on_border) - segment_index += 1; - on_border = false; + run_end_x = p.x; } - 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; } };