3MFConsortium / lib3mf

lib3mf is an implementation of the 3D Manufacturing Format file standard
http://3mf.io
BSD 2-Clause "Simplified" License
228 stars 92 forks source link

How to add multiple stl files to create 3mf assembly ? #364

Closed Suresh3d closed 4 days ago

Suresh3d commented 4 months ago

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 ,

  1. Read all the files using reader->ReadFromFile().
  2. GetBuildItems() from model and remove them .
  3. GetMeshObjects() from model , and iterate over the meshObjects 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 :

  1. 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.
  2. 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 .
  3. 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 ? image
vijaiaeroastro commented 3 months ago

@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?

Suresh3d commented 3 months ago

@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.

vijaiaeroastro commented 3 months ago

@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

vijaiaeroastro commented 3 months ago

@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.

Suresh3d commented 3 months ago

@vijaiaeroastro thank you . I will go with your code for now .

vijaiaeroastro commented 3 months ago

@Suresh3d I am glad you found it useful. I will close the issue now.

Suresh3d commented 2 weeks ago

@vijaiaeroastro Reading stl file using this code , SetGeometry(Positions, triangles); throws exception , image

I have attached stl file here ,

FailedPart.zip.

Other stl files are working fine . only this stl is giving issue , can this ticket re-opened.

vijaiaeroastro commented 2 weeks ago

@Suresh3d I see the issue. It has non-manifold edges and some invalid triangles. I believe it's similar to this issue: https://github.com/3MFConsortium/lib3mf/issues/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.

Suresh3d commented 2 weeks ago

@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 .

vijaiaeroastro commented 2 weeks ago

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.

Suresh3d commented 1 week ago

@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 .

fixedMesh.zip

vijaiaeroastro commented 1 week ago

@Suresh3d You still have issues. I am not sure what you are validating in your tool.

image

Check these areas

image

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.

Suresh3d commented 1 week ago

@vijaiaeroastro seems you are checking my first model . My second file is different one . below is the screenshot from blender , image

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 ?

vijaiaeroastro commented 1 week ago

@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.

vijaiaeroastro commented 1 week ago

Ignore my previous comment. I will update you here.

vijaiaeroastro commented 1 week ago

@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.

vijaiaeroastro commented 1 week ago

@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

vijaiaeroastro commented 1 week ago

@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.

Suresh3d commented 1 week ago

@vijaiaeroastro sure , I will try this and let you know.