1 ///
2 module nwnlibd.wavefrontobj;
3 
4 import std.typecons;
5 import std.string;
6 import std.algorithm;
7 import std.format;
8 import std.conv;
9 import std.array;
10 import std.exception;
11 
12 import gfm.math.vector;
13 
14 ///
15 class WavefrontObj {
16 
17 	string[] mtllibs;
18 
19 	///
20 	static struct WFVertex{
21 		this(in vec3f position, Nullable!vec3f color = Nullable!vec3f()){
22 			this.position.xyz = position;
23 			this.position.w = 1.0;
24 			this.color = color;
25 		}
26 		///
27 		vec4f position;
28 		///
29 		Nullable!vec3f color;
30 	}
31 	///
32 	WFVertex[] vertices;
33 	///
34 	vec2f[] textCoords;
35 	///
36 	vec3f[] normals;
37 
38 	///
39 	static struct WFFace {
40 		///
41 		size_t[] vertices;
42 		///
43 		Nullable!(size_t[]) textCoords;
44 		///
45 		Nullable!(size_t[]) normals;
46 		///
47 		string material;
48 	}
49 	///
50 	static struct WFLine {
51 		///
52 		size_t[] vertices;
53 	}
54 	///
55 	static struct WFGroup {
56 		///
57 		WFFace[] faces;
58 		///
59 		WFLine[] lines;
60 	}
61 	///
62 	static struct WFObject {
63 		///
64 		WFGroup[string] groups;
65 		///
66 		alias groups this;
67 	}
68 	///
69 	WFObject[string] objects;
70 
71 	///
72 	this(){}
73 
74 	///
75 	this(in string data){
76 		import std.uni : isWhite;
77 
78 		string currentMtl;
79 
80 		string object, group;
81 		foreach(ref line ; data.lineSplitter.map!strip.filter!(a => a[0] != '#')){
82 			auto ws = line.countUntil!isWhite;
83 			string type = line[0 .. ws];
84 			line = line[ws .. $].strip;
85 
86 			switch(type){
87 				case "mtllib":
88 					mtllibs ~= line;
89 					break;
90 				case "o":
91 					object = line;
92 					group = null;
93 					objects[object] = WFObject();
94 					break;
95 				case "g":
96 					group = line;
97 					objects[object][group] = WFGroup();
98 					break;
99 				case "v":
100 					auto values = line
101 						.split!isWhite
102 						.filter!(a => a.length > 0)
103 						.array;
104 
105 					WFVertex vtx;
106 					if(values.length >= 3)
107 						vtx.position.v[0 .. 3] = values[0 .. 3].map!(a => a.to!float).array;
108 
109 					if(values.length >= 4)
110 						vtx.position.v[3] = values[3].to!float;
111 					else
112 						vtx.position[3] = 1.0;
113 
114 					if(values.length >= 7)
115 						vtx.color = vec3f(values[4 .. 7].map!(a => a.to!float).array);
116 
117 					vertices ~= vtx;
118 					break;
119 				case "vt":
120 					textCoords ~= vec2f(line
121 						.split!isWhite
122 						.filter!(a => a.length > 0)
123 						.map!(a => a.to!float)
124 						.array[0 .. 2]);
125 					break;
126 				case "vn":
127 					normals ~= vec3f(line
128 						.split!isWhite
129 						.filter!(a => a.length > 0)
130 						.map!(a => a.to!float)
131 						.array[0 .. 3]);
132 					break;
133 				case "f":
134 					if(group !in objects[object])
135 						objects[object][group] = WFGroup();
136 
137 					auto indices = line
138 						.split!isWhite
139 						.filter!(a => a.length > 0)
140 						.map!(a => a.split("/"))
141 						.array;
142 
143 					WFFace face;
144 					face.vertices = indices.map!(a => a[0].to!size_t).array;
145 					if(indices[0].length >= 2 && indices[0][1].length > 0)
146 						face.textCoords = indices.map!(a => a[1].to!size_t).array;
147 					if(indices[0].length >= 3 && indices[0][2].length > 0)
148 						face.normals = indices.map!(a => a[2].to!size_t).array;
149 					face.material = currentMtl;
150 
151 					objects[object][group].faces ~= face;
152 					break;
153 
154 				case "l":
155 					if(group !in objects[object])
156 						objects[object][group] = WFGroup();
157 
158 					objects[object][group].lines ~= WFLine(
159 						line
160 							.split!isWhite
161 							.filter!(a => a.length > 0)
162 							.map!(a => a.to!size_t)
163 							.array
164 					);
165 					break;
166 
167 				case "usemtl":
168 					currentMtl = line;
169 					break;
170 
171 				default: break;
172 			}
173 		}
174 	}
175 	///
176 	string serialize() const {
177 		string objData;
178 
179 		foreach(ref lib ; mtllibs){
180 			objData ~= format!"mtllib %s\n"(lib);
181 		}
182 
183 		foreach(ref v ; vertices){
184 			if(v.color.isNull)
185 				objData ~= format!"v %(%f %)\n"(v.position.v);
186 			else
187 				objData ~= format!"v %(%f %) %(%f %)\n"(v.position.v, v.color.get.v);
188 		}
189 
190 		foreach(ref vt ; textCoords)
191 			objData ~= format!"vt %(%f %)\n"(vt.v);
192 
193 		foreach(ref vn ; normals)
194 			objData ~= format!"vn %(%f %)\n"(vn.v);
195 
196 		string currentMtl;
197 		foreach(ref objName ; objects.keys().sort()){
198 			objData ~= format!"o %s\n"(objName);
199 			foreach(ref groupName ; objects[objName].groups.keys().sort()){
200 				if(groupName != null)
201 					objData ~= format!"g %s\n"(objName);
202 
203 				foreach(ref f ; objects[objName].groups[groupName].faces){
204 
205 					if(f.material != currentMtl){
206 						objData ~= format!"usemtl %s\n"(f.material);
207 						currentMtl = f.material;
208 					}
209 
210 					string[] values;
211 					values.length = f.vertices.length;
212 
213 					foreach(i ; 0 .. f.vertices.length){
214 						values[i] ~= f.vertices[i].to!string;
215 						if(!f.textCoords.isNull || !f.normals.isNull)
216 							values[i] ~= "/";
217 						values[i] ~= f.textCoords.isNull? null : f.textCoords.get[i].to!string;
218 						if(!f.normals.isNull)
219 							values[i] ~= "/";
220 						values[i] ~= f.normals.isNull? null : f.normals.get[i].to!string;
221 					}
222 
223 					objData ~= format!"f %-(%s %)\n"(values);
224 				}
225 
226 				foreach(ref l ; objects[objName].groups[groupName].lines){
227 					objData ~= format!"l %(%d %)\n"(l.vertices);
228 				}
229 			}
230 		}
231 
232 		return objData;
233 	}
234 
235 	void validate() const {
236 		foreach(vi, ref v ; vertices){
237 			if(!v.color.isNull){
238 				foreach(ci, c ; v.color.get.v)
239 					enforce(0 <= c && c <= 1.0,
240 						format!"vertices[%d].color[%d]: value %f is invalid"(vi, ci, c));
241 			}
242 		}
243 
244 		foreach(oname, ref o ; objects){
245 			foreach(gname, ref g ; o.groups){
246 				foreach(fi, ref f ; g.faces){
247 					foreach(vi, ref v ; f.vertices)
248 						enforce(0 < v && v <= vertices.length,
249 							format!"objects[%s][%s].faces[%d].vertices[%d] %d is out of bounds"(oname, gname, fi, vi, v));
250 					if(!f.textCoords.isNull)
251 						foreach(vi, ref v ; f.textCoords.get())
252 							enforce(0 < v && v <= textCoords.length,
253 								format!"objects[%s][%s].faces[%d].textCoords[%d] %d is out of bounds"(oname, gname, fi, vi, v));
254 					if(!f.normals.isNull)
255 						foreach(vi, ref v ; f.normals.get())
256 							enforce(0 < v && v <= normals.length,
257 								format!"objects[%s][%s].faces[%d].normals[%d] %d is out of bounds"(oname, gname, fi, vi, v));
258 				}
259 				foreach(li, ref l ; g.lines){
260 					foreach(vi, ref v ; l.vertices)
261 						enforce(0 < v && v <= vertices.length,
262 							format!"objects[%s][%s].lines[%d].vertices[%d] %d is out of bounds"(oname, gname, li, vi, v));
263 				}
264 			}
265 		}
266 	}
267 }