Merge remote-tracking branch 'origin/master' into c++11
4
cgal/data/self-intersecting.polygon
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
10,10,0
|
||||
0,10,0
|
||||
0,0,0
|
||||
-1,0,0
|
||||
|
|
@ -148,7 +148,7 @@ int main(int argc, char *argv[])
|
|||
}
|
||||
|
||||
std::vector<IndexedTriangle> triangles;
|
||||
bool ok = GeometryUtils::tessellatePolygonWithHoles(polyhole, triangles, normal);
|
||||
bool ok = GeometryUtils::tessellatePolygonWithHoles(&polyhole.vertices.front(), polyhole.faces, triangles, normal);
|
||||
std::cerr << "Tessellated into " << triangles.size() << " triangles" << std::endl;
|
||||
|
||||
IndexedTriangleMesh trimesh;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ macx:isEmpty(OPENSCAD_LIBDIR) {
|
|||
}
|
||||
|
||||
TARGET = polyhole-tessellator-libtess2
|
||||
CONFIG -= qt
|
||||
mac {
|
||||
CONFIG -= app_bundle
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ PACKAGES=(
|
|||
"qscintilla 2.8.4"
|
||||
# NB! For eigen, also update the path in the function
|
||||
# NB! For CGAL, also update the actual download URL in the function
|
||||
"cgal 4.5.1"
|
||||
"cgal 4.5.2"
|
||||
"glew 1.12.0"
|
||||
"gettext 0.19.4"
|
||||
"libffi 3.2.1"
|
||||
|
|
@ -463,8 +463,9 @@ build_cgal()
|
|||
cd $BASEDIR/src
|
||||
rm -rf CGAL-$version
|
||||
if [ ! -f CGAL-$version.tar.gz ]; then
|
||||
# 4.5.1
|
||||
curl -O https://gforge.inria.fr/frs/download.php/file/34400/CGAL-$version.tar.gz
|
||||
# 4.5.2
|
||||
curl -O https://gforge.inria.fr/frs/download.php/file/34512/CGAL-$version.tar.gz
|
||||
# 4.5.1 curl -O https://gforge.inria.fr/frs/download.php/file/34400/CGAL-$version.tar.gz
|
||||
# 4.5 curl -O https://gforge.inria.fr/frs/download.php/file/34149/CGAL-$version.tar.gz
|
||||
# 4.4 curl -O https://gforge.inria.fr/frs/download.php/file/33525/CGAL-$version.tar.gz
|
||||
# 4.3 curl -O https://gforge.inria.fr/frs/download.php/32994/CGAL-$version.tar.gz
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
#include "Reindexer.h"
|
||||
#include "grid.h"
|
||||
#include <boost/foreach.hpp>
|
||||
#include <boost/lexical_cast.hpp>
|
||||
#include <boost/unordered_map.hpp>
|
||||
|
||||
static void *stdAlloc(void* userData, unsigned int size) {
|
||||
TESS_NOTUSED(userData);
|
||||
|
|
@ -15,6 +17,147 @@ static void stdFree(void* userData, void* ptr) {
|
|||
free(ptr);
|
||||
}
|
||||
|
||||
typedef std::pair<int,int> IndexedEdge;
|
||||
|
||||
/*!
|
||||
Helper class for keeping track of edges in a mesh.
|
||||
Can probably be replaced with a proper HalfEdge mesh later on
|
||||
*/
|
||||
class EdgeDict {
|
||||
public:
|
||||
// Counts occurrences of edges
|
||||
typedef boost::unordered_map<IndexedEdge, int> IndexedEdgeDict;
|
||||
|
||||
EdgeDict() { }
|
||||
|
||||
void add(const IndexedFace &face) {
|
||||
for (size_t i=0;i<face.size();i++) {
|
||||
IndexedEdge e(face[(i+1)%face.size()], face[i]);
|
||||
if (this->count(e) > 0) this->remove(e);
|
||||
else this->add(e.second, e.first);
|
||||
}
|
||||
}
|
||||
|
||||
void remove(const IndexedTriangle &t) {
|
||||
for (int i=0;i<3;i++) {
|
||||
IndexedEdge e(t[i], t[(i+1)%3]);
|
||||
// If the edge exist, remove it
|
||||
if (this->count(e) > 0) this->remove(e);
|
||||
else this->add(e.second, e.first);
|
||||
}
|
||||
}
|
||||
|
||||
void add(const IndexedTriangle &t) {
|
||||
for (int i=0;i<3;i++) {
|
||||
IndexedEdge e(t[(i+1)%3], t[i]);
|
||||
// If an opposite edge exists, they cancel out
|
||||
if (this->count(e) > 0) this->remove(e);
|
||||
else this->add(e.second, e.first);
|
||||
}
|
||||
}
|
||||
|
||||
void add(int start, int end) {
|
||||
this->add(IndexedEdge(start,end));
|
||||
}
|
||||
|
||||
void add(const IndexedEdge &e) {
|
||||
this->edges[e]++;
|
||||
PRINTDB("add: (%d,%d)", e.first % e.second);
|
||||
}
|
||||
|
||||
void remove(int start, int end) {
|
||||
this->remove(IndexedEdge(start,end));
|
||||
}
|
||||
|
||||
void remove(const IndexedEdge &e) {
|
||||
this->edges[e]--;
|
||||
if (this->edges[e] == 0) this->edges.erase(e);
|
||||
PRINTDB("remove: (%d,%d)", e.first % e.second);
|
||||
}
|
||||
|
||||
int count(int start, int end) {
|
||||
return this->count(IndexedEdge(start, end));
|
||||
}
|
||||
|
||||
int count(const IndexedEdge &e) {
|
||||
IndexedEdgeDict::const_iterator it = this->edges.find(e);
|
||||
if (it != edges.end()) return it->second;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool empty() const { return this->edges.empty(); }
|
||||
|
||||
size_t size() const { return this->edges.size(); }
|
||||
|
||||
void print() const {
|
||||
BOOST_FOREACH(const IndexedEdgeDict::value_type &v, this->edges) {
|
||||
const IndexedEdge &e = v.first;
|
||||
PRINTDB(" (%d,%d)%s", e.first % e.second % ((v.second > 1) ? boost::lexical_cast<std::string>(v.second).c_str() : ""));
|
||||
}
|
||||
}
|
||||
|
||||
// Triangulate remaining loops and add to triangles
|
||||
void triangulateLoops(std::vector<IndexedTriangle> &triangles) {
|
||||
// First, look for self-intersections in edges
|
||||
boost::unordered_map<int, std::list<int> > v2e;
|
||||
boost::unordered_map<int, std::list<int> > v2e_reverse;
|
||||
|
||||
BOOST_FOREACH(const IndexedEdgeDict::value_type &v, this->edges) {
|
||||
const IndexedEdge &e = v.first;
|
||||
v2e[e.first].push_back(e.second);
|
||||
v2e_reverse[e.second].push_back(e.first);
|
||||
}
|
||||
|
||||
while (!v2e.empty()) {
|
||||
for (boost::unordered_map<int, std::list<int> >::iterator it = v2e.begin();
|
||||
it != v2e.end();
|
||||
it++) {
|
||||
if (it->second.size() == 1) { // First single vertex
|
||||
int vidx = it->first;
|
||||
int next = it->second.front();
|
||||
assert(v2e_reverse.find(vidx) != v2e_reverse.end());
|
||||
assert(!v2e_reverse[vidx].empty());
|
||||
int prev = v2e_reverse[vidx].front();
|
||||
|
||||
IndexedTriangle t(prev, vidx, next);
|
||||
PRINTDB("Clipping ear: %d %d %d", t[0] % t[1] % t[2]);
|
||||
triangles.push_back(t);
|
||||
// Remove the generated triangle from the original.
|
||||
// Add new boundary edges to the edge dict
|
||||
this->remove(t);
|
||||
|
||||
v2e.erase(vidx); // single vertex
|
||||
v2e_reverse.erase(vidx); // single vertex
|
||||
|
||||
// If next->prev exists, remove it, otherwise add prev->next
|
||||
std::list<int>::iterator v2eit = std::find(v2e[next].begin(), v2e[next].end(), prev);
|
||||
if (v2eit != v2e[next].end()) {
|
||||
v2e[next].erase(v2eit);
|
||||
v2e_reverse[prev].remove(next);
|
||||
}
|
||||
else {
|
||||
v2e[prev].push_back(next);
|
||||
v2e_reverse[next].push_back(prev);
|
||||
}
|
||||
if (v2e[next].empty()) v2e.erase(next);
|
||||
if (v2e_reverse[prev].empty()) v2e.erase(prev);
|
||||
|
||||
// Remove prev->vidx
|
||||
v2e[prev].remove(vidx);
|
||||
if (v2e[prev].empty()) v2e.erase(prev);
|
||||
v2e_reverse[next].remove(vidx);
|
||||
if (v2e_reverse[next].empty()) v2e_reverse.erase(next);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IndexedEdgeDict edges;
|
||||
};
|
||||
|
||||
|
||||
/*!
|
||||
Tessellates input contours into a triangle mesh.
|
||||
|
||||
|
|
@ -33,23 +176,33 @@ static void stdFree(void* userData, void* ptr) {
|
|||
|
||||
Returns true on error, false on success.
|
||||
*/
|
||||
bool GeometryUtils::tessellatePolygonWithHoles(const IndexedPolygons &polygons,
|
||||
bool GeometryUtils::tessellatePolygonWithHoles(const Vector3f *vertices,
|
||||
const std::vector<IndexedFace> &faces,
|
||||
std::vector<IndexedTriangle> &triangles,
|
||||
const Vector3f *normal)
|
||||
{
|
||||
// Algorithm outline:
|
||||
// o Remove consecutive equal vertices and null ears (i.e. 23,24,23)
|
||||
// o Ignore polygons with < 3 vertices
|
||||
// o Pass-through polygons with 3 vertices
|
||||
// o Pass polygon to libtess2
|
||||
// o Postprocess to clean up misbehaviors in libtess2
|
||||
|
||||
// No polygon. FIXME: Will this ever happen or can we assert here?
|
||||
if (polygons.faces.empty()) return false;
|
||||
if (faces.empty()) return false;
|
||||
|
||||
// Remove consecutive equal vertices, as well as null ears
|
||||
std::vector<IndexedFace> faces = polygons.faces;
|
||||
BOOST_FOREACH(IndexedFace &face, faces) {
|
||||
int i=0;
|
||||
while (i < face.size()) {
|
||||
std::vector<IndexedFace> cleanfaces = faces;
|
||||
BOOST_FOREACH(IndexedFace &face, cleanfaces) {
|
||||
size_t i=0;
|
||||
while (face.size() >= 3 && i < face.size()) {
|
||||
if (face[i] == face[(i+1)%face.size()]) { // Two consecutively equal indices
|
||||
face.erase(face.begin()+i);
|
||||
}
|
||||
else if (face[i] == face[(i+2)%face.size()]) { // Null ear
|
||||
face.erase(face.begin() + (i+1)%face.size());
|
||||
else if (face[(i+face.size()-1)%face.size()] == face[(i+1)%face.size()]) { // Null ear
|
||||
if (i == 0) face.erase(face.begin() + i, face.begin() + i + 2);
|
||||
else face.erase(face.begin() + i - 1, face.begin() + i + 1);
|
||||
i--;
|
||||
}
|
||||
else {
|
||||
i++;
|
||||
|
|
@ -57,22 +210,30 @@ bool GeometryUtils::tessellatePolygonWithHoles(const IndexedPolygons &polygons,
|
|||
}
|
||||
}
|
||||
// First polygon has < 3 points - no output
|
||||
if (faces[0].size() < 3) return false;
|
||||
if (cleanfaces[0].size() < 3) return false;
|
||||
// Remove collapsed holes
|
||||
for (int i=1;i<faces.size();i++) {
|
||||
if (faces[i].size() < 3) {
|
||||
faces.erase(faces.begin() + i);
|
||||
for (size_t i=1;i<cleanfaces.size();i++) {
|
||||
if (cleanfaces[i].size() < 3) {
|
||||
cleanfaces.erase(cleanfaces.begin() + i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (faces.size() == 1 && faces[0].size() == 3) {
|
||||
if (cleanfaces.size() == 1 && cleanfaces[0].size() == 3) {
|
||||
// Input polygon has 3 points. shortcut tessellation.
|
||||
triangles.push_back(IndexedTriangle(faces[0][0], faces[0][1], faces[0][2]));
|
||||
//PRINTDB(" tri: %d %d %d", cleanfaces[0][0] % cleanfaces[0][1] % cleanfaces[0][2]);
|
||||
triangles.push_back(IndexedTriangle(cleanfaces[0][0], cleanfaces[0][1], cleanfaces[0][2]));
|
||||
return false;
|
||||
}
|
||||
|
||||
const Vector3f *verts = &polygons.vertices.front();
|
||||
// Build edge dict.
|
||||
// This contains all edges in the original polygon.
|
||||
// To maintain connectivity, all these edges must exist in the output.
|
||||
EdgeDict edges;
|
||||
BOOST_FOREACH(IndexedFace &face, cleanfaces) {
|
||||
edges.add(face);
|
||||
}
|
||||
|
||||
TESSreal *normalvec = NULL;
|
||||
TESSreal passednormal[3];
|
||||
if (normal) {
|
||||
|
|
@ -97,10 +258,10 @@ bool GeometryUtils::tessellatePolygonWithHoles(const IndexedPolygons &polygons,
|
|||
// Since libtess2's indices is based on the running number of points added, we need to map back
|
||||
// to our indices. allindices does the mapping.
|
||||
std::vector<int> allindices;
|
||||
BOOST_FOREACH(const IndexedFace &face, faces) {
|
||||
BOOST_FOREACH(const IndexedFace &face, cleanfaces) {
|
||||
contour.clear();
|
||||
BOOST_FOREACH(int idx, face) {
|
||||
const Vector3f &v = verts[idx];
|
||||
const Vector3f &v = vertices[idx];
|
||||
contour.push_back(v[0]);
|
||||
contour.push_back(v[1]);
|
||||
contour.push_back(v[2]);
|
||||
|
|
@ -125,10 +286,10 @@ bool GeometryUtils::tessellatePolygonWithHoles(const IndexedPolygons &polygons,
|
|||
vertices for intersecting edges, we need to detect these and
|
||||
insert dummy triangles to maintain external connectivity.
|
||||
|
||||
FIXME: This currently only works for polygons without holes.
|
||||
In addition, libtess2 may generate flipped triangles, i.e. triangles
|
||||
where the edge direction is reversed compared to the input polygon.
|
||||
This will also destroy connectivity and we need to flip those back.
|
||||
*/
|
||||
if (faces.size() == 1) { // Only works for polygons without holes
|
||||
|
||||
/*
|
||||
Algorithm:
|
||||
A) Collect all triangles using _only_ existing vertices -> triangles
|
||||
|
|
@ -139,62 +300,93 @@ bool GeometryUtils::tessellatePolygonWithHoles(const IndexedPolygons &polygons,
|
|||
std::vector<int> vflags(inputSize); // Inits with 0's
|
||||
|
||||
IndexedTriangle tri;
|
||||
IndexedTriangle mappedtri;
|
||||
for (int t=0;t<numelems;t++) {
|
||||
bool err = false;
|
||||
mappedtri.fill(-1);
|
||||
for (int i=0;i<3;i++) {
|
||||
int vidx = vindices[elements[t*3 + i]];
|
||||
if (vidx == TESS_UNDEF) err = true;
|
||||
else tri[i] = vidx; // A)
|
||||
if (vidx == TESS_UNDEF) {
|
||||
err = true;
|
||||
}
|
||||
else {
|
||||
tri[i] = vidx; // A)
|
||||
mappedtri[i] = allindices[vidx];
|
||||
}
|
||||
}
|
||||
PRINTDB("%d (%d) %d (%d) %d (%d)",
|
||||
elements[t*3 + 0] % allindices[vindices[elements[t*3 + 0]]] %
|
||||
elements[t*3 + 1] % allindices[vindices[elements[t*3 + 1]]] %
|
||||
elements[t*3 + 2] % allindices[vindices[elements[t*3 + 2]]]);
|
||||
elements[t*3 + 0] % mappedtri[0] %
|
||||
elements[t*3 + 1] % mappedtri[1] %
|
||||
elements[t*3 + 2] % mappedtri[2]);
|
||||
// FIXME: We ignore self-intersecting triangles rather than detecting and handling this
|
||||
if (!err) {
|
||||
vflags[tri[0]]++; // B)
|
||||
vflags[tri[1]]++;
|
||||
vflags[tri[2]]++;
|
||||
triangles.push_back(IndexedTriangle(allindices[tri[0]], allindices[tri[1]], allindices[tri[2]]));
|
||||
|
||||
// For each edge in mappedtri, locate the opposite edge in the original polygon.
|
||||
// If an opposite edge was found, we need to flip.
|
||||
// In addition, remove each edge from the dict to be able to later find
|
||||
// missing edges.
|
||||
// Note: In some degenerate cases, we create triangles with mixed edge directions.
|
||||
// In this case, don't reverse, but attempt to carry on
|
||||
bool reverse = false;
|
||||
for (int i=0;i<3;i++) {
|
||||
const IndexedEdge e(mappedtri[i], mappedtri[(i+1)%3]);
|
||||
if (edges.count(e) > 0) {
|
||||
reverse = false;
|
||||
break;
|
||||
}
|
||||
else if (edges.count(e.second, e.first) > 0) {
|
||||
reverse = true;
|
||||
}
|
||||
}
|
||||
if (reverse) {
|
||||
mappedtri.reverseInPlace();
|
||||
PRINTDB(" reversed: %d %d %d",
|
||||
mappedtri[0] % mappedtri[1] % mappedtri[2]);
|
||||
}
|
||||
|
||||
// Remove the generated triangle from the original.
|
||||
// Add new boundary edges to the edge dict
|
||||
edges.remove(mappedtri);
|
||||
triangles.push_back(mappedtri);
|
||||
}
|
||||
}
|
||||
|
||||
if (!edges.empty()) {
|
||||
PRINTDB(" %d edges remaining after main triangulation", edges.size());
|
||||
edges.print();
|
||||
|
||||
// Collect loops from remaining edges and triangulate loops manually
|
||||
edges.triangulateLoops(triangles);
|
||||
|
||||
if (!edges.empty()) {
|
||||
PRINTDB(" %d edges remaining after loop triangulation", edges.size());
|
||||
edges.print();
|
||||
}
|
||||
}
|
||||
#if 0
|
||||
for (int i=0;i<inputSize;i++) {
|
||||
if (!vflags[i]) { // vertex missing in output: C)
|
||||
int starti = (i+inputSize-1)%inputSize;
|
||||
int j;
|
||||
PRINTD(" Fanning left-out vertices");
|
||||
for (j = i; j < inputSize && !vflags[j]; j++) {
|
||||
// Create triangle fan from vertex i-1 to the first existing vertex
|
||||
PRINTDB("(%d) (%d) (%d)\n", allindices[starti] % allindices[j] % allindices[((j+1)%inputSize)]);
|
||||
PRINTDB(" (%d) (%d) (%d)\n", allindices[starti] % allindices[j] % allindices[((j+1)%inputSize)]);
|
||||
tri[0] = allindices[starti];
|
||||
tri[1] = allindices[j];
|
||||
tri[2] = allindices[(j+1)%inputSize];
|
||||
vflags[tri[0]]++;
|
||||
vflags[tri[1]]++;
|
||||
vflags[tri[2]]++;
|
||||
triangles.push_back(tri);
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
IndexedTriangle tri;
|
||||
for (int t=0;t<numelems;t++) {
|
||||
bool err = false;
|
||||
for (int i=0;i<3;i++) {
|
||||
int vidx = vindices[elements[t*3 + i]];
|
||||
if (vidx == TESS_UNDEF) err = true;
|
||||
else tri[i] = allindices[vidx];
|
||||
}
|
||||
PRINTDB("%d (%d) %d (%d) %d (%d)",
|
||||
elements[t*3 + 0] % allindices[vindices[elements[t*3 + 0]]] %
|
||||
elements[t*3 + 1] % allindices[vindices[elements[t*3 + 1]]] %
|
||||
elements[t*3 + 2] % allindices[vindices[elements[t*3 + 2]]]);
|
||||
// FIXME: We ignore self-intersecting triangles rather than detecting and handling this
|
||||
if (!err) {
|
||||
triangles.push_back(tri);
|
||||
}
|
||||
else PRINT("WARNING: Self-intersecting polygon encountered - ignoring");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
tessDeleteTess(tess);
|
||||
|
||||
|
|
@ -209,19 +401,18 @@ bool GeometryUtils::tessellatePolygon(const Polygon &polygon, Polygons &triangle
|
|||
{
|
||||
bool err = false;
|
||||
Reindexer<Vector3f> uniqueVertices;
|
||||
IndexedPolygons indexedpolygons;
|
||||
indexedpolygons.faces.push_back(IndexedFace());
|
||||
IndexedFace &currface = indexedpolygons.faces.back();
|
||||
std::vector<IndexedFace> indexedfaces;
|
||||
indexedfaces.push_back(IndexedFace());
|
||||
IndexedFace &currface = indexedfaces.back();
|
||||
BOOST_FOREACH (const Vector3d &v, polygon) {
|
||||
int idx = uniqueVertices.lookup(v.cast<float>());
|
||||
if (currface.empty() || idx != currface.back()) currface.push_back(idx);
|
||||
}
|
||||
if (currface.front() == currface.back()) currface.pop_back();
|
||||
if (currface.size() >= 3) { // Cull empty triangles
|
||||
uniqueVertices.copy(std::back_inserter(indexedpolygons.vertices));
|
||||
const Vector3f *verts = uniqueVertices.getArray();
|
||||
std::vector<IndexedTriangle> indexedtriangles;
|
||||
err = tessellatePolygonWithHoles(indexedpolygons, indexedtriangles, normal);
|
||||
Vector3f *verts = &indexedpolygons.vertices.front();
|
||||
err = tessellatePolygonWithHoles(verts, indexedfaces, indexedtriangles, normal);
|
||||
BOOST_FOREACH(const IndexedTriangle &t, indexedtriangles) {
|
||||
triangles.push_back(Polygon());
|
||||
Polygon &p = triangles.back();
|
||||
|
|
@ -232,3 +423,36 @@ bool GeometryUtils::tessellatePolygon(const Polygon &polygon, Polygons &triangle
|
|||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
int GeometryUtils::findUnconnectedEdges(const std::vector<std::vector<IndexedFace> > &polygons)
|
||||
{
|
||||
EdgeDict edges;
|
||||
BOOST_FOREACH(const std::vector<IndexedFace> &faces, polygons) {
|
||||
BOOST_FOREACH(const IndexedFace &face, faces) {
|
||||
edges.add(face);
|
||||
}
|
||||
}
|
||||
#if 1 // for debugging
|
||||
if (!edges.empty()) {
|
||||
PRINTD("Unconnected:");
|
||||
edges.print();
|
||||
}
|
||||
#endif
|
||||
return edges.size();
|
||||
}
|
||||
|
||||
int GeometryUtils::findUnconnectedEdges(const std::vector<IndexedTriangle> &triangles)
|
||||
{
|
||||
EdgeDict edges;
|
||||
BOOST_FOREACH(const IndexedTriangle &t, triangles) {
|
||||
edges.add(t);
|
||||
}
|
||||
#if 1 // for debugging
|
||||
if (!edges.empty()) {
|
||||
PRINTD("Unconnected:");
|
||||
edges.print();
|
||||
}
|
||||
#endif
|
||||
|
||||
return edges.size();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,21 @@ struct IndexedTriangleMesh {
|
|||
std::vector<IndexedTriangle> triangles;
|
||||
};
|
||||
|
||||
// Indexed polygon mesh, where each polygon can have holes
|
||||
struct IndexedPolyMesh {
|
||||
std::vector<Vector3f> vertices;
|
||||
std::vector<std::vector<IndexedFace> > polygons;
|
||||
};
|
||||
|
||||
namespace GeometryUtils {
|
||||
bool tessellatePolygon(const Polygon &polygon, Polygons &triangles,
|
||||
bool tessellatePolygon(const Polygon &polygon,
|
||||
Polygons &triangles,
|
||||
const Vector3f *normal = NULL);
|
||||
bool tessellatePolygonWithHoles(const IndexedPolygons &polygons, std::vector<IndexedTriangle> &triangles,
|
||||
bool tessellatePolygonWithHoles(const Vector3f *vertices,
|
||||
const std::vector<IndexedFace> &faces,
|
||||
std::vector<IndexedTriangle> &triangles,
|
||||
const Vector3f *normal = NULL);
|
||||
|
||||
int findUnconnectedEdges(const std::vector<std::vector<IndexedFace> > &polygons);
|
||||
int findUnconnectedEdges(const std::vector<IndexedTriangle> &triangles);
|
||||
}
|
||||
|
|
|
|||
133
src/cgalutils.cc
|
|
@ -1025,7 +1025,7 @@ namespace CGALUtils {
|
|||
return err;
|
||||
}
|
||||
#endif
|
||||
#if 1
|
||||
#if 0
|
||||
bool createPolySetFromNefPolyhedron3(const CGAL_Nef_polyhedron3 &N, PolySet &ps)
|
||||
{
|
||||
bool err = false;
|
||||
|
|
@ -1092,6 +1092,137 @@ namespace CGALUtils {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if 1
|
||||
bool createPolySetFromNefPolyhedron3(const CGAL_Nef_polyhedron3 &N, PolySet &ps)
|
||||
{
|
||||
// 1. Build Indexed PolyMesh
|
||||
// 2. Validate mesh (manifoldness)
|
||||
// 3. Triangulate each face
|
||||
// -> IndexedTriangleMesh
|
||||
// 4. Validate mesh (manifoldness)
|
||||
// 5. Create PolySet
|
||||
|
||||
bool err = false;
|
||||
|
||||
// 1. Build Indexed PolyMesh
|
||||
Reindexer<Vector3f> allVertices;
|
||||
std::vector<std::vector<IndexedFace> > polygons;
|
||||
|
||||
CGAL_Nef_polyhedron3::Halffacet_const_iterator hfaceti;
|
||||
CGAL_forall_halffacets(hfaceti, N) {
|
||||
CGAL::Plane_3<CGAL_Kernel3> plane(hfaceti->plane());
|
||||
// Since we're downscaling to float, vertices might merge during this conversion.
|
||||
// To avoid passing equal vertices to the tessellator, we remove consecutively identical
|
||||
// vertices.
|
||||
polygons.push_back(std::vector<IndexedFace>());
|
||||
std::vector<IndexedFace> &faces = polygons.back();
|
||||
// the 0-mark-volume is the 'empty' volume of space. skip it.
|
||||
if (!hfaceti->incident_volume()->mark()) {
|
||||
CGAL_Nef_polyhedron3::Halffacet_cycle_const_iterator cyclei;
|
||||
CGAL_forall_facet_cycles_of(cyclei, hfaceti) {
|
||||
CGAL_Nef_polyhedron3::SHalfedge_around_facet_const_circulator c1(cyclei);
|
||||
CGAL_Nef_polyhedron3::SHalfedge_around_facet_const_circulator c2(c1);
|
||||
faces.push_back(IndexedFace());
|
||||
IndexedFace &currface = faces.back();
|
||||
CGAL_For_all(c1, c2) {
|
||||
CGAL_Point_3 p = c1->source()->center_vertex()->point();
|
||||
// Create vertex indices and remove consecutive duplicate vertices
|
||||
int idx = allVertices.lookup(vector_convert<Vector3f>(p));
|
||||
if (currface.empty() || idx != currface.back()) currface.push_back(idx);
|
||||
}
|
||||
if (currface.front() == currface.back()) currface.pop_back();
|
||||
if (currface.size() < 3) faces.pop_back(); // Cull empty triangles
|
||||
}
|
||||
}
|
||||
if (faces.empty()) polygons.pop_back(); // Cull empty faces
|
||||
}
|
||||
|
||||
// 2. Validate mesh (manifoldness)
|
||||
int unconnected = GeometryUtils::findUnconnectedEdges(polygons);
|
||||
if (unconnected > 0) {
|
||||
PRINTB("Error: Non-manifold mesh encountered: %d unconnected edges", unconnected);
|
||||
}
|
||||
// 3. Triangulate each face
|
||||
const Vector3f *verts = allVertices.getArray();
|
||||
std::vector<IndexedTriangle> allTriangles;
|
||||
BOOST_FOREACH(const std::vector<IndexedFace> &faces, polygons) {
|
||||
#if 0 // For debugging
|
||||
std::cerr << "---\n";
|
||||
BOOST_FOREACH(const IndexedFace &poly, faces) {
|
||||
BOOST_FOREACH(int i, poly) {
|
||||
std::cerr << i << " ";
|
||||
}
|
||||
std::cerr << "\n";
|
||||
}
|
||||
#if 0
|
||||
std::cerr.precision(20);
|
||||
BOOST_FOREACH(const IndexedFace &poly, faces) {
|
||||
BOOST_FOREACH(int i, poly) {
|
||||
std::cerr << verts[i][0] << "," << verts[i][1] << "," << verts[i][2] << "\n";
|
||||
}
|
||||
std::cerr << "\n";
|
||||
}
|
||||
#endif
|
||||
std::cerr << "-\n";
|
||||
#endif
|
||||
#if 0 // For debugging
|
||||
std::cerr.precision(20);
|
||||
for (int i=0;i<allVertices.size();i++) {
|
||||
std::cerr << verts[i][0] << ", " << verts[i][1] << ", " << verts[i][2] << "\n";
|
||||
}
|
||||
#endif
|
||||
|
||||
/* at this stage, we have a sequence of polygons. the first
|
||||
is the "outside edge' or 'body' or 'border', and the rest of the
|
||||
polygons are 'holes' within the first. there are several
|
||||
options here to get rid of the holes. we choose to go ahead
|
||||
and let the tessellater deal with the holes, and then
|
||||
just output the resulting 3d triangles*/
|
||||
|
||||
// We cannot trust the plane from Nef polyhedron to be correct.
|
||||
// Passing an incorrect normal vector can cause a crash in the constrained delaunay triangulator
|
||||
// See http://cgal-discuss.949826.n4.nabble.com/Nef3-Wrong-normal-vector-reported-causes-triangulator-crash-tt4660282.html
|
||||
// CGAL::Vector_3<CGAL_Kernel3> nvec = plane.orthogonal_vector();
|
||||
// K::Vector_3 normal(CGAL::to_double(nvec.x()), CGAL::to_double(nvec.y()), CGAL::to_double(nvec.z()));
|
||||
std::vector<IndexedTriangle> triangles;
|
||||
bool err = GeometryUtils::tessellatePolygonWithHoles(verts, faces, triangles, NULL);
|
||||
if (!err) {
|
||||
BOOST_FOREACH(const IndexedTriangle &t, triangles) {
|
||||
assert(t[0] >= 0 && t[0] < allVertices.size());
|
||||
assert(t[1] >= 0 && t[1] < allVertices.size());
|
||||
assert(t[2] >= 0 && t[2] < allVertices.size());
|
||||
allTriangles.push_back(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if 0 // For debugging
|
||||
BOOST_FOREACH(const IndexedTriangle &t, allTriangles) {
|
||||
std::cerr << t[0] << " " << t[1] << " " << t[2] << "\n";
|
||||
}
|
||||
#endif
|
||||
// 4. Validate mesh (manifoldness)
|
||||
int unconnected2 = GeometryUtils::findUnconnectedEdges(allTriangles);
|
||||
if (unconnected2 > 0) {
|
||||
PRINTB("Error: Non-manifold triangle mesh created: %d unconnected edges", unconnected2);
|
||||
}
|
||||
|
||||
BOOST_FOREACH(const IndexedTriangle &t, allTriangles) {
|
||||
ps.append_poly();
|
||||
ps.append_vertex(verts[t[0]]);
|
||||
ps.append_vertex(verts[t[1]]);
|
||||
ps.append_vertex(verts[t[2]]);
|
||||
}
|
||||
|
||||
#if 0 // For debugging
|
||||
std::cerr.precision(20);
|
||||
for (int i=0;i<allVertices.size();i++) {
|
||||
std::cerr << verts[i][0] << ", " << verts[i][1] << ", " << verts[i][2] << "\n";
|
||||
}
|
||||
#endif
|
||||
|
||||
return err;
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
#include <boost/algorithm/string.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/circular_buffer.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
namespace fs = boost::filesystem;
|
||||
#include "boosty.h"
|
||||
|
||||
std::list<std::string> print_messages_stack;
|
||||
OutputHandlerFunc *outputhandler = NULL;
|
||||
|
|
@ -52,7 +55,7 @@ void PRINT_NOCACHE(const std::string &msg)
|
|||
if (msg.empty()) return;
|
||||
|
||||
if (boost::starts_with(msg, "WARNING") || boost::starts_with(msg, "ERROR")) {
|
||||
int i;
|
||||
size_t i;
|
||||
for (i=0;i<lastmessages.size();i++) {
|
||||
if (lastmessages[i] != msg) break;
|
||||
}
|
||||
|
|
@ -71,19 +74,13 @@ void PRINTDEBUG(const std::string &filename, const std::string &msg)
|
|||
{
|
||||
// see printutils.h for usage instructions
|
||||
if (OpenSCAD::debug=="") return;
|
||||
std::string fname(filename);
|
||||
std::string lowdebug( OpenSCAD::debug );
|
||||
boost::replace_all( fname, "src/", "" );
|
||||
std::string shortfname(fname);
|
||||
boost::replace_all( shortfname, ".cc", "");
|
||||
boost::replace_all( shortfname, ".h", "");
|
||||
boost::replace_all( shortfname, ".hpp", "");
|
||||
std::string lowshortfname( shortfname );
|
||||
boost::algorithm::to_lower( lowshortfname );
|
||||
boost::algorithm::to_lower( lowdebug );
|
||||
if (OpenSCAD::debug=="all") {
|
||||
PRINT_NOCACHE( shortfname+": "+ msg );
|
||||
} else if (lowdebug.find(lowshortfname) != std::string::npos) {
|
||||
std::string shortfname = boosty::stringy(fs::path(filename).stem());
|
||||
std::string lowshortfname(shortfname);
|
||||
boost::algorithm::to_lower(lowshortfname);
|
||||
std::string lowdebug(OpenSCAD::debug);
|
||||
boost::algorithm::to_lower(lowdebug);
|
||||
if (OpenSCAD::debug=="all" ||
|
||||
lowdebug.find(lowshortfname) != std::string::npos) {
|
||||
PRINT_NOCACHE( shortfname+": "+ msg );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
testdata/scad/bugs/issue1215.scad
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
difference() {
|
||||
rotate([90,0,90])
|
||||
linear_extrude(height=10)
|
||||
polygon(points=[[-4,0],[0,0],[0,3],[-3.5,3],[-4,2]]);
|
||||
|
||||
rotate([90,0,0])
|
||||
linear_extrude(height=4)
|
||||
polygon(points=[[0,3],[0.5,3],[0,2]]);
|
||||
|
||||
}
|
||||
8
testdata/scad/bugs/issue1215b.scad
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
module dieShape(out) {
|
||||
rotate([-90,90,0]) cylinder(r=10,h=15+out, $fn=4);
|
||||
rotate([0,-90,0]) cylinder(r=10,h=15+out, $fn=4);
|
||||
rotate([0,0,0]) cylinder(r=10,h=15+out, $fn=4);
|
||||
}
|
||||
|
||||
hull() dieShape(0);
|
||||
dieShape(.5);
|
||||
7
testdata/scad/bugs/issue1215c.scad
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
difference(){
|
||||
union(){
|
||||
translate([0,0,-1.5]) cylinder(h=9, r=26, center=true);
|
||||
translate([0.0, 0.0, 6.0]) rotate([180, 0, 0]) cylinder(h=3, r1=25, r2=26);
|
||||
}
|
||||
cylinder(h=14, r=23.0, center=true);
|
||||
}
|
||||
1
tests/.gitignore
vendored
|
|
@ -3,6 +3,7 @@
|
|||
/*-output
|
||||
/*.scad
|
||||
/*.png
|
||||
/*.pyc
|
||||
out.*
|
||||
/CMakeFiles
|
||||
/CMakeCache.txt
|
||||
|
|
|
|||
|
|
@ -1140,9 +1140,8 @@ set_test_config(Bugs dxfpngtest_text-font-direction-tests
|
|||
csgpngtest_text-font-direction-tests
|
||||
throwntogethertest_text-font-direction-tests)
|
||||
|
||||
list(APPEND EXPORT3D_TEST_FILES ${EXAMPLE_3D_FILES})
|
||||
list(APPEND EXPORT3D_TEST_FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/3D/features/polyhedron-nonplanar-tests.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/3D/features/polyhedron-tests.scad
|
||||
list(APPEND EXPORT3D_CGALCGAL_TEST_FILES ${EXAMPLE_3D_FILES})
|
||||
list(APPEND EXPORT3D_CGALCGAL_TEST_FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/3D/features/polyhedron-nonplanar-tests.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/3D/features/rotate_extrude-tests.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/3D/features/union-coincident-test.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/3D/features/mirror-tests.scad
|
||||
|
|
@ -1151,11 +1150,24 @@ list(APPEND EXPORT3D_TEST_FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/3D/features
|
|||
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/internal-cavity-polyhedron.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/bad-stl-pcbvicebar.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/bad-stl-tardis.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/bad-stl-wing.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/rotate_extrude-hole.scad)
|
||||
# Issue #910
|
||||
set_test_config(Bugs
|
||||
offcgalpngtest_polyhedron-tests
|
||||
offpngtest_nonmanifold-polyhedron
|
||||
offcgalpngtest_bad-stl-pcbvicebar
|
||||
offcgalpngtest_bad-stl-tardis
|
||||
offpngtest_bad-stl-wing)
|
||||
|
||||
list(APPEND EXPORTCSG_TEST_FILES
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/nonmanifold-polyhedron.scad)
|
||||
list(APPEND EXPORT3D_CGAL_TEST_FILES
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/3D/features/polyhedron-tests.scad)
|
||||
|
||||
list(APPEND EXPORT3D_TEST_FILES
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/nonmanifold-polyhedron.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/misc/bad-stl-wing.scad)
|
||||
|
||||
# No issue - this was introduced when fixing #1033
|
||||
set_test_config(Bugs stlpngtest_bad-stl-wing)
|
||||
|
||||
disable_tests(
|
||||
# These don't output anything
|
||||
|
|
@ -1255,12 +1267,49 @@ list(APPEND BUGS_FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue13.scad
|
|||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1105.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1105b.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1105c.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1105d.scad)
|
||||
list(APPEND EXPORT3D_TEST_FILES ${BUGS_FILES})
|
||||
list(REMOVE_ITEM EXPORT3D_TEST_FILES
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1105d.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1215.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1215b.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1215c.scad)
|
||||
|
||||
# We know that we cannot import weakly manifold files into CGAL, so to make tests easier
|
||||
# to manage, don't try. Once we improve import, we can reenable this
|
||||
# Known good manifold files -> EXPORT3D_CGALCGAL_TEST_FILES
|
||||
# Known weak manifold files -> EXPORT3D_CGAL_TEST_FILES
|
||||
# Known non-manifold files -> EXPORT3D_TEST_FILES
|
||||
list(APPEND EXPORT3D_CGALCGAL_TEST_FILES ${BUGS_FILES})
|
||||
list(REMOVE_ITEM EXPORT3D_CGALCGAL_TEST_FILES
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue13.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue13b.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue945.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue945b.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue945c.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue945d.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1105b.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1105c.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1215.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1215b.scad)
|
||||
list(APPEND EXPORT3D_CGAL_TEST_FILES
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue13.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue13b.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue945.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue945b.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue945c.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue945d.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1105b.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1105c.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1215.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1215b.scad)
|
||||
|
||||
disable_tests(
|
||||
# This triggers a Value to string difference when compiled as testing
|
||||
csgpngtest_issue1215
|
||||
)
|
||||
|
||||
# 2D files
|
||||
list(REMOVE_ITEM EXPORT3D_CGALCGAL_TEST_FILES
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue899.scad
|
||||
${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue1089.scad)
|
||||
#list(APPEND EXPORTCSG_TEST_FILES )
|
||||
list(APPEND ALL_2D_FILES ${CMAKE_SOURCE_DIR}/../testdata/scad/bugs/issue899.scad)
|
||||
|
||||
list(APPEND OPENCSGTEST_FILES ${BUGS_FILES})
|
||||
|
|
@ -1278,7 +1327,13 @@ foreach(FILE ${BUGS_FILES})
|
|||
set_test_config(Bugs ${TEST_FULLNAME})
|
||||
get_test_fullname(stlpngtest ${FILE} TEST_FULLNAME)
|
||||
set_test_config(Bugs ${TEST_FULLNAME})
|
||||
get_test_fullname(stlcsgpngtest ${FILE} TEST_FULLNAME)
|
||||
get_test_fullname(stlcgalpngtest ${FILE} TEST_FULLNAME)
|
||||
set_test_config(Bugs ${TEST_FULLNAME})
|
||||
get_test_fullname(cgalstlcgalpngtest ${FILE} TEST_FULLNAME)
|
||||
set_test_config(Bugs ${TEST_FULLNAME})
|
||||
get_test_fullname(offpngtest ${FILE} TEST_FULLNAME)
|
||||
set_test_config(Bugs ${TEST_FULLNAME})
|
||||
get_test_fullname(offcgalpngtest ${FILE} TEST_FULLNAME)
|
||||
set_test_config(Bugs ${TEST_FULLNAME})
|
||||
endforeach()
|
||||
|
||||
|
|
@ -1297,8 +1352,14 @@ foreach(FILE ${EXAMPLE_FILES})
|
|||
set_test_config(Examples ${TEST_FULLNAME})
|
||||
get_test_fullname(stlpngtest ${FILE} TEST_FULLNAME)
|
||||
set_test_config(Examples ${TEST_FULLNAME})
|
||||
get_test_fullname(stlcgalpngtest ${FILE} TEST_FULLNAME)
|
||||
set_test_config(Examples ${TEST_FULLNAME})
|
||||
get_test_fullname(cgalstlcgalpngtest ${FILE} TEST_FULLNAME)
|
||||
set_test_config(Examples ${TEST_FULLNAME})
|
||||
get_test_fullname(offpngtest ${FILE} TEST_FULLNAME)
|
||||
set_test_config(Examples ${TEST_FULLNAME})
|
||||
get_test_fullname(offcgalpngtest ${FILE} TEST_FULLNAME)
|
||||
set_test_config(Examples ${TEST_FULLNAME})
|
||||
endforeach()
|
||||
|
||||
# Workaround Gallium bugs
|
||||
|
|
@ -1355,10 +1416,11 @@ file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/CTestCustom.cmake ${TMP})
|
|||
# o throwntogethertest: Export to PNG using the Throwntogether renderer
|
||||
# o csgpngtest: 1) Export to .csg, 2) import .csg and export to PNG (--render)
|
||||
# o monotonepngtest: Same as cgalpngtest but with the "Monotone" color scheme
|
||||
# o stlpngtest: Export to STL, Re-import and render to PNG (--render=cgal)
|
||||
# o offpngtest: Export to OFF, Re-import and render to PNG (--render=cgal)
|
||||
# o stlpngtest: Export to STL, Re-import and render to PNG (--render)
|
||||
# o stlcgalpngtest: Export to STL, Re-import and render to PNG (--render=cgal)
|
||||
# o offpngtest: Export to OFF, Re-import and render to PNG (--render)
|
||||
# o offcgalpngtest: Export to STL, Re-import and render to PNG (--render=cgal)
|
||||
# o dxfpngtest: Export to DXF, Re-import and render to PNG (--render=cgal)
|
||||
# o stlcsgpngtest: Export to STL, Re-import and render to PNG (--render)
|
||||
#
|
||||
|
||||
add_cmdline_test(moduledumptest EXE ${OPENSCAD_BINPATH} ARGS -o SUFFIX ast FILES
|
||||
|
|
@ -1388,21 +1450,19 @@ add_cmdline_test(cgalstlsanitytest EXE ${CMAKE_SOURCE_DIR}/cgalstlsanitytest SUF
|
|||
# Export/Import tests
|
||||
#
|
||||
|
||||
# Issue #337
|
||||
set_test_config(Bugs stlpngtest_polyhedron-tests)
|
||||
# Issue #910
|
||||
set_test_config(Bugs offpngtest_polyhedron-tests
|
||||
offpngtest_bad-stl-pcbvicebar
|
||||
offpngtest_bad-stl-tardis
|
||||
offpngtest_bad-stl-wing)
|
||||
# No issue - this was introduced when fixing #1033
|
||||
set_test_config(Bugs stlpngtest_bad-stl-wing)
|
||||
add_cmdline_test(monotonepngtest EXE ${OPENSCAD_BINPATH} ARGS --colorscheme=Monotone --render -o SUFFIX png FILES ${EXPORT3D_CGAL_TEST_FILES} ${EXPORT3D_CGALCGAL_TEST_FILES})
|
||||
|
||||
# stlpngtest: direct STL output, preview rendering
|
||||
add_cmdline_test(stlpngtest EXE ${PYTHON_EXECUTABLE} SCRIPT ${CMAKE_SOURCE_DIR}/export_import_pngtest.py ARGS --openscad=${OPENSCAD_BINPATH} --format=STL EXPECTEDDIR monotonepngtest SUFFIX png FILES ${EXPORT3D_TEST_FILES})
|
||||
# cgalstlpngtest: CGAL STL output, normal rendering
|
||||
add_cmdline_test(stlcgalpngtest EXE ${PYTHON_EXECUTABLE} SCRIPT ${CMAKE_SOURCE_DIR}/export_import_pngtest.py ARGS --openscad=${OPENSCAD_BINPATH} --format=STL --require-manifold --render EXPECTEDDIR monotonepngtest SUFFIX png FILES ${EXPORT3D_CGAL_TEST_FILES})
|
||||
# cgalstlcgalpngtest: CGAL STL output, CGAL rendering
|
||||
add_cmdline_test(cgalstlcgalpngtest EXE ${PYTHON_EXECUTABLE} SCRIPT ${CMAKE_SOURCE_DIR}/export_import_pngtest.py ARGS --openscad=${OPENSCAD_BINPATH} --format=STL --require-manifold --render=cgal EXPECTEDDIR monotonepngtest SUFFIX png FILES ${EXPORT3D_CGALCGAL_TEST_FILES})
|
||||
|
||||
add_cmdline_test(offpngtest EXE ${PYTHON_EXECUTABLE} SCRIPT ${CMAKE_SOURCE_DIR}/export_import_pngtest.py ARGS --openscad=${OPENSCAD_BINPATH} --format=OFF --render EXPECTEDDIR monotonepngtest SUFFIX png FILES ${EXPORT3D_TEST_FILES})
|
||||
add_cmdline_test(offcgalpngtest EXE ${PYTHON_EXECUTABLE} SCRIPT ${CMAKE_SOURCE_DIR}/export_import_pngtest.py ARGS --openscad=${OPENSCAD_BINPATH} --format=OFF --render=cgal EXPECTEDDIR monotonepngtest SUFFIX png FILES ${EXPORT3D_CGAL_TEST_FILES})
|
||||
|
||||
add_cmdline_test(monotonepngtest EXE ${OPENSCAD_BINPATH} ARGS --colorscheme=Monotone --render -o SUFFIX png FILES ${EXPORT3D_TEST_FILES})
|
||||
add_cmdline_test(stlpngtest EXE ${PYTHON_EXECUTABLE} SCRIPT ${CMAKE_SOURCE_DIR}/export_import_pngtest.py ARGS --openscad=${OPENSCAD_BINPATH} --format=STL --render=cgal EXPECTEDDIR monotonepngtest SUFFIX png FILES ${EXPORT3D_TEST_FILES})
|
||||
add_cmdline_test(offpngtest EXE ${PYTHON_EXECUTABLE} SCRIPT ${CMAKE_SOURCE_DIR}/export_import_pngtest.py ARGS --openscad=${OPENSCAD_BINPATH} --format=OFF --render=cgal EXPECTEDDIR monotonepngtest SUFFIX png FILES ${EXPORT3D_TEST_FILES})
|
||||
add_cmdline_test(dxfpngtest EXE ${PYTHON_EXECUTABLE} SCRIPT ${CMAKE_SOURCE_DIR}/export_import_pngtest.py ARGS --openscad=${OPENSCAD_BINPATH} --format=DXF --render=cgal EXPECTEDDIR cgalpngtest SUFFIX png FILES ${FILES_2D})
|
||||
add_cmdline_test(stlcsgpngtest EXE ${PYTHON_EXECUTABLE} SCRIPT ${CMAKE_SOURCE_DIR}/export_import_pngtest.py ARGS --openscad=${OPENSCAD_BINPATH} --format=STL --render EXPECTEDDIR monotonepngtest SUFFIX png FILES ${EXPORTCSG_TEST_FILES})
|
||||
|
||||
|
||||
#
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
# Export-import test
|
||||
#
|
||||
#
|
||||
# Usage: <script> <inputfile> --openscad=<executable-path> --format=<format> [<openscad args>] file.png
|
||||
# Usage: <script> <inputfile> --openscad=<executable-path> --format=<format> --require-manifold [<openscad args>] file.png
|
||||
#
|
||||
#
|
||||
# step 1. If the input file is _not_ an .scad file, create a temporary .scad file importing the input file.
|
||||
|
|
@ -14,6 +14,9 @@
|
|||
# of the original .scad file. they should be the same!
|
||||
#
|
||||
# All the optional openscad args are passed on to OpenSCAD both in step 2 and 4.
|
||||
# Exception: In any --render arguments are passed, the first pass (step 2) will always
|
||||
# be run with --render=cgal while the second pass (step 4) will use the passed --render
|
||||
# argument.
|
||||
#
|
||||
# This script should return 0 on success, not-0 on error.
|
||||
#
|
||||
|
|
@ -24,6 +27,7 @@
|
|||
# Authors: Torsten Paul, Don Bright, Marius Kintel
|
||||
|
||||
import sys, os, re, subprocess, argparse
|
||||
from validatestl import validateSTL
|
||||
|
||||
def failquit(*args):
|
||||
if len(args)!=0: print(args)
|
||||
|
|
@ -50,6 +54,8 @@ formats = ['csg', 'stl','off', 'amf', 'dxf', 'svg']
|
|||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--openscad', required=True, help='Specify OpenSCAD executable')
|
||||
parser.add_argument('--format', required=True, choices=[item for sublist in [(f,f.upper()) for f in formats] for item in sublist], help='Specify 3d export format')
|
||||
parser.add_argument('--require-manifold', dest='requiremanifold', action='store_true', help='Require STL output to be manifold')
|
||||
parser.set_defaults(requiremanifold=False)
|
||||
args,remaining_args = parser.parse_known_args()
|
||||
|
||||
args.format = args.format.lower()
|
||||
|
|
@ -82,14 +88,21 @@ if inputsuffix != '.scad' and inputsuffix != '.csg':
|
|||
|
||||
#
|
||||
# First run: Just export the given filetype
|
||||
# For any --render arguments to --render=cgal
|
||||
#
|
||||
export_cmd = [args.openscad, inputfile, '-o', exportfile] + remaining_args
|
||||
print('Running OpenSCAD #1:')
|
||||
print(' '.join(export_cmd))
|
||||
tmpargs = ['--render=cgal' if arg.startswith('--render') else arg for arg in remaining_args]
|
||||
|
||||
export_cmd = [args.openscad, inputfile, '-o', exportfile] + tmpargs
|
||||
print >> sys.stderr, 'Running OpenSCAD #1:'
|
||||
print >> sys.stderr, ' '.join(export_cmd)
|
||||
result = subprocess.call(export_cmd)
|
||||
if result != 0:
|
||||
failquit('OpenSCAD #1 failed with return code ' + str(result))
|
||||
|
||||
if args.format == 'stl' and args.requiremanifold:
|
||||
if not validateSTL(exportfile):
|
||||
failquit("Error: Non-manifold STL file exported from OpenSCAD")
|
||||
|
||||
|
||||
#
|
||||
# Second run: Import the exported file and render as png
|
||||
|
|
@ -101,8 +114,8 @@ if args.format != 'csg':
|
|||
createImport(exportfile, newscadfile)
|
||||
|
||||
create_png_cmd = [args.openscad, newscadfile, '-o', pngfile] + remaining_args
|
||||
print('Running OpenSCAD #2:')
|
||||
print(' '.join(create_png_cmd))
|
||||
print >> sys.stderr, 'Running OpenSCAD #2:'
|
||||
print >> sys.stderr, ' '.join(create_png_cmd)
|
||||
fontdir = os.path.join(os.path.dirname(args.openscad), "..", "testdata");
|
||||
fontenv = os.environ.copy();
|
||||
fontenv["OPENSCAD_FONT_PATH"] = fontdir;
|
||||
|
|
|
|||
BIN
tests/regression/cgalpngtest/issue1215-expected.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
tests/regression/cgalpngtest/issue1215b-expected.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
tests/regression/monotonepngtest/issue1215-expected.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
tests/regression/monotonepngtest/issue1215b-expected.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.6 KiB |
BIN
tests/regression/opencsgtest/issue1215-expected.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
tests/regression/opencsgtest/issue1215b-expected.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
96
tests/validatestl.py
Executable file
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# Simple tool to validate if an STL has non-manifold (dangling) edges.
|
||||
#
|
||||
# Usage: validatestl.py <file.stl>
|
||||
#
|
||||
# Based on code by Jan Squirrel Koniarik from:
|
||||
# https://github.com/SquirrelCZE/pycad/
|
||||
#
|
||||
# Author: Marius Kintel <marius@kintel.net>
|
||||
# Licence: GPL V2
|
||||
#
|
||||
|
||||
import sys
|
||||
import io
|
||||
import hashlib
|
||||
import os
|
||||
import subprocess
|
||||
from collections import Counter
|
||||
|
||||
def read_stl(filename):
|
||||
triangles = list()
|
||||
with open(filename, "r") as fd:
|
||||
triangle = {
|
||||
"facet": [0, 0, 0],
|
||||
"points": list()
|
||||
}
|
||||
for line in fd:
|
||||
line = line.strip()
|
||||
if line.startswith('solid'):
|
||||
continue
|
||||
elif line.startswith('endsolid'):
|
||||
continue
|
||||
elif line.startswith('outer'):
|
||||
continue
|
||||
elif line.startswith('facet'):
|
||||
parts = line.split(' ')
|
||||
for i in range(2, 5):
|
||||
triangle['facet'][i-2] = float(parts[i])
|
||||
continue
|
||||
elif line.startswith('vertex'):
|
||||
parts = line.split(' ')
|
||||
point = [0, 0, 0]
|
||||
for i in range(1, 4):
|
||||
point[i-1] = float(parts[i])
|
||||
triangle['points'].append(point)
|
||||
continue
|
||||
elif line.startswith('endloop'):
|
||||
continue
|
||||
elif line.startswith('endfacet'):
|
||||
triangles.append(triangle)
|
||||
triangle = {
|
||||
"facet": [0, 0, 0],
|
||||
"points": list()
|
||||
}
|
||||
continue
|
||||
|
||||
return Mesh(
|
||||
triangles=triangles
|
||||
)
|
||||
|
||||
class Mesh():
|
||||
def __init__(self, triangles):
|
||||
points = list()
|
||||
p_triangles = list()
|
||||
for triangle in triangles:
|
||||
p_triangle = list()
|
||||
for point in triangle['points']:
|
||||
if point not in points:
|
||||
points.append(point)
|
||||
p_triangle.append(
|
||||
points.index(point)
|
||||
)
|
||||
p_triangles.append(p_triangle)
|
||||
self.points = points
|
||||
self.triangles = p_triangles
|
||||
|
||||
|
||||
def validateSTL(filename):
|
||||
mesh = read_stl(filename);
|
||||
edges = Counter((t[i], t[(i+1)%3]) for i in range(0,3) for t in mesh.triangles)
|
||||
reverse_edges = Counter((t[(i+1)%3], t[i]) for i in range(0,3) for t in mesh.triangles)
|
||||
edges.subtract(reverse_edges)
|
||||
edges += Counter() # remove zero and negative counts
|
||||
if len(edges) > 0:
|
||||
print "Non-manifold STL: " + str(edges)
|
||||
return False
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
retval = validateSTL(sys.argv[1])
|
||||
if retval:
|
||||
sys.exit(0)
|
||||
else:
|
||||
sys.exit(1)
|
||||