Случайное дерево объектов

This commit is contained in:
2026-02-24 00:39:40 +03:00
parent aeda3ee0d0
commit 1a94cc8bfd
6 changed files with 203 additions and 48 deletions

View File

@@ -60,6 +60,7 @@ pub fn addNewDocument(self: *WindowContext) !void {
const ptr = try self.allocator.create(OpenDocument); const ptr = try self.allocator.create(OpenDocument);
errdefer self.allocator.destroy(ptr); errdefer self.allocator.destroy(ptr);
OpenDocument.init(self.allocator, ptr); OpenDocument.init(self.allocator, ptr);
try ptr.document.addRandomShapes(std.crypto.random);
try self.documents.append(self.allocator, ptr); try self.documents.append(self.allocator, ptr);
self.active_document_index = self.documents.items.len - 1; self.active_document_index = self.documents.items.len - 1;
} }

View File

@@ -40,3 +40,43 @@ pub fn addShape(self: *Document, parent: ?*Object, shape: Object.ShapeKind) !voi
try self.addObject(obj); try self.addObject(obj);
} }
} }
fn randomShapeKind(rng: std.Random) Object.ShapeKind {
const shapes_implemented = [_]Object.ShapeKind{ .line, .ellipse, .broken };
return shapes_implemented[rng.intRangeLessThan(usize, 0, shapes_implemented.len)];
}
/// Создаёт случайное количество фигур в документе (в т.ч. вложенных).
/// Используются только реализованные типы: line, ellipse, broken.
/// Ограничение max_total предотвращает экспоненциальный рост и переполнение.
pub fn addRandomShapes(self: *Document, rng: std.Random) !void {
const max_total: usize = 80;
var total_count: usize = 0;
const n_root = rng.intRangeLessThan(usize, 1, 5);
for (0..n_root) |_| {
if (total_count >= max_total) break;
try self.addShape(null, randomShapeKind(rng));
total_count += 1;
}
var stack = std.ArrayList(*Object).empty;
defer stack.deinit(self.allocator);
for (self.objects.items) |*obj| {
try stack.append(self.allocator, obj);
}
while (stack.pop()) |obj| {
if (total_count >= max_total) continue;
const n_children = rng.intRangeLessThan(usize, 0, 2);
const base_len = obj.children.items.len;
for (0..n_children) |_| {
if (total_count >= max_total) break;
try self.addShape(obj, randomShapeKind(rng));
total_count += 1;
}
// Пушим в стек только после всех append, чтобы не держать указатели при реаллокации obj.children
for (obj.children.items[base_len..]) |*child| {
try stack.append(self.allocator, child);
}
}
}

View File

