diff --git a/src/libslic3r/BeltGCodeWriter.cpp b/src/libslic3r/BeltGCodeWriter.cpp index a2322c9243..7c645edf43 100644 --- a/src/libslic3r/BeltGCodeWriter.cpp +++ b/src/libslic3r/BeltGCodeWriter.cpp @@ -38,7 +38,11 @@ void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config) Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const { // Step 1+2: To Cartesian (back_transform + axis_remap). - Vec3d after_back = m_belt_back_transform.apply(pos); + // In world-coordinates mode (PA line / PA pattern calibration) the input + // already describes a point relative to the belt surface, so the + // slicer->world back-transform is skipped and only the machine kinematics + // (axis remap + frame shear/scale) are applied. + Vec3d after_back = m_world_coordinates ? pos : m_belt_back_transform.apply(pos); Vec3d result = apply_axis_remap(after_back); Vec3d after_remap = result; // Step 3: Machine-frame transform (belt frame tilt) applied LAST so it acts diff --git a/src/libslic3r/BeltGCodeWriter.hpp b/src/libslic3r/BeltGCodeWriter.hpp index 30159f4f33..ff7428d062 100644 --- a/src/libslic3r/BeltGCodeWriter.hpp +++ b/src/libslic3r/BeltGCodeWriter.hpp @@ -23,6 +23,14 @@ public: void set_machine_frame_transform(const PrintConfig &config); Vec3d to_machine_coords(const Vec3d &pos) const; + // World-coordinates mode: incoming coordinates are treated as points + // relative to the physical belt surface (X across, Y along the belt, + // Z height above it) instead of slicing-frame coordinates — the + // slicer->world back-transform is skipped. Used by the PA line / PA + // pattern calibration generators, whose logical bed coordinates describe + // first-layer drawings on the build surface. + void set_world_coordinates(bool enable) { m_world_coordinates = enable; } + // First-layer plane: when set to a non-null active evaluator, travel // speed selection consults the plane per-move and uses // initial_layer_travel_speed for points within first_layer_height_mm @@ -47,6 +55,7 @@ protected: private: BeltBackTransform m_belt_back_transform; MachineFrameTransform m_machine_frame_transform; + bool m_world_coordinates = false; // Borrowed pointer; lifetime owned by GCode. null = inactive. const FirstLayerPlane *m_first_layer_plane = nullptr; double m_first_layer_thickness_mm = 0.; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 3f21616bbb..af59428c89 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1834,6 +1834,12 @@ std::vector GCode::collect_layers_to_print(const PrintObjec last_extrusion_layer = &layers_to_print.back(); } + // ORCA-Belt: objects print at their position along the belt, so the first + // extrusions legitimately start far above Z=0. Drop the spurious + // "empty layers from the bed" range while keeping genuine mid-print gaps. + if (skip_empty_first_layer && !warning_ranges.empty() && warning_ranges.front().first == 0.) + warning_ranges.erase(warning_ranges.begin()); + if (! warning_ranges.empty()) { std::string warning; size_t i = 0; @@ -3307,7 +3313,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato pa_test.set_speed(fast_speed, slow_speed); pa_test.draw_numbers() = print.calib_params().print_numbers; + + // ORCA-Belt: the PA line test draws directly on the build surface in + // logical bed coordinates — on a belt printer that surface is the + // belt plane, not the slicing plane. + BeltGCodeWriter* belt_writer = dynamic_cast(m_writer.get()); + if (belt_writer != nullptr) + belt_writer->set_world_coordinates(true); gcode += pa_test.generate_test(params.start, params.step, std::llround(std::ceil((params.end - params.start) / params.step)) + 1); + if (belt_writer != nullptr) + belt_writer->set_world_coordinates(false); file.write(gcode); } else { @@ -4653,31 +4668,57 @@ LayerResult GCode::process_layer( gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n"; //Calibration Layer-specific GCode + // ORCA-Belt: on belt printers the calibration object is counter-rotated to + // stand upright in slicing space on top of a support wedge, so its first + // layer starts above Z=0 (at its position along the belt) with support-only + // layers below it. Reference the per-height calibration bands to the bottom + // of the object so they keep their designed meaning; on regular printers + // the object base is at Z=0 and calib_z == print_z. + double calib_z = print_z; + if (m_config.belt_printer.value && print.calib_mode() != CalibMode::Calib_None) { + // Skip empty ghost layers the grid may produce below the object. + for (const Layer* l : layer.object()->layers()) + if (!l->lslices.empty()) { + calib_z = print_z - (l->print_z - l->height); + break; + } + } switch (print.calib_mode()) { case CalibMode::Calib_PA_Tower: { - gcode += writer().set_pressure_advance(print.calib_params().start + static_cast(print_z) * print.calib_params().step); + gcode += writer().set_pressure_advance(print.calib_params().start + static_cast(std::max(0.0, calib_z)) * print.calib_params().step); break; } case CalibMode::Calib_Temp_Tower: { - gcode += writer().set_temperature(this->interpolate_value_across_layers(static_cast(print.calib_params().start), static_cast(print.calib_params().end), 5.0f)); + // ORCA-Belt: the sectioned variant prints each temperature as its + // own object in native belt orientation, with the temperature + // encoded in the object name ("temp_230") — step per object + // instead of ramping per layer band. + int sectioned_temp = 0; + if (m_config.belt_printer.value && + sscanf(layer.object()->model_object()->name.c_str(), "temp_%d", §ioned_temp) == 1 && + sectioned_temp > 0) { + gcode += writer().set_temperature(static_cast(sectioned_temp)); + } else { + gcode += writer().set_temperature(this->interpolate_value_across_layers(static_cast(print.calib_params().start), static_cast(print.calib_params().end), 5.0f)); + } break; } case CalibMode::Calib_VFA_Tower: { - auto _speed = print.calib_params().start + std::floor(print_z / 5.0) * print.calib_params().step; + auto _speed = print.calib_params().start + std::floor(std::max(0.0, calib_z) / 5.0) * print.calib_params().step; m_calib_config.set_key_value("outer_wall_speed", new ConfigOptionFloat(std::round(_speed))); break; } case CalibMode::Calib_Vol_speed_Tower: { - auto _speed = print.calib_params().start + print_z * print.calib_params().step; + auto _speed = print.calib_params().start + std::max(0.0, calib_z) * print.calib_params().step; m_calib_config.set_key_value("outer_wall_speed", new ConfigOptionFloat(std::round(_speed))); break; } case CalibMode::Calib_Retraction_tower: { - auto _length = print.calib_params().start + std::floor(std::max(0.0,print_z-0.4)) * print.calib_params().step; + auto _length = print.calib_params().start + std::floor(std::max(0.0,calib_z-0.4)) * print.calib_params().step; DynamicConfig _cfg; _cfg.set_key_value("retraction_length", new ConfigOptionFloats{_length}); writer().config.apply(_cfg); - sprintf(buf, "; Calib_Retraction_tower: Z_HEIGHT: %g, length:%g\n", print_z, _length); + sprintf(buf, "; Calib_Retraction_tower: Z_HEIGHT: %g, length:%g\n", calib_z, _length); gcode += buf; break; } @@ -7415,25 +7456,39 @@ std::string GCode::extrusion_role_to_string_for_parser(const ExtrusionRole & rol // Step = 0 means gradual interpolation finishing at last value. float GCode::interpolate_value_across_layers(float start_value, float end_value, float step) const { - if (m_layer_index <= 1) { + float ratio; + // ORCA-Belt: counter-rotated calibration objects stand on a support wedge, + // so support-only layers below the object would stretch a layer-index + // interpolation. Use the object's own Z span instead, so the value ramps + // across the test geometry only. + if (m_config.belt_printer.value && m_layer != nullptr && !m_layer->object()->layers().empty()) { + const auto& layers = m_layer->object()->layers(); + // Skip empty ghost layers the grid may produce below the object. + double z_min = layers.front()->print_z; + for (const Layer* l : layers) + if (!l->lslices.empty()) { z_min = l->print_z; break; } + const double z_max = layers.back()->print_z; + if (m_layer->print_z <= z_min + EPSILON || z_max - z_min <= EPSILON) + return start_value; + ratio = float(std::min(1.0, (m_layer->print_z - z_min) / (z_max - z_min))); + } else if (m_layer_index <= 1) { return start_value; + } else { + ratio = m_layer_index / (m_layer_count - 1.f); } - else { - bool use_steps = step > 0.f; - if (use_steps) { - if (start_value > end_value) { - start_value += step; - } else { - end_value += step; - } + bool use_steps = step > 0.f; + if (use_steps) { + if (start_value > end_value) { + start_value += step; + } else { + end_value += step; } - float ratio = m_layer_index / (m_layer_count - 1.f); - float value = start_value + ratio * (end_value - start_value); - if (use_steps) { - value = trunc(value / step) * step; - } - return value; } + float value = start_value + ratio * (end_value - start_value); + if (use_steps) { + value = trunc(value / step) * step; + } + return value; } std::string encodeBase64(uint64_t value) diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 7e5203d624..1212100eac 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -966,7 +966,14 @@ void PrintObject::slice() // correct machine-frame coordinates whether or not a global mode is active. double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg) ? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.; - double belt_z_shift = m_belt_min_z - belt_surface_z; + // The compensation must mirror the Z-shift actually applied, which + // is max(0, -m_belt_min_z): when the transformed mesh starts ABOVE + // slicer Z=0 (m_belt_min_z > 0 — possible for counter-rotated or + // asymmetric geometry whose centered-frame minimum lands positive) + // no lift was applied, and an unclamped m_belt_min_z here would + // leak straight into the layer Z values, floating the whole object + // off the belt by exactly that amount. + double belt_z_shift = std::min(m_belt_min_z, 0.) - belt_surface_z; double global_z_offset = belt_z_shift; // Centering correction: trafo_centered pretranslates by diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index e8009cf5c5..8c0fe205a4 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1801,8 +1801,7 @@ void TreeSupport::generate() { BeltFloorContext ctx; if (ctx.init(m_slicing_params, *m_print_config) - && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly - && m_object->support_layer_count() > 0) { + && m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { const auto &sp = m_slicing_params; // Find the lowest non-empty, non-brim support layer. ExPolygons source_areas; @@ -1830,16 +1829,61 @@ void TreeSupport::generate() } } } + // ORCA-Belt calibration: a counter-rotated calibration object + // stands on a support wedge that lies entirely below the object's + // first layer, where the tree pipeline has no layers at all — so + // no support content can exist yet. Seed the extension directly + // from the floating portion of the first layer (anything more + // than one layer height above the belt floor). For objects whose + // first layer rests on the belt the floating region is empty and + // behavior is unchanged. + double first_z = m_object->support_layer_count() > 0 ? m_object->get_support_layer(0)->print_z : 0.; + bool seeded = false; + if (source_areas.empty() && m_object_config->enable_support.value && !m_object->layers().empty()) { + // The layer grid may start with an empty ghost layer just below + // the object (grid rounding against the belt global Z offset) — + // anchor the seed to the first layer that has geometry. Object + // layer print_z and the floor plane are both in the globally + // offset frame here (belt_floor_z_shift was adjusted alongside + // the layer Z values in PrintObject::slice()). + const Layer *first_layer = nullptr; + for (const Layer *l : m_object->layers()) + if (!l->lslices_extrudable.empty()) { first_layer = l; break; } + if (first_layer != nullptr) { + ExPolygons floating = diff_ex(first_layer->lslices_extrudable, + ctx.surface_polygon(first_layer->bottom_z() - first_layer->height)); + BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] wedge seed: obj=" << m_object->model_object()->name + << " bottom_z=" << first_layer->bottom_z() << " floating=" << floating.size(); + if (!floating.empty()) { + source_areas = std::move(floating); + first_z = first_layer->bottom_z(); + seeded = true; + } + } + } if (!source_areas.empty()) { BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config()); double from_extent = std::abs(bb.min(ctx.from_axis())); double bb_min_z = std::abs(bb.min.z()); - double first_z = m_object->get_support_layer(0)->print_z; // Depth = from-axis extent + pre-shear bbox Z offset (ensure_on_bed // distance) + 10mm safety margin. The 10mm is a bodge to avoid // small cutoff artifacts — ideally computed exactly from belt geometry. double extra_depth = std::min(from_extent + bb_min_z + 10., std::max(0., first_z)); + if (seeded) { + // Seeded wedge: the depth is known exactly — down to the lowest + // belt-floor point under the floating footprint. The bbox + // heuristic above under-estimates it for meshes centered + // around their origin (every object loaded through the GUI). + double min_floor = first_z; + for (const ExPolygon &ep : source_areas) + for (const Point &pt : ep.contour.points) + min_floor = std::min(min_floor, ctx.floor_print_z(pt)); + extra_depth = std::min(std::max(0., first_z), first_z - min_floor + 2.); + } int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height)); + // Seeded wedge: top layers become a dense support interface so the + // object's floating first layer bridges a roof, not sparse infill. + const int interface_layers = seeded ? std::max(0, m_object_config->support_interface_top_layers.value) : 0; ExPolygons prev_areas = source_areas; // Build belt extension layers (lowest Z first). SupportLayerPtrs belt_ext_layers; @@ -1853,8 +1897,15 @@ void TreeSupport::generate() sl->base_areas = clipped; // Populate area_groups — generate_toolpaths() iterates these, // not base_areas directly. - for (auto &expoly : sl->base_areas) - sl->area_groups.emplace_back(&expoly, SupportLayer::BaseType, 0); + // Note: base areas only get infill when support_base_pattern + // is explicitly set (with the default pattern tree bases are + // walls-only) — the calibration flow sets rectilinear. + const bool roof = i <= interface_layers; + for (auto &expoly : sl->base_areas) { + sl->area_groups.emplace_back(&expoly, roof ? SupportLayer::RoofType : SupportLayer::BaseType, 0); + if (roof) + sl->area_groups.back().interface_id = i & 1; + } sl->lslices = clipped; sl->lslices_bboxes.reserve(clipped.size()); for (const ExPolygon &ep : clipped) @@ -1865,6 +1916,9 @@ void TreeSupport::generate() if (!belt_ext_layers.empty()) { auto &sl_vec = m_object->support_layers(); sl_vec.insert(sl_vec.begin(), belt_ext_layers.begin(), belt_ext_layers.end()); + BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] wedge ext layers=" << belt_ext_layers.size() + << " z=" << belt_ext_layers.front()->print_z << ".." << belt_ext_layers.back()->print_z + << " seeded=" << seeded; } } } diff --git a/src/libslic3r/calib.cpp b/src/libslic3r/calib.cpp index 4b3e05df8d..3509b32257 100644 --- a/src/libslic3r/calib.cpp +++ b/src/libslic3r/calib.cpp @@ -1,4 +1,5 @@ #include "calib.hpp" +#include "BeltGCodeWriter.hpp" #include "BoundingBox.hpp" #include "Config.hpp" #include "Model.hpp" @@ -587,21 +588,21 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna refresh_setup(config, is_bbl_machine, object, origin); - gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), m_writer, "Move to start XY position"); - gcode << m_writer.travel_to_z(height_first_layer() + height_z_offset(), "Move to start Z position"); - gcode << m_writer.set_pressure_advance(m_params.start); + gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), *m_writer, "Move to start XY position"); + gcode << m_writer->travel_to_z(height_first_layer() + height_z_offset(), "Move to start Z position"); + gcode << m_writer->set_pressure_advance(m_params.start); const DrawBoxOptArgs default_box_opt_args(wall_count(), height_first_layer(), line_width_first_layer(), speed_adjust(speed_first_layer())); // create anchoring frame - gcode << draw_box(m_writer, m_starting_point.x(), m_starting_point.y(), print_size_x(), frame_size_y(), default_box_opt_args); + gcode << draw_box(*m_writer, m_starting_point.x(), m_starting_point.y(), print_size_x(), frame_size_y(), default_box_opt_args); // create tab for numbers DrawBoxOptArgs draw_box_opt_args = default_box_opt_args; draw_box_opt_args.is_filled = true; draw_box_opt_args.num_perimeters = wall_count(); - gcode << draw_box(m_writer, m_starting_point.x(), m_starting_point.y() + frame_size_y() + line_spacing_first_layer(), + gcode << draw_box(*m_writer, m_starting_point.x(), m_starting_point.y() + frame_size_y() + line_spacing_first_layer(), print_size_x(), max_numbering_height() + line_spacing_first_layer() + m_glyph_padding_vertical * 2, draw_box_opt_args); @@ -627,15 +628,15 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna gcode = std::stringstream(); // reset for next layer contents gcode << "; start pressure advance pattern for layer\n"; - gcode << m_writer.travel_to_z(layer_height, "Move to layer height"); - gcode << m_writer.reset_e(); + gcode << m_writer->travel_to_z(layer_height, "Move to layer height"); + gcode << m_writer->reset_e(); } // line numbering if (i == 1) { m_number_len = max_numbering_length(); - gcode << m_writer.set_pressure_advance(m_params.start); + gcode << m_writer->set_pressure_advance(m_params.start); double number_e_per_mm = e_per_mm(line_width(), height_layer(), m_config.option("nozzle_diameter")->get_at(0), @@ -646,20 +647,20 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna for (int j = 0; j < num_patterns; j += 2) { gcode << draw_number(glyph_start_x(j), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(), m_params.start + (j * m_params.step), m_draw_digit_mode, line_width(), number_e_per_mm, - speed_first_layer(), m_writer); + speed_first_layer(), *m_writer); } // flow value int line_num = num_patterns + 2; gcode << draw_number(glyph_start_x(line_num), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(), flow_val(), m_draw_digit_mode, line_width(), number_e_per_mm, - speed_first_layer(), m_writer); + speed_first_layer(), *m_writer); // acceleration line_num = num_patterns + 4; gcode << draw_number(glyph_start_x(line_num), m_starting_point.y() + frame_size_y() + m_glyph_padding_vertical + line_width(), accel, m_draw_digit_mode, line_width(), number_e_per_mm, - speed_first_layer(), m_writer); + speed_first_layer(), *m_writer); } @@ -678,20 +679,20 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna /* Draw a line at slightly slower accel and speed in order to trick gcode writer to force update acceleration and speed. * We do this since several tests may be generated by their own gcode writers which are * not aware about their neighbours updating acceleration/speed */ - gcode << m_writer.set_print_acceleration(std::max(1, accel - 1)); - gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), m_writer, "Move to starting point", zhop_height, layer_height); - gcode << draw_line(m_writer, Vec2d(m_starting_point.x(), m_starting_point.y() + frame_size_y()), line_width(), height_layer(), speed_adjust(std::max(1, speed_perimeter() - 1)), "Accel/flow trick line"); - gcode << m_writer.set_print_acceleration(accel); + gcode << m_writer->set_print_acceleration(std::max(1, accel - 1)); + gcode << move_to(Vec2d(m_starting_point.x(), m_starting_point.y()), *m_writer, "Move to starting point", zhop_height, layer_height); + gcode << draw_line(*m_writer, Vec2d(m_starting_point.x(), m_starting_point.y() + frame_size_y()), line_width(), height_layer(), speed_adjust(std::max(1, speed_perimeter() - 1)), "Accel/flow trick line"); + gcode << m_writer->set_print_acceleration(accel); } double initial_x = to_x; double initial_y = to_y; - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to pattern start",zhop_height,layer_height); + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to pattern start",zhop_height,layer_height); for (int j = 0; j < num_patterns; ++j) { // increment pressure advance - gcode << m_writer.set_pressure_advance(m_params.start + (j * m_params.step)); + gcode << m_writer->set_pressure_advance(m_params.start + (j * m_params.step)); for (int k = 0; k < wall_count(); ++k) { to_x += std::cos(to_radians(m_corner_angle) / 2) * side_length; @@ -701,27 +702,27 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna auto draw_line_arg_line_width = line_width(); // don't use line_width_first_layer so results are consistent across all layers auto draw_line_arg_speed = i == 0 ? speed_adjust(speed_first_layer()) : speed_adjust(speed_perimeter()); auto draw_line_arg_comment = "Print pattern wall"; - gcode << draw_line(m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); + gcode << draw_line(*m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); to_x -= std::cos(to_radians(m_corner_angle) / 2) * side_length; to_y += std::sin(to_radians(m_corner_angle) / 2) * side_length; - gcode << draw_line(m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); + gcode << draw_line(*m_writer, Vec2d(to_x, to_y), draw_line_arg_line_width, draw_line_arg_height, draw_line_arg_speed, draw_line_arg_comment); to_y = initial_y; if (k != wall_count() - 1) { // perimeters not done yet. move to next perimeter to_x += line_spacing_angle(); - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to start next pattern wall", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to start next pattern wall", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift } else if (j != num_patterns - 1) { // patterns not done yet. move to next pattern to_x += m_pattern_spacing + line_width(); - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move to next pattern", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move to next pattern", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift } else if (i != m_num_layers - 1) { // layers not done yet. move back to start to_x = initial_x; - gcode << move_to(Vec2d(to_x, to_y), m_writer, "Move back to start position", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift - gcode << m_writer.reset_e(); // reset extruder before printing placeholder cube to avoid over extrusion + gcode << move_to(Vec2d(to_x, to_y), *m_writer, "Move back to start position", zhop_height, layer_height); // Call move to command with XY as well as z hop and layer height to invoke and undo z lift + gcode << m_writer->reset_e(); // reset extruder before printing placeholder cube to avoid over extrusion } else { // everything done } @@ -729,7 +730,7 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna } } - gcode << m_writer.set_pressure_advance(m_params.start); + gcode << m_writer->set_pressure_advance(m_params.start); gcode << "; end pressure advance pattern for layer\n"; CustomGCode::Item item; @@ -796,13 +797,36 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod PrintConfig print_config; print_config.apply(m_config, true); - m_writer.apply_print_config(print_config); - m_writer.set_xy_offset(origin(0), origin(1)); - m_writer.set_is_bbl_machine(is_bbl_machine); + // ORCA-Belt: the pattern is drawn in logical bed coordinates directly on + // the build surface — on a belt printer that means the belt plane, which + // needs the machine kinematics (axis remap + frame shear/scale) with the + // coordinates interpreted as world points (see set_world_coordinates). + if (print_config.belt_printer.value) { + auto belt_writer = std::make_shared(); + belt_writer->set_belt_back_transform(print_config); + belt_writer->set_machine_frame_transform(print_config); + belt_writer->set_world_coordinates(true); + const int rx = int(print_config.gcode_remap_x.value); + const int ry = int(print_config.gcode_remap_y.value); + const int rz = int(print_config.gcode_remap_z.value); + if (rx != 0 || ry != 1 || rz != 2) { + belt_writer->set_axis_remap(rx, ry, rz); + BoundingBoxf bbox_bed(print_config.printable_area.values); + belt_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(), + print_config.printable_height.value)); + } + m_writer = std::move(belt_writer); + } else if (dynamic_cast(m_writer.get()) != nullptr) { + m_writer = std::make_shared(); + } + + m_writer->apply_print_config(print_config); + m_writer->set_xy_offset(origin(0), origin(1)); + m_writer->set_is_bbl_machine(is_bbl_machine); const unsigned int extruder_id = object.volumes.front()->extruder_id(); - m_writer.set_extruders({extruder_id}); - m_writer.set_extruder(extruder_id); + m_writer->set_extruders({extruder_id}); + m_writer->set_extruder(extruder_id); } double CalibPressureAdvancePattern::object_size_x() const diff --git a/src/libslic3r/calib.hpp b/src/libslic3r/calib.hpp index cbac1d3f86..b36c75db62 100644 --- a/src/libslic3r/calib.hpp +++ b/src/libslic3r/calib.hpp @@ -358,7 +358,10 @@ private: const Calib_Params &m_params; - GCodeWriter m_writer; + // Polymorphic so belt printers get a BeltGCodeWriter in world-coordinates + // mode (_refresh_writer); shared_ptr keeps the class copyable — the writer + // is rebuilt by refresh_setup() before every use anyway. + std::shared_ptr m_writer{std::make_shared()}; Vec3d m_starting_point; bool m_is_start_point_fixed = false; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 823deb0aea..6bc6a30f0f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -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("belt_printer"); + if (belt_opt == nullptr || !belt_opt->value) + return false; + const auto* axis_opt = printer_config.option>("belt_slice_rotation"); + const auto* angle_opt = printer_config.option("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 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(stTree)); + obj->config.set_key_value("support_style", new ConfigOptionEnum(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(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 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> rows; + double row_y = std::numeric_limits::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::quiet_NaN(); + for (std::vector& row : rows) { + double rmin = std::numeric_limits::max(); + double rmax = std::numeric_limits::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 object_idx(boost::counting_iterator(0), boost::counting_iterator(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("nozzle_diameter"); + size_t nozzle_id = static_cast(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 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::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("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); } diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index f2089c1c4f..a718d39464 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -951,6 +951,8 @@ private: void _calib_pa_pattern_gen_gcode(); void _calib_pa_tower(const Calib_Params& params); void _calib_pa_select_added_objects(); + void _calib_apply_belt_mode(); + void _calib_temp_belt_sectioned(const Calib_Params& params, double belt_angle_rad); void cut_horizontal(size_t obj_idx, size_t instance_idx, double z, ModelObjectCutAttributes attributes); diff --git a/src/slic3r/GUI/calib_dlg.cpp b/src/slic3r/GUI/calib_dlg.cpp index 5a06b24427..870b078aa1 100644 --- a/src/slic3r/GUI/calib_dlg.cpp +++ b/src/slic3r/GUI/calib_dlg.cpp @@ -78,6 +78,19 @@ std::vector make_shaper_type_labels() return labels; } +// ORCA-Belt: PA Line / PA Pattern have belt plumbing in place (drawn on the +// belt surface via BeltGCodeWriter world-coordinates mode) but are not +// validated yet — belt printers are restricted to the PA Tower for now. +bool is_belt_printer_selected() +{ + if (auto* preset_bundle = wxGetApp().preset_bundle) { + const auto& cfg = preset_bundle->printers.get_edited_preset().config; + const auto* opt = cfg.option("belt_printer"); + return opt != nullptr && opt->value; + } + return false; +} + } PA_Calibration_Dlg::PA_Calibration_Dlg(wxWindow* parent, wxWindowID id, Plater* plater) @@ -288,6 +301,14 @@ void PA_Calibration_Dlg::on_start(wxCommandEvent& event) { m_params.mode = CalibMode::Calib_PA_Tower; } + // ORCA-Belt: backstop in case the selection slipped past the UI guards. + if (is_belt_printer_selected() && m_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; + } + m_params.print_numbers = m_cbPrintNum->GetValue(); ParseStringValues(m_tiBMAccels->GetTextCtrl()->GetValue().ToStdString(), m_params.accelerations); ParseStringValues(m_tiBMSpeeds->GetTextCtrl()->GetValue().ToStdString(), m_params.speeds); @@ -314,6 +335,9 @@ void PA_Calibration_Dlg::on_extruder_type_changed(wxCommandEvent& event) { event.Skip(); } void PA_Calibration_Dlg::on_method_changed(wxCommandEvent& event) { + // ORCA-Belt: only the PA Tower method is enabled on belt printers so far. + if (is_belt_printer_selected() && m_rbMethod->GetSelection() != 0) + m_rbMethod->SetSelection(0, true); PA_Calibration_Dlg::reset_params(); event.Skip(); } @@ -324,6 +348,17 @@ void PA_Calibration_Dlg::on_dpi_changed(const wxRect& suggested_rect) { } void PA_Calibration_Dlg::on_show(wxShowEvent& event) { + // ORCA-Belt: the dialog is cached across printer switches, so refresh the + // belt restriction on every show. + if (is_belt_printer_selected()) { + m_rbMethod->SetSelection(0); + const wxString tip = _L("Not enabled yet on belt printers — use the PA Tower method instead."); + m_rbMethod->SetRadioTooltip(1, tip); + m_rbMethod->SetRadioTooltip(2, tip); + } else { + m_rbMethod->SetRadioTooltip(1, wxEmptyString); + m_rbMethod->SetRadioTooltip(2, wxEmptyString); + } PA_Calibration_Dlg::reset_params(); }