Compare commits
4 Commits
e5b8e6735d
...
b1177265ea
| Author | SHA1 | Date | |
|---|---|---|---|
| b1177265ea | |||
| 5b1b3a8c5e | |||
| 7aa9673b44 | |||
| 32cffb757d |
@@ -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,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(©_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, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (do_fill) {
|
||||||
|
copy_ctx.stopFill(allocator, fill_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.compositeDrawerContext(©_ctx, copy_ctx.transform.opacity);
|
ctx.compositeDrawerContext(©_ctx, copy_ctx.transform.opacity);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user