How to add multiple stl files to create 3mf assembly ?
Suresh3d opened this issue · 19 comments
Hi ,
My requirement is to create 3mf assembly with multiple stl files . I will have multiple stl files , I have to add each stl to 3mf assembly , each model will have different quantities and transformation matrices. I am using reader->ReadFromFile()
to read stl files, but how can I get Meshobject
after reading the file , to add it to components , build items and apply transformation matrices ? Right now I am using below code to achieve this ,
- Read all the files using
reader->ReadFromFile()
. GetBuildItems()
frommodel
and remove them .GetMeshObjects()
from model , and iterate over themeshObjects
and add it to build item with transformation matrices ,AddBuildItem(meshObject, transform)
PModel model = wrapper->CreateModel();
PReader reader = model->QueryReader("stl");
reader->ReadFromFile("D:\\Sample Part\\Demo STL files\\Cube.stl");
reader->ReadFromFile("D:\\Sample Part\\Demo STL files\\CraneHook.stl");
std::vector<std::string> names{ "Cube", "CraneHook" };
PComponentsObject componentsObject = model->AddComponentsObject();
auto buildItems = model->GetBuildItems();
while (buildItems->MoveNext())
{
auto current = buildItems->GetCurrent();
model->RemoveBuildItem(classParam<CBuildItem>(current)); //deleting everything , because I want meshObjects with transformations.
}
auto meshObjects = model->GetMeshObjects();
int count = 0;
while (meshObjects->MoveNext())
{
auto mesh = meshObjects->GetCurrentMeshObject();
mesh->SetName(names[count]);
model->AddBuildItem(classParam<CObject>(mesh), createTranslationMatrix(100.0f, 0.0f, 0.0f)); // Adding again with transformation matrices.
count++;
}
PWriter writer = model->QueryWriter("3mf");
writer->WriteToFile("output.3mf");
std::cout << "done" << std::endl;
Question :
- Is this procedure correct or is there a better way to handle it ? I guess if I will be able to get
meshObject
of the stl that I added , then I don't have to query all the objects after adding and iterate over them . Because my stl quantities will be high. - If I call ,
GetMeshObjects
&&GetBuildItems()
will I get the models in the same order that I added ? because This is really important to apply transformation matrices for respective instances . - If you check the below image , the original stl for hook model is staying above the floor . But if I add it in 3mf , why it is going below the floor ?
@Suresh3d For your use case, wouldn't it be easier to read the STL from your existing code and then simply create meshes / components / build items using lib3mf with your desired transformation matrices?
@vijaiaeroastro Actually mine is client server architecture , and this work is happening at back end where we don't have stl reader . So I rely on the reader comes with this library itself.
@Suresh3d I understand. Maybe this will be helpful. I have modified the components example for your use case. I have included a readSTL function that is completely self contained and can read both ASCII and binary STL and returns a 3MF Mesh geometry that can be used directly (in your client - server architecture).
#ifndef __GNUC__
#include <Windows.h>
#endif
#include "lib3mf_implicit.hpp"
#include <iostream>
#include <fstream>
#include <vector>
#include <array>
#include <unordered_map>
#include <string>
#include <sstream>
#include <functional>
#include <stdexcept>
using namespace Lib3MF;
// Utility functions to create vertices and triangles
sLib3MFPosition fnCreateVertex(float x, float y, float z) {
sLib3MFPosition result;
result.m_Coordinates[0] = x;
result.m_Coordinates[1] = y;
result.m_Coordinates[2] = z;
return result;
}
sLib3MFTriangle fnCreateTriangle(int v0, int v1, int v2) {
sLib3MFTriangle result;
result.m_Indices[0] = v0;
result.m_Indices[1] = v1;
result.m_Indices[2] = v2;
return result;
}
sLib3MFTransform createTranslationMatrix(float x, float y, float z) {
sLib3MFTransform mMatrix;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 3; j++) {
mMatrix.m_Fields[i][j] = (i == j) ? 1.0f : 0.0f;
}
}
mMatrix.m_Fields[3][0] = x;
mMatrix.m_Fields[3][1] = y;
mMatrix.m_Fields[3][2] = z;
return mMatrix;
}
void printVersion(PWrapper wrapper) {
Lib3MF_uint32 nMajor, nMinor, nMicro;
wrapper->GetLibraryVersion(nMajor, nMinor, nMicro);
std::cout << "lib3mf version = " << nMajor << "." << nMinor << "." << nMicro;
std::string sReleaseInfo, sBuildInfo;
if (wrapper->GetPrereleaseInformation(sReleaseInfo)) {
std::cout << "-" << sReleaseInfo;
}
if (wrapper->GetBuildInformation(sBuildInfo)) {
std::cout << "+" << sBuildInfo;
}
std::cout << std::endl;
}
std::pair<std::vector<sLib3MFPosition>, std::vector<sLib3MFTriangle>>
readSTL(const std::string& filePath) {
std::ifstream file(filePath, std::ios::binary);
if (!file) {
throw std::runtime_error("Could not open STL file.");
}
// Lambda for the hash function
auto arrayHash = [](const std::array<double, 3>& arr) {
std::hash<double> hasher;
return hasher(arr[0]) ^ hasher(arr[1]) ^ hasher(arr[2]);
};
// Lambda to read binary STL files
auto readBinarySTL = [&file, &arrayHash]() {
char header[80];
file.read(header, 80);
uint32_t numTriangles;
file.read(reinterpret_cast<char*>(&numTriangles), sizeof(numTriangles));
std::vector<std::array<double, 3>> vertices;
std::vector<std::array<unsigned int, 3>> triangles;
std::unordered_map<std::array<double, 3>, unsigned int, decltype(arrayHash)> vertexMap(0, arrayHash);
for (uint32_t i = 0; i < numTriangles; ++i) {
float normal[3];
file.read(reinterpret_cast<char*>(normal), 3 * sizeof(float));
std::array<unsigned int, 3> triangle;
for (int j = 0; j < 3; ++j) {
float vertex[3];
file.read(reinterpret_cast<char*>(vertex), 3 * sizeof(float));
std::array<double, 3> vertexArray = {vertex[0], vertex[1], vertex[2]};
auto it = vertexMap.find(vertexArray);
if (it == vertexMap.end()) {
unsigned int index = vertices.size();
vertices.push_back(vertexArray);
vertexMap[vertexArray] = index;
triangle[j] = index;
} else {
triangle[j] = it->second;
}
}
triangles.push_back(triangle);
uint16_t attributeByteCount;
file.read(reinterpret_cast<char*>(&attributeByteCount), sizeof(attributeByteCount));
}
return std::make_pair(vertices, triangles);
};
// Lambda to read ASCII STL files
auto readAsciiSTL = [&file, &arrayHash]() {
std::vector<std::array<double, 3>> vertices;
std::vector<std::array<unsigned int, 3>> triangles;
std::unordered_map<std::array<double, 3>, unsigned int, decltype(arrayHash)> vertexMap(0, arrayHash);
std::string line;
std::array<unsigned int, 3> currentTriangle;
int vertexIndex = 0;
while (std::getline(file, line)) {
std::istringstream iss(line);
std::string keyword;
iss >> keyword;
if (keyword == "vertex") {
std::array<double, 3> vertex;
iss >> vertex[0] >> vertex[1] >> vertex[2];
auto it = vertexMap.find(vertex);
if (it == vertexMap.end()) {
unsigned int index = vertices.size();
vertices.push_back(vertex);
vertexMap[vertex] = index;
currentTriangle[vertexIndex] = index;
} else {
currentTriangle[vertexIndex] = it->second;
}
vertexIndex = (vertexIndex + 1) % 3;
if (vertexIndex == 0) {
triangles.push_back(currentTriangle);
}
}
}
return std::make_pair(vertices, triangles);
};
// Check if the file is binary or ASCII
char header[80];
file.read(header, 80);
file.seekg(0, std::ios::beg);
std::pair<std::vector<std::array<double, 3>>, std::vector<std::array<unsigned int, 3>>> rawResult;
if (header[0] == 's' && header[1] == 'o' && header[2] == 'l' && header[3] == 'i' && header[4] == 'd') {
// ASCII STL
rawResult = readAsciiSTL();
} else {
// Binary STL
rawResult = readBinarySTL();
}
// Convert the result to sLib3MFPosition and sLib3MFTriangle
std::vector<sLib3MFPosition> positions;
for (const auto& vertex : rawResult.first) {
positions.push_back(fnCreateVertex(static_cast<float>(vertex[0]), static_cast<float>(vertex[1]), static_cast<float>(vertex[2])));
}
std::vector<sLib3MFTriangle> triangles;
for (const auto& triangle : rawResult.second) {
triangles.push_back(fnCreateTriangle(static_cast<int>(triangle[0]), static_cast<int>(triangle[1]), static_cast<int>(triangle[2])));
}
return std::make_pair(positions, triangles);
}
int main() {
PWrapper wrapper = CWrapper::loadLibrary();
std::cout << "------------------------------------------------------------------" << std::endl;
std::cout << "3MF Model Converter" << std::endl;
printVersion(wrapper);
std::cout << "------------------------------------------------------------------" << std::endl;
PModel model = wrapper->CreateModel();
std::string model_1 = "/mnt/usb-Generic-_SD_MMC_20120501030900000-0:0-part1/BACKUP/GEOMETRIES/nut_sample.stl";
std::string model_2 = "/mnt/usb-Generic-_SD_MMC_20120501030900000-0:0-part1/BACKUP/GEOMETRIES/OffsetAscii.stl";
auto mesh_1 = readSTL(model_1);
auto mesh_2 = readSTL(model_2);
std::cout << mesh_1.first.size() << " vertices, " << mesh_1.second.size() << " triangles in model 1." << std::endl;
std::cout << mesh_2.first.size() << " vertices, " << mesh_2.second.size() << " triangles in model 2." << std::endl;
PMeshObject meshObject1 = model->AddMeshObject();
meshObject1->SetName("Nut");
meshObject1->SetGeometry(mesh_1.first, mesh_1.second);
PMeshObject meshObject2 = model->AddMeshObject();
meshObject2->SetName("Offset");
meshObject2->SetGeometry(mesh_2.first, mesh_2.second);
// Create Component Object
PComponentsObject componentsObject = model->AddComponentsObject();
// Add components
componentsObject->AddComponent(meshObject1.get(), createTranslationMatrix(0.0f, 0.0f, 0.0f));
componentsObject->AddComponent(meshObject2.get(), createTranslationMatrix(40.0f, 60.0f, 80.0f));
componentsObject->AddComponent(meshObject1.get(), createTranslationMatrix(120.0f, 30.0f, 70.0f));
// Add components object as build item
model->AddBuildItem(componentsObject.get(), createTranslationMatrix(0.0f, 0.0f, 0.0f));
model->AddBuildItem(componentsObject.get(), createTranslationMatrix(200.0f, 40.0f, 10.0f));
model->AddBuildItem(meshObject2.get(), createTranslationMatrix(-40.0f, 0.0f, 20.0f));
// Output scene as 3MF and STL
PWriter _3mfWriter = model->QueryWriter("3mf");
std::cout << "Writing components.3mf..." << std::endl;
_3mfWriter->WriteToFile("components.3mf");
PWriter stlWriter = model->QueryWriter("stl");
std::cout << "Writing components.stl..." << std::endl;
stlWriter->WriteToFile("components.stl");
return 0;
}
In the meantime, I will try to check if there is any issues with your existing approach
@Suresh3d, I have discussed this, QueryReader("stl")
is not really designed for a workflow like yours. We will add a convenience function for Lib3MF in the future to address this. For now, I request you to use the example code I have shared.
@vijaiaeroastro thank you . I will go with your code for now .
@Suresh3d I am glad you found it useful. I will close the issue now.
@vijaiaeroastro Reading stl file using this code , SetGeometry(Positions, triangles);
throws exception ,
I have attached stl file here ,
Other stl files are working fine . only this stl is giving issue , can this ticket re-opened.
@Suresh3d I see the issue. It has non-manifold edges and some invalid triangles. I believe it's similar to this issue: #305.
However, it's unrealistic to expect every model to be perfect. Perhaps it can skip such faces and continue as suggested in the issue.
For now, I have fixed your geometry:
FailedPart_new.zip
I will keep this issue closed since it is not related to the code I provided.
@vijaiaeroastro Thank you . I agree to keep this issue remain closed . just to add to this later issue , I am thinking of doing validation on input stl before conversion , and fix the errors if there are any .Is there any open source or recommended way to validate and fix the stl errors ? I know this question is not related to 3mf conversion and more into fixing stl , but I need this utility to successfully use this lib3mf .
I quickly fixed the issue using meshlab. You can use a library like CGAL to programmatically fix issues in your mesh on the server side.
@vijaiaeroastro We have tool to validate mesh and fix the model . the attached file is the fixed model , it doesn't have any non-manifold geometries. but it still it gives same error .
@Suresh3d You still have issues. I am not sure what you are validating in your tool.
Check these areas
It should not have failed. It should skip those triangles and continue to add everything else. We cannot try to repair these for sure in lib3mf. But we can certainly provide some standalone CLI tools to help users with such issues in future.
@vijaiaeroastro seems you are checking my first model . My second file is different one . below is the screenshot from blender ,
It has some zero faces & edges but no Non-manifolds & self intersections . Programatically we are validating four things , closed , Facet Orientation , manifold , self intersecting , and this model is passing in all these four . Still if we get error means , we are missing something which is important. knowing it would be helpful to identify issues in future.
Right now can't it skip and proceed with creating 3mf ? is it future implementation ?
@Suresh3d Is there any custom attribute in this STL ?
I loaded the STL in another tool, rewrote it and then saved STL again and reloaded and it loaded. This is obviously a bug since no attributes are actually standardized in STL. Color is something that 3mf seems to handle. But if there are additional attributes, it should simply ignore and continue rather than failing like this.
Ignore my previous comment. I will update you here.
@Suresh3d I checked your mesh with lib3mf. Its exactly the same as the issue tagged in #305
INVALID_FACE : 271,273,273
INVALID_FACE : 273,304,273
INVALID_FACE : 34809,35604,35604
INVALID_FACE : 34793,35604,35604
INVALID_FACE : 35604,34809,35604
INVALID_FACE : 35604,34809,35604
INVALID_FACE : 35604,34791,35604
INVALID_FACE : 35604,35604,34825
INVALID_FACE : 34864,35604,35604
INVALID_FACE : 35604,34791,35604
Essentially all of these come out as zero area faces. I don't see this with Meshlab though. Maybe meshlab does some internal clean up of STL as they are loaded. I will discuss this today and update here.
@Suresh3d Use this readSTL
function instead
std::pair<std::vector<sLib3MFPosition>, std::vector<sLib3MFTriangle>>
readSTL(const std::string& filePath) {
std::ifstream file(filePath, std::ios::binary);
if (!file) {
throw std::runtime_error("Could not open STL file.");
}
// Lambda for the hash function
auto arrayHash = [](const std::array<double, 3>& arr) {
std::hash<double> hasher;
return hasher(arr[0]) ^ hasher(arr[1]) ^ hasher(arr[2]);
};
// Lambda to check for degenerate triangles
auto isDegenerateTriangle = [](const std::array<unsigned int, 3>& triangle) {
return (triangle[0] == triangle[1]) || (triangle[1] == triangle[2]) || (triangle[0] == triangle[2]);
};
// Lambda to read binary STL files
auto readBinarySTL = [&file, &arrayHash, &isDegenerateTriangle]() {
char header[80];
file.read(header, 80);
uint32_t numTriangles;
file.read(reinterpret_cast<char*>(&numTriangles), sizeof(numTriangles));
std::vector<std::array<double, 3>> vertices;
std::vector<std::array<unsigned int, 3>> triangles;
std::unordered_map<std::array<double, 3>, unsigned int, decltype(arrayHash)> vertexMap(0, arrayHash);
for (uint32_t i = 0; i < numTriangles; ++i) {
float normal[3];
file.read(reinterpret_cast<char*>(normal), 3 * sizeof(float));
std::array<unsigned int, 3> triangle;
for (int j = 0; j < 3; ++j) {
float vertex[3];
file.read(reinterpret_cast<char*>(vertex), 3 * sizeof(float));
std::array<double, 3> vertexArray = {vertex[0], vertex[1], vertex[2]};
auto it = vertexMap.find(vertexArray);
if (it == vertexMap.end()) {
unsigned int index = vertices.size();
vertices.push_back(vertexArray);
vertexMap[vertexArray] = index;
triangle[j] = index;
} else {
triangle[j] = it->second;
}
}
// Skip degenerate triangles
if (!isDegenerateTriangle(triangle)) {
triangles.push_back(triangle);
}
uint16_t attributeByteCount;
file.read(reinterpret_cast<char*>(&attributeByteCount), sizeof(attributeByteCount));
}
return std::make_pair(vertices, triangles);
};
// Lambda to read ASCII STL files
auto readAsciiSTL = [&file, &arrayHash, &isDegenerateTriangle]() {
std::vector<std::array<double, 3>> vertices;
std::vector<std::array<unsigned int, 3>> triangles;
std::unordered_map<std::array<double, 3>, unsigned int, decltype(arrayHash)> vertexMap(0, arrayHash);
std::string line;
std::array<unsigned int, 3> currentTriangle;
int vertexIndex = 0;
while (std::getline(file, line)) {
std::istringstream iss(line);
std::string keyword;
iss >> keyword;
if (keyword == "vertex") {
std::array<double, 3> vertex;
iss >> vertex[0] >> vertex[1] >> vertex[2];
auto it = vertexMap.find(vertex);
if (it == vertexMap.end()) {
unsigned int index = vertices.size();
vertices.push_back(vertex);
vertexMap[vertex] = index;
currentTriangle[vertexIndex] = index;
} else {
currentTriangle[vertexIndex] = it->second;
}
vertexIndex = (vertexIndex + 1) % 3;
if (vertexIndex == 0) {
// Skip degenerate triangles
if (!isDegenerateTriangle(currentTriangle)) {
triangles.push_back(currentTriangle);
}
}
}
}
return std::make_pair(vertices, triangles);
};
// Check if the file is binary or ASCII by inspecting the header
char header[80];
file.read(header, 80);
file.seekg(0, std::ios::beg);
std::pair<std::vector<std::array<double, 3>>, std::vector<std::array<unsigned int, 3>>> rawResult;
if (std::string(header, 5) == "solid") {
// Heuristic check to ensure it's ASCII (not guaranteed)
std::string line;
std::getline(file, line);
if (line.find("facet") != std::string::npos) {
file.seekg(0, std::ios::beg); // Reset if detected ASCII
rawResult = readAsciiSTL();
} else {
// Reset and treat it as binary STL
file.seekg(0, std::ios::beg);
rawResult = readBinarySTL();
}
} else {
// Binary STL
rawResult = readBinarySTL();
}
// Convert the result to sLib3MFPosition and sLib3MFTriangle
std::vector<sLib3MFPosition> positions;
for (const auto& vertex : rawResult.first) {
positions.push_back(fnCreateVertex(static_cast<float>(vertex[0]), static_cast<float>(vertex[1]), static_cast<float>(vertex[2])));
}
std::vector<sLib3MFTriangle> triangles;
for (const auto& triangle : rawResult.second) {
triangles.push_back(fnCreateTriangle(static_cast<int>(triangle[0]), static_cast<int>(triangle[1]), static_cast<int>(triangle[2])));
}
return std::make_pair(positions, triangles);
}
This checks and skips any degenerate triangles
@Suresh3d Please test and let know if this works for you. lib3mf does not check for any other combinatorial issues. Having this check in readSTL should let your mesh through.
@vijaiaeroastro sure , I will try this and let you know.