422 lines
12 KiB
Zig
422 lines
12 KiB
Zig
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,
|
|
}
|
|
}
|
|
},
|
|
\\ <object id="10" name="rectangle" type="object class" 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
|
|
}
|
|
}
|
|
},
|
|
\\ <object id="3" name="point" type="foo" x="77.125" y="99.875">
|
|
\\ <point/>
|
|
\\ </object>
|
|
);
|
|
|
|
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
|
|
}
|
|
}
|
|
},
|
|
\\ <object id="4" name="ellipse" x="64.25" y="108.25" width="22.375" height="15.375">
|
|
\\ <ellipse/>
|
|
\\ </object>
|
|
);
|
|
|
|
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 },
|
|
}
|
|
}
|
|
}
|
|
},
|
|
\\ <object id="5" x="40.125" y="96.25">
|
|
\\ <polygon points="0,0 13.25,-4.25 10.125,18.625 2.25,17.375 -0.125,25.75 -3.875,20.75"/>
|
|
\\ </object>
|
|
);
|
|
|
|
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
|
|
}
|
|
}
|
|
},
|
|
\\ <object id="2" name="tile" gid="35" x="60.125" y="103.5" width="8" height="8"/>
|
|
);
|
|
|
|
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
|
|
},
|
|
}
|
|
}
|
|
},
|
|
\\ <object id="6" name="text" x="64.3906" y="92.8594" width="87.7188" height="21.7813">
|
|
\\ <text wrap="1" halign="center"> Hello World </text>
|
|
\\ </object>
|
|
);
|
|
}
|