improved visibility calculation - it now considers normals and

accordingly counts only hits which have similar normal
This commit is contained in:
PavelMikus 2022-02-25 16:40:28 +01:00
parent 38a9d870c0
commit ffc7452d9e
2 changed files with 123 additions and 97 deletions

View file

@ -148,7 +148,7 @@ std::vector<HitInfo> raycast_visibility(const AABBTreeIndirect::Tree<3, float> &
auto edge2 = triangles.vertices[face[2]] - triangles.vertices[face[0]]; auto edge2 = triangles.vertices[face[2]] - triangles.vertices[face[0]];
Vec3f hit_pos = (triangles.vertices[face[0]] + edge1 * hitpoint.u + edge2 * hitpoint.v); Vec3f hit_pos = (triangles.vertices[face[0]] + edge1 * hitpoint.u + edge2 * hitpoint.v);
Vec3f surface_normal = edge1.cross(edge2).normalized(); Vec3f surface_normal = its_face_normal(triangles, hitpoint.id);
init.push_back(HitInfo { hit_pos, surface_normal }); init.push_back(HitInfo { hit_pos, surface_normal });
} }
@ -169,12 +169,12 @@ std::vector<HitInfo> raycast_visibility(const AABBTreeIndirect::Tree<3, float> &
std::vector<float> calculate_polygon_angles_at_vertices(const Polygon &polygon, const std::vector<float> &lengths, std::vector<float> calculate_polygon_angles_at_vertices(const Polygon &polygon, const std::vector<float> &lengths,
float min_arm_length) { float min_arm_length) {
if (polygon.size() == 1) {
return {0.0f};
}
std::vector<float> result(polygon.size()); std::vector<float> result(polygon.size());
if (polygon.size() == 1) {
result[0] = 0.0f;
}
auto make_idx_circular = [&](int index) { auto make_idx_circular = [&](int index) {
while (index < 0) { while (index < 0) {
index += polygon.size(); index += polygon.size();
@ -254,13 +254,24 @@ struct GlobalModelInfo {
} }
float calculate_point_visibility(const Vec3f &position) const { float calculate_point_visibility(const Vec3f &position) const {
size_t closest_point_index = find_closest_point(raycast_hits_tree, position);
if (closest_point_index == raycast_hits_tree.npos
||
(position - geometry_raycast_hits[closest_point_index].position).norm()
> SeamPlacer::seam_align_tolerable_dist) {
return 0;
}
auto nearby_points = find_nearby_points(raycast_hits_tree, position, SeamPlacer::considered_area_radius); auto nearby_points = find_nearby_points(raycast_hits_tree, position, SeamPlacer::considered_area_radius);
Vec3f local_normal = geometry_raycast_hits[closest_point_index].surface_normal;
float visibility = 0; float visibility = 0;
for (const auto &hit_point_index : nearby_points) { for (const auto &hit_point_index : nearby_points) {
// The further away from the perimeter point,
// the less representative ray hit is
float distance = float distance =
(position - geometry_raycast_hits[hit_point_index].position).norm(); (position - geometry_raycast_hits[hit_point_index].position).norm();
visibility += SeamPlacer::considered_area_radius - distance; // The further away from the perimeter point, visibility += (SeamPlacer::considered_area_radius - distance) *
// the less representative ray hit is std::max(0.0f, local_normal.dot(geometry_raycast_hits[hit_point_index].surface_normal));
} }
return visibility; return visibility;
@ -288,7 +299,8 @@ struct GlobalModelInfo {
for (size_t i = 0; i < divided_mesh.vertices.size(); ++i) { for (size_t i = 0; i < divided_mesh.vertices.size(); ++i) {
float visibility = calculate_point_visibility(divided_mesh.vertices[i]); float visibility = calculate_point_visibility(divided_mesh.vertices[i]);
float normalized = visibility / SeamPlacer::expected_hits_per_area / 2.0; float normalized = visibility
/ (SeamPlacer::expected_hits_per_area * SeamPlacer::considered_area_radius);
Vec3f color = vis_to_rgb(normalized); Vec3f color = vis_to_rgb(normalized);
fprintf(fp, "v %f %f %f %f %f %f\n", fprintf(fp, "v %f %f %f %f %f %f\n",
divided_mesh.vertices[i](0), divided_mesh.vertices[i](1), divided_mesh.vertices[i](2), divided_mesh.vertices[i](0), divided_mesh.vertices[i](1), divided_mesh.vertices[i](2),
@ -308,9 +320,13 @@ struct GlobalModelInfo {
<< "Couldn't open " << fname << " for writing"; << "Couldn't open " << fname << " for writing";
} }
for (size_t i = 0; i < geometry_raycast_hits.size(); ++i) for (size_t i = 0; i < geometry_raycast_hits.size(); ++i) {
fprintf(fp, "v %f %f %f \n", geometry_raycast_hits[i].position[0], geometry_raycast_hits[i].position[1], Vec3f surface_normal = (geometry_raycast_hits[i].surface_normal + Vec3f(1.0, 1.0, 1.0)) / 2.0;
geometry_raycast_hits[i].position[2]); fprintf(fp, "v %f %f %f %f %f %f \n", geometry_raycast_hits[i].position[0],
geometry_raycast_hits[i].position[1],
geometry_raycast_hits[i].position[2], surface_normal[0], surface_normal[1],
surface_normal[2]);
}
fclose(fp); fclose(fp);
} }
} }
@ -496,11 +512,11 @@ void gather_global_model_info(GlobalModelInfo &result, const PrintObject *po) {
} }
struct DefaultSeamComparator { struct DefaultSeamComparator {
static constexpr float angle_clusters[] { -1.0, 0.4 * PI, 0.6 static constexpr float angle_clusters[] { -1.0, 0.4 * PI, 0.5 * PI, 0.6
* PI, 0.7 * PI, 0.8 * PI, 0.9 * PI }; * PI, 0.7 * PI, 0.8 * PI, 0.9 * PI };
const float get_angle_category(float ccw_angle) const { const float get_angle_category(float ccw_angle) const {
float concave_bonus = ccw_angle < 0 ? 0.1 : 0; float concave_bonus = ccw_angle < 0 ? 0.1 * PI : 0;
float abs_angle = abs(ccw_angle) + concave_bonus; float abs_angle = abs(ccw_angle) + concave_bonus;
auto category = std::find_if_not(std::begin(angle_clusters), std::end(angle_clusters), auto category = std::find_if_not(std::begin(angle_clusters), std::end(angle_clusters),
[&](float category_limit) { [&](float category_limit) {
@ -554,7 +570,7 @@ struct DefaultSeamComparator {
} }
{ //local angles { //local angles
float a_local_category = get_angle_category(a.local_ccw_angle) + 0.1 * PI; //give a slight bonus float a_local_category = get_angle_category(a.local_ccw_angle) + 0.2 * PI; //give a slight bonus
float b_local_category = get_angle_category(b.local_ccw_angle); float b_local_category = get_angle_category(b.local_ccw_angle);
if (a_local_category > b_local_category) { if (a_local_category > b_local_category) {
@ -565,7 +581,7 @@ struct DefaultSeamComparator {
} }
} }
return a.visibility < b.visibility * 1.3; return a.visibility < b.visibility * 1.5;
} }
} }
; ;
@ -573,86 +589,86 @@ struct DefaultSeamComparator {
} // namespace SeamPlacerImpl } // namespace SeamPlacerImpl
void SeamPlacer::gather_seam_candidates(const PrintObject *po, void SeamPlacer::gather_seam_candidates(const PrintObject *po,
const SeamPlacerImpl::GlobalModelInfo &global_model_info) { const SeamPlacerImpl::GlobalModelInfo &global_model_info) {
using namespace SeamPlacerImpl; using namespace SeamPlacerImpl;
m_perimeter_points_per_object.emplace(po, po->layer_count()); m_perimeter_points_per_object.emplace(po, po->layer_count());
m_perimeter_points_trees_per_object.emplace(po, po->layer_count()); m_perimeter_points_trees_per_object.emplace(po, po->layer_count());
tbb::parallel_for(tbb::blocked_range<size_t>(0, po->layers().size()), tbb::parallel_for(tbb::blocked_range<size_t>(0, po->layers().size()),
[&](tbb::blocked_range<size_t> r) { [&](tbb::blocked_range<size_t> r) {
for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) {
std::vector<SeamCandidate> &layer_candidates = m_perimeter_points_per_object[po][layer_idx]; std::vector<SeamCandidate> &layer_candidates = m_perimeter_points_per_object[po][layer_idx];
const Layer *layer = po->get_layer(layer_idx); const Layer *layer = po->get_layer(layer_idx);
auto unscaled_z = layer->slice_z; auto unscaled_z = layer->slice_z;
Polygons polygons = extract_perimeter_polygons(layer); Polygons polygons = extract_perimeter_polygons(layer);
for (const auto &poly : polygons) { for (const auto &poly : polygons) {
process_perimeter_polygon(poly, unscaled_z, layer_candidates, process_perimeter_polygon(poly, unscaled_z, layer_candidates,
global_model_info); global_model_info);
}
auto functor = SeamCandidateCoordinateFunctor { &layer_candidates };
m_perimeter_points_trees_per_object[po][layer_idx] = std::make_unique<SeamCandidatesTree>(
functor, layer_candidates.size());
} }
auto functor = SeamCandidateCoordinateFunctor { &layer_candidates };
m_perimeter_points_trees_per_object[po][layer_idx] = std::make_unique<SeamCandidatesTree>(
functor, layer_candidates.size());
} }
} );
);
} }
void SeamPlacer::calculate_candidates_visibility(const PrintObject *po, void SeamPlacer::calculate_candidates_visibility(const PrintObject *po,
const SeamPlacerImpl::GlobalModelInfo &global_model_info) { const SeamPlacerImpl::GlobalModelInfo &global_model_info) {
using namespace SeamPlacerImpl; using namespace SeamPlacerImpl;
tbb::parallel_for(tbb::blocked_range<size_t>(0, m_perimeter_points_per_object[po].size()), tbb::parallel_for(tbb::blocked_range<size_t>(0, m_perimeter_points_per_object[po].size()),
[&](tbb::blocked_range<size_t> r) { [&](tbb::blocked_range<size_t> r) {
for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) {
for (auto &perimeter_point : m_perimeter_points_per_object[po][layer_idx]) { for (auto &perimeter_point : m_perimeter_points_per_object[po][layer_idx]) {
perimeter_point.visibility = global_model_info.calculate_point_visibility( perimeter_point.visibility = global_model_info.calculate_point_visibility(
perimeter_point.position); perimeter_point.position);
}
} }
} });
});
} }
void SeamPlacer::calculate_overhangs(const PrintObject *po) { void SeamPlacer::calculate_overhangs(const PrintObject *po) {
using namespace SeamPlacerImpl; using namespace SeamPlacerImpl;
tbb::parallel_for(tbb::blocked_range<size_t>(0, m_perimeter_points_per_object[po].size()), tbb::parallel_for(tbb::blocked_range<size_t>(0, m_perimeter_points_per_object[po].size()),
[&](tbb::blocked_range<size_t> r) { [&](tbb::blocked_range<size_t> r) {
for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) {
for (SeamCandidate &perimeter_point : m_perimeter_points_per_object[po][layer_idx]) { for (SeamCandidate &perimeter_point : m_perimeter_points_per_object[po][layer_idx]) {
const auto calculate_layer_overhang = [&](size_t other_layer_idx) { const auto calculate_layer_overhang = [&](size_t other_layer_idx) {
size_t closest_supporter = find_closest_point( size_t closest_supporter = find_closest_point(
*m_perimeter_points_trees_per_object[po][other_layer_idx], *m_perimeter_points_trees_per_object[po][other_layer_idx],
perimeter_point.position); perimeter_point.position);
const SeamCandidate &supporter_point = const SeamCandidate &supporter_point =
m_perimeter_points_per_object[po][other_layer_idx][closest_supporter]; m_perimeter_points_per_object[po][other_layer_idx][closest_supporter];
auto prev_next = find_previous_and_next_perimeter_point(m_perimeter_points_per_object[po][other_layer_idx], closest_supporter); auto prev_next = find_previous_and_next_perimeter_point(m_perimeter_points_per_object[po][other_layer_idx], closest_supporter);
const SeamCandidate &prev_point = const SeamCandidate &prev_point =
m_perimeter_points_per_object[po][other_layer_idx][prev_next.first]; m_perimeter_points_per_object[po][other_layer_idx][prev_next.first];
const SeamCandidate &next_point = const SeamCandidate &next_point =
m_perimeter_points_per_object[po][other_layer_idx][prev_next.second]; m_perimeter_points_per_object[po][other_layer_idx][prev_next.second];
return calculate_overhang(perimeter_point, prev_point, return calculate_overhang(perimeter_point, prev_point,
supporter_point, next_point); supporter_point, next_point);
}; };
if (layer_idx > 0) { //calculate overhang if (layer_idx > 0) { //calculate overhang
perimeter_point.overhang = calculate_layer_overhang(layer_idx-1); perimeter_point.overhang = calculate_layer_overhang(layer_idx-1);
} }
if (layer_idx < m_perimeter_points_per_object[po].size() - 1) { //calculate higher_layer_overhang if (layer_idx < m_perimeter_points_per_object[po].size() - 1) { //calculate higher_layer_overhang
perimeter_point.higher_layer_overhang = calculate_layer_overhang(layer_idx+1); perimeter_point.higher_layer_overhang = calculate_layer_overhang(layer_idx+1);
}
} }
} }
} });
}); }
}
// sadly cannot be const because map access operator[] is not const, since it can create new object // sadly cannot be const because map access operator[] is not const, since it can create new object
template<typename Comparator> template<typename Comparator>
bool SeamPlacer::find_next_seam_in_string(const PrintObject *po, const Vec3f &last_point_pos, bool SeamPlacer::find_next_seam_in_string(const PrintObject *po, Vec3f &last_point_pos,
size_t layer_idx, const Comparator &comparator, size_t layer_idx, const Comparator &comparator,
std::vector<std::pair<size_t, size_t>> &seam_strings, std::vector<std::pair<size_t, size_t>> &seam_string,
std::vector<std::pair<size_t, size_t>> &potential_string_seams) { std::vector<std::pair<size_t, size_t>> &potential_string_seams) {
using namespace SeamPlacerImpl; using namespace SeamPlacerImpl;
@ -674,13 +690,15 @@ bool SeamPlacer::find_next_seam_in_string(const PrintObject *po, const Vec3f &la
if ((next_layer_seam.position - projected_position).norm() if ((next_layer_seam.position - projected_position).norm()
< SeamPlacer::seam_align_tolerable_dist) { //seam point is within limits, put in the close_by_points list < SeamPlacer::seam_align_tolerable_dist) { //seam point is within limits, put in the close_by_points list
seam_strings.emplace_back(layer_idx, closest_point.perimeter->seam_index); seam_string.emplace_back(layer_idx, closest_point.perimeter->seam_index);
last_point_pos = next_layer_seam.position;
return true; return true;
} else if ((closest_point.position - projected_position).norm() } else if ((closest_point.position - projected_position).norm()
< SeamPlacer::seam_align_tolerable_dist < SeamPlacer::seam_align_tolerable_dist
&& comparator.is_first_not_much_worse(closest_point, next_layer_seam)) { && comparator.is_first_not_much_worse(closest_point, next_layer_seam)) {
//seam point is far, but if the close point is not much worse, do not count it as a skip and add it to potential_string_seams //seam point is far, but if the close point is not much worse, do not count it as a skip and add it to potential_string_seams
potential_string_seams.emplace_back(layer_idx, closest_point_index); potential_string_seams.emplace_back(layer_idx, closest_point_index);
last_point_pos = closest_point.position;
return true; return true;
} else { } else {
return false; return false;
@ -702,39 +720,38 @@ void SeamPlacer::align_seam_points(const PrintObject *po, const Comparator &comp
//skip //skip
} else { } else {
int skips = SeamPlacer::seam_align_tolerable_skips; int skips = SeamPlacer::seam_align_tolerable_skips;
int next_layer = layer_idx - 1; int next_layer = layer_idx + 1;
Vec3f last_point_pos = layer_perimeter_points[current_point_index].position; Vec3f last_point_pos = layer_perimeter_points[current_point_index].position;
std::vector<std::pair<size_t, size_t>> seam_string; std::vector<std::pair<size_t, size_t>> seam_string;
std::vector<std::pair<size_t, size_t>> potential_string_seams; std::vector<std::pair<size_t, size_t>> potential_string_seams;
//find close by points and outliers; there is a budget of skips allowed //find close by points and outliers; there is a budget of skips allowed
// search from bottom up in z dir. Heuristics which avoids smooth top surfaces (like head) where the seam is not well defined while (skips >= 0 && next_layer < int(m_perimeter_points_per_object[po].size())) {
while (skips >= 0 && next_layer >= 0) {
if (find_next_seam_in_string(po, last_point_pos, next_layer, comparator, seam_string, if (find_next_seam_in_string(po, last_point_pos, next_layer, comparator, seam_string,
potential_string_seams)) { potential_string_seams)) {
last_point_pos = //String added, last_point_pos updated, nothing to be done
m_perimeter_points_per_object[po][seam_string.back().first][seam_string.back().second].position;
} else { } else {
// Layer skipped, reduce number of available skips
skips--; skips--;
} }
next_layer--; next_layer++;
} }
if (seam_string.size() > 4) { //string long enough to be worth aligning if (seam_string.size() >= seam_align_minimum_string_seams) { //string long enough to be worth aligning
//do additional check in back direction //do additional check in back direction
next_layer = layer_idx; next_layer = layer_idx - 1;
skips = SeamPlacer::seam_align_tolerable_skips; skips = SeamPlacer::seam_align_tolerable_skips;
while (skips >= 0 && next_layer < int(m_perimeter_points_per_object[po].size())) { while (skips >= 0 && next_layer >= 0) {
if (find_next_seam_in_string(po, last_point_pos, next_layer, DefaultSeamComparator { }, if (find_next_seam_in_string(po, last_point_pos, next_layer, comparator,
seam_string, seam_string,
potential_string_seams)) { potential_string_seams)) {
last_point_pos = //String added, last_point_pos updated, nothing to be done
m_perimeter_points_per_object[po][seam_string.back().first][seam_string.back().second].position;
} else { } else {
// Layer skipped, reduce number of available skips
skips--; skips--;
} }
next_layer++; next_layer--;
} }
// all string seams and potential string seams gathered, now do the alignment // all string seams and potential string seams gathered, now do the alignment
@ -746,7 +763,7 @@ void SeamPlacer::align_seam_points(const PrintObject *po, const Comparator &comp
//https://en.wikipedia.org/wiki/Exponential_smoothing //https://en.wikipedia.org/wiki/Exponential_smoothing
//inititalization //inititalization
float smoothing_factor = 0.5; float smoothing_factor = SeamPlacer::seam_align_strength;
std::pair<size_t, size_t> init = seam_string[0]; std::pair<size_t, size_t> init = seam_string[0];
Vec2f prev_pos_xy = m_perimeter_points_per_object[po][init.first][init.second].position.head<2>(); Vec2f prev_pos_xy = m_perimeter_points_per_object[po][init.first][init.second].position.head<2>();
for (const auto &pair : seam_string) { for (const auto &pair : seam_string) {
@ -827,6 +844,7 @@ void SeamPlacer::init(const Print &print) {
} }
void SeamPlacer::place_seam(const Layer *layer, ExtrusionLoop &loop, bool external_first) { void SeamPlacer::place_seam(const Layer *layer, ExtrusionLoop &loop, bool external_first) {
using namespace SeamPlacerImpl;
const PrintObject *po = layer->object(); const PrintObject *po = layer->object();
//NOTE this is necessary, since layer->id() is quite unreliable //NOTE this is necessary, since layer->id() is quite unreliable
size_t layer_index = std::max(0, int(layer->id()) - int(po->slicing_parameters().raft_layers())); size_t layer_index = std::max(0, int(layer->id()) - int(po->slicing_parameters().raft_layers()));
@ -840,8 +858,11 @@ void SeamPlacer::place_seam(const Layer *layer, ExtrusionLoop &loop, bool extern
Vec2f unscaled_p = unscale(fp).cast<float>(); Vec2f unscaled_p = unscale(fp).cast<float>();
size_t closest_perimeter_point_index = find_closest_point(perimeter_points_tree, size_t closest_perimeter_point_index = find_closest_point(perimeter_points_tree,
Vec3f { unscaled_p.x(), unscaled_p.y(), float(unscaled_z) }); Vec3f { unscaled_p.x(), unscaled_p.y(), float(unscaled_z) });
size_t perimeter_seam_index = perimeter_points[closest_perimeter_point_index].perimeter->seam_index; const Perimeter *perimeter = perimeter_points[closest_perimeter_point_index].perimeter.get();
Vec3f seam_position = perimeter_points[perimeter_seam_index].position; Vec3f seam_position = perimeter_points[perimeter->seam_index].position;
if (perimeter->aligned) {
seam_position = perimeter->final_seam_position;
}
Point seam_point = scaled(Vec2d { seam_position.x(), seam_position.y() }); Point seam_point = scaled(Vec2d { seam_position.x(), seam_position.y() });

View file

@ -44,9 +44,11 @@ struct Perimeter {
}; };
struct SeamCandidate { struct SeamCandidate {
SeamCandidate(const Vec3f &pos, std::shared_ptr<Perimeter> perimeter, float local_ccw_angle, SeamCandidate(const Vec3f &pos, std::shared_ptr<Perimeter> perimeter,
float local_ccw_angle,
EnforcedBlockedSeamPoint type) : EnforcedBlockedSeamPoint type) :
position(pos), perimeter(perimeter), visibility(0.0f), overhang(0.0f), higher_layer_overhang(0.0f), local_ccw_angle( position(pos), perimeter(perimeter), visibility(0.0f), overhang(0.0f), higher_layer_overhang(
0.0f), local_ccw_angle(
local_ccw_angle), type(type) { local_ccw_angle), type(type) {
} }
const Vec3f position; const Vec3f position;
@ -88,17 +90,20 @@ class SeamPlacer {
public: public:
using SeamCandidatesTree = using SeamCandidatesTree =
KDTreeIndirect<3, float, SeamPlacerImpl::SeamCandidateCoordinateFunctor>; KDTreeIndirect<3, float, SeamPlacerImpl::SeamCandidateCoordinateFunctor>;
static constexpr float expected_hits_per_area = 200.0f; static constexpr float expected_hits_per_area = 800.0f;
static constexpr float considered_area_radius = 2.0f; static constexpr float considered_area_radius = 5.0f;
static constexpr float cosine_hemisphere_sampling_power = 6.0f; static constexpr float cosine_hemisphere_sampling_power = 4.0f;
static constexpr float polygon_local_angles_arm_distance = 0.6f; static constexpr float polygon_local_angles_arm_distance = 0.6f;
static constexpr float enforcer_blocker_sqr_distance_tolerance = 0.2f; static constexpr float enforcer_blocker_sqr_distance_tolerance = 0.2f;
static constexpr float seam_align_tolerable_dist = 1.5f; static constexpr float seam_align_strength = 1.0f;
static constexpr float seam_align_tolerable_dist = 1.0f;
static constexpr size_t seam_align_tolerable_skips = 4; static constexpr size_t seam_align_tolerable_skips = 4;
static constexpr size_t seam_align_minimum_string_seams = 2;
//perimeter points per object per layer idx, and their corresponding KD trees //perimeter points per object per layer idx, and their corresponding KD trees
std::unordered_map<const PrintObject*, std::vector<std::vector<SeamPlacerImpl::SeamCandidate>>> m_perimeter_points_per_object; std::unordered_map<const PrintObject*, std::vector<std::vector<SeamPlacerImpl::SeamCandidate>>> m_perimeter_points_per_object;
std::unordered_map<const PrintObject*, std::vector<std::unique_ptr<SeamCandidatesTree>>> m_perimeter_points_trees_per_object; std::unordered_map<const PrintObject*, std::vector<std::unique_ptr<SeamCandidatesTree>>> m_perimeter_points_trees_per_object;
@ -115,7 +120,7 @@ private:
template<typename Comparator> template<typename Comparator>
void align_seam_points(const PrintObject *po, const Comparator &comparator); void align_seam_points(const PrintObject *po, const Comparator &comparator);
template<typename Comparator> template<typename Comparator>
bool find_next_seam_in_string(const PrintObject *po, const Vec3f &last_point_pos, bool find_next_seam_in_string(const PrintObject *po, Vec3f &last_point_pos,
size_t layer_idx, const Comparator &comparator, size_t layer_idx, const Comparator &comparator,
std::vector<std::pair<size_t, size_t>> &seam_strings, std::vector<std::pair<size_t, size_t>> &seam_strings,
std::vector<std::pair<size_t, size_t>> &outliers); std::vector<std::pair<size_t, size_t>> &outliers);