Compare commits
3 Commits
b692539a30
...
d1722e3b6b
| Author | SHA1 | Date | |
|---|---|---|---|
| d1722e3b6b | |||
| 37eea6ed89 | |||
| 8ea5d97c2d |
@@ -92,7 +92,7 @@ pub fn exampleReset(self: *Canvas) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn addRandomShapes(self: *Canvas) !void {
|
pub fn addRandomShapes(self: *Canvas) !void {
|
||||||
try random_document.addRandomShapes(self.document, std.crypto.random);
|
try random_document.addRandomShapes(self.document, self.allocator, std.crypto.random);
|
||||||
self.requestRedraw();
|
self.requestRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub const OpenDocument = struct {
|
|||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void {
|
pub fn init(allocator: std.mem.Allocator, self: *OpenDocument) void {
|
||||||
const default_size = basic_models.Size_f{ .w = 800, .h = 600 };
|
const default_size = basic_models.Size_f{ .w = 800, .h = 600 };
|
||||||
self.document = Document.init(allocator, default_size);
|
self.document = Document.init(default_size);
|
||||||
self.cpu_render = CpuRenderEngine.init(allocator, .Squares);
|
self.cpu_render = CpuRenderEngine.init(allocator, .Squares);
|
||||||
self.canvas = Canvas.init(
|
self.canvas = Canvas.init(
|
||||||
allocator,
|
allocator,
|
||||||
@@ -38,8 +38,8 @@ pub const OpenDocument = struct {
|
|||||||
self.selected_object_id = null;
|
self.selected_object_id = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *OpenDocument) void {
|
pub fn deinit(self: *OpenDocument, allocator: std.mem.Allocator) void {
|
||||||
self.document.deinit();
|
self.document.deinit(allocator);
|
||||||
self.canvas.deinit();
|
self.canvas.deinit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -60,7 +60,7 @@ pub fn init(allocator: std.mem.Allocator) !WindowContext {
|
|||||||
|
|
||||||
pub fn deinit(self: *WindowContext) void {
|
pub fn deinit(self: *WindowContext) void {
|
||||||
for (self.documents.items) |ptr| {
|
for (self.documents.items) |ptr| {
|
||||||
ptr.deinit();
|
ptr.deinit(self.allocator);
|
||||||
self.allocator.destroy(ptr);
|
self.allocator.destroy(ptr);
|
||||||
}
|
}
|
||||||
self.documents.deinit(self.allocator);
|
self.documents.deinit(self.allocator);
|
||||||
@@ -85,7 +85,7 @@ pub fn addDocument(self: *WindowContext, doc: Document) !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);
|
||||||
var doc_mut = doc;
|
var doc_mut = doc;
|
||||||
errdefer doc_mut.deinit();
|
errdefer doc_mut.deinit(self.allocator);
|
||||||
OpenDocument.initWithDocument(self.allocator, ptr, doc_mut);
|
OpenDocument.initWithDocument(self.allocator, ptr, doc_mut);
|
||||||
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;
|
||||||
@@ -100,7 +100,7 @@ pub fn setActiveDocument(self: *WindowContext, index: usize) void {
|
|||||||
pub fn closeDocument(self: *WindowContext, index: usize) void {
|
pub fn closeDocument(self: *WindowContext, index: usize) void {
|
||||||
if (index >= self.documents.items.len) return;
|
if (index >= self.documents.items.len) return;
|
||||||
const open_doc = self.documents.items[index];
|
const open_doc = self.documents.items[index];
|
||||||
open_doc.deinit();
|
open_doc.deinit(self.allocator);
|
||||||
self.allocator.destroy(open_doc);
|
self.allocator.destroy(open_doc);
|
||||||
_ = self.documents.orderedRemove(index);
|
_ = self.documents.orderedRemove(index);
|
||||||
|
|
||||||
|
|||||||
@@ -7,71 +7,86 @@ pub const Object = @import("Object.zig");
|
|||||||
const shape = @import("shape/shape.zig");
|
const shape = @import("shape/shape.zig");
|
||||||
|
|
||||||
size: Size_f,
|
size: Size_f,
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
objects: std.ArrayList(Object),
|
objects: std.ArrayList(Object),
|
||||||
next_object_id: u64,
|
next_object_id: u64,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, size: Size_f) Document {
|
pub fn init(size: Size_f) Document {
|
||||||
return .{
|
return .{
|
||||||
.size = size,
|
.size = size,
|
||||||
.allocator = allocator,
|
|
||||||
.objects = std.ArrayList(Object).empty,
|
.objects = std.ArrayList(Object).empty,
|
||||||
.next_object_id = 1,
|
.next_object_id = 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Document) void {
|
pub fn deinit(self: *Document, allocator: std.mem.Allocator) void {
|
||||||
for (self.objects.items) |*obj| obj.deinit(self.allocator);
|
for (self.objects.items) |*obj| obj.deinit(allocator);
|
||||||
self.objects.deinit(self.allocator);
|
self.objects.deinit(allocator);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn addObject(self: *Document, template: Object) !void {
|
pub fn clone(self: *const Document, allocator: std.mem.Allocator) !Document {
|
||||||
const obj = try template.clone(self.allocator, &self.next_object_id);
|
var objects_list = std.ArrayList(Object).empty;
|
||||||
try self.objects.append(self.allocator, obj);
|
errdefer {
|
||||||
|
for (objects_list.items) |*obj| obj.deinit(allocator);
|
||||||
|
objects_list.deinit(allocator);
|
||||||
|
}
|
||||||
|
var next_id = self.next_object_id;
|
||||||
|
for (self.objects.items) |obj| {
|
||||||
|
try objects_list.append(allocator, try obj.clone(allocator, &next_id));
|
||||||
|
}
|
||||||
|
return .{
|
||||||
|
.size = self.size,
|
||||||
|
.objects = objects_list,
|
||||||
|
.next_object_id = next_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addObject(self: *Document, allocator: std.mem.Allocator, template: Object) !void {
|
||||||
|
const obj = try template.clone(allocator, &self.next_object_id);
|
||||||
|
try self.objects.append(allocator, obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Добавляет объект в документ: как ребёнка родителя (если id найден), иначе в корень.
|
/// Добавляет объект в документ: как ребёнка родителя (если id найден), иначе в корень.
|
||||||
pub fn addObjectUnderParentId(self: *Document, parent_id: ?u64, template: Object) !void {
|
pub fn addObjectUnderParentId(self: *Document, allocator: std.mem.Allocator, parent_id: ?u64, template: Object) !void {
|
||||||
if (parent_id) |id| {
|
if (parent_id) |id| {
|
||||||
if (self.findObjectById(id)) |parent| {
|
if (self.findObjectById(id)) |parent| {
|
||||||
try parent.addChild(self.allocator, template, &self.next_object_id);
|
try parent.addChild(allocator, template, &self.next_object_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try self.addObject(template);
|
try self.addObject(allocator, template);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn addShape(self: *Document, parent: ?*Object, shape_kind: Object.ShapeKind) !void {
|
pub fn addShape(self: *Document, allocator: std.mem.Allocator, parent: ?*Object, shape_kind: Object.ShapeKind) !void {
|
||||||
const obj = try shape.createObject(self.allocator, shape_kind);
|
const obj = try shape.createObject(allocator, shape_kind);
|
||||||
if (parent) |p| {
|
if (parent) |p| {
|
||||||
try p.addChild(self.allocator, obj, &self.next_object_id);
|
try p.addChild(allocator, obj, &self.next_object_id);
|
||||||
} else {
|
} else {
|
||||||
try self.addObject(obj);
|
try self.addObject(allocator, obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Удаляет объект из документа (из корня или из детей родителя). Возвращает true, если объект был найден и удалён.
|
/// Удаляет объект из документа (из корня или из детей родителя). Возвращает true, если объект был найден и удалён.
|
||||||
pub fn removeObject(self: *Document, obj: *Object) bool {
|
pub fn removeObject(self: *Document, allocator: std.mem.Allocator, obj: *Object) bool {
|
||||||
for (self.objects.items, 0..) |*item, i| {
|
for (self.objects.items, 0..) |*item, i| {
|
||||||
if (item == obj) {
|
if (item == obj) {
|
||||||
var removed = self.objects.orderedRemove(i);
|
var removed = self.objects.orderedRemove(i);
|
||||||
removed.deinit(self.allocator);
|
removed.deinit(allocator);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (removeFromChildren(self.allocator, &item.children, obj)) return true;
|
if (removeFromChildren(allocator, &item.children, obj)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Удаляет объект по id. Возвращает true, если объект был найден и удалён.
|
/// Удаляет объект по id. Возвращает true, если объект был найден и удалён.
|
||||||
pub fn removeObjectById(self: *Document, obj_id: u64) bool {
|
pub fn removeObjectById(self: *Document, allocator: std.mem.Allocator, obj_id: u64) bool {
|
||||||
for (self.objects.items, 0..) |*item, i| {
|
for (self.objects.items, 0..) |*item, i| {
|
||||||
if (item.id == obj_id) {
|
if (item.id == obj_id) {
|
||||||
var removed = self.objects.orderedRemove(i);
|
var removed = self.objects.orderedRemove(i);
|
||||||
removed.deinit(self.allocator);
|
removed.deinit(allocator);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (removeFromChildrenById(self.allocator, &item.children, obj_id)) return true;
|
if (removeFromChildrenById(allocator, &item.children, obj_id)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,365 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const Document = @import("../models/Document.zig");
|
|
||||||
const Object = Document.Object;
|
|
||||||
const Property = @import("../models/Property.zig").Property;
|
|
||||||
const PropertyData = @import("../models/Property.zig").Data;
|
|
||||||
const basic_models = @import("../models/basic_models.zig");
|
|
||||||
const Point2_f = basic_models.Point2_f;
|
|
||||||
const Size_f = basic_models.Size_f;
|
|
||||||
const Scale2_f = basic_models.Scale2_f;
|
|
||||||
const Radii_f = basic_models.Radii_f;
|
|
||||||
|
|
||||||
pub const json_version: u32 = 1;
|
|
||||||
const JsonError = std.json.Stringify.Error;
|
|
||||||
|
|
||||||
pub fn saveToFile(doc: *const Document, path: []const u8) !void {
|
|
||||||
var file = try std.fs.cwd().createFile(path, .{ .truncate = true });
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
var buffer: [4096]u8 = undefined;
|
|
||||||
var writer = file.writer(&buffer);
|
|
||||||
var jw: std.json.Stringify = .{
|
|
||||||
.writer = &writer.interface,
|
|
||||||
.options = .{ .whitespace = .indent_2 },
|
|
||||||
};
|
|
||||||
|
|
||||||
try writeDocument(&jw, doc);
|
|
||||||
try writer.interface.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn loadFromFile(allocator: std.mem.Allocator, path: []const u8) !Document {
|
|
||||||
var file = try std.fs.cwd().openFile(path, .{});
|
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
const data = try file.readToEndAlloc(allocator, std.math.maxInt(usize));
|
|
||||||
defer allocator.free(data);
|
|
||||||
|
|
||||||
var parsed = try std.json.parseFromSlice(std.json.Value, allocator, data, .{ .parse_numbers = true });
|
|
||||||
defer parsed.deinit();
|
|
||||||
|
|
||||||
return try parseDocumentValue(allocator, parsed.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeDocument(jw: *std.json.Stringify, doc: *const Document) JsonError!void {
|
|
||||||
try jw.beginObject();
|
|
||||||
try jw.objectField("version");
|
|
||||||
try jw.write(json_version);
|
|
||||||
try jw.objectField("size");
|
|
||||||
try writeSize(jw, doc.size);
|
|
||||||
try jw.objectField("next_object_id");
|
|
||||||
try jw.write(doc.next_object_id);
|
|
||||||
try jw.objectField("objects");
|
|
||||||
try writeObjectArray(jw, doc.objects.items);
|
|
||||||
try jw.endObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeSize(jw: *std.json.Stringify, size: Size_f) JsonError!void {
|
|
||||||
try jw.beginObject();
|
|
||||||
try jw.objectField("w");
|
|
||||||
try jw.write(size.w);
|
|
||||||
try jw.objectField("h");
|
|
||||||
try jw.write(size.h);
|
|
||||||
try jw.endObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writePoint(jw: *std.json.Stringify, pt: Point2_f) JsonError!void {
|
|
||||||
try jw.beginObject();
|
|
||||||
try jw.objectField("x");
|
|
||||||
try jw.write(pt.x);
|
|
||||||
try jw.objectField("y");
|
|
||||||
try jw.write(pt.y);
|
|
||||||
try jw.endObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeScale(jw: *std.json.Stringify, scale: Scale2_f) JsonError!void {
|
|
||||||
try jw.beginObject();
|
|
||||||
try jw.objectField("scale_x");
|
|
||||||
try jw.write(scale.scale_x);
|
|
||||||
try jw.objectField("scale_y");
|
|
||||||
try jw.write(scale.scale_y);
|
|
||||||
try jw.endObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeRadii(jw: *std.json.Stringify, radii: Radii_f) JsonError!void {
|
|
||||||
try jw.beginObject();
|
|
||||||
try jw.objectField("x");
|
|
||||||
try jw.write(radii.x);
|
|
||||||
try jw.objectField("y");
|
|
||||||
try jw.write(radii.y);
|
|
||||||
try jw.endObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeObjectArray(jw: *std.json.Stringify, objects: []const Object) JsonError!void {
|
|
||||||
try jw.beginArray();
|
|
||||||
for (objects) |obj| {
|
|
||||||
try writeObject(jw, obj);
|
|
||||||
}
|
|
||||||
try jw.endArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeObject(jw: *std.json.Stringify, obj: Object) JsonError!void {
|
|
||||||
try jw.beginObject();
|
|
||||||
try jw.objectField("id");
|
|
||||||
try jw.write(obj.id);
|
|
||||||
try jw.objectField("shape");
|
|
||||||
try jw.write(@tagName(obj.shape));
|
|
||||||
try jw.objectField("properties");
|
|
||||||
try writeProperties(jw, obj.properties.items);
|
|
||||||
try jw.objectField("children");
|
|
||||||
try writeObjectArray(jw, obj.children.items);
|
|
||||||
try jw.endObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeProperties(jw: *std.json.Stringify, props: []const Property) JsonError!void {
|
|
||||||
try jw.beginArray();
|
|
||||||
for (props) |prop| {
|
|
||||||
try writeProperty(jw, prop);
|
|
||||||
}
|
|
||||||
try jw.endArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn writeProperty(jw: *std.json.Stringify, prop: Property) JsonError!void {
|
|
||||||
try jw.beginObject();
|
|
||||||
try jw.objectField("tag");
|
|
||||||
try jw.write(@tagName(prop.data));
|
|
||||||
try jw.objectField("value");
|
|
||||||
switch (prop.data) {
|
|
||||||
.position => |p| try writePoint(jw, p),
|
|
||||||
.angle => |v| try jw.write(v),
|
|
||||||
.scale => |s| try writeScale(jw, s),
|
|
||||||
.visible => |v| try jw.write(v),
|
|
||||||
.opacity => |v| try jw.write(v),
|
|
||||||
.locked => |v| try jw.write(v),
|
|
||||||
.size => |s| try writeSize(jw, s),
|
|
||||||
.radii => |r| try writeRadii(jw, r),
|
|
||||||
.end_point => |p| try writePoint(jw, p),
|
|
||||||
.points => |list| {
|
|
||||||
try jw.beginArray();
|
|
||||||
for (list.items) |pt| {
|
|
||||||
try writePoint(jw, pt);
|
|
||||||
}
|
|
||||||
try jw.endArray();
|
|
||||||
},
|
|
||||||
.fill_rgba => |v| try jw.write(v),
|
|
||||||
.stroke_rgba => |v| try jw.write(v),
|
|
||||||
.thickness => |v| try jw.write(v),
|
|
||||||
}
|
|
||||||
try jw.endObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseDocumentValue(allocator: std.mem.Allocator, value: std.json.Value) !Document {
|
|
||||||
const obj = try expectObject(value);
|
|
||||||
const version_val = try getField(obj, "version");
|
|
||||||
const version = try valueToU32(version_val);
|
|
||||||
if (version != json_version) return error.UnsupportedVersion;
|
|
||||||
|
|
||||||
const size_val = try getField(obj, "size");
|
|
||||||
const size = try parseSize(size_val);
|
|
||||||
|
|
||||||
var document = Document{
|
|
||||||
.size = size,
|
|
||||||
.allocator = allocator,
|
|
||||||
.objects = std.ArrayList(Object).empty,
|
|
||||||
.next_object_id = 1,
|
|
||||||
};
|
|
||||||
errdefer document.deinit();
|
|
||||||
|
|
||||||
const objects_val = try getField(obj, "objects");
|
|
||||||
const objects_arr = try expectArray(objects_val);
|
|
||||||
for (objects_arr.items) |item| {
|
|
||||||
const obj_item = try parseObject(allocator, item);
|
|
||||||
try document.objects.append(allocator, obj_item);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.get("next_object_id")) |next_val| {
|
|
||||||
const next_id = try valueToU64(next_val);
|
|
||||||
document.next_object_id = next_id;
|
|
||||||
} else {
|
|
||||||
document.next_object_id = computeNextObjectId(document.objects.items);
|
|
||||||
}
|
|
||||||
|
|
||||||
return document;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseObject(allocator: std.mem.Allocator, value: std.json.Value) !Object {
|
|
||||||
const obj = try expectObject(value);
|
|
||||||
const id = try valueToU64(try getField(obj, "id"));
|
|
||||||
const shape_str = try valueToString(try getField(obj, "shape"));
|
|
||||||
const shape = std.meta.stringToEnum(Object.ShapeKind, shape_str) orelse return error.InvalidJson;
|
|
||||||
|
|
||||||
var properties = std.ArrayList(Property).empty;
|
|
||||||
errdefer {
|
|
||||||
for (properties.items) |*p| p.deinit(allocator);
|
|
||||||
properties.deinit(allocator);
|
|
||||||
}
|
|
||||||
var children = std.ArrayList(Object).empty;
|
|
||||||
errdefer {
|
|
||||||
for (children.items) |*c| c.deinit(allocator);
|
|
||||||
children.deinit(allocator);
|
|
||||||
}
|
|
||||||
|
|
||||||
const props_val = try getField(obj, "properties");
|
|
||||||
const props_arr = try expectArray(props_val);
|
|
||||||
for (props_arr.items) |item| {
|
|
||||||
try properties.append(allocator, try parseProperty(allocator, item));
|
|
||||||
}
|
|
||||||
|
|
||||||
const children_val = try getField(obj, "children");
|
|
||||||
const children_arr = try expectArray(children_val);
|
|
||||||
for (children_arr.items) |item| {
|
|
||||||
try children.append(allocator, try parseObject(allocator, item));
|
|
||||||
}
|
|
||||||
|
|
||||||
return .{
|
|
||||||
.id = id,
|
|
||||||
.shape = shape,
|
|
||||||
.properties = properties,
|
|
||||||
.children = children,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseProperty(allocator: std.mem.Allocator, value: std.json.Value) !Property {
|
|
||||||
const obj = try expectObject(value);
|
|
||||||
const tag = try valueToString(try getField(obj, "tag"));
|
|
||||||
const val = try getField(obj, "value");
|
|
||||||
|
|
||||||
const data: PropertyData = if (std.mem.eql(u8, tag, "position")) blk: {
|
|
||||||
break :blk .{ .position = try parsePoint(val) };
|
|
||||||
} else if (std.mem.eql(u8, tag, "angle")) blk: {
|
|
||||||
break :blk .{ .angle = try valueToF32(val) };
|
|
||||||
} else if (std.mem.eql(u8, tag, "scale")) blk: {
|
|
||||||
break :blk .{ .scale = try parseScale(val) };
|
|
||||||
} else if (std.mem.eql(u8, tag, "visible")) blk: {
|
|
||||||
break :blk .{ .visible = try valueToBool(val) };
|
|
||||||
} else if (std.mem.eql(u8, tag, "opacity")) blk: {
|
|
||||||
break :blk .{ .opacity = try valueToF32(val) };
|
|
||||||
} else if (std.mem.eql(u8, tag, "locked")) blk: {
|
|
||||||
break :blk .{ .locked = try valueToBool(val) };
|
|
||||||
} else if (std.mem.eql(u8, tag, "size")) blk: {
|
|
||||||
break :blk .{ .size = try parseSize(val) };
|
|
||||||
} else if (std.mem.eql(u8, tag, "radii")) blk: {
|
|
||||||
break :blk .{ .radii = try parseRadii(val) };
|
|
||||||
} else if (std.mem.eql(u8, tag, "end_point")) blk: {
|
|
||||||
break :blk .{ .end_point = try parsePoint(val) };
|
|
||||||
} else if (std.mem.eql(u8, tag, "points")) blk: {
|
|
||||||
const arr = try expectArray(val);
|
|
||||||
var list = std.ArrayList(Point2_f).empty;
|
|
||||||
errdefer list.deinit(allocator);
|
|
||||||
for (arr.items) |item| {
|
|
||||||
try list.append(allocator, try parsePoint(item));
|
|
||||||
}
|
|
||||||
break :blk .{ .points = list };
|
|
||||||
} else if (std.mem.eql(u8, tag, "fill_rgba")) blk: {
|
|
||||||
break :blk .{ .fill_rgba = try valueToU32(val) };
|
|
||||||
} else if (std.mem.eql(u8, tag, "stroke_rgba")) blk: {
|
|
||||||
break :blk .{ .stroke_rgba = try valueToU32(val) };
|
|
||||||
} else if (std.mem.eql(u8, tag, "thickness")) blk: {
|
|
||||||
break :blk .{ .thickness = try valueToF32(val) };
|
|
||||||
} else {
|
|
||||||
return error.InvalidJson;
|
|
||||||
};
|
|
||||||
|
|
||||||
return .{ .data = data };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parsePoint(value: std.json.Value) !Point2_f {
|
|
||||||
const obj = try expectObject(value);
|
|
||||||
const x = try valueToF32(try getField(obj, "x"));
|
|
||||||
const y = try valueToF32(try getField(obj, "y"));
|
|
||||||
return .{ .x = x, .y = y };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseSize(value: std.json.Value) !Size_f {
|
|
||||||
const obj = try expectObject(value);
|
|
||||||
const w = try valueToF32(try getField(obj, "w"));
|
|
||||||
const h = try valueToF32(try getField(obj, "h"));
|
|
||||||
return .{ .w = w, .h = h };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseScale(value: std.json.Value) !Scale2_f {
|
|
||||||
const obj = try expectObject(value);
|
|
||||||
const sx = try valueToF32(try getField(obj, "scale_x"));
|
|
||||||
const sy = try valueToF32(try getField(obj, "scale_y"));
|
|
||||||
return .{ .scale_x = sx, .scale_y = sy };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parseRadii(value: std.json.Value) !Radii_f {
|
|
||||||
const obj = try expectObject(value);
|
|
||||||
const x = try valueToF32(try getField(obj, "x"));
|
|
||||||
const y = try valueToF32(try getField(obj, "y"));
|
|
||||||
return .{ .x = x, .y = y };
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expectObject(value: std.json.Value) !std.json.ObjectMap {
|
|
||||||
return switch (value) {
|
|
||||||
.object => |o| o,
|
|
||||||
else => error.InvalidJson,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn expectArray(value: std.json.Value) !std.json.Array {
|
|
||||||
return switch (value) {
|
|
||||||
.array => |a| a,
|
|
||||||
else => error.InvalidJson,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getField(obj: std.json.ObjectMap, key: []const u8) !std.json.Value {
|
|
||||||
return obj.get(key) orelse error.InvalidJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn valueToString(value: std.json.Value) ![]const u8 {
|
|
||||||
return switch (value) {
|
|
||||||
.string => |s| s,
|
|
||||||
else => error.InvalidJson,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn valueToBool(value: std.json.Value) !bool {
|
|
||||||
return switch (value) {
|
|
||||||
.bool => |b| b,
|
|
||||||
else => error.InvalidJson,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn valueToF32(value: std.json.Value) !f32 {
|
|
||||||
return switch (value) {
|
|
||||||
.float => |f| @floatCast(f),
|
|
||||||
.integer => |i| @floatFromInt(i),
|
|
||||||
else => error.InvalidJson,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn valueToU32(value: std.json.Value) !u32 {
|
|
||||||
const v = switch (value) {
|
|
||||||
.integer => |i| i,
|
|
||||||
else => return error.InvalidJson,
|
|
||||||
};
|
|
||||||
if (v < 0 or v > std.math.maxInt(u32)) return error.InvalidJson;
|
|
||||||
return @intCast(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn valueToU64(value: std.json.Value) !u64 {
|
|
||||||
const v = switch (value) {
|
|
||||||
.integer => |i| i,
|
|
||||||
else => return error.InvalidJson,
|
|
||||||
};
|
|
||||||
if (v < 0) return error.InvalidJson;
|
|
||||||
return @intCast(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn computeNextObjectId(objects: []const Object) u64 {
|
|
||||||
var max_id: u64 = 0;
|
|
||||||
for (objects) |obj| {
|
|
||||||
max_id = @max(max_id, maxIdInObject(obj));
|
|
||||||
}
|
|
||||||
return max_id + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn maxIdInObject(obj: Object) u64 {
|
|
||||||
var max_id = obj.id;
|
|
||||||
for (obj.children.items) |child| {
|
|
||||||
max_id = @max(max_id, maxIdInObject(child));
|
|
||||||
}
|
|
||||||
return max_id;
|
|
||||||
}
|
|
||||||
33
src/persistence/json_io.zig
Normal file
33
src/persistence/json_io.zig
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Document = @import("../models/Document.zig");
|
||||||
|
|
||||||
|
/// Сохраняет значение произвольного типа T в JSON-файл.
|
||||||
|
pub fn saveToFile(comptime T: type, value: *const T, path: []const u8) !void {
|
||||||
|
var file = try std.fs.cwd().createFile(path, .{ .truncate = true });
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
var buffer: [4096]u8 = undefined;
|
||||||
|
var writer = file.writer(&buffer);
|
||||||
|
|
||||||
|
try std.json.Stringify.value(value, .{ .whitespace = .indent_2 }, &writer.interface);
|
||||||
|
|
||||||
|
try writer.interface.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Загружает значение типа T из JSON-файла.
|
||||||
|
/// Для Document после разбора делается клон через allocator, т.к. парсер выделяет память
|
||||||
|
/// из арены — при закрытии документа её нельзя освобождать нашим аллокатором.
|
||||||
|
pub fn loadFromFile(comptime T: type, allocator: std.mem.Allocator, path: []const u8) !T {
|
||||||
|
var file = try std.fs.cwd().openFile(path, .{});
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
const data = try file.readToEndAlloc(allocator, std.math.maxInt(usize));
|
||||||
|
defer allocator.free(data);
|
||||||
|
|
||||||
|
var parsed = try std.json.parseFromSlice(T, allocator, data, .{ .ignore_unknown_fields = true });
|
||||||
|
if (T == Document) {
|
||||||
|
defer parsed.deinit();
|
||||||
|
return try parsed.value.clone(allocator);
|
||||||
|
}
|
||||||
|
return parsed.value;
|
||||||
|
}
|
||||||
@@ -90,17 +90,16 @@ fn randomizeObjectProperties(allocator: std.mem.Allocator, doc_size: *const Size
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Создаёт в документе случайные фигуры (line, ellipse, broken).
|
/// Создаёт в документе случайные фигуры (line, ellipse, broken).
|
||||||
pub fn addRandomShapes(doc: *Document, rng: std.Random) !void {
|
pub fn addRandomShapes(doc: *Document, allocator: std.mem.Allocator, rng: std.Random) !void {
|
||||||
const max_total: usize = 80;
|
const max_total: usize = 80;
|
||||||
var total_count: usize = 0;
|
var total_count: usize = 0;
|
||||||
const allocator = doc.allocator;
|
|
||||||
|
|
||||||
const n_root = rng.intRangeLessThan(usize, 6, 15);
|
const n_root = rng.intRangeLessThan(usize, 6, 15);
|
||||||
for (0..n_root) |_| {
|
for (0..n_root) |_| {
|
||||||
if (total_count >= max_total) break;
|
if (total_count >= max_total) break;
|
||||||
var obj = try shape.createObject(allocator, randomShapeKind(rng));
|
var obj = try shape.createObject(allocator, randomShapeKind(rng));
|
||||||
try randomizeObjectProperties(allocator, &doc.size, &obj, rng);
|
try randomizeObjectProperties(allocator, &doc.size, &obj, rng);
|
||||||
try doc.addObject(obj);
|
try doc.addObject(allocator, obj);
|
||||||
total_count += 1;
|
total_count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ pub const ToolContext = struct {
|
|||||||
pub fn addObject(self: *const ToolContext, template: Document.Object) !void {
|
pub fn addObject(self: *const ToolContext, template: Document.Object) !void {
|
||||||
var obj = template;
|
var obj = template;
|
||||||
const local_pos = self.computeLocalPosition();
|
const local_pos = self.computeLocalPosition();
|
||||||
try obj.setProperty(self.canvas.document.allocator, .{ .data = .{ .position = local_pos } });
|
try obj.setProperty(self.canvas.allocator, .{ .data = .{ .position = local_pos } });
|
||||||
try self.canvas.document.addObjectUnderParentId(self.selected_object_id, obj);
|
try self.canvas.document.addObjectUnderParentId(self.canvas.allocator, self.selected_object_id, obj);
|
||||||
self.canvas.requestRedraw();
|
self.canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const shape = @import("../../models/shape/shape.zig");
|
|||||||
|
|
||||||
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
||||||
const canvas = ctx.canvas;
|
const canvas = ctx.canvas;
|
||||||
var obj = shape.createObject(canvas.document.allocator, .arc) catch return;
|
var obj = shape.createObject(canvas.allocator, .arc) catch return;
|
||||||
defer obj.deinit(canvas.allocator);
|
defer obj.deinit(canvas.allocator);
|
||||||
try ctx.addObject(obj);
|
try ctx.addObject(obj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const shape = @import("../../models/shape/shape.zig");
|
|||||||
|
|
||||||
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
||||||
const canvas = ctx.canvas;
|
const canvas = ctx.canvas;
|
||||||
var obj = shape.createObject(canvas.document.allocator, .broken) catch return;
|
var obj = shape.createObject(canvas.allocator, .broken) catch return;
|
||||||
defer obj.deinit(canvas.allocator);
|
defer obj.deinit(canvas.allocator);
|
||||||
try ctx.addObject(obj);
|
try ctx.addObject(obj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const shape = @import("../../models/shape/shape.zig");
|
|||||||
|
|
||||||
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
||||||
const canvas = ctx.canvas;
|
const canvas = ctx.canvas;
|
||||||
var obj = shape.createObject(canvas.document.allocator, .ellipse) catch return;
|
var obj = shape.createObject(canvas.allocator, .ellipse) catch return;
|
||||||
defer obj.deinit(canvas.allocator);
|
defer obj.deinit(canvas.allocator);
|
||||||
try ctx.addObject(obj);
|
try ctx.addObject(obj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const shape = @import("../../models/shape/shape.zig");
|
|||||||
|
|
||||||
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
fn onCanvasClick(ctx: *const Tool.ToolContext) !void {
|
||||||
const canvas = ctx.canvas;
|
const canvas = ctx.canvas;
|
||||||
var obj = shape.createObject(canvas.document.allocator, .line) catch return;
|
var obj = shape.createObject(canvas.allocator, .line) catch return;
|
||||||
defer obj.deinit(canvas.allocator);
|
defer obj.deinit(canvas.allocator);
|
||||||
try ctx.addObject(obj);
|
try ctx.addObject(obj);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
|
|||||||
changed = res.changed or changed;
|
changed = res.changed or changed;
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .position = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .position = next } }) catch {};
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -431,7 +431,7 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
|
|||||||
subrow.deinit();
|
subrow.deinit();
|
||||||
if (res.changed) {
|
if (res.changed) {
|
||||||
next = degrees * std.math.pi / 180.0;
|
next = degrees * std.math.pi / 180.0;
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .angle = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .angle = next } }) catch {};
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -456,28 +456,28 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
|
|||||||
changed = res.changed or changed;
|
changed = res.changed or changed;
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .scale = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .scale = next } }) catch {};
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.visible => |v| {
|
.visible => |v| {
|
||||||
var next = v;
|
var next = v;
|
||||||
if (dvui.checkbox(@src(), &next, "Visible", .{})) {
|
if (dvui.checkbox(@src(), &next, "Visible", .{})) {
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .visible = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .visible = next } }) catch {};
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.opacity => |opacity| {
|
.opacity => |opacity| {
|
||||||
var next = opacity;
|
var next = opacity;
|
||||||
if (dvui.sliderEntry(@src(), "{d:0.2}", .{ .value = &next, .min = 0.0, .max = 1.0, .interval = 0.01 }, .{ .expand = .horizontal })) {
|
if (dvui.sliderEntry(@src(), "{d:0.2}", .{ .value = &next, .min = 0.0, .max = 1.0, .interval = 0.01 }, .{ .expand = .horizontal })) {
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .opacity = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .opacity = next } }) catch {};
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
.locked => |v| {
|
.locked => |v| {
|
||||||
var next = v;
|
var next = v;
|
||||||
if (dvui.checkbox(@src(), &next, "Locked", .{})) {
|
if (dvui.checkbox(@src(), &next, "Locked", .{})) {
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .locked = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .locked = next } }) catch {};
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -502,7 +502,7 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
|
|||||||
changed = res.changed or changed;
|
changed = res.changed or changed;
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .size = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .size = next } }) catch {};
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -527,7 +527,7 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
|
|||||||
changed = res.changed or changed;
|
changed = res.changed or changed;
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .radii = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .radii = next } }) catch {};
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -556,7 +556,7 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
|
|||||||
changed = res.changed or changed;
|
changed = res.changed or changed;
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .end_point = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .end_point = next } }) catch {};
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -578,7 +578,7 @@ fn drawPropertyEditor(canvas: *Canvas, obj: *Document.Object, prop: *const Prope
|
|||||||
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next, .min = @as(T, 0.0), .max = @as(T, 100.0) }, .{ .expand = .horizontal });
|
const res = dvui.textEntryNumber(@src(), T, .{ .value = &next, .min = @as(T, 0.0), .max = @as(T, 100.0) }, .{ .expand = .horizontal });
|
||||||
subrow.deinit();
|
subrow.deinit();
|
||||||
if (res.changed) {
|
if (res.changed) {
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .thickness = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .thickness = next } }) catch {};
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -597,9 +597,9 @@ fn drawColorEditor(canvas: *Canvas, obj: *Document.Object, rgba: u32, is_fill: b
|
|||||||
)) {
|
)) {
|
||||||
const next = colorToRgba(hsv.toColor());
|
const next = colorToRgba(hsv.toColor());
|
||||||
if (is_fill) {
|
if (is_fill) {
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .fill_rgba = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .fill_rgba = next } }) catch {};
|
||||||
} else {
|
} else {
|
||||||
obj.setProperty(canvas.document.allocator, .{ .data = .{ .stroke_rgba = next } }) catch {};
|
obj.setProperty(canvas.allocator, .{ .data = .{ .stroke_rgba = next } }) catch {};
|
||||||
}
|
}
|
||||||
canvas.requestRedraw();
|
canvas.requestRedraw();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ fn objectTree(ctx: *WindowContext) void {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
.delete => |obj_id| {
|
.delete => |obj_id| {
|
||||||
_ = doc.removeObjectById(obj_id);
|
_ = doc.removeObjectById(ctx.allocator, obj_id);
|
||||||
if (open_doc.selected_object_id == obj_id)
|
if (open_doc.selected_object_id == obj_id)
|
||||||
open_doc.selected_object_id = null;
|
open_doc.selected_object_id = null;
|
||||||
open_doc.canvas.requestRedraw();
|
open_doc.canvas.requestRedraw();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const dvui = @import("dvui");
|
const dvui = @import("dvui");
|
||||||
const WindowContext = @import("../WindowContext.zig");
|
const WindowContext = @import("../WindowContext.zig");
|
||||||
const document_json = @import("../persistence/document_json.zig");
|
const Document = @import("../models/Document.zig");
|
||||||
|
const json_io = @import("../persistence/json_io.zig");
|
||||||
|
|
||||||
pub fn menuBar(ctx: *WindowContext) void {
|
pub fn menuBar(ctx: *WindowContext) void {
|
||||||
var m = dvui.menu(@src(), .horizontal, .{ .background = true, .expand = .horizontal });
|
var m = dvui.menu(@src(), .horizontal, .{ .background = true, .expand = .horizontal });
|
||||||
@@ -35,7 +36,7 @@ fn openDocumentDialog(ctx: *WindowContext) void {
|
|||||||
defer ctx.allocator.free(path_z);
|
defer ctx.allocator.free(path_z);
|
||||||
|
|
||||||
const path = path_z[0..path_z.len];
|
const path = path_z[0..path_z.len];
|
||||||
const doc = document_json.loadFromFile(ctx.allocator, path) catch |err| {
|
const doc = json_io.loadFromFile(Document, ctx.allocator, path) catch |err| {
|
||||||
std.debug.print("Open file error: {}\n", .{err});
|
std.debug.print("Open file error: {}\n", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -58,7 +59,7 @@ fn saveAsDialog(ctx: *WindowContext) void {
|
|||||||
defer ctx.allocator.free(path_z);
|
defer ctx.allocator.free(path_z);
|
||||||
|
|
||||||
const path_raw = path_z[0..path_z.len];
|
const path_raw = path_z[0..path_z.len];
|
||||||
document_json.saveToFile(&open_doc.document, path_raw) catch |err| {
|
json_io.saveToFile(Document, &open_doc.document, path_raw) catch |err| {
|
||||||
std.debug.print("Save file error: {}\n", .{err});
|
std.debug.print("Save file error: {}\n", .{err});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user