1 | /* |
2 | --------------------------------------------------------------------------- |
3 | Open Asset Import Library (assimp) |
4 | --------------------------------------------------------------------------- |
5 | |
6 | Copyright (c) 2006-2017, assimp team |
7 | |
8 | |
9 | All rights reserved. |
10 | |
11 | Redistribution and use of this software in source and binary forms, |
12 | with or without modification, are permitted provided that the following |
13 | conditions are met: |
14 | |
15 | * Redistributions of source code must retain the above |
16 | copyright notice, this list of conditions and the |
17 | following disclaimer. |
18 | |
19 | * Redistributions in binary form must reproduce the above |
20 | copyright notice, this list of conditions and the |
21 | following disclaimer in the documentation and/or other |
22 | materials provided with the distribution. |
23 | |
24 | * Neither the name of the assimp team, nor the names of its |
25 | contributors may be used to endorse or promote products |
26 | derived from this software without specific prior |
27 | written permission of the assimp team. |
28 | |
29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
30 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
31 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
32 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
33 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
34 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
35 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
36 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
37 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
38 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
39 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
40 | --------------------------------------------------------------------------- |
41 | */ |
42 | |
43 | /** @file Implementation of the STL importer class */ |
44 | |
45 | |
46 | #ifndef ASSIMP_BUILD_NO_STL_IMPORTER |
47 | |
48 | // internal headers |
49 | #include "STLLoader.h" |
50 | #include "ParsingUtils.h" |
51 | #include "fast_atof.h" |
52 | #include <memory> |
53 | #include <assimp/IOSystem.hpp> |
54 | #include <assimp/scene.h> |
55 | #include <assimp/DefaultLogger.hpp> |
56 | #include <assimp/importerdesc.h> |
57 | |
58 | using namespace Assimp; |
59 | |
60 | namespace { |
61 | |
62 | static const aiImporterDesc desc = { |
63 | "Stereolithography (STL) Importer" , |
64 | "" , |
65 | "" , |
66 | "" , |
67 | aiImporterFlags_SupportTextFlavour | aiImporterFlags_SupportBinaryFlavour, |
68 | 0, |
69 | 0, |
70 | 0, |
71 | 0, |
72 | "stl" |
73 | }; |
74 | |
75 | // A valid binary STL buffer should consist of the following elements, in order: |
76 | // 1) 80 byte header |
77 | // 2) 4 byte face count |
78 | // 3) 50 bytes per face |
79 | static bool IsBinarySTL(const char* buffer, unsigned int fileSize) { |
80 | if( fileSize < 84 ) { |
81 | return false; |
82 | } |
83 | |
84 | const char *facecount_pos = buffer + 80; |
85 | uint32_t faceCount( 0 ); |
86 | ::memcpy( &faceCount, facecount_pos, sizeof( uint32_t ) ); |
87 | const uint32_t expectedBinaryFileSize = faceCount * 50 + 84; |
88 | |
89 | return expectedBinaryFileSize == fileSize; |
90 | } |
91 | |
92 | // An ascii STL buffer will begin with "solid NAME", where NAME is optional. |
93 | // Note: The "solid NAME" check is necessary, but not sufficient, to determine |
94 | // if the buffer is ASCII; a binary header could also begin with "solid NAME". |
95 | static bool IsAsciiSTL(const char* buffer, unsigned int fileSize) { |
96 | if (IsBinarySTL(buffer, fileSize)) |
97 | return false; |
98 | |
99 | const char* bufferEnd = buffer + fileSize; |
100 | |
101 | if (!SkipSpaces(&buffer)) |
102 | return false; |
103 | |
104 | if (buffer + 5 >= bufferEnd) |
105 | return false; |
106 | |
107 | bool isASCII( strncmp( buffer, "solid" , 5 ) == 0 ); |
108 | if( isASCII ) { |
109 | // A lot of importers are write solid even if the file is binary. So we have to check for ASCII-characters. |
110 | if( fileSize >= 500 ) { |
111 | isASCII = true; |
112 | for( unsigned int i = 0; i < 500; i++ ) { |
113 | if( buffer[ i ] > 127 ) { |
114 | isASCII = false; |
115 | break; |
116 | } |
117 | } |
118 | } |
119 | } |
120 | return isASCII; |
121 | } |
122 | } // namespace |
123 | |
124 | // ------------------------------------------------------------------------------------------------ |
125 | // Constructor to be privately used by Importer |
126 | STLImporter::STLImporter() |
127 | : mBuffer(), |
128 | fileSize(), |
129 | pScene() |
130 | {} |
131 | |
132 | // ------------------------------------------------------------------------------------------------ |
133 | // Destructor, private as well |
134 | STLImporter::~STLImporter() |
135 | {} |
136 | |
137 | // ------------------------------------------------------------------------------------------------ |
138 | // Returns whether the class can handle the format of the given file. |
139 | bool STLImporter::CanRead( const std::string& pFile, IOSystem* pIOHandler, bool checkSig) const |
140 | { |
141 | const std::string extension = GetExtension(pFile); |
142 | |
143 | if( extension == "stl" ) { |
144 | return true; |
145 | } else if (!extension.length() || checkSig) { |
146 | if( !pIOHandler ) { |
147 | return true; |
148 | } |
149 | const char* tokens[] = {"STL" ,"solid" }; |
150 | return SearchFileHeaderForToken(pIOHandler,pFile,tokens,2); |
151 | } |
152 | |
153 | return false; |
154 | } |
155 | |
156 | // ------------------------------------------------------------------------------------------------ |
157 | const aiImporterDesc* STLImporter::GetInfo () const { |
158 | return &desc; |
159 | } |
160 | |
161 | void addFacesToMesh(aiMesh* pMesh) |
162 | { |
163 | pMesh->mFaces = new aiFace[pMesh->mNumFaces]; |
164 | for (unsigned int i = 0, p = 0; i < pMesh->mNumFaces;++i) { |
165 | |
166 | aiFace& face = pMesh->mFaces[i]; |
167 | face.mIndices = new unsigned int[face.mNumIndices = 3]; |
168 | for (unsigned int o = 0; o < 3;++o,++p) { |
169 | face.mIndices[o] = p; |
170 | } |
171 | } |
172 | } |
173 | |
174 | // ------------------------------------------------------------------------------------------------ |
175 | // Imports the given file into the given scene structure. |
176 | void STLImporter::InternReadFile( const std::string& pFile, aiScene* pScene, IOSystem* pIOHandler ) |
177 | { |
178 | std::unique_ptr<IOStream> file( pIOHandler->Open( pFile, "rb" )); |
179 | |
180 | // Check whether we can read from the file |
181 | if( file.get() == NULL) { |
182 | throw DeadlyImportError( "Failed to open STL file " + pFile + "." ); |
183 | } |
184 | |
185 | fileSize = (unsigned int)file->FileSize(); |
186 | |
187 | // allocate storage and copy the contents of the file to a memory buffer |
188 | // (terminate it with zero) |
189 | std::vector<char> mBuffer2; |
190 | TextFileToBuffer(file.get(),mBuffer2); |
191 | |
192 | this->pScene = pScene; |
193 | this->mBuffer = &mBuffer2[0]; |
194 | |
195 | // the default vertex color is light gray. |
196 | clrColorDefault.r = clrColorDefault.g = clrColorDefault.b = clrColorDefault.a = (ai_real) 0.6; |
197 | |
198 | // allocate a single node |
199 | pScene->mRootNode = new aiNode(); |
200 | |
201 | bool bMatClr = false; |
202 | |
203 | if (IsBinarySTL(mBuffer, fileSize)) { |
204 | bMatClr = LoadBinaryFile(); |
205 | } else if (IsAsciiSTL(mBuffer, fileSize)) { |
206 | LoadASCIIFile( pScene->mRootNode ); |
207 | } else { |
208 | throw DeadlyImportError( "Failed to determine STL storage representation for " + pFile + "." ); |
209 | } |
210 | |
211 | // create a single default material, using a white diffuse color for consistency with |
212 | // other geometric types (e.g., PLY). |
213 | aiMaterial* pcMat = new aiMaterial(); |
214 | aiString s; |
215 | s.Set(AI_DEFAULT_MATERIAL_NAME); |
216 | pcMat->AddProperty(&s, AI_MATKEY_NAME); |
217 | |
218 | aiColor4D clrDiffuse(ai_real(1.0),ai_real(1.0),ai_real(1.0),ai_real(1.0)); |
219 | if (bMatClr) { |
220 | clrDiffuse = clrColorDefault; |
221 | } |
222 | pcMat->AddProperty(&clrDiffuse,1,AI_MATKEY_COLOR_DIFFUSE); |
223 | pcMat->AddProperty(&clrDiffuse,1,AI_MATKEY_COLOR_SPECULAR); |
224 | clrDiffuse = aiColor4D( ai_real(1.0), ai_real(1.0), ai_real(1.0), ai_real(1.0)); |
225 | pcMat->AddProperty(&clrDiffuse,1,AI_MATKEY_COLOR_AMBIENT); |
226 | |
227 | pScene->mNumMaterials = 1; |
228 | pScene->mMaterials = new aiMaterial*[1]; |
229 | pScene->mMaterials[0] = pcMat; |
230 | } |
231 | |
232 | // ------------------------------------------------------------------------------------------------ |
233 | // Read an ASCII STL file |
234 | void STLImporter::LoadASCIIFile( aiNode *root ) { |
235 | std::vector<aiMesh*> meshes; |
236 | std::vector<aiNode*> nodes; |
237 | const char* sz = mBuffer; |
238 | const char* bufferEnd = mBuffer + fileSize; |
239 | std::vector<aiVector3D> positionBuffer; |
240 | std::vector<aiVector3D> normalBuffer; |
241 | |
242 | // try to guess how many vertices we could have |
243 | // assume we'll need 160 bytes for each face |
244 | size_t sizeEstimate = std::max(1u, fileSize / 160u ) * 3; |
245 | positionBuffer.reserve(sizeEstimate); |
246 | normalBuffer.reserve(sizeEstimate); |
247 | |
248 | while (IsAsciiSTL(sz, static_cast<unsigned int>(bufferEnd - sz))) { |
249 | std::vector<unsigned int> meshIndices; |
250 | aiMesh* pMesh = new aiMesh(); |
251 | pMesh->mMaterialIndex = 0; |
252 | meshIndices.push_back((unsigned int) meshes.size() ); |
253 | meshes.push_back(pMesh); |
254 | aiNode *node = new aiNode; |
255 | node->mParent = root; |
256 | nodes.push_back( node ); |
257 | SkipSpaces(&sz); |
258 | ai_assert(!IsLineEnd(sz)); |
259 | |
260 | sz += 5; // skip the "solid" |
261 | SkipSpaces(&sz); |
262 | const char* szMe = sz; |
263 | while (!::IsSpaceOrNewLine(*sz)) { |
264 | sz++; |
265 | } |
266 | |
267 | size_t temp; |
268 | // setup the name of the node |
269 | if ((temp = (size_t)(sz-szMe))) { |
270 | if (temp >= MAXLEN) { |
271 | throw DeadlyImportError( "STL: Node name too long" ); |
272 | } |
273 | std::string name( szMe, temp ); |
274 | node->mName.Set( name.c_str() ); |
275 | //pScene->mRootNode->mName.length = temp; |
276 | //memcpy(pScene->mRootNode->mName.data,szMe,temp); |
277 | //pScene->mRootNode->mName.data[temp] = '\0'; |
278 | } else { |
279 | pScene->mRootNode->mName.Set("<STL_ASCII>" ); |
280 | } |
281 | |
282 | unsigned int faceVertexCounter = 3; |
283 | for ( ;; ) { |
284 | // go to the next token |
285 | if(!SkipSpacesAndLineEnd(&sz)) |
286 | { |
287 | // seems we're finished although there was no end marker |
288 | DefaultLogger::get()->warn("STL: unexpected EOF. \'endsolid\' keyword was expected" ); |
289 | break; |
290 | } |
291 | // facet normal -0.13 -0.13 -0.98 |
292 | if (!strncmp(sz,"facet" ,5) && IsSpaceOrNewLine(*(sz+5)) && *(sz + 5) != '\0') { |
293 | |
294 | if (faceVertexCounter != 3) { |
295 | DefaultLogger::get()->warn("STL: A new facet begins but the old is not yet complete" ); |
296 | } |
297 | faceVertexCounter = 0; |
298 | normalBuffer.push_back(aiVector3D()); |
299 | aiVector3D* vn = &normalBuffer.back(); |
300 | |
301 | sz += 6; |
302 | SkipSpaces(&sz); |
303 | if (strncmp(sz,"normal" ,6)) { |
304 | DefaultLogger::get()->warn("STL: a facet normal vector was expected but not found" ); |
305 | } else { |
306 | if (sz[6] == '\0') { |
307 | throw DeadlyImportError("STL: unexpected EOF while parsing facet" ); |
308 | } |
309 | sz += 7; |
310 | SkipSpaces(&sz); |
311 | sz = fast_atoreal_move<ai_real>(sz, (ai_real&)vn->x ); |
312 | SkipSpaces(&sz); |
313 | sz = fast_atoreal_move<ai_real>(sz, (ai_real&)vn->y ); |
314 | SkipSpaces(&sz); |
315 | sz = fast_atoreal_move<ai_real>(sz, (ai_real&)vn->z ); |
316 | normalBuffer.push_back(*vn); |
317 | normalBuffer.push_back(*vn); |
318 | } |
319 | } else if (!strncmp(sz,"vertex" ,6) && ::IsSpaceOrNewLine(*(sz+6))) { // vertex 1.50000 1.50000 0.00000 |
320 | if (faceVertexCounter >= 3) { |
321 | DefaultLogger::get()->error("STL: a facet with more than 3 vertices has been found" ); |
322 | ++sz; |
323 | } else { |
324 | if (sz[6] == '\0') { |
325 | throw DeadlyImportError("STL: unexpected EOF while parsing facet" ); |
326 | } |
327 | sz += 7; |
328 | SkipSpaces(&sz); |
329 | positionBuffer.push_back(aiVector3D()); |
330 | aiVector3D* vn = &positionBuffer.back(); |
331 | sz = fast_atoreal_move<ai_real>(sz, (ai_real&)vn->x ); |
332 | SkipSpaces(&sz); |
333 | sz = fast_atoreal_move<ai_real>(sz, (ai_real&)vn->y ); |
334 | SkipSpaces(&sz); |
335 | sz = fast_atoreal_move<ai_real>(sz, (ai_real&)vn->z ); |
336 | faceVertexCounter++; |
337 | } |
338 | } else if (!::strncmp(sz,"endsolid" ,8)) { |
339 | do { |
340 | ++sz; |
341 | } while (!::IsLineEnd(*sz)); |
342 | SkipSpacesAndLineEnd(&sz); |
343 | // finished! |
344 | break; |
345 | } else { // else skip the whole identifier |
346 | do { |
347 | ++sz; |
348 | } while (!::IsSpaceOrNewLine(*sz)); |
349 | } |
350 | } |
351 | |
352 | if (positionBuffer.empty()) { |
353 | pMesh->mNumFaces = 0; |
354 | throw DeadlyImportError("STL: ASCII file is empty or invalid; no data loaded" ); |
355 | } |
356 | if (positionBuffer.size() % 3 != 0) { |
357 | pMesh->mNumFaces = 0; |
358 | throw DeadlyImportError("STL: Invalid number of vertices" ); |
359 | } |
360 | if (normalBuffer.size() != positionBuffer.size()) { |
361 | pMesh->mNumFaces = 0; |
362 | throw DeadlyImportError("Normal buffer size does not match position buffer size" ); |
363 | } |
364 | pMesh->mNumFaces = static_cast<unsigned int>(positionBuffer.size() / 3); |
365 | pMesh->mNumVertices = static_cast<unsigned int>(positionBuffer.size()); |
366 | pMesh->mVertices = new aiVector3D[pMesh->mNumVertices]; |
367 | memcpy(pMesh->mVertices, &positionBuffer[0].x, pMesh->mNumVertices * sizeof(aiVector3D)); |
368 | positionBuffer.clear(); |
369 | pMesh->mNormals = new aiVector3D[pMesh->mNumVertices]; |
370 | memcpy(pMesh->mNormals, &normalBuffer[0].x, pMesh->mNumVertices * sizeof(aiVector3D)); |
371 | normalBuffer.clear(); |
372 | |
373 | // now copy faces |
374 | addFacesToMesh(pMesh); |
375 | |
376 | // assign the meshes to the current node |
377 | pushMeshesToNode( meshIndices, node ); |
378 | } |
379 | |
380 | // now add the loaded meshes |
381 | pScene->mNumMeshes = (unsigned int)meshes.size(); |
382 | pScene->mMeshes = new aiMesh*[pScene->mNumMeshes]; |
383 | for (size_t i = 0; i < meshes.size(); i++) { |
384 | pScene->mMeshes[ i ] = meshes[i]; |
385 | } |
386 | |
387 | root->mNumChildren = (unsigned int) nodes.size(); |
388 | root->mChildren = new aiNode*[ root->mNumChildren ]; |
389 | for ( size_t i=0; i<nodes.size(); ++i ) { |
390 | root->mChildren[ i ] = nodes[ i ]; |
391 | } |
392 | } |
393 | |
394 | // ------------------------------------------------------------------------------------------------ |
395 | // Read a binary STL file |
396 | bool STLImporter::LoadBinaryFile() |
397 | { |
398 | // allocate one mesh |
399 | pScene->mNumMeshes = 1; |
400 | pScene->mMeshes = new aiMesh*[1]; |
401 | aiMesh* pMesh = pScene->mMeshes[0] = new aiMesh(); |
402 | pMesh->mMaterialIndex = 0; |
403 | |
404 | // skip the first 80 bytes |
405 | if (fileSize < 84) { |
406 | throw DeadlyImportError("STL: file is too small for the header" ); |
407 | } |
408 | bool bIsMaterialise = false; |
409 | |
410 | // search for an occurrence of "COLOR=" in the header |
411 | const unsigned char* sz2 = (const unsigned char*)mBuffer; |
412 | const unsigned char* const szEnd = sz2+80; |
413 | while (sz2 < szEnd) { |
414 | |
415 | if ('C' == *sz2++ && 'O' == *sz2++ && 'L' == *sz2++ && |
416 | 'O' == *sz2++ && 'R' == *sz2++ && '=' == *sz2++) { |
417 | |
418 | // read the default vertex color for facets |
419 | bIsMaterialise = true; |
420 | DefaultLogger::get()->info("STL: Taking code path for Materialise files" ); |
421 | const ai_real invByte = (ai_real)1.0 / ( ai_real )255.0; |
422 | clrColorDefault.r = (*sz2++) * invByte; |
423 | clrColorDefault.g = (*sz2++) * invByte; |
424 | clrColorDefault.b = (*sz2++) * invByte; |
425 | clrColorDefault.a = (*sz2++) * invByte; |
426 | break; |
427 | } |
428 | } |
429 | const unsigned char* sz = (const unsigned char*)mBuffer + 80; |
430 | |
431 | // now read the number of facets |
432 | pScene->mRootNode->mName.Set("<STL_BINARY>" ); |
433 | |
434 | pMesh->mNumFaces = *((uint32_t*)sz); |
435 | sz += 4; |
436 | |
437 | if (fileSize < 84 + pMesh->mNumFaces*50) { |
438 | throw DeadlyImportError("STL: file is too small to hold all facets" ); |
439 | } |
440 | |
441 | if (!pMesh->mNumFaces) { |
442 | throw DeadlyImportError("STL: file is empty. There are no facets defined" ); |
443 | } |
444 | |
445 | pMesh->mNumVertices = pMesh->mNumFaces*3; |
446 | |
447 | aiVector3D* vp,*vn; |
448 | vp = pMesh->mVertices = new aiVector3D[pMesh->mNumVertices]; |
449 | vn = pMesh->mNormals = new aiVector3D[pMesh->mNumVertices]; |
450 | |
451 | for (unsigned int i = 0; i < pMesh->mNumFaces;++i) { |
452 | |
453 | // NOTE: Blender sometimes writes empty normals ... this is not |
454 | // our fault ... the RemoveInvalidData helper step should fix that |
455 | *vn = *((aiVector3D*)sz); |
456 | sz += sizeof(aiVector3D); |
457 | *(vn+1) = *vn; |
458 | *(vn+2) = *vn; |
459 | vn += 3; |
460 | |
461 | *vp++ = *((aiVector3D*)sz); |
462 | sz += sizeof(aiVector3D); |
463 | |
464 | *vp++ = *((aiVector3D*)sz); |
465 | sz += sizeof(aiVector3D); |
466 | |
467 | *vp++ = *((aiVector3D*)sz); |
468 | sz += sizeof(aiVector3D); |
469 | |
470 | uint16_t color = *((uint16_t*)sz); |
471 | sz += 2; |
472 | |
473 | if (color & (1 << 15)) |
474 | { |
475 | // seems we need to take the color |
476 | if (!pMesh->mColors[0]) |
477 | { |
478 | pMesh->mColors[0] = new aiColor4D[pMesh->mNumVertices]; |
479 | for (unsigned int i = 0; i <pMesh->mNumVertices;++i) |
480 | *pMesh->mColors[0]++ = this->clrColorDefault; |
481 | pMesh->mColors[0] -= pMesh->mNumVertices; |
482 | |
483 | DefaultLogger::get()->info("STL: Mesh has vertex colors" ); |
484 | } |
485 | aiColor4D* clr = &pMesh->mColors[0][i*3]; |
486 | clr->a = 1.0; |
487 | const ai_real invVal( (ai_real)1.0 / ( ai_real )31.0 ); |
488 | if (bIsMaterialise) // this is reversed |
489 | { |
490 | clr->r = (color & 0x31u) *invVal; |
491 | clr->g = ((color & (0x31u<<5))>>5u) *invVal; |
492 | clr->b = ((color & (0x31u<<10))>>10u) *invVal; |
493 | } |
494 | else |
495 | { |
496 | clr->b = (color & 0x31u) *invVal; |
497 | clr->g = ((color & (0x31u<<5))>>5u) *invVal; |
498 | clr->r = ((color & (0x31u<<10))>>10u) *invVal; |
499 | } |
500 | // assign the color to all vertices of the face |
501 | *(clr+1) = *clr; |
502 | *(clr+2) = *clr; |
503 | } |
504 | } |
505 | |
506 | // now copy faces |
507 | addFacesToMesh(pMesh); |
508 | |
509 | // add all created meshes to the single node |
510 | pScene->mRootNode->mNumMeshes = pScene->mNumMeshes; |
511 | pScene->mRootNode->mMeshes = new unsigned int[pScene->mNumMeshes]; |
512 | for (unsigned int i = 0; i < pScene->mNumMeshes; i++) |
513 | pScene->mRootNode->mMeshes[i] = i; |
514 | |
515 | if (bIsMaterialise && !pMesh->mColors[0]) |
516 | { |
517 | // use the color as diffuse material color |
518 | return true; |
519 | } |
520 | return false; |
521 | } |
522 | |
523 | void STLImporter::pushMeshesToNode( std::vector<unsigned int> &meshIndices, aiNode *node ) { |
524 | ai_assert( nullptr != node ); |
525 | if ( meshIndices.empty() ) { |
526 | return; |
527 | } |
528 | |
529 | node->mNumMeshes = static_cast<unsigned int>( meshIndices.size() ); |
530 | node->mMeshes = new unsigned int[ meshIndices.size() ]; |
531 | for ( size_t i=0; i<meshIndices.size(); ++i ) { |
532 | node->mMeshes[ i ] = meshIndices[ i ]; |
533 | } |
534 | meshIndices.clear(); |
535 | } |
536 | |
537 | #endif // !! ASSIMP_BUILD_NO_STL_IMPORTER |
538 | |