const std = @import("std"); const dvui = @import("dvui"); const basic_models = @import("../../models/basic_models.zig"); const Document = @import("../../models/Document.zig"); const Point2_f = basic_models.Point2_f; const Point2_i = basic_models.Point2_i; const Scale2_f = basic_models.Scale2_f; const Rect_i = basic_models.Rect_i; const Color = dvui.Color; /// Трансформ объекта: позиция, угол, масштаб, непрозрачность. pub const Transform = struct { position: Point2_f = .{}, angle: f32 = 0, scale: Scale2_f = .{}, opacity: f32 = 1.0, pub fn init(obj: *const Document.Object) Transform { const pos = obj.getProperty(.position) orelse Point2_f{ .x = 0, .y = 0 }; const angle = obj.getProperty(.angle) orelse 0; const scale = obj.getProperty(.scale) orelse Scale2_f{ .scale_x = 1, .scale_y = 1 }; const opacity = obj.getProperty(.opacity) orelse 1.0; return .{ .position = pos, .angle = angle, .scale = scale, .opacity = opacity, }; } /// Композиция: world = parent * local. pub fn compose(parent: Transform, local: Transform) Transform { const cos_a = std.math.cos(parent.angle); const sin_a = std.math.sin(parent.angle); const sx = parent.scale.scale_x * local.scale.scale_x; const sy = parent.scale.scale_y * local.scale.scale_y; const local_px = local.position.x * parent.scale.scale_x; const local_py = local.position.y * parent.scale.scale_y; const rx = cos_a * local_px - sin_a * local_py; const ry = sin_a * local_px + cos_a * local_py; return .{ .position = .{ .x = parent.position.x + rx, .y = parent.position.y + ry, }, .angle = parent.angle + local.angle, .scale = .{ .scale_x = sx, .scale_y = sy }, .opacity = parent.opacity * local.opacity, }; } }; /// Мировые -> локальные для заданного трансформа. pub fn worldToLocalTransform(t: Transform, wx: f32, wy: f32) Point2_f { const dx = wx - t.position.x; const dy = wy - t.position.y; const cos_a = std.math.cos(-t.angle); const sin_a = std.math.sin(-t.angle); const sx = if (t.scale.scale_x != 0) t.scale.scale_x else 1.0; const sy = if (t.scale.scale_y != 0) t.scale.scale_y else 1.0; return .{ .x = (dx * cos_a - dy * sin_a) / sx, .y = (dx * sin_a + dy * cos_a) / sy, }; } /// Конвейер отрисовки: локальные координаты -> трансформ -> буфер. pub const DrawContext = struct { pixels: []Color.PMA, buf_width: u32, buf_height: u32, visible_rect: Rect_i, scale_x: f32, scale_y: f32, transform: Transform = .{}, /// Если true, blendPixelAtBuffer перезаписывает пиксель без бленда replace_mode: bool = false, _fill_canvas: ?*FillCanvas = null, pub fn setTransform(self: *DrawContext, t: Transform) void { self.transform = t; } /// Локальные -> мировые. pub fn localToWorld(self: *const DrawContext, local_x: f32, local_y: f32) Point2_f { const t = &self.transform; const cos_a = std.math.cos(t.angle); const sin_a = std.math.sin(t.angle); return .{ .x = t.position.x + (local_x * t.scale.scale_x) * cos_a - (local_y * t.scale.scale_y) * sin_a, .y = t.position.y + (local_x * t.scale.scale_x) * sin_a + (local_y * t.scale.scale_y) * cos_a, }; } /// Мировые -> буфер (float). pub fn worldToBufferF(self: *const DrawContext, wx: f32, wy: f32) Point2_f { const canvas_x = wx * self.scale_x; const canvas_y = wy * self.scale_y; const vx = @as(f32, @floatFromInt(self.visible_rect.x)); const vy = @as(f32, @floatFromInt(self.visible_rect.y)); return .{ .x = canvas_x - vx, .y = canvas_y - vy, }; } /// Мировые -> буфер (целые). pub fn worldToBuffer(self: *const DrawContext, wx: f32, wy: f32) Point2_i { const b = self.worldToBufferF(wx, wy); return .{ .x = @intFromFloat(std.math.round(b.x)), .y = @intFromFloat(std.math.round(b.y)), }; } /// Буфер -> мировые. pub fn bufferToWorld(self: *const DrawContext, buf_x: f32, buf_y: f32) Point2_f { const vx = @as(f32, @floatFromInt(self.visible_rect.x)); const vy = @as(f32, @floatFromInt(self.visible_rect.y)); const canvas_x = buf_x + vx; const canvas_y = buf_y + vy; const sx = if (self.scale_x != 0) self.scale_x else 1.0; const sy = if (self.scale_y != 0) self.scale_y else 1.0; return .{ .x = canvas_x / sx, .y = canvas_y / sy, }; } /// Мировые -> локальные. pub fn worldToLocal(self: *const DrawContext, wx: f32, wy: f32) Point2_f { return worldToLocalTransform(self.transform, wx, wy); } /// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель. /// Если активен fill canvas, каждый записанный пиксель помечается как граница для заливки. pub fn blendPixelAtBuffer(self: *DrawContext, bx_i32: i32, by_i32: i32, color: Color.PMA) void { if (self._fill_canvas) |fc| fc.setBorder(bx_i32, by_i32); if (bx_i32 < 0 or by_i32 < 0 or bx_i32 >= self.buf_width or by_i32 >= self.buf_height) return; const bx: u32 = @intCast(bx_i32); const by: u32 = @intCast(by_i32); const idx = by * self.buf_width + bx; const dst = &self.pixels[idx]; if (self.replace_mode) { dst.* = color; return; } const t = &self.transform; const a = @as(f32, @floatFromInt(color.a)) / 255.0 * t.opacity; const src_r = @as(f32, @floatFromInt(color.r)) * t.opacity; const src_g = @as(f32, @floatFromInt(color.g)) * t.opacity; const src_b = @as(f32, @floatFromInt(color.b)) * t.opacity; const inv_a = 1.0 - a; dst.r = @intFromFloat(std.math.clamp(src_r + inv_a * @as(f32, @floatFromInt(dst.r)), 0, 255)); dst.g = @intFromFloat(std.math.clamp(src_g + inv_a * @as(f32, @floatFromInt(dst.g)), 0, 255)); dst.b = @intFromFloat(std.math.clamp(src_b + inv_a * @as(f32, @floatFromInt(dst.b)), 0, 255)); dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255)); } /// Накладывает буфер другого контекста на этот с заданной прозрачностью (один бленд на пиксель). Размеры буферов должны совпадать. pub fn compositeDrawerContext(self: *DrawContext, other: *const DrawContext, opacity: f32) void { if (self.buf_width != other.buf_width or self.buf_height != other.buf_height) return; const n = self.buf_width * self.buf_height; for (0..n) |i| { const src = other.pixels[i]; if (src.a == 0) continue; const dst = &self.pixels[i]; const a = @as(f32, @floatFromInt(src.a)) / 255.0 * opacity; const src_r = @as(f32, @floatFromInt(src.r)) * opacity; const src_g = @as(f32, @floatFromInt(src.g)) * opacity; const src_b = @as(f32, @floatFromInt(src.b)) * opacity; const inv_a = 1.0 - a; dst.r = @intFromFloat(std.math.clamp(src_r + inv_a * @as(f32, @floatFromInt(dst.r)), 0, 255)); dst.g = @intFromFloat(std.math.clamp(src_g + inv_a * @as(f32, @floatFromInt(dst.g)), 0, 255)); dst.b = @intFromFloat(std.math.clamp(src_b + inv_a * @as(f32, @floatFromInt(dst.b)), 0, 255)); dst.a = @intFromFloat(std.math.clamp(a * 255 + inv_a * @as(f32, @floatFromInt(dst.a)), 0, 255)); } } /// Пиксель в локальных координатах (трансформ + PMA). pub fn blendPixelLocal(self: *DrawContext, local_x: f32, local_y: f32, color: Color.PMA) void { const w = self.localToWorld(local_x, local_y); const b = self.worldToBufferF(w.x, w.y); const bx: i32 = @intFromFloat(b.x); const by: i32 = @intFromFloat(b.y); const vw = @as(i32, @intCast(self.visible_rect.w)); const vh = @as(i32, @intCast(self.visible_rect.h)); if (bx < 0 or bx >= vw or by < 0 or by >= vh) return; self.blendPixelAtBuffer(bx, 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, allocator, color); fc.deinit(); allocator.destroy(fc); } }; /// Конвертирует u32 0xRRGGBBAA в Color.PMA. pub fn rgbaToPma(rgba: u32) Color.PMA { const r: u8 = @intCast((rgba >> 24) & 0xFF); const g: u8 = @intCast((rgba >> 16) & 0xFF); const b: u8 = @intCast((rgba >> 8) & 0xFF); const a: u8 = @intCast((rgba >> 0) & 0xFF); if (a == 0) return .{ .r = 0, .g = 0, .b = 0, .a = 0 }; const af: f32 = @as(f32, @floatFromInt(a)) / 255.0; return .{ .r = @intFromFloat(@as(f32, @floatFromInt(r)) * af), .g = @intFromFloat(@as(f32, @floatFromInt(g)) * af), .b = @intFromFloat(@as(f32, @floatFromInt(b)) * af), .a = a, }; } /// Контекст для заполнения фигур цветом. Границы хранятся в set — по x и y можно добавлять произвольные точки. const FillCanvas = struct { /// Множество пикселей границы (x, y) — без ограничения по размеру буфера. border_set: std.AutoHashMap(Point2_i, void), buf_width: u32, buf_height: u32, pub fn init(allocator: std.mem.Allocator, width: u32, height: u32) !FillCanvas { const border_set = std.AutoHashMap(Point2_i, void).init(allocator); return .{ .border_set = border_set, .buf_width = width, .buf_height = height, }; } pub fn deinit(self: *FillCanvas) void { self.border_set.deinit(); } /// Добавляет точку границы; координаты x, y могут быть любыми (условно бесконечное поле). pub fn setBorder(self: *FillCanvas, x: i32, y: i32) void { self.border_set.put(.{ .x = x, .y = y }, {}) catch {}; } /// Заливка четырёхсвязным стековым алгоритмом от первой найденной внутренней точки. 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; 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.*); } 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(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; } } } /// По строкам: рёбра (подряд идущие x) → сегменты между ними. Семена — середины чётных сегментов (при чётном числе границ). fn findFillSeeds( keys: []const Point2_i, buf_w_i: i32, buf_h_i: i32, allocator: std.mem.Allocator, ) !std.ArrayList(Point2_i) { 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; 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; 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 { run_end_x = p.x; } } // Семена только при чётном числе границ 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; } };