#include "SeamPlacerNG.hpp" #include "tbb/parallel_for.h" #include "tbb/blocked_range.h" #include "tbb/parallel_reduce.h" #include #include #include #include //For polynomial fitting #include #include #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/EdgeGrid.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/SVG.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/QuadricEdgeCollapse.hpp" #define DEBUG_FILES #ifdef DEBUG_FILES #include "Subdivide.hpp" #include #endif namespace Slic3r { namespace SeamPlacerImpl { //https://towardsdatascience.com/least-square-polynomial-fitting-using-c-eigen-package-c0673728bd01 // interpolates points in z (treats z coordinates as time) and returns coefficients for axis x and y std::vector polyfit(const std::vector &points, size_t order) { Eigen::VectorXf V0(points.size()); Eigen::VectorXf V1(points.size()); Eigen::VectorXf V2(points.size()); for (size_t index = 0; index < points.size(); index++) { V0(index) = points[index].x(); V1(index) = points[index].y(); V2(index) = points[index].z(); } // Create Matrix Placeholder of size n x k, n= number of datapoints, k = order of polynomial, for exame k = 3 for cubic polynomial Eigen::MatrixXf T(points.size(), order + 1); // check to make sure inputs are correct assert(points.size() >= order + 1); // Populate the matrix for (size_t i = 0; i < points.size(); ++i) { for (size_t j = 0; j < order + 1; ++j) { T(i, j) = pow(V2(i), j); } } // Solve for linear least square fit const auto QR = T.householderQr(); Eigen::VectorXf result0 = QR.solve(V0); Eigen::VectorXf result1 = QR.solve(V1); std::vector coeff { order + 1 }; for (size_t k = 0; k < order + 1; k++) { coeff[k] = Vec2f { result0[k], result1[k] }; } return coeff; } Vec3f get_fitted_point(const std::vector &coefficients, float z) { size_t order = coefficients.size() - 1; float fitted_x = 0; float fitted_y = 0; for (size_t index = 0; index < order + 1; ++index) { float z_pow = pow(z, index); fitted_x += coefficients[index].x() * z_pow; fitted_y += coefficients[index].y() * z_pow; } return Vec3f { fitted_x, fitted_y, z }; } /// Coordinate frame class Frame { public: Frame() { mX = Vec3f(1, 0, 0); mY = Vec3f(0, 1, 0); mZ = Vec3f(0, 0, 1); } Frame(const Vec3f &x, const Vec3f &y, const Vec3f &z) : mX(x), mY(y), mZ(z) { } void set_from_z(const Vec3f &z) { mZ = z.normalized(); Vec3f tmpZ = mZ; Vec3f tmpX = (std::abs(tmpZ.x()) > 0.99f) ? Vec3f(0, 1, 0) : Vec3f(1, 0, 0); mY = (tmpZ.cross(tmpX)).normalized(); mX = mY.cross(tmpZ); } Vec3f to_world(const Vec3f &a) const { return a.x() * mX + a.y() * mY + a.z() * mZ; } Vec3f to_local(const Vec3f &a) const { return Vec3f(mX.dot(a), mY.dot(a), mZ.dot(a)); } const Vec3f& binormal() const { return mX; } const Vec3f& tangent() const { return mY; } const Vec3f& normal() const { return mZ; } private: Vec3f mX, mY, mZ; }; Vec3f sample_sphere_uniform(const Vec2f &samples) { float term1 = 2.0f * float(PI) * samples.x(); float term2 = 2.0f * sqrt(samples.y() - samples.y() * samples.y()); return {cos(term1) * term2, sin(term1) * term2, 1.0f - 2.0f * samples.y()}; } Vec3f sample_power_cosine_hemisphere(const Vec2f &samples, float power) { float term1 = 2.f * float(PI) * samples.x(); float term2 = pow(samples.y(), 1.f / (power + 1.f)); float term3 = sqrt(1.f - term2 * term2); return Vec3f(cos(term1) * term3, sin(term1) * term3, term2); } std::vector raycast_visibility(const AABBTreeIndirect::Tree<3, float> &raycasting_tree, const indexed_triangle_set &triangles) { auto bbox = raycasting_tree.node(0).bbox; Vec3f vision_sphere_center = bbox.center().cast(); Vec3f side_sizes = bbox.sizes().cast(); float vision_sphere_raidus = (side_sizes.norm() * 0.55); // 0.5 (half) covers whole object, // 0.05 added to avoid corner cases // very rough approximation of object surface area from its bounding sphere float approx_area = 4 * PI * vision_sphere_raidus * vision_sphere_raidus; float local_considered_area = PI * SeamPlacer::considered_area_radius * SeamPlacer::considered_area_radius; size_t ray_count = SeamPlacer::expected_hits_per_area * approx_area / local_considered_area; // Prepare random samples per ray // std::random_device rnd_device; // use fixed seed, we can backtrack potential issues easier std::mt19937 mersenne_engine { 131 }; std::uniform_real_distribution dist { 0, 1 }; auto gen = [&dist, &mersenne_engine]() { return Vec2f(dist(mersenne_engine), dist(mersenne_engine)); }; BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: generate random samples: start"; std::vector global_dir_random_samples(ray_count); generate(begin(global_dir_random_samples), end(global_dir_random_samples), gen); std::vector local_dir_random_samples(ray_count); generate(begin(local_dir_random_samples), end(local_dir_random_samples), gen); BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: generate random samples: end"; BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: raycast visibility for " << ray_count << " rays: start"; std::vector hit_points = tbb::parallel_reduce(tbb::blocked_range(0, ray_count), std::vector { }, [&](tbb::blocked_range r, std::vector init) { for (size_t index = r.begin(); index < r.end(); ++index) { //generate global ray direction Vec3f global_ray_dir = sample_sphere_uniform(global_dir_random_samples[index]); //place the ray origin on the bounding sphere Vec3f ray_origin = (vision_sphere_center - global_ray_dir * vision_sphere_raidus); // compute local ray direction as cosine hemisphere sample - the rays dont aim directly to the middle Vec3f local_dir = sample_power_cosine_hemisphere(local_dir_random_samples[index], SeamPlacer::cosine_hemisphere_sampling_power); // apply the local direction via Frame struct - the local_dir is with respect to +Z being forward Frame f; f.set_from_z(global_ray_dir); Vec3f final_ray_dir = (f.to_world(local_dir)); igl::Hit hitpoint; // FIXME: This AABBTTreeIndirect query will not compile for float ray origin and // direction. Vec3d ray_origin_d = ray_origin.cast(); Vec3d final_ray_dir_d = final_ray_dir.cast(); bool hit = AABBTreeIndirect::intersect_ray_first_hit(triangles.vertices, triangles.indices, raycasting_tree, ray_origin_d, final_ray_dir_d, hitpoint); if (hit) { auto face = triangles.indices[hitpoint.id]; auto edge1 = triangles.vertices[face[1]] - 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 surface_normal = its_face_normal(triangles, hitpoint.id); init.push_back(HitInfo { hit_pos, surface_normal }); } } return init; }, [](std::vector left, const std::vector& right) { left.insert(left.end(), right.begin(), right.end()); return left; } ); BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: raycast visibility for " << ray_count << " rays: end"; return hit_points; } std::vector calculate_polygon_angles_at_vertices(const Polygon &polygon, const std::vector &lengths, float min_arm_length) { std::vector result(polygon.size()); if (polygon.size() == 1) { result[0] = 0.0f; } auto make_idx_circular = [&](int index) { while (index < 0) { index += polygon.size(); } return index % polygon.size(); }; int idx_prev = 0; int idx_curr = 0; int idx_next = 0; float distance_to_prev = 0; float distance_to_next = 0; //push idx_prev far enough back as initialization while (distance_to_prev < min_arm_length) { idx_prev = make_idx_circular(idx_prev - 1); distance_to_prev += lengths[idx_prev]; } for (size_t _i = 0; _i < polygon.size(); ++_i) { // pull idx_prev to current as much as possible, while respecting the min_arm_length while (distance_to_prev - lengths[idx_prev] > min_arm_length) { distance_to_prev -= lengths[idx_prev]; idx_prev = make_idx_circular(idx_prev + 1); } //push idx_next forward as far as needed while (distance_to_next < min_arm_length) { distance_to_next += lengths[idx_next]; idx_next = make_idx_circular(idx_next + 1); } // Calculate angle between idx_prev, idx_curr, idx_next. const Point &p0 = polygon.points[idx_prev]; const Point &p1 = polygon.points[idx_curr]; const Point &p2 = polygon.points[idx_next]; const Point v1 = p1 - p0; const Point v2 = p2 - p1; int64_t dot = int64_t(v1(0)) * int64_t(v2(0)) + int64_t(v1(1)) * int64_t(v2(1)); int64_t cross = int64_t(v1(0)) * int64_t(v2(1)) - int64_t(v1(1)) * int64_t(v2(0)); float angle = float(atan2(float(cross), float(dot))); result[idx_curr] = angle; // increase idx_curr by one float curr_distance = lengths[idx_curr]; idx_curr++; distance_to_prev += curr_distance; distance_to_next -= curr_distance; } return result; } // structure to store global information about the model - occlusion hits, enforcers, blockers struct GlobalModelInfo { std::vector geometry_raycast_hits; KDTreeIndirect<3, float, HitInfoCoordinateFunctor> raycast_hits_tree; indexed_triangle_set enforcers; indexed_triangle_set blockers; AABBTreeIndirect::Tree<3, float> enforcers_tree; AABBTreeIndirect::Tree<3, float> blockers_tree; GlobalModelInfo() : raycast_hits_tree(HitInfoCoordinateFunctor { &geometry_raycast_hits }) { } bool is_enforced(const Vec3f &position, float radius) const { if (enforcers.empty()) { return false; } return AABBTreeIndirect::is_any_triangle_in_radius(enforcers.vertices, enforcers.indices, enforcers_tree, position, radius); } bool is_blocked(const Vec3f &position, float radius) const { if (blockers.empty()) { return false; } return AABBTreeIndirect::is_any_triangle_in_radius(blockers.vertices, blockers.indices, blockers_tree, position, radius); } 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::considered_area_radius) { return 0; } 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; for (const auto &hit_point_index : nearby_points) { if (local_normal.dot(geometry_raycast_hits[hit_point_index].surface_normal) > 0) { // The further away from the perimeter point, // the less representative ray hit is float distance = (position - geometry_raycast_hits[hit_point_index].position).norm(); visibility += (SeamPlacer::considered_area_radius - distance); } } return visibility; } #ifdef DEBUG_FILES void debug_export(const indexed_triangle_set &obj_mesh, const char *file_name) const { indexed_triangle_set divided_mesh = subdivide(obj_mesh, SeamPlacer::considered_area_radius); Slic3r::CNumericLocalesSetter locales_setter; { FILE *fp = boost::nowide::fopen(file_name, "w"); if (fp == nullptr) { BOOST_LOG_TRIVIAL(error) << "stl_write_obj: Couldn't open " << file_name << " for writing"; return; } const auto vis_to_rgb = [](float normalized_visibility) { float ratio = 2 * normalized_visibility; float blue = std::max(0.0f, 1.0f - ratio); float red = std::max(0.0f, ratio - 1.0f); float green = std::max(0.0f, 1.0f - blue - red); return Vec3f { red, blue, green }; }; for (size_t i = 0; i < divided_mesh.vertices.size(); ++i) { float visibility = calculate_point_visibility(divided_mesh.vertices[i]); float normalized = visibility / (SeamPlacer::expected_hits_per_area * SeamPlacer::considered_area_radius); Vec3f color = vis_to_rgb(normalized); 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), color(0), color(1), color(2) ); } for (size_t i = 0; i < divided_mesh.indices.size(); ++i) fprintf(fp, "f %d %d %d\n", divided_mesh.indices[i][0] + 1, divided_mesh.indices[i][1] + 1, divided_mesh.indices[i][2] + 1); fclose(fp); } { auto fname = std::string("hits_").append(file_name); FILE *fp = boost::nowide::fopen(fname.c_str(), "w"); if (fp == nullptr) { BOOST_LOG_TRIVIAL(error) << "Couldn't open " << fname << " for writing"; } for (size_t i = 0; i < geometry_raycast_hits.size(); ++i) { Vec3f surface_normal = (geometry_raycast_hits[i].surface_normal + Vec3f(1.0, 1.0, 1.0)) / 2.0; 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); } } #endif } ; //Extract perimeter polygons of the given layer Polygons extract_perimeter_polygons(const Layer *layer) { Polygons polygons; for (const LayerRegion *layer_region : layer->regions()) { for (const ExtrusionEntity *ex_entity : layer_region->perimeters.entities) { if (ex_entity->is_collection()) { //collection of inner, outer, and overhang perimeters for (const ExtrusionEntity *perimeter : static_cast(ex_entity)->entities) { if (perimeter->role() == ExtrusionRole::erExternalPerimeter) { Points p; perimeter->collect_points(p); polygons.emplace_back(p); } } if (polygons.empty()) { Points p; ex_entity->collect_points(p); polygons.emplace_back(p); } } else { Points p; ex_entity->collect_points(p); polygons.emplace_back(p); } } } if (polygons.empty()) { // If there are no perimeter polygons for whatever reason (disabled perimeters .. ) insert dummy point // it is easier than checking everywhere if the layer is not emtpy, no seam will be placed to this layer anyway polygons.emplace_back(std::vector { Point { 0, 0 } }); } return polygons; } // Insert SeamCandidates created from perimeter polygons in to the result vector. // Compute its type (Enfrocer,Blocker), angle, and position //each SeamCandidate also contains pointer to shared Perimeter structure representing the polygon // if Custom Seam modifiers are present, oversamples the polygon if necessary to better fit user intentions void process_perimeter_polygon(const Polygon &orig_polygon, float z_coord, std::vector &result_vec, const GlobalModelInfo &global_model_info) { if (orig_polygon.size() == 0) { return; } Polygon polygon = orig_polygon; bool was_clockwise = polygon.make_counter_clockwise(); std::vector lengths { }; for (size_t point_idx = 0; point_idx < polygon.size() - 1; ++point_idx) { lengths.push_back(std::max((unscale(polygon[point_idx]) - unscale(polygon[point_idx + 1])).norm(), 0.01)); } lengths.push_back(std::max((unscale(polygon[0]) - unscale(polygon[polygon.size() - 1])).norm(), 0.01)); std::vector local_angles = calculate_polygon_angles_at_vertices(polygon, lengths, SeamPlacer::polygon_local_angles_arm_distance); std::shared_ptr perimeter = std::make_shared(); std::queue orig_polygon_points { }; for (size_t index = 0; index < polygon.size(); ++index) { Vec2f unscaled_p = unscale(polygon[index]).cast(); orig_polygon_points.emplace(unscaled_p.x(), unscaled_p.y(), z_coord); } Vec3f first = orig_polygon_points.front(); std::queue oversampled_points { }; size_t orig_angle_index = 0; perimeter->start_index = result_vec.size(); while (!orig_polygon_points.empty() || !oversampled_points.empty()) { EnforcedBlockedSeamPoint type = EnforcedBlockedSeamPoint::Neutral; Vec3f position; float local_ccw_angle = 0; bool orig_point = false; if (!oversampled_points.empty()) { position = oversampled_points.front(); oversampled_points.pop(); } else { position = orig_polygon_points.front(); orig_polygon_points.pop(); local_ccw_angle = was_clockwise ? -local_angles[orig_angle_index] : local_angles[orig_angle_index]; orig_angle_index++; orig_point = true; } if (global_model_info.is_enforced(position, SeamPlacer::enforcer_blocker_distance_tolerance)) { type = EnforcedBlockedSeamPoint::Enforced; } if (global_model_info.is_blocked(position, SeamPlacer::enforcer_blocker_distance_tolerance)) { type = EnforcedBlockedSeamPoint::Blocked; } if (orig_point) { Vec3f pos_of_next = orig_polygon_points.empty() ? first : orig_polygon_points.front(); float distance_to_next = (position - pos_of_next).norm(); if (global_model_info.is_enforced(position, distance_to_next) || global_model_info.is_blocked(position, distance_to_next)) { Vec3f vec_to_next = (pos_of_next - position).normalized(); float step_size = SeamPlacer::enforcer_blocker_distance_tolerance / 2.0f; float step = step_size; while (step < distance_to_next) { oversampled_points.push(position + vec_to_next * step); step += step_size; } } } result_vec.emplace_back(position, perimeter, local_ccw_angle, type); } perimeter->end_index = result_vec.size() - 1; } // Get index of previous and next perimeter point of the layer. Because SeamCandidates of all polygons of the given layer // are sequentially stored in the vector, each perimeter contains info about start and end index. These vales are used to // deduce index of previous and next neigbour in the corresponding perimeter. std::pair find_previous_and_next_perimeter_point(const std::vector &perimeter_points, size_t point_index) { const SeamCandidate ¤t = perimeter_points[point_index]; int prev = point_index - 1; //for majority of points, it is true that neighbours lie behind and in front of them in the vector int next = point_index + 1; if (point_index == current.perimeter->start_index) { // if point_index is equal to start, it means that the previous neighbour is at the end prev = current.perimeter->end_index; } if (point_index == current.perimeter->end_index) { // if point_index is equal to end, than next neighbour is at the start next = current.perimeter->start_index; } assert(prev >= 0); assert(next >= 0); return {size_t(prev),size_t(next)}; } //NOTE: only rough esitmation of overhang distance // value represents distance from edge, positive is overhang, negative is inside shape float calculate_overhang(const SeamCandidate &point, const SeamCandidate &under_a, const SeamCandidate &under_b, const SeamCandidate &under_c) { auto p = Vec2d { point.position.x(), point.position.y() }; auto a = Vec2d { under_a.position.x(), under_a.position.y() }; auto b = Vec2d { under_b.position.x(), under_b.position.y() }; auto c = Vec2d { under_c.position.x(), under_c.position.y() }; auto oriented_line_dist = [](const Vec2d a, const Vec2d b, const Vec2d p) { //signed distance from line return -((b.x() - a.x()) * (a.y() - p.y()) - (a.x() - p.x()) * (b.y() - a.y())) / (a - b).norm(); }; auto dist_ab = oriented_line_dist(a, b, p); auto dist_bc = oriented_line_dist(b, c, p); // from angle and signed distances from the arms of the points on the previous layer, we // can deduce if it is overhang and give estimation of the size. // However, the size of the overhang is rough estimation, the sign is more reliable if (under_b.local_ccw_angle > 0 && dist_ab > 0 && dist_bc > 0) { //convex shape, p is inside return -((p - b).norm() + dist_ab + dist_bc) / 3.0; } if (under_b.local_ccw_angle < 0 && (dist_ab < 0 || dist_bc < 0)) { //concave shape, p is inside return -((p - b).norm() + dist_ab + dist_bc) / 3.0; } return ((p - b).norm() + dist_ab + dist_bc) / 3.0; } // Pick best seam point based on the given comparator template void pick_seam_point(std::vector &perimeter_points, size_t start_index, const Comparator &comparator) { size_t end_index = perimeter_points[start_index].perimeter->end_index; std::vector indices(end_index + 1 - start_index); for (size_t index = start_index; index <= end_index; ++index) { indices[index - start_index] = index; } size_t seam_index = indices[0]; for (size_t index : indices) { if (comparator.is_first_better(perimeter_points[index], perimeter_points[seam_index])) { seam_index = index; } } perimeter_points[start_index].perimeter->seam_index = seam_index; } // Computes all global model info - transforms object, performs raycasting, // stores enforces and blockers void gather_global_model_info(GlobalModelInfo &result, const PrintObject *po) { BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: build AABB tree for raycasting and gather occlusion info: start"; // Build AABB tree for raycasting auto obj_transform = po->trafo(); auto triangle_set = po->model_object()->raw_indexed_triangle_set(); //add model parts for (const ModelVolume* model_volume : po->model_object()->volumes){ if (model_volume->type() == ModelVolumeType::MODEL_PART) { auto model_transformation = model_volume->get_matrix(); indexed_triangle_set model_its = model_volume->mesh().its; its_transform(model_its, model_transformation); its_merge(triangle_set , model_its); } } its_transform(triangle_set, obj_transform); float target_error = SeamPlacer::raycasting_decimation_target_error; its_quadric_edge_collapse(triangle_set, 0, &target_error, nullptr, nullptr); auto raycasting_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(triangle_set.vertices, triangle_set.indices); result.geometry_raycast_hits = raycast_visibility(raycasting_tree, triangle_set); result.raycast_hits_tree.build(result.geometry_raycast_hits.size()); BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: build AABB tree for raycasting and gather occlusion info: end"; BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: build AABB trees for raycasting enforcers/blockers: start"; for (const ModelVolume *mv : po->model_object()->volumes) { if (mv->is_seam_painted()) { auto model_transformation = mv->get_matrix(); indexed_triangle_set enforcers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::ENFORCER); its_transform(enforcers, model_transformation); its_merge(result.enforcers, enforcers); indexed_triangle_set blockers = mv->seam_facets.get_facets(*mv, EnforcerBlockerType::BLOCKER); its_transform(blockers, model_transformation); its_merge(result.blockers, blockers); } } its_transform(result.enforcers, obj_transform); its_transform(result.blockers, obj_transform); result.enforcers_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(result.enforcers.vertices, result.enforcers.indices); result.blockers_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(result.blockers.vertices, result.blockers.indices); BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: build AABB trees for raycasting enforcers/blockers: end"; #ifdef DEBUG_FILES auto filename = "visiblity_of_" + std::to_string(po->id().id) + ".obj"; result.debug_export(triangle_set, filename.c_str()); #endif } //Comparator of seam points. It has two necessary methods: is_first_better and is_first_not_much_worse struct DefaultSeamComparator { float compute_angle_penalty(float ccw_angle) const { if (ccw_angle >= 0) { return PI * PI - ccw_angle * ccw_angle; } else { return (PI * PI - ccw_angle * ccw_angle) * 0.8f; } } // Standard comparator, must respect the requirements of comparators (e.g. give same result on same inputs) for sorting usage // should return if a is better seamCandidate than b bool is_first_better(const SeamCandidate &a, const SeamCandidate &b) const { // Blockers/Enforcers discrimination, top priority if (a.type > b.type) { return true; } if (b.type > a.type) { return false; } //avoid overhangs if (a.overhang > 0.3f && b.overhang < a.overhang) { return false; } return (a.visibility + SeamPlacer::expected_hits_per_area) * compute_angle_penalty(a.local_ccw_angle) < (b.visibility + SeamPlacer::expected_hits_per_area) * compute_angle_penalty(b.local_ccw_angle); } // Comparator used during alignment. If there is close potential aligned point, it is comapred to the current // sema point of the perimeter, to find out if the aligned point is not much worse than the current seam bool is_first_not_much_worse(const SeamCandidate &a, const SeamCandidate &b) const { // Blockers/Enforcers discrimination, top priority if (a.type > b.type) { return true; } if (b.type > a.type) { return false; } //avoid overhangs if (a.overhang > 0.3f && b.overhang < a.overhang) { return false; } return (a.visibility + SeamPlacer::expected_hits_per_area) * compute_angle_penalty(a.local_ccw_angle) * 0.75f <= (b.visibility + SeamPlacer::expected_hits_per_area) * compute_angle_penalty(b.local_ccw_angle); } } ; } // namespace SeamPlacerImpl // Parallel process and extract each perimeter polygon of the given print object. // Gather SeamCandidates of each layer into vector and build KDtree over them // Store results in the SeamPlacer varaibles m_perimeter_points_per_object and m_perimeter_points_trees_per_object void SeamPlacer::gather_seam_candidates(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info) { using namespace SeamPlacerImpl; m_perimeter_points_per_object.emplace(po, po->layer_count()); m_perimeter_points_trees_per_object.emplace(po, po->layer_count()); tbb::parallel_for(tbb::blocked_range(0, po->layers().size()), [&](tbb::blocked_range r) { for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { std::vector &layer_candidates = m_perimeter_points_per_object[po][layer_idx]; const Layer *layer = po->get_layer(layer_idx); auto unscaled_z = layer->slice_z; Polygons polygons = extract_perimeter_polygons(layer); for (const auto &poly : polygons) { process_perimeter_polygon(poly, unscaled_z, layer_candidates, global_model_info); } auto functor = SeamCandidateCoordinateFunctor { &layer_candidates }; m_perimeter_points_trees_per_object[po][layer_idx] = std::make_unique( functor, layer_candidates.size()); } } ); } void SeamPlacer::calculate_candidates_visibility(const PrintObject *po, const SeamPlacerImpl::GlobalModelInfo &global_model_info) { using namespace SeamPlacerImpl; tbb::parallel_for(tbb::blocked_range(0, m_perimeter_points_per_object[po].size()), [&](tbb::blocked_range r) { 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]) { perimeter_point.visibility = global_model_info.calculate_point_visibility( perimeter_point.position); } } }); } void SeamPlacer::calculate_overhangs(const PrintObject *po) { using namespace SeamPlacerImpl; tbb::parallel_for(tbb::blocked_range(0, m_perimeter_points_per_object[po].size()), [&](tbb::blocked_range r) { 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]) { const auto calculate_layer_overhang = [&](size_t other_layer_idx) { size_t closest_supporter = find_closest_point( *m_perimeter_points_trees_per_object[po][other_layer_idx], perimeter_point.position); const SeamCandidate &supporter_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 = m_perimeter_points_per_object[po][other_layer_idx][prev_next.first]; const SeamCandidate &next_point = m_perimeter_points_per_object[po][other_layer_idx][prev_next.second]; return calculate_overhang(perimeter_point, prev_point, supporter_point, next_point); }; if (layer_idx > 0) { //calculate overhang perimeter_point.overhang = calculate_layer_overhang(layer_idx-1); } } } }); } // Estimates, if there is good seam point in the layer_idx which is close to last_point_pos // uses comparator.is_first_not_much_worse method to compare current seam with the closest point // (if current seam is too far away ) // If the current chosen stream is close enough, it is stored in seam_string. returns true and updates last_point_pos // If the closest point is good enough to replace current chosen seam, it is stored in potential_string_seams, returns true and updates last_point_pos // Otherwise does nothing, returns false // sadly cannot be const because map access operator[] is not const, since it can create new object template bool SeamPlacer::find_next_seam_in_string(const PrintObject *po, Vec3f &last_point_pos, size_t layer_idx, const Comparator &comparator, std::vector> &seam_string, std::vector> &potential_string_seams) { using namespace SeamPlacerImpl; Vec3f projected_position { last_point_pos.x(), last_point_pos.y(), float( po->get_layer(layer_idx)->slice_z) }; //find closest point in next layer size_t closest_point_index = find_closest_point( *m_perimeter_points_trees_per_object[po][layer_idx], projected_position); SeamCandidate &closest_point = m_perimeter_points_per_object[po][layer_idx][closest_point_index]; if (closest_point.perimeter->aligned) { //already aligned, skip return false; } //from the closest point, deduce index of seam in the next layer SeamCandidate &next_layer_seam = m_perimeter_points_per_object[po][layer_idx][closest_point.perimeter->seam_index]; 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 seam_string.emplace_back(layer_idx, closest_point.perimeter->seam_index); last_point_pos = next_layer_seam.position; return true; } else if ((closest_point.position - projected_position).norm() < SeamPlacer::seam_align_tolerable_dist && 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 potential_string_seams.emplace_back(layer_idx, closest_point_index); last_point_pos = closest_point.position; return true; } else { return false; } } // clusters already chosen seam points into strings across multiple layers, and then // aligns the strings via polynomial fit // Does not change the positions of the SeamCandidates themselves, instead stores // the new aligned position into the shared Perimeter structure of each perimeter // Note that this position does not necesarilly lay on the perimeter. template void SeamPlacer::align_seam_points(const PrintObject *po, const Comparator &comparator) { using namespace SeamPlacerImpl; // Prepares Debug files for writing. #ifdef DEBUG_FILES Slic3r::CNumericLocalesSetter locales_setter; auto clusters_f = "seam_clusters_of_" + std::to_string(po->id().id) + ".obj"; FILE *clusters = boost::nowide::fopen(clusters_f.c_str(), "w"); if (clusters == nullptr) { BOOST_LOG_TRIVIAL(error) << "stl_write_obj: Couldn't open " << clusters_f << " for writing"; return; } auto aligned_f = "aligned_clusters_of_" + std::to_string(po->id().id) + ".obj"; FILE *aligns = boost::nowide::fopen(aligned_f.c_str(), "w"); if (aligns == nullptr) { BOOST_LOG_TRIVIAL(error) << "stl_write_obj: Couldn't open " << clusters_f << " for writing"; return; } #endif //gahter vector of all seams on the print_object - pair of layer_index and seam__index within that layer std::vector> seams; for (size_t layer_idx = 0; layer_idx < m_perimeter_points_per_object[po].size(); ++layer_idx) { std::vector &layer_perimeter_points = m_perimeter_points_per_object[po][layer_idx]; size_t current_point_index = 0; while (current_point_index < layer_perimeter_points.size()) { seams.emplace_back(layer_idx, layer_perimeter_points[current_point_index].perimeter->seam_index); current_point_index = layer_perimeter_points[current_point_index].perimeter->end_index + 1; } } //sort them before alignment. Alignment is sensitive to intitializaion, this gives it better chance to choose something nice std::sort(seams.begin(), seams.end(), [&](const std::pair &left, const std::pair &right) { return comparator.is_first_better(m_perimeter_points_per_object[po][left.first][left.second], m_perimeter_points_per_object[po][right.first][right.second]); } ); //align the sema points - start with the best, and check if they are aligned, if yes, skip, else start alignment for (const std::pair &seam : seams) { size_t layer_idx = seam.first; size_t seam_index = seam.second; std::vector &layer_perimeter_points = m_perimeter_points_per_object[po][layer_idx]; if (layer_perimeter_points[seam_index].perimeter->aligned) { // This perimeter is already aligned, skip seam continue; } else { //initialize searching for seam string - cluster of nearby seams on previous and next layers int skips = SeamPlacer::seam_align_tolerable_skips / 2; int next_layer = layer_idx + 1; Vec3f last_point_pos = layer_perimeter_points[seam_index].position; std::vector> seam_string; std::vector> potential_string_seams; //find seams or potential seams in forward direction; there is a budget of skips allowed while (skips >= 0 && next_layer < int(m_perimeter_points_per_object[po].size())) { if (find_next_seam_in_string(po, last_point_pos, next_layer, comparator, seam_string, potential_string_seams)) { //String added, last_point_pos updated, nothing to be done } else { // Layer skipped, reduce number of available skips skips--; } next_layer++; } //do additional check in back direction next_layer = layer_idx - 1; skips = SeamPlacer::seam_align_tolerable_skips / 2; last_point_pos = layer_perimeter_points[seam_index].position; while (skips >= 0 && next_layer >= 0) { if (find_next_seam_in_string(po, last_point_pos, next_layer, comparator, seam_string, potential_string_seams)) { //String added, last_point_pos updated, nothing to be done } else { // Layer skipped, reduce number of available skips skips--; } next_layer--; } if (seam_string.size() + potential_string_seams.size() < seam_align_minimum_string_seams) { //string long enough to be worth aligning, skip continue; } // String is long engouh, all string seams and potential string seams gathered, now do the alignment // first merge potential_string_seams and seam_string; from now on, they all will be aligned seam_string.insert(seam_string.end(), potential_string_seams.begin(), potential_string_seams.end()); //sort by layer index std::sort(seam_string.begin(), seam_string.end(), [](const std::pair &left, const std::pair &right) { return left.first < right.first; }); // gather all positions of seams std::vector points(seam_string.size()); for (size_t index = 0; index < seam_string.size(); ++index) { points[index] = m_perimeter_points_per_object[po][seam_string[index].first][seam_string[index].second].position; } // find coefficients of polynomial fit. Z coord is treated as parameter along which to fit both X and Y coords. std::vector coefficients = polyfit(points, 4); // Do alignment - compute fitted point for each point in the string from its Z coord, and store the position into // Perimeter structure of the point; also set flag aligned to true for (const auto &pair : seam_string) { float current_height = m_perimeter_points_per_object[po][pair.first][pair.second].position.z(); Vec3f seam_pos = get_fitted_point(coefficients, current_height); Perimeter *perimeter = m_perimeter_points_per_object[po][pair.first][pair.second].perimeter.get(); perimeter->final_seam_position = seam_pos; perimeter->aligned = true; } // //https://en.wikipedia.org/wiki/Exponential_smoothing // //inititalization // float smoothing_factor = 0.8; // std::pair init = seam_string[0]; // Vec2f prev_pos_xy = m_perimeter_points_per_object[po][init.first][init.second].position.head<2>(); // for (const auto &pair : seam_string) { // Vec3f current_pos = m_perimeter_points_per_object[po][pair.first][pair.second].position; // float current_height = current_pos.z(); // Vec2f current_pos_xy = current_pos.head<2>(); // current_pos_xy = smoothing_factor * prev_pos_xy + (1.0 - smoothing_factor) * current_pos_xy; // // Perimeter *perimeter = // m_perimeter_points_per_object[po][pair.first][pair.second].perimeter.get(); // perimeter->final_seam_position = // Vec3f { current_pos_xy.x(), current_pos_xy.y(), current_height }; // perimeter->aligned = true; // prev_pos_xy = current_pos_xy; // } #ifdef DEBUG_FILES auto randf = []() { return float(rand()) / float(RAND_MAX); }; Vec3f color { randf(), randf(), randf() }; for (size_t i = 0; i < seam_string.size(); ++i) { auto orig_seam = m_perimeter_points_per_object[po][seam_string[i].first][seam_string[i].second]; fprintf(clusters, "v %f %f %f %f %f %f \n", orig_seam.position[0], orig_seam.position[1], orig_seam.position[2], color[0], color[1], color[2]); } color = Vec3f { randf(), randf(), randf() }; for (size_t i = 0; i < seam_string.size(); ++i) { Perimeter *perimeter = m_perimeter_points_per_object[po][seam_string[i].first][seam_string[i].second].perimeter.get(); fprintf(aligns, "v %f %f %f %f %f %f \n", perimeter->final_seam_position[0], perimeter->final_seam_position[1], perimeter->final_seam_position[2], color[0], color[1], color[2]); } #endif } } #ifdef DEBUG_FILES fclose(clusters); fclose(aligns); #endif } void SeamPlacer::init(const Print &print) { using namespace SeamPlacerImpl; m_perimeter_points_trees_per_object.clear(); m_perimeter_points_per_object.clear(); for (const PrintObject *po : print.objects()) { SeamPosition configured_seam_preference = po->config().seam_position.value; GlobalModelInfo global_model_info { }; gather_global_model_info(global_model_info, po); BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: gather_seam_candidates: start"; gather_seam_candidates(po, global_model_info); BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: gather_seam_candidates: end"; if (configured_seam_preference != spAligned) { BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: calculate_candidates_visibility : start"; calculate_candidates_visibility(po, global_model_info); BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: calculate_candidates_visibility : end"; } BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: calculate_overhangs : start"; calculate_overhangs(po); BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: calculate_overhangs : end"; BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: pick_seam_point : start"; //pick seam point tbb::parallel_for(tbb::blocked_range(0, m_perimeter_points_per_object[po].size()), [&](tbb::blocked_range r) { for (size_t layer_idx = r.begin(); layer_idx < r.end(); ++layer_idx) { std::vector &layer_perimeter_points = m_perimeter_points_per_object[po][layer_idx]; size_t current = 0; while (current < layer_perimeter_points.size()) { pick_seam_point(layer_perimeter_points, current, DefaultSeamComparator { }); current = layer_perimeter_points[current].perimeter->end_index + 1; } } }); BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: pick_seam_point : end"; if (configured_seam_preference != spRandom) { BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: align_seam_points : start"; align_seam_points(po, DefaultSeamComparator { }); BOOST_LOG_TRIVIAL(debug) << "SeamPlacer: align_seam_points : end"; } } } void SeamPlacer::place_seam(const Layer *layer, ExtrusionLoop &loop, bool external_first) { using namespace SeamPlacerImpl; const PrintObject *po = layer->object(); //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())); double unscaled_z = layer->slice_z; const auto &perimeter_points_tree = *m_perimeter_points_trees_per_object[po][layer_index]; const auto &perimeter_points = m_perimeter_points_per_object[po][layer_index]; const Point &fp = loop.first_point(); Vec2f unscaled_p = unscale(fp).cast(); size_t closest_perimeter_point_index = find_closest_point(perimeter_points_tree, Vec3f { unscaled_p.x(), unscaled_p.y(), float(unscaled_z) }); const Perimeter *perimeter = perimeter_points[closest_perimeter_point_index].perimeter.get(); 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() }); if (!loop.split_at_vertex(seam_point)) // The point is not in the original loop. // Insert it. loop.split_at(seam_point, true); } } // namespace Slic3r