Belt/Standard calibrations (#54)

Enables supported printing of standard Orcaslicer calibration profiles.

* Build 2 Checkpoint

* fix support generation wedge, ghost layers

* flip cornering tests 180 deg to waste less supports

* fix row spacing on the flow ratio calibrations

* more testing, this didn't fix anything

* switched rotation tools, same issue

* fixed Z-offset issues

* add rest of PA features, may look a bit weird on a belt

* make temp towers work

* re-enable spiral on calibrations that want it

* Final cleanup pre-PR and community testing
This commit is contained in:
Joseph Robertson
2026-06-12 03:14:12 -05:00
committed by GitHub
parent d7b75540d0
commit 0da24cd38b
10 changed files with 554 additions and 59 deletions

View File

@@ -12594,8 +12594,181 @@ void Plater::add_model(bool imperial_units, std::string fname)
}
}
// ORCA-Belt: belt-printer handling for the desktop calibration tests.
//
// Belt slicing applies a global pre-slice rotation R(angle, axis) to every
// mesh (see BeltTransform.hpp) so the slicing planes match the tilted gantry.
// Calibration models are designed for upright slicing: their per-height test
// bands and XY-plane quality features assume slicer Z is the model's own Z
// axis. Counter-rotating each calibration object by the inverse rotation in
// world space cancels the global rotation, so in slicing space the object
// stands upright exactly as on a flat-bed printer and every test keeps its
// designed meaning. Physically the object then leans over the belt with its
// bottom face overhanging, so per-object supports fill the wedge between the
// bottom face and the belt. The wedge prints entirely below the object's base
// plane and leaves the test geometry untouched. Manual tree support is used
// so the deliberate bridge/overhang features of the test models stay
// unsupported; the wedge under the floating bottom face is built by the
// belt-floor extension in TreeSupport::generate(), which stacks the floating
// first-layer footprint down to the belt surface.
static bool belt_calib_rotation_params(double& angle_rad, Vec3d& axis)
{
const auto& printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config;
const auto* belt_opt = printer_config.option<ConfigOptionBool>("belt_printer");
if (belt_opt == nullptr || !belt_opt->value)
return false;
const auto* axis_opt = printer_config.option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation");
const auto* angle_opt = printer_config.option<ConfigOptionFloat>("belt_slice_rotation_angle");
if (axis_opt == nullptr || angle_opt == nullptr)
return false;
switch (axis_opt->value) {
case BeltRotationAxis::X: axis = Vec3d::UnitX(); break;
case BeltRotationAxis::Y: axis = Vec3d::UnitY(); break;
// Z rotation is an in-plane spin and None means no tilt; objects already
// slice upright in those cases and need no special handling.
default: return false;
}
angle_rad = -Geometry::deg2rad(angle_opt->value);
return std::abs(angle_rad) > EPSILON;
}
// ORCA-Belt: flip the ringing tower 180° about Z before the belt
// counter-rotation — its sloped face then leans over the belt and the
// support wedge gets much smaller.
static void belt_calib_flip_ringing_tower(Model &model)
{
double angle_rad = 0.;
Vec3d axis = Vec3d::UnitX();
if (belt_calib_rotation_params(angle_rad, axis) && !model.objects.empty())
model.objects.front()->rotate(M_PI, Vec3d::UnitZ());
}
void Plater::_calib_apply_belt_mode()
{
double angle_rad = 0.;
Vec3d axis = Vec3d::UnitX();
if (!belt_calib_rotation_params(angle_rad, axis))
return;
auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
// Spiral vase stays enabled where the tests request it: the support wedge
// lies strictly below the object, support layers never spiralize
// (spiral_vase_enable requires an object layer), and the spiral/support
// exclusivity check only applies to globally enabled supports — the wedge
// uses per-object supports.
// A skirt would be drawn in the first slicing plane, which lies mostly
// above the belt surface.
print_config->set_key_value("skirt_loops", new ConfigOptionInt(0));
const Matrix3d cancel_rotation = Eigen::AngleAxisd(angle_rad, axis).toRotationMatrix();
std::vector<size_t> obj_idxs;
for (size_t i = 0; i < model().objects.size(); ++i) {
ModelObject* obj = model().objects[i];
if (obj->instances.size() != 1)
continue;
obj_idxs.emplace_back(i);
// Manual tree support: only the floating bottom face gets a support
// wedge (via the belt-floor extension in TreeSupport::generate()),
// leaving the test features untouched. The style is pinned to hybrid
// because the default style resolves to organic, which bypasses the
// non-organic generator that hosts the belt-floor extension.
obj->config.set_key_value("enable_support", new ConfigOptionBool(true));
obj->config.set_key_value("support_type", new ConfigOptionEnum<SupportType>(stTree));
obj->config.set_key_value("support_style", new ConfigOptionEnum<SupportMaterialStyle>(smsTreeHybrid));
obj->config.set_key_value("support_on_build_plate_only", new ConfigOptionBool(false));
// With the default base pattern, tree support base areas print as
// hollow outlines (no infill) — the wedge needs a real pattern.
obj->config.set_key_value("support_base_pattern", new ConfigOptionEnum<SupportMaterialPattern>(smpRectilinear));
// Counter-rotate exactly the way the rotate gizmo would: rotation on
// the instance, then a plain drop to the bed. This leaves the object
// in the same state shape as any manually rotated object, which the
// belt pipeline is known to handle.
ModelInstance* inst = obj->instances.front();
inst->rotate(cancel_rotation);
obj->invalidate_bounding_box();
obj->ensure_on_bed();
{
const BoundingBoxf3 rb = obj->raw_bounding_box();
const Vec3d io = inst->get_offset();
const Vec3d ir = inst->get_rotation();
BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] helper exit: obj=" << obj->name
<< " inst_offset=(" << io.x() << "," << io.y() << "," << io.z() << ")"
<< " inst_rot=(" << ir.x() << "," << ir.y() << "," << ir.z() << ")"
<< " vol0_offset=(" << obj->volumes.front()->get_offset().x() << ","
<< obj->volumes.front()->get_offset().y() << "," << obj->volumes.front()->get_offset().z() << ")"
<< " raw_bbox=(" << rb.min.x() << "," << rb.min.y() << "," << rb.min.z()
<< ")..(" << rb.max.x() << "," << rb.max.y() << "," << rb.max.z() << ")"
<< " min_z=" << obj->min_z();
}
}
// Each object's support wedge extends upstream of it by roughly its own
// depth (at 45°), so the tight flat-bed layouts of the multi-part tests
// leave wedges intersecting the neighbouring parts. Keep the grid rows of
// the test layouts together and open up the space between rows just
// enough for the wedge shadow.
if (obj_idxs.size() > 1) {
std::vector<ModelObject*> sorted_objs;
sorted_objs.reserve(obj_idxs.size());
for (size_t i : obj_idxs)
sorted_objs.emplace_back(model().objects[i]);
std::sort(sorted_objs.begin(), sorted_objs.end(), [](const ModelObject* a, const ModelObject* b) {
return a->instances.front()->get_offset(Y) < b->instances.front()->get_offset(Y);
});
std::vector<std::vector<ModelObject*>> rows;
double row_y = std::numeric_limits<double>::quiet_NaN();
for (ModelObject* o : sorted_objs) {
const double oy = o->instances.front()->get_offset(Y);
if (rows.empty() || oy - row_y > 1.)
rows.emplace_back();
rows.back().emplace_back(o);
row_y = oy;
}
const double wedge_factor = std::abs(std::tan(angle_rad));
double cursor = std::numeric_limits<double>::quiet_NaN();
for (std::vector<ModelObject*>& row : rows) {
double rmin = std::numeric_limits<double>::max();
double rmax = std::numeric_limits<double>::lowest();
for (ModelObject* o : row) {
const BoundingBoxf3 bb = o->instance_bounding_box(0);
rmin = std::min(rmin, bb.min.y());
rmax = std::max(rmax, bb.max.y());
}
if (std::isnan(cursor))
cursor = rmin; // the first row anchors the layout
const double shift = cursor - rmin;
for (ModelObject* o : row) {
ModelInstance* inst = o->instances.front();
inst->set_offset(Y, inst->get_offset(Y) + shift);
o->invalidate_bounding_box();
}
cursor += (rmax - rmin) * (1. + wedge_factor) + 5.;
}
}
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty();
wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config();
changed_objects(obj_idxs);
}
void Plater::calib_pa(const Calib_Params& params)
{
// ORCA-Belt: PA Line / PA Pattern have the belt plumbing in place
// (BeltGCodeWriter::set_world_coordinates draws them on the belt surface)
// but are not validated yet — keep them gated to the PA Tower for now.
{
double angle_rad = 0.;
Vec3d axis = Vec3d::UnitX();
if (belt_calib_rotation_params(angle_rad, axis) && params.mode != CalibMode::Calib_PA_Tower) {
MessageDialog msg_dlg(nullptr, _L("PA Line and PA Pattern tests are not enabled yet on belt printers.\nPlease use the PA Tower method instead."),
wxEmptyString, wxICON_WARNING | wxOK);
msg_dlg.ShowModal();
return;
}
}
const auto calib_pa_name = wxString::Format(L"Pressure Advance Test");
new_project(false, false, calib_pa_name);
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
@@ -12927,6 +13100,7 @@ void Plater::_calib_pa_tower(const Calib_Params& params) {
cut_horizontal(0, 0, new_height, ModelObjectCutAttribute::KeepLower);
}
_calib_apply_belt_mode();
_calib_pa_select_added_objects();
}
@@ -13104,6 +13278,8 @@ void Plater::calib_flowrate(bool is_linear, int pass, InfillPattern pattern) {
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
_calib_apply_belt_mode();
// Refresh object after scaling
const std::vector<size_t> object_idx(boost::counting_iterator<size_t>(0), boost::counting_iterator<size_t>(model().objects.size()));
changed_objects(object_idx);
@@ -13120,7 +13296,19 @@ void Plater::calib_temp(const Calib_Params& params) {
wxGetApp().mainframe->select_tab(size_t(MainFrame::tp3DEditor));
if (params.mode != CalibMode::Calib_Temp_Tower)
return;
// ORCA-Belt: a counter-rotated tower would cantilever every block off a
// support wedge — print the temperature blocks as separate objects in
// native belt orientation instead.
{
double belt_angle_rad = 0.;
Vec3d belt_axis = Vec3d::UnitX();
if (belt_calib_rotation_params(belt_angle_rad, belt_axis)) {
_calib_temp_belt_sectioned(params, std::abs(belt_angle_rad));
return;
}
}
add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc");
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config;
@@ -13190,6 +13378,107 @@ void Plater::calib_temp(const Calib_Params& params) {
p->background_process.fff_print()->set_calib_params(params);
}
// ORCA-Belt: sectioned temperature test. Each temperature gets its own block
// cut out of the temperature tower model, printed in native belt orientation
// (no counter-rotation, no support wedge) and spaced along the belt so the
// blocks' layer ranges are disjoint — they print strictly one after another,
// starting with the start temperature closest to the gantry. The temperature
// is encoded in the object name ("temp_230") and applied per object by the
// Calib_Temp_Tower handler at G-code time, replacing the per-layer-band ramp
// that only makes sense for a monolithic upright tower.
void Plater::_calib_temp_belt_sectioned(const Calib_Params& params, double belt_angle_rad)
{
constexpr double base_temp_tower_nozzle_diameter = 0.4;
constexpr double base_temp_tower_block_height = 10.0;
constexpr int base_temp_tower_temp_step = 5;
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config;
auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
const long start_temp = lround(params.start);
const long end_temp = lround(params.end);
const int n_blocks = std::max(1, int((start_temp - end_temp) / base_temp_tower_temp_step) + 1);
const ConfigOptionFloats* nozzle_diameter_config = printer_config->option<ConfigOptionFloats>("nozzle_diameter");
size_t nozzle_id = static_cast<size_t>(std::max(params.extruder_id, 0));
double nozzle_diameter = base_temp_tower_nozzle_diameter;
if (nozzle_diameter_config && !nozzle_diameter_config->values.empty()) {
nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1);
nozzle_diameter = nozzle_diameter_config->values[nozzle_id];
}
if (nozzle_diameter <= 0.0)
nozzle_diameter = base_temp_tower_nozzle_diameter;
const double nozzle_scale = nozzle_diameter / base_temp_tower_nozzle_diameter;
std::vector<size_t> obj_idxs;
for (int i = 0; i < n_blocks; ++i) {
const long temp = start_temp - long(i) * base_temp_tower_temp_step;
const size_t count_before = model().objects.size();
add_model(false, Slic3r::resources_dir() + "/calib/temperature_tower/temperature_tower.drc");
if (model().objects.size() <= count_before)
break; // model failed to load — don't index into an empty list
// The cut replaces the object at the END of the list, so re-acquire
// the index after every operation.
size_t obj_idx = model().objects.size() - 1;
// Isolate this temperature's block (full-tower coordinates, the same
// 500-down-to-temp indexing the monolithic flow cuts with).
const double block_bottom = double(lround(double(500 - temp) / base_temp_tower_temp_step)) * base_temp_tower_block_height;
auto obj_bb = model().objects[obj_idx]->bounding_box_exact();
if (block_bottom + base_temp_tower_block_height < obj_bb.size().z()) {
cut_horizontal(obj_idx, 0, block_bottom + base_temp_tower_block_height - EPSILON, ModelObjectCutAttribute::KeepLower);
obj_idx = model().objects.size() - 1;
}
if (block_bottom > 0) {
cut_horizontal(obj_idx, 0, block_bottom + EPSILON, ModelObjectCutAttribute::KeepUpper);
obj_idx = model().objects.size() - 1;
}
ModelObject* obj = model().objects[obj_idx];
if (std::abs(nozzle_scale - 1.0) > EPSILON)
obj->scale(nozzle_scale, nozzle_scale, nozzle_scale);
obj->name = std::string("temp_") + std::to_string(temp);
obj->config.set_key_value("layer_height", new ConfigOptionFloat(nozzle_diameter / 2));
obj->config.set_key_value("alternate_extra_wall", new ConfigOptionBool(false));
obj->config.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(SeamScarfType::None));
obj->config.set_key_value("overhang_reverse", new ConfigOptionBool(false));
obj->config.set_key_value("precise_z_height", new ConfigOptionBool(false));
obj->ensure_on_bed();
obj_idxs.emplace_back(obj_idx);
}
// Space the blocks along the belt with strictly increasing layer ranges:
// each block must start past the previous block's highest slicing plane,
// which trails its far edge by height / tan(angle). Anchor the row near
// the gantry so the whole test stays in the plate area.
const double cot_a = 1. / std::max(0.1, std::tan(belt_angle_rad));
double cursor = 20.;
for (size_t idx : obj_idxs) {
ModelObject* obj = model().objects[idx];
ModelInstance* inst = obj->instances.front();
const BoundingBoxf3 bb = obj->instance_bounding_box(0);
inst->set_offset(Y, inst->get_offset(Y) + (cursor - bb.min.y()));
obj->invalidate_bounding_box();
cursor += bb.size().y() + bb.size().z() * cot_a + 5.;
}
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
filament_config->set_key_value("nozzle_temperature_initial_layer", new ConfigOptionInts(1, (int)start_temp));
filament_config->set_key_value("nozzle_temperature", new ConfigOptionInts(1, (int)start_temp));
print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(nozzle_diameter / 2));
print_config->set_key_value("skirt_loops", new ConfigOptionInt(0));
changed_objects(obj_idxs);
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty();
wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_dirty();
wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config();
wxGetApp().get_tab(Preset::TYPE_FILAMENT)->reload_config();
p->background_process.fff_print()->set_calib_params(params);
}
void Plater::calib_max_vol_speed(const Calib_Params& params)
{
const auto calib_vol_speed_name = wxString::Format(L"Max volumetric speed test");
@@ -13256,6 +13545,8 @@ void Plater::calib_max_vol_speed(const Calib_Params& params)
cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower);
}
_calib_apply_belt_mode();
auto new_params = params;
auto mm3_per_mm = Flow(line_width, layer_height, nozzle_diameter).mm3_per_mm() * filament_config->option<ConfigOptionFloatsNullable>("filament_flow_ratio")->get_at(0);
new_params.end = params.end / mm3_per_mm;
@@ -13320,6 +13611,7 @@ void Plater::calib_retraction(const Calib_Params& params)
cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower);
}
_calib_apply_belt_mode();
p->background_process.fff_print()->set_calib_params(params);
}
@@ -13365,6 +13657,7 @@ void Plater::calib_VFA(const Calib_Params& params)
cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower);
}
_calib_apply_belt_mode();
p->background_process.fff_print()->set_calib_params(params);
}
@@ -13377,6 +13670,8 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
return;
add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc"));
if (params.test_model < 1)
belt_calib_flip_ringing_tower(model());
auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config;
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
@@ -13428,6 +13723,7 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings();
wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings();
_calib_apply_belt_mode();
p->background_process.fff_print()->set_calib_params(params);
}
@@ -13440,6 +13736,8 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
return;
add_model(false, Slic3r::resources_dir() + (params.test_model < 1 ? "/calib/input_shaping/ringing_tower.drc" : "/calib/input_shaping/fast_tower_test.drc"));
if (params.test_model < 1)
belt_calib_flip_ringing_tower(model());
auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config;
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
@@ -13490,6 +13788,7 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings();
wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings();
_calib_apply_belt_mode();
p->background_process.fff_print()->set_calib_params(params);
}
@@ -13505,6 +13804,8 @@ void Plater::Calib_Cornering(const Calib_Params& params)
? "/calib/input_shaping/ringing_tower.drc"
: (params.test_model == 1 ? "/calib/input_shaping/fast_tower_test.drc" : "/calib/cornering/SCV-V2.drc");
add_model(false, Slic3r::resources_dir() + cornering_model_path);
if (params.test_model == 0)
belt_calib_flip_ringing_tower(model());
auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config;
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
@@ -13555,6 +13856,7 @@ void Plater::Calib_Cornering(const Calib_Params& params)
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings();
wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings();
_calib_apply_belt_mode();
p->background_process.fff_print()->set_calib_params(params);
}