Arrange every IDEX/IQEX plate inside its own primary zone

Arrange read the zones of the selected plate only and applied them to
every bed it packed. Arranging all plates with a plate in Primary
selected spread a parallel plate's parts across its whole bed, and with
a parallel plate selected, a Primary plate's parts were squeezed into
the zone. Arranging a single plate other than the first also ignored its
collision strips: they were tagged with the plate's index, while the
plate packs into the arranger's bed 0.

The IDEX/IQEX constraints now live in ImexArranger (IMEXArrange), and
ArrangeJob only snapshots each plate's zones on the main thread and
calls it:
- When every bed, plates the arrange may add included, has the same
  primary zone, the bed shape is that zone, so parts stay centered in it.
- Otherwise each plate in a parallel mode fences off the rest of its bed
  with fixed items on its own bed, and its parts are then arranged again
  inside the zone, so they sit centered rather than piled against the
  zone edge nearest the bed's center. Plates in Primary keep the whole
  bed, and plates the arrange adds take the process preset's mode.
- Zone edges inside the bed get the bed's own edge margin, and strips
  also keep room for the brim.
- A part too big for its zone, which libnest2d's first-fit retry places
  across the fixed items, is left unarranged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Clifford GarwoodandClaude Opus 5.5 committed 2026-10-10 01:44:42 -04:00
1 parent 8b5280098f
commit 2ede6d1631
7 files changed
+656 -51

No files matched your search

