Skip to content

Commit d48df24

Browse files
committed
Add a streaming API for more flexible usage
This change is completely backward compatible, but also adds the possibility for users to handle each entry type in a custom way, building up and storing only the data needed into whatever form is best for them, as well as the parsing code only using constant space and taking a Reader. This should be far more flexible than the existing API.
1 parent f7967f0 commit d48df24

File tree

2 files changed

+207
-124
lines changed

2 files changed

+207
-124
lines changed

src/main.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const obj = @import("obj.zig");
44
const mtl = @import("mtl.zig");
55

66
pub const parseObj = obj.parse;
7+
pub const parseObjCustom = obj.parseCustom;
78
pub const ObjData = obj.ObjData;
89
pub const Mesh = obj.Mesh;
910

src/obj.zig

Lines changed: 206 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const std = @import("std");
2-
const tokenize = std.mem.tokenize;
3-
const split = std.mem.split;
4-
const ArrayList = std.ArrayList;
2+
const tokenizeAny = std.mem.tokenizeAny;
3+
const splitAny = std.mem.splitAny;
4+
const ArrayListUnmanaged = std.ArrayListUnmanaged;
55
const Allocator = std.mem.Allocator;
66
const assert = std.debug.assert;
77
const parseFloat = std.fmt.parseFloat;
@@ -17,14 +17,149 @@ pub const ObjData = struct {
1717
meshes: []const Mesh,
1818

1919
pub fn deinit(self: *@This(), allocator: Allocator) void {
20+
for (self.material_libs) |mlib| allocator.free(mlib);
2021
allocator.free(self.material_libs);
22+
2123
allocator.free(self.vertices);
2224
allocator.free(self.tex_coords);
2325
allocator.free(self.normals);
2426

2527
for (self.meshes) |mesh| mesh.deinit(allocator);
2628
allocator.free(self.meshes);
2729
}
30+
31+
const Builder = struct {
32+
allocator: std.mem.Allocator,
33+
material_libs: ArrayListUnmanaged([]const u8) = .{},
34+
vertices: ArrayListUnmanaged(f32) = .{},
35+
tex_coords: ArrayListUnmanaged(f32) = .{},
36+
normals: ArrayListUnmanaged(f32) = .{},
37+
meshes: ArrayListUnmanaged(Mesh) = .{},
38+
39+
// current mesh
40+
name: ?[]const u8 = null,
41+
num_verts: ArrayListUnmanaged(u32) = .{},
42+
indices: ArrayListUnmanaged(Mesh.Index) = .{},
43+
index_i: u32 = 0,
44+
45+
// current mesh material
46+
current_material: ?MeshMaterial = null,
47+
mesh_materials: ArrayListUnmanaged(MeshMaterial) = .{},
48+
num_processed_verts: usize = 0,
49+
50+
fn onError(self: *Builder) void {
51+
for (self.material_libs.items) |mlib| self.allocator.free(mlib);
52+
for (self.meshes.items) |mesh| mesh.deinit(self.allocator);
53+
self.material_libs.deinit(self.allocator);
54+
self.vertices.deinit(self.allocator);
55+
self.tex_coords.deinit(self.allocator);
56+
self.normals.deinit(self.allocator);
57+
self.meshes.deinit(self.allocator);
58+
if (self.name) |n| self.allocator.free(n);
59+
self.num_verts.deinit(self.allocator);
60+
self.indices.deinit(self.allocator);
61+
if (self.current_material) |mat| self.allocator.free(mat.material);
62+
self.mesh_materials.deinit(self.allocator);
63+
}
64+
65+
fn finish(self: *Builder) !ObjData {
66+
defer self.* = undefined;
67+
try self.use_material(null); // add last material if any
68+
try self.object(null); // add last mesh (as long as it is not empty)
69+
return ObjData{
70+
.material_libs = try self.material_libs.toOwnedSlice(self.allocator),
71+
.vertices = try self.vertices.toOwnedSlice(self.allocator),
72+
.tex_coords = try self.tex_coords.toOwnedSlice(self.allocator),
73+
.normals = try self.normals.toOwnedSlice(self.allocator),
74+
.meshes = try self.meshes.toOwnedSlice(self.allocator),
75+
};
76+
}
77+
78+
fn vertex(self: *Builder, x: f32, y: f32, z: f32, w: ?f32) !void {
79+
_ = w;
80+
try self.vertices.appendSlice(self.allocator, &.{ x, y, z });
81+
}
82+
83+
fn tex_coord(self: *Builder, u: f32, v: ?f32, w: ?f32) !void {
84+
_ = w;
85+
try self.tex_coords.appendSlice(self.allocator, &.{ u, v.? });
86+
}
87+
88+
fn normal(self: *Builder, i: f32, j: f32, k: f32) !void {
89+
try self.normals.appendSlice(self.allocator, &.{ i, j, k });
90+
}
91+
92+
fn face_index(self: *Builder, vert: u32, tex: ?u32, norm: ?u32) !void {
93+
try self.indices.append(
94+
self.allocator,
95+
.{ .vertex = vert, .tex_coord = tex, .normal = norm },
96+
);
97+
self.index_i += 1;
98+
}
99+
100+
fn face_end(self: *Builder) !void {
101+
try self.num_verts.append(self.allocator, self.index_i);
102+
self.num_processed_verts += self.index_i;
103+
self.index_i = 0;
104+
}
105+
106+
fn object(self: *Builder, name: ?[]const u8) !void {
107+
if (0 < self.num_verts.items.len) {
108+
if (self.current_material) |*m| {
109+
m.end_index = self.num_processed_verts;
110+
try self.mesh_materials.append(self.allocator, m.*);
111+
}
112+
try self.meshes.append(self.allocator, .{
113+
.name = self.name,
114+
.num_vertices = try self.num_verts.toOwnedSlice(self.allocator),
115+
.indices = try self.indices.toOwnedSlice(self.allocator),
116+
.materials = try self.mesh_materials.toOwnedSlice(self.allocator),
117+
});
118+
}
119+
if (name) |n| {
120+
self.name = try self.allocator.dupe(u8, n);
121+
self.num_verts = .{};
122+
self.indices = .{};
123+
self.num_processed_verts = 0;
124+
self.current_material = null;
125+
}
126+
}
127+
128+
fn use_material(self: *Builder, name: ?[]const u8) !void {
129+
if (self.current_material) |*m| {
130+
m.end_index = self.num_processed_verts;
131+
try self.mesh_materials.append(self.allocator, m.*);
132+
}
133+
if (name) |n| {
134+
self.current_material = MeshMaterial{
135+
.material = try self.allocator.dupe(u8, n),
136+
.start_index = self.num_processed_verts,
137+
.end_index = self.num_processed_verts + 1,
138+
};
139+
} else {
140+
self.current_material = null;
141+
}
142+
}
143+
144+
fn material_lib(self: *Builder, name: []const u8) !void {
145+
try self.material_libs.append(
146+
self.allocator,
147+
try self.allocator.dupe(u8, name),
148+
);
149+
}
150+
151+
fn vertexCount(self: Builder) usize {
152+
return self.vertices.items.len;
153+
}
154+
155+
fn texCoordCount(self: Builder) usize {
156+
return self.tex_coords.items.len;
157+
}
158+
159+
fn normalCount(self: Builder) usize {
160+
return self.normals.items.len;
161+
}
162+
};
28163
};
29164

