From f1a0e84272f698b657c9d80e84f82462342e2f65 Mon Sep 17 00:00:00 2001 From: Roman Pytkov Date: Tue, 24 Feb 2026 19:59:57 +0300 Subject: [PATCH] =?UTF-8?q?Refactor:=20=D0=9F=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D1=91=D1=81=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=84=D0=B8=D0=B3?= =?UTF-8?q?=D1=83=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Я перенёс логику создания объектов фигур из `Document.zig` и `Object.zig` в новый модуль `shape.zig`. Это упрощает добавление новых фигур и улучшает организацию кода. --- src/models/Document.zig | 10 ++-- src/models/Object.zig | 42 --------------- src/models/basic_models.zig | 8 +++ src/models/shape.zig | 99 ++++++++++++++++++++++++++++++++++++ src/models/shape/arc.zig | 21 ++++++++ src/models/shape/broken.zig | 47 +++++++++++++++++ src/models/shape/ellipse.zig | 31 +++++++++++ src/models/shape/line.zig | 35 +++++++++++++ src/tests.zig | 1 + 9 files changed, 245 insertions(+), 49 deletions(-) create mode 100644 src/models/shape.zig create mode 100644 src/models/shape/arc.zig create mode 100644 src/models/shape/broken.zig create mode 100644 src/models/shape/ellipse.zig create mode 100644 src/models/shape/line.zig diff --git a/src/models/Document.zig b/src/models/Document.zig index 5f647ea..4deb110 100644 --- a/src/models/Document.zig +++ b/src/models/Document.zig @@ -4,6 +4,7 @@ const Size = basic_models.Size; const Document = @This(); pub const Object = @import("Object.zig"); +const shape = @import("shape.zig"); size: Size, allocator: std.mem.Allocator, @@ -27,13 +28,8 @@ pub fn addObject(self: *Document, template: Object) !void { try self.objects.append(self.allocator, obj); } -pub fn addShape(self: *Document, parent: ?*Object, shape: Object.ShapeKind) !void { - const obj = switch (shape) { - .line => try Object.createLine(self.allocator), - .ellipse => try Object.createEllipse(self.allocator), - .broken => try Object.createBrokenLine(self.allocator), - .arc => return error.ArcNotImplemented, - }; +pub fn addShape(self: *Document, parent: ?*Object, shape_kind: Object.ShapeKind) !void { + const obj = try shape.createObject(self.allocator, shape_kind); if (parent) |p| { try p.addChild(self.allocator, obj); } else { diff --git a/src/models/Object.zig b/src/models/Object.zig index 8831e7b..164be89 100644 --- a/src/models/Object.zig +++ b/src/models/Object.zig @@ -1,10 +1,6 @@ const std = @import("std"); -const basic_models = @import("basic_models.zig"); -const Size = basic_models.Size; -const Point2 = basic_models.Point2; const Property = @import("Property.zig").Property; const PropertyData = @import("Property.zig").Data; -const defaultCommonProperties = @import("Property.zig").defaultCommonProperties; const Object = @This(); pub const ShapeKind = enum { @@ -68,41 +64,3 @@ pub fn deinit(self: *Object, allocator: std.mem.Allocator) void { self.properties.deinit(allocator); self.* = undefined; } - -fn createWithCommonProperties(allocator: std.mem.Allocator, shape: ShapeKind) !Object { - var properties_list = std.ArrayList(Property).empty; - errdefer properties_list.deinit(allocator); - for (defaultCommonProperties) |prop| try properties_list.append(allocator, prop); - return .{ - .shape = shape, - .properties = properties_list, - .children = std.ArrayList(Object).empty, - }; -} - -pub fn createEllipse(allocator: std.mem.Allocator) !Object { - var obj = try createWithCommonProperties(allocator, .ellipse); - errdefer obj.deinit(allocator); - try obj.properties.append(allocator, .{ .data = .{ .radii = .{ .x = 50, .y = 50 } } }); - return obj; -} - -pub fn createLine(allocator: std.mem.Allocator) !Object { - var obj = try createWithCommonProperties(allocator, .line); - errdefer obj.deinit(allocator); - try obj.properties.append(allocator, .{ .data = .{ .end_point = .{ .x = 100, .y = 0 } } }); - return obj; -} - -pub fn createBrokenLine(allocator: std.mem.Allocator) !Object { - var obj = try createWithCommonProperties(allocator, .broken); - errdefer obj.deinit(allocator); - var points = std.ArrayList(Point2).empty; - try points.appendSlice(allocator, &.{ - .{ .x = 0, .y = 0 }, - .{ .x = 80, .y = 0 }, - .{ .x = 80, .y = 60 }, - }); - try obj.properties.append(allocator, .{ .data = .{ .points = points } }); - return obj; -} diff --git a/src/models/basic_models.zig b/src/models/basic_models.zig index 40bd92c..fcd20da 100644 --- a/src/models/basic_models.zig +++ b/src/models/basic_models.zig @@ -29,3 +29,11 @@ pub const Scale2 = struct { scale_x: f32 = 1, scale_y: f32 = 1, }; + +/// Прямоугольник в координатах документа (f32), например локальные границы объекта. +pub const Rect = struct { + x: f32 = 0, + y: f32 = 0, + w: f32 = 0, + h: f32 = 0, +}; diff --git a/src/models/shape.zig b/src/models/shape.zig new file mode 100644 index 0000000..2a14894 --- /dev/null +++ b/src/models/shape.zig @@ -0,0 +1,99 @@ +const std = @import("std"); +const Object = @import("Object.zig"); +const Property = @import("Property.zig").Property; +const PropertyData = @import("Property.zig").Data; +const defaultCommonProperties = @import("Property.zig").defaultCommonProperties; +const basic_models = @import("basic_models.zig"); +const line = @import("shape/line.zig"); +const ellipse = @import("shape/ellipse.zig"); +const broken = @import("shape/broken.zig"); +const arc = @import("shape/arc.zig"); + +pub const Rect = basic_models.Rect; + +/// Создаёт объект с общими свойствами по умолчанию и специфичными для типа фигуры. +pub fn createObject(allocator: std.mem.Allocator, shape_kind: Object.ShapeKind) !Object { + var obj = try createWithCommonProperties(allocator, shape_kind); + errdefer obj.deinit(allocator); + switch (shape_kind) { + .line => try line.appendDefaultShapeProperties(allocator, &obj), + .ellipse => try ellipse.appendDefaultShapeProperties(allocator, &obj), + .broken => try broken.appendDefaultShapeProperties(allocator, &obj), + .arc => try arc.appendDefaultShapeProperties(allocator, &obj), + } + return obj; +} + +fn createWithCommonProperties(allocator: std.mem.Allocator, shape_kind: Object.ShapeKind) !Object { + var properties_list = std.ArrayList(Property).empty; + errdefer properties_list.deinit(allocator); + for (defaultCommonProperties) |prop| try properties_list.append(allocator, prop); + return .{ + .shape = shape_kind, + .properties = properties_list, + .children = std.ArrayList(Object).empty, + }; +} + +/// Проверяет, что объект имеет ожидаемый тип и все требуемые для этого типа свойства. +pub fn ensure(obj: *const Object, expected_kind: Object.ShapeKind) !void { + if (obj.shape != expected_kind) return error.WrongShapeKind; + const tags = requiredTagsFor(expected_kind); + for (tags) |tag| { + if (obj.getProperty(tag) == null) return error.MissingRequiredProperty; + } +} + +fn requiredTagsFor(kind: Object.ShapeKind) []const std.meta.Tag(PropertyData) { + return switch (kind) { + .line => line.getRequiredTags(), + .ellipse => ellipse.getRequiredTags(), + .broken => broken.getRequiredTags(), + .arc => arc.getRequiredTags(), + }; +} + +/// Локальные границы объекта (AABB в своих координатах). +pub fn getLocalBounds(obj: *const Object) ?Rect { + ensure(obj, obj.shape) catch return null; + return switch (obj.shape) { + .line => line.getLocalBounds(obj), + .ellipse => ellipse.getLocalBounds(obj), + .broken => broken.getLocalBounds(obj), + .arc => arc.getLocalBounds(obj), + }; +} + +test "getLocalBounds" { + const shape = @This(); + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + var line_obj = try shape.createObject(allocator, .line); + defer line_obj.deinit(allocator); + const line_bounds = getLocalBounds(&line_obj); + try std.testing.expect(line_bounds != null); + try std.testing.expect(line_bounds.?.x == 0); + try std.testing.expect(line_bounds.?.y == 0); + try std.testing.expect(line_bounds.?.w == 100); + try std.testing.expect(line_bounds.?.h == 0); + + var ellipse_obj = try shape.createObject(allocator, .ellipse); + defer ellipse_obj.deinit(allocator); + const ellipse_bounds = getLocalBounds(&ellipse_obj); + try std.testing.expect(ellipse_bounds != null); + try std.testing.expect(ellipse_bounds.?.x == -50); + try std.testing.expect(ellipse_bounds.?.y == -50); + try std.testing.expect(ellipse_bounds.?.w == 100); + try std.testing.expect(ellipse_bounds.?.h == 100); + + var broken_obj = try shape.createObject(allocator, .broken); + defer broken_obj.deinit(allocator); + const broken_bounds = getLocalBounds(&broken_obj); + try std.testing.expect(broken_bounds != null); + try std.testing.expect(broken_bounds.?.x == 0); + try std.testing.expect(broken_bounds.?.y == 0); + try std.testing.expect(broken_bounds.?.w == 80); + try std.testing.expect(broken_bounds.?.h == 60); +} diff --git a/src/models/shape/arc.zig b/src/models/shape/arc.zig new file mode 100644 index 0000000..f9fe54f --- /dev/null +++ b/src/models/shape/arc.zig @@ -0,0 +1,21 @@ +const std = @import("std"); +const Object = @import("../Object.zig"); +const PropertyData = @import("../Property.zig").Data; +const Rect = @import("../basic_models.zig").Rect; + +/// Теги обязательных свойств (заглушка: arc пока не реализован). +pub fn getRequiredTags() []const std.meta.Tag(PropertyData) { + return &[_]std.meta.Tag(PropertyData){}; +} + +/// Добавляет свойства по умолчанию для дуги (заглушка). +pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) !void { + _ = allocator; + _ = obj; + return error.ArcNotImplemented; +} + +/// Локальные границы дуги (заглушка: возвращает null). +pub fn getLocalBounds(_: *const Object) ?Rect { + return null; +} diff --git a/src/models/shape/broken.zig b/src/models/shape/broken.zig new file mode 100644 index 0000000..80ad026 --- /dev/null +++ b/src/models/shape/broken.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const Object = @import("../Object.zig"); +const Property = @import("../Property.zig").Property; +const PropertyData = @import("../Property.zig").Data; +const Point2 = @import("../basic_models.zig").Point2; +const Rect = @import("../basic_models.zig").Rect; + +/// Точки ломаной по умолчанию (для создания). +pub const default_points = [_]Point2{ + .{ .x = 0, .y = 0 }, + .{ .x = 80, .y = 0 }, + .{ .x = 80, .y = 60 }, +}; + +/// Теги обязательных свойств (у ломаной нет const default_shape_properties, только default_points). +pub fn getRequiredTags() []const std.meta.Tag(PropertyData) { + return &[_]std.meta.Tag(PropertyData){.points}; +} + +/// Добавляет к объекту свойства по умолчанию для ломаной (points из default_points). +pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) !void { + var points = std.ArrayList(Point2).empty; + try points.appendSlice(allocator, &default_points); + try obj.properties.append(allocator, .{ .data = .{ .points = points } }); +} + +/// Локальные границы ломаной: AABB по всем точкам. Возвращает null, если точек нет. +pub fn getLocalBounds(obj: *const Object) ?Rect { + const p = obj.getProperty(.points) orelse return null; + if (p.points.items.len == 0) return null; + var min_x: f32 = p.points.items[0].x; + var max_x: f32 = min_x; + var min_y: f32 = p.points.items[0].y; + var max_y: f32 = min_y; + for (p.points.items[1..]) |pt| { + min_x = @min(min_x, pt.x); + max_x = @max(max_x, pt.x); + min_y = @min(min_y, pt.y); + max_y = @max(max_y, pt.y); + } + return .{ + .x = min_x, + .y = min_y, + .w = max_x - min_x, + .h = max_y - min_y, + }; +} diff --git a/src/models/shape/ellipse.zig b/src/models/shape/ellipse.zig new file mode 100644 index 0000000..b3da3ce --- /dev/null +++ b/src/models/shape/ellipse.zig @@ -0,0 +1,31 @@ +const std = @import("std"); +const Object = @import("../Object.zig"); +const Property = @import("../Property.zig").Property; +const PropertyData = @import("../Property.zig").Data; +const Rect = @import("../basic_models.zig").Rect; + +/// Свойства фигуры по умолчанию (для создания и проверки типа). Теги для ensure выводятся отсюда. +pub const default_shape_properties = [_]Property{ + .{ .data = .{ .radii = .{ .x = 50, .y = 50 } } }, +}; + +/// Теги обязательных свойств (выводятся из default_shape_properties). +pub fn getRequiredTags() []const std.meta.Tag(PropertyData) { + return &([_]std.meta.Tag(PropertyData){std.meta.activeTag(default_shape_properties[0].data)}); +} + +/// Добавляет к объекту свойства по умолчанию для эллипса. +pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) !void { + for (default_shape_properties) |prop| try obj.properties.append(allocator, prop); +} + +/// Локальные границы эллипса: [-radii.x, -radii.y] .. [radii.x, radii.y]. +pub fn getLocalBounds(obj: *const Object) ?Rect { + const r = obj.getProperty(.radii) orelse return null; + return .{ + .x = -r.radii.x, + .y = -r.radii.y, + .w = 2 * r.radii.x, + .h = 2 * r.radii.y, + }; +} diff --git a/src/models/shape/line.zig b/src/models/shape/line.zig new file mode 100644 index 0000000..a01fe33 --- /dev/null +++ b/src/models/shape/line.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const Object = @import("../Object.zig"); +const Property = @import("../Property.zig").Property; +const PropertyData = @import("../Property.zig").Data; +const Rect = @import("../basic_models.zig").Rect; + +/// Свойства фигуры по умолчанию (для создания и проверки типа). Теги для ensure выводятся отсюда. +pub const default_shape_properties = [_]Property{ + .{ .data = .{ .end_point = .{ .x = 100, .y = 0 } } }, +}; + +/// Теги обязательных свойств (выводятся из default_shape_properties). +pub fn getRequiredTags() []const std.meta.Tag(PropertyData) { + return &([_]std.meta.Tag(PropertyData){std.meta.activeTag(default_shape_properties[0].data)}); +} + +/// Добавляет к объекту свойства по умолчанию для линии. +pub fn appendDefaultShapeProperties(allocator: std.mem.Allocator, obj: *Object) !void { + for (default_shape_properties) |prop| try obj.properties.append(allocator, prop); +} + +/// Локальные границы линии: от (0,0) до end_point. +pub fn getLocalBounds(obj: *const Object) ?Rect { + const ep = obj.getProperty(.end_point) orelse return null; + const min_x = @min(0, ep.end_point.x); + const max_x = @max(0, ep.end_point.x); + const min_y = @min(0, ep.end_point.y); + const max_y = @max(0, ep.end_point.y); + return .{ + .x = min_x, + .y = min_y, + .w = max_x - min_x, + .h = max_y - min_y, + }; +} diff --git a/src/tests.zig b/src/tests.zig index 5f39e71..65a82bd 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -4,6 +4,7 @@ test "discover tests" { _ = @import("main.zig"); _ = @import("models/Property.zig"); + _ = @import("models/shape.zig"); } // Убедиться, что выполнились все ожидаемые тесты: этот тест пройдёт только если до него дошли (т.е. все предыдущие прошли).