mirror of
https://github.com/Dark98/SliceBeam.git
synced 2026-07-07 00:39:25 +00:00
Public source code release
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
///|/ Copyright (c) Prusa Research 2019 - 2022 Tomáš Mészáros @tamasmeszaros, Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "AABBMesh.hpp"
|
||||
#include <Execution/ExecutionTBB.hpp>
|
||||
|
||||
#include <libslic3r/AABBTreeIndirect.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
|
||||
#include <numeric>
|
||||
|
||||
#ifdef SLIC3R_HOLE_RAYCASTER
|
||||
#include <libslic3r/SLA/Hollowing.hpp>
|
||||
#endif
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class AABBMesh::AABBImpl {
|
||||
private:
|
||||
AABBTreeIndirect::Tree3f m_tree;
|
||||
double m_triangle_ray_epsilon;
|
||||
|
||||
public:
|
||||
void init(const indexed_triangle_set &its, bool calculate_epsilon)
|
||||
{
|
||||
m_triangle_ray_epsilon = 0.000001;
|
||||
if (calculate_epsilon) {
|
||||
// Calculate epsilon from average triangle edge length.
|
||||
double l = its_average_edge_length(its);
|
||||
if (l > 0)
|
||||
m_triangle_ray_epsilon = 0.000001 * l * l;
|
||||
}
|
||||
m_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(
|
||||
its.vertices, its.indices);
|
||||
}
|
||||
|
||||
void intersect_ray(const indexed_triangle_set &its,
|
||||
const Vec3d & s,
|
||||
const Vec3d & dir,
|
||||
igl::Hit & hit)
|
||||
{
|
||||
AABBTreeIndirect::intersect_ray_first_hit(its.vertices, its.indices,
|
||||
m_tree, s, dir, hit, m_triangle_ray_epsilon);
|
||||
}
|
||||
|
||||
void intersect_ray(const indexed_triangle_set &its,
|
||||
const Vec3d & s,
|
||||
const Vec3d & dir,
|
||||
std::vector<igl::Hit> & hits)
|
||||
{
|
||||
AABBTreeIndirect::intersect_ray_all_hits(its.vertices, its.indices,
|
||||
m_tree, s, dir, hits, m_triangle_ray_epsilon);
|
||||
}
|
||||
|
||||
double squared_distance(const indexed_triangle_set & its,
|
||||
const Vec3d & point,
|
||||
int & i,
|
||||
Eigen::Matrix<double, 1, 3> &closest)
|
||||
{
|
||||
size_t idx_unsigned = 0;
|
||||
Vec3d closest_vec3d(closest);
|
||||
double dist =
|
||||
AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
|
||||
its.vertices, its.indices, m_tree, point, idx_unsigned,
|
||||
closest_vec3d);
|
||||
i = int(idx_unsigned);
|
||||
closest = closest_vec3d;
|
||||
return dist;
|
||||
}
|
||||
};
|
||||
|
||||
template<class M> void AABBMesh::init(const M &mesh, bool calculate_epsilon)
|
||||
{
|
||||
// Build the AABB accelaration tree
|
||||
m_aabb->init(*m_tm, calculate_epsilon);
|
||||
}
|
||||
|
||||
AABBMesh::AABBMesh(const indexed_triangle_set &tmesh, bool calculate_epsilon)
|
||||
: m_tm(&tmesh)
|
||||
, m_aabb(new AABBImpl())
|
||||
, m_vfidx{tmesh}
|
||||
, m_fnidx{its_face_neighbors(tmesh)}
|
||||
{
|
||||
init(tmesh, calculate_epsilon);
|
||||
}
|
||||
|
||||
AABBMesh::AABBMesh(const TriangleMesh &mesh, bool calculate_epsilon)
|
||||
: m_tm(&mesh.its)
|
||||
, m_aabb(new AABBImpl())
|
||||
, m_vfidx{mesh.its}
|
||||
, m_fnidx{its_face_neighbors(mesh.its)}
|
||||
{
|
||||
init(mesh, calculate_epsilon);
|
||||
}
|
||||
|
||||
AABBMesh::~AABBMesh() {}
|
||||
|
||||
AABBMesh::AABBMesh(const AABBMesh &other)
|
||||
: m_tm(other.m_tm)
|
||||
, m_aabb(new AABBImpl(*other.m_aabb))
|
||||
, m_vfidx{other.m_vfidx}
|
||||
, m_fnidx{other.m_fnidx}
|
||||
{}
|
||||
|
||||
AABBMesh &AABBMesh::operator=(const AABBMesh &other)
|
||||
{
|
||||
m_tm = other.m_tm;
|
||||
m_aabb.reset(new AABBImpl(*other.m_aabb));
|
||||
m_vfidx = other.m_vfidx;
|
||||
m_fnidx = other.m_fnidx;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
AABBMesh &AABBMesh::operator=(AABBMesh &&other) = default;
|
||||
|
||||
AABBMesh::AABBMesh(AABBMesh &&other) = default;
|
||||
|
||||
|
||||
|
||||
const std::vector<Vec3f>& AABBMesh::vertices() const
|
||||
{
|
||||
return m_tm->vertices;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const std::vector<Vec3i>& AABBMesh::indices() const
|
||||
{
|
||||
return m_tm->indices;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Vec3f& AABBMesh::vertices(size_t idx) const
|
||||
{
|
||||
return m_tm->vertices[idx];
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Vec3i& AABBMesh::indices(size_t idx) const
|
||||
{
|
||||
return m_tm->indices[idx];
|
||||
}
|
||||
|
||||
|
||||
Vec3d AABBMesh::normal_by_face_id(int face_id) const {
|
||||
|
||||
return its_unnormalized_normal(*m_tm, face_id).cast<double>().normalized();
|
||||
}
|
||||
|
||||
|
||||
AABBMesh::hit_result
|
||||
AABBMesh::query_ray_hit(const Vec3d &s, const Vec3d &dir) const
|
||||
{
|
||||
assert(is_approx(dir.norm(), 1.));
|
||||
igl::Hit hit{-1, -1, 0.f, 0.f, 0.f};
|
||||
hit.t = std::numeric_limits<float>::infinity();
|
||||
|
||||
#ifdef SLIC3R_HOLE_RAYCASTER
|
||||
if (! m_holes.empty()) {
|
||||
|
||||
// If there are holes, the hit_results will be made by
|
||||
// query_ray_hits (object) and filter_hits (holes):
|
||||
return filter_hits(query_ray_hits(s, dir));
|
||||
}
|
||||
#endif
|
||||
|
||||
m_aabb->intersect_ray(*m_tm, s, dir, hit);
|
||||
hit_result ret(*this);
|
||||
ret.m_t = double(hit.t);
|
||||
ret.m_dir = dir;
|
||||
ret.m_source = s;
|
||||
if(!std::isinf(hit.t) && !std::isnan(hit.t)) {
|
||||
ret.m_normal = this->normal_by_face_id(hit.id);
|
||||
ret.m_face_id = hit.id;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<AABBMesh::hit_result>
|
||||
AABBMesh::query_ray_hits(const Vec3d &s, const Vec3d &dir) const
|
||||
{
|
||||
std::vector<AABBMesh::hit_result> outs;
|
||||
std::vector<igl::Hit> hits;
|
||||
m_aabb->intersect_ray(*m_tm, s, dir, hits);
|
||||
|
||||
// The sort is necessary, the hits are not always sorted.
|
||||
std::sort(hits.begin(), hits.end(),
|
||||
[](const igl::Hit& a, const igl::Hit& b) { return a.t < b.t; });
|
||||
|
||||
// Remove duplicates. They sometimes appear, for example when the ray is cast
|
||||
// along an axis of a cube due to floating-point approximations in igl (?)
|
||||
hits.erase(std::unique(hits.begin(), hits.end(),
|
||||
[](const igl::Hit& a, const igl::Hit& b)
|
||||
{ return a.t == b.t; }),
|
||||
hits.end());
|
||||
|
||||
// Convert the igl::Hit into hit_result
|
||||
outs.reserve(hits.size());
|
||||
for (const igl::Hit& hit : hits) {
|
||||
outs.emplace_back(AABBMesh::hit_result(*this));
|
||||
outs.back().m_t = double(hit.t);
|
||||
outs.back().m_dir = dir;
|
||||
outs.back().m_source = s;
|
||||
if(!std::isinf(hit.t) && !std::isnan(hit.t)) {
|
||||
outs.back().m_normal = this->normal_by_face_id(hit.id);
|
||||
outs.back().m_face_id = hit.id;
|
||||
}
|
||||
}
|
||||
|
||||
return outs;
|
||||
}
|
||||
|
||||
|
||||
#ifdef SLIC3R_HOLE_RAYCASTER
|
||||
AABBMesh::hit_result IndexedMesh::filter_hits(
|
||||
const std::vector<AABBMesh::hit_result>& object_hits) const
|
||||
{
|
||||
assert(! m_holes.empty());
|
||||
hit_result out(*this);
|
||||
|
||||
if (object_hits.empty())
|
||||
return out;
|
||||
|
||||
const Vec3d& s = object_hits.front().source();
|
||||
const Vec3d& dir = object_hits.front().direction();
|
||||
|
||||
// A helper struct to save an intersetion with a hole
|
||||
struct HoleHit {
|
||||
HoleHit(float t_p, const Vec3d& normal_p, bool entry_p) :
|
||||
t(t_p), normal(normal_p), entry(entry_p) {}
|
||||
float t;
|
||||
Vec3d normal;
|
||||
bool entry;
|
||||
};
|
||||
std::vector<HoleHit> hole_isects;
|
||||
hole_isects.reserve(m_holes.size());
|
||||
|
||||
auto sf = s.cast<float>();
|
||||
auto dirf = dir.cast<float>();
|
||||
|
||||
// Collect hits on all holes, preserve information about entry/exit
|
||||
for (const sla::DrainHole& hole : m_holes) {
|
||||
std::array<std::pair<float, Vec3d>, 2> isects;
|
||||
if (hole.get_intersections(sf, dirf, isects)) {
|
||||
// Ignore hole hits behind the source
|
||||
if (isects[0].first > 0.f) hole_isects.emplace_back(isects[0].first, isects[0].second, true);
|
||||
if (isects[1].first > 0.f) hole_isects.emplace_back(isects[1].first, isects[1].second, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Holes can intersect each other, sort the hits by t
|
||||
std::sort(hole_isects.begin(), hole_isects.end(),
|
||||
[](const HoleHit& a, const HoleHit& b) { return a.t < b.t; });
|
||||
|
||||
// Now inspect the intersections with object and holes, in the order of
|
||||
// increasing distance. Keep track how deep are we nested in mesh/holes and
|
||||
// pick the correct intersection.
|
||||
// This needs to be done twice - first to find out how deep in the structure
|
||||
// the source is, then to pick the correct intersection.
|
||||
int hole_nested = 0;
|
||||
int object_nested = 0;
|
||||
for (int dry_run=1; dry_run>=0; --dry_run) {
|
||||
hole_nested = -hole_nested;
|
||||
object_nested = -object_nested;
|
||||
|
||||
bool is_hole = false;
|
||||
bool is_entry = false;
|
||||
const HoleHit* next_hole_hit = hole_isects.empty() ? nullptr : &hole_isects.front();
|
||||
const hit_result* next_mesh_hit = &object_hits.front();
|
||||
|
||||
while (next_hole_hit || next_mesh_hit) {
|
||||
if (next_hole_hit && next_mesh_hit) // still have hole and obj hits
|
||||
is_hole = (next_hole_hit->t < next_mesh_hit->m_t);
|
||||
else
|
||||
is_hole = next_hole_hit; // one or the other ran out
|
||||
|
||||
// Is this entry or exit hit?
|
||||
is_entry = is_hole ? next_hole_hit->entry : ! next_mesh_hit->is_inside();
|
||||
|
||||
if (! dry_run) {
|
||||
if (! is_hole && hole_nested == 0) {
|
||||
// This is a valid object hit
|
||||
return *next_mesh_hit;
|
||||
}
|
||||
if (is_hole && ! is_entry && object_nested != 0) {
|
||||
// This holehit is the one we seek
|
||||
out.m_t = next_hole_hit->t;
|
||||
out.m_normal = next_hole_hit->normal;
|
||||
out.m_source = s;
|
||||
out.m_dir = dir;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
// Increase/decrease the counter
|
||||
(is_hole ? hole_nested : object_nested) += (is_entry ? 1 : -1);
|
||||
|
||||
// Advance the respective pointer
|
||||
if (is_hole && next_hole_hit++ == &hole_isects.back())
|
||||
next_hole_hit = nullptr;
|
||||
if (! is_hole && next_mesh_hit++ == &object_hits.back())
|
||||
next_mesh_hit = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// if we got here, the ray ended up in infinity
|
||||
return out;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
double AABBMesh::squared_distance(const Vec3d &p, int& i, Vec3d& c) const {
|
||||
double sqdst = 0;
|
||||
Eigen::Matrix<double, 1, 3> pp = p;
|
||||
Eigen::Matrix<double, 1, 3> cc;
|
||||
sqdst = m_aabb->squared_distance(*m_tm, pp, i, cc);
|
||||
c = cc;
|
||||
return sqdst;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,146 @@
|
||||
///|/ Copyright (c) Prusa Research 2019 - 2022 Tomáš Mészáros @tamasmeszaros, Lukáš Hejl @hejllukas, Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef PRUSASLICER_AABBMESH_H
|
||||
#define PRUSASLICER_AABBMESH_H
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <libslic3r/Point.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
|
||||
// There is an implementation of a hole-aware raycaster that was eventually
|
||||
// not used in production version. It is now hidden under following define
|
||||
// for possible future use.
|
||||
// #define SLIC3R_HOLE_RAYCASTER
|
||||
|
||||
#ifdef SLIC3R_HOLE_RAYCASTER
|
||||
#include "libslic3r/SLA/Hollowing.hpp"
|
||||
#endif
|
||||
|
||||
struct indexed_triangle_set;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class TriangleMesh;
|
||||
|
||||
// An index-triangle structure coupled with an AABB index to support ray
|
||||
// casting and other higher level operations.
|
||||
class AABBMesh {
|
||||
class AABBImpl;
|
||||
|
||||
const indexed_triangle_set* m_tm;
|
||||
|
||||
std::unique_ptr<AABBImpl> m_aabb;
|
||||
VertexFaceIndex m_vfidx; // vertex-face index
|
||||
std::vector<Vec3i> m_fnidx; // face-neighbor index
|
||||
|
||||
#ifdef SLIC3R_HOLE_RAYCASTER
|
||||
// This holds a copy of holes in the mesh. Initialized externally
|
||||
// by load_mesh setter.
|
||||
std::vector<sla::DrainHole> m_holes;
|
||||
#endif
|
||||
|
||||
template<class M> void init(const M &mesh, bool calculate_epsilon);
|
||||
|
||||
public:
|
||||
|
||||
// calculate_epsilon ... calculate epsilon for triangle-ray intersection from an average triangle edge length.
|
||||
// If set to false, a default epsilon is used, which works for "reasonable" meshes.
|
||||
explicit AABBMesh(const indexed_triangle_set &tmesh, bool calculate_epsilon = false);
|
||||
explicit AABBMesh(const TriangleMesh &mesh, bool calculate_epsilon = false);
|
||||
|
||||
AABBMesh(const AABBMesh& other);
|
||||
AABBMesh& operator=(const AABBMesh&);
|
||||
|
||||
AABBMesh(AABBMesh &&other);
|
||||
AABBMesh& operator=(AABBMesh &&other);
|
||||
|
||||
~AABBMesh();
|
||||
|
||||
const std::vector<Vec3f>& vertices() const;
|
||||
const std::vector<Vec3i>& indices() const;
|
||||
const Vec3f& vertices(size_t idx) const;
|
||||
const Vec3i& indices(size_t idx) const;
|
||||
|
||||
// Result of a raycast
|
||||
class hit_result {
|
||||
// m_t holds a distance from m_source to the intersection.
|
||||
double m_t = infty();
|
||||
int m_face_id = -1;
|
||||
const AABBMesh *m_mesh = nullptr;
|
||||
Vec3d m_dir = Vec3d::Zero();
|
||||
Vec3d m_source = Vec3d::Zero();
|
||||
Vec3d m_normal = Vec3d::Zero();
|
||||
friend class AABBMesh;
|
||||
|
||||
// A valid object of this class can only be obtained from
|
||||
// IndexedMesh::query_ray_hit method.
|
||||
explicit inline hit_result(const AABBMesh& em): m_mesh(&em) {}
|
||||
public:
|
||||
// This denotes no hit on the mesh.
|
||||
static inline constexpr double infty() { return std::numeric_limits<double>::infinity(); }
|
||||
|
||||
explicit inline hit_result(double val = infty()) : m_t(val) {}
|
||||
|
||||
inline double distance() const { return m_t; }
|
||||
inline const Vec3d& direction() const { return m_dir; }
|
||||
inline const Vec3d& source() const { return m_source; }
|
||||
inline Vec3d position() const { return m_source + m_dir * m_t; }
|
||||
inline int face() const { return m_face_id; }
|
||||
inline bool is_valid() const { return m_mesh != nullptr; }
|
||||
inline bool is_hit() const { return m_face_id >= 0 && !std::isinf(m_t); }
|
||||
|
||||
inline const Vec3d& normal() const {
|
||||
assert(is_valid());
|
||||
return m_normal;
|
||||
}
|
||||
|
||||
inline bool is_inside() const {
|
||||
return is_hit() && normal().dot(m_dir) > 0;
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef SLIC3R_HOLE_RAYCASTER
|
||||
// Inform the object about location of holes
|
||||
// creates internal copy of the vector
|
||||
void load_holes(const std::vector<sla::DrainHole>& holes) {
|
||||
m_holes = holes;
|
||||
}
|
||||
|
||||
// Iterates over hits and holes and returns the true hit, possibly
|
||||
// on the inside of a hole.
|
||||
// This function is currently not used anywhere, it was written when the
|
||||
// holes were subtracted on slices, that is, before we started using CGAL
|
||||
// to actually cut the holes into the mesh.
|
||||
hit_result filter_hits(const std::vector<AABBMesh::hit_result>& obj_hits) const;
|
||||
#endif
|
||||
|
||||
// Casting a ray on the mesh, returns the distance where the hit occures.
|
||||
hit_result query_ray_hit(const Vec3d &s, const Vec3d &dir) const;
|
||||
|
||||
// Casts a ray on the mesh and returns all hits
|
||||
std::vector<hit_result> query_ray_hits(const Vec3d &s, const Vec3d &dir) const;
|
||||
|
||||
double squared_distance(const Vec3d& p, int& i, Vec3d& c) const;
|
||||
inline double squared_distance(const Vec3d &p) const
|
||||
{
|
||||
int i;
|
||||
Vec3d c;
|
||||
return squared_distance(p, i, c);
|
||||
}
|
||||
|
||||
Vec3d normal_by_face_id(int face_id) const;
|
||||
|
||||
const indexed_triangle_set * get_triangle_mesh() const { return m_tm; }
|
||||
|
||||
const VertexFaceIndex &vertex_face_index() const { return m_vfidx; }
|
||||
const std::vector<Vec3i> &face_neighbor_index() const { return m_fnidx; }
|
||||
};
|
||||
|
||||
|
||||
} // namespace Slic3r::sla
|
||||
|
||||
#endif // INDEXEDMESH_H
|
||||
@@ -0,0 +1,996 @@
|
||||
///|/ Copyright (c) Prusa Research 2020 - 2023 Vojtěch Bubník @bubnikv, Pavel Mikuš @Godrak, Tomáš Mészáros @tamasmeszaros, Lukáš Matěna @lukasmatena, Lukáš Hejl @hejllukas
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
// AABB tree built upon external data set, referencing the external data by integer indices.
|
||||
// The AABB tree balancing and traversal (ray casting, closest triangle of an indexed triangle mesh)
|
||||
// were adapted from libigl AABB.{cpp,hpp} Copyright (C) 2015 Alec Jacobson <alecjacobson@gmail.com>
|
||||
// while the implicit balanced tree representation and memory optimizations are Vojtech's.
|
||||
|
||||
#ifndef slic3r_AABBTreeIndirect_hpp_
|
||||
#define slic3r_AABBTreeIndirect_hpp_
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include <Eigen/Geometry>
|
||||
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Utils.hpp" // for next_highest_power_of_2()
|
||||
|
||||
// Definition of the ray intersection hit structure.
|
||||
#include <igl/Hit.h>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace AABBTreeIndirect {
|
||||
|
||||
// Static balanced AABB tree for raycasting and closest triangle search.
|
||||
// The balanced tree is built over a single large std::vector of nodes, where the children of nodes
|
||||
// are addressed implicitely using a power of two indexing rule.
|
||||
// Memory for a full balanced tree is allocated, but not all nodes at the last level are used.
|
||||
// This may seem like a waste of memory, but one saves memory for the node links and there is zero
|
||||
// overhead of a memory allocator management (usually the memory allocator adds at least one pointer
|
||||
// before the memory returned). However, allocating memory in a single vector is very fast even
|
||||
// in multi-threaded environment and it is cache friendly.
|
||||
//
|
||||
// A balanced tree is built upon a vector of bounding boxes and their centroids, storing the reference
|
||||
// to the source entity (a 3D triangle, a 2D segment etc, a 3D or 2D point etc).
|
||||
// The source bounding boxes may have an epsilon applied to fight numeric rounding errors when
|
||||
// traversing the AABB tree.
|
||||
template<int ANumDimensions, typename ACoordType>
|
||||
class Tree
|
||||
{
|
||||
public:
|
||||
static constexpr int NumDimensions = ANumDimensions;
|
||||
using CoordType = ACoordType;
|
||||
using VectorType = Eigen::Matrix<CoordType, NumDimensions, 1, Eigen::DontAlign>;
|
||||
using BoundingBox = Eigen::AlignedBox<CoordType, NumDimensions>;
|
||||
// Following could be static constexpr size_t, but that would not link in C++11
|
||||
enum : size_t {
|
||||
// Node is not used.
|
||||
npos = size_t(-1),
|
||||
// Inner node (not leaf).
|
||||
inner = size_t(-2)
|
||||
};
|
||||
|
||||
// Single node of the implicit balanced AABB tree. There are no links to the children nodes,
|
||||
// as these links are calculated implicitely using a power of two rule.
|
||||
struct Node {
|
||||
// Index of the external source entity, for which this AABB tree was built, npos for internal nodes.
|
||||
size_t idx = npos;
|
||||
// Bounding box around this entity, possibly with epsilons applied to fight numeric rounding errors
|
||||
// when traversing the AABB tree.
|
||||
BoundingBox bbox;
|
||||
|
||||
bool is_valid() const { return this->idx != npos; }
|
||||
bool is_inner() const { return this->idx == inner; }
|
||||
bool is_leaf() const { return ! this->is_inner(); }
|
||||
|
||||
template<typename SourceNode>
|
||||
void set(const SourceNode &rhs) {
|
||||
this->idx = rhs.idx();
|
||||
this->bbox = rhs.bbox();
|
||||
}
|
||||
};
|
||||
|
||||
void clear() { m_nodes.clear(); }
|
||||
|
||||
// SourceNode shall implement
|
||||
// size_t SourceNode::idx() const
|
||||
// - Index to the outside entity (triangle, edge, point etc).
|
||||
// const VectorType& SourceNode::centroid() const
|
||||
// - Centroid of this node. The centroid is used for balancing the tree.
|
||||
// const BoundingBox& SourceNode::bbox() const
|
||||
// - Bounding box of this node, likely expanded with epsilon to account for numeric rounding during tree traversal.
|
||||
// Union of bounding boxes at a single level of the AABB tree is used for deciding the longest axis aligned dimension
|
||||
// to split around.
|
||||
template<typename SourceNode>
|
||||
void build(std::vector<SourceNode> &&input)
|
||||
{
|
||||
this->build_modify_input(input);
|
||||
input.clear();
|
||||
}
|
||||
|
||||
template<typename SourceNode>
|
||||
void build_modify_input(std::vector<SourceNode> &input)
|
||||
{
|
||||
if (input.empty())
|
||||
clear();
|
||||
else {
|
||||
// Allocate enough memory for a full binary tree.
|
||||
m_nodes.assign(next_highest_power_of_2(input.size()) * 2 - 1, Node());
|
||||
build_recursive(input, 0, 0, input.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<Node>& nodes() const { return m_nodes; }
|
||||
const Node& node(size_t idx) const { return m_nodes[idx]; }
|
||||
bool empty() const { return m_nodes.empty(); }
|
||||
|
||||
// Addressing the child nodes using the power of two rule.
|
||||
static size_t left_child_idx(size_t idx) { return idx * 2 + 1; }
|
||||
static size_t right_child_idx(size_t idx) { return left_child_idx(idx) + 1; }
|
||||
const Node& left_child(size_t idx) const { return m_nodes[left_child_idx(idx)]; }
|
||||
const Node& right_child(size_t idx) const { return m_nodes[right_child_idx(idx)]; }
|
||||
|
||||
template<typename SourceNode>
|
||||
void build(const std::vector<SourceNode> &input)
|
||||
{
|
||||
std::vector<SourceNode> copy(input);
|
||||
this->build(std::move(copy));
|
||||
}
|
||||
|
||||
private:
|
||||
// Build a balanced tree by splitting the input sequence by an axis aligned plane at a dimension.
|
||||
template<typename SourceNode>
|
||||
void build_recursive(std::vector<SourceNode> &input, size_t node, const size_t left, const size_t right)
|
||||
{
|
||||
assert(node < m_nodes.size());
|
||||
assert(left <= right);
|
||||
|
||||
if (left == right) {
|
||||
// Insert a node into the balanced tree.
|
||||
m_nodes[node].set(input[left]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate bounding box of the input.
|
||||
BoundingBox bbox(input[left].bbox());
|
||||
for (size_t i = left + 1; i <= right; ++ i)
|
||||
bbox.extend(input[i].bbox());
|
||||
int dimension = -1;
|
||||
bbox.diagonal().maxCoeff(&dimension);
|
||||
|
||||
// Partition the input to left / right pieces of the same length to produce a balanced tree.
|
||||
size_t center = (left + right) / 2;
|
||||
partition_input(input, size_t(dimension), left, right, center);
|
||||
// Insert an inner node into the tree. Inner node does not reference any input entity (triangle, line segment etc).
|
||||
m_nodes[node].idx = inner;
|
||||
m_nodes[node].bbox = bbox;
|
||||
build_recursive(input, node * 2 + 1, left, center);
|
||||
build_recursive(input, node * 2 + 2, center + 1, right);
|
||||
}
|
||||
|
||||
// Partition the input m_nodes <left, right> at "k" and "dimension" using the QuickSelect method:
|
||||
// https://en.wikipedia.org/wiki/Quickselect
|
||||
// Items left of the k'th item are lower than the k'th item in the "dimension",
|
||||
// items right of the k'th item are higher than the k'th item in the "dimension",
|
||||
template<typename SourceNode>
|
||||
void partition_input(std::vector<SourceNode> &input, const size_t dimension, size_t left, size_t right, const size_t k) const
|
||||
{
|
||||
while (left < right) {
|
||||
size_t center = (left + right) / 2;
|
||||
CoordType pivot;
|
||||
{
|
||||
// Bubble sort the input[left], input[center], input[right], so that a median of the three values
|
||||
// will end up in input[center].
|
||||
CoordType left_value = input[left ].centroid()(dimension);
|
||||
CoordType center_value = input[center].centroid()(dimension);
|
||||
CoordType right_value = input[right ].centroid()(dimension);
|
||||
if (left_value > center_value) {
|
||||
std::swap(input[left], input[center]);
|
||||
std::swap(left_value, center_value);
|
||||
}
|
||||
if (left_value > right_value) {
|
||||
std::swap(input[left], input[right]);
|
||||
right_value = left_value;
|
||||
}
|
||||
if (center_value > right_value) {
|
||||
std::swap(input[center], input[right]);
|
||||
center_value = right_value;
|
||||
}
|
||||
pivot = center_value;
|
||||
}
|
||||
if (right <= left + 2)
|
||||
// The <left, right> interval is already sorted.
|
||||
break;
|
||||
size_t i = left;
|
||||
size_t j = right - 1;
|
||||
std::swap(input[center], input[j]);
|
||||
// Partition the set based on the pivot.
|
||||
for (;;) {
|
||||
// Skip left points that are already at correct positions.
|
||||
// Search will certainly stop at position (right - 1), which stores the pivot.
|
||||
while (input[++ i].centroid()(dimension) < pivot) ;
|
||||
// Skip right points that are already at correct positions.
|
||||
while (input[-- j].centroid()(dimension) > pivot && i < j) ;
|
||||
if (i >= j)
|
||||
break;
|
||||
std::swap(input[i], input[j]);
|
||||
}
|
||||
// Restore pivot to the center of the sequence.
|
||||
std::swap(input[i], input[right - 1]);
|
||||
// Which side the kth element is in?
|
||||
if (k < i)
|
||||
right = i - 1;
|
||||
else if (k == i)
|
||||
// Sequence is partitioned, kth element is at its place.
|
||||
break;
|
||||
else
|
||||
left = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// The balanced tree storage.
|
||||
std::vector<Node> m_nodes;
|
||||
};
|
||||
|
||||
using Tree2f = Tree<2, float>;
|
||||
using Tree3f = Tree<3, float>;
|
||||
using Tree2d = Tree<2, double>;
|
||||
using Tree3d = Tree<3, double>;
|
||||
|
||||
// Wrap a 2D Slic3r own BoundingBox to be passed to Tree::build() and similar
|
||||
// to build an AABBTree over coord_t 2D bounding boxes.
|
||||
class BoundingBoxWrapper {
|
||||
public:
|
||||
using BoundingBox = Eigen::AlignedBox<coord_t, 2>;
|
||||
BoundingBoxWrapper(const size_t idx, const Slic3r::BoundingBox &bbox) :
|
||||
m_idx(idx),
|
||||
// Inflate the bounding box a bit to account for numerical issues.
|
||||
m_bbox(bbox.min - Point(SCALED_EPSILON, SCALED_EPSILON), bbox.max + Point(SCALED_EPSILON, SCALED_EPSILON)) {}
|
||||
size_t idx() const { return m_idx; }
|
||||
const BoundingBox& bbox() const { return m_bbox; }
|
||||
Point centroid() const { return ((m_bbox.min().cast<int64_t>() + m_bbox.max().cast<int64_t>()) / 2).cast<int32_t>(); }
|
||||
private:
|
||||
size_t m_idx;
|
||||
BoundingBox m_bbox;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
template<typename AVertexType, typename AIndexedFaceType, typename ATreeType, typename AVectorType>
|
||||
struct RayIntersector {
|
||||
using VertexType = AVertexType;
|
||||
using IndexedFaceType = AIndexedFaceType;
|
||||
using TreeType = ATreeType;
|
||||
using VectorType = AVectorType;
|
||||
|
||||
const std::vector<VertexType> &vertices;
|
||||
const std::vector<IndexedFaceType> &faces;
|
||||
const TreeType &tree;
|
||||
|
||||
const VectorType origin;
|
||||
const VectorType dir;
|
||||
const VectorType invdir;
|
||||
|
||||
// epsilon for ray-triangle intersection, see intersect_triangle1()
|
||||
const double eps;
|
||||
};
|
||||
|
||||
template<typename VertexType, typename IndexedFaceType, typename TreeType, typename VectorType>
|
||||
struct RayIntersectorHits : RayIntersector<VertexType, IndexedFaceType, TreeType, VectorType> {
|
||||
std::vector<igl::Hit> hits;
|
||||
};
|
||||
|
||||
//FIXME implement SSE for float AABB trees with float ray queries.
|
||||
// SSE/SSE2 is supported by any Intel/AMD x64 processor.
|
||||
// SSE support requires 16 byte alignment of the AABB nodes, representing the bounding boxes with 4+4 floats,
|
||||
// storing the node index as the 4th element of the bounding box min value etc.
|
||||
// https://www.flipcode.com/archives/SSE_RayBox_Intersection_Test.shtml
|
||||
template <typename Derivedsource, typename Deriveddir, typename Scalar>
|
||||
inline bool ray_box_intersect_invdir(
|
||||
const Eigen::MatrixBase<Derivedsource> &origin,
|
||||
const Eigen::MatrixBase<Deriveddir> &inv_dir,
|
||||
Eigen::AlignedBox<Scalar,3> box,
|
||||
const Scalar &t0,
|
||||
const Scalar &t1) {
|
||||
// http://people.csail.mit.edu/amy/papers/box-jgt.pdf
|
||||
// "An Efficient and Robust Ray–Box Intersection Algorithm"
|
||||
if (inv_dir.x() < 0)
|
||||
std::swap(box.min().x(), box.max().x());
|
||||
if (inv_dir.y() < 0)
|
||||
std::swap(box.min().y(), box.max().y());
|
||||
Scalar tmin = (box.min().x() - origin.x()) * inv_dir.x();
|
||||
Scalar tymax = (box.max().y() - origin.y()) * inv_dir.y();
|
||||
if (tmin > tymax)
|
||||
return false;
|
||||
Scalar tmax = (box.max().x() - origin.x()) * inv_dir.x();
|
||||
Scalar tymin = (box.min().y() - origin.y()) * inv_dir.y();
|
||||
if (tymin > tmax)
|
||||
return false;
|
||||
if (tymin > tmin)
|
||||
tmin = tymin;
|
||||
if (tymax < tmax)
|
||||
tmax = tymax;
|
||||
if (inv_dir.z() < 0)
|
||||
std::swap(box.min().z(), box.max().z());
|
||||
Scalar tzmin = (box.min().z() - origin.z()) * inv_dir.z();
|
||||
if (tzmin > tmax)
|
||||
return false;
|
||||
Scalar tzmax = (box.max().z() - origin.z()) * inv_dir.z();
|
||||
if (tmin > tzmax)
|
||||
return false;
|
||||
if (tzmin > tmin)
|
||||
tmin = tzmin;
|
||||
if (tzmax < tmax)
|
||||
tmax = tzmax;
|
||||
return tmin < t1 && tmax > t0;
|
||||
}
|
||||
|
||||
// The following intersect_triangle() is derived from raytri.c routine intersect_triangle1()
|
||||
// Ray-Triangle Intersection Test Routines
|
||||
// Different optimizations of my and Ben Trumbore's
|
||||
// code from journals of graphics tools (JGT)
|
||||
// http://www.acm.org/jgt/
|
||||
// by Tomas Moller, May 2000
|
||||
template<typename V, typename W>
|
||||
std::enable_if_t<std::is_same<typename V::Scalar, double>::value&& std::is_same<typename W::Scalar, double>::value, bool>
|
||||
intersect_triangle(const V &orig, const V &dir, const W &vert0, const W &vert1, const W &vert2, double &t, double &u, double &v, double eps)
|
||||
{
|
||||
// find vectors for two edges sharing vert0
|
||||
const V edge1 = vert1 - vert0;
|
||||
const V edge2 = vert2 - vert0;
|
||||
// begin calculating determinant - also used to calculate U parameter
|
||||
const V pvec = dir.cross(edge2);
|
||||
// if determinant is near zero, ray lies in plane of triangle
|
||||
const double det = edge1.dot(pvec);
|
||||
V qvec;
|
||||
|
||||
if (det > eps) {
|
||||
// calculate distance from vert0 to ray origin
|
||||
V tvec = orig - vert0;
|
||||
// calculate U parameter and test bounds
|
||||
u = tvec.dot(pvec);
|
||||
if (u < 0.0 || u > det)
|
||||
return false;
|
||||
// prepare to test V parameter
|
||||
qvec = tvec.cross(edge1);
|
||||
// calculate V parameter and test bounds
|
||||
v = dir.dot(qvec);
|
||||
if (v < 0.0 || u + v > det)
|
||||
return false;
|
||||
} else if (det < -eps) {
|
||||
// calculate distance from vert0 to ray origin
|
||||
V tvec = orig - vert0;
|
||||
// calculate U parameter and test bounds
|
||||
u = tvec.dot(pvec);
|
||||
if (u > 0.0 || u < det)
|
||||
return false;
|
||||
// prepare to test V parameter
|
||||
qvec = tvec.cross(edge1);
|
||||
// calculate V parameter and test bounds
|
||||
v = dir.dot(qvec);
|
||||
if (v > 0.0 || u + v < det)
|
||||
return false;
|
||||
} else
|
||||
// ray is parallel to the plane of the triangle
|
||||
return false;
|
||||
|
||||
double inv_det = 1.0 / det;
|
||||
// calculate t, ray intersects triangle
|
||||
t = edge2.dot(qvec) * inv_det;
|
||||
u *= inv_det;
|
||||
v *= inv_det;
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename V, typename W>
|
||||
std::enable_if_t<std::is_same<typename V::Scalar, double>::value && !std::is_same<typename W::Scalar, double>::value, bool>
|
||||
intersect_triangle(const V &origin, const V &dir, const W &v0, const W &v1, const W &v2, double &t, double &u, double &v, double eps) {
|
||||
return intersect_triangle(origin, dir, v0.template cast<double>(), v1.template cast<double>(), v2.template cast<double>(), t, u, v, eps);
|
||||
}
|
||||
|
||||
template<typename V, typename W>
|
||||
std::enable_if_t<! std::is_same<typename V::Scalar, double>::value && std::is_same<typename W::Scalar, double>::value, bool>
|
||||
intersect_triangle(const V &origin, const V &dir, const W &v0, const W &v1, const W &v2, double &t, double &u, double &v, double eps) {
|
||||
return intersect_triangle(origin.template cast<double>(), dir.template cast<double>(), v0, v1, v2, t, u, v, eps);
|
||||
}
|
||||
|
||||
template<typename V, typename W>
|
||||
std::enable_if_t<! std::is_same<typename V::Scalar, double>::value && ! std::is_same<typename W::Scalar, double>::value, bool>
|
||||
intersect_triangle(const V &origin, const V &dir, const W &v0, const W &v1, const W &v2, double &t, double &u, double &v, double eps) {
|
||||
return intersect_triangle(origin.template cast<double>(), dir.template cast<double>(), v0.template cast<double>(), v1.template cast<double>(), v2.template cast<double>(), t, u, v, eps);
|
||||
}
|
||||
|
||||
template<typename Tree>
|
||||
double intersect_triangle_epsilon(const Tree &tree) {
|
||||
double eps = 0.000001;
|
||||
if (! tree.empty()) {
|
||||
const typename Tree::BoundingBox &bbox = tree.nodes().front().bbox;
|
||||
double l = (bbox.max() - bbox.min()).cwiseMax();
|
||||
if (l > 0)
|
||||
eps /= (l * l);
|
||||
}
|
||||
return eps;
|
||||
}
|
||||
|
||||
template<typename RayIntersectorType, typename Scalar>
|
||||
static inline bool intersect_ray_recursive_first_hit(
|
||||
RayIntersectorType &ray_intersector,
|
||||
size_t node_idx,
|
||||
Scalar min_t,
|
||||
igl::Hit &hit)
|
||||
{
|
||||
const auto &node = ray_intersector.tree.node(node_idx);
|
||||
assert(node.is_valid());
|
||||
|
||||
if (! ray_box_intersect_invdir(ray_intersector.origin, ray_intersector.invdir, node.bbox.template cast<Scalar>(), Scalar(0), min_t))
|
||||
return false;
|
||||
|
||||
if (node.is_leaf()) {
|
||||
// shoot ray, record hit
|
||||
auto face = ray_intersector.faces[node.idx];
|
||||
double t, u, v;
|
||||
if (intersect_triangle(
|
||||
ray_intersector.origin, ray_intersector.dir,
|
||||
ray_intersector.vertices[face(0)], ray_intersector.vertices[face(1)], ray_intersector.vertices[face(2)],
|
||||
t, u, v, ray_intersector.eps)
|
||||
&& t > 0.) {
|
||||
hit = igl::Hit { int(node.idx), -1, float(u), float(v), float(t) };
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
} else {
|
||||
// Left / right child node index.
|
||||
size_t left = node_idx * 2 + 1;
|
||||
size_t right = left + 1;
|
||||
igl::Hit left_hit;
|
||||
igl::Hit right_hit;
|
||||
bool left_ret = intersect_ray_recursive_first_hit(ray_intersector, left, min_t, left_hit);
|
||||
if (left_ret && left_hit.t < min_t) {
|
||||
min_t = left_hit.t;
|
||||
hit = left_hit;
|
||||
} else
|
||||
left_ret = false;
|
||||
bool right_ret = intersect_ray_recursive_first_hit(ray_intersector, right, min_t, right_hit);
|
||||
if (right_ret && right_hit.t < min_t)
|
||||
hit = right_hit;
|
||||
else
|
||||
right_ret = false;
|
||||
return left_ret || right_ret;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename RayIntersectorType>
|
||||
static inline void intersect_ray_recursive_all_hits(RayIntersectorType &ray_intersector, size_t node_idx)
|
||||
{
|
||||
using Scalar = typename RayIntersectorType::VectorType::Scalar;
|
||||
|
||||
const auto &node = ray_intersector.tree.node(node_idx);
|
||||
assert(node.is_valid());
|
||||
|
||||
if (! ray_box_intersect_invdir(ray_intersector.origin, ray_intersector.invdir, node.bbox.template cast<Scalar>(),
|
||||
Scalar(0), std::numeric_limits<Scalar>::infinity()))
|
||||
return;
|
||||
|
||||
if (node.is_leaf()) {
|
||||
auto face = ray_intersector.faces[node.idx];
|
||||
double t, u, v;
|
||||
if (intersect_triangle(
|
||||
ray_intersector.origin, ray_intersector.dir,
|
||||
ray_intersector.vertices[face(0)], ray_intersector.vertices[face(1)], ray_intersector.vertices[face(2)],
|
||||
t, u, v, ray_intersector.eps)
|
||||
&& t > 0.) {
|
||||
ray_intersector.hits.emplace_back(igl::Hit{ int(node.idx), -1, float(u), float(v), float(t) });
|
||||
}
|
||||
} else {
|
||||
// Left / right child node index.
|
||||
size_t left = node_idx * 2 + 1;
|
||||
size_t right = left + 1;
|
||||
intersect_ray_recursive_all_hits(ray_intersector, left);
|
||||
intersect_ray_recursive_all_hits(ray_intersector, right);
|
||||
}
|
||||
}
|
||||
|
||||
// Real-time collision detection, Ericson, Chapter 5
|
||||
template<typename Vector>
|
||||
static inline Vector closest_point_to_triangle(const Vector &p, const Vector &a, const Vector &b, const Vector &c)
|
||||
{
|
||||
using Scalar = typename Vector::Scalar;
|
||||
// Check if P in vertex region outside A
|
||||
Vector ab = b - a;
|
||||
Vector ac = c - a;
|
||||
Vector ap = p - a;
|
||||
Scalar d1 = ab.dot(ap);
|
||||
Scalar d2 = ac.dot(ap);
|
||||
if (d1 <= 0 && d2 <= 0)
|
||||
return a;
|
||||
// Check if P in vertex region outside B
|
||||
Vector bp = p - b;
|
||||
Scalar d3 = ab.dot(bp);
|
||||
Scalar d4 = ac.dot(bp);
|
||||
if (d3 >= 0 && d4 <= d3)
|
||||
return b;
|
||||
// Check if P in edge region of AB, if so return projection of P onto AB
|
||||
Scalar vc = d1*d4 - d3*d2;
|
||||
if (a != b && vc <= 0 && d1 >= 0 && d3 <= 0) {
|
||||
Scalar v = d1 / (d1 - d3);
|
||||
return a + v * ab;
|
||||
}
|
||||
// Check if P in vertex region outside C
|
||||
Vector cp = p - c;
|
||||
Scalar d5 = ab.dot(cp);
|
||||
Scalar d6 = ac.dot(cp);
|
||||
if (d6 >= 0 && d5 <= d6)
|
||||
return c;
|
||||
// Check if P in edge region of AC, if so return projection of P onto AC
|
||||
Scalar vb = d5*d2 - d1*d6;
|
||||
if (vb <= 0 && d2 >= 0 && d6 <= 0) {
|
||||
Scalar w = d2 / (d2 - d6);
|
||||
return a + w * ac;
|
||||
}
|
||||
// Check if P in edge region of BC, if so return projection of P onto BC
|
||||
Scalar va = d3*d6 - d5*d4;
|
||||
if (va <= 0 && (d4 - d3) >= 0 && (d5 - d6) >= 0) {
|
||||
Scalar w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
|
||||
return b + w * (c - b);
|
||||
}
|
||||
// P inside face region. Compute Q through its barycentric coordinates (u,v,w)
|
||||
Scalar denom = Scalar(1.0) / (va + vb + vc);
|
||||
Scalar v = vb * denom;
|
||||
Scalar w = vc * denom;
|
||||
return a + ab * v + ac * w; // = u*a + v*b + w*c, u = va * denom = 1.0-v-w
|
||||
};
|
||||
|
||||
|
||||
// Nothing to do with COVID-19 social distancing.
|
||||
template<typename AVertexType, typename AIndexedFaceType, typename ATreeType, typename AVectorType>
|
||||
struct IndexedTriangleSetDistancer {
|
||||
using VertexType = AVertexType;
|
||||
using IndexedFaceType = AIndexedFaceType;
|
||||
using TreeType = ATreeType;
|
||||
using VectorType = AVectorType;
|
||||
using ScalarType = typename VectorType::Scalar;
|
||||
|
||||
const std::vector<VertexType> &vertices;
|
||||
const std::vector<IndexedFaceType> &faces;
|
||||
const TreeType &tree;
|
||||
|
||||
const VectorType origin;
|
||||
|
||||
inline VectorType closest_point_to_origin(size_t primitive_index,
|
||||
ScalarType& squared_distance) const {
|
||||
const auto &triangle = this->faces[primitive_index];
|
||||
VectorType closest_point = closest_point_to_triangle<VectorType>(origin,
|
||||
this->vertices[triangle(0)].template cast<ScalarType>(),
|
||||
this->vertices[triangle(1)].template cast<ScalarType>(),
|
||||
this->vertices[triangle(2)].template cast<ScalarType>());
|
||||
squared_distance = (origin - closest_point).squaredNorm();
|
||||
return closest_point;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename IndexedPrimitivesDistancerType, typename Scalar>
|
||||
static inline Scalar squared_distance_to_indexed_primitives_recursive(
|
||||
IndexedPrimitivesDistancerType &distancer,
|
||||
size_t node_idx,
|
||||
Scalar low_sqr_d,
|
||||
Scalar up_sqr_d,
|
||||
size_t &i,
|
||||
Eigen::PlainObjectBase<typename IndexedPrimitivesDistancerType::VectorType> &c)
|
||||
{
|
||||
using Vector = typename IndexedPrimitivesDistancerType::VectorType;
|
||||
|
||||
if (low_sqr_d > up_sqr_d)
|
||||
return low_sqr_d;
|
||||
|
||||
// Save the best achieved hit.
|
||||
auto set_min = [&i, &c, &up_sqr_d](const Scalar sqr_d_candidate, const size_t i_candidate, const Vector &c_candidate) {
|
||||
if (sqr_d_candidate < up_sqr_d) {
|
||||
i = i_candidate;
|
||||
c = c_candidate;
|
||||
up_sqr_d = sqr_d_candidate;
|
||||
}
|
||||
};
|
||||
|
||||
const auto &node = distancer.tree.node(node_idx);
|
||||
assert(node.is_valid());
|
||||
if (node.is_leaf())
|
||||
{
|
||||
Scalar sqr_dist;
|
||||
Vector c_candidate = distancer.closest_point_to_origin(node.idx, sqr_dist);
|
||||
set_min(sqr_dist, node.idx, c_candidate);
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t left_node_idx = node_idx * 2 + 1;
|
||||
size_t right_node_idx = left_node_idx + 1;
|
||||
const auto &node_left = distancer.tree.node(left_node_idx);
|
||||
const auto &node_right = distancer.tree.node(right_node_idx);
|
||||
assert(node_left.is_valid());
|
||||
assert(node_right.is_valid());
|
||||
|
||||
bool looked_left = false;
|
||||
bool looked_right = false;
|
||||
const auto &look_left = [&]()
|
||||
{
|
||||
size_t i_left;
|
||||
Vector c_left = c;
|
||||
Scalar sqr_d_left = squared_distance_to_indexed_primitives_recursive(distancer, left_node_idx, low_sqr_d, up_sqr_d, i_left, c_left);
|
||||
set_min(sqr_d_left, i_left, c_left);
|
||||
looked_left = true;
|
||||
};
|
||||
const auto &look_right = [&]()
|
||||
{
|
||||
size_t i_right;
|
||||
Vector c_right = c;
|
||||
Scalar sqr_d_right = squared_distance_to_indexed_primitives_recursive(distancer, right_node_idx, low_sqr_d, up_sqr_d, i_right, c_right);
|
||||
set_min(sqr_d_right, i_right, c_right);
|
||||
looked_right = true;
|
||||
};
|
||||
|
||||
// must look left or right if in box
|
||||
using BBoxScalar = typename IndexedPrimitivesDistancerType::TreeType::BoundingBox::Scalar;
|
||||
if (node_left.bbox.contains(distancer.origin.template cast<BBoxScalar>()))
|
||||
look_left();
|
||||
if (node_right.bbox.contains(distancer.origin.template cast<BBoxScalar>()))
|
||||
look_right();
|
||||
// if haven't looked left and could be less than current min, then look
|
||||
Scalar left_up_sqr_d = node_left.bbox.squaredExteriorDistance(distancer.origin);
|
||||
Scalar right_up_sqr_d = node_right.bbox.squaredExteriorDistance(distancer.origin);
|
||||
if (left_up_sqr_d < right_up_sqr_d) {
|
||||
if (! looked_left && left_up_sqr_d < up_sqr_d)
|
||||
look_left();
|
||||
if (! looked_right && right_up_sqr_d < up_sqr_d)
|
||||
look_right();
|
||||
} else {
|
||||
if (! looked_right && right_up_sqr_d < up_sqr_d)
|
||||
look_right();
|
||||
if (! looked_left && left_up_sqr_d < up_sqr_d)
|
||||
look_left();
|
||||
}
|
||||
}
|
||||
return up_sqr_d;
|
||||
}
|
||||
|
||||
template<typename IndexedPrimitivesDistancerType, typename Scalar>
|
||||
static inline void indexed_primitives_within_distance_squared_recurisve(const IndexedPrimitivesDistancerType &distancer,
|
||||
size_t node_idx,
|
||||
Scalar squared_distance_limit,
|
||||
std::vector<size_t> &found_primitives_indices)
|
||||
{
|
||||
const auto &node = distancer.tree.node(node_idx);
|
||||
assert(node.is_valid());
|
||||
if (node.is_leaf()) {
|
||||
Scalar sqr_dist;
|
||||
distancer.closest_point_to_origin(node.idx, sqr_dist);
|
||||
if (sqr_dist < squared_distance_limit) { found_primitives_indices.push_back(node.idx); }
|
||||
} else {
|
||||
size_t left_node_idx = node_idx * 2 + 1;
|
||||
size_t right_node_idx = left_node_idx + 1;
|
||||
const auto &node_left = distancer.tree.node(left_node_idx);
|
||||
const auto &node_right = distancer.tree.node(right_node_idx);
|
||||
assert(node_left.is_valid());
|
||||
assert(node_right.is_valid());
|
||||
|
||||
if (node_left.bbox.squaredExteriorDistance(distancer.origin) < squared_distance_limit) {
|
||||
indexed_primitives_within_distance_squared_recurisve(distancer, left_node_idx, squared_distance_limit,
|
||||
found_primitives_indices);
|
||||
}
|
||||
if (node_right.bbox.squaredExteriorDistance(distancer.origin) < squared_distance_limit) {
|
||||
indexed_primitives_within_distance_squared_recurisve(distancer, right_node_idx, squared_distance_limit,
|
||||
found_primitives_indices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Build a balanced AABB Tree over an indexed triangles set, balancing the tree
|
||||
// on centroids of the triangles.
|
||||
// Epsilon is applied to the bounding boxes of the AABB Tree to cope with numeric inaccuracies
|
||||
// during tree traversal.
|
||||
template<typename VertexType, typename IndexedFaceType>
|
||||
inline Tree<3, typename VertexType::Scalar> build_aabb_tree_over_indexed_triangle_set(
|
||||
// Indexed triangle set - 3D vertices.
|
||||
const std::vector<VertexType> &vertices,
|
||||
// Indexed triangle set - triangular faces, references to vertices.
|
||||
const std::vector<IndexedFaceType> &faces,
|
||||
//FIXME do we want to apply an epsilon?
|
||||
const typename VertexType::Scalar eps = 0)
|
||||
{
|
||||
using TreeType = Tree<3, typename VertexType::Scalar>;
|
||||
// using CoordType = typename TreeType::CoordType;
|
||||
using VectorType = typename TreeType::VectorType;
|
||||
using BoundingBox = typename TreeType::BoundingBox;
|
||||
|
||||
struct InputType {
|
||||
size_t idx() const { return m_idx; }
|
||||
const BoundingBox& bbox() const { return m_bbox; }
|
||||
const VectorType& centroid() const { return m_centroid; }
|
||||
|
||||
size_t m_idx;
|
||||
BoundingBox m_bbox;
|
||||
VectorType m_centroid;
|
||||
};
|
||||
|
||||
std::vector<InputType> input;
|
||||
input.reserve(faces.size());
|
||||
const VectorType veps(eps, eps, eps);
|
||||
for (size_t i = 0; i < faces.size(); ++ i) {
|
||||
const IndexedFaceType &face = faces[i];
|
||||
const VertexType &v1 = vertices[face(0)];
|
||||
const VertexType &v2 = vertices[face(1)];
|
||||
const VertexType &v3 = vertices[face(2)];
|
||||
InputType n;
|
||||
n.m_idx = i;
|
||||
n.m_centroid = (1./3.) * (v1 + v2 + v3);
|
||||
n.m_bbox = BoundingBox(v1, v1);
|
||||
n.m_bbox.extend(v2);
|
||||
n.m_bbox.extend(v3);
|
||||
n.m_bbox.min() -= veps;
|
||||
n.m_bbox.max() += veps;
|
||||
input.emplace_back(n);
|
||||
}
|
||||
|
||||
TreeType out;
|
||||
out.build(std::move(input));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Find a first intersection of a ray with indexed triangle set.
|
||||
// Intersection test is calculated with the accuracy of VectorType::Scalar
|
||||
// even if the triangle mesh and the AABB Tree are built with floats.
|
||||
template<typename VertexType, typename IndexedFaceType, typename TreeType, typename VectorType>
|
||||
inline bool intersect_ray_first_hit(
|
||||
// Indexed triangle set - 3D vertices.
|
||||
const std::vector<VertexType> &vertices,
|
||||
// Indexed triangle set - triangular faces, references to vertices.
|
||||
const std::vector<IndexedFaceType> &faces,
|
||||
// AABBTreeIndirect::Tree over vertices & faces, bounding boxes built with the accuracy of vertices.
|
||||
const TreeType &tree,
|
||||
// Origin of the ray.
|
||||
const VectorType &origin,
|
||||
// Direction of the ray.
|
||||
const VectorType &dir,
|
||||
// First intersection of the ray with the indexed triangle set.
|
||||
igl::Hit &hit,
|
||||
// Epsilon for the ray-triangle intersection, it should be proportional to an average triangle edge length.
|
||||
const double eps = 0.000001)
|
||||
{
|
||||
using Scalar = typename VectorType::Scalar;
|
||||
auto ray_intersector = detail::RayIntersector<VertexType, IndexedFaceType, TreeType, VectorType> {
|
||||
vertices, faces, tree,
|
||||
origin, dir, VectorType(dir.cwiseInverse()),
|
||||
eps
|
||||
};
|
||||
return ! tree.empty() && detail::intersect_ray_recursive_first_hit(
|
||||
ray_intersector, size_t(0), std::numeric_limits<Scalar>::infinity(), hit);
|
||||
}
|
||||
|
||||
// Find all intersections of a ray with indexed triangle set.
|
||||
// Intersection test is calculated with the accuracy of VectorType::Scalar
|
||||
// even if the triangle mesh and the AABB Tree are built with floats.
|
||||
// The output hits are sorted by the ray parameter.
|
||||
// If the ray intersects a shared edge of two triangles, hits for both triangles are returned.
|
||||
template<typename VertexType, typename IndexedFaceType, typename TreeType, typename VectorType>
|
||||
inline bool intersect_ray_all_hits(
|
||||
// Indexed triangle set - 3D vertices.
|
||||
const std::vector<VertexType> &vertices,
|
||||
// Indexed triangle set - triangular faces, references to vertices.
|
||||
const std::vector<IndexedFaceType> &faces,
|
||||
// AABBTreeIndirect::Tree over vertices & faces, bounding boxes built with the accuracy of vertices.
|
||||
const TreeType &tree,
|
||||
// Origin of the ray.
|
||||
const VectorType &origin,
|
||||
// Direction of the ray.
|
||||
const VectorType &dir,
|
||||
// All intersections of the ray with the indexed triangle set, sorted by parameter t.
|
||||
std::vector<igl::Hit> &hits,
|
||||
// Epsilon for the ray-triangle intersection, it should be proportional to an average triangle edge length.
|
||||
const double eps = 0.000001)
|
||||
{
|
||||
auto ray_intersector = detail::RayIntersectorHits<VertexType, IndexedFaceType, TreeType, VectorType> {
|
||||
{ vertices, faces, {tree},
|
||||
origin, dir, VectorType(dir.cwiseInverse()),
|
||||
eps }
|
||||
};
|
||||
if (tree.empty()) {
|
||||
hits.clear();
|
||||
} else {
|
||||
// Reusing the output memory if there is some memory already pre-allocated.
|
||||
ray_intersector.hits = std::move(hits);
|
||||
ray_intersector.hits.clear();
|
||||
ray_intersector.hits.reserve(8);
|
||||
detail::intersect_ray_recursive_all_hits(ray_intersector, 0);
|
||||
hits = std::move(ray_intersector.hits);
|
||||
std::sort(hits.begin(), hits.end(), [](const auto &l, const auto &r) { return l.t < r.t; });
|
||||
}
|
||||
return ! hits.empty();
|
||||
}
|
||||
|
||||
// Finding a closest triangle, its closest point and squared distance to the closest point
|
||||
// on a 3D indexed triangle set using a pre-built AABBTreeIndirect::Tree.
|
||||
// Closest point to triangle test will be performed with the accuracy of VectorType::Scalar
|
||||
// even if the triangle mesh and the AABB Tree are built with floats.
|
||||
// Returns squared distance to the closest point or -1 if the input is empty.
|
||||
template<typename VertexType, typename IndexedFaceType, typename TreeType, typename VectorType>
|
||||
inline typename VectorType::Scalar squared_distance_to_indexed_triangle_set(
|
||||
// Indexed triangle set - 3D vertices.
|
||||
const std::vector<VertexType> &vertices,
|
||||
// Indexed triangle set - triangular faces, references to vertices.
|
||||
const std::vector<IndexedFaceType> &faces,
|
||||
// AABBTreeIndirect::Tree over vertices & faces, bounding boxes built with the accuracy of vertices.
|
||||
const TreeType &tree,
|
||||
// Point to which the closest point on the indexed triangle set is searched for.
|
||||
const VectorType &point,
|
||||
// Index of the closest triangle in faces.
|
||||
size_t &hit_idx_out,
|
||||
// Position of the closest point on the indexed triangle set.
|
||||
Eigen::PlainObjectBase<VectorType> &hit_point_out)
|
||||
{
|
||||
using Scalar = typename VectorType::Scalar;
|
||||
auto distancer = detail::IndexedTriangleSetDistancer<VertexType, IndexedFaceType, TreeType, VectorType>
|
||||
{ vertices, faces, tree, point };
|
||||
return tree.empty() ? Scalar(-1) :
|
||||
detail::squared_distance_to_indexed_primitives_recursive(distancer, size_t(0), Scalar(0), std::numeric_limits<Scalar>::infinity(), hit_idx_out, hit_point_out);
|
||||
}
|
||||
|
||||
// Decides if exists some triangle in defined radius on a 3D indexed triangle set using a pre-built AABBTreeIndirect::Tree.
|
||||
// Closest point to triangle test will be performed with the accuracy of VectorType::Scalar
|
||||
// even if the triangle mesh and the AABB Tree are built with floats.
|
||||
// Returns true if exists some triangle in defined radius, false otherwise.
|
||||
template<typename VertexType, typename IndexedFaceType, typename TreeType, typename VectorType>
|
||||
inline bool is_any_triangle_in_radius(
|
||||
// Indexed triangle set - 3D vertices.
|
||||
const std::vector<VertexType> &vertices,
|
||||
// Indexed triangle set - triangular faces, references to vertices.
|
||||
const std::vector<IndexedFaceType> &faces,
|
||||
// AABBTreeIndirect::Tree over vertices & faces, bounding boxes built with the accuracy of vertices.
|
||||
const TreeType &tree,
|
||||
// Point to which the closest point on the indexed triangle set is searched for.
|
||||
const VectorType &point,
|
||||
//Square of maximum distance in which triangle is searched for
|
||||
typename VectorType::Scalar &max_distance_squared)
|
||||
{
|
||||
using Scalar = typename VectorType::Scalar;
|
||||
auto distancer = detail::IndexedTriangleSetDistancer<VertexType, IndexedFaceType, TreeType, VectorType>
|
||||
{ vertices, faces, tree, point };
|
||||
|
||||
size_t hit_idx;
|
||||
VectorType hit_point = VectorType::Ones() * (NaN<typename VectorType::Scalar>);
|
||||
|
||||
if(tree.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
detail::squared_distance_to_indexed_primitives_recursive(distancer, size_t(0), Scalar(0), max_distance_squared, hit_idx, hit_point);
|
||||
|
||||
return hit_point.allFinite();
|
||||
}
|
||||
|
||||
// Returns all triangles within the given radius limit
|
||||
template<typename VertexType, typename IndexedFaceType, typename TreeType, typename VectorType>
|
||||
inline std::vector<size_t> all_triangles_in_radius(
|
||||
// Indexed triangle set - 3D vertices.
|
||||
const std::vector<VertexType> &vertices,
|
||||
// Indexed triangle set - triangular faces, references to vertices.
|
||||
const std::vector<IndexedFaceType> &faces,
|
||||
// AABBTreeIndirect::Tree over vertices & faces, bounding boxes built with the accuracy of vertices.
|
||||
const TreeType &tree,
|
||||
// Point to which the distances on the indexed triangle set is searched for.
|
||||
const VectorType &point,
|
||||
//Square of maximum distance in which triangles are searched for
|
||||
typename VectorType::Scalar max_distance_squared)
|
||||
{
|
||||
auto distancer = detail::IndexedTriangleSetDistancer<VertexType, IndexedFaceType, TreeType, VectorType>
|
||||
{ vertices, faces, tree, point };
|
||||
|
||||
if(tree.empty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<size_t> found_triangles{};
|
||||
detail::indexed_primitives_within_distance_squared_recurisve(distancer, size_t(0), max_distance_squared, found_triangles);
|
||||
return found_triangles;
|
||||
}
|
||||
|
||||
|
||||
// Traverse the tree and return the index of an entity whose bounding box
|
||||
// contains a given point. Returns size_t(-1) when the point is outside.
|
||||
template<typename TreeType, typename VectorType>
|
||||
void get_candidate_idxs(const TreeType& tree, const VectorType& v, std::vector<size_t>& candidates, size_t node_idx = 0)
|
||||
{
|
||||
if (tree.empty() || ! tree.node(node_idx).bbox.contains(v))
|
||||
return;
|
||||
|
||||
decltype(tree.node(node_idx)) node = tree.node(node_idx);
|
||||
static_assert(std::is_reference<decltype(node)>::value,
|
||||
"Nodes shall be addressed by reference.");
|
||||
assert(node.is_valid());
|
||||
assert(node.bbox.contains(v));
|
||||
|
||||
if (! node.is_leaf()) {
|
||||
if (tree.left_child(node_idx).bbox.contains(v))
|
||||
get_candidate_idxs(tree, v, candidates, tree.left_child_idx(node_idx));
|
||||
if (tree.right_child(node_idx).bbox.contains(v))
|
||||
get_candidate_idxs(tree, v, candidates, tree.right_child_idx(node_idx));
|
||||
} else
|
||||
candidates.push_back(node.idx);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Predicate: need to be specialized for intersections of different geomteries
|
||||
template<class G> struct Intersecting {};
|
||||
|
||||
// Intersection predicate specialization for box-box intersections
|
||||
template<class CoordType, int NumD>
|
||||
struct Intersecting<Eigen::AlignedBox<CoordType, NumD>> {
|
||||
Eigen::AlignedBox<CoordType, NumD> box;
|
||||
|
||||
Intersecting(const Eigen::AlignedBox<CoordType, NumD> &bb): box{bb} {}
|
||||
|
||||
bool operator() (const typename Tree<NumD, CoordType>::Node &node) const
|
||||
{
|
||||
return box.intersects(node.bbox);
|
||||
}
|
||||
};
|
||||
|
||||
template<class G> auto intersecting(const G &g) { return Intersecting<G>{g}; }
|
||||
|
||||
template<class G> struct Within {};
|
||||
|
||||
// Intersection predicate specialization for box-box intersections
|
||||
template<class CoordType, int NumD>
|
||||
struct Within<Eigen::AlignedBox<CoordType, NumD>> {
|
||||
Eigen::AlignedBox<CoordType, NumD> box;
|
||||
|
||||
Within(const Eigen::AlignedBox<CoordType, NumD> &bb): box{bb} {}
|
||||
|
||||
bool operator() (const typename Tree<NumD, CoordType>::Node &node) const
|
||||
{
|
||||
return node.is_leaf() ? box.contains(node.bbox) : box.intersects(node.bbox);
|
||||
}
|
||||
};
|
||||
|
||||
template<class G> auto within(const G &g) { return Within<G>{g}; }
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Returns true in case traversal should continue,
|
||||
// returns false if traversal should stop (for example if the first hit was found).
|
||||
template<int Dims, typename T, typename Pred, typename Fn>
|
||||
bool traverse_recurse(const Tree<Dims, T> &tree,
|
||||
size_t idx,
|
||||
Pred && pred,
|
||||
Fn && callback)
|
||||
{
|
||||
assert(tree.node(idx).is_valid());
|
||||
|
||||
if (!pred(tree.node(idx)))
|
||||
// Continue traversal.
|
||||
return true;
|
||||
|
||||
if (tree.node(idx).is_leaf()) {
|
||||
// Callback returns true to continue traversal, false to stop traversal.
|
||||
return callback(tree.node(idx));
|
||||
} else {
|
||||
|
||||
// call this with left and right node idx:
|
||||
auto trv = [&](size_t idx) -> bool {
|
||||
return traverse_recurse(tree, idx, std::forward<Pred>(pred),
|
||||
std::forward<Fn>(callback));
|
||||
};
|
||||
|
||||
// Left / right child node index.
|
||||
// Returns true if both children allow the traversal to continue.
|
||||
return trv(Tree<Dims, T>::left_child_idx(idx)) &&
|
||||
trv(Tree<Dims, T>::right_child_idx(idx));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Tree traversal with a predicate. Example usage:
|
||||
// traverse(tree, intersecting(QueryBox), [](size_t face_idx) {
|
||||
// /* ... */
|
||||
// });
|
||||
// Callback shall return true to continue traversal, false if it wants to stop traversal, for example if it found the answer.
|
||||
template<int Dims, typename T, typename Predicate, typename Fn>
|
||||
void traverse(const Tree<Dims, T> &tree, Predicate &&pred, Fn &&callback)
|
||||
{
|
||||
if (tree.empty()) return;
|
||||
|
||||
detail::traverse_recurse(tree, size_t(0), std::forward<Predicate>(pred),
|
||||
std::forward<Fn>(callback));
|
||||
}
|
||||
|
||||
} // namespace AABBTreeIndirect
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_AABBTreeIndirect_hpp_ */
|
||||
@@ -0,0 +1,402 @@
|
||||
///|/ Copyright (c) Prusa Research 2022 - 2023 Pavel Mikuš @Godrak
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef SRC_LIBSLIC3R_AABBTREELINES_HPP_
|
||||
#define SRC_LIBSLIC3R_AABBTREELINES_HPP_
|
||||
|
||||
#include "Point.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "libslic3r.h"
|
||||
#include "libslic3r/AABBTreeIndirect.hpp"
|
||||
#include "libslic3r/Line.hpp"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r { namespace AABBTreeLines {
|
||||
|
||||
namespace detail {
|
||||
|
||||
template<typename ALineType, typename ATreeType, typename AVectorType> struct IndexedLinesDistancer
|
||||
{
|
||||
using LineType = ALineType;
|
||||
using TreeType = ATreeType;
|
||||
using VectorType = AVectorType;
|
||||
using ScalarType = typename VectorType::Scalar;
|
||||
|
||||
const std::vector<LineType> &lines;
|
||||
const TreeType &tree;
|
||||
|
||||
const VectorType origin;
|
||||
|
||||
inline VectorType closest_point_to_origin(size_t primitive_index, ScalarType &squared_distance) const
|
||||
{
|
||||
Vec<LineType::Dim, typename LineType::Scalar> nearest_point;
|
||||
const LineType &line = lines[primitive_index];
|
||||
squared_distance = line_alg::distance_to_squared(line, origin.template cast<typename LineType::Scalar>(), &nearest_point);
|
||||
return nearest_point.template cast<ScalarType>();
|
||||
}
|
||||
};
|
||||
|
||||
// returns number of intersections of ray starting in ray_origin and following the specified coordinate line with lines in tree
|
||||
// first number is hits in positive direction of ray, second number hits in negative direction. returns neagtive numbers when ray_origin is
|
||||
// on some line exactly.
|
||||
template<typename LineType, typename TreeType, typename VectorType, int coordinate>
|
||||
inline std::tuple<int, int> coordinate_aligned_ray_hit_count(size_t node_idx,
|
||||
const TreeType &tree,
|
||||
const std::vector<LineType> &lines,
|
||||
const VectorType &ray_origin)
|
||||
{
|
||||
static constexpr int other_coordinate = (coordinate + 1) % 2;
|
||||
using Scalar = typename LineType::Scalar;
|
||||
using Floating = typename std::conditional<std::is_floating_point<Scalar>::value, Scalar, double>::type;
|
||||
const auto &node = tree.node(node_idx);
|
||||
assert(node.is_valid());
|
||||
if (node.is_leaf()) {
|
||||
const LineType &line = lines[node.idx];
|
||||
if (ray_origin[other_coordinate] < std::min(line.a[other_coordinate], line.b[other_coordinate]) ||
|
||||
ray_origin[other_coordinate] >= std::max(line.a[other_coordinate], line.b[other_coordinate])) {
|
||||
// the second inequality is nonsharp for a reason
|
||||
// without it, we may count contour border twice when the lines meet exactly at the spot of intersection. this prevents is
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
Scalar line_max = std::max(line.a[coordinate], line.b[coordinate]);
|
||||
Scalar line_min = std::min(line.a[coordinate], line.b[coordinate]);
|
||||
if (ray_origin[coordinate] > line_max) {
|
||||
return {1, 0};
|
||||
} else if (ray_origin[coordinate] < line_min) {
|
||||
return {0, 1};
|
||||
} else {
|
||||
// find intersection of ray with line
|
||||
// that is when ( line.a + t * (line.b - line.a) )[other_coordinate] == ray_origin[other_coordinate]
|
||||
// t = ray_origin[oc] - line.a[oc] / (line.b[oc] - line.a[oc]);
|
||||
// then we want to get value of intersection[ coordinate]
|
||||
// val_c = line.a[c] + t * (line.b[c] - line.a[c]);
|
||||
// Note that ray and line may overlap, when (line.b[oc] - line.a[oc]) is zero
|
||||
// In that case, we return negative number
|
||||
Floating distance_oc = line.b[other_coordinate] - line.a[other_coordinate];
|
||||
Floating t = (ray_origin[other_coordinate] - line.a[other_coordinate]) / distance_oc;
|
||||
Floating val_c = line.a[coordinate] + t * (line.b[coordinate] - line.a[coordinate]);
|
||||
if (ray_origin[coordinate] > val_c) {
|
||||
return {1, 0};
|
||||
} else if (ray_origin[coordinate] < val_c) {
|
||||
return {0, 1};
|
||||
} else { // ray origin is on boundary
|
||||
return {-1, -1};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int intersections_above = 0;
|
||||
int intersections_below = 0;
|
||||
size_t left_node_idx = node_idx * 2 + 1;
|
||||
size_t right_node_idx = left_node_idx + 1;
|
||||
const auto &node_left = tree.node(left_node_idx);
|
||||
const auto &node_right = tree.node(right_node_idx);
|
||||
assert(node_left.is_valid());
|
||||
assert(node_right.is_valid());
|
||||
|
||||
if (node_left.bbox.min()[other_coordinate] <= ray_origin[other_coordinate] &&
|
||||
node_left.bbox.max()[other_coordinate] >=
|
||||
ray_origin[other_coordinate]) {
|
||||
auto [above, below] = coordinate_aligned_ray_hit_count<LineType, TreeType, VectorType, coordinate>(left_node_idx, tree, lines,
|
||||
ray_origin);
|
||||
if (above < 0 || below < 0) return {-1, -1};
|
||||
intersections_above += above;
|
||||
intersections_below += below;
|
||||
}
|
||||
|
||||
if (node_right.bbox.min()[other_coordinate] <= ray_origin[other_coordinate] &&
|
||||
node_right.bbox.max()[other_coordinate] >= ray_origin[other_coordinate]) {
|
||||
auto [above, below] = coordinate_aligned_ray_hit_count<LineType, TreeType, VectorType, coordinate>(right_node_idx, tree, lines,
|
||||
ray_origin);
|
||||
if (above < 0 || below < 0) return {-1, -1};
|
||||
intersections_above += above;
|
||||
intersections_below += below;
|
||||
}
|
||||
return {intersections_above, intersections_below};
|
||||
}
|
||||
}
|
||||
|
||||
template<typename LineType, typename TreeType, typename VectorType>
|
||||
inline void insert_intersections_with_line(std::vector<std::pair<VectorType, size_t>> &result,
|
||||
size_t node_idx,
|
||||
const TreeType &tree,
|
||||
const std::vector<LineType> &lines,
|
||||
const LineType &line,
|
||||
const typename TreeType::BoundingBox &line_bb)
|
||||
{
|
||||
const auto &node = tree.node(node_idx);
|
||||
assert(node.is_valid());
|
||||
if (node.is_leaf()) {
|
||||
VectorType intersection_pt;
|
||||
if (line_alg::intersection(line, lines[node.idx], &intersection_pt)) {
|
||||
result.emplace_back(intersection_pt, node.idx);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
size_t left_node_idx = node_idx * 2 + 1;
|
||||
size_t right_node_idx = left_node_idx + 1;
|
||||
const auto &node_left = tree.node(left_node_idx);
|
||||
const auto &node_right = tree.node(right_node_idx);
|
||||
assert(node_left.is_valid());
|
||||
assert(node_right.is_valid());
|
||||
|
||||
if (node_left.bbox.intersects(line_bb)) {
|
||||
insert_intersections_with_line<LineType, TreeType, VectorType>(result, left_node_idx, tree, lines, line, line_bb);
|
||||
}
|
||||
|
||||
if (node_right.bbox.intersects(line_bb)) {
|
||||
insert_intersections_with_line<LineType, TreeType, VectorType>(result, right_node_idx, tree, lines, line, line_bb);
|
||||
}
|
||||
|
||||
//// NOTE: Non recursive implementation - for my case was slower ;-(
|
||||
// std::vector<size_t> node_indicies_for_check; // evaluation queue
|
||||
// size_t approx_size = static_cast<size_t>(std::ceil(std::sqrt(tree.nodes().size())));
|
||||
// node_indicies_for_check.reserve(approx_size);
|
||||
// do {
|
||||
// const auto &node = tree.node(node_index);
|
||||
// assert(node.is_valid());
|
||||
// if (node.is_leaf()) {
|
||||
// VectorType intersection_pt;
|
||||
// if (line_alg::intersection(line, lines[node.idx], &intersection_pt))
|
||||
// result.emplace_back(intersection_pt, node.idx);
|
||||
// node_index = 0;// clear next node
|
||||
// } else {
|
||||
// size_t left_node_idx = node_index * 2 + 1;
|
||||
// size_t right_node_idx = left_node_idx + 1;
|
||||
// const auto &node_left = tree.node(left_node_idx);
|
||||
// const auto &node_right = tree.node(right_node_idx);
|
||||
// assert(node_left.is_valid());
|
||||
// assert(node_right.is_valid());
|
||||
|
||||
// // Set next node index
|
||||
// node_index = 0; // clear next node
|
||||
// if (node_left.bbox.intersects(line_bb))
|
||||
// node_index = left_node_idx;
|
||||
|
||||
// if (node_right.bbox.intersects(line_bb)) {
|
||||
// if (node_index == 0)
|
||||
// node_index = right_node_idx;
|
||||
// else
|
||||
// node_indicies_for_check.push_back(right_node_idx); // store for later evaluation
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (node_index == 0 && !node_indicies_for_check.empty()) {
|
||||
// // no direct next node take one from queue
|
||||
// node_index = node_indicies_for_check.back();
|
||||
// node_indicies_for_check.pop_back();
|
||||
// }
|
||||
//} while (node_index != 0);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
// Build a balanced AABB Tree over a vector of lines, balancing the tree
|
||||
// on centroids of the lines.
|
||||
// Epsilon is applied to the bounding boxes of the AABB Tree to cope with numeric inaccuracies
|
||||
// during tree traversal.
|
||||
template<typename LineType>
|
||||
inline AABBTreeIndirect::Tree<LineType::Dim, typename LineType::Scalar> build_aabb_tree_over_indexed_lines(const std::vector<LineType> &lines)
|
||||
{
|
||||
using TreeType = AABBTreeIndirect::Tree<LineType::Dim, typename LineType::Scalar>;
|
||||
// using CoordType = typename TreeType::CoordType;
|
||||
using VectorType = typename TreeType::VectorType;
|
||||
using BoundingBox = typename TreeType::BoundingBox;
|
||||
|
||||
struct InputType
|
||||
{
|
||||
size_t idx() const { return m_idx; }
|
||||
const BoundingBox &bbox() const { return m_bbox; }
|
||||
const VectorType ¢roid() const { return m_centroid; }
|
||||
|
||||
size_t m_idx;
|
||||
BoundingBox m_bbox;
|
||||
VectorType m_centroid;
|
||||
};
|
||||
|
||||
std::vector<InputType> input;
|
||||
input.reserve(lines.size());
|
||||
for (size_t i = 0; i < lines.size(); ++i) {
|
||||
const LineType &line = lines[i];
|
||||
InputType n;
|
||||
n.m_idx = i;
|
||||
n.m_centroid = (line.a + line.b) * 0.5;
|
||||
n.m_bbox = BoundingBox(line.a, line.a);
|
||||
n.m_bbox.extend(line.b);
|
||||
input.emplace_back(n);
|
||||
}
|
||||
|
||||
TreeType out;
|
||||
out.build(std::move(input));
|
||||
return out;
|
||||
}
|
||||
|
||||
// Finding a closest line, its closest point and squared distance to the closest point
|
||||
// Returns squared distance to the closest point or -1 if the input is empty.
|
||||
// or no closer point than max_sq_dist
|
||||
template<typename LineType, typename TreeType, typename VectorType>
|
||||
inline typename VectorType::Scalar squared_distance_to_indexed_lines(
|
||||
const std::vector<LineType> &lines,
|
||||
const TreeType &tree,
|
||||
const VectorType &point,
|
||||
size_t &hit_idx_out,
|
||||
Eigen::PlainObjectBase<VectorType> &hit_point_out,
|
||||
typename VectorType::Scalar max_sqr_dist = std::numeric_limits<typename VectorType::Scalar>::infinity())
|
||||
{
|
||||
using Scalar = typename VectorType::Scalar;
|
||||
if (tree.empty()) return Scalar(-1);
|
||||
auto distancer = detail::IndexedLinesDistancer<LineType, TreeType, VectorType>{lines, tree, point};
|
||||
return AABBTreeIndirect::detail::squared_distance_to_indexed_primitives_recursive(distancer, size_t(0), Scalar(0), max_sqr_dist,
|
||||
hit_idx_out, hit_point_out);
|
||||
}
|
||||
|
||||
// Returns all lines within the given radius limit
|
||||
template<typename LineType, typename TreeType, typename VectorType>
|
||||
inline std::vector<size_t> all_lines_in_radius(const std::vector<LineType> &lines,
|
||||
const TreeType &tree,
|
||||
const VectorType &point,
|
||||
typename VectorType::Scalar max_distance_squared)
|
||||
{
|
||||
auto distancer = detail::IndexedLinesDistancer<LineType, TreeType, VectorType>{lines, tree, point};
|
||||
|
||||
if (tree.empty()) { return {}; }
|
||||
|
||||
std::vector<size_t> found_lines{};
|
||||
AABBTreeIndirect::detail::indexed_primitives_within_distance_squared_recurisve(distancer, size_t(0), max_distance_squared, found_lines);
|
||||
return found_lines;
|
||||
}
|
||||
|
||||
// return 1 if true, -1 if false, 0 for point on contour (or if cannot be determined)
|
||||
template<typename LineType, typename TreeType, typename VectorType>
|
||||
inline int point_outside_closed_contours(const std::vector<LineType> &lines, const TreeType &tree, const VectorType &point)
|
||||
{
|
||||
if (tree.empty()) { return 1; }
|
||||
|
||||
auto [hits_above, hits_below] = detail::coordinate_aligned_ray_hit_count<LineType, TreeType, VectorType, 0>(0, tree, lines, point);
|
||||
if (hits_above < 0 || hits_below < 0) {
|
||||
return 0;
|
||||
} else if (hits_above % 2 == 1 && hits_below % 2 == 1) {
|
||||
return -1;
|
||||
} else if (hits_above % 2 == 0 && hits_below % 2 == 0) {
|
||||
return 1;
|
||||
} else { // this should not happen with closed contours. lets check it in Y direction
|
||||
auto [hits_above, hits_below] = detail::coordinate_aligned_ray_hit_count<LineType, TreeType, VectorType, 1>(0, tree, lines, point);
|
||||
if (hits_above < 0 || hits_below < 0) {
|
||||
return 0;
|
||||
} else if (hits_above % 2 == 1 && hits_below % 2 == 1) {
|
||||
return -1;
|
||||
} else if (hits_above % 2 == 0 && hits_below % 2 == 0) {
|
||||
return 1;
|
||||
} else { // both results were unclear
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<bool sorted, typename VectorType, typename LineType, typename TreeType>
|
||||
inline std::vector<std::pair<VectorType, size_t>> get_intersections_with_line(const std::vector<LineType> &lines,
|
||||
const TreeType &tree,
|
||||
const LineType &line)
|
||||
{
|
||||
if (tree.empty()) {
|
||||
return {};
|
||||
}
|
||||
auto line_bb = typename TreeType::BoundingBox(line.a, line.a);
|
||||
line_bb.extend(line.b);
|
||||
|
||||
std::vector<std::pair<VectorType, size_t>> intersections; // result
|
||||
detail::insert_intersections_with_line(intersections, 0, tree, lines, line, line_bb);
|
||||
|
||||
if (sorted) {
|
||||
using Floating =
|
||||
typename std::conditional<std::is_floating_point<typename LineType::Scalar>::value, typename LineType::Scalar, double>::type;
|
||||
|
||||
std::vector<std::pair<Floating, std::pair<VectorType, size_t>>> points_with_sq_distance{};
|
||||
for (const auto &p : intersections) {
|
||||
points_with_sq_distance.emplace_back((p.first - line.a).template cast<Floating>().squaredNorm(), p);
|
||||
}
|
||||
std::sort(points_with_sq_distance.begin(), points_with_sq_distance.end(),
|
||||
[](const std::pair<Floating, std::pair<VectorType, size_t>> &left,
|
||||
std::pair<Floating, std::pair<VectorType, size_t>> &right) { return left.first < right.first; });
|
||||
for (size_t i = 0; i < points_with_sq_distance.size(); i++) {
|
||||
intersections[i] = points_with_sq_distance[i].second;
|
||||
}
|
||||
}
|
||||
return intersections;
|
||||
}
|
||||
|
||||
template<typename LineType> class LinesDistancer
|
||||
{
|
||||
public:
|
||||
using Scalar = typename LineType::Scalar;
|
||||
using Floating = typename std::conditional<std::is_floating_point<Scalar>::value, Scalar, double>::type;
|
||||
private:
|
||||
std::vector<LineType> lines;
|
||||
AABBTreeIndirect::Tree<2, Scalar> tree;
|
||||
|
||||
public:
|
||||
explicit LinesDistancer(const std::vector<LineType> &lines) : lines(lines)
|
||||
{
|
||||
tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(this->lines);
|
||||
}
|
||||
|
||||
explicit LinesDistancer(std::vector<LineType> &&lines) : lines(lines)
|
||||
{
|
||||
tree = AABBTreeLines::build_aabb_tree_over_indexed_lines(this->lines);
|
||||
}
|
||||
|
||||
LinesDistancer() = default;
|
||||
|
||||
// 1 true, -1 false, 0 cannot determine
|
||||
int outside(const Vec<2, Scalar> &point) const { return point_outside_closed_contours(lines, tree, point); }
|
||||
|
||||
// negative sign means inside
|
||||
template<bool SIGNED_DISTANCE>
|
||||
std::tuple<Floating, size_t, Vec<2, Floating>> distance_from_lines_extra(const Vec<2, Scalar> &point) const
|
||||
{
|
||||
size_t nearest_line_index_out = size_t(-1);
|
||||
Vec<2, Floating> nearest_point_out = Vec<2, Floating>::Zero();
|
||||
Vec<2, Floating> p = point.template cast<Floating>();
|
||||
auto distance = AABBTreeLines::squared_distance_to_indexed_lines(lines, tree, p, nearest_line_index_out, nearest_point_out);
|
||||
|
||||
if (distance < 0) {
|
||||
return {std::numeric_limits<Floating>::infinity(), nearest_line_index_out, nearest_point_out};
|
||||
}
|
||||
distance = sqrt(distance);
|
||||
|
||||
if (SIGNED_DISTANCE) {
|
||||
distance *= outside(point);
|
||||
}
|
||||
|
||||
return {distance, nearest_line_index_out, nearest_point_out};
|
||||
}
|
||||
|
||||
template<bool SIGNED_DISTANCE> Floating distance_from_lines(const Vec<2, Scalar> &point) const
|
||||
{
|
||||
auto [dist, idx, np] = distance_from_lines_extra<SIGNED_DISTANCE>(point);
|
||||
return dist;
|
||||
}
|
||||
|
||||
std::vector<size_t> all_lines_in_radius(const Vec<2, Scalar> &point, Floating radius) const
|
||||
{
|
||||
return AABBTreeLines::all_lines_in_radius(this->lines, this->tree, point.template cast<Floating>(), radius * radius);
|
||||
}
|
||||
|
||||
template<bool sorted> std::vector<std::pair<Vec<2, Scalar>, size_t>> intersections_with_line(const LineType &line) const
|
||||
{
|
||||
return get_intersections_with_line<sorted, Vec<2, Scalar>>(lines, tree, line);
|
||||
}
|
||||
|
||||
const LineType &get_line(size_t line_idx) const { return lines[line_idx]; }
|
||||
|
||||
const std::vector<LineType> &get_lines() const { return lines; }
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::AABBTreeLines
|
||||
|
||||
#endif /* SRC_LIBSLIC3R_AABBTREELINES_HPP_ */
|
||||
@@ -0,0 +1,197 @@
|
||||
///|/ Copyright (c) Prusa Research 2022 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ASTAR_HPP
|
||||
#define ASTAR_HPP
|
||||
|
||||
#include <cmath> // std::isinf() is here
|
||||
#include <unordered_map>
|
||||
|
||||
#include "libslic3r/MutablePriorityQueue.hpp"
|
||||
|
||||
namespace Slic3r { namespace astar {
|
||||
|
||||
// Borrowed from C++20
|
||||
template<class T>
|
||||
using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;
|
||||
|
||||
// Input interface for the Astar algorithm. Specialize this struct for a
|
||||
// particular type and implement all the 4 methods and specify the Node type
|
||||
// to register the new type for the astar implementation.
|
||||
template<class T> struct TracerTraits_
|
||||
{
|
||||
// The type of a node used by this tracer. Usually a point in space.
|
||||
using Node = typename T::Node;
|
||||
|
||||
// Call fn for every new node reachable from node 'src'. fn should have the
|
||||
// candidate node as its only argument.
|
||||
template<class Fn>
|
||||
static void foreach_reachable(const T &tracer, const Node &src, Fn &&fn)
|
||||
{
|
||||
tracer.foreach_reachable(src, fn);
|
||||
}
|
||||
|
||||
// Get the distance from node 'a' to node 'b'. This is sometimes referred
|
||||
// to as the g value of a node in AStar context.
|
||||
static float distance(const T &tracer, const Node &a, const Node &b)
|
||||
{
|
||||
return tracer.distance(a, b);
|
||||
}
|
||||
|
||||
// Get the estimated distance heuristic from node 'n' to the destination.
|
||||
// This is referred to as the h value in AStar context.
|
||||
// If node 'n' is the goal, this function should return a negative value.
|
||||
// Note that this heuristic should be admissible (never bigger than the real
|
||||
// cost) in order for Astar to work.
|
||||
static float goal_heuristic(const T &tracer, const Node &n)
|
||||
{
|
||||
return tracer.goal_heuristic(n);
|
||||
}
|
||||
|
||||
// Return a unique identifier (hash) for node 'n'.
|
||||
static size_t unique_id(const T &tracer, const Node &n)
|
||||
{
|
||||
return tracer.unique_id(n);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper definition to get the node type of a tracer
|
||||
template<class T>
|
||||
using TracerNodeT = typename TracerTraits_<remove_cvref_t<T>>::Node;
|
||||
|
||||
constexpr auto Unassigned = std::numeric_limits<size_t>::max();
|
||||
|
||||
template<class Tracer>
|
||||
struct QNode // Queue node. Keeps track of scores g, and h
|
||||
{
|
||||
TracerNodeT<Tracer> node; // The actual node itself
|
||||
size_t queue_id; // Position in the open queue or Unassigned if closed
|
||||
size_t parent; // unique id of the parent or Unassigned
|
||||
|
||||
float g, h;
|
||||
float f() const { return g + h; }
|
||||
|
||||
QNode(TracerNodeT<Tracer> n = {},
|
||||
size_t p = Unassigned,
|
||||
float gval = std::numeric_limits<float>::infinity(),
|
||||
float hval = 0.f)
|
||||
: node{std::move(n)}
|
||||
, parent{p}
|
||||
, queue_id{InvalidQueueID}
|
||||
, g{gval}
|
||||
, h{hval}
|
||||
{}
|
||||
};
|
||||
|
||||
// Run the AStar algorithm on a tracer implementation.
|
||||
// The 'tracer' argument encapsulates the domain (grid, point cloud, etc...)
|
||||
// The 'source' argument is the starting node.
|
||||
// The 'out' argument is the output iterator into which the output nodes are
|
||||
// written. For performance reasons, the order is reverse, from the destination
|
||||
// to the source -- (destination included, source is not).
|
||||
// The 'cached_nodes' argument is an optional associative container to hold a
|
||||
// QNode entry for each visited node. Any compatible container can be used
|
||||
// (like std::map or maps with different allocators, even a sufficiently large
|
||||
// std::vector).
|
||||
//
|
||||
// Note that no destination node is given in the signature. The tracer's
|
||||
// goal_heuristic() method should return a negative value if a node is a
|
||||
// destination node.
|
||||
template<class Tracer,
|
||||
class It,
|
||||
class NodeMap = std::unordered_map<size_t, QNode<Tracer>>>
|
||||
bool search_route(const Tracer &tracer,
|
||||
const TracerNodeT<Tracer> &source,
|
||||
It out,
|
||||
NodeMap &&cached_nodes = {})
|
||||
{
|
||||
using Node = TracerNodeT<Tracer>;
|
||||
using QNode = QNode<Tracer>;
|
||||
using TracerTraits = TracerTraits_<remove_cvref_t<Tracer>>;
|
||||
|
||||
struct LessPred { // Comparison functor needed by the priority queue
|
||||
NodeMap &m;
|
||||
bool operator ()(size_t node_a, size_t node_b) {
|
||||
return m[node_a].f() < m[node_b].f();
|
||||
}
|
||||
};
|
||||
|
||||
auto qopen = make_mutable_priority_queue<size_t, true>(
|
||||
[&cached_nodes](size_t el, size_t qidx) {
|
||||
cached_nodes[el].queue_id = qidx;
|
||||
},
|
||||
LessPred{cached_nodes});
|
||||
|
||||
QNode initial{source, /*parent = */ Unassigned, /*g = */0.f};
|
||||
size_t source_id = TracerTraits::unique_id(tracer, source);
|
||||
cached_nodes[source_id] = initial;
|
||||
qopen.push(source_id);
|
||||
|
||||
size_t goal_id = TracerTraits::goal_heuristic(tracer, source) < 0.f ?
|
||||
source_id :
|
||||
Unassigned;
|
||||
|
||||
while (goal_id == Unassigned && !qopen.empty()) {
|
||||
size_t q_id = qopen.top();
|
||||
qopen.pop();
|
||||
QNode &q = cached_nodes[q_id];
|
||||
|
||||
// This should absolutely be initialized in the cache already
|
||||
assert(!std::isinf(q.g));
|
||||
|
||||
TracerTraits::foreach_reachable(tracer, q.node, [&](const Node &succ_nd) {
|
||||
if (goal_id != Unassigned)
|
||||
return true;
|
||||
|
||||
float h = TracerTraits::goal_heuristic(tracer, succ_nd);
|
||||
float dst = TracerTraits::distance(tracer, q.node, succ_nd);
|
||||
size_t succ_id = TracerTraits::unique_id(tracer, succ_nd);
|
||||
QNode qsucc_nd{succ_nd, q_id, q.g + dst, h};
|
||||
|
||||
if (h < 0.f) {
|
||||
goal_id = succ_id;
|
||||
cached_nodes[succ_id] = qsucc_nd;
|
||||
} else {
|
||||
// If succ_id is not in cache, it gets created with g = infinity
|
||||
QNode &prev_nd = cached_nodes[succ_id];
|
||||
|
||||
if (qsucc_nd.g < prev_nd.g) {
|
||||
// new route is better, apply it:
|
||||
|
||||
// Save the old queue id, it would be lost after the next line
|
||||
size_t queue_id = prev_nd.queue_id;
|
||||
|
||||
// The cache needs to be updated either way
|
||||
prev_nd = qsucc_nd;
|
||||
|
||||
if (queue_id == InvalidQueueID)
|
||||
// was in closed or unqueued, rescheduling
|
||||
qopen.push(succ_id);
|
||||
else // was in open, updating
|
||||
qopen.update(queue_id);
|
||||
}
|
||||
}
|
||||
|
||||
return goal_id != Unassigned;
|
||||
});
|
||||
}
|
||||
|
||||
// Write the output, do not reverse. Clients can do so if they need to.
|
||||
if (goal_id != Unassigned) {
|
||||
const QNode *q = &cached_nodes[goal_id];
|
||||
while (q->parent != Unassigned) {
|
||||
assert(!std::isinf(q->g)); // Uninitialized nodes are NOT allowed
|
||||
|
||||
*out = q->node;
|
||||
++out;
|
||||
q = &cached_nodes[q->parent];
|
||||
}
|
||||
}
|
||||
|
||||
return goal_id != Unassigned;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::astar
|
||||
|
||||
#endif // ASTAR_HPP
|
||||
@@ -0,0 +1,132 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Pavel Mikuš @Godrak
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef SRC_LIBSLIC3R_PATH_SORTING_HPP_
|
||||
#define SRC_LIBSLIC3R_PATH_SORTING_HPP_
|
||||
|
||||
#include "AABBTreeLines.hpp"
|
||||
#include "BoundingBox.hpp"
|
||||
#include "Line.hpp"
|
||||
#include "ankerl/unordered_dense.h"
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <libslic3r/Point.hpp>
|
||||
#include <libslic3r/Polygon.hpp>
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace Algorithm {
|
||||
|
||||
//Sorts the paths such that all paths between begin and last_seed are printed first, in some order. The rest of the paths is sorted
|
||||
// such that the paths that are touching some of the already printed are printed first, sorted secondary by the distance to the last point of the last
|
||||
// printed path.
|
||||
// begin, end, and last_seed are random access iterators. touch_limit_distance is used to check if the paths are touching - if any part of the path gets this close
|
||||
// to the second, then they touch.
|
||||
// convert_to_lines is a lambda that should accept the path as argument and return it as Lines vector, in correct order.
|
||||
template<typename RandomAccessIterator, typename ToLines>
|
||||
void sort_paths(RandomAccessIterator begin, RandomAccessIterator end, Point start, double touch_limit_distance, ToLines convert_to_lines)
|
||||
{
|
||||
size_t paths_count = std::distance(begin, end);
|
||||
if (paths_count <= 1)
|
||||
return;
|
||||
|
||||
auto paths_touch = [touch_limit_distance](const AABBTreeLines::LinesDistancer<Line> &left,
|
||||
const AABBTreeLines::LinesDistancer<Line> &right) {
|
||||
for (const Line &l : left.get_lines()) {
|
||||
if (right.distance_from_lines<false>(l.a) < touch_limit_distance) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (right.distance_from_lines<false>(left.get_lines().back().b) < touch_limit_distance) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const Line &l : right.get_lines()) {
|
||||
if (left.distance_from_lines<false>(l.a) < touch_limit_distance) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (left.distance_from_lines<false>(right.get_lines().back().b) < touch_limit_distance) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
std::vector<AABBTreeLines::LinesDistancer<Line>> distancers(paths_count);
|
||||
for (size_t path_idx = 0; path_idx < paths_count; path_idx++) {
|
||||
distancers[path_idx] = AABBTreeLines::LinesDistancer<Line>{convert_to_lines(*std::next(begin, path_idx))};
|
||||
}
|
||||
|
||||
std::vector<std::unordered_set<size_t>> dependencies(paths_count);
|
||||
for (size_t path_idx = 0; path_idx < paths_count; path_idx++) {
|
||||
for (size_t next_path_idx = path_idx + 1; next_path_idx < paths_count; next_path_idx++) {
|
||||
if (paths_touch(distancers[path_idx], distancers[next_path_idx])) {
|
||||
dependencies[next_path_idx].insert(path_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Point current_point = start;
|
||||
|
||||
std::vector<std::pair<size_t, bool>> correct_order_and_direction(paths_count);
|
||||
size_t unsorted_idx = 0;
|
||||
size_t null_idx = size_t(-1);
|
||||
size_t next_idx = null_idx;
|
||||
bool reverse = false;
|
||||
while (unsorted_idx < paths_count) {
|
||||
next_idx = null_idx;
|
||||
double lines_dist = std::numeric_limits<double>::max();
|
||||
for (size_t path_idx = 0; path_idx < paths_count; path_idx++) {
|
||||
if (!dependencies[path_idx].empty())
|
||||
continue;
|
||||
|
||||
double ldist = distancers[path_idx].distance_from_lines<false>(current_point);
|
||||
if (ldist < lines_dist) {
|
||||
const auto &lines = distancers[path_idx].get_lines();
|
||||
double dist_a = (lines.front().a - current_point).cast<double>().squaredNorm();
|
||||
double dist_b = (lines.back().b - current_point).cast<double>().squaredNorm();
|
||||
next_idx = path_idx;
|
||||
reverse = dist_b < dist_a;
|
||||
lines_dist = ldist;
|
||||
}
|
||||
}
|
||||
|
||||
// we have valid next_idx, sort it, update dependencies, update current point
|
||||
correct_order_and_direction[next_idx] = {unsorted_idx, reverse};
|
||||
unsorted_idx++;
|
||||
current_point = reverse ? distancers[next_idx].get_lines().front().a : distancers[next_idx].get_lines().back().b;
|
||||
|
||||
dependencies[next_idx].insert(null_idx); // prevent it from being selected again
|
||||
for (size_t path_idx = 0; path_idx < paths_count; path_idx++) {
|
||||
dependencies[path_idx].erase(next_idx);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t path_idx = 0; path_idx < paths_count; path_idx++) {
|
||||
if (correct_order_and_direction[path_idx].second) {
|
||||
std::next(begin, path_idx)->reverse();
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < correct_order_and_direction.size() - 1; i++) {
|
||||
bool swapped = false;
|
||||
for (size_t j = 0; j < correct_order_and_direction.size() - i - 1; j++) {
|
||||
if (correct_order_and_direction[j].first > correct_order_and_direction[j + 1].first) {
|
||||
std::swap(correct_order_and_direction[j], correct_order_and_direction[j + 1]);
|
||||
std::iter_swap(std::next(begin, j), std::next(begin, j + 1));
|
||||
swapped = true;
|
||||
}
|
||||
}
|
||||
if (swapped == false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::Algorithm
|
||||
|
||||
#endif /*SRC_LIBSLIC3R_PATH_SORTING_HPP_*/
|
||||
@@ -0,0 +1,562 @@
|
||||
///|/ Copyright (c) Prusa Research 2022 - 2023 Vojtěch Bubník @bubnikv
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "RegionExpansion.hpp"
|
||||
|
||||
#include <libslic3r/AABBTreeIndirect.hpp>
|
||||
#include <libslic3r/ClipperZUtils.hpp>
|
||||
#include <libslic3r/ClipperUtils.hpp>
|
||||
#include <libslic3r/Utils.hpp>
|
||||
|
||||
#include <numeric>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace Algorithm {
|
||||
|
||||
// Calculating radius discretization according to ClipperLib offsetter code, see void ClipperOffset::DoOffset(double delta)
|
||||
inline double clipper_round_offset_error(double offset, double arc_tolerance)
|
||||
{
|
||||
static constexpr const double def_arc_tolerance = 0.25;
|
||||
const double y =
|
||||
arc_tolerance <= 0 ?
|
||||
def_arc_tolerance :
|
||||
arc_tolerance > offset * def_arc_tolerance ?
|
||||
offset * def_arc_tolerance :
|
||||
arc_tolerance;
|
||||
double steps = std::min(M_PI / std::acos(1. - y / offset), offset * M_PI);
|
||||
return offset * (1. - cos(M_PI / steps));
|
||||
}
|
||||
|
||||
RegionExpansionParameters RegionExpansionParameters::build(
|
||||
// Scaled expansion value
|
||||
float full_expansion,
|
||||
// Expand by waves of expansion_step size (expansion_step is scaled).
|
||||
float expansion_step,
|
||||
// Don't take more than max_nr_steps for small expansion_step.
|
||||
size_t max_nr_expansion_steps)
|
||||
{
|
||||
assert(full_expansion > 0);
|
||||
assert(expansion_step > 0);
|
||||
assert(max_nr_expansion_steps > 0);
|
||||
|
||||
RegionExpansionParameters out;
|
||||
// Initial expansion of src to make the source regions intersect with boundary regions just a bit.
|
||||
// The expansion should not be too tiny, but also small enough, so the following expansion will
|
||||
// compensate for tiny_expansion and bring the wave back to the boundary without producing
|
||||
// ugly cusps where it touches the boundary.
|
||||
out.tiny_expansion = std::min(0.25f * full_expansion, scaled<float>(0.05f));
|
||||
size_t nsteps = size_t(ceil((full_expansion - out.tiny_expansion) / expansion_step));
|
||||
if (max_nr_expansion_steps > 0)
|
||||
nsteps = std::min(nsteps, max_nr_expansion_steps);
|
||||
assert(nsteps > 0);
|
||||
out.initial_step = (full_expansion - out.tiny_expansion) / nsteps;
|
||||
if (nsteps > 1 && 0.25 * out.initial_step < out.tiny_expansion) {
|
||||
// Decrease the step size by lowering number of steps.
|
||||
nsteps = std::max<size_t>(1, (floor((full_expansion - out.tiny_expansion) / (4. * out.tiny_expansion))));
|
||||
out.initial_step = (full_expansion - out.tiny_expansion) / nsteps;
|
||||
}
|
||||
if (0.25 * out.initial_step < out.tiny_expansion || nsteps == 1) {
|
||||
out.tiny_expansion = 0.2f * full_expansion;
|
||||
out.initial_step = 0.8f * full_expansion;
|
||||
}
|
||||
out.other_step = out.initial_step;
|
||||
out.num_other_steps = nsteps - 1;
|
||||
|
||||
// Accuracy of the offsetter for wave propagation.
|
||||
out.arc_tolerance = scaled<double>(0.1);
|
||||
out.shortest_edge_length = out.initial_step * ClipperOffsetShortestEdgeFactor;
|
||||
|
||||
// Maximum inflation of seed contours over the boundary. Used to trim boundary to speed up
|
||||
// clipping during wave propagation. Needs to be in sync with the offsetter accuracy.
|
||||
// Clipper positive round offset should rather offset less than more.
|
||||
// Still a little bit of additional offset was added.
|
||||
out.max_inflation = (out.tiny_expansion + nsteps * out.initial_step) * 1.1;
|
||||
// (clipper_round_offset_error(out.tiny_expansion, co.ArcTolerance) + nsteps * clipper_round_offset_error(out.initial_step, co.ArcTolerance) * 1.5; // Account for uncertainty
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
// similar to expolygons_to_zpaths(), but each contour is expanded before converted to zpath.
|
||||
// The expanded contours are then opened (the first point is repeated at the end).
|
||||
static ClipperLib_Z::Paths expolygons_to_zpaths_expanded_opened(
|
||||
const ExPolygons &src, const float expansion, coord_t &base_idx)
|
||||
{
|
||||
ClipperLib_Z::Paths out;
|
||||
out.reserve(2 * std::accumulate(src.begin(), src.end(), size_t(0),
|
||||
[](const size_t acc, const ExPolygon &expoly) { return acc + expoly.num_contours(); }));
|
||||
ClipperLib::ClipperOffset offsetter;
|
||||
offsetter.ShortestEdgeLength = expansion * ClipperOffsetShortestEdgeFactor;
|
||||
ClipperLib::Paths expansion_cache;
|
||||
for (const ExPolygon &expoly : src) {
|
||||
for (size_t icontour = 0; icontour < expoly.num_contours(); ++ icontour) {
|
||||
// Execute reorients the contours so that the outer most contour has a positive area. Thus the output
|
||||
// contours will be CCW oriented even though the input paths are CW oriented.
|
||||
// Offset is applied after contour reorientation, thus the signum of the offset value is reversed.
|
||||
offsetter.Clear();
|
||||
offsetter.AddPath(expoly.contour_or_hole(icontour).points, ClipperLib::jtSquare, ClipperLib::etClosedPolygon);
|
||||
expansion_cache.clear();
|
||||
offsetter.Execute(expansion_cache, icontour == 0 ? expansion : -expansion);
|
||||
append(out, ClipperZUtils::to_zpaths<true>(expansion_cache, base_idx));
|
||||
}
|
||||
++ base_idx;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Paths were created by splitting closed polygons into open paths and then by clipping them.
|
||||
// Thus some pieces of the clipped polygons may now become split at the ends of the source polygons.
|
||||
// Those ends are sorted lexicographically in "splits".
|
||||
// Reconnect those split pieces.
|
||||
static inline void merge_splits(ClipperLib_Z::Paths &paths, std::vector<std::pair<ClipperLib_Z::IntPoint, int>> &splits)
|
||||
{
|
||||
for (auto it_path = paths.begin(); it_path != paths.end(); ) {
|
||||
ClipperLib_Z::Path &path = *it_path;
|
||||
assert(path.size() >= 2);
|
||||
bool merged = false;
|
||||
if (path.size() >= 2) {
|
||||
const ClipperLib_Z::IntPoint &front = path.front();
|
||||
const ClipperLib_Z::IntPoint &back = path.back();
|
||||
// The path before clipping was supposed to cross the clipping boundary or be fully out of it.
|
||||
// Thus the clipped contour is supposed to become open, with one exception: The anchor expands into a closed hole.
|
||||
if (front.x() != back.x() || front.y() != back.y()) {
|
||||
// Look up the ends in "splits", possibly join the contours.
|
||||
// "splits" maps into the other piece connected to the same end point.
|
||||
auto find_end = [&splits](const ClipperLib_Z::IntPoint &pt) -> std::pair<ClipperLib_Z::IntPoint, int>* {
|
||||
auto it = std::lower_bound(splits.begin(), splits.end(), pt,
|
||||
[](const auto &l, const auto &r){ return ClipperZUtils::zpoint_lower(l.first, r); });
|
||||
return it != splits.end() && it->first == pt ? &(*it) : nullptr;
|
||||
};
|
||||
auto *end = find_end(front);
|
||||
bool end_front = true;
|
||||
if (! end) {
|
||||
end_front = false;
|
||||
end = find_end(back);
|
||||
}
|
||||
if (end) {
|
||||
// This segment ends at a split point of the source closed contour before clipping.
|
||||
if (end->second == -1) {
|
||||
// Open end was found, not matched yet.
|
||||
end->second = int(it_path - paths.begin());
|
||||
} else {
|
||||
// Open end was found and matched with end->second
|
||||
ClipperLib_Z::Path &other_path = paths[end->second];
|
||||
polylines_merge(other_path, other_path.front() == end->first, std::move(path), end_front);
|
||||
if (std::next(it_path) == paths.end()) {
|
||||
paths.pop_back();
|
||||
break;
|
||||
}
|
||||
path = std::move(paths.back());
|
||||
paths.pop_back();
|
||||
merged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! merged)
|
||||
++ it_path;
|
||||
}
|
||||
}
|
||||
|
||||
using AABBTreeBBoxes = AABBTreeIndirect::Tree<2, coord_t>;
|
||||
|
||||
static AABBTreeBBoxes build_aabb_tree_over_expolygons(const ExPolygons &expolygons)
|
||||
{
|
||||
// Calculate bounding boxes of internal slices.
|
||||
std::vector<AABBTreeIndirect::BoundingBoxWrapper> bboxes;
|
||||
bboxes.reserve(expolygons.size());
|
||||
for (size_t i = 0; i < expolygons.size(); ++ i)
|
||||
bboxes.emplace_back(i, get_extents(expolygons[i].contour));
|
||||
// Build AABB tree over bounding boxes of boundary expolygons.
|
||||
AABBTreeBBoxes out;
|
||||
out.build_modify_input(bboxes);
|
||||
return out;
|
||||
}
|
||||
|
||||
static int sample_in_expolygons(
|
||||
// AABB tree over boundary expolygons
|
||||
const AABBTreeBBoxes &aabb_tree,
|
||||
const ExPolygons &expolygons,
|
||||
const Point &sample)
|
||||
{
|
||||
int out = -1;
|
||||
AABBTreeIndirect::traverse(aabb_tree,
|
||||
[&sample](const AABBTreeBBoxes::Node &node) {
|
||||
return node.bbox.contains(sample);
|
||||
},
|
||||
[&expolygons, &sample, &out](const AABBTreeBBoxes::Node &node) {
|
||||
assert(node.is_leaf());
|
||||
assert(node.is_valid());
|
||||
if (expolygons[node.idx].contains(sample)) {
|
||||
out = int(node.idx);
|
||||
// Stop traversal.
|
||||
return false;
|
||||
}
|
||||
// Continue traversal.
|
||||
return true;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<WaveSeed> wave_seeds(
|
||||
// Source regions that are supposed to touch the boundary.
|
||||
const ExPolygons &src,
|
||||
// Boundaries of source regions touching the "boundary" regions will be expanded into the "boundary" region.
|
||||
const ExPolygons &boundary,
|
||||
// Initial expansion of src to make the source regions intersect with boundary regions just a bit.
|
||||
float tiny_expansion,
|
||||
// Sort output by boundary ID and source ID.
|
||||
bool sorted)
|
||||
{
|
||||
assert(tiny_expansion > 0);
|
||||
|
||||
if (src.empty() || boundary.empty())
|
||||
return {};
|
||||
|
||||
using Intersection = ClipperZUtils::ClipperZIntersectionVisitor::Intersection;
|
||||
using Intersections = ClipperZUtils::ClipperZIntersectionVisitor::Intersections;
|
||||
|
||||
ClipperLib_Z::Paths segments;
|
||||
Intersections intersections;
|
||||
|
||||
coord_t idx_boundary_begin = 1;
|
||||
coord_t idx_boundary_end = idx_boundary_begin;
|
||||
coord_t idx_src_end;
|
||||
|
||||
{
|
||||
ClipperLib_Z::Clipper zclipper;
|
||||
ClipperZUtils::ClipperZIntersectionVisitor visitor(intersections);
|
||||
zclipper.ZFillFunction(visitor.clipper_callback());
|
||||
// as closed contours
|
||||
zclipper.AddPaths(ClipperZUtils::expolygons_to_zpaths(boundary, idx_boundary_end), ClipperLib_Z::ptClip, true);
|
||||
// as open contours
|
||||
std::vector<std::pair<ClipperLib_Z::IntPoint, int>> zsrc_splits;
|
||||
{
|
||||
idx_src_end = idx_boundary_end;
|
||||
ClipperLib_Z::Paths zsrc = expolygons_to_zpaths_expanded_opened(src, tiny_expansion, idx_src_end);
|
||||
zclipper.AddPaths(zsrc, ClipperLib_Z::ptSubject, false);
|
||||
zsrc_splits.reserve(zsrc.size());
|
||||
for (const ClipperLib_Z::Path &path : zsrc) {
|
||||
assert(path.size() >= 2);
|
||||
assert(path.front() == path.back());
|
||||
zsrc_splits.emplace_back(path.front(), -1);
|
||||
}
|
||||
std::sort(zsrc_splits.begin(), zsrc_splits.end(), [](const auto &l, const auto &r){ return ClipperZUtils::zpoint_lower(l.first, r.first); });
|
||||
}
|
||||
ClipperLib_Z::PolyTree polytree;
|
||||
zclipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero);
|
||||
ClipperLib_Z::PolyTreeToPaths(std::move(polytree), segments);
|
||||
merge_splits(segments, zsrc_splits);
|
||||
}
|
||||
|
||||
// AABBTree over bounding boxes of boundaries.
|
||||
// Only built if necessary, that is if any of the seed contours is closed, thus there is no intersection point
|
||||
// with the boundary and all Z coordinates of the closed contour point to the source contour.
|
||||
AABBTreeBBoxes aabb_tree;
|
||||
|
||||
// Sort paths into their respective islands.
|
||||
// Each src x boundary will be processed (wave expanded) independently.
|
||||
// Multiple pieces of a single src may intersect the same boundary.
|
||||
WaveSeeds out;
|
||||
out.reserve(segments.size());
|
||||
int iseed = 0;
|
||||
for (const ClipperLib_Z::Path &path : segments) {
|
||||
assert(path.size() >= 2);
|
||||
ClipperLib_Z::IntPoint front = path.front();
|
||||
ClipperLib_Z::IntPoint back = path.back();
|
||||
// Both ends of a seed segment are supposed to be inside a single boundary expolygon.
|
||||
// Thus as long as the seed contour is not closed, it should be open at a boundary point.
|
||||
assert((front == back && front.z() >= idx_boundary_end && front.z() < idx_src_end) ||
|
||||
//(front.z() < 0 && back.z() < 0));
|
||||
// Hope that at least one end of an open polyline is clipped by the boundary, thus an intersection point is created.
|
||||
(front.z() < 0 || back.z() < 0));
|
||||
|
||||
if (front == back && (front.z() < idx_boundary_end)) {
|
||||
// This should be a very rare exception.
|
||||
// See https://github.com/prusa3d/PrusaSlicer/issues/12469.
|
||||
// Segement is open, yet its first point seems to be part of boundary polygon.
|
||||
// Take the first point with src polygon index.
|
||||
for (const ClipperLib_Z::IntPoint &point : path) {
|
||||
if (point.z() >= idx_boundary_end) {
|
||||
front = point;
|
||||
back = point;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Intersection *intersection = nullptr;
|
||||
auto intersection_point_valid = [idx_boundary_end, idx_src_end](const Intersection &is) {
|
||||
return is.first >= 1 && is.first < idx_boundary_end &&
|
||||
is.second >= idx_boundary_end && is.second < idx_src_end;
|
||||
};
|
||||
if (front.z() < 0) {
|
||||
const Intersection &is = intersections[- front.z() - 1];
|
||||
assert(intersection_point_valid(is));
|
||||
if (intersection_point_valid(is))
|
||||
intersection = &is;
|
||||
}
|
||||
if (! intersection && back.z() < 0) {
|
||||
const Intersection &is = intersections[- back.z() - 1];
|
||||
assert(intersection_point_valid(is));
|
||||
if (intersection_point_valid(is))
|
||||
intersection = &is;
|
||||
}
|
||||
if (intersection) {
|
||||
// The path intersects the boundary contour at least at one side.
|
||||
out.push_back({ uint32_t(intersection->second - idx_boundary_end), uint32_t(intersection->first - 1), ClipperZUtils::from_zpath(path) });
|
||||
} else {
|
||||
// This should be a closed contour.
|
||||
assert(front == back && front.z() >= idx_boundary_end && front.z() < idx_src_end);
|
||||
// Find a source boundary expolygon of one sample of this closed path.
|
||||
if (aabb_tree.empty())
|
||||
aabb_tree = build_aabb_tree_over_expolygons(boundary);
|
||||
int boundary_id = sample_in_expolygons(aabb_tree, boundary, Point(front.x(), front.y()));
|
||||
// Boundary that contains the sample point was found.
|
||||
assert(boundary_id >= 0);
|
||||
if (boundary_id >= 0)
|
||||
out.push_back({ uint32_t(front.z() - idx_boundary_end), uint32_t(boundary_id), ClipperZUtils::from_zpath(path) });
|
||||
}
|
||||
++ iseed;
|
||||
}
|
||||
|
||||
if (sorted)
|
||||
// Sort the seeds by their intersection boundary and source contour.
|
||||
std::sort(out.begin(), out.end(), lower_by_boundary_and_src);
|
||||
return out;
|
||||
}
|
||||
|
||||
static ClipperLib::Paths wavefront_initial(ClipperLib::ClipperOffset &co, const ClipperLib::Paths &polylines, float offset)
|
||||
{
|
||||
ClipperLib::Paths out;
|
||||
out.reserve(polylines.size());
|
||||
ClipperLib::Paths out_this;
|
||||
for (const ClipperLib::Path &path : polylines) {
|
||||
assert(path.size() >= 2);
|
||||
co.Clear();
|
||||
co.AddPath(path, jtRound, path.front() == path.back() ? ClipperLib::etClosedLine : ClipperLib::etOpenRound);
|
||||
co.Execute(out_this, offset);
|
||||
append(out, std::move(out_this));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Input polygons may consist of multiple expolygons, even nested expolygons.
|
||||
// After inflation some polygons may thus overlap, however the overlap is being resolved during the successive
|
||||
// clipping operation, thus it is not being done here.
|
||||
static ClipperLib::Paths wavefront_step(ClipperLib::ClipperOffset &co, const ClipperLib::Paths &polygons, float offset)
|
||||
{
|
||||
ClipperLib::Paths out;
|
||||
out.reserve(polygons.size());
|
||||
ClipperLib::Paths out_this;
|
||||
for (const ClipperLib::Path &polygon : polygons) {
|
||||
co.Clear();
|
||||
// Execute reorients the contours so that the outer most contour has a positive area. Thus the output
|
||||
// contours will be CCW oriented even though the input paths are CW oriented.
|
||||
// Offset is applied after contour reorientation, thus the signum of the offset value is reversed.
|
||||
co.AddPath(polygon, jtRound, ClipperLib::etClosedPolygon);
|
||||
bool ccw = ClipperLib::Orientation(polygon);
|
||||
co.Execute(out_this, ccw ? offset : - offset);
|
||||
if (! ccw) {
|
||||
// Reverse the resulting contours.
|
||||
for (ClipperLib::Path &path : out_this)
|
||||
std::reverse(path.begin(), path.end());
|
||||
}
|
||||
append(out, std::move(out_this));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static ClipperLib::Paths wavefront_clip(const ClipperLib::Paths &wavefront, const Polygons &clipping)
|
||||
{
|
||||
ClipperLib::Clipper clipper;
|
||||
clipper.AddPaths(wavefront, ClipperLib::ptSubject, true);
|
||||
clipper.AddPaths(ClipperUtils::PolygonsProvider(clipping), ClipperLib::ptClip, true);
|
||||
ClipperLib::Paths out;
|
||||
clipper.Execute(ClipperLib::ctIntersection, out, ClipperLib::pftPositive, ClipperLib::pftPositive);
|
||||
return out;
|
||||
}
|
||||
|
||||
static Polygons propagate_wave_from_boundary(
|
||||
ClipperLib::ClipperOffset &co,
|
||||
// Seed of the wave: Open polylines very close to the boundary.
|
||||
const ClipperLib::Paths &seed,
|
||||
// Boundary inside which the waveform will propagate.
|
||||
const ExPolygon &boundary,
|
||||
// How much to inflate the seed lines to produce the first wave area.
|
||||
const float initial_step,
|
||||
// How much to inflate the first wave area and the successive wave areas in each step.
|
||||
const float other_step,
|
||||
// Number of inflate steps after the initial step.
|
||||
const size_t num_other_steps,
|
||||
// Maximum inflation of seed contours over the boundary. Used to trim boundary to speed up
|
||||
// clipping during wave propagation.
|
||||
const float max_inflation)
|
||||
{
|
||||
assert(! seed.empty() && seed.front().size() >= 2);
|
||||
Polygons clipping = ClipperUtils::clip_clipper_polygons_with_subject_bbox(boundary, get_extents<true>(seed).inflated(max_inflation));
|
||||
ClipperLib::Paths polygons = wavefront_clip(wavefront_initial(co, seed, initial_step), clipping);
|
||||
// Now offset the remaining
|
||||
for (size_t ioffset = 0; ioffset < num_other_steps; ++ ioffset)
|
||||
polygons = wavefront_clip(wavefront_step(co, polygons, other_step), clipping);
|
||||
return to_polygons(polygons);
|
||||
}
|
||||
|
||||
// Resulting regions are sorted by boundary id and source id.
|
||||
std::vector<RegionExpansion> propagate_waves(const WaveSeeds &seeds, const ExPolygons &boundary, const RegionExpansionParameters ¶ms)
|
||||
{
|
||||
std::vector<RegionExpansion> out;
|
||||
ClipperLib::Paths paths;
|
||||
ClipperLib::ClipperOffset co;
|
||||
co.ArcTolerance = params.arc_tolerance;
|
||||
co.ShortestEdgeLength = params.shortest_edge_length;
|
||||
for (auto it_seed = seeds.begin(); it_seed != seeds.end();) {
|
||||
auto it = it_seed;
|
||||
paths.clear();
|
||||
for (; it != seeds.end() && it->boundary == it_seed->boundary && it->src == it_seed->src; ++ it)
|
||||
paths.emplace_back(it->path);
|
||||
// Propagate the wavefront while clipping it with the trimmed boundary.
|
||||
// Collect the expanded polygons, merge them with the source polygons.
|
||||
RegionExpansion re;
|
||||
for (Polygon &polygon : propagate_wave_from_boundary(co, paths, boundary[it_seed->boundary], params.initial_step, params.other_step, params.num_other_steps, params.max_inflation))
|
||||
out.push_back({ std::move(polygon), it_seed->src, it_seed->boundary });
|
||||
it_seed = it;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<RegionExpansion> propagate_waves(const ExPolygons &src, const ExPolygons &boundary, const RegionExpansionParameters ¶ms)
|
||||
{
|
||||
return propagate_waves(wave_seeds(src, boundary, params.tiny_expansion, true), boundary, params);
|
||||
}
|
||||
|
||||
std::vector<RegionExpansion> propagate_waves(const ExPolygons &src, const ExPolygons &boundary,
|
||||
// Scaled expansion value
|
||||
float expansion,
|
||||
// Expand by waves of expansion_step size (expansion_step is scaled).
|
||||
float expansion_step,
|
||||
// Don't take more than max_nr_steps for small expansion_step.
|
||||
size_t max_nr_steps)
|
||||
{
|
||||
return propagate_waves(src, boundary, RegionExpansionParameters::build(expansion, expansion_step, max_nr_steps));
|
||||
}
|
||||
|
||||
// Returns regions per source ExPolygon expanded into boundary.
|
||||
std::vector<RegionExpansionEx> propagate_waves_ex(const WaveSeeds &seeds, const ExPolygons &boundary, const RegionExpansionParameters ¶ms)
|
||||
{
|
||||
std::vector<RegionExpansion> expanded = propagate_waves(seeds, boundary, params);
|
||||
assert(std::is_sorted(seeds.begin(), seeds.end(), [](const auto &l, const auto &r){ return l.boundary < r.boundary || (l.boundary == r.boundary && l.src < r.src); }));
|
||||
Polygons acc;
|
||||
std::vector<RegionExpansionEx> out;
|
||||
for (auto it = expanded.begin(); it != expanded.end(); ) {
|
||||
auto it2 = it;
|
||||
acc.clear();
|
||||
for (; it2 != expanded.end() && it2->boundary_id == it->boundary_id && it2->src_id == it->src_id; ++ it2)
|
||||
acc.emplace_back(std::move(it2->polygon));
|
||||
size_t size = it2 - it;
|
||||
if (size == 1)
|
||||
out.push_back({ ExPolygon{std::move(acc.front())}, it->src_id, it->boundary_id });
|
||||
else {
|
||||
ExPolygons expolys = union_ex(acc);
|
||||
reserve_more_power_of_2(out, expolys.size());
|
||||
for (ExPolygon &ex : expolys)
|
||||
out.push_back({ std::move(ex), it->src_id, it->boundary_id });
|
||||
}
|
||||
it = it2;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Returns regions per source ExPolygon expanded into boundary.
|
||||
std::vector<RegionExpansionEx> propagate_waves_ex(
|
||||
// Source regions that are supposed to touch the boundary.
|
||||
// Boundaries of source regions touching the "boundary" regions will be expanded into the "boundary" region.
|
||||
const ExPolygons &src,
|
||||
const ExPolygons &boundary,
|
||||
// Scaled expansion value
|
||||
float full_expansion,
|
||||
// Expand by waves of expansion_step size (expansion_step is scaled).
|
||||
float expansion_step,
|
||||
// Don't take more than max_nr_steps for small expansion_step.
|
||||
size_t max_nr_expansion_steps)
|
||||
{
|
||||
auto params = RegionExpansionParameters::build(full_expansion, expansion_step, max_nr_expansion_steps);
|
||||
return propagate_waves_ex(wave_seeds(src, boundary, params.tiny_expansion, true), boundary, params);
|
||||
}
|
||||
|
||||
std::vector<Polygons> expand_expolygons(const ExPolygons &src, const ExPolygons &boundary,
|
||||
// Scaled expansion value
|
||||
float expansion,
|
||||
// Expand by waves of expansion_step size (expansion_step is scaled).
|
||||
float expansion_step,
|
||||
// Don't take more than max_nr_steps for small expansion_step.
|
||||
size_t max_nr_steps)
|
||||
{
|
||||
std::vector<Polygons> out(src.size(), Polygons{});
|
||||
for (RegionExpansion &r : propagate_waves(src, boundary, expansion, expansion_step, max_nr_steps))
|
||||
out[r.src_id].emplace_back(std::move(r.polygon));
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ExPolygon> merge_expansions_into_expolygons(ExPolygons &&src, std::vector<RegionExpansion> &&expanded)
|
||||
{
|
||||
// expanded regions will be merged into source regions, thus they will be re-sorted by source id.
|
||||
std::sort(expanded.begin(), expanded.end(), [](const auto &l, const auto &r) { return l.src_id < r.src_id; });
|
||||
uint32_t last = 0;
|
||||
Polygons acc;
|
||||
ExPolygons out;
|
||||
out.reserve(src.size());
|
||||
for (auto it = expanded.begin(); it != expanded.end();) {
|
||||
for (; last < it->src_id; ++ last)
|
||||
out.emplace_back(std::move(src[last]));
|
||||
acc.clear();
|
||||
assert(it->src_id == last);
|
||||
for (; it != expanded.end() && it->src_id == last; ++ it)
|
||||
acc.emplace_back(std::move(it->polygon));
|
||||
//FIXME offset & merging could be more efficient, for example one does not need to copy the source expolygon
|
||||
ExPolygon &src_ex = src[last ++];
|
||||
assert(! src_ex.contour.empty());
|
||||
#if 0
|
||||
{
|
||||
static int iRun = 0;
|
||||
BoundingBox bbox = get_extents(acc);
|
||||
bbox.merge(get_extents(src_ex));
|
||||
SVG svg(debug_out_path("expand_merge_expolygons-failed-union=%d.svg", iRun ++).c_str(), bbox);
|
||||
svg.draw(acc);
|
||||
svg.draw_outline(acc, "black", scale_(0.05));
|
||||
svg.draw(src_ex, "red");
|
||||
svg.Close();
|
||||
}
|
||||
#endif
|
||||
Point sample = src_ex.contour.front();
|
||||
append(acc, to_polygons(std::move(src_ex)));
|
||||
ExPolygons merged = union_safety_offset_ex(acc);
|
||||
// Expanding one expolygon by waves should not change connectivity of the source expolygon:
|
||||
// Single expolygon should be produced possibly with increased number of holes.
|
||||
if (merged.size() > 1) {
|
||||
// assert(merged.size() == 1);
|
||||
// There is something wrong with the initial waves. Most likely the bridge was not valid at all
|
||||
// or the boundary region was very close to some bridge edge, but not really touching.
|
||||
// Pick only a single merged expolygon, which contains one sample point of the source expolygon.
|
||||
auto aabb_tree = build_aabb_tree_over_expolygons(merged);
|
||||
int id = sample_in_expolygons(aabb_tree, merged, sample);
|
||||
assert(id != -1);
|
||||
if (id != -1)
|
||||
out.emplace_back(std::move(merged[id]));
|
||||
} else if (merged.size() == 1)
|
||||
out.emplace_back(std::move(merged.front()));
|
||||
}
|
||||
for (; last < uint32_t(src.size()); ++ last)
|
||||
out.emplace_back(std::move(src[last]));
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ExPolygon> expand_merge_expolygons(ExPolygons &&src, const ExPolygons &boundary, const RegionExpansionParameters ¶ms)
|
||||
{
|
||||
// expanded regions are sorted by boundary id and source id
|
||||
std::vector<RegionExpansion> expanded = propagate_waves(src, boundary, params);
|
||||
return merge_expansions_into_expolygons(std::move(src), std::move(expanded));
|
||||
}
|
||||
|
||||
} // Algorithm
|
||||
} // Slic3r
|
||||
@@ -0,0 +1,122 @@
|
||||
///|/ Copyright (c) Prusa Research 2022 - 2023 Vojtěch Bubník @bubnikv
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef SRC_LIBSLIC3R_ALGORITHM_REGION_EXPANSION_HPP_
|
||||
#define SRC_LIBSLIC3R_ALGORITHM_REGION_EXPANSION_HPP_
|
||||
|
||||
#include <cstdint>
|
||||
#include <libslic3r/Point.hpp>
|
||||
#include <libslic3r/Polygon.hpp>
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
namespace Algorithm {
|
||||
|
||||
struct RegionExpansionParameters
|
||||
{
|
||||
// Initial expansion of src to make the source regions intersect with boundary regions just a bit.
|
||||
float tiny_expansion;
|
||||
// How much to inflate the seed lines to produce the first wave area.
|
||||
float initial_step;
|
||||
// How much to inflate the first wave area and the successive wave areas in each step.
|
||||
float other_step;
|
||||
// Number of inflate steps after the initial step.
|
||||
size_t num_other_steps;
|
||||
// Maximum inflation of seed contours over the boundary. Used to trim boundary to speed up
|
||||
// clipping during wave propagation.
|
||||
float max_inflation;
|
||||
|
||||
// Accuracy of the offsetter for wave propagation.
|
||||
double arc_tolerance;
|
||||
double shortest_edge_length;
|
||||
|
||||
static RegionExpansionParameters build(
|
||||
// Scaled expansion value
|
||||
float full_expansion,
|
||||
// Expand by waves of expansion_step size (expansion_step is scaled).
|
||||
float expansion_step,
|
||||
// Don't take more than max_nr_steps for small expansion_step.
|
||||
size_t max_nr_expansion_steps);
|
||||
};
|
||||
|
||||
struct WaveSeed {
|
||||
uint32_t src;
|
||||
uint32_t boundary;
|
||||
Points path;
|
||||
};
|
||||
using WaveSeeds = std::vector<WaveSeed>;
|
||||
|
||||
inline bool lower_by_boundary_and_src(const WaveSeed &l, const WaveSeed &r)
|
||||
{
|
||||
return l.boundary < r.boundary || (l.boundary == r.boundary && l.src < r.src);
|
||||
}
|
||||
|
||||
inline bool lower_by_src_and_boundary(const WaveSeed &l, const WaveSeed &r)
|
||||
{
|
||||
return l.src < r.src || (l.src == r.src && l.boundary < r.boundary);
|
||||
}
|
||||
|
||||
// Expand src slightly outwards to intersect boundaries, trim the offsetted src polylines by the boundaries.
|
||||
// Return the trimmed paths annotated with their origin (source of the path, index of the boundary region).
|
||||
WaveSeeds wave_seeds(
|
||||
// Source regions that are supposed to touch the boundary.
|
||||
const ExPolygons &src,
|
||||
// Boundaries of source regions touching the "boundary" regions will be expanded into the "boundary" region.
|
||||
const ExPolygons &boundary,
|
||||
// Initial expansion of src to make the source regions intersect with boundary regions just a bit.
|
||||
float tiny_expansion,
|
||||
bool sorted);
|
||||
|
||||
struct RegionExpansion
|
||||
{
|
||||
Polygon polygon;
|
||||
uint32_t src_id;
|
||||
uint32_t boundary_id;
|
||||
};
|
||||
|
||||
std::vector<RegionExpansion> propagate_waves(const WaveSeeds &seeds, const ExPolygons &boundary, const RegionExpansionParameters ¶ms);
|
||||
std::vector<RegionExpansion> propagate_waves(const ExPolygons &src, const ExPolygons &boundary, const RegionExpansionParameters ¶ms);
|
||||
|
||||
std::vector<RegionExpansion> propagate_waves(const ExPolygons &src, const ExPolygons &boundary,
|
||||
// Scaled expansion value
|
||||
float expansion,
|
||||
// Expand by waves of expansion_step size (expansion_step is scaled).
|
||||
float expansion_step,
|
||||
// Don't take more than max_nr_steps for small expansion_step.
|
||||
size_t max_nr_steps);
|
||||
|
||||
struct RegionExpansionEx
|
||||
{
|
||||
ExPolygon expolygon;
|
||||
uint32_t src_id;
|
||||
uint32_t boundary_id;
|
||||
};
|
||||
|
||||
std::vector<RegionExpansionEx> propagate_waves_ex(const WaveSeeds &seeds, const ExPolygons &boundary, const RegionExpansionParameters ¶ms);
|
||||
|
||||
std::vector<RegionExpansionEx> propagate_waves_ex(const ExPolygons &src, const ExPolygons &boundary,
|
||||
// Scaled expansion value
|
||||
float expansion,
|
||||
// Expand by waves of expansion_step size (expansion_step is scaled).
|
||||
float expansion_step,
|
||||
// Don't take more than max_nr_steps for small expansion_step.
|
||||
size_t max_nr_steps);
|
||||
|
||||
std::vector<Polygons> expand_expolygons(const ExPolygons &src, const ExPolygons &boundary,
|
||||
// Scaled expansion value
|
||||
float expansion,
|
||||
// Expand by waves of expansion_step size (expansion_step is scaled).
|
||||
float expansion_step,
|
||||
// Don't take more than max_nr_steps for small expansion_step.
|
||||
size_t max_nr_steps);
|
||||
|
||||
// Merge src with expansions, return the merged expolygons.
|
||||
std::vector<ExPolygon> merge_expansions_into_expolygons(ExPolygons &&src, std::vector<RegionExpansion> &&expanded);
|
||||
|
||||
std::vector<ExPolygon> expand_merge_expolygons(ExPolygons &&src, const ExPolygons &boundary, const RegionExpansionParameters ¶ms);
|
||||
|
||||
} // Algorithm
|
||||
} // Slic3r
|
||||
|
||||
#endif /* SRC_LIBSLIC3R_ALGORITHM_REGION_EXPANSION_HPP_ */
|
||||
@@ -0,0 +1,170 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ANYPTR_HPP
|
||||
#define ANYPTR_HPP
|
||||
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <boost/variant.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// A general purpose pointer holder that can hold any type of smart pointer
|
||||
// or raw pointer which can own or not own any object they point to.
|
||||
// In case a raw pointer is stored, it is not destructed so ownership is
|
||||
// assumed to be foreign.
|
||||
//
|
||||
// The stored pointer is not checked for being null when dereferenced.
|
||||
//
|
||||
// This is a movable only object due to the fact that it can possibly hold
|
||||
// a unique_ptr which can only be moved.
|
||||
//
|
||||
// Drawbacks:
|
||||
// No custom deleters are supported when storing a unique_ptr, but overloading
|
||||
// std::default_delete for a particular type could be a workaround
|
||||
//
|
||||
// raw array types are problematic, since std::default_delete also does not
|
||||
// support them well.
|
||||
template<class T>
|
||||
class AnyPtr {
|
||||
enum { RawPtr, UPtr, ShPtr };
|
||||
|
||||
boost::variant<T*, std::unique_ptr<T>, std::shared_ptr<T>> ptr;
|
||||
|
||||
template<class Self> static T *get_ptr(Self &&s)
|
||||
{
|
||||
switch (s.ptr.which()) {
|
||||
case RawPtr: return boost::get<T *>(s.ptr);
|
||||
case UPtr: return boost::get<std::unique_ptr<T>>(s.ptr).get();
|
||||
case ShPtr: return boost::get<std::shared_ptr<T>>(s.ptr).get();
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template<class TT> friend class AnyPtr;
|
||||
|
||||
template<class TT>
|
||||
using SimilarPtrOnly = std::enable_if_t<std::is_convertible_v<TT*, T*>>;
|
||||
|
||||
public:
|
||||
|
||||
AnyPtr() noexcept = default;
|
||||
|
||||
AnyPtr(T *p) noexcept: ptr{p} {}
|
||||
|
||||
AnyPtr(std::nullptr_t) noexcept {};
|
||||
|
||||
template<class TT, class = SimilarPtrOnly<TT>>
|
||||
AnyPtr(TT *p) noexcept : ptr{p}
|
||||
{}
|
||||
template<class TT = T, class = SimilarPtrOnly<TT>>
|
||||
AnyPtr(std::unique_ptr<TT> p) noexcept : ptr{std::unique_ptr<T>(std::move(p))}
|
||||
{}
|
||||
template<class TT = T, class = SimilarPtrOnly<TT>>
|
||||
AnyPtr(std::shared_ptr<TT> p) noexcept : ptr{std::shared_ptr<T>(std::move(p))}
|
||||
{}
|
||||
|
||||
AnyPtr(AnyPtr &&other) noexcept : ptr{std::move(other.ptr)} {}
|
||||
|
||||
template<class TT, class = SimilarPtrOnly<TT>>
|
||||
AnyPtr(AnyPtr<TT> &&other) noexcept
|
||||
{
|
||||
this->operator=(std::move(other));
|
||||
}
|
||||
|
||||
AnyPtr(const AnyPtr &other) = delete;
|
||||
|
||||
AnyPtr &operator=(AnyPtr &&other) noexcept
|
||||
{
|
||||
ptr = std::move(other.ptr);
|
||||
return *this;
|
||||
}
|
||||
|
||||
AnyPtr &operator=(const AnyPtr &other) = delete;
|
||||
|
||||
template<class TT, class = SimilarPtrOnly<TT>>
|
||||
AnyPtr& operator=(AnyPtr<TT> &&other) noexcept
|
||||
{
|
||||
switch (other.ptr.which()) {
|
||||
case RawPtr: *this = boost::get<TT *>(other.ptr); break;
|
||||
case UPtr: *this = std::move(boost::get<std::unique_ptr<TT>>(other.ptr)); break;
|
||||
case ShPtr: *this = std::move(boost::get<std::shared_ptr<TT>>(other.ptr)); break;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<class TT, class = SimilarPtrOnly<TT>>
|
||||
AnyPtr &operator=(TT *p) noexcept
|
||||
{
|
||||
ptr = static_cast<T *>(p);
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<class TT, class = SimilarPtrOnly<TT>>
|
||||
AnyPtr &operator=(std::unique_ptr<TT> p) noexcept
|
||||
{
|
||||
ptr = std::unique_ptr<T>(std::move(p));
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<class TT, class = SimilarPtrOnly<TT>>
|
||||
AnyPtr &operator=(std::shared_ptr<TT> p) noexcept
|
||||
{
|
||||
ptr = std::shared_ptr<T>(std::move(p));
|
||||
return *this;
|
||||
}
|
||||
|
||||
const T &operator*() const noexcept { return *get_ptr(*this); }
|
||||
T &operator*() noexcept { return *get_ptr(*this); }
|
||||
|
||||
T *operator->() noexcept { return get_ptr(*this); }
|
||||
const T *operator->() const noexcept { return get_ptr(*this); }
|
||||
|
||||
T *get() noexcept { return get_ptr(*this); }
|
||||
const T *get() const noexcept { return get_ptr(*this); }
|
||||
|
||||
operator bool() const noexcept
|
||||
{
|
||||
switch (ptr.which()) {
|
||||
case RawPtr: return bool(boost::get<T *>(ptr));
|
||||
case UPtr: return bool(boost::get<std::unique_ptr<T>>(ptr));
|
||||
case ShPtr: return bool(boost::get<std::shared_ptr<T>>(ptr));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the stored pointer is a shared pointer, returns a reference
|
||||
// counted copy. Empty shared pointer is returned otherwise.
|
||||
std::shared_ptr<T> get_shared_cpy() const noexcept
|
||||
{
|
||||
std::shared_ptr<T> ret;
|
||||
|
||||
if (ptr.which() == ShPtr)
|
||||
ret = boost::get<std::shared_ptr<T>>(ptr);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// If the underlying pointer is unique, convert to shared pointer
|
||||
void convert_unique_to_shared() noexcept
|
||||
{
|
||||
if (ptr.which() == UPtr)
|
||||
ptr = std::shared_ptr<T>{std::move(boost::get<std::unique_ptr<T>>(ptr))};
|
||||
}
|
||||
|
||||
// Returns true if the data is owned by this AnyPtr instance
|
||||
bool is_owned() const noexcept
|
||||
{
|
||||
return ptr.which() == UPtr || ptr.which() == ShPtr;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // ANYPTR_HPP
|
||||
@@ -0,0 +1,778 @@
|
||||
///|/ Copyright (c) Prusa Research 2017 - 2023 Oleksandra Iushchenko @YuSanka, Vojtěch Bubník @bubnikv, Pavel Mikuš @Godrak, David Kocík @kocikdav, Lukáš Matěna @lukasmatena, Enrico Turri @enricoturri1966, Lukáš Hejl @hejllukas, Filip Sykala @Jony01, Vojtěch Král @vojtechkral
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "AppConfig.hpp"
|
||||
#include "Exception.hpp"
|
||||
#include "LocalesUtils.hpp"
|
||||
#include "Thread.hpp"
|
||||
#include "format.hpp"
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/filesystem/operations.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
#include <boost/property_tree/ini_parser.hpp>
|
||||
#include <boost/property_tree/ptree_fwd.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/format/format_fwd.hpp>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#ifdef WIN32
|
||||
//FIXME replace the two following includes with <boost/md5.hpp> after it becomes mainstream.
|
||||
#include <boost/uuid/detail/md5.hpp>
|
||||
#include <boost/algorithm/hex.hpp>
|
||||
#endif
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
static const std::string VENDOR_PREFIX = "vendor:";
|
||||
static const std::string MODEL_PREFIX = "model:";
|
||||
// Because of a crash in PrusaSlicer 2.3.0/2.3.1 when showing an update notification with some locales, we don't want PrusaSlicer 2.3.0/2.3.1
|
||||
// to show this notification. On the other hand, we would like PrusaSlicer 2.3.2 to show an update notification of the upcoming PrusaSlicer 2.4.0.
|
||||
// Thus we will let PrusaSlicer 2.3.2 and couple of follow-up versions to download the version number from an alternate file until the PrusaSlicer 2.3.0/2.3.1
|
||||
// are phased out, then we will revert to the original name.
|
||||
// For 2.6.0-alpha1 we have switched back to the original. The file should contain data for AppUpdater.cpp
|
||||
static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version";
|
||||
//static const std::string VERSION_CHECK_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaSlicer.version2";
|
||||
// Url to index archive zip that contains latest indicies
|
||||
static const std::string INDEX_ARCHIVE_URL= "https://files.prusa3d.com/wp-content/uploads/repository/vendor_indices.zip";
|
||||
// Url to folder with vendor profile files. Used when downloading new profiles that are not in resources folder.
|
||||
static const std::string PROFILE_FOLDER_URL = "https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/";
|
||||
|
||||
const std::string AppConfig::SECTION_FILAMENTS = "filaments";
|
||||
const std::string AppConfig::SECTION_MATERIALS = "sla_materials";
|
||||
const std::string AppConfig::SECTION_EMBOSS_STYLE = "font";
|
||||
|
||||
void AppConfig::reset()
|
||||
{
|
||||
m_storage.clear();
|
||||
m_vendors.clear();
|
||||
m_dirty = false;
|
||||
m_orig_version = Semver::invalid();
|
||||
m_legacy_datadir = false;
|
||||
set_defaults();
|
||||
};
|
||||
|
||||
// Override missing or keys with their defaults.
|
||||
void AppConfig::set_defaults()
|
||||
{
|
||||
if (m_mode == EAppMode::Editor) {
|
||||
// Reset the empty fields to defaults.
|
||||
if (get("autocenter").empty())
|
||||
set("autocenter", "0");
|
||||
// Disable background processing by default as it is not stable.
|
||||
if (get("background_processing").empty())
|
||||
set("background_processing", "0");
|
||||
// Enable support issues alerts by default
|
||||
if (get("alert_when_supports_needed").empty())
|
||||
set("alert_when_supports_needed", "1");
|
||||
// If set, the "Controller" tab for the control of the printer over serial line and the serial port settings are hidden.
|
||||
// By default, Prusa has the controller hidden.
|
||||
if (get("no_controller").empty())
|
||||
set("no_controller", "1");
|
||||
// If set, the "- default -" selections of print/filament/printer are suppressed, if there is a valid preset available.
|
||||
if (get("no_defaults").empty())
|
||||
set("no_defaults", "1");
|
||||
if (get("no_templates").empty())
|
||||
set("no_templates", "0");
|
||||
if (get("show_incompatible_presets").empty())
|
||||
set("show_incompatible_presets", "0");
|
||||
|
||||
if (get("show_drop_project_dialog").empty())
|
||||
set("show_drop_project_dialog", "1");
|
||||
if (get("drop_project_action").empty())
|
||||
set("drop_project_action", "1");
|
||||
|
||||
if (get("preset_update").empty())
|
||||
set("preset_update", "1");
|
||||
|
||||
if (get("export_sources_full_pathnames").empty())
|
||||
set("export_sources_full_pathnames", "0");
|
||||
|
||||
#ifdef _WIN32
|
||||
if (get("associate_3mf").empty())
|
||||
set("associate_3mf", "0");
|
||||
if (get("associate_stl").empty())
|
||||
set("associate_stl", "0");
|
||||
|
||||
if (get("suppress_round_corners").empty())
|
||||
set("suppress_round_corners", "1");
|
||||
#endif // _WIN32
|
||||
|
||||
// remove old 'use_legacy_opengl' parameter from this config, if present
|
||||
if (!get("use_legacy_opengl").empty())
|
||||
erase("", "use_legacy_opengl");
|
||||
|
||||
if (get("single_instance").empty())
|
||||
set("single_instance",
|
||||
#ifdef __APPLE__
|
||||
"1"
|
||||
#else // __APPLE__
|
||||
"0"
|
||||
#endif // __APPLE__
|
||||
);
|
||||
|
||||
if (get("remember_output_path").empty())
|
||||
set("remember_output_path", "1");
|
||||
|
||||
if (get("remember_output_path_removable").empty())
|
||||
set("remember_output_path_removable", "1");
|
||||
|
||||
if (get("use_custom_toolbar_size").empty())
|
||||
set("use_custom_toolbar_size", "0");
|
||||
|
||||
if (get("custom_toolbar_size").empty())
|
||||
set("custom_toolbar_size", "100");
|
||||
|
||||
if (get("auto_toolbar_size").empty())
|
||||
set("auto_toolbar_size", "100");
|
||||
|
||||
if (get("use_binary_gcode_when_supported").empty())
|
||||
set("use_binary_gcode_when_supported", "1");
|
||||
|
||||
if (get("notify_release").empty())
|
||||
set("notify_release", "all"); // or "none" or "release"
|
||||
|
||||
#if ENABLE_ENVIRONMENT_MAP
|
||||
if (get("use_environment_map").empty())
|
||||
set("use_environment_map", "0");
|
||||
#endif // ENABLE_ENVIRONMENT_MAP
|
||||
|
||||
if (get("use_inches").empty())
|
||||
set("use_inches", "0");
|
||||
|
||||
if (get("default_action_on_close_application").empty())
|
||||
set("default_action_on_close_application", "none"); // , "discard" or "save"
|
||||
|
||||
if (get("default_action_on_select_preset").empty())
|
||||
set("default_action_on_select_preset", "none"); // , "transfer", "discard" or "save"
|
||||
|
||||
if (get("default_action_on_new_project").empty())
|
||||
set("default_action_on_new_project", "none"); // , "keep(transfer)", "discard" or "save"
|
||||
|
||||
if (get("color_mapinulation_panel").empty())
|
||||
set("color_mapinulation_panel", "0");
|
||||
|
||||
if (get("order_volumes").empty())
|
||||
set("order_volumes", "1");
|
||||
|
||||
if (get("non_manifold_edges").empty())
|
||||
set("non_manifold_edges", "1");
|
||||
|
||||
if (get("clear_undo_redo_stack_on_new_project").empty())
|
||||
set("clear_undo_redo_stack_on_new_project", "1");
|
||||
}
|
||||
else {
|
||||
#ifdef _WIN32
|
||||
if (get("associate_gcode").empty())
|
||||
set("associate_gcode", "0");
|
||||
if (get("associate_bgcode").empty())
|
||||
set("associate_bgcode", "0");
|
||||
#endif // _WIN32
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (get("use_retina_opengl").empty())
|
||||
set("use_retina_opengl", "1");
|
||||
#endif // __APPLE__
|
||||
|
||||
if (get("seq_top_layer_only").empty())
|
||||
set("seq_top_layer_only", "1");
|
||||
|
||||
if (get("use_perspective_camera").empty())
|
||||
set("use_perspective_camera", "1");
|
||||
|
||||
if (get("use_free_camera").empty())
|
||||
set("use_free_camera", "0");
|
||||
|
||||
if (get("reverse_mouse_wheel_zoom").empty())
|
||||
set("reverse_mouse_wheel_zoom", "0");
|
||||
|
||||
if (get("show_splash_screen").empty())
|
||||
set("show_splash_screen", "1");
|
||||
|
||||
if (get("restore_win_position").empty())
|
||||
set("restore_win_position", "1"); // allowed values - "1", "0", "crashed_at_..."
|
||||
|
||||
if (get("show_hints").empty())
|
||||
set("show_hints", "1");
|
||||
|
||||
if (get("allow_auto_color_change").empty())
|
||||
set("allow_auto_color_change", "1");
|
||||
|
||||
if (get("allow_ip_resolve").empty())
|
||||
set("allow_ip_resolve", "1");
|
||||
|
||||
if (get("wifi_config_dialog_declined").empty())
|
||||
set("wifi_config_dialog_declined", "0");
|
||||
|
||||
if (get("connect_polling").empty())
|
||||
set("connect_polling", "1");
|
||||
|
||||
if (get("auth_login_dialog_confirmed").empty())
|
||||
set("auth_login_dialog_confirmed", "0");
|
||||
|
||||
if (get("show_estimated_times_in_dbl_slider").empty())
|
||||
set("show_estimated_times_in_dbl_slider", "1");
|
||||
|
||||
if (get("show_ruler_in_dbl_slider").empty())
|
||||
set("show_ruler_in_dbl_slider", "0");
|
||||
|
||||
if (get("show_ruler_bg_in_dbl_slider").empty())
|
||||
set("show_ruler_bg_in_dbl_slider", "1");
|
||||
|
||||
#ifdef _WIN32
|
||||
if (get("use_legacy_3DConnexion").empty())
|
||||
set("use_legacy_3DConnexion", "0");
|
||||
|
||||
if (get("dark_color_mode").empty())
|
||||
set("dark_color_mode", "0");
|
||||
|
||||
if (get("sys_menu_enabled").empty())
|
||||
set("sys_menu_enabled", "1");
|
||||
#endif // _WIN32
|
||||
|
||||
// Remove legacy window positions/sizes
|
||||
erase("", "main_frame_maximized");
|
||||
erase("", "main_frame_pos");
|
||||
erase("", "main_frame_size");
|
||||
erase("", "object_settings_maximized");
|
||||
erase("", "object_settings_pos");
|
||||
erase("", "object_settings_size");
|
||||
}
|
||||
|
||||
#ifdef WIN32
|
||||
static std::string appconfig_md5_hash_line(const std::string_view data)
|
||||
{
|
||||
//FIXME replace the two following includes with <boost/md5.hpp> after it becomes mainstream.
|
||||
// return boost::md5(data).hex_str_value();
|
||||
// boost::uuids::detail::md5 is an internal namespace thus it may change in the future.
|
||||
// Also this implementation is not the fastest, it was designed for short blocks of text.
|
||||
using boost::uuids::detail::md5;
|
||||
md5 md5_hash;
|
||||
// unsigned int[4], 128 bits
|
||||
md5::digest_type md5_digest{};
|
||||
std::string md5_digest_str;
|
||||
md5_hash.process_bytes(data.data(), data.size());
|
||||
md5_hash.get_digest(md5_digest);
|
||||
boost::algorithm::hex(md5_digest, md5_digest + std::size(md5_digest), std::back_inserter(md5_digest_str));
|
||||
// MD5 hash is 32 HEX digits long.
|
||||
assert(md5_digest_str.size() == 32);
|
||||
// This line will be emited at the end of the file.
|
||||
return "# MD5 checksum " + md5_digest_str + "\n";
|
||||
};
|
||||
|
||||
struct ConfigFileInfo {
|
||||
bool correct_checksum {false};
|
||||
bool contains_null {false};
|
||||
};
|
||||
|
||||
// Assume that the last line with the comment inside the config file contains a checksum and that the user didn't modify the config file.
|
||||
static ConfigFileInfo check_config_file_and_verify_checksum(boost::nowide::ifstream &ifs)
|
||||
{
|
||||
auto read_whole_config_file = [&ifs]() -> std::string {
|
||||
std::stringstream ss;
|
||||
ss << ifs.rdbuf();
|
||||
return ss.str();
|
||||
};
|
||||
|
||||
ifs.seekg(0, boost::nowide::ifstream::beg);
|
||||
const std::string whole_config = read_whole_config_file();
|
||||
const bool contains_null = whole_config.find_first_of('\0') != std::string::npos;
|
||||
|
||||
// The checksum should be on the last line in the config file.
|
||||
if (size_t last_comment_pos = whole_config.find_last_of('#'); last_comment_pos != std::string::npos) {
|
||||
// Split read config into two parts, one with checksum, and the second part is part with configuration from the checksum was computed.
|
||||
// Verify existence and validity of the MD5 checksum line at the end of the file.
|
||||
// When the checksum isn't found, the checksum was not saved correctly, it was removed or it is an older config file without the checksum.
|
||||
// If the checksum is incorrect, then the file was either not saved correctly or modified.
|
||||
if (std::string_view(whole_config.c_str() + last_comment_pos, whole_config.size() - last_comment_pos) == appconfig_md5_hash_line({ whole_config.data(), last_comment_pos }))
|
||||
return {true, contains_null};
|
||||
}
|
||||
return {false, contains_null};
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string AppConfig::load(const std::string &path)
|
||||
{
|
||||
this->reset();
|
||||
|
||||
// 1) Read the complete config file into a boost::property_tree.
|
||||
namespace pt = boost::property_tree;
|
||||
pt::ptree tree;
|
||||
boost::nowide::ifstream ifs;
|
||||
bool recovered = false;
|
||||
|
||||
try {
|
||||
ifs.open(path);
|
||||
#ifdef WIN32
|
||||
// Verify the checksum of the config file without taking just for debugging purpose.
|
||||
const ConfigFileInfo config_file_info = check_config_file_and_verify_checksum(ifs);
|
||||
if (!config_file_info.correct_checksum)
|
||||
BOOST_LOG_TRIVIAL(info)
|
||||
<< "The configuration file " << path
|
||||
<< " has a wrong MD5 checksum or the checksum is missing. This may indicate a file corruption or a harmless user edit.";
|
||||
|
||||
if (!config_file_info.correct_checksum && config_file_info.contains_null) {
|
||||
BOOST_LOG_TRIVIAL(info) << "The configuration file " + path + " is corrupted, because it is contains null characters.";
|
||||
throw Slic3r::CriticalException("The configuration file contains null characters.");
|
||||
}
|
||||
|
||||
ifs.seekg(0, boost::nowide::ifstream::beg);
|
||||
#endif
|
||||
try {
|
||||
pt::read_ini(ifs, tree);
|
||||
} catch (pt::ptree_error &ex) {
|
||||
throw Slic3r::CriticalException(ex.what());
|
||||
}
|
||||
} catch (Slic3r::CriticalException &ex) {
|
||||
#ifdef WIN32
|
||||
// The configuration file is corrupted, try replacing it with the backup configuration.
|
||||
ifs.close();
|
||||
std::string backup_path = (boost::format("%1%.bak") % path).str();
|
||||
if (boost::filesystem::exists(backup_path)) {
|
||||
// Compute checksum of the configuration backup file and try to load configuration from it when the checksum is correct.
|
||||
boost::nowide::ifstream backup_ifs(backup_path);
|
||||
if (const ConfigFileInfo config_file_info = check_config_file_and_verify_checksum(backup_ifs); !config_file_info.correct_checksum || config_file_info.contains_null) {
|
||||
BOOST_LOG_TRIVIAL(error) << format(R"(Both "%1%" and "%2%" are corrupted. It isn't possible to restore configuration from the backup.)", path, backup_path);
|
||||
backup_ifs.close();
|
||||
boost::filesystem::remove(backup_path);
|
||||
} else if (std::string error_message; copy_file(backup_path, path, error_message, false) != SUCCESS) {
|
||||
BOOST_LOG_TRIVIAL(error) << format(R"(Configuration file "%1%" is corrupted. Failed to restore from backup "%2%": %3%)", path, backup_path, error_message);
|
||||
backup_ifs.close();
|
||||
boost::filesystem::remove(backup_path);
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(info) << format(R"(Configuration file "%1%" was corrupted. It has been successfully restored from the backup "%2%".)", path, backup_path);
|
||||
// Try parse configuration file after restore from backup.
|
||||
try {
|
||||
ifs.open(path);
|
||||
pt::read_ini(ifs, tree);
|
||||
recovered = true;
|
||||
} catch (pt::ptree_error& ex) {
|
||||
BOOST_LOG_TRIVIAL(info) << format(R"(Failed to parse configuration file "%1%" after it has been restored from backup: %2%)", path, ex.what());
|
||||
}
|
||||
}
|
||||
} else
|
||||
#endif // WIN32
|
||||
BOOST_LOG_TRIVIAL(info) << format(R"(Failed to parse configuration file "%1%": %2%)", path, ex.what());
|
||||
if (!recovered) {
|
||||
// Report the initial error of parsing PrusaSlicer.ini.
|
||||
// Error while parsing config file. We'll customize the error message and rethrow to be displayed.
|
||||
// ! But to avoid the use of _utf8 (related to use of wxWidgets)
|
||||
// we will rethrow this exception from the place of load() call, if returned value wouldn't be empty
|
||||
return ex.what();
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Parse the property_tree, extract the sections and key / value pairs.
|
||||
for (const auto §ion : tree) {
|
||||
if (section.second.empty()) {
|
||||
// This may be a top level (no section) entry, or an empty section.
|
||||
std::string data = section.second.data();
|
||||
if (! data.empty())
|
||||
// If there is a non-empty data, then it must be a top-level (without a section) config entry.
|
||||
m_storage[""][section.first] = data;
|
||||
} else if (boost::starts_with(section.first, VENDOR_PREFIX)) {
|
||||
// This is a vendor section listing enabled model / variants
|
||||
const auto vendor_name = section.first.substr(VENDOR_PREFIX.size());
|
||||
auto &vendor = m_vendors[vendor_name];
|
||||
for (const auto &kvp : section.second) {
|
||||
if (! boost::starts_with(kvp.first, MODEL_PREFIX)) { continue; }
|
||||
const auto model_name = kvp.first.substr(MODEL_PREFIX.size());
|
||||
std::vector<std::string> variants;
|
||||
if (! unescape_strings_cstyle(kvp.second.data(), variants)) { continue; }
|
||||
for (const auto &variant : variants) {
|
||||
vendor[model_name].insert(variant);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This must be a section name. Read the entries of a section.
|
||||
std::map<std::string, std::string> &storage = m_storage[section.first];
|
||||
for (auto &kvp : section.second)
|
||||
storage[kvp.first] = kvp.second.data();
|
||||
}
|
||||
}
|
||||
|
||||
// Figure out if datadir has legacy presets
|
||||
auto ini_ver = Semver::parse(get("version"));
|
||||
m_legacy_datadir = false;
|
||||
if (ini_ver) {
|
||||
m_orig_version = *ini_ver;
|
||||
// Make 1.40.0 alphas compare well
|
||||
ini_ver->set_metadata(boost::none);
|
||||
ini_ver->set_prerelease(boost::none);
|
||||
m_legacy_datadir = ini_ver < Semver(1, 40, 0);
|
||||
}
|
||||
|
||||
// Legacy conversion
|
||||
if (m_mode == EAppMode::Editor) {
|
||||
// Convert [extras] "physical_printer" to [presets] "physical_printer",
|
||||
// remove the [extras] section if it becomes empty.
|
||||
if (auto it_section = m_storage.find("extras"); it_section != m_storage.end()) {
|
||||
if (auto it_physical_printer = it_section->second.find("physical_printer"); it_physical_printer != it_section->second.end()) {
|
||||
m_storage["presets"]["physical_printer"] = it_physical_printer->second;
|
||||
it_section->second.erase(it_physical_printer);
|
||||
}
|
||||
if (it_section->second.empty())
|
||||
m_storage.erase(it_section);
|
||||
}
|
||||
}
|
||||
|
||||
// Override missing or keys with their defaults.
|
||||
this->set_defaults();
|
||||
m_dirty = false;
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string AppConfig::load()
|
||||
{
|
||||
return this->load(AppConfig::config_path());
|
||||
}
|
||||
|
||||
void AppConfig::save()
|
||||
{
|
||||
if (! is_main_thread_active())
|
||||
throw CriticalException("Calling AppConfig::save() from a worker thread!");
|
||||
|
||||
// The config is first written to a file with a PID suffix and then moved
|
||||
// to avoid race conditions with multiple instances of Slic3r
|
||||
const auto path = config_path();
|
||||
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
||||
|
||||
std::stringstream config_ss;
|
||||
if (m_mode == EAppMode::Editor)
|
||||
config_ss << "# " << Slic3r::header_slic3r_generated() << std::endl;
|
||||
else
|
||||
config_ss << "# " << Slic3r::header_gcodeviewer_generated() << std::endl;
|
||||
// Make sure the "no" category is written first.
|
||||
for (const auto& kvp : m_storage[""])
|
||||
config_ss << kvp.first << " = " << kvp.second << std::endl;
|
||||
// Write the other categories.
|
||||
for (const auto& category : m_storage) {
|
||||
if (category.first.empty())
|
||||
continue;
|
||||
config_ss << std::endl << "[" << category.first << "]" << std::endl;
|
||||
for (const auto& kvp : category.second)
|
||||
config_ss << kvp.first << " = " << kvp.second << std::endl;
|
||||
}
|
||||
// Write vendor sections
|
||||
for (const auto &vendor : m_vendors) {
|
||||
size_t size_sum = 0;
|
||||
for (const auto &model : vendor.second) { size_sum += model.second.size(); }
|
||||
if (size_sum == 0) { continue; }
|
||||
|
||||
config_ss << std::endl << "[" << VENDOR_PREFIX << vendor.first << "]" << std::endl;
|
||||
|
||||
for (const auto &model : vendor.second) {
|
||||
if (model.second.empty()) { continue; }
|
||||
const std::vector<std::string> variants(model.second.begin(), model.second.end());
|
||||
const auto escaped = escape_strings_cstyle(variants);
|
||||
config_ss << MODEL_PREFIX << model.first << " = " << escaped << std::endl;
|
||||
}
|
||||
}
|
||||
// One empty line before the MD5 sum.
|
||||
config_ss << std::endl;
|
||||
|
||||
std::string config_str = config_ss.str();
|
||||
boost::nowide::ofstream c;
|
||||
c.open(path_pid, std::ios::out | std::ios::trunc);
|
||||
c << config_str;
|
||||
#ifdef WIN32
|
||||
// WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API
|
||||
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
|
||||
// we save the config file into a backup first before moving it to the final destination.
|
||||
c << appconfig_md5_hash_line(config_str);
|
||||
#endif
|
||||
c.close();
|
||||
|
||||
#ifdef WIN32
|
||||
// Make a backup of the configuration file before copying it to the final destination.
|
||||
std::string error_message;
|
||||
std::string backup_path = (boost::format("%1%.bak") % path).str();
|
||||
// Copy configuration file with PID suffix into the configuration file with "bak" suffix.
|
||||
if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS)
|
||||
BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration.";
|
||||
#endif
|
||||
|
||||
// Rename the config atomically.
|
||||
// On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile.
|
||||
// To cope with that, we already made a backup of the config on Windows.
|
||||
rename_file(path_pid, path);
|
||||
m_dirty = false;
|
||||
}
|
||||
|
||||
bool AppConfig::erase(const std::string §ion, const std::string &key)
|
||||
{
|
||||
if (auto it_storage = m_storage.find(section); it_storage != m_storage.end()) {
|
||||
auto §ion = it_storage->second;
|
||||
auto it = section.find(key);
|
||||
if (it != section.end()) {
|
||||
section.erase(it);
|
||||
m_dirty = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AppConfig::set_section(const std::string §ion, std::map<std::string, std::string> data)
|
||||
{
|
||||
auto it_section = m_storage.find(section);
|
||||
if (it_section == m_storage.end()) {
|
||||
if (data.empty())
|
||||
return false;
|
||||
it_section = m_storage.insert({ section, {} }).first;
|
||||
}
|
||||
auto &dst = it_section->second;
|
||||
if (dst == data)
|
||||
return false;
|
||||
dst = std::move(data);
|
||||
m_dirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AppConfig::clear_section(const std::string §ion)
|
||||
{
|
||||
if (auto it_section = m_storage.find(section); it_section != m_storage.end() && ! it_section->second.empty()) {
|
||||
it_section->second.clear();
|
||||
m_dirty = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
|
||||
{
|
||||
const auto it_v = m_vendors.find(vendor);
|
||||
if (it_v == m_vendors.end()) { return false; }
|
||||
const auto it_m = it_v->second.find(model);
|
||||
return it_m == it_v->second.end() ? false : it_m->second.find(variant) != it_m->second.end();
|
||||
}
|
||||
|
||||
bool AppConfig::set_variant(const std::string &vendor, const std::string &model, const std::string &variant, bool enable)
|
||||
{
|
||||
if (enable) {
|
||||
if (get_variant(vendor, model, variant))
|
||||
return false;
|
||||
m_vendors[vendor][model].insert(variant);
|
||||
} else {
|
||||
auto it_v = m_vendors.find(vendor);
|
||||
if (it_v == m_vendors.end())
|
||||
return false;
|
||||
auto it_m = it_v->second.find(model);
|
||||
if (it_m == it_v->second.end())
|
||||
return false;
|
||||
auto it_var = it_m->second.find(variant);
|
||||
if (it_var == it_m->second.end())
|
||||
return false;
|
||||
it_m->second.erase(it_var);
|
||||
}
|
||||
// If we got here, there was an update
|
||||
m_dirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AppConfig::set_vendors(const VendorMap &vendors)
|
||||
{
|
||||
if (m_vendors != vendors) {
|
||||
m_vendors = vendors;
|
||||
m_dirty = true;
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AppConfig::set_vendors(VendorMap &&vendors)
|
||||
{
|
||||
if (m_vendors != vendors) {
|
||||
m_vendors = std::move(vendors);
|
||||
m_dirty = true;
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string AppConfig::get_last_dir() const
|
||||
{
|
||||
const auto it = m_storage.find("recent");
|
||||
if (it != m_storage.end()) {
|
||||
{
|
||||
const auto it2 = it->second.find("skein_directory");
|
||||
if (it2 != it->second.end() && ! it2->second.empty())
|
||||
return it2->second;
|
||||
}
|
||||
{
|
||||
const auto it2 = it->second.find("config_directory");
|
||||
if (it2 != it->second.end() && ! it2->second.empty())
|
||||
return it2->second;
|
||||
}
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::vector<std::string> AppConfig::get_recent_projects() const
|
||||
{
|
||||
std::vector<std::string> ret;
|
||||
const auto it = m_storage.find("recent_projects");
|
||||
if (it != m_storage.end())
|
||||
{
|
||||
for (const std::map<std::string, std::string>::value_type& item : it->second)
|
||||
{
|
||||
ret.push_back(item.second);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool AppConfig::set_recent_projects(const std::vector<std::string>& recent_projects)
|
||||
{
|
||||
static constexpr const char *section = "recent_projects";
|
||||
auto it_section = m_storage.find(section);
|
||||
if (it_section == m_storage.end()) {
|
||||
if (recent_projects.empty())
|
||||
return false;
|
||||
it_section = m_storage.insert({ std::string(section), {} }).first;
|
||||
}
|
||||
auto &dst = it_section->second;
|
||||
|
||||
std::map<std::string, std::string> src;
|
||||
for (unsigned int i = 0; i < (unsigned int)recent_projects.size(); ++i)
|
||||
src[std::to_string(i + 1)] = recent_projects[i];
|
||||
|
||||
if (src != dst) {
|
||||
dst = std::move(src);
|
||||
m_dirty = true;
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AppConfig::set_mouse_device(const std::string& name, double translation_speed, double translation_deadzone,
|
||||
float rotation_speed, float rotation_deadzone, double zoom_speed, bool swap_yz)
|
||||
{
|
||||
const std::string key = std::string("mouse_device:") + name;
|
||||
auto it_section = m_storage.find(key);
|
||||
if (it_section == m_storage.end())
|
||||
it_section = m_storage.insert({ key, {} }).first;
|
||||
auto &dst = it_section->second;
|
||||
|
||||
std::map<std::string, std::string> src;
|
||||
src["translation_speed"] = float_to_string_decimal_point(translation_speed);
|
||||
src["translation_deadzone"] = float_to_string_decimal_point(translation_deadzone);
|
||||
src["rotation_speed"] = float_to_string_decimal_point(rotation_speed);
|
||||
src["rotation_deadzone"] = float_to_string_decimal_point(rotation_deadzone);
|
||||
src["zoom_speed"] = float_to_string_decimal_point(zoom_speed);
|
||||
src["swap_yz"] = swap_yz ? "1" : "0";
|
||||
|
||||
if (src != dst) {
|
||||
dst = std::move(src);
|
||||
m_dirty = true;
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> AppConfig::get_mouse_device_names() const
|
||||
{
|
||||
static constexpr const char *prefix = "mouse_device:";
|
||||
static const size_t prefix_len = strlen(prefix);
|
||||
std::vector<std::string> out;
|
||||
for (const auto& key_value_pair : m_storage)
|
||||
if (boost::starts_with(key_value_pair.first, prefix) && key_value_pair.first.size() > prefix_len)
|
||||
out.emplace_back(key_value_pair.first.substr(prefix_len));
|
||||
return out;
|
||||
}
|
||||
|
||||
bool AppConfig::update_config_dir(const std::string &dir)
|
||||
{
|
||||
return this->set("recent", "config_directory", dir);
|
||||
}
|
||||
|
||||
bool AppConfig::update_skein_dir(const std::string &dir)
|
||||
{
|
||||
if (is_shapes_dir(dir))
|
||||
return false; // do not save "shapes gallery" directory
|
||||
return this->set("recent", "skein_directory", dir);
|
||||
}
|
||||
|
||||
std::string AppConfig::get_last_output_dir(const std::string& alt, const bool removable) const
|
||||
{
|
||||
std::string s1 = (removable ? "last_output_path_removable" : "last_output_path");
|
||||
std::string s2 = (removable ? "remember_output_path_removable" : "remember_output_path");
|
||||
const auto it = m_storage.find("");
|
||||
if (it != m_storage.end()) {
|
||||
const auto it2 = it->second.find(s1);
|
||||
const auto it3 = it->second.find(s2);
|
||||
if (it2 != it->second.end() && it3 != it->second.end() && !it2->second.empty() && it3->second == "1")
|
||||
return it2->second;
|
||||
}
|
||||
return is_shapes_dir(alt) ? get_last_dir() : alt;
|
||||
}
|
||||
|
||||
bool AppConfig::update_last_output_dir(const std::string& dir, const bool removable)
|
||||
{
|
||||
return this->set("", (removable ? "last_output_path_removable" : "last_output_path"), dir);
|
||||
}
|
||||
|
||||
|
||||
void AppConfig::reset_selections()
|
||||
{
|
||||
auto it = m_storage.find("presets");
|
||||
if (it != m_storage.end()) {
|
||||
it->second.erase("print");
|
||||
it->second.erase("filament");
|
||||
it->second.erase("sla_print");
|
||||
it->second.erase("sla_material");
|
||||
it->second.erase("printer");
|
||||
it->second.erase("physical_printer");
|
||||
m_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
std::string AppConfig::config_path() const
|
||||
{
|
||||
std::string path = (m_mode == EAppMode::Editor) ?
|
||||
(boost::filesystem::path(Slic3r::data_dir()) / (SLIC3R_APP_KEY ".ini")).make_preferred().string() :
|
||||
(boost::filesystem::path(Slic3r::data_dir()) / (GCODEVIEWER_APP_KEY ".ini")).make_preferred().string();
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
std::string AppConfig::version_check_url() const
|
||||
{
|
||||
auto from_settings = get("version_check_url");
|
||||
return from_settings.empty() ? VERSION_CHECK_URL : from_settings;
|
||||
}
|
||||
|
||||
std::string AppConfig::index_archive_url() const
|
||||
{
|
||||
#if 0
|
||||
// this code is for debug & testing purposes only - changed url wont get trough inner checks anyway.
|
||||
auto from_settings = get("index_archive_url");
|
||||
return from_settings.empty() ? INDEX_ARCHIVE_URL : from_settings;
|
||||
#endif
|
||||
return INDEX_ARCHIVE_URL;
|
||||
}
|
||||
|
||||
std::string AppConfig::profile_folder_url() const
|
||||
{
|
||||
#if 0
|
||||
// this code is for debug & testing purposes only - changed url wont get trough inner checks anyway.
|
||||
auto from_settings = get("profile_folder_url");
|
||||
return from_settings.empty() ? PROFILE_FOLDER_URL : from_settings;
|
||||
#endif
|
||||
return PROFILE_FOLDER_URL;
|
||||
}
|
||||
|
||||
bool AppConfig::exists() const
|
||||
{
|
||||
return boost::filesystem::exists(config_path());
|
||||
}
|
||||
|
||||
}; // namespace Slic3r
|
||||
@@ -0,0 +1,210 @@
|
||||
///|/ Copyright (c) Prusa Research 2017 - 2023 Vojtěch Bubník @bubnikv, David Kocík @kocikdav, Lukáš Matěna @lukasmatena, Filip Sykala @Jony01, Enrico Turri @enricoturri1966, Oleksandra Iushchenko @YuSanka, Vojtěch Král @vojtechkral
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef slic3r_AppConfig_hpp_
|
||||
#define slic3r_AppConfig_hpp_
|
||||
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include <boost/algorithm/string/trim_all.hpp>
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Semver.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class AppConfig
|
||||
{
|
||||
public:
|
||||
enum class EAppMode : unsigned char
|
||||
{
|
||||
Editor,
|
||||
GCodeViewer
|
||||
};
|
||||
|
||||
explicit AppConfig(EAppMode mode) :
|
||||
m_mode(mode)
|
||||
{
|
||||
this->reset();
|
||||
}
|
||||
|
||||
// Clear and reset to defaults.
|
||||
void reset();
|
||||
// Override missing or keys with their defaults.
|
||||
void set_defaults();
|
||||
|
||||
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
|
||||
// return error string or empty strinf
|
||||
std::string load();
|
||||
// Load from an explicit path.
|
||||
std::string load(const std::string &path);
|
||||
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
|
||||
void save();
|
||||
|
||||
// Does this config need to be saved?
|
||||
bool dirty() const { return m_dirty; }
|
||||
|
||||
// Const accessor, it will return false if a section or a key does not exist.
|
||||
bool get(const std::string §ion, const std::string &key, std::string &value) const
|
||||
{
|
||||
value.clear();
|
||||
auto it = m_storage.find(section);
|
||||
if (it == m_storage.end())
|
||||
return false;
|
||||
auto it2 = it->second.find(key);
|
||||
if (it2 == it->second.end())
|
||||
return false;
|
||||
value = it2->second;
|
||||
return true;
|
||||
}
|
||||
std::string get(const std::string §ion, const std::string &key) const
|
||||
{ std::string value; this->get(section, key, value); return value; }
|
||||
bool get_bool(const std::string §ion, const std::string &key) const
|
||||
{ return this->get(section, key) == "1"; }
|
||||
std::string get(const std::string &key) const
|
||||
{ std::string value; this->get("", key, value); return value; }
|
||||
bool get_bool(const std::string &key) const
|
||||
{ return this->get(key) == "1"; }
|
||||
bool set(const std::string §ion, const std::string &key, const std::string &value)
|
||||
{
|
||||
#ifndef NDEBUG
|
||||
{
|
||||
std::string key_trimmed = key;
|
||||
boost::trim_all(key_trimmed);
|
||||
assert(key_trimmed == key);
|
||||
assert(! key_trimmed.empty());
|
||||
}
|
||||
#endif // NDEBUG
|
||||
std::string &old = m_storage[section][key];
|
||||
if (old != value) {
|
||||
old = value;
|
||||
m_dirty = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool set(const std::string &key, const std::string &value)
|
||||
{ return this->set("", key, value); }
|
||||
bool has(const std::string §ion, const std::string &key) const
|
||||
{
|
||||
auto it = m_storage.find(section);
|
||||
if (it == m_storage.end())
|
||||
return false;
|
||||
auto it2 = it->second.find(key);
|
||||
return it2 != it->second.end() && ! it2->second.empty();
|
||||
}
|
||||
bool has(const std::string &key) const
|
||||
{ return this->has("", key); }
|
||||
|
||||
bool erase(const std::string §ion, const std::string &key);
|
||||
|
||||
bool has_section(const std::string §ion) const
|
||||
{ return m_storage.find(section) != m_storage.end(); }
|
||||
const std::map<std::string, std::string>& get_section(const std::string §ion) const
|
||||
{ auto it = m_storage.find(section); assert(it != m_storage.end()); return it->second; }
|
||||
bool set_section(const std::string §ion, std::map<std::string, std::string> data);
|
||||
bool clear_section(const std::string §ion);
|
||||
|
||||
typedef std::map<std::string, std::map<std::string, std::set<std::string>>> VendorMap;
|
||||
bool get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const;
|
||||
bool set_variant(const std::string &vendor, const std::string &model, const std::string &variant, bool enable);
|
||||
bool set_vendors(const AppConfig &from) { return this->set_vendors(from.vendors()); }
|
||||
bool set_vendors(const VendorMap &vendors);
|
||||
bool set_vendors(VendorMap &&vendors);
|
||||
const VendorMap& vendors() const { return m_vendors; }
|
||||
|
||||
// return recent/skein_directory or recent/config_directory or empty string.
|
||||
std::string get_last_dir() const;
|
||||
bool update_config_dir(const std::string &dir);
|
||||
bool update_skein_dir(const std::string &dir);
|
||||
|
||||
//std::string get_last_output_dir(const std::string &alt) const;
|
||||
//void update_last_output_dir(const std::string &dir);
|
||||
std::string get_last_output_dir(const std::string& alt, const bool removable = false) const;
|
||||
bool update_last_output_dir(const std::string &dir, const bool removable = false);
|
||||
|
||||
// reset the current print / filament / printer selections, so that
|
||||
// the PresetBundle::load_selections(const AppConfig &config) call will select
|
||||
// the first non-default preset when called.
|
||||
void reset_selections();
|
||||
|
||||
// Get the default config path from Slic3r::data_dir().
|
||||
std::string config_path() const;
|
||||
|
||||
// Returns true if the user's data directory comes from before Slic3r 1.40.0 (no updating)
|
||||
bool legacy_datadir() const { return m_legacy_datadir; }
|
||||
void set_legacy_datadir(bool value) { m_legacy_datadir = value; }
|
||||
|
||||
// Get the Slic3r version check url.
|
||||
// This returns a hardcoded string unless it is overriden by "version_check_url" in the ini file.
|
||||
std::string version_check_url() const;
|
||||
// Get the Slic3r url to vendor index archive zip.
|
||||
std::string index_archive_url() const;
|
||||
// Get the Slic3r url to folder with vendor profile files.
|
||||
std::string profile_folder_url() const;
|
||||
|
||||
|
||||
// Returns the original Slic3r version found in the ini file before it was overwritten
|
||||
// by the current version
|
||||
Semver orig_version() const { return m_orig_version; }
|
||||
|
||||
// Does the config file exist?
|
||||
bool exists() const;
|
||||
|
||||
std::vector<std::string> get_recent_projects() const;
|
||||
bool set_recent_projects(const std::vector<std::string>& recent_projects);
|
||||
|
||||
bool set_mouse_device(const std::string& name, double translation_speed, double translation_deadzone, float rotation_speed, float rotation_deadzone, double zoom_speed, bool swap_yz);
|
||||
std::vector<std::string> get_mouse_device_names() const;
|
||||
bool get_mouse_device_translation_speed(const std::string& name, double& speed) const
|
||||
{ return get_3dmouse_device_numeric_value(name, "translation_speed", speed); }
|
||||
bool get_mouse_device_translation_deadzone(const std::string& name, double& deadzone) const
|
||||
{ return get_3dmouse_device_numeric_value(name, "translation_deadzone", deadzone); }
|
||||
bool get_mouse_device_rotation_speed(const std::string& name, float& speed) const
|
||||
{ return get_3dmouse_device_numeric_value(name, "rotation_speed", speed); }
|
||||
bool get_mouse_device_rotation_deadzone(const std::string& name, float& deadzone) const
|
||||
{ return get_3dmouse_device_numeric_value(name, "rotation_deadzone", deadzone); }
|
||||
bool get_mouse_device_zoom_speed(const std::string& name, double& speed) const
|
||||
{ return get_3dmouse_device_numeric_value(name, "zoom_speed", speed); }
|
||||
bool get_mouse_device_swap_yz(const std::string& name, bool& swap) const
|
||||
{ return get_3dmouse_device_numeric_value(name, "swap_yz", swap); }
|
||||
|
||||
static const std::string SECTION_FILAMENTS;
|
||||
static const std::string SECTION_MATERIALS;
|
||||
static const std::string SECTION_EMBOSS_STYLE;
|
||||
|
||||
private:
|
||||
template<typename T>
|
||||
bool get_3dmouse_device_numeric_value(const std::string &device_name, const char *parameter_name, T &out) const
|
||||
{
|
||||
std::string key = std::string("mouse_device:") + device_name;
|
||||
auto it = m_storage.find(key);
|
||||
if (it == m_storage.end())
|
||||
return false;
|
||||
auto it_val = it->second.find(parameter_name);
|
||||
if (it_val == it->second.end())
|
||||
return false;
|
||||
out = T(string_to_double_decimal_point(it_val->second));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Type of application: Editor or GCodeViewer
|
||||
EAppMode m_mode { EAppMode::Editor };
|
||||
// Map of section, name -> value
|
||||
std::map<std::string, std::map<std::string, std::string>> m_storage;
|
||||
// Map of enabled vendors / models / variants
|
||||
VendorMap m_vendors;
|
||||
// Has any value been modified since the config.ini has been last saved or loaded?
|
||||
bool m_dirty;
|
||||
// Original version found in the ini file before it was overwritten
|
||||
Semver m_orig_version;
|
||||
// Whether the existing version is before system profiles & configuration updating
|
||||
bool m_legacy_datadir;
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_AppConfig_hpp_ */
|
||||
@@ -0,0 +1,79 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "BeadingStrategy.hpp"
|
||||
#include "Point.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
BeadingStrategy::BeadingStrategy(coord_t optimal_width, double wall_split_middle_threshold, double wall_add_middle_threshold, coord_t default_transition_length, float transitioning_angle)
|
||||
: optimal_width(optimal_width)
|
||||
, wall_split_middle_threshold(wall_split_middle_threshold)
|
||||
, wall_add_middle_threshold(wall_add_middle_threshold)
|
||||
, default_transition_length(default_transition_length)
|
||||
, transitioning_angle(transitioning_angle)
|
||||
{
|
||||
name = "Unknown";
|
||||
}
|
||||
|
||||
BeadingStrategy::BeadingStrategy(const BeadingStrategy &other)
|
||||
: optimal_width(other.optimal_width)
|
||||
, wall_split_middle_threshold(other.wall_split_middle_threshold)
|
||||
, wall_add_middle_threshold(other.wall_add_middle_threshold)
|
||||
, default_transition_length(other.default_transition_length)
|
||||
, transitioning_angle(other.transitioning_angle)
|
||||
, name(other.name)
|
||||
{}
|
||||
|
||||
coord_t BeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const
|
||||
{
|
||||
if (lower_bead_count == 0)
|
||||
return scaled<coord_t>(0.01);
|
||||
return default_transition_length;
|
||||
}
|
||||
|
||||
float BeadingStrategy::getTransitionAnchorPos(coord_t lower_bead_count) const
|
||||
{
|
||||
coord_t lower_optimum = getOptimalThickness(lower_bead_count);
|
||||
coord_t transition_point = getTransitionThickness(lower_bead_count);
|
||||
coord_t upper_optimum = getOptimalThickness(lower_bead_count + 1);
|
||||
return 1.0 - float(transition_point - lower_optimum) / float(upper_optimum - lower_optimum);
|
||||
}
|
||||
|
||||
std::vector<coord_t> BeadingStrategy::getNonlinearThicknesses(coord_t lower_bead_count) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string BeadingStrategy::toString() const
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
double BeadingStrategy::getSplitMiddleThreshold() const
|
||||
{
|
||||
return wall_split_middle_threshold;
|
||||
}
|
||||
|
||||
double BeadingStrategy::getTransitioningAngle() const
|
||||
{
|
||||
return transitioning_angle;
|
||||
}
|
||||
|
||||
coord_t BeadingStrategy::getOptimalThickness(coord_t bead_count) const
|
||||
{
|
||||
return optimal_width * bead_count;
|
||||
}
|
||||
|
||||
coord_t BeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const
|
||||
{
|
||||
const coord_t lower_ideal_width = getOptimalThickness(lower_bead_count);
|
||||
const coord_t higher_ideal_width = getOptimalThickness(lower_bead_count + 1);
|
||||
const double threshold = lower_bead_count % 2 == 1 ? wall_split_middle_threshold : wall_add_middle_threshold;
|
||||
return lower_ideal_width + threshold * (higher_ideal_width - lower_ideal_width);
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2022 Ultimaker B.V.
|
||||
// CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef BEADING_STRATEGY_H
|
||||
#define BEADING_STRATEGY_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "../../libslic3r.h"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
template<typename T> constexpr T pi_div(const T div) { return static_cast<T>(M_PI) / div; }
|
||||
|
||||
/*!
|
||||
* Mostly virtual base class template.
|
||||
*
|
||||
* Strategy for covering a given (constant) horizontal model thickness with a number of beads.
|
||||
*
|
||||
* The beads may have different widths.
|
||||
*
|
||||
* TODO: extend with printing order?
|
||||
*/
|
||||
class BeadingStrategy
|
||||
{
|
||||
public:
|
||||
/*!
|
||||
* The beading for a given horizontal model thickness.
|
||||
*/
|
||||
struct Beading
|
||||
{
|
||||
coord_t total_thickness;
|
||||
std::vector<coord_t> bead_widths; //! The line width of each bead from the outer inset inward
|
||||
std::vector<coord_t> toolpath_locations; //! The distance of the toolpath location of each bead from the outline
|
||||
coord_t left_over; //! The distance not covered by any bead; gap area.
|
||||
};
|
||||
|
||||
BeadingStrategy(coord_t optimal_width, double wall_split_middle_threshold, double wall_add_middle_threshold, coord_t default_transition_length, float transitioning_angle = pi_div(3));
|
||||
|
||||
BeadingStrategy(const BeadingStrategy &other);
|
||||
|
||||
virtual ~BeadingStrategy() = default;
|
||||
|
||||
/*!
|
||||
* Retrieve the bead widths with which to cover a given thickness.
|
||||
*
|
||||
* Requirement: Given a constant \p bead_count the output of each bead width must change gradually along with the \p thickness.
|
||||
*
|
||||
* \note The \p bead_count might be different from the \ref BeadingStrategy::optimal_bead_count
|
||||
*/
|
||||
virtual Beading compute(coord_t thickness, coord_t bead_count) const = 0;
|
||||
|
||||
/*!
|
||||
* The ideal thickness for a given \param bead_count
|
||||
*/
|
||||
virtual coord_t getOptimalThickness(coord_t bead_count) const;
|
||||
|
||||
/*!
|
||||
* The model thickness at which \ref BeadingStrategy::optimal_bead_count transitions from \p lower_bead_count to \p lower_bead_count + 1
|
||||
*/
|
||||
virtual coord_t getTransitionThickness(coord_t lower_bead_count) const;
|
||||
|
||||
/*!
|
||||
* The number of beads should we ideally usefor a given model thickness
|
||||
*/
|
||||
virtual coord_t getOptimalBeadCount(coord_t thickness) const = 0;
|
||||
|
||||
/*!
|
||||
* The length of the transitioning region along the marked / significant regions of the skeleton.
|
||||
*
|
||||
* Transitions are used to smooth out the jumps in integer bead count; the jumps turn into ramps with some incline defined by their length.
|
||||
*/
|
||||
virtual coord_t getTransitioningLength(coord_t lower_bead_count) const;
|
||||
|
||||
/*!
|
||||
* The fraction of the transition length to put between the lower end of the transition and the point where the unsmoothed bead count jumps.
|
||||
*
|
||||
* Transitions are used to smooth out the jumps in integer bead count; the jumps turn into ramps which could be positioned relative to the jump location.
|
||||
*/
|
||||
virtual float getTransitionAnchorPos(coord_t lower_bead_count) const;
|
||||
|
||||
/*!
|
||||
* Get the locations in a bead count region where \ref BeadingStrategy::compute exhibits a bend in the widths.
|
||||
* Ordered from lower thickness to higher.
|
||||
*
|
||||
* This is used to insert extra support bones into the skeleton, so that the resulting beads in long trapezoids don't linearly change between the two ends.
|
||||
*/
|
||||
virtual std::vector<coord_t> getNonlinearThicknesses(coord_t lower_bead_count) const;
|
||||
|
||||
virtual std::string toString() const;
|
||||
|
||||
double getSplitMiddleThreshold() const;
|
||||
double getTransitioningAngle() const;
|
||||
|
||||
protected:
|
||||
std::string name;
|
||||
|
||||
coord_t optimal_width; //! Optimal bead width, nominal width off the walls in 'ideal' circumstances.
|
||||
|
||||
double wall_split_middle_threshold; //! Threshold when a middle wall should be split into two, as a ratio of the optimal wall width.
|
||||
|
||||
double wall_add_middle_threshold; //! Threshold when a new middle wall should be added between an even number of walls, as a ratio of the optimal wall width.
|
||||
|
||||
coord_t default_transition_length; //! The length of the region to smoothly transfer between bead counts
|
||||
|
||||
/*!
|
||||
* The maximum angle between outline segments smaller than which we are going to add transitions
|
||||
* Equals 180 - the "limit bisector angle" from the paper
|
||||
*/
|
||||
double transitioning_angle;
|
||||
};
|
||||
|
||||
using BeadingStrategyPtr = std::unique_ptr<BeadingStrategy>;
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // BEADING_STRATEGY_H
|
||||
@@ -0,0 +1,55 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include "BeadingStrategyFactory.hpp"
|
||||
|
||||
#include "LimitedBeadingStrategy.hpp"
|
||||
#include "WideningBeadingStrategy.hpp"
|
||||
#include "DistributedBeadingStrategy.hpp"
|
||||
#include "RedistributeBeadingStrategy.hpp"
|
||||
#include "OuterWallInsetBeadingStrategy.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
namespace Slic3r::Arachne {
|
||||
|
||||
BeadingStrategyPtr BeadingStrategyFactory::makeStrategy(const coord_t preferred_bead_width_outer,
|
||||
const coord_t preferred_bead_width_inner,
|
||||
const coord_t preferred_transition_length,
|
||||
const float transitioning_angle,
|
||||
const bool print_thin_walls,
|
||||
const coord_t min_bead_width,
|
||||
const coord_t min_feature_size,
|
||||
const double wall_split_middle_threshold,
|
||||
const double wall_add_middle_threshold,
|
||||
const coord_t max_bead_count,
|
||||
const coord_t outer_wall_offset,
|
||||
const int inward_distributed_center_wall_count,
|
||||
const double minimum_variable_line_ratio)
|
||||
{
|
||||
// Handle a special case when there is just one external perimeter.
|
||||
// Because big differences in bead width for inner and other perimeters cause issues with current beading strategies.
|
||||
const coord_t optimal_width = max_bead_count <= 2 ? preferred_bead_width_outer : preferred_bead_width_inner;
|
||||
BeadingStrategyPtr ret = std::make_unique<DistributedBeadingStrategy>(optimal_width, preferred_transition_length, transitioning_angle,
|
||||
wall_split_middle_threshold, wall_add_middle_threshold,
|
||||
inward_distributed_center_wall_count);
|
||||
|
||||
BOOST_LOG_TRIVIAL(trace) << "Applying the Redistribute meta-strategy with outer-wall width = " << preferred_bead_width_outer << ", inner-wall width = " << preferred_bead_width_inner << ".";
|
||||
ret = std::make_unique<RedistributeBeadingStrategy>(preferred_bead_width_outer, minimum_variable_line_ratio, std::move(ret));
|
||||
|
||||
if (print_thin_walls) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "Applying the Widening Beading meta-strategy with minimum input width " << min_feature_size << " and minimum output width " << min_bead_width << ".";
|
||||
ret = std::make_unique<WideningBeadingStrategy>(std::move(ret), min_feature_size, min_bead_width);
|
||||
}
|
||||
|
||||
if (outer_wall_offset > 0) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "Applying the OuterWallOffset meta-strategy with offset = " << outer_wall_offset << ".";
|
||||
ret = std::make_unique<OuterWallInsetBeadingStrategy>(outer_wall_offset, std::move(ret));
|
||||
}
|
||||
|
||||
// Apply the LimitedBeadingStrategy last, since that adds a 0-width marker wall which other beading strategies shouldn't touch.
|
||||
BOOST_LOG_TRIVIAL(trace) << "Applying the Limited Beading meta-strategy with maximum bead count = " << max_bead_count << ".";
|
||||
ret = std::make_unique<LimitedBeadingStrategy>(max_bead_count, std::move(ret));
|
||||
return ret;
|
||||
}
|
||||
} // namespace Slic3r::Arachne
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2022 Ultimaker B.V.
|
||||
// CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef BEADING_STRATEGY_FACTORY_H
|
||||
#define BEADING_STRATEGY_FACTORY_H
|
||||
|
||||
#include "BeadingStrategy.hpp"
|
||||
#include "../../Point.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
class BeadingStrategyFactory
|
||||
{
|
||||
public:
|
||||
static BeadingStrategyPtr makeStrategy
|
||||
(
|
||||
coord_t preferred_bead_width_outer = scaled<coord_t>(0.0005),
|
||||
coord_t preferred_bead_width_inner = scaled<coord_t>(0.0005),
|
||||
coord_t preferred_transition_length = scaled<coord_t>(0.0004),
|
||||
float transitioning_angle = M_PI / 4.0,
|
||||
bool print_thin_walls = false,
|
||||
coord_t min_bead_width = 0,
|
||||
coord_t min_feature_size = 0,
|
||||
double wall_split_middle_threshold = 0.5,
|
||||
double wall_add_middle_threshold = 0.5,
|
||||
coord_t max_bead_count = 0,
|
||||
coord_t outer_wall_offset = 0,
|
||||
int inward_distributed_center_wall_count = 2,
|
||||
double minimum_variable_line_width = 0.5
|
||||
);
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // BEADING_STRATEGY_FACTORY_H
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2022 Ultimaker B.V.
|
||||
// CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
#include <numeric>
|
||||
#include "DistributedBeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
DistributedBeadingStrategy::DistributedBeadingStrategy(const coord_t optimal_width,
|
||||
const coord_t default_transition_length,
|
||||
const double transitioning_angle,
|
||||
const double wall_split_middle_threshold,
|
||||
const double wall_add_middle_threshold,
|
||||
const int distribution_radius)
|
||||
: BeadingStrategy(optimal_width, wall_split_middle_threshold, wall_add_middle_threshold, default_transition_length, transitioning_angle)
|
||||
{
|
||||
if(distribution_radius >= 2)
|
||||
one_over_distribution_radius_squared = 1.0f / (distribution_radius - 1) * 1.0f / (distribution_radius - 1);
|
||||
else
|
||||
one_over_distribution_radius_squared = 1.0f / 1 * 1.0f / 1;
|
||||
name = "DistributedBeadingStrategy";
|
||||
}
|
||||
|
||||
DistributedBeadingStrategy::Beading DistributedBeadingStrategy::compute(const coord_t thickness, const coord_t bead_count) const
|
||||
{
|
||||
Beading ret;
|
||||
|
||||
ret.total_thickness = thickness;
|
||||
if (bead_count > 2) {
|
||||
const coord_t to_be_divided = thickness - bead_count * optimal_width;
|
||||
const float middle = static_cast<float>(bead_count - 1) / 2;
|
||||
|
||||
const auto getWeight = [middle, this](coord_t bead_idx) {
|
||||
const float dev_from_middle = bead_idx - middle;
|
||||
return std::max(0.0f, 1.0f - one_over_distribution_radius_squared * dev_from_middle * dev_from_middle);
|
||||
};
|
||||
|
||||
std::vector<float> weights;
|
||||
weights.resize(bead_count);
|
||||
for (coord_t bead_idx = 0; bead_idx < bead_count; bead_idx++)
|
||||
weights[bead_idx] = getWeight(bead_idx);
|
||||
|
||||
const float total_weight = std::accumulate(weights.cbegin(), weights.cend(), 0.f);
|
||||
coord_t accumulated_width = 0;
|
||||
for (coord_t bead_idx = 0; bead_idx < bead_count; bead_idx++) {
|
||||
const float weight_fraction = weights[bead_idx] / total_weight;
|
||||
const coord_t splitup_left_over_weight = to_be_divided * weight_fraction;
|
||||
const coord_t width = (bead_idx == bead_count - 1) ? thickness - accumulated_width : optimal_width + splitup_left_over_weight;
|
||||
|
||||
// Be aware that toolpath_locations is computed by dividing the width by 2, so toolpath_locations
|
||||
// could be off by 1 because of rounding errors.
|
||||
if (bead_idx == 0)
|
||||
ret.toolpath_locations.emplace_back(width / 2);
|
||||
else
|
||||
ret.toolpath_locations.emplace_back(ret.toolpath_locations.back() + (ret.bead_widths.back() + width) / 2);
|
||||
ret.bead_widths.emplace_back(width);
|
||||
accumulated_width += width;
|
||||
}
|
||||
ret.left_over = 0;
|
||||
assert((accumulated_width + ret.left_over) == thickness);
|
||||
} else if (bead_count == 2) {
|
||||
const coord_t outer_width = thickness / 2;
|
||||
ret.bead_widths.emplace_back(outer_width);
|
||||
ret.bead_widths.emplace_back(outer_width);
|
||||
ret.toolpath_locations.emplace_back(outer_width / 2);
|
||||
ret.toolpath_locations.emplace_back(thickness - outer_width / 2);
|
||||
ret.left_over = 0;
|
||||
} else if (bead_count == 1) {
|
||||
const coord_t outer_width = thickness;
|
||||
ret.bead_widths.emplace_back(outer_width);
|
||||
ret.toolpath_locations.emplace_back(outer_width / 2);
|
||||
ret.left_over = 0;
|
||||
} else {
|
||||
ret.left_over = thickness;
|
||||
}
|
||||
|
||||
assert(([&ret = std::as_const(ret), thickness]() -> bool {
|
||||
coord_t total_bead_width = 0;
|
||||
for (const coord_t &bead_width : ret.bead_widths)
|
||||
total_bead_width += bead_width;
|
||||
return (total_bead_width + ret.left_over) == thickness;
|
||||
}()));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
coord_t DistributedBeadingStrategy::getOptimalBeadCount(coord_t thickness) const
|
||||
{
|
||||
const coord_t naive_count = thickness / optimal_width; // How many lines we can fit in for sure.
|
||||
const coord_t remainder = thickness - naive_count * optimal_width; // Space left after fitting that many lines.
|
||||
const coord_t minimum_line_width = optimal_width * (naive_count % 2 == 1 ? wall_split_middle_threshold : wall_add_middle_threshold);
|
||||
return naive_count + (remainder >= minimum_line_width); // If there's enough space, fit an extra one.
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2022 Ultimaker B.V.
|
||||
// CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef DISTRIBUTED_BEADING_STRATEGY_H
|
||||
#define DISTRIBUTED_BEADING_STRATEGY_H
|
||||
|
||||
#include "BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
/*!
|
||||
* This beading strategy chooses a wall count that would make the line width
|
||||
* deviate the least from the optimal line width, and then distributes the lines
|
||||
* evenly among the thickness available.
|
||||
*/
|
||||
class DistributedBeadingStrategy : public BeadingStrategy
|
||||
{
|
||||
protected:
|
||||
float one_over_distribution_radius_squared; // (1 / distribution_radius)^2
|
||||
|
||||
public:
|
||||
/*!
|
||||
* \param distribution_radius the radius (in number of beads) over which to distribute the discrepancy between the feature size and the optimal thickness
|
||||
*/
|
||||
DistributedBeadingStrategy(coord_t optimal_width,
|
||||
coord_t default_transition_length,
|
||||
double transitioning_angle,
|
||||
double wall_split_middle_threshold,
|
||||
double wall_add_middle_threshold,
|
||||
int distribution_radius);
|
||||
|
||||
~DistributedBeadingStrategy() override = default;
|
||||
|
||||
Beading compute(coord_t thickness, coord_t bead_count) const override;
|
||||
coord_t getOptimalBeadCount(coord_t thickness) const override;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // DISTRIBUTED_BEADING_STRATEGY_H
|
||||
@@ -0,0 +1,126 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include <cassert>
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include "LimitedBeadingStrategy.hpp"
|
||||
#include "Point.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
std::string LimitedBeadingStrategy::toString() const
|
||||
{
|
||||
return std::string("LimitedBeadingStrategy+") + parent->toString();
|
||||
}
|
||||
|
||||
coord_t LimitedBeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const
|
||||
{
|
||||
return parent->getTransitioningLength(lower_bead_count);
|
||||
}
|
||||
|
||||
float LimitedBeadingStrategy::getTransitionAnchorPos(coord_t lower_bead_count) const
|
||||
{
|
||||
return parent->getTransitionAnchorPos(lower_bead_count);
|
||||
}
|
||||
|
||||
LimitedBeadingStrategy::LimitedBeadingStrategy(const coord_t max_bead_count, BeadingStrategyPtr parent)
|
||||
: BeadingStrategy(*parent)
|
||||
, max_bead_count(max_bead_count)
|
||||
, parent(std::move(parent))
|
||||
{
|
||||
if (max_bead_count % 2 == 1)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << "LimitedBeadingStrategy with odd bead count is odd indeed!";
|
||||
}
|
||||
}
|
||||
|
||||
LimitedBeadingStrategy::Beading LimitedBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const
|
||||
{
|
||||
if (bead_count <= max_bead_count)
|
||||
{
|
||||
Beading ret = parent->compute(thickness, bead_count);
|
||||
bead_count = ret.toolpath_locations.size();
|
||||
|
||||
if (bead_count % 2 == 0 && bead_count == max_bead_count)
|
||||
{
|
||||
const coord_t innermost_toolpath_location = ret.toolpath_locations[max_bead_count / 2 - 1];
|
||||
const coord_t innermost_toolpath_width = ret.bead_widths[max_bead_count / 2 - 1];
|
||||
ret.toolpath_locations.insert(ret.toolpath_locations.begin() + max_bead_count / 2, innermost_toolpath_location + innermost_toolpath_width / 2);
|
||||
ret.bead_widths.insert(ret.bead_widths.begin() + max_bead_count / 2, 0);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
assert(bead_count == max_bead_count + 1);
|
||||
if(bead_count != max_bead_count + 1)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << "Too many beads! " << bead_count << " != " << max_bead_count + 1;
|
||||
}
|
||||
|
||||
coord_t optimal_thickness = parent->getOptimalThickness(max_bead_count);
|
||||
Beading ret = parent->compute(optimal_thickness, max_bead_count);
|
||||
bead_count = ret.toolpath_locations.size();
|
||||
ret.left_over += thickness - ret.total_thickness;
|
||||
ret.total_thickness = thickness;
|
||||
|
||||
// Enforce symmetry
|
||||
if (bead_count % 2 == 1) {
|
||||
ret.toolpath_locations[bead_count / 2] = thickness / 2;
|
||||
ret.bead_widths[bead_count / 2] = thickness - optimal_thickness;
|
||||
}
|
||||
for (coord_t bead_idx = 0; bead_idx < (bead_count + 1) / 2; bead_idx++)
|
||||
ret.toolpath_locations[bead_count - 1 - bead_idx] = thickness - ret.toolpath_locations[bead_idx];
|
||||
|
||||
//Create a "fake" inner wall with 0 width to indicate the edge of the walled area.
|
||||
//This wall can then be used by other structures to e.g. fill the infill area adjacent to the variable-width walls.
|
||||
coord_t innermost_toolpath_location = ret.toolpath_locations[max_bead_count / 2 - 1];
|
||||
coord_t innermost_toolpath_width = ret.bead_widths[max_bead_count / 2 - 1];
|
||||
ret.toolpath_locations.insert(ret.toolpath_locations.begin() + max_bead_count / 2, innermost_toolpath_location + innermost_toolpath_width / 2);
|
||||
ret.bead_widths.insert(ret.bead_widths.begin() + max_bead_count / 2, 0);
|
||||
|
||||
//Symmetry on both sides. Symmetry is guaranteed since this code is stopped early if the bead_count <= max_bead_count, and never reaches this point then.
|
||||
const size_t opposite_bead = bead_count - (max_bead_count / 2 - 1);
|
||||
innermost_toolpath_location = ret.toolpath_locations[opposite_bead];
|
||||
innermost_toolpath_width = ret.bead_widths[opposite_bead];
|
||||
ret.toolpath_locations.insert(ret.toolpath_locations.begin() + opposite_bead, innermost_toolpath_location - innermost_toolpath_width / 2);
|
||||
ret.bead_widths.insert(ret.bead_widths.begin() + opposite_bead, 0);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
coord_t LimitedBeadingStrategy::getOptimalThickness(coord_t bead_count) const
|
||||
{
|
||||
if (bead_count <= max_bead_count)
|
||||
return parent->getOptimalThickness(bead_count);
|
||||
assert(false);
|
||||
return scaled<coord_t>(1000.); // 1 meter (Cura was returning 10 meter)
|
||||
}
|
||||
|
||||
coord_t LimitedBeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const
|
||||
{
|
||||
if (lower_bead_count < max_bead_count)
|
||||
return parent->getTransitionThickness(lower_bead_count);
|
||||
|
||||
if (lower_bead_count == max_bead_count)
|
||||
return parent->getOptimalThickness(lower_bead_count + 1) - scaled<coord_t>(0.01);
|
||||
|
||||
assert(false);
|
||||
return scaled<coord_t>(900.); // 0.9 meter;
|
||||
}
|
||||
|
||||
coord_t LimitedBeadingStrategy::getOptimalBeadCount(coord_t thickness) const
|
||||
{
|
||||
coord_t parent_bead_count = parent->getOptimalBeadCount(thickness);
|
||||
if (parent_bead_count <= max_bead_count) {
|
||||
return parent->getOptimalBeadCount(thickness);
|
||||
} else if (parent_bead_count == max_bead_count + 1) {
|
||||
if (thickness < parent->getOptimalThickness(max_bead_count + 1) - scaled<coord_t>(0.01))
|
||||
return max_bead_count;
|
||||
else
|
||||
return max_bead_count + 1;
|
||||
}
|
||||
else return max_bead_count + 1;
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
@@ -0,0 +1,49 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef LIMITED_BEADING_STRATEGY_H
|
||||
#define LIMITED_BEADING_STRATEGY_H
|
||||
|
||||
#include "BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
/*!
|
||||
* This is a meta-strategy that can be applied on top of any other beading
|
||||
* strategy, which limits the thickness of the walls to the thickness that the
|
||||
* lines can reasonably print.
|
||||
*
|
||||
* The width of the wall is limited to the maximum number of contours times the
|
||||
* maximum width of each of these contours.
|
||||
*
|
||||
* If the width of the wall gets limited, this strategy outputs one additional
|
||||
* bead with 0 width. This bead is used to denote the limits of the walled area.
|
||||
* Other structures can then use this border to align their structures to, such
|
||||
* as to create correctly overlapping infill or skin, or to align the infill
|
||||
* pattern to any extra infill walls.
|
||||
*/
|
||||
class LimitedBeadingStrategy : public BeadingStrategy
|
||||
{
|
||||
public:
|
||||
LimitedBeadingStrategy(coord_t max_bead_count, BeadingStrategyPtr parent);
|
||||
|
||||
~LimitedBeadingStrategy() override = default;
|
||||
|
||||
Beading compute(coord_t thickness, coord_t bead_count) const override;
|
||||
coord_t getOptimalThickness(coord_t bead_count) const override;
|
||||
coord_t getTransitionThickness(coord_t lower_bead_count) const override;
|
||||
coord_t getOptimalBeadCount(coord_t thickness) const override;
|
||||
std::string toString() const override;
|
||||
|
||||
coord_t getTransitioningLength(coord_t lower_bead_count) const override;
|
||||
|
||||
float getTransitionAnchorPos(coord_t lower_bead_count) const override;
|
||||
|
||||
protected:
|
||||
const coord_t max_bead_count;
|
||||
const BeadingStrategyPtr parent;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // LIMITED_DISTRIBUTED_BEADING_STRATEGY_H
|
||||
@@ -0,0 +1,59 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include "OuterWallInsetBeadingStrategy.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
OuterWallInsetBeadingStrategy::OuterWallInsetBeadingStrategy(coord_t outer_wall_offset, BeadingStrategyPtr parent)
|
||||
: BeadingStrategy(*parent), parent(std::move(parent)), outer_wall_offset(outer_wall_offset)
|
||||
{
|
||||
name = "OuterWallOfsetBeadingStrategy";
|
||||
}
|
||||
|
||||
coord_t OuterWallInsetBeadingStrategy::getOptimalThickness(coord_t bead_count) const
|
||||
{
|
||||
return parent->getOptimalThickness(bead_count);
|
||||
}
|
||||
|
||||
coord_t OuterWallInsetBeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const
|
||||
{
|
||||
return parent->getTransitionThickness(lower_bead_count);
|
||||
}
|
||||
|
||||
coord_t OuterWallInsetBeadingStrategy::getOptimalBeadCount(coord_t thickness) const
|
||||
{
|
||||
return parent->getOptimalBeadCount(thickness);
|
||||
}
|
||||
|
||||
coord_t OuterWallInsetBeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const
|
||||
{
|
||||
return parent->getTransitioningLength(lower_bead_count);
|
||||
}
|
||||
|
||||
std::string OuterWallInsetBeadingStrategy::toString() const
|
||||
{
|
||||
return std::string("OuterWallOfsetBeadingStrategy+") + parent->toString();
|
||||
}
|
||||
|
||||
BeadingStrategy::Beading OuterWallInsetBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const
|
||||
{
|
||||
Beading ret = parent->compute(thickness, bead_count);
|
||||
|
||||
// Actual count and thickness as represented by extant walls. Don't count any potential zero-width 'signaling' walls.
|
||||
bead_count = std::count_if(ret.bead_widths.begin(), ret.bead_widths.end(), [](const coord_t width) { return width > 0; });
|
||||
|
||||
// No need to apply any inset if there is just a single wall.
|
||||
if (bead_count < 2)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Actually move the outer wall inside. Ensure that the outer wall never goes beyond the middle line.
|
||||
ret.toolpath_locations[0] = std::min(ret.toolpath_locations[0] + outer_wall_offset, thickness / 2);
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
@@ -0,0 +1,35 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef OUTER_WALL_INSET_BEADING_STRATEGY_H
|
||||
#define OUTER_WALL_INSET_BEADING_STRATEGY_H
|
||||
|
||||
#include "BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
/*
|
||||
* This is a meta strategy that allows for the outer wall to be inset towards the inside of the model.
|
||||
*/
|
||||
class OuterWallInsetBeadingStrategy : public BeadingStrategy
|
||||
{
|
||||
public:
|
||||
OuterWallInsetBeadingStrategy(coord_t outer_wall_offset, BeadingStrategyPtr parent);
|
||||
|
||||
~OuterWallInsetBeadingStrategy() override = default;
|
||||
|
||||
Beading compute(coord_t thickness, coord_t bead_count) const override;
|
||||
|
||||
coord_t getOptimalThickness(coord_t bead_count) const override;
|
||||
coord_t getTransitionThickness(coord_t lower_bead_count) const override;
|
||||
coord_t getOptimalBeadCount(coord_t thickness) const override;
|
||||
coord_t getTransitioningLength(coord_t lower_bead_count) const override;
|
||||
|
||||
std::string toString() const override;
|
||||
|
||||
private:
|
||||
BeadingStrategyPtr parent;
|
||||
coord_t outer_wall_offset;
|
||||
};
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // OUTER_WALL_INSET_BEADING_STRATEGY_H
|
||||
@@ -0,0 +1,97 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include "RedistributeBeadingStrategy.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <numeric>
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
RedistributeBeadingStrategy::RedistributeBeadingStrategy(const coord_t optimal_width_outer,
|
||||
const double minimum_variable_line_ratio,
|
||||
BeadingStrategyPtr parent)
|
||||
: BeadingStrategy(*parent)
|
||||
, parent(std::move(parent))
|
||||
, optimal_width_outer(optimal_width_outer)
|
||||
, minimum_variable_line_ratio(minimum_variable_line_ratio)
|
||||
{
|
||||
name = "RedistributeBeadingStrategy";
|
||||
}
|
||||
|
||||
coord_t RedistributeBeadingStrategy::getOptimalThickness(coord_t bead_count) const
|
||||
{
|
||||
const coord_t inner_bead_count = std::max(static_cast<coord_t>(0), bead_count - 2);
|
||||
const coord_t outer_bead_count = bead_count - inner_bead_count;
|
||||
return parent->getOptimalThickness(inner_bead_count) + optimal_width_outer * outer_bead_count;
|
||||
}
|
||||
|
||||
coord_t RedistributeBeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const
|
||||
{
|
||||
switch (lower_bead_count) {
|
||||
case 0: return minimum_variable_line_ratio * optimal_width_outer;
|
||||
case 1: return (1.0 + parent->getSplitMiddleThreshold()) * optimal_width_outer;
|
||||
default: return parent->getTransitionThickness(lower_bead_count - 2) + 2 * optimal_width_outer;
|
||||
}
|
||||
}
|
||||
|
||||
coord_t RedistributeBeadingStrategy::getOptimalBeadCount(coord_t thickness) const
|
||||
{
|
||||
if (thickness < minimum_variable_line_ratio * optimal_width_outer)
|
||||
return 0;
|
||||
if (thickness <= 2 * optimal_width_outer)
|
||||
return thickness > (1.0 + parent->getSplitMiddleThreshold()) * optimal_width_outer ? 2 : 1;
|
||||
return parent->getOptimalBeadCount(thickness - 2 * optimal_width_outer) + 2;
|
||||
}
|
||||
|
||||
coord_t RedistributeBeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const
|
||||
{
|
||||
return parent->getTransitioningLength(lower_bead_count);
|
||||
}
|
||||
|
||||
float RedistributeBeadingStrategy::getTransitionAnchorPos(coord_t lower_bead_count) const
|
||||
{
|
||||
return parent->getTransitionAnchorPos(lower_bead_count);
|
||||
}
|
||||
|
||||
std::string RedistributeBeadingStrategy::toString() const
|
||||
{
|
||||
return std::string("RedistributeBeadingStrategy+") + parent->toString();
|
||||
}
|
||||
|
||||
BeadingStrategy::Beading RedistributeBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const
|
||||
{
|
||||
Beading ret;
|
||||
|
||||
// Take care of all situations in which no lines are actually produced:
|
||||
if (bead_count == 0 || thickness < minimum_variable_line_ratio * optimal_width_outer) {
|
||||
ret.left_over = thickness;
|
||||
ret.total_thickness = thickness;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Compute the beadings of the inner walls, if any:
|
||||
const coord_t inner_bead_count = bead_count - 2;
|
||||
const coord_t inner_thickness = thickness - 2 * optimal_width_outer;
|
||||
if (inner_bead_count > 0 && inner_thickness > 0) {
|
||||
ret = parent->compute(inner_thickness, inner_bead_count);
|
||||
for (auto &toolpath_location : ret.toolpath_locations) toolpath_location += optimal_width_outer;
|
||||
}
|
||||
|
||||
// Insert the outer wall(s) around the previously computed inner wall(s), which may be empty:
|
||||
const coord_t actual_outer_thickness = bead_count > 2 ? std::min(thickness / 2, optimal_width_outer) : thickness / bead_count;
|
||||
ret.bead_widths.insert(ret.bead_widths.begin(), actual_outer_thickness);
|
||||
ret.toolpath_locations.insert(ret.toolpath_locations.begin(), actual_outer_thickness / 2);
|
||||
if (bead_count > 1) {
|
||||
ret.bead_widths.push_back(actual_outer_thickness);
|
||||
ret.toolpath_locations.push_back(thickness - actual_outer_thickness / 2);
|
||||
}
|
||||
|
||||
// Ensure correct total and left over thickness.
|
||||
ret.total_thickness = thickness;
|
||||
ret.left_over = thickness - std::accumulate(ret.bead_widths.cbegin(), ret.bead_widths.cend(), static_cast<coord_t>(0));
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
@@ -0,0 +1,56 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef REDISTRIBUTE_DISTRIBUTED_BEADING_STRATEGY_H
|
||||
#define REDISTRIBUTE_DISTRIBUTED_BEADING_STRATEGY_H
|
||||
|
||||
#include "BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
/*!
|
||||
* A meta-beading-strategy that takes outer and inner wall widths into account.
|
||||
*
|
||||
* The outer wall will try to keep a constant width by only applying the beading strategy on the inner walls. This
|
||||
* ensures that this outer wall doesn't react to changes happening to inner walls. It will limit print artifacts on
|
||||
* the surface of the print. Although this strategy technically deviates from the original philosophy of the paper.
|
||||
* It will generally results in better prints because of a smoother motion and less variation in extrusion width in
|
||||
* the outer walls.
|
||||
*
|
||||
* If the thickness of the model is less then two times the optimal outer wall width and once the minimum inner wall
|
||||
* width it will keep the minimum inner wall at a minimum constant and vary the outer wall widths symmetrical. Until
|
||||
* The thickness of the model is that of at least twice the optimal outer wall width it will then use two
|
||||
* symmetrical outer walls only. Until it transitions into a single outer wall. These last scenario's are always
|
||||
* symmetrical in nature, disregarding the user specified strategy.
|
||||
*/
|
||||
class RedistributeBeadingStrategy : public BeadingStrategy
|
||||
{
|
||||
public:
|
||||
/*!
|
||||
* /param optimal_width_outer Outer wall width, guaranteed to be the actual (save rounding errors) at a
|
||||
* bead count if the parent strategies' optimum bead width is a weighted
|
||||
* average of the outer and inner walls at that bead count.
|
||||
* /param minimum_variable_line_ratio Minimum factor that the variable line might deviate from the optimal width.
|
||||
*/
|
||||
RedistributeBeadingStrategy(coord_t optimal_width_outer, double minimum_variable_line_ratio, BeadingStrategyPtr parent);
|
||||
|
||||
~RedistributeBeadingStrategy() override = default;
|
||||
|
||||
Beading compute(coord_t thickness, coord_t bead_count) const override;
|
||||
|
||||
coord_t getOptimalThickness(coord_t bead_count) const override;
|
||||
coord_t getTransitionThickness(coord_t lower_bead_count) const override;
|
||||
coord_t getOptimalBeadCount(coord_t thickness) const override;
|
||||
coord_t getTransitioningLength(coord_t lower_bead_count) const override;
|
||||
float getTransitionAnchorPos(coord_t lower_bead_count) const override;
|
||||
|
||||
std::string toString() const override;
|
||||
|
||||
protected:
|
||||
BeadingStrategyPtr parent;
|
||||
coord_t optimal_width_outer;
|
||||
double minimum_variable_line_ratio;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // INWARD_DISTRIBUTED_BEADING_STRATEGY_H
|
||||
@@ -0,0 +1,81 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include "WideningBeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
WideningBeadingStrategy::WideningBeadingStrategy(BeadingStrategyPtr parent, const coord_t min_input_width, const coord_t min_output_width)
|
||||
: BeadingStrategy(*parent)
|
||||
, parent(std::move(parent))
|
||||
, min_input_width(min_input_width)
|
||||
, min_output_width(min_output_width)
|
||||
{
|
||||
}
|
||||
|
||||
std::string WideningBeadingStrategy::toString() const
|
||||
{
|
||||
return std::string("Widening+") + parent->toString();
|
||||
}
|
||||
|
||||
WideningBeadingStrategy::Beading WideningBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const
|
||||
{
|
||||
if (thickness < optimal_width) {
|
||||
Beading ret;
|
||||
ret.total_thickness = thickness;
|
||||
if (thickness >= min_input_width) {
|
||||
ret.bead_widths.emplace_back(std::max(thickness, min_output_width));
|
||||
ret.toolpath_locations.emplace_back(thickness / 2);
|
||||
ret.left_over = 0;
|
||||
} else
|
||||
ret.left_over = thickness;
|
||||
|
||||
return ret;
|
||||
} else
|
||||
return parent->compute(thickness, bead_count);
|
||||
}
|
||||
|
||||
coord_t WideningBeadingStrategy::getOptimalThickness(coord_t bead_count) const
|
||||
{
|
||||
return parent->getOptimalThickness(bead_count);
|
||||
}
|
||||
|
||||
coord_t WideningBeadingStrategy::getTransitionThickness(coord_t lower_bead_count) const
|
||||
{
|
||||
if (lower_bead_count == 0)
|
||||
return min_input_width;
|
||||
else
|
||||
return parent->getTransitionThickness(lower_bead_count);
|
||||
}
|
||||
|
||||
coord_t WideningBeadingStrategy::getOptimalBeadCount(coord_t thickness) const
|
||||
{
|
||||
if (thickness < min_input_width)
|
||||
return 0;
|
||||
coord_t ret = parent->getOptimalBeadCount(thickness);
|
||||
if (thickness >= min_input_width && ret < 1)
|
||||
return 1;
|
||||
return ret;
|
||||
}
|
||||
|
||||
coord_t WideningBeadingStrategy::getTransitioningLength(coord_t lower_bead_count) const
|
||||
{
|
||||
return parent->getTransitioningLength(lower_bead_count);
|
||||
}
|
||||
|
||||
float WideningBeadingStrategy::getTransitionAnchorPos(coord_t lower_bead_count) const
|
||||
{
|
||||
return parent->getTransitionAnchorPos(lower_bead_count);
|
||||
}
|
||||
|
||||
std::vector<coord_t> WideningBeadingStrategy::getNonlinearThicknesses(coord_t lower_bead_count) const
|
||||
{
|
||||
std::vector<coord_t> ret;
|
||||
ret.emplace_back(min_output_width);
|
||||
std::vector<coord_t> pret = parent->getNonlinearThicknesses(lower_bead_count);
|
||||
ret.insert(ret.end(), pret.begin(), pret.end());
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
@@ -0,0 +1,46 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef WIDENING_BEADING_STRATEGY_H
|
||||
#define WIDENING_BEADING_STRATEGY_H
|
||||
|
||||
#include "BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
/*!
|
||||
* This is a meta-strategy that can be applied on any other beading strategy. If
|
||||
* the part is thinner than a single line, this strategy adjusts the part so
|
||||
* that it becomes the minimum thickness of one line.
|
||||
*
|
||||
* This way, tiny pieces that are smaller than a single line will still be
|
||||
* printed.
|
||||
*/
|
||||
class WideningBeadingStrategy : public BeadingStrategy
|
||||
{
|
||||
public:
|
||||
/*!
|
||||
* Takes responsibility for deleting \param parent
|
||||
*/
|
||||
WideningBeadingStrategy(BeadingStrategyPtr parent, coord_t min_input_width, coord_t min_output_width);
|
||||
|
||||
~WideningBeadingStrategy() override = default;
|
||||
|
||||
Beading compute(coord_t thickness, coord_t bead_count) const override;
|
||||
coord_t getOptimalThickness(coord_t bead_count) const override;
|
||||
coord_t getTransitionThickness(coord_t lower_bead_count) const override;
|
||||
coord_t getOptimalBeadCount(coord_t thickness) const override;
|
||||
coord_t getTransitioningLength(coord_t lower_bead_count) const override;
|
||||
float getTransitionAnchorPos(coord_t lower_bead_count) const override;
|
||||
std::vector<coord_t> getNonlinearThicknesses(coord_t lower_bead_count) const override;
|
||||
std::string toString() const override;
|
||||
|
||||
protected:
|
||||
BeadingStrategyPtr parent;
|
||||
const coord_t min_input_width;
|
||||
const coord_t min_output_width;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // WIDENING_BEADING_STRATEGY_H
|
||||
@@ -0,0 +1,276 @@
|
||||
#include <stack>
|
||||
#include "PerimeterOrder.hpp"
|
||||
|
||||
namespace Slic3r::Arachne::PerimeterOrder {
|
||||
|
||||
using namespace Arachne;
|
||||
|
||||
static size_t get_extrusion_lines_count(const Perimeters &perimeters) {
|
||||
size_t extrusion_lines_count = 0;
|
||||
for (const Perimeter &perimeter : perimeters)
|
||||
extrusion_lines_count += perimeter.size();
|
||||
|
||||
return extrusion_lines_count;
|
||||
}
|
||||
|
||||
static PerimeterExtrusions get_sorted_perimeter_extrusions_by_area(const Perimeters &perimeters) {
|
||||
PerimeterExtrusions sorted_perimeter_extrusions;
|
||||
sorted_perimeter_extrusions.reserve(get_extrusion_lines_count(perimeters));
|
||||
|
||||
for (const Perimeter &perimeter : perimeters) {
|
||||
for (const ExtrusionLine &extrusion_line : perimeter) {
|
||||
if (extrusion_line.empty())
|
||||
continue; // This shouldn't ever happen.
|
||||
|
||||
const BoundingBox bbox = get_extents(extrusion_line);
|
||||
// Be aware that Arachne produces contours with clockwise orientation and holes with counterclockwise orientation.
|
||||
const double area = std::abs(extrusion_line.area());
|
||||
const Polygon polygon = extrusion_line.is_closed ? to_polygon(extrusion_line) : Polygon{};
|
||||
|
||||
sorted_perimeter_extrusions.emplace_back(extrusion_line, area, polygon, bbox);
|
||||
}
|
||||
}
|
||||
|
||||
// Open extrusions have an area equal to zero, so sorting based on the area ensures that open extrusions will always be before closed ones.
|
||||
std::sort(sorted_perimeter_extrusions.begin(), sorted_perimeter_extrusions.end(),
|
||||
[](const PerimeterExtrusion &l, const PerimeterExtrusion &r) { return l.area < r.area; });
|
||||
|
||||
return sorted_perimeter_extrusions;
|
||||
}
|
||||
|
||||
// Functions fill adjacent_perimeter_extrusions field for every PerimeterExtrusion by pointers to PerimeterExtrusions that contain or are inside this PerimeterExtrusion.
|
||||
static void construct_perimeter_extrusions_adjacency_graph(PerimeterExtrusions &sorted_perimeter_extrusions) {
|
||||
// Construct a graph (defined using adjacent_perimeter_extrusions field) where two PerimeterExtrusion are adjacent when one is inside the other.
|
||||
std::vector<bool> root_candidates(sorted_perimeter_extrusions.size(), false);
|
||||
for (PerimeterExtrusion &perimeter_extrusion : sorted_perimeter_extrusions) {
|
||||
const size_t perimeter_extrusion_idx = &perimeter_extrusion - sorted_perimeter_extrusions.data();
|
||||
|
||||
if (!perimeter_extrusion.is_closed()) {
|
||||
root_candidates[perimeter_extrusion_idx] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (PerimeterExtrusion &root_candidate : sorted_perimeter_extrusions) {
|
||||
const size_t root_candidate_idx = &root_candidate - sorted_perimeter_extrusions.data();
|
||||
|
||||
if (!root_candidates[root_candidate_idx])
|
||||
continue;
|
||||
|
||||
if (perimeter_extrusion.bbox.contains(root_candidate.bbox) && perimeter_extrusion.polygon.contains(root_candidate.extrusion.junctions.front().p)) {
|
||||
perimeter_extrusion.adjacent_perimeter_extrusions.emplace_back(&root_candidate);
|
||||
root_candidate.adjacent_perimeter_extrusions.emplace_back(&perimeter_extrusion);
|
||||
root_candidates[root_candidate_idx] = false;
|
||||
}
|
||||
}
|
||||
|
||||
root_candidates[perimeter_extrusion_idx] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the depth-first search to assign the nearest external perimeter for every PerimeterExtrusion.
|
||||
// When some PerimeterExtrusion is achievable from more than one external perimeter, then we choose the
|
||||
// one that comes from a contour.
|
||||
static void assign_nearest_external_perimeter(PerimeterExtrusions &sorted_perimeter_extrusions) {
|
||||
std::stack<PerimeterExtrusion *> stack;
|
||||
for (PerimeterExtrusion &perimeter_extrusion : sorted_perimeter_extrusions) {
|
||||
if (perimeter_extrusion.is_external_perimeter()) {
|
||||
perimeter_extrusion.depth = 0;
|
||||
perimeter_extrusion.nearest_external_perimeter = &perimeter_extrusion;
|
||||
stack.push(&perimeter_extrusion);
|
||||
}
|
||||
}
|
||||
|
||||
while (!stack.empty()) {
|
||||
PerimeterExtrusion *current_extrusion = stack.top();
|
||||
stack.pop();
|
||||
|
||||
for (PerimeterExtrusion *adjacent_extrusion : current_extrusion->adjacent_perimeter_extrusions) {
|
||||
const size_t adjacent_extrusion_depth = current_extrusion->depth + 1;
|
||||
// Update depth when the new depth is smaller or when we can achieve the same depth from a contour.
|
||||
// This will ensure that the internal perimeter will be extruded before the outer external perimeter
|
||||
// when there are two external perimeters and one internal.
|
||||
if (adjacent_extrusion_depth < adjacent_extrusion->depth) {
|
||||
adjacent_extrusion->nearest_external_perimeter = current_extrusion->nearest_external_perimeter;
|
||||
adjacent_extrusion->depth = adjacent_extrusion_depth;
|
||||
stack.push(adjacent_extrusion);
|
||||
} else if (adjacent_extrusion_depth == adjacent_extrusion->depth && !adjacent_extrusion->nearest_external_perimeter->is_contour() && current_extrusion->is_contour()) {
|
||||
adjacent_extrusion->nearest_external_perimeter = current_extrusion->nearest_external_perimeter;
|
||||
stack.push(adjacent_extrusion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline Point get_end_position(const ExtrusionLine &extrusion) {
|
||||
if (extrusion.is_closed)
|
||||
return extrusion.junctions[0].p; // We ended where we started.
|
||||
else
|
||||
return extrusion.junctions.back().p; // Pick the other end from where we started.
|
||||
}
|
||||
|
||||
// Returns ordered extrusions.
|
||||
static std::vector<const PerimeterExtrusion *> ordered_perimeter_extrusions_to_minimize_distances(Point current_position, std::vector<const PerimeterExtrusion *> extrusions) {
|
||||
// Ensure that open extrusions will be placed before the closed one.
|
||||
std::sort(extrusions.begin(), extrusions.end(),
|
||||
[](const PerimeterExtrusion *l, const PerimeterExtrusion *r) -> bool { return l->is_closed() < r->is_closed(); });
|
||||
|
||||
std::vector<const PerimeterExtrusion *> ordered_extrusions;
|
||||
std::vector<bool> already_selected(extrusions.size(), false);
|
||||
while (ordered_extrusions.size() < extrusions.size()) {
|
||||
double nearest_distance_sqr = std::numeric_limits<double>::max();
|
||||
size_t nearest_extrusion_idx = 0;
|
||||
bool is_nearest_closed = false;
|
||||
|
||||
for (size_t extrusion_idx = 0; extrusion_idx < extrusions.size(); ++extrusion_idx) {
|
||||
if (already_selected[extrusion_idx])
|
||||
continue;
|
||||
|
||||
const ExtrusionLine &extrusion_line = extrusions[extrusion_idx]->extrusion;
|
||||
const Point &extrusion_start_position = extrusion_line.junctions.front().p;
|
||||
const double distance_sqr = (current_position - extrusion_start_position).cast<double>().squaredNorm();
|
||||
if (distance_sqr < nearest_distance_sqr) {
|
||||
if (extrusion_line.is_closed || (!extrusion_line.is_closed && nearest_distance_sqr != std::numeric_limits<double>::max()) || (!extrusion_line.is_closed && !is_nearest_closed)) {
|
||||
nearest_extrusion_idx = extrusion_idx;
|
||||
nearest_distance_sqr = distance_sqr;
|
||||
is_nearest_closed = extrusion_line.is_closed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
already_selected[nearest_extrusion_idx] = true;
|
||||
const PerimeterExtrusion *nearest_extrusion = extrusions[nearest_extrusion_idx];
|
||||
current_position = get_end_position(nearest_extrusion->extrusion);
|
||||
ordered_extrusions.emplace_back(nearest_extrusion);
|
||||
}
|
||||
|
||||
return ordered_extrusions;
|
||||
}
|
||||
|
||||
struct GroupedPerimeterExtrusions
|
||||
{
|
||||
GroupedPerimeterExtrusions() = delete;
|
||||
explicit GroupedPerimeterExtrusions(const PerimeterExtrusion *external_perimeter_extrusion)
|
||||
: external_perimeter_extrusion(external_perimeter_extrusion) {}
|
||||
|
||||
std::vector<const PerimeterExtrusion *> extrusions;
|
||||
const PerimeterExtrusion *external_perimeter_extrusion = nullptr;
|
||||
};
|
||||
|
||||
// Returns vector of indexes that represent the order of grouped extrusions in grouped_extrusions.
|
||||
static std::vector<size_t> order_of_grouped_perimeter_extrusions_to_minimize_distances(Point current_position, std::vector<GroupedPerimeterExtrusions> grouped_extrusions) {
|
||||
// Ensure that holes will be placed before contour and open extrusions before the closed one.
|
||||
std::sort(grouped_extrusions.begin(), grouped_extrusions.end(), [](const GroupedPerimeterExtrusions &l, const GroupedPerimeterExtrusions &r) -> bool {
|
||||
return (l.external_perimeter_extrusion->is_contour() < r.external_perimeter_extrusion->is_contour()) ||
|
||||
(l.external_perimeter_extrusion->is_contour() == r.external_perimeter_extrusion->is_contour() && l.external_perimeter_extrusion->is_closed() < r.external_perimeter_extrusion->is_closed());
|
||||
});
|
||||
|
||||
const size_t holes_cnt = std::count_if(grouped_extrusions.begin(), grouped_extrusions.end(), [](const GroupedPerimeterExtrusions &grouped_extrusions) {
|
||||
return !grouped_extrusions.external_perimeter_extrusion->is_contour();
|
||||
});
|
||||
|
||||
std::vector<size_t> grouped_extrusions_order;
|
||||
std::vector<bool> already_selected(grouped_extrusions.size(), false);
|
||||
while (grouped_extrusions_order.size() < grouped_extrusions.size()) {
|
||||
double nearest_distance_sqr = std::numeric_limits<double>::max();
|
||||
size_t nearest_grouped_extrusions_idx = 0;
|
||||
bool is_nearest_closed = false;
|
||||
|
||||
// First we order all holes and then we start ordering contours.
|
||||
const size_t grouped_extrusion_end = grouped_extrusions_order.size() < holes_cnt ? holes_cnt: grouped_extrusions.size();
|
||||
for (size_t grouped_extrusion_idx = 0; grouped_extrusion_idx < grouped_extrusion_end; ++grouped_extrusion_idx) {
|
||||
if (already_selected[grouped_extrusion_idx])
|
||||
continue;
|
||||
|
||||
const ExtrusionLine &external_perimeter_extrusion_line = grouped_extrusions[grouped_extrusion_idx].external_perimeter_extrusion->extrusion;
|
||||
const Point &extrusion_start_position = external_perimeter_extrusion_line.junctions.front().p;
|
||||
const double distance_sqr = (current_position - extrusion_start_position).cast<double>().squaredNorm();
|
||||
if (distance_sqr < nearest_distance_sqr) {
|
||||
if (external_perimeter_extrusion_line.is_closed || (!external_perimeter_extrusion_line.is_closed && nearest_distance_sqr != std::numeric_limits<double>::max()) || (!external_perimeter_extrusion_line.is_closed && !is_nearest_closed)) {
|
||||
nearest_grouped_extrusions_idx = grouped_extrusion_idx;
|
||||
nearest_distance_sqr = distance_sqr;
|
||||
is_nearest_closed = external_perimeter_extrusion_line.is_closed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
grouped_extrusions_order.emplace_back(nearest_grouped_extrusions_idx);
|
||||
already_selected[nearest_grouped_extrusions_idx] = true;
|
||||
const GroupedPerimeterExtrusions &nearest_grouped_extrusions = grouped_extrusions[nearest_grouped_extrusions_idx];
|
||||
const ExtrusionLine &last_extrusion_line = nearest_grouped_extrusions.extrusions.back()->extrusion;
|
||||
current_position = get_end_position(last_extrusion_line);
|
||||
}
|
||||
|
||||
return grouped_extrusions_order;
|
||||
}
|
||||
|
||||
static PerimeterExtrusions extract_ordered_perimeter_extrusions(const PerimeterExtrusions &sorted_perimeter_extrusions, const bool external_perimeters_first) {
|
||||
// Extrusions are ordered inside each group.
|
||||
std::vector<GroupedPerimeterExtrusions> grouped_extrusions;
|
||||
|
||||
std::stack<const PerimeterExtrusion *> stack;
|
||||
std::vector<bool> visited(sorted_perimeter_extrusions.size(), false);
|
||||
for (const PerimeterExtrusion &perimeter_extrusion : sorted_perimeter_extrusions) {
|
||||
if (!perimeter_extrusion.is_external_perimeter())
|
||||
continue;
|
||||
|
||||
stack.push(&perimeter_extrusion);
|
||||
visited.assign(sorted_perimeter_extrusions.size(), false);
|
||||
|
||||
grouped_extrusions.emplace_back(&perimeter_extrusion);
|
||||
while (!stack.empty()) {
|
||||
const PerimeterExtrusion *current_extrusion = stack.top();
|
||||
const size_t current_extrusion_idx = current_extrusion - sorted_perimeter_extrusions.data();
|
||||
stack.pop();
|
||||
|
||||
if (visited[current_extrusion_idx])
|
||||
continue;
|
||||
|
||||
if (current_extrusion->nearest_external_perimeter == &perimeter_extrusion)
|
||||
grouped_extrusions.back().extrusions.emplace_back(current_extrusion);
|
||||
|
||||
if (current_extrusion->adjacent_perimeter_extrusions.size() == 1) {
|
||||
const PerimeterExtrusion *adjacent_extrusion = current_extrusion->adjacent_perimeter_extrusions.front();
|
||||
stack.push(adjacent_extrusion);
|
||||
} else if (current_extrusion->adjacent_perimeter_extrusions.size() > 1) {
|
||||
// When there is more than one available candidate, then order candidates to minimize distances between
|
||||
// candidates and also to minimize the distance from the current_position.
|
||||
std::vector<const PerimeterExtrusion *> available_candidates;
|
||||
for (const PerimeterExtrusion *adjacent_extrusion : current_extrusion->adjacent_perimeter_extrusions) {
|
||||
if (const size_t adjacent_extrusion_idx = adjacent_extrusion - sorted_perimeter_extrusions.data(); !visited[adjacent_extrusion_idx])
|
||||
available_candidates.emplace_back(adjacent_extrusion);
|
||||
}
|
||||
|
||||
std::vector<const PerimeterExtrusion *> adjacent_extrusions = ordered_perimeter_extrusions_to_minimize_distances(Point::Zero(), available_candidates);
|
||||
std::reverse(adjacent_extrusions.begin(), adjacent_extrusions.end());
|
||||
for (const PerimeterExtrusion *adjacent_extrusion : adjacent_extrusions)
|
||||
stack.push(adjacent_extrusion);
|
||||
}
|
||||
|
||||
visited[current_extrusion_idx] = true;
|
||||
}
|
||||
|
||||
if (!external_perimeters_first)
|
||||
std::reverse(grouped_extrusions.back().extrusions.begin(), grouped_extrusions.back().extrusions.end());
|
||||
}
|
||||
|
||||
const std::vector<size_t> grouped_extrusion_order = order_of_grouped_perimeter_extrusions_to_minimize_distances(Point::Zero(), grouped_extrusions);
|
||||
|
||||
PerimeterExtrusions ordered_extrusions;
|
||||
for (size_t order_idx : grouped_extrusion_order) {
|
||||
for (const PerimeterExtrusion *perimeter_extrusion : grouped_extrusions[order_idx].extrusions)
|
||||
ordered_extrusions.emplace_back(*perimeter_extrusion);
|
||||
}
|
||||
|
||||
return ordered_extrusions;
|
||||
}
|
||||
|
||||
// FIXME: From the point of better patch planning, it should be better to do ordering when we have generated all extrusions (for now, when G-Code is exported).
|
||||
// FIXME: It would be better to extract the adjacency graph of extrusions from the SkeletalTrapezoidation graph.
|
||||
PerimeterExtrusions ordered_perimeter_extrusions(const Perimeters &perimeters, const bool external_perimeters_first) {
|
||||
PerimeterExtrusions sorted_perimeter_extrusions = get_sorted_perimeter_extrusions_by_area(perimeters);
|
||||
construct_perimeter_extrusions_adjacency_graph(sorted_perimeter_extrusions);
|
||||
assign_nearest_external_perimeter(sorted_perimeter_extrusions);
|
||||
return extract_ordered_perimeter_extrusions(sorted_perimeter_extrusions, external_perimeters_first);
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne::PerimeterOrder
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef slic3r_GCode_PerimeterOrder_hpp_
|
||||
#define slic3r_GCode_PerimeterOrder_hpp_
|
||||
|
||||
#include <Arachne/utils/ExtrusionLine.hpp>
|
||||
|
||||
namespace Slic3r::Arachne::PerimeterOrder {
|
||||
|
||||
// Data structure stores ExtrusionLine (closed and open) together with additional data.
|
||||
struct PerimeterExtrusion
|
||||
{
|
||||
explicit PerimeterExtrusion(const Arachne::ExtrusionLine &extrusion, const double area, const Polygon &polygon, const BoundingBox &bbox)
|
||||
: extrusion(extrusion), area(area), polygon(polygon), bbox(bbox) {}
|
||||
|
||||
Arachne::ExtrusionLine extrusion;
|
||||
// Absolute value of the area of the polygon. The value is always non-negative, even for holes.
|
||||
double area = 0;
|
||||
|
||||
// Polygon is non-empty only for closed extrusions.
|
||||
Polygon polygon;
|
||||
BoundingBox bbox;
|
||||
|
||||
std::vector<PerimeterExtrusion *> adjacent_perimeter_extrusions;
|
||||
|
||||
// How far is this perimeter from the nearest external perimeter. Contour is always preferred over holes.
|
||||
size_t depth = std::numeric_limits<size_t>::max();
|
||||
PerimeterExtrusion *nearest_external_perimeter = nullptr;
|
||||
|
||||
// Should this extrusion be fuzzyfied during path generation?
|
||||
bool fuzzify = false;
|
||||
|
||||
// Returns if ExtrusionLine is a contour or a hole.
|
||||
bool is_contour() const { return extrusion.is_contour(); }
|
||||
|
||||
// Returns if ExtrusionLine is closed or opened.
|
||||
bool is_closed() const { return extrusion.is_closed; }
|
||||
|
||||
// Returns if ExtrusionLine is an external or an internal perimeter.
|
||||
bool is_external_perimeter() const { return extrusion.is_external_perimeter(); }
|
||||
};
|
||||
|
||||
using PerimeterExtrusions = std::vector<PerimeterExtrusion>;
|
||||
|
||||
PerimeterExtrusions ordered_perimeter_extrusions(const Perimeters &perimeters, bool external_perimeters_first);
|
||||
|
||||
} // namespace Slic3r::Arachne::PerimeterOrder
|
||||
|
||||
#endif // slic3r_GCode_Travels_hpp_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,552 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef SKELETAL_TRAPEZOIDATION_H
|
||||
#define SKELETAL_TRAPEZOIDATION_H
|
||||
|
||||
#include <boost/polygon/voronoi.hpp>
|
||||
|
||||
#include <memory> // smart pointers
|
||||
#include <utility> // pair
|
||||
|
||||
#include <ankerl/unordered_dense.h>
|
||||
|
||||
#include "utils/HalfEdgeGraph.hpp"
|
||||
#include "utils/PolygonsSegmentIndex.hpp"
|
||||
#include "utils/ExtrusionJunction.hpp"
|
||||
#include "utils/ExtrusionLine.hpp"
|
||||
#include "SkeletalTrapezoidationEdge.hpp"
|
||||
#include "SkeletalTrapezoidationJoint.hpp"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
|
||||
#include "SkeletalTrapezoidationGraph.hpp"
|
||||
#include "../Geometry/Voronoi.hpp"
|
||||
|
||||
//#define ARACHNE_DEBUG
|
||||
//#define ARACHNE_DEBUG_VORONOI
|
||||
|
||||
namespace Slic3r::Arachne {
|
||||
|
||||
using VD = Slic3r::Geometry::VoronoiDiagram;
|
||||
|
||||
/*!
|
||||
* Main class of the dynamic beading strategies.
|
||||
*
|
||||
* The input polygon region is decomposed into trapezoids and represented as a half-edge data-structure.
|
||||
*
|
||||
* We determine which edges are 'central' accordinding to the transitioning_angle of the beading strategy,
|
||||
* and determine the bead count for these central regions and apply them outward when generating toolpaths. [oversimplified]
|
||||
*
|
||||
* The method can be visually explained as generating the 3D union of cones surface on the outline polygons,
|
||||
* and changing the heights along central regions of that surface so that they are flat.
|
||||
* For more info, please consult the paper "A framework for adaptive width control of dense contour-parallel toolpaths in fused
|
||||
deposition modeling" by Kuipers et al.
|
||||
* This visual explanation aid explains the use of "upward", "lower" etc,
|
||||
* i.e. the radial distance and/or the bead count are used as heights of this visualization, there is no coordinate called 'Z'.
|
||||
*
|
||||
* TODO: split this class into two:
|
||||
* 1. Class for generating the decomposition and aux functions for performing updates
|
||||
* 2. Class for editing the structure for our purposes.
|
||||
*/
|
||||
class SkeletalTrapezoidation
|
||||
{
|
||||
using graph_t = SkeletalTrapezoidationGraph;
|
||||
using edge_t = STHalfEdge;
|
||||
using node_t = STHalfEdgeNode;
|
||||
using Beading = BeadingStrategy::Beading;
|
||||
using BeadingPropagation = SkeletalTrapezoidationJoint::BeadingPropagation;
|
||||
using TransitionMiddle = SkeletalTrapezoidationEdge::TransitionMiddle;
|
||||
using TransitionEnd = SkeletalTrapezoidationEdge::TransitionEnd;
|
||||
|
||||
template<typename T>
|
||||
using ptr_vector_t = std::vector<std::shared_ptr<T>>;
|
||||
|
||||
double transitioning_angle; //!< How pointy a region should be before we apply the method. Equals 180* - limit_bisector_angle
|
||||
coord_t discretization_step_size; //!< approximate size of segments when parabolic VD edges get discretized (and vertex-vertex edges)
|
||||
coord_t transition_filter_dist; //!< Filter transition mids (i.e. anchors) closer together than this
|
||||
coord_t allowed_filter_deviation; //!< The allowed line width deviation induced by filtering
|
||||
coord_t beading_propagation_transition_dist; //!< When there are different beadings propagated from below and from above, use this transitioning distance
|
||||
static constexpr coord_t central_filter_dist = scaled<coord_t>(0.02); //!< Filter areas marked as 'central' smaller than this
|
||||
static constexpr coord_t snap_dist = scaled<coord_t>(0.02); //!< Generic arithmatic inaccuracy. Only used to determine whether a transition really needs to insert an extra edge.
|
||||
|
||||
/*!
|
||||
* The strategy to use to fill a certain shape with lines.
|
||||
*
|
||||
* Various BeadingStrategies are available that differ in which lines get to
|
||||
* print at their optimal width, where the play is being compensated, and
|
||||
* how the joints are handled where we transition to different numbers of
|
||||
* lines.
|
||||
*/
|
||||
const BeadingStrategy& beading_strategy;
|
||||
|
||||
public:
|
||||
using Segment = PolygonsSegmentIndex;
|
||||
using NodeSet = ankerl::unordered_dense::set<node_t*>;
|
||||
|
||||
/*!
|
||||
* Construct a new trapezoidation problem to solve.
|
||||
* \param polys The shapes to fill with walls.
|
||||
* \param beading_strategy The strategy to use to fill these shapes.
|
||||
* \param transitioning_angle Where we transition to a different number of
|
||||
* walls, how steep should this transition be? A lower angle means that the
|
||||
* transition will be longer.
|
||||
* \param discretization_step_size Since g-code can't represent smooth
|
||||
* transitions in line width, the line width must change with discretized
|
||||
* steps. This indicates how long the line segments between those steps will
|
||||
* be.
|
||||
* \param transition_filter_dist The minimum length of transitions.
|
||||
* Transitions shorter than this will be considered for dissolution.
|
||||
* \param beading_propagation_transition_dist When there are different
|
||||
* beadings propagated from below and from above, use this transitioning
|
||||
* distance.
|
||||
*/
|
||||
SkeletalTrapezoidation(const Polygons& polys,
|
||||
const BeadingStrategy& beading_strategy,
|
||||
double transitioning_angle
|
||||
, coord_t discretization_step_size
|
||||
, coord_t transition_filter_dist
|
||||
, coord_t allowed_filter_deviation
|
||||
, coord_t beading_propagation_transition_dist);
|
||||
|
||||
/*!
|
||||
* A skeletal graph through the polygons that we need to fill with beads.
|
||||
*
|
||||
* The skeletal graph represents the medial axes through each part of the
|
||||
* polygons, and the lines from these medial axes towards each vertex of the
|
||||
* polygons. The graph can be used to see what the width is of a polygon in
|
||||
* each place and where the width transitions.
|
||||
*/
|
||||
graph_t graph;
|
||||
|
||||
/*!
|
||||
* Generate the paths that the printer must extrude, to print the outlines
|
||||
* in the input polygons.
|
||||
* \param filter_outermost_central_edges Some edges are "central" but still
|
||||
* touch the outside of the polygon. If enabled, don't treat these as
|
||||
* "central" but as if it's a obtuse corner. As a result, sharp corners will
|
||||
* no longer end in a single line but will just loop.
|
||||
*/
|
||||
void generateToolpaths(std::vector<VariableWidthLines> &generated_toolpaths, bool filter_outermost_central_edges = false);
|
||||
|
||||
#ifdef ARACHNE_DEBUG
|
||||
Polygons outline;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
/*!
|
||||
* Auxiliary for referencing one transition along an edge which may contain multiple transitions
|
||||
*/
|
||||
struct TransitionMidRef
|
||||
{
|
||||
edge_t* edge;
|
||||
std::list<TransitionMiddle>::iterator transition_it;
|
||||
TransitionMidRef(edge_t* edge, std::list<TransitionMiddle>::iterator transition_it)
|
||||
: edge(edge)
|
||||
, transition_it(transition_it)
|
||||
{}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Compute the skeletal trapezoidation decomposition of the input shape.
|
||||
*
|
||||
* Compute the Voronoi Diagram (VD) and transfer all inside edges into our half-edge (HE) datastructure.
|
||||
*
|
||||
* The algorithm is currently a bit overcomplicated, because the discretization of parabolic edges is performed at the same time as all edges are being transfered,
|
||||
* which means that there is no one-to-one mapping from VD edges to HE edges.
|
||||
* Instead we map from a VD edge to the last HE edge.
|
||||
* This could be cimplified by recording the edges which should be discretized and discretizing the mafterwards.
|
||||
*
|
||||
* Another complication arises because the VD uses floating logic, which can result in zero-length segments after rounding to integers.
|
||||
* We therefore collapse edges and their whole cells afterwards.
|
||||
*/
|
||||
void constructFromPolygons(const Polygons& polys);
|
||||
|
||||
/*!
|
||||
* mapping each voronoi VD edge to the corresponding halfedge HE edge
|
||||
* In case the result segment is discretized, we map the VD edge to the *last* HE edge
|
||||
*/
|
||||
ankerl::unordered_dense::map<const VD::edge_type *, edge_t *> vd_edge_to_he_edge;
|
||||
ankerl::unordered_dense::map<const VD::vertex_type *, node_t *> vd_node_to_he_node;
|
||||
node_t &makeNode(const VD::vertex_type &vd_node, Point p); //!< Get the node which the VD node maps to, or create a new mapping if there wasn't any yet.
|
||||
|
||||
/*!
|
||||
* (Eventual) returned 'polylines per index' result (from generateToolpaths):
|
||||
*/
|
||||
std::vector<VariableWidthLines> *p_generated_toolpaths;
|
||||
|
||||
/*!
|
||||
* Transfer an edge from the VD to the HE and perform discretization of parabolic edges (and vertex-vertex edges)
|
||||
* \p prev_edge serves as input and output. May be null as input.
|
||||
*/
|
||||
void transferEdge(const Point &from, const Point &to, const VD::edge_type &vd_edge, edge_t *&prev_edge, const Point &start_source_point, const Point &end_source_point, const std::vector<Segment> &segments);
|
||||
|
||||
/*!
|
||||
* Discretize a Voronoi edge that represents the medial axis of a vertex-
|
||||
* line region or vertex-vertex region into small segments that can be
|
||||
* considered to have a straight medial axis and a linear line width
|
||||
* transition.
|
||||
*
|
||||
* The medial axis between a point and a line is a parabola. The rest of the
|
||||
* algorithm doesn't want to have to deal with parabola, so this discretises
|
||||
* the parabola into straight line segments. This is necessary if there is a
|
||||
* sharp inner corner (acts as a point) that comes close to a straight edge.
|
||||
*
|
||||
* The medial axis between a point and a point is a straight line segment.
|
||||
* However the distance from the medial axis to either of those points draws
|
||||
* a parabola as you go along the medial axis. That means that the resulting
|
||||
* line width along the medial axis would not be linearly increasing or
|
||||
* linearly decreasing, but needs to take the shape of a parabola. Instead,
|
||||
* we'll break this edge up into tiny line segments that can approximate the
|
||||
* parabola with tiny linear increases or decreases in line width.
|
||||
* \param segment The variable-width Voronoi edge to discretize.
|
||||
* \param points All vertices of the original Polygons to fill with beads.
|
||||
* \param segments All line segments of the original Polygons to fill with
|
||||
* beads.
|
||||
* \return A number of coordinates along the edge where the edge is broken
|
||||
* up into discrete pieces.
|
||||
*/
|
||||
Points discretize(const VD::edge_type& segment, const std::vector<Segment>& segments);
|
||||
|
||||
/*!
|
||||
* For VD cells associated with an input polygon vertex, we need to separate the node at the end and start of the cell into two
|
||||
* That way we can reach both the quad_start and the quad_end from the [incident_edge] of the two new nodes
|
||||
* Otherwise if node.incident_edge = quad_start you couldnt reach quad_end.twin by normal iteration (i.e. it = it.twin.next)
|
||||
*/
|
||||
void separatePointyQuadEndNodes();
|
||||
|
||||
|
||||
// ^ init | v transitioning
|
||||
|
||||
void updateIsCentral(); // Update the "is_central" flag for each edge based on the transitioning_angle
|
||||
|
||||
/*!
|
||||
* Filter out small central areas.
|
||||
*
|
||||
* Only used to get rid of small edges which get marked as central because
|
||||
* of rounding errors because the region is so small.
|
||||
*/
|
||||
void filterCentral(coord_t max_length);
|
||||
|
||||
/*!
|
||||
* Filter central areas connected to starting_edge recursively.
|
||||
* \return Whether we should unmark this section marked as central, on the
|
||||
* way back out of the recursion.
|
||||
*/
|
||||
bool filterCentral(edge_t* starting_edge, coord_t traveled_dist, coord_t max_length);
|
||||
|
||||
/*!
|
||||
* Unmark the outermost edges directly connected to the outline, as not
|
||||
* being central.
|
||||
*
|
||||
* Only used to emulate some related literature.
|
||||
*
|
||||
* The paper shows that this function is bad for the stability of the framework.
|
||||
*/
|
||||
void filterOuterCentral();
|
||||
|
||||
/*!
|
||||
* Set bead count in central regions based on the optimal_bead_count of the
|
||||
* beading strategy.
|
||||
*/
|
||||
void updateBeadCount();
|
||||
|
||||
/*!
|
||||
* Add central regions and set bead counts where there is an end of the
|
||||
* central area and when traveling upward we get to another region with the
|
||||
* same bead count.
|
||||
*/
|
||||
void filterNoncentralRegions();
|
||||
|
||||
/*!
|
||||
* Add central regions and set bead counts for a particular edge and all of
|
||||
* its adjacent edges.
|
||||
*
|
||||
* Recursive subroutine for \ref filterNoncentralRegions().
|
||||
* \return Whether to set the bead count on the way back
|
||||
*/
|
||||
bool filterNoncentralRegions(edge_t* to_edge, coord_t bead_count, coord_t traveled_dist, coord_t max_dist);
|
||||
|
||||
/*!
|
||||
* Generate middle points of all transitions on edges.
|
||||
*
|
||||
* The transition middle points are saved in the graph itself. They are also
|
||||
* returned via the output parameter.
|
||||
* \param[out] edge_transitions A list of transitions that were generated.
|
||||
*/
|
||||
void generateTransitionMids(ptr_vector_t<std::list<TransitionMiddle>>& edge_transitions);
|
||||
|
||||
/*!
|
||||
* Removes some transition middle points.
|
||||
*
|
||||
* Transitions can be removed if there are multiple intersecting transitions
|
||||
* that are too close together. If transitions have opposite effects, both
|
||||
* are removed.
|
||||
*/
|
||||
void filterTransitionMids();
|
||||
|
||||
/*!
|
||||
* Merge transitions that are too close together.
|
||||
* \param edge_to_start Edge pointing to the node from which to start
|
||||
* traveling in all directions except along \p edge_to_start .
|
||||
* \param origin_transition The transition for which we are checking nearby
|
||||
* transitions.
|
||||
* \param traveled_dist The distance traveled before we came to
|
||||
* \p edge_to_start.to .
|
||||
* \param going_up Whether we are traveling in the upward direction as seen
|
||||
* from the \p origin_transition. If this doesn't align with the direction
|
||||
* according to the R diff on a consecutive edge we know there was a local
|
||||
* optimum.
|
||||
* \return Whether the origin transition should be dissolved.
|
||||
*/
|
||||
std::list<TransitionMidRef> dissolveNearbyTransitions(edge_t* edge_to_start, TransitionMiddle& origin_transition, coord_t traveled_dist, coord_t max_dist, bool going_up);
|
||||
|
||||
/*!
|
||||
* Spread a certain bead count over a region in the graph.
|
||||
* \param edge_to_start One edge of the region to spread the bead count in.
|
||||
* \param from_bead_count All edges with this bead count will be changed.
|
||||
* \param to_bead_count The new bead count for those edges.
|
||||
*/
|
||||
void dissolveBeadCountRegion(edge_t* edge_to_start, coord_t from_bead_count, coord_t to_bead_count);
|
||||
|
||||
/*!
|
||||
* Change the bead count if the given edge is at the end of a central
|
||||
* region.
|
||||
*
|
||||
* This is necessary to provide a transitioning bead count to the edges of a
|
||||
* central region to transition more smoothly from a high bead count in the
|
||||
* central region to a lower bead count at the edge.
|
||||
* \param edge_to_start One edge from a zone that needs to be filtered.
|
||||
* \param traveled_dist The distance along the edges we've traveled so far.
|
||||
* \param max_distance Don't filter beyond this range.
|
||||
* \param replacing_bead_count The new bead count for this region.
|
||||
* \return ``true`` if the bead count of this edge was changed.
|
||||
*/
|
||||
bool filterEndOfCentralTransition(edge_t* edge_to_start, coord_t traveled_dist, coord_t max_dist, coord_t replacing_bead_count);
|
||||
|
||||
/*!
|
||||
* Generate the endpoints of all transitions for all edges in the graph.
|
||||
* \param[out] edge_transition_ends The resulting transition endpoints.
|
||||
*/
|
||||
void generateAllTransitionEnds(ptr_vector_t<std::list<TransitionEnd>>& edge_transition_ends);
|
||||
|
||||
/*!
|
||||
* Also set the rest values at nodes in between the transition ends
|
||||
*/
|
||||
void applyTransitions(ptr_vector_t<std::list<TransitionEnd>>& edge_transition_ends);
|
||||
|
||||
/*!
|
||||
* Create extra edges along all edges, where it needs to transition from one
|
||||
* bead count to another.
|
||||
*
|
||||
* For example, if an edge of the graph goes from a bead count of 6 to a
|
||||
* bead count of 1, it needs to generate 5 places where the beads around
|
||||
* this line transition to a lower bead count. These are the "ribs". They
|
||||
* reach from the edge to the border of the polygon. Where the beads hit
|
||||
* those ribs the beads know to make a transition.
|
||||
*/
|
||||
void generateTransitioningRibs();
|
||||
|
||||
/*!
|
||||
* Generate the endpoints of a specific transition midpoint.
|
||||
* \param edge The edge to create transitions on.
|
||||
* \param mid_R The radius of the transition middle point.
|
||||
* \param transition_lower_bead_count The bead count at the lower end of the
|
||||
* transition.
|
||||
* \param[out] edge_transition_ends A list of endpoints to add the new
|
||||
* endpoints to.
|
||||
*/
|
||||
void generateTransitionEnds(edge_t& edge, coord_t mid_R, coord_t transition_lower_bead_count, ptr_vector_t<std::list<TransitionEnd>>& edge_transition_ends);
|
||||
|
||||
/*!
|
||||
* Compute a single endpoint of a transition.
|
||||
* \param edge The edge to generate the endpoint for.
|
||||
* \param start_pos The position where the transition starts.
|
||||
* \param end_pos The position where the transition ends on the other side.
|
||||
* \param transition_half_length The distance to the transition middle
|
||||
* point.
|
||||
* \param start_rest The gap between the start of the transition and the
|
||||
* starting endpoint, as ratio of the inner bead width at the high end of
|
||||
* the transition.
|
||||
* \param end_rest The gap between the end of the transition and the ending
|
||||
* endpoint, as ratio of the inner bead width at the high end of the
|
||||
* transition.
|
||||
* \param transition_lower_bead_count The bead count at the lower end of the
|
||||
* transition.
|
||||
* \param[out] edge_transition_ends The list to put the resulting endpoints
|
||||
* in.
|
||||
* \return Whether the given edge is going downward (i.e. towards a thinner
|
||||
* region of the polygon).
|
||||
*/
|
||||
bool generateTransitionEnd(edge_t& edge, coord_t start_pos, coord_t end_pos, coord_t transition_half_length, double start_rest, double end_rest, coord_t transition_lower_bead_count, ptr_vector_t<std::list<TransitionEnd>>& edge_transition_ends);
|
||||
|
||||
/*!
|
||||
* Determines whether an edge is going downwards or upwards in the graph.
|
||||
*
|
||||
* An edge is said to go "downwards" if it's going towards a narrower part
|
||||
* of the polygon. The notion of "downwards" comes from the conical
|
||||
* representation of the graph, where the polygon is filled with a cone of
|
||||
* maximum radius.
|
||||
*
|
||||
* This function works by recursively checking adjacent edges until the edge
|
||||
* is reached.
|
||||
* \param outgoing The edge to check.
|
||||
* \param traveled_dist The distance traversed so far.
|
||||
* \param transition_half_length The radius of the transition width.
|
||||
* \param lower_bead_count The bead count at the lower end of the edge.
|
||||
* \return ``true`` if this edge is going down, or ``false`` if it's going
|
||||
* up.
|
||||
*/
|
||||
bool isGoingDown(edge_t* outgoing, coord_t traveled_dist, coord_t transition_half_length, coord_t lower_bead_count) const;
|
||||
|
||||
/*!
|
||||
* Determines whether this edge marks the end of the central region.
|
||||
* \param edge The edge to check.
|
||||
* \return ``true`` if this edge goes from a central region to a non-central
|
||||
* region, or ``false`` in every other case (central to central, non-central
|
||||
* to non-central, non-central to central, or end-of-the-line).
|
||||
*/
|
||||
bool isEndOfCentral(const edge_t& edge) const;
|
||||
|
||||
/*!
|
||||
* Create extra ribs in the graph where the graph contains a parabolic arc
|
||||
* or a straight between two inner corners.
|
||||
*
|
||||
* There might be transitions there as the beads go through a narrow
|
||||
* bottleneck in the polygon.
|
||||
*/
|
||||
void generateExtraRibs();
|
||||
|
||||
// ^ transitioning ^
|
||||
|
||||
// v toolpath generation v
|
||||
|
||||
/*!
|
||||
* \param[out] segments the generated segments
|
||||
*/
|
||||
void generateSegments();
|
||||
|
||||
/*!
|
||||
* From a quad (a group of linked edges in one cell of the Voronoi), find
|
||||
* the edge pointing to the node that is furthest away from the border of the polygon.
|
||||
* \param quad_start_edge The first edge of the quad.
|
||||
* \return The edge of the quad that is furthest away from the border.
|
||||
*/
|
||||
edge_t* getQuadMaxRedgeTo(edge_t* quad_start_edge);
|
||||
|
||||
/*!
|
||||
* Propagate beading information from nodes that are closer to the edge
|
||||
* (low radius R) to nodes that are farther from the edge (high R).
|
||||
*
|
||||
* only propagate from nodes with beading info upward to nodes without beading info
|
||||
*
|
||||
* Edges are sorted by their radius, so that we can do a depth-first walk
|
||||
* without employing a recursive algorithm.
|
||||
*
|
||||
* In upward propagated beadings we store the distance traveled, so that we can merge these beadings with the downward propagated beadings in \ref propagateBeadingsDownward(.)
|
||||
*
|
||||
* \param upward_quad_mids all upward halfedges of the inner skeletal edges (not directly connected to the outline) sorted on their highest [distance_to_boundary]. Higher dist first.
|
||||
*/
|
||||
void propagateBeadingsUpward(std::vector<edge_t*>& upward_quad_mids, ptr_vector_t<BeadingPropagation>& node_beadings);
|
||||
|
||||
/*!
|
||||
* propagate beading info from higher R nodes to lower R nodes
|
||||
*
|
||||
* merge with upward propagated beadings if they are encountered
|
||||
*
|
||||
* don't transfer to nodes which lie on the outline polygon
|
||||
*
|
||||
* edges are sorted so that we can do a depth-first walk without employing a recursive algorithm
|
||||
*
|
||||
* \param upward_quad_mids all upward halfedges of the inner skeletal edges (not directly connected to the outline) sorted on their highest [distance_to_boundary]. Higher dist first.
|
||||
*/
|
||||
void propagateBeadingsDownward(std::vector<edge_t*>& upward_quad_mids, ptr_vector_t<BeadingPropagation>& node_beadings);
|
||||
|
||||
/*!
|
||||
* Subroutine of \ref propagateBeadingsDownward(std::vector<edge_t*>&, ptr_vector_t<BeadingPropagation>&)
|
||||
*/
|
||||
void propagateBeadingsDownward(edge_t* edge_to_peak, ptr_vector_t<BeadingPropagation>& node_beadings);
|
||||
|
||||
/*!
|
||||
* Find a beading in between two other beadings.
|
||||
*
|
||||
* This creates a new beading. With this we can find the coordinates of the
|
||||
* endpoints of the actual line segments to draw.
|
||||
*
|
||||
* The parameters \p left and \p right are not actually always left or right
|
||||
* but just arbitrary directions to visually indicate the difference.
|
||||
* \param left One of the beadings to interpolate between.
|
||||
* \param ratio_left_to_whole The position within the two beadings to sample
|
||||
* an interpolation. Should be a ratio between 0 and 1.
|
||||
* \param right One of the beadings to interpolate between.
|
||||
* \param switching_radius The bead radius at which we switch from the left
|
||||
* beading to the merged beading, if the beadings have a different number of
|
||||
* beads.
|
||||
* \return The beading at the interpolated location.
|
||||
*/
|
||||
Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right, coord_t switching_radius) const;
|
||||
|
||||
/*!
|
||||
* Subroutine of \ref interpolate(const Beading&, Ratio, const Beading&, coord_t)
|
||||
*
|
||||
* This creates a new Beading between two beadings, assuming that both have
|
||||
* the same number of beads.
|
||||
* \param left One of the beadings to interpolate between.
|
||||
* \param ratio_left_to_whole The position within the two beadings to sample
|
||||
* an interpolation. Should be a ratio between 0 and 1.
|
||||
* \param right One of the beadings to interpolate between.
|
||||
* \return The beading at the interpolated location.
|
||||
*/
|
||||
Beading interpolate(const Beading& left, double ratio_left_to_whole, const Beading& right) const;
|
||||
|
||||
/*!
|
||||
* Get the beading at a certain node of the skeletal graph, or create one if
|
||||
* it doesn't have one yet.
|
||||
*
|
||||
* This is a lazy get.
|
||||
* \param node The node to get the beading from.
|
||||
* \param node_beadings A list of all beadings for nodes.
|
||||
* \return The beading of that node.
|
||||
*/
|
||||
std::shared_ptr<BeadingPropagation> getOrCreateBeading(node_t* node, ptr_vector_t<BeadingPropagation>& node_beadings);
|
||||
|
||||
/*!
|
||||
* In case we cannot find the beading of a node, get a beading from the
|
||||
* nearest node.
|
||||
* \param node The node to attempt to get a beading from. The actual node
|
||||
* that the returned beading is from may be a different, nearby node.
|
||||
* \param max_dist The maximum distance to search for.
|
||||
* \return A beading for the node, or ``nullptr`` if there is no node nearby
|
||||
* with a beading.
|
||||
*/
|
||||
std::shared_ptr<BeadingPropagation> getNearestBeading(node_t* node, coord_t max_dist);
|
||||
|
||||
/*!
|
||||
* generate junctions for each bone
|
||||
* \param edge_to_junctions junctions ordered high R to low R
|
||||
*/
|
||||
void generateJunctions(ptr_vector_t<BeadingPropagation>& node_beadings, ptr_vector_t<LineJunctions>& edge_junctions);
|
||||
|
||||
/*!
|
||||
* Add a new toolpath segment, defined between two extrusion-juntions.
|
||||
*
|
||||
* \param from The junction from which to add a segment.
|
||||
* \param to The junction to which to add a segment.
|
||||
* \param is_odd Whether this segment is an odd gap filler along the middle of the skeleton.
|
||||
* \param force_new_path Whether to prevent adding this path to an existing path which ends in \p from
|
||||
* \param from_is_3way Whether the \p from junction is a splitting junction where two normal wall lines and a gap filler line come together.
|
||||
* \param to_is_3way Whether the \p to junction is a splitting junction where two normal wall lines and a gap filler line come together.
|
||||
*/
|
||||
void addToolpathSegment(const ExtrusionJunction& from, const ExtrusionJunction& to, bool is_odd, bool force_new_path, bool from_is_3way, bool to_is_3way);
|
||||
|
||||
/*!
|
||||
* connect junctions in each quad
|
||||
*/
|
||||
void connectJunctions(ptr_vector_t<LineJunctions>& edge_junctions);
|
||||
|
||||
/*!
|
||||
* Genrate small segments for local maxima where the beading would only result in a single bead
|
||||
*/
|
||||
void generateLocalMaximaSingleBeads();
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // VORONOI_QUADRILATERALIZATION_H
|
||||
@@ -0,0 +1,122 @@
|
||||
//Copyright (c) 2021 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef SKELETAL_TRAPEZOIDATION_EDGE_H
|
||||
#define SKELETAL_TRAPEZOIDATION_EDGE_H
|
||||
|
||||
#include <memory> // smart pointers
|
||||
#include <list>
|
||||
#include <vector>
|
||||
|
||||
#include "utils/ExtrusionJunction.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
class SkeletalTrapezoidationEdge
|
||||
{
|
||||
private:
|
||||
enum class Central { UNKNOWN = -1, NO, YES };
|
||||
|
||||
public:
|
||||
/*!
|
||||
* Representing the location along an edge where the anchor position of a transition should be placed.
|
||||
*/
|
||||
struct TransitionMiddle
|
||||
{
|
||||
coord_t pos; // Position along edge as measure from edge.from.p
|
||||
int lower_bead_count;
|
||||
coord_t feature_radius; // The feature radius at which this transition is placed
|
||||
TransitionMiddle(coord_t pos, int lower_bead_count, coord_t feature_radius)
|
||||
: pos(pos), lower_bead_count(lower_bead_count)
|
||||
, feature_radius(feature_radius)
|
||||
{}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Represents the location along an edge where the lower or upper end of a transition should be placed.
|
||||
*/
|
||||
struct TransitionEnd
|
||||
{
|
||||
coord_t pos; // Position along edge as measure from edge.from.p, where the edge is always the half edge oriented from lower to higher R
|
||||
int lower_bead_count;
|
||||
bool is_lower_end; // Whether this is the ed of the transition with lower bead count
|
||||
TransitionEnd(coord_t pos, int lower_bead_count, bool is_lower_end)
|
||||
: pos(pos), lower_bead_count(lower_bead_count), is_lower_end(is_lower_end)
|
||||
{}
|
||||
};
|
||||
|
||||
enum class EdgeType
|
||||
{
|
||||
NORMAL = 0, // from voronoi diagram
|
||||
EXTRA_VD = 1, // introduced to voronoi diagram in order to make the gMAT
|
||||
TRANSITION_END = 2 // introduced to voronoi diagram in order to make the gMAT
|
||||
};
|
||||
EdgeType type;
|
||||
|
||||
SkeletalTrapezoidationEdge() : SkeletalTrapezoidationEdge(EdgeType::NORMAL) {}
|
||||
SkeletalTrapezoidationEdge(const EdgeType &type) : type(type), is_central(Central::UNKNOWN) {}
|
||||
|
||||
bool isCentral() const
|
||||
{
|
||||
assert(is_central != Central::UNKNOWN);
|
||||
return is_central == Central::YES;
|
||||
}
|
||||
void setIsCentral(bool b)
|
||||
{
|
||||
is_central = b ? Central::YES : Central::NO;
|
||||
}
|
||||
bool centralIsSet() const
|
||||
{
|
||||
return is_central != Central::UNKNOWN;
|
||||
}
|
||||
|
||||
bool hasTransitions(bool ignore_empty = false) const
|
||||
{
|
||||
return transitions.use_count() > 0 && (ignore_empty || ! transitions.lock()->empty());
|
||||
}
|
||||
void setTransitions(std::shared_ptr<std::list<TransitionMiddle>> storage)
|
||||
{
|
||||
transitions = storage;
|
||||
}
|
||||
std::shared_ptr<std::list<TransitionMiddle>> getTransitions()
|
||||
{
|
||||
return transitions.lock();
|
||||
}
|
||||
|
||||
bool hasTransitionEnds(bool ignore_empty = false) const
|
||||
{
|
||||
return transition_ends.use_count() > 0 && (ignore_empty || ! transition_ends.lock()->empty());
|
||||
}
|
||||
void setTransitionEnds(std::shared_ptr<std::list<TransitionEnd>> storage)
|
||||
{
|
||||
transition_ends = storage;
|
||||
}
|
||||
std::shared_ptr<std::list<TransitionEnd>> getTransitionEnds()
|
||||
{
|
||||
return transition_ends.lock();
|
||||
}
|
||||
|
||||
bool hasExtrusionJunctions(bool ignore_empty = false) const
|
||||
{
|
||||
return extrusion_junctions.use_count() > 0 && (ignore_empty || ! extrusion_junctions.lock()->empty());
|
||||
}
|
||||
void setExtrusionJunctions(std::shared_ptr<LineJunctions> storage)
|
||||
{
|
||||
extrusion_junctions = storage;
|
||||
}
|
||||
std::shared_ptr<LineJunctions> getExtrusionJunctions()
|
||||
{
|
||||
return extrusion_junctions.lock();
|
||||
}
|
||||
|
||||
private:
|
||||
Central is_central; //! whether the edge is significant; whether the source segments have a sharp angle; -1 is unknown
|
||||
|
||||
std::weak_ptr<std::list<TransitionMiddle>> transitions;
|
||||
std::weak_ptr<std::list<TransitionEnd>> transition_ends;
|
||||
std::weak_ptr<LineJunctions> extrusion_junctions;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // SKELETAL_TRAPEZOIDATION_EDGE_H
|
||||
@@ -0,0 +1,467 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include "SkeletalTrapezoidationGraph.hpp"
|
||||
|
||||
#include <ankerl/unordered_dense.h>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include "utils/linearAlg2D.hpp"
|
||||
#include "../Line.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
STHalfEdge::STHalfEdge(SkeletalTrapezoidationEdge data) : HalfEdge(data) {}
|
||||
|
||||
bool STHalfEdge::canGoUp(bool strict) const
|
||||
{
|
||||
if (to->data.distance_to_boundary > from->data.distance_to_boundary)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (to->data.distance_to_boundary < from->data.distance_to_boundary || strict)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Edge is between equidistqant verts; recurse!
|
||||
for (edge_t* outgoing = next; outgoing != twin; outgoing = outgoing->twin->next)
|
||||
{
|
||||
if (outgoing->canGoUp())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
assert(outgoing->twin); if (!outgoing->twin) return false;
|
||||
assert(outgoing->twin->next); if (!outgoing->twin->next) return true; // This point is on the boundary?! Should never occur
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool STHalfEdge::isUpward() const
|
||||
{
|
||||
if (to->data.distance_to_boundary > from->data.distance_to_boundary)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (to->data.distance_to_boundary < from->data.distance_to_boundary)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Equidistant edge case:
|
||||
std::optional<coord_t> forward_up_dist = this->distToGoUp();
|
||||
std::optional<coord_t> backward_up_dist = twin->distToGoUp();
|
||||
if (forward_up_dist && backward_up_dist)
|
||||
{
|
||||
return forward_up_dist < backward_up_dist;
|
||||
}
|
||||
|
||||
if (forward_up_dist)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (backward_up_dist)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return to->p < from->p; // Arbitrary ordering, which returns the opposite for the twin edge
|
||||
}
|
||||
|
||||
std::optional<coord_t> STHalfEdge::distToGoUp() const
|
||||
{
|
||||
if (to->data.distance_to_boundary > from->data.distance_to_boundary)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (to->data.distance_to_boundary < from->data.distance_to_boundary)
|
||||
{
|
||||
return std::optional<coord_t>();
|
||||
}
|
||||
|
||||
// Edge is between equidistqant verts; recurse!
|
||||
std::optional<coord_t> ret;
|
||||
for (edge_t* outgoing = next; outgoing != twin; outgoing = outgoing->twin->next)
|
||||
{
|
||||
std::optional<coord_t> dist_to_up = outgoing->distToGoUp();
|
||||
if (dist_to_up)
|
||||
{
|
||||
if (ret)
|
||||
{
|
||||
ret = std::min(*ret, *dist_to_up);
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = dist_to_up;
|
||||
}
|
||||
}
|
||||
assert(outgoing->twin); if (!outgoing->twin) return std::optional<coord_t>();
|
||||
assert(outgoing->twin->next); if (!outgoing->twin->next) return 0; // This point is on the boundary?! Should never occur
|
||||
}
|
||||
if (ret)
|
||||
{
|
||||
ret = *ret + (to->p - from->p).cast<int64_t>().norm();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
STHalfEdge* STHalfEdge::getNextUnconnected()
|
||||
{
|
||||
edge_t* result = static_cast<STHalfEdge*>(this);
|
||||
while (result->next)
|
||||
{
|
||||
result = result->next;
|
||||
if (result == this)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return result->twin;
|
||||
}
|
||||
|
||||
STHalfEdgeNode::STHalfEdgeNode(SkeletalTrapezoidationJoint data, Point p) : HalfEdgeNode(data, p) {}
|
||||
|
||||
bool STHalfEdgeNode::isMultiIntersection()
|
||||
{
|
||||
int odd_path_count = 0;
|
||||
edge_t* outgoing = this->incident_edge;
|
||||
do
|
||||
{
|
||||
if ( ! outgoing)
|
||||
{ // This is a node on the outside
|
||||
return false;
|
||||
}
|
||||
if (outgoing->data.isCentral())
|
||||
{
|
||||
odd_path_count++;
|
||||
}
|
||||
} while (outgoing = outgoing->twin->next, outgoing != this->incident_edge);
|
||||
return odd_path_count > 2;
|
||||
}
|
||||
|
||||
bool STHalfEdgeNode::isCentral() const
|
||||
{
|
||||
edge_t* edge = incident_edge;
|
||||
do
|
||||
{
|
||||
if (edge->data.isCentral())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
assert(edge->twin); if (!edge->twin) return false;
|
||||
} while (edge = edge->twin->next, edge != incident_edge);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool STHalfEdgeNode::isLocalMaximum(bool strict) const
|
||||
{
|
||||
if (data.distance_to_boundary == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
edge_t* edge = incident_edge;
|
||||
do
|
||||
{
|
||||
if (edge->canGoUp(strict))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
assert(edge->twin); if (!edge->twin) return false;
|
||||
|
||||
if (!edge->twin->next)
|
||||
{ // This point is on the boundary
|
||||
return false;
|
||||
}
|
||||
} while (edge = edge->twin->next, edge != incident_edge);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SkeletalTrapezoidationGraph::collapseSmallEdges(coord_t snap_dist)
|
||||
{
|
||||
ankerl::unordered_dense::map<edge_t*, Edges::iterator> edge_locator;
|
||||
ankerl::unordered_dense::map<node_t*, Nodes::iterator> node_locator;
|
||||
|
||||
for (auto edge_it = edges.begin(); edge_it != edges.end(); ++edge_it)
|
||||
{
|
||||
edge_locator.emplace(&*edge_it, edge_it);
|
||||
}
|
||||
|
||||
for (auto node_it = nodes.begin(); node_it != nodes.end(); ++node_it)
|
||||
{
|
||||
node_locator.emplace(&*node_it, node_it);
|
||||
}
|
||||
|
||||
auto safelyRemoveEdge = [this, &edge_locator](edge_t* to_be_removed, Edges::iterator& current_edge_it, bool& edge_it_is_updated)
|
||||
{
|
||||
if (current_edge_it != edges.end()
|
||||
&& to_be_removed == &*current_edge_it)
|
||||
{
|
||||
current_edge_it = edges.erase(current_edge_it);
|
||||
edge_it_is_updated = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
edges.erase(edge_locator[to_be_removed]);
|
||||
}
|
||||
};
|
||||
|
||||
auto should_collapse = [snap_dist](node_t* a, node_t* b)
|
||||
{
|
||||
return shorter_then(a->p - b->p, snap_dist);
|
||||
};
|
||||
|
||||
for (auto edge_it = edges.begin(); edge_it != edges.end();)
|
||||
{
|
||||
if (edge_it->prev)
|
||||
{
|
||||
edge_it++;
|
||||
continue;
|
||||
}
|
||||
|
||||
edge_t* quad_start = &*edge_it;
|
||||
edge_t* quad_end = quad_start; while (quad_end->next) quad_end = quad_end->next;
|
||||
edge_t* quad_mid = (quad_start->next == quad_end)? nullptr : quad_start->next;
|
||||
|
||||
bool edge_it_is_updated = false;
|
||||
if (quad_mid && should_collapse(quad_mid->from, quad_mid->to))
|
||||
{
|
||||
assert(quad_mid->twin);
|
||||
if(!quad_mid->twin)
|
||||
{
|
||||
BOOST_LOG_TRIVIAL(warning) << "Encountered quad edge without a twin.";
|
||||
continue; //Prevent accessing unallocated memory.
|
||||
}
|
||||
int count = 0;
|
||||
for (edge_t* edge_from_3 = quad_end; edge_from_3 && edge_from_3 != quad_mid->twin; edge_from_3 = edge_from_3->twin->next)
|
||||
{
|
||||
edge_from_3->from = quad_mid->from;
|
||||
edge_from_3->twin->to = quad_mid->from;
|
||||
if (count > 50)
|
||||
{
|
||||
std::cerr << edge_from_3->from->p << " - " << edge_from_3->to->p << '\n';
|
||||
}
|
||||
if (++count > 1000)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// o-o > collapse top
|
||||
// | |
|
||||
// | |
|
||||
// | |
|
||||
// o o
|
||||
if (quad_mid->from->incident_edge == quad_mid)
|
||||
{
|
||||
if (quad_mid->twin->next)
|
||||
{
|
||||
quad_mid->from->incident_edge = quad_mid->twin->next;
|
||||
}
|
||||
else
|
||||
{
|
||||
quad_mid->from->incident_edge = quad_mid->prev->twin;
|
||||
}
|
||||
}
|
||||
|
||||
nodes.erase(node_locator[quad_mid->to]);
|
||||
|
||||
quad_mid->prev->next = quad_mid->next;
|
||||
quad_mid->next->prev = quad_mid->prev;
|
||||
quad_mid->twin->next->prev = quad_mid->twin->prev;
|
||||
quad_mid->twin->prev->next = quad_mid->twin->next;
|
||||
|
||||
safelyRemoveEdge(quad_mid->twin, edge_it, edge_it_is_updated);
|
||||
safelyRemoveEdge(quad_mid, edge_it, edge_it_is_updated);
|
||||
}
|
||||
|
||||
// o-o
|
||||
// | | > collapse sides
|
||||
// o o
|
||||
if ( should_collapse(quad_start->from, quad_end->to) && should_collapse(quad_start->to, quad_end->from))
|
||||
{ // Collapse start and end edges and remove whole cell
|
||||
|
||||
quad_start->twin->to = quad_end->to;
|
||||
quad_end->to->incident_edge = quad_end->twin;
|
||||
if (quad_end->from->incident_edge == quad_end)
|
||||
{
|
||||
if (quad_end->twin->next)
|
||||
{
|
||||
quad_end->from->incident_edge = quad_end->twin->next;
|
||||
}
|
||||
else
|
||||
{
|
||||
quad_end->from->incident_edge = quad_end->prev->twin;
|
||||
}
|
||||
}
|
||||
nodes.erase(node_locator[quad_start->from]);
|
||||
|
||||
quad_start->twin->twin = quad_end->twin;
|
||||
quad_end->twin->twin = quad_start->twin;
|
||||
safelyRemoveEdge(quad_start, edge_it, edge_it_is_updated);
|
||||
safelyRemoveEdge(quad_end, edge_it, edge_it_is_updated);
|
||||
}
|
||||
// If only one side had collapsable length then the cell on the other side of that edge has to collapse
|
||||
// if we would collapse that one edge then that would change the quad_start and/or quad_end of neighboring cells
|
||||
// this is to do with the constraint that !prev == !twin.next
|
||||
|
||||
if (!edge_it_is_updated)
|
||||
{
|
||||
edge_it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SkeletalTrapezoidationGraph::makeRib(edge_t *&prev_edge, const Point &start_source_point, const Point &end_source_point) {
|
||||
Point p;
|
||||
Line(start_source_point, end_source_point).distance_to_infinite_squared(prev_edge->to->p, &p);
|
||||
coord_t dist = (prev_edge->to->p - p).cast<int64_t>().norm();
|
||||
prev_edge->to->data.distance_to_boundary = dist;
|
||||
assert(dist >= 0);
|
||||
|
||||
nodes.emplace_front(SkeletalTrapezoidationJoint(), p);
|
||||
node_t* node = &nodes.front();
|
||||
node->data.distance_to_boundary = 0;
|
||||
|
||||
edges.emplace_front(SkeletalTrapezoidationEdge(SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD));
|
||||
edge_t* forth_edge = &edges.front();
|
||||
edges.emplace_front(SkeletalTrapezoidationEdge(SkeletalTrapezoidationEdge::EdgeType::EXTRA_VD));
|
||||
edge_t* back_edge = &edges.front();
|
||||
|
||||
prev_edge->next = forth_edge;
|
||||
forth_edge->prev = prev_edge;
|
||||
forth_edge->from = prev_edge->to;
|
||||
forth_edge->to = node;
|
||||
forth_edge->twin = back_edge;
|
||||
back_edge->twin = forth_edge;
|
||||
back_edge->from = node;
|
||||
back_edge->to = prev_edge->to;
|
||||
node->incident_edge = back_edge;
|
||||
|
||||
prev_edge = back_edge;
|
||||
}
|
||||
|
||||
std::pair<SkeletalTrapezoidationGraph::edge_t*, SkeletalTrapezoidationGraph::edge_t*> SkeletalTrapezoidationGraph::insertRib(edge_t& edge, node_t* mid_node)
|
||||
{
|
||||
edge_t* edge_before = edge.prev;
|
||||
edge_t* edge_after = edge.next;
|
||||
node_t* node_before = edge.from;
|
||||
node_t* node_after = edge.to;
|
||||
|
||||
Point p = mid_node->p;
|
||||
|
||||
const Line source_segment = getSource(edge);
|
||||
Point px;
|
||||
source_segment.distance_to_squared(p, &px);
|
||||
coord_t dist = (p - px).cast<int64_t>().norm();
|
||||
assert(dist > 0);
|
||||
mid_node->data.distance_to_boundary = dist;
|
||||
mid_node->data.transition_ratio = 0; // Both transition end should have rest = 0, because at the ends a whole number of beads fits without rest
|
||||
|
||||
nodes.emplace_back(SkeletalTrapezoidationJoint(), px);
|
||||
node_t* source_node = &nodes.back();
|
||||
source_node->data.distance_to_boundary = 0;
|
||||
|
||||
edge_t* first = &edge;
|
||||
edges.emplace_back(SkeletalTrapezoidationEdge());
|
||||
edge_t* second = &edges.back();
|
||||
edges.emplace_back(SkeletalTrapezoidationEdge(SkeletalTrapezoidationEdge::EdgeType::TRANSITION_END));
|
||||
edge_t* outward_edge = &edges.back();
|
||||
edges.emplace_back(SkeletalTrapezoidationEdge(SkeletalTrapezoidationEdge::EdgeType::TRANSITION_END));
|
||||
edge_t* inward_edge = &edges.back();
|
||||
|
||||
if (edge_before)
|
||||
{
|
||||
edge_before->next = first;
|
||||
}
|
||||
first->next = outward_edge;
|
||||
outward_edge->next = nullptr;
|
||||
inward_edge->next = second;
|
||||
second->next = edge_after;
|
||||
|
||||
if (edge_after)
|
||||
{
|
||||
edge_after->prev = second;
|
||||
}
|
||||
second->prev = inward_edge;
|
||||
inward_edge->prev = nullptr;
|
||||
outward_edge->prev = first;
|
||||
first->prev = edge_before;
|
||||
|
||||
first->to = mid_node;
|
||||
outward_edge->to = source_node;
|
||||
inward_edge->to = mid_node;
|
||||
second->to = node_after;
|
||||
|
||||
first->from = node_before;
|
||||
outward_edge->from = mid_node;
|
||||
inward_edge->from = source_node;
|
||||
second->from = mid_node;
|
||||
|
||||
node_before->incident_edge = first;
|
||||
mid_node->incident_edge = outward_edge;
|
||||
source_node->incident_edge = inward_edge;
|
||||
if (edge_after)
|
||||
{
|
||||
node_after->incident_edge = edge_after;
|
||||
}
|
||||
|
||||
first->data.setIsCentral(true);
|
||||
outward_edge->data.setIsCentral(false); // TODO verify this is always the case.
|
||||
inward_edge->data.setIsCentral(false);
|
||||
second->data.setIsCentral(true);
|
||||
|
||||
outward_edge->twin = inward_edge;
|
||||
inward_edge->twin = outward_edge;
|
||||
|
||||
first->twin = nullptr; // we don't know these yet!
|
||||
second->twin = nullptr;
|
||||
|
||||
assert(second->prev->from->data.distance_to_boundary == 0);
|
||||
|
||||
return std::make_pair(first, second);
|
||||
}
|
||||
|
||||
SkeletalTrapezoidationGraph::edge_t* SkeletalTrapezoidationGraph::insertNode(edge_t* edge, Point mid, coord_t mide_node_bead_count)
|
||||
{
|
||||
edge_t* last_edge_replacing_input = edge;
|
||||
|
||||
nodes.emplace_back(SkeletalTrapezoidationJoint(), mid);
|
||||
node_t* mid_node = &nodes.back();
|
||||
|
||||
edge_t* twin = last_edge_replacing_input->twin;
|
||||
last_edge_replacing_input->twin = nullptr;
|
||||
twin->twin = nullptr;
|
||||
std::pair<edge_t*, edge_t*> left_pair = insertRib(*last_edge_replacing_input, mid_node);
|
||||
std::pair<edge_t*, edge_t*> right_pair = insertRib(*twin, mid_node);
|
||||
edge_t* first_edge_replacing_input = left_pair.first;
|
||||
last_edge_replacing_input = left_pair.second;
|
||||
edge_t* first_edge_replacing_twin = right_pair.first;
|
||||
edge_t* last_edge_replacing_twin = right_pair.second;
|
||||
|
||||
first_edge_replacing_input->twin = last_edge_replacing_twin;
|
||||
last_edge_replacing_twin->twin = first_edge_replacing_input;
|
||||
last_edge_replacing_input->twin = first_edge_replacing_twin;
|
||||
first_edge_replacing_twin->twin = last_edge_replacing_input;
|
||||
|
||||
mid_node->data.bead_count = mide_node_bead_count;
|
||||
|
||||
return last_edge_replacing_input;
|
||||
}
|
||||
|
||||
Line SkeletalTrapezoidationGraph::getSource(const edge_t &edge) const
|
||||
{
|
||||
const edge_t *from_edge = &edge;
|
||||
while (from_edge->prev)
|
||||
from_edge = from_edge->prev;
|
||||
|
||||
const edge_t *to_edge = &edge;
|
||||
while (to_edge->next)
|
||||
to_edge = to_edge->next;
|
||||
|
||||
return Line(from_edge->from->p, to_edge->to->p);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef SKELETAL_TRAPEZOIDATION_GRAPH_H
|
||||
#define SKELETAL_TRAPEZOIDATION_GRAPH_H
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "utils/HalfEdgeGraph.hpp"
|
||||
#include "SkeletalTrapezoidationEdge.hpp"
|
||||
#include "SkeletalTrapezoidationJoint.hpp"
|
||||
|
||||
namespace Slic3r
|
||||
{
|
||||
class Line;
|
||||
};
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
class STHalfEdgeNode;
|
||||
|
||||
class STHalfEdge : public HalfEdge<SkeletalTrapezoidationJoint, SkeletalTrapezoidationEdge, STHalfEdgeNode, STHalfEdge>
|
||||
{
|
||||
using edge_t = STHalfEdge;
|
||||
using node_t = STHalfEdgeNode;
|
||||
public:
|
||||
STHalfEdge(SkeletalTrapezoidationEdge data);
|
||||
|
||||
/*!
|
||||
* Check (recursively) whether there is any upward edge from the distance_to_boundary of the from of the \param edge
|
||||
*
|
||||
* \param strict Whether equidistant edges can count as a local maximum
|
||||
*/
|
||||
bool canGoUp(bool strict = false) const;
|
||||
|
||||
/*!
|
||||
* Check whether the edge goes from a lower to a higher distance_to_boundary.
|
||||
* Effectively deals with equidistant edges by looking beyond this edge.
|
||||
*/
|
||||
bool isUpward() const;
|
||||
|
||||
/*!
|
||||
* Calculate the traversed distance until we meet an upward edge.
|
||||
* Useful for calling on edges between equidistant points.
|
||||
*
|
||||
* If we can go up then the distance includes the length of the \param edge
|
||||
*/
|
||||
std::optional<coord_t> distToGoUp() const;
|
||||
|
||||
STHalfEdge* getNextUnconnected();
|
||||
};
|
||||
|
||||
class STHalfEdgeNode : public HalfEdgeNode<SkeletalTrapezoidationJoint, SkeletalTrapezoidationEdge, STHalfEdgeNode, STHalfEdge>
|
||||
{
|
||||
using edge_t = STHalfEdge;
|
||||
using node_t = STHalfEdgeNode;
|
||||
public:
|
||||
STHalfEdgeNode(SkeletalTrapezoidationJoint data, Point p);
|
||||
|
||||
bool isMultiIntersection();
|
||||
|
||||
bool isCentral() const;
|
||||
|
||||
/*!
|
||||
* Check whether this node has a locally maximal distance_to_boundary
|
||||
*
|
||||
* \param strict Whether equidistant edges can count as a local maximum
|
||||
*/
|
||||
bool isLocalMaximum(bool strict = false) const;
|
||||
};
|
||||
|
||||
class SkeletalTrapezoidationGraph: public HalfEdgeGraph<SkeletalTrapezoidationJoint, SkeletalTrapezoidationEdge, STHalfEdgeNode, STHalfEdge>
|
||||
{
|
||||
using edge_t = STHalfEdge;
|
||||
using node_t = STHalfEdgeNode;
|
||||
public:
|
||||
|
||||
/*!
|
||||
* If an edge is too small, collapse it and its twin and fix the surrounding edges to ensure a consistent graph.
|
||||
*
|
||||
* Don't collapse support edges, unless we can collapse the whole quad.
|
||||
*
|
||||
* o-,
|
||||
* | "-o
|
||||
* | | > Don't collapse this edge only.
|
||||
* o o
|
||||
*/
|
||||
void collapseSmallEdges(coord_t snap_dist = 5);
|
||||
|
||||
void makeRib(edge_t*& prev_edge, const Point &start_source_point, const Point &end_source_point);
|
||||
|
||||
/*!
|
||||
* Insert a node into the graph and connect it to the input polygon using ribs
|
||||
*
|
||||
* \return the last edge which replaced [edge], which points to the same [to] node
|
||||
*/
|
||||
edge_t* insertNode(edge_t* edge, Point mid, coord_t mide_node_bead_count);
|
||||
|
||||
/*!
|
||||
* Return the first and last edge of the edges replacing \p edge pointing to the same node
|
||||
*/
|
||||
std::pair<edge_t*, edge_t*> insertRib(edge_t& edge, node_t* mid_node);
|
||||
|
||||
protected:
|
||||
Line getSource(const edge_t& edge) const;
|
||||
};
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,60 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef SKELETAL_TRAPEZOIDATION_JOINT_H
|
||||
#define SKELETAL_TRAPEZOIDATION_JOINT_H
|
||||
|
||||
#include <memory> // smart pointers
|
||||
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
class SkeletalTrapezoidationJoint
|
||||
{
|
||||
using Beading = BeadingStrategy::Beading;
|
||||
public:
|
||||
struct BeadingPropagation
|
||||
{
|
||||
Beading beading;
|
||||
coord_t dist_to_bottom_source;
|
||||
coord_t dist_from_top_source;
|
||||
bool is_upward_propagated_only;
|
||||
BeadingPropagation(const Beading& beading)
|
||||
: beading(beading)
|
||||
, dist_to_bottom_source(0)
|
||||
, dist_from_top_source(0)
|
||||
, is_upward_propagated_only(false)
|
||||
{}
|
||||
};
|
||||
|
||||
coord_t distance_to_boundary;
|
||||
coord_t bead_count;
|
||||
float transition_ratio; //! The distance near the skeleton to leave free because this joint is in the middle of a transition, as a fraction of the inner bead width of the bead at the higher transition.
|
||||
SkeletalTrapezoidationJoint()
|
||||
: distance_to_boundary(-1)
|
||||
, bead_count(-1)
|
||||
, transition_ratio(0)
|
||||
{}
|
||||
|
||||
bool hasBeading() const
|
||||
{
|
||||
return beading.use_count() > 0;
|
||||
}
|
||||
void setBeading(std::shared_ptr<BeadingPropagation> storage)
|
||||
{
|
||||
beading = storage;
|
||||
}
|
||||
std::shared_ptr<BeadingPropagation> getBeading()
|
||||
{
|
||||
return beading.lock();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
std::weak_ptr<BeadingPropagation> beading;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // SKELETAL_TRAPEZOIDATION_JOINT_H
|
||||
@@ -0,0 +1,761 @@
|
||||
// Copyright (c) 2022 Ultimaker B.V.
|
||||
// CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include <algorithm> //For std::partition_copy and std::min_element.
|
||||
|
||||
#include "WallToolPaths.hpp"
|
||||
|
||||
#include "SkeletalTrapezoidation.hpp"
|
||||
#include "utils/linearAlg2D.hpp"
|
||||
#include "EdgeGrid.hpp"
|
||||
#include "utils/SparseLineGrid.hpp"
|
||||
#include "Geometry.hpp"
|
||||
#include "utils/PolylineStitcher.hpp"
|
||||
#include "SVG.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "ClipperUtils.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
//#define ARACHNE_STITCH_PATCH_DEBUG
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
WallToolPaths::WallToolPaths(const Polygons& outline, const coord_t bead_width_0, const coord_t bead_width_x,
|
||||
const size_t inset_count, const coord_t wall_0_inset, const coordf_t layer_height,
|
||||
const PrintObjectConfig &print_object_config, const PrintConfig &print_config)
|
||||
: outline(outline)
|
||||
, bead_width_0(bead_width_0)
|
||||
, bead_width_x(bead_width_x)
|
||||
, inset_count(inset_count)
|
||||
, wall_0_inset(wall_0_inset)
|
||||
, layer_height(layer_height)
|
||||
, print_thin_walls(Slic3r::Arachne::fill_outline_gaps)
|
||||
, min_feature_size(scaled<coord_t>(print_object_config.min_feature_size.value))
|
||||
, min_bead_width(scaled<coord_t>(print_object_config.min_bead_width.value))
|
||||
, small_area_length(static_cast<double>(bead_width_0) / 2.)
|
||||
, wall_transition_filter_deviation(scaled<coord_t>(print_object_config.wall_transition_filter_deviation.value))
|
||||
, wall_transition_length(scaled<coord_t>(print_object_config.wall_transition_length.value))
|
||||
, toolpaths_generated(false)
|
||||
, print_object_config(print_object_config)
|
||||
{
|
||||
assert(!print_config.nozzle_diameter.empty());
|
||||
this->min_nozzle_diameter = float(*std::min_element(print_config.nozzle_diameter.values.begin(), print_config.nozzle_diameter.values.end()));
|
||||
|
||||
if (const auto &min_feature_size_opt = print_object_config.min_feature_size; min_feature_size_opt.percent)
|
||||
this->min_feature_size = scaled<coord_t>(min_feature_size_opt.value * 0.01 * this->min_nozzle_diameter);
|
||||
|
||||
if (const auto &min_bead_width_opt = print_object_config.min_bead_width; min_bead_width_opt.percent)
|
||||
this->min_bead_width = scaled<coord_t>(min_bead_width_opt.value * 0.01 * this->min_nozzle_diameter);
|
||||
|
||||
if (const auto &wall_transition_filter_deviation_opt = print_object_config.wall_transition_filter_deviation; wall_transition_filter_deviation_opt.percent)
|
||||
this->wall_transition_filter_deviation = scaled<coord_t>(wall_transition_filter_deviation_opt.value * 0.01 * this->min_nozzle_diameter);
|
||||
|
||||
if (const auto &wall_transition_length_opt = print_object_config.wall_transition_length; wall_transition_length_opt.percent)
|
||||
this->wall_transition_length = scaled<coord_t>(wall_transition_length_opt.value * 0.01 * this->min_nozzle_diameter);
|
||||
}
|
||||
|
||||
void simplify(Polygon &thiss, const int64_t smallest_line_segment_squared, const int64_t allowed_error_distance_squared)
|
||||
{
|
||||
if (thiss.size() < 3) {
|
||||
thiss.points.clear();
|
||||
return;
|
||||
}
|
||||
if (thiss.size() == 3)
|
||||
return;
|
||||
|
||||
Polygon new_path;
|
||||
Point previous = thiss.points.back();
|
||||
Point previous_previous = thiss.points.at(thiss.points.size() - 2);
|
||||
Point current = thiss.points.at(0);
|
||||
|
||||
/* When removing a vertex, we check the height of the triangle of the area
|
||||
being removed from the original polygon by the simplification. However,
|
||||
when consecutively removing multiple vertices the height of the previously
|
||||
removed vertices w.r.t. the shortcut path changes.
|
||||
In order to not recompute the new height value of previously removed
|
||||
vertices we compute the height of a representative triangle, which covers
|
||||
the same amount of area as the area being cut off. We use the Shoelace
|
||||
formula to accumulate the area under the removed segments. This works by
|
||||
computing the area in a 'fan' where each of the blades of the fan go from
|
||||
the origin to one of the segments. While removing vertices the area in
|
||||
this fan accumulates. By subtracting the area of the blade connected to
|
||||
the short-cutting segment we obtain the total area of the cutoff region.
|
||||
From this area we compute the height of the representative triangle using
|
||||
the standard formula for a triangle area: A = .5*b*h
|
||||
*/
|
||||
int64_t accumulated_area_removed = int64_t(previous.x()) * int64_t(current.y()) - int64_t(previous.y()) * int64_t(current.x()); // Twice the Shoelace formula for area of polygon per line segment.
|
||||
|
||||
for (size_t point_idx = 0; point_idx < thiss.points.size(); point_idx++) {
|
||||
current = thiss.points.at(point_idx % thiss.points.size());
|
||||
|
||||
//Check if the accumulated area doesn't exceed the maximum.
|
||||
Point next;
|
||||
if (point_idx + 1 < thiss.points.size()) {
|
||||
next = thiss.points.at(point_idx + 1);
|
||||
} else if (point_idx + 1 == thiss.points.size() && new_path.size() > 1) { // don't spill over if the [next] vertex will then be equal to [previous]
|
||||
next = new_path[0]; //Spill over to new polygon for checking removed area.
|
||||
} else {
|
||||
next = thiss.points.at((point_idx + 1) % thiss.points.size());
|
||||
}
|
||||
const int64_t removed_area_next = int64_t(current.x()) * int64_t(next.y()) - int64_t(current.y()) * int64_t(next.x()); // Twice the Shoelace formula for area of polygon per line segment.
|
||||
const int64_t negative_area_closing = int64_t(next.x()) * int64_t(previous.y()) - int64_t(next.y()) * int64_t(previous.x()); // area between the origin and the short-cutting segment
|
||||
accumulated_area_removed += removed_area_next;
|
||||
|
||||
const int64_t length2 = (current - previous).cast<int64_t>().squaredNorm();
|
||||
if (length2 < scaled<int64_t>(25.)) {
|
||||
// We're allowed to always delete segments of less than 5 micron.
|
||||
continue;
|
||||
}
|
||||
|
||||
const int64_t area_removed_so_far = accumulated_area_removed + negative_area_closing; // close the shortcut area polygon
|
||||
const int64_t base_length_2 = (next - previous).cast<int64_t>().squaredNorm();
|
||||
|
||||
if (base_length_2 == 0) //Two line segments form a line back and forth with no area.
|
||||
continue; //Remove the vertex.
|
||||
//We want to check if the height of the triangle formed by previous, current and next vertices is less than allowed_error_distance_squared.
|
||||
//1/2 L = A [actual area is half of the computed shoelace value] // Shoelace formula is .5*(...) , but we simplify the computation and take out the .5
|
||||
//A = 1/2 * b * h [triangle area formula]
|
||||
//L = b * h [apply above two and take out the 1/2]
|
||||
//h = L / b [divide by b]
|
||||
//h^2 = (L / b)^2 [square it]
|
||||
//h^2 = L^2 / b^2 [factor the divisor]
|
||||
const int64_t height_2 = double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2);
|
||||
if ((height_2 <= Slic3r::sqr(scaled<coord_t>(0.005)) //Almost exactly colinear (barring rounding errors).
|
||||
&& Line::distance_to_infinite(current, previous, next) <= scaled<double>(0.005))) // make sure that height_2 is not small because of cancellation of positive and negative areas
|
||||
continue;
|
||||
|
||||
if (length2 < smallest_line_segment_squared
|
||||
&& height_2 <= allowed_error_distance_squared) // removing the vertex doesn't introduce too much error.)
|
||||
{
|
||||
const int64_t next_length2 = (current - next).cast<int64_t>().squaredNorm();
|
||||
if (next_length2 > 4 * smallest_line_segment_squared) {
|
||||
// Special case; The next line is long. If we were to remove this, it could happen that we get quite noticeable artifacts.
|
||||
// We should instead move this point to a location where both edges are kept and then remove the previous point that we wanted to keep.
|
||||
// By taking the intersection of these two lines, we get a point that preserves the direction (so it makes the corner a bit more pointy).
|
||||
// We just need to be sure that the intersection point does not introduce an artifact itself.
|
||||
Point intersection_point;
|
||||
bool has_intersection = Line(previous_previous, previous).intersection_infinite(Line(current, next), &intersection_point);
|
||||
if (!has_intersection
|
||||
|| Line::distance_to_infinite_squared(intersection_point, previous, current) > double(allowed_error_distance_squared)
|
||||
|| (intersection_point - previous).cast<int64_t>().squaredNorm() > smallest_line_segment_squared // The intersection point is way too far from the 'previous'
|
||||
|| (intersection_point - next).cast<int64_t>().squaredNorm() > smallest_line_segment_squared) // and 'next' points, so it shouldn't replace 'current'
|
||||
{
|
||||
// We can't find a better spot for it, but the size of the line is more than 5 micron.
|
||||
// So the only thing we can do here is leave it in...
|
||||
}
|
||||
else {
|
||||
// New point seems like a valid one.
|
||||
current = intersection_point;
|
||||
// If there was a previous point added, remove it.
|
||||
if(!new_path.empty()) {
|
||||
new_path.points.pop_back();
|
||||
previous = previous_previous;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
continue; //Remove the vertex.
|
||||
}
|
||||
}
|
||||
//Don't remove the vertex.
|
||||
accumulated_area_removed = removed_area_next; // so that in the next iteration it's the area between the origin, [previous] and [current]
|
||||
previous_previous = previous;
|
||||
previous = current; //Note that "previous" is only updated if we don't remove the vertex.
|
||||
new_path.points.push_back(current);
|
||||
}
|
||||
|
||||
thiss = new_path;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Removes vertices of the polygons to make sure that they are not too high
|
||||
* resolution.
|
||||
*
|
||||
* This removes points which are connected to line segments that are shorter
|
||||
* than the `smallest_line_segment`, unless that would introduce a deviation
|
||||
* in the contour of more than `allowed_error_distance`.
|
||||
*
|
||||
* Criteria:
|
||||
* 1. Never remove a vertex if either of the connceted segments is larger than \p smallest_line_segment
|
||||
* 2. Never remove a vertex if the distance between that vertex and the final resulting polygon would be higher than \p allowed_error_distance
|
||||
* 3. The direction of segments longer than \p smallest_line_segment always
|
||||
* remains unaltered (but their end points may change if it is connected to
|
||||
* a small segment)
|
||||
*
|
||||
* Simplify uses a heuristic and doesn't neccesarily remove all removable
|
||||
* vertices under the above criteria, but simplify may never violate these
|
||||
* criteria. Unless the segments or the distance is smaller than the
|
||||
* rounding error of 5 micron.
|
||||
*
|
||||
* Vertices which introduce an error of less than 5 microns are removed
|
||||
* anyway, even if the segments are longer than the smallest line segment.
|
||||
* This makes sure that (practically) colinear line segments are joined into
|
||||
* a single line segment.
|
||||
* \param smallest_line_segment Maximal length of removed line segments.
|
||||
* \param allowed_error_distance If removing a vertex introduces a deviation
|
||||
* from the original path that is more than this distance, the vertex may
|
||||
* not be removed.
|
||||
*/
|
||||
void simplify(Polygons &thiss, const int64_t smallest_line_segment = scaled<coord_t>(0.01), const int64_t allowed_error_distance = scaled<coord_t>(0.005))
|
||||
{
|
||||
const int64_t allowed_error_distance_squared = int64_t(allowed_error_distance) * int64_t(allowed_error_distance);
|
||||
const int64_t smallest_line_segment_squared = int64_t(smallest_line_segment) * int64_t(smallest_line_segment);
|
||||
for (size_t p = 0; p < thiss.size(); p++)
|
||||
{
|
||||
simplify(thiss[p], smallest_line_segment_squared, allowed_error_distance_squared);
|
||||
if (thiss[p].size() < 3)
|
||||
{
|
||||
thiss.erase(thiss.begin() + p);
|
||||
p--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef SparseLineGrid<PolygonsPointIndex, PolygonsPointIndexSegmentLocator> LocToLineGrid;
|
||||
std::unique_ptr<LocToLineGrid> createLocToLineGrid(const Polygons &polygons, int square_size)
|
||||
{
|
||||
unsigned int n_points = 0;
|
||||
for (const auto &poly : polygons)
|
||||
n_points += poly.size();
|
||||
|
||||
auto ret = std::make_unique<LocToLineGrid>(square_size, n_points);
|
||||
|
||||
for (unsigned int poly_idx = 0; poly_idx < polygons.size(); poly_idx++)
|
||||
for (unsigned int point_idx = 0; point_idx < polygons[poly_idx].size(); point_idx++)
|
||||
ret->insert(PolygonsPointIndex(&polygons, poly_idx, point_idx));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Note: Also tries to solve for near-self intersections, when epsilon >= 1
|
||||
*/
|
||||
void fixSelfIntersections(const coord_t epsilon, Polygons &thiss)
|
||||
{
|
||||
if (epsilon < 1) {
|
||||
ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss), ClipperLib::pftEvenOdd);
|
||||
return;
|
||||
}
|
||||
|
||||
const int64_t half_epsilon = (epsilon + 1) / 2;
|
||||
|
||||
// Points too close to line segments should be moved a little away from those line segments, but less than epsilon,
|
||||
// so at least half-epsilon distance between points can still be guaranteed.
|
||||
constexpr coord_t grid_size = scaled<coord_t>(2.);
|
||||
auto query_grid = createLocToLineGrid(thiss, grid_size);
|
||||
|
||||
const auto move_dist = std::max<int64_t>(2L, half_epsilon - 2);
|
||||
const int64_t half_epsilon_sqrd = half_epsilon * half_epsilon;
|
||||
|
||||
const size_t n = thiss.size();
|
||||
for (size_t poly_idx = 0; poly_idx < n; poly_idx++) {
|
||||
const size_t pathlen = thiss[poly_idx].size();
|
||||
for (size_t point_idx = 0; point_idx < pathlen; ++point_idx) {
|
||||
Point &pt = thiss[poly_idx][point_idx];
|
||||
for (const auto &line : query_grid->getNearby(pt, epsilon)) {
|
||||
const size_t line_next_idx = (line.point_idx + 1) % thiss[line.poly_idx].size();
|
||||
if (poly_idx == line.poly_idx && (point_idx == line.point_idx || point_idx == line_next_idx))
|
||||
continue;
|
||||
|
||||
const Line segment(thiss[line.poly_idx][line.point_idx], thiss[line.poly_idx][line_next_idx]);
|
||||
Point segment_closest_point;
|
||||
segment.distance_to_squared(pt, &segment_closest_point);
|
||||
|
||||
if (half_epsilon_sqrd >= (pt - segment_closest_point).cast<int64_t>().squaredNorm()) {
|
||||
const Point &other = thiss[poly_idx][(point_idx + 1) % pathlen];
|
||||
const Vec2i64 vec = (LinearAlg2D::pointIsLeftOfLine(other, segment.a, segment.b) > 0 ? segment.b - segment.a : segment.a - segment.b).cast<int64_t>();
|
||||
assert(Slic3r::sqr(double(vec.x())) < double(std::numeric_limits<int64_t>::max()));
|
||||
assert(Slic3r::sqr(double(vec.y())) < double(std::numeric_limits<int64_t>::max()));
|
||||
const int64_t len = vec.norm();
|
||||
pt.x() += (-vec.y() * move_dist) / len;
|
||||
pt.y() += (vec.x() * move_dist) / len;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss), ClipperLib::pftEvenOdd);
|
||||
}
|
||||
|
||||
/*!
|
||||
* Removes overlapping consecutive line segments which don't delimit a positive area.
|
||||
*/
|
||||
void removeDegenerateVerts(Polygons &thiss)
|
||||
{
|
||||
for (size_t poly_idx = 0; poly_idx < thiss.size(); poly_idx++) {
|
||||
Polygon &poly = thiss[poly_idx];
|
||||
Polygon result;
|
||||
|
||||
auto isDegenerate = [](const Point &last, const Point &now, const Point &next) {
|
||||
Vec2i64 last_line = (now - last).cast<int64_t>();
|
||||
Vec2i64 next_line = (next - now).cast<int64_t>();
|
||||
return last_line.dot(next_line) == -1 * last_line.norm() * next_line.norm();
|
||||
};
|
||||
bool isChanged = false;
|
||||
for (size_t idx = 0; idx < poly.size(); idx++) {
|
||||
const Point &last = (result.size() == 0) ? poly.back() : result.back();
|
||||
if (idx + 1 == poly.size() && result.size() == 0)
|
||||
break;
|
||||
|
||||
const Point &next = (idx + 1 == poly.size()) ? result[0] : poly[idx + 1];
|
||||
if (isDegenerate(last, poly[idx], next)) { // lines are in the opposite direction
|
||||
// don't add vert to the result
|
||||
isChanged = true;
|
||||
while (result.size() > 1 && isDegenerate(result[result.size() - 2], result.back(), next))
|
||||
result.points.pop_back();
|
||||
} else {
|
||||
result.points.emplace_back(poly[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isChanged) {
|
||||
if (result.size() > 2) {
|
||||
poly = result;
|
||||
} else {
|
||||
thiss.erase(thiss.begin() + poly_idx);
|
||||
poly_idx--; // effectively the next iteration has the same poly_idx (referring to a new poly which is not yet processed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void removeSmallAreas(Polygons &thiss, const double min_area_size, const bool remove_holes)
|
||||
{
|
||||
auto to_path = [](const Polygon &poly) -> ClipperLib::Path {
|
||||
ClipperLib::Path out;
|
||||
for (const Point &pt : poly.points)
|
||||
out.emplace_back(ClipperLib::cInt(pt.x()), ClipperLib::cInt(pt.y()));
|
||||
return out;
|
||||
};
|
||||
|
||||
auto new_end = thiss.end();
|
||||
if (remove_holes) {
|
||||
for (auto it = thiss.begin(); it < new_end;) {
|
||||
// All polygons smaller than target are removed by replacing them with a polygon from the back of the vector.
|
||||
if (fabs(ClipperLib::Area(to_path(*it))) < min_area_size) {
|
||||
--new_end;
|
||||
*it = std::move(*new_end);
|
||||
continue; // Don't increment the iterator such that the polygon just swapped in is checked next.
|
||||
}
|
||||
++it;
|
||||
}
|
||||
} else {
|
||||
// For each polygon, computes the signed area, move small outlines at the end of the vector and keep pointer on small holes
|
||||
Polygons small_holes;
|
||||
for (auto it = thiss.begin(); it < new_end;) {
|
||||
if (double area = ClipperLib::Area(to_path(*it)); fabs(area) < min_area_size) {
|
||||
if (area >= 0) {
|
||||
--new_end;
|
||||
if (it < new_end) {
|
||||
std::swap(*new_end, *it);
|
||||
continue;
|
||||
} else { // Don't self-swap the last Path
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
small_holes.push_back(*it);
|
||||
}
|
||||
}
|
||||
++it;
|
||||
}
|
||||
|
||||
// Removes small holes that have their first point inside one of the removed outlines
|
||||
// Iterating in reverse ensures that unprocessed small holes won't be moved
|
||||
const auto removed_outlines_start = new_end;
|
||||
for (auto hole_it = small_holes.rbegin(); hole_it < small_holes.rend(); hole_it++)
|
||||
for (auto outline_it = removed_outlines_start; outline_it < thiss.end(); outline_it++)
|
||||
if (Polygon(*outline_it).contains(*hole_it->begin())) {
|
||||
new_end--;
|
||||
*hole_it = std::move(*new_end);
|
||||
break;
|
||||
}
|
||||
}
|
||||
thiss.resize(new_end-thiss.begin());
|
||||
}
|
||||
|
||||
void removeColinearEdges(Polygon &poly, const double max_deviation_angle)
|
||||
{
|
||||
// TODO: Can be made more efficient (for example, use pointer-types for process-/skip-indices, so we can swap them without copy).
|
||||
size_t num_removed_in_iteration = 0;
|
||||
do {
|
||||
num_removed_in_iteration = 0;
|
||||
std::vector<bool> process_indices(poly.points.size(), true);
|
||||
|
||||
bool go = true;
|
||||
while (go) {
|
||||
go = false;
|
||||
|
||||
const auto &rpath = poly;
|
||||
const size_t pathlen = rpath.size();
|
||||
if (pathlen <= 3)
|
||||
return;
|
||||
|
||||
std::vector<bool> skip_indices(poly.points.size(), false);
|
||||
|
||||
Polygon new_path;
|
||||
for (size_t point_idx = 0; point_idx < pathlen; ++point_idx) {
|
||||
// Don't iterate directly over process-indices, but do it this way, because there are points _in_ process-indices that should nonetheless
|
||||
// be skipped:
|
||||
if (!process_indices[point_idx]) {
|
||||
new_path.points.push_back(rpath[point_idx]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Should skip the last point for this iteration if the old first was removed (which can be seen from the fact that the new first was skipped):
|
||||
if (point_idx == (pathlen - 1) && skip_indices[0]) {
|
||||
skip_indices[new_path.size()] = true;
|
||||
go = true;
|
||||
new_path.points.push_back(rpath[point_idx]);
|
||||
break;
|
||||
}
|
||||
|
||||
const Point &prev = rpath[(point_idx - 1 + pathlen) % pathlen];
|
||||
const Point &pt = rpath[point_idx];
|
||||
const Point &next = rpath[(point_idx + 1) % pathlen];
|
||||
|
||||
float angle = LinearAlg2D::getAngleLeft(prev, pt, next); // [0 : 2 * pi]
|
||||
if (angle >= float(M_PI)) { angle -= float(M_PI); } // map [pi : 2 * pi] to [0 : pi]
|
||||
|
||||
// Check if the angle is within limits for the point to 'make sense', given the maximum deviation.
|
||||
// If the angle indicates near-parallel segments ignore the point 'pt'
|
||||
if (angle > max_deviation_angle && angle < M_PI - max_deviation_angle) {
|
||||
new_path.points.push_back(pt);
|
||||
} else if (point_idx != (pathlen - 1)) {
|
||||
// Skip the next point, since the current one was removed:
|
||||
skip_indices[new_path.size()] = true;
|
||||
go = true;
|
||||
new_path.points.push_back(next);
|
||||
++point_idx;
|
||||
}
|
||||
}
|
||||
poly = new_path;
|
||||
num_removed_in_iteration += pathlen - poly.points.size();
|
||||
|
||||
process_indices.clear();
|
||||
process_indices.insert(process_indices.end(), skip_indices.begin(), skip_indices.end());
|
||||
}
|
||||
} while (num_removed_in_iteration > 0);
|
||||
}
|
||||
|
||||
void removeColinearEdges(Polygons &thiss, const double max_deviation_angle = 0.0005)
|
||||
{
|
||||
for (int p = 0; p < int(thiss.size()); p++) {
|
||||
removeColinearEdges(thiss[p], max_deviation_angle);
|
||||
if (thiss[p].size() < 3) {
|
||||
thiss.erase(thiss.begin() + p);
|
||||
p--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<VariableWidthLines> &WallToolPaths::generate()
|
||||
{
|
||||
if (this->inset_count < 1)
|
||||
return toolpaths;
|
||||
|
||||
const coord_t smallest_segment = Slic3r::Arachne::meshfix_maximum_resolution;
|
||||
const coord_t allowed_distance = Slic3r::Arachne::meshfix_maximum_deviation;
|
||||
const coord_t epsilon_offset = (allowed_distance / 2) - 1;
|
||||
const double transitioning_angle = Geometry::deg2rad(this->print_object_config.wall_transition_angle.value);
|
||||
constexpr coord_t discretization_step_size = scaled<coord_t>(0.8);
|
||||
|
||||
// Simplify outline for boost::voronoi consumption. Absolutely no self intersections or near-self intersections allowed:
|
||||
// TODO: Open question: Does this indeed fix all (or all-but-one-in-a-million) cases for manifold but otherwise possibly complex polygons?
|
||||
Polygons prepared_outline = offset(offset(offset(outline, -epsilon_offset), epsilon_offset * 2), -epsilon_offset);
|
||||
simplify(prepared_outline, smallest_segment, allowed_distance);
|
||||
fixSelfIntersections(epsilon_offset, prepared_outline);
|
||||
removeDegenerateVerts(prepared_outline);
|
||||
removeColinearEdges(prepared_outline, 0.005);
|
||||
// Removing collinear edges may introduce self intersections, so we need to fix them again
|
||||
fixSelfIntersections(epsilon_offset, prepared_outline);
|
||||
removeDegenerateVerts(prepared_outline);
|
||||
removeSmallAreas(prepared_outline, small_area_length * small_area_length, false);
|
||||
|
||||
// The functions above could produce intersecting polygons that could cause a crash inside Arachne.
|
||||
// Applying Clipper union should be enough to get rid of this issue.
|
||||
// Clipper union also fixed an issue in Arachne that in post-processing Voronoi diagram, some edges
|
||||
// didn't have twin edges. (a non-planar Voronoi diagram probably caused this).
|
||||
prepared_outline = union_(prepared_outline);
|
||||
|
||||
if (area(prepared_outline) <= 0) {
|
||||
assert(toolpaths.empty());
|
||||
return toolpaths;
|
||||
}
|
||||
|
||||
const float external_perimeter_extrusion_width = Flow::rounded_rectangle_extrusion_width_from_spacing(unscale<float>(bead_width_0), float(this->layer_height));
|
||||
const float perimeter_extrusion_width = Flow::rounded_rectangle_extrusion_width_from_spacing(unscale<float>(bead_width_x), float(this->layer_height));
|
||||
|
||||
const double wall_split_middle_threshold = std::clamp(2. * unscaled<double>(this->min_bead_width) / external_perimeter_extrusion_width - 1., 0.01, 0.99); // For an uneven nr. of lines: When to split the middle wall into two.
|
||||
const double wall_add_middle_threshold = std::clamp(unscaled<double>(this->min_bead_width) / perimeter_extrusion_width, 0.01, 0.99); // For an even nr. of lines: When to add a new middle in between the innermost two walls.
|
||||
|
||||
const int wall_distribution_count = this->print_object_config.wall_distribution_count.value;
|
||||
const size_t max_bead_count = (inset_count < std::numeric_limits<coord_t>::max() / 2) ? 2 * inset_count : std::numeric_limits<coord_t>::max();
|
||||
const auto beading_strat = BeadingStrategyFactory::makeStrategy
|
||||
(
|
||||
bead_width_0,
|
||||
bead_width_x,
|
||||
wall_transition_length,
|
||||
transitioning_angle,
|
||||
print_thin_walls,
|
||||
min_bead_width,
|
||||
min_feature_size,
|
||||
wall_split_middle_threshold,
|
||||
wall_add_middle_threshold,
|
||||
max_bead_count,
|
||||
wall_0_inset,
|
||||
wall_distribution_count
|
||||
);
|
||||
const coord_t transition_filter_dist = scaled<coord_t>(100.f);
|
||||
const coord_t allowed_filter_deviation = wall_transition_filter_deviation;
|
||||
SkeletalTrapezoidation wall_maker
|
||||
(
|
||||
prepared_outline,
|
||||
*beading_strat,
|
||||
beading_strat->getTransitioningAngle(),
|
||||
discretization_step_size,
|
||||
transition_filter_dist,
|
||||
allowed_filter_deviation,
|
||||
wall_transition_length
|
||||
);
|
||||
wall_maker.generateToolpaths(toolpaths);
|
||||
|
||||
stitchToolPaths(toolpaths, this->bead_width_x);
|
||||
|
||||
removeSmallLines(toolpaths);
|
||||
|
||||
separateOutInnerContour();
|
||||
|
||||
simplifyToolPaths(toolpaths);
|
||||
|
||||
removeEmptyToolPaths(toolpaths);
|
||||
assert(std::is_sorted(toolpaths.cbegin(), toolpaths.cend(),
|
||||
[](const VariableWidthLines& l, const VariableWidthLines& r)
|
||||
{
|
||||
return l.front().inset_idx < r.front().inset_idx;
|
||||
}) && "WallToolPaths should be sorted from the outer 0th to inner_walls");
|
||||
toolpaths_generated = true;
|
||||
return toolpaths;
|
||||
}
|
||||
|
||||
void WallToolPaths::stitchToolPaths(std::vector<VariableWidthLines> &toolpaths, const coord_t bead_width_x)
|
||||
{
|
||||
const coord_t stitch_distance = bead_width_x - 1; //In 0-width contours, junctions can cause up to 1-line-width gaps. Don't stitch more than 1 line width.
|
||||
|
||||
for (unsigned int wall_idx = 0; wall_idx < toolpaths.size(); wall_idx++) {
|
||||
VariableWidthLines& wall_lines = toolpaths[wall_idx];
|
||||
|
||||
VariableWidthLines stitched_polylines;
|
||||
VariableWidthLines closed_polygons;
|
||||
PolylineStitcher<VariableWidthLines, ExtrusionLine, ExtrusionJunction>::stitch(wall_lines, stitched_polylines, closed_polygons, stitch_distance);
|
||||
#ifdef ARACHNE_STITCH_PATCH_DEBUG
|
||||
for (const ExtrusionLine& line : stitched_polylines) {
|
||||
if ( ! line.is_odd && line.polylineLength() > 3 * stitch_distance && line.size() > 3) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Some even contour lines could not be closed into polygons!";
|
||||
assert(false && "Some even contour lines could not be closed into polygons!");
|
||||
BoundingBox aabb;
|
||||
for (auto line2 : wall_lines)
|
||||
for (auto j : line2)
|
||||
aabb.merge(j.p);
|
||||
{
|
||||
static int iRun = 0;
|
||||
SVG svg(debug_out_path("contours_before.svg-%d.png", iRun), aabb);
|
||||
std::array<const char *, 8> colors = {"gray", "black", "blue", "green", "lime", "purple", "red", "yellow"};
|
||||
size_t color_idx = 0;
|
||||
for (auto& inset : toolpaths)
|
||||
for (auto& line2 : inset) {
|
||||
// svg.writePolyline(line2.toPolygon(), col);
|
||||
|
||||
Polygon poly = line2.toPolygon();
|
||||
Point last = poly.front();
|
||||
for (size_t idx = 1 ; idx < poly.size(); idx++) {
|
||||
Point here = poly[idx];
|
||||
svg.draw(Line(last, here), colors[color_idx]);
|
||||
// svg.draw_text((last + here) / 2, std::to_string(line2.junctions[idx].region_id).c_str(), "black");
|
||||
last = here;
|
||||
}
|
||||
svg.draw(poly[0], colors[color_idx]);
|
||||
// svg.nextLayer();
|
||||
// svg.writePoints(poly, true, 0.1);
|
||||
// svg.nextLayer();
|
||||
color_idx = (color_idx + 1) % colors.size();
|
||||
}
|
||||
}
|
||||
{
|
||||
static int iRun = 0;
|
||||
SVG svg(debug_out_path("contours-%d.svg", iRun), aabb);
|
||||
for (auto& inset : toolpaths)
|
||||
for (auto& line2 : inset)
|
||||
svg.draw_outline(line2.toPolygon(), "gray");
|
||||
for (auto& line2 : stitched_polylines) {
|
||||
const char *col = line2.is_odd ? "gray" : "red";
|
||||
if ( ! line2.is_odd)
|
||||
std::cerr << "Non-closed even wall of size: " << line2.size() << " at " << line2.front().p << "\n";
|
||||
if ( ! line2.is_odd)
|
||||
svg.draw(line2.front().p);
|
||||
Polygon poly = line2.toPolygon();
|
||||
Point last = poly.front();
|
||||
for (size_t idx = 1 ; idx < poly.size(); idx++)
|
||||
{
|
||||
Point here = poly[idx];
|
||||
svg.draw(Line(last, here), col);
|
||||
last = here;
|
||||
}
|
||||
}
|
||||
for (auto line2 : closed_polygons)
|
||||
svg.draw(line2.toPolygon());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // ARACHNE_STITCH_PATCH_DEBUG
|
||||
wall_lines = stitched_polylines; // replace input toolpaths with stitched polylines
|
||||
|
||||
for (ExtrusionLine& wall_polygon : closed_polygons)
|
||||
{
|
||||
if (wall_polygon.junctions.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// PolylineStitcher, in some cases, produced closed extrusion (polygons),
|
||||
// but the endpoints differ by a small distance. So we reconnect them.
|
||||
// FIXME Lukas H.: Investigate more deeply why it is happening.
|
||||
if (wall_polygon.junctions.front().p != wall_polygon.junctions.back().p &&
|
||||
(wall_polygon.junctions.back().p - wall_polygon.junctions.front().p).cast<double>().norm() < stitch_distance) {
|
||||
wall_polygon.junctions.emplace_back(wall_polygon.junctions.front());
|
||||
}
|
||||
wall_polygon.is_closed = true;
|
||||
wall_lines.emplace_back(std::move(wall_polygon)); // add stitched polygons to result
|
||||
}
|
||||
#ifdef DEBUG
|
||||
for (ExtrusionLine& line : wall_lines)
|
||||
{
|
||||
assert(line.inset_idx == wall_idx);
|
||||
}
|
||||
#endif // DEBUG
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T> bool shorterThan(const T &shape, const coord_t check_length)
|
||||
{
|
||||
const auto *p0 = &shape.back();
|
||||
int64_t length = 0;
|
||||
for (const auto &p1 : shape) {
|
||||
length += (*p0 - p1).template cast<int64_t>().norm();
|
||||
if (length >= check_length)
|
||||
return false;
|
||||
p0 = &p1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void WallToolPaths::removeSmallLines(std::vector<VariableWidthLines> &toolpaths)
|
||||
{
|
||||
for (VariableWidthLines &inset : toolpaths) {
|
||||
for (size_t line_idx = 0; line_idx < inset.size(); line_idx++) {
|
||||
ExtrusionLine &line = inset[line_idx];
|
||||
coord_t min_width = std::numeric_limits<coord_t>::max();
|
||||
for (const ExtrusionJunction &j : line)
|
||||
min_width = std::min(min_width, j.w);
|
||||
if (line.is_odd && !line.is_closed && shorterThan(line, min_width / 2)) { // remove line
|
||||
line = std::move(inset.back());
|
||||
inset.erase(--inset.end());
|
||||
line_idx--; // reconsider the current position
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WallToolPaths::simplifyToolPaths(std::vector<VariableWidthLines> &toolpaths)
|
||||
{
|
||||
for (size_t toolpaths_idx = 0; toolpaths_idx < toolpaths.size(); ++toolpaths_idx)
|
||||
{
|
||||
const int64_t maximum_resolution = Slic3r::Arachne::meshfix_maximum_resolution;
|
||||
const int64_t maximum_deviation = Slic3r::Arachne::meshfix_maximum_deviation;
|
||||
const int64_t maximum_extrusion_area_deviation = Slic3r::Arachne::meshfix_maximum_extrusion_area_deviation; // unit: μm²
|
||||
for (auto& line : toolpaths[toolpaths_idx])
|
||||
{
|
||||
line.simplify(maximum_resolution * maximum_resolution, maximum_deviation * maximum_deviation, maximum_extrusion_area_deviation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<VariableWidthLines> &WallToolPaths::getToolPaths()
|
||||
{
|
||||
if (!toolpaths_generated)
|
||||
return generate();
|
||||
return toolpaths;
|
||||
}
|
||||
|
||||
void WallToolPaths::separateOutInnerContour()
|
||||
{
|
||||
//We'll remove all 0-width paths from the original toolpaths and store them separately as polygons.
|
||||
std::vector<VariableWidthLines> actual_toolpaths;
|
||||
actual_toolpaths.reserve(toolpaths.size()); //A bit too much, but the correct order of magnitude.
|
||||
std::vector<VariableWidthLines> contour_paths;
|
||||
contour_paths.reserve(toolpaths.size() / inset_count);
|
||||
inner_contour.clear();
|
||||
for (const VariableWidthLines &inset : toolpaths) {
|
||||
if (inset.empty())
|
||||
continue;
|
||||
bool is_contour = false;
|
||||
for (const ExtrusionLine &line : inset) {
|
||||
for (const ExtrusionJunction &j : line) {
|
||||
if (j.w == 0)
|
||||
is_contour = true;
|
||||
else
|
||||
is_contour = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_contour) {
|
||||
#ifdef DEBUG
|
||||
for (const ExtrusionLine &line : inset)
|
||||
for (const ExtrusionJunction &j : line)
|
||||
assert(j.w == 0);
|
||||
#endif // DEBUG
|
||||
for (const ExtrusionLine &line : inset) {
|
||||
if (line.is_odd)
|
||||
continue; // odd lines don't contribute to the contour
|
||||
else if (line.is_closed) // sometimes an very small even polygonal wall is not stitched into a polygon
|
||||
inner_contour.emplace_back(line.toPolygon());
|
||||
}
|
||||
} else {
|
||||
actual_toolpaths.emplace_back(inset);
|
||||
}
|
||||
}
|
||||
if (!actual_toolpaths.empty())
|
||||
toolpaths = std::move(actual_toolpaths); // Filtered out the 0-width paths.
|
||||
else
|
||||
toolpaths.clear();
|
||||
|
||||
//The output walls from the skeletal trapezoidation have no known winding order, especially if they are joined together from polylines.
|
||||
//They can be in any direction, clockwise or counter-clockwise, regardless of whether the shapes are positive or negative.
|
||||
//To get a correct shape, we need to make the outside contour positive and any holes inside negative.
|
||||
//This can be done by applying the even-odd rule to the shape. This rule is not sensitive to the winding order of the polygon.
|
||||
//The even-odd rule would be incorrect if the polygon self-intersects, but that should never be generated by the skeletal trapezoidation.
|
||||
inner_contour = union_(inner_contour, ClipperLib::PolyFillType::pftEvenOdd);
|
||||
}
|
||||
|
||||
const Polygons& WallToolPaths::getInnerContour()
|
||||
{
|
||||
if (!toolpaths_generated && inset_count > 0)
|
||||
{
|
||||
generate();
|
||||
}
|
||||
else if(inset_count == 0)
|
||||
{
|
||||
return outline;
|
||||
}
|
||||
return inner_contour;
|
||||
}
|
||||
|
||||
bool WallToolPaths::removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpaths)
|
||||
{
|
||||
toolpaths.erase(std::remove_if(toolpaths.begin(), toolpaths.end(), [](const VariableWidthLines& lines)
|
||||
{
|
||||
return lines.empty();
|
||||
}), toolpaths.end());
|
||||
return toolpaths.empty();
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2020 Ultimaker B.V.
|
||||
// CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef CURAENGINE_WALLTOOLPATHS_H
|
||||
#define CURAENGINE_WALLTOOLPATHS_H
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <ankerl/unordered_dense.h>
|
||||
|
||||
#include "BeadingStrategy/BeadingStrategyFactory.hpp"
|
||||
#include "utils/ExtrusionLine.hpp"
|
||||
#include "../Polygon.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
constexpr bool fill_outline_gaps = true;
|
||||
constexpr coord_t meshfix_maximum_resolution = scaled<coord_t>(0.5);
|
||||
constexpr coord_t meshfix_maximum_deviation = scaled<coord_t>(0.025);
|
||||
constexpr coord_t meshfix_maximum_extrusion_area_deviation = scaled<coord_t>(2.);
|
||||
|
||||
class WallToolPaths
|
||||
{
|
||||
public:
|
||||
/*!
|
||||
* A class that creates the toolpaths given an outline, nominal bead width and maximum amount of walls
|
||||
* \param outline An outline of the area in which the ToolPaths are to be generated
|
||||
* \param bead_width_0 The bead width of the first wall used in the generation of the toolpaths
|
||||
* \param bead_width_x The bead width of the inner walls used in the generation of the toolpaths
|
||||
* \param inset_count The maximum number of parallel extrusion lines that make up the wall
|
||||
* \param wall_0_inset How far to inset the outer wall, to make it adhere better to other walls.
|
||||
*/
|
||||
WallToolPaths(const Polygons& outline, coord_t bead_width_0, coord_t bead_width_x, size_t inset_count, coord_t wall_0_inset, coordf_t layer_height, const PrintObjectConfig &print_object_config, const PrintConfig &print_config);
|
||||
|
||||
/*!
|
||||
* Generates the Toolpaths
|
||||
* \return A reference to the newly create ToolPaths
|
||||
*/
|
||||
const std::vector<VariableWidthLines> &generate();
|
||||
|
||||
/*!
|
||||
* Gets the toolpaths, if this called before \p generate() it will first generate the Toolpaths
|
||||
* \return a reference to the toolpaths
|
||||
*/
|
||||
const std::vector<VariableWidthLines> &getToolPaths();
|
||||
|
||||
/*!
|
||||
* Compute the inner contour of the walls. This contour indicates where the walled area ends and its infill begins.
|
||||
* The inside can then be filled, e.g. with skin/infill for the walls of a part, or with a pattern in the case of
|
||||
* infill with extra infill walls.
|
||||
*/
|
||||
void separateOutInnerContour();
|
||||
|
||||
/*!
|
||||
* Gets the inner contour of the area which is inside of the generated tool
|
||||
* paths.
|
||||
*
|
||||
* If the walls haven't been generated yet, this will lazily call the
|
||||
* \p generate() function to generate the walls with variable width.
|
||||
* The resulting polygon will snugly match the inside of the variable-width
|
||||
* walls where the walls get limited by the LimitedBeadingStrategy to a
|
||||
* maximum wall count.
|
||||
* If there are no walls, the outline will be returned.
|
||||
* \return The inner contour of the generated walls.
|
||||
*/
|
||||
const Polygons& getInnerContour();
|
||||
|
||||
/*!
|
||||
* Removes empty paths from the toolpaths
|
||||
* \param toolpaths the VariableWidthPaths generated with \p generate()
|
||||
* \return true if there are still paths left. If all toolpaths were removed it returns false
|
||||
*/
|
||||
static bool removeEmptyToolPaths(std::vector<VariableWidthLines> &toolpaths);
|
||||
|
||||
using ExtrusionLineSet = ankerl::unordered_dense::set<std::pair<const ExtrusionLine *, const ExtrusionLine *>, boost::hash<std::pair<const ExtrusionLine *, const ExtrusionLine *>>>;
|
||||
|
||||
protected:
|
||||
/*!
|
||||
* Stitch the polylines together and form closed polygons.
|
||||
*
|
||||
* Works on both toolpaths and inner contours simultaneously.
|
||||
*/
|
||||
static void stitchToolPaths(std::vector<VariableWidthLines> &toolpaths, coord_t bead_width_x);
|
||||
|
||||
/*!
|
||||
* Remove polylines shorter than half the smallest line width along that polyline.
|
||||
*/
|
||||
static void removeSmallLines(std::vector<VariableWidthLines> &toolpaths);
|
||||
|
||||
/*!
|
||||
* Simplifies the variable-width toolpaths by calling the simplify on every line in the toolpath using the provided
|
||||
* settings.
|
||||
* \param settings The settings as provided by the user
|
||||
* \return
|
||||
*/
|
||||
static void simplifyToolPaths(std::vector<VariableWidthLines> &toolpaths);
|
||||
|
||||
private:
|
||||
const Polygons& outline; //<! A reference to the outline polygon that is the designated area
|
||||
coord_t bead_width_0; //<! The nominal or first extrusion line width with which libArachne generates its walls
|
||||
coord_t bead_width_x; //<! The subsequently extrusion line width with which libArachne generates its walls if WallToolPaths was called with the nominal_bead_width Constructor this is the same as bead_width_0
|
||||
size_t inset_count; //<! The maximum number of walls to generate
|
||||
coord_t wall_0_inset; //<! How far to inset the outer wall. Should only be applied when printing the actual walls, not extra infill/skin/support walls.
|
||||
coordf_t layer_height;
|
||||
bool print_thin_walls; //<! Whether to enable the widening beading meta-strategy for thin features
|
||||
coord_t min_feature_size; //<! The minimum size of the features that can be widened by the widening beading meta-strategy. Features thinner than that will not be printed
|
||||
coord_t min_bead_width; //<! The minimum bead size to use when widening thin model features with the widening beading meta-strategy
|
||||
double small_area_length; //<! The length of the small features which are to be filtered out, this is squared into a surface
|
||||
coord_t wall_transition_filter_deviation; //!< The allowed line width deviation induced by filtering
|
||||
coord_t wall_transition_length;
|
||||
float min_nozzle_diameter;
|
||||
bool toolpaths_generated; //<! Are the toolpaths generated
|
||||
std::vector<VariableWidthLines> toolpaths; //<! The generated toolpaths
|
||||
Polygons inner_contour; //<! The inner contour of the generated toolpaths
|
||||
const PrintObjectConfig &print_object_config;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
|
||||
#endif // CURAENGINE_WALLTOOLPATHS_H
|
||||
@@ -0,0 +1,61 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
|
||||
#ifndef UTILS_EXTRUSION_JUNCTION_H
|
||||
#define UTILS_EXTRUSION_JUNCTION_H
|
||||
|
||||
#include "../../Point.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
/*!
|
||||
* This struct represents one vertex in an extruded path.
|
||||
*
|
||||
* It contains information on how wide the extruded path must be at this point,
|
||||
* and which perimeter it represents.
|
||||
*/
|
||||
struct ExtrusionJunction
|
||||
{
|
||||
/*!
|
||||
* The position of the centreline of the path when it reaches this junction.
|
||||
* This is the position that should end up in the g-code eventually.
|
||||
*/
|
||||
Point p;
|
||||
|
||||
/*!
|
||||
* The width of the extruded path at this junction.
|
||||
*/
|
||||
coord_t w;
|
||||
|
||||
/*!
|
||||
* Which perimeter this junction is part of.
|
||||
*
|
||||
* Perimeters are counted from the outside inwards. The outer wall has index
|
||||
* 0.
|
||||
*/
|
||||
size_t perimeter_index;
|
||||
|
||||
ExtrusionJunction(const Point p, const coord_t w, const coord_t perimeter_index) : p(p), w(w), perimeter_index(perimeter_index) {}
|
||||
|
||||
bool operator==(const ExtrusionJunction &other) const {
|
||||
return p == other.p && w == other.w && perimeter_index == other.perimeter_index;
|
||||
}
|
||||
};
|
||||
|
||||
inline Point operator-(const ExtrusionJunction& a, const ExtrusionJunction& b)
|
||||
{
|
||||
return a.p - b.p;
|
||||
}
|
||||
|
||||
// Identity function, used to be able to make templated algorithms that do their operations on 'point-like' input.
|
||||
inline const Point& make_point(const ExtrusionJunction& ej)
|
||||
{
|
||||
return ej.p;
|
||||
}
|
||||
|
||||
using LineJunctions = std::vector<ExtrusionJunction>; //<! The junctions along a line without further information. See \ref ExtrusionLine for a more extensive class.
|
||||
|
||||
}
|
||||
#endif // UTILS_EXTRUSION_JUNCTION_H
|
||||
@@ -0,0 +1,278 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "ExtrusionLine.hpp"
|
||||
#include "linearAlg2D.hpp"
|
||||
#include "../../PerimeterGenerator.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
ExtrusionLine::ExtrusionLine(const size_t inset_idx, const bool is_odd) : inset_idx(inset_idx), is_odd(is_odd), is_closed(false) {}
|
||||
|
||||
int64_t ExtrusionLine::getLength() const
|
||||
{
|
||||
if (junctions.empty())
|
||||
return 0;
|
||||
|
||||
int64_t len = 0;
|
||||
ExtrusionJunction prev = junctions.front();
|
||||
for (const ExtrusionJunction &next : junctions) {
|
||||
len += (next.p - prev.p).cast<int64_t>().norm();
|
||||
prev = next;
|
||||
}
|
||||
if (is_closed)
|
||||
len += (front().p - back().p).cast<int64_t>().norm();
|
||||
|
||||
return len;
|
||||
}
|
||||
|
||||
void ExtrusionLine::simplify(const int64_t smallest_line_segment_squared, const int64_t allowed_error_distance_squared, const int64_t maximum_extrusion_area_deviation)
|
||||
{
|
||||
const size_t min_path_size = is_closed ? 3 : 2;
|
||||
if (junctions.size() <= min_path_size)
|
||||
return;
|
||||
|
||||
// TODO: allow for the first point to be removed in case of simplifying closed Extrusionlines.
|
||||
|
||||
/* ExtrusionLines are treated as (open) polylines, so in case an ExtrusionLine is actually a closed polygon, its
|
||||
* starting and ending points will be equal (or almost equal). Therefore, the simplification of the ExtrusionLine
|
||||
* should not touch the first and last points. As a result, start simplifying from point at index 1.
|
||||
* */
|
||||
std::vector<ExtrusionJunction> new_junctions;
|
||||
// Starting junction should always exist in the simplified path
|
||||
new_junctions.emplace_back(junctions.front());
|
||||
|
||||
/* Initially, previous_previous is always the same as previous because, for open ExtrusionLines the last junction
|
||||
* cannot be taken into consideration when checking the points at index 1. For closed ExtrusionLines, the first and
|
||||
* last junctions are anyway the same.
|
||||
* */
|
||||
ExtrusionJunction previous_previous = junctions.front();
|
||||
ExtrusionJunction previous = junctions.front();
|
||||
|
||||
/* When removing a vertex, we check the height of the triangle of the area
|
||||
being removed from the original polygon by the simplification. However,
|
||||
when consecutively removing multiple vertices the height of the previously
|
||||
removed vertices w.r.t. the shortcut path changes.
|
||||
In order to not recompute the new height value of previously removed
|
||||
vertices we compute the height of a representative triangle, which covers
|
||||
the same amount of area as the area being cut off. We use the Shoelace
|
||||
formula to accumulate the area under the removed segments. This works by
|
||||
computing the area in a 'fan' where each of the blades of the fan go from
|
||||
the origin to one of the segments. While removing vertices the area in
|
||||
this fan accumulates. By subtracting the area of the blade connected to
|
||||
the short-cutting segment we obtain the total area of the cutoff region.
|
||||
From this area we compute the height of the representative triangle using
|
||||
the standard formula for a triangle area: A = .5*b*h
|
||||
*/
|
||||
const ExtrusionJunction& initial = junctions.at(1);
|
||||
int64_t accumulated_area_removed = int64_t(previous.p.x()) * int64_t(initial.p.y()) - int64_t(previous.p.y()) * int64_t(initial.p.x()); // Twice the Shoelace formula for area of polygon per line segment.
|
||||
|
||||
for (size_t point_idx = 1; point_idx < junctions.size() - 1; point_idx++)
|
||||
{
|
||||
const ExtrusionJunction& current = junctions[point_idx];
|
||||
|
||||
// Spill over in case of overflow, unless the [next] vertex will then be equal to [previous].
|
||||
const bool spill_over = point_idx + 1 == junctions.size() && new_junctions.size() > 1;
|
||||
ExtrusionJunction& next = spill_over ? new_junctions[0] : junctions[point_idx + 1];
|
||||
|
||||
const int64_t removed_area_next = int64_t(current.p.x()) * int64_t(next.p.y()) - int64_t(current.p.y()) * int64_t(next.p.x()); // Twice the Shoelace formula for area of polygon per line segment.
|
||||
const int64_t negative_area_closing = int64_t(next.p.x()) * int64_t(previous.p.y()) - int64_t(next.p.y()) * int64_t(previous.p.x()); // Area between the origin and the short-cutting segment
|
||||
accumulated_area_removed += removed_area_next;
|
||||
|
||||
const int64_t length2 = (current - previous).cast<int64_t>().squaredNorm();
|
||||
if (length2 < scaled<coord_t>(0.025))
|
||||
{
|
||||
// We're allowed to always delete segments of less than 5 micron. The width in this case doesn't matter that much.
|
||||
continue;
|
||||
}
|
||||
|
||||
const int64_t area_removed_so_far = accumulated_area_removed + negative_area_closing; // Close the shortcut area polygon
|
||||
const int64_t base_length_2 = (next - previous).cast<int64_t>().squaredNorm();
|
||||
|
||||
if (base_length_2 == 0) // Two line segments form a line back and forth with no area.
|
||||
{
|
||||
continue; // Remove the junction (vertex).
|
||||
}
|
||||
//We want to check if the height of the triangle formed by previous, current and next vertices is less than allowed_error_distance_squared.
|
||||
//1/2 L = A [actual area is half of the computed shoelace value] // Shoelace formula is .5*(...) , but we simplify the computation and take out the .5
|
||||
//A = 1/2 * b * h [triangle area formula]
|
||||
//L = b * h [apply above two and take out the 1/2]
|
||||
//h = L / b [divide by b]
|
||||
//h^2 = (L / b)^2 [square it]
|
||||
//h^2 = L^2 / b^2 [factor the divisor]
|
||||
const auto height_2 = int64_t(double(area_removed_so_far) * double(area_removed_so_far) / double(base_length_2));
|
||||
const int64_t extrusion_area_error = calculateExtrusionAreaDeviationError(previous, current, next);
|
||||
if ((height_2 <= scaled<coord_t>(0.001) //Almost exactly colinear (barring rounding errors).
|
||||
&& Line::distance_to_infinite(current.p, previous.p, next.p) <= scaled<double>(0.001)) // Make sure that height_2 is not small because of cancellation of positive and negative areas
|
||||
// We shouldn't remove middle junctions of colinear segments if the area changed for the C-P segment is exceeding the maximum allowed
|
||||
&& extrusion_area_error <= maximum_extrusion_area_deviation)
|
||||
{
|
||||
// Remove the current junction (vertex).
|
||||
continue;
|
||||
}
|
||||
|
||||
if (length2 < smallest_line_segment_squared
|
||||
&& height_2 <= allowed_error_distance_squared) // Removing the junction (vertex) doesn't introduce too much error.
|
||||
{
|
||||
const int64_t next_length2 = (current - next).cast<int64_t>().squaredNorm();
|
||||
if (next_length2 > 4 * smallest_line_segment_squared)
|
||||
{
|
||||
// Special case; The next line is long. If we were to remove this, it could happen that we get quite noticeable artifacts.
|
||||
// We should instead move this point to a location where both edges are kept and then remove the previous point that we wanted to keep.
|
||||
// By taking the intersection of these two lines, we get a point that preserves the direction (so it makes the corner a bit more pointy).
|
||||
// We just need to be sure that the intersection point does not introduce an artifact itself.
|
||||
Point intersection_point;
|
||||
bool has_intersection = Line(previous_previous.p, previous.p).intersection_infinite(Line(current.p, next.p), &intersection_point);
|
||||
if (!has_intersection
|
||||
|| Line::distance_to_infinite_squared(intersection_point, previous.p, current.p) > double(allowed_error_distance_squared)
|
||||
|| (intersection_point - previous.p).cast<int64_t>().squaredNorm() > smallest_line_segment_squared // The intersection point is way too far from the 'previous'
|
||||
|| (intersection_point - next.p).cast<int64_t>().squaredNorm() > smallest_line_segment_squared) // and 'next' points, so it shouldn't replace 'current'
|
||||
{
|
||||
// We can't find a better spot for it, but the size of the line is more than 5 micron.
|
||||
// So the only thing we can do here is leave it in...
|
||||
}
|
||||
else
|
||||
{
|
||||
// New point seems like a valid one.
|
||||
const ExtrusionJunction new_to_add = ExtrusionJunction(intersection_point, current.w, current.perimeter_index);
|
||||
// If there was a previous point added, remove it.
|
||||
if(!new_junctions.empty())
|
||||
{
|
||||
new_junctions.pop_back();
|
||||
previous = previous_previous;
|
||||
}
|
||||
|
||||
// The junction (vertex) is replaced by the new one.
|
||||
accumulated_area_removed = removed_area_next; // So that in the next iteration it's the area between the origin, [previous] and [current]
|
||||
previous_previous = previous;
|
||||
previous = new_to_add; // Note that "previous" is only updated if we don't remove the junction (vertex).
|
||||
new_junctions.push_back(new_to_add);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
continue; // Remove the junction (vertex).
|
||||
}
|
||||
}
|
||||
// The junction (vertex) isn't removed.
|
||||
accumulated_area_removed = removed_area_next; // So that in the next iteration it's the area between the origin, [previous] and [current]
|
||||
previous_previous = previous;
|
||||
previous = current; // Note that "previous" is only updated if we don't remove the junction (vertex).
|
||||
new_junctions.push_back(current);
|
||||
}
|
||||
|
||||
// Ending junction (vertex) should always exist in the simplified path
|
||||
new_junctions.emplace_back(junctions.back());
|
||||
|
||||
/* In case this is a closed polygon (instead of a poly-line-segments), the invariant that the first and last points are the same should be enforced.
|
||||
* Since one of them didn't move, and the other can't have been moved further than the constraints, if originally equal, they can simply be equated.
|
||||
*/
|
||||
if ((junctions.front().p - junctions.back().p).cast<int64_t>().squaredNorm() == 0)
|
||||
{
|
||||
new_junctions.back().p = junctions.front().p;
|
||||
}
|
||||
|
||||
junctions = new_junctions;
|
||||
}
|
||||
|
||||
int64_t ExtrusionLine::calculateExtrusionAreaDeviationError(ExtrusionJunction A, ExtrusionJunction B, ExtrusionJunction C) {
|
||||
/*
|
||||
* A B C A C
|
||||
* --------------- **************
|
||||
* | | ------------------------------------------
|
||||
* | |--------------------------| B removed | |***************************|
|
||||
* | | | ---------> | | |
|
||||
* | |--------------------------| | |***************************|
|
||||
* | | ------------------------------------------
|
||||
* --------------- ^ **************
|
||||
* ^ B.w + C.w / 2 ^
|
||||
* A.w + B.w / 2 new_width = weighted_average_width
|
||||
*
|
||||
*
|
||||
* ******** denote the total extrusion area deviation error in the consecutive segments as a result of using the
|
||||
* weighted-average width for the entire extrusion line.
|
||||
*
|
||||
* */
|
||||
const int64_t ab_length = (B.p - A.p).cast<int64_t>().norm();
|
||||
const int64_t bc_length = (C.p - B.p).cast<int64_t>().norm();
|
||||
if (const coord_t width_diff = std::max(std::abs(B.w - A.w), std::abs(C.w - B.w)); width_diff > 1) {
|
||||
// Adjust the width only if there is a difference, or else the rounding errors may produce the wrong
|
||||
// weighted average value.
|
||||
const int64_t ab_weight = (A.w + B.w) / 2;
|
||||
const int64_t bc_weight = (B.w + C.w) / 2;
|
||||
const int64_t weighted_average_width = (ab_length * ab_weight + bc_length * bc_weight) / (ab_length + bc_length);
|
||||
const int64_t ac_length = (C.p - A.p).cast<int64_t>().norm();
|
||||
return std::abs((ab_weight * ab_length + bc_weight * bc_length) - (weighted_average_width * ac_length));
|
||||
} else {
|
||||
// If the width difference is very small, then select the width of the segment that is longer
|
||||
return ab_length > bc_length ? int64_t(width_diff) * bc_length : int64_t(width_diff) * ab_length;
|
||||
}
|
||||
}
|
||||
|
||||
bool ExtrusionLine::is_contour() const
|
||||
{
|
||||
if (!this->is_closed)
|
||||
return false;
|
||||
|
||||
Polygon poly;
|
||||
poly.points.reserve(this->junctions.size());
|
||||
for (const ExtrusionJunction &junction : this->junctions)
|
||||
poly.points.emplace_back(junction.p);
|
||||
|
||||
// Arachne produces contour with clockwise orientation and holes with counterclockwise orientation.
|
||||
return poly.is_clockwise();
|
||||
}
|
||||
|
||||
double ExtrusionLine::area() const {
|
||||
if (!this->is_closed)
|
||||
return 0.;
|
||||
|
||||
double a = 0.;
|
||||
if (this->junctions.size() >= 3) {
|
||||
Vec2d p1 = this->junctions.back().p.cast<double>();
|
||||
for (const ExtrusionJunction &junction : this->junctions) {
|
||||
Vec2d p2 = junction.p.cast<double>();
|
||||
a += cross2(p1, p2);
|
||||
p1 = p2;
|
||||
}
|
||||
}
|
||||
|
||||
return 0.5 * a;
|
||||
}
|
||||
|
||||
Points to_points(const ExtrusionLine &extrusion_line) {
|
||||
Points points;
|
||||
points.reserve(extrusion_line.junctions.size());
|
||||
for (const ExtrusionJunction &junction : extrusion_line.junctions)
|
||||
points.emplace_back(junction.p);
|
||||
return points;
|
||||
}
|
||||
|
||||
BoundingBox get_extents(const ExtrusionLine &extrusion_line) {
|
||||
BoundingBox bbox;
|
||||
for (const ExtrusionJunction &junction : extrusion_line.junctions)
|
||||
bbox.merge(junction.p);
|
||||
return bbox;
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
|
||||
namespace Slic3r {
|
||||
void extrusion_paths_append(ExtrusionPaths &dst, const ClipperLib_Z::Paths &extrusion_paths, const ExtrusionRole role, const Flow &flow)
|
||||
{
|
||||
for (const ClipperLib_Z::Path &extrusion_path : extrusion_paths) {
|
||||
ThickPolyline thick_polyline = Arachne::to_thick_polyline(extrusion_path);
|
||||
Slic3r::append(dst, PerimeterGenerator::thick_polyline_to_multi_path(thick_polyline, role, flow, scaled<float>(0.05), float(SCALED_EPSILON)).paths);
|
||||
}
|
||||
}
|
||||
|
||||
void extrusion_paths_append(ExtrusionPaths &dst, const Arachne::ExtrusionLine &extrusion, const ExtrusionRole role, const Flow &flow)
|
||||
{
|
||||
ThickPolyline thick_polyline = Arachne::to_thick_polyline(extrusion);
|
||||
Slic3r::append(dst, PerimeterGenerator::thick_polyline_to_multi_path(thick_polyline, role, flow, scaled<float>(0.05), float(SCALED_EPSILON)).paths);
|
||||
}
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,293 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
|
||||
#ifndef UTILS_EXTRUSION_LINE_H
|
||||
#define UTILS_EXTRUSION_LINE_H
|
||||
|
||||
#include "ExtrusionJunction.hpp"
|
||||
#include "../../Polyline.hpp"
|
||||
#include "../../Polygon.hpp"
|
||||
#include "../../BoundingBox.hpp"
|
||||
#include "../../ExtrusionEntity.hpp"
|
||||
#include "../../Flow.hpp"
|
||||
#include "../../../clipper/clipper_z.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
struct ThickPolyline;
|
||||
}
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
/*!
|
||||
* Represents a polyline (not just a line) that is to be extruded with variable
|
||||
* line width.
|
||||
*
|
||||
* This polyline is a sequence of \ref ExtrusionJunction, with a bit of metadata
|
||||
* about which inset it represents.
|
||||
*/
|
||||
struct ExtrusionLine
|
||||
{
|
||||
/*!
|
||||
* Which inset this path represents, counted from the outside inwards.
|
||||
*
|
||||
* The outer wall has index 0.
|
||||
*/
|
||||
size_t inset_idx;
|
||||
|
||||
/*!
|
||||
* If a thin piece needs to be printed with an odd number of walls (e.g. 5
|
||||
* walls) then there will be one wall in the middle that is not a loop. This
|
||||
* field indicates whether this path is such a line through the middle, that
|
||||
* has no companion line going back on the other side and is not a closed
|
||||
* loop.
|
||||
*/
|
||||
bool is_odd;
|
||||
|
||||
/*!
|
||||
* Whether this is a closed polygonal path
|
||||
*/
|
||||
bool is_closed;
|
||||
|
||||
/*!
|
||||
* Gets the number of vertices in this polygon.
|
||||
* \return The number of vertices in this polygon.
|
||||
*/
|
||||
size_t size() const { return junctions.size(); }
|
||||
|
||||
/*!
|
||||
* Whether there are no junctions.
|
||||
*/
|
||||
bool empty() const { return junctions.empty(); }
|
||||
|
||||
/*!
|
||||
* The list of vertices along which this path runs.
|
||||
*
|
||||
* Each junction has a width, making this path a variable-width path.
|
||||
*/
|
||||
std::vector<ExtrusionJunction> junctions;
|
||||
|
||||
ExtrusionLine(const size_t inset_idx, const bool is_odd);
|
||||
ExtrusionLine() : inset_idx(-1), is_odd(true), is_closed(false) {}
|
||||
ExtrusionLine(const ExtrusionLine &other) : inset_idx(other.inset_idx), is_odd(other.is_odd), is_closed(other.is_closed), junctions(other.junctions) {}
|
||||
|
||||
ExtrusionLine &operator=(ExtrusionLine &&other)
|
||||
{
|
||||
junctions = std::move(other.junctions);
|
||||
inset_idx = other.inset_idx;
|
||||
is_odd = other.is_odd;
|
||||
is_closed = other.is_closed;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ExtrusionLine &operator=(const ExtrusionLine &other)
|
||||
{
|
||||
junctions = other.junctions;
|
||||
inset_idx = other.inset_idx;
|
||||
is_odd = other.is_odd;
|
||||
is_closed = other.is_closed;
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::vector<ExtrusionJunction>::const_iterator begin() const { return junctions.begin(); }
|
||||
std::vector<ExtrusionJunction>::const_iterator end() const { return junctions.end(); }
|
||||
std::vector<ExtrusionJunction>::const_reverse_iterator rbegin() const { return junctions.rbegin(); }
|
||||
std::vector<ExtrusionJunction>::const_reverse_iterator rend() const { return junctions.rend(); }
|
||||
std::vector<ExtrusionJunction>::const_reference front() const { return junctions.front(); }
|
||||
std::vector<ExtrusionJunction>::const_reference back() const { return junctions.back(); }
|
||||
const ExtrusionJunction &operator[](unsigned int index) const { return junctions[index]; }
|
||||
ExtrusionJunction &operator[](unsigned int index) { return junctions[index]; }
|
||||
std::vector<ExtrusionJunction>::iterator begin() { return junctions.begin(); }
|
||||
std::vector<ExtrusionJunction>::iterator end() { return junctions.end(); }
|
||||
std::vector<ExtrusionJunction>::reference front() { return junctions.front(); }
|
||||
std::vector<ExtrusionJunction>::reference back() { return junctions.back(); }
|
||||
|
||||
template<typename... Args> void emplace_back(Args &&...args) { junctions.emplace_back(args...); }
|
||||
void remove(unsigned int index) { junctions.erase(junctions.begin() + index); }
|
||||
void insert(size_t index, const ExtrusionJunction &p) { junctions.insert(junctions.begin() + index, p); }
|
||||
|
||||
template<class iterator>
|
||||
std::vector<ExtrusionJunction>::iterator insert(std::vector<ExtrusionJunction>::const_iterator pos, iterator first, iterator last)
|
||||
{
|
||||
return junctions.insert(pos, first, last);
|
||||
}
|
||||
|
||||
void clear() { junctions.clear(); }
|
||||
void reverse() { std::reverse(junctions.begin(), junctions.end()); }
|
||||
|
||||
/*!
|
||||
* Sum the total length of this path.
|
||||
*/
|
||||
int64_t getLength() const;
|
||||
int64_t polylineLength() const { return getLength(); }
|
||||
|
||||
/*!
|
||||
* Put all junction locations into a polygon object.
|
||||
*
|
||||
* When this path is not closed the returned Polygon should be handled as a polyline, rather than a polygon.
|
||||
*/
|
||||
Polygon toPolygon() const
|
||||
{
|
||||
Polygon ret;
|
||||
for (const ExtrusionJunction &j : junctions)
|
||||
ret.points.emplace_back(j.p);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Removes vertices of the ExtrusionLines to make sure that they are not too high
|
||||
* resolution.
|
||||
*
|
||||
* This removes junctions which are connected to line segments that are shorter
|
||||
* than the `smallest_line_segment`, unless that would introduce a deviation
|
||||
* in the contour of more than `allowed_error_distance`.
|
||||
*
|
||||
* Criteria:
|
||||
* 1. Never remove a junction if either of the connected segments is larger than \p smallest_line_segment
|
||||
* 2. Never remove a junction if the distance between that junction and the final resulting polygon would be higher
|
||||
* than \p allowed_error_distance
|
||||
* 3. The direction of segments longer than \p smallest_line_segment always
|
||||
* remains unaltered (but their end points may change if it is connected to
|
||||
* a small segment)
|
||||
* 4. Never remove a junction if it has a distinctively different width than the next junction, as this can
|
||||
* introduce unwanted irregularities on the wall widths.
|
||||
*
|
||||
* Simplify uses a heuristic and doesn't necessarily remove all removable
|
||||
* vertices under the above criteria, but simplify may never violate these
|
||||
* criteria. Unless the segments or the distance is smaller than the
|
||||
* rounding error of 5 micron.
|
||||
*
|
||||
* Vertices which introduce an error of less than 5 microns are removed
|
||||
* anyway, even if the segments are longer than the smallest line segment.
|
||||
* This makes sure that (practically) co-linear line segments are joined into
|
||||
* a single line segment.
|
||||
* \param smallest_line_segment Maximal length of removed line segments.
|
||||
* \param allowed_error_distance If removing a vertex introduces a deviation
|
||||
* from the original path that is more than this distance, the vertex may
|
||||
* not be removed.
|
||||
* \param maximum_extrusion_area_deviation The maximum extrusion area deviation allowed when removing intermediate
|
||||
* junctions from a straight ExtrusionLine
|
||||
*/
|
||||
void simplify(int64_t smallest_line_segment_squared, int64_t allowed_error_distance_squared, int64_t maximum_extrusion_area_deviation);
|
||||
|
||||
/*!
|
||||
* Computes and returns the total area error (in μm²) of the AB and BC segments of an ABC straight ExtrusionLine
|
||||
* when the junction B with a width B.w is removed from the ExtrusionLine. The area changes due to the fact that the
|
||||
* new simplified line AC has a uniform width which equals to the weighted average of the width of the subsegments
|
||||
* (based on their length).
|
||||
*
|
||||
* \param A Start point of the 3-point-straight line
|
||||
* \param B Intermediate point of the 3-point-straight line
|
||||
* \param C End point of the 3-point-straight line
|
||||
* */
|
||||
static int64_t calculateExtrusionAreaDeviationError(ExtrusionJunction A, ExtrusionJunction B, ExtrusionJunction C);
|
||||
|
||||
bool is_contour() const;
|
||||
|
||||
double area() const;
|
||||
|
||||
bool is_external_perimeter() const { return this->inset_idx == 0; }
|
||||
};
|
||||
|
||||
static inline Slic3r::ThickPolyline to_thick_polyline(const Arachne::ExtrusionLine &line_junctions)
|
||||
{
|
||||
assert(line_junctions.size() >= 2);
|
||||
Slic3r::ThickPolyline out;
|
||||
out.points.emplace_back(line_junctions.front().p);
|
||||
out.width.emplace_back(line_junctions.front().w);
|
||||
out.points.emplace_back(line_junctions[1].p);
|
||||
out.width.emplace_back(line_junctions[1].w);
|
||||
|
||||
auto it_prev = line_junctions.begin() + 1;
|
||||
for (auto it = line_junctions.begin() + 2; it != line_junctions.end(); ++it) {
|
||||
out.points.emplace_back(it->p);
|
||||
out.width.emplace_back(it_prev->w);
|
||||
out.width.emplace_back(it->w);
|
||||
it_prev = it;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static inline Slic3r::ThickPolyline to_thick_polyline(const ClipperLib_Z::Path &path)
|
||||
{
|
||||
assert(path.size() >= 2);
|
||||
Slic3r::ThickPolyline out;
|
||||
out.points.emplace_back(path.front().x(), path.front().y());
|
||||
out.width.emplace_back(path.front().z());
|
||||
out.points.emplace_back(path[1].x(), path[1].y());
|
||||
out.width.emplace_back(path[1].z());
|
||||
|
||||
auto it_prev = path.begin() + 1;
|
||||
for (auto it = path.begin() + 2; it != path.end(); ++it) {
|
||||
out.points.emplace_back(it->x(), it->y());
|
||||
out.width.emplace_back(it_prev->z());
|
||||
out.width.emplace_back(it->z());
|
||||
it_prev = it;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static inline Polygon to_polygon(const ExtrusionLine &line)
|
||||
{
|
||||
Polygon out;
|
||||
assert(line.is_closed);
|
||||
assert(line.junctions.size() >= 3);
|
||||
assert(line.junctions.front().p == line.junctions.back().p);
|
||||
out.points.reserve(line.junctions.size() - 1);
|
||||
for (auto it = line.junctions.begin(); it != line.junctions.end() - 1; ++it)
|
||||
out.points.emplace_back(it->p);
|
||||
return out;
|
||||
}
|
||||
|
||||
Points to_points(const ExtrusionLine &extrusion_line);
|
||||
|
||||
BoundingBox get_extents(const ExtrusionLine &extrusion_line);
|
||||
|
||||
#if 0
|
||||
static BoundingBox get_extents(const std::vector<ExtrusionLine> &extrusion_lines)
|
||||
{
|
||||
BoundingBox bbox;
|
||||
for (const ExtrusionLine &extrusion_line : extrusion_lines)
|
||||
bbox.merge(get_extents(extrusion_line));
|
||||
return bbox;
|
||||
}
|
||||
|
||||
static BoundingBox get_extents(const std::vector<const ExtrusionLine *> &extrusion_lines)
|
||||
{
|
||||
BoundingBox bbox;
|
||||
for (const ExtrusionLine *extrusion_line : extrusion_lines) {
|
||||
assert(extrusion_line != nullptr);
|
||||
bbox.merge(get_extents(*extrusion_line));
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
|
||||
static std::vector<Points> to_points(const std::vector<const ExtrusionLine *> &extrusion_lines)
|
||||
{
|
||||
std::vector<Points> points;
|
||||
for (const ExtrusionLine *extrusion_line : extrusion_lines) {
|
||||
assert(extrusion_line != nullptr);
|
||||
points.emplace_back(to_points(*extrusion_line));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
#endif
|
||||
|
||||
using VariableWidthLines = std::vector<ExtrusionLine>; //<! The ExtrusionLines generated by libArachne
|
||||
using Perimeter = VariableWidthLines;
|
||||
using Perimeters = std::vector<Perimeter>;
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void extrusion_paths_append(ExtrusionPaths &dst, const ClipperLib_Z::Paths &extrusion_paths, const ExtrusionRole role, const Flow &flow);
|
||||
void extrusion_paths_append(ExtrusionPaths &dst, const Arachne::ExtrusionLine &extrusion, const ExtrusionRole role, const Flow &flow);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // UTILS_EXTRUSION_LINE_H
|
||||
@@ -0,0 +1,39 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef UTILS_HALF_EDGE_H
|
||||
#define UTILS_HALF_EDGE_H
|
||||
|
||||
#include <forward_list>
|
||||
#include <optional>
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
template<typename node_data_t, typename edge_data_t, typename derived_node_t, typename derived_edge_t>
|
||||
class HalfEdgeNode;
|
||||
|
||||
|
||||
template<typename node_data_t, typename edge_data_t, typename derived_node_t, typename derived_edge_t>
|
||||
class HalfEdge
|
||||
{
|
||||
using edge_t = derived_edge_t;
|
||||
using node_t = derived_node_t;
|
||||
public:
|
||||
edge_data_t data;
|
||||
edge_t* twin = nullptr;
|
||||
edge_t* next = nullptr;
|
||||
edge_t* prev = nullptr;
|
||||
node_t* from = nullptr;
|
||||
node_t* to = nullptr;
|
||||
HalfEdge(edge_data_t data)
|
||||
: data(data)
|
||||
{}
|
||||
bool operator==(const edge_t& other)
|
||||
{
|
||||
return this == &other;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // UTILS_HALF_EDGE_H
|
||||
@@ -0,0 +1,31 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef UTILS_HALF_EDGE_GRAPH_H
|
||||
#define UTILS_HALF_EDGE_GRAPH_H
|
||||
|
||||
|
||||
#include <list>
|
||||
#include <cassert>
|
||||
|
||||
|
||||
|
||||
#include "HalfEdge.hpp"
|
||||
#include "HalfEdgeNode.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
template<class node_data_t, class edge_data_t, class derived_node_t, class derived_edge_t> // types of data contained in nodes and edges
|
||||
class HalfEdgeGraph
|
||||
{
|
||||
public:
|
||||
using edge_t = derived_edge_t;
|
||||
using node_t = derived_node_t;
|
||||
using Edges = std::list<edge_t>;
|
||||
using Nodes = std::list<node_t>;
|
||||
Edges edges;
|
||||
Nodes nodes;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // UTILS_HALF_EDGE_GRAPH_H
|
||||
@@ -0,0 +1,38 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef UTILS_HALF_EDGE_NODE_H
|
||||
#define UTILS_HALF_EDGE_NODE_H
|
||||
|
||||
#include <list>
|
||||
|
||||
#include "../../Point.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
template<typename node_data_t, typename edge_data_t, typename derived_node_t, typename derived_edge_t>
|
||||
class HalfEdge;
|
||||
|
||||
template<typename node_data_t, typename edge_data_t, typename derived_node_t, typename derived_edge_t>
|
||||
class HalfEdgeNode
|
||||
{
|
||||
using edge_t = derived_edge_t;
|
||||
using node_t = derived_node_t;
|
||||
public:
|
||||
node_data_t data;
|
||||
Point p;
|
||||
edge_t* incident_edge = nullptr;
|
||||
HalfEdgeNode(node_data_t data, Point p)
|
||||
: data(data)
|
||||
, p(p)
|
||||
{}
|
||||
|
||||
bool operator==(const node_t& other)
|
||||
{
|
||||
return this == &other;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // UTILS_HALF_EDGE_NODE_H
|
||||
@@ -0,0 +1,178 @@
|
||||
//Copyright (c) 2018 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef UTILS_POLYGONS_POINT_INDEX_H
|
||||
#define UTILS_POLYGONS_POINT_INDEX_H
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "../../Point.hpp"
|
||||
#include "../../Polygon.hpp"
|
||||
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
// Identity function, used to be able to make templated algorithms where the input is sometimes points, sometimes things that contain or can be converted to points.
|
||||
inline const Point &make_point(const Point &p) { return p; }
|
||||
|
||||
/*!
|
||||
* A class for iterating over the points in one of the polygons in a \ref Polygons object
|
||||
*/
|
||||
template<typename Paths>
|
||||
class PathsPointIndex
|
||||
{
|
||||
public:
|
||||
/*!
|
||||
* The polygons into which this index is indexing.
|
||||
*/
|
||||
const Paths* polygons; // (pointer to const polygons)
|
||||
|
||||
unsigned int poly_idx; //!< The index of the polygon in \ref PolygonsPointIndex::polygons
|
||||
|
||||
unsigned int point_idx; //!< The index of the point in the polygon in \ref PolygonsPointIndex::polygons
|
||||
|
||||
/*!
|
||||
* Constructs an empty point index to no polygon.
|
||||
*
|
||||
* This is used as a placeholder for when there is a zero-construction
|
||||
* needed. Since the `polygons` field is const you can't ever make this
|
||||
* initialisation useful.
|
||||
*/
|
||||
PathsPointIndex() : polygons(nullptr), poly_idx(0), point_idx(0) {}
|
||||
|
||||
/*!
|
||||
* Constructs a new point index to a vertex of a polygon.
|
||||
* \param polygons The Polygons instance to which this index points.
|
||||
* \param poly_idx The index of the sub-polygon to point to.
|
||||
* \param point_idx The index of the vertex in the sub-polygon.
|
||||
*/
|
||||
PathsPointIndex(const Paths *polygons, unsigned int poly_idx, unsigned int point_idx) : polygons(polygons), poly_idx(poly_idx), point_idx(point_idx) {}
|
||||
|
||||
/*!
|
||||
* Copy constructor to copy these indices.
|
||||
*/
|
||||
PathsPointIndex(const PathsPointIndex& original) = default;
|
||||
|
||||
Point p() const
|
||||
{
|
||||
if (!polygons)
|
||||
return {0, 0};
|
||||
|
||||
return make_point((*polygons)[poly_idx][point_idx]);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Returns whether this point is initialised.
|
||||
*/
|
||||
bool initialized() const { return polygons; }
|
||||
|
||||
/*!
|
||||
* Get the polygon to which this PolygonsPointIndex refers
|
||||
*/
|
||||
const Polygon &getPolygon() const { return (*polygons)[poly_idx]; }
|
||||
|
||||
/*!
|
||||
* Test whether two iterators refer to the same polygon in the same polygon list.
|
||||
*
|
||||
* \param other The PolygonsPointIndex to test for equality
|
||||
* \return Wether the right argument refers to the same polygon in the same ListPolygon as the left argument.
|
||||
*/
|
||||
bool operator==(const PathsPointIndex &other) const
|
||||
{
|
||||
return polygons == other.polygons && poly_idx == other.poly_idx && point_idx == other.point_idx;
|
||||
}
|
||||
bool operator!=(const PathsPointIndex &other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
bool operator<(const PathsPointIndex &other) const
|
||||
{
|
||||
return this->p() < other.p();
|
||||
}
|
||||
PathsPointIndex &operator=(const PathsPointIndex &other)
|
||||
{
|
||||
polygons = other.polygons;
|
||||
poly_idx = other.poly_idx;
|
||||
point_idx = other.point_idx;
|
||||
return *this;
|
||||
}
|
||||
//! move the iterator forward (and wrap around at the end)
|
||||
PathsPointIndex &operator++()
|
||||
{
|
||||
point_idx = (point_idx + 1) % (*polygons)[poly_idx].size();
|
||||
return *this;
|
||||
}
|
||||
//! move the iterator backward (and wrap around at the beginning)
|
||||
PathsPointIndex &operator--()
|
||||
{
|
||||
if (point_idx == 0)
|
||||
point_idx = (*polygons)[poly_idx].size();
|
||||
point_idx--;
|
||||
return *this;
|
||||
}
|
||||
//! move the iterator forward (and wrap around at the end)
|
||||
PathsPointIndex next() const
|
||||
{
|
||||
PathsPointIndex ret(*this);
|
||||
++ret;
|
||||
return ret;
|
||||
}
|
||||
//! move the iterator backward (and wrap around at the beginning)
|
||||
PathsPointIndex prev() const
|
||||
{
|
||||
PathsPointIndex ret(*this);
|
||||
--ret;
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
using PolygonsPointIndex = PathsPointIndex<Polygons>;
|
||||
|
||||
/*!
|
||||
* Locator to extract a line segment out of a \ref PolygonsPointIndex
|
||||
*/
|
||||
struct PolygonsPointIndexSegmentLocator
|
||||
{
|
||||
std::pair<Point, Point> operator()(const PolygonsPointIndex &val) const
|
||||
{
|
||||
const Polygon &poly = (*val.polygons)[val.poly_idx];
|
||||
Point start = poly[val.point_idx];
|
||||
unsigned int next_point_idx = (val.point_idx + 1) % poly.size();
|
||||
Point end = poly[next_point_idx];
|
||||
return std::pair<Point, Point>(start, end);
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Locator of a \ref PolygonsPointIndex
|
||||
*/
|
||||
template<typename Paths>
|
||||
struct PathsPointIndexLocator
|
||||
{
|
||||
Point operator()(const PathsPointIndex<Paths>& val) const
|
||||
{
|
||||
return make_point(val.p());
|
||||
}
|
||||
};
|
||||
|
||||
}//namespace Slic3r::Arachne
|
||||
|
||||
namespace std
|
||||
{
|
||||
/*!
|
||||
* Hash function for \ref PolygonsPointIndex
|
||||
*/
|
||||
template <>
|
||||
struct hash<Slic3r::Arachne::PolygonsPointIndex>
|
||||
{
|
||||
size_t operator()(const Slic3r::Arachne::PolygonsPointIndex& lpi) const
|
||||
{
|
||||
return Slic3r::PointHash{}(lpi.p());
|
||||
}
|
||||
};
|
||||
}//namespace std
|
||||
|
||||
|
||||
|
||||
#endif//UTILS_POLYGONS_POINT_INDEX_H
|
||||
@@ -0,0 +1,50 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef UTILS_POLYGONS_SEGMENT_INDEX_H
|
||||
#define UTILS_POLYGONS_SEGMENT_INDEX_H
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "PolygonsPointIndex.hpp"
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
/*!
|
||||
* A class for iterating over the points in one of the polygons in a \ref Polygons object
|
||||
*/
|
||||
class PolygonsSegmentIndex : public PolygonsPointIndex
|
||||
{
|
||||
public:
|
||||
PolygonsSegmentIndex() : PolygonsPointIndex(){};
|
||||
PolygonsSegmentIndex(const Polygons *polygons, unsigned int poly_idx, unsigned int point_idx) : PolygonsPointIndex(polygons, poly_idx, point_idx){};
|
||||
|
||||
Point from() const { return PolygonsPointIndex::p(); }
|
||||
|
||||
Point to() const { return PolygonsSegmentIndex::next().p(); }
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
|
||||
namespace boost::polygon {
|
||||
|
||||
template<> struct geometry_concept<Slic3r::Arachne::PolygonsSegmentIndex>
|
||||
{
|
||||
typedef segment_concept type;
|
||||
};
|
||||
|
||||
template<> struct segment_traits<Slic3r::Arachne::PolygonsSegmentIndex>
|
||||
{
|
||||
typedef coord_t coordinate_type;
|
||||
typedef Slic3r::Point point_type;
|
||||
|
||||
static inline point_type get(const Slic3r::Arachne::PolygonsSegmentIndex &CSegment, direction_1d dir)
|
||||
{
|
||||
return dir.to_int() ? CSegment.to() : CSegment.from();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace boost::polygon
|
||||
|
||||
#endif//UTILS_POLYGONS_SEGMENT_INDEX_H
|
||||
@@ -0,0 +1,42 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include "PolylineStitcher.hpp"
|
||||
#include "ExtrusionLine.hpp"
|
||||
|
||||
namespace Slic3r::Arachne {
|
||||
|
||||
template<> bool PolylineStitcher<VariableWidthLines, ExtrusionLine, ExtrusionJunction>::canReverse(const PathsPointIndex<VariableWidthLines> &ppi)
|
||||
{
|
||||
if ((*ppi.polygons)[ppi.poly_idx].is_odd)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
template<> bool PolylineStitcher<Polygons, Polygon, Point>::canReverse(const PathsPointIndex<Polygons> &)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
template<> bool PolylineStitcher<VariableWidthLines, ExtrusionLine, ExtrusionJunction>::canConnect(const ExtrusionLine &a, const ExtrusionLine &b)
|
||||
{
|
||||
return a.is_odd == b.is_odd;
|
||||
}
|
||||
|
||||
template<> bool PolylineStitcher<Polygons, Polygon, Point>::canConnect(const Polygon &, const Polygon &)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
template<> bool PolylineStitcher<VariableWidthLines, ExtrusionLine, ExtrusionJunction>::isOdd(const ExtrusionLine &line)
|
||||
{
|
||||
return line.is_odd;
|
||||
}
|
||||
|
||||
template<> bool PolylineStitcher<Polygons, Polygon, Point>::isOdd(const Polygon &)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
@@ -0,0 +1,233 @@
|
||||
//Copyright (c) 2022 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef UTILS_POLYLINE_STITCHER_H
|
||||
#define UTILS_POLYLINE_STITCHER_H
|
||||
|
||||
#include "SparsePointGrid.hpp"
|
||||
#include "PolygonsPointIndex.hpp"
|
||||
#include "../../Polygon.hpp"
|
||||
#include <cassert>
|
||||
|
||||
namespace Slic3r::Arachne
|
||||
{
|
||||
|
||||
/*!
|
||||
* Class for stitching polylines into longer polylines or into polygons
|
||||
*/
|
||||
template<typename Paths, typename Path, typename Junction>
|
||||
class PolylineStitcher
|
||||
{
|
||||
public:
|
||||
/*!
|
||||
* Stitch together the separate \p lines into \p result_lines and if they
|
||||
* can be closed into \p result_polygons.
|
||||
*
|
||||
* Only introduce new segments shorter than \p max_stitch_distance, and
|
||||
* larger than \p snap_distance but always try to take the shortest
|
||||
* connection possible.
|
||||
*
|
||||
* Only stitch polylines into closed polygons if they are larger than 3 *
|
||||
* \p max_stitch_distance, in order to prevent small segments to
|
||||
* accidentally get closed into a polygon.
|
||||
*
|
||||
* \warning Tiny polylines (smaller than 3 * max_stitch_distance) will not
|
||||
* be closed into polygons.
|
||||
*
|
||||
* \note Resulting polylines and polygons are added onto the existing
|
||||
* containers, so you can directly output onto a polygons container with
|
||||
* existing polygons in it. However, you shouldn't call this function with
|
||||
* the same parameter in \p lines as \p result_lines, because that would
|
||||
* duplicate (some of) the polylines.
|
||||
* \param lines The lines to stitch together.
|
||||
* \param result_lines[out] The stitched parts that are not closed polygons
|
||||
* will be stored in here.
|
||||
* \param result_polygons[out] The stitched parts that were closed as
|
||||
* polygons will be stored in here.
|
||||
* \param max_stitch_distance The maximum distance that will be bridged to
|
||||
* connect two lines.
|
||||
* \param snap_distance Points closer than this distance are considered to
|
||||
* be the same point.
|
||||
*/
|
||||
static void stitch(const Paths& lines, Paths& result_lines, Paths& result_polygons, coord_t max_stitch_distance = scaled<coord_t>(0.1), coord_t snap_distance = scaled<coord_t>(0.01))
|
||||
{
|
||||
if (lines.empty())
|
||||
return;
|
||||
|
||||
SparsePointGrid<PathsPointIndex<Paths>, PathsPointIndexLocator<Paths>> grid(max_stitch_distance, lines.size() * 2);
|
||||
|
||||
// populate grid
|
||||
for (size_t line_idx = 0; line_idx < lines.size(); line_idx++)
|
||||
{
|
||||
const auto line = lines[line_idx];
|
||||
grid.insert(PathsPointIndex<Paths>(&lines, line_idx, 0));
|
||||
grid.insert(PathsPointIndex<Paths>(&lines, line_idx, line.size() - 1));
|
||||
}
|
||||
|
||||
std::vector<bool> processed(lines.size(), false);
|
||||
|
||||
for (size_t line_idx = 0; line_idx < lines.size(); line_idx++)
|
||||
{
|
||||
if (processed[line_idx])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
processed[line_idx] = true;
|
||||
const auto line = lines[line_idx];
|
||||
bool should_close = isOdd(line);
|
||||
|
||||
Path chain = line;
|
||||
bool closest_is_closing_polygon = false;
|
||||
for (bool go_in_reverse_direction : { false, true }) // first go in the unreversed direction, to try to prevent the chain.reverse() operation.
|
||||
{ // NOTE: Implementation only works for this order; we currently only re-reverse the chain when it's closed.
|
||||
if (go_in_reverse_direction)
|
||||
{ // try extending chain in the other direction
|
||||
chain.reverse();
|
||||
}
|
||||
int64_t chain_length = chain.polylineLength();
|
||||
|
||||
while (true)
|
||||
{
|
||||
Point from = make_point(chain.back());
|
||||
|
||||
PathsPointIndex<Paths> closest;
|
||||
coord_t closest_distance = std::numeric_limits<coord_t>::max();
|
||||
grid.processNearby(from, max_stitch_distance,
|
||||
std::function<bool (const PathsPointIndex<Paths>&)> (
|
||||
[from, &chain, &closest, &closest_is_closing_polygon, &closest_distance, &processed, &chain_length, go_in_reverse_direction, max_stitch_distance, snap_distance, should_close]
|
||||
(const PathsPointIndex<Paths>& nearby)->bool
|
||||
{
|
||||
bool is_closing_segment = false;
|
||||
coord_t dist = (nearby.p().template cast<int64_t>() - from.template cast<int64_t>()).norm();
|
||||
if (dist > max_stitch_distance)
|
||||
{
|
||||
return true; // keep looking
|
||||
}
|
||||
if ((nearby.p().template cast<int64_t>() - make_point(chain.front()).template cast<int64_t>()).squaredNorm() < snap_distance * snap_distance)
|
||||
{
|
||||
if (chain_length + dist < 3 * max_stitch_distance // prevent closing of small poly, cause it might be able to continue making a larger polyline
|
||||
|| chain.size() <= 2) // don't make 2 vert polygons
|
||||
{
|
||||
return true; // look for a better next line
|
||||
}
|
||||
is_closing_segment = true;
|
||||
if (!should_close)
|
||||
{
|
||||
dist += scaled<coord_t>(0.01); // prefer continuing polyline over closing a polygon; avoids closed zigzags from being printed separately
|
||||
// continue to see if closing segment is also the closest
|
||||
// there might be a segment smaller than [max_stitch_distance] which closes the polygon better
|
||||
}
|
||||
else
|
||||
{
|
||||
dist -= scaled<coord_t>(0.01); //Prefer closing the polygon if it's 100% even lines. Used to create closed contours.
|
||||
//Continue to see if closing segment is also the closest.
|
||||
}
|
||||
}
|
||||
else if (processed[nearby.poly_idx])
|
||||
{ // it was already moved to output
|
||||
return true; // keep looking for a connection
|
||||
}
|
||||
bool nearby_would_be_reversed = nearby.point_idx != 0;
|
||||
nearby_would_be_reversed = nearby_would_be_reversed != go_in_reverse_direction; // flip nearby_would_be_reversed when searching in the reverse direction
|
||||
if (!canReverse(nearby) && nearby_would_be_reversed)
|
||||
{ // connecting the segment would reverse the polygon direction
|
||||
return true; // keep looking for a connection
|
||||
}
|
||||
if (!canConnect(chain, (*nearby.polygons)[nearby.poly_idx]))
|
||||
{
|
||||
return true; // keep looking for a connection
|
||||
}
|
||||
if (dist < closest_distance)
|
||||
{
|
||||
closest_distance = dist;
|
||||
closest = nearby;
|
||||
closest_is_closing_polygon = is_closing_segment;
|
||||
}
|
||||
if (dist < snap_distance)
|
||||
{ // we have found a good enough next line
|
||||
return false; // stop looking for alternatives
|
||||
}
|
||||
return true; // keep processing elements
|
||||
})
|
||||
);
|
||||
|
||||
if (!closest.initialized() // we couldn't find any next line
|
||||
|| closest_is_closing_polygon // we closed the polygon
|
||||
)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
coord_t segment_dist = (make_point(chain.back()).template cast<int64_t>() - closest.p().template cast<int64_t>()).norm();
|
||||
assert(segment_dist <= max_stitch_distance + scaled<coord_t>(0.01));
|
||||
const size_t old_size = chain.size();
|
||||
if (closest.point_idx == 0)
|
||||
{
|
||||
auto start_pos = (*closest.polygons)[closest.poly_idx].begin();
|
||||
if (segment_dist < snap_distance)
|
||||
{
|
||||
++start_pos;
|
||||
}
|
||||
chain.insert(chain.end(), start_pos, (*closest.polygons)[closest.poly_idx].end());
|
||||
}
|
||||
else
|
||||
{
|
||||
auto start_pos = (*closest.polygons)[closest.poly_idx].rbegin();
|
||||
if (segment_dist < snap_distance)
|
||||
{
|
||||
++start_pos;
|
||||
}
|
||||
chain.insert(chain.end(), start_pos, (*closest.polygons)[closest.poly_idx].rend());
|
||||
}
|
||||
for(size_t i = old_size; i < chain.size(); ++i) //Update chain length.
|
||||
{
|
||||
chain_length += (make_point(chain[i]).template cast<int64_t>() - make_point(chain[i - 1]).template cast<int64_t>()).norm();
|
||||
}
|
||||
should_close = should_close & !isOdd((*closest.polygons)[closest.poly_idx]); //If we connect an even to an odd line, we should no longer try to close it.
|
||||
assert( ! processed[closest.poly_idx]);
|
||||
processed[closest.poly_idx] = true;
|
||||
}
|
||||
if (closest_is_closing_polygon)
|
||||
{
|
||||
if (go_in_reverse_direction)
|
||||
{ // re-reverse chain to retain original direction
|
||||
// NOTE: not sure if this code could ever be reached, since if a polygon can be closed that should be already possible in the forward direction
|
||||
chain.reverse();
|
||||
}
|
||||
|
||||
break; // don't consider reverse direction
|
||||
}
|
||||
}
|
||||
if (closest_is_closing_polygon)
|
||||
{
|
||||
result_polygons.emplace_back(chain);
|
||||
}
|
||||
else
|
||||
{
|
||||
PathsPointIndex<Paths> ppi_here(&lines, line_idx, 0);
|
||||
if ( ! canReverse(ppi_here))
|
||||
{ // Since closest_is_closing_polygon is false we went through the second iterations of the for-loop, where go_in_reverse_direction is true
|
||||
// the polyline isn't allowed to be reversed, so we re-reverse it.
|
||||
chain.reverse();
|
||||
}
|
||||
result_lines.emplace_back(chain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
* Whether a polyline is allowed to be reversed. (Not true for wall polylines which are not odd)
|
||||
*/
|
||||
static bool canReverse(const PathsPointIndex<Paths> &polyline);
|
||||
|
||||
/*!
|
||||
* Whether two paths are allowed to be connected.
|
||||
* (Not true for an odd and an even wall.)
|
||||
*/
|
||||
static bool canConnect(const Path &a, const Path &b);
|
||||
|
||||
static bool isOdd(const Path &line);
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
#endif // UTILS_POLYLINE_STITCHER_H
|
||||
@@ -0,0 +1,132 @@
|
||||
//Copyright (c) 2016 Scott Lenser
|
||||
//Copyright (c) 2018 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef UTILS_SPARSE_GRID_H
|
||||
#define UTILS_SPARSE_GRID_H
|
||||
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
#include "../../Point.hpp"
|
||||
#include "SquareGrid.hpp"
|
||||
|
||||
namespace Slic3r::Arachne {
|
||||
|
||||
/*! \brief Sparse grid which can locate spatially nearby elements efficiently.
|
||||
*
|
||||
* \note This is an abstract template class which doesn't have any functions to insert elements.
|
||||
* \see SparsePointGrid
|
||||
*
|
||||
* \tparam ElemT The element type to store.
|
||||
*/
|
||||
template<class ElemT> class SparseGrid : public SquareGrid
|
||||
{
|
||||
public:
|
||||
using Elem = ElemT;
|
||||
|
||||
using GridPoint = SquareGrid::GridPoint;
|
||||
using grid_coord_t = SquareGrid::grid_coord_t;
|
||||
using GridMap = std::unordered_multimap<GridPoint, Elem, PointHash>;
|
||||
|
||||
using iterator = typename GridMap::iterator;
|
||||
using const_iterator = typename GridMap::const_iterator;
|
||||
|
||||
/*! \brief Constructs a sparse grid with the specified cell size.
|
||||
*
|
||||
* \param[in] cell_size The size to use for a cell (square) in the grid.
|
||||
* Typical values would be around 0.5-2x of expected query radius.
|
||||
* \param[in] elem_reserve Number of elements to research space for.
|
||||
* \param[in] max_load_factor Maximum average load factor before rehashing.
|
||||
*/
|
||||
SparseGrid(coord_t cell_size, size_t elem_reserve=0U, float max_load_factor=1.0f);
|
||||
|
||||
iterator begin() { return m_grid.begin(); }
|
||||
iterator end() { return m_grid.end(); }
|
||||
const_iterator begin() const { return m_grid.begin(); }
|
||||
const_iterator end() const { return m_grid.end(); }
|
||||
|
||||
/*! \brief Returns all data within radius of query_pt.
|
||||
*
|
||||
* Finds all elements with location within radius of \p query_pt. May
|
||||
* return additional elements that are beyond radius.
|
||||
*
|
||||
* Average running time is a*(1 + 2 * radius / cell_size)**2 +
|
||||
* b*cnt where a and b are proportionality constance and cnt is
|
||||
* the number of returned items. The search will return items in
|
||||
* an area of (2*radius + cell_size)**2 on average. The max range
|
||||
* of an item from the query_point is radius + cell_size.
|
||||
*
|
||||
* \param[in] query_pt The point to search around.
|
||||
* \param[in] radius The search radius.
|
||||
* \return Vector of elements found
|
||||
*/
|
||||
std::vector<Elem> getNearby(const Point &query_pt, coord_t radius) const;
|
||||
|
||||
/*! \brief Process elements from cells that might contain sought after points.
|
||||
*
|
||||
* Processes elements from cell that might have elements within \p
|
||||
* radius of \p query_pt. Processes all elements that are within
|
||||
* radius of query_pt. May process elements that are up to radius +
|
||||
* cell_size from query_pt.
|
||||
*
|
||||
* \param[in] query_pt The point to search around.
|
||||
* \param[in] radius The search radius.
|
||||
* \param[in] process_func Processes each element. process_func(elem) is
|
||||
* called for each element in the cell. Processing stops if function returns false.
|
||||
* \return Whether we need to continue processing after this function
|
||||
*/
|
||||
bool processNearby(const Point &query_pt, coord_t radius, const std::function<bool(const ElemT &)> &process_func) const;
|
||||
|
||||
protected:
|
||||
/*! \brief Process elements from the cell indicated by \p grid_pt.
|
||||
*
|
||||
* \param[in] grid_pt The grid coordinates of the cell.
|
||||
* \param[in] process_func Processes each element. process_func(elem) is
|
||||
* called for each element in the cell. Processing stops if function returns false.
|
||||
* \return Whether we need to continue processing a next cell.
|
||||
*/
|
||||
bool processFromCell(const GridPoint &grid_pt, const std::function<bool(const Elem &)> &process_func) const;
|
||||
|
||||
/*! \brief Map from grid locations (GridPoint) to elements (Elem). */
|
||||
GridMap m_grid;
|
||||
};
|
||||
|
||||
template<class ElemT> SparseGrid<ElemT>::SparseGrid(coord_t cell_size, size_t elem_reserve, float max_load_factor) : SquareGrid(cell_size)
|
||||
{
|
||||
// Must be before the reserve call.
|
||||
m_grid.max_load_factor(max_load_factor);
|
||||
if (elem_reserve != 0U)
|
||||
m_grid.reserve(elem_reserve);
|
||||
}
|
||||
|
||||
template<class ElemT> bool SparseGrid<ElemT>::processFromCell(const GridPoint &grid_pt, const std::function<bool(const Elem &)> &process_func) const
|
||||
{
|
||||
auto grid_range = m_grid.equal_range(grid_pt);
|
||||
for (auto iter = grid_range.first; iter != grid_range.second; ++iter)
|
||||
if (!process_func(iter->second))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class ElemT>
|
||||
bool SparseGrid<ElemT>::processNearby(const Point &query_pt, coord_t radius, const std::function<bool(const Elem &)> &process_func) const
|
||||
{
|
||||
return SquareGrid::processNearby(query_pt, radius, [&process_func, this](const GridPoint &grid_pt) { return processFromCell(grid_pt, process_func); });
|
||||
}
|
||||
|
||||
template<class ElemT> std::vector<typename SparseGrid<ElemT>::Elem> SparseGrid<ElemT>::getNearby(const Point &query_pt, coord_t radius) const
|
||||
{
|
||||
std::vector<Elem> ret;
|
||||
const std::function<bool(const Elem &)> process_func = [&ret](const Elem &elem) {
|
||||
ret.push_back(elem);
|
||||
return true;
|
||||
};
|
||||
processNearby(query_pt, radius, process_func);
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
|
||||
#endif // UTILS_SPARSE_GRID_H
|
||||
@@ -0,0 +1,76 @@
|
||||
//Copyright (c) 2018 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
|
||||
#ifndef UTILS_SPARSE_LINE_GRID_H
|
||||
#define UTILS_SPARSE_LINE_GRID_H
|
||||
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
#include "SparseGrid.hpp"
|
||||
|
||||
namespace Slic3r::Arachne {
|
||||
|
||||
/*! \brief Sparse grid which can locate spatially nearby elements efficiently.
|
||||
*
|
||||
* \tparam ElemT The element type to store.
|
||||
* \tparam Locator The functor to get the start and end locations from ElemT.
|
||||
* must have: std::pair<Point, Point> operator()(const ElemT &elem) const
|
||||
* which returns the location associated with val.
|
||||
*/
|
||||
template<class ElemT, class Locator> class SparseLineGrid : public SparseGrid<ElemT>
|
||||
{
|
||||
public:
|
||||
using Elem = ElemT;
|
||||
|
||||
/*! \brief Constructs a sparse grid with the specified cell size.
|
||||
*
|
||||
* \param[in] cell_size The size to use for a cell (square) in the grid.
|
||||
* Typical values would be around 0.5-2x of expected query radius.
|
||||
* \param[in] elem_reserve Number of elements to research space for.
|
||||
* \param[in] max_load_factor Maximum average load factor before rehashing.
|
||||
*/
|
||||
SparseLineGrid(coord_t cell_size, size_t elem_reserve = 0U, float max_load_factor = 1.0f);
|
||||
|
||||
/*! \brief Inserts elem into the sparse grid.
|
||||
*
|
||||
* \param[in] elem The element to be inserted.
|
||||
*/
|
||||
void insert(const Elem &elem);
|
||||
|
||||
protected:
|
||||
using GridPoint = typename SparseGrid<ElemT>::GridPoint;
|
||||
|
||||
/*! \brief Accessor for getting locations from elements. */
|
||||
Locator m_locator;
|
||||
};
|
||||
|
||||
template<class ElemT, class Locator>
|
||||
SparseLineGrid<ElemT, Locator>::SparseLineGrid(coord_t cell_size, size_t elem_reserve, float max_load_factor)
|
||||
: SparseGrid<ElemT>(cell_size, elem_reserve, max_load_factor) {}
|
||||
|
||||
template<class ElemT, class Locator> void SparseLineGrid<ElemT, Locator>::insert(const Elem &elem)
|
||||
{
|
||||
const std::pair<Point, Point> line = m_locator(elem);
|
||||
using GridMap = std::unordered_multimap<GridPoint, Elem, PointHash>;
|
||||
// below is a workaround for the fact that lambda functions cannot access private or protected members
|
||||
// first we define a lambda which works on any GridMap and then we bind it to the actual protected GridMap of the parent class
|
||||
std::function<bool(GridMap *, const GridPoint)> process_cell_func_ = [&elem](GridMap *m_grid, const GridPoint grid_loc) {
|
||||
m_grid->emplace(grid_loc, elem);
|
||||
return true;
|
||||
};
|
||||
using namespace std::placeholders; // for _1, _2, _3...
|
||||
GridMap *m_grid = &(this->m_grid);
|
||||
std::function<bool(const GridPoint)> process_cell_func(std::bind(process_cell_func_, m_grid, _1));
|
||||
|
||||
SparseGrid<ElemT>::processLineCells(line, process_cell_func);
|
||||
}
|
||||
|
||||
#undef SGI_TEMPLATE
|
||||
#undef SGI_THIS
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
|
||||
#endif // UTILS_SPARSE_LINE_GRID_H
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2016 Scott Lenser
|
||||
// Copyright (c) 2020 Ultimaker B.V.
|
||||
// CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef UTILS_SPARSE_POINT_GRID_H
|
||||
#define UTILS_SPARSE_POINT_GRID_H
|
||||
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
|
||||
#include "SparseGrid.hpp"
|
||||
|
||||
namespace Slic3r::Arachne {
|
||||
|
||||
/*! \brief Sparse grid which can locate spatially nearby elements efficiently.
|
||||
*
|
||||
* \tparam ElemT The element type to store.
|
||||
* \tparam Locator The functor to get the location from ElemT. Locator
|
||||
* must have: Point operator()(const ElemT &elem) const
|
||||
* which returns the location associated with val.
|
||||
*/
|
||||
template<class ElemT, class Locator> class SparsePointGrid : public SparseGrid<ElemT>
|
||||
{
|
||||
public:
|
||||
using Elem = ElemT;
|
||||
|
||||
/*! \brief Constructs a sparse grid with the specified cell size.
|
||||
*
|
||||
* \param[in] cell_size The size to use for a cell (square) in the grid.
|
||||
* Typical values would be around 0.5-2x of expected query radius.
|
||||
* \param[in] elem_reserve Number of elements to research space for.
|
||||
* \param[in] max_load_factor Maximum average load factor before rehashing.
|
||||
*/
|
||||
SparsePointGrid(coord_t cell_size, size_t elem_reserve = 0U, float max_load_factor = 1.0f);
|
||||
|
||||
/*! \brief Inserts elem into the sparse grid.
|
||||
*
|
||||
* \param[in] elem The element to be inserted.
|
||||
*/
|
||||
void insert(const Elem &elem);
|
||||
|
||||
protected:
|
||||
using GridPoint = typename SparseGrid<ElemT>::GridPoint;
|
||||
|
||||
/*! \brief Accessor for getting locations from elements. */
|
||||
Locator m_locator;
|
||||
};
|
||||
|
||||
template<class ElemT, class Locator>
|
||||
SparsePointGrid<ElemT, Locator>::SparsePointGrid(coord_t cell_size, size_t elem_reserve, float max_load_factor) : SparseGrid<ElemT>(cell_size, elem_reserve, max_load_factor) {}
|
||||
|
||||
template<class ElemT, class Locator>
|
||||
void SparsePointGrid<ElemT, Locator>::insert(const Elem &elem)
|
||||
{
|
||||
Point loc = m_locator(elem);
|
||||
GridPoint grid_loc = SparseGrid<ElemT>::toGridPoint(loc.template cast<int64_t>());
|
||||
|
||||
SparseGrid<ElemT>::m_grid.emplace(grid_loc, elem);
|
||||
}
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
|
||||
#endif // UTILS_SPARSE_POINT_GRID_H
|
||||
@@ -0,0 +1,146 @@
|
||||
//Copyright (c) 2021 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#include "SquareGrid.hpp"
|
||||
|
||||
using namespace Slic3r::Arachne;
|
||||
|
||||
|
||||
SquareGrid::SquareGrid(coord_t cell_size) : cell_size(cell_size)
|
||||
{
|
||||
assert(cell_size > 0U);
|
||||
}
|
||||
|
||||
|
||||
SquareGrid::GridPoint SquareGrid::toGridPoint(const Vec2i64 &point) const
|
||||
{
|
||||
return Point(toGridCoord(point.x()), toGridCoord(point.y()));
|
||||
}
|
||||
|
||||
|
||||
SquareGrid::grid_coord_t SquareGrid::toGridCoord(const int64_t &coord) const
|
||||
{
|
||||
// This mapping via truncation results in the cells with
|
||||
// GridPoint.x==0 being twice as large and similarly for
|
||||
// GridPoint.y==0. This doesn't cause any incorrect behavior,
|
||||
// just changes the running time slightly. The change in running
|
||||
// time from this is probably not worth doing a proper floor
|
||||
// operation.
|
||||
return coord / cell_size;
|
||||
}
|
||||
|
||||
coord_t SquareGrid::toLowerCoord(const grid_coord_t& grid_coord) const
|
||||
{
|
||||
// This mapping via truncation results in the cells with
|
||||
// GridPoint.x==0 being twice as large and similarly for
|
||||
// GridPoint.y==0. This doesn't cause any incorrect behavior,
|
||||
// just changes the running time slightly. The change in running
|
||||
// time from this is probably not worth doing a proper floor
|
||||
// operation.
|
||||
return grid_coord * cell_size;
|
||||
}
|
||||
|
||||
|
||||
bool SquareGrid::processLineCells(const std::pair<Point, Point> line, const std::function<bool (GridPoint)>& process_cell_func)
|
||||
{
|
||||
return static_cast<const SquareGrid*>(this)->processLineCells(line, process_cell_func);
|
||||
}
|
||||
|
||||
|
||||
bool SquareGrid::processLineCells(const std::pair<Point, Point> line, const std::function<bool (GridPoint)>& process_cell_func) const
|
||||
{
|
||||
Point start = line.first;
|
||||
Point end = line.second;
|
||||
if (end.x() < start.x())
|
||||
{ // make sure X increases between start and end
|
||||
std::swap(start, end);
|
||||
}
|
||||
|
||||
const GridPoint start_cell = toGridPoint(start.cast<int64_t>());
|
||||
const GridPoint end_cell = toGridPoint(end.cast<int64_t>());
|
||||
const int64_t y_diff = int64_t(end.y() - start.y());
|
||||
const grid_coord_t y_dir = nonzeroSign(y_diff);
|
||||
|
||||
/* This line drawing algorithm iterates over the range of Y coordinates, and
|
||||
for each Y coordinate computes the range of X coordinates crossed in one
|
||||
unit of Y. These ranges are rounded to be inclusive, so effectively this
|
||||
creates a "fat" line, marking more cells than a strict one-cell-wide path.*/
|
||||
grid_coord_t x_cell_start = start_cell.x();
|
||||
for (grid_coord_t cell_y = start_cell.y(); cell_y * y_dir <= end_cell.y() * y_dir; cell_y += y_dir)
|
||||
{ // for all Y from start to end
|
||||
// nearest y coordinate of the cells in the next row
|
||||
const coord_t nearest_next_y = toLowerCoord(cell_y + ((nonzeroSign(cell_y) == y_dir || cell_y == 0) ? y_dir : coord_t(0)));
|
||||
grid_coord_t x_cell_end; // the X coord of the last cell to include from this row
|
||||
if (y_diff == 0)
|
||||
{
|
||||
x_cell_end = end_cell.x();
|
||||
}
|
||||
else
|
||||
{
|
||||
const int64_t area = int64_t(end.x() - start.x()) * int64_t(nearest_next_y - start.y());
|
||||
// corresponding_x: the x coordinate corresponding to nearest_next_y
|
||||
int64_t corresponding_x = int64_t(start.x()) + area / y_diff;
|
||||
x_cell_end = toGridCoord(corresponding_x + ((corresponding_x < 0) && ((area % y_diff) != 0)));
|
||||
if (x_cell_end < start_cell.x())
|
||||
{ // process at least one cell!
|
||||
x_cell_end = x_cell_start;
|
||||
}
|
||||
}
|
||||
|
||||
for (grid_coord_t cell_x = x_cell_start; cell_x <= x_cell_end; ++cell_x)
|
||||
{
|
||||
GridPoint grid_loc(cell_x, cell_y);
|
||||
if (! process_cell_func(grid_loc))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (grid_loc == end_cell)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// TODO: this causes at least a one cell overlap for each row, which
|
||||
// includes extra cells when crossing precisely on the corners
|
||||
// where positive slope where x > 0 and negative slope where x < 0
|
||||
x_cell_start = x_cell_end;
|
||||
}
|
||||
assert(false && "We should have returned already before here!");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SquareGrid::processNearby
|
||||
(
|
||||
const Point &query_pt,
|
||||
coord_t radius,
|
||||
const std::function<bool (const GridPoint&)>& process_func
|
||||
) const
|
||||
{
|
||||
const Point min_loc(query_pt.x() - radius, query_pt.y() - radius);
|
||||
const Point max_loc(query_pt.x() + radius, query_pt.y() + radius);
|
||||
|
||||
GridPoint min_grid = toGridPoint(min_loc.cast<int64_t>());
|
||||
GridPoint max_grid = toGridPoint(max_loc.cast<int64_t>());
|
||||
|
||||
for (coord_t grid_y = min_grid.y(); grid_y <= max_grid.y(); ++grid_y)
|
||||
{
|
||||
for (coord_t grid_x = min_grid.x(); grid_x <= max_grid.x(); ++grid_x)
|
||||
{
|
||||
GridPoint grid_pt(grid_x,grid_y);
|
||||
if (!process_func(grid_pt))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
SquareGrid::grid_coord_t SquareGrid::nonzeroSign(const grid_coord_t z) const
|
||||
{
|
||||
return (z >= 0) - (z < 0);
|
||||
}
|
||||
|
||||
coord_t SquareGrid::getCellSize() const
|
||||
{
|
||||
return cell_size;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
//Copyright (c) 2021 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef UTILS_SQUARE_GRID_H
|
||||
#define UTILS_SQUARE_GRID_H
|
||||
|
||||
#include "../../Point.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
namespace Slic3r::Arachne {
|
||||
|
||||
/*!
|
||||
* Helper class to calculate coordinates on a square grid, and providing some
|
||||
* utility functions to process grids.
|
||||
*
|
||||
* Doesn't contain any data, except cell size. The purpose is only to
|
||||
* automatically generate coordinates on a grid, and automatically feed them to
|
||||
* functions.
|
||||
* The grid is theoretically infinite (bar integer limits).
|
||||
*/
|
||||
class SquareGrid
|
||||
{
|
||||
public:
|
||||
/*! \brief Constructs a grid with the specified cell size.
|
||||
* \param[in] cell_size The size to use for a cell (square) in the grid.
|
||||
*/
|
||||
SquareGrid(const coord_t cell_size);
|
||||
|
||||
/*!
|
||||
* Get the cell size this grid was created for.
|
||||
*/
|
||||
coord_t getCellSize() const;
|
||||
|
||||
using GridPoint = Point;
|
||||
using grid_coord_t = coord_t;
|
||||
|
||||
/*! \brief Process cells along a line indicated by \p line.
|
||||
*
|
||||
* \param line The line along which to process cells.
|
||||
* \param process_func Processes each cell. ``process_func(elem)`` is called
|
||||
* for each cell. Processing stops if function returns false.
|
||||
* \return Whether we need to continue processing after this function.
|
||||
*/
|
||||
bool processLineCells(const std::pair<Point, Point> line, const std::function<bool (GridPoint)>& process_cell_func);
|
||||
|
||||
/*! \brief Process cells along a line indicated by \p line.
|
||||
*
|
||||
* \param line The line along which to process cells
|
||||
* \param process_func Processes each cell. ``process_func(elem)`` is called
|
||||
* for each cell. Processing stops if function returns false.
|
||||
* \return Whether we need to continue processing after this function.
|
||||
*/
|
||||
bool processLineCells(const std::pair<Point, Point> line, const std::function<bool (GridPoint)>& process_cell_func) const;
|
||||
|
||||
/*! \brief Process cells that might contain sought after points.
|
||||
*
|
||||
* Processes cells that might be within a square with twice \p radius as
|
||||
* width, centered around \p query_pt.
|
||||
* May process elements that are up to radius + cell_size from query_pt.
|
||||
* \param query_pt The point to search around.
|
||||
* \param radius The search radius.
|
||||
* \param process_func Processes each cell. ``process_func(loc)`` is called
|
||||
* for each cell coord within range. Processing stops if function returns
|
||||
* ``false``.
|
||||
* \return Whether we need to continue processing after this function.
|
||||
*/
|
||||
bool processNearby(const Point &query_pt, coord_t radius, const std::function<bool(const GridPoint &)> &process_func) const;
|
||||
|
||||
/*! \brief Compute the grid coordinates of a point.
|
||||
* \param point The actual location.
|
||||
* \return The grid coordinates that correspond to \p point.
|
||||
*/
|
||||
GridPoint toGridPoint(const Vec2i64 &point) const;
|
||||
|
||||
/*! \brief Compute the grid coordinate of a real space coordinate.
|
||||
* \param coord The actual location.
|
||||
* \return The grid coordinate that corresponds to \p coord.
|
||||
*/
|
||||
grid_coord_t toGridCoord(const int64_t &coord) const;
|
||||
|
||||
/*! \brief Compute the lowest coord in a grid cell.
|
||||
* The lowest point is the point in the grid cell closest to the origin.
|
||||
*
|
||||
* \param grid_coord The grid coordinate.
|
||||
* \return The print space coordinate that corresponds to \p grid_coord.
|
||||
*/
|
||||
coord_t toLowerCoord(const grid_coord_t &grid_coord) const;
|
||||
|
||||
protected:
|
||||
/*! \brief The cell (square) size. */
|
||||
coord_t cell_size;
|
||||
|
||||
/*!
|
||||
* Compute the sign of a number.
|
||||
*
|
||||
* The number 0 will result in a positive sign (1).
|
||||
* \param z The number to find the sign of.
|
||||
* \return 1 if the number is positive or 0, or -1 if the number is
|
||||
* negative.
|
||||
*/
|
||||
grid_coord_t nonzeroSign(grid_coord_t z) const;
|
||||
};
|
||||
|
||||
} // namespace Slic3r::Arachne
|
||||
|
||||
#endif //UTILS_SQUARE_GRID_H
|
||||
@@ -0,0 +1,65 @@
|
||||
//Copyright (c) 2020 Ultimaker B.V.
|
||||
//CuraEngine is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
#ifndef UTILS_LINEAR_ALG_2D_H
|
||||
#define UTILS_LINEAR_ALG_2D_H
|
||||
|
||||
#include "../../Point.hpp"
|
||||
|
||||
namespace Slic3r::Arachne::LinearAlg2D
|
||||
{
|
||||
|
||||
/*!
|
||||
* Returns the determinant of the 2D matrix defined by the the vectors ab and ap as rows.
|
||||
*
|
||||
* The returned value is zero for \p p lying (approximately) on the line going through \p a and \p b
|
||||
* The value is positive for values lying to the left and negative for values lying to the right when looking from \p a to \p b.
|
||||
*
|
||||
* \param p the point to check
|
||||
* \param a the from point of the line
|
||||
* \param b the to point of the line
|
||||
* \return a positive value when \p p lies to the left of the line from \p a to \p b
|
||||
*/
|
||||
static inline int64_t pointIsLeftOfLine(const Point &p, const Point &a, const Point &b)
|
||||
{
|
||||
return int64_t(b.x() - a.x()) * int64_t(p.y() - a.y()) - int64_t(b.y() - a.y()) * int64_t(p.x() - a.x());
|
||||
}
|
||||
|
||||
/*!
|
||||
* Compute the angle between two consecutive line segments.
|
||||
*
|
||||
* The angle is computed from the left side of b when looking from a.
|
||||
*
|
||||
* c
|
||||
* \ .
|
||||
* \ b
|
||||
* angle|
|
||||
* |
|
||||
* a
|
||||
*
|
||||
* \param a start of first line segment
|
||||
* \param b end of first segment and start of second line segment
|
||||
* \param c end of second line segment
|
||||
* \return the angle in radians between 0 and 2 * pi of the corner in \p b
|
||||
*/
|
||||
static inline float getAngleLeft(const Point &a, const Point &b, const Point &c)
|
||||
{
|
||||
const Vec2i64 ba = (a - b).cast<int64_t>();
|
||||
const Vec2i64 bc = (c - b).cast<int64_t>();
|
||||
const int64_t dott = ba.dot(bc); // dot product
|
||||
const int64_t det = cross2(ba, bc); // determinant
|
||||
if (det == 0) {
|
||||
if ((ba.x() != 0 && (ba.x() > 0) == (bc.x() > 0)) || (ba.x() == 0 && (ba.y() > 0) == (bc.y() > 0)))
|
||||
return 0; // pointy bit
|
||||
else
|
||||
return float(M_PI); // straight bit
|
||||
}
|
||||
const float angle = -atan2(double(det), double(dott)); // from -pi to pi
|
||||
if (angle >= 0)
|
||||
return angle;
|
||||
else
|
||||
return M_PI * 2 + angle;
|
||||
}
|
||||
|
||||
}//namespace Slic3r::Arachne
|
||||
#endif//UTILS_LINEAR_ALG_2D_H
|
||||
@@ -0,0 +1,780 @@
|
||||
///|/ Copyright (c) Prusa Research 2018 - 2023 Tomáš Mészáros @tamasmeszaros, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "Arrange.hpp"
|
||||
|
||||
#include "BoundingBox.hpp"
|
||||
|
||||
#include <libnest2d/backends/libslic3r/geometries.hpp>
|
||||
#include <libnest2d/optimizers/nlopt/subplex.hpp>
|
||||
#include <libnest2d/placers/nfpplacer.hpp>
|
||||
#include <libnest2d/selections/firstfit.hpp>
|
||||
#include <libnest2d/utils/rotcalipers.hpp>
|
||||
|
||||
#include <numeric>
|
||||
#include <ClipperUtils.hpp>
|
||||
|
||||
#include <boost/geometry/index/rtree.hpp>
|
||||
#include <boost/container/small_vector.hpp>
|
||||
|
||||
#if defined(_MSC_VER) && defined(__clang__)
|
||||
#define BOOST_NO_CXX17_HDR_STRING_VIEW
|
||||
#endif
|
||||
|
||||
#include <boost/multiprecision/integer.hpp>
|
||||
#include <boost/rational.hpp>
|
||||
|
||||
namespace libnest2d {
|
||||
#if !defined(_MSC_VER) && defined(__SIZEOF_INT128__) && !defined(__APPLE__)
|
||||
using LargeInt = __int128;
|
||||
#else
|
||||
using LargeInt = boost::multiprecision::int128_t;
|
||||
template<> struct _NumTag<LargeInt>
|
||||
{
|
||||
using Type = ScalarTag;
|
||||
};
|
||||
#endif
|
||||
|
||||
template<class T> struct _NumTag<boost::rational<T>>
|
||||
{
|
||||
using Type = RationalTag;
|
||||
};
|
||||
|
||||
namespace nfp {
|
||||
|
||||
template<class S> struct NfpImpl<S, NfpLevel::CONVEX_ONLY>
|
||||
{
|
||||
NfpResult<S> operator()(const S &sh, const S &other)
|
||||
{
|
||||
return nfpConvexOnly<S, boost::rational<LargeInt>>(sh, other);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace nfp
|
||||
} // namespace libnest2d
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
template<class Tout = double, class = FloatingOnly<Tout>, int...EigenArgs>
|
||||
inline constexpr Eigen::Matrix<Tout, 2, EigenArgs...> unscaled(
|
||||
const Slic3r::ClipperLib::IntPoint &v) noexcept
|
||||
{
|
||||
return Eigen::Matrix<Tout, 2, EigenArgs...>{unscaled<Tout>(v.x()),
|
||||
unscaled<Tout>(v.y())};
|
||||
}
|
||||
|
||||
namespace arrangement {
|
||||
|
||||
using namespace libnest2d;
|
||||
|
||||
// Get the libnest2d types for clipper backend
|
||||
using Item = _Item<ExPolygon>;
|
||||
using Box = _Box<Point>;
|
||||
using Circle = _Circle<Point>;
|
||||
using Segment = _Segment<Point>;
|
||||
using MultiPolygon = ExPolygons;
|
||||
|
||||
// Summon the spatial indexing facilities from boost
|
||||
namespace bgi = boost::geometry::index;
|
||||
using SpatElement = std::pair<Box, unsigned>;
|
||||
using SpatIndex = bgi::rtree< SpatElement, bgi::rstar<16, 4> >;
|
||||
using ItemGroup = std::vector<std::reference_wrapper<Item>>;
|
||||
|
||||
// A coefficient used in separating bigger items and smaller items.
|
||||
const double BIG_ITEM_TRESHOLD = 0.02;
|
||||
|
||||
// Fill in the placer algorithm configuration with values carefully chosen for
|
||||
// Slic3r.
|
||||
template<class PConf>
|
||||
void fill_config(PConf& pcfg, const ArrangeParams ¶ms) {
|
||||
|
||||
// Align the arranged pile into the center of the bin
|
||||
switch (params.alignment) {
|
||||
case Pivots::Center: pcfg.alignment = PConf::Alignment::CENTER; break;
|
||||
case Pivots::BottomLeft: pcfg.alignment = PConf::Alignment::BOTTOM_LEFT; break;
|
||||
case Pivots::BottomRight: pcfg.alignment = PConf::Alignment::BOTTOM_RIGHT; break;
|
||||
case Pivots::TopLeft: pcfg.alignment = PConf::Alignment::TOP_LEFT; break;
|
||||
case Pivots::TopRight: pcfg.alignment = PConf::Alignment::TOP_RIGHT; break;
|
||||
}
|
||||
|
||||
// Start placing the items from the center of the print bed
|
||||
pcfg.starting_point = PConf::Alignment::CENTER;
|
||||
|
||||
// TODO cannot use rotations until multiple objects of same geometry can
|
||||
// handle different rotations.
|
||||
if (params.allow_rotations)
|
||||
pcfg.rotations = {0., PI / 2., PI, 3. * PI / 2. };
|
||||
else
|
||||
pcfg.rotations = {0.};
|
||||
|
||||
// The accuracy of optimization.
|
||||
// Goes from 0.0 to 1.0 and scales performance as well
|
||||
pcfg.accuracy = params.accuracy;
|
||||
|
||||
// Allow parallel execution.
|
||||
pcfg.parallel = params.parallel;
|
||||
}
|
||||
|
||||
// Apply penalty to object function result. This is used only when alignment
|
||||
// after arrange is explicitly disabled (PConfig::Alignment::DONT_ALIGN)
|
||||
// Also, this will only work well for Box shaped beds.
|
||||
static double fixed_overfit(const std::tuple<double, Box>& result, const Box &binbb)
|
||||
{
|
||||
double score = std::get<0>(result);
|
||||
Box pilebb = std::get<1>(result);
|
||||
Box fullbb = sl::boundingBox(pilebb, binbb);
|
||||
auto diff = double(fullbb.area()) - binbb.area();
|
||||
if(diff > 0) score += diff;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
// A class encapsulating the libnest2d Nester class and extending it with other
|
||||
// management and spatial index structures for acceleration.
|
||||
template<class TBin>
|
||||
class AutoArranger {
|
||||
public:
|
||||
// Useful type shortcuts...
|
||||
using Placer = typename placers::_NofitPolyPlacer<ExPolygon, TBin>;
|
||||
using Selector = selections::_FirstFitSelection<ExPolygon>;
|
||||
using Packer = _Nester<Placer, Selector>;
|
||||
using PConfig = typename Packer::PlacementConfig;
|
||||
using Distance = TCoord<PointImpl>;
|
||||
|
||||
protected:
|
||||
Packer m_pck;
|
||||
PConfig m_pconf; // Placement configuration
|
||||
TBin m_bin;
|
||||
double m_bin_area;
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable: 4244)
|
||||
#pragma warning(disable: 4267)
|
||||
#endif
|
||||
SpatIndex m_rtree; // spatial index for the normal (bigger) objects
|
||||
SpatIndex m_smallsrtree; // spatial index for only the smaller items
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
|
||||
double m_norm; // A coefficient to scale distances
|
||||
MultiPolygon m_merged_pile; // The already merged pile (vector of items)
|
||||
Box m_pilebb; // The bounding box of the merged pile.
|
||||
ItemGroup m_remaining; // Remaining items
|
||||
ItemGroup m_items; // allready packed items
|
||||
size_t m_item_count = 0; // Number of all items to be packed
|
||||
|
||||
template<class T> ArithmeticOnly<T, double> norm(T val)
|
||||
{
|
||||
return double(val) / m_norm;
|
||||
}
|
||||
|
||||
// This is "the" object function which is evaluated many times for each
|
||||
// vertex (decimated with the accuracy parameter) of each object.
|
||||
// Therefore it is upmost crucial for this function to be as efficient
|
||||
// as it possibly can be but at the same time, it has to provide
|
||||
// reasonable results.
|
||||
std::tuple<double /*score*/, Box /*farthest point from bin center*/>
|
||||
objfunc(const Item &item, const Point &bincenter)
|
||||
{
|
||||
const double bin_area = m_bin_area;
|
||||
const SpatIndex& spatindex = m_rtree;
|
||||
const SpatIndex& smalls_spatindex = m_smallsrtree;
|
||||
|
||||
// We will treat big items (compared to the print bed) differently
|
||||
auto isBig = [bin_area](double a) {
|
||||
return a/bin_area > BIG_ITEM_TRESHOLD ;
|
||||
};
|
||||
|
||||
// Candidate item bounding box
|
||||
auto ibb = item.boundingBox();
|
||||
|
||||
// Calculate the full bounding box of the pile with the candidate item
|
||||
auto fullbb = sl::boundingBox(m_pilebb, ibb);
|
||||
|
||||
// The bounding box of the big items (they will accumulate in the center
|
||||
// of the pile
|
||||
Box bigbb;
|
||||
if(spatindex.empty()) bigbb = fullbb;
|
||||
else {
|
||||
auto boostbb = spatindex.bounds();
|
||||
boost::geometry::convert(boostbb, bigbb);
|
||||
}
|
||||
|
||||
// Will hold the resulting score
|
||||
double score = 0;
|
||||
|
||||
// Density is the pack density: how big is the arranged pile
|
||||
double density = 0;
|
||||
|
||||
// Distinction of cases for the arrangement scene
|
||||
enum e_cases {
|
||||
// This branch is for big items in a mixed (big and small) scene
|
||||
// OR for all items in a small-only scene.
|
||||
BIG_ITEM,
|
||||
|
||||
// This branch is for the last big item in a mixed scene
|
||||
LAST_BIG_ITEM,
|
||||
|
||||
// For small items in a mixed scene.
|
||||
SMALL_ITEM
|
||||
} compute_case;
|
||||
|
||||
bool bigitems = isBig(item.area()) || spatindex.empty();
|
||||
if(bigitems && !m_remaining.empty()) compute_case = BIG_ITEM;
|
||||
else if (bigitems && m_remaining.empty()) compute_case = LAST_BIG_ITEM;
|
||||
else compute_case = SMALL_ITEM;
|
||||
|
||||
switch (compute_case) {
|
||||
case BIG_ITEM: {
|
||||
const Point& minc = ibb.minCorner(); // bottom left corner
|
||||
const Point& maxc = ibb.maxCorner(); // top right corner
|
||||
|
||||
// top left and bottom right corners
|
||||
Point top_left{getX(minc), getY(maxc)};
|
||||
Point bottom_right{getX(maxc), getY(minc)};
|
||||
|
||||
// Now the distance of the gravity center will be calculated to the
|
||||
// five anchor points and the smallest will be chosen.
|
||||
std::array<double, 5> dists;
|
||||
auto cc = fullbb.center(); // The gravity center
|
||||
dists[0] = pl::distance(minc, cc);
|
||||
dists[1] = pl::distance(maxc, cc);
|
||||
dists[2] = pl::distance(ibb.center(), cc);
|
||||
dists[3] = pl::distance(top_left, cc);
|
||||
dists[4] = pl::distance(bottom_right, cc);
|
||||
|
||||
// The smalles distance from the arranged pile center:
|
||||
double dist = norm(*(std::min_element(dists.begin(), dists.end())));
|
||||
double bindist = norm(pl::distance(ibb.center(), bincenter));
|
||||
dist = 0.8 * dist + 0.2 * bindist;
|
||||
|
||||
// Prepare a variable for the alignment score.
|
||||
// This will indicate: how well is the candidate item
|
||||
// aligned with its neighbors. We will check the alignment
|
||||
// with all neighbors and return the score for the best
|
||||
// alignment. So it is enough for the candidate to be
|
||||
// aligned with only one item.
|
||||
auto alignment_score = 1.0;
|
||||
|
||||
auto query = bgi::intersects(ibb);
|
||||
auto& index = isBig(item.area()) ? spatindex : smalls_spatindex;
|
||||
|
||||
// Query the spatial index for the neighbors
|
||||
boost::container::small_vector<SpatElement, 100> result;
|
||||
result.reserve(index.size());
|
||||
|
||||
index.query(query, std::back_inserter(result));
|
||||
|
||||
// now get the score for the best alignment
|
||||
for(auto& e : result) {
|
||||
auto idx = e.second;
|
||||
Item& p = m_items[idx];
|
||||
auto parea = p.area();
|
||||
if(std::abs(1.0 - parea/item.area()) < 1e-6) {
|
||||
auto bb = sl::boundingBox(p.boundingBox(), ibb);
|
||||
auto bbarea = bb.area();
|
||||
auto ascore = 1.0 - (item.area() + parea)/bbarea;
|
||||
|
||||
if(ascore < alignment_score) alignment_score = ascore;
|
||||
}
|
||||
}
|
||||
|
||||
density = std::sqrt(norm(fullbb.width()) * norm(fullbb.height()));
|
||||
double R = double(m_remaining.size()) / m_item_count;
|
||||
|
||||
// The final mix of the score is the balance between the
|
||||
// distance from the full pile center, the pack density and
|
||||
// the alignment with the neighbors
|
||||
if (result.empty())
|
||||
score = 0.50 * dist + 0.50 * density;
|
||||
else
|
||||
// Let the density matter more when fewer objects remain
|
||||
score = 0.50 * dist + (1.0 - R) * 0.20 * density +
|
||||
0.30 * alignment_score;
|
||||
|
||||
break;
|
||||
}
|
||||
case LAST_BIG_ITEM: {
|
||||
score = norm(pl::distance(ibb.center(), m_pilebb.center()));
|
||||
break;
|
||||
}
|
||||
case SMALL_ITEM: {
|
||||
// Here there are the small items that should be placed around the
|
||||
// already processed bigger items.
|
||||
// No need to play around with the anchor points, the center will be
|
||||
// just fine for small items
|
||||
score = norm(pl::distance(ibb.center(), bigbb.center()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return std::make_tuple(score, fullbb);
|
||||
}
|
||||
|
||||
std::function<double(const Item&)> get_objfn();
|
||||
|
||||
public:
|
||||
AutoArranger(const TBin & bin,
|
||||
const ArrangeParams ¶ms,
|
||||
std::function<void(unsigned)> progressind,
|
||||
std::function<bool(void)> stopcond)
|
||||
: m_pck(bin, params.min_obj_distance)
|
||||
, m_bin(bin)
|
||||
, m_bin_area(sl::area(bin))
|
||||
, m_norm(std::sqrt(m_bin_area))
|
||||
{
|
||||
fill_config(m_pconf, params);
|
||||
|
||||
// Set up a callback that is called just before arranging starts
|
||||
// This functionality is provided by the Nester class (m_pack).
|
||||
m_pconf.before_packing =
|
||||
[this](const MultiPolygon& merged_pile, // merged pile
|
||||
const ItemGroup& items, // packed items
|
||||
const ItemGroup& remaining) // future items to be packed
|
||||
{
|
||||
m_items = items;
|
||||
m_merged_pile = merged_pile;
|
||||
m_remaining = remaining;
|
||||
|
||||
m_pilebb = sl::boundingBox(merged_pile);
|
||||
|
||||
m_rtree.clear();
|
||||
m_smallsrtree.clear();
|
||||
|
||||
// We will treat big items (compared to the print bed) differently
|
||||
auto isBig = [this](double a) {
|
||||
return a / m_bin_area > BIG_ITEM_TRESHOLD ;
|
||||
};
|
||||
|
||||
for(unsigned idx = 0; idx < items.size(); ++idx) {
|
||||
Item& itm = items[idx];
|
||||
if(isBig(itm.area())) m_rtree.insert({itm.boundingBox(), idx});
|
||||
m_smallsrtree.insert({itm.boundingBox(), idx});
|
||||
}
|
||||
};
|
||||
|
||||
m_pconf.object_function = get_objfn();
|
||||
|
||||
m_pconf.on_preload = [this](const ItemGroup &items, PConfig &cfg) {
|
||||
if (items.empty()) return;
|
||||
|
||||
cfg.alignment = PConfig::Alignment::DONT_ALIGN;
|
||||
auto bb = sl::boundingBox(m_bin);
|
||||
auto bbcenter = bb.center();
|
||||
cfg.object_function = [this, bb, bbcenter](const Item &item) {
|
||||
return fixed_overfit(objfunc(item, bbcenter), bb);
|
||||
};
|
||||
};
|
||||
|
||||
auto on_packed = params.on_packed;
|
||||
|
||||
if (progressind || on_packed)
|
||||
m_pck.progressIndicator([this, progressind, on_packed](unsigned rem) {
|
||||
|
||||
if (progressind)
|
||||
progressind(rem);
|
||||
|
||||
if (on_packed) {
|
||||
int last_bed = m_pck.lastPackedBinId();
|
||||
if (last_bed >= 0) {
|
||||
Item &last_packed = m_pck.lastResult()[last_bed].back();
|
||||
ArrangePolygon ap;
|
||||
ap.bed_idx = last_packed.binId();
|
||||
ap.priority = last_packed.priority();
|
||||
on_packed(ap);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (stopcond) m_pck.stopCondition(stopcond);
|
||||
|
||||
m_pck.configure(m_pconf);
|
||||
}
|
||||
|
||||
template<class It> inline void operator()(It from, It to) {
|
||||
m_rtree.clear();
|
||||
m_item_count += size_t(to - from);
|
||||
m_pck.execute(from, to);
|
||||
m_item_count = 0;
|
||||
}
|
||||
|
||||
PConfig& config() { return m_pconf; }
|
||||
const PConfig& config() const { return m_pconf; }
|
||||
|
||||
inline void preload(std::vector<Item>& fixeditems) {
|
||||
for(unsigned idx = 0; idx < fixeditems.size(); ++idx) {
|
||||
Item& itm = fixeditems[idx];
|
||||
itm.markAsFixedInBin(itm.binId());
|
||||
}
|
||||
|
||||
m_item_count += fixeditems.size();
|
||||
}
|
||||
};
|
||||
|
||||
template<> std::function<double(const Item&)> AutoArranger<Box>::get_objfn()
|
||||
{
|
||||
auto bincenter = m_bin.center();
|
||||
|
||||
return [this, bincenter](const Item &itm) {
|
||||
auto result = objfunc(itm, bincenter);
|
||||
|
||||
double score = std::get<0>(result);
|
||||
auto& fullbb = std::get<1>(result);
|
||||
|
||||
double miss = Placer::overfit(fullbb, m_bin);
|
||||
miss = miss > 0? miss : 0;
|
||||
score += miss * miss;
|
||||
|
||||
return score;
|
||||
};
|
||||
}
|
||||
|
||||
template<> std::function<double(const Item&)> AutoArranger<Circle>::get_objfn()
|
||||
{
|
||||
auto bincenter = m_bin.center();
|
||||
return [this, bincenter](const Item &item) {
|
||||
|
||||
auto result = objfunc(item, bincenter);
|
||||
|
||||
double score = std::get<0>(result);
|
||||
|
||||
return score;
|
||||
};
|
||||
}
|
||||
|
||||
// Specialization for a generalized polygon.
|
||||
// Warning: this is unfinished business. It may or may not work.
|
||||
template<>
|
||||
std::function<double(const Item &)> AutoArranger<ExPolygon>::get_objfn()
|
||||
{
|
||||
auto bincenter = sl::boundingBox(m_bin).center();
|
||||
return [this, bincenter](const Item &item) {
|
||||
return std::get<0>(objfunc(item, bincenter));
|
||||
};
|
||||
}
|
||||
|
||||
template<class Bin> void remove_large_items(std::vector<Item> &items, Bin &&bin)
|
||||
{
|
||||
auto it = items.begin();
|
||||
while (it != items.end())
|
||||
sl::isInside(it->transformedShape(), bin) ?
|
||||
++it : it = items.erase(it);
|
||||
}
|
||||
|
||||
template<class S> Radians min_area_boundingbox_rotation(const S &sh)
|
||||
{
|
||||
return minAreaBoundingBox<S, TCompute<S>, boost::rational<LargeInt>>(sh)
|
||||
.angleToX();
|
||||
}
|
||||
|
||||
template<class S>
|
||||
Radians fit_into_box_rotation(const S &sh, const _Box<TPoint<S>> &box)
|
||||
{
|
||||
return fitIntoBoxRotation<S, TCompute<S>, boost::rational<LargeInt>>(sh, box);
|
||||
}
|
||||
|
||||
template<class BinT> // Arrange for arbitrary bin type
|
||||
void _arrange(
|
||||
std::vector<Item> & shapes,
|
||||
std::vector<Item> & excludes,
|
||||
const BinT & bin,
|
||||
const ArrangeParams ¶ms,
|
||||
std::function<void(unsigned)> progressfn,
|
||||
std::function<bool()> stopfn)
|
||||
{
|
||||
// Integer ceiling the min distance from the bed perimeters
|
||||
coord_t md = params.min_obj_distance;
|
||||
md = md / 2 - params.min_bed_distance;
|
||||
|
||||
auto corrected_bin = bin;
|
||||
sl::offset(corrected_bin, md);
|
||||
ArrangeParams mod_params = params;
|
||||
mod_params.min_obj_distance = 0;
|
||||
|
||||
AutoArranger<BinT> arranger{corrected_bin, mod_params, progressfn, stopfn};
|
||||
|
||||
auto infl = coord_t(std::ceil(params.min_obj_distance / 2.0));
|
||||
for (Item& itm : shapes) itm.inflate(infl);
|
||||
for (Item& itm : excludes) itm.inflate(infl);
|
||||
|
||||
remove_large_items(excludes, corrected_bin);
|
||||
|
||||
// If there is something on the plate
|
||||
if (!excludes.empty()) arranger.preload(excludes);
|
||||
|
||||
std::vector<std::reference_wrapper<Item>> inp;
|
||||
inp.reserve(shapes.size() + excludes.size());
|
||||
for (auto &itm : shapes ) inp.emplace_back(itm);
|
||||
for (auto &itm : excludes) inp.emplace_back(itm);
|
||||
|
||||
// Use the minimum bounding box rotation as a starting point.
|
||||
// TODO: This only works for convex hull. If we ever switch to concave
|
||||
// polygon nesting, a convex hull needs to be calculated.
|
||||
if (params.allow_rotations) {
|
||||
for (auto &itm : shapes) {
|
||||
itm.rotation(min_area_boundingbox_rotation(itm.rawShape()));
|
||||
|
||||
// If the item is too big, try to find a rotation that makes it fit
|
||||
if constexpr (std::is_same_v<BinT, Box>) {
|
||||
auto bb = itm.boundingBox();
|
||||
if (bb.width() >= bin.width() || bb.height() >= bin.height())
|
||||
itm.rotate(fit_into_box_rotation(itm.transformedShape(), bin));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sl::area(corrected_bin) > 0)
|
||||
arranger(inp.begin(), inp.end());
|
||||
else {
|
||||
for (Item &itm : inp)
|
||||
itm.binId(BIN_ID_UNSET);
|
||||
}
|
||||
|
||||
for (Item &itm : inp) itm.inflate(-infl);
|
||||
}
|
||||
|
||||
inline Box to_nestbin(const BoundingBox &bb) { return Box{{bb.min(X), bb.min(Y)}, {bb.max(X), bb.max(Y)}};}
|
||||
inline Circle to_nestbin(const CircleBed &c) { return Circle({c.center()(0), c.center()(1)}, c.radius()); }
|
||||
inline ExPolygon to_nestbin(const Polygon &p) { return ExPolygon{p}; }
|
||||
inline Box to_nestbin(const InfiniteBed &bed) { return Box::infinite({bed.center.x(), bed.center.y()}); }
|
||||
|
||||
inline coord_t width(const BoundingBox& box) { return box.max.x() - box.min.x(); }
|
||||
inline coord_t height(const BoundingBox& box) { return box.max.y() - box.min.y(); }
|
||||
inline double area(const BoundingBox& box) { return double(width(box)) * height(box); }
|
||||
inline double poly_area(const Points &pts) { return std::abs(Polygon::area(pts)); }
|
||||
inline double distance_to(const Point& p1, const Point& p2)
|
||||
{
|
||||
double dx = p2.x() - p1.x();
|
||||
double dy = p2.y() - p1.y();
|
||||
return std::sqrt(dx*dx + dy*dy);
|
||||
}
|
||||
|
||||
static CircleBed to_circle(const Point ¢er, const Points& points) {
|
||||
std::vector<double> vertex_distances;
|
||||
double avg_dist = 0;
|
||||
|
||||
for (const Point& pt : points)
|
||||
{
|
||||
double distance = distance_to(center, pt);
|
||||
vertex_distances.push_back(distance);
|
||||
avg_dist += distance;
|
||||
}
|
||||
|
||||
avg_dist /= vertex_distances.size();
|
||||
|
||||
CircleBed ret(center, avg_dist);
|
||||
for(auto el : vertex_distances)
|
||||
{
|
||||
if (std::abs(el - avg_dist) > 10 * SCALED_EPSILON) {
|
||||
ret = {};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Create Item from Arrangeable
|
||||
static void process_arrangeable(const ArrangePolygon &arrpoly,
|
||||
std::vector<Item> & outp)
|
||||
{
|
||||
Polygon p = arrpoly.poly.contour;
|
||||
const Vec2crd &offs = arrpoly.translation;
|
||||
double rotation = arrpoly.rotation;
|
||||
|
||||
outp.emplace_back(std::move(p));
|
||||
outp.back().rotation(rotation);
|
||||
outp.back().translation({offs.x(), offs.y()});
|
||||
outp.back().inflate(arrpoly.inflation);
|
||||
outp.back().binId(arrpoly.bed_idx);
|
||||
outp.back().priority(arrpoly.priority);
|
||||
outp.back().setOnPackedFn([&arrpoly](Item &itm){
|
||||
itm.inflate(-arrpoly.inflation);
|
||||
});
|
||||
}
|
||||
|
||||
template<class Fn> auto call_with_bed(const Points &bed, Fn &&fn)
|
||||
{
|
||||
if (bed.empty())
|
||||
return fn(InfiniteBed{});
|
||||
else if (bed.size() == 1)
|
||||
return fn(InfiniteBed{bed.front()});
|
||||
else {
|
||||
auto bb = BoundingBox(bed);
|
||||
CircleBed circ = to_circle(bb.center(), bed);
|
||||
auto parea = poly_area(bed);
|
||||
|
||||
if ((1.0 - parea / area(bb)) < 1e-3)
|
||||
return fn(RectangleBed{bb});
|
||||
else if (!std::isnan(circ.radius()))
|
||||
return fn(circ);
|
||||
else
|
||||
return fn(IrregularBed{ExPolygon(bed)});
|
||||
}
|
||||
}
|
||||
|
||||
bool is_box(const Points &bed)
|
||||
{
|
||||
return !bed.empty() &&
|
||||
((1.0 - poly_area(bed) / area(BoundingBox(bed))) < 1e-3);
|
||||
}
|
||||
|
||||
template<>
|
||||
void arrange(ArrangePolygons & items,
|
||||
const ArrangePolygons &excludes,
|
||||
const Points & bed,
|
||||
const ArrangeParams & params)
|
||||
{
|
||||
arrange(items, excludes, to_arrange_bed(bed), params);
|
||||
}
|
||||
|
||||
template<class BedT>
|
||||
void arrange(ArrangePolygons & arrangables,
|
||||
const ArrangePolygons &excludes,
|
||||
const BedT & bed,
|
||||
const ArrangeParams & params)
|
||||
{
|
||||
namespace clppr = Slic3r::ClipperLib;
|
||||
|
||||
std::vector<Item> items, fixeditems;
|
||||
items.reserve(arrangables.size());
|
||||
|
||||
for (ArrangePolygon &arrangeable : arrangables)
|
||||
process_arrangeable(arrangeable, items);
|
||||
|
||||
for (const ArrangePolygon &fixed: excludes)
|
||||
process_arrangeable(fixed, fixeditems);
|
||||
|
||||
for (Item &itm : fixeditems) itm.inflate(scaled(-2. * EPSILON));
|
||||
|
||||
auto &cfn = params.stopcondition;
|
||||
auto &pri = params.progressind;
|
||||
|
||||
_arrange(items, fixeditems, to_nestbin(bed), params, pri, cfn);
|
||||
|
||||
for(size_t i = 0; i < items.size(); ++i) {
|
||||
Point tr = items[i].translation();
|
||||
arrangables[i].translation = {coord_t(tr.x()), coord_t(tr.y())};
|
||||
arrangables[i].rotation = items[i].rotation();
|
||||
arrangables[i].bed_idx = items[i].binId();
|
||||
}
|
||||
}
|
||||
|
||||
template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const BoundingBox &bed, const ArrangeParams ¶ms);
|
||||
template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const CircleBed &bed, const ArrangeParams ¶ms);
|
||||
template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const Polygon &bed, const ArrangeParams ¶ms);
|
||||
template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const InfiniteBed &bed, const ArrangeParams ¶ms);
|
||||
|
||||
ArrangeBed to_arrange_bed(const Points &bedpts)
|
||||
{
|
||||
ArrangeBed ret;
|
||||
|
||||
call_with_bed(bedpts, [&](const auto &bed) {
|
||||
ret = bed;
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void arrange(ArrangePolygons &items,
|
||||
const ArrangePolygons &excludes,
|
||||
const SegmentedRectangleBed &bed,
|
||||
const ArrangeParams ¶ms)
|
||||
{
|
||||
arrange(items, excludes, bed.bb, params);
|
||||
|
||||
if (! excludes.empty())
|
||||
return;
|
||||
|
||||
auto it = std::max_element(items.begin(), items.end(),
|
||||
[](auto &i1, auto &i2) {
|
||||
return i1.bed_idx < i2.bed_idx;
|
||||
});
|
||||
|
||||
size_t beds = 0;
|
||||
if (it != items.end())
|
||||
beds = it->bed_idx + 1;
|
||||
|
||||
std::vector<BoundingBox> pilebb(beds);
|
||||
|
||||
for (auto &itm : items) {
|
||||
if (itm.bed_idx >= 0)
|
||||
pilebb[itm.bed_idx].merge(get_extents(itm.transformed_poly()));
|
||||
}
|
||||
|
||||
auto piecesz = unscaled(bed.bb).size();
|
||||
piecesz.x() /= bed.segments.x();
|
||||
piecesz.y() /= bed.segments.y();
|
||||
|
||||
for (size_t bedidx = 0; bedidx < beds; ++bedidx) {
|
||||
BoundingBox bb;
|
||||
auto pilesz = unscaled(pilebb[bedidx]).size();
|
||||
bb.max.x() = scaled(std::ceil(pilesz.x() / piecesz.x()) * piecesz.x());
|
||||
bb.max.y() = scaled(std::ceil(pilesz.y() / piecesz.y()) * piecesz.y());
|
||||
switch (params.alignment) {
|
||||
case Pivots::BottomLeft:
|
||||
bb.translate(bed.bb.min - bb.min);
|
||||
break;
|
||||
case Pivots::TopRight:
|
||||
bb.translate(bed.bb.max - bb.max);
|
||||
break;
|
||||
case Pivots::BottomRight: {
|
||||
Point bedref{bed.bb.max.x(), bed.bb.min.y()};
|
||||
Point bbref {bb.max.x(), bb.min.y()};
|
||||
bb.translate(bedref - bbref);
|
||||
break;
|
||||
}
|
||||
case Pivots::TopLeft: {
|
||||
Point bedref{bed.bb.min.x(), bed.bb.max.y()};
|
||||
Point bbref {bb.min.x(), bb.max.y()};
|
||||
bb.translate(bedref - bbref);
|
||||
break;
|
||||
}
|
||||
case Pivots::Center: {
|
||||
bb.translate(bed.bb.center() - bb.center());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Vec2crd d = bb.center() - pilebb[bedidx].center();
|
||||
|
||||
auto bedbb = bed.bb;
|
||||
bedbb.offset(-params.min_bed_distance);
|
||||
auto pilebbx = pilebb[bedidx];
|
||||
pilebbx.translate(d);
|
||||
|
||||
Point corr{0, 0};
|
||||
corr.x() = -std::min(0, pilebbx.min.x() - bedbb.min.x())
|
||||
-std::max(0, pilebbx.max.x() - bedbb.max.x());
|
||||
corr.y() = -std::min(0, pilebbx.min.y() - bedbb.min.y())
|
||||
-std::max(0, pilebbx.max.y() - bedbb.max.y());
|
||||
|
||||
d += corr;
|
||||
|
||||
for (auto &itm : items)
|
||||
if (itm.bed_idx == int(bedidx))
|
||||
itm.translation += d;
|
||||
}
|
||||
}
|
||||
|
||||
BoundingBox bounding_box(const InfiniteBed &bed)
|
||||
{
|
||||
BoundingBox ret;
|
||||
using C = coord_t;
|
||||
|
||||
// It is important for Mx and My to be strictly less than half of the
|
||||
// range of type C. width(), height() and area() will not overflow this way.
|
||||
C Mx = C((std::numeric_limits<C>::lowest() + 2 * bed.center.x()) / 4.01);
|
||||
C My = C((std::numeric_limits<C>::lowest() + 2 * bed.center.y()) / 4.01);
|
||||
|
||||
ret.max = bed.center - Point{Mx, My};
|
||||
ret.min = bed.center + Point{Mx, My};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace arr
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,220 @@
|
||||
///|/ Copyright (c) Prusa Research 2018 - 2023 Tomáš Mészáros @tamasmeszaros, Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGE_HPP
|
||||
#define ARRANGE_HPP
|
||||
|
||||
#include <boost/variant.hpp>
|
||||
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
#include <libslic3r/BoundingBox.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class BoundingBox;
|
||||
|
||||
namespace arrangement {
|
||||
|
||||
/// Representing an unbounded bed.
|
||||
struct InfiniteBed {
|
||||
Point center;
|
||||
explicit InfiniteBed(const Point &p = {0, 0}): center{p} {}
|
||||
};
|
||||
|
||||
struct RectangleBed {
|
||||
BoundingBox bb;
|
||||
};
|
||||
|
||||
/// A geometry abstraction for a circular print bed. Similarly to BoundingBox.
|
||||
class CircleBed {
|
||||
Point center_;
|
||||
double radius_;
|
||||
public:
|
||||
|
||||
inline CircleBed(): center_(0, 0), radius_(NaNd) {}
|
||||
explicit inline CircleBed(const Point& c, double r): center_(c), radius_(r) {}
|
||||
|
||||
inline double radius() const { return radius_; }
|
||||
inline const Point& center() const { return center_; }
|
||||
};
|
||||
|
||||
struct SegmentedRectangleBed {
|
||||
Vec<2, size_t> segments;
|
||||
BoundingBox bb;
|
||||
|
||||
SegmentedRectangleBed (const BoundingBox &bb,
|
||||
size_t segments_x,
|
||||
size_t segments_y)
|
||||
: segments{segments_x, segments_y}
|
||||
, bb{bb}
|
||||
{}
|
||||
};
|
||||
|
||||
struct IrregularBed {
|
||||
ExPolygon poly;
|
||||
};
|
||||
|
||||
//enum BedType { Infinite, Rectangle, Circle, SegmentedRectangle, Irregular };
|
||||
|
||||
using ArrangeBed = boost::variant<InfiniteBed, RectangleBed, CircleBed, SegmentedRectangleBed, IrregularBed>;
|
||||
|
||||
BoundingBox bounding_box(const InfiniteBed &bed);
|
||||
inline BoundingBox bounding_box(const RectangleBed &b) { return b.bb; }
|
||||
inline BoundingBox bounding_box(const SegmentedRectangleBed &b) { return b.bb; }
|
||||
inline BoundingBox bounding_box(const CircleBed &b)
|
||||
{
|
||||
auto r = static_cast<coord_t>(std::round(b.radius()));
|
||||
Point R{r, r};
|
||||
|
||||
return {b.center() - R, b.center() + R};
|
||||
}
|
||||
inline BoundingBox bounding_box(const ArrangeBed &b)
|
||||
{
|
||||
BoundingBox ret;
|
||||
auto visitor = [&ret](const auto &b) { ret = bounding_box(b); };
|
||||
boost::apply_visitor(visitor, b);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
ArrangeBed to_arrange_bed(const Points &bedpts);
|
||||
|
||||
/// A logical bed representing an object not being arranged. Either the arrange
|
||||
/// has not yet successfully run on this ArrangePolygon or it could not fit the
|
||||
/// object due to overly large size or invalid geometry.
|
||||
static const constexpr int UNARRANGED = -1;
|
||||
|
||||
/// Input/Output structure for the arrange() function. The poly field will not
|
||||
/// be modified during arrangement. Instead, the translation and rotation fields
|
||||
/// will mark the needed transformation for the polygon to be in the arranged
|
||||
/// position. These can also be set to an initial offset and rotation.
|
||||
///
|
||||
/// The bed_idx field will indicate the logical bed into which the
|
||||
/// polygon belongs: UNARRANGED means no place for the polygon
|
||||
/// (also the initial state before arrange), 0..N means the index of the bed.
|
||||
/// Zero is the physical bed, larger than zero means a virtual bed.
|
||||
struct ArrangePolygon {
|
||||
ExPolygon poly; /// The 2D silhouette to be arranged
|
||||
Vec2crd translation{0, 0}; /// The translation of the poly
|
||||
double rotation{0.0}; /// The rotation of the poly in radians
|
||||
coord_t inflation = 0; /// Arrange with inflated polygon
|
||||
int bed_idx{UNARRANGED}; /// To which logical bed does poly belong...
|
||||
int priority{0};
|
||||
|
||||
// If empty, any rotation is allowed (currently unsupported)
|
||||
// If only a zero is there, no rotation is allowed
|
||||
std::vector<double> allowed_rotations = {0.};
|
||||
|
||||
/// Optional setter function which can store arbitrary data in its closure
|
||||
std::function<void(const ArrangePolygon&)> setter = nullptr;
|
||||
|
||||
/// Helper function to call the setter with the arrange data arguments
|
||||
void apply() const { if (setter) setter(*this); }
|
||||
|
||||
/// Test if arrange() was called previously and gave a successful result.
|
||||
bool is_arranged() const { return bed_idx != UNARRANGED; }
|
||||
|
||||
inline ExPolygon transformed_poly() const
|
||||
{
|
||||
ExPolygon ret = poly;
|
||||
ret.rotate(rotation);
|
||||
ret.translate(translation.x(), translation.y());
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
using ArrangePolygons = std::vector<ArrangePolygon>;
|
||||
|
||||
enum class Pivots {
|
||||
Center, TopLeft, BottomLeft, BottomRight, TopRight
|
||||
};
|
||||
|
||||
struct ArrangeParams {
|
||||
|
||||
/// The minimum distance which is allowed for any
|
||||
/// pair of items on the print bed in any direction.
|
||||
coord_t min_obj_distance = 0;
|
||||
|
||||
/// The minimum distance of any object from bed edges
|
||||
coord_t min_bed_distance = 0;
|
||||
|
||||
/// The accuracy of optimization.
|
||||
/// Goes from 0.0 to 1.0 and scales performance as well
|
||||
float accuracy = 1.f;
|
||||
|
||||
/// Allow parallel execution.
|
||||
bool parallel = true;
|
||||
|
||||
bool allow_rotations = false;
|
||||
|
||||
/// Final alignment of the merged pile after arrangement
|
||||
Pivots alignment = Pivots::Center;
|
||||
|
||||
/// Starting position hint for the arrangement
|
||||
Pivots starting_point = Pivots::Center;
|
||||
|
||||
/// Progress indicator callback called when an object gets packed.
|
||||
/// The unsigned argument is the number of items remaining to pack.
|
||||
std::function<void(unsigned)> progressind;
|
||||
|
||||
std::function<void(const ArrangePolygon &)> on_packed;
|
||||
|
||||
/// A predicate returning true if abort is needed.
|
||||
std::function<bool(void)> stopcondition;
|
||||
|
||||
ArrangeParams() = default;
|
||||
explicit ArrangeParams(coord_t md) : min_obj_distance(md) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief Arranges the input polygons.
|
||||
*
|
||||
* WARNING: Currently, only convex polygons are supported by the libnest2d
|
||||
* library which is used to do the arrangement. This might change in the future
|
||||
* this is why the interface contains a general polygon capable to have holes.
|
||||
*
|
||||
* \param items Input vector of ArrangePolygons. The transformation, rotation
|
||||
* and bin_idx fields will be changed after the call finished and can be used
|
||||
* to apply the result on the input polygon.
|
||||
*/
|
||||
template<class TBed> void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const TBed &bed, const ArrangeParams ¶ms = {});
|
||||
|
||||
// A dispatch function that determines the bed shape from a set of points.
|
||||
template<> void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const Points &bed, const ArrangeParams ¶ms);
|
||||
|
||||
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const BoundingBox &bed, const ArrangeParams ¶ms);
|
||||
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const CircleBed &bed, const ArrangeParams ¶ms);
|
||||
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const Polygon &bed, const ArrangeParams ¶ms);
|
||||
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const InfiniteBed &bed, const ArrangeParams ¶ms);
|
||||
|
||||
inline void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const RectangleBed &bed, const ArrangeParams ¶ms)
|
||||
{
|
||||
arrange(items, excludes, bed.bb, params);
|
||||
}
|
||||
|
||||
inline void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const IrregularBed &bed, const ArrangeParams ¶ms)
|
||||
{
|
||||
arrange(items, excludes, bed.poly.contour, params);
|
||||
}
|
||||
|
||||
void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const SegmentedRectangleBed &bed, const ArrangeParams ¶ms);
|
||||
|
||||
inline void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const ArrangeBed &bed, const ArrangeParams ¶ms)
|
||||
{
|
||||
auto call_arrange = [&](const auto &realbed) { arrange(items, excludes, realbed, params); };
|
||||
boost::apply_visitor(call_arrange, bed);
|
||||
}
|
||||
|
||||
inline void arrange(ArrangePolygons &items, const Points &bed, const ArrangeParams ¶ms = {}) { arrange(items, {}, bed, params); }
|
||||
inline void arrange(ArrangePolygons &items, const BoundingBox &bed, const ArrangeParams ¶ms = {}) { arrange(items, {}, bed, params); }
|
||||
inline void arrange(ArrangePolygons &items, const CircleBed &bed, const ArrangeParams ¶ms = {}) { arrange(items, {}, bed, params); }
|
||||
inline void arrange(ArrangePolygons &items, const Polygon &bed, const ArrangeParams ¶ms = {}) { arrange(items, {}, bed, params); }
|
||||
inline void arrange(ArrangePolygons &items, const InfiniteBed &bed, const ArrangeParams ¶ms = {}) { arrange(items, {}, bed, params); }
|
||||
|
||||
bool is_box(const Points &bed);
|
||||
|
||||
}} // namespace Slic3r::arrangement
|
||||
|
||||
#endif // MODELARRANGE_HPP
|
||||
@@ -0,0 +1,272 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGE2_HPP
|
||||
#define ARRANGE2_HPP
|
||||
|
||||
#include "Scene.hpp"
|
||||
#include "Items/MutableItemTraits.hpp"
|
||||
#include "Core/NFP/NFPArrangeItemTraits.hpp"
|
||||
|
||||
#include "libslic3r/MinAreaBoundingBox.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
template<class ArrItem> class Arranger
|
||||
{
|
||||
public:
|
||||
class Ctl : public ArrangeTaskCtl {
|
||||
public:
|
||||
virtual void on_packed(ArrItem &item) {};
|
||||
};
|
||||
|
||||
virtual ~Arranger() = default;
|
||||
|
||||
virtual void arrange(std::vector<ArrItem> &items,
|
||||
const std::vector<ArrItem> &fixed,
|
||||
const ExtendedBed &bed,
|
||||
Ctl &ctl) = 0;
|
||||
|
||||
void arrange(std::vector<ArrItem> &items,
|
||||
const std::vector<ArrItem> &fixed,
|
||||
const ExtendedBed &bed,
|
||||
ArrangeTaskCtl &ctl);
|
||||
|
||||
void arrange(std::vector<ArrItem> &items,
|
||||
const std::vector<ArrItem> &fixed,
|
||||
const ExtendedBed &bed,
|
||||
Ctl &&ctl)
|
||||
{
|
||||
arrange(items, fixed, bed, ctl);
|
||||
}
|
||||
|
||||
void arrange(std::vector<ArrItem> &items,
|
||||
const std::vector<ArrItem> &fixed,
|
||||
const ExtendedBed &bed,
|
||||
ArrangeTaskCtl &&ctl)
|
||||
{
|
||||
arrange(items, fixed, bed, ctl);
|
||||
}
|
||||
|
||||
static std::unique_ptr<Arranger> create(const ArrangeSettingsView &settings);
|
||||
};
|
||||
|
||||
template<class ArrItem> using ArrangerCtl = typename Arranger<ArrItem>::Ctl;
|
||||
|
||||
template<class ArrItem>
|
||||
class DefaultArrangerCtl : public Arranger<ArrItem>::Ctl {
|
||||
ArrangeTaskCtl *taskctl = nullptr;
|
||||
|
||||
public:
|
||||
DefaultArrangerCtl() = default;
|
||||
|
||||
explicit DefaultArrangerCtl(ArrangeTaskCtl &ctl) : taskctl{&ctl} {}
|
||||
|
||||
void update_status(int st) override
|
||||
{
|
||||
if (taskctl)
|
||||
taskctl->update_status(st);
|
||||
}
|
||||
|
||||
bool was_canceled() const override
|
||||
{
|
||||
if (taskctl)
|
||||
return taskctl->was_canceled();
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
template<class ArrItem>
|
||||
void Arranger<ArrItem>::arrange(std::vector<ArrItem> &items,
|
||||
const std::vector<ArrItem> &fixed,
|
||||
const ExtendedBed &bed,
|
||||
ArrangeTaskCtl &ctl)
|
||||
{
|
||||
arrange(items, fixed, bed, DefaultArrangerCtl<ArrItem>{ctl});
|
||||
}
|
||||
|
||||
class EmptyItemOutlineError: public std::exception {
|
||||
static constexpr const char *Msg = "No outline can be derived for object";
|
||||
|
||||
public:
|
||||
const char* what() const noexcept override { return Msg; }
|
||||
};
|
||||
|
||||
template<class ArrItem> class ArrangeableToItemConverter
|
||||
{
|
||||
public:
|
||||
virtual ~ArrangeableToItemConverter() = default;
|
||||
|
||||
// May throw EmptyItemOutlineError
|
||||
virtual ArrItem convert(const Arrangeable &arrbl, coord_t offs = 0) const = 0;
|
||||
|
||||
// Returns the extent of simplification that the converter utilizes when
|
||||
// creating arrange items. Zero shall mean no simplification at all.
|
||||
virtual coord_t simplification_tolerance() const { return 0; }
|
||||
|
||||
static std::unique_ptr<ArrangeableToItemConverter> create(
|
||||
ArrangeSettingsView::GeometryHandling geometry_handling,
|
||||
coord_t safety_d);
|
||||
|
||||
static std::unique_ptr<ArrangeableToItemConverter> create(
|
||||
const Scene &sc)
|
||||
{
|
||||
return create(sc.settings().get_geometry_handling(),
|
||||
scaled(sc.settings().get_distance_from_objects()));
|
||||
}
|
||||
};
|
||||
|
||||
template<class DStore, class = WritableDataStoreOnly<DStore>>
|
||||
class AnyWritableDataStore: public AnyWritable
|
||||
{
|
||||
DStore &dstore;
|
||||
|
||||
public:
|
||||
AnyWritableDataStore(DStore &store): dstore{store} {}
|
||||
|
||||
void write(std::string_view key, std::any d) override
|
||||
{
|
||||
set_data(dstore, std::string{key}, std::move(d));
|
||||
}
|
||||
};
|
||||
|
||||
template<class ArrItem>
|
||||
class BasicItemConverter : public ArrangeableToItemConverter<ArrItem>
|
||||
{
|
||||
coord_t m_safety_d;
|
||||
coord_t m_simplify_tol;
|
||||
|
||||
public:
|
||||
BasicItemConverter(coord_t safety_d = 0, coord_t simpl_tol = 0)
|
||||
: m_safety_d{safety_d}, m_simplify_tol{simpl_tol}
|
||||
{}
|
||||
|
||||
coord_t safety_dist() const noexcept { return m_safety_d; }
|
||||
|
||||
coord_t simplification_tolerance() const override
|
||||
{
|
||||
return m_simplify_tol;
|
||||
}
|
||||
};
|
||||
|
||||
template<class ArrItem>
|
||||
class ConvexItemConverter : public BasicItemConverter<ArrItem>
|
||||
{
|
||||
public:
|
||||
using BasicItemConverter<ArrItem>::BasicItemConverter;
|
||||
|
||||
ArrItem convert(const Arrangeable &arrbl, coord_t offs) const override;
|
||||
};
|
||||
|
||||
template<class ArrItem>
|
||||
class AdvancedItemConverter : public BasicItemConverter<ArrItem>
|
||||
{
|
||||
protected:
|
||||
virtual ArrItem get_arritem(const Arrangeable &arrbl, coord_t eps) const;
|
||||
|
||||
public:
|
||||
using BasicItemConverter<ArrItem>::BasicItemConverter;
|
||||
|
||||
ArrItem convert(const Arrangeable &arrbl, coord_t offs) const override;
|
||||
};
|
||||
|
||||
template<class ArrItem>
|
||||
class BalancedItemConverter : public AdvancedItemConverter<ArrItem>
|
||||
{
|
||||
protected:
|
||||
ArrItem get_arritem(const Arrangeable &arrbl, coord_t offs) const override;
|
||||
|
||||
public:
|
||||
using AdvancedItemConverter<ArrItem>::AdvancedItemConverter;
|
||||
};
|
||||
|
||||
template<class ArrItem, class En = void> struct ImbueableItemTraits_
|
||||
{
|
||||
static constexpr const char *Key = "object_id";
|
||||
|
||||
static void imbue_id(ArrItem &itm, const ObjectID &id)
|
||||
{
|
||||
set_arbitrary_data(itm, Key, id);
|
||||
}
|
||||
|
||||
static std::optional<ObjectID> retrieve_id(const ArrItem &itm)
|
||||
{
|
||||
std::optional<ObjectID> ret;
|
||||
auto idptr = get_data<const ObjectID>(itm, Key);
|
||||
if (idptr)
|
||||
ret = *idptr;
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
template<class ArrItem>
|
||||
using ImbueableItemTraits = ImbueableItemTraits_<StripCVRef<ArrItem>>;
|
||||
|
||||
template<class ArrItem>
|
||||
void imbue_id(ArrItem &itm, const ObjectID &id)
|
||||
{
|
||||
ImbueableItemTraits<ArrItem>::imbue_id(itm, id);
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
std::optional<ObjectID> retrieve_id(const ArrItem &itm)
|
||||
{
|
||||
return ImbueableItemTraits<ArrItem>::retrieve_id(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
bool apply_arrangeitem(const ArrItem &itm, ArrangeableModel &mdl)
|
||||
{
|
||||
bool ret = false;
|
||||
|
||||
if (auto id = retrieve_id(itm)) {
|
||||
mdl.visit_arrangeable(*id, [&itm, &ret](Arrangeable &arrbl) {
|
||||
if ((ret = arrbl.assign_bed(get_bed_index(itm))))
|
||||
arrbl.transform(unscaled(get_translation(itm)), get_rotation(itm));
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
double get_min_area_bounding_box_rotation(const ArrItem &itm)
|
||||
{
|
||||
return MinAreaBoundigBox{envelope_convex_hull(itm),
|
||||
MinAreaBoundigBox::pcConvex}
|
||||
.angle_to_X();
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
double get_fit_into_bed_rotation(const ArrItem &itm, const RectangleBed &bed)
|
||||
{
|
||||
double ret = 0.;
|
||||
|
||||
auto bbsz = envelope_bounding_box(itm).size();
|
||||
auto binbb = bounding_box(bed);
|
||||
auto binbbsz = binbb.size();
|
||||
|
||||
if (bbsz.x() >= binbbsz.x() || bbsz.y() >= binbbsz.y())
|
||||
ret = fit_into_box_rotation(envelope_convex_hull(itm), binbb);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
auto get_corrected_bed(const ExtendedBed &bed,
|
||||
const ArrangeableToItemConverter<ArrItem> &converter)
|
||||
{
|
||||
auto bedcpy = bed;
|
||||
visit_bed([tol = -converter.simplification_tolerance()](auto &rawbed) {
|
||||
rawbed = offset(rawbed, tol);
|
||||
}, bedcpy);
|
||||
|
||||
return bedcpy;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // ARRANGE2_HPP
|
||||
@@ -0,0 +1,501 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGEIMPL_HPP
|
||||
#define ARRANGEIMPL_HPP
|
||||
|
||||
#include <random>
|
||||
#include <map>
|
||||
|
||||
#include "Arrange.hpp"
|
||||
|
||||
#include "Core/ArrangeBase.hpp"
|
||||
#include "Core/ArrangeFirstFit.hpp"
|
||||
#include "Core/NFP/PackStrategyNFP.hpp"
|
||||
#include "Core/NFP/Kernels/TMArrangeKernel.hpp"
|
||||
#include "Core/NFP/Kernels/GravityKernel.hpp"
|
||||
#include "Core/NFP/RectangleOverfitPackingStrategy.hpp"
|
||||
#include "Core/Beds.hpp"
|
||||
|
||||
#include "Items/MutableItemTraits.hpp"
|
||||
|
||||
#include "SegmentedRectangleBed.hpp"
|
||||
|
||||
#include "libslic3r/Execution/ExecutionTBB.hpp"
|
||||
#include "libslic3r/Geometry/ConvexHull.hpp"
|
||||
|
||||
#ifndef NDEBUG
|
||||
#include "Core/NFP/Kernels/SVGDebugOutputKernelWrapper.hpp"
|
||||
#endif
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// arrange overload for SegmentedRectangleBed which is exactly what is used
|
||||
// by XL printers.
|
||||
template<class It,
|
||||
class ConstIt,
|
||||
class SelectionStrategy,
|
||||
class PackStrategy, class...SBedArgs>
|
||||
void arrange(SelectionStrategy &&selstrategy,
|
||||
PackStrategy &&packingstrategy,
|
||||
const Range<It> &items,
|
||||
const Range<ConstIt> &fixed,
|
||||
const SegmentedRectangleBed<SBedArgs...> &bed)
|
||||
{
|
||||
// Dispatch:
|
||||
arrange(std::forward<SelectionStrategy>(selstrategy),
|
||||
std::forward<PackStrategy>(packingstrategy), items, fixed,
|
||||
RectangleBed{bed.bb}, SelStrategyTag<SelectionStrategy>{});
|
||||
|
||||
std::vector<int> bed_indices = get_bed_indices(items, fixed);
|
||||
std::map<int, BoundingBox> pilebb;
|
||||
std::map<int, bool> bed_occupied;
|
||||
|
||||
for (auto &itm : items) {
|
||||
auto bedidx = get_bed_index(itm);
|
||||
if (bedidx >= 0) {
|
||||
pilebb[bedidx].merge(fixed_bounding_box(itm));
|
||||
if (is_wipe_tower(itm))
|
||||
bed_occupied[bedidx] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &fxitm : fixed) {
|
||||
auto bedidx = get_bed_index(fxitm);
|
||||
if (bedidx >= 0)
|
||||
bed_occupied[bedidx] = true;
|
||||
}
|
||||
|
||||
auto bedbb = bounding_box(bed);
|
||||
auto piecesz = unscaled(bedbb).size();
|
||||
piecesz.x() /= bed.segments_x();
|
||||
piecesz.y() /= bed.segments_y();
|
||||
|
||||
using Pivots = RectPivots;
|
||||
|
||||
Pivots pivot = bed.alignment();
|
||||
|
||||
for (int bedidx : bed_indices) {
|
||||
if (auto occup_it = bed_occupied.find(bedidx);
|
||||
occup_it != bed_occupied.end() && occup_it->second)
|
||||
continue;
|
||||
|
||||
BoundingBox bb;
|
||||
auto pilesz = unscaled(pilebb[bedidx]).size();
|
||||
bb.max.x() = scaled(std::ceil(pilesz.x() / piecesz.x()) * piecesz.x());
|
||||
bb.max.y() = scaled(std::ceil(pilesz.y() / piecesz.y()) * piecesz.y());
|
||||
|
||||
switch (pivot) {
|
||||
case Pivots::BottomLeft:
|
||||
bb.translate(bedbb.min - bb.min);
|
||||
break;
|
||||
case Pivots::TopRight:
|
||||
bb.translate(bedbb.max - bb.max);
|
||||
break;
|
||||
case Pivots::BottomRight: {
|
||||
Point bedref{bedbb.max.x(), bedbb.min.y()};
|
||||
Point bbref {bb.max.x(), bb.min.y()};
|
||||
bb.translate(bedref - bbref);
|
||||
break;
|
||||
}
|
||||
case Pivots::TopLeft: {
|
||||
Point bedref{bedbb.min.x(), bedbb.max.y()};
|
||||
Point bbref {bb.min.x(), bb.max.y()};
|
||||
bb.translate(bedref - bbref);
|
||||
break;
|
||||
}
|
||||
case Pivots::Center: {
|
||||
bb.translate(bedbb.center() - bb.center());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
;
|
||||
}
|
||||
|
||||
Vec2crd d = bb.center() - pilebb[bedidx].center();
|
||||
|
||||
auto pilebbx = pilebb[bedidx];
|
||||
pilebbx.translate(d);
|
||||
|
||||
Point corr{0, 0};
|
||||
corr.x() = -std::min(0, pilebbx.min.x() - bedbb.min.x())
|
||||
-std::max(0, pilebbx.max.x() - bedbb.max.x());
|
||||
corr.y() = -std::min(0, pilebbx.min.y() - bedbb.min.y())
|
||||
-std::max(0, pilebbx.max.y() - bedbb.max.y());
|
||||
|
||||
d += corr;
|
||||
|
||||
for (auto &itm : items)
|
||||
if (get_bed_index(itm) == static_cast<int>(bedidx) && !is_wipe_tower(itm))
|
||||
translate(itm, d);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
using VariantKernel =
|
||||
boost::variant<TMArrangeKernel, GravityKernel>;
|
||||
|
||||
template<> struct KernelTraits_<VariantKernel> {
|
||||
template<class ArrItem>
|
||||
static double placement_fitness(const VariantKernel &kernel,
|
||||
const ArrItem &itm,
|
||||
const Vec2crd &transl)
|
||||
{
|
||||
double ret = NaNd;
|
||||
boost::apply_visitor(
|
||||
[&](auto &k) { ret = k.placement_fitness(itm, transl); }, kernel);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem, class Bed, class Ctx, class RemIt>
|
||||
static bool on_start_packing(VariantKernel &kernel,
|
||||
ArrItem &itm,
|
||||
const Bed &bed,
|
||||
const Ctx &packing_context,
|
||||
const Range<RemIt> &remaining_items)
|
||||
{
|
||||
bool ret = false;
|
||||
|
||||
boost::apply_visitor([&](auto &k) {
|
||||
ret = k.on_start_packing(itm, bed, packing_context, remaining_items);
|
||||
}, kernel);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
static bool on_item_packed(VariantKernel &kernel, ArrItem &itm)
|
||||
{
|
||||
bool ret = false;
|
||||
boost::apply_visitor([&](auto &k) { ret = k.on_item_packed(itm); },
|
||||
kernel);
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
template<class ArrItem>
|
||||
struct firstfit::ItemArrangedVisitor<ArrItem, DataStoreOnly<ArrItem>> {
|
||||
template<class Bed, class PIt, class RIt>
|
||||
static void on_arranged(ArrItem &itm,
|
||||
const Bed &bed,
|
||||
const Range<PIt> &packed,
|
||||
const Range<RIt> &remaining)
|
||||
{
|
||||
using OnArrangeCb = std::function<void(StripCVRef<ArrItem> &)>;
|
||||
|
||||
auto cb = get_data<OnArrangeCb>(itm, "on_arranged");
|
||||
|
||||
if (cb) {
|
||||
(*cb)(itm);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
inline RectPivots xlpivots_to_rect_pivots(ArrangeSettingsView::XLPivots xlpivot)
|
||||
{
|
||||
if (xlpivot == arr2::ArrangeSettingsView::xlpRandom) {
|
||||
// means it should be random
|
||||
std::random_device rd{};
|
||||
std::mt19937 rng(rd());
|
||||
std::uniform_int_distribution<std::mt19937::result_type>
|
||||
dist(0, arr2::ArrangeSettingsView::xlpRandom - 1);
|
||||
xlpivot = static_cast<ArrangeSettingsView::XLPivots>(dist(rng));
|
||||
}
|
||||
|
||||
RectPivots rectpivot = RectPivots::Center;
|
||||
|
||||
switch(xlpivot) {
|
||||
case arr2::ArrangeSettingsView::xlpCenter: rectpivot = RectPivots::Center; break;
|
||||
case arr2::ArrangeSettingsView::xlpFrontLeft: rectpivot = RectPivots::BottomLeft; break;
|
||||
case arr2::ArrangeSettingsView::xlpFrontRight: rectpivot = RectPivots::BottomRight; break;
|
||||
case arr2::ArrangeSettingsView::xlpRearLeft: rectpivot = RectPivots::TopLeft; break;
|
||||
case arr2::ArrangeSettingsView::xlpRearRight: rectpivot = RectPivots::TopRight; break;
|
||||
default:
|
||||
;
|
||||
}
|
||||
|
||||
return rectpivot;
|
||||
}
|
||||
|
||||
template<class It, class Bed>
|
||||
void fill_rotations(const Range<It> &items,
|
||||
const Bed &bed,
|
||||
const ArrangeSettingsView &settings)
|
||||
{
|
||||
if (!settings.is_rotation_enabled())
|
||||
return;
|
||||
|
||||
for (auto &itm : items) {
|
||||
if (is_wipe_tower(itm)) // Rotating the wipe tower is currently problematic
|
||||
continue;
|
||||
|
||||
// Use the minimum bounding box rotation as a starting point.
|
||||
auto minbbr = get_min_area_bounding_box_rotation(itm);
|
||||
std::vector<double> rotations =
|
||||
{minbbr,
|
||||
minbbr + PI / 4., minbbr + PI / 2.,
|
||||
minbbr + PI, minbbr + 3 * PI / 4.};
|
||||
|
||||
// Add the original rotation of the item if minbbr
|
||||
// is not already the original rotation (zero)
|
||||
if (std::abs(minbbr) > 0.)
|
||||
rotations.emplace_back(0.);
|
||||
|
||||
// Also try to find the rotation that fits the item
|
||||
// into a rectangular bed, given that it cannot fit,
|
||||
// and there exists a rotation which can fit.
|
||||
if constexpr (std::is_convertible_v<Bed, RectangleBed>) {
|
||||
double fitbrot = get_fit_into_bed_rotation(itm, bed);
|
||||
if (std::abs(fitbrot) > 0.)
|
||||
rotations.emplace_back(fitbrot);
|
||||
}
|
||||
|
||||
set_allowed_rotations(itm, rotations);
|
||||
}
|
||||
}
|
||||
|
||||
// An arranger put together to fulfill all the requirements of PrusaSlicer based
|
||||
// on the supplied ArrangeSettings
|
||||
template<class ArrItem>
|
||||
class DefaultArranger: public Arranger<ArrItem> {
|
||||
ArrangeSettings m_settings;
|
||||
|
||||
static constexpr auto Accuracy = 1.;
|
||||
|
||||
template<class It, class FixIt, class Bed>
|
||||
void arrange_(
|
||||
const Range<It> &items,
|
||||
const Range<FixIt> &fixed,
|
||||
const Bed &bed,
|
||||
ArrangerCtl<ArrItem> &ctl)
|
||||
{
|
||||
auto cmpfn = [](const auto &itm1, const auto &itm2) {
|
||||
int pa = get_priority(itm1);
|
||||
int pb = get_priority(itm2);
|
||||
|
||||
return pa == pb ? area(envelope_convex_hull(itm1)) > area(envelope_convex_hull(itm2)) :
|
||||
pa > pb;
|
||||
};
|
||||
|
||||
auto on_arranged = [&ctl](auto &itm, auto &bed, auto &ctx, auto &rem) {
|
||||
ctl.update_status(rem.size());
|
||||
|
||||
ctl.on_packed(itm);
|
||||
|
||||
firstfit::DefaultOnArrangedFn{}(itm, bed, ctx, rem);
|
||||
};
|
||||
|
||||
auto stop_cond = [&ctl] { return ctl.was_canceled(); };
|
||||
|
||||
firstfit::SelectionStrategy sel{cmpfn, on_arranged, stop_cond};
|
||||
|
||||
constexpr auto ep = ex_tbb;
|
||||
|
||||
VariantKernel basekernel;
|
||||
switch (m_settings.get_arrange_strategy()) {
|
||||
default:
|
||||
[[fallthrough]];
|
||||
case ArrangeSettingsView::asAuto:
|
||||
if constexpr (std::is_convertible_v<Bed, CircleBed>){
|
||||
basekernel = GravityKernel{};
|
||||
} else {
|
||||
basekernel = TMArrangeKernel{items.size(), area(bed)};
|
||||
}
|
||||
break;
|
||||
case ArrangeSettingsView::asPullToCenter:
|
||||
basekernel = GravityKernel{};
|
||||
break;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
SVGDebugOutputKernelWrapper<VariantKernel> kernel{bounding_box(bed), basekernel};
|
||||
#else
|
||||
auto & kernel = basekernel;
|
||||
#endif
|
||||
|
||||
fill_rotations(items, bed, m_settings);
|
||||
|
||||
bool with_wipe_tower = std::any_of(items.begin(), items.end(),
|
||||
[](auto &itm) {
|
||||
return is_wipe_tower(itm);
|
||||
});
|
||||
|
||||
// With rectange bed, and no fixed items, let's use an infinite bed
|
||||
// with RectangleOverfitKernelWrapper. It produces better results than
|
||||
// a pure RectangleBed with inner-fit polygon calculation.
|
||||
if (!with_wipe_tower &&
|
||||
m_settings.get_arrange_strategy() == ArrangeSettingsView::asAuto &&
|
||||
IsRectangular<Bed>) {
|
||||
PackStrategyNFP base_strategy{std::move(kernel), ep, Accuracy, stop_cond};
|
||||
|
||||
RectangleOverfitPackingStrategy final_strategy{std::move(base_strategy)};
|
||||
|
||||
arr2::arrange(sel, final_strategy, items, fixed, bed);
|
||||
} else {
|
||||
PackStrategyNFP ps{std::move(kernel), ep, Accuracy, stop_cond};
|
||||
|
||||
arr2::arrange(sel, ps, items, fixed, bed);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
explicit DefaultArranger(const ArrangeSettingsView &settings)
|
||||
{
|
||||
m_settings.set_from(settings);
|
||||
}
|
||||
|
||||
void arrange(
|
||||
std::vector<ArrItem> &items,
|
||||
const std::vector<ArrItem> &fixed,
|
||||
const ExtendedBed &bed,
|
||||
ArrangerCtl<ArrItem> &ctl) override
|
||||
{
|
||||
visit_bed([this, &items, &fixed, &ctl](auto rawbed) {
|
||||
|
||||
if constexpr (IsSegmentedBed<decltype(rawbed)>)
|
||||
rawbed.pivot = xlpivots_to_rect_pivots(
|
||||
m_settings.get_xl_alignment());
|
||||
|
||||
arrange_(range(items), crange(fixed), rawbed, ctl);
|
||||
}, bed);
|
||||
}
|
||||
};
|
||||
|
||||
template<class ArrItem>
|
||||
std::unique_ptr<Arranger<ArrItem>> Arranger<ArrItem>::create(
|
||||
const ArrangeSettingsView &settings)
|
||||
{
|
||||
// Currently all that is needed is handled by DefaultArranger
|
||||
return std::make_unique<DefaultArranger<ArrItem>>(settings);
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
ArrItem ConvexItemConverter<ArrItem>::convert(const Arrangeable &arrbl,
|
||||
coord_t offs) const
|
||||
{
|
||||
auto bed_index = arrbl.get_bed_index();
|
||||
Polygon outline = arrbl.convex_outline();
|
||||
|
||||
if (outline.empty())
|
||||
throw EmptyItemOutlineError{};
|
||||
|
||||
Polygon envelope = arrbl.convex_envelope();
|
||||
|
||||
coord_t infl = offs + coord_t(std::ceil(this->safety_dist() / 2.));
|
||||
|
||||
if (infl != 0) {
|
||||
outline = Geometry::convex_hull(offset(outline, infl));
|
||||
if (! envelope.empty())
|
||||
envelope = Geometry::convex_hull(offset(envelope, infl));
|
||||
}
|
||||
|
||||
ArrItem ret;
|
||||
set_convex_shape(ret, outline);
|
||||
if (! envelope.empty())
|
||||
set_convex_envelope(ret, envelope);
|
||||
|
||||
set_bed_index(ret, bed_index);
|
||||
set_priority(ret, arrbl.priority());
|
||||
|
||||
imbue_id(ret, arrbl.id());
|
||||
if constexpr (IsWritableDataStore<ArrItem>)
|
||||
arrbl.imbue_data(AnyWritableDataStore{ret});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
ArrItem AdvancedItemConverter<ArrItem>::convert(const Arrangeable &arrbl,
|
||||
coord_t offs) const
|
||||
{
|
||||
auto bed_index = arrbl.get_bed_index();
|
||||
ArrItem ret = get_arritem(arrbl, offs);
|
||||
|
||||
set_bed_index(ret, bed_index);
|
||||
set_priority(ret, arrbl.priority());
|
||||
imbue_id(ret, arrbl.id());
|
||||
if constexpr (IsWritableDataStore<ArrItem>)
|
||||
arrbl.imbue_data(AnyWritableDataStore{ret});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
ArrItem AdvancedItemConverter<ArrItem>::get_arritem(const Arrangeable &arrbl,
|
||||
coord_t offs) const
|
||||
{
|
||||
coord_t infl = offs + coord_t(std::ceil(this->safety_dist() / 2.));
|
||||
|
||||
auto outline = arrbl.full_outline();
|
||||
|
||||
if (outline.empty())
|
||||
throw EmptyItemOutlineError{};
|
||||
|
||||
auto envelope = arrbl.full_envelope();
|
||||
|
||||
if (infl != 0) {
|
||||
outline = offset_ex(outline, infl);
|
||||
if (! envelope.empty())
|
||||
envelope = offset_ex(envelope, infl);
|
||||
}
|
||||
|
||||
auto simpl_tol = static_cast<double>(this->simplification_tolerance());
|
||||
|
||||
if (simpl_tol > 0.)
|
||||
{
|
||||
outline = expolygons_simplify(outline, simpl_tol);
|
||||
if (!envelope.empty())
|
||||
envelope = expolygons_simplify(envelope, simpl_tol);
|
||||
}
|
||||
|
||||
ArrItem ret;
|
||||
set_shape(ret, outline);
|
||||
if (! envelope.empty())
|
||||
set_envelope(ret, envelope);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
ArrItem BalancedItemConverter<ArrItem>::get_arritem(const Arrangeable &arrbl,
|
||||
coord_t offs) const
|
||||
{
|
||||
ArrItem ret = AdvancedItemConverter<ArrItem>::get_arritem(arrbl, offs);
|
||||
set_convex_envelope(ret, envelope_convex_hull(ret));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
std::unique_ptr<ArrangeableToItemConverter<ArrItem>>
|
||||
ArrangeableToItemConverter<ArrItem>::create(
|
||||
ArrangeSettingsView::GeometryHandling gh,
|
||||
coord_t safety_d)
|
||||
{
|
||||
std::unique_ptr<ArrangeableToItemConverter<ArrItem>> ret;
|
||||
|
||||
constexpr coord_t SimplifyTol = scaled(.2);
|
||||
|
||||
switch(gh) {
|
||||
case arr2::ArrangeSettingsView::ghConvex:
|
||||
ret = std::make_unique<ConvexItemConverter<ArrItem>>(safety_d);
|
||||
break;
|
||||
case arr2::ArrangeSettingsView::ghBalanced:
|
||||
ret = std::make_unique<BalancedItemConverter<ArrItem>>(safety_d, SimplifyTol);
|
||||
break;
|
||||
case arr2::ArrangeSettingsView::ghAdvanced:
|
||||
ret = std::make_unique<AdvancedItemConverter<ArrItem>>(safety_d, SimplifyTol);
|
||||
break;
|
||||
default:
|
||||
;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // ARRANGEIMPL_HPP
|
||||
@@ -0,0 +1,189 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "ArrangeSettingsDb_AppCfg.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
ArrangeSettingsDb_AppCfg::ArrangeSettingsDb_AppCfg(AppConfig *appcfg) : m_appcfg{appcfg}
|
||||
{
|
||||
sync();
|
||||
}
|
||||
|
||||
void ArrangeSettingsDb_AppCfg::sync()
|
||||
{
|
||||
m_settings_fff.postfix = "_fff";
|
||||
m_settings_fff_seq.postfix = "_fff_seq_print";
|
||||
m_settings_sla.postfix = "_sla";
|
||||
|
||||
std::string dist_fff_str =
|
||||
m_appcfg->get("arrange", "min_object_distance_fff");
|
||||
|
||||
std::string dist_bed_fff_str =
|
||||
m_appcfg->get("arrange", "min_bed_distance_fff");
|
||||
|
||||
std::string dist_fff_seq_print_str =
|
||||
m_appcfg->get("arrange", "min_object_distance_fff_seq_print");
|
||||
|
||||
std::string dist_bed_fff_seq_print_str =
|
||||
m_appcfg->get("arrange", "min_bed_distance_fff_seq_print");
|
||||
|
||||
std::string dist_sla_str =
|
||||
m_appcfg->get("arrange", "min_object_distance_sla");
|
||||
|
||||
std::string dist_bed_sla_str =
|
||||
m_appcfg->get("arrange", "min_bed_distance_sla");
|
||||
|
||||
std::string en_rot_fff_str =
|
||||
m_appcfg->get("arrange", "enable_rotation_fff");
|
||||
|
||||
std::string en_rot_fff_seqp_str =
|
||||
m_appcfg->get("arrange", "enable_rotation_fff_seq_print");
|
||||
|
||||
std::string en_rot_sla_str =
|
||||
m_appcfg->get("arrange", "enable_rotation_sla");
|
||||
|
||||
std::string alignment_xl_str =
|
||||
m_appcfg->get("arrange", "alignment_xl");
|
||||
|
||||
std::string geom_handling_str =
|
||||
m_appcfg->get("arrange", "geometry_handling");
|
||||
|
||||
std::string strategy_str =
|
||||
m_appcfg->get("arrange", "arrange_strategy");
|
||||
|
||||
if (!dist_fff_str.empty())
|
||||
m_settings_fff.vals.d_obj = string_to_float_decimal_point(dist_fff_str);
|
||||
else
|
||||
m_settings_fff.vals.d_obj = m_settings_fff.defaults.d_obj;
|
||||
|
||||
if (!dist_bed_fff_str.empty())
|
||||
m_settings_fff.vals.d_bed = string_to_float_decimal_point(dist_bed_fff_str);
|
||||
else
|
||||
m_settings_fff.vals.d_bed = m_settings_fff.defaults.d_bed;
|
||||
|
||||
if (!dist_fff_seq_print_str.empty())
|
||||
m_settings_fff_seq.vals.d_obj = string_to_float_decimal_point(dist_fff_seq_print_str);
|
||||
else
|
||||
m_settings_fff_seq.vals.d_obj = m_settings_fff_seq.defaults.d_obj;
|
||||
|
||||
if (!dist_bed_fff_seq_print_str.empty())
|
||||
m_settings_fff_seq.vals.d_bed = string_to_float_decimal_point(dist_bed_fff_seq_print_str);
|
||||
else
|
||||
m_settings_fff_seq.vals.d_bed = m_settings_fff_seq.defaults.d_bed;
|
||||
|
||||
if (!dist_sla_str.empty())
|
||||
m_settings_sla.vals.d_obj = string_to_float_decimal_point(dist_sla_str);
|
||||
else
|
||||
m_settings_sla.vals.d_obj = m_settings_sla.defaults.d_obj;
|
||||
|
||||
if (!dist_bed_sla_str.empty())
|
||||
m_settings_sla.vals.d_bed = string_to_float_decimal_point(dist_bed_sla_str);
|
||||
else
|
||||
m_settings_sla.vals.d_bed = m_settings_sla.defaults.d_bed;
|
||||
|
||||
if (!en_rot_fff_str.empty())
|
||||
m_settings_fff.vals.rotations = (en_rot_fff_str == "1" || en_rot_fff_str == "yes");
|
||||
|
||||
if (!en_rot_fff_seqp_str.empty())
|
||||
m_settings_fff_seq.vals.rotations = (en_rot_fff_seqp_str == "1" || en_rot_fff_seqp_str == "yes");
|
||||
else
|
||||
m_settings_fff_seq.vals.rotations = m_settings_fff_seq.defaults.rotations;
|
||||
|
||||
if (!en_rot_sla_str.empty())
|
||||
m_settings_sla.vals.rotations = (en_rot_sla_str == "1" || en_rot_sla_str == "yes");
|
||||
else
|
||||
m_settings_sla.vals.rotations = m_settings_sla.defaults.rotations;
|
||||
|
||||
// Override default alignment and save/load it to a temporary slot "alignment_xl"
|
||||
auto arr_alignment = ArrangeSettingsView::to_xl_pivots(alignment_xl_str)
|
||||
.value_or(m_settings_fff.defaults.xl_align);
|
||||
|
||||
m_settings_sla.vals.xl_align = arr_alignment ;
|
||||
m_settings_fff.vals.xl_align = arr_alignment ;
|
||||
m_settings_fff_seq.vals.xl_align = arr_alignment ;
|
||||
|
||||
auto geom_handl = ArrangeSettingsView::to_geometry_handling(geom_handling_str)
|
||||
.value_or(m_settings_fff.defaults.geom_handling);
|
||||
|
||||
m_settings_sla.vals.geom_handling = geom_handl;
|
||||
m_settings_fff.vals.geom_handling = geom_handl;
|
||||
m_settings_fff_seq.vals.geom_handling = geom_handl;
|
||||
|
||||
auto arr_strategy = ArrangeSettingsView::to_arrange_strategy(strategy_str)
|
||||
.value_or(m_settings_fff.defaults.arr_strategy);
|
||||
|
||||
m_settings_sla.vals.arr_strategy = arr_strategy;
|
||||
m_settings_fff.vals.arr_strategy = arr_strategy;
|
||||
m_settings_fff_seq.vals.arr_strategy = arr_strategy;
|
||||
}
|
||||
|
||||
void ArrangeSettingsDb_AppCfg::distance_from_obj_range(float &min,
|
||||
float &max) const
|
||||
{
|
||||
min = get_slot(this).dobj_range.minval;
|
||||
max = get_slot(this).dobj_range.maxval;
|
||||
}
|
||||
|
||||
void ArrangeSettingsDb_AppCfg::distance_from_bed_range(float &min,
|
||||
float &max) const
|
||||
{
|
||||
min = get_slot(this).dbed_range.minval;
|
||||
max = get_slot(this).dbed_range.maxval;
|
||||
}
|
||||
|
||||
arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_distance_from_objects(float v)
|
||||
{
|
||||
Slot &slot = get_slot(this);
|
||||
slot.vals.d_obj = v;
|
||||
m_appcfg->set("arrange", "min_object_distance" + slot.postfix,
|
||||
float_to_string_decimal_point(v));
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_distance_from_bed(float v)
|
||||
{
|
||||
Slot &slot = get_slot(this);
|
||||
slot.vals.d_bed = v;
|
||||
m_appcfg->set("arrange", "min_bed_distance" + slot.postfix,
|
||||
float_to_string_decimal_point(v));
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_rotation_enabled(bool v)
|
||||
{
|
||||
Slot &slot = get_slot(this);
|
||||
slot.vals.rotations = v;
|
||||
m_appcfg->set("arrange", "enable_rotation" + slot.postfix, v ? "1" : "0");
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_xl_alignment(XLPivots v)
|
||||
{
|
||||
m_settings_fff.vals.xl_align = v;
|
||||
m_appcfg->set("arrange", "alignment_xl", std::string{get_label(v)});
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_geometry_handling(GeometryHandling v)
|
||||
{
|
||||
m_settings_fff.vals.geom_handling = v;
|
||||
m_appcfg->set("arrange", "geometry_handling", std::string{get_label(v)});
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
arr2::ArrangeSettingsDb& ArrangeSettingsDb_AppCfg::set_arrange_strategy(ArrangeStrategy v)
|
||||
{
|
||||
m_settings_fff.vals.arr_strategy = v;
|
||||
m_appcfg->set("arrange", "arrange_strategy", std::string{get_label(v)});
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,97 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGESETTINGSDB_APPCFG_HPP
|
||||
#define ARRANGESETTINGSDB_APPCFG_HPP
|
||||
|
||||
#include "ArrangeSettingsView.hpp"
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ArrangeSettingsDb_AppCfg: public arr2::ArrangeSettingsDb
|
||||
{
|
||||
public:
|
||||
enum Slots { slotFFF, slotFFFSeqPrint, slotSLA };
|
||||
|
||||
private:
|
||||
AppConfig *m_appcfg;
|
||||
Slots m_current_slot = slotFFF;
|
||||
|
||||
struct FloatRange { float minval = 0.f, maxval = 100.f; };
|
||||
struct Slot
|
||||
{
|
||||
Values vals;
|
||||
Values defaults;
|
||||
FloatRange dobj_range, dbed_range;
|
||||
std::string postfix;
|
||||
};
|
||||
|
||||
// Settings and their defaults are stored separately for fff,
|
||||
// sla and fff sequential mode
|
||||
Slot m_settings_fff, m_settings_fff_seq, m_settings_sla;
|
||||
|
||||
template<class Self>
|
||||
static auto & get_slot(Self *self, Slots slot) {
|
||||
switch(slot) {
|
||||
case slotFFF: return self->m_settings_fff;
|
||||
case slotFFFSeqPrint: return self->m_settings_fff_seq;
|
||||
case slotSLA: return self->m_settings_sla;
|
||||
}
|
||||
|
||||
return self->m_settings_fff;
|
||||
}
|
||||
|
||||
template<class Self> static auto &get_slot(Self *self)
|
||||
{
|
||||
return get_slot(self, self->m_current_slot);
|
||||
}
|
||||
|
||||
template<class Self>
|
||||
static auto& get_ref(Self *self) { return get_slot(self).vals; }
|
||||
|
||||
public:
|
||||
explicit ArrangeSettingsDb_AppCfg(AppConfig *appcfg);
|
||||
|
||||
void sync();
|
||||
|
||||
float get_distance_from_objects() const override { return get_ref(this).d_obj; }
|
||||
float get_distance_from_bed() const override { return get_ref(this).d_bed; }
|
||||
bool is_rotation_enabled() const override { return get_ref(this).rotations; }
|
||||
|
||||
XLPivots get_xl_alignment() const override { return m_settings_fff.vals.xl_align; }
|
||||
GeometryHandling get_geometry_handling() const override { return m_settings_fff.vals.geom_handling; }
|
||||
ArrangeStrategy get_arrange_strategy() const override { return m_settings_fff.vals.arr_strategy; }
|
||||
|
||||
void distance_from_obj_range(float &min, float &max) const override;
|
||||
void distance_from_bed_range(float &min, float &max) const override;
|
||||
|
||||
ArrangeSettingsDb& set_distance_from_objects(float v) override;
|
||||
ArrangeSettingsDb& set_distance_from_bed(float v) override;
|
||||
ArrangeSettingsDb& set_rotation_enabled(bool v) override;
|
||||
|
||||
ArrangeSettingsDb& set_xl_alignment(XLPivots v) override;
|
||||
ArrangeSettingsDb& set_geometry_handling(GeometryHandling v) override;
|
||||
ArrangeSettingsDb& set_arrange_strategy(ArrangeStrategy v) override;
|
||||
|
||||
Values get_defaults() const override { return get_slot(this).defaults; }
|
||||
|
||||
void set_active_slot(Slots slot) noexcept { m_current_slot = slot; }
|
||||
void set_distance_from_obj_range(Slots slot, float min, float max)
|
||||
{
|
||||
get_slot(this, slot).dobj_range = FloatRange{min, max};
|
||||
}
|
||||
|
||||
void set_distance_from_bed_range(Slots slot, float min, float max)
|
||||
{
|
||||
get_slot(this, slot).dbed_range = FloatRange{min, max};
|
||||
}
|
||||
|
||||
Values &get_defaults(Slots slot) { return get_slot(this, slot).defaults; }
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // ARRANGESETTINGSDB_APPCFG_HPP
|
||||
@@ -0,0 +1,238 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGESETTINGSVIEW_HPP
|
||||
#define ARRANGESETTINGSVIEW_HPP
|
||||
|
||||
#include <string_view>
|
||||
#include <array>
|
||||
|
||||
#include "libslic3r/StaticMap.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
class ArrangeSettingsView
|
||||
{
|
||||
public:
|
||||
enum GeometryHandling { ghConvex, ghBalanced, ghAdvanced, ghCount };
|
||||
enum ArrangeStrategy { asAuto, asPullToCenter, asCount };
|
||||
enum XLPivots {
|
||||
xlpCenter,
|
||||
xlpRearLeft,
|
||||
xlpFrontLeft,
|
||||
xlpFrontRight,
|
||||
xlpRearRight,
|
||||
xlpRandom,
|
||||
xlpCount
|
||||
};
|
||||
|
||||
virtual ~ArrangeSettingsView() = default;
|
||||
|
||||
virtual float get_distance_from_objects() const = 0;
|
||||
virtual float get_distance_from_bed() const = 0;
|
||||
virtual bool is_rotation_enabled() const = 0;
|
||||
|
||||
virtual XLPivots get_xl_alignment() const = 0;
|
||||
virtual GeometryHandling get_geometry_handling() const = 0;
|
||||
virtual ArrangeStrategy get_arrange_strategy() const = 0;
|
||||
|
||||
static constexpr std::string_view get_label(GeometryHandling v)
|
||||
{
|
||||
constexpr auto STR = std::array{
|
||||
"0"sv, // convex
|
||||
"1"sv, // balanced
|
||||
"2"sv, // advanced
|
||||
"-1"sv, // undefined
|
||||
};
|
||||
|
||||
return STR[v];
|
||||
}
|
||||
|
||||
static constexpr std::string_view get_label(ArrangeStrategy v)
|
||||
{
|
||||
constexpr auto STR = std::array{
|
||||
"0"sv, // auto
|
||||
"1"sv, // pulltocenter
|
||||
"-1"sv, // undefined
|
||||
};
|
||||
|
||||
return STR[v];
|
||||
}
|
||||
|
||||
static constexpr std::string_view get_label(XLPivots v)
|
||||
{
|
||||
constexpr auto STR = std::array{
|
||||
"0"sv, // center
|
||||
"1"sv, // rearleft
|
||||
"2"sv, // frontleft
|
||||
"3"sv, // frontright
|
||||
"4"sv, // rearright
|
||||
"5"sv, // random
|
||||
"-1"sv, // undefined
|
||||
};
|
||||
|
||||
return STR[v];
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
template<class EnumType, size_t N>
|
||||
using EnumMap = StaticMap<std::string_view, EnumType, N>;
|
||||
|
||||
template<class EnumType, size_t N>
|
||||
static constexpr std::optional<EnumType> get_enumval(std::string_view str,
|
||||
const EnumMap<EnumType, N> &emap)
|
||||
{
|
||||
std::optional<EnumType> ret;
|
||||
|
||||
if (auto v = query(emap, str); v.has_value()) {
|
||||
ret = *v;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
static constexpr std::optional<GeometryHandling> to_geometry_handling(std::string_view str)
|
||||
{
|
||||
return get_enumval(str, GeometryHandlingLabels);
|
||||
}
|
||||
|
||||
static constexpr std::optional<ArrangeStrategy> to_arrange_strategy(std::string_view str)
|
||||
{
|
||||
return get_enumval(str, ArrangeStrategyLabels);
|
||||
}
|
||||
|
||||
static constexpr std::optional<XLPivots> to_xl_pivots(std::string_view str)
|
||||
{
|
||||
return get_enumval(str, XLPivotsLabels);
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
static constexpr const auto GeometryHandlingLabels = make_staticmap<std::string_view, GeometryHandling>({
|
||||
{"convex"sv, ghConvex},
|
||||
{"balanced"sv, ghBalanced},
|
||||
{"advanced"sv, ghAdvanced},
|
||||
|
||||
{"0"sv, ghConvex},
|
||||
{"1"sv, ghBalanced},
|
||||
{"2"sv, ghAdvanced},
|
||||
});
|
||||
|
||||
static constexpr const auto ArrangeStrategyLabels = make_staticmap<std::string_view, ArrangeStrategy>({
|
||||
{"auto"sv, asAuto},
|
||||
{"pulltocenter"sv, asPullToCenter},
|
||||
|
||||
{"0"sv, asAuto},
|
||||
{"1"sv, asPullToCenter}
|
||||
});
|
||||
|
||||
static constexpr const auto XLPivotsLabels = make_staticmap<std::string_view, XLPivots>({
|
||||
{"center"sv, xlpCenter },
|
||||
{"rearleft"sv, xlpRearLeft },
|
||||
{"frontleft"sv, xlpFrontLeft },
|
||||
{"frontright"sv, xlpFrontRight },
|
||||
{"rearright"sv, xlpRearRight },
|
||||
{"random"sv, xlpRandom },
|
||||
|
||||
{"0"sv, xlpCenter },
|
||||
{"1"sv, xlpRearLeft },
|
||||
{"2"sv, xlpFrontLeft },
|
||||
{"3"sv, xlpFrontRight },
|
||||
{"4"sv, xlpRearRight },
|
||||
{"5"sv, xlpRandom }
|
||||
});
|
||||
};
|
||||
|
||||
class ArrangeSettingsDb: public ArrangeSettingsView
|
||||
{
|
||||
public:
|
||||
|
||||
virtual void distance_from_obj_range(float &min, float &max) const = 0;
|
||||
virtual void distance_from_bed_range(float &min, float &max) const = 0;
|
||||
|
||||
virtual ArrangeSettingsDb& set_distance_from_objects(float v) = 0;
|
||||
virtual ArrangeSettingsDb& set_distance_from_bed(float v) = 0;
|
||||
virtual ArrangeSettingsDb& set_rotation_enabled(bool v) = 0;
|
||||
|
||||
virtual ArrangeSettingsDb& set_xl_alignment(XLPivots v) = 0;
|
||||
virtual ArrangeSettingsDb& set_geometry_handling(GeometryHandling v) = 0;
|
||||
virtual ArrangeSettingsDb& set_arrange_strategy(ArrangeStrategy v) = 0;
|
||||
|
||||
struct Values {
|
||||
float d_obj = 6.f, d_bed = 0.f;
|
||||
bool rotations = false;
|
||||
XLPivots xl_align = XLPivots::xlpFrontLeft;
|
||||
GeometryHandling geom_handling = GeometryHandling::ghConvex;
|
||||
ArrangeStrategy arr_strategy = ArrangeStrategy::asAuto;
|
||||
|
||||
Values() = default;
|
||||
Values(const ArrangeSettingsView &sv)
|
||||
{
|
||||
d_bed = sv.get_distance_from_bed();
|
||||
d_obj = sv.get_distance_from_objects();
|
||||
arr_strategy = sv.get_arrange_strategy();
|
||||
geom_handling = sv.get_geometry_handling();
|
||||
rotations = sv.is_rotation_enabled();
|
||||
xl_align = sv.get_xl_alignment();
|
||||
}
|
||||
};
|
||||
|
||||
virtual Values get_defaults() const { return {}; }
|
||||
|
||||
ArrangeSettingsDb& set_from(const ArrangeSettingsView &sv)
|
||||
{
|
||||
set_distance_from_bed(sv.get_distance_from_bed());
|
||||
set_distance_from_objects(sv.get_distance_from_objects());
|
||||
set_arrange_strategy(sv.get_arrange_strategy());
|
||||
set_geometry_handling(sv.get_geometry_handling());
|
||||
set_rotation_enabled(sv.is_rotation_enabled());
|
||||
set_xl_alignment(sv.get_xl_alignment());
|
||||
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
class ArrangeSettings: public Slic3r::arr2::ArrangeSettingsDb
|
||||
{
|
||||
ArrangeSettingsDb::Values m_v = {};
|
||||
|
||||
public:
|
||||
explicit ArrangeSettings(
|
||||
const ArrangeSettingsDb::Values &v = {})
|
||||
: m_v{v}
|
||||
{}
|
||||
|
||||
explicit ArrangeSettings(const ArrangeSettingsView &v)
|
||||
: m_v{v}
|
||||
{}
|
||||
|
||||
float get_distance_from_objects() const override { return m_v.d_obj; }
|
||||
float get_distance_from_bed() const override { return m_v.d_bed; }
|
||||
bool is_rotation_enabled() const override { return m_v.rotations; }
|
||||
XLPivots get_xl_alignment() const override { return m_v.xl_align; }
|
||||
GeometryHandling get_geometry_handling() const override { return m_v.geom_handling; }
|
||||
ArrangeStrategy get_arrange_strategy() const override { return m_v.arr_strategy; }
|
||||
|
||||
void distance_from_obj_range(float &min, float &max) const override { min = 0.f; max = 100.f; }
|
||||
void distance_from_bed_range(float &min, float &max) const override { min = 0.f; max = 100.f; }
|
||||
|
||||
ArrangeSettings& set_distance_from_objects(float v) override { m_v.d_obj = v; return *this; }
|
||||
ArrangeSettings& set_distance_from_bed(float v) override { m_v.d_bed = v; return *this; }
|
||||
ArrangeSettings& set_rotation_enabled(bool v) override { m_v.rotations = v; return *this; }
|
||||
ArrangeSettings& set_xl_alignment(XLPivots v) override { m_v.xl_align = v; return *this; }
|
||||
ArrangeSettings& set_geometry_handling(GeometryHandling v) override { m_v.geom_handling = v; return *this; }
|
||||
ArrangeSettings& set_arrange_strategy(ArrangeStrategy v) override { m_v.arr_strategy = v; return *this; }
|
||||
|
||||
auto & values() const { return m_v; }
|
||||
auto & values() { return m_v; }
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // ARRANGESETTINGSVIEW_HPP
|
||||
@@ -0,0 +1,298 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGEBASE_HPP
|
||||
#define ARRANGEBASE_HPP
|
||||
|
||||
#include <iterator>
|
||||
#include <type_traits>
|
||||
|
||||
#include "ArrangeItemTraits.hpp"
|
||||
#include "PackingContext.hpp"
|
||||
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
namespace detail_is_const_it {
|
||||
|
||||
template<class It, class En = void>
|
||||
struct IsConstIt_ { static constexpr bool value = false; };
|
||||
|
||||
template<class It>
|
||||
using iterator_category_t = typename std::iterator_traits<It>::iterator_category;
|
||||
|
||||
template<class It>
|
||||
using iterator_reference_t = typename std::iterator_traits<It>::reference;
|
||||
|
||||
template<class It>
|
||||
struct IsConstIt_ <It, std::enable_if_t<std::is_class_v<iterator_category_t<It>>> >
|
||||
{
|
||||
static constexpr bool value =
|
||||
std::is_const_v<std::remove_reference_t<iterator_reference_t<It>>>;
|
||||
};
|
||||
|
||||
} // namespace detail_is_const_it
|
||||
|
||||
template<class It>
|
||||
static constexpr bool IsConstIterator = detail_is_const_it::IsConstIt_<It>::value;
|
||||
|
||||
template<class It>
|
||||
constexpr bool is_const_iterator(const It &it) noexcept { return IsConstIterator<It>; }
|
||||
|
||||
// The pack() function will use tag dispatching, based on the given strategy
|
||||
// object that is used as its first argument.
|
||||
|
||||
// This tag is derived for a packing strategy as default, and will be used
|
||||
// to cast a compile error.
|
||||
struct UnimplementedPacking {};
|
||||
|
||||
// PackStrategyTag_ needs to be specialized for any valid packing strategy class
|
||||
template<class PackStrategy> struct PackStrategyTag_ {
|
||||
using Tag = UnimplementedPacking;
|
||||
};
|
||||
|
||||
// Helper metafunc to derive packing strategy tag from a strategy object.
|
||||
template<class Strategy>
|
||||
using PackStrategyTag =
|
||||
typename PackStrategyTag_<remove_cvref_t<Strategy>>::Tag;
|
||||
|
||||
|
||||
template<class PackStrategy, class En = void> struct PackStrategyTraits_ {
|
||||
template<class ArrItem> using Context = DefaultPackingContext<ArrItem>;
|
||||
|
||||
template<class ArrItem, class Bed>
|
||||
static Context<ArrItem> create_context(PackStrategy &ps,
|
||||
const Bed &bed,
|
||||
int bed_index)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
template<class PS> using PackStrategyTraits = PackStrategyTraits_<StripCVRef<PS>>;
|
||||
|
||||
template<class PS, class ArrItem>
|
||||
using PackStrategyContext =
|
||||
typename PackStrategyTraits<PS>::template Context<StripCVRef<ArrItem>>;
|
||||
|
||||
template<class ArrItem, class PackStrategy, class Bed>
|
||||
PackStrategyContext<PackStrategy, ArrItem> create_context(PackStrategy &&ps,
|
||||
const Bed &bed,
|
||||
int bed_index)
|
||||
{
|
||||
return PackStrategyTraits<PackStrategy>::template create_context<
|
||||
StripCVRef<ArrItem>>(ps, bed, bed_index);
|
||||
}
|
||||
|
||||
// Function to pack one item into a bed.
|
||||
// strategy parameter holds clue to what packing strategy to use. This function
|
||||
// needs to be overloaded for the strategy tag belonging to the given
|
||||
// strategy.
|
||||
// 'bed' parameter is the type of bed into which the new item should be packed.
|
||||
// See beds.hpp for valid bed classes.
|
||||
// 'item' parameter is the item to be packed. After succesful arrangement
|
||||
// (see return value) the item will have it's translation and rotation
|
||||
// set correctly. If the function returns false, the translation and
|
||||
// rotation of the input item might be changed to arbitrary values.
|
||||
// 'fixed_items' paramter holds a range of ArrItem type objects that are already
|
||||
// on the bed and need to be avoided by the newly packed item.
|
||||
// 'remaining_items' is a range of ArrItem type objects that are intended to be
|
||||
// packed in the future. This information can be leveradged by
|
||||
// the packing strategy to make more intelligent placement
|
||||
// decisions for the input item.
|
||||
template<class Strategy, class Bed, class ArrItem, class RemIt>
|
||||
bool pack(Strategy &&strategy,
|
||||
const Bed &bed,
|
||||
ArrItem &item,
|
||||
const PackStrategyContext<Strategy, ArrItem> &context,
|
||||
const Range<RemIt> &remaining_items)
|
||||
{
|
||||
static_assert(IsConstIterator<RemIt>, "Remaining item iterator is not const!");
|
||||
|
||||
// Dispatch:
|
||||
return pack(std::forward<Strategy>(strategy), bed, item, context,
|
||||
remaining_items, PackStrategyTag<Strategy>{});
|
||||
}
|
||||
|
||||
// Overload without fixed items:
|
||||
template<class Strategy, class Bed, class ArrItem>
|
||||
bool pack(Strategy &&strategy, const Bed &bed, ArrItem &item)
|
||||
{
|
||||
std::vector<ArrItem> dummy;
|
||||
auto context = create_context<ArrItem>(strategy, bed, PhysicalBedId);
|
||||
return pack(std::forward<Strategy>(strategy), bed, item, context,
|
||||
crange(dummy));
|
||||
}
|
||||
|
||||
// Overload when strategy is unkown, yields compile error:
|
||||
template<class Strategy, class Bed, class ArrItem, class RemIt>
|
||||
bool pack(Strategy &&strategy,
|
||||
const Bed &bed,
|
||||
ArrItem &item,
|
||||
const PackStrategyContext<Strategy, ArrItem> &context,
|
||||
const Range<RemIt> &remaining_items,
|
||||
const UnimplementedPacking &)
|
||||
{
|
||||
static_assert(always_false<Strategy>::value,
|
||||
"Packing unimplemented for this placement strategy");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper function to remove unpackable items from the input container.
|
||||
template<class PackStrategy, class Container, class Bed, class StopCond>
|
||||
void remove_unpackable_items(PackStrategy &&ps,
|
||||
Container &c,
|
||||
const Bed &bed,
|
||||
const StopCond &stopcond)
|
||||
{
|
||||
// Safety test: try to pack each item into an empty bed. If it fails
|
||||
// then it should be removed from the list
|
||||
auto it = c.begin();
|
||||
while (it != c.end() && !stopcond()) {
|
||||
StripCVRef<decltype(*it)> &itm = *it;
|
||||
auto cpy{itm};
|
||||
|
||||
if (!pack(ps, bed, cpy)) {
|
||||
set_bed_index(itm, Unarranged);
|
||||
it = c.erase(it);
|
||||
} else
|
||||
it++;
|
||||
}
|
||||
}
|
||||
|
||||
// arrange() function will use tag dispatching based on the selection strategy
|
||||
// given as its first argument.
|
||||
|
||||
// This tag is derived for a selection strategy as default, and will be used
|
||||
// to cast a compile error.
|
||||
struct UnimplementedSelection {};
|
||||
|
||||
// SelStrategyTag_ needs to be specialized for any valid selection strategy class
|
||||
template<class SelStrategy> struct SelStrategyTag_ {
|
||||
using Tag = UnimplementedSelection;
|
||||
};
|
||||
|
||||
// Helper metafunc to derive the selection strategy tag from a strategy object.
|
||||
template<class Strategy>
|
||||
using SelStrategyTag = typename SelStrategyTag_<remove_cvref_t<Strategy>>::Tag;
|
||||
|
||||
// Main function to start the arrangement. Takes a selection and a packing
|
||||
// strategy object as the first two parameters. An implementation
|
||||
// (function overload) must exist for this function that takes the coresponding
|
||||
// selection strategy tag belonging to the given selstrategy argument.
|
||||
//
|
||||
// items parameter is a range of arrange items to arrange.
|
||||
// fixed parameter is a range of arrange items that have fixed position and will
|
||||
// not move during the arrangement but need to be avoided by the
|
||||
// moving items.
|
||||
// bed parameter is the type of bed into which the items need to fit.
|
||||
template<class It,
|
||||
class ConstIt,
|
||||
class TBed,
|
||||
class SelectionStrategy,
|
||||
class PackStrategy>
|
||||
void arrange(SelectionStrategy &&selstrategy,
|
||||
PackStrategy &&packingstrategy,
|
||||
const Range<It> &items,
|
||||
const Range<ConstIt> &fixed,
|
||||
const TBed &bed)
|
||||
{
|
||||
static_assert(IsConstIterator<ConstIt>, "Fixed item iterator is not const!");
|
||||
|
||||
// Dispatch:
|
||||
arrange(std::forward<SelectionStrategy>(selstrategy),
|
||||
std::forward<PackStrategy>(packingstrategy), items, fixed, bed,
|
||||
SelStrategyTag<SelectionStrategy>{});
|
||||
}
|
||||
|
||||
template<class It, class TBed, class SelectionStrategy, class PackStrategy>
|
||||
void arrange(SelectionStrategy &&selstrategy,
|
||||
PackStrategy &&packingstrategy,
|
||||
const Range<It> &items,
|
||||
const TBed &bed)
|
||||
{
|
||||
std::vector<typename std::iterator_traits<It>::value_type> dummy;
|
||||
arrange(std::forward<SelectionStrategy>(selstrategy),
|
||||
std::forward<PackStrategy>(packingstrategy), items, crange(dummy),
|
||||
bed);
|
||||
}
|
||||
|
||||
// Overload for unimplemented selection strategy, yields compile error:
|
||||
template<class It,
|
||||
class ConstIt,
|
||||
class TBed,
|
||||
class SelectionStrategy,
|
||||
class PackStrategy>
|
||||
void arrange(SelectionStrategy &&selstrategy,
|
||||
PackStrategy &&packingstrategy,
|
||||
const Range<It> &items,
|
||||
const Range<ConstIt> &fixed,
|
||||
const TBed &bed,
|
||||
const UnimplementedSelection &)
|
||||
{
|
||||
static_assert(always_false<SelectionStrategy>::value,
|
||||
"Arrange unimplemented for this selection strategy");
|
||||
}
|
||||
|
||||
template<class It>
|
||||
std::vector<int> get_bed_indices(const Range<It> &items)
|
||||
{
|
||||
auto bed_indices = reserve_vector<int>(items.size());
|
||||
|
||||
for (auto &itm : items)
|
||||
bed_indices.emplace_back(get_bed_index(itm));
|
||||
|
||||
std::sort(bed_indices.begin(), bed_indices.end());
|
||||
auto endit = std::unique(bed_indices.begin(), bed_indices.end());
|
||||
|
||||
bed_indices.erase(endit, bed_indices.end());
|
||||
|
||||
return bed_indices;
|
||||
}
|
||||
|
||||
template<class It, class CIt>
|
||||
std::vector<int> get_bed_indices(const Range<It> &items, const Range<CIt> &fixed)
|
||||
{
|
||||
std::vector<int> ret;
|
||||
|
||||
auto iitems = get_bed_indices(items);
|
||||
auto ifixed = get_bed_indices(fixed);
|
||||
ret.reserve(std::max(iitems.size(), ifixed.size()));
|
||||
std::set_union(iitems.begin(), iitems.end(),
|
||||
ifixed.begin(), ifixed.end(),
|
||||
std::back_inserter(ret));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class It>
|
||||
size_t get_bed_count(const Range<It> &items)
|
||||
{
|
||||
return get_bed_indices(items).size();
|
||||
}
|
||||
|
||||
template<class It> int get_max_bed_index(const Range<It> &items)
|
||||
{
|
||||
auto it = std::max_element(items.begin(),
|
||||
items.end(),
|
||||
[](auto &i1, auto &i2) {
|
||||
return get_bed_index(i1) < get_bed_index(i2);
|
||||
});
|
||||
|
||||
int ret = Unarranged;
|
||||
if (it != items.end())
|
||||
ret = get_bed_index(*it);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
struct DefaultStopCondition {
|
||||
constexpr bool operator()() const noexcept { return false; }
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // ARRANGEBASE_HPP
|
||||
@@ -0,0 +1,169 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGEFIRSTFIT_HPP
|
||||
#define ARRANGEFIRSTFIT_HPP
|
||||
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
|
||||
#include <libslic3r/Arrange/Core/ArrangeBase.hpp>
|
||||
|
||||
namespace Slic3r { namespace arr2 { namespace firstfit {
|
||||
|
||||
struct SelectionTag {};
|
||||
|
||||
// Can be specialized by Items
|
||||
template<class ArrItem, class En = void>
|
||||
struct ItemArrangedVisitor {
|
||||
template<class Bed, class PIt, class RIt>
|
||||
static void on_arranged(ArrItem &itm,
|
||||
const Bed &bed,
|
||||
const Range<PIt> &packed_items,
|
||||
const Range<RIt> &remaining_items)
|
||||
{}
|
||||
};
|
||||
|
||||
// Use the the visitor baked into the ArrItem type by default
|
||||
struct DefaultOnArrangedFn {
|
||||
template<class ArrItem, class Bed, class PIt, class RIt>
|
||||
void operator()(ArrItem &itm,
|
||||
const Bed &bed,
|
||||
const Range<PIt> &packed,
|
||||
const Range<RIt> &remaining)
|
||||
{
|
||||
ItemArrangedVisitor<StripCVRef<ArrItem>>::on_arranged(itm, bed, packed,
|
||||
remaining);
|
||||
}
|
||||
};
|
||||
|
||||
struct DefaultItemCompareFn {
|
||||
template<class ArrItem>
|
||||
bool operator() (const ArrItem &ia, const ArrItem &ib)
|
||||
{
|
||||
return get_priority(ia) > get_priority(ib);
|
||||
}
|
||||
};
|
||||
|
||||
template<class CompareFn = DefaultItemCompareFn,
|
||||
class OnArrangedFn = DefaultOnArrangedFn,
|
||||
class StopCondition = DefaultStopCondition>
|
||||
struct SelectionStrategy
|
||||
{
|
||||
CompareFn cmpfn;
|
||||
OnArrangedFn on_arranged_fn;
|
||||
StopCondition cancel_fn;
|
||||
|
||||
SelectionStrategy(CompareFn cmp = {},
|
||||
OnArrangedFn on_arranged = {},
|
||||
StopCondition stopcond = {})
|
||||
: cmpfn{cmp},
|
||||
on_arranged_fn{std::move(on_arranged)},
|
||||
cancel_fn{std::move(stopcond)}
|
||||
{}
|
||||
};
|
||||
|
||||
} // namespace firstfit
|
||||
|
||||
template<class... Args> struct SelStrategyTag_<firstfit::SelectionStrategy<Args...>> {
|
||||
using Tag = firstfit::SelectionTag;
|
||||
};
|
||||
|
||||
template<class It,
|
||||
class ConstIt,
|
||||
class TBed,
|
||||
class SelStrategy,
|
||||
class PackStrategy>
|
||||
void arrange(
|
||||
SelStrategy &&sel,
|
||||
PackStrategy &&ps,
|
||||
const Range<It> &items,
|
||||
const Range<ConstIt> &fixed,
|
||||
const TBed &bed,
|
||||
const firstfit::SelectionTag &)
|
||||
{
|
||||
using ArrItem = typename std::iterator_traits<It>::value_type;
|
||||
using ArrItemRef = std::reference_wrapper<ArrItem>;
|
||||
|
||||
auto sorted_items = reserve_vector<ArrItemRef>(items.size());
|
||||
|
||||
for (auto &itm : items) {
|
||||
set_bed_index(itm, Unarranged);
|
||||
sorted_items.emplace_back(itm);
|
||||
}
|
||||
|
||||
using Context = PackStrategyContext<PackStrategy, ArrItem>;
|
||||
|
||||
std::map<int, Context> bed_contexts;
|
||||
auto get_or_init_context = [&ps, &bed, &bed_contexts](int bedidx) -> Context& {
|
||||
auto ctx_it = bed_contexts.find(bedidx);
|
||||
if (ctx_it == bed_contexts.end()) {
|
||||
auto res = bed_contexts.emplace(
|
||||
bedidx, create_context<ArrItem>(ps, bed, bedidx));
|
||||
|
||||
assert(res.second);
|
||||
|
||||
ctx_it = res.first;
|
||||
}
|
||||
|
||||
return ctx_it->second;
|
||||
};
|
||||
|
||||
for (auto &itm : fixed) {
|
||||
auto bedidx = get_bed_index(itm);
|
||||
if (bedidx >= 0) {
|
||||
Context &ctx = get_or_init_context(bedidx);
|
||||
add_fixed_item(ctx, itm);
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (!std::is_null_pointer_v<decltype(sel.cmpfn)>) {
|
||||
std::stable_sort(sorted_items.begin(), sorted_items.end(), sel.cmpfn);
|
||||
}
|
||||
|
||||
auto is_cancelled = [&sel]() {
|
||||
return sel.cancel_fn();
|
||||
};
|
||||
|
||||
remove_unpackable_items(ps, sorted_items, bed, [&is_cancelled]() {
|
||||
return is_cancelled();
|
||||
});
|
||||
|
||||
auto it = sorted_items.begin();
|
||||
|
||||
using SConstIt = typename std::vector<ArrItemRef>::const_iterator;
|
||||
|
||||
while (it != sorted_items.end() && !is_cancelled()) {
|
||||
bool was_packed = false;
|
||||
int bedidx = 0;
|
||||
while (!was_packed && !is_cancelled()) {
|
||||
for (; !was_packed && !is_cancelled(); bedidx++) {
|
||||
set_bed_index(*it, bedidx);
|
||||
|
||||
auto remaining = Range{std::next(static_cast<SConstIt>(it)),
|
||||
sorted_items.cend()};
|
||||
|
||||
Context &ctx = get_or_init_context(bedidx);
|
||||
|
||||
was_packed = pack(ps, bed, *it, ctx, remaining);
|
||||
|
||||
if(was_packed) {
|
||||
add_packed_item(ctx, *it);
|
||||
|
||||
auto packed_range = Range{sorted_items.cbegin(),
|
||||
static_cast<SConstIt>(it)};
|
||||
|
||||
sel.on_arranged_fn(*it, bed, packed_range, remaining);
|
||||
} else {
|
||||
set_bed_index(*it, Unarranged);
|
||||
}
|
||||
}
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // ARRANGEFIRSTFIT_HPP
|
||||
@@ -0,0 +1,117 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGE_ITEM_TRAITS_HPP
|
||||
#define ARRANGE_ITEM_TRAITS_HPP
|
||||
|
||||
#include <libslic3r/Point.hpp>
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// A logical bed representing an object not being arranged. Either the arrange
|
||||
// has not yet successfully run on this ArrangePolygon or it could not fit the
|
||||
// object due to overly large size or invalid geometry.
|
||||
const constexpr int Unarranged = -1;
|
||||
|
||||
const constexpr int PhysicalBedId = 0;
|
||||
|
||||
// Basic interface of an arrange item. This struct can be specialized for any
|
||||
// type that is arrangeable.
|
||||
template<class ArrItem, class En = void> struct ArrangeItemTraits_ {
|
||||
static Vec2crd get_translation(const ArrItem &ap)
|
||||
{
|
||||
return ap.get_translation();
|
||||
}
|
||||
|
||||
static double get_rotation(const ArrItem &ap)
|
||||
{
|
||||
return ap.get_rotation();
|
||||
}
|
||||
|
||||
static int get_bed_index(const ArrItem &ap) { return ap.get_bed_index(); }
|
||||
|
||||
static int get_priority(const ArrItem &ap) { return ap.get_priority(); }
|
||||
|
||||
// Setters:
|
||||
|
||||
static void set_translation(ArrItem &ap, const Vec2crd &v)
|
||||
{
|
||||
ap.set_translation(v);
|
||||
}
|
||||
|
||||
static void set_rotation(ArrItem &ap, double v) { ap.set_rotation(v); }
|
||||
|
||||
static void set_bed_index(ArrItem &ap, int v) { ap.set_bed_index(v); }
|
||||
};
|
||||
|
||||
template<class T> using ArrangeItemTraits = ArrangeItemTraits_<StripCVRef<T>>;
|
||||
|
||||
// Getters:
|
||||
|
||||
template<class T> Vec2crd get_translation(const T &itm)
|
||||
{
|
||||
return ArrangeItemTraits<T>::get_translation(itm);
|
||||
}
|
||||
|
||||
template<class T> double get_rotation(const T &itm)
|
||||
{
|
||||
return ArrangeItemTraits<T>::get_rotation(itm);
|
||||
}
|
||||
|
||||
template<class T> int get_bed_index(const T &itm)
|
||||
{
|
||||
return ArrangeItemTraits<T>::get_bed_index(itm);
|
||||
}
|
||||
|
||||
template<class T> int get_priority(const T &itm)
|
||||
{
|
||||
return ArrangeItemTraits<T>::get_priority(itm);
|
||||
}
|
||||
|
||||
// Setters:
|
||||
|
||||
template<class T> void set_translation(T &itm, const Vec2crd &v)
|
||||
{
|
||||
ArrangeItemTraits<T>::set_translation(itm, v);
|
||||
}
|
||||
|
||||
template<class T> void set_rotation(T &itm, double v)
|
||||
{
|
||||
ArrangeItemTraits<T>::set_rotation(itm, v);
|
||||
}
|
||||
|
||||
template<class T> void set_bed_index(T &itm, int v)
|
||||
{
|
||||
ArrangeItemTraits<T>::set_bed_index(itm, v);
|
||||
}
|
||||
|
||||
// Helper functions for arrange items
|
||||
template<class ArrItem> bool is_arranged(const ArrItem &ap)
|
||||
{
|
||||
return get_bed_index(ap) > Unarranged;
|
||||
}
|
||||
|
||||
template<class ArrItem> bool is_fixed(const ArrItem &ap)
|
||||
{
|
||||
return get_bed_index(ap) >= PhysicalBedId;
|
||||
}
|
||||
|
||||
template<class ArrItem> bool is_on_physical_bed(const ArrItem &ap)
|
||||
{
|
||||
return get_bed_index(ap) == PhysicalBedId;
|
||||
}
|
||||
|
||||
template<class ArrItem> void translate(ArrItem &ap, const Vec2crd &t)
|
||||
{
|
||||
set_translation(ap, get_translation(ap) + t);
|
||||
}
|
||||
|
||||
template<class ArrItem> void rotate(ArrItem &ap, double rads)
|
||||
{
|
||||
set_rotation(ap, get_rotation(ap) + rads);
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // ARRANGE_ITEM_HPP
|
||||
@@ -0,0 +1,133 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "Beds.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
BoundingBox bounding_box(const InfiniteBed &bed)
|
||||
{
|
||||
BoundingBox ret;
|
||||
using C = coord_t;
|
||||
|
||||
// It is important for Mx and My to be strictly less than half of the
|
||||
// range of type C. width(), height() and area() will not overflow this way.
|
||||
C Mx = C((std::numeric_limits<C>::lowest() + 2 * bed.center.x()) / 4.01);
|
||||
C My = C((std::numeric_limits<C>::lowest() + 2 * bed.center.y()) / 4.01);
|
||||
|
||||
ret.max = bed.center - Point{Mx, My};
|
||||
ret.min = bed.center + Point{Mx, My};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Polygon to_rectangle(const BoundingBox &bb)
|
||||
{
|
||||
Polygon ret;
|
||||
ret.points = {
|
||||
bb.min,
|
||||
Point{bb.max.x(), bb.min.y()},
|
||||
bb.max,
|
||||
Point{bb.min.x(), bb.max.y()}
|
||||
};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Polygon approximate_circle_with_polygon(const arr2::CircleBed &bed, int nedges)
|
||||
{
|
||||
Polygon ret;
|
||||
|
||||
double angle_incr = (2 * M_PI) / nedges; // Angle increment for each edge
|
||||
double angle = 0; // Starting angle
|
||||
|
||||
// Loop to generate vertices for each edge
|
||||
for (int i = 0; i < nedges; i++) {
|
||||
// Calculate coordinates of the vertices using trigonometry
|
||||
auto x = bed.center().x() + static_cast<coord_t>(bed.radius() * std::cos(angle));
|
||||
auto y = bed.center().y() + static_cast<coord_t>(bed.radius() * std::sin(angle));
|
||||
|
||||
// Add vertex to the vector
|
||||
ret.points.emplace_back(x, y);
|
||||
|
||||
// Update the angle for the next iteration
|
||||
angle += angle_incr;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline coord_t width(const BoundingBox &box)
|
||||
{
|
||||
return box.max.x() - box.min.x();
|
||||
}
|
||||
inline coord_t height(const BoundingBox &box)
|
||||
{
|
||||
return box.max.y() - box.min.y();
|
||||
}
|
||||
inline double poly_area(const Points &pts)
|
||||
{
|
||||
return std::abs(Polygon::area(pts));
|
||||
}
|
||||
inline double distance_to(const Point &p1, const Point &p2)
|
||||
{
|
||||
double dx = p2.x() - p1.x();
|
||||
double dy = p2.y() - p1.y();
|
||||
return std::sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
static CircleBed to_circle(const Point ¢er, const Points &points)
|
||||
{
|
||||
std::vector<double> vertex_distances;
|
||||
double avg_dist = 0;
|
||||
|
||||
for (const Point &pt : points) {
|
||||
double distance = distance_to(center, pt);
|
||||
vertex_distances.push_back(distance);
|
||||
avg_dist += distance;
|
||||
}
|
||||
|
||||
avg_dist /= vertex_distances.size();
|
||||
|
||||
CircleBed ret(center, avg_dist);
|
||||
for (auto el : vertex_distances) {
|
||||
if (std::abs(el - avg_dist) > 10 * SCALED_EPSILON) {
|
||||
ret = {};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class Fn> auto call_with_bed(const Points &bed, Fn &&fn)
|
||||
{
|
||||
if (bed.empty())
|
||||
return fn(InfiniteBed{});
|
||||
else if (bed.size() == 1)
|
||||
return fn(InfiniteBed{bed.front()});
|
||||
else {
|
||||
auto bb = BoundingBox(bed);
|
||||
CircleBed circ = to_circle(bb.center(), bed);
|
||||
auto parea = poly_area(bed);
|
||||
|
||||
if ((1.0 - parea / area(bb)) < 1e-3) {
|
||||
return fn(RectangleBed{bb});
|
||||
} else if (!std::isnan(circ.radius()) && (1.0 - parea / area(circ)) < 1e-2)
|
||||
return fn(circ);
|
||||
else
|
||||
return fn(IrregularBed{{ExPolygon(bed)}});
|
||||
}
|
||||
}
|
||||
|
||||
ArrangeBed to_arrange_bed(const Points &bedpts)
|
||||
{
|
||||
ArrangeBed ret;
|
||||
|
||||
call_with_bed(bedpts, [&](const auto &bed) { ret = bed; });
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
@@ -0,0 +1,201 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef BEDS_HPP
|
||||
#define BEDS_HPP
|
||||
|
||||
#include <numeric>
|
||||
|
||||
#include <libslic3r/Point.hpp>
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
#include <libslic3r/BoundingBox.hpp>
|
||||
#include <libslic3r/ClipperUtils.hpp>
|
||||
|
||||
#include <boost/variant.hpp>
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// Bed types to be used with arrangement. Most generic bed is a simple polygon
|
||||
// with holes, but other special bed types are also valid, like a bed without
|
||||
// boundaries, or a special case of a rectangular or circular bed which leaves
|
||||
// a lot of room for optimizations.
|
||||
|
||||
// Representing an unbounded bed.
|
||||
struct InfiniteBed {
|
||||
Point center;
|
||||
explicit InfiniteBed(const Point &p = {0, 0}): center{p} {}
|
||||
};
|
||||
|
||||
BoundingBox bounding_box(const InfiniteBed &bed);
|
||||
|
||||
inline InfiniteBed offset(const InfiniteBed &bed, coord_t) { return bed; }
|
||||
|
||||
struct RectangleBed {
|
||||
BoundingBox bb;
|
||||
|
||||
explicit RectangleBed(const BoundingBox &bedbb) : bb{bedbb} {}
|
||||
explicit RectangleBed(coord_t w, coord_t h, Point c = {0, 0}):
|
||||
bb{{c.x() - w / 2, c.y() - h / 2}, {c.x() + w / 2, c.y() + h / 2}}
|
||||
{}
|
||||
|
||||
coord_t width() const { return bb.size().x(); }
|
||||
coord_t height() const { return bb.size().y(); }
|
||||
};
|
||||
|
||||
inline BoundingBox bounding_box(const RectangleBed &bed) { return bed.bb; }
|
||||
inline RectangleBed offset(RectangleBed bed, coord_t v)
|
||||
{
|
||||
bed.bb.offset(v);
|
||||
return bed;
|
||||
}
|
||||
|
||||
Polygon to_rectangle(const BoundingBox &bb);
|
||||
|
||||
inline Polygon to_rectangle(const RectangleBed &bed)
|
||||
{
|
||||
return to_rectangle(bed.bb);
|
||||
}
|
||||
|
||||
class CircleBed {
|
||||
Point m_center;
|
||||
double m_radius;
|
||||
|
||||
public:
|
||||
CircleBed(): m_center(0, 0), m_radius(NaNd) {}
|
||||
explicit CircleBed(const Point& c, double r)
|
||||
: m_center(c)
|
||||
, m_radius(r)
|
||||
{}
|
||||
|
||||
double radius() const { return m_radius; }
|
||||
const Point& center() const { return m_center; }
|
||||
};
|
||||
|
||||
// Function to approximate a circle with a convex polygon
|
||||
Polygon approximate_circle_with_polygon(const CircleBed &bed, int nedges = 24);
|
||||
|
||||
inline BoundingBox bounding_box(const CircleBed &bed)
|
||||
{
|
||||
auto r = static_cast<coord_t>(std::round(bed.radius()));
|
||||
Point R{r, r};
|
||||
|
||||
return {bed.center() - R, bed.center() + R};
|
||||
}
|
||||
inline CircleBed offset(const CircleBed &bed, coord_t v)
|
||||
{
|
||||
return CircleBed{bed.center(), bed.radius() + v};
|
||||
}
|
||||
|
||||
struct IrregularBed { ExPolygons poly; };
|
||||
inline BoundingBox bounding_box(const IrregularBed &bed)
|
||||
{
|
||||
return get_extents(bed.poly);
|
||||
}
|
||||
|
||||
inline IrregularBed offset(IrregularBed bed, coord_t v)
|
||||
{
|
||||
bed.poly = offset_ex(bed.poly, v);
|
||||
return bed;
|
||||
}
|
||||
|
||||
using ArrangeBed =
|
||||
boost::variant<InfiniteBed, RectangleBed, CircleBed, IrregularBed>;
|
||||
|
||||
inline BoundingBox bounding_box(const ArrangeBed &bed)
|
||||
{
|
||||
BoundingBox ret;
|
||||
auto visitor = [&ret](const auto &b) { ret = bounding_box(b); };
|
||||
boost::apply_visitor(visitor, bed);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline ArrangeBed offset(ArrangeBed bed, coord_t v)
|
||||
{
|
||||
auto visitor = [v](auto &b) { b = offset(b, v); };
|
||||
boost::apply_visitor(visitor, bed);
|
||||
|
||||
return bed;
|
||||
}
|
||||
|
||||
inline double area(const BoundingBox &bb)
|
||||
{
|
||||
auto bbsz = bb.size();
|
||||
return double(bbsz.x()) * bbsz.y();
|
||||
}
|
||||
|
||||
inline double area(const RectangleBed &bed)
|
||||
{
|
||||
auto bbsz = bed.bb.size();
|
||||
return double(bbsz.x()) * bbsz.y();
|
||||
}
|
||||
|
||||
inline double area(const InfiniteBed &bed)
|
||||
{
|
||||
return std::numeric_limits<double>::infinity();
|
||||
}
|
||||
|
||||
inline double area(const IrregularBed &bed)
|
||||
{
|
||||
return std::accumulate(bed.poly.begin(), bed.poly.end(), 0.,
|
||||
[](double s, auto &p) { return s + p.area(); });
|
||||
}
|
||||
|
||||
inline double area(const CircleBed &bed)
|
||||
{
|
||||
return bed.radius() * bed.radius() * PI;
|
||||
}
|
||||
|
||||
inline double area(const ArrangeBed &bed)
|
||||
{
|
||||
double ret = 0.;
|
||||
auto visitor = [&ret](auto &b) { ret = area(b); };
|
||||
boost::apply_visitor(visitor, bed);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline ExPolygons to_expolygons(const InfiniteBed &bed)
|
||||
{
|
||||
return {ExPolygon{to_rectangle(RectangleBed{scaled(1000.), scaled(1000.)})}};
|
||||
}
|
||||
|
||||
inline ExPolygons to_expolygons(const RectangleBed &bed)
|
||||
{
|
||||
return {ExPolygon{to_rectangle(bed)}};
|
||||
}
|
||||
|
||||
inline ExPolygons to_expolygons(const CircleBed &bed)
|
||||
{
|
||||
return {ExPolygon{approximate_circle_with_polygon(bed)}};
|
||||
}
|
||||
|
||||
inline ExPolygons to_expolygons(const IrregularBed &bed) { return bed.poly; }
|
||||
|
||||
inline ExPolygons to_expolygons(const ArrangeBed &bed)
|
||||
{
|
||||
ExPolygons ret;
|
||||
auto visitor = [&ret](const auto &b) { ret = to_expolygons(b); };
|
||||
boost::apply_visitor(visitor, bed);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
ArrangeBed to_arrange_bed(const Points &bedpts);
|
||||
|
||||
template<class Bed, class En = void> struct IsRectangular_ : public std::false_type {};
|
||||
template<> struct IsRectangular_<RectangleBed>: public std::true_type {};
|
||||
template<> struct IsRectangular_<BoundingBox>: public std::true_type {};
|
||||
|
||||
template<class Bed> static constexpr bool IsRectangular = IsRectangular_<Bed>::value;
|
||||
|
||||
} // namespace arr2
|
||||
|
||||
inline BoundingBox &bounding_box(BoundingBox &bb) { return bb; }
|
||||
inline const BoundingBox &bounding_box(const BoundingBox &bb) { return bb; }
|
||||
inline BoundingBox bounding_box(const Polygon &p) { return get_extents(p); }
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // BEDS_HPP
|
||||
@@ -0,0 +1,82 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef DATASTORETRAITS_HPP
|
||||
#define DATASTORETRAITS_HPP
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// Some items can be containers of arbitrary data stored under string keys.
|
||||
template<class ArrItem, class En = void> struct DataStoreTraits_
|
||||
{
|
||||
static constexpr bool Implemented = false;
|
||||
|
||||
template<class T> static const T *get(const ArrItem &, const std::string &key)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Same as above just not const.
|
||||
template<class T> static T *get(ArrItem &, const std::string &key)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static bool has_key(const ArrItem &itm, const std::string &key)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
template<class ArrItem, class En = void> struct WritableDataStoreTraits_
|
||||
{
|
||||
static constexpr bool Implemented = false;
|
||||
|
||||
template<class T> static void set(ArrItem &, const std::string &key, T &&data)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
template<class T> using DataStoreTraits = DataStoreTraits_<StripCVRef<T>>;
|
||||
template<class T> constexpr bool IsDataStore = DataStoreTraits<StripCVRef<T>>::Implemented;
|
||||
template<class T, class TT = T> using DataStoreOnly = std::enable_if_t<IsDataStore<T>, TT>;
|
||||
|
||||
template<class T, class ArrItem>
|
||||
const T *get_data(const ArrItem &itm, const std::string &key)
|
||||
{
|
||||
return DataStoreTraits<ArrItem>::template get<T>(itm, key);
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
bool has_key(const ArrItem &itm, const std::string &key)
|
||||
{
|
||||
return DataStoreTraits<ArrItem>::has_key(itm, key);
|
||||
}
|
||||
|
||||
template<class T, class ArrItem>
|
||||
T *get_data(ArrItem &itm, const std::string &key)
|
||||
{
|
||||
return DataStoreTraits<ArrItem>::template get<T>(itm, key);
|
||||
}
|
||||
|
||||
template<class T> using WritableDataStoreTraits = WritableDataStoreTraits_<StripCVRef<T>>;
|
||||
template<class T> constexpr bool IsWritableDataStore = WritableDataStoreTraits<StripCVRef<T>>::Implemented;
|
||||
template<class T, class TT = T> using WritableDataStoreOnly = std::enable_if_t<IsWritableDataStore<T>, TT>;
|
||||
|
||||
template<class T, class ArrItem>
|
||||
void set_data(ArrItem &itm, const std::string &key, T &&data)
|
||||
{
|
||||
WritableDataStoreTraits<ArrItem>::template set(itm, key, std::forward<T>(data));
|
||||
}
|
||||
|
||||
template<class T> constexpr bool IsReadWritableDataStore = IsDataStore<T> && IsWritableDataStore<T>;
|
||||
template<class T, class TT = T> using ReadWritableDataStoreOnly = std::enable_if_t<IsReadWritableDataStore<T>, TT>;
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // DATASTORETRAITS_HPP
|
||||
@@ -0,0 +1,114 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef CIRCULAR_EDGEITERATOR_HPP
|
||||
#define CIRCULAR_EDGEITERATOR_HPP
|
||||
|
||||
#include <libslic3r/Polygon.hpp>
|
||||
#include <libslic3r/Line.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Circular iterator over a polygon yielding individual edges as Line objects
|
||||
// if flip_lines is true, the orientation of each line is flipped (not the
|
||||
// direction of traversal)
|
||||
template<bool flip_lines = false>
|
||||
class CircularEdgeIterator_ {
|
||||
const Polygon *m_poly = nullptr;
|
||||
size_t m_i = 0;
|
||||
size_t m_c = 0; // counting how many times the iterator has circled over
|
||||
|
||||
public:
|
||||
|
||||
// i: vertex position of first line's starting vertex
|
||||
// poly: target polygon
|
||||
CircularEdgeIterator_(size_t i, const Polygon &poly)
|
||||
: m_poly{&poly}
|
||||
, m_i{!poly.empty() ? i % poly.size() : 0}
|
||||
, m_c{!poly.empty() ? i / poly.size() : 0}
|
||||
{}
|
||||
|
||||
explicit CircularEdgeIterator_ (const Polygon &poly)
|
||||
: CircularEdgeIterator_(0, poly) {}
|
||||
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using value_type = Line;
|
||||
using pointer = Line*;
|
||||
using reference = Line&;
|
||||
|
||||
CircularEdgeIterator_ & operator++()
|
||||
{
|
||||
assert (m_poly);
|
||||
++m_i;
|
||||
if (m_i == m_poly->size()) { // faster than modulo (?)
|
||||
m_i = 0;
|
||||
++m_c;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
CircularEdgeIterator_ operator++(int)
|
||||
{
|
||||
auto cpy = *this; ++(*this); return cpy;
|
||||
}
|
||||
|
||||
Line operator*() const
|
||||
{
|
||||
size_t nx = m_i == m_poly->size() - 1 ? 0 : m_i + 1;
|
||||
Line ret;
|
||||
if constexpr (flip_lines)
|
||||
ret = Line((*m_poly)[nx], (*m_poly)[m_i]);
|
||||
else
|
||||
ret = Line((*m_poly)[m_i], (*m_poly)[nx]);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Line operator->() const { return *(*this); }
|
||||
|
||||
bool operator==(const CircularEdgeIterator_& other) const
|
||||
{
|
||||
return m_i == other.m_i && m_c == other.m_c;
|
||||
}
|
||||
|
||||
bool operator!=(const CircularEdgeIterator_& other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
CircularEdgeIterator_& operator +=(size_t dist)
|
||||
{
|
||||
m_i = (m_i + dist) % m_poly->size();
|
||||
m_c = (m_i + (m_c * m_poly->size()) + dist) / m_poly->size();
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
CircularEdgeIterator_ operator +(size_t dist)
|
||||
{
|
||||
auto cpy = *this;
|
||||
cpy += dist;
|
||||
|
||||
return cpy;
|
||||
}
|
||||
};
|
||||
|
||||
using CircularEdgeIterator = CircularEdgeIterator_<>;
|
||||
using CircularReverseEdgeIterator = CircularEdgeIterator_<true>;
|
||||
|
||||
inline Range<CircularEdgeIterator> line_range(const Polygon &poly)
|
||||
{
|
||||
return Range{CircularEdgeIterator{0, poly}, CircularEdgeIterator{poly.size(), poly}};
|
||||
}
|
||||
|
||||
inline Range<CircularReverseEdgeIterator> line_range_flp(const Polygon &poly)
|
||||
{
|
||||
return Range{CircularReverseEdgeIterator{0, poly}, CircularReverseEdgeIterator{poly.size(), poly}};
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // CIRCULAR_EDGEITERATOR_HPP
|
||||
@@ -0,0 +1,103 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "EdgeCache.hpp"
|
||||
#include "CircularEdgeIterator.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
void EdgeCache::create_cache(const ExPolygon &sh)
|
||||
{
|
||||
m_contour.distances.reserve(sh.contour.size());
|
||||
m_holes.reserve(sh.holes.size());
|
||||
|
||||
m_contour.poly = &sh.contour;
|
||||
|
||||
fill_distances(sh.contour, m_contour.distances);
|
||||
|
||||
for (const Polygon &hole : sh.holes) {
|
||||
auto &hc = m_holes.emplace_back();
|
||||
hc.poly = &hole;
|
||||
fill_distances(hole, hc.distances);
|
||||
}
|
||||
}
|
||||
|
||||
Vec2crd EdgeCache::coords(const ContourCache &cache, double distance) const
|
||||
{
|
||||
assert(cache.poly);
|
||||
return arr2::coords(*cache.poly, cache.distances, distance);
|
||||
}
|
||||
|
||||
void EdgeCache::sample_contour(double accuracy, std::vector<ContourLocation> &samples)
|
||||
{
|
||||
const auto N = m_contour.distances.size();
|
||||
const auto S = stride(N, accuracy);
|
||||
|
||||
if (N == 0 || S == 0)
|
||||
return;
|
||||
|
||||
samples.reserve(N / S + 1);
|
||||
for(size_t i = 0; i < N; i += S) {
|
||||
samples.emplace_back(
|
||||
ContourLocation{0, m_contour.distances[i] / m_contour.distances.back()});
|
||||
}
|
||||
|
||||
for (size_t hidx = 1; hidx <= m_holes.size(); ++hidx) {
|
||||
auto& hc = m_holes[hidx - 1];
|
||||
|
||||
const auto NH = hc.distances.size();
|
||||
const auto SH = stride(NH, accuracy);
|
||||
|
||||
if (NH == 0 || SH == 0)
|
||||
continue;
|
||||
|
||||
samples.reserve(samples.size() + NH / SH + 1);
|
||||
for (size_t i = 0; i < NH; i += SH) {
|
||||
samples.emplace_back(
|
||||
ContourLocation{hidx, hc.distances[i] / hc.distances.back()});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vec2crd coords(const Polygon &poly, const std::vector<double> &distances, double distance)
|
||||
{
|
||||
assert(poly.size() > 1 && distance >= .0 && distance <= 1.0);
|
||||
|
||||
// distance is from 0.0 to 1.0, we scale it up to the full length of
|
||||
// the circumference
|
||||
double d = distance * distances.back();
|
||||
|
||||
// Magic: we find the right edge in log time
|
||||
auto it = std::lower_bound(distances.begin(), distances.end(), d);
|
||||
|
||||
assert(it != distances.end());
|
||||
|
||||
auto idx = it - distances.begin(); // get the index of the edge
|
||||
auto &pts = poly.points;
|
||||
auto edge = idx == long(pts.size() - 1) ? Line(pts.back(), pts.front()) :
|
||||
Line(pts[idx], pts[idx + 1]);
|
||||
|
||||
// Get the remaining distance on the target edge
|
||||
auto ed = d - (idx > 0 ? *std::prev(it) : 0 );
|
||||
|
||||
double t = ed / edge.length();
|
||||
Vec2d n {double(edge.b.x()) - edge.a.x(), double(edge.b.y()) - edge.a.y()};
|
||||
Vec2crd ret = (edge.a.cast<double>() + t * n).cast<coord_t>();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void fill_distances(const Polygon &poly, std::vector<double> &distances)
|
||||
{
|
||||
distances.reserve(poly.size());
|
||||
|
||||
double dist = 0.;
|
||||
auto lrange = line_range(poly);
|
||||
for (const Line l : lrange) {
|
||||
dist += l.length();
|
||||
distances.emplace_back(dist);
|
||||
}
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
@@ -0,0 +1,75 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef EDGECACHE_HPP
|
||||
#define EDGECACHE_HPP
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// Position on the circumference of an ExPolygon.
|
||||
// countour_id: 0th is contour, 1..N are holes
|
||||
// dist: position given as a floating point number within <0., 1.>
|
||||
struct ContourLocation { size_t contour_id; double dist; };
|
||||
|
||||
void fill_distances(const Polygon &poly, std::vector<double> &distances);
|
||||
|
||||
Vec2crd coords(const Polygon &poly, const std::vector<double>& distances, double distance);
|
||||
|
||||
// A class for getting a point on the circumference of the polygon (in log time)
|
||||
//
|
||||
// This is a transformation of the provided polygon to be able to pinpoint
|
||||
// locations on the circumference. The optimizer will pass a floating point
|
||||
// value e.g. within <0,1> and we have to transform this value quickly into a
|
||||
// coordinate on the circumference. By definition 0 should yield the first
|
||||
// vertex and 1.0 would be the last (which should coincide with first).
|
||||
//
|
||||
// We also have to make this work for the holes of the captured polygon.
|
||||
class EdgeCache {
|
||||
struct ContourCache {
|
||||
const Polygon *poly;
|
||||
std::vector<double> distances;
|
||||
} m_contour;
|
||||
|
||||
std::vector<ContourCache> m_holes;
|
||||
|
||||
void create_cache(const ExPolygon& sh);
|
||||
|
||||
Vec2crd coords(const ContourCache& cache, double distance) const;
|
||||
|
||||
public:
|
||||
|
||||
explicit EdgeCache(const ExPolygon *sh)
|
||||
{
|
||||
create_cache(*sh);
|
||||
}
|
||||
|
||||
// Given coeff for accuracy <0., 1.>, return the number of vertices to skip
|
||||
// when fetching corners.
|
||||
static inline size_t stride(const size_t N, double accuracy)
|
||||
{
|
||||
size_t n = std::max(size_t{1}, N);
|
||||
return static_cast<coord_t>(
|
||||
std::round(N / std::pow(n, std::pow(accuracy, 1./3.)))
|
||||
);
|
||||
}
|
||||
|
||||
void sample_contour(double accuracy, std::vector<ContourLocation> &samples);
|
||||
|
||||
Vec2crd coords(const ContourLocation &loc) const
|
||||
{
|
||||
assert(loc.contour_id <= m_holes.size());
|
||||
|
||||
return loc.contour_id > 0 ?
|
||||
coords(m_holes[loc.contour_id - 1], loc.dist) :
|
||||
coords(m_contour, loc.dist);
|
||||
}
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // EDGECACHE_HPP
|
||||
@@ -0,0 +1,65 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef COMPACTIFYKERNEL_HPP
|
||||
#define COMPACTIFYKERNEL_HPP
|
||||
|
||||
#include <numeric>
|
||||
|
||||
#include "libslic3r/Arrange/Core/NFP/NFPArrangeItemTraits.hpp"
|
||||
#include "libslic3r/Arrange/Core/Beds.hpp"
|
||||
|
||||
#include <libslic3r/Geometry/ConvexHull.hpp>
|
||||
#include <libslic3r/ClipperUtils.hpp>
|
||||
|
||||
#include "KernelUtils.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
struct CompactifyKernel {
|
||||
ExPolygons merged_pile;
|
||||
|
||||
template<class ArrItem>
|
||||
double placement_fitness(const ArrItem &itm, const Vec2crd &transl) const
|
||||
{
|
||||
auto pile = merged_pile;
|
||||
|
||||
ExPolygons itm_tr = to_expolygons(envelope_outline(itm));
|
||||
for (auto &p : itm_tr)
|
||||
p.translate(transl);
|
||||
|
||||
append(pile, std::move(itm_tr));
|
||||
|
||||
pile = union_ex(pile);
|
||||
|
||||
Polygon chull = Geometry::convex_hull(pile);
|
||||
|
||||
return -(chull.area());
|
||||
}
|
||||
|
||||
template<class ArrItem, class Bed, class Context, class RemIt>
|
||||
bool on_start_packing(ArrItem &itm,
|
||||
const Bed &bed,
|
||||
const Context &packing_context,
|
||||
const Range<RemIt> & /*remaining_items*/)
|
||||
{
|
||||
bool ret = find_initial_position(itm, bounding_box(bed).center(), bed,
|
||||
packing_context);
|
||||
|
||||
merged_pile.clear();
|
||||
for (const auto &gitm : all_items_range(packing_context)) {
|
||||
append(merged_pile, to_expolygons(fixed_outline(gitm)));
|
||||
}
|
||||
merged_pile = union_ex(merged_pile);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
bool on_item_packed(ArrItem &itm) { return true; }
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // COMPACTIFYKERNEL_HPP
|
||||
@@ -0,0 +1,64 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef GRAVITYKERNEL_HPP
|
||||
#define GRAVITYKERNEL_HPP
|
||||
|
||||
#include "libslic3r/Arrange/Core/NFP/NFPArrangeItemTraits.hpp"
|
||||
#include "libslic3r/Arrange/Core/Beds.hpp"
|
||||
|
||||
#include "KernelUtils.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
struct GravityKernel {
|
||||
std::optional<Vec2crd> sink;
|
||||
std::optional<Vec2crd> item_sink;
|
||||
Vec2d active_sink;
|
||||
|
||||
GravityKernel(Vec2crd gravity_center) :
|
||||
sink{gravity_center}, active_sink{unscaled(gravity_center)} {}
|
||||
|
||||
GravityKernel() = default;
|
||||
|
||||
template<class ArrItem>
|
||||
double placement_fitness(const ArrItem &itm, const Vec2crd &transl) const
|
||||
{
|
||||
Vec2d center = unscaled(envelope_centroid(itm));
|
||||
|
||||
center += unscaled(transl);
|
||||
|
||||
return - (center - active_sink).squaredNorm();
|
||||
}
|
||||
|
||||
template<class ArrItem, class Bed, class Ctx, class RemIt>
|
||||
bool on_start_packing(ArrItem &itm,
|
||||
const Bed &bed,
|
||||
const Ctx &packing_context,
|
||||
const Range<RemIt> & /*remaining_items*/)
|
||||
{
|
||||
bool ret = false;
|
||||
|
||||
item_sink = get_gravity_sink(itm);
|
||||
|
||||
if (!sink) {
|
||||
sink = bounding_box(bed).center();
|
||||
}
|
||||
|
||||
if (item_sink)
|
||||
active_sink = unscaled(*item_sink);
|
||||
else
|
||||
active_sink = unscaled(*sink);
|
||||
|
||||
ret = find_initial_position(itm, scaled(active_sink), bed, packing_context);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem> bool on_item_packed(ArrItem &itm) { return true; }
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // GRAVITYKERNEL_HPP
|
||||
@@ -0,0 +1,61 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef KERNELTRAITS_HPP
|
||||
#define KERNELTRAITS_HPP
|
||||
|
||||
#include "libslic3r/Arrange/Core/ArrangeItemTraits.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// An arrangement kernel that specifies the object function to the arrangement
|
||||
// optimizer and additional callback functions to be able to track the state
|
||||
// of the arranged pile during arrangement.
|
||||
template<class Kernel, class En = void> struct KernelTraits_
|
||||
{
|
||||
// Has to return a score value marking the quality of the arrangement. The
|
||||
// higher this value is, the better a particular placement of the item is.
|
||||
// parameter transl is the translation needed for the item to be moved to
|
||||
// the candidate position.
|
||||
// To discard the item, return NaN as score for every translation.
|
||||
template<class ArrItem>
|
||||
static double placement_fitness(const Kernel &k,
|
||||
const ArrItem &itm,
|
||||
const Vec2crd &transl)
|
||||
{
|
||||
return k.placement_fitness(itm, transl);
|
||||
}
|
||||
|
||||
// Called whenever a new item is about to be processed by the optimizer.
|
||||
// The current state of the arrangement can be saved by the kernel: the
|
||||
// already placed items and the remaining items that need to fit into a
|
||||
// particular bed.
|
||||
// Returns true if the item is can be packed immediately, false if it
|
||||
// should be processed further. This way, a kernel have the power to
|
||||
// choose an initial position for the item that is not on the NFP.
|
||||
template<class ArrItem, class Bed, class Ctx, class RemIt>
|
||||
static bool on_start_packing(Kernel &k,
|
||||
ArrItem &itm,
|
||||
const Bed &bed,
|
||||
const Ctx &packing_context,
|
||||
const Range<RemIt> &remaining_items)
|
||||
{
|
||||
return k.on_start_packing(itm, bed, packing_context, remaining_items);
|
||||
}
|
||||
|
||||
// Called when an item has been succesfully packed. itm should have the
|
||||
// final translation and rotation already set.
|
||||
// Can return false to discard the item after the optimization.
|
||||
template<class ArrItem>
|
||||
static bool on_item_packed(Kernel &k, ArrItem &itm)
|
||||
{
|
||||
return k.on_item_packed(itm);
|
||||
}
|
||||
};
|
||||
|
||||
template<class K> using KernelTraits = KernelTraits_<StripCVRef<K>>;
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // KERNELTRAITS_HPP
|
||||
@@ -0,0 +1,80 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGEKERNELUTILS_HPP
|
||||
#define ARRANGEKERNELUTILS_HPP
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
#include "libslic3r/Arrange/Core/NFP/NFPArrangeItemTraits.hpp"
|
||||
#include "libslic3r/Arrange/Core/Beds.hpp"
|
||||
#include "libslic3r/Arrange/Core/DataStoreTraits.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
template<class Itm, class Bed, class Context>
|
||||
bool find_initial_position(Itm &itm,
|
||||
const Vec2crd &sink,
|
||||
const Bed &bed,
|
||||
const Context &packing_context)
|
||||
{
|
||||
bool ret = false;
|
||||
|
||||
if constexpr (std::is_convertible_v<Bed, RectangleBed> ||
|
||||
std::is_convertible_v<Bed, InfiniteBed> ||
|
||||
std::is_convertible_v<Bed, CircleBed>)
|
||||
{
|
||||
if (all_items_range(packing_context).empty()) {
|
||||
auto rotations = allowed_rotations(itm);
|
||||
set_rotation(itm, 0.);
|
||||
auto chull = envelope_convex_hull(itm);
|
||||
|
||||
for (double rot : rotations) {
|
||||
auto chullcpy = chull;
|
||||
chullcpy.rotate(rot);
|
||||
auto bbitm = bounding_box(chullcpy);
|
||||
|
||||
Vec2crd cb = sink;
|
||||
Vec2crd ci = bbitm.center();
|
||||
|
||||
Vec2crd d = cb - ci;
|
||||
bbitm.translate(d);
|
||||
|
||||
if (bounding_box(bed).contains(bbitm)) {
|
||||
rotate(itm, rot);
|
||||
translate(itm, d);
|
||||
ret = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem> std::optional<Vec2crd> get_gravity_sink(const ArrItem &itm)
|
||||
{
|
||||
constexpr const char * SinkKey = "sink";
|
||||
|
||||
std::optional<Vec2crd> ret;
|
||||
|
||||
auto ptr = get_data<Vec2crd>(itm, SinkKey);
|
||||
|
||||
if (ptr)
|
||||
ret = *ptr;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem> bool is_wipe_tower(const ArrItem &itm)
|
||||
{
|
||||
constexpr const char * Key = "is_wipe_tower";
|
||||
|
||||
return has_key(itm, Key);
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // ARRANGEKERNELUTILS_HPP
|
||||
@@ -0,0 +1,98 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef RECTANGLEOVERFITKERNELWRAPPER_HPP
|
||||
#define RECTANGLEOVERFITKERNELWRAPPER_HPP
|
||||
|
||||
#include "KernelTraits.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Core/NFP/NFPArrangeItemTraits.hpp"
|
||||
#include "libslic3r/Arrange/Core/Beds.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// This is a kernel wrapper that will apply a penality to the object function
|
||||
// if the result cannot fit into the given rectangular bounds. This can be used
|
||||
// to arrange into rectangular boundaries without calculating the IFP of the
|
||||
// rectangle bed. Note that after the arrangement, what is garanteed is that
|
||||
// the resulting pile will fit into the rectangular boundaries, but it will not
|
||||
// be within the given rectangle. The items need to be moved afterwards manually.
|
||||
// Use RectangeOverfitPackingStrategy to automate this post process step.
|
||||
template<class Kernel>
|
||||
struct RectangleOverfitKernelWrapper {
|
||||
Kernel &k;
|
||||
BoundingBox binbb;
|
||||
BoundingBox pilebb;
|
||||
|
||||
RectangleOverfitKernelWrapper(Kernel &kern, const BoundingBox &limits)
|
||||
: k{kern}
|
||||
, binbb{limits}
|
||||
{}
|
||||
|
||||
double overfit(const BoundingBox &itmbb) const
|
||||
{
|
||||
auto fullbb = pilebb;
|
||||
fullbb.merge(itmbb);
|
||||
auto fullbbsz = fullbb.size();
|
||||
auto binbbsz = binbb.size();
|
||||
|
||||
auto wdiff = fullbbsz.x() - binbbsz.x() - SCALED_EPSILON;
|
||||
auto hdiff = fullbbsz.y() - binbbsz.y() - SCALED_EPSILON;
|
||||
double miss = .0;
|
||||
if (wdiff > 0)
|
||||
miss += double(wdiff);
|
||||
if (hdiff > 0)
|
||||
miss += double(hdiff);
|
||||
|
||||
miss = miss > 0? miss : 0;
|
||||
|
||||
return miss;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
double placement_fitness(const ArrItem &item, const Vec2crd &transl) const
|
||||
{
|
||||
double score = KernelTraits<Kernel>::placement_fitness(k, item, transl);
|
||||
|
||||
auto itmbb = envelope_bounding_box(item);
|
||||
itmbb.translate(transl);
|
||||
double miss = overfit(itmbb);
|
||||
score -= miss * miss;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
template<class ArrItem, class Bed, class Ctx, class RemIt>
|
||||
bool on_start_packing(ArrItem &itm,
|
||||
const Bed &bed,
|
||||
const Ctx &packing_context,
|
||||
const Range<RemIt> &remaining_items)
|
||||
{
|
||||
pilebb = BoundingBox{};
|
||||
|
||||
for (auto &fitm : all_items_range(packing_context))
|
||||
pilebb.merge(fixed_bounding_box(fitm));
|
||||
|
||||
return KernelTraits<Kernel>::on_start_packing(k, itm, RectangleBed{binbb},
|
||||
packing_context,
|
||||
remaining_items);
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
bool on_item_packed(ArrItem &itm)
|
||||
{
|
||||
bool ret = KernelTraits<Kernel>::on_item_packed(k, itm);
|
||||
|
||||
double miss = overfit(envelope_bounding_box(itm));
|
||||
|
||||
if (miss > 0.)
|
||||
ret = false;
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // RECTANGLEOVERFITKERNELWRAPPER_H
|
||||
@@ -0,0 +1,100 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef SVGDEBUGOUTPUTKERNELWRAPPER_HPP
|
||||
#define SVGDEBUGOUTPUTKERNELWRAPPER_HPP
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "KernelTraits.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Core/PackingContext.hpp"
|
||||
#include "libslic3r/Arrange/Core/NFP/NFPArrangeItemTraits.hpp"
|
||||
#include "libslic3r/Arrange/Core/Beds.hpp"
|
||||
|
||||
#include <libslic3r/SVG.hpp>
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
template<class Kernel>
|
||||
struct SVGDebugOutputKernelWrapper {
|
||||
Kernel &k;
|
||||
std::unique_ptr<Slic3r::SVG> svg;
|
||||
BoundingBox drawbounds;
|
||||
|
||||
template<class... Args>
|
||||
SVGDebugOutputKernelWrapper(const BoundingBox &bounds, Kernel &kern)
|
||||
: k{kern}, drawbounds{bounds}
|
||||
{}
|
||||
|
||||
template<class ArrItem, class Bed, class Context, class RemIt>
|
||||
bool on_start_packing(ArrItem &itm,
|
||||
const Bed &bed,
|
||||
const Context &packing_context,
|
||||
const Range<RemIt> &rem)
|
||||
{
|
||||
using namespace Slic3r;
|
||||
|
||||
bool ret = KernelTraits<Kernel>::on_start_packing(k, itm, bed,
|
||||
packing_context,
|
||||
rem);
|
||||
|
||||
if (arr2::get_bed_index(itm) < 0)
|
||||
return ret;
|
||||
|
||||
svg.reset();
|
||||
auto bounds = drawbounds;
|
||||
auto fixed = all_items_range(packing_context);
|
||||
svg = std::make_unique<SVG>(std::string("arrange_bed") +
|
||||
std::to_string(
|
||||
arr2::get_bed_index(itm)) +
|
||||
"_" + std::to_string(fixed.size()) +
|
||||
".svg",
|
||||
bounds, 0, false);
|
||||
|
||||
svg->draw(ExPolygon{arr2::to_rectangle(drawbounds)}, "blue", .2f);
|
||||
|
||||
auto nfp = calculate_nfp(itm, packing_context, bed);
|
||||
svg->draw_outline(nfp);
|
||||
svg->draw(nfp, "green", 0.2f);
|
||||
|
||||
for (const auto &fixeditm : fixed) {
|
||||
ExPolygons fixeditm_outline = to_expolygons(fixed_outline(fixeditm));
|
||||
svg->draw_outline(fixeditm_outline);
|
||||
svg->draw(fixeditm_outline, "yellow", 0.5f);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
double placement_fitness(const ArrItem &item, const Vec2crd &transl) const
|
||||
{
|
||||
return KernelTraits<Kernel>::placement_fitness(k, item, transl);
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
bool on_item_packed(ArrItem &itm)
|
||||
{
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::arr2;
|
||||
|
||||
bool ret = KernelTraits<Kernel>::on_item_packed(k, itm);
|
||||
|
||||
if (svg) {
|
||||
ExPolygons itm_outline = to_expolygons(fixed_outline(itm));
|
||||
|
||||
svg->draw_outline(itm_outline);
|
||||
svg->draw(itm_outline, "grey");
|
||||
|
||||
svg->Close();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // SVGDEBUGOUTPUTKERNELWRAPPER_HPP
|
||||
@@ -0,0 +1,249 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef TMARRANGEKERNEL_HPP
|
||||
#define TMARRANGEKERNEL_HPP
|
||||
|
||||
#include "libslic3r/Arrange/Core/NFP/NFPArrangeItemTraits.hpp"
|
||||
#include "libslic3r/Arrange/Core/Beds.hpp"
|
||||
|
||||
#include "KernelUtils.hpp"
|
||||
|
||||
#include <boost/geometry/index/rtree.hpp>
|
||||
#include <libslic3r/BoostAdapter.hpp>
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// Summon the spatial indexing facilities from boost
|
||||
namespace bgi = boost::geometry::index;
|
||||
using SpatElement = std::pair<BoundingBox, unsigned>;
|
||||
using SpatIndex = bgi::rtree<SpatElement, bgi::rstar<16, 4> >;
|
||||
|
||||
class TMArrangeKernel {
|
||||
SpatIndex m_rtree; // spatial index for the normal (bigger) objects
|
||||
SpatIndex m_smallsrtree; // spatial index for only the smaller items
|
||||
BoundingBox m_pilebb;
|
||||
double m_bin_area = NaNd;
|
||||
double m_norm;
|
||||
size_t m_rem_cnt = 0;
|
||||
size_t m_item_cnt = 0;
|
||||
|
||||
|
||||
struct ItemStats { double area = 0.; BoundingBox bb; };
|
||||
std::vector<ItemStats> m_itemstats;
|
||||
|
||||
// A coefficient used in separating bigger items and smaller items.
|
||||
static constexpr double BigItemTreshold = 0.02;
|
||||
|
||||
template<class T> ArithmeticOnly<T, double> norm(T val) const
|
||||
{
|
||||
return double(val) / m_norm;
|
||||
}
|
||||
|
||||
// Treat big items (compared to the print bed) differently
|
||||
bool is_big(double a) const { return a / m_bin_area > BigItemTreshold; }
|
||||
|
||||
protected:
|
||||
std::optional<Point> sink;
|
||||
std::optional<Point> item_sink;
|
||||
Point active_sink;
|
||||
|
||||
const BoundingBox & pilebb() const { return m_pilebb; }
|
||||
|
||||
public:
|
||||
TMArrangeKernel() = default;
|
||||
TMArrangeKernel(Vec2crd gravity_center, size_t itm_cnt, double bedarea = NaNd)
|
||||
: m_bin_area(bedarea)
|
||||
, m_item_cnt{itm_cnt}
|
||||
, sink{gravity_center}
|
||||
{}
|
||||
|
||||
TMArrangeKernel(size_t itm_cnt, double bedarea = NaNd)
|
||||
: m_bin_area(bedarea), m_item_cnt{itm_cnt}
|
||||
{}
|
||||
|
||||
template<class ArrItem>
|
||||
double placement_fitness(const ArrItem &item, const Vec2crd &transl) const
|
||||
{
|
||||
// Candidate item bounding box
|
||||
auto ibb = envelope_bounding_box(item);
|
||||
ibb.translate(transl);
|
||||
auto itmcntr = envelope_centroid(item);
|
||||
itmcntr += transl;
|
||||
|
||||
// Calculate the full bounding box of the pile with the candidate item
|
||||
auto fullbb = m_pilebb;
|
||||
fullbb.merge(ibb);
|
||||
|
||||
// The bounding box of the big items (they will accumulate in the center
|
||||
// of the pile
|
||||
BoundingBox bigbb;
|
||||
if(m_rtree.empty()) {
|
||||
bigbb = fullbb;
|
||||
}
|
||||
else {
|
||||
auto boostbb = m_rtree.bounds();
|
||||
boost::geometry::convert(boostbb, bigbb);
|
||||
}
|
||||
|
||||
// Will hold the resulting score
|
||||
double score = 0;
|
||||
|
||||
// Distinction of cases for the arrangement scene
|
||||
enum e_cases {
|
||||
// This branch is for big items in a mixed (big and small) scene
|
||||
// OR for all items in a small-only scene.
|
||||
BIG_ITEM,
|
||||
|
||||
// For small items in a mixed scene.
|
||||
SMALL_ITEM,
|
||||
|
||||
WIPE_TOWER,
|
||||
} compute_case;
|
||||
|
||||
bool is_wt = is_wipe_tower(item);
|
||||
bool bigitems = is_big(envelope_area(item)) || m_rtree.empty();
|
||||
if (is_wt)
|
||||
compute_case = WIPE_TOWER;
|
||||
else if (bigitems)
|
||||
compute_case = BIG_ITEM;
|
||||
else
|
||||
compute_case = SMALL_ITEM;
|
||||
|
||||
switch (compute_case) {
|
||||
case WIPE_TOWER: {
|
||||
score = (unscaled(itmcntr) - unscaled(active_sink)).squaredNorm();
|
||||
break;
|
||||
}
|
||||
case BIG_ITEM: {
|
||||
const Point& minc = ibb.min; // bottom left corner
|
||||
const Point& maxc = ibb.max; // top right corner
|
||||
|
||||
// top left and bottom right corners
|
||||
Point top_left{minc.x(), maxc.y()};
|
||||
Point bottom_right{maxc.x(), minc.y()};
|
||||
|
||||
// The smallest distance from the arranged pile center:
|
||||
double dist = norm((itmcntr - m_pilebb.center()).template cast<double>().norm());
|
||||
|
||||
// Prepare a variable for the alignment score.
|
||||
// This will indicate: how well is the candidate item
|
||||
// aligned with its neighbors. We will check the alignment
|
||||
// with all neighbors and return the score for the best
|
||||
// alignment. So it is enough for the candidate to be
|
||||
// aligned with only one item.
|
||||
auto alignment_score = 1.;
|
||||
|
||||
auto query = bgi::intersects(ibb);
|
||||
auto& index = is_big(envelope_area(item)) ? m_rtree : m_smallsrtree;
|
||||
|
||||
// Query the spatial index for the neighbors
|
||||
std::vector<SpatElement> result;
|
||||
result.reserve(index.size());
|
||||
|
||||
index.query(query, std::back_inserter(result));
|
||||
|
||||
// now get the score for the best alignment
|
||||
for(auto& e : result) {
|
||||
auto idx = e.second;
|
||||
const ItemStats& p = m_itemstats[idx];
|
||||
auto parea = p.area;
|
||||
if(std::abs(1.0 - parea / fixed_area(item)) < 1e-6) {
|
||||
auto bb = p.bb;
|
||||
bb.merge(ibb);
|
||||
auto bbarea = area(bb);
|
||||
auto ascore = 1.0 - (area(fixed_bounding_box(item)) + area(p.bb)) / bbarea;
|
||||
|
||||
if(ascore < alignment_score)
|
||||
alignment_score = ascore;
|
||||
}
|
||||
}
|
||||
|
||||
double R = double(m_rem_cnt) / (m_item_cnt);
|
||||
R = std::pow(R, 1./3.);
|
||||
|
||||
// The final mix of the score is the balance between the
|
||||
// distance from the full pile center, the pack density and
|
||||
// the alignment with the neighbors
|
||||
|
||||
// Let the density matter more when fewer objects remain
|
||||
score = 0.6 * dist + 0.1 * alignment_score + (1.0 - R) * (0.3 * dist) + R * 0.3 * alignment_score;
|
||||
|
||||
break;
|
||||
}
|
||||
case SMALL_ITEM: {
|
||||
// Here there are the small items that should be placed around the
|
||||
// already processed bigger items.
|
||||
// No need to play around with the anchor points, the center will be
|
||||
// just fine for small items
|
||||
score = norm((itmcntr - bigbb.center()).template cast<double>().norm());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return -score;
|
||||
}
|
||||
|
||||
template<class ArrItem, class Bed, class Context, class RemIt>
|
||||
bool on_start_packing(ArrItem &itm,
|
||||
const Bed &bed,
|
||||
const Context &packing_context,
|
||||
const Range<RemIt> &remaining_items)
|
||||
{
|
||||
item_sink = get_gravity_sink(itm);
|
||||
|
||||
if (!sink) {
|
||||
sink = bounding_box(bed).center();
|
||||
}
|
||||
|
||||
if (item_sink)
|
||||
active_sink = *item_sink;
|
||||
else
|
||||
active_sink = *sink;
|
||||
|
||||
auto fixed = all_items_range(packing_context);
|
||||
|
||||
bool ret = find_initial_position(itm, active_sink, bed, packing_context);
|
||||
|
||||
m_rem_cnt = remaining_items.size();
|
||||
|
||||
if (m_item_cnt == 0)
|
||||
m_item_cnt = m_rem_cnt + fixed.size() + 1;
|
||||
|
||||
if (std::isnan(m_bin_area)) {
|
||||
auto sz = bounding_box(bed).size();
|
||||
|
||||
m_bin_area = scaled<double>(unscaled(sz.x()) * unscaled(sz.y()));
|
||||
}
|
||||
|
||||
m_norm = std::sqrt(m_bin_area);
|
||||
|
||||
m_itemstats.clear();
|
||||
m_itemstats.reserve(fixed.size());
|
||||
m_rtree.clear();
|
||||
m_smallsrtree.clear();
|
||||
m_pilebb = {active_sink, active_sink};
|
||||
unsigned idx = 0;
|
||||
for (auto &fixitem : fixed) {
|
||||
auto fixitmbb = fixed_bounding_box(fixitem);
|
||||
m_itemstats.emplace_back(ItemStats{fixed_area(fixitem), fixitmbb});
|
||||
m_pilebb.merge(fixitmbb);
|
||||
|
||||
if(is_big(fixed_area(fixitem)))
|
||||
m_rtree.insert({fixitmbb, idx});
|
||||
|
||||
m_smallsrtree.insert({fixitmbb, idx});
|
||||
idx++;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
bool on_item_packed(ArrItem &itm) { return true; }
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // TMARRANGEKERNEL_HPP
|
||||
@@ -0,0 +1,422 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef NFP_CPP
|
||||
#define NFP_CPP
|
||||
|
||||
#include "NFP.hpp"
|
||||
#include "CircularEdgeIterator.hpp"
|
||||
|
||||
#include "NFPConcave_Tesselate.hpp"
|
||||
|
||||
#if !defined(_MSC_VER) && defined(__SIZEOF_INT128__) && !defined(__APPLE__)
|
||||
namespace Slic3r { using LargeInt = __int128; }
|
||||
#else
|
||||
#include <boost/multiprecision/integer.hpp>
|
||||
namespace Slic3r { using LargeInt = boost::multiprecision::int128_t; }
|
||||
#endif
|
||||
|
||||
#include <boost/rational.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
static bool line_cmp(const Line& e1, const Line& e2)
|
||||
{
|
||||
using Ratio = boost::rational<LargeInt>;
|
||||
|
||||
const Vec<2, int64_t> ax(1, 0); // Unit vector for the X axis
|
||||
|
||||
Vec<2, int64_t> p1 = (e1.b - e1.a).cast<int64_t>();
|
||||
Vec<2, int64_t> p2 = (e2.b - e2.a).cast<int64_t>();
|
||||
|
||||
// Quadrant mapping array. The quadrant of a vector can be determined
|
||||
// from the dot product of the vector and its perpendicular pair
|
||||
// with the unit vector X axis. The products will carry the values
|
||||
// lcos = dot(p, ax) = l * cos(phi) and
|
||||
// lsin = -dotperp(p, ax) = l * sin(phi) where
|
||||
// l is the length of vector p. From the signs of these values we can
|
||||
// construct an index which has the sign of lcos as MSB and the
|
||||
// sign of lsin as LSB. This index can be used to retrieve the actual
|
||||
// quadrant where vector p resides using the following map:
|
||||
// (+ is 0, - is 1)
|
||||
// cos | sin | decimal | quadrant
|
||||
// + | + | 0 | 0
|
||||
// + | - | 1 | 3
|
||||
// - | + | 2 | 1
|
||||
// - | - | 3 | 2
|
||||
std::array<int, 4> quadrants {0, 3, 1, 2 };
|
||||
|
||||
std::array<int, 2> q {0, 0}; // Quadrant indices for p1 and p2
|
||||
|
||||
using TDots = std::array<int64_t, 2>;
|
||||
TDots lcos { p1.dot(ax), p2.dot(ax) };
|
||||
TDots lsin { -dotperp(p1, ax), -dotperp(p2, ax) };
|
||||
|
||||
// Construct the quadrant indices for p1 and p2
|
||||
for(size_t i = 0; i < 2; ++i) {
|
||||
if (lcos[i] == 0)
|
||||
q[i] = lsin[i] > 0 ? 1 : 3;
|
||||
else if (lsin[i] == 0)
|
||||
q[i] = lcos[i] > 0 ? 0 : 2;
|
||||
else
|
||||
q[i] = quadrants[((lcos[i] < 0) << 1) + (lsin[i] < 0)];
|
||||
}
|
||||
|
||||
if (q[0] == q[1]) { // only bother if p1 and p2 are in the same quadrant
|
||||
auto lsq1 = p1.squaredNorm(); // squared magnitudes, avoid sqrt
|
||||
auto lsq2 = p2.squaredNorm(); // squared magnitudes, avoid sqrt
|
||||
|
||||
// We will actually compare l^2 * cos^2(phi) which saturates the
|
||||
// cos function. But with the quadrant info we can get the sign back
|
||||
int sign = q[0] == 1 || q[0] == 2 ? -1 : 1;
|
||||
|
||||
// If Ratio is an actual rational type, there is no precision loss
|
||||
auto pcos1 = Ratio(lcos[0]) / lsq1 * sign * lcos[0];
|
||||
auto pcos2 = Ratio(lcos[1]) / lsq2 * sign * lcos[1];
|
||||
|
||||
return q[0] < 2 ? pcos1 > pcos2 : pcos1 < pcos2;
|
||||
}
|
||||
|
||||
// If in different quadrants, compare the quadrant indices only.
|
||||
return q[0] < q[1];
|
||||
}
|
||||
|
||||
static inline bool vsort(const Vec2crd& v1, const Vec2crd& v2)
|
||||
{
|
||||
return v1.y() == v2.y() ? v1.x() < v2.x() : v1.y() < v2.y();
|
||||
}
|
||||
|
||||
ExPolygons ifp_convex(const arr2::RectangleBed &obed, const Polygon &convexpoly)
|
||||
{
|
||||
ExPolygon ret;
|
||||
|
||||
auto sbox = bounding_box(convexpoly);
|
||||
auto sboxsize = sbox.size();
|
||||
coord_t sheight = sboxsize.y();
|
||||
coord_t swidth = sboxsize.x();
|
||||
Point sliding_top = reference_vertex(convexpoly);
|
||||
auto leftOffset = sliding_top.x() - sbox.min.x();
|
||||
auto rightOffset = sliding_top.x() - sbox.max.x();
|
||||
coord_t topOffset = 0;
|
||||
auto bottomOffset = sheight;
|
||||
|
||||
auto bedbb = obed.bb;
|
||||
// bedbb.offset(1);
|
||||
auto bedsz = bedbb.size();
|
||||
auto boxWidth = bedsz.x();
|
||||
auto boxHeight = bedsz.y();
|
||||
|
||||
auto bedMinx = bedbb.min.x();
|
||||
auto bedMiny = bedbb.min.y();
|
||||
auto bedMaxx = bedbb.max.x();
|
||||
auto bedMaxy = bedbb.max.y();
|
||||
|
||||
Polygon innerNfp{ Point{bedMinx + leftOffset, bedMaxy + topOffset},
|
||||
Point{bedMaxx + rightOffset, bedMaxy + topOffset},
|
||||
Point{bedMaxx + rightOffset, bedMiny + bottomOffset},
|
||||
Point{bedMinx + leftOffset, bedMiny + bottomOffset},
|
||||
Point{bedMinx + leftOffset, bedMaxy + topOffset} };
|
||||
|
||||
if (sheight <= boxHeight && swidth <= boxWidth)
|
||||
ret.contour = std::move(innerNfp);
|
||||
|
||||
return {ret};
|
||||
}
|
||||
|
||||
Polygon ifp_convex_convex(const Polygon &fixed, const Polygon &movable)
|
||||
{
|
||||
auto subnfps = reserve_polygons(fixed.size());
|
||||
|
||||
// For each edge of the bed polygon, determine the nfp of convexpoly and
|
||||
// the zero area polygon formed by the edge. The union of all these sub-nfps
|
||||
// will contain a hole that is the actual ifp.
|
||||
auto lrange = line_range(fixed);
|
||||
for (const Line l : lrange) { // Older mac compilers generate warnging if line_range is called in-place
|
||||
Polygon fixed = {l.a, l.b};
|
||||
subnfps.emplace_back(nfp_convex_convex_legacy(fixed, movable));
|
||||
}
|
||||
|
||||
// Do the union and then keep only the holes (should be only one or zero, if
|
||||
// the convexpoly cannot fit into the bed)
|
||||
Polygons ifp = union_(subnfps);
|
||||
Polygon ret;
|
||||
|
||||
// find the first hole
|
||||
auto it = std::find_if(ifp.begin(), ifp.end(), [](const Polygon &subifp){
|
||||
return subifp.is_clockwise();
|
||||
});
|
||||
|
||||
if (it != ifp.end()) {
|
||||
ret = std::move(*it);
|
||||
std::reverse(ret.begin(), ret.end());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
ExPolygons ifp_convex(const arr2::CircleBed &bed, const Polygon &convexpoly)
|
||||
{
|
||||
Polygon circle = approximate_circle_with_polygon(bed);
|
||||
|
||||
return {ExPolygon{ifp_convex_convex(circle, convexpoly)}};
|
||||
}
|
||||
|
||||
ExPolygons ifp_convex(const arr2::IrregularBed &bed, const Polygon &convexpoly)
|
||||
{
|
||||
auto bb = get_extents(bed.poly);
|
||||
bb.offset(scaled(1.));
|
||||
|
||||
Polygon rect = arr2::to_rectangle(bb);
|
||||
|
||||
ExPolygons blueprint = diff_ex(rect, bed.poly);
|
||||
Polygons ifp;
|
||||
for (const ExPolygon &part : blueprint) {
|
||||
Polygons triangles = Slic3r::convex_decomposition_tess(part);
|
||||
for (const Polygon &tr : triangles) {
|
||||
Polygon subifp = nfp_convex_convex_legacy(tr, convexpoly);
|
||||
ifp.emplace_back(std::move(subifp));
|
||||
}
|
||||
}
|
||||
|
||||
ifp = union_(ifp);
|
||||
|
||||
Polygons ret;
|
||||
|
||||
std::copy_if(ifp.begin(), ifp.end(), std::back_inserter(ret),
|
||||
[](const Polygon &p) { return p.is_clockwise(); });
|
||||
|
||||
for (Polygon &p : ret)
|
||||
std::reverse(p.begin(), p.end());
|
||||
|
||||
return to_expolygons(ret);
|
||||
}
|
||||
|
||||
Vec2crd reference_vertex(const Polygon &poly)
|
||||
{
|
||||
Vec2crd ret{std::numeric_limits<coord_t>::min(),
|
||||
std::numeric_limits<coord_t>::min()};
|
||||
|
||||
auto it = std::max_element(poly.points.begin(), poly.points.end(), vsort);
|
||||
if (it != poly.points.end())
|
||||
ret = std::max(ret, static_cast<const Vec2crd &>(*it), vsort);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Vec2crd reference_vertex(const ExPolygon &expoly)
|
||||
{
|
||||
return reference_vertex(expoly.contour);
|
||||
}
|
||||
|
||||
Vec2crd reference_vertex(const Polygons &outline)
|
||||
{
|
||||
Vec2crd ret{std::numeric_limits<coord_t>::min(),
|
||||
std::numeric_limits<coord_t>::min()};
|
||||
|
||||
for (const Polygon &poly : outline)
|
||||
ret = std::max(ret, reference_vertex(poly), vsort);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Vec2crd reference_vertex(const ExPolygons &outline)
|
||||
{
|
||||
Vec2crd ret{std::numeric_limits<coord_t>::min(),
|
||||
std::numeric_limits<coord_t>::min()};
|
||||
|
||||
for (const ExPolygon &expoly : outline)
|
||||
ret = std::max(ret, reference_vertex(expoly), vsort);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Vec2crd min_vertex(const Polygon &poly)
|
||||
{
|
||||
Vec2crd ret{std::numeric_limits<coord_t>::max(),
|
||||
std::numeric_limits<coord_t>::max()};
|
||||
|
||||
auto it = std::min_element(poly.points.begin(), poly.points.end(), vsort);
|
||||
if (it != poly.points.end())
|
||||
ret = std::min(ret, static_cast<const Vec2crd&>(*it), vsort);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Find the vertex corresponding to the edge with minimum angle to X axis.
|
||||
// Only usable with CircularEdgeIterator<> template.
|
||||
template<class It> It find_min_anglex_edge(It from)
|
||||
{
|
||||
bool found = false;
|
||||
auto it = from;
|
||||
while (!found ) {
|
||||
found = !line_cmp(*it, *std::next(it));
|
||||
++it;
|
||||
}
|
||||
|
||||
return it;
|
||||
}
|
||||
|
||||
// Only usable if both fixed and movable polygon is convex. In that case,
|
||||
// their edges are already sorted by angle to X axis, only the starting
|
||||
// (lowest X axis) edge needs to be found first.
|
||||
void nfp_convex_convex(const Polygon &fixed, const Polygon &movable, Polygon &poly)
|
||||
{
|
||||
if (fixed.empty() || movable.empty())
|
||||
return;
|
||||
|
||||
// Clear poly and adjust its capacity. Nothing happens if poly is
|
||||
// already sufficiently large and and empty.
|
||||
poly.clear();
|
||||
poly.points.reserve(fixed.size() + movable.size());
|
||||
|
||||
// Find starting positions on the fixed and moving polygons
|
||||
auto it_fx = find_min_anglex_edge(CircularEdgeIterator{fixed});
|
||||
auto it_mv = find_min_anglex_edge(CircularReverseEdgeIterator{movable});
|
||||
|
||||
// End positions are at the same vertex after completing one full circle
|
||||
auto end_fx = it_fx + fixed.size();
|
||||
auto end_mv = it_mv + movable.size();
|
||||
|
||||
// Pos zero is just fine as starting point:
|
||||
poly.points.emplace_back(0, 0);
|
||||
|
||||
// Output iterator adapter for std::merge
|
||||
struct OutItAdaptor {
|
||||
using value_type [[maybe_unused]] = Line;
|
||||
using difference_type [[maybe_unused]] = std::ptrdiff_t;
|
||||
using pointer [[maybe_unused]] = Line*;
|
||||
using reference [[maybe_unused]] = Line& ;
|
||||
using iterator_category [[maybe_unused]] = std::output_iterator_tag;
|
||||
|
||||
Polygon *outpoly;
|
||||
OutItAdaptor(Polygon &out) : outpoly{&out} {}
|
||||
|
||||
OutItAdaptor &operator *() { return *this; }
|
||||
void operator=(const Line &l)
|
||||
{
|
||||
// Yielding l.b, offsetted so that l.a touches the last vertex in
|
||||
// in outpoly
|
||||
outpoly->points.emplace_back(l.b + outpoly->back() - l.a);
|
||||
}
|
||||
|
||||
OutItAdaptor& operator++() { return *this; };
|
||||
};
|
||||
|
||||
// Use std algo to merge the edges from the two polygons
|
||||
std::merge(it_fx, end_fx, it_mv, end_mv, OutItAdaptor{poly}, line_cmp);
|
||||
}
|
||||
|
||||
Polygon nfp_convex_convex(const Polygon &fixed, const Polygon &movable)
|
||||
{
|
||||
Polygon ret;
|
||||
nfp_convex_convex(fixed, movable, ret);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void buildPolygon(const std::vector<Line>& edgelist,
|
||||
Polygon& rpoly,
|
||||
Point& top_nfp)
|
||||
{
|
||||
auto& rsh = rpoly.points;
|
||||
|
||||
rsh.reserve(2 * edgelist.size());
|
||||
|
||||
// Add the two vertices from the first edge into the final polygon.
|
||||
rsh.emplace_back(edgelist.front().a);
|
||||
rsh.emplace_back(edgelist.front().b);
|
||||
|
||||
// Sorting function for the nfp reference vertex search
|
||||
|
||||
// the reference (rightmost top) vertex so far
|
||||
top_nfp = *std::max_element(std::cbegin(rsh), std::cend(rsh), vsort);
|
||||
|
||||
auto tmp = std::next(std::begin(rsh));
|
||||
|
||||
// Construct final nfp by placing each edge to the end of the previous
|
||||
for(auto eit = std::next(edgelist.begin()); eit != edgelist.end(); ++eit) {
|
||||
auto d = *tmp - eit->a;
|
||||
Vec2crd p = eit->b + d;
|
||||
|
||||
rsh.emplace_back(p);
|
||||
|
||||
// Set the new reference vertex
|
||||
if (vsort(top_nfp, p))
|
||||
top_nfp = p;
|
||||
|
||||
tmp = std::next(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
Polygon nfp_convex_convex_legacy(const Polygon &fixed, const Polygon &movable)
|
||||
{
|
||||
assert (!fixed.empty());
|
||||
assert (!movable.empty());
|
||||
|
||||
Polygon rsh; // Final nfp placeholder
|
||||
Point max_nfp;
|
||||
std::vector<Line> edgelist;
|
||||
|
||||
auto cap = fixed.points.size() + movable.points.size();
|
||||
|
||||
// Reserve the needed memory
|
||||
edgelist.reserve(cap);
|
||||
rsh.points.reserve(cap);
|
||||
|
||||
auto add_edge = [&edgelist](const Point &v1, const Point &v2) {
|
||||
Line e{v1, v2};
|
||||
if ((e.b - e.a).cast<int64_t>().squaredNorm() > 0)
|
||||
edgelist.emplace_back(e);
|
||||
};
|
||||
|
||||
Point max_fixed = fixed.points.front();
|
||||
{ // place all edges from fixed into edgelist
|
||||
auto first = std::cbegin(fixed);
|
||||
auto next = std::next(first);
|
||||
|
||||
while(next != std::cend(fixed)) {
|
||||
add_edge(*(first), *(next));
|
||||
max_fixed = std::max(max_fixed, *first, vsort);
|
||||
|
||||
++first; ++next;
|
||||
}
|
||||
|
||||
add_edge(*std::crbegin(fixed), *std::cbegin(fixed));
|
||||
max_fixed = std::max(max_fixed, *std::crbegin(fixed), vsort);
|
||||
}
|
||||
|
||||
Point max_movable = movable.points.front();
|
||||
Point min_movable = movable.points.front();
|
||||
{ // place all edges from movable into edgelist
|
||||
auto first = std::cbegin(movable);
|
||||
auto next = std::next(first);
|
||||
|
||||
while(next != std::cend(movable)) {
|
||||
add_edge(*(next), *(first));
|
||||
min_movable = std::min(min_movable, *first, vsort);
|
||||
max_movable = std::max(max_movable, *first, vsort);
|
||||
|
||||
++first; ++next;
|
||||
}
|
||||
|
||||
add_edge(*std::cbegin(movable), *std::crbegin(movable));
|
||||
min_movable = std::min(min_movable, *std::crbegin(movable), vsort);
|
||||
max_movable = std::max(max_movable, *std::crbegin(movable), vsort);
|
||||
}
|
||||
|
||||
std::sort(edgelist.begin(), edgelist.end(), line_cmp);
|
||||
|
||||
buildPolygon(edgelist, rsh, max_nfp);
|
||||
|
||||
auto dtouch = max_fixed - min_movable;
|
||||
auto top_other = max_movable + dtouch;
|
||||
auto dnfp = top_other - max_nfp;
|
||||
rsh.translate(dnfp);
|
||||
|
||||
return rsh;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // NFP_CPP
|
||||
@@ -0,0 +1,54 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef NFP_HPP
|
||||
#define NFP_HPP
|
||||
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
#include <libslic3r/Arrange/Core/Beds.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
template<class Unit = int64_t, class T>
|
||||
Unit dotperp(const Vec<2, T> &a, const Vec<2, T> &b)
|
||||
{
|
||||
return Unit(a.x()) * Unit(b.y()) - Unit(a.y()) * Unit(b.x());
|
||||
}
|
||||
|
||||
// Convex-Convex nfp in linear time (fixed.size() + movable.size()),
|
||||
// no memory allocations (if out param is used).
|
||||
// FIXME: Currently broken for very sharp triangles.
|
||||
Polygon nfp_convex_convex(const Polygon &fixed, const Polygon &movable);
|
||||
void nfp_convex_convex(const Polygon &fixed, const Polygon &movable, Polygon &out);
|
||||
Polygon nfp_convex_convex_legacy(const Polygon &fixed, const Polygon &movable);
|
||||
|
||||
Polygon ifp_convex_convex(const Polygon &fixed, const Polygon &movable);
|
||||
|
||||
ExPolygons ifp_convex(const arr2::RectangleBed &bed, const Polygon &convexpoly);
|
||||
ExPolygons ifp_convex(const arr2::CircleBed &bed, const Polygon &convexpoly);
|
||||
ExPolygons ifp_convex(const arr2::IrregularBed &bed, const Polygon &convexpoly);
|
||||
inline ExPolygons ifp_convex(const arr2::InfiniteBed &bed, const Polygon &convexpoly)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
inline ExPolygons ifp_convex(const arr2::ArrangeBed &bed, const Polygon &convexpoly)
|
||||
{
|
||||
ExPolygons ret;
|
||||
auto visitor = [&ret, &convexpoly](const auto &b) { ret = ifp_convex(b, convexpoly); };
|
||||
boost::apply_visitor(visitor, bed);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Vec2crd reference_vertex(const Polygon &outline);
|
||||
Vec2crd reference_vertex(const ExPolygon &outline);
|
||||
Vec2crd reference_vertex(const Polygons &outline);
|
||||
Vec2crd reference_vertex(const ExPolygons &outline);
|
||||
|
||||
Vec2crd min_vertex(const Polygon &outline);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // NFP_HPP
|
||||
@@ -0,0 +1,200 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef NFPARRANGEITEMTRAITS_HPP
|
||||
#define NFPARRANGEITEMTRAITS_HPP
|
||||
|
||||
#include <numeric>
|
||||
|
||||
#include "libslic3r/Arrange/Core/ArrangeBase.hpp"
|
||||
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// Additional methods that an ArrangeItem object has to implement in order
|
||||
// to be usable with PackStrategyNFP.
|
||||
template<class ArrItem, class En = void> struct NFPArrangeItemTraits_
|
||||
{
|
||||
template<class Context, class Bed, class StopCond = DefaultStopCondition>
|
||||
static ExPolygons calculate_nfp(const ArrItem &item,
|
||||
const Context &packing_context,
|
||||
const Bed &bed,
|
||||
StopCond stop_condition = {})
|
||||
{
|
||||
static_assert(always_false<ArrItem>::value,
|
||||
"NFP unimplemented for this item type.");
|
||||
return {};
|
||||
}
|
||||
|
||||
static Vec2crd reference_vertex(const ArrItem &item)
|
||||
{
|
||||
return item.reference_vertex();
|
||||
}
|
||||
|
||||
static BoundingBox envelope_bounding_box(const ArrItem &itm)
|
||||
{
|
||||
return itm.envelope_bounding_box();
|
||||
}
|
||||
|
||||
static BoundingBox fixed_bounding_box(const ArrItem &itm)
|
||||
{
|
||||
return itm.fixed_bounding_box();
|
||||
}
|
||||
|
||||
static const Polygons & envelope_outline(const ArrItem &itm)
|
||||
{
|
||||
return itm.envelope_outline();
|
||||
}
|
||||
|
||||
static const Polygons & fixed_outline(const ArrItem &itm)
|
||||
{
|
||||
return itm.fixed_outline();
|
||||
}
|
||||
|
||||
static const Polygon & envelope_convex_hull(const ArrItem &itm)
|
||||
{
|
||||
return itm.envelope_convex_hull();
|
||||
}
|
||||
|
||||
static const Polygon & fixed_convex_hull(const ArrItem &itm)
|
||||
{
|
||||
return itm.fixed_convex_hull();
|
||||
}
|
||||
|
||||
static double envelope_area(const ArrItem &itm)
|
||||
{
|
||||
return itm.envelope_area();
|
||||
}
|
||||
|
||||
static double fixed_area(const ArrItem &itm)
|
||||
{
|
||||
return itm.fixed_area();
|
||||
}
|
||||
|
||||
static auto allowed_rotations(const ArrItem &)
|
||||
{
|
||||
return std::array{0.};
|
||||
}
|
||||
|
||||
static Vec2crd fixed_centroid(const ArrItem &itm)
|
||||
{
|
||||
return fixed_bounding_box(itm).center();
|
||||
}
|
||||
|
||||
static Vec2crd envelope_centroid(const ArrItem &itm)
|
||||
{
|
||||
return envelope_bounding_box(itm).center();
|
||||
}
|
||||
};
|
||||
|
||||
template<class T>
|
||||
using NFPArrangeItemTraits = NFPArrangeItemTraits_<StripCVRef<T>>;
|
||||
|
||||
template<class ArrItem,
|
||||
class Context,
|
||||
class Bed,
|
||||
class StopCond = DefaultStopCondition>
|
||||
ExPolygons calculate_nfp(const ArrItem &itm,
|
||||
const Context &context,
|
||||
const Bed &bed,
|
||||
StopCond stopcond = {})
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::calculate_nfp(itm, context, bed,
|
||||
std::move(stopcond));
|
||||
}
|
||||
|
||||
template<class ArrItem> Vec2crd reference_vertex(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::reference_vertex(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem> BoundingBox envelope_bounding_box(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::envelope_bounding_box(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem> BoundingBox fixed_bounding_box(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::fixed_bounding_box(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem> decltype(auto) envelope_convex_hull(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::envelope_convex_hull(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem> decltype(auto) fixed_convex_hull(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::fixed_convex_hull(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem> decltype(auto) envelope_outline(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::envelope_outline(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem> decltype(auto) fixed_outline(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::fixed_outline(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem> double envelope_area(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::envelope_area(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem> double fixed_area(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::fixed_area(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem> Vec2crd fixed_centroid(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::fixed_centroid(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem> Vec2crd envelope_centroid(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::envelope_centroid(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
auto allowed_rotations(const ArrItem &itm)
|
||||
{
|
||||
return NFPArrangeItemTraits<ArrItem>::allowed_rotations(itm);
|
||||
}
|
||||
|
||||
template<class It>
|
||||
BoundingBox bounding_box(const Range<It> &itms) noexcept
|
||||
{
|
||||
auto pilebb =
|
||||
std::accumulate(itms.begin(), itms.end(), BoundingBox{},
|
||||
[](BoundingBox bb, const auto &itm) {
|
||||
bb.merge(fixed_bounding_box(itm));
|
||||
return bb;
|
||||
});
|
||||
|
||||
return pilebb;
|
||||
}
|
||||
|
||||
template<class It>
|
||||
BoundingBox bounding_box_on_bedidx(const Range<It> &itms, int bed_index) noexcept
|
||||
{
|
||||
auto pilebb =
|
||||
std::accumulate(itms.begin(), itms.end(), BoundingBox{},
|
||||
[bed_index](BoundingBox bb, const auto &itm) {
|
||||
if (bed_index == get_bed_index(itm))
|
||||
bb.merge(fixed_bounding_box(itm));
|
||||
|
||||
return bb;
|
||||
});
|
||||
|
||||
return pilebb;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // ARRANGEITEMTRAITSNFP_HPP
|
||||
@@ -0,0 +1,115 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "NFP.hpp"
|
||||
#include "NFPConcave_CGAL.hpp"
|
||||
|
||||
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
|
||||
#include <CGAL/partition_2.h>
|
||||
#include <CGAL/Partition_traits_2.h>
|
||||
#include <CGAL/property_map.h>
|
||||
#include <CGAL/Polygon_vertical_decomposition_2.h>
|
||||
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
using K = CGAL::Exact_predicates_inexact_constructions_kernel;
|
||||
using Partition_traits_2 = CGAL::Partition_traits_2<K, CGAL::Pointer_property_map<K::Point_2>::type >;
|
||||
using Point_2 = Partition_traits_2::Point_2;
|
||||
using Polygon_2 = Partition_traits_2::Polygon_2; // a polygon of indices
|
||||
|
||||
ExPolygons nfp_concave_concave_cgal(const ExPolygon &fixed, const ExPolygon &movable)
|
||||
{
|
||||
Polygons fixed_decomp = convex_decomposition_cgal(fixed);
|
||||
Polygons movable_decomp = convex_decomposition_cgal(movable);
|
||||
|
||||
auto refs_mv = reserve_vector<Vec2crd>(movable_decomp.size());
|
||||
|
||||
for (const Polygon &p : movable_decomp)
|
||||
refs_mv.emplace_back(reference_vertex(p));
|
||||
|
||||
auto nfps = reserve_polygons(fixed_decomp.size() *movable_decomp.size());
|
||||
|
||||
Vec2crd ref_whole = reference_vertex(movable);
|
||||
for (const Polygon &fixed_part : fixed_decomp) {
|
||||
size_t mvi = 0;
|
||||
for (const Polygon &movable_part : movable_decomp) {
|
||||
Polygon subnfp = nfp_convex_convex(fixed_part, movable_part);
|
||||
const Vec2crd &ref_mp = refs_mv[mvi];
|
||||
auto d = ref_whole - ref_mp;
|
||||
subnfp.translate(d);
|
||||
nfps.emplace_back(subnfp);
|
||||
mvi++;
|
||||
}
|
||||
}
|
||||
|
||||
return union_ex(nfps);
|
||||
}
|
||||
|
||||
// TODO: holes
|
||||
Polygons convex_decomposition_cgal(const ExPolygon &expoly)
|
||||
{
|
||||
CGAL::Polygon_vertical_decomposition_2<K> decomp;
|
||||
|
||||
CGAL::Polygon_2<K> contour;
|
||||
for (auto &p : expoly.contour.points)
|
||||
contour.push_back({unscaled(p.x()), unscaled(p.y())});
|
||||
|
||||
CGAL::Polygon_with_holes_2<K> cgalpoly{contour};
|
||||
for (const Polygon &h : expoly.holes) {
|
||||
CGAL::Polygon_2<K> hole;
|
||||
for (auto &p : h.points)
|
||||
hole.push_back({unscaled(p.x()), unscaled(p.y())});
|
||||
|
||||
cgalpoly.add_hole(hole);
|
||||
}
|
||||
|
||||
std::vector<CGAL::Polygon_2<K>> out;
|
||||
decomp(cgalpoly, std::back_inserter(out));
|
||||
|
||||
Polygons ret;
|
||||
for (auto &pwh : out) {
|
||||
Polygon poly;
|
||||
for (auto &p : pwh)
|
||||
poly.points.emplace_back(scaled(p.x()), scaled(p.y()));
|
||||
ret.emplace_back(std::move(poly));
|
||||
}
|
||||
|
||||
return ret; //convex_decomposition_cgal(expoly.contour);
|
||||
}
|
||||
|
||||
Polygons convex_decomposition_cgal(const Polygon &poly)
|
||||
{
|
||||
auto pts = reserve_vector<K::Point_2>(poly.size());
|
||||
|
||||
for (const Point &p : poly.points)
|
||||
pts.emplace_back(unscaled(p.x()), unscaled(p.y()));
|
||||
|
||||
Partition_traits_2 traits(CGAL::make_property_map(pts));
|
||||
|
||||
Polygon_2 polyidx;
|
||||
for (size_t i = 0; i < pts.size(); ++i)
|
||||
polyidx.push_back(i);
|
||||
|
||||
std::vector<Polygon_2> outp;
|
||||
|
||||
CGAL::optimal_convex_partition_2(polyidx.vertices_begin(),
|
||||
polyidx.vertices_end(),
|
||||
std::back_inserter(outp),
|
||||
traits);
|
||||
|
||||
Polygons ret;
|
||||
for (const Polygon_2& poly : outp){
|
||||
Polygon r;
|
||||
for(Point_2 p : poly.container())
|
||||
r.points.emplace_back(scaled(pts[p].x()), scaled(pts[p].y()));
|
||||
|
||||
ret.emplace_back(std::move(r));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,18 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef NFPCONCAVE_CGAL_HPP
|
||||
#define NFPCONCAVE_CGAL_HPP
|
||||
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
Polygons convex_decomposition_cgal(const Polygon &expoly);
|
||||
Polygons convex_decomposition_cgal(const ExPolygon &expoly);
|
||||
ExPolygons nfp_concave_concave_cgal(const ExPolygon &fixed, const ExPolygon &movable);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // NFPCONCAVE_CGAL_HPP
|
||||
@@ -0,0 +1,74 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "NFPConcave_Tesselate.hpp"
|
||||
|
||||
#include <libslic3r/ClipperUtils.hpp>
|
||||
#include <libslic3r/Tesselate.hpp>
|
||||
|
||||
#include "NFP.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
Polygons convex_decomposition_tess(const Polygon &expoly)
|
||||
{
|
||||
return convex_decomposition_tess(ExPolygon{expoly});
|
||||
}
|
||||
|
||||
Polygons convex_decomposition_tess(const ExPolygon &expoly)
|
||||
{
|
||||
std::vector<Vec2d> tr = Slic3r::triangulate_expolygon_2d(expoly);
|
||||
|
||||
auto ret = Slic3r::reserve_polygons(tr.size() / 3);
|
||||
for (size_t i = 0; i < tr.size(); i += 3) {
|
||||
ret.emplace_back(
|
||||
Polygon{scaled(tr[i]), scaled(tr[i + 1]), scaled(tr[i + 2])});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Polygons convex_decomposition_tess(const ExPolygons &expolys)
|
||||
{
|
||||
constexpr size_t AvgTriangleCountGuess = 50;
|
||||
|
||||
auto ret = reserve_polygons(AvgTriangleCountGuess * expolys.size());
|
||||
for (const ExPolygon &expoly : expolys) {
|
||||
Polygons convparts = convex_decomposition_tess(expoly);
|
||||
std::move(convparts.begin(), convparts.end(), std::back_inserter(ret));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
ExPolygons nfp_concave_concave_tess(const ExPolygon &fixed,
|
||||
const ExPolygon &movable)
|
||||
{
|
||||
Polygons fixed_decomp = convex_decomposition_tess(fixed);
|
||||
Polygons movable_decomp = convex_decomposition_tess(movable);
|
||||
|
||||
auto refs_mv = reserve_vector<Vec2crd>(movable_decomp.size());
|
||||
|
||||
for (const Polygon &p : movable_decomp)
|
||||
refs_mv.emplace_back(reference_vertex(p));
|
||||
|
||||
auto nfps = reserve_polygons(fixed_decomp.size() * movable_decomp.size());
|
||||
|
||||
Vec2crd ref_whole = reference_vertex(movable);
|
||||
for (const Polygon &fixed_part : fixed_decomp) {
|
||||
size_t mvi = 0;
|
||||
for (const Polygon &movable_part : movable_decomp) {
|
||||
Polygon subnfp = nfp_convex_convex(fixed_part, movable_part);
|
||||
const Vec2crd &ref_mp = refs_mv[mvi];
|
||||
auto d = ref_whole - ref_mp;
|
||||
subnfp.translate(d);
|
||||
nfps.emplace_back(subnfp);
|
||||
mvi++;
|
||||
}
|
||||
}
|
||||
|
||||
return union_ex(nfps);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,19 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef NFPCONCAVE_TESSELATE_HPP
|
||||
#define NFPCONCAVE_TESSELATE_HPP
|
||||
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
Polygons convex_decomposition_tess(const Polygon &expoly);
|
||||
Polygons convex_decomposition_tess(const ExPolygon &expoly);
|
||||
Polygons convex_decomposition_tess(const ExPolygons &expolys);
|
||||
ExPolygons nfp_concave_concave_tess(const ExPolygon &fixed, const ExPolygon &movable);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // NFPCONCAVE_TESSELATE_HPP
|
||||
@@ -0,0 +1,289 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef PACKSTRATEGYNFP_HPP
|
||||
#define PACKSTRATEGYNFP_HPP
|
||||
|
||||
#include "libslic3r/Arrange/Core/ArrangeBase.hpp"
|
||||
|
||||
#include "EdgeCache.hpp"
|
||||
#include "Kernels/KernelTraits.hpp"
|
||||
|
||||
#include "NFPArrangeItemTraits.hpp"
|
||||
|
||||
#include "libslic3r/Optimize/NLoptOptimizer.hpp"
|
||||
#include "libslic3r/Execution/ExecutionSeq.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
struct NFPPackingTag{};
|
||||
|
||||
struct DummyArrangeKernel
|
||||
{
|
||||
template<class ArrItem>
|
||||
double placement_fitness(const ArrItem &itm, const Vec2crd &dest_pos) const
|
||||
{
|
||||
return NaNd;
|
||||
}
|
||||
|
||||
template<class ArrItem, class Bed, class Context, class RemIt>
|
||||
bool on_start_packing(ArrItem &itm,
|
||||
const Bed &bed,
|
||||
const Context &packing_context,
|
||||
const Range<RemIt> &remaining_items)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
template<class ArrItem> bool on_item_packed(ArrItem &itm) { return true; }
|
||||
};
|
||||
|
||||
template<class Strategy> using OptAlg = typename Strategy::OptAlg;
|
||||
|
||||
template<class ArrangeKernel = DummyArrangeKernel,
|
||||
class ExecPolicy = ExecutionSeq,
|
||||
class OptMethod = opt::AlgNLoptSubplex,
|
||||
class StopCond = DefaultStopCondition>
|
||||
struct PackStrategyNFP {
|
||||
using OptAlg = OptMethod;
|
||||
|
||||
ArrangeKernel kernel;
|
||||
ExecPolicy ep;
|
||||
double accuracy = 1.;
|
||||
opt::Optimizer<OptMethod> solver;
|
||||
StopCond stop_condition;
|
||||
|
||||
PackStrategyNFP(opt::Optimizer<OptMethod> slv,
|
||||
ArrangeKernel k = {},
|
||||
ExecPolicy execpolicy = {},
|
||||
double accur = 1.,
|
||||
StopCond stop_cond = {})
|
||||
: kernel{std::move(k)},
|
||||
ep{std::move(execpolicy)},
|
||||
accuracy{accur},
|
||||
solver{std::move(slv)},
|
||||
stop_condition{std::move(stop_cond)}
|
||||
{}
|
||||
|
||||
PackStrategyNFP(ArrangeKernel k = {},
|
||||
ExecPolicy execpolicy = {},
|
||||
double accur = 1.,
|
||||
StopCond stop_cond = {})
|
||||
: PackStrategyNFP{opt::Optimizer<OptMethod>{}, std::move(k),
|
||||
std::move(execpolicy), accur, std::move(stop_cond)}
|
||||
{
|
||||
// Defaults for AlgNLoptSubplex
|
||||
auto iters = static_cast<unsigned>(std::floor(1000 * accuracy));
|
||||
auto optparams =
|
||||
opt::StopCriteria{}.max_iterations(iters).rel_score_diff(
|
||||
1e-20) /*.abs_score_diff(1e-20)*/;
|
||||
|
||||
solver.set_criteria(optparams);
|
||||
}
|
||||
};
|
||||
|
||||
template<class...Args>
|
||||
struct PackStrategyTag_<PackStrategyNFP<Args...>>
|
||||
{
|
||||
using Tag = NFPPackingTag;
|
||||
};
|
||||
|
||||
|
||||
template<class ArrItem, class Bed, class PStrategy>
|
||||
double pick_best_spot_on_nfp_verts_only(ArrItem &item,
|
||||
const ExPolygons &nfp,
|
||||
const Bed &bed,
|
||||
const PStrategy &strategy)
|
||||
{
|
||||
using KernelT = KernelTraits<decltype(strategy.kernel)>;
|
||||
|
||||
auto score = -std::numeric_limits<double>::infinity();
|
||||
Vec2crd orig_tr = get_translation(item);
|
||||
Vec2crd translation{0, 0};
|
||||
|
||||
auto eval_fitness = [&score, &strategy, &item, &translation,
|
||||
&orig_tr](const Vec2crd &p) {
|
||||
set_translation(item, orig_tr);
|
||||
Vec2crd ref_v = reference_vertex(item);
|
||||
Vec2crd tr = p - ref_v;
|
||||
double fitness = KernelT::placement_fitness(strategy.kernel, item, tr);
|
||||
if (fitness > score) {
|
||||
score = fitness;
|
||||
translation = tr;
|
||||
}
|
||||
};
|
||||
|
||||
for (const ExPolygon &expoly : nfp) {
|
||||
for (const Point &p : expoly.contour) {
|
||||
eval_fitness(p);
|
||||
}
|
||||
|
||||
for (const Polygon &h : expoly.holes)
|
||||
for (const Point &p : h.points)
|
||||
eval_fitness(p);
|
||||
}
|
||||
|
||||
set_translation(item, orig_tr + translation);
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
struct CornerResult
|
||||
{
|
||||
size_t contour_id;
|
||||
opt::Result<1> oresult;
|
||||
};
|
||||
|
||||
template<class ArrItem, class Bed, class... Args>
|
||||
double pick_best_spot_on_nfp(ArrItem &item,
|
||||
const ExPolygons &nfp,
|
||||
const Bed &bed,
|
||||
const PackStrategyNFP<Args...> &strategy)
|
||||
{
|
||||
auto &ex_policy = strategy.ep;
|
||||
using KernelT = KernelTraits<decltype(strategy.kernel)>;
|
||||
|
||||
auto score = -std::numeric_limits<double>::infinity();
|
||||
Vec2crd orig_tr = get_translation(item);
|
||||
Vec2crd translation{0, 0};
|
||||
Vec2crd ref_v = reference_vertex(item);
|
||||
|
||||
auto edge_caches = reserve_vector<EdgeCache>(nfp.size());
|
||||
auto sample_sets = reserve_vector<std::vector<ContourLocation>>(
|
||||
nfp.size());
|
||||
|
||||
for (const ExPolygon &expoly : nfp) {
|
||||
edge_caches.emplace_back(EdgeCache{&expoly});
|
||||
edge_caches.back().sample_contour(strategy.accuracy,
|
||||
sample_sets.emplace_back());
|
||||
}
|
||||
|
||||
auto nthreads = execution::max_concurrency(ex_policy);
|
||||
|
||||
std::vector<CornerResult> gresults(edge_caches.size());
|
||||
|
||||
auto resultcmp = [](auto &a, auto &b) {
|
||||
return a.oresult.score < b.oresult.score;
|
||||
};
|
||||
|
||||
execution::for_each(
|
||||
ex_policy, size_t(0), edge_caches.size(),
|
||||
[&](size_t edge_cache_idx) {
|
||||
auto &ec_contour = edge_caches[edge_cache_idx];
|
||||
auto &corners = sample_sets[edge_cache_idx];
|
||||
std::vector<CornerResult> results(corners.size());
|
||||
|
||||
auto cornerfn = [&](size_t i) {
|
||||
ContourLocation cr = corners[i];
|
||||
auto objfn = [&](opt::Input<1> &in) {
|
||||
Vec2crd p = ec_contour.coords(ContourLocation{cr.contour_id, in[0]});
|
||||
Vec2crd tr = p - ref_v;
|
||||
|
||||
return KernelT::placement_fitness(strategy.kernel, item, tr);
|
||||
};
|
||||
|
||||
// Assuming that solver is a lightweight object
|
||||
auto solver = strategy.solver;
|
||||
solver.to_max();
|
||||
auto oresult = solver.optimize(objfn,
|
||||
opt::initvals({cr.dist}),
|
||||
opt::bounds({{0., 1.}}));
|
||||
|
||||
results[i] = CornerResult{cr.contour_id, oresult};
|
||||
};
|
||||
|
||||
execution::for_each(ex_policy, size_t(0), results.size(),
|
||||
cornerfn, nthreads);
|
||||
|
||||
auto it = std::max_element(results.begin(), results.end(),
|
||||
resultcmp);
|
||||
|
||||
if (it != results.end())
|
||||
gresults[edge_cache_idx] = *it;
|
||||
},
|
||||
nthreads);
|
||||
|
||||
auto it = std::max_element(gresults.begin(), gresults.end(), resultcmp);
|
||||
if (it != gresults.end()) {
|
||||
score = it->oresult.score;
|
||||
size_t path_id = std::distance(gresults.begin(), it);
|
||||
size_t contour_id = it->contour_id;
|
||||
double dist = it->oresult.optimum[0];
|
||||
|
||||
Vec2crd pos = edge_caches[path_id].coords(ContourLocation{contour_id, dist});
|
||||
Vec2crd tr = pos - ref_v;
|
||||
|
||||
set_translation(item, orig_tr + tr);
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
template<class Strategy, class ArrItem, class Bed, class RemIt>
|
||||
bool pack(Strategy &strategy,
|
||||
const Bed &bed,
|
||||
ArrItem &item,
|
||||
const PackStrategyContext<Strategy, ArrItem> &packing_context,
|
||||
const Range<RemIt> &remaining_items,
|
||||
const NFPPackingTag &)
|
||||
{
|
||||
using KernelT = KernelTraits<decltype(strategy.kernel)>;
|
||||
|
||||
// The kernel might pack the item immediately
|
||||
bool packed = KernelT::on_start_packing(strategy.kernel, item, bed,
|
||||
packing_context, remaining_items);
|
||||
|
||||
double orig_rot = get_rotation(item);
|
||||
double final_rot = 0.;
|
||||
double final_score = -std::numeric_limits<double>::infinity();
|
||||
Vec2crd orig_tr = get_translation(item);
|
||||
Vec2crd final_tr = orig_tr;
|
||||
|
||||
bool cancelled = strategy.stop_condition();
|
||||
const auto & rotations = allowed_rotations(item);
|
||||
|
||||
// Check all rotations but only if item is not already packed
|
||||
for (auto rot_it = rotations.begin();
|
||||
!cancelled && !packed && rot_it != rotations.end(); ++rot_it) {
|
||||
|
||||
double rot = *rot_it;
|
||||
|
||||
set_rotation(item, orig_rot + rot);
|
||||
set_translation(item, orig_tr);
|
||||
|
||||
auto nfp = calculate_nfp(item, packing_context, bed,
|
||||
strategy.stop_condition);
|
||||
double score = NaNd;
|
||||
if (!nfp.empty()) {
|
||||
score = pick_best_spot_on_nfp(item, nfp, bed, strategy);
|
||||
|
||||
cancelled = strategy.stop_condition();
|
||||
if (score > final_score) {
|
||||
final_score = score;
|
||||
final_rot = rot;
|
||||
final_tr = get_translation(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the score is not valid, and the item is not already packed, or
|
||||
// the packing was cancelled asynchronously by stop condition, then
|
||||
// discard the packing
|
||||
bool is_score_valid = !std::isnan(final_score) && !std::isinf(final_score);
|
||||
packed = !cancelled && (packed || is_score_valid);
|
||||
|
||||
if (packed) {
|
||||
set_translation(item, final_tr);
|
||||
set_rotation(item, orig_rot + final_rot);
|
||||
|
||||
// Finally, consult the kernel if the packing is sane
|
||||
packed = KernelT::on_item_packed(strategy.kernel, item);
|
||||
}
|
||||
|
||||
return packed;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // PACKSTRATEGYNFP_HPP
|
||||
@@ -0,0 +1,145 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef RECTANGLEOVERFITPACKINGSTRATEGY_HPP
|
||||
#define RECTANGLEOVERFITPACKINGSTRATEGY_HPP
|
||||
|
||||
#include "Kernels/RectangleOverfitKernelWrapper.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Core/NFP/PackStrategyNFP.hpp"
|
||||
#include "libslic3r/Arrange/Core/Beds.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
using PostAlignmentFn = std::function<Vec2crd(const BoundingBox &bedbb,
|
||||
const BoundingBox &pilebb)>;
|
||||
|
||||
struct CenterAlignmentFn {
|
||||
Vec2crd operator() (const BoundingBox &bedbb,
|
||||
const BoundingBox &pilebb)
|
||||
{
|
||||
return bedbb.center() - pilebb.center();
|
||||
}
|
||||
};
|
||||
|
||||
template<class ArrItem>
|
||||
struct RectangleOverfitPackingContext : public DefaultPackingContext<ArrItem>
|
||||
{
|
||||
BoundingBox limits;
|
||||
int bed_index;
|
||||
PostAlignmentFn post_alignment_fn;
|
||||
|
||||
explicit RectangleOverfitPackingContext(const BoundingBox limits,
|
||||
int bedidx,
|
||||
PostAlignmentFn alignfn = CenterAlignmentFn{})
|
||||
: limits{limits}, bed_index{bedidx}, post_alignment_fn{alignfn}
|
||||
{}
|
||||
|
||||
void align_pile()
|
||||
{
|
||||
// Here, the post alignment can be safely done. No throwing
|
||||
// functions are called!
|
||||
if (fixed_items_range(*this).empty()) {
|
||||
auto itms = packed_items_range(*this);
|
||||
auto pilebb = bounding_box(itms);
|
||||
|
||||
for (auto &itm : itms) {
|
||||
translate(itm, post_alignment_fn(limits, pilebb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~RectangleOverfitPackingContext() { align_pile(); }
|
||||
};
|
||||
|
||||
// With rectange bed, and no fixed items, an infinite bed with
|
||||
// RectangleOverfitKernelWrapper can produce better results than a pure
|
||||
// RectangleBed with inner-fit polygon calculation.
|
||||
template<class ...Args>
|
||||
struct RectangleOverfitPackingStrategy {
|
||||
PackStrategyNFP<Args...> base_strategy;
|
||||
|
||||
PostAlignmentFn post_alignment_fn = CenterAlignmentFn{};
|
||||
|
||||
template<class ArrItem>
|
||||
using Context = RectangleOverfitPackingContext<ArrItem>;
|
||||
|
||||
RectangleOverfitPackingStrategy(PackStrategyNFP<Args...> s,
|
||||
PostAlignmentFn post_align_fn)
|
||||
: base_strategy{std::move(s)}, post_alignment_fn{post_align_fn}
|
||||
{}
|
||||
|
||||
RectangleOverfitPackingStrategy(PackStrategyNFP<Args...> s)
|
||||
: base_strategy{std::move(s)}
|
||||
{}
|
||||
};
|
||||
|
||||
struct RectangleOverfitPackingStrategyTag {};
|
||||
|
||||
template<class... Args>
|
||||
struct PackStrategyTag_<RectangleOverfitPackingStrategy<Args...>> {
|
||||
using Tag = RectangleOverfitPackingStrategyTag;
|
||||
};
|
||||
|
||||
template<class... Args>
|
||||
struct PackStrategyTraits_<RectangleOverfitPackingStrategy<Args...>> {
|
||||
template<class ArrItem>
|
||||
using Context = typename RectangleOverfitPackingStrategy<
|
||||
Args...>::template Context<StripCVRef<ArrItem>>;
|
||||
|
||||
template<class ArrItem, class Bed>
|
||||
static Context<ArrItem> create_context(
|
||||
RectangleOverfitPackingStrategy<Args...> &ps,
|
||||
const Bed &bed,
|
||||
int bed_index)
|
||||
{
|
||||
return Context<ArrItem>{bounding_box(bed), bed_index,
|
||||
ps.post_alignment_fn};
|
||||
}
|
||||
};
|
||||
|
||||
template<class ArrItem>
|
||||
struct PackingContextTraits_<RectangleOverfitPackingContext<ArrItem>>
|
||||
: public PackingContextTraits_<DefaultPackingContext<ArrItem>>
|
||||
{
|
||||
static void add_packed_item(RectangleOverfitPackingContext<ArrItem> &ctx, ArrItem &itm)
|
||||
{
|
||||
ctx.add_packed_item(itm);
|
||||
|
||||
// to prevent coords going out of range
|
||||
ctx.align_pile();
|
||||
}
|
||||
};
|
||||
|
||||
template<class Strategy, class ArrItem, class Bed, class RemIt>
|
||||
bool pack(Strategy &strategy,
|
||||
const Bed &bed,
|
||||
ArrItem &item,
|
||||
const PackStrategyContext<Strategy, ArrItem> &packing_context,
|
||||
const Range<RemIt> &remaining_items,
|
||||
const RectangleOverfitPackingStrategyTag &)
|
||||
{
|
||||
bool ret = false;
|
||||
|
||||
if (fixed_items_range(packing_context).empty()) {
|
||||
auto &base = strategy.base_strategy;
|
||||
PackStrategyNFP modded_strategy{
|
||||
base.solver,
|
||||
RectangleOverfitKernelWrapper{base.kernel, packing_context.limits},
|
||||
base.ep, base.accuracy};
|
||||
|
||||
ret = pack(modded_strategy,
|
||||
InfiniteBed{packing_context.limits.center()}, item,
|
||||
packing_context, remaining_items, NFPPackingTag{});
|
||||
} else {
|
||||
ret = pack(strategy.base_strategy, bed, item, packing_context,
|
||||
remaining_items, NFPPackingTag{});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // RECTANGLEOVERFITPACKINGSTRATEGY_HPP
|
||||
@@ -0,0 +1,128 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef PACKINGCONTEXT_HPP
|
||||
#define PACKINGCONTEXT_HPP
|
||||
|
||||
#include "ArrangeItemTraits.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
template<class Ctx, class En = void>
|
||||
struct PackingContextTraits_ {
|
||||
template<class ArrItem>
|
||||
static void add_fixed_item(Ctx &ctx, const ArrItem &itm)
|
||||
{
|
||||
ctx.add_fixed_item(itm);
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
static void add_packed_item(Ctx &ctx, ArrItem &itm)
|
||||
{
|
||||
ctx.add_packed_item(itm);
|
||||
}
|
||||
|
||||
// returns a range of all packed items in the context ctx
|
||||
static auto all_items_range(const Ctx &ctx)
|
||||
{
|
||||
return ctx.all_items_range();
|
||||
}
|
||||
|
||||
static auto fixed_items_range(const Ctx &ctx)
|
||||
{
|
||||
return ctx.fixed_items_range();
|
||||
}
|
||||
|
||||
static auto packed_items_range(const Ctx &ctx)
|
||||
{
|
||||
return ctx.packed_items_range();
|
||||
}
|
||||
|
||||
static auto packed_items_range(Ctx &ctx)
|
||||
{
|
||||
return ctx.packed_items_range();
|
||||
}
|
||||
};
|
||||
|
||||
template<class Ctx, class ArrItem>
|
||||
void add_fixed_item(Ctx &ctx, const ArrItem &itm)
|
||||
{
|
||||
PackingContextTraits_<StripCVRef<Ctx>>::add_fixed_item(ctx, itm);
|
||||
}
|
||||
|
||||
template<class Ctx, class ArrItem>
|
||||
void add_packed_item(Ctx &ctx, ArrItem &itm)
|
||||
{
|
||||
PackingContextTraits_<StripCVRef<Ctx>>::add_packed_item(ctx, itm);
|
||||
}
|
||||
|
||||
template<class Ctx>
|
||||
auto all_items_range(const Ctx &ctx)
|
||||
{
|
||||
return PackingContextTraits_<StripCVRef<Ctx>>::all_items_range(ctx);
|
||||
}
|
||||
|
||||
template<class Ctx>
|
||||
auto fixed_items_range(const Ctx &ctx)
|
||||
{
|
||||
return PackingContextTraits_<StripCVRef<Ctx>>::fixed_items_range(ctx);
|
||||
}
|
||||
|
||||
template<class Ctx>
|
||||
auto packed_items_range(Ctx &&ctx)
|
||||
{
|
||||
return PackingContextTraits_<StripCVRef<Ctx>>::packed_items_range(ctx);
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
class DefaultPackingContext {
|
||||
using ArrItemRaw = StripCVRef<ArrItem>;
|
||||
std::vector<std::reference_wrapper<const ArrItemRaw>> m_fixed;
|
||||
std::vector<std::reference_wrapper<ArrItemRaw>> m_packed;
|
||||
std::vector<std::reference_wrapper<const ArrItemRaw>> m_items;
|
||||
|
||||
public:
|
||||
DefaultPackingContext() = default;
|
||||
|
||||
template<class It>
|
||||
explicit DefaultPackingContext(const Range<It> &fixed_items)
|
||||
{
|
||||
std::copy(fixed_items.begin(), fixed_items.end(), std::back_inserter(m_fixed));
|
||||
std::copy(fixed_items.begin(), fixed_items.end(), std::back_inserter(m_items));
|
||||
}
|
||||
|
||||
auto all_items_range() const noexcept { return crange(m_items); }
|
||||
auto fixed_items_range() const noexcept { return crange(m_fixed); }
|
||||
auto packed_items_range() const noexcept { return crange(m_packed); }
|
||||
auto packed_items_range() noexcept { return range(m_packed); }
|
||||
|
||||
void add_fixed_item(const ArrItem &itm)
|
||||
{
|
||||
m_fixed.emplace_back(itm);
|
||||
m_items.emplace_back(itm);
|
||||
}
|
||||
|
||||
void add_packed_item(ArrItem &itm)
|
||||
{
|
||||
m_packed.emplace_back(itm);
|
||||
m_items.emplace_back(itm);
|
||||
}
|
||||
};
|
||||
|
||||
template<class It>
|
||||
auto default_context(const Range<It> &items)
|
||||
{
|
||||
using ArrItem = StripCVRef<typename std::iterator_traits<It>::value_type>;
|
||||
return DefaultPackingContext<ArrItem>{items};
|
||||
}
|
||||
|
||||
template<class Cont, class ArrItem = typename Cont::value_type>
|
||||
auto default_context(const Cont &container)
|
||||
{
|
||||
return DefaultPackingContext<ArrItem>{crange(container)};
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // PACKINGCONTEXT_HPP
|
||||
@@ -0,0 +1,95 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARBITRARYDATASTORE_HPP
|
||||
#define ARBITRARYDATASTORE_HPP
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <any>
|
||||
|
||||
#include "libslic3r/Arrange/Core/DataStoreTraits.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// An associative container able to store and retrieve any data type.
|
||||
// Based on std::any
|
||||
class ArbitraryDataStore {
|
||||
std::map<std::string, std::any> m_data;
|
||||
|
||||
public:
|
||||
template<class T> void add(const std::string &key, T &&data)
|
||||
{
|
||||
m_data[key] = std::any{std::forward<T>(data)};
|
||||
}
|
||||
|
||||
void add(const std::string &key, std::any &&data)
|
||||
{
|
||||
m_data[key] = std::move(data);
|
||||
}
|
||||
|
||||
// Return nullptr if the key does not exist or the stored data has a
|
||||
// type other then T. Otherwise returns a pointer to the stored data.
|
||||
template<class T> const T *get(const std::string &key) const
|
||||
{
|
||||
auto it = m_data.find(key);
|
||||
return it != m_data.end() ? std::any_cast<T>(&(it->second)) :
|
||||
nullptr;
|
||||
}
|
||||
|
||||
// Same as above just not const.
|
||||
template<class T> T *get(const std::string &key)
|
||||
{
|
||||
auto it = m_data.find(key);
|
||||
return it != m_data.end() ? std::any_cast<T>(&(it->second)) : nullptr;
|
||||
}
|
||||
|
||||
bool has_key(const std::string &key) const
|
||||
{
|
||||
auto it = m_data.find(key);
|
||||
return it != m_data.end();
|
||||
}
|
||||
};
|
||||
|
||||
// Some items can be containers of arbitrary data stored under string keys.
|
||||
template<> struct DataStoreTraits_<ArbitraryDataStore>
|
||||
{
|
||||
static constexpr bool Implemented = true;
|
||||
|
||||
template<class T>
|
||||
static const T *get(const ArbitraryDataStore &s, const std::string &key)
|
||||
{
|
||||
return s.get<T>(key);
|
||||
}
|
||||
|
||||
// Same as above just not const.
|
||||
template<class T>
|
||||
static T *get(ArbitraryDataStore &s, const std::string &key)
|
||||
{
|
||||
return s.get<T>(key);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
static bool has_key(ArbitraryDataStore &s, const std::string &key)
|
||||
{
|
||||
return s.has_key(key);
|
||||
}
|
||||
};
|
||||
|
||||
template<> struct WritableDataStoreTraits_<ArbitraryDataStore>
|
||||
{
|
||||
static constexpr bool Implemented = true;
|
||||
|
||||
template<class T>
|
||||
static void set(ArbitraryDataStore &store,
|
||||
const std::string &key,
|
||||
T &&data)
|
||||
{
|
||||
store.add(key, std::forward<T>(data));
|
||||
}
|
||||
};
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // ARBITRARYDATASTORE_HPP
|
||||
@@ -0,0 +1,209 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "ArrangeItem.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Core/NFP/NFPConcave_Tesselate.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/ArrangeImpl.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/ArrangeTaskImpl.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/FillBedTaskImpl.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/MultiplySelectionTaskImpl.hpp"
|
||||
|
||||
#include "libslic3r/Geometry/ConvexHull.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
const Polygons &DecomposedShape::transformed_outline() const
|
||||
{
|
||||
constexpr auto sc = scaled<double>(1.) * scaled<double>(1.);
|
||||
|
||||
if (!m_transformed_outline_valid) {
|
||||
m_transformed_outline = contours();
|
||||
for (Polygon &poly : m_transformed_outline) {
|
||||
poly.rotate(rotation());
|
||||
poly.translate(translation());
|
||||
}
|
||||
|
||||
m_area = std::accumulate(m_transformed_outline.begin(),
|
||||
m_transformed_outline.end(), 0.,
|
||||
[sc](double s, const auto &p) {
|
||||
return s + p.area() / sc;
|
||||
});
|
||||
|
||||
m_convex_hull = Geometry::convex_hull(m_transformed_outline);
|
||||
m_bounding_box = get_extents(m_convex_hull);
|
||||
|
||||
m_transformed_outline_valid = true;
|
||||
}
|
||||
|
||||
return m_transformed_outline;
|
||||
}
|
||||
|
||||
const Polygon &DecomposedShape::convex_hull() const
|
||||
{
|
||||
if (!m_transformed_outline_valid)
|
||||
transformed_outline();
|
||||
|
||||
return m_convex_hull;
|
||||
}
|
||||
|
||||
const BoundingBox &DecomposedShape::bounding_box() const
|
||||
{
|
||||
if (!m_transformed_outline_valid)
|
||||
transformed_outline();
|
||||
|
||||
return m_bounding_box;
|
||||
}
|
||||
|
||||
const Vec2crd &DecomposedShape::reference_vertex() const
|
||||
{
|
||||
if (!m_reference_vertex_valid) {
|
||||
m_reference_vertex = Slic3r::reference_vertex(transformed_outline());
|
||||
m_refs.clear();
|
||||
m_mins.clear();
|
||||
m_refs.reserve(m_transformed_outline.size());
|
||||
m_mins.reserve(m_transformed_outline.size());
|
||||
for (auto &poly : m_transformed_outline) {
|
||||
m_refs.emplace_back(Slic3r::reference_vertex(poly));
|
||||
m_mins.emplace_back(Slic3r::min_vertex(poly));
|
||||
}
|
||||
m_reference_vertex_valid = true;
|
||||
}
|
||||
|
||||
return m_reference_vertex;
|
||||
}
|
||||
|
||||
const Vec2crd &DecomposedShape::reference_vertex(size_t i) const
|
||||
{
|
||||
if (!m_reference_vertex_valid) {
|
||||
reference_vertex();
|
||||
}
|
||||
|
||||
return m_refs[i];
|
||||
}
|
||||
|
||||
const Vec2crd &DecomposedShape::min_vertex(size_t idx) const
|
||||
{
|
||||
if (!m_reference_vertex_valid) {
|
||||
reference_vertex();
|
||||
}
|
||||
|
||||
return m_mins[idx];
|
||||
}
|
||||
|
||||
Vec2crd DecomposedShape::centroid() const
|
||||
{
|
||||
constexpr double area_sc = scaled<double>(1.) * scaled(1.);
|
||||
|
||||
if (!m_centroid_valid) {
|
||||
double total_area = 0.0;
|
||||
Vec2d cntr = Vec2d::Zero();
|
||||
|
||||
for (const Polygon& poly : transformed_outline()) {
|
||||
double parea = poly.area() / area_sc;
|
||||
Vec2d pcntr = unscaled(poly.centroid());
|
||||
total_area += parea;
|
||||
cntr += pcntr * parea;
|
||||
}
|
||||
|
||||
cntr /= total_area;
|
||||
m_centroid = scaled(cntr);
|
||||
m_centroid_valid = true;
|
||||
}
|
||||
|
||||
return m_centroid;
|
||||
}
|
||||
|
||||
DecomposedShape decompose(const ExPolygons &shape)
|
||||
{
|
||||
return DecomposedShape{convex_decomposition_tess(shape)};
|
||||
}
|
||||
|
||||
DecomposedShape decompose(const Polygon &shape)
|
||||
{
|
||||
Polygons convex_shapes;
|
||||
|
||||
bool is_convex = polygon_is_convex(shape);
|
||||
if (is_convex) {
|
||||
convex_shapes.emplace_back(shape);
|
||||
} else {
|
||||
convex_shapes = convex_decomposition_tess(shape);
|
||||
}
|
||||
|
||||
return DecomposedShape{std::move(convex_shapes)};
|
||||
}
|
||||
|
||||
ArrangeItem::ArrangeItem(const ExPolygons &shape)
|
||||
: m_shape{decompose(shape)}, m_envelope{&m_shape}
|
||||
{}
|
||||
|
||||
ArrangeItem::ArrangeItem(Polygon shape)
|
||||
: m_shape{decompose(shape)}, m_envelope{&m_shape}
|
||||
{}
|
||||
|
||||
ArrangeItem::ArrangeItem(const ArrangeItem &other)
|
||||
{
|
||||
this->operator= (other);
|
||||
}
|
||||
|
||||
ArrangeItem::ArrangeItem(ArrangeItem &&other) noexcept
|
||||
{
|
||||
this->operator=(std::move(other));
|
||||
}
|
||||
|
||||
ArrangeItem &ArrangeItem::operator=(const ArrangeItem &other)
|
||||
{
|
||||
m_shape = other.m_shape;
|
||||
m_datastore = other.m_datastore;
|
||||
m_bed_idx = other.m_bed_idx;
|
||||
m_priority = other.m_priority;
|
||||
|
||||
if (other.m_envelope.get() == &other.m_shape)
|
||||
m_envelope = &m_shape;
|
||||
else
|
||||
m_envelope = std::make_unique<DecomposedShape>(other.envelope());
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
void ArrangeItem::set_shape(DecomposedShape shape)
|
||||
{
|
||||
m_shape = std::move(shape);
|
||||
m_envelope = &m_shape;
|
||||
}
|
||||
|
||||
void ArrangeItem::set_envelope(DecomposedShape envelope)
|
||||
{
|
||||
m_envelope = std::make_unique<DecomposedShape>(std::move(envelope));
|
||||
|
||||
// Initial synch of transformations of envelope and shape.
|
||||
// They need to be in synch all the time
|
||||
m_envelope->translation(m_shape.translation());
|
||||
m_envelope->rotation(m_shape.rotation());
|
||||
}
|
||||
|
||||
ArrangeItem &ArrangeItem::operator=(ArrangeItem &&other) noexcept
|
||||
{
|
||||
m_shape = std::move(other.m_shape);
|
||||
m_datastore = std::move(other.m_datastore);
|
||||
m_bed_idx = other.m_bed_idx;
|
||||
m_priority = other.m_priority;
|
||||
|
||||
if (other.m_envelope.get() == &other.m_shape)
|
||||
m_envelope = &m_shape;
|
||||
else
|
||||
m_envelope = std::move(other.m_envelope);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
template struct ImbueableItemTraits_<ArrangeItem>;
|
||||
template class ArrangeableToItemConverter<ArrangeItem>;
|
||||
template struct ArrangeTask<ArrangeItem>;
|
||||
template struct FillBedTask<ArrangeItem>;
|
||||
template struct MultiplySelectionTask<ArrangeItem>;
|
||||
template class Arranger<ArrangeItem>;
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
@@ -0,0 +1,484 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGEITEM_HPP
|
||||
#define ARRANGEITEM_HPP
|
||||
|
||||
#include <optional>
|
||||
#include <boost/variant.hpp>
|
||||
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/AnyPtr.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Core/PackingContext.hpp"
|
||||
#include "libslic3r/Arrange/Core/NFP/NFPArrangeItemTraits.hpp"
|
||||
#include "libslic3r/Arrange/Core/NFP/NFP.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Items/MutableItemTraits.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Arrange.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/ArrangeTask.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/FillBedTask.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/MultiplySelectionTask.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Items/ArbitraryDataStore.hpp"
|
||||
|
||||
#include <libslic3r/ClipperUtils.hpp>
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
inline bool check_polygons_are_convex(const Polygons &pp) {
|
||||
return std::all_of(pp.begin(), pp.end(), [](const Polygon &p) {
|
||||
return polygon_is_convex(p);
|
||||
});
|
||||
}
|
||||
|
||||
// A class that stores a set of polygons that are garanteed to be all convex.
|
||||
// They collectively represent a decomposition of a more complex shape into
|
||||
// its convex part. Note that this class only stores the result of the decomp,
|
||||
// does not do the job itself. In debug mode, an explicit check is done for
|
||||
// each component to be convex.
|
||||
//
|
||||
// Additionally class stores a translation vector and a rotation angle for the
|
||||
// stored polygon, plus additional privitives that are all cached cached after
|
||||
// appying a the transformations. The caching is not thread safe!
|
||||
class DecomposedShape
|
||||
{
|
||||
Polygons m_shape;
|
||||
|
||||
Vec2crd m_translation{0, 0}; // The translation of the poly
|
||||
double m_rotation{0.0}; // The rotation of the poly in radians
|
||||
|
||||
mutable Polygons m_transformed_outline;
|
||||
mutable bool m_transformed_outline_valid = false;
|
||||
|
||||
mutable Point m_reference_vertex;
|
||||
mutable std::vector<Point> m_refs;
|
||||
mutable std::vector<Point> m_mins;
|
||||
mutable bool m_reference_vertex_valid = false;
|
||||
|
||||
mutable Point m_centroid;
|
||||
mutable bool m_centroid_valid = false;
|
||||
|
||||
mutable Polygon m_convex_hull;
|
||||
mutable BoundingBox m_bounding_box;
|
||||
mutable double m_area = 0;
|
||||
|
||||
public:
|
||||
DecomposedShape() = default;
|
||||
|
||||
explicit DecomposedShape(Polygon sh)
|
||||
{
|
||||
m_shape.emplace_back(std::move(sh));
|
||||
assert(check_polygons_are_convex(m_shape));
|
||||
}
|
||||
|
||||
explicit DecomposedShape(std::initializer_list<Point> pts)
|
||||
: DecomposedShape(Polygon{pts})
|
||||
{}
|
||||
|
||||
explicit DecomposedShape(Polygons sh) : m_shape{std::move(sh)}
|
||||
{
|
||||
assert(check_polygons_are_convex(m_shape));
|
||||
}
|
||||
|
||||
const Polygons &contours() const { return m_shape; }
|
||||
|
||||
const Vec2crd &translation() const { return m_translation; }
|
||||
double rotation() const { return m_rotation; }
|
||||
|
||||
void translation(const Vec2crd &v)
|
||||
{
|
||||
m_translation = v;
|
||||
m_transformed_outline_valid = false;
|
||||
m_reference_vertex_valid = false;
|
||||
m_centroid_valid = false;
|
||||
}
|
||||
|
||||
void rotation(double v)
|
||||
{
|
||||
m_rotation = v;
|
||||
m_transformed_outline_valid = false;
|
||||
m_reference_vertex_valid = false;
|
||||
m_centroid_valid = false;
|
||||
}
|
||||
|
||||
const Polygons &transformed_outline() const;
|
||||
const Polygon &convex_hull() const;
|
||||
const BoundingBox &bounding_box() const;
|
||||
|
||||
// The cached reference vertex in the context of NFP creation. Always
|
||||
// refers to the leftmost upper vertex.
|
||||
const Vec2crd &reference_vertex() const;
|
||||
const Vec2crd &reference_vertex(size_t idx) const;
|
||||
|
||||
// Also for NFP calculations, the rightmost lowest vertex of the shape.
|
||||
const Vec2crd &min_vertex(size_t idx) const;
|
||||
|
||||
double area_unscaled() const
|
||||
{
|
||||
// update cache
|
||||
transformed_outline();
|
||||
|
||||
return m_area;
|
||||
}
|
||||
|
||||
Vec2crd centroid() const;
|
||||
};
|
||||
|
||||
DecomposedShape decompose(const ExPolygons &polys);
|
||||
DecomposedShape decompose(const Polygon &p);
|
||||
|
||||
class ArrangeItem
|
||||
{
|
||||
private:
|
||||
DecomposedShape m_shape; // Shape of item when it's not moving
|
||||
AnyPtr<DecomposedShape> m_envelope; // Possibly different shape when packed
|
||||
|
||||
ArbitraryDataStore m_datastore;
|
||||
|
||||
int m_bed_idx{Unarranged}; // To which logical bed does this item belong
|
||||
int m_priority{0}; // For sorting
|
||||
|
||||
public:
|
||||
ArrangeItem() = default;
|
||||
|
||||
explicit ArrangeItem(DecomposedShape shape)
|
||||
: m_shape(std::move(shape)), m_envelope{&m_shape}
|
||||
{}
|
||||
|
||||
explicit ArrangeItem(DecomposedShape shape, DecomposedShape envelope)
|
||||
: m_shape(std::move(shape))
|
||||
, m_envelope{std::make_unique<DecomposedShape>(std::move(envelope))}
|
||||
{}
|
||||
|
||||
explicit ArrangeItem(const ExPolygons &shape);
|
||||
explicit ArrangeItem(Polygon shape);
|
||||
explicit ArrangeItem(std::initializer_list<Point> pts)
|
||||
: ArrangeItem(Polygon{pts})
|
||||
{}
|
||||
|
||||
ArrangeItem(const ArrangeItem &);
|
||||
ArrangeItem(ArrangeItem &&) noexcept;
|
||||
ArrangeItem & operator=(const ArrangeItem &);
|
||||
ArrangeItem & operator=(ArrangeItem &&) noexcept;
|
||||
|
||||
int bed_idx() const { return m_bed_idx; }
|
||||
int priority() const { return m_priority; }
|
||||
|
||||
void bed_idx(int v) { m_bed_idx = v; }
|
||||
void priority(int v) { m_priority = v; }
|
||||
|
||||
const ArbitraryDataStore &datastore() const { return m_datastore; }
|
||||
ArbitraryDataStore &datastore() { return m_datastore; }
|
||||
|
||||
const DecomposedShape & shape() const { return m_shape; }
|
||||
void set_shape(DecomposedShape shape);
|
||||
|
||||
const DecomposedShape & envelope() const { return *m_envelope; }
|
||||
void set_envelope(DecomposedShape envelope);
|
||||
|
||||
const Vec2crd &translation() const { return m_shape.translation(); }
|
||||
double rotation() const { return m_shape.rotation(); }
|
||||
|
||||
void translation(const Vec2crd &v)
|
||||
{
|
||||
m_shape.translation(v);
|
||||
m_envelope->translation(v);
|
||||
}
|
||||
|
||||
void rotation(double v)
|
||||
{
|
||||
m_shape.rotation(v);
|
||||
m_envelope->rotation(v);
|
||||
}
|
||||
|
||||
void update_caches() const
|
||||
{
|
||||
m_shape.reference_vertex();
|
||||
m_envelope->reference_vertex();
|
||||
m_shape.centroid();
|
||||
m_envelope->centroid();
|
||||
}
|
||||
};
|
||||
|
||||
template<> struct ArrangeItemTraits_<ArrangeItem>
|
||||
{
|
||||
static const Vec2crd &get_translation(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.translation();
|
||||
}
|
||||
|
||||
static double get_rotation(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.rotation();
|
||||
}
|
||||
|
||||
static int get_bed_index(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.bed_idx();
|
||||
}
|
||||
|
||||
static int get_priority(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.priority();
|
||||
}
|
||||
|
||||
// Setters:
|
||||
|
||||
static void set_translation(ArrangeItem &itm, const Vec2crd &v)
|
||||
{
|
||||
itm.translation(v);
|
||||
}
|
||||
|
||||
static void set_rotation(ArrangeItem &itm, double v)
|
||||
{
|
||||
itm.rotation(v);
|
||||
}
|
||||
|
||||
static void set_bed_index(ArrangeItem &itm, int v)
|
||||
{
|
||||
itm.bed_idx(v);
|
||||
}
|
||||
};
|
||||
|
||||
// Some items can be containers of arbitrary data stored under string keys.
|
||||
template<> struct DataStoreTraits_<ArrangeItem>
|
||||
{
|
||||
static constexpr bool Implemented = true;
|
||||
|
||||
template<class T>
|
||||
static const T *get(const ArrangeItem &itm, const std::string &key)
|
||||
{
|
||||
return itm.datastore().get<T>(key);
|
||||
}
|
||||
|
||||
// Same as above just not const.
|
||||
template<class T>
|
||||
static T *get(ArrangeItem &itm, const std::string &key)
|
||||
{
|
||||
return itm.datastore().get<T>(key);
|
||||
}
|
||||
|
||||
static bool has_key(const ArrangeItem &itm, const std::string &key)
|
||||
{
|
||||
return itm.datastore().has_key(key);
|
||||
}
|
||||
};
|
||||
|
||||
template<> struct WritableDataStoreTraits_<ArrangeItem>
|
||||
{
|
||||
static constexpr bool Implemented = true;
|
||||
|
||||
template<class T>
|
||||
static void set(ArrangeItem &itm,
|
||||
const std::string &key,
|
||||
T &&data)
|
||||
{
|
||||
itm.datastore().add(key, std::forward<T>(data));
|
||||
}
|
||||
};
|
||||
|
||||
template<class FixedIt, class StopCond = DefaultStopCondition>
|
||||
static Polygons calculate_nfp_unnormalized(const ArrangeItem &item,
|
||||
const Range<FixedIt> &fixed_items,
|
||||
StopCond &&stop_cond = {})
|
||||
{
|
||||
size_t cap = 0;
|
||||
|
||||
for (const ArrangeItem &fixitem : fixed_items) {
|
||||
const Polygons &outlines = fixitem.shape().transformed_outline();
|
||||
cap += outlines.size();
|
||||
}
|
||||
|
||||
const Polygons &item_outlines = item.envelope().transformed_outline();
|
||||
|
||||
auto nfps = reserve_polygons(cap * item_outlines.size());
|
||||
|
||||
Vec2crd ref_whole = item.envelope().reference_vertex();
|
||||
Polygon subnfp;
|
||||
|
||||
for (const ArrangeItem &fixed : fixed_items) {
|
||||
// fixed_polys should already be a set of strictly convex polygons,
|
||||
// as ArrangeItem stores convex-decomposed polygons
|
||||
const Polygons & fixed_polys = fixed.shape().transformed_outline();
|
||||
|
||||
for (const Polygon &fixed_poly : fixed_polys) {
|
||||
Point max_fixed = Slic3r::reference_vertex(fixed_poly);
|
||||
for (size_t mi = 0; mi < item_outlines.size(); ++mi) {
|
||||
const Polygon &movable = item_outlines[mi];
|
||||
const Vec2crd &mref = item.envelope().reference_vertex(mi);
|
||||
subnfp = nfp_convex_convex_legacy(fixed_poly, movable);
|
||||
|
||||
Vec2crd min_movable = item.envelope().min_vertex(mi);
|
||||
|
||||
Vec2crd dtouch = max_fixed - min_movable;
|
||||
Vec2crd top_other = mref + dtouch;
|
||||
Vec2crd max_nfp = Slic3r::reference_vertex(subnfp);
|
||||
auto dnfp = top_other - max_nfp;
|
||||
|
||||
auto d = ref_whole - mref + dnfp;
|
||||
subnfp.translate(d);
|
||||
nfps.emplace_back(subnfp);
|
||||
}
|
||||
|
||||
if (stop_cond())
|
||||
break;
|
||||
|
||||
nfps = union_(nfps);
|
||||
}
|
||||
|
||||
if (stop_cond()) {
|
||||
nfps.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return nfps;
|
||||
}
|
||||
|
||||
template<> struct NFPArrangeItemTraits_<ArrangeItem> {
|
||||
template<class Context, class Bed, class StopCond>
|
||||
static ExPolygons calculate_nfp(const ArrangeItem &item,
|
||||
const Context &packing_context,
|
||||
const Bed &bed,
|
||||
StopCond &&stopcond)
|
||||
{
|
||||
auto static_items = all_items_range(packing_context);
|
||||
Polygons nfps = arr2::calculate_nfp_unnormalized(item, static_items, stopcond);
|
||||
|
||||
ExPolygons nfp_ex;
|
||||
|
||||
if (!stopcond()) {
|
||||
if constexpr (!std::is_convertible_v<Bed, InfiniteBed>) {
|
||||
ExPolygons ifpbed = ifp_convex(bed, item.envelope().convex_hull());
|
||||
nfp_ex = diff_ex(ifpbed, nfps);
|
||||
} else {
|
||||
nfp_ex = union_ex(nfps);
|
||||
}
|
||||
}
|
||||
|
||||
item.update_caches();
|
||||
|
||||
return nfp_ex;
|
||||
}
|
||||
|
||||
static const Vec2crd& reference_vertex(const ArrangeItem &item)
|
||||
{
|
||||
return item.envelope().reference_vertex();
|
||||
}
|
||||
|
||||
static BoundingBox envelope_bounding_box(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.envelope().bounding_box();
|
||||
}
|
||||
|
||||
static BoundingBox fixed_bounding_box(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.shape().bounding_box();
|
||||
}
|
||||
|
||||
static double envelope_area(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.envelope().area_unscaled() * scaled<double>(1.) *
|
||||
scaled<double>(1.);
|
||||
}
|
||||
|
||||
static double fixed_area(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.shape().area_unscaled() * scaled<double>(1.) *
|
||||
scaled<double>(1.);
|
||||
}
|
||||
|
||||
static const Polygons & envelope_outline(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.envelope().transformed_outline();
|
||||
}
|
||||
|
||||
static const Polygons & fixed_outline(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.shape().transformed_outline();
|
||||
}
|
||||
|
||||
static const Polygon & envelope_convex_hull(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.envelope().convex_hull();
|
||||
}
|
||||
|
||||
static const Polygon & fixed_convex_hull(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.shape().convex_hull();
|
||||
}
|
||||
|
||||
static const std::vector<double>& allowed_rotations(const ArrangeItem &itm)
|
||||
{
|
||||
static const std::vector<double> ret_zero = {0.};
|
||||
|
||||
const std::vector<double> * ret_ptr = &ret_zero;
|
||||
|
||||
auto rots = get_data<std::vector<double>>(itm, "rotations");
|
||||
if (rots) {
|
||||
ret_ptr = rots;
|
||||
}
|
||||
|
||||
return *ret_ptr;
|
||||
}
|
||||
|
||||
static Vec2crd fixed_centroid(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.shape().centroid();
|
||||
}
|
||||
|
||||
static Vec2crd envelope_centroid(const ArrangeItem &itm)
|
||||
{
|
||||
return itm.envelope().centroid();
|
||||
}
|
||||
};
|
||||
|
||||
template<> struct IsMutableItem_<ArrangeItem>: public std::true_type {};
|
||||
|
||||
template<>
|
||||
struct MutableItemTraits_<ArrangeItem> {
|
||||
|
||||
static void set_priority(ArrangeItem &itm, int p) { itm.priority(p); }
|
||||
static void set_convex_shape(ArrangeItem &itm, const Polygon &shape)
|
||||
{
|
||||
itm.set_shape(DecomposedShape{shape});
|
||||
}
|
||||
static void set_shape(ArrangeItem &itm, const ExPolygons &shape)
|
||||
{
|
||||
itm.set_shape(decompose(shape));
|
||||
}
|
||||
static void set_convex_envelope(ArrangeItem &itm, const Polygon &envelope)
|
||||
{
|
||||
itm.set_envelope(DecomposedShape{envelope});
|
||||
}
|
||||
static void set_envelope(ArrangeItem &itm, const ExPolygons &envelope)
|
||||
{
|
||||
itm.set_envelope(decompose(envelope));
|
||||
}
|
||||
|
||||
template<class T>
|
||||
static void set_arbitrary_data(ArrangeItem &itm, const std::string &key, T &&data)
|
||||
{
|
||||
set_data(itm, key, std::forward<T>(data));
|
||||
}
|
||||
|
||||
static void set_allowed_rotations(ArrangeItem &itm, const std::vector<double> &rotations)
|
||||
{
|
||||
set_data(itm, "rotations", rotations);
|
||||
}
|
||||
};
|
||||
|
||||
extern template struct ImbueableItemTraits_<ArrangeItem>;
|
||||
extern template class ArrangeableToItemConverter<ArrangeItem>;
|
||||
extern template struct ArrangeTask<ArrangeItem>;
|
||||
extern template struct FillBedTask<ArrangeItem>;
|
||||
extern template struct MultiplySelectionTask<ArrangeItem>;
|
||||
extern template class Arranger<ArrangeItem>;
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // ARRANGEITEM_HPP
|
||||
@@ -0,0 +1,140 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef MutableItemTraits_HPP
|
||||
#define MutableItemTraits_HPP
|
||||
|
||||
#include "libslic3r/Arrange/Core/ArrangeItemTraits.hpp"
|
||||
#include "libslic3r/Arrange/Core/DataStoreTraits.hpp"
|
||||
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
template<class Itm> struct IsMutableItem_ : public std::false_type
|
||||
{};
|
||||
|
||||
// Using this interface to set up any arrange item. Provides default
|
||||
// implementation but it needs to be explicitly switched on with
|
||||
// IsMutableItem_ or completely reimplement a specialization.
|
||||
template<class Itm, class En = void> struct MutableItemTraits_
|
||||
{
|
||||
static_assert(IsMutableItem_<Itm>::value, "Not a Writable item type!");
|
||||
|
||||
static void set_priority(Itm &itm, int p) { itm.set_priority(p); }
|
||||
|
||||
static void set_convex_shape(Itm &itm, const Polygon &shape)
|
||||
{
|
||||
itm.set_convex_shape(shape);
|
||||
}
|
||||
|
||||
static void set_shape(Itm &itm, const ExPolygons &shape)
|
||||
{
|
||||
itm.set_shape(shape);
|
||||
}
|
||||
|
||||
static void set_convex_envelope(Itm &itm, const Polygon &envelope)
|
||||
{
|
||||
itm.set_convex_envelope(envelope);
|
||||
}
|
||||
|
||||
static void set_envelope(Itm &itm, const ExPolygons &envelope)
|
||||
{
|
||||
itm.set_envelope(envelope);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
static void set_arbitrary_data(Itm &itm, const std::string &key, T &&data)
|
||||
{
|
||||
if constexpr (IsWritableDataStore<Itm>)
|
||||
set_data(itm, key, std::forward<T>(data));
|
||||
}
|
||||
|
||||
static void set_allowed_rotations(Itm &itm,
|
||||
const std::vector<double> &rotations)
|
||||
{
|
||||
itm.set_allowed_rotations(rotations);
|
||||
}
|
||||
};
|
||||
|
||||
template<class T>
|
||||
using MutableItemTraits = MutableItemTraits_<StripCVRef<T>>;
|
||||
|
||||
template<class T> constexpr bool IsMutableItem = IsMutableItem_<T>::value;
|
||||
template<class T, class TT = T>
|
||||
using MutableItemOnly = std::enable_if_t<IsMutableItem<T>, TT>;
|
||||
|
||||
template<class Itm> void set_priority(Itm &itm, int p)
|
||||
{
|
||||
MutableItemTraits<Itm>::set_priority(itm, p);
|
||||
}
|
||||
|
||||
template<class Itm> void set_convex_shape(Itm &itm, const Polygon &shape)
|
||||
{
|
||||
MutableItemTraits<Itm>::set_convex_shape(itm, shape);
|
||||
}
|
||||
|
||||
template<class Itm> void set_shape(Itm &itm, const ExPolygons &shape)
|
||||
{
|
||||
MutableItemTraits<Itm>::set_shape(itm, shape);
|
||||
}
|
||||
|
||||
template<class Itm>
|
||||
void set_convex_envelope(Itm &itm, const Polygon &envelope)
|
||||
{
|
||||
MutableItemTraits<Itm>::set_convex_envelope(itm, envelope);
|
||||
}
|
||||
|
||||
template<class Itm> void set_envelope(Itm &itm, const ExPolygons &envelope)
|
||||
{
|
||||
MutableItemTraits<Itm>::set_envelope(itm, envelope);
|
||||
}
|
||||
|
||||
template<class T, class Itm>
|
||||
void set_arbitrary_data(Itm &itm, const std::string &key, T &&data)
|
||||
{
|
||||
MutableItemTraits<Itm>::set_arbitrary_data(itm, key, std::forward<T>(data));
|
||||
}
|
||||
|
||||
template<class Itm>
|
||||
void set_allowed_rotations(Itm &itm, const std::vector<double> &rotations)
|
||||
{
|
||||
MutableItemTraits<Itm>::set_allowed_rotations(itm, rotations);
|
||||
}
|
||||
|
||||
template<class ArrItem> int raise_priority(ArrItem &itm)
|
||||
{
|
||||
int ret = get_priority(itm) + 1;
|
||||
set_priority(itm, ret);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem> int reduce_priority(ArrItem &itm)
|
||||
{
|
||||
int ret = get_priority(itm) - 1;
|
||||
set_priority(itm, ret);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class It> int lowest_priority(const Range<It> &item_range)
|
||||
{
|
||||
auto minp_it = std::min_element(item_range.begin(),
|
||||
item_range.end(),
|
||||
[](auto &itm1, auto &itm2) {
|
||||
return get_priority(itm1) <
|
||||
get_priority(itm2);
|
||||
});
|
||||
|
||||
int min_priority = 0;
|
||||
if (minp_it != item_range.end())
|
||||
min_priority = get_priority(*minp_it);
|
||||
|
||||
return min_priority;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // MutableItemTraits_HPP
|
||||
@@ -0,0 +1,28 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "SimpleArrangeItem.hpp"
|
||||
#include "libslic3r/Arrange/ArrangeImpl.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/ArrangeTaskImpl.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/FillBedTaskImpl.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/MultiplySelectionTaskImpl.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
Polygon SimpleArrangeItem::outline() const
|
||||
{
|
||||
Polygon ret = shape();
|
||||
ret.rotate(m_rotation);
|
||||
ret.translate(m_translation);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template class ArrangeableToItemConverter<SimpleArrangeItem>;
|
||||
template struct ArrangeTask<SimpleArrangeItem>;
|
||||
template struct FillBedTask<SimpleArrangeItem>;
|
||||
template struct MultiplySelectionTask<SimpleArrangeItem>;
|
||||
template class Arranger<SimpleArrangeItem>;
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
@@ -0,0 +1,222 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef SIMPLEARRANGEITEM_HPP
|
||||
#define SIMPLEARRANGEITEM_HPP
|
||||
|
||||
#include "libslic3r/Arrange/Core/PackingContext.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Core/NFP/NFPArrangeItemTraits.hpp"
|
||||
#include "libslic3r/Arrange/Core/NFP/NFP.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Arrange.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/ArrangeTask.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/FillBedTask.hpp"
|
||||
#include "libslic3r/Arrange/Tasks/MultiplySelectionTask.hpp"
|
||||
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/Geometry/ConvexHull.hpp"
|
||||
|
||||
#include "MutableItemTraits.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
class SimpleArrangeItem {
|
||||
Polygon m_shape;
|
||||
|
||||
Vec2crd m_translation = Vec2crd::Zero();
|
||||
double m_rotation = 0.;
|
||||
int m_priority = 0;
|
||||
int m_bed_idx = Unarranged;
|
||||
|
||||
std::vector<double> m_allowed_rotations = {0.};
|
||||
ObjectID m_obj_id;
|
||||
|
||||
public:
|
||||
explicit SimpleArrangeItem(Polygon chull = {}): m_shape{std::move(chull)} {}
|
||||
|
||||
void set_shape(Polygon chull) { m_shape = std::move(chull); }
|
||||
|
||||
const Vec2crd& get_translation() const noexcept { return m_translation; }
|
||||
double get_rotation() const noexcept { return m_rotation; }
|
||||
int get_priority() const noexcept { return m_priority; }
|
||||
int get_bed_index() const noexcept { return m_bed_idx; }
|
||||
|
||||
void set_translation(const Vec2crd &v) { m_translation = v; }
|
||||
void set_rotation(double v) noexcept { m_rotation = v; }
|
||||
void set_priority(int v) noexcept { m_priority = v; }
|
||||
void set_bed_index(int v) noexcept { m_bed_idx = v; }
|
||||
|
||||
const Polygon &shape() const { return m_shape; }
|
||||
Polygon outline() const;
|
||||
|
||||
const auto &allowed_rotations() const noexcept
|
||||
{
|
||||
return m_allowed_rotations;
|
||||
}
|
||||
|
||||
void set_allowed_rotations(std::vector<double> rots)
|
||||
{
|
||||
m_allowed_rotations = std::move(rots);
|
||||
}
|
||||
|
||||
void set_object_id(const ObjectID &id) noexcept { m_obj_id = id; }
|
||||
const ObjectID & get_object_id() const noexcept { return m_obj_id; }
|
||||
};
|
||||
|
||||
template<> struct NFPArrangeItemTraits_<SimpleArrangeItem>
|
||||
{
|
||||
template<class Context, class Bed, class StopCond>
|
||||
static ExPolygons calculate_nfp(const SimpleArrangeItem &item,
|
||||
const Context &packing_context,
|
||||
const Bed &bed,
|
||||
StopCond &&stop_cond)
|
||||
{
|
||||
auto fixed_items = all_items_range(packing_context);
|
||||
auto nfps = reserve_polygons(fixed_items.size());
|
||||
for (const SimpleArrangeItem &fixed_part : fixed_items) {
|
||||
Polygon subnfp = nfp_convex_convex_legacy(fixed_part.outline(),
|
||||
item.outline());
|
||||
nfps.emplace_back(subnfp);
|
||||
|
||||
|
||||
if (stop_cond()) {
|
||||
nfps.clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ExPolygons nfp_ex;
|
||||
if (!stop_cond()) {
|
||||
if constexpr (!std::is_convertible_v<Bed, InfiniteBed>) {
|
||||
ExPolygons ifpbed = ifp_convex(bed, item.outline());
|
||||
nfp_ex = diff_ex(ifpbed, nfps);
|
||||
} else {
|
||||
nfp_ex = union_ex(nfps);
|
||||
}
|
||||
}
|
||||
|
||||
return nfp_ex;
|
||||
}
|
||||
|
||||
static Vec2crd reference_vertex(const SimpleArrangeItem &item)
|
||||
{
|
||||
return Slic3r::reference_vertex(item.outline());
|
||||
}
|
||||
|
||||
static BoundingBox envelope_bounding_box(const SimpleArrangeItem &itm)
|
||||
{
|
||||
return get_extents(itm.outline());
|
||||
}
|
||||
|
||||
static BoundingBox fixed_bounding_box(const SimpleArrangeItem &itm)
|
||||
{
|
||||
return get_extents(itm.outline());
|
||||
}
|
||||
|
||||
static Polygons envelope_outline(const SimpleArrangeItem &itm)
|
||||
{
|
||||
return {itm.outline()};
|
||||
}
|
||||
|
||||
static Polygons fixed_outline(const SimpleArrangeItem &itm)
|
||||
{
|
||||
return {itm.outline()};
|
||||
}
|
||||
|
||||
static Polygon envelope_convex_hull(const SimpleArrangeItem &itm)
|
||||
{
|
||||
return Geometry::convex_hull(itm.outline());
|
||||
}
|
||||
|
||||
static Polygon fixed_convex_hull(const SimpleArrangeItem &itm)
|
||||
{
|
||||
return Geometry::convex_hull(itm.outline());
|
||||
}
|
||||
|
||||
static double envelope_area(const SimpleArrangeItem &itm)
|
||||
{
|
||||
return itm.shape().area();
|
||||
}
|
||||
|
||||
static double fixed_area(const SimpleArrangeItem &itm)
|
||||
{
|
||||
return itm.shape().area();
|
||||
}
|
||||
|
||||
static const auto& allowed_rotations(const SimpleArrangeItem &itm) noexcept
|
||||
{
|
||||
return itm.allowed_rotations();
|
||||
}
|
||||
|
||||
static Vec2crd fixed_centroid(const SimpleArrangeItem &itm) noexcept
|
||||
{
|
||||
return itm.outline().centroid();
|
||||
}
|
||||
|
||||
static Vec2crd envelope_centroid(const SimpleArrangeItem &itm) noexcept
|
||||
{
|
||||
return itm.outline().centroid();
|
||||
}
|
||||
};
|
||||
|
||||
template<> struct IsMutableItem_<SimpleArrangeItem>: public std::true_type {};
|
||||
|
||||
template<>
|
||||
struct MutableItemTraits_<SimpleArrangeItem> {
|
||||
|
||||
static void set_priority(SimpleArrangeItem &itm, int p) { itm.set_priority(p); }
|
||||
static void set_convex_shape(SimpleArrangeItem &itm, const Polygon &shape)
|
||||
{
|
||||
itm.set_shape(shape);
|
||||
}
|
||||
static void set_shape(SimpleArrangeItem &itm, const ExPolygons &shape)
|
||||
{
|
||||
itm.set_shape(Geometry::convex_hull(shape));
|
||||
}
|
||||
static void set_convex_envelope(SimpleArrangeItem &itm, const Polygon &envelope)
|
||||
{
|
||||
itm.set_shape(envelope);
|
||||
}
|
||||
static void set_envelope(SimpleArrangeItem &itm, const ExPolygons &envelope)
|
||||
{
|
||||
itm.set_shape(Geometry::convex_hull(envelope));
|
||||
}
|
||||
|
||||
template<class T>
|
||||
static void set_data(SimpleArrangeItem &itm, const std::string &key, T &&data)
|
||||
{}
|
||||
|
||||
static void set_allowed_rotations(SimpleArrangeItem &itm, const std::vector<double> &rotations)
|
||||
{
|
||||
itm.set_allowed_rotations(rotations);
|
||||
}
|
||||
};
|
||||
|
||||
template<> struct ImbueableItemTraits_<SimpleArrangeItem>
|
||||
{
|
||||
static void imbue_id(SimpleArrangeItem &itm, const ObjectID &id)
|
||||
{
|
||||
itm.set_object_id(id);
|
||||
}
|
||||
|
||||
static std::optional<ObjectID> retrieve_id(const SimpleArrangeItem &itm)
|
||||
{
|
||||
std::optional<ObjectID> ret;
|
||||
if (itm.get_object_id().valid())
|
||||
ret = itm.get_object_id();
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
extern template class ArrangeableToItemConverter<SimpleArrangeItem>;
|
||||
extern template struct ArrangeTask<SimpleArrangeItem>;
|
||||
extern template struct FillBedTask<SimpleArrangeItem>;
|
||||
extern template struct MultiplySelectionTask<SimpleArrangeItem>;
|
||||
extern template class Arranger<SimpleArrangeItem>;
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // SIMPLEARRANGEITEM_HPP
|
||||
@@ -0,0 +1,83 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef TRAFOONLYARRANGEITEM_HPP
|
||||
#define TRAFOONLYARRANGEITEM_HPP
|
||||
|
||||
#include "libslic3r/Arrange/Core/ArrangeItemTraits.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Items/ArbitraryDataStore.hpp"
|
||||
#include "libslic3r/Arrange/Items/MutableItemTraits.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
class TrafoOnlyArrangeItem {
|
||||
int m_bed_idx = Unarranged;
|
||||
int m_priority = 0;
|
||||
Vec2crd m_translation = Vec2crd::Zero();
|
||||
double m_rotation = 0.;
|
||||
|
||||
ArbitraryDataStore m_datastore;
|
||||
|
||||
public:
|
||||
TrafoOnlyArrangeItem() = default;
|
||||
|
||||
template<class ArrItm>
|
||||
explicit TrafoOnlyArrangeItem(const ArrItm &other)
|
||||
: m_bed_idx{arr2::get_bed_index(other)},
|
||||
m_priority{arr2::get_priority(other)},
|
||||
m_translation(arr2::get_translation(other)),
|
||||
m_rotation{arr2::get_rotation(other)}
|
||||
{}
|
||||
|
||||
const Vec2crd& get_translation() const noexcept { return m_translation; }
|
||||
double get_rotation() const noexcept { return m_rotation; }
|
||||
int get_bed_index() const noexcept { return m_bed_idx; }
|
||||
int get_priority() const noexcept { return m_priority; }
|
||||
|
||||
const ArbitraryDataStore &datastore() const noexcept { return m_datastore; }
|
||||
ArbitraryDataStore &datastore() { return m_datastore; }
|
||||
};
|
||||
|
||||
template<> struct DataStoreTraits_<TrafoOnlyArrangeItem>
|
||||
{
|
||||
static constexpr bool Implemented = true;
|
||||
|
||||
template<class T>
|
||||
static const T *get(const TrafoOnlyArrangeItem &itm, const std::string &key)
|
||||
{
|
||||
return itm.datastore().get<T>(key);
|
||||
}
|
||||
|
||||
template<class T>
|
||||
static T *get(TrafoOnlyArrangeItem &itm, const std::string &key)
|
||||
{
|
||||
return itm.datastore().get<T>(key);
|
||||
}
|
||||
|
||||
static bool has_key(const TrafoOnlyArrangeItem &itm, const std::string &key)
|
||||
{
|
||||
return itm.datastore().has_key(key);
|
||||
}
|
||||
};
|
||||
|
||||
template<> struct IsMutableItem_<TrafoOnlyArrangeItem>: public std::true_type {};
|
||||
|
||||
template<> struct WritableDataStoreTraits_<TrafoOnlyArrangeItem>
|
||||
{
|
||||
static constexpr bool Implemented = true;
|
||||
|
||||
template<class T>
|
||||
static void set(TrafoOnlyArrangeItem &itm,
|
||||
const std::string &key,
|
||||
T &&data)
|
||||
{
|
||||
set_data(itm.datastore(), key, std::forward<T>(data));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace arr2
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // TRAFOONLYARRANGEITEM_HPP
|
||||
@@ -0,0 +1,68 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#include "Scene.hpp"
|
||||
|
||||
#include "Items/ArrangeItem.hpp"
|
||||
|
||||
#include "Tasks/ArrangeTask.hpp"
|
||||
#include "Tasks/FillBedTask.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
std::vector<ObjectID> Scene::selected_ids() const
|
||||
{
|
||||
auto items = reserve_vector<ObjectID>(model().arrangeable_count());
|
||||
|
||||
model().for_each_arrangeable([ &items](auto &arrbl) mutable {
|
||||
if (arrbl.is_selected())
|
||||
items.emplace_back(arrbl.id());
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
using DefaultArrangeItem = ArrangeItem;
|
||||
|
||||
std::unique_ptr<ArrangeTaskBase> ArrangeTaskBase::create(Tasks task_type, const Scene &sc)
|
||||
{
|
||||
std::unique_ptr<ArrangeTaskBase> ret;
|
||||
switch(task_type) {
|
||||
case Tasks::Arrange:
|
||||
ret = ArrangeTask<ArrangeItem>::create(sc);
|
||||
break;
|
||||
case Tasks::FillBed:
|
||||
ret = FillBedTask<ArrangeItem>::create(sc);
|
||||
break;
|
||||
default:
|
||||
;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::set<ObjectID> selected_geometry_ids(const Scene &sc)
|
||||
{
|
||||
std::set<ObjectID> result;
|
||||
|
||||
std::vector<ObjectID> selected_ids = sc.selected_ids();
|
||||
for (const ObjectID &id : selected_ids) {
|
||||
sc.model().visit_arrangeable(id, [&result](const Arrangeable &arrbl) {
|
||||
auto id = arrbl.geometry_id();
|
||||
if (id.valid())
|
||||
result.insert(arrbl.geometry_id());
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool arrange(Scene &scene, ArrangeTaskCtl &ctl)
|
||||
{
|
||||
auto task = ArrangeTaskBase::create(Tasks::Arrange, scene);
|
||||
auto result = task->process(ctl);
|
||||
return result->apply_on(scene.model());
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
@@ -0,0 +1,415 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARR2_SCENE_HPP
|
||||
#define ARR2_SCENE_HPP
|
||||
|
||||
#include <any>
|
||||
#include <string_view>
|
||||
|
||||
#include "libslic3r/ObjectID.hpp"
|
||||
#include "libslic3r/AnyPtr.hpp"
|
||||
#include "libslic3r/Arrange/ArrangeSettingsView.hpp"
|
||||
#include "libslic3r/Arrange/SegmentedRectangleBed.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// This module contains all the necessary high level interfaces for
|
||||
// arrangement. No dependency on the rest of libslic3r is intoduced here. (No
|
||||
// Model, ModelObject, etc...) except for ObjectID.
|
||||
|
||||
|
||||
// An interface that allows to store arbitrary data (std::any) under a specific
|
||||
// key in an object implementing the interface. This is later used to pass
|
||||
// arbitrary parameters from any arrangeable object down to the arrangement core.
|
||||
class AnyWritable
|
||||
{
|
||||
public:
|
||||
virtual ~AnyWritable() = default;
|
||||
|
||||
virtual void write(std::string_view key, std::any d) = 0;
|
||||
};
|
||||
|
||||
// The interface that captures the objects which are actually moved around.
|
||||
// Implementations must provide means to extract the 2D outline that is used
|
||||
// by the arrangement core.
|
||||
class Arrangeable
|
||||
{
|
||||
public:
|
||||
virtual ~Arrangeable() = default;
|
||||
|
||||
// ID is implementation specific, must uniquely identify an Arrangeable
|
||||
// object.
|
||||
virtual ObjectID id() const = 0;
|
||||
|
||||
// This is different than id(), and identifies an underlying group into
|
||||
// which the Arrangeable belongs. Can be used to group arrangeables sharing
|
||||
// the same outline.
|
||||
virtual ObjectID geometry_id() const = 0;
|
||||
|
||||
// Outline extraction can be a demanding operation, so there is a separate
|
||||
// method the extract the full outline of an object and the convex hull only
|
||||
// It will depend on the arrangement config to choose which one is called.
|
||||
// convex_outline might be considerably faster than calling full_outline()
|
||||
// and then calculating the convex hull from that.
|
||||
virtual ExPolygons full_outline() const = 0;
|
||||
virtual Polygon convex_outline() const = 0;
|
||||
|
||||
// Envelope is the boundary that an arrangeble object might have which
|
||||
// is used when the object is being placed or moved around. Once it is
|
||||
// placed, the outline (convex or full) will be used to determine the
|
||||
// boundaries instead of the envelope. This concept can be used to
|
||||
// implement arranging objects with support structures that can overlap,
|
||||
// but never touch the actual object. In this case, full envelope would
|
||||
// return the silhouette of the object with supports (pad, brim, etc...) and
|
||||
// outline would be the actual object boundary.
|
||||
virtual ExPolygons full_envelope() const { return {}; }
|
||||
virtual Polygon convex_envelope() const { return {}; }
|
||||
|
||||
// Write the transformations determined by the arrangement into the object
|
||||
virtual void transform(const Vec2d &transl, double rot) = 0;
|
||||
|
||||
// An arrangeable can be printable or unprintable, they should not be on
|
||||
// the same bed. (See arrange tasks)
|
||||
virtual bool is_printable() const { return true; }
|
||||
|
||||
// An arrangeable can be selected or not, this will determine if treated
|
||||
// as static objects or movable ones.
|
||||
virtual bool is_selected() const { return true; }
|
||||
|
||||
// Determines the order in which the objects are arranged. Higher priority
|
||||
// objects are arranged first.
|
||||
virtual int priority() const { return 0; }
|
||||
|
||||
// Any implementation specific properties can be passed to the arrangement
|
||||
// core by overriding this method. This implies that the specific Arranger
|
||||
// will be able to interpret these properties. An example usage is to mark
|
||||
// special objects (like a wipe tower)
|
||||
virtual void imbue_data(AnyWritable &datastore) const {}
|
||||
|
||||
// for convinience to pass an AnyWritable created in the same expression
|
||||
// as the method call
|
||||
void imbue_data(AnyWritable &&datastore) const { imbue_data(datastore); }
|
||||
|
||||
// An Arrangeable might reside on a logical bed instead of the real one
|
||||
// in case that the arrangement can not fit it onto the real bed. Handling
|
||||
// of logical beds is also implementation specific and are specified with
|
||||
// the next two methods:
|
||||
|
||||
// Returns the bed index on which the given Arrangeable is sitting.
|
||||
virtual int get_bed_index() const = 0;
|
||||
|
||||
// Assign the Arrangeable to the given bed index. Note that this
|
||||
// method can return false, indicating that the given bed is not available
|
||||
// to be occupied.
|
||||
virtual bool assign_bed(int bed_idx) = 0;
|
||||
};
|
||||
|
||||
// Arrangeable objects are provided by an ArrangeableModel which is also able to
|
||||
// create new arrangeables given a prototype id to copy.
|
||||
class ArrangeableModel
|
||||
{
|
||||
public:
|
||||
virtual ~ArrangeableModel() = default;
|
||||
|
||||
// Visit all arrangeable in this model and call the provided visitor
|
||||
virtual void for_each_arrangeable(std::function<void(Arrangeable &)>) = 0;
|
||||
virtual void for_each_arrangeable(std::function<void(const Arrangeable&)>) const = 0;
|
||||
|
||||
// Visit a specific arrangeable identified by it's id
|
||||
virtual void visit_arrangeable(const ObjectID &id, std::function<void(const Arrangeable &)>) const = 0;
|
||||
virtual void visit_arrangeable(const ObjectID &id, std::function<void(Arrangeable &)>) = 0;
|
||||
|
||||
// Add a new arrangeable which is a copy of the one matching prototype_id
|
||||
// Return the new object id or an invalid id if the new object was not
|
||||
// created.
|
||||
virtual ObjectID add_arrangeable(const ObjectID &prototype_id) = 0;
|
||||
|
||||
size_t arrangeable_count() const
|
||||
{
|
||||
size_t cnt = 0;
|
||||
for_each_arrangeable([&cnt](auto &) { ++cnt; });
|
||||
|
||||
return cnt;
|
||||
}
|
||||
};
|
||||
|
||||
// The special bed type used by XL printers
|
||||
using XLBed = SegmentedRectangleBed<std::integral_constant<size_t, 4>,
|
||||
std::integral_constant<size_t, 4>>;
|
||||
|
||||
// ExtendedBed is a variant type holding all bed types supported by the
|
||||
// arrange core and the additional XLBed
|
||||
|
||||
template<class... Args> struct ExtendedBed_
|
||||
{
|
||||
using Type =
|
||||
boost::variant<XLBed, /* insert other types if needed*/ Args...>;
|
||||
};
|
||||
|
||||
template<class... Args> struct ExtendedBed_<boost::variant<Args...>>
|
||||
{
|
||||
using Type = boost::variant<XLBed, Args...>;
|
||||
};
|
||||
|
||||
using ExtendedBed = typename ExtendedBed_<ArrangeBed>::Type;
|
||||
|
||||
template<class BedFn> void visit_bed(BedFn &&fn, const ExtendedBed &bed)
|
||||
{
|
||||
boost::apply_visitor(fn, bed);
|
||||
}
|
||||
|
||||
template<class BedFn> void visit_bed(BedFn &&fn, ExtendedBed &bed)
|
||||
{
|
||||
boost::apply_visitor(fn, bed);
|
||||
}
|
||||
|
||||
inline BoundingBox bounding_box(const ExtendedBed &bed)
|
||||
{
|
||||
BoundingBox bedbb;
|
||||
visit_bed([&bedbb](auto &rawbed) { bedbb = bounding_box(rawbed); }, bed);
|
||||
|
||||
return bedbb;
|
||||
}
|
||||
|
||||
class Scene;
|
||||
|
||||
// SceneBuilderBase is intended for Scene construction. A simple constructor
|
||||
// is not enough here to capture all the possible ways of constructing a Scene.
|
||||
// Subclasses of SceneBuilderBase can add more domain specific methods and
|
||||
// overloads. An rvalue object of this class is handed over to the Scene
|
||||
// constructor which can then establish itself using the provided builder.
|
||||
|
||||
// A little CRTP is used to implement fluent interface returning Subclass
|
||||
// references.
|
||||
template<class Subclass>
|
||||
class SceneBuilderBase
|
||||
{
|
||||
protected:
|
||||
AnyPtr<ArrangeableModel> m_arrangeable_model;
|
||||
|
||||
AnyPtr<const ArrangeSettingsView> m_settings;
|
||||
|
||||
ExtendedBed m_bed = arr2::InfiniteBed{};
|
||||
|
||||
coord_t m_brims_offs = 0;
|
||||
coord_t m_skirt_offs = 0;
|
||||
|
||||
public:
|
||||
|
||||
virtual ~SceneBuilderBase() = default;
|
||||
|
||||
SceneBuilderBase() = default;
|
||||
SceneBuilderBase(const SceneBuilderBase &) = delete;
|
||||
SceneBuilderBase& operator=(const SceneBuilderBase &) = delete;
|
||||
SceneBuilderBase(SceneBuilderBase &&) = default;
|
||||
SceneBuilderBase& operator=(SceneBuilderBase &&) = default;
|
||||
|
||||
// All setters return an rvalue reference so that at the end, the
|
||||
// build_scene method can be called fluently
|
||||
|
||||
Subclass &&set_arrange_settings(AnyPtr<const ArrangeSettingsView> settings)
|
||||
{
|
||||
m_settings = std::move(settings);
|
||||
return std::move(static_cast<Subclass&>(*this));
|
||||
}
|
||||
|
||||
Subclass &&set_arrange_settings(const ArrangeSettingsView &settings)
|
||||
{
|
||||
m_settings = std::make_unique<ArrangeSettings>(settings);
|
||||
return std::move(static_cast<Subclass&>(*this));
|
||||
}
|
||||
|
||||
Subclass &&set_bed(const Points &pts)
|
||||
{
|
||||
m_bed = arr2::to_arrange_bed(pts);
|
||||
return std::move(static_cast<Subclass&>(*this));
|
||||
}
|
||||
|
||||
Subclass && set_bed(const arr2::ArrangeBed &bed)
|
||||
{
|
||||
m_bed = bed;
|
||||
return std::move(static_cast<Subclass&>(*this));
|
||||
}
|
||||
|
||||
Subclass &&set_bed(const XLBed &bed)
|
||||
{
|
||||
m_bed = bed;
|
||||
return std::move(static_cast<Subclass&>(*this));
|
||||
}
|
||||
|
||||
Subclass &&set_arrangeable_model(AnyPtr<ArrangeableModel> model)
|
||||
{
|
||||
m_arrangeable_model = std::move(model);
|
||||
return std::move(static_cast<Subclass&>(*this));
|
||||
}
|
||||
|
||||
// Can only be called on an rvalue instance (hence the && at the end),
|
||||
// the method will potentially move its content into sc
|
||||
virtual void build_scene(Scene &sc) &&;
|
||||
};
|
||||
|
||||
class BasicSceneBuilder: public SceneBuilderBase<BasicSceneBuilder> {};
|
||||
|
||||
// The Scene class captures all data needed to do an arrangement.
|
||||
class Scene
|
||||
{
|
||||
template <class Sub> friend class SceneBuilderBase;
|
||||
|
||||
// These fields always need to be initialized to valid objects after
|
||||
// construction of Scene which is ensured by the SceneBuilder
|
||||
AnyPtr<ArrangeableModel> m_amodel;
|
||||
AnyPtr<const ArrangeSettingsView> m_settings;
|
||||
ExtendedBed m_bed;
|
||||
|
||||
public:
|
||||
// Scene can only be built from an rvalue SceneBuilder whose content will
|
||||
// potentially be moved to the constructed Scene object.
|
||||
template<class Sub>
|
||||
explicit Scene(SceneBuilderBase<Sub> &&bld)
|
||||
{
|
||||
std::move(bld).build_scene(*this);
|
||||
}
|
||||
|
||||
const ArrangeableModel &model() const noexcept { return *m_amodel; }
|
||||
ArrangeableModel &model() noexcept { return *m_amodel; }
|
||||
|
||||
const ArrangeSettingsView &settings() const noexcept { return *m_settings; }
|
||||
|
||||
template<class BedFn> void visit_bed(BedFn &&fn) const
|
||||
{
|
||||
arr2::visit_bed(fn, m_bed);
|
||||
}
|
||||
|
||||
const ExtendedBed & bed() const { return m_bed; }
|
||||
|
||||
std::vector<ObjectID> selected_ids() const;
|
||||
};
|
||||
|
||||
// Get all the ObjectIDs of Arrangeables which are in selected state
|
||||
std::set<ObjectID> selected_geometry_ids(const Scene &sc);
|
||||
|
||||
// A dummy, empty ArrangeableModel for testing and as placeholder to avoiod using nullptr
|
||||
class EmptyArrangeableModel: public ArrangeableModel
|
||||
{
|
||||
public:
|
||||
void for_each_arrangeable(std::function<void(Arrangeable &)>) override {}
|
||||
void for_each_arrangeable(std::function<void(const Arrangeable&)>) const override {}
|
||||
void visit_arrangeable(const ObjectID &id, std::function<void(const Arrangeable &)>) const override {}
|
||||
void visit_arrangeable(const ObjectID &id, std::function<void(Arrangeable &)>) override {}
|
||||
ObjectID add_arrangeable(const ObjectID &prototype_id) override { return {}; }
|
||||
};
|
||||
|
||||
template<class Subclass>
|
||||
void SceneBuilderBase<Subclass>::build_scene(Scene &sc) &&
|
||||
{
|
||||
if (!m_arrangeable_model)
|
||||
m_arrangeable_model = std::make_unique<EmptyArrangeableModel>();
|
||||
|
||||
if (!m_settings)
|
||||
m_settings = std::make_unique<arr2::ArrangeSettings>();
|
||||
|
||||
// Apply the bed minimum distance by making the original bed smaller
|
||||
// and arranging on this smaller bed.
|
||||
coord_t inset = std::max(scaled(m_settings->get_distance_from_bed()),
|
||||
m_skirt_offs + m_brims_offs);
|
||||
|
||||
// Objects have also a minimum distance from each other implemented
|
||||
// as inflation applied to object outlines. This object distance
|
||||
// does not apply to the bed, so the bed is inflated by this amount
|
||||
// to compensate.
|
||||
coord_t md = scaled(m_settings->get_distance_from_objects());
|
||||
md = md / 2 - inset;
|
||||
|
||||
// Applying the final bed with the corrected dimensions to account
|
||||
// for safety distances
|
||||
visit_bed([md](auto &rawbed) { rawbed = offset(rawbed, md); }, m_bed);
|
||||
|
||||
sc.m_settings = std::move(m_settings);
|
||||
sc.m_amodel = std::move(m_arrangeable_model);
|
||||
sc.m_bed = std::move(m_bed);
|
||||
}
|
||||
|
||||
// Arrange tasks produce an object implementing this interface. The arrange
|
||||
// result can be applied to an ArrangeableModel which may or may not succeed.
|
||||
// The ArrangeableModel could be in a different state (it's objects may have
|
||||
// changed or removed) than it was at the time of arranging.
|
||||
class ArrangeResult
|
||||
{
|
||||
public:
|
||||
virtual ~ArrangeResult() = default;
|
||||
|
||||
virtual bool apply_on(ArrangeableModel &mdlwt) = 0;
|
||||
};
|
||||
|
||||
enum class Tasks { Arrange, FillBed };
|
||||
|
||||
class ArrangeTaskCtl
|
||||
{
|
||||
public:
|
||||
virtual ~ArrangeTaskCtl() = default;
|
||||
|
||||
virtual void update_status(int st) = 0;
|
||||
|
||||
virtual bool was_canceled() const = 0;
|
||||
};
|
||||
|
||||
class DummyCtl : public ArrangeTaskCtl
|
||||
{
|
||||
public:
|
||||
void update_status(int) override {}
|
||||
bool was_canceled() const override { return false; }
|
||||
};
|
||||
|
||||
class ArrangeTaskBase
|
||||
{
|
||||
public:
|
||||
using Ctl = ArrangeTaskCtl;
|
||||
|
||||
virtual ~ArrangeTaskBase() = default;
|
||||
|
||||
[[nodiscard]] virtual std::unique_ptr<ArrangeResult> process(Ctl &ctl) = 0;
|
||||
|
||||
[[nodiscard]] virtual int item_count_to_process() const = 0;
|
||||
|
||||
[[nodiscard]] static std::unique_ptr<ArrangeTaskBase> create(
|
||||
Tasks task_type, const Scene &sc);
|
||||
|
||||
[[nodiscard]] std::unique_ptr<ArrangeResult> process(Ctl &&ctl)
|
||||
{
|
||||
return process(ctl);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::unique_ptr<ArrangeResult> process()
|
||||
{
|
||||
return process(DummyCtl{});
|
||||
}
|
||||
};
|
||||
|
||||
bool arrange(Scene &scene, ArrangeTaskCtl &ctl);
|
||||
inline bool arrange(Scene &scene, ArrangeTaskCtl &&ctl = DummyCtl{})
|
||||
{
|
||||
return arrange(scene, ctl);
|
||||
}
|
||||
|
||||
inline bool arrange(Scene &&scene, ArrangeTaskCtl &ctl)
|
||||
{
|
||||
return arrange(scene, ctl);
|
||||
}
|
||||
|
||||
inline bool arrange(Scene &&scene, ArrangeTaskCtl &&ctl = DummyCtl{})
|
||||
{
|
||||
return arrange(scene, ctl);
|
||||
}
|
||||
|
||||
template<class Builder, class Ctl = DummyCtl>
|
||||
bool arrange(SceneBuilderBase<Builder> &&builder, Ctl &&ctl = {})
|
||||
{
|
||||
return arrange(Scene{std::move(builder)}, ctl);
|
||||
}
|
||||
|
||||
} // namespace arr2
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // ARR2_SCENE_HPP
|
||||
@@ -0,0 +1,932 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef SCENEBUILDER_CPP
|
||||
#define SCENEBUILDER_CPP
|
||||
|
||||
#include "SceneBuilder.hpp"
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/SLAPrint.hpp"
|
||||
|
||||
#include "Core/ArrangeItemTraits.hpp"
|
||||
#include "Geometry/ConvexHull.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
coord_t get_skirt_inset(const Print &fffprint)
|
||||
{
|
||||
float skirt_inset = 0.f;
|
||||
|
||||
if (fffprint.has_skirt()) {
|
||||
float skirtflow = fffprint.objects().empty()
|
||||
? 0
|
||||
: fffprint.skirt_flow().width();
|
||||
skirt_inset = fffprint.config().skirts.value * skirtflow
|
||||
+ fffprint.config().skirt_distance.value;
|
||||
}
|
||||
|
||||
return scaled(skirt_inset);
|
||||
}
|
||||
|
||||
coord_t brim_offset(const PrintObject &po)
|
||||
{
|
||||
const BrimType brim_type = po.config().brim_type.value;
|
||||
const float brim_separation = po.config().brim_separation.getFloat();
|
||||
const float brim_width = po.config().brim_width.getFloat();
|
||||
const bool has_outer_brim = brim_type == BrimType::btOuterOnly ||
|
||||
brim_type == BrimType::btOuterAndInner;
|
||||
|
||||
// How wide is the brim? (in scaled units)
|
||||
return has_outer_brim ? scaled(brim_width + brim_separation) : 0;
|
||||
}
|
||||
|
||||
size_t model_instance_count (const Model &m)
|
||||
{
|
||||
return std::accumulate(m.objects.begin(),
|
||||
m.objects.end(),
|
||||
size_t(0),
|
||||
[](size_t s, const Slic3r::ModelObject *mo) {
|
||||
return s + mo->instances.size();
|
||||
});
|
||||
}
|
||||
|
||||
void transform_instance(ModelInstance &mi,
|
||||
const Vec2d &transl_unscaled,
|
||||
double rot,
|
||||
const Transform3d &physical_tr)
|
||||
{
|
||||
auto trafo = mi.get_transformation().get_matrix();
|
||||
auto tr = Transform3d::Identity();
|
||||
tr.translate(to_3d(transl_unscaled, 0.));
|
||||
trafo = physical_tr.inverse() * tr * Eigen::AngleAxisd(rot, Vec3d::UnitZ()) * physical_tr * trafo;
|
||||
|
||||
mi.set_transformation(Geometry::Transformation{trafo});
|
||||
|
||||
mi.invalidate_object_bounding_box();
|
||||
}
|
||||
|
||||
BoundingBoxf3 instance_bounding_box(const ModelInstance &mi,
|
||||
const Transform3d &tr,
|
||||
bool dont_translate)
|
||||
{
|
||||
BoundingBoxf3 bb;
|
||||
const Transform3d inst_matrix
|
||||
= dont_translate ? mi.get_transformation().get_matrix_no_offset()
|
||||
: mi.get_transformation().get_matrix();
|
||||
|
||||
for (ModelVolume *v : mi.get_object()->volumes) {
|
||||
if (v->is_model_part()) {
|
||||
bb.merge(v->mesh().transformed_bounding_box(tr * inst_matrix
|
||||
* v->get_matrix()));
|
||||
}
|
||||
}
|
||||
|
||||
return bb;
|
||||
}
|
||||
|
||||
BoundingBoxf3 instance_bounding_box(const ModelInstance &mi, bool dont_translate)
|
||||
{
|
||||
return instance_bounding_box(mi, Transform3d::Identity(), dont_translate);
|
||||
}
|
||||
|
||||
bool check_coord_bounds(const BoundingBoxf &bb)
|
||||
{
|
||||
return std::abs(bb.min.x()) < UnscaledCoordLimit &&
|
||||
std::abs(bb.min.y()) < UnscaledCoordLimit &&
|
||||
std::abs(bb.max.x()) < UnscaledCoordLimit &&
|
||||
std::abs(bb.max.y()) < UnscaledCoordLimit;
|
||||
}
|
||||
|
||||
ExPolygons extract_full_outline(const ModelInstance &inst, const Transform3d &tr)
|
||||
{
|
||||
ExPolygons outline;
|
||||
|
||||
if (check_coord_bounds(to_2d(instance_bounding_box(inst, tr)))) {
|
||||
for (const ModelVolume *v : inst.get_object()->volumes) {
|
||||
Polygons vol_outline;
|
||||
|
||||
vol_outline = project_mesh(v->mesh().its,
|
||||
tr * inst.get_matrix() * v->get_matrix(),
|
||||
[] {});
|
||||
switch (v->type()) {
|
||||
case ModelVolumeType::MODEL_PART:
|
||||
outline = union_ex(outline, vol_outline);
|
||||
break;
|
||||
case ModelVolumeType::NEGATIVE_VOLUME:
|
||||
outline = diff_ex(outline, vol_outline);
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outline;
|
||||
}
|
||||
|
||||
Polygon extract_convex_outline(const ModelInstance &inst, const Transform3d &tr)
|
||||
{
|
||||
auto bb = to_2d(instance_bounding_box(inst, tr));
|
||||
Polygon ret;
|
||||
|
||||
if (check_coord_bounds(bb)) {
|
||||
ret = inst.get_object()->convex_hull_2d(tr * inst.get_matrix());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline static bool is_infinite_bed(const ExtendedBed &ebed) noexcept
|
||||
{
|
||||
bool ret = false;
|
||||
visit_bed(
|
||||
[&ret](auto &rawbed) {
|
||||
ret = std::is_convertible_v<decltype(rawbed), InfiniteBed>;
|
||||
},
|
||||
ebed);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void SceneBuilder::set_brim_and_skirt()
|
||||
{
|
||||
if (!m_fff_print)
|
||||
return;
|
||||
|
||||
m_brims_offs = 0;
|
||||
|
||||
for (const PrintObject *po : m_fff_print->objects()) {
|
||||
if (po) {
|
||||
m_brims_offs = std::max(m_brims_offs, brim_offset(*po));
|
||||
}
|
||||
}
|
||||
|
||||
m_skirt_offs = get_skirt_inset(*m_fff_print);
|
||||
}
|
||||
|
||||
void SceneBuilder::build_scene(Scene &sc) &&
|
||||
{
|
||||
if (m_sla_print && !m_fff_print) {
|
||||
m_arrangeable_model = std::make_unique<ArrangeableSLAPrint>(m_sla_print.get(), *this);
|
||||
} else {
|
||||
m_arrangeable_model = std::make_unique<ArrangeableSlicerModel>(*this);
|
||||
}
|
||||
|
||||
if (m_fff_print && !m_sla_print) {
|
||||
if (is_infinite_bed(m_bed)) {
|
||||
set_bed(*m_fff_print);
|
||||
} else {
|
||||
set_brim_and_skirt();
|
||||
}
|
||||
}
|
||||
|
||||
// Call the parent class implementation of build_scene to finish constructing of the scene
|
||||
std::move(*this).SceneBuilderBase<SceneBuilder>::build_scene(sc);
|
||||
}
|
||||
|
||||
void SceneBuilder::build_arrangeable_slicer_model(ArrangeableSlicerModel &amodel)
|
||||
{
|
||||
if (!m_model)
|
||||
m_model = std::make_unique<Model>();
|
||||
|
||||
if (!m_selection)
|
||||
m_selection = std::make_unique<FixedSelection>(*m_model);
|
||||
|
||||
if (!m_vbed_handler) {
|
||||
m_vbed_handler = VirtualBedHandler::create(m_bed);
|
||||
}
|
||||
|
||||
if (!m_wipetower_handler) {
|
||||
m_wipetower_handler = std::make_unique<MissingWipeTowerHandler>();
|
||||
}
|
||||
|
||||
if (m_fff_print && !m_xl_printer)
|
||||
m_xl_printer = is_XL_printer(m_fff_print->config());
|
||||
|
||||
bool has_wipe_tower = false;
|
||||
m_wipetower_handler->visit(
|
||||
[&has_wipe_tower](const Arrangeable &arrbl) { has_wipe_tower = true; });
|
||||
|
||||
if (m_xl_printer && !has_wipe_tower) {
|
||||
m_bed = XLBed{bounding_box(m_bed)};
|
||||
}
|
||||
|
||||
amodel.m_vbed_handler = std::move(m_vbed_handler);
|
||||
amodel.m_model = std::move(m_model);
|
||||
amodel.m_selmask = std::move(m_selection);
|
||||
amodel.m_wth = std::move(m_wipetower_handler);
|
||||
|
||||
amodel.m_wth->set_selection_predicate(
|
||||
[&amodel] { return amodel.m_selmask->is_wipe_tower(); });
|
||||
}
|
||||
|
||||
int XStriderVBedHandler::get_bed_index(const VBedPlaceable &obj) const
|
||||
{
|
||||
int bedidx = 0;
|
||||
auto stride_s = stride_scaled();
|
||||
if (stride_s > 0) {
|
||||
double bedx = unscaled(m_start);
|
||||
auto instance_bb = obj.bounding_box();
|
||||
auto reference_pos_x = (instance_bb.min.x() - bedx);
|
||||
auto stride = unscaled(stride_s);
|
||||
|
||||
auto bedidx_d = std::floor(reference_pos_x / stride);
|
||||
|
||||
if (bedidx_d < std::numeric_limits<int>::min())
|
||||
bedidx = std::numeric_limits<int>::min();
|
||||
else if (bedidx_d > std::numeric_limits<int>::max())
|
||||
bedidx = std::numeric_limits<int>::max();
|
||||
else
|
||||
bedidx = static_cast<int>(bedidx_d);
|
||||
}
|
||||
|
||||
return bedidx;
|
||||
}
|
||||
|
||||
bool XStriderVBedHandler::assign_bed(VBedPlaceable &obj, int bed_index)
|
||||
{
|
||||
bool ret = false;
|
||||
auto stride_s = stride_scaled();
|
||||
if (bed_index == 0 || (bed_index > 0 && stride_s > 0)) {
|
||||
auto current_bed_index = get_bed_index(obj);
|
||||
auto stride = unscaled(stride_s);
|
||||
auto transl = Vec2d{(bed_index - current_bed_index) * stride, 0.};
|
||||
obj.displace(transl, 0.);
|
||||
|
||||
ret = true;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Transform3d XStriderVBedHandler::get_physical_bed_trafo(int bed_index) const
|
||||
{
|
||||
auto stride_s = stride_scaled();
|
||||
auto tr = Transform3d::Identity();
|
||||
tr.translate(Vec3d{-bed_index * unscaled(stride_s), 0., 0.});
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
int YStriderVBedHandler::get_bed_index(const VBedPlaceable &obj) const
|
||||
{
|
||||
int bedidx = 0;
|
||||
auto stride_s = stride_scaled();
|
||||
if (stride_s > 0) {
|
||||
double ystart = unscaled(m_start);
|
||||
auto instance_bb = obj.bounding_box();
|
||||
auto reference_pos_y = (instance_bb.min.y() - ystart);
|
||||
auto stride = unscaled(stride_s);
|
||||
|
||||
auto bedidx_d = std::floor(reference_pos_y / stride);
|
||||
|
||||
if (bedidx_d < std::numeric_limits<int>::min())
|
||||
bedidx = std::numeric_limits<int>::min();
|
||||
else if (bedidx_d > std::numeric_limits<int>::max())
|
||||
bedidx = std::numeric_limits<int>::max();
|
||||
else
|
||||
bedidx = static_cast<int>(bedidx_d);
|
||||
}
|
||||
|
||||
return bedidx;
|
||||
}
|
||||
|
||||
bool YStriderVBedHandler::assign_bed(VBedPlaceable &obj, int bed_index)
|
||||
{
|
||||
bool ret = false;
|
||||
auto stride_s = stride_scaled();
|
||||
if (bed_index == 0 || (bed_index > 0 && stride_s > 0)) {
|
||||
auto current_bed_index = get_bed_index(obj);
|
||||
auto stride = unscaled(stride_s);
|
||||
auto transl = Vec2d{0., (bed_index - current_bed_index) * stride};
|
||||
obj.displace(transl, 0.);
|
||||
|
||||
ret = true;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Transform3d YStriderVBedHandler::get_physical_bed_trafo(int bed_index) const
|
||||
{
|
||||
auto stride_s = stride_scaled();
|
||||
auto tr = Transform3d::Identity();
|
||||
tr.translate(Vec3d{0., -bed_index * unscaled(stride_s), 0.});
|
||||
|
||||
return tr;
|
||||
}
|
||||
|
||||
const int GridStriderVBedHandler::Cols =
|
||||
2 * static_cast<int>(std::sqrt(std::numeric_limits<int>::max()) / 2);
|
||||
|
||||
const int GridStriderVBedHandler::HalfCols = Cols / 2;
|
||||
const int GridStriderVBedHandler::Offset = HalfCols + Cols * HalfCols;
|
||||
|
||||
Vec2i GridStriderVBedHandler::raw2grid(int bed_idx) const
|
||||
{
|
||||
bed_idx += Offset;
|
||||
|
||||
Vec2i ret{bed_idx % Cols - HalfCols, bed_idx / Cols - HalfCols};
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int GridStriderVBedHandler::grid2raw(const Vec2i &crd) const
|
||||
{
|
||||
// Overlapping virtual beds will happen if the crd values exceed limits
|
||||
assert((crd.x() < HalfCols - 1 && crd.x() >= -HalfCols) &&
|
||||
(crd.y() < HalfCols - 1 && crd.y() >= -HalfCols));
|
||||
|
||||
return (crd.x() + HalfCols) + Cols * (crd.y() + HalfCols) - Offset;
|
||||
}
|
||||
|
||||
int GridStriderVBedHandler::get_bed_index(const VBedPlaceable &obj) const
|
||||
{
|
||||
Vec2i crd = {m_xstrider.get_bed_index(obj), m_ystrider.get_bed_index(obj)};
|
||||
|
||||
return grid2raw(crd);
|
||||
}
|
||||
|
||||
bool GridStriderVBedHandler::assign_bed(VBedPlaceable &inst, int bed_idx)
|
||||
{
|
||||
Vec2i crd = raw2grid(bed_idx);
|
||||
|
||||
bool retx = m_xstrider.assign_bed(inst, crd.x());
|
||||
bool rety = m_ystrider.assign_bed(inst, crd.y());
|
||||
|
||||
return retx && rety;
|
||||
}
|
||||
|
||||
Transform3d GridStriderVBedHandler::get_physical_bed_trafo(int bed_idx) const
|
||||
{
|
||||
Vec2i crd = raw2grid(bed_idx);
|
||||
|
||||
Transform3d ret = m_xstrider.get_physical_bed_trafo(crd.x()) *
|
||||
m_ystrider.get_physical_bed_trafo(crd.y());
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
FixedSelection::FixedSelection(const Model &m) : m_wp{true}
|
||||
{
|
||||
m_seldata.resize(m.objects.size());
|
||||
for (size_t i = 0; i < m.objects.size(); ++i) {
|
||||
m_seldata[i].resize(m.objects[i]->instances.size(), true);
|
||||
}
|
||||
}
|
||||
|
||||
FixedSelection::FixedSelection(const SelectionMask &other)
|
||||
{
|
||||
auto obj_sel = other.selected_objects();
|
||||
m_seldata.reserve(obj_sel.size());
|
||||
for (int oidx = 0; oidx < static_cast<int>(obj_sel.size()); ++oidx)
|
||||
m_seldata.emplace_back(other.selected_instances(oidx));
|
||||
}
|
||||
|
||||
std::vector<bool> FixedSelection::selected_objects() const
|
||||
{
|
||||
auto ret = Slic3r::reserve_vector<bool>(m_seldata.size());
|
||||
std::transform(m_seldata.begin(),
|
||||
m_seldata.end(),
|
||||
std::back_inserter(ret),
|
||||
[](auto &a) {
|
||||
return std::any_of(a.begin(), a.end(), [](bool b) {
|
||||
return b;
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
static std::vector<size_t> find_true_indices(const std::vector<bool> &v)
|
||||
{
|
||||
auto ret = reserve_vector<size_t>(v.size());
|
||||
|
||||
for (size_t i = 0; i < v.size(); ++i)
|
||||
if (v[i])
|
||||
ret.emplace_back(i);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<size_t> selected_object_indices(const SelectionMask &sm)
|
||||
{
|
||||
auto sel = sm.selected_objects();
|
||||
return find_true_indices(sel);
|
||||
}
|
||||
|
||||
std::vector<size_t> selected_instance_indices(int obj_idx, const SelectionMask &sm)
|
||||
{
|
||||
auto sel = sm.selected_instances(obj_idx);
|
||||
return find_true_indices(sel);
|
||||
}
|
||||
|
||||
SceneBuilder::SceneBuilder() = default;
|
||||
SceneBuilder::~SceneBuilder() = default;
|
||||
SceneBuilder::SceneBuilder(SceneBuilder &&) = default;
|
||||
SceneBuilder& SceneBuilder::operator=(SceneBuilder&&) = default;
|
||||
|
||||
SceneBuilder &&SceneBuilder::set_model(AnyPtr<Model> mdl)
|
||||
{
|
||||
m_model = std::move(mdl);
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
SceneBuilder &&SceneBuilder::set_model(Model &mdl)
|
||||
{
|
||||
m_model = &mdl;
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
SceneBuilder &&SceneBuilder::set_fff_print(AnyPtr<const Print> mdl_print)
|
||||
{
|
||||
m_fff_print = std::move(mdl_print);
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
SceneBuilder &&SceneBuilder::set_sla_print(AnyPtr<const SLAPrint> mdl_print)
|
||||
{
|
||||
m_sla_print = std::move(mdl_print);
|
||||
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
SceneBuilder &&SceneBuilder::set_bed(const DynamicPrintConfig &cfg)
|
||||
{
|
||||
Points bedpts = get_bed_shape(cfg);
|
||||
|
||||
if (is_XL_printer(cfg)) {
|
||||
m_xl_printer = true;
|
||||
}
|
||||
|
||||
m_bed = arr2::to_arrange_bed(bedpts);
|
||||
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
SceneBuilder &&SceneBuilder::set_bed(const Print &print)
|
||||
{
|
||||
Points bedpts = get_bed_shape(print.config());
|
||||
|
||||
if (is_XL_printer(print.config())) {
|
||||
m_bed = XLBed{get_extents(bedpts)};
|
||||
} else {
|
||||
m_bed = arr2::to_arrange_bed(bedpts);
|
||||
}
|
||||
|
||||
set_brim_and_skirt();
|
||||
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
SceneBuilder &&SceneBuilder::set_sla_print(const SLAPrint *slaprint)
|
||||
{
|
||||
m_sla_print = slaprint;
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
int ArrangeableWipeTowerBase::get_bed_index() const { return PhysicalBedId; }
|
||||
|
||||
bool ArrangeableWipeTowerBase::assign_bed(int bed_idx)
|
||||
{
|
||||
return bed_idx == PhysicalBedId;
|
||||
}
|
||||
|
||||
bool PhysicalOnlyVBedHandler::assign_bed(VBedPlaceable &inst, int bed_idx)
|
||||
{
|
||||
return bed_idx == PhysicalBedId;
|
||||
}
|
||||
|
||||
ArrangeableSlicerModel::ArrangeableSlicerModel(SceneBuilder &builder)
|
||||
{
|
||||
builder.build_arrangeable_slicer_model(*this);
|
||||
}
|
||||
|
||||
ArrangeableSlicerModel::~ArrangeableSlicerModel() = default;
|
||||
|
||||
void ArrangeableSlicerModel::for_each_arrangeable(
|
||||
std::function<void(Arrangeable &)> fn)
|
||||
{
|
||||
for_each_arrangeable_(*this, fn);
|
||||
|
||||
m_wth->visit(fn);
|
||||
}
|
||||
|
||||
void ArrangeableSlicerModel::for_each_arrangeable(
|
||||
std::function<void(const Arrangeable &)> fn) const
|
||||
{
|
||||
for_each_arrangeable_(*this, fn);
|
||||
|
||||
m_wth->visit(fn);
|
||||
}
|
||||
|
||||
ObjectID ArrangeableSlicerModel::add_arrangeable(const ObjectID &prototype_id)
|
||||
{
|
||||
ObjectID ret;
|
||||
|
||||
auto [inst, pos] = find_instance_by_id(*m_model, prototype_id);
|
||||
if (inst) {
|
||||
auto new_inst = inst->get_object()->add_instance(*inst);
|
||||
if (new_inst) {
|
||||
ret = new_inst->id();
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class Self, class Fn>
|
||||
void ArrangeableSlicerModel::for_each_arrangeable_(Self &&self, Fn &&fn)
|
||||
{
|
||||
InstPos pos;
|
||||
for (auto *obj : self.m_model->objects) {
|
||||
for (auto *inst : obj->instances) {
|
||||
ArrangeableModelInstance ainst{inst, self.m_vbed_handler.get(), self.m_selmask.get(), pos};
|
||||
fn(ainst);
|
||||
++pos.inst_idx;
|
||||
}
|
||||
pos.inst_idx = 0;
|
||||
++pos.obj_idx;
|
||||
}
|
||||
}
|
||||
|
||||
template<class Self, class Fn>
|
||||
void ArrangeableSlicerModel::visit_arrangeable_(Self &&self, const ObjectID &id, Fn &&fn)
|
||||
{
|
||||
if (id == self.m_model->wipe_tower.id()) {
|
||||
self.m_wth->visit(fn);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto [inst, pos] = find_instance_by_id(*self.m_model, id);
|
||||
|
||||
if (inst) {
|
||||
ArrangeableModelInstance ainst{inst, self.m_vbed_handler.get(), self.m_selmask.get(), pos};
|
||||
fn(ainst);
|
||||
}
|
||||
}
|
||||
|
||||
void ArrangeableSlicerModel::visit_arrangeable(
|
||||
const ObjectID &id, std::function<void(const Arrangeable &)> fn) const
|
||||
{
|
||||
visit_arrangeable_(*this, id, fn);
|
||||
}
|
||||
|
||||
void ArrangeableSlicerModel::visit_arrangeable(
|
||||
const ObjectID &id, std::function<void(Arrangeable &)> fn)
|
||||
{
|
||||
visit_arrangeable_(*this, id, fn);
|
||||
}
|
||||
|
||||
template<class Self, class Fn>
|
||||
void ArrangeableSLAPrint::for_each_arrangeable_(Self &&self, Fn &&fn)
|
||||
{
|
||||
InstPos pos;
|
||||
for (auto *obj : self.m_model->objects) {
|
||||
for (auto *inst : obj->instances) {
|
||||
ArrangeableModelInstance ainst{inst, self.m_vbed_handler.get(),
|
||||
self.m_selmask.get(), pos};
|
||||
|
||||
auto obj_id = inst->get_object()->id();
|
||||
const SLAPrintObject *po =
|
||||
self.m_slaprint->get_print_object_by_model_object_id(obj_id);
|
||||
|
||||
if (po) {
|
||||
auto &vbh = self.m_vbed_handler;
|
||||
auto phtr = vbh->get_physical_bed_trafo(vbh->get_bed_index(VBedPlaceableMI{*inst}));
|
||||
ArrangeableSLAPrintObject ainst_po{po, &ainst, phtr * inst->get_matrix()};
|
||||
fn(ainst_po);
|
||||
} else {
|
||||
fn(ainst);
|
||||
}
|
||||
|
||||
++pos.inst_idx;
|
||||
}
|
||||
pos.inst_idx = 0;
|
||||
++pos.obj_idx;
|
||||
}
|
||||
}
|
||||
|
||||
void ArrangeableSLAPrint::for_each_arrangeable(
|
||||
std::function<void(Arrangeable &)> fn)
|
||||
{
|
||||
for_each_arrangeable_(*this, fn);
|
||||
|
||||
m_wth->visit(fn);
|
||||
}
|
||||
|
||||
void ArrangeableSLAPrint::for_each_arrangeable(
|
||||
std::function<void(const Arrangeable &)> fn) const
|
||||
{
|
||||
for_each_arrangeable_(*this, fn);
|
||||
|
||||
m_wth->visit(fn);
|
||||
}
|
||||
|
||||
template<class Self, class Fn>
|
||||
void ArrangeableSLAPrint::visit_arrangeable_(Self &&self, const ObjectID &id, Fn &&fn)
|
||||
{
|
||||
auto [inst, pos] = find_instance_by_id(*self.m_model, id);
|
||||
|
||||
if (inst) {
|
||||
ArrangeableModelInstance ainst{inst, self.m_vbed_handler.get(),
|
||||
self.m_selmask.get(), pos};
|
||||
|
||||
auto obj_id = inst->get_object()->id();
|
||||
const SLAPrintObject *po =
|
||||
self.m_slaprint->get_print_object_by_model_object_id(obj_id);
|
||||
|
||||
if (po) {
|
||||
auto &vbh = self.m_vbed_handler;
|
||||
auto phtr = vbh->get_physical_bed_trafo(vbh->get_bed_index(VBedPlaceableMI{*inst}));
|
||||
ArrangeableSLAPrintObject ainst_po{po, &ainst, phtr * inst->get_matrix()};
|
||||
fn(ainst_po);
|
||||
} else {
|
||||
fn(ainst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ArrangeableSLAPrint::visit_arrangeable(
|
||||
const ObjectID &id, std::function<void(const Arrangeable &)> fn) const
|
||||
{
|
||||
visit_arrangeable_(*this, id, fn);
|
||||
}
|
||||
|
||||
void ArrangeableSLAPrint::visit_arrangeable(
|
||||
const ObjectID &id, std::function<void(Arrangeable &)> fn)
|
||||
{
|
||||
visit_arrangeable_(*this, id, fn);
|
||||
}
|
||||
|
||||
template<class InstPtr, class VBedHPtr>
|
||||
ExPolygons ArrangeableModelInstance<InstPtr, VBedHPtr>::full_outline() const
|
||||
{
|
||||
int bedidx = m_vbedh->get_bed_index(*this);
|
||||
auto tr = m_vbedh->get_physical_bed_trafo(bedidx);
|
||||
|
||||
return extract_full_outline(*m_mi, tr);
|
||||
}
|
||||
|
||||
template<class InstPtr, class VBedHPtr>
|
||||
Polygon ArrangeableModelInstance<InstPtr, VBedHPtr>::convex_outline() const
|
||||
{
|
||||
int bedidx = m_vbedh->get_bed_index(*this);
|
||||
auto tr = m_vbedh->get_physical_bed_trafo(bedidx);
|
||||
|
||||
return extract_convex_outline(*m_mi, tr);
|
||||
}
|
||||
|
||||
template<class InstPtr, class VBedHPtr>
|
||||
bool ArrangeableModelInstance<InstPtr, VBedHPtr>::is_selected() const
|
||||
{
|
||||
bool ret = false;
|
||||
|
||||
if (m_selmask) {
|
||||
auto sel = m_selmask->selected_instances(m_pos_within_model.obj_idx);
|
||||
if (m_pos_within_model.inst_idx < sel.size() &&
|
||||
sel[m_pos_within_model.inst_idx])
|
||||
ret = true;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class InstPtr, class VBedHPtr>
|
||||
void ArrangeableModelInstance<InstPtr, VBedHPtr>::transform(const Vec2d &transl, double rot)
|
||||
{
|
||||
if constexpr (!std::is_const_v<InstPtr> && !std::is_const_v<VBedHPtr>) {
|
||||
int bedidx = m_vbedh->get_bed_index(*this);
|
||||
auto physical_trafo = m_vbedh->get_physical_bed_trafo(bedidx);
|
||||
|
||||
transform_instance(*m_mi, transl, rot, physical_trafo);
|
||||
}
|
||||
}
|
||||
|
||||
template<class InstPtr, class VBedHPtr>
|
||||
bool ArrangeableModelInstance<InstPtr, VBedHPtr>::assign_bed(int bed_idx)
|
||||
{
|
||||
bool ret = false;
|
||||
|
||||
if constexpr (!std::is_const_v<InstPtr> && !std::is_const_v<VBedHPtr>)
|
||||
ret = m_vbedh->assign_bed(*this, bed_idx);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template class ArrangeableModelInstance<ModelInstance, VirtualBedHandler>;
|
||||
template class ArrangeableModelInstance<const ModelInstance, const VirtualBedHandler>;
|
||||
|
||||
ExPolygons ArrangeableSLAPrintObject::full_outline() const
|
||||
{
|
||||
ExPolygons ret;
|
||||
|
||||
auto laststep = m_po->last_completed_step();
|
||||
if (laststep < slaposCount && laststep > slaposSupportTree) {
|
||||
Polygons polys;
|
||||
auto omesh = m_po->get_mesh_to_print();
|
||||
auto &smesh = m_po->support_mesh();
|
||||
|
||||
Transform3d trafo_instance = m_inst_trafo * m_po->trafo().inverse();
|
||||
|
||||
if (omesh) {
|
||||
Polygons ptmp = project_mesh(*omesh, trafo_instance, [] {});
|
||||
std::move(ptmp.begin(), ptmp.end(), std::back_inserter(polys));
|
||||
}
|
||||
|
||||
Polygons ptmp = project_mesh(smesh.its, trafo_instance, [] {});
|
||||
std::move(ptmp.begin(), ptmp.end(), std::back_inserter(polys));
|
||||
ret = union_ex(polys);
|
||||
} else {
|
||||
ret = m_arrbl->full_outline();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
ExPolygons ArrangeableSLAPrintObject::full_envelope() const
|
||||
{
|
||||
ExPolygons ret = full_outline();
|
||||
|
||||
auto laststep = m_po->last_completed_step();
|
||||
if (laststep < slaposCount && laststep > slaposSupportTree) {
|
||||
auto &pmesh = m_po->pad_mesh();
|
||||
if (!pmesh.empty()) {
|
||||
|
||||
Transform3d trafo_instance = m_inst_trafo * m_po->trafo().inverse();
|
||||
|
||||
Polygons ptmp = project_mesh(pmesh.its, trafo_instance, [] {});
|
||||
ret = union_ex(ret, ptmp);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Polygon ArrangeableSLAPrintObject::convex_outline() const
|
||||
{
|
||||
Polygons polys;
|
||||
|
||||
polys.emplace_back(m_arrbl->convex_outline());
|
||||
|
||||
auto laststep = m_po->last_completed_step();
|
||||
if (laststep < slaposCount && laststep > slaposSupportTree) {
|
||||
auto omesh = m_po->get_mesh_to_print();
|
||||
auto &smesh = m_po->support_mesh();
|
||||
|
||||
Transform3f trafo_instance = m_inst_trafo.cast<float>();
|
||||
trafo_instance = trafo_instance * m_po->trafo().cast<float>().inverse();
|
||||
|
||||
Polygons polys;
|
||||
polys.reserve(3);
|
||||
auto zlvl = -m_po->get_elevation();
|
||||
|
||||
if (omesh) {
|
||||
polys.emplace_back(
|
||||
its_convex_hull_2d_above(*omesh, trafo_instance, zlvl));
|
||||
}
|
||||
|
||||
polys.emplace_back(
|
||||
its_convex_hull_2d_above(smesh.its, trafo_instance, zlvl));
|
||||
}
|
||||
|
||||
return Geometry::convex_hull(polys);
|
||||
}
|
||||
|
||||
Polygon ArrangeableSLAPrintObject::convex_envelope() const
|
||||
{
|
||||
Polygons polys;
|
||||
|
||||
polys.emplace_back(convex_outline());
|
||||
|
||||
auto laststep = m_po->last_completed_step();
|
||||
if (laststep < slaposCount && laststep > slaposSupportTree) {
|
||||
auto &pmesh = m_po->pad_mesh();
|
||||
if (!pmesh.empty()) {
|
||||
|
||||
Transform3f trafo_instance = m_inst_trafo.cast<float>();
|
||||
trafo_instance = trafo_instance * m_po->trafo().cast<float>().inverse();
|
||||
auto zlvl = -m_po->get_elevation();
|
||||
|
||||
polys.emplace_back(
|
||||
its_convex_hull_2d_above(pmesh.its, trafo_instance, zlvl));
|
||||
}
|
||||
}
|
||||
|
||||
return Geometry::convex_hull(polys);
|
||||
}
|
||||
|
||||
DuplicableModel::DuplicableModel(AnyPtr<Model> mdl, AnyPtr<VirtualBedHandler> vbh, const BoundingBox &bedbb)
|
||||
: m_model{std::move(mdl)}, m_vbh{std::move(vbh)}, m_duplicates(1), m_bedbb{bedbb}
|
||||
{
|
||||
}
|
||||
|
||||
DuplicableModel::~DuplicableModel() = default;
|
||||
|
||||
ObjectID DuplicableModel::add_arrangeable(const ObjectID &prototype_id)
|
||||
{
|
||||
ObjectID ret;
|
||||
if (prototype_id.valid()) {
|
||||
size_t idx = prototype_id.id - 1;
|
||||
if (idx < m_duplicates.size()) {
|
||||
ModelDuplicate md = m_duplicates[idx];
|
||||
md.id = m_duplicates.size();
|
||||
ret = md.id.id + 1;
|
||||
m_duplicates.emplace_back(std::move(md));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void DuplicableModel::apply_duplicates()
|
||||
{
|
||||
for (ModelObject *o : m_model->objects) {
|
||||
// make a copy of the pointers in order to avoid recursion
|
||||
// when appending their copies
|
||||
ModelInstancePtrs instances = o->instances;
|
||||
o->instances.clear();
|
||||
for (const ModelInstance *i : instances) {
|
||||
for (const ModelDuplicate &md : m_duplicates) {
|
||||
ModelInstance *instance = o->add_instance(*i);
|
||||
arr2::transform_instance(*instance, md.tr, md.rot);
|
||||
}
|
||||
}
|
||||
for (auto *i : instances)
|
||||
delete i;
|
||||
|
||||
instances.clear();
|
||||
|
||||
o->invalidate_bounding_box();
|
||||
}
|
||||
}
|
||||
|
||||
template<class Mdl, class Dup, class VBH>
|
||||
ObjectID ArrangeableFullModel<Mdl, Dup, VBH>::geometry_id() const { return m_mdl->id(); }
|
||||
|
||||
template<class Mdl, class Dup, class VBH>
|
||||
ExPolygons ArrangeableFullModel<Mdl, Dup, VBH>::full_outline() const
|
||||
{
|
||||
auto ret = reserve_vector<ExPolygon>(arr2::model_instance_count(*m_mdl));
|
||||
|
||||
auto transl = Transform3d::Identity();
|
||||
transl.translate(to_3d(m_dup->tr, 0.));
|
||||
Transform3d trafo = transl* Eigen::AngleAxisd(m_dup->rot, Vec3d::UnitZ());
|
||||
|
||||
for (auto *mo : m_mdl->objects) {
|
||||
for (auto *mi : mo->instances) {
|
||||
auto expolys = arr2::extract_full_outline(*mi, trafo);
|
||||
std::move(expolys.begin(), expolys.end(), std::back_inserter(ret));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class Mdl, class Dup, class VBH>
|
||||
Polygon ArrangeableFullModel<Mdl, Dup, VBH>::convex_outline() const
|
||||
{
|
||||
auto ret = reserve_polygons(arr2::model_instance_count(*m_mdl));
|
||||
|
||||
auto transl = Transform3d::Identity();
|
||||
transl.translate(to_3d(m_dup->tr, 0.));
|
||||
Transform3d trafo = transl* Eigen::AngleAxisd(m_dup->rot, Vec3d::UnitZ());
|
||||
|
||||
for (auto *mo : m_mdl->objects) {
|
||||
for (auto *mi : mo->instances) {
|
||||
ret.emplace_back(arr2::extract_convex_outline(*mi, trafo));
|
||||
}
|
||||
}
|
||||
|
||||
return Geometry::convex_hull(ret);
|
||||
}
|
||||
|
||||
template class ArrangeableFullModel<Model, ModelDuplicate, VirtualBedHandler>;
|
||||
template class ArrangeableFullModel<const Model, const ModelDuplicate, const VirtualBedHandler>;
|
||||
|
||||
std::unique_ptr<VirtualBedHandler> VirtualBedHandler::create(const ExtendedBed &bed)
|
||||
{
|
||||
std::unique_ptr<VirtualBedHandler> ret;
|
||||
if (is_infinite_bed(bed)) {
|
||||
ret = std::make_unique<PhysicalOnlyVBedHandler>();
|
||||
} else {
|
||||
// The gap between logical beds expressed in ratio of
|
||||
// the current bed width.
|
||||
constexpr double LogicalBedGap = 1. / 10.;
|
||||
|
||||
BoundingBox bedbb;
|
||||
visit_bed([&bedbb](auto &rawbed) { bedbb = bounding_box(rawbed); }, bed);
|
||||
|
||||
auto bedwidth = bedbb.size().x();
|
||||
coord_t xgap = LogicalBedGap * bedwidth;
|
||||
ret = std::make_unique<GridStriderVBedHandler>(bedbb, xgap);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // SCENEBUILDER_CPP
|
||||
@@ -0,0 +1,697 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef SCENEBUILDER_HPP
|
||||
#define SCENEBUILDER_HPP
|
||||
|
||||
#include "Scene.hpp"
|
||||
|
||||
#include "Core/ArrangeItemTraits.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class Model;
|
||||
class ModelInstance;
|
||||
class ModelWipeTower;
|
||||
class Print;
|
||||
class SLAPrint;
|
||||
class SLAPrintObject;
|
||||
class PrintObject;
|
||||
class DynamicPrintConfig;
|
||||
|
||||
namespace arr2 {
|
||||
|
||||
using SelectionPredicate = std::function<bool()>;
|
||||
|
||||
// Objects implementing this interface should know how to present the wipe tower
|
||||
// as an Arrangeable. If the wipe tower is not present, the overloads of visit() shouldn't do
|
||||
// anything. (See MissingWipeTowerHandler)
|
||||
class WipeTowerHandler
|
||||
{
|
||||
public:
|
||||
virtual ~WipeTowerHandler() = default;
|
||||
|
||||
virtual void visit(std::function<void(Arrangeable &)>) = 0;
|
||||
virtual void visit(std::function<void(const Arrangeable &)>) const = 0;
|
||||
virtual void set_selection_predicate(SelectionPredicate pred) = 0;
|
||||
};
|
||||
|
||||
// Something that has a bounding box and can be displaced by arbitrary 2D offset and rotated
|
||||
// by arbitrary rotation. Used as targets to place on virtual beds. Normally this would correspond
|
||||
// to ModelInstances but the same functionality was needed in more contexts.
|
||||
class VBedPlaceable {
|
||||
public:
|
||||
virtual ~VBedPlaceable() = default;
|
||||
|
||||
virtual BoundingBoxf bounding_box() const = 0;
|
||||
virtual void displace(const Vec2d &transl, double rot) = 0;
|
||||
};
|
||||
|
||||
// An interface to handle virtual beds for VBedPlaceable objects. A VBedPlaceable
|
||||
// may be assigned to a logical bed identified by an integer index value (zero
|
||||
// is the actual physical bed). The VBedPlaceable may still be outside of it's
|
||||
// bed, regardless of being assigned to it. The handler object should provide
|
||||
// means to read the assigned bed index of a VBedPlaceable, to assign a
|
||||
// different bed index and to provide a trafo that maps it to the physical bed
|
||||
// given a logical bed index. The reason is that the arrangement expects items
|
||||
// to be in the coordinate system of the physical bed.
|
||||
class VirtualBedHandler
|
||||
{
|
||||
public:
|
||||
virtual ~VirtualBedHandler() = default;
|
||||
|
||||
// Returns the bed index on which the given VBedPlaceable is sitting.
|
||||
virtual int get_bed_index(const VBedPlaceable &obj) const = 0;
|
||||
|
||||
// The returned trafo can be used to displace the VBedPlaceable
|
||||
// to the coordinate system of the physical bed, should that differ from
|
||||
// the coordinate space of a logical bed.
|
||||
virtual Transform3d get_physical_bed_trafo(int bed_index) const = 0;
|
||||
|
||||
// Assign the VBedPlaceable to the given bed index. Note that this
|
||||
// method can return false, indicating that the given bed is not available
|
||||
// to be occupied (e.g. the handler has a limited amount of logical bed)
|
||||
virtual bool assign_bed(VBedPlaceable &obj, int bed_idx) = 0;
|
||||
|
||||
bool assign_bed(VBedPlaceable &&obj, int bed_idx)
|
||||
{
|
||||
return assign_bed(obj, bed_idx);
|
||||
}
|
||||
|
||||
static std::unique_ptr<VirtualBedHandler> create(const ExtendedBed &bed);
|
||||
};
|
||||
|
||||
// Holds the info about which object (ID) is selected/unselected
|
||||
class SelectionMask
|
||||
{
|
||||
public:
|
||||
virtual ~SelectionMask() = default;
|
||||
|
||||
virtual std::vector<bool> selected_objects() const = 0;
|
||||
virtual std::vector<bool> selected_instances(int obj_id) const = 0;
|
||||
virtual bool is_wipe_tower() const = 0;
|
||||
};
|
||||
|
||||
class FixedSelection : public Slic3r::arr2::SelectionMask
|
||||
{
|
||||
std::vector<std::vector<bool>> m_seldata;
|
||||
bool m_wp = false;
|
||||
|
||||
public:
|
||||
FixedSelection() = default;
|
||||
|
||||
explicit FixedSelection(std::initializer_list<std::vector<bool>> seld,
|
||||
bool wp = false)
|
||||
: m_seldata{std::move(seld)}, m_wp{wp}
|
||||
{}
|
||||
|
||||
explicit FixedSelection(const Model &m);
|
||||
|
||||
explicit FixedSelection(const SelectionMask &other);
|
||||
|
||||
std::vector<bool> selected_objects() const override;
|
||||
|
||||
std::vector<bool> selected_instances(int obj_id) const override
|
||||
{
|
||||
return obj_id < int(m_seldata.size()) ? m_seldata[obj_id] :
|
||||
std::vector<bool>{};
|
||||
}
|
||||
|
||||
bool is_wipe_tower() const override { return m_wp; }
|
||||
};
|
||||
|
||||
// Common part of any Arrangeable which is a wipe tower
|
||||
struct ArrangeableWipeTowerBase: public Arrangeable
|
||||
{
|
||||
ObjectID oid;
|
||||
|
||||
Polygon poly;
|
||||
SelectionPredicate selection_pred;
|
||||
|
||||
ArrangeableWipeTowerBase(
|
||||
const ObjectID &objid,
|
||||
Polygon shape,
|
||||
SelectionPredicate selection_predicate = [] { return false; })
|
||||
: oid{objid},
|
||||
poly{std::move(shape)},
|
||||
selection_pred{std::move(selection_predicate)}
|
||||
{}
|
||||
|
||||
ObjectID id() const override { return oid; }
|
||||
ObjectID geometry_id() const override { return {}; }
|
||||
|
||||
ExPolygons full_outline() const override
|
||||
{
|
||||
auto cpy = poly;
|
||||
return {ExPolygon{std::move(cpy)}};
|
||||
}
|
||||
|
||||
Polygon convex_outline() const override
|
||||
{
|
||||
return poly;
|
||||
}
|
||||
|
||||
bool is_selected() const override
|
||||
{
|
||||
return selection_pred();
|
||||
}
|
||||
|
||||
int get_bed_index() const override;
|
||||
bool assign_bed(int /*bed_idx*/) override;
|
||||
|
||||
int priority() const override { return 1; }
|
||||
|
||||
void transform(const Vec2d &transl, double rot) override {}
|
||||
|
||||
void imbue_data(AnyWritable &datastore) const override
|
||||
{
|
||||
datastore.write("is_wipe_tower", {});
|
||||
}
|
||||
};
|
||||
|
||||
class SceneBuilder;
|
||||
|
||||
struct InstPos { size_t obj_idx = 0, inst_idx = 0; };
|
||||
|
||||
// Implementing ArrangeableModel interface for PrusaSlicer's Model, ModelObject, ModelInstance data
|
||||
// hierarchy
|
||||
class ArrangeableSlicerModel: public ArrangeableModel
|
||||
{
|
||||
protected:
|
||||
AnyPtr<Model> m_model;
|
||||
AnyPtr<WipeTowerHandler> m_wth; // Determines how wipe tower is handled
|
||||
AnyPtr<VirtualBedHandler> m_vbed_handler; // Determines how virtual beds are handled
|
||||
AnyPtr<const SelectionMask> m_selmask; // Determines which objects are selected/unselected
|
||||
|
||||
private:
|
||||
friend class SceneBuilder;
|
||||
|
||||
template<class Self, class Fn>
|
||||
static void for_each_arrangeable_(Self &&self, Fn &&fn);
|
||||
|
||||
template<class Self, class Fn>
|
||||
static void visit_arrangeable_(Self &&self, const ObjectID &id, Fn &&fn);
|
||||
|
||||
public:
|
||||
explicit ArrangeableSlicerModel(SceneBuilder &builder);
|
||||
~ArrangeableSlicerModel();
|
||||
|
||||
void for_each_arrangeable(std::function<void(Arrangeable &)>) override;
|
||||
void for_each_arrangeable(std::function<void(const Arrangeable&)>) const override;
|
||||
|
||||
void visit_arrangeable(const ObjectID &id, std::function<void(const Arrangeable &)>) const override;
|
||||
void visit_arrangeable(const ObjectID &id, std::function<void(Arrangeable &)>) override;
|
||||
|
||||
ObjectID add_arrangeable(const ObjectID &prototype_id) override;
|
||||
|
||||
Model & get_model() { return *m_model; }
|
||||
const Model &get_model() const { return *m_model; }
|
||||
};
|
||||
|
||||
// SceneBuilder implementation for PrusaSlicer API.
|
||||
class SceneBuilder: public SceneBuilderBase<SceneBuilder>
|
||||
{
|
||||
protected:
|
||||
AnyPtr<Model> m_model;
|
||||
AnyPtr<WipeTowerHandler> m_wipetower_handler;
|
||||
AnyPtr<VirtualBedHandler> m_vbed_handler;
|
||||
AnyPtr<const SelectionMask> m_selection;
|
||||
|
||||
AnyPtr<const SLAPrint> m_sla_print;
|
||||
AnyPtr<const Print> m_fff_print;
|
||||
bool m_xl_printer = false;
|
||||
|
||||
void set_brim_and_skirt();
|
||||
|
||||
public:
|
||||
SceneBuilder();
|
||||
~SceneBuilder();
|
||||
SceneBuilder(SceneBuilder&&);
|
||||
SceneBuilder& operator=(SceneBuilder&&);
|
||||
|
||||
SceneBuilder && set_model(AnyPtr<Model> mdl);
|
||||
|
||||
SceneBuilder && set_model(Model &mdl);
|
||||
|
||||
SceneBuilder && set_fff_print(AnyPtr<const Print> fffprint);
|
||||
SceneBuilder && set_sla_print(AnyPtr<const SLAPrint> mdl_print);
|
||||
|
||||
using SceneBuilderBase<SceneBuilder>::set_bed;
|
||||
|
||||
SceneBuilder &&set_bed(const DynamicPrintConfig &cfg);
|
||||
SceneBuilder &&set_bed(const Print &print);
|
||||
|
||||
SceneBuilder && set_wipe_tower_handler(WipeTowerHandler &wth)
|
||||
{
|
||||
m_wipetower_handler = &wth;
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
SceneBuilder && set_wipe_tower_handler(AnyPtr<WipeTowerHandler> wth)
|
||||
{
|
||||
m_wipetower_handler = std::move(wth);
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
SceneBuilder && set_virtual_bed_handler(AnyPtr<VirtualBedHandler> vbedh)
|
||||
{
|
||||
m_vbed_handler = std::move(vbedh);
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
SceneBuilder && set_sla_print(const SLAPrint *slaprint);
|
||||
|
||||
SceneBuilder && set_selection(AnyPtr<const SelectionMask> sel)
|
||||
{
|
||||
m_selection = std::move(sel);
|
||||
return std::move(*this);
|
||||
}
|
||||
|
||||
// Can only be called on an rvalue instance (hence the && at the end),
|
||||
// the method will potentially move its content into sc
|
||||
void build_scene(Scene &sc) && override;
|
||||
|
||||
void build_arrangeable_slicer_model(ArrangeableSlicerModel &amodel);
|
||||
};
|
||||
|
||||
struct MissingWipeTowerHandler : public WipeTowerHandler
|
||||
{
|
||||
void visit(std::function<void(Arrangeable &)>) override {}
|
||||
void visit(std::function<void(const Arrangeable &)>) const override {}
|
||||
void set_selection_predicate(std::function<bool()>) override {}
|
||||
};
|
||||
|
||||
// Only a physical bed, non-zero bed index values are discarded.
|
||||
class PhysicalOnlyVBedHandler final : public VirtualBedHandler
|
||||
{
|
||||
public:
|
||||
using VirtualBedHandler::assign_bed;
|
||||
|
||||
int get_bed_index(const VBedPlaceable &obj) const override { return 0; }
|
||||
|
||||
Transform3d get_physical_bed_trafo(int bed_index) const override
|
||||
{
|
||||
return Transform3d::Identity();
|
||||
}
|
||||
|
||||
bool assign_bed(VBedPlaceable &inst, int bed_idx) override;
|
||||
};
|
||||
|
||||
// A virtual bed handler implementation, that defines logical beds to be created
|
||||
// on the right side of the physical bed along the X axis in a row
|
||||
class XStriderVBedHandler final : public VirtualBedHandler
|
||||
{
|
||||
coord_t m_stride_scaled;
|
||||
coord_t m_start;
|
||||
|
||||
public:
|
||||
explicit XStriderVBedHandler(const BoundingBox &bedbb, coord_t xgap)
|
||||
: m_stride_scaled{bedbb.size().x() + 2 * std::max(0, xgap)},
|
||||
m_start{bedbb.min.x() - std::max(0, xgap)}
|
||||
{
|
||||
}
|
||||
|
||||
coord_t stride_scaled() const { return m_stride_scaled; }
|
||||
|
||||
// Can return negative indices when the instance is to the left of the
|
||||
// physical bed
|
||||
int get_bed_index(const VBedPlaceable &obj) const override;
|
||||
|
||||
// Only positive beds are accepted
|
||||
bool assign_bed(VBedPlaceable &inst, int bed_idx) override;
|
||||
|
||||
using VirtualBedHandler::assign_bed;
|
||||
|
||||
Transform3d get_physical_bed_trafo(int bed_index) const override;
|
||||
};
|
||||
|
||||
// Same as XStriderVBedHandler only that it lays out vbeds on the Y axis
|
||||
class YStriderVBedHandler final : public VirtualBedHandler
|
||||
{
|
||||
coord_t m_stride_scaled;
|
||||
coord_t m_start;
|
||||
|
||||
public:
|
||||
coord_t stride_scaled() const { return m_stride_scaled; }
|
||||
|
||||
explicit YStriderVBedHandler(const BoundingBox &bedbb, coord_t ygap)
|
||||
: m_stride_scaled{bedbb.size().y() + 2 * std::max(0, ygap)}
|
||||
, m_start{bedbb.min.y() - std::max(0, ygap)}
|
||||
{}
|
||||
|
||||
int get_bed_index(const VBedPlaceable &obj) const override;
|
||||
bool assign_bed(VBedPlaceable &inst, int bed_idx) override;
|
||||
|
||||
Transform3d get_physical_bed_trafo(int bed_index) const override;
|
||||
};
|
||||
|
||||
class GridStriderVBedHandler: public VirtualBedHandler
|
||||
{
|
||||
// This vbed handler defines a grid of virtual beds with a large number
|
||||
// of columns so that it behaves as XStrider for regular cases.
|
||||
// The goal is to handle objects residing at world coordinates
|
||||
// not representable with scaled coordinates. Combining XStrider with
|
||||
// YStrider takes care of the X and Y axis to be mapped into the physical
|
||||
// bed's coordinate region (which is representable in scaled coords)
|
||||
static const int Cols;
|
||||
static const int HalfCols;
|
||||
static const int Offset;
|
||||
|
||||
XStriderVBedHandler m_xstrider;
|
||||
YStriderVBedHandler m_ystrider;
|
||||
|
||||
public:
|
||||
GridStriderVBedHandler(const BoundingBox &bedbb,
|
||||
coord_t gap)
|
||||
: m_xstrider{bedbb, gap}
|
||||
, m_ystrider{bedbb, gap}
|
||||
{}
|
||||
|
||||
Vec2i raw2grid(int bedidx) const;
|
||||
int grid2raw(const Vec2i &crd) const;
|
||||
|
||||
int get_bed_index(const VBedPlaceable &obj) const override;
|
||||
bool assign_bed(VBedPlaceable &inst, int bed_idx) override;
|
||||
|
||||
Transform3d get_physical_bed_trafo(int bed_index) const override;
|
||||
};
|
||||
|
||||
std::vector<size_t> selected_object_indices(const SelectionMask &sm);
|
||||
std::vector<size_t> selected_instance_indices(int obj_idx, const SelectionMask &sm);
|
||||
|
||||
coord_t get_skirt_inset(const Print &fffprint);
|
||||
|
||||
coord_t brim_offset(const PrintObject &po);
|
||||
|
||||
// unscaled coords are necessary to be able to handle bigger coordinate range
|
||||
// than what is available with scaled coords. This is useful when working with
|
||||
// virtual beds.
|
||||
void transform_instance(ModelInstance &mi,
|
||||
const Vec2d &transl_unscaled,
|
||||
double rot,
|
||||
const Transform3d &physical_tr = Transform3d::Identity());
|
||||
|
||||
BoundingBoxf3 instance_bounding_box(const ModelInstance &mi,
|
||||
bool dont_translate = false);
|
||||
|
||||
BoundingBoxf3 instance_bounding_box(const ModelInstance &mi,
|
||||
const Transform3d &tr,
|
||||
bool dont_translate = false);
|
||||
|
||||
constexpr double UnscaledCoordLimit = 1000.;
|
||||
|
||||
ExPolygons extract_full_outline(const ModelInstance &inst,
|
||||
const Transform3d &tr = Transform3d::Identity());
|
||||
|
||||
Polygon extract_convex_outline(const ModelInstance &inst,
|
||||
const Transform3d &tr = Transform3d::Identity());
|
||||
|
||||
size_t model_instance_count (const Model &m);
|
||||
|
||||
class VBedPlaceableMI : public VBedPlaceable
|
||||
{
|
||||
ModelInstance *m_mi;
|
||||
|
||||
public:
|
||||
explicit VBedPlaceableMI(ModelInstance &mi) : m_mi{&mi} {}
|
||||
|
||||
BoundingBoxf bounding_box() const override { return to_2d(instance_bounding_box(*m_mi)); }
|
||||
void displace(const Vec2d &transl, double rot) override
|
||||
{
|
||||
transform_instance(*m_mi, transl, rot);
|
||||
}
|
||||
};
|
||||
|
||||
// Arrangeable interface implementation for ModelInstances
|
||||
template<class InstPtr, class VBedHPtr>
|
||||
class ArrangeableModelInstance : public Arrangeable, VBedPlaceable
|
||||
{
|
||||
InstPtr *m_mi;
|
||||
VBedHPtr *m_vbedh;
|
||||
const SelectionMask *m_selmask;
|
||||
InstPos m_pos_within_model;
|
||||
|
||||
public:
|
||||
explicit ArrangeableModelInstance(InstPtr *mi,
|
||||
VBedHPtr *vbedh,
|
||||
const SelectionMask *selmask,
|
||||
const InstPos &pos)
|
||||
: m_mi{mi}, m_vbedh{vbedh}, m_selmask{selmask}, m_pos_within_model{pos}
|
||||
{
|
||||
assert(m_mi != nullptr && m_vbedh != nullptr);
|
||||
}
|
||||
|
||||
// Arrangeable:
|
||||
ObjectID id() const override { return m_mi->id(); }
|
||||
ObjectID geometry_id() const override { return m_mi->get_object()->id(); }
|
||||
ExPolygons full_outline() const override;
|
||||
Polygon convex_outline() const override;
|
||||
bool is_printable() const override { return m_mi->printable; }
|
||||
bool is_selected() const override;
|
||||
void transform(const Vec2d &tr, double rot) override;
|
||||
|
||||
int get_bed_index() const override { return m_vbedh->get_bed_index(*this); }
|
||||
bool assign_bed(int bed_idx) override;
|
||||
|
||||
// VBedPlaceable:
|
||||
BoundingBoxf bounding_box() const override { return to_2d(instance_bounding_box(*m_mi)); }
|
||||
void displace(const Vec2d &transl, double rot) override
|
||||
{
|
||||
if constexpr (!std::is_const_v<InstPtr>)
|
||||
transform_instance(*m_mi, transl, rot);
|
||||
}
|
||||
};
|
||||
|
||||
extern template class ArrangeableModelInstance<ModelInstance, VirtualBedHandler>;
|
||||
extern template class ArrangeableModelInstance<const ModelInstance, const VirtualBedHandler>;
|
||||
|
||||
// Arrangeable implementation for an SLAPrintObject to be able to arrange with the supports and pad
|
||||
class ArrangeableSLAPrintObject : public Arrangeable
|
||||
{
|
||||
const SLAPrintObject *m_po;
|
||||
Arrangeable *m_arrbl;
|
||||
Transform3d m_inst_trafo;
|
||||
|
||||
public:
|
||||
ArrangeableSLAPrintObject(const SLAPrintObject *po,
|
||||
Arrangeable *arrbl,
|
||||
const Transform3d &inst_tr = Transform3d::Identity())
|
||||
: m_po{po}, m_arrbl{arrbl}, m_inst_trafo{inst_tr}
|
||||
{}
|
||||
|
||||
ObjectID id() const override { return m_arrbl->id(); }
|
||||
ObjectID geometry_id() const override { return m_arrbl->geometry_id(); }
|
||||
|
||||
ExPolygons full_outline() const override;
|
||||
ExPolygons full_envelope() const override;
|
||||
|
||||
Polygon convex_outline() const override;
|
||||
Polygon convex_envelope() const override;
|
||||
|
||||
void transform(const Vec2d &transl, double rot) override
|
||||
{
|
||||
m_arrbl->transform(transl, rot);
|
||||
}
|
||||
int get_bed_index() const override { return m_arrbl->get_bed_index(); }
|
||||
bool assign_bed(int bedidx) override
|
||||
{
|
||||
return m_arrbl->assign_bed(bedidx);
|
||||
}
|
||||
|
||||
bool is_printable() const override { return m_arrbl->is_printable(); }
|
||||
bool is_selected() const override { return m_arrbl->is_selected(); }
|
||||
int priority() const override { return m_arrbl->priority(); }
|
||||
};
|
||||
|
||||
// Extension of ArrangeableSlicerModel for SLA
|
||||
class ArrangeableSLAPrint : public ArrangeableSlicerModel {
|
||||
const SLAPrint *m_slaprint;
|
||||
|
||||
friend class SceneBuilder;
|
||||
|
||||
template<class Self, class Fn>
|
||||
static void for_each_arrangeable_(Self &&self, Fn &&fn);
|
||||
|
||||
template<class Self, class Fn>
|
||||
static void visit_arrangeable_(Self &&self, const ObjectID &id, Fn &&fn);
|
||||
|
||||
public:
|
||||
explicit ArrangeableSLAPrint(const SLAPrint *slaprint, SceneBuilder &builder)
|
||||
: m_slaprint{slaprint}
|
||||
, ArrangeableSlicerModel{builder}
|
||||
{
|
||||
assert(slaprint != nullptr);
|
||||
}
|
||||
|
||||
void for_each_arrangeable(std::function<void(Arrangeable &)>) override;
|
||||
|
||||
void for_each_arrangeable(
|
||||
std::function<void(const Arrangeable &)>) const override;
|
||||
|
||||
void visit_arrangeable(
|
||||
const ObjectID &id,
|
||||
std::function<void(const Arrangeable &)>) const override;
|
||||
|
||||
void visit_arrangeable(const ObjectID &id,
|
||||
std::function<void(Arrangeable &)>) override;
|
||||
};
|
||||
|
||||
template<class Mdl>
|
||||
auto find_instance_by_id(Mdl &&model, const ObjectID &id)
|
||||
{
|
||||
std::remove_reference_t<
|
||||
decltype(std::declval<Mdl>().objects[0]->instances[0])>
|
||||
ret = nullptr;
|
||||
|
||||
InstPos pos;
|
||||
|
||||
for (auto * obj : model.objects) {
|
||||
for (auto *inst : obj->instances) {
|
||||
if (inst->id() == id) {
|
||||
ret = inst;
|
||||
break;
|
||||
}
|
||||
++pos.inst_idx;
|
||||
}
|
||||
|
||||
if (ret)
|
||||
break;
|
||||
|
||||
++pos.obj_idx;
|
||||
pos.inst_idx = 0;
|
||||
}
|
||||
|
||||
return std::make_pair(ret, pos);
|
||||
}
|
||||
|
||||
struct ModelDuplicate
|
||||
{
|
||||
ObjectID id;
|
||||
Vec2d tr = Vec2d::Zero();
|
||||
double rot = 0.;
|
||||
int bed_idx = Unarranged;
|
||||
};
|
||||
|
||||
// Implementing the Arrangeable interface with the whole Model being one outline
|
||||
// with all its objects and instances.
|
||||
template<class Mdl, class Dup, class VBH>
|
||||
class ArrangeableFullModel: public Arrangeable, VBedPlaceable
|
||||
{
|
||||
Mdl *m_mdl;
|
||||
Dup *m_dup;
|
||||
VBH *m_vbh;
|
||||
|
||||
public:
|
||||
explicit ArrangeableFullModel(Mdl *mdl,
|
||||
Dup *md,
|
||||
VBH *vbh)
|
||||
: m_mdl{mdl}, m_dup{md}, m_vbh{vbh}
|
||||
{
|
||||
assert(m_mdl != nullptr);
|
||||
}
|
||||
|
||||
ObjectID id() const override { return m_dup->id.id + 1; }
|
||||
ObjectID geometry_id() const override;
|
||||
|
||||
ExPolygons full_outline() const override;
|
||||
|
||||
Polygon convex_outline() const override;
|
||||
|
||||
bool is_printable() const override { return true; }
|
||||
bool is_selected() const override { return m_dup->id == 0; }
|
||||
|
||||
int get_bed_index() const override
|
||||
{
|
||||
return m_vbh->get_bed_index(*this);
|
||||
}
|
||||
|
||||
void transform(const Vec2d &tr, double rot) override
|
||||
{
|
||||
if constexpr (!std::is_const_v<Mdl> && !std::is_const_v<Dup>) {
|
||||
m_dup->tr += tr;
|
||||
m_dup->rot += rot;
|
||||
}
|
||||
}
|
||||
|
||||
bool assign_bed(int bed_idx) override
|
||||
{
|
||||
bool ret = false;
|
||||
|
||||
if constexpr (!std::is_const_v<VBH> && !std::is_const_v<Dup>) {
|
||||
if ((ret = m_vbh->assign_bed(*this, bed_idx)))
|
||||
m_dup->bed_idx = bed_idx;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
BoundingBoxf bounding_box() const override { return unscaled(get_extents(convex_outline())); }
|
||||
void displace(const Vec2d &transl, double rot) override
|
||||
{
|
||||
transform(transl, rot);
|
||||
}
|
||||
};
|
||||
|
||||
extern template class ArrangeableFullModel<Model, ModelDuplicate, VirtualBedHandler>;
|
||||
extern template class ArrangeableFullModel<const Model, const ModelDuplicate, const VirtualBedHandler>;
|
||||
|
||||
// An implementation of the ArrangeableModel to be used for the full model 'duplicate' feature
|
||||
// accessible from CLI
|
||||
class DuplicableModel: public ArrangeableModel {
|
||||
AnyPtr<Model> m_model;
|
||||
AnyPtr<VirtualBedHandler> m_vbh;
|
||||
std::vector<ModelDuplicate> m_duplicates;
|
||||
BoundingBox m_bedbb;
|
||||
|
||||
template<class Self, class Fn>
|
||||
static void visit_arrangeable_(Self &&self, const ObjectID &id, Fn &&fn)
|
||||
{
|
||||
if (id.valid()) {
|
||||
size_t idx = id.id - 1;
|
||||
if (idx < self.m_duplicates.size()) {
|
||||
auto &md = self.m_duplicates[idx];
|
||||
ArrangeableFullModel arrbl{self.m_model.get(), &md, self.m_vbh.get()};
|
||||
fn(arrbl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
explicit DuplicableModel(AnyPtr<Model> mdl,
|
||||
AnyPtr<VirtualBedHandler> vbh,
|
||||
const BoundingBox &bedbb);
|
||||
~DuplicableModel();
|
||||
|
||||
void for_each_arrangeable(std::function<void(Arrangeable &)> fn) override
|
||||
{
|
||||
for (ModelDuplicate &md : m_duplicates) {
|
||||
ArrangeableFullModel arrbl{m_model.get(), &md, m_vbh.get()};
|
||||
fn(arrbl);
|
||||
}
|
||||
}
|
||||
void for_each_arrangeable(std::function<void(const Arrangeable&)> fn) const override
|
||||
{
|
||||
for (const ModelDuplicate &md : m_duplicates) {
|
||||
ArrangeableFullModel arrbl{m_model.get(), &md, m_vbh.get()};
|
||||
fn(arrbl);
|
||||
}
|
||||
}
|
||||
void visit_arrangeable(const ObjectID &id, std::function<void(const Arrangeable &)> fn) const override
|
||||
{
|
||||
visit_arrangeable_(*this, id, fn);
|
||||
}
|
||||
void visit_arrangeable(const ObjectID &id, std::function<void(Arrangeable &)> fn) override
|
||||
{
|
||||
visit_arrangeable_(*this, id, fn);
|
||||
}
|
||||
|
||||
ObjectID add_arrangeable(const ObjectID &prototype_id) override;
|
||||
|
||||
void apply_duplicates();
|
||||
};
|
||||
|
||||
} // namespace arr2
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // SCENEBUILDER_HPP
|
||||
@@ -0,0 +1,113 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef SEGMENTEDRECTANGLEBED_HPP
|
||||
#define SEGMENTEDRECTANGLEBED_HPP
|
||||
|
||||
#include "libslic3r/Arrange/Core/Beds.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
enum class RectPivots {
|
||||
Center, BottomLeft, BottomRight, TopLeft, TopRight
|
||||
};
|
||||
|
||||
template<class T> struct IsSegmentedBed_ : public std::false_type {};
|
||||
template<class T> constexpr bool IsSegmentedBed = IsSegmentedBed_<StripCVRef<T>>::value;
|
||||
|
||||
template<class SegX = void, class SegY = void, class Pivot = void>
|
||||
struct SegmentedRectangleBed {
|
||||
Vec<2, size_t> segments = Vec<2, size_t>::Ones();
|
||||
BoundingBox bb;
|
||||
RectPivots pivot = RectPivots::Center;
|
||||
|
||||
SegmentedRectangleBed() = default;
|
||||
SegmentedRectangleBed(const BoundingBox &bb,
|
||||
size_t segments_x,
|
||||
size_t segments_y,
|
||||
const RectPivots pivot = RectPivots::Center)
|
||||
: segments{segments_x, segments_y}, bb{bb}, pivot{pivot}
|
||||
{}
|
||||
|
||||
size_t segments_x() const noexcept { return segments.x(); }
|
||||
size_t segments_y() const noexcept { return segments.y(); }
|
||||
|
||||
auto alignment() const noexcept { return pivot; }
|
||||
};
|
||||
|
||||
template<size_t SegX, size_t SegY>
|
||||
struct SegmentedRectangleBed<std::integral_constant<size_t, SegX>,
|
||||
std::integral_constant<size_t, SegY>>
|
||||
{
|
||||
BoundingBox bb;
|
||||
RectPivots pivot = RectPivots::Center;
|
||||
|
||||
SegmentedRectangleBed() = default;
|
||||
|
||||
explicit SegmentedRectangleBed(const BoundingBox &b,
|
||||
const RectPivots pivot = RectPivots::Center)
|
||||
: bb{b}
|
||||
{}
|
||||
|
||||
size_t segments_x() const noexcept { return SegX; }
|
||||
size_t segments_y() const noexcept { return SegY; }
|
||||
|
||||
auto alignment() const noexcept { return pivot; }
|
||||
};
|
||||
|
||||
template<size_t SegX, size_t SegY, RectPivots pivot>
|
||||
struct SegmentedRectangleBed<std::integral_constant<size_t, SegX>,
|
||||
std::integral_constant<size_t, SegY>,
|
||||
std::integral_constant<RectPivots, pivot>>
|
||||
{
|
||||
BoundingBox bb;
|
||||
|
||||
SegmentedRectangleBed() = default;
|
||||
|
||||
explicit SegmentedRectangleBed(const BoundingBox &b) : bb{b} {}
|
||||
|
||||
size_t segments_x() const noexcept { return SegX; }
|
||||
size_t segments_y() const noexcept { return SegY; }
|
||||
|
||||
auto alignment() const noexcept { return pivot; }
|
||||
};
|
||||
|
||||
template<class... Args>
|
||||
struct IsSegmentedBed_<SegmentedRectangleBed<Args...>>
|
||||
: public std::true_type {};
|
||||
|
||||
template<class... Args>
|
||||
auto offset(const SegmentedRectangleBed<Args...> &bed, coord_t val_scaled)
|
||||
{
|
||||
auto cpy = bed;
|
||||
cpy.bb.offset(val_scaled);
|
||||
|
||||
return cpy;
|
||||
}
|
||||
|
||||
template<class...Args>
|
||||
auto bounding_box(const SegmentedRectangleBed<Args...> &bed)
|
||||
{
|
||||
return bed.bb;
|
||||
}
|
||||
|
||||
template<class...Args>
|
||||
auto area(const SegmentedRectangleBed<Args...> &bed)
|
||||
{
|
||||
return arr2::area(bed.bb);
|
||||
}
|
||||
|
||||
template<class...Args>
|
||||
ExPolygons to_expolygons(const SegmentedRectangleBed<Args...> &bed)
|
||||
{
|
||||
return to_expolygons(RectangleBed{bed.bb});
|
||||
}
|
||||
|
||||
template<class SegB>
|
||||
struct IsRectangular_<SegB, std::enable_if_t<IsSegmentedBed<SegB>, void>> : public std::true_type
|
||||
{};
|
||||
|
||||
}} // namespace Slic3r::arr2
|
||||
|
||||
#endif // SEGMENTEDRECTANGLEBED_HPP
|
||||
@@ -0,0 +1,85 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGETASK_HPP
|
||||
#define ARRANGETASK_HPP
|
||||
|
||||
#include "libslic3r/Arrange/Arrange.hpp"
|
||||
#include "libslic3r/Arrange/Items/TrafoOnlyArrangeItem.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
struct ArrangeTaskResult : public ArrangeResult
|
||||
{
|
||||
std::vector<TrafoOnlyArrangeItem> items;
|
||||
|
||||
bool apply_on(ArrangeableModel &mdl) override
|
||||
{
|
||||
bool ret = true;
|
||||
for (auto &itm : items) {
|
||||
if (is_arranged(itm))
|
||||
ret = ret && apply_arrangeitem(itm, mdl);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
void add_item(const ArrItem &itm)
|
||||
{
|
||||
items.emplace_back(itm);
|
||||
if (auto id = retrieve_id(itm))
|
||||
imbue_id(items.back(), *id);
|
||||
}
|
||||
|
||||
template<class It>
|
||||
void add_items(const Range<It> &items_range)
|
||||
{
|
||||
for (auto &itm : items_range)
|
||||
add_item(itm);
|
||||
}
|
||||
};
|
||||
|
||||
template<class ArrItem> struct ArrangeTask : public ArrangeTaskBase
|
||||
{
|
||||
struct ArrangeSet
|
||||
{
|
||||
std::vector<ArrItem> selected, unselected;
|
||||
} printable, unprintable;
|
||||
|
||||
ExtendedBed bed;
|
||||
ArrangeSettings settings;
|
||||
|
||||
static std::unique_ptr<ArrangeTask> create(
|
||||
const Scene &sc,
|
||||
const ArrangeableToItemConverter<ArrItem> &converter);
|
||||
|
||||
static std::unique_ptr<ArrangeTask> create(const Scene &sc)
|
||||
{
|
||||
auto conv = ArrangeableToItemConverter<ArrItem>::create(sc);
|
||||
return create(sc, *conv);
|
||||
}
|
||||
|
||||
std::unique_ptr<ArrangeResult> process(Ctl &ctl) override
|
||||
{
|
||||
return process_native(ctl);
|
||||
}
|
||||
|
||||
std::unique_ptr<ArrangeTaskResult> process_native(Ctl &ctl);
|
||||
std::unique_ptr<ArrangeTaskResult> process_native(Ctl &&ctl)
|
||||
{
|
||||
return process_native(ctl);
|
||||
}
|
||||
|
||||
int item_count_to_process() const override
|
||||
{
|
||||
return static_cast<int>(printable.selected.size() +
|
||||
unprintable.selected.size());
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace arr2
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // ARRANGETASK_HPP
|
||||
@@ -0,0 +1,154 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef ARRANGETASK_IMPL_HPP
|
||||
#define ARRANGETASK_IMPL_HPP
|
||||
|
||||
#include <random>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include "ArrangeTask.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
// Prepare the selected and unselected items separately. If nothing is
|
||||
// selected, behaves as if everything would be selected.
|
||||
template<class ArrItem>
|
||||
void extract_selected(ArrangeTask<ArrItem> &task,
|
||||
const ArrangeableModel &mdl,
|
||||
const ArrangeableToItemConverter<ArrItem> &itm_conv)
|
||||
{
|
||||
// Go through the objects and check if inside the selection
|
||||
mdl.for_each_arrangeable(
|
||||
[&task, &itm_conv](const Arrangeable &arrbl) {
|
||||
bool selected = arrbl.is_selected();
|
||||
bool printable = arrbl.is_printable();
|
||||
|
||||
try {
|
||||
auto itm = itm_conv.convert(arrbl, selected ? 0 : -SCALED_EPSILON);
|
||||
|
||||
auto &container_parent = printable ? task.printable :
|
||||
task.unprintable;
|
||||
|
||||
auto &container = selected ?
|
||||
container_parent.selected :
|
||||
container_parent.unselected;
|
||||
|
||||
container.emplace_back(std::move(itm));
|
||||
} catch (const EmptyItemOutlineError &ex) {
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< "ObjectID " << std::to_string(arrbl.id().id) << ": " << ex.what();
|
||||
}
|
||||
});
|
||||
|
||||
// If the selection was empty arrange everything
|
||||
if (task.printable.selected.empty() && task.unprintable.selected.empty()) {
|
||||
task.printable.selected.swap(task.printable.unselected);
|
||||
task.unprintable.selected.swap(task.unprintable.unselected);
|
||||
}
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
std::unique_ptr<ArrangeTask<ArrItem>> ArrangeTask<ArrItem>::create(
|
||||
const Scene &sc, const ArrangeableToItemConverter<ArrItem> &converter)
|
||||
{
|
||||
auto task = std::make_unique<ArrangeTask<ArrItem>>();
|
||||
|
||||
task->settings.set_from(sc.settings());
|
||||
|
||||
task->bed = get_corrected_bed(sc.bed(), converter);
|
||||
|
||||
extract_selected(*task, sc.model(), converter);
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
// Remove all items on the physical bed (not occupyable for unprintable items)
|
||||
// and shift all items to the next lower bed index, so that arrange will think
|
||||
// that logical bed no. 1 is the physical one
|
||||
template<class ItemCont>
|
||||
void prepare_fixed_unselected(ItemCont &items, int shift)
|
||||
{
|
||||
for (auto &itm : items)
|
||||
set_bed_index(itm, get_bed_index(itm) - shift);
|
||||
|
||||
items.erase(std::remove_if(items.begin(), items.end(),
|
||||
[](auto &itm) { return !is_arranged(itm); }),
|
||||
items.end());
|
||||
}
|
||||
|
||||
inline int find_first_empty_bed(const std::vector<int>& bed_indices,
|
||||
int starting_from = 0) {
|
||||
int ret = starting_from;
|
||||
|
||||
for (int idx : bed_indices) {
|
||||
if (idx == ret) {
|
||||
ret++;
|
||||
} else if (idx > ret) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
std::unique_ptr<ArrangeTaskResult>
|
||||
ArrangeTask<ArrItem>::process_native(Ctl &ctl)
|
||||
{
|
||||
auto result = std::make_unique<ArrangeTaskResult>();
|
||||
|
||||
auto arranger = Arranger<ArrItem>::create(settings);
|
||||
|
||||
class TwoStepArrangeCtl: public Ctl
|
||||
{
|
||||
Ctl &parent;
|
||||
ArrangeTask &self;
|
||||
public:
|
||||
TwoStepArrangeCtl(Ctl &p, ArrangeTask &slf) : parent{p}, self{slf} {}
|
||||
|
||||
void update_status(int remaining) override
|
||||
{
|
||||
parent.update_status(remaining + self.unprintable.selected.size());
|
||||
}
|
||||
|
||||
bool was_canceled() const override { return parent.was_canceled(); }
|
||||
|
||||
} subctl{ctl, *this};
|
||||
|
||||
arranger->arrange(printable.selected, printable.unselected, bed, subctl);
|
||||
|
||||
std::vector<int> printable_bed_indices =
|
||||
get_bed_indices(crange(printable.selected), crange(printable.unselected));
|
||||
|
||||
// If there are no printables, leave the physical bed empty
|
||||
static constexpr int SearchFrom = 1;
|
||||
|
||||
// Unprintable items should go to the first logical (!) bed not containing
|
||||
// any printable items
|
||||
int first_empty_bed = find_first_empty_bed(printable_bed_indices, SearchFrom);
|
||||
|
||||
prepare_fixed_unselected(unprintable.unselected, first_empty_bed);
|
||||
|
||||
arranger->arrange(unprintable.selected, unprintable.unselected, bed, ctl);
|
||||
|
||||
result->add_items(crange(printable.selected));
|
||||
|
||||
for (auto &itm : unprintable.selected) {
|
||||
if (is_arranged(itm)) {
|
||||
int bedidx = get_bed_index(itm) + first_empty_bed;
|
||||
arr2::set_bed_index(itm, bedidx);
|
||||
}
|
||||
|
||||
result->add_item(itm);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace arr2
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif //ARRANGETASK_IMPL_HPP
|
||||
@@ -0,0 +1,61 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef FILLBEDTASK_HPP
|
||||
#define FILLBEDTASK_HPP
|
||||
|
||||
#include "MultiplySelectionTask.hpp"
|
||||
|
||||
#include "libslic3r/Arrange/Arrange.hpp"
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
struct FillBedTaskResult: public MultiplySelectionTaskResult {};
|
||||
|
||||
template<class ArrItem>
|
||||
struct FillBedTask: public ArrangeTaskBase
|
||||
{
|
||||
std::optional<ArrItem> prototype_item;
|
||||
|
||||
std::vector<ArrItem> selected, unselected;
|
||||
|
||||
// For workaround regarding "holes" when filling the bed with the same
|
||||
// item's copies
|
||||
std::vector<ArrItem> selected_fillers;
|
||||
|
||||
ArrangeSettings settings;
|
||||
ExtendedBed bed;
|
||||
size_t selected_existing_count = 0;
|
||||
|
||||
std::unique_ptr<FillBedTaskResult> process_native(Ctl &ctl);
|
||||
std::unique_ptr<FillBedTaskResult> process_native(Ctl &&ctl)
|
||||
{
|
||||
return process_native(ctl);
|
||||
}
|
||||
|
||||
std::unique_ptr<ArrangeResult> process(Ctl &ctl) override
|
||||
{
|
||||
return process_native(ctl);
|
||||
}
|
||||
|
||||
int item_count_to_process() const override
|
||||
{
|
||||
return selected.size();
|
||||
}
|
||||
|
||||
static std::unique_ptr<FillBedTask> create(
|
||||
const Scene &sc,
|
||||
const ArrangeableToItemConverter<ArrItem> &converter);
|
||||
|
||||
static std::unique_ptr<FillBedTask> create(const Scene &sc)
|
||||
{
|
||||
auto conv = ArrangeableToItemConverter<ArrItem>::create(sc);
|
||||
return create(sc, *conv);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace arr2
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // FILLBEDTASK_HPP
|
||||
@@ -0,0 +1,211 @@
|
||||
///|/ Copyright (c) Prusa Research 2023 Tomáš Mészáros @tamasmeszaros
|
||||
///|/
|
||||
///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher
|
||||
///|/
|
||||
#ifndef FILLBEDTASKIMPL_HPP
|
||||
#define FILLBEDTASKIMPL_HPP
|
||||
|
||||
#include "FillBedTask.hpp"
|
||||
|
||||
#include "Arrange/Core/NFP/NFPArrangeItemTraits.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
namespace Slic3r { namespace arr2 {
|
||||
|
||||
template<class ArrItem>
|
||||
int calculate_items_needed_to_fill_bed(const ExtendedBed &bed,
|
||||
const ArrItem &prototype_item,
|
||||
size_t prototype_count,
|
||||
const std::vector<ArrItem> &fixed)
|
||||
{
|
||||
double poly_area = fixed_area(prototype_item);
|
||||
|
||||
auto area_sum_fn = [](double s, const auto &itm) {
|
||||
return s + (get_bed_index(itm) == 0) * fixed_area(itm);
|
||||
};
|
||||
|
||||
double unsel_area = std::accumulate(fixed.begin(),
|
||||
fixed.end(),
|
||||
0.,
|
||||
area_sum_fn);
|
||||
|
||||
double fixed_area = unsel_area + prototype_count * poly_area;
|
||||
double bed_area = 0.;
|
||||
|
||||
visit_bed([&bed_area] (auto &realbed) { bed_area = area(realbed); }, bed);
|
||||
|
||||
// This is the maximum number of items,
|
||||
// the real number will always be close but less.
|
||||
auto needed_items = static_cast<int>(
|
||||
std::ceil((bed_area - fixed_area) / poly_area));
|
||||
|
||||
return needed_items;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
void extract(FillBedTask<ArrItem> &task,
|
||||
const Scene &scene,
|
||||
const ArrangeableToItemConverter<ArrItem> &itm_conv)
|
||||
{
|
||||
task.prototype_item = {};
|
||||
|
||||
auto selected_ids = scene.selected_ids();
|
||||
|
||||
if (selected_ids.empty())
|
||||
return;
|
||||
|
||||
std::set<ObjectID> selected_objects = selected_geometry_ids(scene);
|
||||
|
||||
if (selected_objects.size() != 1)
|
||||
return;
|
||||
|
||||
ObjectID prototype_geometry_id = *(selected_objects.begin());
|
||||
|
||||
auto set_prototype_item = [&task, &itm_conv](const Arrangeable &arrbl) {
|
||||
if (arrbl.is_printable())
|
||||
task.prototype_item = itm_conv.convert(arrbl);
|
||||
};
|
||||
|
||||
scene.model().visit_arrangeable(selected_ids.front(), set_prototype_item);
|
||||
|
||||
if (!task.prototype_item)
|
||||
return;
|
||||
|
||||
// Workaround for missing items when arranging the same geometry only:
|
||||
// Injecting a number of items but with slightly shrinked shape, so that
|
||||
// they can fill the emerging holes.
|
||||
ArrItem prototype_item_shrinked;
|
||||
scene.model().visit_arrangeable(selected_ids.front(),
|
||||
[&prototype_item_shrinked, &itm_conv](const Arrangeable &arrbl) {
|
||||
if (arrbl.is_printable())
|
||||
prototype_item_shrinked = itm_conv.convert(arrbl, -SCALED_EPSILON);
|
||||
});
|
||||
|
||||
set_bed_index(*task.prototype_item, Unarranged);
|
||||
|
||||
auto collect_task_items = [&prototype_geometry_id, &task,
|
||||
&itm_conv](const Arrangeable &arrbl) {
|
||||
try {
|
||||
if (arrbl.geometry_id() == prototype_geometry_id) {
|
||||
if (arrbl.is_printable()) {
|
||||
auto itm = itm_conv.convert(arrbl);
|
||||
raise_priority(itm);
|
||||
task.selected.emplace_back(std::move(itm));
|
||||
}
|
||||
} else {
|
||||
auto itm = itm_conv.convert(arrbl, -SCALED_EPSILON);
|
||||
task.unselected.emplace_back(std::move(itm));
|
||||
}
|
||||
} catch (const EmptyItemOutlineError &ex) {
|
||||
BOOST_LOG_TRIVIAL(error)
|
||||
<< "ObjectID " << std::to_string(arrbl.id().id) << ": " << ex.what();
|
||||
}
|
||||
};
|
||||
|
||||
scene.model().for_each_arrangeable(collect_task_items);
|
||||
|
||||
int needed_items = calculate_items_needed_to_fill_bed(task.bed,
|
||||
*task.prototype_item,
|
||||
task.selected.size(),
|
||||
task.unselected);
|
||||
|
||||
task.selected_existing_count = task.selected.size();
|
||||
task.selected.reserve(task.selected.size() + needed_items);
|
||||
std::fill_n(std::back_inserter(task.selected), needed_items,
|
||||
*task.prototype_item);
|
||||
|
||||
// Add as many filler items as there are needed items. Most of them will
|
||||
// be discarded anyways.
|
||||
std::fill_n(std::back_inserter(task.selected_fillers), needed_items,
|
||||
prototype_item_shrinked);
|
||||
}
|
||||
|
||||
|
||||
template<class ArrItem>
|
||||
std::unique_ptr<FillBedTask<ArrItem>> FillBedTask<ArrItem>::create(
|
||||
const Scene &sc, const ArrangeableToItemConverter<ArrItem> &converter)
|
||||
{
|
||||
auto task = std::make_unique<FillBedTask<ArrItem>>();
|
||||
|
||||
task->settings.set_from(sc.settings());
|
||||
|
||||
task->bed = get_corrected_bed(sc.bed(), converter);
|
||||
|
||||
extract(*task, sc, converter);
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
template<class ArrItem>
|
||||
std::unique_ptr<FillBedTaskResult> FillBedTask<ArrItem>::process_native(
|
||||
Ctl &ctl)
|
||||
{
|
||||
auto result = std::make_unique<FillBedTaskResult>();
|
||||
|
||||
if (!prototype_item)
|
||||
return result;
|
||||
|
||||
result->prototype_id = retrieve_id(*prototype_item).value_or(ObjectID{});
|
||||
|
||||
class FillBedCtl: public ArrangerCtl<ArrItem>
|
||||
{
|
||||
ArrangeTaskCtl &parent;
|
||||
FillBedTask &self;
|
||||
bool do_stop = false;
|
||||
|
||||
public:
|
||||
FillBedCtl(ArrangeTaskCtl &p, FillBedTask &slf) : parent{p}, self{slf} {}
|
||||
|
||||
void update_status(int remaining) override
|
||||
{
|
||||
parent.update_status(remaining);
|
||||
}
|
||||
|
||||
bool was_canceled() const override
|
||||
{
|
||||
return parent.was_canceled() || do_stop;
|
||||
}
|
||||
|
||||
void on_packed(ArrItem &itm) override
|
||||
{
|
||||
// Stop at the first filler that is not on the physical bed
|
||||
do_stop = get_bed_index(itm) > PhysicalBedId && get_priority(itm) == 0;
|
||||
}
|
||||
|
||||
} subctl(ctl, *this);
|
||||
|
||||
auto arranger = Arranger<ArrItem>::create(settings);
|
||||
|
||||
arranger->arrange(selected, unselected, bed, subctl);
|
||||
|
||||
auto unsel_cpy = unselected;
|
||||
for (const auto &itm : selected) {
|
||||
unsel_cpy.emplace_back(itm);
|
||||
}
|
||||
|
||||
arranger->arrange(selected_fillers, unsel_cpy, bed, FillBedCtl{ctl, *this});
|
||||
|
||||
auto arranged_range = Range{selected.begin(),
|
||||
selected.begin() + selected_existing_count};
|
||||
|
||||
result->add_arranged_items(arranged_range);
|
||||
|
||||
auto to_add_range = Range{selected.begin() + selected_existing_count,
|
||||
selected.end()};
|
||||
|
||||
for (auto &itm : to_add_range)
|
||||
if (get_bed_index(itm) == PhysicalBedId)
|
||||
result->add_new_item(itm);
|
||||
|
||||
for (auto &itm : selected_fillers)
|
||||
if (get_bed_index(itm) == PhysicalBedId)
|
||||
result->add_new_item(itm);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace arr2
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // FILLBEDTASKIMPL_HPP
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user