From 9ca360c6b306ee43d4c48c19dae1d79ec483a807 Mon Sep 17 00:00:00 2001 From: "andrew.kraevskii" Date: Wed, 4 Mar 2026 01:56:32 +0300 Subject: [PATCH 1/8] Im trying not to die of old age waiting on llvm :( --- build.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index 56ebed9..aed4363 100644 --- a/build.zig +++ b/build.zig @@ -8,8 +8,8 @@ pub fn build(b: *std.Build) void { const exe = b.addExecutable(.{ .name = "Zivro", - .use_llvm = true, - .use_lld = true, + // .use_llvm = true, + // .use_lld = true, .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, From 3348b2e91ce7f6f17f408133157279ca4594d875 Mon Sep 17 00:00:00 2001 From: "andrew.kraevskii" Date: Wed, 4 Mar 2026 00:22:46 +0300 Subject: [PATCH 2/8] Update to latest dvui. --- build.zig.zon | 4 ++-- review.txt | 0 src/render/CpuRenderEngine.zig | 4 ++-- src/ui/canvas_view.zig | 15 ++++++++------- src/ui/left_panel.zig | 12 +++++++----- 5 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 review.txt diff --git a/build.zig.zon b/build.zig.zon index c0ed749..e126f18 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -33,8 +33,8 @@ // internet connectivity. .dependencies = .{ .dvui = .{ - .url = "https://github.com/david-vanderson/dvui/archive/main.tar.gz", - .hash = "dvui-0.4.0-dev-AQFJmev72QC6e0ojhnW8a_wRhZDgzWWLgeyoNuIPZc2m", + .url = "git+https://github.com/david-vanderson/dvui#edb2d5a4cd2981fca74ee5f096277b91333c1316", + .hash = "dvui-0.4.0-dev-AQFJmeGB3QAwun9qF76CDE5IopA4nUVRgD-IwwTsOo4H", }, // See `zig fetch --save ` for a command-line interface for adding dependencies. diff --git a/review.txt b/review.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/render/CpuRenderEngine.zig b/src/render/CpuRenderEngine.zig index 6ec4187..c6e0451 100644 --- a/src/render/CpuRenderEngine.zig +++ b/src/render/CpuRenderEngine.zig @@ -172,7 +172,7 @@ pub fn example(self: CpuRenderEngine, canvas_size: Size_i, visible_rect: Rect_i) .Squares => self.renderSquares(pixels, canvas_size, visible_rect), } - return try dvui.textureCreate(pixels, width, height, .nearest); + return try dvui.textureCreate(pixels, width, height, .nearest, .rgba_8_8_8_8); } pub fn renderEngine(self: *CpuRenderEngine) RenderEngine { @@ -191,7 +191,7 @@ pub fn renderDocument(self: *CpuRenderEngine, document: *const Document, canvas_ try cpu_draw.drawDocument(pixels, width, height, visible_rect, document, canvas_size, self._allocator); self._renderStats.render_time_ns = t.read(); - return try dvui.textureCreate(pixels, width, height, .nearest); + return try dvui.textureCreate(pixels, width, height, .nearest, .rgba_8_8_8_8); } pub fn getStats(self: CpuRenderEngine) RenderStats { diff --git a/src/ui/canvas_view.zig b/src/ui/canvas_view.zig index aff748c..e44ec83 100644 --- a/src/ui/canvas_view.zig +++ b/src/ui/canvas_view.zig @@ -17,14 +17,15 @@ pub fn canvasView(canvas: *Canvas, selected_object_id: ?u64, content_rect_scale: var overlay = dvui.overlay(@src(), .{ .expand = .both }); { const overlay_parent = dvui.parentGet(); + const init_options: dvui.ScrollAreaWidget.InitOpts = .{ + .scroll_info = &canvas.scroll, + .vertical_bar = .auto, + .horizontal_bar = .auto, + .process_events_after = false, + }; var scroll = dvui.scrollArea( @src(), - .{ - .scroll_info = &canvas.scroll, - .vertical_bar = .auto, - .horizontal_bar = .auto, - .process_events_after = false, - }, + init_options, .{ .expand = .both, .background = false, @@ -100,7 +101,7 @@ pub fn canvasView(canvas: *Canvas, selected_object_id: ?u64, content_rect_scale: } } - if (!scroll.init_opts.process_events_after) { + if (!init_options.process_events_after) { if (scroll.scroll) |*sc| { dvui.clipSet(sc.prevClip); sc.processEventsAfter(); diff --git a/src/ui/left_panel.zig b/src/ui/left_panel.zig index 67370ee..d036ba0 100644 --- a/src/ui/left_panel.zig +++ b/src/ui/left_panel.zig @@ -222,11 +222,13 @@ pub fn leftPanel(ctx: *WindowContext) void { }, ); { - dvui.label(@src(), "Objects", .{}, .{ .font = .{ - .id = dvui.themeGet().font_heading.id, - .line_height_factor = dvui.themeGet().font_heading.line_height_factor, - .size = dvui.themeGet().font_heading.size + 8, - }, .gravity_x = 0.5 }); + dvui.label(@src(), "Objects", .{}, .{ + .font = .{ + .line_height_factor = dvui.themeGet().font_heading.line_height_factor, + .size = dvui.themeGet().font_heading.size + 8, + }, + .gravity_x = 0.5, + }); var scroll = dvui.scrollArea( @src(), .{ .vertical = .auto, .horizontal = .auto }, From e3a45061948de2511955e419bdde58942b853684 Mon Sep 17 00:00:00 2001 From: "andrew.kraevskii" Date: Wed, 4 Mar 2026 01:13:03 +0300 Subject: [PATCH 3/8] Use std.mem.Allocator.dupe instead of @memcpy. --- src/models/Property.zig | 3 +-- src/models/shape/shape.zig | 3 +-- src/random_document.zig | 3 +-- src/ui/canvas_view.zig | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/models/Property.zig b/src/models/Property.zig index 8404742..39a7553 100644 --- a/src/models/Property.zig +++ b/src/models/Property.zig @@ -51,8 +51,7 @@ pub const Property = struct { .points => |slice| .{ .data = .{ .points = blk: { - const copy = try allocator.alloc(Point2_f, slice.len); - @memcpy(copy, slice); + const copy = try allocator.dupe(Point2_f, slice); break :blk copy; }, }, diff --git a/src/models/shape/shape.zig b/src/models/shape/shape.zig index 9b5ae27..6aef5cd 100644 --- a/src/models/shape/shape.zig +++ b/src/models/shape/shape.zig @@ -15,8 +15,7 @@ fn appendShapeProperties(allocator: std.mem.Allocator, obj: *Object, props: []co for (props) |prop| { if (prop.data == .points) { const pts = prop.data.points; - const copy = try allocator.alloc(Point2_f, pts.len); - @memcpy(copy, pts); + const copy = try allocator.dupe(Point2_f, pts); try obj.properties.append(allocator, .{ .data = .{ .points = copy } }); } else { try obj.properties.append(allocator, prop); diff --git a/src/random_document.zig b/src/random_document.zig index edc60e0..96c37d6 100644 --- a/src/random_document.zig +++ b/src/random_document.zig @@ -84,8 +84,7 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size x += randFloat(rng, -40, 80); y += randFloat(rng, -30, 60); } - const slice = try allocator.alloc(Point2_f, list.items.len); - @memcpy(slice, list.items); + const slice = try allocator.dupe(Point2_f, list.items); try obj.setProperty(allocator, .{ .data = .{ .points = slice } }); }, } diff --git a/src/ui/canvas_view.zig b/src/ui/canvas_view.zig index e44ec83..4062b5f 100644 --- a/src/ui/canvas_view.zig +++ b/src/ui/canvas_view.zig @@ -676,8 +676,7 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope } if (changed) { - const slice = canvas.allocator.alloc(Point2_f, list.items.len) catch return; - @memcpy(slice, list.items); + const slice = canvas.allocator.dupe(Point2_f, list.items) catch return; obj.setProperty(canvas.allocator, .{ .data = .{ .points = slice } }) catch { canvas.allocator.free(slice); return; From cc10d806fe8c93f0d138f809c9950e2b370c293e Mon Sep 17 00:00:00 2001 From: "andrew.kraevskii" Date: Wed, 4 Mar 2026 01:13:03 +0300 Subject: [PATCH 4/8] Return type of field of property instead of property. --- src/models/Object.zig | 4 ++-- src/models/shape/shape.zig | 2 +- src/render/cpu/broken.zig | 13 ++++++------- src/render/cpu/draw.zig | 2 +- src/render/cpu/ellipse.zig | 18 +++++++++--------- src/render/cpu/line.zig | 10 +++++----- src/render/cpu/pipeline.zig | 8 ++++---- 7 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/models/Object.zig b/src/models/Object.zig index 959b6cd..91c610b 100644 --- a/src/models/Object.zig +++ b/src/models/Object.zig @@ -33,9 +33,9 @@ shape: ShapeKind, properties: std.ArrayList(Property), children: std.ArrayList(Object), -pub fn getProperty(self: Object, tag: std.meta.Tag(PropertyData)) ?*const PropertyData { +pub fn getProperty(self: Object, comptime tag: std.meta.Tag(PropertyData)) ?@FieldType(PropertyData, @tagName(tag)) { for (self.properties.items) |*prop| { - if (std.meta.activeTag(prop.data) == tag) return &prop.data; + if (std.meta.activeTag(prop.data) == tag) return @field(prop.data, @tagName(tag)); } return null; } diff --git a/src/models/shape/shape.zig b/src/models/shape/shape.zig index 6aef5cd..fd6da90 100644 --- a/src/models/shape/shape.zig +++ b/src/models/shape/shape.zig @@ -32,7 +32,7 @@ pub fn createObject(allocator: std.mem.Allocator, shape_kind: Object.ShapeKind) .ellipse => try appendShapeProperties(allocator, &obj, &ellipse.default_shape_properties), .broken => { try appendShapeProperties(allocator, &obj, &broken.default_shape_properties); - try obj.setProperty(allocator, .{ .data = .{ .fill_rgba = obj.getProperty(.stroke_rgba).?.stroke_rgba } }); + try obj.setProperty(allocator, .{ .data = .{ .fill_rgba = obj.getProperty(.stroke_rgba).? } }); }, } return obj; diff --git a/src/render/cpu/broken.zig b/src/render/cpu/broken.zig index e2d2e7d..d0dc486 100644 --- a/src/render/cpu/broken.zig +++ b/src/render/cpu/broken.zig @@ -16,14 +16,13 @@ pub fn draw( obj: *const Object, allocator: std.mem.Allocator, ) !void { - const p_prop = obj.getProperty(.points) orelse return; - const pts = p_prop.points; + const pts = obj.getProperty(.points) orelse return; if (pts.len < 2) return; - 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 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 stroke = if (obj.getProperty(.stroke_rgba)) |stroke_rgba| pipeline.rgbaToPma(stroke_rgba) else default_stroke; + const thickness = obj.getProperty(.thickness) orelse default_thickness; + const closed = obj.getProperty(.closed) orelse false; + const filled = obj.getProperty(.filled) orelse true; + const fill_color = if (obj.getProperty(.fill_rgba)) |fill_rgba| pipeline.rgbaToPma(fill_rgba) else default_fill; const buffer = try allocator.alloc(Color.PMA, ctx.buf_width * ctx.buf_height); @memset(buffer, .{ .r = 0, .g = 0, .b = 0, .a = 0 }); diff --git a/src/render/cpu/draw.zig b/src/render/cpu/draw.zig index 3997b83..abaa033 100644 --- a/src/render/cpu/draw.zig +++ b/src/render/cpu/draw.zig @@ -13,7 +13,7 @@ const DrawContext = pipeline.DrawContext; const Transform = pipeline.Transform; fn isVisible(obj: *const Object) bool { - return if (obj.getProperty(.visible)) |p| p.visible else true; + return obj.getProperty(.visible) orelse true; } fn drawObject( diff --git a/src/render/cpu/ellipse.zig b/src/render/cpu/ellipse.zig index c5dd031..301b2b8 100644 --- a/src/render/cpu/ellipse.zig +++ b/src/render/cpu/ellipse.zig @@ -17,17 +17,17 @@ const default_thickness: f32 = 2.0; /// arc_percent: 100 — полный эллипс, иначе одна дуга; обход в коде от (0,ry) по квадрантам (визуально может казаться от низа против часовой из‑за экранной Y). /// Отрисовка в отдельный буфер и один composite, чтобы при alpha<255 пиксели не накладывались несколько раз. pub fn draw(ctx: *DrawContext, obj: *const Object, allocator: std.mem.Allocator) !void { - const r_prop = obj.getProperty(.radii) orelse return; - const rx = r_prop.radii.x; - const ry = r_prop.radii.y; + const radii = obj.getProperty(.radii) orelse return; + const rx = radii.x; + const ry = radii.y; if (rx <= 0 or ry <= 0) return; - 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 arc_percent = std_math.clamp(if (obj.getProperty(.arc_percent)) |p| p.arc_percent else 100.0, 0.0, 100.0); - const closed = if (obj.getProperty(.closed)) |c| c.closed else true; - const filled = if (obj.getProperty(.filled)) |f| f.filled else false; - const fill_color = if (obj.getProperty(.fill_rgba)) |f| pipeline.rgbaToPma(f.fill_rgba) else default_fill; + const stroke = if (obj.getProperty(.stroke_rgba)) |stroke_rgba| pipeline.rgbaToPma(stroke_rgba) else default_stroke; + const thickness = if (obj.getProperty(.thickness)) |thickness| thickness else default_thickness; + const arc_percent = std_math.clamp(if (obj.getProperty(.arc_percent)) |arc_percent| arc_percent else 100.0, 0.0, 100.0); + const closed = obj.getProperty(.closed) orelse true; + const filled = obj.getProperty(.filled) orelse false; + const fill_color = if (obj.getProperty(.fill_rgba)) |fill_rgba| pipeline.rgbaToPma(fill_rgba) else default_fill; const do_fill = filled and (closed or arc_percent >= 100.0); const t = &ctx.transform; diff --git a/src/render/cpu/line.zig b/src/render/cpu/line.zig index 0715482..f12fdae 100644 --- a/src/render/cpu/line.zig +++ b/src/render/cpu/line.zig @@ -12,11 +12,11 @@ const default_thickness: f32 = 2.0; /// Линия от (0,0) до end_point. pub fn draw(ctx: *DrawContext, obj: *const Object) void { - const ep_prop = obj.getProperty(.end_point) orelse return; - const end_x = ep_prop.end_point.x; - 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; + const end_point = obj.getProperty(.end_point) orelse return; + const end_x = end_point.x; + const end_y = end_point.y; + const stroke = if (obj.getProperty(.stroke_rgba)) |stroke_rgba| pipeline.rgbaToPma(stroke_rgba) else default_stroke; + const thickness = obj.getProperty(.thickness) orelse default_thickness; drawLine(ctx, 0, 0, end_x, end_y, stroke, thickness, false); } diff --git a/src/render/cpu/pipeline.zig b/src/render/cpu/pipeline.zig index 6713857..d4db169 100644 --- a/src/render/cpu/pipeline.zig +++ b/src/render/cpu/pipeline.zig @@ -16,10 +16,10 @@ pub const Transform = struct { opacity: f32 = 1.0, pub fn init(obj: *const Document.Object) Transform { - const pos = if (obj.getProperty(.position)) |p| p.position else Point2_f{ .x = 0, .y = 0 }; - const angle = if (obj.getProperty(.angle)) |p| p.angle else 0; - const scale = if (obj.getProperty(.scale)) |p| p.scale else Scale2_f{ .scale_x = 1, .scale_y = 1 }; - const opacity = if (obj.getProperty(.opacity)) |p| p.opacity else 1.0; + 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, From 0a47ea1e43e174aed593cb3381ddbb64a5454a9d Mon Sep 17 00:00:00 2001 From: "andrew.kraevskii" Date: Wed, 4 Mar 2026 01:32:03 +0300 Subject: [PATCH 5/8] If function doesn't do anything related to object just don't pass object to it --- src/render/CpuRenderEngine.zig | 6 ++---- src/render/cpu/pipeline.zig | 4 +--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/render/CpuRenderEngine.zig b/src/render/CpuRenderEngine.zig index c6e0451..eb3766c 100644 --- a/src/render/CpuRenderEngine.zig +++ b/src/render/CpuRenderEngine.zig @@ -66,9 +66,7 @@ fn renderGradient(self: CpuRenderEngine, pixels: []Color.PMA, width: u32, height } } -fn renderSquares(self: CpuRenderEngine, pixels: []Color.PMA, canvas_size: Size_i, visible_rect: Rect_i) void { - _ = self; - +fn renderSquares(pixels: []Color.PMA, canvas_size: Size_i, visible_rect: Rect_i) void { const colors = [_]Color.PMA{ .{ .r = 255, .g = 0, .b = 0, .a = 255 }, .{ .r = 255, .g = 165, .b = 0, .a = 255 }, @@ -169,7 +167,7 @@ pub fn example(self: CpuRenderEngine, canvas_size: Size_i, visible_rect: Rect_i) switch (self.type) { .Gradient => self.renderGradient(pixels, width, height, full_w, full_h, visible_rect), - .Squares => self.renderSquares(pixels, canvas_size, visible_rect), + .Squares => renderSquares(pixels, canvas_size, visible_rect), } return try dvui.textureCreate(pixels, width, height, .nearest, .rgba_8_8_8_8); diff --git a/src/render/cpu/pipeline.zig b/src/render/cpu/pipeline.zig index d4db169..933163f 100644 --- a/src/render/cpu/pipeline.zig +++ b/src/render/cpu/pipeline.zig @@ -273,7 +273,7 @@ const FillCanvas = struct { }.lessThan); // Семена: по строкам находим сегменты (пары x), пересекаем с окном буфера, берём середину сегмента. - var seeds = findFillSeeds(self, keys_buf.items, buf_w_i, buf_h_i, allocator) catch return; + 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; @@ -304,13 +304,11 @@ const FillCanvas = struct { /// По строкам: рёбра (подряд идущие 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; From b0259c5788c7efc7bd81ab5138dd0a3f22061fe4 Mon Sep 17 00:00:00 2001 From: "andrew.kraevskii" Date: Wed, 4 Mar 2026 01:43:59 +0300 Subject: [PATCH 6/8] Stuff. --- src/WindowContext.zig | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/WindowContext.zig b/src/WindowContext.zig index 9e14d05..a702416 100644 --- a/src/WindowContext.zig +++ b/src/WindowContext.zig @@ -16,15 +16,10 @@ pub const OpenDocument = struct { selected_object_id: ?u64 = null, pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void { - const default_size = basic_models.Size_f{ .w = 800, .h = 600 }; - self.document = Document.init(default_size); - self.cpu_render = CpuRenderEngine.init(allocator, .Squares); - self.canvas = Canvas.init( - allocator, - &self.document, - (&self.cpu_render).renderEngine(), - ); - self.selected_object_id = null; + initWithDocument(allocator, self, .init(.{ + .w = 800, + .h = 600, + })); } pub fn initWithDocument(allocator: std.mem.Allocator, self: *OpenDocument, doc: Document) void { From 045624beac13f7a2c8422700dd13dae3d82c35f4 Mon Sep 17 00:00:00 2001 From: "andrew.kraevskii" Date: Wed, 4 Mar 2026 01:43:59 +0300 Subject: [PATCH 7/8] Use leak detection of DebugAllocator. When allocating memory it remebers where memory was allocated but it only for leaks in .deinit(). So not calling it is a sure way to miss a bunch of leaks. deinit() returns if it found leaks but we don't really care. Its usefull for tests but not here. --- src/main.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 0a46dd0..f641daa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,7 +5,9 @@ const WindowContext = @import("WindowContext.zig"); const ui = @import("ui/frame.zig"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + // std.heap.GeneralPurposeAllocator was renamed to DebugAllocator recently. + var gpa: std.heap.DebugAllocator(.{}) = .init; + defer _ = gpa.deinit(); const allocator = gpa.allocator(); var backend = try SDLBackend.initWindow(.{ From d6c8a062cd430ae7a50d326d5bfa58d5e57afd58 Mon Sep 17 00:00:00 2001 From: "andrew.kraevskii" Date: Wed, 4 Mar 2026 01:46:16 +0300 Subject: [PATCH 8/8] Fix memory leaks found by DebugAllocator. --- src/main.zig | 1 + src/random_document.zig | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/main.zig b/src/main.zig index f641daa..9a5da46 100644 --- a/src/main.zig +++ b/src/main.zig @@ -8,6 +8,7 @@ pub fn main() !void { // std.heap.GeneralPurposeAllocator was renamed to DebugAllocator recently. var gpa: std.heap.DebugAllocator(.{}) = .init; defer _ = gpa.deinit(); + const allocator = gpa.allocator(); var backend = try SDLBackend.initWindow(.{ diff --git a/src/random_document.zig b/src/random_document.zig index 96c37d6..27db6db 100644 --- a/src/random_document.zig +++ b/src/random_document.zig @@ -85,6 +85,7 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size y += randFloat(rng, -30, 60); } const slice = try allocator.dupe(Point2_f, list.items); + errdefer allocator.free(slice); try obj.setProperty(allocator, .{ .data = .{ .points = slice } }); }, } @@ -99,6 +100,7 @@ pub fn addRandomShapes(doc: *Document, allocator: std.mem.Allocator, rng: std.Ra for (0..n_root) |_| { if (total_count >= max_total) break; var obj = try shape.createObject(allocator, randomShapeKind(rng)); + defer obj.deinit(allocator); try randomizeObjectProperties(allocator, &doc.size, &obj, rng); try doc.addObject(allocator, obj); total_count += 1; @@ -116,6 +118,7 @@ pub fn addRandomShapes(doc: *Document, allocator: std.mem.Allocator, rng: std.Ra for (0..n_children) |_| { if (total_count >= max_total) break; var child = try shape.createObject(allocator, randomShapeKind(rng)); + defer child.deinit(allocator); try randomizeObjectProperties(allocator, &doc.size, &child, rng); try obj.addChild(allocator, child, &doc.next_object_id); total_count += 1;