Merge upstream main: belt printing, texture displacement color mixing, 3MF component cycle checks, undo blocked during background jobs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Clifford GarwoodandClaude Opus 5.5 committed 2026-10-09 07:44:12 -04:00
commit f970a607ae
227 files changed
+14506 -1079

No files matched your search

+97
View File
@@ -38,6 +38,11 @@
#include <exception>
#include "libslic3r/LifecycleEvents.hpp"
#include <optional>
#include "libslic3r/Geometry.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Config.hpp"
#include <cmath>
#include <string>
#define SAVE_ARRANGE_POLY 0
@@ -86,6 +91,14 @@ public:
}
};
// The belt purge prism is generated from the arranged parts (ensure_belt_purge_tower),
// so arrange neither moves it nor packs around it; it reserves the prism's strip instead.
static bool is_belt_purge_prism(const ModelObject *mo)
{
const ConfigOption *opt = mo->config.option("belt_purge_tower_object");
return opt != nullptr && opt->getBool();
}
// BBS: add partplate logic
static WipeTower get_wipe_tower(const Plater &plater, int plate_idx)
{
@@ -100,6 +113,63 @@ arrangement::ArrangePolygon get_wipetower_arrange_poly(WipeTower* tower)
return ap;
}
// Belt printers pack their parts against the edges of the bed, so what has to stay
// free there is reserved with fixed virtual items on every plate, the way the bed's
// own exclusion areas are:
// - the strip the purge prism comes back to, flush with the far lateral edge (see
// ensure_belt_purge_tower), when the parts use more than one filament;
// - the brim along every edge: a belt brim is printed brim_width wide for every brim
// type but none. Between parts the brims may overlap, as on any printer.
void ArrangeJob::prepare_belt_regions(int num_plates)
{
if (!params.is_belt || params.is_seq_print)
return;
const DynamicPrintConfig &config = wxGetApp().preset_bundle->full_config();
const BoundingBoxf bed = get_extents(config.opt<ConfigOptionPoints>("printable_area")->values);
const bool belt_is_y = params.belt_axis == 1;
std::vector<BoundingBoxf> regions;
std::set<int> filaments;
for (const ArrangePolygons *items : { &m_selected, &m_unselected })
for (const ArrangePolygon &ap : *items)
if (!ap.is_virt_object)
filaments.insert(ap.extrude_ids.begin(), ap.extrude_ids.end());
if (config.opt_bool("enable_belt_purge_tower") && filaments.size() > 1) {
// The prism is at least one millimetre per island plus the gaps between them.
const int islands = int(filaments.size()) - 1;
const double width = std::max(config.opt_float("belt_purge_tower_width"), 2. * islands - 1.) + 1.; // + the prism's edge inset
BoundingBoxf strip = bed;
if (belt_is_y)
strip.min.x() = std::max(bed.min.x(), bed.max.x() - width);
else
strip.min.y() = std::max(bed.min.y(), bed.max.y() - width);
regions.push_back(strip);
}
// Virtual items are inflated by the one millimetre exclusion gap already.
const double brim = config.opt_enum<BrimType>("brim_type") == btNoBrim ? 0. :
config.opt_float("brim_width") + config.opt_float("brim_object_gap") + config.opt_float("extra_brim_width") - 1.;
if (brim > 0.) {
regions.emplace_back(bed.min, Vec2d(bed.min.x() + brim, bed.max.y()));
regions.emplace_back(Vec2d(bed.max.x() - brim, bed.min.y()), bed.max);
regions.emplace_back(bed.min, Vec2d(bed.max.x(), bed.min.y() + brim));
regions.emplace_back(Vec2d(bed.min.x(), bed.max.y() - brim), bed.max);
}
for (int j = 0; j < num_plates; ++j)
for (size_t i = 0; i < regions.size(); ++i) {
ArrangePolygon ap;
ap.poly.contour = scaled(regions[i]).polygon();
ap.translation = Vec2crd(0, 0);
ap.rotation = 0.f;
ap.is_virt_object = true;
ap.bed_idx = j;
ap.height = 1;
ap.name = "BeltRegion" + std::to_string(i);
m_unselected.emplace_back(std::move(ap));
}
}
void ArrangeJob::clear_input()
{
const Model &model = m_plater->model();
@@ -151,6 +221,8 @@ void ArrangeJob::prepare_selected() {
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) {
const Selection::InstanceIdxsList* instlist = obj_sel[oidx];
ModelObject* mo = model.objects[oidx];
if (is_belt_purge_prism(mo))
continue;
std::vector<bool> inst_sel(mo->instances.size(), false);
@@ -199,6 +271,7 @@ void ArrangeJob::prepare_selected() {
}
prepare_wipe_tower();
prepare_belt_regions(MAX_NUM_PLATES);
// The strides have to be removed from the fixed items. For the
@@ -229,6 +302,8 @@ void ArrangeJob::prepare_all() {
// Go through the objects and check if inside the selection
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) {
ModelObject *mo = model.objects[oidx];
if (is_belt_purge_prism(mo))
continue;
for (size_t i = 0; i < mo->instances.size(); ++i) {
ModelInstance * mi = mo->instances[i];
@@ -275,6 +350,7 @@ void ArrangeJob::prepare_all() {
// add the virtual object into unselect list if has
plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, MAX_NUM_PLATES);
prepare_belt_regions(MAX_NUM_PLATES);
}
arrangement::ArrangePolygon estimate_wipe_tower_info(int plate_index, std::set<int>& extruder_ids)
@@ -313,6 +389,11 @@ void ArrangeJob::prepare_wipe_tower()
bool enable_prime_tower = op && op->getBool();
if (!enable_prime_tower || params.is_seq_print) return;
// Belt printers have no classic wipe tower; purging goes into the belt purge
// prism, whose strip prepare_belt_regions() reserves.
if (params.is_belt)
return;
bool smooth_timelapse = false;
auto sop = current_config.option("timelapse_type");
if (sop) { smooth_timelapse = sop->getInt() == TimelapseType::tlSmooth; }
@@ -416,6 +497,8 @@ void ArrangeJob::prepare_partplate() {
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx)
{
ModelObject* mo = model.objects[oidx];
if (is_belt_purge_prism(mo))
continue;
for (size_t inst_idx = 0; inst_idx < mo->instances.size(); ++inst_idx)
{
bool in_plate = plate->contain_instance(oidx, inst_idx) || plate->intersect_instance(oidx, inst_idx);
@@ -451,6 +534,7 @@ void ArrangeJob::prepare_partplate() {
// add the virtual object into unselect list if has
plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, current_plate_index + 1);
prepare_belt_regions(current_plate_index + 1);
}
//BBS: add partplate logic
@@ -858,6 +942,19 @@ arrangement::ArrangeParams init_arrange_params(Plater *p)
params.is_seq_print = settings.is_seq_print;
params.min_obj_distance = scaled(settings.distance);
params.align_to_y_axis = settings.align_to_y_axis;
if (print_config.belt_printer.value) {
// Parts print in belt order: the belt runs across the gantry's tilt axis, a
// rotation about X prints toward +Y and one about Y toward -X (see
// BeltTransform), a negative angle flips that, and a tilted layer reaches
// cot(angle) * height past a part's far edge.
const BeltRotationAxis axis = print_config.belt_slice_rotation.value;
const double angle = print_config.belt_slice_rotation_angle.value;
const bool tilted = (axis == BeltRotationAxis::X || axis == BeltRotationAxis::Y) && std::abs(angle) > EPSILON;
params.is_belt = true;
params.belt_axis = axis == BeltRotationAxis::Y ? 0 : 1;
params.belt_reversed = tilted && ((axis == BeltRotationAxis::Y) != (angle < 0.));
params.belt_tilt_slope = tilted ? float(1. / std::tan(Geometry::deg2rad(std::clamp(std::abs(angle), 5., 90.)))) : 0.f;
}
int state = p->get_prepare_state();
if (state == Job::JobPrepareState::PREPARE_STATE_MENU) {