PrusaSlicer-NonPlainar/src/slic3r/GUI/Jobs/ArrangeJob.cpp
2023-05-31 18:10:12 +02:00

445 lines
14 KiB
C++

#include "ArrangeJob.hpp"
#include "libslic3r/BuildVolume.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/SLAPrint.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI_ObjectManipulation.hpp"
#include "slic3r/GUI/NotificationManager.hpp"
#include "slic3r/GUI/format.hpp"
#include "libnest2d/common.hpp"
#include <numeric>
#include <random>
namespace Slic3r { namespace GUI {
// Cache the wti info
class WipeTower: public GLCanvas3D::WipeTowerInfo {
using ArrangePolygon = arrangement::ArrangePolygon;
public:
explicit WipeTower(const GLCanvas3D::WipeTowerInfo &wti)
: GLCanvas3D::WipeTowerInfo(wti)
{}
explicit WipeTower(GLCanvas3D::WipeTowerInfo &&wti)
: GLCanvas3D::WipeTowerInfo(std::move(wti))
{}
void apply_arrange_result(const Vec2d& tr, double rotation)
{
m_pos = unscaled(tr); m_rotation = rotation;
apply_wipe_tower();
}
ArrangePolygon get_arrange_polygon() const
{
Polygon ap({
{scaled(m_bb.min)},
{scaled(m_bb.max.x()), scaled(m_bb.min.y())},
{scaled(m_bb.max)},
{scaled(m_bb.min.x()), scaled(m_bb.max.y())}
});
ArrangePolygon ret;
ret.poly.contour = std::move(ap);
ret.translation = scaled(m_pos);
ret.rotation = m_rotation;
++ret.priority;
return ret;
}
};
static WipeTower get_wipe_tower(const Plater &plater)
{
return WipeTower{plater.canvas3D()->get_wipe_tower_info()};
}
void ArrangeJob::clear_input()
{
const Model &model = m_plater->model();
size_t count = 0, cunprint = 0; // To know how much space to reserve
for (auto obj : model.objects)
for (auto mi : obj->instances)
mi->printable ? count++ : cunprint++;
m_selected.clear();
m_unselected.clear();
m_unprintable.clear();
m_unarranged.clear();
m_selected.reserve(count + 1 /* for optional wti */);
m_unselected.reserve(count + 1 /* for optional wti */);
m_unprintable.reserve(cunprint /* for optional wti */);
}
void ArrangeJob::prepare_all() {
clear_input();
for (ModelObject *obj: m_plater->model().objects)
for (ModelInstance *mi : obj->instances) {
ArrangePolygons & cont = mi->printable ? m_selected : m_unprintable;
cont.emplace_back(get_arrange_poly_(mi));
}
if (auto wti = get_wipe_tower_arrangepoly(*m_plater))
m_selected.emplace_back(std::move(*wti));
}
void ArrangeJob::prepare_selected() {
clear_input();
Model &model = m_plater->model();
double stride = bed_stride(m_plater);
std::vector<const Selection::InstanceIdxsList *>
obj_sel(model.objects.size(), nullptr);
for (auto &s : m_plater->get_selection().get_content())
if (s.first < int(obj_sel.size()))
obj_sel[size_t(s.first)] = &s.second;
// Go through the objects and check if inside the selection
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) {
const Selection::InstanceIdxsList * instlist = obj_sel[oidx];
ModelObject *mo = model.objects[oidx];
std::vector<bool> inst_sel(mo->instances.size(), false);
if (instlist)
for (auto inst_id : *instlist)
inst_sel[size_t(inst_id)] = true;
for (size_t i = 0; i < inst_sel.size(); ++i) {
ModelInstance * mi = mo->instances[i];
ArrangePolygon &&ap = get_arrange_poly_(mi);
ArrangePolygons &cont = mo->instances[i]->printable ?
(inst_sel[i] ? m_selected :
m_unselected) :
m_unprintable;
cont.emplace_back(std::move(ap));
}
}
if (auto wti = get_wipe_tower(*m_plater)) {
ArrangePolygon &&ap = get_arrange_poly(wti, m_plater);
auto &cont = m_plater->get_selection().is_wipe_tower() ? m_selected :
m_unselected;
cont.emplace_back(std::move(ap));
}
// If the selection was empty arrange everything
if (m_selected.empty())
m_selected.swap(m_unselected);
// The strides have to be removed from the fixed items. For the
// arrangeable (selected) items bed_idx is ignored and the
// translation is irrelevant.
for (auto &p : m_unselected)
p.translation(X) -= p.bed_idx * stride;
}
static void update_arrangepoly_slaprint(arrangement::ArrangePolygon &ret,
const SLAPrintObject &po,
const ModelInstance &inst)
{
// The 1.1 multiplier is a safety gap, as the offset might be bigger
// in sharp edges of a polygon, depending on clipper's offset algorithm
coord_t pad_infl = 0;
{
double infl = po.config().pad_enable.getBool() * (
po.config().pad_brim_size.getFloat() +
po.config().pad_around_object.getBool() *
po.config().pad_object_gap.getFloat() );
pad_infl = scaled(1.1 * infl);
}
auto laststep = po.last_completed_step();
if (laststep < slaposCount && laststep > slaposSupportTree) {
auto omesh = po.get_mesh_to_print();
auto &smesh = po.support_mesh();
Vec3d rotation = inst.get_rotation();
rotation.z() = 0.;
Transform3f trafo_instance =
Geometry::assemble_transform(inst.get_offset().z() * Vec3d::UnitZ(),
rotation,
inst.get_scaling_factor(),
inst.get_mirror()).cast<float>();
trafo_instance = trafo_instance * po.trafo().cast<float>().inverse();
Polygons polys;
polys.reserve(3);
auto zlvl = -po.get_elevation();
if (omesh) {
polys.emplace_back(its_convex_hull_2d_above(*omesh, trafo_instance, zlvl));
ret.poly.contour = polys.back();
ret.poly.holes = {};
}
polys.emplace_back(its_convex_hull_2d_above(smesh.its, trafo_instance, zlvl));
ret.poly.contour = Geometry::convex_hull(polys);
ret.poly.holes = {};
}
ret.inflation = pad_infl;
}
static coord_t brim_offset(const PrintObject &po, const ModelInstance &inst)
{
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;
}
arrangement::ArrangePolygon ArrangeJob::get_arrange_poly_(ModelInstance *mi)
{
arrangement::ArrangePolygon ap = get_arrange_poly(mi, m_plater);
auto setter = ap.setter;
ap.setter = [this, setter, mi](const arrangement::ArrangePolygon &set_ap) {
setter(set_ap);
if (!set_ap.is_arranged())
m_unarranged.emplace_back(mi);
};
return ap;
}
coord_t get_skirt_offset(const Plater* plater) {
float skirt_inset = 0.f;
// Try to subtract the skirt from the bed shape so we don't arrange outside of it.
if (plater->printer_technology() == ptFFF && plater->fff_print().has_skirt()) {
const auto& print = plater->fff_print();
if (!print.objects().empty()) {
skirt_inset = print.config().skirts.value * print.skirt_flow().width() +
print.config().skirt_distance.value;
}
}
return scaled(skirt_inset);
}
void ArrangeJob::prepare()
{
m_selection_only ? prepare_selected() : prepare_all();
coord_t min_offset = 0;
for (auto &ap : m_selected) {
min_offset = std::max(ap.inflation, min_offset);
}
if (m_plater->printer_technology() == ptSLA) {
// Apply the max offset for all the objects
for (auto &ap : m_selected) {
ap.inflation = min_offset;
}
} else { // it's fff, brims only need to be minded from bed edges
for (auto &ap : m_selected) {
ap.inflation = 0;
}
m_min_bed_inset = min_offset;
}
}
void ArrangeJob::process(Ctl &ctl)
{
static const auto arrangestr = _u8L("Arranging");
arrangement::ArrangeParams params;
arrangement::ArrangeBed bed;
ctl.call_on_main_thread([this, &params, &bed]{
prepare();
params = get_arrange_params(m_plater);
get_bed_shape(*m_plater->config(), bed);
coord_t min_inset = get_skirt_offset(m_plater) + m_min_bed_inset;
params.min_bed_distance = std::max(params.min_bed_distance, min_inset);
}).wait();
auto count = unsigned(m_selected.size() + m_unprintable.size());
if (count == 0) // Should be taken care of by plater, but doesn't hurt
return;
ctl.update_status(0, arrangestr);
params.stopcondition = [&ctl]() { return ctl.was_canceled(); };
params.progressind = [this, count, &ctl](unsigned st) {
st += m_unprintable.size();
if (st > 0) ctl.update_status(int(count - st) * 100 / status_range(), arrangestr);
};
ctl.update_status(0, arrangestr);
arrangement::arrange(m_selected, m_unselected, bed, params);
params.progressind = [this, count, &ctl](unsigned st) {
if (st > 0) ctl.update_status(int(count - st) * 100 / status_range(), arrangestr);
};
arrangement::arrange(m_unprintable, {}, bed, params);
// finalize just here.
ctl.update_status(int(count) * 100 / status_range(), ctl.was_canceled() ?
_u8L("Arranging canceled.") :
_u8L("Arranging done."));
}
ArrangeJob::ArrangeJob(Mode mode)
: m_plater{wxGetApp().plater()},
m_selection_only{mode == Mode::SelectionOnly}
{}
static std::string concat_strings(const std::set<std::string> &strings,
const std::string &delim = "\n")
{
return std::accumulate(
strings.begin(), strings.end(), std::string(""),
[delim](const std::string &s, const std::string &name) {
return s + name + delim;
});
}
void ArrangeJob::finalize(bool canceled, std::exception_ptr &eptr) {
try {
if (eptr)
std::rethrow_exception(eptr);
} catch (libnest2d::GeometryException &) {
show_error(m_plater, _(L("Could not arrange model objects! "
"Some geometries may be invalid.")));
eptr = nullptr;
} catch(...) {
eptr = std::current_exception();
}
if (canceled || eptr)
return;
// Unprintable items go to the last virtual bed
int beds = 0;
// Apply the arrange result to all selected objects
for (ArrangePolygon &ap : m_selected) {
beds = std::max(ap.bed_idx, beds);
ap.apply();
}
// Get the virtual beds from the unselected items
for (ArrangePolygon &ap : m_unselected)
beds = std::max(ap.bed_idx, beds);
// Move the unprintable items to the last virtual bed.
for (ArrangePolygon &ap : m_unprintable) {
if (ap.bed_idx >= 0)
ap.bed_idx += beds + 1;
ap.apply();
}
m_plater->update((unsigned int)Plater::UpdateParams::FORCE_FULL_SCREEN_REFRESH);
wxGetApp().obj_manipul()->set_dirty();
if (!m_unarranged.empty()) {
std::set<std::string> names;
for (ModelInstance *mi : m_unarranged)
names.insert(mi->get_object()->name);
m_plater->get_notification_manager()->push_notification(GUI::format(
_L("Arrangement ignored the following objects which can't fit into a single bed:\n%s"),
concat_strings(names, "\n")));
}
}
std::optional<arrangement::ArrangePolygon>
get_wipe_tower_arrangepoly(const Plater &plater)
{
if (auto wti = get_wipe_tower(plater))
return get_arrange_poly(wti, &plater);
return {};
}
double bed_stride(const Plater *plater) {
double bedwidth = plater->build_volume().bounding_volume().size().x();
return scaled<double>((1. + LOGICAL_BED_GAP) * bedwidth);
}
template<>
arrangement::ArrangePolygon get_arrange_poly(ModelInstance *inst,
const Plater * plater)
{
auto ap = get_arrange_poly(PtrWrapper{inst}, plater);
auto obj_id = inst->get_object()->id();
if (plater->printer_technology() == ptSLA) {
const SLAPrintObject *po =
plater->sla_print().get_print_object_by_model_object_id(obj_id);
if (po) {
update_arrangepoly_slaprint(ap, *po, *inst);
}
} else {
const PrintObject *po =
plater->fff_print().get_print_object_by_model_object_id(obj_id);
if (po) {
ap.inflation = brim_offset(*po, *inst);
}
}
return ap;
}
arrangement::ArrangeParams get_arrange_params(Plater *p)
{
const GLCanvas3D::ArrangeSettings &settings =
p->canvas3D()->get_arrange_settings();
arrangement::ArrangeParams params;
params.allow_rotations = settings.enable_rotation;
params.min_obj_distance = scaled(settings.distance);
params.min_bed_distance = scaled(settings.distance_from_bed);
arrangement::Pivots pivot = arrangement::Pivots::Center;
int pivot_max = static_cast<int>(arrangement::Pivots::TopRight);
if (settings.alignment < 0) {
pivot = arrangement::Pivots::Center;
} else if (settings.alignment > pivot_max) {
// means it should be random
std::random_device rd{};
std::mt19937 rng(rd());
std::uniform_int_distribution<std::mt19937::result_type> dist(0, pivot_max);
pivot = static_cast<arrangement::Pivots>(dist(rng));
} else {
pivot = static_cast<arrangement::Pivots>(settings.alignment);
}
params.alignment = pivot;
return params;
}
}} // namespace Slic3r::GUI