source: trunk/src/easymesh/csgbsp.cpp @ 2227

Last change on this file since 2227 was 2226, checked in by touky, 7 years ago

Added CSG v0.001a : not optimized, too much operation makes it slow. triangle count is not optimized.
Useage : "...[.... csg*]" equals "current mesh -CSG- the mesh in the braces".
Keywords : CsgUnion<csgu>, CsgSubstract<csgs>, CsgAnd<csga>, CsgXor<csgx>
TODO : cleanup useless code.
TODO : Some bugs are still present, some face that disappear should not.
TODO : Correct epsilon useage (see geometry files).
TODO : Coplanar face are not handled -at all-.
TODO : Vertex count goes through the roof in case of a Xor.

File size: 23.0 KB
Line 
1//
2// Lol Engine
3//
4// Copyright: (c) 2010-2013 Sam Hocevar <sam@hocevar.net>
5//            (c) 2010-2013 Benjamin "Touky" Huet <huet.benjamin@gmail.com>
6//   This program is free software; you can redistribute it and/or
7//   modify it under the terms of the Do What The Fuck You Want To
8//   Public License, Version 2, as published by Sam Hocevar. See
9//   http://www.wtfpl.net/ for more details.
10//
11
12//
13// The EasyMesh class
14// ------------------
15//
16
17#if defined HAVE_CONFIG_H
18#   include "config.h"
19#endif
20
21#if defined _XBOX
22#   define _USE_MATH_DEFINES /* for M_PI */
23#   include <xtl.h>
24#   undef near /* Fuck Microsoft */
25#   undef far /* Fuck Microsoft again */
26#elif defined _WIN32
27#   define _USE_MATH_DEFINES /* for M_PI */
28#   define WIN32_LEAN_AND_MEAN
29#   include <windows.h>
30#   undef near /* Fuck Microsoft */
31#   undef far /* Fuck Microsoft again */
32#endif
33
34#include "core.h"
35
36namespace lol
37{
38
39int CsgBsp::AddLeaf(int leaf_type, vec3 origin, vec3 normal, int above_idx)
40{
41    if (leaf_type > 2 && leaf_type < -1)
42        return -1;
43
44    if ((m_tree.Count() == 0 && above_idx == -1) ||
45        (above_idx >= 0 &&
46            above_idx < m_tree.Count() &&
47            leaf_type > LEAF_CURRENT &&
48            leaf_type < LEAF_ABOVE &&
49            m_tree[above_idx].m_leaves[leaf_type] == -1))
50    {
51        if (m_tree.Count() != 0)
52            m_tree[above_idx].m_leaves[leaf_type] = m_tree.Count();
53        m_tree.Push(CsgBspLeaf(origin, normal, above_idx));
54        return m_tree.Count() - 1;
55    }
56
57    return -1;
58}
59
60int CsgBsp::TestPoint(int leaf_idx, vec3 point)
61{
62    if (leaf_idx >= 0 && leaf_idx < m_tree.Count())
63    {
64        vec3 p2o = point - m_tree[leaf_idx].m_origin;
65
66        if (length(p2o) < CSG_EPSILON)
67            return LEAF_CURRENT;
68
69        float p2o_dot = dot(normalize(p2o), m_tree[leaf_idx].m_normal);
70
71        if (p2o_dot > CSG_EPSILON)
72            return LEAF_FRONT;
73        else if (p2o_dot < -CSG_EPSILON)
74            return LEAF_BACK;
75    }
76    return LEAF_CURRENT;
77}
78
79void CsgBsp::AddTriangleToTree(int const &tri_idx, vec3 const &tri_v0, vec3 const &tri_v1, vec3 const &tri_v2)
80{
81    //<Leaf_Id, v0, v1, v2>
82    Array< int, vec3, vec3, vec3 > tri_to_process;
83    //<FW/BW, Leaf_Id, v0, v1, v2, twin_leaf>
84    Array< int, int, vec3, vec3, vec3, int > Leaf_to_add;
85
86    //Tree is empty, so this leaf is the first
87    if (m_tree.Count() == 0)
88    {
89        AddLeaf(LEAF_CURRENT, tri_v0, cross(normalize(tri_v1 - tri_v0), normalize(tri_v2 - tri_v1)), LEAF_CURRENT);
90        m_tree.Last().m_tri_list.Push(tri_idx, tri_v0, tri_v1, tri_v2);
91        return;
92    }
93
94    tri_to_process.Reserve(20);
95    tri_to_process.Push(0, tri_v0, tri_v1, tri_v2);
96
97    while (tri_to_process.Count())
98    {
99        int leaf_idx = tri_to_process.Last().m1;
100        vec3 v[3] = { tri_to_process.Last().m2, tri_to_process.Last().m3, tri_to_process.Last().m4 };
101
102        int res_nb[3] = { 0, 0, 0 };
103        int res_side[3] = { -1, -1, -1 };
104
105        //Check where each point is located
106        for (int i = 0; i < 3; i++)
107        {
108            int result = TestPoint(leaf_idx, v[i]);
109            if (result != LEAF_CURRENT)
110            {
111                res_nb[result]++;
112                res_side[i] = result;
113            }
114        }
115
116        //Points are located on each sides, let's do the mumbo-jumbo
117        if (res_nb[LEAF_BACK] && res_nb[LEAF_FRONT])
118        {
119            //there are two intersections, no more.
120            vec3 isec_v[2] = { vec3(.0f), vec3(.0f) };
121            int isec_i[2] = { 0, 0 };
122            int isec_base = 0;
123            int isec_idx = 0;
124
125            for (int i = 0; i < 3; i++)
126            {
127                vec3 ray = v[(i + 1) % 3] - v[i];
128
129                if (RayIsectPlane(v[i], v[(i + 1) % 3],
130                                    m_tree[leaf_idx].m_origin, m_tree[leaf_idx].m_normal,
131                                    isec_v[isec_idx]))
132                    isec_i[isec_idx++] = i;
133                else
134                    isec_base = i;
135            }
136
137            int v_idx0 = (isec_base == 1)?(1):(0);
138            int v_idx1 = (isec_base == 1)?(0):(1);
139            int leaf_type = res_side[(isec_base + 2) % 3];
140
141            tri_to_process.Pop();
142               
143#if 1
144            if (m_tree[leaf_idx].m_leaves[leaf_type] == LEAF_CURRENT)
145                Leaf_to_add.Push(leaf_type, leaf_idx, v[((isec_base + 2) % 3)], isec_v[v_idx1], isec_v[v_idx0], -1);
146            else
147                tri_to_process.Push(leaf_idx, v[((isec_base + 2) % 3)], isec_v[v_idx1], isec_v[v_idx0]);
148
149            if (m_tree[leaf_idx].m_leaves[1 - leaf_type] == LEAF_CURRENT)
150            {
151                Leaf_to_add.Push(1 - leaf_type, leaf_idx, v[isec_base], v[((isec_base + 1) % 3)], isec_v[v_idx0], -1);
152                Leaf_to_add.Push(1 - leaf_type, leaf_idx, v[isec_base], isec_v[v_idx0], isec_v[v_idx1], Leaf_to_add.Count() - 1);
153            }
154            else
155            {
156                tri_to_process.Push(m_tree[leaf_idx].m_leaves[1 - leaf_type], v[isec_base], v[((isec_base + 1) % 3)], isec_v[v_idx0]);
157                tri_to_process.Push(m_tree[leaf_idx].m_leaves[1 - leaf_type], v[isec_base], isec_v[v_idx0], isec_v[v_idx1]);
158            }
159#else
160            vec3 new_v[9] = { v[((isec_base + 2) % 3)], isec_v[v_idx1],             isec_v[v_idx0],
161                                v[isec_base],             v[((isec_base + 1) % 3)],   isec_v[v_idx0],
162                                v[isec_base],             isec_v[v_idx0],             isec_v[v_idx1] };
163
164            //Error check : Skip the triangle where two points are on the same location.
165            //it fixes the problem of having an intersection with one of the isec-point being on one of the triangle vertices.
166            //(the problem being a very funny infinite loop)
167            for(int k = 0; k < 9; k += 3)
168            {
169                bool skip_tri = false;
170                for(int l = 0; l < 3; l++)
171                {
172                    if (length(new_v[k + l] - new_v[k + (l + 1) % 3]) < CSG_EPSILON)
173                    {
174                        skip_tri = true;
175                        break;
176                    }
177                }
178
179                if (skip_tri)
180                    continue;
181
182                tri_to_process.Push(0, new_v[k], new_v[k + 1], new_v[k + 2]);
183            }
184#endif
185        }
186        //All points are on one side, transfer to the next leaf
187        else if (res_nb[LEAF_BACK] || res_nb[LEAF_FRONT])
188        {
189            int new_leaf_type = ((res_nb[LEAF_FRONT])?(LEAF_FRONT):(LEAF_BACK));
190            int new_leaf = m_tree[leaf_idx].m_leaves[new_leaf_type];
191
192            //No leaf exist, so add a new one
193            if (new_leaf == LEAF_CURRENT)
194            {
195                tri_to_process.Pop();
196                Leaf_to_add.Push(new_leaf_type, leaf_idx, v[0], v[1], v[2], -1);
197            }
198            else
199                tri_to_process.Last().m1 = new_leaf;
200        }
201        //All points are on the current leaf, add the tri_idx to the list of this leaf.
202        else
203        {
204            tri_to_process.Pop();
205
206            bool already_exist = false;
207            for (int i = 0; !already_exist && i < m_tree[leaf_idx].m_tri_list.Count(); i++)
208                already_exist = (m_tree[leaf_idx].m_tri_list[i].m1 == tri_idx);
209            if (!already_exist)
210                m_tree[leaf_idx].m_tri_list.Push(tri_idx, tri_v0, tri_v1, tri_v2);
211        }
212    }
213
214    //Add all leaves to the tree.
215    for (int i = 0; i < Leaf_to_add.Count(); i++)
216    {
217        //If we had it to an already existing leaf.
218        if (Leaf_to_add[i].m2 < m_tree.Count() && m_tree[Leaf_to_add[i].m2].m_leaves[Leaf_to_add[i].m1] == LEAF_CURRENT)
219        {
220            AddLeaf(Leaf_to_add[i].m1, tri_v0, cross(normalize(tri_v1 - tri_v0), normalize(tri_v2 - tri_v1)), Leaf_to_add[i].m2);
221            m_tree.Last().m_tri_list.Push(tri_idx, tri_v0, tri_v1, tri_v2);
222        }
223
224        /*
225        if (Leaf_to_add[i].m6 == -1)
226        {
227            AddLeaf(Leaf_to_add[i].m1, tri_v0, cross(normalize(tri_v1 - tri_v0), normalize(tri_v2 - tri_v1)), Leaf_to_add[i].m2);
228            m_tree.Last().m_tri_list.Push(tri_idx, tri_v0, tri_v1, tri_v2);
229        }
230        else
231            m_tree[Leaf_to_add[i].m6].m_tri_list.Push(tri_idx, tri_v0, tri_v1, tri_v2);
232        */
233    }
234}
235
236//return 0 when no split has been done.
237//return 1 when split has been done.
238//return -1 when error.
239int CsgBsp::TestTriangleToTree(vec3 const &tri_v0, vec3 const &tri_v1, vec3 const &tri_v2,
240                               //In order to easily build the actual vertices list afterward, this list stores each Vertices location and its source vertices & Alpha.
241                               //<Point_Loc, Src_V0, Src_V1, Alpha> as { Point_Loc = Src_V0 + (Src_V1 - Src_V0) * Alpha; }
242                               Array< vec3, int, int, float > &vert_list,
243                               //This is the final triangle list : If Side_Status is LEAF_CURRENT, a new test will be done point by point.
244                               //<{IN|OUT}side_status, v0, v1, v2>
245                               Array< int, int, int, int > &tri_list)
246{
247    //This list stores the current triangles to process.
248    //<Leaf_Id_List, v0, v1, v2, Should_Point_Test>
249    Array< Array< int >, int, int, int, int > tri_to_process;
250
251    //Tree is empty, ABORT !
252    if (m_tree.Count() == 0)
253        return -1;
254
255    //Let's push the source vertices in here.
256    vert_list.Push(tri_v0, -1, -1, .0f);
257    vert_list.Push(tri_v1, -1, -1, .0f);
258    vert_list.Push(tri_v2, -1, -1, .0f);
259
260    //Let's push the triangle in here.
261    tri_to_process.Reserve(20);
262    tri_to_process.Push( Array< int >(), 0, 1, 2, 0);
263    tri_to_process.Last().m1.Push(0);
264
265    while (tri_to_process.Count())
266    {
267        while (tri_to_process.Count())
268        {
269            int leaf_idx = tri_to_process.Last().m1.Last();
270            int t[3] = { tri_to_process.Last().m2,
271                            tri_to_process.Last().m3,
272                            tri_to_process.Last().m4 };
273            vec3 v[3] = { vert_list[t[0]].m1,
274                            vert_list[t[1]].m1,
275                            vert_list[t[2]].m1 };
276
277            int res_nb[3] = { 0, 0, 0 };
278            int res_side[3] = { -1, -1, -1 };
279            //Check where each point is located
280            for (int i = 0; i < 3; i++)
281            {
282                int result = TestPoint(leaf_idx, v[i]);
283                if (result != LEAF_CURRENT)
284                {
285                    res_nb[result]++;
286                    res_side[i] = result;
287                }
288            }
289
290            //Points are located on each sides, let's do the mumbo-jumbo
291            if (res_nb[LEAF_BACK] && res_nb[LEAF_FRONT])
292            {
293                //there are two intersections, no more.
294                vec3 isec_v[2] = { vec3(.0f), vec3(.0f) };
295                int isec_i[2] = { 0, 0 };
296                int new_v_idx[2] = { 0, 0 };
297                int isec_base = 0;
298                int isec_idx = 0;
299
300                int i = 0;
301                for (; i < m_tree[leaf_idx].m_tri_list.Count(); i++)
302                {
303                    if (TriangleIsectTriangle(v[0], v[1], v[2],
304                                                m_tree[leaf_idx].m_tri_list[i].m2, m_tree[leaf_idx].m_tri_list[i].m3, m_tree[leaf_idx].m_tri_list[i].m4,
305                                                isec_v[0], isec_v[1]))
306                        break;
307                }
308
309                //There was no triangle intersection, the complex case.
310                if (i == m_tree[leaf_idx].m_tri_list.Count())
311                {
312                    tri_to_process.Last().m1.Pop();
313
314                    //Register the triangle as needing to intersect with Front & back leaves.
315                    if (m_tree[leaf_idx].m_leaves[LEAF_FRONT] != LEAF_CURRENT)
316                        tri_to_process.Last().m1.Push(m_tree[leaf_idx].m_leaves[LEAF_FRONT]);
317                    if (m_tree[leaf_idx].m_leaves[LEAF_BACK] != LEAF_CURRENT)
318                        tri_to_process.Last().m1.Push(m_tree[leaf_idx].m_leaves[LEAF_BACK]);
319                    //Mark the triangle as needing point by point test
320                    tri_to_process.Last().m5 = 1;
321                }
322                //there was an intersection, so let's split the triangle.
323                else
324                {
325                    //Get intersection on actual triangle sides.
326                    if (RayIsectTriangleSide(v[0], v[1], v[2],
327                                                isec_v[0], isec_v[1],
328                                                isec_v[0], isec_i[0], isec_v[1], isec_i[1]))
329                    {
330                        {
331                            for(int k = 0; k < 2; k++)
332                            {
333                                if (isec_base == isec_i[k])
334                                    isec_base++;
335
336#if 1 //Skip point creation if it's on the same location a one of the triangle.
337                                bool skip_point = false;
338                                int l = 0;
339                                for(; l < 3; l++)
340                                {
341                                    if (length(v[l] - isec_v[k]) < CSG_EPSILON)
342                                    {
343                                        skip_point = true;
344                                        new_v_idx[k] = t[l];
345                                        break;
346                                    }
347                                }
348
349                                if (skip_point)
350                                    continue;
351#endif
352                                new_v_idx[k] = vert_list.Count();
353                                vec3 PmV0 = (isec_v[k] - vert_list[t[isec_i[k]]].m1);
354                                vec3 V1mV0 = (vert_list[t[(isec_i[k] + 1) % 3]].m1 - vert_list[t[isec_i[k]]].m1);
355                                float alpha = length(PmV0) / length(V1mV0);
356                                vert_list.Push(isec_v[k],
357                                                t[isec_i[k]], t[(isec_i[k] + 1) % 3],
358                                                //Alpha = length((Point_Loc - Src_V0) / (Src_V1 - Src_V0));
359                                                alpha);
360                            }
361
362                            int v_idx0 = (isec_base == 1)?(1):(0);
363                            int v_idx1 = (isec_base == 1)?(0):(1);
364                            //Leaf_type is the type for the triangle that is alone on its side.
365                            int leaf_type = res_side[(isec_base + 2) % 3];
366                            int tri_to_remove = tri_to_process.Count() - 1;
367
368#if 0
369                            if (m_tree[leaf_idx].m_leaves[leaf_type] == LEAF_CURRENT && tri_to_process.Last().m1.Last() == 1)
370                                tri_list.Push(leaf_type,
371                                                t[(isec_base + 2) % 3], new_v_idx[v_idx1], new_v_idx[v_idx0]);
372                            else
373                            {
374                                tri_to_process.Push(Array< int >(), t[(isec_base + 2) % 3], new_v_idx[v_idx1], new_v_idx[v_idx0], 0);
375                                tri_to_process.Last().m1.Push(0);
376                            }
377
378                            if (m_tree[leaf_idx].m_leaves[1 - leaf_type] == LEAF_CURRENT && tri_to_process.Last().m1.Last() == 1)
379                            {
380                                tri_list.Push((tri_to_process.Last().m5)?(LEAF_CURRENT):(1 - leaf_type),
381                                                t[isec_base], new_v_idx[((isec_base + 1) % 3)], new_v_idx[v_idx0]);
382                                tri_list.Push((tri_to_process.Last().m5)?(LEAF_CURRENT):(1 - leaf_type),
383                                                t[isec_base], new_v_idx[v_idx0], new_v_idx[v_idx1]);
384                            }
385                            else
386                            {
387                                tri_to_process.Push(Array< int >(), t[isec_base], t[((isec_base + 1) % 3)], new_v_idx[v_idx0], 0);
388                                tri_to_process.Last().m1.Push(0);
389                                tri_to_process.Push(Array< int >(), t[isec_base], new_v_idx[v_idx0], new_v_idx[v_idx1], 0);
390                                tri_to_process.Last().m1.Push(0);
391                            }
392#else
393                            int new_t[9] = { t[(isec_base + 2) % 3], new_v_idx[v_idx1],         new_v_idx[v_idx0],
394                                                t[isec_base],           t[((isec_base + 1) % 3)],  new_v_idx[v_idx0],
395                                                t[isec_base],           new_v_idx[v_idx0],         new_v_idx[v_idx1] };
396                            int new_side[3] = { res_side[(isec_base + 2) % 3],
397                                                (res_side[isec_base] == LEAF_CURRENT)?(res_side[((isec_base + 1) % 3)]):(res_side[isec_base]),
398                                                res_side[isec_base] };
399
400                            //Error check : Skip the triangle where two points are on the same location.
401                            //it fixes the problem of having an intersection with one of the isec-point being on one of the triangle vertices.
402                            //(the problem being a very funny infinite loop)
403                            for(int k = 0; k < 9; k += 3)
404                            {
405#if 1 //Error check
406                                bool skip_tri = false;
407                                for(int l = 0; l < 3; l++)
408                                {
409                                    if (length(vert_list[new_t[k + l]].m1 - vert_list[new_t[k + (l + 1) % 3]].m1) < CSG_EPSILON)
410                                    {
411                                        skip_tri = true;
412                                        break;
413                                    }
414                                }
415
416                                if (skip_tri)
417                                    continue;
418#endif
419#if 0 //Send the newly created triangle back to the beginning
420                                tri_to_process.Push(Array< int >(), new_t[k], new_t[k + 1], new_t[k + 2], 0);
421                                tri_to_process.Last().m1.Push(0);
422#else //Inherit parent tree
423                                if (m_tree[leaf_idx].m_leaves[new_side[k / 3]] == LEAF_CURRENT && tri_to_process[tri_to_remove].m1.Count() == 1)
424                                    tri_list.Push(new_side[k / 3], new_t[k], new_t[k + 1], new_t[k + 2]);
425                                else
426                                {
427                                    tri_to_process.Push(Array< int >(), new_t[k], new_t[k + 1], new_t[k + 2], 0);
428                                    tri_to_process.Last().m1 = tri_to_process[tri_to_remove].m1;
429                                    if (m_tree[leaf_idx].m_leaves[new_side[k / 3]] == LEAF_CURRENT)
430                                        tri_to_process.Last().m1.Pop();
431                                    else
432                                        tri_to_process.Last().m1.Last() = m_tree[leaf_idx].m_leaves[new_side[k / 3]];
433                                }
434#endif
435                            }
436#endif
437
438                            tri_to_process.Remove(tri_to_remove);
439                        }
440                    }
441                }
442            }
443            //All points are on one side, transfer to the next leaf
444            else if (res_nb[LEAF_BACK] || res_nb[LEAF_FRONT])
445            {
446                int new_leaf_type = ((res_nb[LEAF_FRONT])?(LEAF_FRONT):(LEAF_BACK));
447                int new_leaf = m_tree[leaf_idx].m_leaves[new_leaf_type];
448
449                //No leaf exist, we're at the end
450                if (new_leaf == LEAF_CURRENT)
451                {
452                    //We still need to test other leaves.
453                    if (tri_to_process.Last().m1.Count() > 1)
454                        tri_to_process.Last().m1.Pop();
455                    else
456                    {
457                        tri_list.Push((tri_to_process.Last().m5)?(LEAF_CURRENT):(new_leaf_type),
458                                        tri_to_process.Last().m2, tri_to_process.Last().m3, tri_to_process.Last().m4);
459                        tri_to_process.Pop();
460                    }
461                }
462                else
463                    tri_to_process.Last().m1.Last() = new_leaf;
464            }
465            //All points are on the current leaf, add the tri_idx to the list of this leaf.
466            else
467            {
468                //TODO : Special case, handle coplanar cut.
469                tri_list.Push(LEAF_CURRENT, tri_to_process.Last().m2, tri_to_process.Last().m3, tri_to_process.Last().m4);
470                tri_to_process.Pop();
471            }
472        }
473
474        //Now that we have all the split points, let's double-check the results
475        for (int i = 0; i < tri_list.Count(); i++)
476        {
477#define TEST_MAX 4
478            int t[3] = { tri_list[i].m2,
479                            tri_list[i].m3,
480                            tri_list[i].m4 };
481            vec3 v[4] = { vert_list[t[0]].m1,
482                            vert_list[t[1]].m1,
483                            vert_list[t[2]].m1,
484                            (vert_list[t[0]].m1 +
485                            vert_list[t[1]].m1 +
486                            vert_list[t[2]].m1) / 3.0f };
487
488            int res_total = 0;
489            int res_nb[3] = { 0, 0, 0 };
490           
491            int res_Leaf[4] = { 0, 0, 0, 0 };
492            int res_side[4] = { -1, -1, -1, -1 };
493            while (res_total < TEST_MAX)
494            {
495                for (int k = 0; k < TEST_MAX; k++)
496                {
497                    if (res_Leaf[k] != LEAF_CURRENT)
498                    {
499                        int result = TestPoint(res_Leaf[k], v[k]);
500                        if (result != LEAF_CURRENT)
501                        {
502                            res_Leaf[k] = m_tree[res_Leaf[k]].m_leaves[result];
503                            res_side[k] = result;
504                            if (res_Leaf[k] == LEAF_CURRENT)
505                            {
506                                res_total++;
507                                res_nb[result]++;
508                            }
509                        }
510                        else
511                        {
512                            res_Leaf[k] = LEAF_CURRENT;
513                            res_side[k] = LEAF_CURRENT;
514                            res_total++;
515                        }
516                    }
517                }
518            }
519            int k = 0;
520            if (res_nb[LEAF_BACK] && res_nb[LEAF_FRONT])
521            {
522                res_total = res_total;
523                tri_list[i].m1 = LEAF_BACK;
524#if 0
525                tri_to_process.Push( Array< int >(), tri_list[i].m2, tri_list[i].m3, tri_list[i].m4, 0);
526                tri_to_process.Last().m1.Push(0);
527                tri_list.Remove(i--);
528                break;
529#endif
530            }
531            else
532            {
533                for (; k < TEST_MAX; k++)
534                {
535                    if (res_side[k] != LEAF_CURRENT)
536                    {
537                        tri_list[i].m1 = res_side[k];
538                        break;
539                    }
540                }
541                if (k == TEST_MAX)
542                    tri_list[i].m1 = LEAF_FRONT;
543            }
544        }
545    }
546
547    if (tri_list.Count() == 1)
548        return 0;
549    return 1;
550}
551
552} /* namespace lol */
553
Note: See TracBrowser for help on using the repository browser.