+41 -48
View File
@@ -5,6 +5,7 @@
#include "libslic3r/MTUtils.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "libslic3r/IMEXArrange.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
@@ -537,6 +538,36 @@ void ArrangeJob::prepare_partplate() {
prepare_belt_regions(current_plate_index + 1);
}
// Snapshot each plate's IMEX zones on the main thread, so process() never has to touch
// PartPlate's IMEX cache; see the members' declaration for why that matters. The beds are the
// arranger's: arranging one plate packs into bed 0, and arranging all of them numbers the beds
// over the unlocked plates, as postprocess_bed_index_for_selected() maps them back.
void ArrangeJob::prepare_imex_zones()
{
m_imex = {};
PartPlateList& plate_list = m_plater->get_partplate_list();
std::vector<PartPlate*> plates;
if (only_on_partplate)
plates.push_back(plate_list.get_curr_plate());
else
for (int i = 0; i < plate_list.get_plate_count(); ++i)
if (!plate_list.get_plate(i)->is_locked())
plates.push_back(plate_list.get_plate(i));
for (PartPlate* plate : plates) {
std::optional<ImexArrangeZones> zones;
if (const std::optional<BoundingBoxf> pz = plate ? plate->imex_primary_zone() : std::nullopt) {
const Vec2d o = plate->get_origin().head<2>();
zones = ImexArrangeZones{BoundingBoxf(Vec2d(pz->min - o), Vec2d(pz->max - o)), {}};
for (const BoundingBoxf3& strip : plate->imex_collision_zones())
zones->collision_zones.emplace_back(Vec2d(strip.min.head<2>() - o), Vec2d(strip.max.head<2>() - o));
}
m_imex.beds.push_back(std::move(zones));
}
m_imex.adds_plates = !only_on_partplate;
m_imex.read_config(wxGetApp().preset_bundle->full_config());
}
//BBS: add partplate logic
void ArrangeJob::prepare()
{
@@ -555,22 +586,6 @@ void ArrangeJob::prepare()
Model::setExtruderParams(config, numExtruders);
Model::setPrintSpeedTable(config, print_config);
// Snapshot the IMEX zones here, on the main thread, so process() never has to touch
// PartPlate's IMEX cache. See the members' declaration for why that matters.
m_imex_primary_zone_local.reset();
m_imex_collision_zones_local.clear();
if (PartPlate* curr_plate = m_plater->get_partplate_list().get_curr_plate()) {
if (auto pz = curr_plate->imex_primary_zone()) {
const Vec3d plate_origin = curr_plate->get_origin();
const double ox = plate_origin.x(), oy = plate_origin.y();
m_imex_primary_zone_local = BoundingBoxf(Vec2d(pz->min.x() - ox, pz->min.y() - oy),
Vec2d(pz->max.x() - ox, pz->max.y() - oy));
for (const BoundingBoxf3& cz : curr_plate->imex_collision_zones())
m_imex_collision_zones_local.emplace_back(Vec2d(cz.min.x() - ox, cz.min.y() - oy),
Vec2d(cz.max.x() - ox, cz.max.y() - oy));
}
}
int state = m_plater->get_prepare_state();
if (state == Job::JobPrepareState::PREPARE_STATE_DEFAULT) {
only_on_partplate = false;
@@ -581,6 +596,9 @@ void ArrangeJob::prepare()
prepare_partplate();
}
// After prepare_all(), which locks the plates it must leave alone.
prepare_imex_zones();
#if SAVE_ARRANGE_POLY
if (1)
@@ -662,38 +680,10 @@ void ArrangeJob::process(Ctl &ctl)
Points bedpts = get_shrink_bedpts(m_plater->config(),params);
// When an IDEX/IQEX parallel mode is active, constrain auto-arrange to the primary zone only
// and treat carriage collision strips as hard excluded regions.
// NOTE: the plate's zone boxes are in global (world) coordinates because they derive from
// m_shape, which includes the plate origin offset. The arranger works in plate-local space
// (origin = 0,0), so the plate origin is subtracted when the snapshot is taken in prepare();
// m_imex_primary_zone_local / m_imex_collision_zones_local are already plate-local here.
if (const auto& pz = m_imex_primary_zone_local) {
BoundingBox scaled_pz = scaled(*pz);
bedpts = {
{ scaled_pz.min.x(), scaled_pz.min.y() },
{ scaled_pz.max.x(), scaled_pz.min.y() },
{ scaled_pz.max.x(), scaled_pz.max.y() },
{ scaled_pz.min.x(), scaled_pz.max.y() },
};
for (const BoundingBoxf& cz : m_imex_collision_zones_local) {
Polygon poly({
{ scaled(cz.min.x()), scaled(cz.min.y()) },
{ scaled(cz.max.x()), scaled(cz.min.y()) },
{ scaled(cz.max.x()), scaled(cz.max.y()) },
{ scaled(cz.min.x()), scaled(cz.max.y()) },
});
arrangement::ArrangePolygon ap;
ap.poly.contour = poly;
ap.translation = Vec2crd(0, 0);
ap.rotation = 0.0;
ap.is_virt_object = true;
ap.bed_idx = current_plate_index;
ap.height = 1;
ap.name = "IMEXCollisionZone";
m_unselected.emplace_back(std::move(ap));
}
}
// Keep the parts on IDEX/IQEX plates in their primary zones; see ImexArranger.
const ImexArranger imex(m_imex, params, bedpts);
bedpts = imex.bed_shape();
imex.add_keep_outs(m_unselected);
bool enable_wrapping = global_config.option<ConfigOptionBool>("enable_wrapping_detection")->value;
partplate_list.preprocess_exclude_areas(params.excluded_regions, enable_wrapping, 1, scale_(1));
@@ -719,7 +709,10 @@ void ArrangeJob::process(Ctl &ctl)
<<", bbox:"<<get_extents(item.poly).min.transpose()<<","<<get_extents(item.poly).max.transpose();
}
const ArrangePolygons before = imex.active() ? m_selected : ArrangePolygons();
arrangement::arrange(m_selected, m_unselected, bedpts, params);
if (!ctl.was_canceled())
imex.finish(m_selected, before, m_unselected, params);
// sort by item id
std::sort(m_selected.begin(), m_selected.end(), [](auto a, auto b) {return a.itemid < b.itemid; });