const std = @import("std");
const xml = @import("./xml.zig");
const Position = @import("./position.zig");
const Color = @import("./color.zig");
const Object = @This();
pub const Shape = union (Type) {
pub const Type = enum {
rectangle,
point,
ellipse,
polygon,
tile,
// TODO: template
text
};
pub const Rectangle = struct {
x: f32,
y: f32,
width: f32,
height: f32,
};
pub const Tile = struct {
x: f32,
y: f32,
width: f32,
height: f32,
gid: u32
};
pub const Ellipse = struct {
x: f32,
y: f32,
width: f32,
height: f32,
};
pub const Polygon = struct {
x: f32,
y: f32,
points: []const Position
};
pub const Text = struct {
pub const Font = struct {
family: []const u8,
pixel_size: f32,
bold: bool,
italic: bool,
underline: bool,
strikeout: bool,
kerning: bool
};
pub const HorizontalAlign = enum {
left,
center,
right,
justify,
const map: std.StaticStringMap(HorizontalAlign) = .initComptime(.{
.{ "left", .left },
.{ "center", .center },
.{ "right", .right },
.{ "justify", .justify },
});
};
pub const VerticalAlign = enum {
top,
center,
bottom,
const map: std.StaticStringMap(VerticalAlign) = .initComptime(.{
.{ "top", .top },
.{ "center", .center },
.{ "bottom", .bottom },
});
};
x: f32,
y: f32,
width: f32,
height: f32,
word_wrap: bool,
color: Color,
font: Font,
horizontal_align: HorizontalAlign,
vertical_align: VerticalAlign,
content: []const u8
};
pub const Point = struct {
x: f32,
y: f32,
};
rectangle: Rectangle,
point: Point,
ellipse: Ellipse,
polygon: Polygon,
tile: Tile,
text: Text
};
id: u32,
name: []const u8,
class: []const u8,
rotation: f32, // TODO: maybe this field should be moved to Shape struct
visible: bool,
shape: Shape,
pub fn initFromXml(
arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer,
) !Object {
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin("object");
const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
const name = try attrs.getDupe(arena, "name") orelse "";
const class = try attrs.getDupe(arena, "type") orelse "";
const x = try attrs.getNumber(f32, "x") orelse 0;
const y = try attrs.getNumber(f32, "y") orelse 0;
const width = try attrs.getNumber(f32, "width") orelse 0;
const height = try attrs.getNumber(f32, "height") orelse 0;
const rotation = try attrs.getNumber(f32, "rotation") orelse 0;
const visible = try attrs.getBool("visible", "1", "0") orelse true;
var shape: ?Shape = null;
while (try iter.next()) |node| {
if (shape == null) {
if (node.isTag("point")) {
shape = .{
.point = Shape.Point{
.x = x,
.y = y,
}
};
} else if (node.isTag("ellipse")) {
shape = .{
.ellipse = Shape.Ellipse{
.x = x,
.y = y,
.width = width,
.height = height
}
};
} else if (node.isTag("text")) {
const HorizontalShape = Shape.Text.HorizontalAlign;
const VerticalAlign = Shape.Text.VerticalAlign;
const text_attrs = node.tag.attributes;
const word_wrap = try text_attrs.getBool("wrap", "1", "0") orelse false;
const color = try text_attrs.getColor("color", true) orelse Color.black;
const horizontal_align = try text_attrs.getEnum(HorizontalShape, "halign", HorizontalShape.map) orelse .left;
const vertical_align = try text_attrs.getEnum(VerticalAlign, "valign", VerticalAlign.map) orelse .top;
const bold = try text_attrs.getBool("bold", "1", "0") orelse false;
const italic = try text_attrs.getBool("italic", "1", "0") orelse false;
const strikeout = try text_attrs.getBool("strikeout", "1", "0") orelse false;
const underline = try text_attrs.getBool("underline", "1", "0") orelse false;
const kerning = try text_attrs.getBool("kerning", "1", "0") orelse true;
const pixel_size = try text_attrs.getNumber(f32, "pixelsize") orelse 16;
const font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif";
_ = try lexer.nextExpectStartTag("text");
var content: []const u8 = "";
const content_value = try lexer.peek();
if (content_value != null and content_value.? == .text) {
content = try arena.dupe(u8, content_value.?.text);
}
try lexer.skipUntilMatchingEndTag("text");
shape = .{
.text = Shape.Text{
.x = x,
.y = y,
.width = width,
.height = height,
.word_wrap = word_wrap,
.color = color,
.horizontal_align = horizontal_align,
.vertical_align = vertical_align,
.content = try arena.dupe(u8, content),
.font = .{
.bold = bold,
.italic = italic,
.strikeout = strikeout,
.underline = underline,
.pixel_size = pixel_size,
.kerning = kerning,
.family = font_family,
}
}
};
continue;
} else if (node.isTag("polygon")) {
const points_str = node.tag.attributes.get("points") orelse "";
var points: std.ArrayList(Position) = .empty;
var point_iter = std.mem.splitScalar(u8, points_str, ' ');
while (point_iter.next()) |point_str| {
const point = try Position.parseCommaDelimited(point_str);
try points.append(scratch.allocator(), point);
}
shape = .{
.polygon = Shape.Polygon{
.x = x,
.y = y,
.points = try arena.dupe(Position, points.items)
}
};
}
}
try iter.skip();
}
if (shape == null) {
if (try attrs.getNumber(u32, "gid")) |gid| {
shape = .{
.tile = Shape.Tile{
.x = x,
.y = y,
.width = width,
.height = height,
.gid = gid
}
};
} else {
shape = .{
.rectangle = Shape.Rectangle{
.x = x,
.y = y,
.width = width,
.height = height
}
};
}
}
try iter.finish("object");
return Object{
.id = id,
.name = name,
.class = class,
.rotation = rotation,
.visible = visible,
.shape = shape orelse return error.UnknownShapeType
};
}
fn expectParsedEquals(expected: Object, body: []const u8) !void {
const allocator = std.testing.allocator;
var ctx: xml.Lexer.TestingContext = undefined;
ctx.init(allocator, body);
defer ctx.deinit();
var scratch = std.heap.ArenaAllocator.init(allocator);
defer scratch.deinit();
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const parsed = try initFromXml(arena.allocator(), &scratch, &ctx.lexer);
try std.testing.expectEqualDeep(expected, parsed);
}
test Object {
try expectParsedEquals(
Object{
.id = 10,
.name = "rectangle",
.class = "object class",
.rotation = 0,
.visible = true,
.shape = .{
.rectangle = .{
.x = 12.34,
.y = 56.78,
.width = 31.5,
.height = 20.25,
}
}
},
\\
);
try expectParsedEquals(
Object{
.id = 3,
.name = "point",
.class = "foo",
.rotation = 0,
.visible = true,
.shape = .{
.point = .{
.x = 77.125,
.y = 99.875
}
}
},
\\
);
try expectParsedEquals(
Object{
.id = 4,
.name = "ellipse",
.class = "",
.rotation = 0,
.visible = true,
.shape = .{
.ellipse = .{
.x = 64.25,
.y = 108.25,
.width = 22.375,
.height = 15.375
}
}
},
\\
);
try expectParsedEquals(
Object{
.id = 5,
.name = "",
.class = "",
.rotation = 0,
.visible = true,
.shape = .{
.polygon = .{
.x = 40.125,
.y = 96.25,
.points = &[_]Position{
.{ .x = 0, .y = 0 },
.{ .x = 13.25, .y = -4.25 },
.{ .x = 10.125, .y = 18.625 },
.{ .x = 2.25, .y = 17.375 },
.{ .x = -0.125, .y = 25.75 },
.{ .x = -3.875, .y = 20.75 },
}
}
}
},
\\
);
try expectParsedEquals(
Object{
.id = 2,
.name = "tile",
.class = "",
.rotation = 0,
.visible = true,
.shape = .{
.tile = .{
.x = 60.125,
.y = 103.5,
.width = 8,
.height = 8,
.gid = 35
}
}
},
\\
);
try expectParsedEquals(
Object{
.id = 6,
.name = "text",
.class = "",
.rotation = 0,
.visible = true,
.shape = .{
.text = .{
.x = 64.3906,
.y = 92.8594,
.width = 87.7188,
.height = 21.7813,
.content = "Hello World",
.word_wrap = true,
.color = .black,
.horizontal_align = .center,
.vertical_align = .top,
.font = .{
.family = "sans-serif",
.pixel_size = 16,
.bold = false,
.italic = false,
.underline = false,
.strikeout = false,
.kerning = true
},
}
}
},
\\
);
}