@@ -97,7 +97,7 @@ pub fn createLine(allocator: std.mem.Allocator) !Object {
pub fn createBrokenLine(allocator: std.mem.Allocator) !Object { pub fn createBrokenLine(allocator: std.mem.Allocator) !Object {
var obj = try createWithCommonProperties(allocator, .broken); var obj = try createWithCommonProperties(allocator, .broken);
errdefer obj.deinit(allocator); errdefer obj.deinit(allocator);
var points = std.ArrayList(Point2).init(allocator); var points = std.ArrayList(Point2).empty;
try points.appendSlice(allocator, &.{ try points.appendSlice(allocator, &.{
.{ .x = 0, .y = 0 }, .{ .x = 0, .y = 0 },
.{ .x = 80, .y = 0 }, .{ .x = 80, .y = 0 },

View File

@@ -22,15 +22,7 @@ pub fn guiFrame(ctx: *WindowContext) bool {
{ {
left_panel.leftPanel(ctx); left_panel.leftPanel(ctx);
var back = dvui.box( right_panel.rightPanel(ctx);
@src(),
.{ .dir = .horizontal },
.{ .expand = .both, .padding = dvui.Rect.all(12), .background = true },
);
{
right_panel.rightPanel(ctx);
}
back.deinit();
} }
content_row.deinit(); content_row.deinit();
} }

View File

@@ -1,30 +1,130 @@
const dvui = @import("dvui"); const dvui = @import("dvui");
const WindowContext = @import("../WindowContext.zig"); const WindowContext = @import("../WindowContext.zig");
const Document = @import("../models/Document.zig");
const Object = Document.Object;
const panel_gap: f32 = 12;
const panel_padding: f32 = 5;
const panel_radius: f32 = 24;
const fill_color = dvui.Color.black.opacity(0.2);
fn shapeLabel(shape: Object.ShapeKind) []const u8 {
return switch (shape) {
.line => "Line",
.ellipse => "Ellipse",
.arc => "Arc",
.broken => "Broken line",
};
}
fn objectTreeRow(obj: *const Object, depth: u32, row_id: *usize) void {
const id = row_id.*;
row_id.* += 1;
const indent_px = depth * 18;
var row = dvui.box(
@src(),
.{ .dir = .horizontal },
.{ .padding = dvui.Rect{ .x = @floatFromInt(indent_px) }, .id_extra = id },
);
{
dvui.labelNoFmt(@src(), shapeLabel(obj.shape), .{}, .{ .id_extra = id });
}
row.deinit();
for (obj.children.items) |*child| {
objectTreeRow(child, depth + 1, row_id);
}
}
fn objectTree(ctx: *WindowContext) void {
const active_doc = ctx.activeDocument();
if (active_doc) |open_doc| {
const doc = &open_doc.document;
if (doc.objects.items.len == 0) {
dvui.label(@src(), "No objects", .{}, .{});
} else {
var row_id: usize = 0;
for (doc.objects.items) |*obj| {
objectTreeRow(obj, 0, &row_id);
}
}
} else {
dvui.label(@src(), "No document", .{}, .{});
}
}
pub fn leftPanel(ctx: *WindowContext) void { pub fn leftPanel(ctx: *WindowContext) void {
var padding = dvui.Rect.all(panel_gap);
padding.w = 0;
var panel = dvui.box( var panel = dvui.box(
@src(), @src(),
.{ .dir = .vertical }, .{ .dir = .vertical },
.{ .expand = .vertical, .min_size_content = .{ .w = 200 }, .background = true }, .{
.expand = .vertical,
.min_size_content = .{ .w = 220 },
.background = true,
.padding = padding,
},
); );
{ {
dvui.label(@src(), "Tools", .{}, .{}); // Верхняя часть: дерево объектов
var tree_section = dvui.box(
const active_doc = ctx.activeDocument(); @src(),
if (active_doc) |doc| { .{ .dir = .vertical },
const canvas = &doc.canvas; .{
if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {} .expand = .both,
if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) { .padding = dvui.Rect.all(panel_padding),
if (doc.cpu_render.type == .Gradient) { .corner_radius = dvui.Rect.all(panel_radius),
doc.cpu_render.type = .Squares; .color_fill = fill_color,
} else { .background = true,
doc.cpu_render.type = .Gradient; },
} );
canvas.redrawExample() catch {}; {
dvui.label(@src(), "Objects", .{}, .{});
var scroll = dvui.scrollArea(
@src(),
.{ .vertical = .auto },
.{ .expand = .vertical, .background = false },
);
{
objectTree(ctx);
} }
} else { scroll.deinit();
dvui.label(@src(), "No document", .{}, .{});
} }
tree_section.deinit();
// Нижняя часть: настройки
var settings_section = dvui.box(
@src(),
.{ .dir = .vertical },
.{
.expand = .horizontal,
.margin = .{ .y = 5 },
.padding = dvui.Rect.all(panel_padding),
.corner_radius = dvui.Rect.all(panel_radius),
.color_fill = fill_color,
.background = true,
},
);
{
dvui.label(@src(), "Settings", .{}, .{});
const active_doc = ctx.activeDocument();
if (active_doc) |doc| {
const canvas = &doc.canvas;
if (dvui.checkbox(@src(), &canvas.native_scaling, "Scaling", .{})) {}
if (dvui.button(@src(), if (doc.cpu_render.type == .Gradient) "Gradient" else "Squares", .{}, .{})) {
if (doc.cpu_render.type == .Gradient) {
doc.cpu_render.type = .Squares;
} else {
doc.cpu_render.type = .Gradient;
}
canvas.redrawExample() catch {};
}
} else {
dvui.label(@src(), "No document", .{}, .{});
}
}
settings_section.deinit();
} }
panel.deinit(); panel.deinit();
} }

View File

@@ -5,42 +5,64 @@ const canvas_view = @import("canvas_view.zig");
pub fn rightPanel(ctx: *WindowContext) void { pub fn rightPanel(ctx: *WindowContext) void {
const fill_color = dvui.Color.black.opacity(0.25); const fill_color = dvui.Color.black.opacity(0.25);
var panel = dvui.box( var back = dvui.box(
@src(), @src(),
.{ .dir = .vertical }, .{ .dir = .horizontal },
.{ .{ .expand = .both, .padding = dvui.Rect.all(12), .background = true },
.expand = .both,
.background = true,
.padding = dvui.Rect.all(5),
.corner_radius = dvui.Rect.all(24),
.color_fill = fill_color,
},
); );
{ {
const active_doc = ctx.activeDocument(); var panel = dvui.box(
if (active_doc) |doc| { @src(),
const content_rect_scale = panel.data().contentRectScale(); .{ .dir = .vertical },
canvas_view.canvasView(&doc.canvas, content_rect_scale); .{
} else { .expand = .both,
noDocView(ctx); .background = true,
.padding = dvui.Rect.all(5),
.corner_radius = dvui.Rect.all(24),
.color_fill = fill_color,
},
);
{
const active_doc = ctx.activeDocument();
if (active_doc) |doc| {
const content_rect_scale = panel.data().contentRectScale();
canvas_view.canvasView(&doc.canvas, content_rect_scale);
} else {
noDocView(ctx);
}
} }
panel.deinit();
} }
panel.deinit(); back.deinit();
} }
fn noDocView(ctx: *WindowContext) void { fn noDocView(ctx: *WindowContext) void {
var center = dvui.box( var center = dvui.box(
@src(), @src(),
.{ .dir = .vertical }, .{ .dir = .vertical },
.{ .expand = .both, .padding = dvui.Rect.all(20) }, .{
.expand = .both,
.padding = dvui.Rect.all(20),
},
); );
{ {
dvui.label(@src(), "No document open", .{}, .{}); var box = dvui.box(
if (dvui.button(@src(), "New document", .{}, .{})) { @src(),
ctx.addNewDocument() catch |err| { .{ .dir = .vertical },
std.debug.print("addNewDocument error: {}\n", .{err}); .{
}; .gravity_x = 0.5,
.gravity_y = 0.5,
},
);
{
dvui.label(@src(), "No document open", .{}, .{});
if (dvui.button(@src(), "New document", .{}, .{})) {
ctx.addNewDocument() catch |err| {
std.debug.print("addNewDocument error: {}\n", .{err});
};
}
} }
box.deinit();
} }
center.deinit(); center.deinit();
} }