SLA Contour3D expanded with conversions supporting quads.

This commit is contained in:
tamasmeszaros 2019-11-04 14:33:29 +01:00
parent a8a5a884f9
commit 7808d09d06
14 changed files with 7777 additions and 259 deletions

View File

@ -181,6 +181,7 @@ add_library(libslic3r STATIC
miniz_extension.cpp
${OpenVDBUtils_SOURCES}
SLA/SLACommon.hpp
SLA/SLACommon.cpp
SLA/SLABoilerPlate.hpp
SLA/SLAPad.hpp
SLA/SLAPad.cpp

View File

@ -355,6 +355,35 @@ bool objparse(const char *path, ObjData &data)
return true;
}
bool objparse(std::istream &stream, ObjData &data)
{
try {
char buf[65536 * 2];
size_t len = 0;
size_t lenPrev = 0;
while ((len = size_t(stream.read(buf + lenPrev, 65536).gcount())) != 0) {
len += lenPrev;
size_t lastLine = 0;
for (size_t i = 0; i < len; ++ i)
if (buf[i] == '\r' || buf[i] == '\n') {
buf[i] = 0;
char *c = buf + lastLine;
while (*c == ' ' || *c == '\t')
++ c;
obj_parseline(c, data);
lastLine = i + 1;
}
lenPrev = len - lastLine;
memmove(buf, buf + lastLine, lenPrev);
}
}
catch (std::bad_alloc&) {
printf("Out of memory\r\n");
}
return true;
}
template<typename T>
bool savevector(FILE *pFile, const std::vector<T> &v)
{

View File

@ -3,6 +3,7 @@
#include <string>
#include <vector>
#include <istream>
namespace ObjParser {
@ -97,6 +98,7 @@ struct ObjData {
};
extern bool objparse(const char *path, ObjData &data);
extern bool objparse(std::istream &stream, ObjData &data);
extern bool objbinsave(const char *path, const ObjData &data);

View File

@ -17,128 +17,6 @@ typedef Eigen::Matrix<int, 4, 1, Eigen::DontAlign> Vec4i;
namespace sla {
/// Intermediate struct for a 3D mesh
struct Contour3D {
Pointf3s points;
std::vector<Vec3i> faces3;
std::vector<Vec4i> faces4;
Contour3D& merge(const Contour3D& ctr)
{
auto N = coord_t(points.size());
auto N_f3 = faces3.size();
auto N_f4 = faces4.size();
points.insert(points.end(), ctr.points.begin(), ctr.points.end());
faces3.insert(faces3.end(), ctr.faces3.begin(), ctr.faces3.end());
faces4.insert(faces4.end(), ctr.faces4.begin(), ctr.faces4.end());
for(size_t n = N_f3; n < faces3.size(); n++) {
auto& idx = faces3[n]; idx.x() += N; idx.y() += N; idx.z() += N;
}
for(size_t n = N_f4; n < faces4.size(); n++) {
auto& idx = faces4[n]; for (int k = 0; k < 4; k++) idx(k) += N;
}
return *this;
}
Contour3D& merge(const Pointf3s& triangles)
{
const size_t offs = points.size();
points.insert(points.end(), triangles.begin(), triangles.end());
faces3.reserve(faces3.size() + points.size() / 3);
for(int i = int(offs); i < int(points.size()); i += 3)
faces3.emplace_back(i, i + 1, i + 2);
return *this;
}
// Write the index triangle structure to OBJ file for debugging purposes.
void to_obj(std::ostream& stream)
{
for(auto& p : points) {
stream << "v " << p.transpose() << "\n";
}
for(auto& f : faces3) {
stream << "f " << (f + Vec3i(1, 1, 1)).transpose() << "\n";
}
for(auto& f : faces4) {
stream << "f " << (f + Vec4i(1, 1, 1, 1)).transpose() << "\n";
}
}
bool empty() const { return points.empty() || (faces4.empty() && faces3.empty()); }
};
using ClusterEl = std::vector<unsigned>;
using ClusteredPoints = std::vector<ClusterEl>;
// Clustering a set of points by the given distance.
ClusteredPoints cluster(const std::vector<unsigned>& indices,
std::function<Vec3d(unsigned)> pointfn,
double dist,
unsigned max_points);
ClusteredPoints cluster(const PointSet& points,
double dist,
unsigned max_points);
ClusteredPoints cluster(
const std::vector<unsigned>& indices,
std::function<Vec3d(unsigned)> pointfn,
std::function<bool(const PointIndexEl&, const PointIndexEl&)> predicate,
unsigned max_points);
// Calculate the normals for the selected points (from 'points' set) on the
// mesh. This will call squared distance for each point.
PointSet normals(const PointSet& points,
const EigenMesh3D& convert_mesh,
double eps = 0.05, // min distance from edges
std::function<void()> throw_on_cancel = [](){},
const std::vector<unsigned>& selected_points = {});
/// Mesh from an existing contour.
inline TriangleMesh convert_mesh(const Contour3D& ctour) {
return {ctour.points, ctour.faces3};
}
/// Mesh from an evaporating 3D contour
inline TriangleMesh convert_mesh(Contour3D&& ctour) {
return {std::move(ctour.points), std::move(ctour.faces3)};
}
inline Contour3D convert_mesh(const TriangleMesh &trmesh) {
Contour3D ret;
ret.points.reserve(trmesh.its.vertices.size());
ret.faces3.reserve(trmesh.its.indices.size());
for (auto &v : trmesh.its.vertices)
ret.points.emplace_back(v.cast<double>());
std::copy(trmesh.its.indices.begin(), trmesh.its.indices.end(),
std::back_inserter(ret.faces3));
return ret;
}
inline Contour3D convert_mesh(TriangleMesh &&trmesh) {
Contour3D ret;
ret.points.reserve(trmesh.its.vertices.size());
for (auto &v : trmesh.its.vertices)
ret.points.emplace_back(v.cast<double>());
ret.faces3.swap(trmesh.its.indices);
return ret;
}
}
}

View File

@ -0,0 +1,147 @@
#include "SLACommon.hpp"
#include <libslic3r/Format/objparser.hpp>
namespace Slic3r { namespace sla {
Contour3D::Contour3D(const TriangleMesh &trmesh)
{
points.reserve(trmesh.its.vertices.size());
faces3.reserve(trmesh.its.indices.size());
for (auto &v : trmesh.its.vertices)
points.emplace_back(v.cast<double>());
std::copy(trmesh.its.indices.begin(), trmesh.its.indices.end(),
std::back_inserter(faces3));
}
Contour3D::Contour3D(TriangleMesh &&trmesh)
{
points.reserve(trmesh.its.vertices.size());
for (auto &v : trmesh.its.vertices)
points.emplace_back(v.cast<double>());
faces3.swap(trmesh.its.indices);
}
Contour3D::Contour3D(const EigenMesh3D &emesh) {
points.reserve(size_t(emesh.V().rows()));
faces3.reserve(size_t(emesh.F().rows()));
for (int r = 0; r < emesh.V().rows(); r++)
points.emplace_back(emesh.V().row(r).cast<double>());
for (int i = 0; i < emesh.F().rows(); i++)
faces3.emplace_back(emesh.F().row(i));
}
Contour3D &Contour3D::merge(const Contour3D &ctr)
{
auto N = coord_t(points.size());
auto N_f3 = faces3.size();
auto N_f4 = faces4.size();
points.insert(points.end(), ctr.points.begin(), ctr.points.end());
faces3.insert(faces3.end(), ctr.faces3.begin(), ctr.faces3.end());
faces4.insert(faces4.end(), ctr.faces4.begin(), ctr.faces4.end());
for(size_t n = N_f3; n < faces3.size(); n++) {
auto& idx = faces3[n]; idx.x() += N; idx.y() += N; idx.z() += N;
}
for(size_t n = N_f4; n < faces4.size(); n++) {
auto& idx = faces4[n]; for (int k = 0; k < 4; k++) idx(k) += N;
}
return *this;
}
Contour3D &Contour3D::merge(const Pointf3s &triangles)
{
const size_t offs = points.size();
points.insert(points.end(), triangles.begin(), triangles.end());
faces3.reserve(faces3.size() + points.size() / 3);
for(int i = int(offs); i < int(points.size()); i += 3)
faces3.emplace_back(i, i + 1, i + 2);
return *this;
}
void Contour3D::to_obj(std::ostream &stream)
{
for(auto& p : points)
stream << "v " << p.transpose() << "\n";
for(auto& f : faces3)
stream << "f " << (f + Vec3i(1, 1, 1)).transpose() << "\n";
for(auto& f : faces4)
stream << "f " << (f + Vec4i(1, 1, 1, 1)).transpose() << "\n";
}
void Contour3D::from_obj(std::istream &stream)
{
ObjParser::ObjData data;
ObjParser::objparse(stream, data);
points.reserve(data.coordinates.size() / 4 + 1);
auto &coords = data.coordinates;
for (size_t i = 0; i < coords.size(); i += 4)
points.emplace_back(coords[i], coords[i + 1], coords[i + 2]);
Vec3i triangle;
Vec4i quad;
size_t v = 0;
while(v < data.vertices.size()) {
size_t N = 0;
size_t i = v;
while (data.vertices[v++].coordIdx != -1) ++N;
std::function<void(int, int)> setfn;
if (N < 3 || N > 4) continue;
else if (N == 3) setfn = [&triangle](int k, int f) { triangle(k) = f; };
else setfn = [&quad](int k, int f) { quad(k) = f; };
for (size_t j = 0; j < N; ++j)
setfn(int(j), data.vertices[i + j].coordIdx);
}
}
TriangleMesh to_triangle_mesh(const Contour3D &ctour) {
if (ctour.faces4.empty()) return {ctour.points, ctour.faces3};
std::vector<Vec3i> triangles;
triangles.reserve(ctour.faces3.size() + 2 * ctour.faces4.size());
std::copy(ctour.faces3.begin(), ctour.faces3.end(),
std::back_inserter(triangles));
for (auto &quad : ctour.faces4) {
triangles.emplace_back(quad(0), quad(1), quad(2));
triangles.emplace_back(quad(2), quad(3), quad(0));
}
return {ctour.points, std::move(triangles)};
}
TriangleMesh to_triangle_mesh(Contour3D &&ctour) {
if (ctour.faces4.empty())
return {std::move(ctour.points), std::move(ctour.faces3)};
std::vector<Vec3i> triangles;
triangles.reserve(ctour.faces3.size() + 2 * ctour.faces4.size());
std::copy(ctour.faces3.begin(), ctour.faces3.end(),
std::back_inserter(triangles));
for (auto &quad : ctour.faces4) {
triangles.emplace_back(quad(0), quad(1), quad(2));
triangles.emplace_back(quad(2), quad(3), quad(0));
}
return {std::move(ctour.points), std::move(triangles)};
}
}} // namespace Slic3r::sla

View File

@ -5,6 +5,11 @@
#include <vector>
#include <Eigen/Geometry>
#include "SLASpatIndex.hpp"
#include <libslic3r/ExPolygon.hpp>
#include <libslic3r/TriangleMesh.hpp>
// #define SLIC3R_SLA_NEEDS_WINDTREE
namespace Slic3r {
@ -12,8 +17,7 @@ namespace Slic3r {
// Typedefs from Point.hpp
typedef Eigen::Matrix<float, 3, 1, Eigen::DontAlign> Vec3f;
typedef Eigen::Matrix<double, 3, 1, Eigen::DontAlign> Vec3d;
class TriangleMesh;
typedef Eigen::Matrix<int, 4, 1, Eigen::DontAlign> Vec4i;
namespace sla {
@ -59,9 +63,11 @@ struct SupportPoint
bool operator==(const SupportPoint &sp) const
{
return (pos == sp.pos) && head_front_radius == sp.head_front_radius &&
float rdiff = std::abs(head_front_radius - sp.head_front_radius);
return (pos == sp.pos) && rdiff < float(EPSILON) &&
is_new_island == sp.is_new_island;
}
bool operator!=(const SupportPoint &sp) const { return !(sp == (*this)); }
template<class Archive> void serialize(Archive &ar)
@ -72,8 +78,11 @@ struct SupportPoint
using SupportPoints = std::vector<SupportPoint>;
struct Contour3D;
/// An index-triangle structure for libIGL functions. Also serves as an
/// alternative (raw) input format for the SLASupportTree
/// alternative (raw) input format for the SLASupportTree.
// Implemented in SLASupportTreeIGL.cpp
class EigenMesh3D {
class AABBImpl;
@ -86,6 +95,7 @@ public:
EigenMesh3D(const TriangleMesh&);
EigenMesh3D(const EigenMesh3D& other);
EigenMesh3D(const Contour3D &other);
EigenMesh3D& operator=(const EigenMesh3D&);
~EigenMesh3D();
@ -180,6 +190,63 @@ public:
using PointSet = Eigen::MatrixXd;
/// Dumb vertex mesh consisting of triangles (or) quads. Capable of merging with
/// other meshes of this type and converting to and from other mesh formats.
struct Contour3D {
Pointf3s points;
std::vector<Vec3i> faces3;
std::vector<Vec4i> faces4;
Contour3D() = default;
Contour3D(const TriangleMesh &trmesh);
Contour3D(TriangleMesh &&trmesh);
Contour3D(const EigenMesh3D &emesh);
Contour3D& merge(const Contour3D& ctr);
Contour3D& merge(const Pointf3s& triangles);
// Write the index triangle structure to OBJ file for debugging purposes.
void to_obj(std::ostream& stream);
void from_obj(std::istream &stream);
inline bool empty() const { return points.empty() || (faces4.empty() && faces3.empty()); }
};
using ClusterEl = std::vector<unsigned>;
using ClusteredPoints = std::vector<ClusterEl>;
// Clustering a set of points by the given distance.
ClusteredPoints cluster(const std::vector<unsigned>& indices,
std::function<Vec3d(unsigned)> pointfn,
double dist,
unsigned max_points);
ClusteredPoints cluster(const PointSet& points,
double dist,
unsigned max_points);
ClusteredPoints cluster(
const std::vector<unsigned>& indices,
std::function<Vec3d(unsigned)> pointfn,
std::function<bool(const PointIndexEl&, const PointIndexEl&)> predicate,
unsigned max_points);
// Calculate the normals for the selected points (from 'points' set) on the
// mesh. This will call squared distance for each point.
PointSet normals(const PointSet& points,
const EigenMesh3D& convert_mesh,
double eps = 0.05, // min distance from edges
std::function<void()> throw_on_cancel = [](){},
const std::vector<unsigned>& selected_points = {});
/// Mesh from an existing contour.
TriangleMesh to_triangle_mesh(const Contour3D& ctour);
/// Mesh from an evaporating 3D contour
TriangleMesh to_triangle_mesh(Contour3D&& ctour);
} // namespace sla
} // namespace Slic3r

View File

@ -677,7 +677,7 @@ void create_pad(const ExPolygons &sup_blueprint,
ThrowOnCancel thr)
{
Contour3D t = create_pad_geometry(sup_blueprint, model_blueprint, cfg, thr);
out.merge(convert_mesh(std::move(t)));
out.merge(to_triangle_mesh(std::move(t)));
}
std::string PadConfig::validate() const

View File

@ -466,7 +466,7 @@ const TriangleMesh &SupportTreeBuilder::merged_mesh() const
return m_meshcache;
}
m_meshcache = convert_mesh(merged);
m_meshcache = to_triangle_mesh(merged);
// The mesh will be passed by const-pointer to TriangleMeshSlicer,
// which will need this.

View File

@ -228,6 +228,26 @@ EigenMesh3D::EigenMesh3D(const EigenMesh3D &other):
m_V(other.m_V), m_F(other.m_F), m_ground_level(other.m_ground_level),
m_aabb( new AABBImpl(*other.m_aabb) ) {}
EigenMesh3D::EigenMesh3D(const Contour3D &other)
{
m_V.resize(Eigen::Index(other.points.size()), 3);
m_F.resize(Eigen::Index(other.faces3.size() + 2 * other.faces4.size()), 3);
for (Eigen::Index i = 0; i < Eigen::Index(other.points.size()); ++i)
m_V.row(i) = other.points[size_t(i)];
for (Eigen::Index i = 0; i < Eigen::Index(other.faces3.size()); ++i)
m_F.row(i) = other.faces3[size_t(i)];
size_t N = other.faces3.size() + 2 * other.faces4.size();
for (size_t i = other.faces3.size(); i < N; i += 2) {
size_t quad_idx = (i - other.faces3.size()) / 2;
auto & quad = other.faces4[quad_idx];
m_F.row(Eigen::Index(i)) = Vec3i{quad(0), quad(1), quad(2)};
m_F.row(Eigen::Index(i + 1)) = Vec3i{quad(2), quad(3), quad(0)};
}
}
EigenMesh3D &EigenMesh3D::operator=(const EigenMesh3D &other)
{
m_V = other.m_V;

File diff suppressed because it is too large Load Diff

View File

@ -19,10 +19,19 @@ static Slic3r::TriangleMesh load_model(const std::string &obj_filename)
return mesh;
}
TEST_CASE("Load object", "[Hollowing]") {
Slic3r::TriangleMesh mesh = load_model("20mm_cube.obj");
static bool _check_normals(const Slic3r::sla::Contour3D &mesh)
{
for (auto & face : mesh.faces3)
{
}
Slic3r::sla::Contour3D imesh = Slic3r::sla::convert_mesh(mesh);
return false;
}
TEST_CASE("Negative 3D offset should produce smaller object.", "[Hollowing]")
{
Slic3r::sla::Contour3D imesh = Slic3r::sla::Contour3D{load_model("20mm_cube.obj")};
auto ptr = Slic3r::meshToVolume(imesh, {});
REQUIRE(ptr);
@ -31,6 +40,8 @@ TEST_CASE("Load object", "[Hollowing]") {
REQUIRE(!omesh.empty());
std::fstream outfile{"out.obj", std::ios::out};
omesh.to_obj(outfile);

View File

@ -1,5 +1,5 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests.cpp)
add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp sla_print_tests.cpp)
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r)
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")

View File

@ -1,9 +1,9 @@
#include <catch_main.hpp>
#include <unordered_set>
#include <unordered_map>
#include <random>
#include <catch2/catch.hpp>
// Debug
#include <fstream>
@ -50,21 +50,21 @@ void check_validity(const TriangleMesh &input_mesh,
ASSUME_NO_REPAIR)
{
TriangleMesh mesh{input_mesh};
if (flags & ASSUME_NO_EMPTY) {
REQUIRE_FALSE(mesh.empty());
} else if (mesh.empty())
return; // If it can be empty and it is, there is nothing left to do.
REQUIRE(stl_validate(&mesh.stl));
bool do_update_shared_vertices = false;
mesh.repair(do_update_shared_vertices);
if (flags & ASSUME_NO_REPAIR) {
REQUIRE_FALSE(mesh.needed_repair());
}
if (flags & ASSUME_MANIFOLD) {
mesh.require_shared_vertices();
if (!mesh.is_manifold()) mesh.WriteOBJFile("non_manifold.obj");
@ -82,36 +82,36 @@ struct PadByproducts
void _test_concave_hull(const Polygons &hull, const ExPolygons &polys)
{
REQUIRE(polys.size() >=hull.size());
double polys_area = 0;
for (const ExPolygon &p : polys) polys_area += p.area();
double cchull_area = 0;
for (const Slic3r::Polygon &p : hull) cchull_area += p.area();
REQUIRE(cchull_area >= Approx(polys_area));
size_t cchull_holes = 0;
for (const Slic3r::Polygon &p : hull)
cchull_holes += p.is_clockwise() ? 1 : 0;
REQUIRE(cchull_holes == 0);
Polygons intr = diff(to_polygons(polys), hull);
REQUIRE(intr.empty());
}
void test_concave_hull(const ExPolygons &polys) {
sla::PadConfig pcfg;
Slic3r::sla::ConcaveHull cchull{polys, pcfg.max_merge_dist_mm, []{}};
_test_concave_hull(cchull.polygons(), polys);
coord_t delta = scaled(pcfg.brim_size_mm + pcfg.wing_distance());
ExPolygons wafflex = sla::offset_waffle_style_ex(cchull, delta);
Polygons waffl = sla::offset_waffle_style(cchull, delta);
_test_concave_hull(to_polygons(wafflex), polys);
_test_concave_hull(waffl, polys);
}
@ -121,23 +121,23 @@ void test_pad(const std::string & obj_filename,
PadByproducts & out)
{
REQUIRE(padcfg.validate().empty());
TriangleMesh mesh = load_model(obj_filename);
REQUIRE_FALSE(mesh.empty());
// Create pad skeleton only from the model
Slic3r::sla::pad_blueprint(mesh, out.model_contours);
test_concave_hull(out.model_contours);
REQUIRE_FALSE(out.model_contours.empty());
// Create the pad geometry for the model contours only
Slic3r::sla::create_pad({}, out.model_contours, out.mesh, padcfg);
check_validity(out.mesh);
auto bb = out.mesh.bounding_box();
REQUIRE(bb.max.z() - bb.min.z() == Approx(padcfg.full_height()));
}
@ -166,42 +166,42 @@ void check_support_tree_integrity(const sla::SupportTreeBuilder &stree,
double gnd = stree.ground_level;
double H1 = cfg.max_solo_pillar_height_mm;
double H2 = cfg.max_dual_pillar_height_mm;
for (const sla::Head &head : stree.heads()) {
REQUIRE((!head.is_valid() || head.pillar_id != sla::ID_UNSET ||
head.bridge_id != sla::ID_UNSET));
}
for (const sla::Pillar &pillar : stree.pillars()) {
if (std::abs(pillar.endpoint().z() - gnd) < EPSILON) {
double h = pillar.height;
if (h > H1) REQUIRE(pillar.links >= 1);
else if(h > H2) { REQUIRE(pillar.links >= 2); }
}
REQUIRE(pillar.links <= cfg.pillar_cascade_neighbors);
REQUIRE(pillar.bridges <= cfg.max_bridges_on_pillar);
}
double max_bridgelen = 0.;
auto chck_bridge = [&cfg](const sla::Bridge &bridge, double &max_brlen) {
Vec3d n = bridge.endp - bridge.startp;
double d = sla::distance(n);
max_brlen = std::max(d, max_brlen);
double z = n.z();
double polar = std::acos(z / d);
double slope = -polar + PI / 2.;
REQUIRE(std::abs(slope) >= cfg.bridge_slope - EPSILON);
};
for (auto &bridge : stree.bridges()) chck_bridge(bridge, max_bridgelen);
REQUIRE(max_bridgelen <= cfg.max_bridge_length_mm);
max_bridgelen = 0;
for (auto &bridge : stree.crossbridges()) chck_bridge(bridge, max_bridgelen);
double md = cfg.max_pillar_link_distance_mm / std::cos(-cfg.bridge_slope);
REQUIRE(max_bridgelen <= md);
}
@ -212,35 +212,35 @@ void test_supports(const std::string & obj_filename,
{
using namespace Slic3r;
TriangleMesh mesh = load_model(obj_filename);
REQUIRE_FALSE(mesh.empty());
TriangleMeshSlicer slicer{&mesh};
auto bb = mesh.bounding_box();
double zmin = bb.min.z();
double zmax = bb.max.z();
double gnd = zmin - supportcfg.object_elevation_mm;
auto layer_h = 0.05f;
out.slicegrid = grid(float(gnd), float(zmax), layer_h);
slicer.slice(out.slicegrid , CLOSING_RADIUS, &out.model_slices, []{});
// Create the special index-triangle mesh with spatial indexing which
// is the input of the support point and support mesh generators
sla::EigenMesh3D emesh{mesh};
// Create the support point generator
sla::SLAAutoSupports::Config autogencfg;
autogencfg.head_diameter = float(2 * supportcfg.head_front_radius_mm);
sla::SLAAutoSupports point_gen{emesh, out.model_slices, out.slicegrid,
autogencfg, [] {}, [](int) {}};
// Get the calculated support points.
std::vector<sla::SupportPoint> support_points = point_gen.output();
int validityflags = ASSUME_NO_REPAIR;
// If there is no elevation, support points shall be removed from the
// bottom of the object.
if (std::abs(supportcfg.object_elevation_mm) < EPSILON) {
@ -249,32 +249,32 @@ void test_supports(const std::string & obj_filename,
} else {
// Should be support points at least on the bottom of the model
REQUIRE_FALSE(support_points.empty());
// Also the support mesh should not be empty.
validityflags |= ASSUME_NO_EMPTY;
}
// Generate the actual support tree
sla::SupportTreeBuilder treebuilder;
treebuilder.build(sla::SupportableMesh{emesh, support_points, supportcfg});
check_support_tree_integrity(treebuilder, supportcfg);
const TriangleMesh &output_mesh = treebuilder.retrieve_mesh();
check_validity(output_mesh, validityflags);
// Quick check if the dimensions and placement of supports are correct
auto obb = output_mesh.bounding_box();
double allowed_zmin = zmin - supportcfg.object_elevation_mm;
if (std::abs(supportcfg.object_elevation_mm) < EPSILON)
allowed_zmin = zmin - 2 * supportcfg.head_back_radius_mm;
REQUIRE(obb.min.z() >= allowed_zmin);
REQUIRE(obb.max.z() <= zmax);
// Move out the support tree into the byproducts, we can examine it further
// in various tests.
out.obj_fname = std::move(obj_filename);
@ -296,7 +296,7 @@ void export_failed_case(const std::vector<ExPolygons> &support_slices,
const ExPolygons &sup_slice = support_slices[n];
const ExPolygons &mod_slice = byproducts.model_slices[n];
Polygons intersections = intersection(sup_slice, mod_slice);
std::stringstream ss;
if (!intersections.empty()) {
ss << byproducts.obj_fname << std::setprecision(4) << n << ".svg";
@ -307,7 +307,7 @@ void export_failed_case(const std::vector<ExPolygons> &support_slices,
svg.Close();
}
}
TriangleMesh m;
byproducts.supporttree.retrieve_full_mesh(m);
m.merge(byproducts.input_mesh);
@ -321,56 +321,56 @@ void test_support_model_collision(
const sla::SupportConfig &input_supportcfg = {})
{
SupportByproducts byproducts;
sla::SupportConfig supportcfg = input_supportcfg;
// Set head penetration to a small negative value which should ensure that
// the supports will not touch the model body.
supportcfg.head_penetration_mm = -0.15;
// TODO: currently, the tailheads penetrating into the model body do not
// respect the penetration parameter properly. No issues were reported so
// far but we should definitely fix this.
supportcfg.ground_facing_only = true;
test_supports(obj_filename, supportcfg, byproducts);
// Slice the support mesh given the slice grid of the model.
std::vector<ExPolygons> support_slices =
byproducts.supporttree.slice(byproducts.slicegrid, CLOSING_RADIUS);
// The slices originate from the same slice grid so the numbers must match
bool support_mesh_is_empty =
byproducts.supporttree.retrieve_mesh(sla::MeshType::Pad).empty() &&
byproducts.supporttree.retrieve_mesh(sla::MeshType::Support).empty();
if (support_mesh_is_empty)
REQUIRE(support_slices.empty());
else
REQUIRE(support_slices.size() == byproducts.model_slices.size());
bool notouch = true;
for (size_t n = 0; notouch && n < support_slices.size(); ++n) {
const ExPolygons &sup_slice = support_slices[n];
const ExPolygons &mod_slice = byproducts.model_slices[n];
Polygons intersections = intersection(sup_slice, mod_slice);
notouch = notouch && intersections.empty();
}
if (!notouch) export_failed_case(support_slices, byproducts);
REQUIRE(notouch);
}
const char * const BELOW_PAD_TEST_OBJECTS[] = {
const char *const BELOW_PAD_TEST_OBJECTS[] = {
"20mm_cube.obj",
"V.obj",
};
const char * const AROUND_PAD_TEST_OBJECTS[] = {
const char *const AROUND_PAD_TEST_OBJECTS[] = {
"20mm_cube.obj",
"V.obj",
"frog_legs.obj",
@ -392,46 +392,46 @@ template <class I, class II> void test_pairhash()
I A[nums] = {0}, B[nums] = {0};
std::unordered_set<I> CH;
std::unordered_map<II, std::pair<I, I>> ints;
std::random_device rd;
std::mt19937 gen(rd());
const I Ibits = int(sizeof(I) * CHAR_BIT);
const II IIbits = int(sizeof(II) * CHAR_BIT);
int bits = IIbits / 2 < Ibits ? Ibits / 2 : Ibits;
if (std::is_signed<I>::value) bits -= 1;
const I Imin = 0;
const I Imax = I(std::pow(2., bits) - 1);
std::uniform_int_distribution<I> dis(Imin, Imax);
for (size_t i = 0; i < nums;) {
I a = dis(gen);
if (CH.find(a) == CH.end()) { CH.insert(a); A[i] = a; ++i; }
}
for (size_t i = 0; i < nums;) {
I b = dis(gen);
if (CH.find(b) == CH.end()) { CH.insert(b); B[i] = b; ++i; }
}
for (size_t i = 0; i < nums; ++i) {
I a = A[i], b = B[i];
REQUIRE(a != b);
II hash_ab = sla::pairhash<I, II>(a, b);
II hash_ba = sla::pairhash<I, II>(b, a);
REQUIRE(hash_ab == hash_ba);
auto it = ints.find(hash_ab);
if (it != ints.end()) {
REQUIRE((
(it->second.first == a && it->second.second == b) ||
(it->second.first == b && it->second.second == a)
));
));
} else
ints[hash_ab] = std::make_pair(a, b);
}
@ -446,72 +446,72 @@ TEST_CASE("Pillar pairhash should be unique", "[SLASupportGeneration]") {
TEST_CASE("Flat pad geometry is valid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Disable wings
padcfg.wall_height_mm = .0;
for (auto &fname : BELOW_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("WingedPadGeometryIsValid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Add some wings to the pad to test the cavity
padcfg.wall_height_mm = 1.;
for (auto &fname : BELOW_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("FlatPadAroundObjectIsValid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Add some wings to the pad to test the cavity
padcfg.wall_height_mm = 0.;
// padcfg.embed_object.stick_stride_mm = 0.;
padcfg.embed_object.enabled = true;
padcfg.embed_object.everywhere = true;
for (auto &fname : AROUND_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("WingedPadAroundObjectIsValid", "[SLASupportGeneration]") {
sla::PadConfig padcfg;
// Add some wings to the pad to test the cavity
padcfg.wall_height_mm = 1.;
padcfg.embed_object.enabled = true;
padcfg.embed_object.everywhere = true;
for (auto &fname : AROUND_PAD_TEST_OBJECTS) test_pad(fname, padcfg);
}
TEST_CASE("ElevatedSupportGeometryIsValid", "[SLASupportGeneration]") {
sla::SupportConfig supportcfg;
supportcfg.object_elevation_mm = 5.;
for (auto fname : SUPPORT_TEST_MODELS) test_supports(fname);
}
TEST_CASE("FloorSupportGeometryIsValid", "[SLASupportGeneration]") {
sla::SupportConfig supportcfg;
supportcfg.object_elevation_mm = 0;
for (auto &fname: SUPPORT_TEST_MODELS) test_supports(fname, supportcfg);
}
TEST_CASE("ElevatedSupportsDoNotPierceModel", "[SLASupportGeneration]") {
sla::SupportConfig supportcfg;
for (auto fname : SUPPORT_TEST_MODELS)
test_support_model_collision(fname, supportcfg);
}
TEST_CASE("FloorSupportsDoNotPierceModel", "[SLASupportGeneration]") {
sla::SupportConfig supportcfg;
supportcfg.object_elevation_mm = 0;
for (auto fname : SUPPORT_TEST_MODELS)
test_support_model_collision(fname, supportcfg);
}
@ -525,7 +525,7 @@ TEST_CASE("InitializedRasterShouldBeNONEmpty", "[SLARasterOutput]") {
// Default Prusa SL1 display parameters
sla::Raster::Resolution res{2560, 1440};
sla::Raster::PixelDim pixdim{120. / res.width_px, 68. / res.height_px};
sla::Raster raster;
raster.reset(res, pixdim);
REQUIRE_FALSE(raster.empty());
@ -547,54 +547,54 @@ static void check_raster_transformations(sla::Raster::Orientation o,
double disp_w = 120., disp_h = 68.;
sla::Raster::Resolution res{2560, 1440};
sla::Raster::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
sla::Raster::Trafo trafo{o, mirroring};
trafo.origin_x = bb.center().x();
trafo.origin_y = bb.center().y();
sla::Raster raster{res, pixdim, trafo};
// create box of size 32x32 pixels (not 1x1 to avoid antialiasing errors)
coord_t pw = 32 * coord_t(std::ceil(scaled<double>(pixdim.w_mm)));
coord_t ph = 32 * coord_t(std::ceil(scaled<double>(pixdim.h_mm)));
ExPolygon box;
box.contour.points = {{-pw, -ph}, {pw, -ph}, {pw, ph}, {-pw, ph}};
double tr_x = scaled<double>(20.), tr_y = tr_x;
box.translate(tr_x, tr_y);
ExPolygon expected_box = box;
// Now calculate the position of the translated box according to output
// trafo.
if (o == sla::Raster::Orientation::roPortrait) expected_box.rotate(PI / 2.);
if (mirroring[X])
for (auto &p : expected_box.contour.points) p.x() = -p.x();
if (mirroring[Y])
for (auto &p : expected_box.contour.points) p.y() = -p.y();
raster.draw(box);
Point expected_coords = expected_box.contour.bounding_box().center();
double rx = unscaled(expected_coords.x() + bb.center().x()) / pixdim.w_mm;
double ry = unscaled(expected_coords.y() + bb.center().y()) / pixdim.h_mm;
auto w = size_t(std::floor(rx));
auto h = res.height_px - size_t(std::floor(ry));
REQUIRE((w < res.width_px && h < res.height_px));
auto px = raster.read_pixel(w, h);
if (px != FullWhite) {
sla::PNGImage img;
std::fstream outf("out.png", std::ios::out);
outf << img.serialize(raster);
}
REQUIRE(px == FullWhite);
}
@ -603,7 +603,7 @@ TEST_CASE("MirroringShouldBeCorrect", "[SLARasterOutput]") {
sla::Raster::MirrorX,
sla::Raster::MirrorY,
sla::Raster::MirrorXY};
sla::Raster::Orientation orientations[] = {sla::Raster::roLandscape,
sla::Raster::roPortrait};
for (auto orientation : orientations)
@ -615,7 +615,7 @@ static ExPolygon square_with_hole(double v)
{
ExPolygon poly;
coord_t V = scaled(v / 2.);
poly.contour.points = {{-V, -V}, {V, -V}, {V, V}, {-V, V}};
poly.holes.emplace_back();
V = V / 2;
@ -631,16 +631,16 @@ static double pixel_area(TPixel px, const sla::Raster::PixelDim &pxdim)
static double raster_white_area(const sla::Raster &raster)
{
if (raster.empty()) return std::nan("");
auto res = raster.resolution();
double a = 0;
for (size_t x = 0; x < res.width_px; ++x)
for (size_t y = 0; y < res.height_px; ++y) {
auto px = raster.read_pixel(x, y);
a += pixel_area(px, raster.pixel_dimensions());
}
return a;
}
@ -648,15 +648,15 @@ static double predict_error(const ExPolygon &p, const sla::Raster::PixelDim &pd)
{
auto lines = p.lines();
double pix_err = pixel_area(FullWhite, pd) / 2.;
// Worst case is when a line is parallel to the shorter axis of one pixel,
// when the line will be composed of the max number of pixels
double pix_l = std::min(pd.h_mm, pd.w_mm);
double error = 0.;
for (auto &l : lines)
error += (unscaled(l.length()) / pix_l) * pix_err;
return error;
}
@ -664,28 +664,42 @@ TEST_CASE("RasterizedPolygonAreaShouldMatch", "[SLARasterOutput]") {
double disp_w = 120., disp_h = 68.;
sla::Raster::Resolution res{2560, 1440};
sla::Raster::PixelDim pixdim{disp_w / res.width_px, disp_h / res.height_px};
sla::Raster raster{res, pixdim};
auto bb = BoundingBox({0, 0}, {scaled(disp_w), scaled(disp_h)});
ExPolygon poly = square_with_hole(10.);
poly.translate(bb.center().x(), bb.center().y());
raster.draw(poly);
double a = poly.area() / (scaled<double>(1.) * scaled(1.));
double ra = raster_white_area(raster);
double diff = std::abs(a - ra);
REQUIRE(diff <= predict_error(poly, pixdim));
raster.clear();
poly = square_with_hole(60.);
poly.translate(bb.center().x(), bb.center().y());
raster.draw(poly);
a = poly.area() / (scaled<double>(1.) * scaled(1.));
ra = raster_white_area(raster);
diff = std::abs(a - ra);
REQUIRE(diff <= predict_error(poly, pixdim));
}
TEST_CASE("Triangle mesh conversions should be correct", "[SLAConversions]")
{
sla::Contour3D cntr;
{
std::fstream infile{"extruder_idler_quads.obj", std::ios::in};
cntr.from_obj(infile);
}
}

View File

@ -0,0 +1 @@
#include <catch_main.hpp>