Compare commits

..

4 Commits

7 changed files with 223 additions and 18 deletions

View File

@@ -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,

View File

@@ -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 по точкам.

View File

@@ -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,9 +33,23 @@ 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(&copy_ctx, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, stroke, thickness); line.drawLine(&copy_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(&copy_ctx, pts[last].x, pts[last].y, pts[0].x, pts[0].y, stroke, thickness, true);
}
if (do_fill) {
copy_ctx.stopFill(allocator, fill_color);
} }
ctx.compositeDrawerContext(&copy_ctx, copy_ctx.transform.opacity); ctx.compositeDrawerContext(&copy_ctx, copy_ctx.transform.opacity);

View File

@@ -89,7 +89,7 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void {
const ny = loc_y * inv_ry; const ny = loc_y * inv_ry;
const d = nx * nx + ny * ny; const d = nx * nx + ny * ny;
if (d >= d_inner_sq and d <= d_outer_sq) { if (d >= d_inner_sq and d <= d_outer_sq) {
ctx.blendPixelAtBuffer(@intCast(bx), @intCast(by), stroke); ctx.blendPixelAtBuffer(bx, by, stroke);
} }
} }
} }

View File

@@ -17,11 +17,12 @@ pub fn draw(ctx: *DrawContext, obj: *const Object) void {
const end_y = ep_prop.end_point.y; 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 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;
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 w0 = ctx.localToWorld(x0, y0);
const w1 = ctx.localToWorld(x1, y1); const w1 = ctx.localToWorld(x1, y1);
const b0 = ctx.worldToBuffer(w0.x, w0.y); 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 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))); const thickness_px: u32 = @as(u32, @intFromFloat(std.math.round(thickness * scale)));
if (thickness_px > 0) 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 { 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; 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 thickness_corrected: u32 = thickness_px;
var use_vertical: bool = undefined; 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)))); thickness_corrected = @max(@as(u32, 1), @as(u32, @intFromFloat(std.math.round(corrected_f))));
} }
const half_thickness: i32 = @intCast(thickness_corrected / 2); 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 p0 = Point2_i{ .x = bx0, .y = by0 };
var p1 = Point2_i{ .x = bx1, .y = by1 }; var p1 = Point2_i{ .x = bx1, .y = by1 };
// Отсечение отрезка буфером. Если он целиком вне — рисовать нечего. // Отсечение только когда не рисуем вне viewport: иначе линия идёт целиком, толщина 1 px снаружи.
if (!draw_when_outside) {
if (!clipLineToBuffer(ctx, &p0, &p1, @as(i32, @intCast(thickness_corrected)))) return; if (!clipLineToBuffer(ctx, &p0, &p1, @as(i32, @intCast(thickness_corrected)))) return;
}
var x0 = p0.x; var x0 = p0.x;
var y0 = p0.y; var y0 = p0.y;
@@ -156,15 +160,20 @@ fn drawLineInBuffer(ctx: *DrawContext, bx0: i32, by0: i32, bx1: i32, by1: i32, c
const sy: i32 = if (y0 < ey) 1 else -1; const sy: i32 = if (y0 < ey) 1 else -1;
var err: i32 = dx + dy; 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) { while (true) {
var thick: i32 = -half_thickness; // Внутри viewport — полная толщина; снаружи при draw_when_outside — только 1 пиксель.
while (thick <= half_thickness) { 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 x = if (use_vertical) x0 + thick else x0;
const y = if (use_vertical) y0 else y0 + thick; const y = if (use_vertical) y0 else y0 + thick;
if (x >= 0 and y >= 0) { ctx.blendPixelAtBuffer(x, y, color);
ctx.blendPixelAtBuffer(@intCast(x), @intCast(y), color);
}
thick += 1; thick += 1;
} }

View File

@@ -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,13 @@ pub const DrawContext = struct {
} }
/// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель. /// Смешивает цвет в пикселе буфера с учётом opacity трансформа. В replace_mode просто перезаписывает пиксель.
pub fn blendPixelAtBuffer(self: *DrawContext, bx: u32, by: u32, color: Color.PMA) void { /// Если активен fill canvas, каждый записанный пиксель помечается как граница для заливки.
if (bx >= self.buf_width or by >= self.buf_height) return; 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 idx = by * self.buf_width + bx;
const dst = &self.pixels[idx]; const dst = &self.pixels[idx];
if (self.replace_mode) { if (self.replace_mode) {
@@ -181,7 +187,24 @@ pub const DrawContext = struct {
const vw = @as(i32, @intCast(self.visible_rect.w)); const vw = @as(i32, @intCast(self.visible_rect.w));
const vh = @as(i32, @intCast(self.visible_rect.h)); const vh = @as(i32, @intCast(self.visible_rect.h));
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(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);
} }
}; };
@@ -200,3 +223,129 @@ pub fn rgbaToPma(rgba: u32) Color.PMA {
.a = a, .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(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;
}
}
}
/// По строкам: рёбра (подряд идущие 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);
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;
}
};

View File

@@ -381,17 +381,22 @@ fn drawStatsPanel(stats: RenderStats, frame_index: u64) void {
fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Property, row_index: usize) void { fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Property, row_index: usize) void {
const row_id: usize = row_index * 16; const row_id: usize = row_index * 16;
const is_even = row_index % 2 == 0;
var row = dvui.box( var row = dvui.box(
@src(), @src(),
.{ .dir = .vertical }, .{ .dir = .vertical },
.{ .{
.id_extra = row_id, .id_extra = row_id,
.expand = .horizontal, .expand = .horizontal,
.padding = dvui.Rect{ .y = 2 }, .padding = dvui.Rect{ .y = 2, .x = 4 },
.corner_radius = dvui.Rect.all(4),
.background = is_even,
.color_fill = if (is_even) dvui.Color.black.opacity(0.4) else .{},
}, },
); );
{ {
const tag = std.meta.activeTag(prop.data); const tag = std.meta.activeTag(prop.data);
dvui.labelNoFmt(@src(), propertyLabel(tag), .{}, .{}); dvui.labelNoFmt(@src(), propertyLabel(tag), .{}, .{});
switch (prop.data) { switch (prop.data) {
@@ -694,6 +699,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 +750,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",
}; };
} }