mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 19:31:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
377034104c | ||
|
|
468fa3be86 | ||
|
|
824b092a6e | ||
|
|
ec0d8c225f | ||
|
|
3040ebaac1 | ||
|
|
5298e49dd2 | ||
|
|
d347a80ef8 | ||
|
|
e340a13c18 | ||
|
|
9e16cdb23b | ||
|
|
58842bab05 | ||
|
|
04204ec0d3 | ||
|
|
9ed459e132 |
@@ -48,24 +48,37 @@ bool load_drc(const char *path, TriangleMesh *meshptr)
|
|||||||
indexed_triangle_set its;
|
indexed_triangle_set its;
|
||||||
|
|
||||||
const PointAttribute *const positions = dracoMesh.GetNamedAttribute(GeometryAttribute::POSITION);
|
const PointAttribute *const positions = dracoMesh.GetNamedAttribute(GeometryAttribute::POSITION);
|
||||||
|
if (positions == nullptr) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "load_drc: the mesh has no POSITION attribute";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
size_t num_vertices = positions->size();
|
size_t num_vertices = positions->size();
|
||||||
its.vertices.reserve(num_vertices);
|
its.vertices.reserve(num_vertices);
|
||||||
for (AttributeValueIndex i(0); i < num_vertices; ++ i) {
|
for (AttributeValueIndex i(0); i < num_vertices; ++ i) {
|
||||||
float pos[3];
|
float pos[3];
|
||||||
positions->ConvertValue<float>(i, 3, pos);
|
if (!positions->ConvertValue<float>(i, 3, pos)) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "load_drc: invalid vertex position";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
its.vertices.emplace_back(pos[0], pos[1], pos[2]);
|
its.vertices.emplace_back(pos[0], pos[1], pos[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The Draco decoder does not check face indices against the point count.
|
||||||
|
const uint32_t num_points = dracoMesh.num_points();
|
||||||
size_t num_faces = dracoMesh.num_faces();
|
size_t num_faces = dracoMesh.num_faces();
|
||||||
its.indices.reserve(num_faces);
|
its.indices.reserve(num_faces);
|
||||||
for (FaceIndex i(0); i < num_faces; ++ i) {
|
for (FaceIndex i(0); i < num_faces; ++ i) {
|
||||||
Mesh::Face face = dracoMesh.face(i);
|
const Mesh::Face &face = dracoMesh.face(i);
|
||||||
|
stl_triangle_vertex_indices facet;
|
||||||
its.indices.emplace_back(
|
for (int k = 0; k < 3; ++ k) {
|
||||||
positions->mapped_index(face[0]).value(),
|
const size_t vertex_idx = face[k].value() < num_points ? positions->mapped_index(face[k]).value() : num_vertices;
|
||||||
positions->mapped_index(face[1]).value(),
|
if (vertex_idx >= num_vertices) {
|
||||||
positions->mapped_index(face[2]).value()
|
BOOST_LOG_TRIVIAL(error) << "load_drc: invalid vertex index";
|
||||||
);
|
return false;
|
||||||
|
}
|
||||||
|
facet[k] = static_cast<int>(vertex_idx);
|
||||||
|
}
|
||||||
|
its.indices.emplace_back(facet);
|
||||||
}
|
}
|
||||||
|
|
||||||
*meshptr = TriangleMesh(std::move(its));
|
*meshptr = TriangleMesh(std::move(its));
|
||||||
|
|||||||
@@ -166,10 +166,15 @@ bool load_obj(const char *path, TriangleMesh *meshptr, ObjInfo& obj_info, std::s
|
|||||||
obj_info.uv_map_pngs[face_index] = png_name;
|
obj_info.uv_map_pngs[face_index] = png_name;
|
||||||
}
|
}
|
||||||
if (data.textureCoordinates.size() > 0) {
|
if (data.textureCoordinates.size() > 0) {
|
||||||
Vec2f uv0(data.textureCoordinates[uvs[0] * 2], data.textureCoordinates[uvs[0] * 2 + 1]);
|
// A face vertex may omit vt or reference a missing one. Fall back to (0, 0) rather than
|
||||||
Vec2f uv1(data.textureCoordinates[uvs[1] * 2], data.textureCoordinates[uvs[1] * 2 + 1]);
|
// skipping the face, so obj_info.uvs stays aligned with the face indices.
|
||||||
Vec2f uv2(data.textureCoordinates[uvs[2] * 2], data.textureCoordinates[uvs[2] * 2 + 1]);
|
const int uv_count = static_cast<int>(data.textureCoordinates.size() / OBJ_TEXCOORD_LENGTH);
|
||||||
std::array<Vec2f, 3> uv_array{uv0, uv1, uv2};
|
auto uv_at = [&data, uv_count](int idx) -> Vec2f {
|
||||||
|
if (idx < 0 || idx >= uv_count)
|
||||||
|
return Vec2f::Zero();
|
||||||
|
return Vec2f(data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH], data.textureCoordinates[idx * OBJ_TEXCOORD_LENGTH + 1]);
|
||||||
|
};
|
||||||
|
std::array<Vec2f, 3> uv_array{uv_at(uvs[0]), uv_at(uvs[1]), uv_at(uvs[2])};
|
||||||
obj_info.uvs.emplace_back(uv_array);
|
obj_info.uvs.emplace_back(uv_array);
|
||||||
}
|
}
|
||||||
obj_info.face_colors.emplace_back(face_color);
|
obj_info.face_colors.emplace_back(face_color);
|
||||||
|
|||||||
@@ -1629,7 +1629,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
}
|
}
|
||||||
while (it != m_plater_data.end())
|
while (it != m_plater_data.end())
|
||||||
{
|
{
|
||||||
if (it->first <= 0 || static_cast<size_t>(it->first) > m_plater_data.size())
|
if (it->first > m_plater_data.size())
|
||||||
{
|
{
|
||||||
add_error("invalid plate index");
|
add_error("invalid plate index");
|
||||||
return false;
|
return false;
|
||||||
@@ -2312,7 +2312,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
|
|||||||
}
|
}
|
||||||
while (it != m_plater_data.end())
|
while (it != m_plater_data.end())
|
||||||
{
|
{
|
||||||
if (it->first <= 0 || static_cast<size_t>(it->first) > m_plater_data.size())
|
if (it->first > m_plater_data.size())
|
||||||
{
|
{
|
||||||
add_error("invalid plate index");
|
add_error("invalid plate index");
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -51,19 +51,18 @@ static bool obj_parseline(const char *line, ObjData &data)
|
|||||||
line = endptr;
|
line = endptr;
|
||||||
EATWS();
|
EATWS();
|
||||||
}
|
}
|
||||||
/*double w = 0;
|
// The optional w is accepted but not stored: only u and v are used.
|
||||||
if (*line != 0) {
|
if (*line != 0) {
|
||||||
w = strtod(line, &endptr);
|
strtod(line, &endptr);
|
||||||
if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0))
|
if (endptr == 0 || (*endptr != ' ' && *endptr != '\t' && *endptr != 0))
|
||||||
return false;
|
return false;
|
||||||
line = endptr;
|
line = endptr;
|
||||||
EATWS();
|
EATWS();
|
||||||
}*/
|
}
|
||||||
if (*line != 0)
|
if (*line != 0)
|
||||||
return false;
|
return false;
|
||||||
data.textureCoordinates.push_back((float)u);
|
data.textureCoordinates.push_back((float)u);
|
||||||
data.textureCoordinates.push_back((float)v);
|
data.textureCoordinates.push_back((float)v);
|
||||||
//data.textureCoordinates.push_back((float)w);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'n':
|
case 'n':
|
||||||
@@ -245,7 +244,7 @@ static bool obj_parseline(const char *line, ObjData &data)
|
|||||||
else
|
else
|
||||||
-- vertex.normalIdx;
|
-- vertex.normalIdx;
|
||||||
if (vertex.textureCoordIdx < 0)
|
if (vertex.textureCoordIdx < 0)
|
||||||
vertex.textureCoordIdx += (int)data.textureCoordinates.size() / 3;
|
vertex.textureCoordIdx += (int)data.textureCoordinates.size() / OBJ_TEXCOORD_LENGTH;
|
||||||
else
|
else
|
||||||
-- vertex.textureCoordIdx;
|
-- vertex.textureCoordIdx;
|
||||||
data.vertices.push_back(vertex);
|
data.vertices.push_back(vertex);
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ inline bool operator==(const ObjSmoothingGroup &v1, const ObjSmoothingGroup &v2)
|
|||||||
}
|
}
|
||||||
#define OBJ_VERTEX_COLOR_ALPHA 6
|
#define OBJ_VERTEX_COLOR_ALPHA 6
|
||||||
#define OBJ_VERTEX_LENGTH 7 // x, y, z, color_x,color_y,color_z,color_w
|
#define OBJ_VERTEX_LENGTH 7 // x, y, z, color_x,color_y,color_z,color_w
|
||||||
|
#define OBJ_TEXCOORD_LENGTH 2 // u, v
|
||||||
#define ONE_FACE_SIZE 4//ONE_FACE format: f 8/4/6 7/3/6 6/2/6 -1/-1/-1
|
#define ONE_FACE_SIZE 4//ONE_FACE format: f 8/4/6 7/3/6 6/2/6 -1/-1/-1
|
||||||
struct ObjData {
|
struct ObjData {
|
||||||
// Version of the data structure for load / store in the private binary format.
|
// Version of the data structure for load / store in the private binary format.
|
||||||
@@ -100,7 +101,7 @@ struct ObjData {
|
|||||||
// x, y, z, color_x,color_y,color_z,color_w
|
// x, y, z, color_x,color_y,color_z,color_w
|
||||||
std::vector<float> coordinates;
|
std::vector<float> coordinates;
|
||||||
bool has_vertex_color{false};
|
bool has_vertex_color{false};
|
||||||
// u, v, w
|
// u, v
|
||||||
std::vector<float> textureCoordinates;
|
std::vector<float> textureCoordinates;
|
||||||
// x, y, z
|
// x, y, z
|
||||||
std::vector<float> normals;
|
std::vector<float> normals;
|
||||||
|
|||||||
+10
-5
@@ -2504,7 +2504,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = path;
|
ctx.msg = path;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2556,7 +2557,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\n" + err_msg;
|
ctx.msg = std::string(path) + "\n" + err_msg;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2580,7 +2582,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
boost::nowide::remove(path_tmp.c_str());
|
boost::nowide::remove(path_tmp.c_str());
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\n" + ex.what();
|
ctx.msg = std::string(path) + "\n" + ex.what();
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2693,7 +2696,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
if (ret) {
|
if (ret) {
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
|
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2712,7 +2716,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = path;
|
ctx.msg = path;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
|
|||||||
@@ -3702,11 +3702,7 @@ void FacetsAnnotation::set_triangle_from_string(int triangle_id, const std::stri
|
|||||||
m_data.bitstream.insert(m_data.bitstream.end(), bool(dec & (1 << i)));
|
m_data.bitstream.insert(m_data.bitstream.end(), bool(dec & (1 << i)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_data.update_used_states(bitstream_start_idx)) {
|
m_data.update_used_states(bitstream_start_idx);
|
||||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": dropping malformed paint data of triangle " << triangle_id;
|
|
||||||
m_data.bitstream.resize(bitstream_start_idx);
|
|
||||||
m_data.triangles_to_split.pop_back();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FacetsAnnotation::equals(const FacetsAnnotation &other) const
|
bool FacetsAnnotation::equals(const FacetsAnnotation &other) const
|
||||||
|
|||||||
+10
-5
@@ -2717,7 +2717,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
|
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
|
||||||
@@ -3344,7 +3345,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
|
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
|
||||||
@@ -4950,7 +4952,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
|||||||
{
|
{
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = file;
|
ctx.msg = file;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
@@ -4980,7 +4983,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
|||||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = file + "\n" + ex.what();
|
ctx.msg = file + "\n" + ex.what();
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
@@ -4994,7 +4998,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = file;
|
ctx.msg = file;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
|
|||||||
@@ -1778,13 +1778,6 @@ TriangleSelector::TriangleSplittingData TriangleSelector::serialize() const {
|
|||||||
return out.data;
|
return out.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A split code keeps the split side (one split) or the kept side (two splits) in its upper two
|
|
||||||
// bits, where 3 is not a side. The value is ignored for a three-side split.
|
|
||||||
static bool split_code_valid(int code)
|
|
||||||
{
|
|
||||||
return (code & 0b11) == 3 || (code >> 2) != 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
||||||
bool needs_reset,
|
bool needs_reset,
|
||||||
EnforcerBlockerType max_ebt,
|
EnforcerBlockerType max_ebt,
|
||||||
@@ -1819,12 +1812,11 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
|||||||
|
|
||||||
for (auto [triangle_id, ibit] : data.triangles_to_split) {
|
for (auto [triangle_id, ibit] : data.triangles_to_split) {
|
||||||
assert(triangle_id < int(m_triangles.size()));
|
assert(triangle_id < int(m_triangles.size()));
|
||||||
// Set when the bitstream runs out or holds an impossible split before this triangle's tree is complete.
|
assert(ibit < int(data.bitstream.size()));
|
||||||
bool corrupt = false;
|
auto next_nibble = [&data, &ibit = ibit]() {
|
||||||
auto next_nibble = [&data, &ibit = ibit, &corrupt]() {
|
|
||||||
int n = 0;
|
int n = 0;
|
||||||
if (! data.read_nibble(ibit, n))
|
for (int i = 0; i < 4; ++ i)
|
||||||
corrupt = true;
|
n |= data.bitstream[ibit ++] << i;
|
||||||
return n;
|
return n;
|
||||||
};
|
};
|
||||||
// Decode a leaf state stored behind the "11" prefix: one nibble of (state-3) for states
|
// Decode a leaf state stored behind the "11" prefix: one nibble of (state-3) for states
|
||||||
@@ -1843,10 +1835,6 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
|||||||
bool is_split = num_of_children != 0;
|
bool is_split = num_of_children != 0;
|
||||||
// Only valid if not is_split.
|
// Only valid if not is_split.
|
||||||
auto state = is_split ? EnforcerBlockerType::NONE : ((code & 0b1100) == 0b1100 ? decode_leaf_state() : EnforcerBlockerType(code >> 2));
|
auto state = is_split ? EnforcerBlockerType::NONE : ((code & 0b1100) == 0b1100 ? decode_leaf_state() : EnforcerBlockerType(code >> 2));
|
||||||
if (is_split && ! split_code_valid(code))
|
|
||||||
corrupt = true;
|
|
||||||
if (corrupt)
|
|
||||||
break;
|
|
||||||
|
|
||||||
// BBS
|
// BBS
|
||||||
if (state == to_delete_filament)
|
if (state == to_delete_filament)
|
||||||
@@ -1861,7 +1849,7 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only valid if is_split.
|
// Only valid if is_split.
|
||||||
int special_side = num_of_split_sides == 3 ? 0 : code >> 2;
|
int special_side = code >> 2;
|
||||||
|
|
||||||
// Take care of the first iteration separately, so handling of the others is simpler.
|
// Take care of the first iteration separately, so handling of the others is simpler.
|
||||||
if (parents.empty()) {
|
if (parents.empty()) {
|
||||||
@@ -1916,55 +1904,47 @@ void TriangleSelector::deserialize(const TriangleSplittingData &data,
|
|||||||
if (parents.empty())
|
if (parents.empty())
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (corrupt) {
|
|
||||||
// Every split above allocated all of its children, so the partial tree unwinds cleanly.
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": malformed paint data, dropping paint of triangle " << triangle_id;
|
|
||||||
undivide_triangle(triangle_id);
|
|
||||||
m_triangles[triangle_id].set_state(EnforcerBlockerType::NONE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TriangleSelector::TriangleSplittingData::update_used_states(const size_t bitstream_start_idx) {
|
void TriangleSelector::TriangleSplittingData::update_used_states(const size_t bitstream_start_idx) {
|
||||||
int ibit = static_cast<int>(bitstream_start_idx);
|
assert(bitstream_start_idx < this->bitstream.size());
|
||||||
uint64_t states = 0;
|
assert(!this->bitstream.empty() && this->bitstream.size() != bitstream_start_idx);
|
||||||
do {
|
assert((this->bitstream.size() - bitstream_start_idx) % 4 == 0);
|
||||||
// Walk one triangle's tree depth-first, counting the nodes still to be read; a split node adds its children.
|
|
||||||
for (int pending_nodes = 1; pending_nodes > 0; --pending_nodes) {
|
|
||||||
int code;
|
|
||||||
if (!this->read_nibble(ibit, code))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (const int num_of_split_sides = code & 0b11; num_of_split_sides != 0) {
|
if (this->bitstream.empty() || this->bitstream.size() == bitstream_start_idx)
|
||||||
if (!split_code_valid(code))
|
return;
|
||||||
return false;
|
|
||||||
pending_nodes += num_of_split_sides + 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int facet_state = code >> 2;
|
size_t nibble_idx = bitstream_start_idx;
|
||||||
if (facet_state == 0b11) {
|
|
||||||
// Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18).
|
auto read_next_nibble = [&data_bitstream = std::as_const(this->bitstream), &nibble_idx]() -> uint8_t {
|
||||||
int nibble;
|
assert(nibble_idx + 3 < data_bitstream.size());
|
||||||
if (!this->read_nibble(ibit, nibble))
|
uint8_t code = 0;
|
||||||
return false;
|
for (size_t bit_idx = 0; bit_idx < 4; ++bit_idx)
|
||||||
facet_state = nibble + 3;
|
code |= data_bitstream[nibble_idx++] << bit_idx;
|
||||||
if (nibble == 0b1111) {
|
return code;
|
||||||
if (!this->read_nibble(ibit, nibble))
|
};
|
||||||
return false;
|
|
||||||
facet_state = nibble + 18;
|
while (nibble_idx < this->bitstream.size()) {
|
||||||
}
|
const uint8_t code = read_next_nibble();
|
||||||
}
|
|
||||||
states |= uint64_t(1) << facet_state;
|
if (const bool is_split = (code & 0b11) != 0; is_split)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
uint8_t facet_state;
|
||||||
|
if ((code & 0b1100) == 0b1100) {
|
||||||
|
// Leaf behind the "11" prefix: one nibble of (state-3), or 0b1111 + (state-18).
|
||||||
|
const uint8_t nibble = read_next_nibble();
|
||||||
|
facet_state = nibble == 0b1111 ? uint8_t(read_next_nibble() + 18) : uint8_t(nibble + 3);
|
||||||
|
} else {
|
||||||
|
facet_state = code >> 2;
|
||||||
}
|
}
|
||||||
} while (static_cast<size_t>(ibit) < this->bitstream.size());
|
assert(facet_state < this->used_states.size());
|
||||||
|
if (facet_state >= this->used_states.size())
|
||||||
|
continue;
|
||||||
|
|
||||||
// The leaf encoding tops out at state 33, so every state fits the 64-bit mask.
|
this->used_states[facet_state] = true;
|
||||||
for (size_t state_idx = 0; state_idx < std::min<size_t>(this->used_states.size(), 64); ++state_idx)
|
}
|
||||||
if (states & (uint64_t(1) << state_idx))
|
|
||||||
this->used_states[state_idx] = true;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lightweight variant of deserialization, which only tests whether a face of test_state exists.
|
// Lightweight variant of deserialization, which only tests whether a face of test_state exists.
|
||||||
@@ -1976,12 +1956,11 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
|
|||||||
|
|
||||||
for (const TriangleBitStreamMapping &triangle_id_and_ibit : data.triangles_to_split) {
|
for (const TriangleBitStreamMapping &triangle_id_and_ibit : data.triangles_to_split) {
|
||||||
int ibit = triangle_id_and_ibit.bitstream_start_idx;
|
int ibit = triangle_id_and_ibit.bitstream_start_idx;
|
||||||
// Stop reading a triangle whose stream is truncated.
|
assert(ibit < int(data.bitstream.size()));
|
||||||
bool truncated = false;
|
auto next_nibble = [&data, &ibit = ibit]() {
|
||||||
auto next_nibble = [&data, &ibit = ibit, &truncated]() {
|
|
||||||
int n = 0;
|
int n = 0;
|
||||||
if (! data.read_nibble(ibit, n))
|
for (int i = 0; i < 4; ++ i)
|
||||||
truncated = true;
|
n |= data.bitstream[ibit ++] << i;
|
||||||
return n;
|
return n;
|
||||||
};
|
};
|
||||||
// < 0 -> negative of a number of children
|
// < 0 -> negative of a number of children
|
||||||
@@ -1999,8 +1978,6 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
|
|||||||
};
|
};
|
||||||
|
|
||||||
int state = num_children_or_state();
|
int state = num_children_or_state();
|
||||||
if (truncated)
|
|
||||||
continue;
|
|
||||||
if (state < 0) {
|
if (state < 0) {
|
||||||
// Root is split.
|
// Root is split.
|
||||||
parents_children.clear();
|
parents_children.clear();
|
||||||
@@ -2008,8 +1985,6 @@ bool TriangleSelector::has_facets(const TriangleSplittingData &data, const Enfor
|
|||||||
do {
|
do {
|
||||||
if (-- parents_children.back() >= 0) {
|
if (-- parents_children.back() >= 0) {
|
||||||
int state = num_children_or_state();
|
int state = num_children_or_state();
|
||||||
if (truncated)
|
|
||||||
break;
|
|
||||||
if (state < 0)
|
if (state < 0)
|
||||||
// Child is split.
|
// Child is split.
|
||||||
parents_children.emplace_back(- state);
|
parents_children.emplace_back(- state);
|
||||||
|
|||||||
@@ -297,20 +297,8 @@ public:
|
|||||||
std::fill(used_states.begin(), used_states.end(), false);
|
std::fill(used_states.begin(), used_states.end(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update used states from the triangle trees stored between bitstream_start_idx and the end of the bitstream.
|
// Update used states based on the bitstream. It just iterated over the bitstream from the bitstream_start_idx till the end.
|
||||||
// Returns false and leaves used states untouched if a tree is truncated or malformed.
|
void update_used_states(size_t bitstream_start_idx);
|
||||||
bool update_used_states(size_t bitstream_start_idx);
|
|
||||||
|
|
||||||
// Read the 4-bit code at bit index ibit (LSB first) and advance ibit past it.
|
|
||||||
// Returns false without advancing when fewer than 4 bits remain.
|
|
||||||
bool read_nibble(int &ibit, int &nibble) const {
|
|
||||||
if (ibit < 0 || static_cast<size_t>(ibit) + 4 > bitstream.size())
|
|
||||||
return false;
|
|
||||||
nibble = 0;
|
|
||||||
for (int i = 0; i < 4; ++i)
|
|
||||||
nibble |= static_cast<int>(bitstream[ibit++]) << i;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class cereal::access;
|
friend class cereal::access;
|
||||||
|
|||||||
@@ -12800,8 +12800,6 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
|||||||
notification_manager->set_slicing_progress_export_possible();
|
notification_manager->set_slicing_progress_export_possible();
|
||||||
|
|
||||||
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
|
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
|
||||||
const std::string lifecycle_job_name = this->background_process.fff_print() ?
|
|
||||||
this->background_process.fff_print()->output_filename() : std::string();
|
|
||||||
this->background_process.reset_export();
|
this->background_process.reset_export();
|
||||||
// This bool stops showing export finished notification even when process_completed_with_error is false
|
// This bool stops showing export finished notification even when process_completed_with_error is false
|
||||||
bool has_error = false;
|
bool has_error = false;
|
||||||
@@ -12848,8 +12846,18 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
|||||||
|
|
||||||
{
|
{
|
||||||
Slic3r::LifecycleEventContext ctx;
|
Slic3r::LifecycleEventContext ctx;
|
||||||
ctx.name = lifecycle_job_name;
|
if (const PrintBase* print = this->background_process.current_print()) {
|
||||||
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
|
const Model& model = print->model();
|
||||||
|
ctx.id = std::to_string(model.id().id);
|
||||||
|
if (model.model_info)
|
||||||
|
ctx.name = model.model_info->model_name;
|
||||||
|
} else {
|
||||||
|
// Realistically Printbase* print will never be null because select_technology already asserts an active print
|
||||||
|
// and the worker thread asserts it before processing.
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": slicing completed without an active print; lifecycle event has no model ID";
|
||||||
|
}
|
||||||
|
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn :
|
||||||
|
(has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
|
||||||
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
|
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
|
||||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
|
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <boost/optional.hpp>
|
#include <boost/optional.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
#include <wx/app.h>
|
#include <wx/app.h>
|
||||||
@@ -115,10 +116,67 @@ std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config)
|
|||||||
return webui_url;
|
return webui_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Moonraker (Klipper's API server) reports a raised exception as { "error": { "code", "message", "traceback" } }
|
||||||
|
// under every host type that connects to it, often with the cause only in the traceback. Returns the reason to show,
|
||||||
|
// or empty for any other body.
|
||||||
|
std::string moonraker_error_reason(const std::string &body)
|
||||||
|
{
|
||||||
|
const auto root = nlohmann::json::parse(body, nullptr, false);
|
||||||
|
const auto err = root.find("error");
|
||||||
|
if (err == root.end())
|
||||||
|
return {};
|
||||||
|
const auto message = err->find("message");
|
||||||
|
const auto traceback = err->find("traceback");
|
||||||
|
if (message == err->end() || traceback == err->end() || !message->is_string() || !traceback->is_string())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const auto &msg = message->get_ref<const std::string &>();
|
||||||
|
const auto &tb = traceback->get_ref<const std::string &>();
|
||||||
|
if (msg.empty())
|
||||||
|
return {};
|
||||||
|
const auto end = tb.find_last_not_of(" \t\r\n");
|
||||||
|
if (end == std::string::npos)
|
||||||
|
return msg;
|
||||||
|
|
||||||
|
// Chained exceptions each start a new traceback; the one that failed the request is the last.
|
||||||
|
const auto header = tb.rfind("Traceback (most recent call last):", end);
|
||||||
|
|
||||||
|
// Tornado renders a raised HTTPError as "HTTP <code>: <reason>[ (<detail>)]", and the detail may span lines.
|
||||||
|
const auto code = err->find("code");
|
||||||
|
if (code != err->end() && code->is_number_integer()) {
|
||||||
|
const std::string marker = "HTTP " + std::to_string(code->get<int>()) + ": ";
|
||||||
|
const auto pos = tb.rfind(marker, end);
|
||||||
|
if (pos != std::string::npos && (header == std::string::npos || pos > header) && pos + marker.size() <= end) {
|
||||||
|
const std::string reason = tb.substr(pos + marker.size(), end + 1 - pos - marker.size());
|
||||||
|
// An HTTPError whose detail equals its reason, like HTTPError(401, "Unauthorized"), renders the phrase twice.
|
||||||
|
return reason == msg + " (" + msg + ")" ? msg : reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any other exception's type and message are everything from the first unindented line after its frames.
|
||||||
|
auto begin = (header == std::string::npos) ? std::string::npos : tb.find('\n', header);
|
||||||
|
while (begin != std::string::npos && begin < end) {
|
||||||
|
++begin;
|
||||||
|
if (tb[begin] != ' ' && tb[begin] != '\r' && tb[begin] != '\n')
|
||||||
|
break;
|
||||||
|
begin = tb.find('\n', begin);
|
||||||
|
}
|
||||||
|
if (begin == std::string::npos || begin > end) {
|
||||||
|
const auto nl = tb.rfind('\n', end);
|
||||||
|
begin = (nl == std::string::npos) ? 0 : nl + 1;
|
||||||
|
}
|
||||||
|
return msg + " (" + tb.substr(begin, end + 1 - begin) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
|
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
|
||||||
{
|
{
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
auto wxbody = wxString::FromUTF8(body.data());
|
const std::string reason = moonraker_error_reason(body);
|
||||||
|
auto wxbody = wxString::FromUTF8(reason.empty() ? body : reason);
|
||||||
return wxString::Format("HTTP %u: %s", status, wxbody);
|
return wxString::Format("HTTP %u: %s", status, wxbody);
|
||||||
} else {
|
} else {
|
||||||
if (error.find("curl:Timeout was reached") != std::string::npos) {
|
if (error.find("curl:Timeout was reached") != std::string::npos) {
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
#include "libslic3r/Layer.hpp"
|
#include "libslic3r/Layer.hpp"
|
||||||
#include "libslic3r/Model.hpp"
|
#include "libslic3r/Model.hpp"
|
||||||
#include "libslic3r/GCodeReader.hpp"
|
#include "libslic3r/GCodeReader.hpp"
|
||||||
|
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||||
|
#include "libslic3r/Exception.hpp"
|
||||||
|
#include "libslic3r/LifecycleEvents.hpp"
|
||||||
|
|
||||||
#include "test_helpers.hpp"
|
#include "test_helpers.hpp"
|
||||||
#include "test_utils.hpp"
|
#include "test_utils.hpp"
|
||||||
@@ -22,7 +25,10 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
using namespace Slic3r;
|
using namespace Slic3r;
|
||||||
using namespace Slic3r::Test;
|
using namespace Slic3r::Test;
|
||||||
@@ -224,8 +230,88 @@ std::string resolved_output_name(Model& model, const std::string& format, const
|
|||||||
return print.output_filename(filename_base);
|
return print.output_filename(filename_base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ScopedLifecycleHook
|
||||||
|
{
|
||||||
|
explicit ScopedLifecycleHook(LifecycleHookFn hook) { set_lifecycle_hook_fn(std::move(hook)); }
|
||||||
|
~ScopedLifecycleHook() { set_lifecycle_hook_fn(nullptr); }
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("Slicing lifecycle events identify the model", "[Print][LifecycleEvents]")
|
||||||
|
{
|
||||||
|
struct ObservedEvent {
|
||||||
|
LifecycleEvent event;
|
||||||
|
std::string id;
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
std::vector<ObservedEvent> events;
|
||||||
|
ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) {
|
||||||
|
events.push_back({ event, ctx.id, ctx.name });
|
||||||
|
});
|
||||||
|
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
ModelInfo info;
|
||||||
|
info.model_name = "Lifecycle test model";
|
||||||
|
model.model_info = std::make_shared<ModelInfo>(std::move(info));
|
||||||
|
init_print({cube(20)}, print, model);
|
||||||
|
|
||||||
|
print.process();
|
||||||
|
ScopedTemporaryFile temp(".gcode");
|
||||||
|
print.export_gcode(temp.string(), nullptr, nullptr);
|
||||||
|
GCodeProcessorResult result;
|
||||||
|
print.export_gcode_from_previous_file(temp.string(), &result);
|
||||||
|
|
||||||
|
const std::string expected_id = std::to_string(print.model().id().id);
|
||||||
|
const std::vector<LifecycleEvent> expected_events = {
|
||||||
|
LifecycleEvent::SliceStarted,
|
||||||
|
LifecycleEvent::SliceGeometryFinished,
|
||||||
|
LifecycleEvent::GCodeExportStarted,
|
||||||
|
LifecycleEvent::GCodeExportFinished,
|
||||||
|
LifecycleEvent::GCodeExportStarted,
|
||||||
|
LifecycleEvent::GCodeExportFinished,
|
||||||
|
};
|
||||||
|
REQUIRE(events.size() == expected_events.size());
|
||||||
|
for (size_t i = 0; i < expected_events.size(); ++i) {
|
||||||
|
CHECK(events[i].event == expected_events[i]);
|
||||||
|
CHECK(events[i].id == expected_id);
|
||||||
|
CHECK(events[i].name == "Lifecycle test model");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Slicing lifecycle event name is empty without model metadata", "[Print][LifecycleEvents]")
|
||||||
|
{
|
||||||
|
std::string event_id;
|
||||||
|
std::string event_name = "unset";
|
||||||
|
ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) {
|
||||||
|
if (event == LifecycleEvent::SliceStarted) {
|
||||||
|
event_id = ctx.id;
|
||||||
|
event_name = ctx.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
init_print({cube(20)}, print, model);
|
||||||
|
print.process();
|
||||||
|
|
||||||
|
CHECK(event_id == std::to_string(print.model().id().id));
|
||||||
|
CHECK(event_name.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Output filenames with numeric statistics fail before slicing finishes", "[Print][Regression]")
|
||||||
|
{
|
||||||
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||||
|
config.set_key_value("filename_format", new ConfigOptionString("{int(total_weight*10) / 10.0}"));
|
||||||
|
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
init_print({cube(20)}, print, model, config);
|
||||||
|
|
||||||
|
CHECK_THROWS_AS(print.output_filename(), PlaceholderParserError);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]")
|
TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]")
|
||||||
{
|
{
|
||||||
Model model;
|
Model model;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_clipper_utils.cpp
|
test_clipper_utils.cpp
|
||||||
test_config.cpp
|
test_config.cpp
|
||||||
test_config_variant_expansion.cpp
|
test_config_variant_expansion.cpp
|
||||||
|
test_drc.cpp
|
||||||
test_toolordering_nozzle_group.cpp
|
test_toolordering_nozzle_group.cpp
|
||||||
test_preset_bundle_loading.cpp
|
test_preset_bundle_loading.cpp
|
||||||
test_preset_setting_id.cpp
|
test_preset_setting_id.cpp
|
||||||
@@ -32,6 +33,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_mutable_priority_queue.cpp
|
test_mutable_priority_queue.cpp
|
||||||
test_minimum_spanning_tree.cpp
|
test_minimum_spanning_tree.cpp
|
||||||
test_nozzle_volume_type.cpp
|
test_nozzle_volume_type.cpp
|
||||||
|
test_obj.cpp
|
||||||
test_step.cpp
|
test_step.cpp
|
||||||
test_stl.cpp
|
test_stl.cpp
|
||||||
test_triangle_selector.cpp
|
test_triangle_selector.cpp
|
||||||
@@ -67,7 +69,9 @@ if (TARGET OpenVDB::openvdb)
|
|||||||
target_sources(${_TEST_NAME}_tests PRIVATE test_hollowing.cpp)
|
target_sources(${_TEST_NAME}_tests PRIVATE test_hollowing.cpp)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r Catch2::Catch2WithMain)
|
# libslic3r links Draco privately; test_drc.cpp encodes its own fixtures.
|
||||||
|
find_package(Draco REQUIRED)
|
||||||
|
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r draco::draco Catch2::Catch2WithMain)
|
||||||
target_include_directories(${_TEST_NAME}_tests PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
target_include_directories(${_TEST_NAME}_tests PRIVATE ${CMAKE_SOURCE_DIR}/src)
|
||||||
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
|
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
|
||||||
|
|
||||||
|
|||||||
+15
-131
@@ -19,7 +19,6 @@
|
|||||||
#include <boost/filesystem/operations.hpp>
|
#include <boost/filesystem/operations.hpp>
|
||||||
#include <boost/algorithm/string/predicate.hpp>
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
#include <catch2/catch_tostring.hpp>
|
#include <catch2/catch_tostring.hpp>
|
||||||
#include <Eigen/Core>
|
#include <Eigen/Core>
|
||||||
@@ -185,13 +184,16 @@ static bool read_cad_recipe_entry(const std::string& path, std::string& out,
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrites the archive at `path`, letting `edit` change the name or data of each entry; returns
|
// Rewrites the archive at `path` with the recipe entry back under the name it had before the
|
||||||
// whether `edit` reported a change for any of them. miniz cannot edit in place and
|
// rename, which is what every project saved by an earlier build looks like on disk. Generated
|
||||||
// open_zip_writer truncates, so the entries are held across the switch.
|
// rather than checked in because a whole project archive is not frozen evidence the way a bare
|
||||||
static bool rewrite_3mf_entries(const std::string& path, const std::function<bool(std::string& name, std::string& data)>& edit)
|
// recipe blob is -- it has to be whatever today's exporter writes, with only the name aged.
|
||||||
|
// miniz cannot rename in place and open_zip_writer truncates, so the entries are held across
|
||||||
|
// the switch.
|
||||||
|
static void rename_cad_recipe_entry_to_legacy(const std::string& path)
|
||||||
{
|
{
|
||||||
std::vector<std::pair<std::string, std::string>> entries;
|
std::vector<std::pair<std::string, std::string>> entries;
|
||||||
bool changed = false;
|
bool renamed = false;
|
||||||
{
|
{
|
||||||
mz_zip_archive zip;
|
mz_zip_archive zip;
|
||||||
mz_zip_zero_struct(&zip);
|
mz_zip_zero_struct(&zip);
|
||||||
@@ -206,140 +208,22 @@ static bool rewrite_3mf_entries(const std::string& path, const std::function<boo
|
|||||||
std::string data((size_t) st.m_uncomp_size, '\0');
|
std::string data((size_t) st.m_uncomp_size, '\0');
|
||||||
if (st.m_uncomp_size > 0)
|
if (st.m_uncomp_size > 0)
|
||||||
REQUIRE(mz_zip_reader_extract_to_mem(&zip, i, data.data(), data.size(), 0));
|
REQUIRE(mz_zip_reader_extract_to_mem(&zip, i, data.data(), data.size(), 0));
|
||||||
changed |= edit(name, data);
|
if (boost::algorithm::iequals(name, CAD_RECIPE_ENTRY)) {
|
||||||
|
name = LEGACY_CAD_RECIPE_ENTRY;
|
||||||
|
renamed = true;
|
||||||
|
}
|
||||||
entries.emplace_back(std::move(name), std::move(data));
|
entries.emplace_back(std::move(name), std::move(data));
|
||||||
}
|
}
|
||||||
close_zip_reader(&zip);
|
close_zip_reader(&zip);
|
||||||
}
|
}
|
||||||
|
// Without this the scenario would degrade silently into re-testing the new name if the
|
||||||
|
// exporter's constant ever moved again: every load below would still pass.
|
||||||
|
REQUIRE(renamed);
|
||||||
|
|
||||||
Zipper out(path);
|
Zipper out(path);
|
||||||
for (const auto& e : entries)
|
for (const auto& e : entries)
|
||||||
out.add_entry(e.first, e.second.data(), e.second.size());
|
out.add_entry(e.first, e.second.data(), e.second.size());
|
||||||
out.finalize();
|
out.finalize();
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rewrites the archive at `path` with the recipe entry back under the name it had before the
|
|
||||||
// rename, which is what every project saved by an earlier build looks like on disk. Generated
|
|
||||||
// rather than checked in because a whole project archive is not frozen evidence the way a bare
|
|
||||||
// recipe blob is -- it has to be whatever today's exporter writes, with only the name aged.
|
|
||||||
static void rename_cad_recipe_entry_to_legacy(const std::string& path)
|
|
||||||
{
|
|
||||||
const bool renamed = rewrite_3mf_entries(path, [](std::string& name, std::string&) {
|
|
||||||
if (!boost::algorithm::iequals(name, CAD_RECIPE_ENTRY))
|
|
||||||
return false;
|
|
||||||
name = LEGACY_CAD_RECIPE_ENTRY;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
// Without this the scenario would degrade silently into re-testing the new name if the
|
|
||||||
// exporter's constant ever moved again: every load below would still pass.
|
|
||||||
REQUIRE(renamed);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replaces the first occurrence of `from` in any entry whose name ends with `suffix`.
|
|
||||||
static bool replace_in_3mf_entry(const std::string& path, const std::string& suffix, const std::string& from, const std::string& to)
|
|
||||||
{
|
|
||||||
bool replaced = false;
|
|
||||||
rewrite_3mf_entries(path, [&](std::string& name, std::string& data) {
|
|
||||||
if (replaced || !boost::algorithm::ends_with(name, suffix))
|
|
||||||
return false;
|
|
||||||
if (const size_t pos = data.find(from); pos != std::string::npos) {
|
|
||||||
data.replace(pos, from.size(), to);
|
|
||||||
replaced = true;
|
|
||||||
}
|
|
||||||
return replaced;
|
|
||||||
});
|
|
||||||
return replaced;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stores a one-plate project holding a cube whose first two facets are painted Extruder2 and
|
|
||||||
// Extruder3, which the exporter writes as paint_color="8" and paint_color="0C".
|
|
||||||
static void store_painted_cube(const std::string& path)
|
|
||||||
{
|
|
||||||
Model model;
|
|
||||||
ModelObject* object = model.add_object();
|
|
||||||
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
|
|
||||||
object->add_instance();
|
|
||||||
{
|
|
||||||
TriangleSelector selector(volume->mesh());
|
|
||||||
selector.set_facet(0, EnforcerBlockerType::Extruder2);
|
|
||||||
selector.set_facet(1, EnforcerBlockerType::Extruder3);
|
|
||||||
REQUIRE(volume->mmu_segmentation_facets.set(selector));
|
|
||||||
}
|
|
||||||
ScopedTemporaryDir backup_dir("orca_paint_src");
|
|
||||||
model.set_backup_path(backup_dir.string());
|
|
||||||
|
|
||||||
DynamicPrintConfig cfg;
|
|
||||||
PlateData plate;
|
|
||||||
plate.plate_index = 0;
|
|
||||||
StoreParams sp;
|
|
||||||
sp.path = path.c_str();
|
|
||||||
sp.model = &model;
|
|
||||||
sp.config = &cfg;
|
|
||||||
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
|
|
||||||
sp.plate_data_list.push_back(&plate);
|
|
||||||
REQUIRE(store_bbs_3mf(sp));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loads `path` through the BBS importer into `model`, releasing the plates it returns. The
|
|
||||||
// importer stages metadata through `backup_dir`, which has to outlive the model.
|
|
||||||
static bool load_project(const std::string& path, Model& model, const ScopedTemporaryDir& backup_dir)
|
|
||||||
{
|
|
||||||
model.set_backup_path(backup_dir.string());
|
|
||||||
DynamicPrintConfig config;
|
|
||||||
ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable };
|
|
||||||
PlateDataPtrs plates;
|
|
||||||
std::vector<Preset*> project_presets;
|
|
||||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
|
||||||
Semver file_version;
|
|
||||||
const bool loaded = load_bbs_3mf(path.c_str(), &config, &ctxt, &model, &plates, &project_presets, &is_bbl_3mf,
|
|
||||||
&is_orca_3mf, &file_version, nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig);
|
|
||||||
release_PlateData_list(plates);
|
|
||||||
return loaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("A project with a plate id below 1 fails to load", "[3mf][Regression]")
|
|
||||||
{
|
|
||||||
const int plate_id = GENERATE(0, -1);
|
|
||||||
INFO("plater_id " << plate_id);
|
|
||||||
|
|
||||||
ScopedTemporaryFile temp(".3mf");
|
|
||||||
store_painted_cube(temp.string());
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir backup_dir("orca_plate_dst");
|
|
||||||
Model model;
|
|
||||||
REQUIRE(load_project(temp.string(), model, backup_dir));
|
|
||||||
}
|
|
||||||
|
|
||||||
REQUIRE(replace_in_3mf_entry(temp.string(), "model_settings.config", "key=\"plater_id\" value=\"1\"",
|
|
||||||
"key=\"plater_id\" value=\"" + std::to_string(plate_id) + "\""));
|
|
||||||
ScopedTemporaryDir backup_dir("orca_plate_dst");
|
|
||||||
Model model;
|
|
||||||
bool loaded = true;
|
|
||||||
REQUIRE_NOTHROW(loaded = load_project(temp.string(), model, backup_dir));
|
|
||||||
REQUIRE_FALSE(loaded);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("A project with malformed paint data loads without the damaged facet", "[3mf][Regression]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryFile temp(".3mf");
|
|
||||||
store_painted_cube(temp.string());
|
|
||||||
// Split codes with no children behind them: the stream runs out mid-tree.
|
|
||||||
REQUIRE(replace_in_3mf_entry(temp.string(), ".model", "paint_color=\"8\"", "paint_color=\"FFFFFFFFFFFFFFFF3\""));
|
|
||||||
|
|
||||||
ScopedTemporaryDir backup_dir("orca_paint_dst");
|
|
||||||
Model model;
|
|
||||||
REQUIRE(load_project(temp.string(), model, backup_dir));
|
|
||||||
REQUIRE(model.objects.size() == 1);
|
|
||||||
const ModelVolume& volume = *model.objects.front()->volumes.front();
|
|
||||||
const auto& data = volume.mmu_segmentation_facets.get_data();
|
|
||||||
REQUIRE_FALSE(data.used_states[size_t(EnforcerBlockerType::Extruder2)]);
|
|
||||||
REQUIRE(data.used_states[size_t(EnforcerBlockerType::Extruder3)]);
|
|
||||||
|
|
||||||
TriangleSelector selector(volume.mesh());
|
|
||||||
REQUIRE_NOTHROW(selector.deserialize(data));
|
|
||||||
REQUIRE(selector.num_facets(EnforcerBlockerType::Extruder2) == 0);
|
|
||||||
REQUIRE(selector.num_facets(EnforcerBlockerType::Extruder3) == 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The recipe lives only in the BBS-native backend, because that is the only one that runs:
|
// The recipe lives only in the BBS-native backend, because that is the only one that runs:
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include "libslic3r/Format/DRC.hpp"
|
||||||
|
#include "libslic3r/TriangleMesh.hpp"
|
||||||
|
|
||||||
|
#include <boost/nowide/fstream.hpp>
|
||||||
|
|
||||||
|
#include <draco/compression/encode.h>
|
||||||
|
#include <draco/mesh/mesh.h>
|
||||||
|
|
||||||
|
#include "test_utils.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Encodes one triangle whose vertices carry a single attribute of the given type.
|
||||||
|
// The decoder accepts both a mesh without POSITION and a face index past the point count.
|
||||||
|
void write_drc_triangle(const std::string &path, draco::GeometryAttribute::Type attribute_type, uint32_t second_point)
|
||||||
|
{
|
||||||
|
draco::Mesh mesh;
|
||||||
|
mesh.set_num_points(3);
|
||||||
|
draco::GeometryAttribute attribute;
|
||||||
|
attribute.Init(attribute_type, nullptr, 3, draco::DT_FLOAT32, false, sizeof(float) * 3, 0);
|
||||||
|
const int attribute_id = mesh.AddAttribute(attribute, true, 3);
|
||||||
|
const float points[3][3] = {{0, 0, 0}, {1, 0, 0}, {0, 1, 0}};
|
||||||
|
for (int i = 0; i < 3; ++i)
|
||||||
|
mesh.attribute(attribute_id)->SetAttributeValue(draco::AttributeValueIndex(i), points[i]);
|
||||||
|
draco::Mesh::Face face;
|
||||||
|
face[0] = draco::PointIndex(0);
|
||||||
|
face[1] = draco::PointIndex(second_point);
|
||||||
|
face[2] = draco::PointIndex(2);
|
||||||
|
mesh.AddFace(face);
|
||||||
|
|
||||||
|
draco::Encoder encoder;
|
||||||
|
encoder.SetEncodingMethod(draco::MESH_SEQUENTIAL_ENCODING);
|
||||||
|
draco::EncoderBuffer buffer;
|
||||||
|
REQUIRE(encoder.EncodeMeshToBuffer(mesh, &buffer).ok());
|
||||||
|
boost::nowide::ofstream out(path, std::ios::binary);
|
||||||
|
out.write(buffer.data(), static_cast<std::streamsize>(buffer.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("A Draco triangle with positions loads", "[DRC]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile drc(".drc");
|
||||||
|
write_drc_triangle(drc.string(), draco::GeometryAttribute::POSITION, 1);
|
||||||
|
|
||||||
|
TriangleMesh mesh;
|
||||||
|
REQUIRE(load_drc(drc.string().c_str(), &mesh));
|
||||||
|
CHECK(mesh.facets_count() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A Draco mesh without a POSITION attribute fails to load", "[DRC][Regression]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile drc(".drc");
|
||||||
|
write_drc_triangle(drc.string(), draco::GeometryAttribute::GENERIC, 1);
|
||||||
|
|
||||||
|
TriangleMesh mesh;
|
||||||
|
CHECK_FALSE(load_drc(drc.string().c_str(), &mesh));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A Draco face referencing a missing point fails to load", "[DRC][Regression]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile drc(".drc");
|
||||||
|
write_drc_triangle(drc.string(), draco::GeometryAttribute::POSITION, 200);
|
||||||
|
|
||||||
|
TriangleMesh mesh;
|
||||||
|
CHECK_FALSE(load_drc(drc.string().c_str(), &mesh));
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include "libslic3r/TriangleMesh.hpp"
|
||||||
|
#include "libslic3r/Format/OBJ.hpp"
|
||||||
|
|
||||||
|
#include <boost/nowide/fstream.hpp>
|
||||||
|
|
||||||
|
#include "test_utils.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
using Catch::Matchers::WithinAbs;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct LoadedObj
|
||||||
|
{
|
||||||
|
bool ok{false};
|
||||||
|
TriangleMesh mesh;
|
||||||
|
ObjInfo info;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A tetrahedron with a material and two texture coordinates, (0.25, 0.5) and (0.75, 1).
|
||||||
|
// Only the first face and the vt lines are varied; the other three faces reference vt 1.
|
||||||
|
LoadedObj load_textured_tetrahedron(const std::string &first_face, const std::string &vts = "vt 0.25 0.5\nvt 0.75 1\n")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile obj(".obj");
|
||||||
|
ScopedTemporaryFile mtl(".mtl");
|
||||||
|
{
|
||||||
|
boost::nowide::ofstream out(mtl.string());
|
||||||
|
out << "newmtl a\nKd 1 0 0\n";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
boost::nowide::ofstream out(obj.string());
|
||||||
|
out << "mtllib " << mtl.path().filename().string() << "\n"
|
||||||
|
<< "v 0 0 0\nv 10 0 0\nv 0 10 0\nv 0 0 10\n"
|
||||||
|
<< vts
|
||||||
|
<< "usemtl a\n"
|
||||||
|
<< first_face << "\n"
|
||||||
|
<< "f 1/1 2/1 4/1\nf 1/1 4/1 3/1\nf 2/1 3/1 4/1\n";
|
||||||
|
}
|
||||||
|
LoadedObj loaded;
|
||||||
|
std::string message;
|
||||||
|
loaded.ok = load_obj(obj.string().c_str(), &loaded.mesh, loaded.info, message);
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("An out-of-range texture index falls back to a zero UV and keeps the geometry", "[OBJ][Regression]")
|
||||||
|
{
|
||||||
|
const LoadedObj loaded = load_textured_tetrahedron("f 1/1000000000 3/1 2/1");
|
||||||
|
|
||||||
|
REQUIRE(loaded.ok);
|
||||||
|
CHECK(loaded.mesh.facets_count() == 4);
|
||||||
|
REQUIRE(loaded.info.uvs.size() == 4);
|
||||||
|
const std::array<Vec2f, 3> &uv = loaded.info.uvs.front();
|
||||||
|
CHECK_THAT(uv[0].x(), WithinAbs(0., 1e-6));
|
||||||
|
CHECK_THAT(uv[0].y(), WithinAbs(0., 1e-6));
|
||||||
|
CHECK_THAT(uv[1].x(), WithinAbs(0.25, 1e-6));
|
||||||
|
CHECK_THAT(uv[1].y(), WithinAbs(0.5, 1e-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A face without texture indices loads among faces that have them", "[OBJ][Regression]")
|
||||||
|
{
|
||||||
|
const LoadedObj loaded = load_textured_tetrahedron("f 1 3 2");
|
||||||
|
|
||||||
|
REQUIRE(loaded.ok);
|
||||||
|
CHECK(loaded.mesh.facets_count() == 4);
|
||||||
|
// One UV entry per face, so later faces keep their own coordinates.
|
||||||
|
REQUIRE(loaded.info.uvs.size() == 4);
|
||||||
|
for (const Vec2f &uv : loaded.info.uvs.front()) {
|
||||||
|
CHECK_THAT(uv.x(), WithinAbs(0., 1e-6));
|
||||||
|
CHECK_THAT(uv.y(), WithinAbs(0., 1e-6));
|
||||||
|
}
|
||||||
|
CHECK_THAT(loaded.info.uvs[1][0].x(), WithinAbs(0.25, 1e-6));
|
||||||
|
CHECK_THAT(loaded.info.uvs[1][0].y(), WithinAbs(0.5, 1e-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A negative texture index counts back from the last texture coordinate", "[OBJ]")
|
||||||
|
{
|
||||||
|
// -1 is the most recent vt (0.75, 1), -2 the one before it (0.25, 0.5).
|
||||||
|
const LoadedObj loaded = load_textured_tetrahedron("f 1/-2 3/-1 2/-1");
|
||||||
|
|
||||||
|
REQUIRE(loaded.ok);
|
||||||
|
CHECK(loaded.mesh.facets_count() == 4);
|
||||||
|
REQUIRE(loaded.info.uvs.size() == 4);
|
||||||
|
const std::array<Vec2f, 3> &uv = loaded.info.uvs.front();
|
||||||
|
CHECK_THAT(uv[0].x(), WithinAbs(0.25, 1e-6));
|
||||||
|
CHECK_THAT(uv[0].y(), WithinAbs(0.5, 1e-6));
|
||||||
|
CHECK_THAT(uv[1].x(), WithinAbs(0.75, 1e-6));
|
||||||
|
CHECK_THAT(uv[1].y(), WithinAbs(1., 1e-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Texture coordinates with a w component are kept", "[OBJ][Regression]")
|
||||||
|
{
|
||||||
|
const LoadedObj loaded = load_textured_tetrahedron("f 1/1 3/2 2/2", "vt 0.25 0.5 0\nvt 0.75 1 0\n");
|
||||||
|
|
||||||
|
REQUIRE(loaded.ok);
|
||||||
|
CHECK(loaded.mesh.facets_count() == 4);
|
||||||
|
REQUIRE(loaded.info.uvs.size() == 4);
|
||||||
|
const std::array<Vec2f, 3> &uv = loaded.info.uvs.front();
|
||||||
|
CHECK_THAT(uv[0].x(), WithinAbs(0.25, 1e-6));
|
||||||
|
CHECK_THAT(uv[0].y(), WithinAbs(0.5, 1e-6));
|
||||||
|
CHECK_THAT(uv[1].x(), WithinAbs(0.75, 1e-6));
|
||||||
|
CHECK_THAT(uv[1].y(), WithinAbs(1., 1e-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A texture coordinate with w does not shift the indices of the ones after it", "[OBJ][Regression]")
|
||||||
|
{
|
||||||
|
// The w on the first vt used to drop that line, so vt 2 resolved to the third coordinate.
|
||||||
|
const LoadedObj loaded = load_textured_tetrahedron("f 1/2 3/3 2/-1", "vt 0.1 0.2 0\nvt 0.25 0.5\nvt 0.75 1\n");
|
||||||
|
|
||||||
|
REQUIRE(loaded.ok);
|
||||||
|
REQUIRE(loaded.info.uvs.size() == 4);
|
||||||
|
const std::array<Vec2f, 3> &uv = loaded.info.uvs.front();
|
||||||
|
CHECK_THAT(uv[0].x(), WithinAbs(0.25, 1e-6));
|
||||||
|
CHECK_THAT(uv[0].y(), WithinAbs(0.5, 1e-6));
|
||||||
|
CHECK_THAT(uv[1].x(), WithinAbs(0.75, 1e-6));
|
||||||
|
CHECK_THAT(uv[1].y(), WithinAbs(1., 1e-6));
|
||||||
|
CHECK_THAT(uv[2].x(), WithinAbs(0.75, 1e-6));
|
||||||
|
CHECK_THAT(uv[2].y(), WithinAbs(1., 1e-6));
|
||||||
|
}
|
||||||
@@ -3,8 +3,6 @@
|
|||||||
#include "libslic3r/TriangleSelector.hpp"
|
#include "libslic3r/TriangleSelector.hpp"
|
||||||
#include "libslic3r/TriangleMesh.hpp"
|
#include "libslic3r/TriangleMesh.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
using namespace Slic3r;
|
using namespace Slic3r;
|
||||||
|
|
||||||
// A sphere gives well over ExtruderMax original facets, so every extruder state can be assigned
|
// A sphere gives well over ExtruderMax original facets, so every extruder state can be assigned
|
||||||
@@ -125,77 +123,3 @@ TEST_CASE("Extruder states match the CONST_FILAMENTS hex encoding", "[TriangleSe
|
|||||||
INFO("Hex " << c.hex << " -> extruder " << c.state);
|
INFO("Hex " << c.hex << " -> extruder " << c.state);
|
||||||
REQUIRE(TriangleSelector::has_facets(data, EnforcerBlockerType(c.state)));
|
REQUIRE(TriangleSelector::has_facets(data, EnforcerBlockerType(c.state)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pack 4-bit codes into a bitstream, least significant bit first, in the order the decoder reads them.
|
|
||||||
static std::vector<bool> pack_nibbles(const std::vector<int> &nibbles)
|
|
||||||
{
|
|
||||||
std::vector<bool> bitstream;
|
|
||||||
for (const int nibble : nibbles)
|
|
||||||
for (int bit = 0; bit < 4; ++bit)
|
|
||||||
bitstream.push_back((nibble >> bit) & 1);
|
|
||||||
return bitstream;
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("A valid paint stream with nested splits round-trips bit for bit", "[TriangleSelector]")
|
|
||||||
{
|
|
||||||
const TriangleMesh mesh = test_mesh();
|
|
||||||
|
|
||||||
TriangleSelector::TriangleSplittingData data;
|
|
||||||
data.triangles_to_split.emplace_back(0, 0);
|
|
||||||
// A three-side split whose children, in stream order, are: a one-side split (side 2) into two
|
|
||||||
// leaves, a two-side split (side 1) into leaves of states 20, 0 and 8, then two plain leaves.
|
|
||||||
const std::vector<int> triangle_0 = {0b0011,
|
|
||||||
0b1001, 0b1000, 0b0100,
|
|
||||||
0b0110, 0b1100, 0b1111, 20 - 18, 0b0000, 0b1100, 8 - 3,
|
|
||||||
0b1000,
|
|
||||||
0b0100};
|
|
||||||
data.bitstream = pack_nibbles(triangle_0);
|
|
||||||
data.triangles_to_split.emplace_back(5, int(data.bitstream.size()));
|
|
||||||
const std::vector<bool> triangle_5 = pack_nibbles({0b1100, 3 - 3});
|
|
||||||
data.bitstream.insert(data.bitstream.end(), triangle_5.begin(), triangle_5.end());
|
|
||||||
data.reset_used_states();
|
|
||||||
REQUIRE(data.update_used_states(0));
|
|
||||||
|
|
||||||
TriangleSelector restored(mesh);
|
|
||||||
restored.deserialize(data);
|
|
||||||
|
|
||||||
REQUIRE(restored.num_facets(EnforcerBlockerType::Extruder20) == 1);
|
|
||||||
REQUIRE(restored.num_facets(EnforcerBlockerType::Extruder3) == 1);
|
|
||||||
REQUIRE(restored.serialize() == data);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("A truncated or malformed paint stream drops only the damaged triangle", "[TriangleSelector][Regression]")
|
|
||||||
{
|
|
||||||
struct Case { const char *name; std::vector<int> nibbles; };
|
|
||||||
const auto c = GENERATE(values<Case>({
|
|
||||||
{"three-side split missing two children", {0b0011, 0b1000, 0b1000}},
|
|
||||||
{"leaf missing its state nibble", {0b1100}},
|
|
||||||
{"leaf missing its second state nibble", {0b1100, 0b1111}},
|
|
||||||
{"splits nested past the end", {0b0011, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF,
|
|
||||||
0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF}},
|
|
||||||
{"one-side split of the nonexistent side 3", {0b1101, 0b1000, 0b1000}},
|
|
||||||
}));
|
|
||||||
INFO(c.name);
|
|
||||||
|
|
||||||
const TriangleMesh mesh = test_mesh();
|
|
||||||
TriangleSelector intact(mesh);
|
|
||||||
intact.set_facet(0, EnforcerBlockerType::Extruder2);
|
|
||||||
|
|
||||||
// Triangle 0 stays intact, triangle 1 carries the damaged stream.
|
|
||||||
TriangleSelector::TriangleSplittingData data = intact.serialize();
|
|
||||||
data.triangles_to_split.emplace_back(1, int(data.bitstream.size()));
|
|
||||||
const std::vector<bool> damaged = pack_nibbles(c.nibbles);
|
|
||||||
data.bitstream.insert(data.bitstream.end(), damaged.begin(), damaged.end());
|
|
||||||
|
|
||||||
TriangleSelector restored(mesh);
|
|
||||||
REQUIRE_NOTHROW(restored.deserialize(data));
|
|
||||||
// Triangle 1 unwinds completely, so the selector holds exactly the intact paint.
|
|
||||||
REQUIRE(restored.serialize() == intact.serialize());
|
|
||||||
|
|
||||||
REQUIRE_NOTHROW(TriangleSelector::has_facets(data, EnforcerBlockerType::Extruder3));
|
|
||||||
|
|
||||||
TriangleSelector::TriangleSplittingData recomputed = data;
|
|
||||||
recomputed.reset_used_states();
|
|
||||||
REQUIRE_FALSE(recomputed.update_used_states(0));
|
|
||||||
REQUIRE(std::none_of(recomputed.used_states.begin(), recomputed.used_states.end(), [](bool used) { return used; }));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_plugin_install.cpp
|
test_plugin_install.cpp
|
||||||
test_plugin_lifecycle.cpp
|
test_plugin_lifecycle.cpp
|
||||||
test_plugin_printer_agent.cpp
|
test_plugin_printer_agent.cpp
|
||||||
|
test_printhost.cpp
|
||||||
test_slicing_pipeline_bindings.cpp
|
test_slicing_pipeline_bindings.cpp
|
||||||
test_slicing_pipeline_config.cpp
|
test_slicing_pipeline_config.cpp
|
||||||
test_plugin_sort.cpp
|
test_plugin_sort.cpp
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include "slic3r/Utils/PrintHost.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class TestPrintHost : public PrintHost
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using PrintHost::format_error;
|
||||||
|
|
||||||
|
const char* get_name() const override { return "Test"; }
|
||||||
|
bool test(wxString&) const override { return true; }
|
||||||
|
wxString get_test_ok_msg() const override { return {}; }
|
||||||
|
wxString get_test_failed_msg(wxString&) const override { return {}; }
|
||||||
|
bool upload(PrintHostUpload, ProgressFn, ErrorFn, InfoFn) const override { return true; }
|
||||||
|
bool has_auto_discovery() const override { return false; }
|
||||||
|
bool can_test() const override { return false; }
|
||||||
|
PrintHostPostUploadActions get_post_upload_actions() const override { return {}; }
|
||||||
|
std::string get_host() const override { return {}; }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string format_error(const std::string& body, const std::string& error, unsigned status)
|
||||||
|
{
|
||||||
|
return TestPrintHost().format_error(body, error, status).ToStdString();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string envelope(int code, const std::string& message, const std::string& traceback)
|
||||||
|
{
|
||||||
|
return nlohmann::json{{"error", {{"code", code}, {"message", message}, {"traceback", traceback}}}}.dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string moonraker_error(int code, const std::string& message, const std::string& detail = {})
|
||||||
|
{
|
||||||
|
std::string line = "tornado.web.HTTPError: HTTP " + std::to_string(code) + ": " + message;
|
||||||
|
if (!detail.empty())
|
||||||
|
line += " (" + detail + ")";
|
||||||
|
return envelope(code, message, "Traceback (most recent call last):\n ...\n" + line + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A real Moonraker body for uploading a file that is being printed.
|
||||||
|
constexpr const char* k_busy_file_403 =
|
||||||
|
R"JSON({"error": {"code": 403, "message": "Forbidden", "traceback": "Traceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/file_manager/file_manager.py\", line 1017, in _finish_gcode_upload\n can_start = self._handle_operation_check(check_path)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nmoonraker.utils.exceptions.ServerError: File currently in use\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/application.py\", line 1069, in post\n raise tornado.web.HTTPError(\ntornado.web.HTTPError: HTTP 403: Forbidden (File is loaded, upload not permitted)\n"}})JSON";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("A Klipper upload error shows its reason instead of a Python traceback", "[PrintHost][Regression]")
|
||||||
|
{
|
||||||
|
const std::string msg = format_error(k_busy_file_403, "", 403);
|
||||||
|
INFO("actual: " << msg);
|
||||||
|
|
||||||
|
CHECK(msg == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
|
||||||
|
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("Traceback"));
|
||||||
|
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("file_manager.py"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("The specific cause is recovered from a file endpoint's traceback", "[PrintHost]")
|
||||||
|
{
|
||||||
|
SECTION("a plain detail")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(403, "Forbidden", "File is loaded, upload not permitted");
|
||||||
|
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail whose own parentheses nest (a filename)")
|
||||||
|
{
|
||||||
|
const std::string detail = "Directory does not exist (/home/pi/gcodes/plate (1).gcode)";
|
||||||
|
const std::string body = moonraker_error(400, "Bad Request", detail);
|
||||||
|
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail that contains the reason phrase")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(403, "Forbidden", "Forbidden zone: access denied");
|
||||||
|
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (Forbidden zone: access denied)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail that spans lines")
|
||||||
|
{
|
||||||
|
const std::string detail = "Move out of range\nX=250.000 Y=10.000";
|
||||||
|
const std::string body = moonraker_error(400, "Bad Request", detail);
|
||||||
|
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail that only repeats the reason phrase is dropped")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(401, "Unauthorized", "Unauthorized");
|
||||||
|
CHECK(format_error(body, "", 401) == "HTTP 401: Unauthorized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("An unhandled exception shows its type and message", "[PrintHost]")
|
||||||
|
{
|
||||||
|
const std::string frame = "Traceback (most recent call last):\n"
|
||||||
|
" File \"/home/pi/moonraker/moonraker/components/file_manager/file_manager.py\", line 1, in write\n"
|
||||||
|
" self._write(data)\n";
|
||||||
|
|
||||||
|
SECTION("a one-line message")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", frame + "OSError: [Errno 28] No space left on device\n");
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 28] No space left on device)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a message that spans lines")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", frame + "ServerError: Klippy request failed\n see klippy.log\n");
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (ServerError: Klippy request failed\n see klippy.log)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("raised while handling an HTTPError with the same code")
|
||||||
|
{
|
||||||
|
const std::string traceback = frame + "tornado.web.HTTPError: HTTP 500: Internal Server Error (Database locked)\n\n"
|
||||||
|
"During handling of the above exception, another exception occurred:\n\n" +
|
||||||
|
frame + "OSError: [Errno 5] Input/output error\n";
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", traceback);
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a traceback with no header")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", "OSError: [Errno 5] Input/output error");
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A reason already complete in message is shown unchanged", "[PrintHost]")
|
||||||
|
{
|
||||||
|
SECTION("message is the whole reason, no trailing detail")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(503, "Klippy is not ready");
|
||||||
|
CHECK(format_error(body, "", 503) == "HTTP 503: Klippy is not ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a message that itself contains parentheses is not duplicated")
|
||||||
|
{
|
||||||
|
const std::string reason = "Requested blocks (0-5) are unavailable";
|
||||||
|
const std::string body = moonraker_error(400, reason);
|
||||||
|
CHECK(format_error(body, "", 400) == "HTTP 400: " + reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A Moonraker error with no usable detail shows just the reason phrase", "[PrintHost]")
|
||||||
|
{
|
||||||
|
struct Case
|
||||||
|
{
|
||||||
|
const char* name;
|
||||||
|
const char* body;
|
||||||
|
unsigned status;
|
||||||
|
const char* expected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto c = GENERATE(
|
||||||
|
Case{"an empty traceback", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": ""}})JSON", 500,
|
||||||
|
"HTTP 500: Internal Server Error"},
|
||||||
|
Case{"a traceback of only whitespace", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": "\n \n"}})JSON",
|
||||||
|
500, "HTTP 500: Internal Server Error"});
|
||||||
|
|
||||||
|
DYNAMIC_SECTION(c.name) { CHECK(format_error(c.body, "", c.status) == c.expected); }
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A percent sign in the reason is not a format specifier", "[PrintHost]")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(507, "Insufficient Storage", "disk 100% full");
|
||||||
|
CHECK(format_error(body, "", 507) == "HTTP 507: Insufficient Storage (disk 100% full)");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Error bodies that are not a Moonraker envelope are left unchanged", "[PrintHost]")
|
||||||
|
{
|
||||||
|
SECTION("OctoPrint's string-valued error member")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"error": "File not found"})JSON";
|
||||||
|
CHECK(format_error(body, "", 404) == "HTTP 404: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("PrusaLink's top-level message, not under error")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"title": "Conflict", "message": "Printer is printing"})JSON";
|
||||||
|
CHECK(format_error(body, "", 409) == "HTTP 409: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a body that is not JSON")
|
||||||
|
{
|
||||||
|
const std::string html = "<html><head><title>502 Bad Gateway</title></head></html>";
|
||||||
|
CHECK(format_error(html, "", 502) == "HTTP 502: " + html);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("an error object with no traceback")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error"}})JSON";
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("an error object whose traceback is null")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": null}})JSON";
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("an envelope whose reason phrase is empty")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(403, "", "Traceback (most recent call last):\nOSError: denied\n");
|
||||||
|
CHECK(format_error(body, "", 403) == "HTTP 403: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a transport error with no HTTP status")
|
||||||
|
{
|
||||||
|
CHECK(format_error("", "curl:Could not connect", 0) == "curl:Could not connect");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user