Skip to content

Commit

Permalink
Support predictable output for more file formats (openscad#4950)
Browse files Browse the repository at this point in the history
  • Loading branch information
kintel authored Jan 26, 2024
1 parent 88d244a commit 358af34
Show file tree
Hide file tree
Showing 16 changed files with 778 additions and 730 deletions.
19 changes: 11 additions & 8 deletions src/io/export.cc
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,12 @@ std::unique_ptr<PolySet> createSortedPolySet(const PolySet& ps)
std::map<Vector3d, int, LexographicLess> vertexMap;

for (const auto& poly : ps.indices) {
auto pos1 = vertexMap.emplace(remove_negative_zero(ps.vertices[poly[0]]), vertexMap.size());
auto pos2 = vertexMap.emplace(remove_negative_zero(ps.vertices[poly[1]]), vertexMap.size());
auto pos3 = vertexMap.emplace(remove_negative_zero(ps.vertices[poly[2]]), vertexMap.size());
out->indices.push_back({pos1.first->second, pos2.first->second, pos3.first->second});
IndexedFace face;
for (const auto idx : poly) {
auto pos = vertexMap.emplace(remove_negative_zero(ps.vertices[idx]), vertexMap.size());
face.push_back(pos.first->second);
}
out->indices.push_back(face);
}

std::vector<int> indexTranslationMap(vertexMap.size());
Expand All @@ -215,13 +217,14 @@ std::unique_ptr<PolySet> createSortedPolySet(const PolySet& ps)
}

for (auto& poly : out->indices) {
IndexedFace polygon = {indexTranslationMap[poly[0]], indexTranslationMap[poly[1]], indexTranslationMap[poly[2]]};
IndexedFace polygon;
for (const auto idx : poly) {
polygon.push_back(indexTranslationMap[idx]);
}
std::rotate(polygon.begin(), std::min_element(polygon.begin(), polygon.end()), polygon.end());
poly = polygon;
}
std::sort(out->indices.begin(), out->indices.end(), [](const IndexedFace& t1, const IndexedFace& t2) -> bool {
return t1 < t2;
});
std::sort(out->indices.begin(), out->indices.end());

