Public source code release

This commit is contained in:
YTKAB0BP
2024-11-01 08:28:55 +03:00
parent 20b730b1c8
commit 0b2ba24c7f
6691 changed files with 2325292 additions and 1 deletions
+326
View File
@@ -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
+146
View File
@@ -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 RayBox 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 &centroid() 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_ */
+197
View File
@@ -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 &params)
{
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 &params)
{
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 &params)
{
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 &params)
{
// 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 &params);
std::vector<RegionExpansion> propagate_waves(const ExPolygons &src, const ExPolygons &boundary, const RegionExpansionParameters &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);
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 &params);
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 &params);
} // Algorithm
} // Slic3r
#endif /* SRC_LIBSLIC3R_ALGORITHM_REGION_EXPANSION_HPP_ */
+170
View File
@@ -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
+778
View File
@@ -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 &section : 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 &section, const std::string &key)
{
if (auto it_storage = m_storage.find(section); it_storage != m_storage.end()) {
auto &section = 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 &section, 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 &section)
{
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
+210
View File
@@ -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 &section, 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 &section, const std::string &key) const
{ std::string value; this->get(section, key, value); return value; }
bool get_bool(const std::string &section, 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 &section, 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 &section, 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 &section, const std::string &key);
bool has_section(const std::string &section) const
{ return m_storage.find(section) != m_storage.end(); }
const std::map<std::string, std::string>& get_section(const std::string &section) const
{ auto it = m_storage.find(section); assert(it != m_storage.end()); return it->second; }
bool set_section(const std::string &section, std::map<std::string, std::string> data);
bool clear_section(const std::string &section);
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
+780
View File
@@ -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 &params) {
// 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 &params,
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 &params,
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 &center, 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 &params);
template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const CircleBed &bed, const ArrangeParams &params);
template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const Polygon &bed, const ArrangeParams &params);
template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const InfiniteBed &bed, const ArrangeParams &params);
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 &params)
{
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
+220
View File
@@ -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 &params = {});
// 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 &params);
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const BoundingBox &bed, const ArrangeParams &params);
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const CircleBed &bed, const ArrangeParams &params);
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const Polygon &bed, const ArrangeParams &params);
extern template void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const InfiniteBed &bed, const ArrangeParams &params);
inline void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const RectangleBed &bed, const ArrangeParams &params)
{
arrange(items, excludes, bed.bb, params);
}
inline void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const IrregularBed &bed, const ArrangeParams &params)
{
arrange(items, excludes, bed.poly.contour, params);
}
void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const SegmentedRectangleBed &bed, const ArrangeParams &params);
inline void arrange(ArrangePolygons &items, const ArrangePolygons &excludes, const ArrangeBed &bed, const ArrangeParams &params)
{
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 &params = {}) { arrange(items, {}, bed, params); }
inline void arrange(ArrangePolygons &items, const BoundingBox &bed, const ArrangeParams &params = {}) { arrange(items, {}, bed, params); }
inline void arrange(ArrangePolygons &items, const CircleBed &bed, const ArrangeParams &params = {}) { arrange(items, {}, bed, params); }
inline void arrange(ArrangePolygons &items, const Polygon &bed, const ArrangeParams &params = {}) { arrange(items, {}, bed, params); }
inline void arrange(ArrangePolygons &items, const InfiniteBed &bed, const ArrangeParams &params = {}) { 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 &center, 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