30165
fn compareOpt(a: ?u32, b: ?u32) bool {
@@ -117,146 +252,93 @@ const DefType = enum {
117252
};
118253

119254
pub fn parse(allocator: Allocator, data: []const u8) !ObjData {
120-
var material_libs = ArrayList([]const u8).init(allocator);
121-
errdefer material_libs.deinit();
122-
123-
var vertices = ArrayList(f32).init(allocator);
124-
errdefer vertices.deinit();
125-
126-
var tex_coords = ArrayList(f32).init(allocator);
127-
errdefer tex_coords.deinit();
128-
129-
var normals = ArrayList(f32).init(allocator);
130-
errdefer normals.deinit();
131-
132-
var meshes = ArrayList(Mesh).init(allocator);
133-
errdefer meshes.deinit();
134-
135-
// current mesh
136-
var name: ?[]const u8 = null;
137-
var num_verts = ArrayList(u32).init(allocator);
138-
errdefer num_verts.deinit();
139-
var indices = ArrayList(Mesh.Index).init(allocator);
140-
errdefer indices.deinit();
141-
142-
// current mesh material
143-
var current_material: ?MeshMaterial = null;
144-
var mesh_materials = ArrayList(MeshMaterial).init(allocator);
145-
errdefer mesh_materials.deinit();
146-
var num_processed_verts: usize = 0;
147-
148-
var lines = tokenize(u8, data, "\r\n");
149-
while (lines.next()) |line| {
150-
var iter = tokenize(u8, line, " ");
151-
const def_type = try parseType(iter.next().?);
255+
var b = ObjData.Builder{ .allocator = allocator };
256+
errdefer b.onError();
257+
var fbs = std.io.fixedBufferStream(data);
258+
return try parseCustom(ObjData, &b, fbs.reader());
259+
}
260+
261+
pub fn parseCustom(comptime T: type, b: *T.Builder, rdr: anytype) !T {
262+
var buffer: [128]u8 = undefined;
263+
var lines = lineIterator(rdr, &buffer);
264+
while (try lines.next()) |line| {
265+
var iter = tokenizeAny(u8, line, " ");
266+
const def_type =
267+
if (iter.next()) |tok| try parseType(tok) else continue;
152268
switch (def_type) {
153-
.vertex => {
154-
try vertices.append(try parseFloat(f32, iter.next().?));
155-
try vertices.append(try parseFloat(f32, iter.next().?));
156-
try vertices.append(try parseFloat(f32, iter.next().?));
157-
},
158-
.tex_coord => {
159-
try tex_coords.append(try parseFloat(f32, iter.next().?));
160-
try tex_coords.append(try parseFloat(f32, iter.next().?));
161-
},
162-
.normal => {
163-
try normals.append(try parseFloat(f32, iter.next().?));
164-
try normals.append(try parseFloat(f32, iter.next().?));
165-
try normals.append(try parseFloat(f32, iter.next().?));
166-
},
269+
.vertex => try b.vertex(
270+
try parseFloat(f32, iter.next().?),
271+
try parseFloat(f32, iter.next().?),
272+
try parseFloat(f32, iter.next().?),
273+
if (iter.next()) |w| (try parseFloat(f32, w)) else null,
274+
),
275+
.tex_coord => try b.tex_coord(
276+
try parseFloat(f32, iter.next().?),
277+
if (iter.next()) |v| (try parseFloat(f32, v)) else null,
278+
if (iter.next()) |w| (try parseFloat(f32, w)) else null,
279+
),
280+
.normal => try b.normal(
281+
try parseFloat(f32, iter.next().?),
282+
try parseFloat(f32, iter.next().?),
283+
try parseFloat(f32, iter.next().?),
284+
),
167285
.face => {
168-
var i: u32 = 0;
169286
while (iter.next()) |entry| {
170-
var entry_iter = split(u8, entry, "/");
171-
// TODO support x//y and similar
172-
// NOTE obj is one-indexed - let's make it zero-indexed
173-
try indices.append(.{
174-
.vertex = if (entry_iter.next()) |e| (try parseOptionalIndex(e, vertices.items)) else null,
175-
.tex_coord = if (entry_iter.next()) |e| (try parseOptionalIndex(e, tex_coords.items)) else null,
176-
.normal = if (entry_iter.next()) |e| (try parseOptionalIndex(e, normals.items)) else null,
177-
});
178-
179-
i += 1;
180-
}
181-
try num_verts.append(i);
182-
num_processed_verts += i;
183-
},
184-
.object => {
185-
if (num_verts.items.len > 0) {
186-
// add last material if any
187-
if (current_material) |*m| {
188-
m.end_index = num_processed_verts;
189-
try mesh_materials.append(m.*);
190-
}
191-
192-
try meshes.append(.{
193-
.name = if (name) |n| try allocator.dupe(u8, n) else null,
194-
.num_vertices = try num_verts.toOwnedSlice(),
195-
.indices = try indices.toOwnedSlice(),
196-
.materials = try mesh_materials.toOwnedSlice(),
197-
});
287+
var entry_iter = splitAny(u8, entry, "/");
288+
try b.face_index(
289+
(try parseOptionalIndex(entry_iter.next().?, b.vertexCount())).?,
290+
if (entry_iter.next()) |e| (try parseOptionalIndex(e, b.texCoordCount())) else null,
291+
if (entry_iter.next()) |e| (try parseOptionalIndex(e, b.normalCount())) else null,
292+
);
198293
}
199-
200-
name = iter.next().?;
201-
num_verts = ArrayList(u32).init(allocator);
202-
errdefer num_verts.deinit();
203-
indices = ArrayList(Mesh.Index).init(allocator);
204-
errdefer indices.deinit();
205-
num_processed_verts = 0;
206-
current_material = null;
207-
},
208-
.use_material => {
209-
if (current_material) |*m| {
210-
m.end_index = num_processed_verts;
211-
try mesh_materials.append(m.*);
212-
}
213-
const material = try allocator.dupe(u8, iter.next().?);
214-
current_material = MeshMaterial{
215-
.material = material,
216-
.start_index = num_processed_verts,
217-
.end_index = num_processed_verts + 1,
218-
};
219-
},
220-
.material_lib => {
221-
try material_libs.append(iter.next().?);
294+
try b.face_end();
222295
},
296+
.object => try b.object(iter.next().?),
297+
.use_material => try b.use_material(iter.next().?),
298+
.material_lib => while (iter.next()) |lib| try b.material_lib(lib),
223299
else => {},
224300
}
225301
}
226302

227-
// add last material if any
228-
if (current_material) |*m| {
229-
m.end_index = num_processed_verts;
230-
try mesh_materials.append(m.*);
231-
}
232-
233-
// add last mesh (as long as it is not empty)
234-
if (num_verts.items.len > 0) {
235-
try meshes.append(Mesh{
236-
.name = if (name) |n| try allocator.dupe(u8, n) else null,
237-
.num_vertices = try num_verts.toOwnedSlice(),
238-
.indices = try indices.toOwnedSlice(),
239-
.materials = try mesh_materials.toOwnedSlice(),
240-
});
241-
}
303+
return try b.finish();
304+
}
242305

243-
return ObjData{
244-
.material_libs = try material_libs.toOwnedSlice(),
245-
.vertices = try vertices.toOwnedSlice(),
246-
.tex_coords = try tex_coords.toOwnedSlice(),
247-
.normals = try normals.toOwnedSlice(),
248-
.meshes = try meshes.toOwnedSlice(),
306+
fn LineIterator(comptime Reader: type) type {
307+
return struct {
308+
buffer: []u8,
309+
reader: Reader,
310+
311+
fn next(self: *@This()) !?[]const u8 {
312+
var fbs = std.io.fixedBufferStream(self.buffer);
313+
self.reader.streamUntilDelimiter(
314+
fbs.writer(),
315+
'\n',
316+
fbs.buffer.len,
317+
) catch |err| switch (err) {
318+
error.EndOfStream => if (fbs.getWritten().len == 0) return null,
319+
else => |e| return e,
320+
};
321+
var line = fbs.getWritten();
322+
if (0 < line.len and line[line.len - 1] == '\r')
323+
line = line[0 .. line.len - 1];
324+
return line;
325+
}
249326
};
250327
}
251328

252-
fn parseOptionalIndex(v: []const u8, indices: []f32) !?u32 {
329+
fn lineIterator(rdr: anytype, buffer: []u8) LineIterator(@TypeOf(rdr)) {
330+
return .{ .buffer = buffer, .reader = rdr };
331+
}
332+
333+
fn parseOptionalIndex(v: []const u8, n_items: usize) !?u32 {
253334
if (std.mem.eql(u8, v, "")) return null;
254335
const i = try parseInt(i32, v, 10);
255336

256337
if (i < 0) {
257338
// index is relative to end of indices list, -1 meaning the last element
258-
return @as(u32, @intCast(@as(i32, @intCast(indices.len)) + i));
339+
return @as(u32, @intCast(@as(i32, @intCast(n_items)) + i));
259340
} else {
341+
// obj is one-indexed - let's make it zero-indexed
260342
return @as(u32, @intCast(i)) - 1;
261343
}
262344
}

0 commit comments

Comments
 (0)