return out;
}
35 changes: 19 additions & 16 deletions src/io/export_3mf.cc
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ static void export_3mf_error(std::string msg, PLib3MFModel *& model)
/*
* PolySet must be triangulated.
*/
static bool append_polyset(const PolySet& ps, PLib3MFModelMeshObject *& model)
static bool append_polyset(std::shared_ptr<const PolySet> ps, PLib3MFModelMeshObject *& model)
{
PLib3MFModelMeshObject *mesh;
if (lib3mf_model_addmeshobject(model, &mesh) != LIB3MF_OK) {
Expand All @@ -99,7 +99,7 @@ static bool append_polyset(const PolySet& ps, PLib3MFModelMeshObject *& model)
return lib3mf_meshobject_addtriangle(mesh, &t, nullptr) == LIB3MF_OK;
};

auto sorted_ps = createSortedPolySet(ps);
auto sorted_ps = createSortedPolySet(*ps);

for (const auto &v : sorted_ps->vertices) {
if (!vertexFunc(v)) {
Expand Down Expand Up @@ -137,8 +137,8 @@ static bool append_nef(const CGAL_Nef_polyhedron& root_N, PLib3MFModelMeshObject
}


if (const auto ps = CGALUtils::createPolySetFromNefPolyhedron3(*root_N.p3)) {
return append_polyset(*ps, model);
if (std::shared_ptr<PolySet> ps = CGALUtils::createPolySetFromNefPolyhedron3(*root_N.p3)) {
return append_polyset(ps, model);
}

export_3mf_error("Error converting NEF Polyhedron.", model);
Expand All @@ -156,14 +156,14 @@ static bool append_3mf(const std::shared_ptr<const Geometry>& geom, PLib3MFModel
} else if (const auto N = std::dynamic_pointer_cast<const CGAL_Nef_polyhedron>(geom)) {
return append_nef(*N, model);
} else if (const auto hybrid = std::dynamic_pointer_cast<const CGALHybridPolyhedron>(geom)) {
return append_polyset(*hybrid->toPolySet(), model);
return append_polyset(hybrid->toPolySet(), model);
#endif
#ifdef ENABLE_MANIFOLD
} else if (const auto mani = std::dynamic_pointer_cast<const ManifoldGeometry>(geom)) {
return append_polyset(*mani->toPolySet(), model);
return append_polyset(mani->toPolySet(), model);
#endif
} else if (const auto ps = std::dynamic_pointer_cast<const PolySet>(geom)) {
return append_polyset(*PolySetUtils::tessellate_faces(*ps), model);
return append_polyset(PolySetUtils::tessellate_faces(*ps), model);
} else if (std::dynamic_pointer_cast<const Polygon2d>(geom)) { // NOLINT(bugprone-branch-clone)
assert(false && "Unsupported file format");
} else { // NOLINT(bugprone-branch-clone)
Expand Down Expand Up @@ -240,7 +240,7 @@ static void export_3mf_error(std::string msg)
/*
* PolySet must be triangulated.
*/
static bool append_polyset(const PolySet& ps, Lib3MF::PWrapper& wrapper, Lib3MF::PModel& model)
static bool append_polyset(std::shared_ptr<const PolySet> ps, Lib3MF::PWrapper& wrapper, Lib3MF::PModel& model)
{
try {
auto mesh = model->AddMeshObject();
Expand Down Expand Up @@ -270,16 +270,19 @@ static bool append_polyset(const PolySet& ps, Lib3MF::PWrapper& wrapper, Lib3MF:
return true;
};

auto sorted_ps = createSortedPolySet(ps);
std::shared_ptr<const PolySet> out_ps = ps;
if (Feature::ExperimentalPredictibleOutput.is_enabled()) {
out_ps = createSortedPolySet(*ps);
}

for (const auto &v : sorted_ps->vertices) {
for (const auto &v : out_ps->vertices) {
if (!vertexFunc(v)) {
export_3mf_error("Can't add vertex to 3MF model.");
return false;
}
}

for (const auto& poly : sorted_ps->indices) {
for (const auto& poly : out_ps->indices) {
if (!triangleFunc(poly)) {
export_3mf_error("Can't add triangle to 3MF model.");
return false;
Expand Down Expand Up @@ -311,8 +314,8 @@ static bool append_nef(const CGAL_Nef_polyhedron& root_N, Lib3MF::PWrapper& wrap
LOG(message_group::Export_Warning, "Exported object may not be a valid 2-manifold and may need repair");
}

if (const auto ps = CGALUtils::createPolySetFromNefPolyhedron3(*root_N.p3)) {
return append_polyset(*ps, wrapper, model);
if (std::shared_ptr<PolySet> ps = CGALUtils::createPolySetFromNefPolyhedron3(*root_N.p3)) {
return append_polyset(ps, wrapper, model);
}
export_3mf_error("Error converting NEF Polyhedron.");
return false;
Expand All @@ -329,14 +332,14 @@ static bool append_3mf(const std::shared_ptr<const Geometry>& geom, Lib3MF::PWra
} else if (const auto N = std::dynamic_pointer_cast<const CGAL_Nef_polyhedron>(geom)) {
return append_nef(*N, wrapper, model);
} else if (const auto hybrid = std::dynamic_pointer_cast<const CGALHybridPolyhedron>(geom)) {
return append_polyset(*hybrid->toPolySet(), wrapper, model);
return append_polyset(hybrid->toPolySet(), wrapper, model);
#endif
#ifdef ENABLE_MANIFOLD
} else if (const auto mani = std::dynamic_pointer_cast<const ManifoldGeometry>(geom)) {
return append_polyset(*mani->toPolySet(), wrapper, model);
return append_polyset(mani->toPolySet(), wrapper, model);
#endif
} else if (const auto ps = std::dynamic_pointer_cast<const PolySet>(geom)) {
return append_polyset(*PolySetUtils::tessellate_faces(*ps), wrapper, model);
return append_polyset(PolySetUtils::tessellate_faces(*ps), wrapper, model);
} else if (std::dynamic_pointer_cast<const Polygon2d>(geom)) {
assert(false && "Unsupported file format");
} else {
Expand Down
10 changes: 7 additions & 3 deletions src/io/export_obj.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,19 @@
void export_obj(const std::shared_ptr<const Geometry>& geom, std::ostream& output)
{
// FIXME: In lazy union mode, should we export multiple objects?
auto ps = PolySetUtils::getGeometryAsPolySet(geom);

std::shared_ptr<const PolySet> out = PolySetUtils::getGeometryAsPolySet(geom);
if (Feature::ExperimentalPredictibleOutput.is_enabled()) {
out = createSortedPolySet(*out);
}

output << "# OpenSCAD obj exporter\n";

for (const auto &v : ps->vertices) {
for (const auto &v : out->vertices) {
output << "v " <<v[0] << " " << v[1] << " " << v[2] << "\n";
}

for (const auto& poly : ps->indices) {
for (const auto& poly : out->indices) {
output << "f ";
for (const auto idx : poly) {
output << " " << idx + 1;
Expand Down
3 changes: 3 additions & 0 deletions src/io/export_off.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
void export_off(const std::shared_ptr<const Geometry>& geom, std::ostream& output)
{
auto ps = PolySetUtils::getGeometryAsPolySet(geom);
if (Feature::ExperimentalPredictibleOutput.is_enabled()) {
ps = createSortedPolySet(*ps);
}

output << "OFF " << ps->vertices.size() << " " << ps->indices.size() << " 0\n";
const auto& v = ps->vertices;
Expand Down
43 changes: 19 additions & 24 deletions src/io/export_stl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -97,41 +97,36 @@ void write_floats(std::ostream& output, const std::array<float, N>& data) {
}


uint64_t append_stl(const PolySet& polyset, std::ostream& output, bool binary)
uint64_t append_stl(std::shared_ptr<const PolySet> polyset, std::ostream& output, bool binary)
{
static_assert(sizeof(float) == 4, "Need 32 bit float");
// check if tessellation is needed
std::unique_ptr<PolySet> tmp_ps;
if (!polyset.isTriangular) {
tmp_ps = PolySetUtils::tessellate_faces(polyset);
}
const PolySet &tri_ps = tmp_ps ? *tmp_ps : polyset;

std::unique_ptr<PolySet> sorted_ps;
std::shared_ptr<const PolySet> ps = polyset;
if (!ps->isTriangular) {
ps = PolySetUtils::tessellate_faces(*ps);
}
if (Feature::ExperimentalPredictibleOutput.is_enabled()) {
sorted_ps = createSortedPolySet(tri_ps);
ps = createSortedPolySet(*ps);
}

const PolySet &ps = sorted_ps ? *sorted_ps : tri_ps;

uint64_t triangle_count = 0;

// In ASCII mode only, convert each vertex to string.
std::vector<std::string> vertexStrings;
if (!binary) {
vertexStrings.resize(ps.vertices.size());
std::transform(ps.vertices.begin(), ps.vertices.end(), vertexStrings.begin(),
vertexStrings.resize(ps->vertices.size());
std::transform(ps->vertices.begin(), ps->vertices.end(), vertexStrings.begin(),
[](const auto& p)
{ return toString({static_cast<float>(p.x()), static_cast<float>(p.y()) , static_cast<float>(p.z()) }); });
}

// Used for binary mode only
std::array<float, 4lu * 3> coords;

for (const auto &t : ps.indices) {
const auto &p0 = ps.vertices[t[0]];
const auto &p1 = ps.vertices[t[1]];
const auto &p2 = ps.vertices[t[2]];
for (const auto &t : ps->indices) {
const auto &p0 = ps->vertices[t[0]];
const auto &p1 = ps->vertices[t[1]];
const auto &p2 = ps->vertices[t[2]];

// Tessellation already eliminated these cases.
assert(p0 != p1 && p0 != p2 && p1 != p2);
Expand Down Expand Up @@ -194,8 +189,8 @@ uint64_t append_stl(const CGAL_Nef_polyhedron& root_N, std::ostream& output,
LOG(message_group::Export_Warning, "Exported object may not be a valid 2-manifold and may need repair");
}

if (auto ps = CGALUtils::createPolySetFromNefPolyhedron3(*(root_N.p3))) {
triangle_count += append_stl(*ps, output, binary);
if (std::shared_ptr<PolySet> ps = CGALUtils::createPolySetFromNefPolyhedron3(*(root_N.p3))) {
triangle_count += append_stl(ps, output, binary);
} else {
LOG(message_group::Export_Error, "Nef->PolySet failed");
}
Expand All @@ -217,7 +212,7 @@ uint64_t append_stl(const CGALHybridPolyhedron& hybrid, std::ostream& output,

const auto ps = hybrid.toPolySet();
if (ps) {
triangle_count += append_stl(*ps, output, binary);
triangle_count += append_stl(ps, output, binary);
} else {
LOG(message_group::Export_Error, "Nef->PolySet failed");
}
Expand All @@ -241,7 +236,7 @@ uint64_t append_stl(const ManifoldGeometry& mani, std::ostream& output,

const auto ps = mani.toPolySet();
if (ps) {
triangle_count += append_stl(*ps, output, binary);
triangle_count += append_stl(ps, output, binary);
} else {
LOG(message_group::Export_Error, "Manifold->PolySet failed");
}
Expand All @@ -260,16 +255,16 @@ uint64_t append_stl(const std::shared_ptr<const Geometry>& geom, std::ostream& o
triangle_count += append_stl(item.second, output, binary);
}
} else if (const auto ps = std::dynamic_pointer_cast<const PolySet>(geom)) {
triangle_count += append_stl(*ps, output, binary);
triangle_count += append_stl(ps, output, binary);
#ifdef ENABLE_CGAL
} else if (const auto N = std::dynamic_pointer_cast<const CGAL_Nef_polyhedron>(geom)) {
triangle_count += append_stl(*N, output, binary);
} else if (const auto hybrid = std::dynamic_pointer_cast<const CGALHybridPolyhedron>(geom)) {
triangle_count += append_stl(*hybrid, output, binary);
triangle_count += append_stl(hybrid, output, binary);
#endif
#ifdef ENABLE_MANIFOLD
} else if (const auto mani = std::dynamic_pointer_cast<const ManifoldGeometry>(geom)) {
triangle_count += append_stl(*mani, output, binary);
triangle_count += append_stl(mani, output, binary);
#endif
} else if (std::dynamic_pointer_cast<const Polygon2d>(geom)) { //NOLINT(bugprone-branch-clone)
assert(false && "Unsupported file format");
Expand Down
3 changes: 3 additions & 0 deletions src/io/export_wrl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ void export_wrl(const std::shared_ptr<const Geometry>& geom, std::ostream& outpu
{
// FIXME: In lazy union mode, should we export multiple IndexedFaceSets?
auto ps = PolySetUtils::getGeometryAsPolySet(geom);
if (Feature::ExperimentalPredictibleOutput.is_enabled()) {
ps = createSortedPolySet(*ps);
}

output << "#VRML V2.0 utf8\n\n";

Expand Down
5 changes: 3 additions & 2 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,7 @@ list(APPEND EXPORT_STL_TEST_FILES ${TEST_SCAD_DIR}/stl/stl-export.scad)
list(APPEND EXPORT_OBJ_TEST_FILES ${TEST_SCAD_DIR}/obj/obj-export.scad)
list(APPEND EXPORT_OBJ_TEST_FILES ${TEST_SCAD_DIR}/obj/obj-import-export_dodecahedron.scad)
list(APPEND EXPORT_OBJ_TEST_FILES ${TEST_SCAD_DIR}/obj/obj-import-export_cube.scad)
list(APPEND EXPORT_OBJ_TEST_FILES ${TEST_SCAD_DIR}/3D/features/polyhedron-cube.scad)

list(APPEND EXPORT_3MF_TEST_FILES ${TEST_SCAD_DIR}/3mf/3mf-export.scad)

Expand Down Expand Up @@ -1028,8 +1029,8 @@ add_cmdline_test(pdfexporttest SCRIPT ${EXPORT_PNGTEST_PY} SUFFIX png FILES ${SC
add_cmdline_test(monotonepngtest OPENSCAD SUFFIX png FILES ${EXPORT3D_CGAL_TEST_FILES} ${EXPORT3D_CGALCGAL_TEST_FILES} ARGS --colorscheme=Monotone --render)
add_cmdline_test(stlexport EXPERIMENTAL OPENSCAD SUFFIX stl FILES ${EXPORT_STL_TEST_FILES} ARGS --enable=predictible-output --render)
add_cmdline_test(manifold-stlexport EXPERIMENTAL OPENSCAD SUFFIX stl FILES ${EXPORT_STL_TEST_FILES} EXPECTEDDIR stlexport ARGS --enable=predictible-output --enable=manifold --render)
add_cmdline_test(objexport EXPERIMENTAL OPENSCAD SUFFIX obj FILES ${EXPORT_OBJ_TEST_FILES} ARGS --render)
add_cmdline_test(3mfexport OPENSCAD ARGS SUFFIX 3mf FILES ${EXPORT_3MF_TEST_FILES})
add_cmdline_test(objexport EXPERIMENTAL OPENSCAD SUFFIX obj FILES ${EXPORT_OBJ_TEST_FILES} ARGS --render --enable=predictible-output)
add_cmdline_test(3mfexport OPENSCAD ARGS SUFFIX 3mf FILES ${EXPORT_3MF_TEST_FILES} ARGS --enable=predictible-output)

# stlpngtest: direct STL output, preview rendering
add_cmdline_test(stlpngtest SCRIPT ${EX_IM_PNGTEST_PY} ARGS ${OPENSCAD_ARG} --format=STL EXPECTEDDIR monotonepngtest SUFFIX png FILES ${EXPORT3D_TEST_FILES})
Expand Down
19 changes: 19 additions & 0 deletions tests/data/scad/3D/features/polyhedron-cube.scad
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
polyhedron(
points=[
[0, 0, 0],
[1, 0, 0],
[0, 1, 0],
[1, 1, 0],
[0, 0, 1],
[1, 0, 1],
[0, 1, 1],
[1, 1, 1],
],
faces=[
[6,7,5,4],
[0,1,3,2],
[4,5,1,0],
[5,7,3,1],
[7,6,2,3],
[6,4,0,2],
]);
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions tests/regression/dumptest/polyhedron-cube-expected.csg
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
polyhedron(points = [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1]], faces = [[6, 7, 5, 4], [0, 1, 3, 2], [4, 5, 1, 0], [5, 7, 3, 1], [7, 6, 2, 3], [6, 4, 0, 2]], convexity = 1);

20 changes: 10 additions & 10 deletions tests/regression/objexport/obj-export-expected.obj
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
# OpenSCAD obj exporter
v -0.5 -0.5 -0.5
v 0.5 -0.5 -0.5
v -0.5 0.5 -0.5
v 0.5 0.5 -0.5
v -0.5 -0.5 0.5
v 0.5 -0.5 0.5
v -0.5 0.5 -0.5
v -0.5 0.5 0.5
v 0.5 -0.5 -0.5
v 0.5 -0.5 0.5
v 0.5 0.5 -0.5
v 0.5 0.5 0.5
f 5 6 8 7
f 3 4 2 1
f 1 2 6 5
f 2 4 8 6
f 4 3 7 8
f 3 1 5 7
f 1 2 4 3
f 1 3 7 5
f 1 5 6 2
f 2 6 8 4
f 3 4 8 7
f 5 7 8 6
Loading

0 comments on commit 358af34

Please sign in to comment.