Mesh improvements:
+ support of Simple Model Format (smf) + improve reading of STL files (reduce by ~70%)
This commit is contained in:
@@ -32,6 +32,8 @@
|
||||
|
||||
#include "Builder.h"
|
||||
#include "MeshKernel.h"
|
||||
#include "Functional.h"
|
||||
#include <QVector>
|
||||
|
||||
using namespace MeshCore;
|
||||
|
||||
@@ -253,3 +255,108 @@ void MeshBuilder::Finish (bool freeMemory)
|
||||
|
||||
_meshKernel.RecalcBoundBox();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
struct MeshFastBuilder::Private {
|
||||
struct Vertex
|
||||
{
|
||||
Vertex() : x(0), y(0), z(0), i(0) {}
|
||||
Vertex(float x, float y, float z) : x(x), y(y), z(z), i(0) {}
|
||||
|
||||
float x, y, z;
|
||||
size_t i;
|
||||
|
||||
bool operator!=(const Vertex& rhs) const
|
||||
{
|
||||
return x != rhs.x || y != rhs.y || z != rhs.z;
|
||||
}
|
||||
bool operator<(const Vertex& rhs) const
|
||||
{
|
||||
if (x != rhs.x) return x < rhs.x;
|
||||
else if (y != rhs.y) return y < rhs.y;
|
||||
else if (z != rhs.z) return z < rhs.z;
|
||||
else return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Hint: Using a QVector instead of std::vector is a bit faster
|
||||
QVector<Vertex> verts;
|
||||
};
|
||||
|
||||
MeshFastBuilder::MeshFastBuilder(MeshKernel &rclM) : _meshKernel(rclM), p(new Private)
|
||||
{
|
||||
}
|
||||
|
||||
MeshFastBuilder::~MeshFastBuilder(void)
|
||||
{
|
||||
delete p;
|
||||
}
|
||||
|
||||
void MeshFastBuilder::Initialize (unsigned long ctFacets)
|
||||
{
|
||||
p->verts.reserve(ctFacets * 3);
|
||||
}
|
||||
|
||||
void MeshFastBuilder::AddFacet (const Base::Vector3f* facetPoints)
|
||||
{
|
||||
Private::Vertex v;
|
||||
for (int i=0; i<3; i++) {
|
||||
v.x = facetPoints[i].x;
|
||||
v.y = facetPoints[i].y;
|
||||
v.z = facetPoints[i].z;
|
||||
p->verts.push_back(v);
|
||||
}
|
||||
}
|
||||
|
||||
void MeshFastBuilder::AddFacet (const MeshGeomFacet& facetPoints)
|
||||
{
|
||||
Private::Vertex v;
|
||||
for (int i=0; i<3; i++) {
|
||||
v.x = facetPoints._aclPoints[i].x;
|
||||
v.y = facetPoints._aclPoints[i].y;
|
||||
v.z = facetPoints._aclPoints[i].z;
|
||||
p->verts.push_back(v);
|
||||
}
|
||||
}
|
||||
|
||||
void MeshFastBuilder::Finish ()
|
||||
{
|
||||
QVector<Private::Vertex>& verts = p->verts;
|
||||
size_t ulCtPts = verts.size();
|
||||
for (size_t i=0; i < ulCtPts; ++i) {
|
||||
verts[i].i = i;
|
||||
}
|
||||
|
||||
//std::sort(verts.begin(), verts.end());
|
||||
int threads = std::max(1, QThread::idealThreadCount());
|
||||
MeshCore::parallel_sort(verts.begin(), verts.end(), std::less<Private::Vertex>(), threads);
|
||||
|
||||
QVector<unsigned long> indices(ulCtPts);
|
||||
|
||||
size_t vertex_count = 0;
|
||||
for (QVector<Private::Vertex>::iterator v = verts.begin(); v != verts.end(); ++v) {
|
||||
if (!vertex_count || *v != verts[vertex_count-1])
|
||||
verts[vertex_count++] = *v;
|
||||
|
||||
indices[v->i] = vertex_count - 1;
|
||||
}
|
||||
|
||||
size_t ulCt = verts.size()/3;
|
||||
MeshFacetArray rFacets(ulCt);
|
||||
for (size_t i=0; i < ulCt; ++i) {
|
||||
rFacets[i]._aulPoints[0] = indices[3*i];
|
||||
rFacets[i]._aulPoints[1] = indices[3*i + 1];
|
||||
rFacets[i]._aulPoints[2] = indices[3*i + 2];
|
||||
}
|
||||
|
||||
verts.resize(vertex_count);
|
||||
|
||||
MeshPointArray rPoints;
|
||||
rPoints.reserve(vertex_count);
|
||||
for (QVector<Private::Vertex>::iterator v = verts.begin(); v != verts.end(); ++v) {
|
||||
rPoints.push_back(MeshPoint(v->x, v->y, v->z));
|
||||
}
|
||||
|
||||
_meshKernel.Adopt(rPoints, rFacets, true);
|
||||
}
|
||||
|
||||
@@ -164,6 +164,52 @@ private:
|
||||
float _fSaveTolerance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Class for creating the mesh structure by adding facets. Building the structure needs 3 steps:
|
||||
* 1. initializing
|
||||
* 2. adding the facets
|
||||
* 3. finishing
|
||||
* \code
|
||||
* // Sample Code for building a mesh structure
|
||||
* MeshFastBuilder builder(someMeshReference);
|
||||
* builder.Initialize(numberOfFacets);
|
||||
* ...
|
||||
* for (...)
|
||||
* builder.AddFacet(...);
|
||||
* ...
|
||||
* builder.Finish();
|
||||
* \endcode
|
||||
* @author Werner Mayer
|
||||
*/
|
||||
class MeshExport MeshFastBuilder
|
||||
{
|
||||
private:
|
||||
MeshKernel& _meshKernel;
|
||||
|
||||
public:
|
||||
MeshFastBuilder(MeshKernel &rclM);
|
||||
~MeshFastBuilder(void);
|
||||
|
||||
/** Initializes the class. Must be done before adding facets
|
||||
* @param ctFacets count of facets.
|
||||
*/
|
||||
void Initialize (unsigned long ctFacets);
|
||||
/** Add new facet
|
||||
*/
|
||||
void AddFacet (const Base::Vector3f* facetPoints);
|
||||
/** Add new facet
|
||||
*/
|
||||
void AddFacet (const MeshGeomFacet& facetPoints);
|
||||
|
||||
/** Finishes building up the mesh structure. Must be done after adding facets.
|
||||
*/
|
||||
void Finish ();
|
||||
|
||||
private:
|
||||
struct Private;
|
||||
Private* p;
|
||||
};
|
||||
|
||||
} // namespace MeshCore
|
||||
|
||||
#endif
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "Helpers.h"
|
||||
#include "Grid.h"
|
||||
#include "TopoAlgorithm.h"
|
||||
#include "Functional.h"
|
||||
#include <Base/Matrix.h>
|
||||
|
||||
#include <Base/Sequencer.h>
|
||||
@@ -966,7 +967,9 @@ void MeshKernel::RebuildNeighbours (unsigned long index)
|
||||
}
|
||||
|
||||
// sort the edges
|
||||
std::sort(edges.begin(), edges.end(), Edge_Less());
|
||||
//std::sort(edges.begin(), edges.end(), Edge_Less());
|
||||
int threads = std::max(1, QThread::idealThreadCount());
|
||||
MeshCore::parallel_sort(edges.begin(), edges.end(), Edge_Less(), threads);
|
||||
|
||||
unsigned long p0 = ULONG_MAX, p1 = ULONG_MAX;
|
||||
unsigned long f0 = ULONG_MAX, f1 = ULONG_MAX;
|
||||
|
||||
64
src/Mod/Mesh/App/Core/Functional.h
Normal file
64
src/Mod/Mesh/App/Core/Functional.h
Normal file
@@ -0,0 +1,64 @@
|
||||
/***************************************************************************
|
||||
* Copyright (c) 2018 Werner Mayer <wmayer[at]users.sourceforge.net> *
|
||||
* *
|
||||
* This file is part of the FreeCAD CAx development system. *
|
||||
* *
|
||||
* This library is free software; you can redistribute it and/or *
|
||||
* modify it under the terms of the GNU Library General Public *
|
||||
* License as published by the Free Software Foundation; either *
|
||||
* version 2 of the License, or (at your option) any later version. *
|
||||
* *
|
||||
* This library is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* GNU Library General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the GNU Library General Public *
|
||||
* License along with this library; see the file COPYING.LIB. If not, *
|
||||
* write to the Free Software Foundation, Inc., 59 Temple Place, *
|
||||
* Suite 330, Boston, MA 02111-1307, USA *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
|
||||
#ifndef MESH_FUNCTIONAL_H
|
||||
#define MESH_FUNCTIONAL_H
|
||||
|
||||
#include <algorithm>
|
||||
#include <QtConcurrentRun>
|
||||
#include <QFuture>
|
||||
#include <QThread>
|
||||
|
||||
namespace MeshCore
|
||||
{
|
||||
template <class Iter, class Pred>
|
||||
static void parallel_sort(Iter begin, Iter end, Pred comp, int threads)
|
||||
{
|
||||
if (threads < 2 || end - begin < 2)
|
||||
{
|
||||
std::sort(begin, end, comp);
|
||||
}
|
||||
else
|
||||
{
|
||||
Iter mid = begin + (end - begin) / 2;
|
||||
if (threads == 2)
|
||||
{
|
||||
QFuture<void> future = QtConcurrent::run(parallel_sort<Iter, Pred>, begin, mid, comp, threads / 2);
|
||||
std::sort(mid, end, comp);
|
||||
future.waitForFinished();
|
||||
}
|
||||
else
|
||||
{
|
||||
QFuture<void> a = QtConcurrent::run(parallel_sort<Iter, Pred>, begin, mid, comp, threads / 2);
|
||||
QFuture<void> b = QtConcurrent::run(parallel_sort<Iter, Pred>, mid, end, comp, threads / 2);
|
||||
a.waitForFinished();
|
||||
b.waitForFinished();
|
||||
}
|
||||
std::inplace_merge(begin, mid, end, comp);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace MeshCore
|
||||
|
||||
|
||||
#endif // MESH_FUNCTIONAL_H
|
||||
@@ -171,6 +171,9 @@ bool MeshInput::LoadAny(const char* FileName)
|
||||
else if (fi.hasExtension("obj")) {
|
||||
ok = LoadOBJ( str );
|
||||
}
|
||||
else if (fi.hasExtension("smf")) {
|
||||
ok = LoadSMF( str );
|
||||
}
|
||||
else if (fi.hasExtension("off")) {
|
||||
ok = LoadOFF( str );
|
||||
}
|
||||
@@ -200,6 +203,8 @@ bool MeshInput::LoadFormat(std::istream &str, MeshIO::Format fmt)
|
||||
return LoadBinarySTL(str);
|
||||
case MeshIO::OBJ:
|
||||
return LoadOBJ(str);
|
||||
case MeshIO::SMF:
|
||||
return LoadSMF(str);
|
||||
case MeshIO::OFF:
|
||||
return LoadOFF(str);
|
||||
case MeshIO::IV:
|
||||
@@ -444,6 +449,65 @@ bool MeshInput::LoadOBJ (std::istream &rstrIn)
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Loads an SMF file. */
|
||||
bool MeshInput::LoadSMF (std::istream &rstrIn)
|
||||
{
|
||||
boost::regex rx_p("^v\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)"
|
||||
"\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)"
|
||||
"\\s+([-+]?[0-9]*)\\.?([0-9]+([eE][-+]?[0-9]+)?)\\s*$");
|
||||
boost::regex rx_f3("^f\\s+([-+]?[0-9]+)"
|
||||
"\\s+([-+]?[0-9]+)"
|
||||
"\\s+([-+]?[0-9]+)\\s*$");
|
||||
boost::cmatch what;
|
||||
|
||||
unsigned long segment=0;
|
||||
MeshPointArray meshPoints;
|
||||
MeshFacetArray meshFacets;
|
||||
|
||||
std::string line;
|
||||
float fX, fY, fZ;
|
||||
int i1=1,i2=1,i3=1;
|
||||
MeshFacet item;
|
||||
|
||||
if (!rstrIn || rstrIn.bad() == true)
|
||||
return false;
|
||||
|
||||
std::streambuf* buf = rstrIn.rdbuf();
|
||||
if (!buf)
|
||||
return false;
|
||||
|
||||
while (std::getline(rstrIn, line)) {
|
||||
if (boost::regex_match(line.c_str(), what, rx_p)) {
|
||||
fX = (float)std::atof(what[1].first);
|
||||
fY = (float)std::atof(what[4].first);
|
||||
fZ = (float)std::atof(what[7].first);
|
||||
meshPoints.push_back(MeshPoint(Base::Vector3f(fX, fY, fZ)));
|
||||
}
|
||||
else if (boost::regex_match(line.c_str(), what, rx_f3)) {
|
||||
// 3-vertex face
|
||||
i1 = std::atoi(what[1].first);
|
||||
i1 = i1 > 0 ? i1-1 : i1+static_cast<int>(meshPoints.size());
|
||||
i2 = std::atoi(what[2].first);
|
||||
i2 = i2 > 0 ? i2-1 : i2+static_cast<int>(meshPoints.size());
|
||||
i3 = std::atoi(what[3].first);
|
||||
i3 = i3 > 0 ? i3-1 : i3+static_cast<int>(meshPoints.size());
|
||||
item.SetVertices(i1,i2,i3);
|
||||
item.SetProperty(segment);
|
||||
meshFacets.push_back(item);
|
||||
}
|
||||
}
|
||||
|
||||
this->_rclMesh.Clear(); // remove all data before
|
||||
|
||||
MeshCleanup meshCleanup(meshPoints,meshFacets);
|
||||
meshCleanup.RemoveInvalids();
|
||||
MeshPointFacetAdjacency meshAdj(meshPoints.size(),meshFacets);
|
||||
meshAdj.SetFacetNeighbourhood();
|
||||
this->_rclMesh.Adopt(meshPoints,meshFacets);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Loads an OFF file. */
|
||||
bool MeshInput::LoadOFF (std::istream &rstrIn)
|
||||
{
|
||||
@@ -1164,7 +1228,11 @@ bool MeshInput::LoadAsciiSTL (std::istream &rstrIn)
|
||||
// restart from the beginning
|
||||
buf->pubseekoff(0, std::ios::beg, std::ios::in);
|
||||
|
||||
#if 0
|
||||
MeshBuilder builder(this->_rclMesh);
|
||||
#else
|
||||
MeshFastBuilder builder(this->_rclMesh);
|
||||
#endif
|
||||
builder.Initialize(ulFacetCt);
|
||||
|
||||
ulVertexCt = 0;
|
||||
@@ -1229,7 +1297,11 @@ bool MeshInput::LoadBinarySTL (std::istream &rstrIn)
|
||||
if (ulCt > ulFac)
|
||||
return false;// not a valid STL file
|
||||
|
||||
#if 0
|
||||
MeshBuilder builder(this->_rclMesh);
|
||||
#else
|
||||
MeshFastBuilder builder(this->_rclMesh);
|
||||
#endif
|
||||
builder.Initialize(ulCt);
|
||||
|
||||
for (uint32_t i = 0; i < ulCt; i++) {
|
||||
@@ -1637,6 +1709,9 @@ MeshIO::Format MeshOutput::GetFormat(const char* FileName)
|
||||
else if (file.hasExtension("amf")) {
|
||||
return MeshIO::AMF;
|
||||
}
|
||||
else if (file.hasExtension("smf")) {
|
||||
return MeshIO::SMF;
|
||||
}
|
||||
else {
|
||||
return MeshIO::Undefined;
|
||||
}
|
||||
@@ -1688,6 +1763,11 @@ bool MeshOutput::SaveAny(const char* FileName, MeshIO::Format format) const
|
||||
if (!SaveOBJ(str))
|
||||
throw Base::FileException("Export of OBJ mesh failed",FileName);
|
||||
}
|
||||
else if (fileformat == MeshIO::SMF) {
|
||||
// write file
|
||||
if (!SaveSMF(str))
|
||||
throw Base::FileException("Export of SMF mesh failed",FileName);
|
||||
}
|
||||
else if (fileformat == MeshIO::OFF) {
|
||||
// write file
|
||||
if (!SaveOFF(str))
|
||||
@@ -1769,6 +1849,8 @@ bool MeshOutput::SaveFormat(std::ostream &str, MeshIO::Format fmt) const
|
||||
return SaveBinarySTL(str);
|
||||
case MeshIO::OBJ:
|
||||
return SaveOBJ(str);
|
||||
case MeshIO::SMF:
|
||||
return SaveSMF(str);
|
||||
case MeshIO::OFF:
|
||||
return SaveOFF(str);
|
||||
case MeshIO::IDTF:
|
||||
@@ -2114,6 +2196,54 @@ bool MeshOutput::SaveMTL(std::ostream &out) const
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Saves an SMF file. */
|
||||
bool MeshOutput::SaveSMF (std::ostream &out) const
|
||||
{
|
||||
// http://people.sc.fsu.edu/~jburkardt/data/smf/smf.txt
|
||||
const MeshPointArray& rPoints = _rclMesh.GetPoints();
|
||||
const MeshFacetArray& rFacets = _rclMesh.GetFacets();
|
||||
|
||||
if (!out || out.bad() == true)
|
||||
return false;
|
||||
|
||||
Base::SequencerLauncher seq("saving...", _rclMesh.CountPoints() + _rclMesh.CountFacets());
|
||||
|
||||
// Header
|
||||
out << "#$SMF 1.0" << std::endl;
|
||||
out << "#$vertices " << rPoints.size() << std::endl;
|
||||
out << "#$faces " << rFacets.size() << std::endl;
|
||||
out << "#" << std::endl;
|
||||
out << "# Created by FreeCAD <http://www.freecadweb.org>" << std::endl;
|
||||
|
||||
out.precision(6);
|
||||
out.setf(std::ios::fixed | std::ios::showpoint);
|
||||
|
||||
// vertices
|
||||
Base::Vector3f pt;
|
||||
std::size_t index = 0;
|
||||
for (MeshPointArray::_TConstIterator it = rPoints.begin(); it != rPoints.end(); ++it, ++index) {
|
||||
if (this->apply_transform) {
|
||||
pt = this->_transform * *it;
|
||||
}
|
||||
else {
|
||||
pt.Set(it->x, it->y, it->z);
|
||||
}
|
||||
|
||||
out << "v " << pt.x << " " << pt.y << " " << pt.z << std::endl;
|
||||
seq.next(true); // allow to cancel
|
||||
}
|
||||
|
||||
// facet indices
|
||||
for (MeshFacetArray::_TConstIterator it = rFacets.begin(); it != rFacets.end(); ++it) {
|
||||
out << "f " << it->_aulPoints[0]+1 << " "
|
||||
<< it->_aulPoints[1]+1 << " "
|
||||
<< it->_aulPoints[2]+1 << std::endl;
|
||||
seq.next(true); // allow to cancel
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Saves an OFF file. */
|
||||
bool MeshOutput::SaveOFF (std::ostream &out) const
|
||||
{
|
||||
|
||||
@@ -56,7 +56,8 @@ namespace MeshIO {
|
||||
PLY,
|
||||
APLY,
|
||||
PY,
|
||||
AMF
|
||||
AMF,
|
||||
SMF
|
||||
};
|
||||
enum Binding {
|
||||
OVERALL,
|
||||
@@ -109,6 +110,8 @@ public:
|
||||
bool LoadBinarySTL (std::istream &rstrIn);
|
||||
/** Loads an OBJ Mesh file. */
|
||||
bool LoadOBJ (std::istream &rstrIn);
|
||||
/** Loads an SMF Mesh file. */
|
||||
bool LoadSMF (std::istream &rstrIn);
|
||||
/** Loads an OFF Mesh file. */
|
||||
bool LoadOFF (std::istream &rstrIn);
|
||||
/** Loads a PLY Mesh file. */
|
||||
@@ -170,6 +173,8 @@ public:
|
||||
bool SaveOBJ (std::ostream &rstrOut) const;
|
||||
/** Saves the materials of an OBJ file. */
|
||||
bool SaveMTL(std::ostream &rstrOut) const;
|
||||
/** Saves the mesh object into an SMF file. */
|
||||
bool SaveSMF (std::ostream &rstrOut) const;
|
||||
/** Saves the mesh object into an OFF file. */
|
||||
bool SaveOFF (std::ostream &rstrOut) const;
|
||||
/** Saves the mesh object into a binary PLY file. */
|
||||
|
||||
@@ -458,6 +458,7 @@ void MeshObject::swapKernel(MeshCore::MeshKernel& kernel,
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
#ifndef FC_DEBUG
|
||||
try {
|
||||
MeshCore::MeshEvalNeighbourhood nb(_kernel);
|
||||
@@ -477,6 +478,7 @@ void MeshObject::swapKernel(MeshCore::MeshKernel& kernel,
|
||||
Base::Console().Log("Check for defects in mesh data structure failed\n");
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void MeshObject::save(std::ostream& out) const
|
||||
|
||||
@@ -164,6 +164,7 @@ PyObject* MeshPy::read(PyObject *args, PyObject *kwds)
|
||||
ext["STL" ] = MeshCore::MeshIO::BSTL;
|
||||
ext["AST" ] = MeshCore::MeshIO::ASTL;
|
||||
ext["OBJ" ] = MeshCore::MeshIO::OBJ;
|
||||
ext["SMF" ] = MeshCore::MeshIO::SMF;
|
||||
ext["OFF" ] = MeshCore::MeshIO::OFF;
|
||||
ext["IV" ] = MeshCore::MeshIO::IV;
|
||||
ext["X3D" ] = MeshCore::MeshIO::X3D;
|
||||
@@ -212,6 +213,7 @@ PyObject* MeshPy::write(PyObject *args, PyObject *kwds)
|
||||
ext["STL" ] = MeshCore::MeshIO::BSTL;
|
||||
ext["AST" ] = MeshCore::MeshIO::ASTL;
|
||||
ext["OBJ" ] = MeshCore::MeshIO::OBJ;
|
||||
ext["SMF" ] = MeshCore::MeshIO::SMF;
|
||||
ext["OFF" ] = MeshCore::MeshIO::OFF;
|
||||
ext["IDTF"] = MeshCore::MeshIO::IDTF;
|
||||
ext["MGL" ] = MeshCore::MeshIO::MGL;
|
||||
|
||||
@@ -7,6 +7,7 @@ FreeCAD.addImportType("Binary Mesh (*.bms)","Mesh")
|
||||
FreeCAD.addImportType("Alias Mesh (*.obj)","Mesh")
|
||||
FreeCAD.addImportType("Object File Format Mesh (*.off)","Mesh")
|
||||
FreeCAD.addImportType("Stanford Triangle Mesh (*.ply)","Mesh")
|
||||
FreeCAD.addImportType("Simple Model Format (*.smf)","Mesh")
|
||||
|
||||
FreeCAD.addExportType("STL Mesh (*.stl *.ast)", "Mesh")
|
||||
FreeCAD.addExportType("Binary Mesh (*.bms)","Mesh")
|
||||
@@ -14,3 +15,4 @@ FreeCAD.addExportType("Alias Mesh (*.obj)","Mesh")
|
||||
FreeCAD.addExportType("Object File Format Mesh (*.off)","Mesh")
|
||||
FreeCAD.addExportType("Stanford Triangle Mesh (*.ply)","Mesh")
|
||||
FreeCAD.addExportType("Additive Manufacturing Format (*.amf)","Mesh")
|
||||
FreeCAD.addExportType("Simple Model Format (*.smf)","Mesh")
|
||||
|
||||
Reference in New Issue
Block a user